|
root / src / commands / install.reef
install.reef Reef 852 lines 30.1 KB
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
/******************************************************************************
               __               ____                __
              / /   ___  ____ _/ __/_____________ _/ /__
             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/

   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com

   Project: Zygaena
  Filename: install.reef
   Authors: Chris Tusa <chris.tusa@leafscale.com>
   License: <see LICENSE file included with this source code>
Description: Install command - install packages from archives

******************************************************************************/

module commands.install

import sys.args
import sys.process
import io.file
import io.dir
import io.path
import core.str
import core.config
import core.database
import core.pkgdb
import core.package
import core.groups
import core.repository
import core.signing
import core.resolver
import core.port
import core.portindex
import util.checksum
import util.mtree
import util.http as uhttp
import util.prompt
import util.version as ver
import types
import util.color
import util.priv
import exitcodes as ec

export
    fn execute(opts: types.GlobalOptions): int
end export

fn execute(opts: types.GlobalOptions): int
    let argc = args.count()

    // Package arguments start after the command
    if argc <= opts.cmd_index + 1
        print_usage()
        return ec.EXIT_USAGE()
    end if

    // Load configuration and apply command-line overrides
    let base_cfg = config.load()
    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix, opts.no_chroot)
    uhttp.enable_proxy(config.get_use_proxy(cfg))

    // Auto-refresh port index if configured
    if config.get_auto_refresh(cfg)
        portindex.rebuild_port_index()
    end if

    // Show root/prefix if set (verbose mode)
    if opts.verbose
        let root = config.get_root(cfg)
        let prefix = config.get_prefix(cfg)
        if str.length(root) > 0
            color.print_info("Using root: " + root)
        end if
        if str.length(prefix) > 0
            color.print_info("Using prefix: " + prefix)
        end if
    end if

    // Collect all packages to install (expanding groups)
    mut packages: [string] = new [string](256)
    mut pkg_count = 0
    mut direct_files: [string] = new [string](16)
    mut direct_count = 0

    // Process each package argument (starting after the command)
    mut i = opts.cmd_index + 1
    while i < argc and pkg_count < 256
        let pkg_arg = args.get(i)

        // Skip flags and their values
        if pkg_arg == "--root" or pkg_arg == "--prefix"
            i = i + 2  // Skip flag and its value
            continue
        elif pkg_arg == "--no-chroot-scripts"
            i = i + 1  // valueless flag: skip only itself
            continue
        end if
        if str.starts_with(pkg_arg, "-")
            i = i + 1
            continue
        end if

        // Check if this is a direct file path to a .pkg.tar.xz
        if str.ends_with(pkg_arg, ".pkg.tar.xz") and file.fileExists(pkg_arg)
            direct_files[direct_count] = pkg_arg
            direct_count = direct_count + 1
            i = i + 1
            continue
        end if

        // Check if this is a group reference
        if groups.is_group(pkg_arg)
            // Expand group to member packages
            mut group_members: [string] = new [string](128)
            let member_count = groups.expand_group(pkg_arg, group_members, 128)

            if member_count == 0
                color.print_error("Group not found or empty: " + pkg_arg)
                i = i + 1
                continue
            end if

            color.print_info("Expanding group " + pkg_arg + " (" + int_to_str(member_count) + " packages)")

            // Add all group members to packages list
            mut j = 0
            while j < member_count and pkg_count < 256
                packages[pkg_count] = group_members[j]
                pkg_count = pkg_count + 1
                j = j + 1
            end while
        else
            // Regular package
            packages[pkg_count] = pkg_arg
            pkg_count = pkg_count + 1
        end if

        i = i + 1
    end while

    // Handle direct file installs — bypass resolver, install immediately
    if direct_count > 0
        let root = get_root(cfg)
        let root_for_db = config.get_root(cfg)
        if str.length(root_for_db) > 0
            if not database.init_db_rooted(root_for_db)
                color.print_error("Failed to initialize database in " + root_for_db)
                return ec.EXIT_DB_ERROR()
            end if
        else
            if not database.init_db()
                color.print_error("Failed to initialize package database")
                return ec.EXIT_DB_ERROR()
            end if
        end if

        mut di = 0
        while di < direct_count
            if not install_package(direct_files[di], cfg, root_for_db, opts)
                color.print_error("Failed to install: " + direct_files[di])
                return ec.EXIT_INSTALL_FAILED()
            end if
            di = di + 1
        end while

        if pkg_count == 0
            return ec.EXIT_SUCCESS()
        end if
    end if

    // Resolve dependencies for all packages
    // Use rooted resolver if installing to alternate root
    mut install_order: [string] = new [string](512)
    let root_for_resolve = config.get_root(cfg)
    mut install_count = 0
    if str.length(root_for_resolve) > 0
        install_count = resolver.get_install_order_rooted(packages, pkg_count, install_order, 512, root_for_resolve)
    else
        install_count = resolver.get_install_order(packages, pkg_count, install_order, 512)
    end if

    // When force/reinstall is set, add requested packages that the resolver
    // skipped (because they're already installed) back into the install order
    if opts.force and install_count < pkg_count
        mut fi = 0
        while fi < pkg_count
            let pkg = packages[fi]
            if not array_contains(install_order, install_count, pkg)
                install_order[install_count] = pkg
                install_count = install_count + 1
            end if
            fi = fi + 1
        end while
    end if

    if install_count == 0 and pkg_count > 0
        color.print_warning("No packages to install (all already installed or not found)")
        return ec.EXIT_SUCCESS()
    end if

    // Separate new installs from already-installed (for display)
    mut to_install: [string] = new [string](512)
    mut install_idx = 0

    mut dep_idx = 0
    while dep_idx < install_count
        to_install[install_idx] = install_order[dep_idx]
        install_idx = install_idx + 1
        dep_idx = dep_idx + 1
    end while

    // Ask for confirmation (unless -y flag)
    mut empty_upgrade: [string] = new [string](1)
    if not prompt.confirm_install(to_install, install_idx, empty_upgrade, 0, opts)
        color.print_info("Installation cancelled")
        return ec.EXIT_CANCELLED()
    end if

    // Dry-run: show what would be done and exit
    if opts.dry_run
        color.print_info("Dry run — no changes made")
        return ec.EXIT_SUCCESS()
    end if

    // Get root path for installation
    let root = get_root(cfg)

    // Initialize database in the target root
    let root_for_db = config.get_root(cfg)
    if str.length(root_for_db) > 0
        if not database.init_db_rooted(root_for_db)
            color.print_error("Failed to initialize database in " + root_for_db)
            return ec.EXIT_DB_ERROR()
        end if
    else
        if not database.init_db()
            color.print_error("Failed to initialize package database")
            return ec.EXIT_DB_ERROR()
        end if
    end if

    // Install all packages in resolved order (dependencies first)
    i = 0
    while i < install_count
        let pkg_arg = install_order[i]

        // Install this package
        if not install_package(pkg_arg, cfg, root_for_db, opts)
            color.print_error("Failed to install: " + pkg_arg)
            color.print_error("Aborting installation - dependency failed")
            return ec.EXIT_INSTALL_FAILED()
        end if

        i = i + 1
    end while

    return ec.EXIT_SUCCESS()
