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,226 @@
1
+ /**
2
+ * Path resolver utility with security-focused traversal protection.
3
+ *
4
+ * This module provides safe path resolution functions that prevent directory
5
+ * traversal attacks. All functions normalize paths and validate that resolved
6
+ * paths remain within allowed directory boundaries.
7
+ *
8
+ * SECURITY NOTE: Always validate paths before using them in file operations.
9
+ * The validatePath() function throws descriptive errors for traversal attempts.
10
+ *
11
+ * @module path-resolver
12
+ */
13
+
14
+ import path from 'path';
15
+ import os from 'os';
16
+ import fs from 'fs';
17
+
18
+ /**
19
+ * Expand a path string, resolving ~ to home directory and relative paths.
20
+ *
21
+ * @param {string} pathStr - Path string to expand (may contain ~ or relative segments)
22
+ * @returns {string} Absolute, normalized path
23
+ * @throws {Error} If path contains null bytes (potential injection attack)
24
+ *
25
+ * @example
26
+ * expandPath('~/.config/opencode')
27
+ * // Returns: '/Users/username/.config/opencode' (macOS)
28
+ *
29
+ * expandPath('./relative/path')
30
+ * // Returns: '/absolute/path/to/relative/path'
31
+ */
32
+ export function expandPath(pathStr) {
33
+ if (typeof pathStr !== 'string') {
34
+ throw new Error('Path must be a string');
35
+ }
36
+
37
+ // Security: Reject null bytes which could be used for injection attacks
38
+ if (pathStr.includes('\0')) {
39
+ throw new Error('Path contains null bytes');
40
+ }
41
+
42
+ let expanded = pathStr;
43
+
44
+ // Expand ~ to home directory
45
+ if (expanded.startsWith('~')) {
46
+ expanded = expanded.replace('~', os.homedir());
47
+ }
48
+
49
+ // Resolve to absolute path
50
+ const resolved = path.resolve(expanded);
51
+
52
+ // Normalize to handle .. and . segments consistently
53
+ return normalizePath(resolved);
54
+ }
55
+
56
+ /**
57
+ * Normalize a path consistently across platforms.
58
+ *
59
+ * @param {string} pathStr - Path to normalize
60
+ * @returns {string} Normalized path with consistent separators
61
+ */
62
+ export function normalizePath(pathStr) {
63
+ if (typeof pathStr !== 'string') {
64
+ throw new Error('Path must be a string');
65
+ }
66
+
67
+ // Use path.normalize() to resolve .. and . segments
68
+ // This ensures consistent path representation across platforms
69
+ return path.normalize(pathStr);
70
+ }
71
+
72
+ /**
73
+ * Check if a child path is within a parent path.
74
+ *
75
+ * @param {string} childPath - Path to check (child candidate)
76
+ * @param {string} parentPath - Parent directory path
77
+ * @returns {boolean} True if child is within or equal to parent
78
+ *
79
+ * @example
80
+ * isSubPath('/home/user/.config', '/home/user') // true
81
+ * isSubPath('/home/user', '/home/user') // true
82
+ * isSubPath('/etc/passwd', '/home/user') // false
83
+ */
84
+ export function isSubPath(childPath, parentPath) {
85
+ if (typeof childPath !== 'string' || typeof parentPath !== 'string') {
86
+ throw new Error('Paths must be strings');
87
+ }
88
+
89
+ // Normalize both paths to ensure consistent comparison
90
+ const normalizedChild = normalizePath(childPath);
91
+ const normalizedParent = normalizePath(parentPath);
92
+
93
+ // Get relative path from parent to child
94
+ const relative = path.relative(normalizedParent, normalizedChild);
95
+
96
+ // If relative path starts with '..', child is outside parent
97
+ // Empty string means same path
98
+ // Path starting without '..' means child is inside or equal to parent
99
+ if (relative === '') {
100
+ return true; // Same path
101
+ }
102
+
103
+ // On Windows, relative paths don't use .. for same-level directories
104
+ // Check if relative path contains '..' at the start
105
+ return !relative.startsWith('..');
106
+ }
107
+
108
+ /**
109
+ * Validate that a target path does not escape an allowed base directory.
110
+ *
111
+ * SECURITY CRITICAL: This function must be called before any file operation
112
+ * that uses user-provided paths. It prevents directory traversal attacks
113
+ * where malicious input like '../../../etc/passwd' attempts to access
114
+ * files outside the intended directory.
115
+ *
116
+ * @param {string} targetPath - Path to validate
117
+ * @param {string} allowedBasePath - Base directory the target must remain within
118
+ * @returns {string} The resolved, validated absolute path
119
+ * @throws {Error} If path escapes allowed base directory (traversal detected)
120
+ * @throws {Error} If path contains null bytes
121
+ *
122
+ * @example
123
+ * validatePath('/home/user/.config/opencode', '/home/user') // OK
124
+ * validatePath('/etc/passwd', '/home/user') // Throws error
125
+ *
126
+ * // Handles symlink resolution
127
+ * const realTarget = fs.realpathSync(targetPath);
128
+ * validatePath(realTarget, allowedBasePath);
129
+ */
130
+ export function validatePath(targetPath, allowedBasePath) {
131
+ if (typeof targetPath !== 'string' || typeof allowedBasePath !== 'string') {
132
+ throw new Error('Paths must be strings');
133
+ }
134
+
135
+ // Security: Reject null bytes
136
+ if (targetPath.includes('\0')) {
137
+ throw new Error('Path contains null bytes');
138
+ }
139
+
140
+ // Expand and normalize both paths
141
+ const resolvedTarget = expandPath(targetPath);
142
+ const resolvedBase = expandPath(allowedBasePath);
143
+
144
+ // Check if target is within allowed base
145
+ if (!isSubPath(resolvedTarget, resolvedBase)) {
146
+ throw new Error(
147
+ 'Path traversal detected. Use absolute or relative paths within allowed directories.'
148
+ );
149
+ }
150
+
151
+ return resolvedTarget;
152
+ }
153
+
154
+ /**
155
+ * Validate a path after resolving symlinks.
156
+ *
157
+ * Symlinks can be used to bypass directory restrictions by pointing to
158
+ * locations outside the allowed base. This function resolves symlinks
159
+ * before validation to ensure the actual file location is checked.
160
+ *
161
+ * @param {string} targetPath - Path to validate (may contain symlinks)
162
+ * @param {string} allowedBasePath - Base directory the target must remain within
163
+ * @returns {Promise<string>} The real, resolved absolute path
164
+ * @throws {Error} If path escapes allowed base directory after symlink resolution
165
+ *
166
+ * @example
167
+ * // If ~/.config is a symlink to /etc/config:
168
+ * await validatePathSafe('~/.config/opencode', os.homedir());
169
+ * // Throws error because resolved path is outside homedir
170
+ */
171
+ export async function validatePathSafe(targetPath, allowedBasePath) {
172
+ const expandedTarget = expandPath(targetPath);
173
+
174
+ try {
175
+ // Resolve symlinks to get the real path
176
+ const realTarget = await fs.promises.realpath(expandedTarget);
177
+
178
+ // Now validate the resolved path
179
+ return validatePath(realTarget, allowedBasePath);
180
+ } catch (error) {
181
+ // If realpath fails (file doesn't exist), validate the non-resolved path
182
+ // This allows validation of paths that will be created
183
+ if (error.code === 'ENOENT') {
184
+ return validatePath(expandedTarget, allowedBasePath);
185
+ }
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Synchronous version of validatePathSafe.
192
+ *
193
+ * @param {string} targetPath - Path to validate (may contain symlinks)
194
+ * @param {string} allowedBasePath - Base directory the target must remain within
195
+ * @returns {string} The real, resolved absolute path
196
+ * @throws {Error} If path escapes allowed base directory after symlink resolution
197
+ */
198
+ export function validatePathSafeSync(targetPath, allowedBasePath) {
199
+ const expandedTarget = expandPath(targetPath);
200
+
201
+ try {
202
+ // Resolve symlinks synchronously
203
+ const realTarget = fs.realpathSync(expandedTarget);
204
+
205
+ // Now validate the resolved path
206
+ return validatePath(realTarget, allowedBasePath);
207
+ } catch (error) {
208
+ // If realpath fails (file doesn't exist), validate the non-resolved path
209
+ if (error.code === 'ENOENT') {
210
+ return validatePath(expandedTarget, allowedBasePath);
211
+ }
212
+ throw error;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Default export combining all path resolver functions.
218
+ */
219
+ export default {
220
+ expandPath,
221
+ normalizePath,
222
+ isSubPath,
223
+ validatePath,
224
+ validatePathSafe,
225
+ validatePathSafeSync
226
+ };
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes