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.
- package/agents/gsd-debugger.md +5 -5
- package/bin/gsd-install.js +105 -0
- package/bin/gsd.js +352 -0
- package/{command → commands}/gsd/add-phase.md +1 -1
- package/{command → commands}/gsd/audit-milestone.md +1 -1
- package/{command → commands}/gsd/debug.md +3 -3
- package/{command → commands}/gsd/discuss-phase.md +1 -1
- package/{command → commands}/gsd/execute-phase.md +1 -1
- package/{command → commands}/gsd/list-phase-assumptions.md +1 -1
- package/{command → commands}/gsd/map-codebase.md +1 -1
- package/{command → commands}/gsd/new-milestone.md +1 -1
- package/{command → commands}/gsd/new-project.md +3 -3
- package/{command → commands}/gsd/plan-phase.md +2 -2
- package/{command → commands}/gsd/research-phase.md +1 -1
- package/{command → commands}/gsd/verify-work.md +1 -1
- package/get-shit-done/workflows/list-phase-assumptions.md +1 -1
- package/get-shit-done/workflows/verify-work.md +5 -5
- package/lib/constants.js +193 -0
- package/package.json +34 -20
- package/src/commands/check.js +329 -0
- package/src/commands/config.js +337 -0
- package/src/commands/install.js +608 -0
- package/src/commands/list.js +256 -0
- package/src/commands/repair.js +519 -0
- package/src/commands/uninstall.js +732 -0
- package/src/commands/update.js +444 -0
- package/src/services/backup-manager.js +585 -0
- package/src/services/config.js +262 -0
- package/src/services/file-ops.js +830 -0
- package/src/services/health-checker.js +475 -0
- package/src/services/manifest-manager.js +301 -0
- package/src/services/migration-service.js +831 -0
- package/src/services/repair-service.js +846 -0
- package/src/services/scope-manager.js +303 -0
- package/src/services/settings.js +553 -0
- package/src/services/structure-detector.js +240 -0
- package/src/services/update-service.js +863 -0
- package/src/utils/hash.js +71 -0
- package/src/utils/interactive.js +222 -0
- package/src/utils/logger.js +128 -0
- package/src/utils/npm-registry.js +255 -0
- package/src/utils/path-resolver.js +226 -0
- /package/{command → commands}/gsd/add-todo.md +0 -0
- /package/{command → commands}/gsd/check-todos.md +0 -0
- /package/{command → commands}/gsd/complete-milestone.md +0 -0
- /package/{command → commands}/gsd/help.md +0 -0
- /package/{command → commands}/gsd/insert-phase.md +0 -0
- /package/{command → commands}/gsd/pause-work.md +0 -0
- /package/{command → commands}/gsd/plan-milestone-gaps.md +0 -0
- /package/{command → commands}/gsd/progress.md +0 -0
- /package/{command → commands}/gsd/quick.md +0 -0
- /package/{command → commands}/gsd/remove-phase.md +0 -0
- /package/{command → commands}/gsd/resume-work.md +0 -0
- /package/{command → commands}/gsd/set-model.md +0 -0
- /package/{command → commands}/gsd/set-profile.md +0 -0
- /package/{command → commands}/gsd/settings.md +0 -0
- /package/{command → commands}/gsd/update.md +0 -0
- /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;
|