end execute

// Helper to check if array contains a string
fn array_contains(arr: [string], count: int, s: string): bool
    mut i = 0
    while i < count
        if str.equals(arr[i], s)
            return true
        end if
        i = i + 1
    end while
    return false
end array_contains

proc print_usage()
    println("Usage: coral install <package...>")
    println("")
    println("Install one or more packages.")
    println("")
    println("Arguments:")
    println("  <package>    Package name or path to .pkg.tar.xz file")
    println("")
    println("Options:")
    println("  -y, --yes    Don't ask for confirmation")
    println("  --force      Force reinstall if already installed")
    println("")
    println("Examples:")
    println("  coral install vim")
    println("  coral install /path/to/package.pkg.tar.xz")
    println("  coral install vim nano htop")
end print_usage

fn install_package(pkg_arg: string, cfg: config.Config, root_path: string, opts: types.GlobalOptions): bool
    // Determine if this is a file path or package name
    let pkg_path = resolve_package_path(pkg_arg, cfg)

    if str.length(pkg_path) == 0
        color.print_error("Package not found: " + pkg_arg)
        return false
    end if

    // Verify signature if required (for local packages not already verified by resolve_package_path)
    let require_signed = config.get_require_signed_packages(cfg)
    if require_signed
        let sig_path = pkg_path + ".sig"
        if file.fileExists(sig_path)
            color.print_info("Verifying package signature...")
            let verify_result = signing.verify_package(pkg_path, sig_path)
            if not verify_result.success or not verify_result.valid
                color.print_error("Signature verification failed: " + verify_result.error)
                return false
            end if
            color.print_success("Signature verified (key: " + verify_result.keyid + ")")
        else
            color.print_error("Package signature required but not found: " + sig_path)
            return false
        end if
    end if

    color.print_action("Installing " + pkg_arg + "...")

    // Create extraction directory
    let extract_dir = create_extract_dir(cfg)
    if str.length(extract_dir) == 0
        color.print_error("Failed to create extraction directory")
        return false
    end if

    // Extract package
    color.print_info("Extracting package...")
    let result = package.extract_package(pkg_path, extract_dir)
    if not result.success
        color.print_error(result.error)
        cleanup_extract_dir(extract_dir)
        return false
    end if

    // Read package info
    let info = package.read_pkginfo(extract_dir)
    if str.length(info.name) == 0
        color.print_error("Failed to read package info")
        cleanup_extract_dir(extract_dir)
        return false
    end if

    color.print_info("Package: " + info.name + " " + info.version)

    // Check for conflicts with installed packages
    if info.conflicts_count > 0
        mut ci = 0
        while ci < info.conflicts_count
            let conflict = info.conflicts[ci]
            if check_installed(conflict, root_path)
                color.print_error("Package " + info.name + " conflicts with installed package: " + conflict)
                if not opts.force
                    color.print_error("Use --force to override conflict check")
                    cleanup_extract_dir(extract_dir)
                    return false
                end if
                color.print_warning("Proceeding despite conflict (--force specified)")
            end if
            ci = ci + 1
        end while
    end if

    // Check if already installed (in target root)
    let already_installed = check_installed(info.name, root_path)
    mut is_upgrade = false
    mut old_version = ""

    if already_installed
        // Get old package info to check version
        let old_pkg = get_old_installed(info.name, root_path)
        old_version = old_pkg.info.version

        if not str.equals(old_version, info.version)
            // Different version — upgrade
            is_upgrade = true
            color.print_info("Upgrading " + info.name + " " + old_version + " -> " + info.version)
        else
            // Same version — skip or reinstall with --force
            if not opts.force
                color.print_warning(info.name + " " + info.version + " is already installed (use --force to reinstall)")
                cleanup_extract_dir(extract_dir)
                return true
            end if
            color.print_info("Reinstalling " + info.name + "...")
        end if
    end if

    let install_root = get_root(cfg)
    // Resolve script execution context; abort early if confinement is required but impossible.
    // Defense-in-depth: main.reef already bars non-root for all non-query commands, so this abort
    // is currently unreachable via the CLI. Kept so confinement fails safely if that global gate is
    // ever relaxed.
    if package.script_wants_chroot(install_root, config.get_chroot_scripts(cfg)) and not priv.is_root()
        color.print_error("--root " + install_root + " runs maintainer scripts confined to the target via chroot, which requires root privileges.")
        color.print_error("Re-run as root, or pass --no-chroot-scripts to run them on the host (unconfined).")
        cleanup_extract_dir(extract_dir)
        return false
    end if
    let script_ctx = package.resolve_script_ctx(install_root, config.get_chroot_scripts(cfg), priv.is_root())

    if is_upgrade
        // Upgrade flow: pre-upgrade → remove old files → install new files → post-upgrade

        // Run pre-upgrade from stored scripts (old package)
        let old_scripts = database.get_scripts_dir(info.name)
        if database.has_stored_scripts(info.name)
            color.print_info("Running pre-upgrade script...")
            if not package.run_stored_pre_upgrade(script_ctx, old_scripts, old_version, info.version)
                color.print_error("Pre-upgrade script failed, aborting")
                cleanup_extract_dir(extract_dir)
                return false
            end if
        end if

        // Remove old files
        color.print_info("Removing old version files...")
        mut old_files: [string] = new [string](4096)
        let old_file_count = get_old_files(info.name, old_files, 4096, root_path)
        if old_file_count > 0
            package.remove_files(old_files, old_file_count, install_root)
        end if

        // Load old manifest for config file checksum protection
        mut old_manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
        let old_manifest_count = load_old_manifest(info.name, root_path, old_manifest_entries, 8192)

        // Install new files (with upgrade config protection)
        color.print_info("Installing files...")
        if old_manifest_count > 0
            if not package.install_files_upgrade(extract_dir, install_root, old_manifest_entries, old_manifest_count)
                color.print_error("Failed to install files")
                cleanup_extract_dir(extract_dir)
                return false
            end if
        else
            if not package.install_files(extract_dir, install_root)
                color.print_error("Failed to install files")
                cleanup_extract_dir(extract_dir)
                return false
            end if
        end if

        // Run post-upgrade from new package's scripts
        if package.has_scripts(extract_dir)
            color.print_info("Running post-upgrade script...")
            if not package.run_post_upgrade(script_ctx, extract_dir, old_version, info.version)
                color.print_warning("Post-upgrade script failed")
            end if
        end if
    else
        // Fresh install flow

        // Run pre-install script (abort on failure)
        if package.has_scripts(extract_dir)
            color.print_info("Running pre-install script...")
            if not package.run_pre_install(script_ctx, extract_dir, info.version)
                color.print_error("Pre-install script failed")
                cleanup_extract_dir(extract_dir)
                return false
            end if
        end if

        // Install files to target root
        color.print_info("Installing files...")
        if not package.install_files(extract_dir, install_root)
            color.print_error("Failed to install files")
            cleanup_extract_dir(extract_dir)
            return false
        end if

        // Run post-install script (warn on failure, don't abort)
        if package.has_scripts(extract_dir)
            color.print_info("Running post-install script...")
            if not package.run_post_install(script_ctx, extract_dir, info.version)
                color.print_warning("Post-install script failed")
            end if
        end if
    end if

    // Register in database (use rooted version if root is set)
    color.print_info("Registering package...")
    if not register_installed_fast_rooted(info, extract_dir, root_path)
        color.print_error("Failed to register package in database")
        // Files are installed but not registered - warn but don't fail
    end if

    // Store scripts for later use (removal)
    if package.has_scripts(extract_dir)
        if not store_scripts_rooted(info.name, extract_dir, root_path)
            color.print_warning("Failed to store scripts for removal")
        end if
    end if

    // Store config files list for upgrade handling
    if not store_config_files_rooted(info.name, extract_dir, root_path)
        color.print_warning("Failed to store config files list")
    end if

    // Update package database indexes
    if not pkgdb.rebuild_indexes_rooted(root_path)
        color.print_warning("Failed to update package indexes")
    end if

    // Cleanup
    cleanup_extract_dir(extract_dir)

    color.print_success("Installed " + info.name + " " + info.version)
    return true
end install_package

// Check if package is installed, using rooted check if root is set
fn check_installed(name: string, root_path: string): bool
    if str.length(root_path) > 0
        return database.is_installed_rooted(name, root_path)
    end if
    return database.is_installed(name)
end check_installed

// Register package using rooted version if root is set
fn register_installed_fast_rooted(info: types.PackageInfo, extract_dir: string, root_path: string): bool
    mut files: [string] = new [string](1)
    mut pkg = new_installed_pkg(info, files, 0)

    if str.length(root_path) > 0
        return database.register_from_extract_dir_rooted(pkg, extract_dir, root_path)
    end if
    return database.register_from_extract_dir(pkg, extract_dir)
end register_installed_fast_rooted

// Store scripts using rooted version if root is set
fn store_scripts_rooted(name: string, extract_dir: string, root_path: string): bool
    if str.length(root_path) > 0
        return database.store_scripts_rooted(name, extract_dir, root_path)
    end if
    return database.store_scripts(name, extract_dir)
end store_scripts_rooted

// Store config files using rooted version if root is set
fn store_config_files_rooted(name: string, extract_dir: string, root_path: string): bool
    if str.length(root_path) > 0
        return database.store_config_files_rooted(name, extract_dir, root_path)
    end if
    return database.store_config_files(name, extract_dir)
end store_config_files_rooted

// Find package file - either direct path, repository, or search in packages directory
fn resolve_package_path(pkg_arg: string, cfg: config.Config): string
    // If it's a file path that exists, use it directly
    if file.fileExists(pkg_arg)
        return pkg_arg
    end if

    // Search in local packages cache first
    let packages_dir = config.get_packages_dir(cfg)

    // Find all matching packages and select highest version
    mut entries: [string] = new [string](256)
    let entry_count = dir.list_dir(packages_dir, entries, 256)

    // Collect all matching package files
    mut best_file = ""
    mut best_version = ""
    mut best_release = 0

    mut i = 0
    while i < entry_count
        let entry = entries[i]
        // Check if entry starts with package name and has correct suffix
        if str.starts_with(entry, pkg_arg + "-") and str.ends_with(entry, ".pkg.tar.xz")
            // Extract version from filename
            let entry_version = ver.extract_version_from_filename(entry, pkg_arg)
            let entry_release = ver.extract_release_from_filename(entry)

            // Compare with current best
            if str.length(best_file) == 0
                // First match
                best_file = entry
                best_version = entry_version
                best_release = entry_release
            else
                // Compare versions
                let cmp = ver.compare_versions(entry_version, best_version)
                if cmp > 0
                    // Entry has higher version
                    best_file = entry
                    best_version = entry_version
                    best_release = entry_release
                elif cmp == 0 and entry_release > best_release
                    // Same version but higher release
                    best_file = entry
                    best_version = entry_version
                    best_release = entry_release
                end if
            end if
        end if
        i = i + 1
    end while

    // Return the best matching package
    if str.length(best_file) > 0
        return path.join_path(packages_dir, best_file)
    end if

    // Also try with .pkg.tar.xz appended
    let direct_path = path.join_path(packages_dir, pkg_arg + ".pkg.tar.xz")
    if file.fileExists(direct_path)
        return direct_path
    end if

    // Try to build from source if configured
    if config.get_fallback_to_source(cfg)
        let port_result = port.find(pkg_arg)
        if port_result.success
            color.print_info("No binary package found for " + pkg_arg + ", building from source...")
            let build_cmd = "coral build -y " + pkg_arg
            let build_pid = process.process_spawn_shell(build_cmd)
            if build_pid > 0
                let build_exit = process.process_wait(build_pid)
                if build_exit == 0
                    // Build succeeded, look for the package it produced
                    let p = port_result.port
                    let built_pkg = p.info.name + "-" + p.info.version + "-" + int_to_str(p.info.release) + ".pkg.tar.xz"
                    let built_path = path.join_path(packages_dir, built_pkg)
                    if file.fileExists(built_path)
                        return built_path
                    end if
                end if
            end if
            color.print_warning("Source build failed for " + pkg_arg + ", trying repositories...")
        end if
    end if

    // Try to find in repositories and download
    let remote_result = repository.find_remote_package(pkg_arg)
    if remote_result.success
        let remote_pkg = remote_result.pkg
        color.print_info("Found " + pkg_arg + " " + remote_pkg.version + " in repository: " + remote_pkg.repo_name)

        // Download the package
        let downloaded_path = repository.download_package(remote_pkg)
        if str.length(downloaded_path) > 0
            // Verify checksum if available
            if str.length(remote_pkg.sha256) > 0
                color.print_info("Verifying checksum...")
                if not checksum.verify_checksum(downloaded_path, remote_pkg.sha256)
                    color.print_error("Checksum verification failed!")
                    return ""
                end if
                color.print_success("Checksum verified")
            end if

            // Verify signature if required or available
            let require_signed = config.get_require_signed_packages(cfg)
            if require_signed or str.length(remote_pkg.signature) > 0
                let sig_path = downloaded_path + ".sig"

                // Download signature file if we don't have it
                if not file.fileExists(sig_path) and str.length(remote_pkg.signature) > 0
                    color.print_info("Downloading signature...")
                    let sig_url = get_sig_url(remote_pkg)
                    if not download_signature(sig_url, sig_path)
                        if require_signed
                            color.print_error("Failed to download package signature")
                            return ""
                        else
                            color.print_warning("Could not download signature, skipping verification")
                        end if
                    end if
                end if

                // Verify signature if we have it
                if file.fileExists(sig_path)
                    color.print_info("Verifying signature...")
                    let verify_result = signing.verify_package(downloaded_path, sig_path)
                    if verify_result.success and verify_result.valid
                        color.print_success("Signature verified (key: " + verify_result.keyid + ")")
                    else
                        if require_signed
                            color.print_error("Signature verification failed: " + verify_result.error)
                            return ""
                        else
                            color.print_warning("Signature verification failed: " + verify_result.error)
                        end if
                    end if
                elif require_signed
                    color.print_error("Package signature required but not available")
                    return ""
                end if
            end if

            return downloaded_path
        end if
    end if

    return ""
