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,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope manager for handling global vs local installation paths.
|
|
3
|
+
*
|
|
4
|
+
* This module provides centralized scope management for the GSD-OpenCode CLI,
|
|
5
|
+
* handling path resolution for global (~/.config/opencode) and local (./.opencode)
|
|
6
|
+
* installations. Supports custom configuration directories via environment variable
|
|
7
|
+
* or explicit option.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY NOTE: All paths are validated to prevent directory traversal attacks.
|
|
10
|
+
* The constructor validates custom config directories to ensure they don't escape
|
|
11
|
+
* the allowed base directories.
|
|
12
|
+
*
|
|
13
|
+
* @module scope-manager
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import os from 'os';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import { expandPath, validatePath } from '../utils/path-resolver.js';
|
|
20
|
+
import { DEFAULT_CONFIG_DIR, VERSION_FILE } from '../../lib/constants.js';
|
|
21
|
+
import { StructureDetector, STRUCTURE_TYPES } from './structure-detector.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manages installation scope (global vs local) and path resolution.
|
|
25
|
+
*
|
|
26
|
+
* This class centralizes the logic for determining where GSD-OpenCode should be
|
|
27
|
+
* installed, whether globally in the user's home directory or locally in the
|
|
28
|
+
* current project. It handles:
|
|
29
|
+
*
|
|
30
|
+
* - Path resolution for global and local directories
|
|
31
|
+
* - Custom configuration directory support
|
|
32
|
+
* - Installation status detection (via VERSION file)
|
|
33
|
+
* - Path traversal prevention for security
|
|
34
|
+
*
|
|
35
|
+
* @class ScopeManager
|
|
36
|
+
* @example
|
|
37
|
+
* const scope = new ScopeManager({ scope: 'global' });
|
|
38
|
+
* console.log(scope.getTargetDir()); // '/home/user/.config/opencode'
|
|
39
|
+
* console.log(scope.isGlobal()); // true
|
|
40
|
+
*/
|
|
41
|
+
export class ScopeManager {
|
|
42
|
+
/**
|
|
43
|
+
* Creates a new ScopeManager instance.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} options - Configuration options
|
|
46
|
+
* @param {string} options.scope - Installation scope: 'global' or 'local'
|
|
47
|
+
* @param {string} [options.configDir] - Custom configuration directory (overrides default and env var)
|
|
48
|
+
* @throws {Error} If scope is not 'global' or 'local'
|
|
49
|
+
* @throws {Error} If configDir contains path traversal attempts
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Global installation (default location)
|
|
53
|
+
* const globalScope = new ScopeManager({ scope: 'global' });
|
|
54
|
+
*
|
|
55
|
+
* // Local installation (project-specific)
|
|
56
|
+
* const localScope = new ScopeManager({ scope: 'local' });
|
|
57
|
+
*
|
|
58
|
+
* // Custom global directory
|
|
59
|
+
* const customScope = new ScopeManager({
|
|
60
|
+
* scope: 'global',
|
|
61
|
+
* configDir: '/custom/path'
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // Via environment variable
|
|
65
|
+
* process.env.OPENCODE_CONFIG_DIR = '/env/path';
|
|
66
|
+
* const envScope = new ScopeManager({ scope: 'global' });
|
|
67
|
+
*/
|
|
68
|
+
constructor(options = {}) {
|
|
69
|
+
if (!options.scope || !['global', 'local'].includes(options.scope)) {
|
|
70
|
+
throw new Error('Scope must be either "global" or "local"');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.scope = options.scope;
|
|
74
|
+
|
|
75
|
+
// Determine global directory: explicit option > env var > default
|
|
76
|
+
const explicitConfigDir = options.configDir;
|
|
77
|
+
const envConfigDir = process.env.OPENCODE_CONFIG_DIR;
|
|
78
|
+
const defaultGlobalDir = path.join(os.homedir(), DEFAULT_CONFIG_DIR);
|
|
79
|
+
|
|
80
|
+
if (explicitConfigDir) {
|
|
81
|
+
// Validate custom config directory to prevent traversal
|
|
82
|
+
const expandedDir = expandPath(explicitConfigDir);
|
|
83
|
+
// Custom dirs must be within home directory or be absolute system paths
|
|
84
|
+
this.globalDir = validatePath(expandedDir, '/');
|
|
85
|
+
} else if (envConfigDir) {
|
|
86
|
+
this.globalDir = path.join(os.homedir(), envConfigDir);
|
|
87
|
+
} else {
|
|
88
|
+
this.globalDir = defaultGlobalDir;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Local directory is always relative to current working directory
|
|
92
|
+
this.localDir = path.join(process.cwd(), '.opencode');
|
|
93
|
+
|
|
94
|
+
// Track if using non-default config directory
|
|
95
|
+
this._isCustomConfig = Boolean(explicitConfigDir);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns the target installation directory based on scope.
|
|
100
|
+
*
|
|
101
|
+
* @returns {string} Absolute path to the installation directory
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* const globalScope = new ScopeManager({ scope: 'global' });
|
|
105
|
+
* globalScope.getTargetDir(); // '/home/user/.config/opencode'
|
|
106
|
+
*
|
|
107
|
+
* const localScope = new ScopeManager({ scope: 'local' });
|
|
108
|
+
* localScope.getTargetDir(); // '/current/working/dir/.opencode'
|
|
109
|
+
*/
|
|
110
|
+
getTargetDir() {
|
|
111
|
+
return this.scope === 'global' ? this.globalDir : this.localDir;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns a display-friendly path prefix.
|
|
116
|
+
*
|
|
117
|
+
* Converts absolute paths to user-friendly representations:
|
|
118
|
+
* - Home directory paths show as ~/
|
|
119
|
+
* - Other absolute paths show relative to cwd if possible
|
|
120
|
+
*
|
|
121
|
+
* @returns {string} Display-friendly path prefix
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* const globalScope = new ScopeManager({ scope: 'global' });
|
|
125
|
+
* globalScope.getPathPrefix(); // '~/.config/opencode'
|
|
126
|
+
*
|
|
127
|
+
* const localScope = new ScopeManager({ scope: 'local' });
|
|
128
|
+
* localScope.getPathPrefix(); // './.opencode'
|
|
129
|
+
*/
|
|
130
|
+
getPathPrefix() {
|
|
131
|
+
const targetDir = this.getTargetDir();
|
|
132
|
+
|
|
133
|
+
if (this.scope === 'local') {
|
|
134
|
+
return './.opencode';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// For global, try to use ~ shorthand if within home directory
|
|
138
|
+
const homeDir = os.homedir();
|
|
139
|
+
if (targetDir.startsWith(homeDir)) {
|
|
140
|
+
return '~' + targetDir.substring(homeDir.length);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return targetDir;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Checks if GSD-OpenCode is installed at the target directory.
|
|
148
|
+
*
|
|
149
|
+
* This method verifies installation by checking for:
|
|
150
|
+
* 1. The presence of a VERSION file at the target directory
|
|
151
|
+
* 2. OR the presence of GSD files in either old (command/gsd/) or new (commands/gsd/) structure
|
|
152
|
+
*
|
|
153
|
+
* This ensures we detect both new installations and legacy installations that may
|
|
154
|
+
* be missing a VERSION file or using the old directory structure.
|
|
155
|
+
*
|
|
156
|
+
* @returns {boolean} True if GSD-OpenCode is installed at target directory
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* const scope = new ScopeManager({ scope: 'global' });
|
|
160
|
+
* if (scope.isInstalled()) {
|
|
161
|
+
* console.log('GSD-OpenCode is installed');
|
|
162
|
+
* }
|
|
163
|
+
*/
|
|
164
|
+
async isInstalled() {
|
|
165
|
+
try {
|
|
166
|
+
const targetDir = this.getTargetDir();
|
|
167
|
+
|
|
168
|
+
// Check for VERSION file (normal case)
|
|
169
|
+
const versionPath = path.join(targetDir, VERSION_FILE);
|
|
170
|
+
if (fs.existsSync(versionPath)) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for actual GSD installation (old or new structure)
|
|
175
|
+
// This handles legacy installations that might not have VERSION file
|
|
176
|
+
const structureDetector = new StructureDetector(targetDir);
|
|
177
|
+
const structure = await structureDetector.detect();
|
|
178
|
+
|
|
179
|
+
return structure !== STRUCTURE_TYPES.NONE;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Synchronous version of isInstalled() for backwards compatibility.
|
|
187
|
+
*
|
|
188
|
+
* Note: This only checks for VERSION file existence. For comprehensive
|
|
189
|
+
* detection including old structure installations, use isInstalled() (async).
|
|
190
|
+
*
|
|
191
|
+
* @returns {boolean} True if VERSION file exists at target directory
|
|
192
|
+
* @deprecated Use async isInstalled() for complete detection
|
|
193
|
+
*/
|
|
194
|
+
isInstalledSync() {
|
|
195
|
+
try {
|
|
196
|
+
const versionPath = path.join(this.getTargetDir(), VERSION_FILE);
|
|
197
|
+
return fs.existsSync(versionPath);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reads the installed version from the VERSION file.
|
|
205
|
+
*
|
|
206
|
+
* @returns {string|null} The installed version string, or null if not installed
|
|
207
|
+
* @throws {Error} If VERSION file exists but cannot be read
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* const scope = new ScopeManager({ scope: 'global' });
|
|
211
|
+
* const version = scope.getInstalledVersion();
|
|
212
|
+
* if (version) {
|
|
213
|
+
* console.log(`Installed: ${version}`);
|
|
214
|
+
* }
|
|
215
|
+
*/
|
|
216
|
+
getInstalledVersion() {
|
|
217
|
+
try {
|
|
218
|
+
const versionPath = path.join(this.getTargetDir(), VERSION_FILE);
|
|
219
|
+
if (!fs.existsSync(versionPath)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return fs.readFileSync(versionPath, 'utf-8').trim();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Returns the current installation scope.
|
|
230
|
+
*
|
|
231
|
+
* @returns {string} 'global' or 'local'
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* const scope = new ScopeManager({ scope: 'global' });
|
|
235
|
+
* scope.getScope(); // 'global'
|
|
236
|
+
*/
|
|
237
|
+
getScope() {
|
|
238
|
+
return this.scope;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Checks if the current scope is global.
|
|
243
|
+
*
|
|
244
|
+
* @returns {boolean} True if scope is 'global'
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* const scope = new ScopeManager({ scope: 'global' });
|
|
248
|
+
* scope.isGlobal(); // true
|
|
249
|
+
* scope.isLocal(); // false
|
|
250
|
+
*/
|
|
251
|
+
isGlobal() {
|
|
252
|
+
return this.scope === 'global';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Checks if the current scope is local.
|
|
257
|
+
*
|
|
258
|
+
* @returns {boolean} True if scope is 'local'
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const scope = new ScopeManager({ scope: 'local' });
|
|
262
|
+
* scope.isLocal(); // true
|
|
263
|
+
* scope.isGlobal(); // false
|
|
264
|
+
*/
|
|
265
|
+
isLocal() {
|
|
266
|
+
return this.scope === 'local';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Checks if using a non-default configuration directory.
|
|
271
|
+
*
|
|
272
|
+
* Returns true if a custom configDir was explicitly provided to the
|
|
273
|
+
* constructor, indicating the user wants to use a non-standard location.
|
|
274
|
+
*
|
|
275
|
+
* @returns {boolean} True if using custom config directory
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* // Default configuration
|
|
279
|
+
* const defaultScope = new ScopeManager({ scope: 'global' });
|
|
280
|
+
* defaultScope.isCustomConfig(); // false
|
|
281
|
+
*
|
|
282
|
+
* // Custom configuration
|
|
283
|
+
* const customScope = new ScopeManager({
|
|
284
|
+
* scope: 'global',
|
|
285
|
+
* configDir: '/custom/path'
|
|
286
|
+
* });
|
|
287
|
+
* customScope.isCustomConfig(); // true
|
|
288
|
+
*/
|
|
289
|
+
isCustomConfig() {
|
|
290
|
+
return this._isCustomConfig;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Default export for the scope-manager module.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* import { ScopeManager } from './services/scope-manager.js';
|
|
299
|
+
* const scope = new ScopeManager({ scope: 'global' });
|
|
300
|
+
*/
|
|
301
|
+
export default {
|
|
302
|
+
ScopeManager
|
|
303
|
+
};
|