gsd-opencode 1.9.2 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/agents/gsd-debugger.md +5 -5
  2. package/bin/gsd-install.js +105 -0
  3. package/bin/gsd.js +352 -0
  4. package/{command → commands}/gsd/add-phase.md +1 -1
  5. package/{command → commands}/gsd/audit-milestone.md +1 -1
  6. package/{command → commands}/gsd/debug.md +3 -3
  7. package/{command → commands}/gsd/discuss-phase.md +1 -1
  8. package/{command → commands}/gsd/execute-phase.md +1 -1
  9. package/{command → commands}/gsd/list-phase-assumptions.md +1 -1
  10. package/{command → commands}/gsd/map-codebase.md +1 -1
  11. package/{command → commands}/gsd/new-milestone.md +1 -1
  12. package/{command → commands}/gsd/new-project.md +3 -3
  13. package/{command → commands}/gsd/plan-phase.md +2 -2
  14. package/{command → commands}/gsd/research-phase.md +1 -1
  15. package/{command → commands}/gsd/verify-work.md +1 -1
  16. package/get-shit-done/workflows/list-phase-assumptions.md +1 -1
  17. package/get-shit-done/workflows/verify-work.md +5 -5
  18. package/lib/constants.js +193 -0
  19. package/package.json +34 -20
  20. package/src/commands/check.js +329 -0
  21. package/src/commands/config.js +337 -0
  22. package/src/commands/install.js +608 -0
  23. package/src/commands/list.js +256 -0
  24. package/src/commands/repair.js +519 -0
  25. package/src/commands/uninstall.js +732 -0
  26. package/src/commands/update.js +444 -0
  27. package/src/services/backup-manager.js +585 -0
  28. package/src/services/config.js +262 -0
  29. package/src/services/file-ops.js +830 -0
  30. package/src/services/health-checker.js +475 -0
  31. package/src/services/manifest-manager.js +301 -0
  32. package/src/services/migration-service.js +831 -0
  33. package/src/services/repair-service.js +846 -0
  34. package/src/services/scope-manager.js +303 -0
  35. package/src/services/settings.js +553 -0
  36. package/src/services/structure-detector.js +240 -0
  37. package/src/services/update-service.js +863 -0
  38. package/src/utils/hash.js +71 -0
  39. package/src/utils/interactive.js +222 -0
  40. package/src/utils/logger.js +128 -0
  41. package/src/utils/npm-registry.js +255 -0
  42. package/src/utils/path-resolver.js +226 -0
  43. /package/{command → commands}/gsd/add-todo.md +0 -0
  44. /package/{command → commands}/gsd/check-todos.md +0 -0
  45. /package/{command → commands}/gsd/complete-milestone.md +0 -0
  46. /package/{command → commands}/gsd/help.md +0 -0
  47. /package/{command → commands}/gsd/insert-phase.md +0 -0
  48. /package/{command → commands}/gsd/pause-work.md +0 -0
  49. /package/{command → commands}/gsd/plan-milestone-gaps.md +0 -0
  50. /package/{command → commands}/gsd/progress.md +0 -0
  51. /package/{command → commands}/gsd/quick.md +0 -0
  52. /package/{command → commands}/gsd/remove-phase.md +0 -0
  53. /package/{command → commands}/gsd/resume-work.md +0 -0
  54. /package/{command → commands}/gsd/set-model.md +0 -0
  55. /package/{command → commands}/gsd/set-profile.md +0 -0
  56. /package/{command → commands}/gsd/settings.md +0 -0
  57. /package/{command → commands}/gsd/update.md +0 -0
  58. /package/{command → commands}/gsd/whats-new.md +0 -0
@@ -0,0 +1,608 @@
1
+ /**
2
+ * Install command for GSD-OpenCode CLI.
3
+ *
4
+ * This module provides the main install functionality, orchestrating the
5
+ * installation process with support for global/local scope, interactive prompts,
6
+ * file operations with progress indicators, and comprehensive error handling.
7
+ *
8
+ * Implements requirements:
9
+ * - CLI-01: User can run gsd-opencode install to install the system
10
+ * - INST-01: Install supports --global flag for global installation
11
+ * - INST-02: Install supports --local flag for local installation
12
+ * - INST-03: Install prompts interactively for location if neither flag provided
13
+ * - INST-04: Install performs path replacement in .md files
14
+ * - INST-05: Install supports --config-dir to specify custom directory
15
+ * - INST-06: Install shows clear progress indicators during file operations
16
+ * - INST-07: Install creates VERSION file to track installed version
17
+ * - INST-08: Install validates target paths to prevent path traversal attacks
18
+ * - INST-09: Install uses atomic operations (temp-then-move)
19
+ * - INST-10: Install handles permission errors gracefully
20
+ * - ERROR-02: All commands handle signal interrupts gracefully with cleanup
21
+ *
22
+ * @module install
23
+ */
24
+
25
+ import { ScopeManager } from "../services/scope-manager.js";
26
+ import { ConfigManager } from "../services/config.js";
27
+ import { FileOperations } from "../services/file-ops.js";
28
+ import { ManifestManager } from "../services/manifest-manager.js";
29
+ import { logger, setVerbose } from "../utils/logger.js";
30
+ import {
31
+ promptInstallScope,
32
+ promptRepairOrFresh,
33
+ } from "../utils/interactive.js";
34
+ import {
35
+ ERROR_CODES,
36
+ DIRECTORIES_TO_COPY,
37
+ ALLOWED_NAMESPACES,
38
+ } from "../../lib/constants.js";
39
+ import fs from "fs/promises";
40
+ import path from "path";
41
+ import { fileURLToPath } from "url";
42
+
43
+ // Colors for banner
44
+ const cyan = "\x1b[36m";
45
+ const green = "\x1b[32m";
46
+ const yellow = "\x1b[33m";
47
+ const dim = "\x1b[2m";
48
+ const gray = "\x1b[90m";
49
+ const white = "\x1b[37m";
50
+ const reset = "\x1b[0m";
51
+
52
+ /**
53
+ * ASCII art banner for GSD-OpenCode
54
+ * @param {string} version - Package version
55
+ * @returns {string} Formatted banner string
56
+ */
57
+ function getBanner(version) {
58
+ return `
59
+ ${cyan} ██████╗ ███████╗██████╗
60
+ ██╔════╝ ██╔════╝██╔══██╗
61
+ ██║ ███╗███████╗██║ ██║
62
+ ██║ ██║╚════██║██║ ██║
63
+ ╚██████╔╝███████║██████╔╝
64
+ ╚═════╝ ╚══════╝╚═════╝${reset}
65
+
66
+ ${white}▄${reset}
67
+ ${gray}█▀▀█${reset} ${gray}█▀▀█${reset} ${gray}█▀▀█${reset} ${gray}█▀▀▄${reset} ${white}█▀▀▀${reset} ${white}█▀▀█${reset} ${white}█▀▀█${reset} ${white}█▀▀█${reset}
68
+ ${gray}█░░█${reset} ${gray}█░░█${reset} ${gray}█▀▀▀${reset} ${gray}█░░█${reset} ${white}█░░░${reset} ${white}█░░█${reset} ${white}█░░█${reset} ${white}█▀▀▀${reset}
69
+ ${gray}▀▀▀▀${reset} ${gray}█▀▀▀${reset} ${gray}▀▀▀▀${reset} ${gray}▀ ▀${reset} ${white}▀▀▀▀${reset} ${white}▀▀▀▀${reset} ${white}▀▀▀▀${reset} ${white}▀▀▀▀${reset}
70
+
71
+ Get Shit Done ${dim}v${version}${reset}
72
+ A meta-prompting, context engineering and spec-driven
73
+ development system for Cloude Code by TÂCHES
74
+ (adopted for OpenCode by rokicool, GLM4.7, and Kimi K2.5)
75
+
76
+ `;
77
+ }
78
+
79
+ /**
80
+ * Gets the package version from the source directory package.json.
81
+ *
82
+ * @param {string} sourceDir - Source directory containing the distribution
83
+ * @returns {Promise<string>} The package version
84
+ * @private
85
+ */
86
+ async function getPackageVersion(sourceDir) {
87
+ try {
88
+ // Read from the source directory's package.json
89
+ const packageJsonPath = path.join(sourceDir, "package.json");
90
+
91
+ const content = await fs.readFile(packageJsonPath, "utf-8");
92
+ const pkg = JSON.parse(content);
93
+ return pkg.version || "1.0.0";
94
+ } catch (error) {
95
+ logger.warning("Could not read package version from source, using 1.0.0");
96
+ return "1.0.0";
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Gets the source directory containing GSD-OpenCode files.
102
+ *
103
+ * @returns {string} Absolute path to the source directory
104
+ * @private
105
+ */
106
+ function getSourceDirectory() {
107
+ const __filename = fileURLToPath(import.meta.url);
108
+ const __dirname = path.dirname(__filename);
109
+ const packageRoot = path.resolve(__dirname, "../..");
110
+
111
+ // Source is the package root directory
112
+ // This contains the distribution files (agents, command, get-shit-done)
113
+ return packageRoot;
114
+ }
115
+
116
+ /**
117
+ * Handles errors with helpful messages and appropriate exit codes.
118
+ *
119
+ * Categorizes errors by code and provides actionable suggestions:
120
+ * - EACCES: Permission denied - suggest --local or sudo
121
+ * - ENOENT: File not found - check source directory exists
122
+ * - ENOSPC: Disk full - suggest freeing space
123
+ * - Path traversal: Invalid path - suggest valid paths
124
+ * - Generic: Show message with --verbose suggestion
125
+ *
126
+ * @param {Error} error - The error to handle
127
+ * @param {boolean} verbose - Whether verbose mode is enabled
128
+ * @returns {number} Exit code for the error
129
+ */
130
+ function handleError(error, verbose) {
131
+ // Log error in verbose mode
132
+ if (verbose) {
133
+ logger.debug(`Error details: ${error.stack || error.message}`);
134
+ logger.debug(`Error code: ${error.code}`);
135
+ }
136
+
137
+ // Categorize by error code
138
+ switch (error.code) {
139
+ case "EACCES":
140
+ logger.error("Permission denied: Cannot write to installation directory");
141
+ logger.dim("");
142
+ logger.dim("Suggestion: Try one of the following:");
143
+ logger.dim(" - Use --local for user directory installation");
144
+ logger.dim(" - Use sudo for global system-wide install");
145
+ logger.dim(" - Check directory ownership and permissions");
146
+ return ERROR_CODES.PERMISSION_ERROR;
147
+
148
+ case "ENOENT":
149
+ logger.error(`File or directory not found: ${error.message}`);
150
+ logger.dim("");
151
+ logger.dim(
152
+ "Suggestion: Check that the source directory exists and is accessible.",
153
+ );
154
+ if (error.message.includes("gsd-opencode")) {
155
+ logger.dim(
156
+ "The gsd-opencode directory may be missing from the package.",
157
+ );
158
+ }
159
+ return ERROR_CODES.GENERAL_ERROR;
160
+
161
+ case "ENOSPC":
162
+ logger.error("Insufficient disk space for installation");
163
+ logger.dim("");
164
+ logger.dim("Suggestion: Free up disk space and try again");
165
+ return ERROR_CODES.GENERAL_ERROR;
166
+
167
+ case "EEXIST":
168
+ logger.error(
169
+ "Installation target already exists and cannot be overwritten",
170
+ );
171
+ logger.dim("");
172
+ logger.dim(
173
+ "Suggestion: Use --force or remove the existing installation first",
174
+ );
175
+ return ERROR_CODES.GENERAL_ERROR;
176
+
177
+ case "ENOTEMPTY":
178
+ // This is handled internally by file-ops, but catch it here too
179
+ logger.error("Target directory is not empty");
180
+ return ERROR_CODES.GENERAL_ERROR;
181
+
182
+ default:
183
+ // Check for path traversal errors from validatePath
184
+ if (
185
+ error.message?.includes("traversal") ||
186
+ error.message?.includes("outside allowed")
187
+ ) {
188
+ logger.error("Invalid installation path: Path traversal detected");
189
+ logger.dim("");
190
+ logger.dim(
191
+ "Suggestion: Use absolute or relative paths within allowed directories",
192
+ );
193
+ logger.dim(" - Global: within home directory (~/)");
194
+ logger.dim(" - Local: within current working directory");
195
+ return ERROR_CODES.PATH_TRAVERSAL;
196
+ }
197
+
198
+ // Generic error
199
+ logger.error(`Installation failed: ${error.message}`);
200
+ logger.dim("");
201
+ if (!verbose) {
202
+ logger.dim(
203
+ "Suggestion: Run with --verbose for detailed error information",
204
+ );
205
+ }
206
+ return ERROR_CODES.GENERAL_ERROR;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Performs pre-flight checks before installation.
212
+ *
213
+ * Verifies:
214
+ * - Source directory exists
215
+ * - Source directory contains expected subdirectories
216
+ * - Parent directory of target is writable (if exists)
217
+ *
218
+ * @param {string} sourceDir - Source directory to check
219
+ * @param {string} targetDir - Target directory for installation
220
+ * @returns {Promise<void>}
221
+ * @throws {Error} If pre-flight checks fail
222
+ * @private
223
+ */
224
+ async function preflightChecks(sourceDir, targetDir) {
225
+ // Check source directory exists
226
+ try {
227
+ const sourceStat = await fs.stat(sourceDir);
228
+ if (!sourceStat.isDirectory()) {
229
+ throw new Error(`Source path is not a directory: ${sourceDir}`);
230
+ }
231
+ } catch (error) {
232
+ if (error.code === "ENOENT") {
233
+ throw new Error(
234
+ `Source directory not found: ${sourceDir}\n` +
235
+ "The gsd-opencode directory may be missing from the package installation.",
236
+ );
237
+ }
238
+ throw error;
239
+ }
240
+
241
+ // Check target parent directory exists and is writable
242
+ const targetParent = path.dirname(targetDir);
243
+ try {
244
+ const parentStat = await fs.stat(targetParent);
245
+ if (!parentStat.isDirectory()) {
246
+ throw new Error(`Target parent is not a directory: ${targetParent}`);
247
+ }
248
+
249
+ // Test write permission by trying to access with write intent
250
+ try {
251
+ await fs.access(targetParent, fs.constants.W_OK);
252
+ } catch (accessError) {
253
+ // On some systems, access check might fail even if we can write
254
+ // Try to create a test file
255
+ const testFile = path.join(targetParent, ".gsd-write-test");
256
+ try {
257
+ await fs.writeFile(testFile, "", "utf-8");
258
+ await fs.unlink(testFile);
259
+ } catch (writeError) {
260
+ throw new Error(
261
+ `Cannot write to target directory: ${targetParent}\n` +
262
+ "Check directory permissions or run with appropriate privileges.",
263
+ );
264
+ }
265
+ }
266
+ } catch (error) {
267
+ if (error.code === "ENOENT") {
268
+ // Parent doesn't exist, we'll create it during install
269
+ logger.debug(
270
+ `Target parent directory does not exist, will create: ${targetParent}`,
271
+ );
272
+ } else {
273
+ throw error;
274
+ }
275
+ }
276
+
277
+ // Check if target is a file (not directory)
278
+ try {
279
+ const targetStat = await fs.stat(targetDir);
280
+ if (targetStat.isFile()) {
281
+ throw new Error(`Target path exists and is a file: ${targetDir}`);
282
+ }
283
+ } catch (error) {
284
+ if (error.code !== "ENOENT") {
285
+ throw error;
286
+ }
287
+ // ENOENT is fine - target doesn't exist yet
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Cleans up empty directories in allowed namespaces.
293
+ * Only removes directories that are empty and within gsd-opencode namespaces.
294
+ *
295
+ * @param {string} targetDir - Target installation directory
296
+ * @param {RegExp[]} namespaces - Allowed namespace patterns
297
+ * @param {object} logger - Logger instance
298
+ * @returns {Promise<void>}
299
+ * @private
300
+ */
301
+ async function cleanupEmptyDirectories(targetDir, namespaces, logger) {
302
+ // Directories to check (in reverse order to remove deepest first)
303
+ const dirsToCheck = [
304
+ "get-shit-done",
305
+ "commands/gsd",
306
+ "command/gsd",
307
+ "agents/gsd-debugger",
308
+ "agents/gsd-executor",
309
+ "agents/gsd-integration-checker",
310
+ "agents/gsd-phase-researcher",
311
+ "agents/gsd-plan-checker",
312
+ "agents/gsd-planner",
313
+ "agents/gsd-project-researcher",
314
+ "agents/gsd-research-synthesizer",
315
+ "agents/gsd-roadmapper",
316
+ "agents/gsd-set-model",
317
+ "agents/gsd-verifier",
318
+ ];
319
+
320
+ for (const dir of dirsToCheck) {
321
+ const fullPath = path.join(targetDir, dir);
322
+ try {
323
+ const entries = await fs.readdir(fullPath);
324
+ if (entries.length === 0) {
325
+ await fs.rmdir(fullPath);
326
+ logger.debug(`Removed empty directory: ${dir}`);
327
+ }
328
+ } catch (error) {
329
+ // Directory doesn't exist or can't be removed, ignore
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Conservative cleanup for when no manifest exists.
336
+ * Only removes known gsd-opencode files, never the entire directory.
337
+ *
338
+ * @param {string} targetDir - Target installation directory
339
+ * @param {object} logger - Logger instance
340
+ * @returns {Promise<void>}
341
+ * @private
342
+ */
343
+ async function conservativeCleanup(targetDir, logger) {
344
+ // Only remove specific files we know belong to gsd-opencode
345
+ const filesToRemove = [
346
+ "get-shit-done/VERSION",
347
+ "get-shit-done/INSTALLED_FILES.json",
348
+ ];
349
+
350
+ for (const file of filesToRemove) {
351
+ try {
352
+ await fs.unlink(path.join(targetDir, file));
353
+ logger.debug(`Removed: ${file}`);
354
+ } catch (error) {
355
+ if (error.code !== "ENOENT") {
356
+ logger.debug(`Could not remove ${file}: ${error.message}`);
357
+ }
358
+ }
359
+ }
360
+
361
+ // Clean up empty directories
362
+ await cleanupEmptyDirectories(targetDir, ALLOWED_NAMESPACES, logger);
363
+ }
364
+
365
+ /**
366
+ * Main install command function.
367
+ *
368
+ * Orchestrates the installation process:
369
+ * 1. Parse options and set verbose mode
370
+ * 2. Determine installation scope (global/local) via flags or prompt
371
+ * 3. Check for existing installation and prompt for action
372
+ * 4. Perform installation with file operations
373
+ * 5. Create VERSION file
374
+ * 6. Show success summary
375
+ *
376
+ * @param {Object} options - Command options
377
+ * @param {boolean} [options.global] - Install globally
378
+ * @param {boolean} [options.local] - Install locally
379
+ * @param {string} [options.configDir] - Custom configuration directory
380
+ * @param {boolean} [options.verbose] - Enable verbose output
381
+ * @returns {Promise<number>} Exit code (0 for success, non-zero for errors)
382
+ *
383
+ * @example
384
+ * // Install globally
385
+ * await installCommand({ global: true });
386
+ *
387
+ * // Install locally with verbose output
388
+ * await installCommand({ local: true, verbose: true });
389
+ *
390
+ * // Install interactively (prompts for scope)
391
+ * await installCommand({});
392
+ */
393
+ export async function installCommand(options = {}) {
394
+ // Set verbose mode early
395
+ const verbose = options.verbose || false;
396
+ setVerbose(verbose);
397
+
398
+ logger.debug("Starting install command");
399
+ logger.debug(
400
+ `Options: global=${options.global}, local=${options.local}, configDir=${options.configDir}, verbose=${verbose}`,
401
+ );
402
+
403
+ try {
404
+ // Display banner
405
+ const sourceDir = getSourceDirectory();
406
+ const version = await getPackageVersion(sourceDir);
407
+ console.log(getBanner(version));
408
+
409
+ // Step 1: Determine scope
410
+ let scope;
411
+ if (options.global) {
412
+ scope = "global";
413
+ logger.debug("Scope determined by --global flag");
414
+ } else if (options.local) {
415
+ scope = "local";
416
+ logger.debug("Scope determined by --local flag");
417
+ } else {
418
+ // Prompt user interactively
419
+ logger.debug("No scope flags provided, prompting user...");
420
+ scope = await promptInstallScope();
421
+
422
+ if (scope === null) {
423
+ // User cancelled (Ctrl+C)
424
+ logger.info("Installation cancelled by user");
425
+ return ERROR_CODES.INTERRUPTED;
426
+ }
427
+ }
428
+
429
+ logger.debug(`Selected scope: ${scope}`);
430
+
431
+ // Step 2: Create ScopeManager and ConfigManager
432
+ const scopeManager = new ScopeManager({
433
+ scope,
434
+ configDir: options.configDir,
435
+ });
436
+ const config = new ConfigManager(scopeManager);
437
+
438
+ logger.debug(`Target directory: ${scopeManager.getTargetDir()}`);
439
+
440
+ // Step 3: Check for existing installation
441
+ const isInstalled = await scopeManager.isInstalled();
442
+ if (isInstalled) {
443
+ const existingVersion = scopeManager.getInstalledVersion();
444
+ logger.warning(
445
+ `Existing installation detected${existingVersion ? ` (version ${existingVersion})` : ""}`,
446
+ );
447
+
448
+ const action = await promptRepairOrFresh();
449
+
450
+ if (action === "cancel" || action === null) {
451
+ logger.info("Installation cancelled by user");
452
+ return ERROR_CODES.INTERRUPTED;
453
+ }
454
+
455
+ if (action === "repair") {
456
+ // Phase 4 will implement proper repair
457
+ // For now, treat as fresh install
458
+ logger.info(
459
+ "Repair selected - performing fresh install (repair functionality coming in Phase 4)",
460
+ );
461
+ } else {
462
+ logger.info(
463
+ "Fresh install selected - removing existing gsd-opencode files",
464
+ );
465
+ }
466
+
467
+ // Fresh install: remove only gsd-opencode files (not entire directory)
468
+ // This preserves other opencode configuration and files
469
+ const targetDir = scopeManager.getTargetDir();
470
+ try {
471
+ const manifestManager = new ManifestManager(targetDir);
472
+ const manifestEntries = await manifestManager.load();
473
+
474
+ if (manifestEntries && manifestEntries.length > 0) {
475
+ // Filter to only files in allowed namespaces
476
+ const filesToRemove = manifestEntries.filter((entry) =>
477
+ manifestManager.isInAllowedNamespace(
478
+ entry.relativePath,
479
+ ALLOWED_NAMESPACES,
480
+ ),
481
+ );
482
+
483
+ logger.debug(
484
+ `Removing ${filesToRemove.length} tracked files in allowed namespaces`,
485
+ );
486
+
487
+ // Remove files only (directories will be cleaned up later if empty)
488
+ for (const entry of filesToRemove) {
489
+ try {
490
+ await fs.unlink(entry.path);
491
+ logger.debug(`Removed: ${entry.relativePath}`);
492
+ } catch (error) {
493
+ if (error.code !== "ENOENT") {
494
+ logger.debug(
495
+ `Could not remove ${entry.relativePath}: ${error.message}`,
496
+ );
497
+ }
498
+ }
499
+ }
500
+
501
+ // Clean up empty directories in allowed namespaces
502
+ await cleanupEmptyDirectories(targetDir, ALLOWED_NAMESPACES, logger);
503
+
504
+ // Forcefully remove structure directories to ensure fresh install works
505
+ // This handles cases where files remain in the structure directories
506
+ const structureDirs = ["commands/gsd", "command/gsd"];
507
+ for (const dir of structureDirs) {
508
+ const fullPath = path.join(targetDir, dir);
509
+ try {
510
+ await fs.rm(fullPath, { recursive: true, force: true });
511
+ logger.debug(`Removed structure directory: ${dir}`);
512
+ } catch (error) {
513
+ // Directory might not exist, ignore
514
+ }
515
+ }
516
+
517
+ logger.debug(
518
+ "Removed existing gsd-opencode files while preserving other config",
519
+ );
520
+ } else {
521
+ // No manifest found - use conservative fallback
522
+ logger.debug(
523
+ "No manifest found, using conservative fallback cleanup",
524
+ );
525
+ await conservativeCleanup(targetDir, logger);
526
+
527
+ // Forcefully remove structure directories to ensure fresh install works
528
+ const structureDirs = ["commands/gsd", "command/gsd"];
529
+ for (const dir of structureDirs) {
530
+ const fullPath = path.join(targetDir, dir);
531
+ try {
532
+ await fs.rm(fullPath, { recursive: true, force: true });
533
+ logger.debug(`Removed structure directory: ${dir}`);
534
+ } catch (error) {
535
+ // Directory might not exist, ignore
536
+ }
537
+ }
538
+ }
539
+ } catch (error) {
540
+ logger.warning(
541
+ `Could not remove existing installation: ${error.message}`,
542
+ );
543
+ // Continue anyway - file-ops will handle conflicts
544
+ }
545
+ }
546
+
547
+ // Step 4: Show starting message
548
+ const scopeLabel = scope === "global" ? "Global" : "Local";
549
+ const pathPrefix = scopeManager.getPathPrefix();
550
+ logger.heading(`${scopeLabel} Installation`);
551
+ logger.info(`Installing to ${pathPrefix}...`);
552
+
553
+ // Step 5: Pre-flight checks
554
+ const targetDir = scopeManager.getTargetDir();
555
+
556
+ logger.debug(`Source directory: ${sourceDir}`);
557
+ logger.debug(`Target directory: ${targetDir}`);
558
+
559
+ await preflightChecks(sourceDir, targetDir);
560
+
561
+ // Step 6: Perform installation
562
+ const fileOps = new FileOperations(scopeManager, logger);
563
+ const result = await fileOps.install(sourceDir, targetDir);
564
+
565
+ // Step 7: Create VERSION file
566
+ await config.setVersion(version);
567
+ logger.debug(`Created VERSION file with version: ${version}`);
568
+
569
+ // Step 8: Show success summary
570
+ logger.success("Installation complete!");
571
+ logger.dim("");
572
+ logger.dim("Summary:");
573
+ logger.dim(` Files copied: ${result.filesCopied}`);
574
+ logger.dim(` Directories: ${result.directories}`);
575
+ logger.dim(` Location: ${pathPrefix}`);
576
+ logger.dim(` Version: ${version}`);
577
+
578
+ if (verbose) {
579
+ logger.dim("");
580
+ logger.dim("Additional details:");
581
+ logger.dim(` Full path: ${targetDir}`);
582
+ logger.dim(` Scope: ${scope}`);
583
+ }
584
+
585
+ return ERROR_CODES.SUCCESS;
586
+ } catch (error) {
587
+ // Handle Ctrl+C during async operations
588
+ if (
589
+ error.name === "AbortPromptError" ||
590
+ error.message?.includes("cancel")
591
+ ) {
592
+ logger.info("\nInstallation cancelled by user");
593
+ return ERROR_CODES.INTERRUPTED;
594
+ }
595
+
596
+ // Handle all other errors
597
+ return handleError(error, verbose);
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Default export for the install command.
603
+ *
604
+ * @example
605
+ * import installCommand from './commands/install.js';
606
+ * await installCommand({ global: true });
607
+ */
608
+ export default installCommand;