end resolve_package_path

// Create a temporary extraction directory
fn create_extract_dir(cfg: config.Config): string
    let work_dir = config.get_work_dir(cfg)
    let pid = process.getpid()
    let extract_dir = path.join_path(work_dir, "install_" + int_to_str(pid))

    if dir.dir_exists(extract_dir)
        // Clean existing
        let cmd = "rm -rf \"" + extract_dir + "\""
        let rm_pid = process.process_spawn_shell(cmd)
        if rm_pid > 0
            process.process_wait(rm_pid)
        end if
    end if

    if not dir.create_dir_all(extract_dir)
        return ""
    end if

    return extract_dir
end create_extract_dir

// Cleanup extraction directory
fn cleanup_extract_dir(dir_path: string): bool
    if str.length(dir_path) == 0
        return true
    end if

    let cmd = "rm -rf \"" + dir_path + "\""
    let pid = process.process_spawn_shell(cmd)
    if pid < 0
        return false
    end if
    process.process_wait(pid)
    return true
end cleanup_extract_dir

// Get installation root from config (can be overridden with --root)
fn get_root(cfg: config.Config): string
    let root = config.get_root(cfg)
    if str.length(root) == 0
        return "/"
    end if
    return root
end get_root

// Check for file conflicts with other installed packages
// TODO: Implement efficient conflict detection using a file database
fn check_conflicts(files: [string], file_count: int, pkg_name: string): bool
    // Skip conflict checking for performance - file overwrites are handled
    // by the manifest-driven installer and dependencies are managed separately
    return true
