code-as-plan 2.0.0

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 (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja-JP.md +834 -0
  3. package/README.ko-KR.md +823 -0
  4. package/README.md +1006 -0
  5. package/README.pt-BR.md +452 -0
  6. package/README.zh-CN.md +800 -0
  7. package/agents/cap-brainstormer.md +154 -0
  8. package/agents/cap-debugger.md +221 -0
  9. package/agents/cap-prototyper.md +170 -0
  10. package/agents/cap-reviewer.md +230 -0
  11. package/agents/cap-tester.md +193 -0
  12. package/bin/install.js +5002 -0
  13. package/cap/bin/gsd-tools.cjs +1141 -0
  14. package/cap/bin/lib/arc-scanner.cjs +341 -0
  15. package/cap/bin/lib/cap-feature-map.cjs +506 -0
  16. package/cap/bin/lib/cap-session.cjs +191 -0
  17. package/cap/bin/lib/cap-stack-docs.cjs +598 -0
  18. package/cap/bin/lib/cap-tag-scanner.cjs +458 -0
  19. package/cap/bin/lib/commands.cjs +959 -0
  20. package/cap/bin/lib/config.cjs +466 -0
  21. package/cap/bin/lib/convention-reader.cjs +180 -0
  22. package/cap/bin/lib/core.cjs +1230 -0
  23. package/cap/bin/lib/feature-aggregator.cjs +422 -0
  24. package/cap/bin/lib/frontmatter.cjs +336 -0
  25. package/cap/bin/lib/init.cjs +1442 -0
  26. package/cap/bin/lib/manifest-generator.cjs +381 -0
  27. package/cap/bin/lib/milestone.cjs +252 -0
  28. package/cap/bin/lib/model-profiles.cjs +68 -0
  29. package/cap/bin/lib/monorepo-context.cjs +224 -0
  30. package/cap/bin/lib/monorepo-migrator.cjs +507 -0
  31. package/cap/bin/lib/phase.cjs +888 -0
  32. package/cap/bin/lib/profile-output.cjs +952 -0
  33. package/cap/bin/lib/profile-pipeline.cjs +539 -0
  34. package/cap/bin/lib/roadmap.cjs +329 -0
  35. package/cap/bin/lib/security.cjs +382 -0
  36. package/cap/bin/lib/session-manager.cjs +290 -0
  37. package/cap/bin/lib/skeleton-generator.cjs +177 -0
  38. package/cap/bin/lib/state.cjs +1031 -0
  39. package/cap/bin/lib/template.cjs +222 -0
  40. package/cap/bin/lib/test-detector.cjs +61 -0
  41. package/cap/bin/lib/uat.cjs +282 -0
  42. package/cap/bin/lib/verify.cjs +888 -0
  43. package/cap/bin/lib/workspace-detector.cjs +369 -0
  44. package/cap/bin/lib/workstream.cjs +491 -0
  45. package/cap/commands/gsd/workstreams.md +63 -0
  46. package/cap/references/arc-standard.md +315 -0
  47. package/cap/references/cap-agent-architecture.md +102 -0
  48. package/cap/references/cap-gitignore-template +9 -0
  49. package/cap/references/cap-zero-deps.md +158 -0
  50. package/cap/references/checkpoints.md +778 -0
  51. package/cap/references/continuation-format.md +249 -0
  52. package/cap/references/decimal-phase-calculation.md +64 -0
  53. package/cap/references/feature-map-template.md +25 -0
  54. package/cap/references/git-integration.md +295 -0
  55. package/cap/references/git-planning-commit.md +38 -0
  56. package/cap/references/model-profile-resolution.md +36 -0
  57. package/cap/references/model-profiles.md +139 -0
  58. package/cap/references/phase-argument-parsing.md +61 -0
  59. package/cap/references/planning-config.md +202 -0
  60. package/cap/references/questioning.md +162 -0
  61. package/cap/references/session-template.json +8 -0
  62. package/cap/references/tdd.md +263 -0
  63. package/cap/references/ui-brand.md +160 -0
  64. package/cap/references/user-profiling.md +681 -0
  65. package/cap/references/verification-patterns.md +612 -0
  66. package/cap/references/workstream-flag.md +58 -0
  67. package/cap/templates/DEBUG.md +164 -0
  68. package/cap/templates/UAT.md +265 -0
  69. package/cap/templates/UI-SPEC.md +100 -0
  70. package/cap/templates/VALIDATION.md +76 -0
  71. package/cap/templates/claude-md.md +122 -0
  72. package/cap/templates/codebase/architecture.md +255 -0
  73. package/cap/templates/codebase/concerns.md +310 -0
  74. package/cap/templates/codebase/conventions.md +307 -0
  75. package/cap/templates/codebase/integrations.md +280 -0
  76. package/cap/templates/codebase/stack.md +186 -0
  77. package/cap/templates/codebase/structure.md +285 -0
  78. package/cap/templates/codebase/testing.md +480 -0
  79. package/cap/templates/config.json +44 -0
  80. package/cap/templates/context.md +352 -0
  81. package/cap/templates/continue-here.md +78 -0
  82. package/cap/templates/copilot-instructions.md +7 -0
  83. package/cap/templates/debug-subagent-prompt.md +91 -0
  84. package/cap/templates/dev-preferences.md +21 -0
  85. package/cap/templates/discovery.md +146 -0
  86. package/cap/templates/discussion-log.md +63 -0
  87. package/cap/templates/milestone-archive.md +123 -0
  88. package/cap/templates/milestone.md +115 -0
  89. package/cap/templates/phase-prompt.md +610 -0
  90. package/cap/templates/planner-subagent-prompt.md +117 -0
  91. package/cap/templates/project.md +186 -0
  92. package/cap/templates/requirements.md +231 -0
  93. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  94. package/cap/templates/research-project/FEATURES.md +147 -0
  95. package/cap/templates/research-project/PITFALLS.md +200 -0
  96. package/cap/templates/research-project/STACK.md +120 -0
  97. package/cap/templates/research-project/SUMMARY.md +170 -0
  98. package/cap/templates/research.md +552 -0
  99. package/cap/templates/retrospective.md +54 -0
  100. package/cap/templates/roadmap.md +202 -0
  101. package/cap/templates/state.md +176 -0
  102. package/cap/templates/summary-complex.md +59 -0
  103. package/cap/templates/summary-minimal.md +41 -0
  104. package/cap/templates/summary-standard.md +48 -0
  105. package/cap/templates/summary.md +248 -0
  106. package/cap/templates/user-profile.md +146 -0
  107. package/cap/templates/user-setup.md +311 -0
  108. package/cap/templates/verification-report.md +322 -0
  109. package/cap/workflows/add-phase.md +112 -0
  110. package/cap/workflows/add-tests.md +351 -0
  111. package/cap/workflows/add-todo.md +158 -0
  112. package/cap/workflows/audit-milestone.md +340 -0
  113. package/cap/workflows/audit-uat.md +109 -0
  114. package/cap/workflows/autonomous.md +891 -0
  115. package/cap/workflows/check-todos.md +177 -0
  116. package/cap/workflows/cleanup.md +152 -0
  117. package/cap/workflows/complete-milestone.md +767 -0
  118. package/cap/workflows/diagnose-issues.md +231 -0
  119. package/cap/workflows/discovery-phase.md +289 -0
  120. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  121. package/cap/workflows/discuss-phase.md +1049 -0
  122. package/cap/workflows/do.md +104 -0
  123. package/cap/workflows/execute-phase.md +846 -0
  124. package/cap/workflows/execute-plan.md +514 -0
  125. package/cap/workflows/fast.md +105 -0
  126. package/cap/workflows/forensics.md +265 -0
  127. package/cap/workflows/health.md +181 -0
  128. package/cap/workflows/help.md +660 -0
  129. package/cap/workflows/insert-phase.md +130 -0
  130. package/cap/workflows/list-phase-assumptions.md +178 -0
  131. package/cap/workflows/list-workspaces.md +56 -0
  132. package/cap/workflows/manager.md +362 -0
  133. package/cap/workflows/map-codebase.md +377 -0
  134. package/cap/workflows/milestone-summary.md +223 -0
  135. package/cap/workflows/new-milestone.md +486 -0
  136. package/cap/workflows/new-project.md +1250 -0
  137. package/cap/workflows/new-workspace.md +237 -0
  138. package/cap/workflows/next.md +97 -0
  139. package/cap/workflows/node-repair.md +92 -0
  140. package/cap/workflows/note.md +156 -0
  141. package/cap/workflows/pause-work.md +176 -0
  142. package/cap/workflows/plan-milestone-gaps.md +273 -0
  143. package/cap/workflows/plan-phase.md +859 -0
  144. package/cap/workflows/plant-seed.md +169 -0
  145. package/cap/workflows/pr-branch.md +129 -0
  146. package/cap/workflows/profile-user.md +450 -0
  147. package/cap/workflows/progress.md +507 -0
  148. package/cap/workflows/quick.md +757 -0
  149. package/cap/workflows/remove-phase.md +155 -0
  150. package/cap/workflows/remove-workspace.md +90 -0
  151. package/cap/workflows/research-phase.md +82 -0
  152. package/cap/workflows/resume-project.md +326 -0
  153. package/cap/workflows/review.md +228 -0
  154. package/cap/workflows/session-report.md +146 -0
  155. package/cap/workflows/settings.md +283 -0
  156. package/cap/workflows/ship.md +228 -0
  157. package/cap/workflows/stats.md +60 -0
  158. package/cap/workflows/transition.md +671 -0
  159. package/cap/workflows/ui-phase.md +302 -0
  160. package/cap/workflows/ui-review.md +165 -0
  161. package/cap/workflows/update.md +323 -0
  162. package/cap/workflows/validate-phase.md +174 -0
  163. package/cap/workflows/verify-phase.md +254 -0
  164. package/cap/workflows/verify-work.md +637 -0
  165. package/commands/cap/annotate.md +165 -0
  166. package/commands/cap/brainstorm.md +238 -0
  167. package/commands/cap/debug.md +297 -0
  168. package/commands/cap/init.md +262 -0
  169. package/commands/cap/iterate.md +234 -0
  170. package/commands/cap/prototype.md +281 -0
  171. package/commands/cap/refresh-docs.md +37 -0
  172. package/commands/cap/review.md +272 -0
  173. package/commands/cap/scan.md +249 -0
  174. package/commands/cap/start.md +234 -0
  175. package/commands/cap/status.md +189 -0
  176. package/commands/cap/test.md +250 -0
  177. package/hooks/dist/gsd-check-update.js +114 -0
  178. package/hooks/dist/gsd-context-monitor.js +156 -0
  179. package/hooks/dist/gsd-prompt-guard.js +96 -0
  180. package/hooks/dist/gsd-statusline.js +119 -0
  181. package/hooks/dist/gsd-workflow-guard.js +94 -0
  182. package/package.json +51 -0
  183. package/scripts/base64-scan.sh +262 -0
  184. package/scripts/build-hooks.js +82 -0
  185. package/scripts/cap-removal-checklist.md +202 -0
  186. package/scripts/prompt-injection-scan.sh +198 -0
  187. package/scripts/run-tests.cjs +29 -0
  188. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,369 @@
1
+ // @gsd-context Workspace detector for monorepo mode -- discovers NX, Turbo, and pnpm workspaces and enumerates apps/packages
2
+ // @gsd-decision Regex-parses pnpm-workspace.yaml instead of adding a YAML parser -- keeps zero-dep constraint
3
+ // @gsd-constraint Zero external dependencies -- uses only Node.js built-ins (fs, path)
4
+ // @gsd-ref(ref:AC-1) GSD auto-detects NX/Turbo/pnpm workspaces and lists available apps and packages on project initialization
5
+ // @gsd-pattern Workspace detection returns a structured WorkspaceInfo object that downstream modules consume uniformly
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+
12
+ // @gsd-api detectWorkspace(projectRoot) -- returns WorkspaceInfo | null describing the monorepo type, apps, and packages
13
+
14
+ /**
15
+ * @typedef {Object} WorkspaceApp
16
+ * @property {string} name - Package name from package.json or directory name
17
+ * @property {string} path - Relative path from project root (e.g., 'apps/dashboard')
18
+ * @property {string} absolutePath - Absolute path on disk
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} WorkspacePackage
23
+ * @property {string} name - Package name from package.json or directory name
24
+ * @property {string} path - Relative path from project root (e.g., 'packages/ui')
25
+ * @property {string} absolutePath - Absolute path on disk
26
+ * @property {string[]} exports - Exported entry points (from package.json exports field)
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} WorkspaceInfo
31
+ * @property {'nx'|'turbo'|'pnpm'|'npm'|null} type - Detected workspace manager
32
+ * @property {string} rootPath - Absolute path to monorepo root
33
+ * @property {WorkspaceApp[]} apps - Detected applications
34
+ * @property {WorkspacePackage[]} packages - Detected shared packages
35
+ * @property {string[]} workspaceGlobs - Raw glob patterns from workspace config
36
+ */
37
+
38
+ /**
39
+ * Detect the workspace type by checking for config files.
40
+ *
41
+ * @param {string} projectRoot - Absolute path to project root
42
+ * @returns {WorkspaceInfo|null} Workspace info or null if not a monorepo
43
+ */
44
+ function detectWorkspace(projectRoot) {
45
+ // @gsd-decision Check nx.json first, then turbo.json, then pnpm-workspace.yaml, then package.json workspaces -- priority matches market share
46
+ const nxPath = path.join(projectRoot, 'nx.json');
47
+ const turboPath = path.join(projectRoot, 'turbo.json');
48
+ const pnpmWsPath = path.join(projectRoot, 'pnpm-workspace.yaml');
49
+ const pkgPath = path.join(projectRoot, 'package.json');
50
+
51
+ let type = null;
52
+ let workspaceGlobs = [];
53
+
54
+ if (fs.existsSync(nxPath)) {
55
+ type = 'nx';
56
+ workspaceGlobs = resolveNxWorkspaces(nxPath, pkgPath);
57
+ } else if (fs.existsSync(turboPath)) {
58
+ type = 'turbo';
59
+ workspaceGlobs = resolveTurboWorkspaces(pkgPath);
60
+ } else if (fs.existsSync(pnpmWsPath)) {
61
+ type = 'pnpm';
62
+ workspaceGlobs = resolvePnpmWorkspaces(pnpmWsPath);
63
+ } else if (fs.existsSync(pkgPath)) {
64
+ const pkg = safeReadJson(pkgPath);
65
+ if (pkg && Array.isArray(pkg.workspaces)) {
66
+ type = 'npm';
67
+ workspaceGlobs = pkg.workspaces;
68
+ } else if (pkg && pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
69
+ type = 'npm';
70
+ workspaceGlobs = pkg.workspaces.packages;
71
+ }
72
+ }
73
+
74
+ if (!type) return null;
75
+
76
+ // @gsd-risk Glob expansion uses simple fs.readdirSync matching, not full glob semantics -- patterns like apps/** work but complex negations do not
77
+ const resolved = expandWorkspaceGlobs(projectRoot, workspaceGlobs);
78
+
79
+ // @gsd-decision Classify directories under apps/ or packages/ by convention -- NX/Turbo monorepos use this standard structure
80
+ const apps = [];
81
+ const packages = [];
82
+
83
+ for (const entry of resolved) {
84
+ const relPath = entry.relativePath;
85
+ const pkgJson = safeReadJson(path.join(entry.absolutePath, 'package.json'));
86
+ const name = (pkgJson && pkgJson.name) || path.basename(relPath);
87
+ const exports = (pkgJson && pkgJson.exports) ? Object.keys(pkgJson.exports) : [];
88
+
89
+ const item = {
90
+ name,
91
+ path: relPath,
92
+ absolutePath: entry.absolutePath,
93
+ };
94
+
95
+ if (relPath.startsWith('apps/') || relPath.startsWith('apps\\')) {
96
+ apps.push(item);
97
+ } else if (relPath.startsWith('packages/') || relPath.startsWith('libs/') || relPath.startsWith('packages\\') || relPath.startsWith('libs\\')) {
98
+ packages.push({ ...item, exports });
99
+ } else {
100
+ // @gsd-risk Directories not under apps/ or packages/ are classified as packages by default -- may misclassify standalone tools
101
+ packages.push({ ...item, exports });
102
+ }
103
+ }
104
+
105
+ return {
106
+ type,
107
+ rootPath: projectRoot,
108
+ apps,
109
+ packages,
110
+ workspaceGlobs,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Resolve NX workspace globs. NX uses package.json workspaces or project.json files.
116
+ *
117
+ * @param {string} nxPath - Path to nx.json
118
+ * @param {string} pkgPath - Path to root package.json
119
+ * @returns {string[]}
120
+ */
121
+ function resolveNxWorkspaces(nxPath, pkgPath) {
122
+ const pkg = safeReadJson(pkgPath);
123
+ if (pkg && Array.isArray(pkg.workspaces)) return pkg.workspaces;
124
+ if (pkg && pkg.workspaces && Array.isArray(pkg.workspaces.packages)) return pkg.workspaces.packages;
125
+
126
+ // NX project.json-based discovery: scan first-level subdirectories for project.json files
127
+ const projectRoot = path.dirname(nxPath);
128
+ const discoveredGlobs = new Set();
129
+
130
+ try {
131
+ const topEntries = fs.readdirSync(projectRoot, { withFileTypes: true });
132
+ for (const topEntry of topEntries) {
133
+ if (!topEntry.isDirectory()) continue;
134
+ if (topEntry.name === 'node_modules' || topEntry.name === '.git') continue;
135
+
136
+ const topDir = path.join(projectRoot, topEntry.name);
137
+ try {
138
+ const subEntries = fs.readdirSync(topDir, { withFileTypes: true });
139
+ for (const subEntry of subEntries) {
140
+ if (!subEntry.isDirectory()) continue;
141
+ const projectJsonPath = path.join(topDir, subEntry.name, 'project.json');
142
+ if (fs.existsSync(projectJsonPath)) {
143
+ // Add the parent-level glob pattern (e.g., 'apps/*')
144
+ discoveredGlobs.add(`${topEntry.name}/*`);
145
+ }
146
+ }
147
+ } catch {
148
+ // Permission errors on subdirectory
149
+ }
150
+ }
151
+ } catch {
152
+ // Permission errors on project root
153
+ }
154
+
155
+ if (discoveredGlobs.size > 0) {
156
+ return Array.from(discoveredGlobs);
157
+ }
158
+
159
+ // Fallback: NX convention
160
+ return ['apps/*', 'packages/*', 'libs/*'];
161
+ }
162
+
163
+ /**
164
+ * Resolve Turbo workspace globs. Turbo reads from package.json workspaces.
165
+ *
166
+ * @param {string} pkgPath - Path to root package.json
167
+ * @returns {string[]}
168
+ */
169
+ function resolveTurboWorkspaces(pkgPath) {
170
+ const pkg = safeReadJson(pkgPath);
171
+ if (pkg && Array.isArray(pkg.workspaces)) return pkg.workspaces;
172
+ if (pkg && pkg.workspaces && Array.isArray(pkg.workspaces.packages)) return pkg.workspaces.packages;
173
+ return ['apps/*', 'packages/*'];
174
+ }
175
+
176
+ /**
177
+ * Parse pnpm-workspace.yaml to extract workspace globs.
178
+ * Uses simple regex instead of a YAML parser to maintain zero-dep constraint.
179
+ *
180
+ * @param {string} pnpmWsPath - Path to pnpm-workspace.yaml
181
+ * @returns {string[]}
182
+ */
183
+ function resolvePnpmWorkspaces(pnpmWsPath) {
184
+ // @gsd-decision Parse pnpm-workspace.yaml with regex -- avoids adding js-yaml dependency; works for the simple list format pnpm uses
185
+ // @gsd-risk Regex YAML parsing will break on complex YAML features (anchors, flow sequences) -- sufficient for pnpm-workspace.yaml which is always a simple list
186
+ try {
187
+ const content = fs.readFileSync(pnpmWsPath, 'utf-8');
188
+ const globs = [];
189
+ const lines = content.split('\n');
190
+ let inPackages = false;
191
+ for (const line of lines) {
192
+ const trimmed = line.trim();
193
+ if (trimmed === 'packages:') {
194
+ inPackages = true;
195
+ continue;
196
+ }
197
+ if (inPackages) {
198
+ if (trimmed.startsWith('- ')) {
199
+ const glob = trimmed.slice(2).replace(/['"]/g, '').trim();
200
+ if (glob) globs.push(glob);
201
+ } else if (trimmed && !trimmed.startsWith('#')) {
202
+ // Non-list line ends the packages section
203
+ break;
204
+ }
205
+ }
206
+ }
207
+ return globs.length > 0 ? globs : ['packages/*', 'apps/*'];
208
+ } catch {
209
+ return ['packages/*', 'apps/*'];
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Expand workspace globs to actual directories on disk.
215
+ *
216
+ * @param {string} rootPath - Absolute path to monorepo root
217
+ * @param {string[]} globs - Workspace glob patterns (e.g., ['apps/*', 'packages/*'])
218
+ * @returns {Array<{relativePath: string, absolutePath: string}>}
219
+ */
220
+ function expandWorkspaceGlobs(rootPath, globs) {
221
+ // @gsd-constraint Uses readdirSync (not glob library) per project zero-dep constraint
222
+ const results = [];
223
+ const seen = new Set();
224
+
225
+ for (const glob of globs) {
226
+ // Skip negation patterns (e.g., '!packages/internal')
227
+ if (glob.startsWith('!')) continue;
228
+
229
+ // Detect two-level glob patterns like 'packages/*/sub/*'
230
+ const segments = glob.split('/');
231
+ const starPositions = segments.reduce((acc, seg, i) => {
232
+ if (seg === '*' || seg === '**') acc.push(i);
233
+ return acc;
234
+ }, []);
235
+
236
+ if (starPositions.length >= 2) {
237
+ // Two-level pattern: walk two directory levels
238
+ const firstParent = path.join(rootPath, segments.slice(0, starPositions[0]).join('/'));
239
+ if (!fs.existsSync(firstParent)) continue;
240
+
241
+ try {
242
+ const level1Entries = fs.readdirSync(firstParent, { withFileTypes: true });
243
+ for (const l1 of level1Entries) {
244
+ if (!l1.isDirectory() || l1.name === 'node_modules' || l1.name === '.git') continue;
245
+ // Build the path to the second-level parent (may have fixed segments between stars)
246
+ const midSegments = segments.slice(starPositions[0] + 1, starPositions[1]);
247
+ const level2Parent = path.join(firstParent, l1.name, ...midSegments);
248
+ if (!fs.existsSync(level2Parent)) continue;
249
+
250
+ try {
251
+ const level2Entries = fs.readdirSync(level2Parent, { withFileTypes: true });
252
+ for (const l2 of level2Entries) {
253
+ if (!l2.isDirectory() || l2.name === 'node_modules' || l2.name === '.git') continue;
254
+ const absPath = path.join(level2Parent, l2.name);
255
+ const relPath = path.relative(rootPath, absPath);
256
+ if (!seen.has(relPath)) {
257
+ seen.add(relPath);
258
+ results.push({ relativePath: relPath, absolutePath: absPath });
259
+ }
260
+ }
261
+ } catch {
262
+ // Permission errors
263
+ }
264
+ }
265
+ } catch {
266
+ // Permission errors
267
+ }
268
+ } else {
269
+ // Single-level pattern: 'apps/*', 'packages/*', 'libs/*'
270
+ const cleanGlob = glob.replace(/\/\*\*?$/, '').replace(/\\\*\*?$/, '');
271
+ const parentDir = path.join(rootPath, cleanGlob);
272
+
273
+ if (!fs.existsSync(parentDir)) continue;
274
+
275
+ try {
276
+ const entries = fs.readdirSync(parentDir, { withFileTypes: true });
277
+ for (const entry of entries) {
278
+ if (!entry.isDirectory()) continue;
279
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
280
+
281
+ const absPath = path.join(parentDir, entry.name);
282
+ const relPath = path.relative(rootPath, absPath);
283
+
284
+ if (!seen.has(relPath)) {
285
+ seen.add(relPath);
286
+ results.push({ relativePath: relPath, absolutePath: absPath });
287
+ }
288
+ }
289
+ } catch {
290
+ // Permission errors, etc.
291
+ }
292
+ }
293
+ }
294
+
295
+ return results;
296
+ }
297
+
298
+ /**
299
+ * Safely read and parse a JSON file.
300
+ *
301
+ * @param {string} filePath
302
+ * @returns {Object|null}
303
+ */
304
+ function safeReadJson(filePath) {
305
+ try {
306
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
307
+ } catch {
308
+ return null;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Validate that an --app path exists within the workspace.
314
+ *
315
+ * @param {WorkspaceInfo} workspace - Detected workspace info
316
+ * @param {string} appPath - User-provided app path (e.g., 'apps/dashboard')
317
+ * @returns {{valid: boolean, resolved: WorkspaceApp|null, error: string|null}}
318
+ */
319
+ // @gsd-api validateAppPath(workspace, appPath) -- returns {valid, resolved, error} for --app flag validation
320
+ function validateAppPath(workspace, appPath) {
321
+ if (!workspace) {
322
+ return { valid: false, resolved: null, error: 'No workspace detected. Run monorepo-init first.' };
323
+ }
324
+
325
+ const normalized = appPath.replace(/\\/g, '/').replace(/\/$/, '');
326
+ const allEntries = [...workspace.apps, ...workspace.packages];
327
+ const match = allEntries.find(e => e.path.replace(/\\/g, '/') === normalized);
328
+
329
+ if (match) {
330
+ return { valid: true, resolved: match, error: null };
331
+ }
332
+
333
+ return {
334
+ valid: false,
335
+ resolved: null,
336
+ error: `App '${appPath}' not found in workspace. Available: ${allEntries.map(e => e.path).join(', ')}`,
337
+ };
338
+ }
339
+
340
+ /**
341
+ * CLI entry point for detect-workspace subcommand.
342
+ *
343
+ * @param {string} cwd - Current working directory
344
+ * @param {boolean} raw - Whether to output raw JSON
345
+ */
346
+ function cmdDetectWorkspace(cwd, raw) {
347
+ const workspace = detectWorkspace(cwd);
348
+ if (!workspace) {
349
+ if (raw) {
350
+ process.stdout.write('null\n');
351
+ } else {
352
+ process.stderr.write('No workspace detected. Not a monorepo or missing workspace config.\n');
353
+ }
354
+ return;
355
+ }
356
+
357
+ const output = JSON.stringify(workspace, null, 2);
358
+ process.stdout.write(output + '\n');
359
+ }
360
+
361
+ module.exports = {
362
+ detectWorkspace,
363
+ validateAppPath,
364
+ expandWorkspaceGlobs,
365
+ resolvePnpmWorkspaces,
366
+ resolveNxWorkspaces,
367
+ resolveTurboWorkspaces,
368
+ cmdDetectWorkspace,
369
+ };