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,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
+ };