end check_conflicts

// Helper to create InstalledPackage
fn new_installed_pkg(info: types.PackageInfo, files: [string], file_count: int): types.InstalledPackage
    mut pkg = types.new_installed_package()
    pkg.info = info
    pkg.install_date = get_date_string()
    pkg.install_reason = "explicit"
    pkg.files = files
    pkg.files_count = file_count
    return pkg
end new_installed_pkg

// Get current date as string (simplified)
fn get_date_string(): string
    // Use date command for simplicity
    // TODO: Use proper time functions when available
    return "2025-01-24"
end get_date_string

// Get installed package info (root-aware)
fn get_old_installed(name: string, root_path: string): types.InstalledPackage
    if str.length(root_path) > 0
        return database.get_installed_rooted(name, root_path)
    end if
    return database.get_installed(name)
end get_old_installed

// Get old file list for a package (root-aware)
fn get_old_files(name: string, files: [string], max_count: int, root_path: string): int
    if str.length(root_path) > 0
        return database.get_files_rooted(name, files, max_count, root_path)
    end if
    return database.get_files(name, files, max_count)
end get_old_files

// Load old manifest from database for upgrade config protection
fn load_old_manifest(name: string, root_path: string, entries: [mtree.ManifestEntry], max_count: int): int
    let manifest_path = database.get_manifest_path(name)
    if not file.fileExists(manifest_path)
        return 0
    end if

    let content = file.readFile(manifest_path)
    if str.length(content) == 0
        return 0
    end if

    return mtree.parse_manifest(content, entries, max_count)
end load_old_manifest

// Helper: convert int to string
fn int_to_str(n: int): string
    if n == 0
        return "0"
    end if

    mut negative = false
    mut value = n
    if n < 0
        negative = true
        value = 0 - n
    end if

    mut result = ""
    while value > 0
        let digit = value % 10
        result = str.concat(str.substring("0123456789", digit, 1), result)
        value = value / 10
    end while

    if negative
        result = str.concat("-", result)
    end if

    return result
end int_to_str

// Build signature URL from remote package info
fn get_sig_url(pkg: repository.RemotePackage): string
    mut base_url = pkg.repo_url
    if not str.ends_with(base_url, "/")
        base_url = base_url + "/"
    end if

    // If signature field contains a full URL, use it
    if str.starts_with(pkg.signature, "http")
        return pkg.signature
    end if

    // Otherwise construct from filename
    return base_url + "packages/" + pkg.filename + ".sig"
end get_sig_url

// Download a signature file
fn download_signature(url: string, dest: string): bool
    return uhttp.download(url, dest)
end download_signature

end module