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.
- package/LICENSE +21 -0
- package/README.ja-JP.md +834 -0
- package/README.ko-KR.md +823 -0
- package/README.md +1006 -0
- package/README.pt-BR.md +452 -0
- package/README.zh-CN.md +800 -0
- package/agents/cap-brainstormer.md +154 -0
- package/agents/cap-debugger.md +221 -0
- package/agents/cap-prototyper.md +170 -0
- package/agents/cap-reviewer.md +230 -0
- package/agents/cap-tester.md +193 -0
- package/bin/install.js +5002 -0
- package/cap/bin/gsd-tools.cjs +1141 -0
- package/cap/bin/lib/arc-scanner.cjs +341 -0
- package/cap/bin/lib/cap-feature-map.cjs +506 -0
- package/cap/bin/lib/cap-session.cjs +191 -0
- package/cap/bin/lib/cap-stack-docs.cjs +598 -0
- package/cap/bin/lib/cap-tag-scanner.cjs +458 -0
- package/cap/bin/lib/commands.cjs +959 -0
- package/cap/bin/lib/config.cjs +466 -0
- package/cap/bin/lib/convention-reader.cjs +180 -0
- package/cap/bin/lib/core.cjs +1230 -0
- package/cap/bin/lib/feature-aggregator.cjs +422 -0
- package/cap/bin/lib/frontmatter.cjs +336 -0
- package/cap/bin/lib/init.cjs +1442 -0
- package/cap/bin/lib/manifest-generator.cjs +381 -0
- package/cap/bin/lib/milestone.cjs +252 -0
- package/cap/bin/lib/model-profiles.cjs +68 -0
- package/cap/bin/lib/monorepo-context.cjs +224 -0
- package/cap/bin/lib/monorepo-migrator.cjs +507 -0
- package/cap/bin/lib/phase.cjs +888 -0
- package/cap/bin/lib/profile-output.cjs +952 -0
- package/cap/bin/lib/profile-pipeline.cjs +539 -0
- package/cap/bin/lib/roadmap.cjs +329 -0
- package/cap/bin/lib/security.cjs +382 -0
- package/cap/bin/lib/session-manager.cjs +290 -0
- package/cap/bin/lib/skeleton-generator.cjs +177 -0
- package/cap/bin/lib/state.cjs +1031 -0
- package/cap/bin/lib/template.cjs +222 -0
- package/cap/bin/lib/test-detector.cjs +61 -0
- package/cap/bin/lib/uat.cjs +282 -0
- package/cap/bin/lib/verify.cjs +888 -0
- package/cap/bin/lib/workspace-detector.cjs +369 -0
- package/cap/bin/lib/workstream.cjs +491 -0
- package/cap/commands/gsd/workstreams.md +63 -0
- package/cap/references/arc-standard.md +315 -0
- package/cap/references/cap-agent-architecture.md +102 -0
- package/cap/references/cap-gitignore-template +9 -0
- package/cap/references/cap-zero-deps.md +158 -0
- package/cap/references/checkpoints.md +778 -0
- package/cap/references/continuation-format.md +249 -0
- package/cap/references/decimal-phase-calculation.md +64 -0
- package/cap/references/feature-map-template.md +25 -0
- package/cap/references/git-integration.md +295 -0
- package/cap/references/git-planning-commit.md +38 -0
- package/cap/references/model-profile-resolution.md +36 -0
- package/cap/references/model-profiles.md +139 -0
- package/cap/references/phase-argument-parsing.md +61 -0
- package/cap/references/planning-config.md +202 -0
- package/cap/references/questioning.md +162 -0
- package/cap/references/session-template.json +8 -0
- package/cap/references/tdd.md +263 -0
- package/cap/references/ui-brand.md +160 -0
- package/cap/references/user-profiling.md +681 -0
- package/cap/references/verification-patterns.md +612 -0
- package/cap/references/workstream-flag.md +58 -0
- package/cap/templates/DEBUG.md +164 -0
- package/cap/templates/UAT.md +265 -0
- package/cap/templates/UI-SPEC.md +100 -0
- package/cap/templates/VALIDATION.md +76 -0
- package/cap/templates/claude-md.md +122 -0
- package/cap/templates/codebase/architecture.md +255 -0
- package/cap/templates/codebase/concerns.md +310 -0
- package/cap/templates/codebase/conventions.md +307 -0
- package/cap/templates/codebase/integrations.md +280 -0
- package/cap/templates/codebase/stack.md +186 -0
- package/cap/templates/codebase/structure.md +285 -0
- package/cap/templates/codebase/testing.md +480 -0
- package/cap/templates/config.json +44 -0
- package/cap/templates/context.md +352 -0
- package/cap/templates/continue-here.md +78 -0
- package/cap/templates/copilot-instructions.md +7 -0
- package/cap/templates/debug-subagent-prompt.md +91 -0
- package/cap/templates/dev-preferences.md +21 -0
- package/cap/templates/discovery.md +146 -0
- package/cap/templates/discussion-log.md +63 -0
- package/cap/templates/milestone-archive.md +123 -0
- package/cap/templates/milestone.md +115 -0
- package/cap/templates/phase-prompt.md +610 -0
- package/cap/templates/planner-subagent-prompt.md +117 -0
- package/cap/templates/project.md +186 -0
- package/cap/templates/requirements.md +231 -0
- package/cap/templates/research-project/ARCHITECTURE.md +204 -0
- package/cap/templates/research-project/FEATURES.md +147 -0
- package/cap/templates/research-project/PITFALLS.md +200 -0
- package/cap/templates/research-project/STACK.md +120 -0
- package/cap/templates/research-project/SUMMARY.md +170 -0
- package/cap/templates/research.md +552 -0
- package/cap/templates/retrospective.md +54 -0
- package/cap/templates/roadmap.md +202 -0
- package/cap/templates/state.md +176 -0
- package/cap/templates/summary-complex.md +59 -0
- package/cap/templates/summary-minimal.md +41 -0
- package/cap/templates/summary-standard.md +48 -0
- package/cap/templates/summary.md +248 -0
- package/cap/templates/user-profile.md +146 -0
- package/cap/templates/user-setup.md +311 -0
- package/cap/templates/verification-report.md +322 -0
- package/cap/workflows/add-phase.md +112 -0
- package/cap/workflows/add-tests.md +351 -0
- package/cap/workflows/add-todo.md +158 -0
- package/cap/workflows/audit-milestone.md +340 -0
- package/cap/workflows/audit-uat.md +109 -0
- package/cap/workflows/autonomous.md +891 -0
- package/cap/workflows/check-todos.md +177 -0
- package/cap/workflows/cleanup.md +152 -0
- package/cap/workflows/complete-milestone.md +767 -0
- package/cap/workflows/diagnose-issues.md +231 -0
- package/cap/workflows/discovery-phase.md +289 -0
- package/cap/workflows/discuss-phase-assumptions.md +653 -0
- package/cap/workflows/discuss-phase.md +1049 -0
- package/cap/workflows/do.md +104 -0
- package/cap/workflows/execute-phase.md +846 -0
- package/cap/workflows/execute-plan.md +514 -0
- package/cap/workflows/fast.md +105 -0
- package/cap/workflows/forensics.md +265 -0
- package/cap/workflows/health.md +181 -0
- package/cap/workflows/help.md +660 -0
- package/cap/workflows/insert-phase.md +130 -0
- package/cap/workflows/list-phase-assumptions.md +178 -0
- package/cap/workflows/list-workspaces.md +56 -0
- package/cap/workflows/manager.md +362 -0
- package/cap/workflows/map-codebase.md +377 -0
- package/cap/workflows/milestone-summary.md +223 -0
- package/cap/workflows/new-milestone.md +486 -0
- package/cap/workflows/new-project.md +1250 -0
- package/cap/workflows/new-workspace.md +237 -0
- package/cap/workflows/next.md +97 -0
- package/cap/workflows/node-repair.md +92 -0
- package/cap/workflows/note.md +156 -0
- package/cap/workflows/pause-work.md +176 -0
- package/cap/workflows/plan-milestone-gaps.md +273 -0
- package/cap/workflows/plan-phase.md +859 -0
- package/cap/workflows/plant-seed.md +169 -0
- package/cap/workflows/pr-branch.md +129 -0
- package/cap/workflows/profile-user.md +450 -0
- package/cap/workflows/progress.md +507 -0
- package/cap/workflows/quick.md +757 -0
- package/cap/workflows/remove-phase.md +155 -0
- package/cap/workflows/remove-workspace.md +90 -0
- package/cap/workflows/research-phase.md +82 -0
- package/cap/workflows/resume-project.md +326 -0
- package/cap/workflows/review.md +228 -0
- package/cap/workflows/session-report.md +146 -0
- package/cap/workflows/settings.md +283 -0
- package/cap/workflows/ship.md +228 -0
- package/cap/workflows/stats.md +60 -0
- package/cap/workflows/transition.md +671 -0
- package/cap/workflows/ui-phase.md +302 -0
- package/cap/workflows/ui-review.md +165 -0
- package/cap/workflows/update.md +323 -0
- package/cap/workflows/validate-phase.md +174 -0
- package/cap/workflows/verify-phase.md +254 -0
- package/cap/workflows/verify-work.md +637 -0
- package/commands/cap/annotate.md +165 -0
- package/commands/cap/brainstorm.md +238 -0
- package/commands/cap/debug.md +297 -0
- package/commands/cap/init.md +262 -0
- package/commands/cap/iterate.md +234 -0
- package/commands/cap/prototype.md +281 -0
- package/commands/cap/refresh-docs.md +37 -0
- package/commands/cap/review.md +272 -0
- package/commands/cap/scan.md +249 -0
- package/commands/cap/start.md +234 -0
- package/commands/cap/status.md +189 -0
- package/commands/cap/test.md +250 -0
- package/hooks/dist/gsd-check-update.js +114 -0
- package/hooks/dist/gsd-context-monitor.js +156 -0
- package/hooks/dist/gsd-prompt-guard.js +96 -0
- package/hooks/dist/gsd-statusline.js +119 -0
- package/hooks/dist/gsd-workflow-guard.js +94 -0
- package/package.json +51 -0
- package/scripts/base64-scan.sh +262 -0
- package/scripts/build-hooks.js +82 -0
- package/scripts/cap-removal-checklist.md +202 -0
- package/scripts/prompt-injection-scan.sh +198 -0
- package/scripts/run-tests.cjs +29 -0
- 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
|
+
};
|