create-sdd-project 0.16.10 → 0.17.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/lib/adapt-agents.js +121 -29
- package/lib/diff-generator.js +7 -1
- package/lib/doctor.js +166 -0
- package/lib/generator.js +8 -0
- package/lib/init-generator.js +67 -160
- package/lib/meta.js +344 -0
- package/lib/scanner.js +127 -6
- package/lib/stack-adaptations.js +335 -0
- package/lib/upgrade-generator.js +552 -157
- package/package.json +1 -1
- package/template/gitignore +3 -0
package/lib/scanner.js
CHANGED
|
@@ -7,17 +7,56 @@ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.n
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Scan an existing project directory and return detected configuration.
|
|
10
|
+
*
|
|
11
|
+
* v0.17.1: monorepo-aware. If the root `package.json` does not yield a
|
|
12
|
+
* backend/frontend detection AND the project is a monorepo with
|
|
13
|
+
* `package.json#workspaces`, enumerate workspace `package.json` files in
|
|
14
|
+
* declaration order (pattern outer, lexical inner, deduped by normalized
|
|
15
|
+
* path) and run `detectBackend` / `detectFrontend` per workspace. The
|
|
16
|
+
* FIRST workspace returning `detected: true` wins and its result is merged
|
|
17
|
+
* into `result.backend` / `result.frontend` with a `workspaceSource` field
|
|
18
|
+
* recording the detected workspace's relative path (for diagnostics).
|
|
19
|
+
*
|
|
20
|
+
* Scanner additive invariant (v0.17.1): for single-package projects, or
|
|
21
|
+
* monorepos where root detection already succeeded, the workspace
|
|
22
|
+
* enumeration never fires and the result is byte-identical to v0.17.0.
|
|
10
23
|
*/
|
|
11
24
|
function scan(projectDir) {
|
|
12
25
|
const pkg = readPackageJson(projectDir);
|
|
13
26
|
|
|
14
|
-
const
|
|
27
|
+
const backend = detectBackend(projectDir, pkg);
|
|
28
|
+
const frontend = detectFrontend(projectDir, pkg);
|
|
29
|
+
const isMonorepo = detectMonorepo(projectDir, pkg);
|
|
30
|
+
|
|
31
|
+
// v0.17.1 monorepo fallback
|
|
32
|
+
if (isMonorepo && (!backend.detected || !frontend.detected)) {
|
|
33
|
+
const workspaces = enumerateWorkspaces(projectDir, pkg);
|
|
34
|
+
for (const wsRel of workspaces) {
|
|
35
|
+
const wsAbs = path.join(projectDir, ...wsRel.split('/'));
|
|
36
|
+
const wsPkg = readPackageJson(wsAbs);
|
|
37
|
+
if (!backend.detected) {
|
|
38
|
+
const wsBackend = detectBackend(wsAbs, wsPkg);
|
|
39
|
+
if (wsBackend.detected) {
|
|
40
|
+
Object.assign(backend, wsBackend, { workspaceSource: wsRel });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!frontend.detected) {
|
|
44
|
+
const wsFrontend = detectFrontend(wsAbs, wsPkg);
|
|
45
|
+
if (wsFrontend.detected) {
|
|
46
|
+
Object.assign(frontend, wsFrontend, { workspaceSource: wsRel });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (backend.detected && frontend.detected) break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
15
54
|
projectName: pkg.name || path.basename(projectDir),
|
|
16
55
|
description: pkg.description || '',
|
|
17
56
|
language: detectLanguage(projectDir),
|
|
18
|
-
backend
|
|
19
|
-
frontend
|
|
20
|
-
isMonorepo
|
|
57
|
+
backend,
|
|
58
|
+
frontend,
|
|
59
|
+
isMonorepo,
|
|
21
60
|
rootDirs: listRootDirs(projectDir),
|
|
22
61
|
srcStructure: detectArchitecture(projectDir, pkg),
|
|
23
62
|
tests: detectTests(projectDir, pkg),
|
|
@@ -25,8 +64,90 @@ function scan(projectDir) {
|
|
|
25
64
|
gitBranch: detectGitBranch(projectDir),
|
|
26
65
|
hasGit: fs.existsSync(path.join(projectDir, '.git')),
|
|
27
66
|
};
|
|
67
|
+
}
|
|
28
68
|
|
|
29
|
-
|
|
69
|
+
/**
|
|
70
|
+
* v0.17.1: enumerate workspace paths declared in `pkg.workspaces`.
|
|
71
|
+
*
|
|
72
|
+
* Supports:
|
|
73
|
+
* - Array form: `"workspaces": ["packages/*", "apps/*"]`
|
|
74
|
+
* - Object form: `"workspaces": { "packages": ["packages/*"] }`
|
|
75
|
+
* - Literal paths: `"packages/api"` (no glob)
|
|
76
|
+
* - Single-wildcard patterns: `"packages/*"` (expand immediate subdirs)
|
|
77
|
+
*
|
|
78
|
+
* Does NOT support: `**` recursive patterns, `!exclude` negation, or
|
|
79
|
+
* `pnpm-workspace.yaml` — all deferred to v0.17.2.
|
|
80
|
+
*
|
|
81
|
+
* Returns a deterministic, deduplicated array of POSIX-style relative
|
|
82
|
+
* workspace paths. Ordering: outer = declaration order of patterns; inner
|
|
83
|
+
* = lexical Unicode codepoint sort of expanded subdirs; dedupe = first
|
|
84
|
+
* occurrence wins after flattening (Codex + Gemini round-2 Q7).
|
|
85
|
+
*/
|
|
86
|
+
function enumerateWorkspaces(dir, pkg) {
|
|
87
|
+
let patterns = [];
|
|
88
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
89
|
+
patterns = pkg.workspaces;
|
|
90
|
+
} else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
|
|
91
|
+
patterns = pkg.workspaces.packages;
|
|
92
|
+
}
|
|
93
|
+
if (patterns.length === 0) return [];
|
|
94
|
+
|
|
95
|
+
const flat = [];
|
|
96
|
+
for (const pattern of patterns) {
|
|
97
|
+
flat.push(...expandWorkspacePattern(dir, pattern));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const deduped = [];
|
|
102
|
+
for (const wsPath of flat) {
|
|
103
|
+
const normalized = wsPath.replace(/\\/g, '/').replace(/\/$/, '');
|
|
104
|
+
if (seen.has(normalized)) continue;
|
|
105
|
+
seen.add(normalized);
|
|
106
|
+
deduped.push(normalized);
|
|
107
|
+
}
|
|
108
|
+
return deduped;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function expandWorkspacePattern(dir, pattern) {
|
|
112
|
+
// npm/yarn workspace semantics: a workspace is a DIRECTORY CONTAINING
|
|
113
|
+
// package.json. Directories without package.json are not workspaces,
|
|
114
|
+
// even if they live under a matched glob (Codex round-3 finding 1).
|
|
115
|
+
// This prevents the scanner from wasting work on stray folders
|
|
116
|
+
// (docs/, shared assets, build outputs) and keeps first-match-wins
|
|
117
|
+
// deterministic against only declared workspace packages.
|
|
118
|
+
const hasPkgJson = (absDir) => {
|
|
119
|
+
try {
|
|
120
|
+
return fs.statSync(path.join(absDir, 'package.json')).isFile();
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (!pattern.includes('*')) {
|
|
127
|
+
const absPath = path.join(dir, pattern);
|
|
128
|
+
try {
|
|
129
|
+
if (fs.statSync(absPath).isDirectory() && hasPkgJson(absPath)) {
|
|
130
|
+
return [pattern.replace(/\\/g, '/')];
|
|
131
|
+
}
|
|
132
|
+
} catch { /* not found or not a dir */ }
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
// Only support trailing single-wildcard: `foo/*` or `foo/bar/*`
|
|
136
|
+
const match = pattern.match(/^(.+)\/\*$/);
|
|
137
|
+
if (!match) return [];
|
|
138
|
+
const baseDir = match[1];
|
|
139
|
+
const baseDirAbs = path.join(dir, baseDir);
|
|
140
|
+
let entries;
|
|
141
|
+
try {
|
|
142
|
+
entries = fs.readdirSync(baseDirAbs, { withFileTypes: true });
|
|
143
|
+
} catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
return entries
|
|
147
|
+
.filter((e) => e.isDirectory() && hasPkgJson(path.join(baseDirAbs, e.name)))
|
|
148
|
+
.map((e) => e.name)
|
|
149
|
+
.sort()
|
|
150
|
+
.map((name) => `${baseDir}/${name}`);
|
|
30
151
|
}
|
|
31
152
|
|
|
32
153
|
// --- Helpers ---
|
|
@@ -541,4 +662,4 @@ function detectGitBranch(dir) {
|
|
|
541
662
|
return 'main';
|
|
542
663
|
}
|
|
543
664
|
|
|
544
|
-
module.exports = { scan };
|
|
665
|
+
module.exports = { scan, enumerateWorkspaces, expandWorkspacePattern };
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SDD DevFlow stack-specific adaptations — shared module (v0.17.0+).
|
|
5
|
+
*
|
|
6
|
+
* Extracted from lib/init-generator.js `adaptCopiedFiles` in v0.17.0 so
|
|
7
|
+
* the upgrade path can re-apply the same transformations after a
|
|
8
|
+
* hash-based smart-diff replacement. Previously init-generator.js ran
|
|
9
|
+
* these adaptations on install but upgrade-generator.js did not, so an
|
|
10
|
+
* init'd project upgrading would lose its stack customizations — the
|
|
11
|
+
* cross-path drift discovered during v0.16.10 implementation.
|
|
12
|
+
*
|
|
13
|
+
* Public API:
|
|
14
|
+
*
|
|
15
|
+
* applyStackAdaptations(dest, scan, config, allowlist = null)
|
|
16
|
+
* → walks the filesystem, applies adaptation rules to each file in
|
|
17
|
+
* the candidate set, respects the allowlist (upgrade path uses
|
|
18
|
+
* this to avoid touching preserved user-edited files). Returns the
|
|
19
|
+
* list of POSIX relative paths that were touched.
|
|
20
|
+
*
|
|
21
|
+
* applyStackAdaptationsToContent(content, posixRelativePath, scan, config)
|
|
22
|
+
* → pure, in-memory variant. Returns the adapted content for a
|
|
23
|
+
* single file. Used by upgrade-generator.js's FALLBACK path
|
|
24
|
+
* (when .sdd-meta.json is missing) to construct the "what init
|
|
25
|
+
* would have written" comparison target. This is critical for
|
|
26
|
+
* pre-v0.17.0 --init projects on their first v0.17.0 upgrade
|
|
27
|
+
* (Gemini M1 fix from plan v1.0 review).
|
|
28
|
+
*
|
|
29
|
+
* Idempotency invariant: every rule's source pattern MUST NOT appear in
|
|
30
|
+
* its own replacement value. The current rules satisfy this because they
|
|
31
|
+
* replace literal template strings like "Prisma ORM, and PostgreSQL"
|
|
32
|
+
* with "Mongoose, and MongoDB" — the source no longer appears after one
|
|
33
|
+
* pass. Verified by smoke scenario 56 (run every rule twice, assert
|
|
34
|
+
* second application is a no-op).
|
|
35
|
+
*
|
|
36
|
+
* Ordering: some rules run in phases. Phase 1 ("Zod data schemas" →
|
|
37
|
+
* "validation schemas") MUST run before phase 2 ("validation schemas in
|
|
38
|
+
* `shared/src/schemas/`" → "validation schemas") because phase 2's
|
|
39
|
+
* source depends on phase 1's replacement having happened. The rule
|
|
40
|
+
* arrays preserve this ordering; callers must apply them in sequence
|
|
41
|
+
* per file.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require('node:fs');
|
|
45
|
+
const path = require('node:path');
|
|
46
|
+
|
|
47
|
+
const { toPosix } = require('./meta');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Compute the ordered list of [from, to] replacement rules for a given
|
|
51
|
+
* (file, scan, config). Rules are pure data — no filesystem access.
|
|
52
|
+
*
|
|
53
|
+
* Returns null if this file has no adaptations for the given project
|
|
54
|
+
* state (e.g., a Zod project's backend-developer.md needs no Zod
|
|
55
|
+
* substitutions).
|
|
56
|
+
*
|
|
57
|
+
* The rules here mirror the imperative body of the original
|
|
58
|
+
* lib/init-generator.js adaptCopiedFiles function. Extracting them into
|
|
59
|
+
* a data-driven table allows both file-based and in-memory application.
|
|
60
|
+
*/
|
|
61
|
+
function computeRulesFor(posixRelativePath, scan, config) {
|
|
62
|
+
const backend = scan.backend || {};
|
|
63
|
+
const orm = backend.orm || 'your ORM';
|
|
64
|
+
const db = backend.db || 'your database';
|
|
65
|
+
const validation = backend.validation;
|
|
66
|
+
const structure = scan.srcStructure || {};
|
|
67
|
+
const arch = structure.pattern || 'ddd';
|
|
68
|
+
|
|
69
|
+
// Phase 1: Zod → generic validation (applies only when validation !== 'Zod').
|
|
70
|
+
const zodReplacements = [
|
|
71
|
+
['Zod data schemas', 'validation schemas'],
|
|
72
|
+
['Zod schemas', 'validation schemas'],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Phase 2: shared/src/schemas/ path cleanup. Applied AFTER phase 1, so
|
|
76
|
+
// these match the post-replacement text.
|
|
77
|
+
const schemaPathReplacements = [
|
|
78
|
+
['validation schemas in `shared/src/schemas/` if applicable', 'validation schemas if applicable'],
|
|
79
|
+
['validation schemas in `shared/src/schemas/` (if shared workspace exists)', 'validation schemas (if shared workspace exists)'],
|
|
80
|
+
['validation schemas in `shared/src/schemas/`', 'validation schemas'],
|
|
81
|
+
['validation schemas (`shared/src/schemas/`)', 'validation schemas'],
|
|
82
|
+
['`shared/src/schemas/` (if exists) for current validation schemas', 'project validation schemas'],
|
|
83
|
+
// Gemini spec-creator: no "Zod" prefix, standalone path reference
|
|
84
|
+
['and `shared/src/schemas/` (if exists)', ''],
|
|
85
|
+
['schemas vs `shared/src/schemas/`', 'validation schemas up to date'],
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// ORM/DB replacements for backend agents. Only apply when the detected
|
|
89
|
+
// ORM differs from Prisma (the template default) OR no ORM was
|
|
90
|
+
// detected at all (replace with generic text).
|
|
91
|
+
let ormReplacements = [];
|
|
92
|
+
if (backend.orm && backend.orm !== 'Prisma') {
|
|
93
|
+
ormReplacements = [
|
|
94
|
+
['Prisma ORM, and PostgreSQL', `${orm}${db !== 'your database' ? `, and ${db}` : ''}`],
|
|
95
|
+
['Repository implementations (Prisma)', `Repository implementations (${orm})`],
|
|
96
|
+
];
|
|
97
|
+
} else if (!backend.orm) {
|
|
98
|
+
const dbLabel = db !== 'your database' ? `, and ${db}` : '';
|
|
99
|
+
ormReplacements = [
|
|
100
|
+
['Prisma ORM, and PostgreSQL', dbLabel ? dbLabel.slice(6) : 'your database'],
|
|
101
|
+
['Repository implementations (Prisma)', 'Repository implementations'],
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Architecture (DDD → layered) replacements, applied to backend agents
|
|
106
|
+
// when the detected structure is NOT DDD.
|
|
107
|
+
const archReplacementsBackendPlanner = (arch !== 'ddd') ? [
|
|
108
|
+
['specializing in Domain-Driven Design (DDD) layered architecture with deep knowledge of',
|
|
109
|
+
'specializing in layered architecture with deep knowledge of'],
|
|
110
|
+
['(DDD architecture)', '(layered architecture)'],
|
|
111
|
+
[/\d+\. Read `shared\/src\/schemas\/` \(if exists\) for current .* (?:data )?schemas\n/, ''],
|
|
112
|
+
[/\d+\. Explore existing domain entities, services, validators, repositories\n/,
|
|
113
|
+
'5. Explore the codebase for existing patterns, layer structure, and reusable code\n'],
|
|
114
|
+
[/\d+\. Explore `backend\/src\/infrastructure\/` for existing repositories\n/, ''],
|
|
115
|
+
['following DDD layer order: Domain > Application > Infrastructure > Presentation > Tests',
|
|
116
|
+
'following the layer order defined in backend-standards.mdc'],
|
|
117
|
+
['Implementation Order (Domain > Application > Infrastructure > Presentation > Tests)',
|
|
118
|
+
'Implementation Order (see backend-standards.mdc for layer order)'],
|
|
119
|
+
['Follow DDD layer separation: Domain > Application > Infrastructure > Presentation',
|
|
120
|
+
'Follow the layer separation defined in backend-standards.mdc'],
|
|
121
|
+
] : [];
|
|
122
|
+
|
|
123
|
+
const archReplacementsBackendDeveloper = (arch !== 'ddd') ? [
|
|
124
|
+
['follows DDD layered architecture', 'follows layered architecture'],
|
|
125
|
+
['specializing in Domain-Driven Design (DDD) with', 'specializing in layered architecture with'],
|
|
126
|
+
['(DDD architecture)', '(layered architecture)'],
|
|
127
|
+
[/\d+\. Read `shared\/src\/schemas\/` \(if exists\) for current .* (?:data )?schemas\n/, ''],
|
|
128
|
+
['Follow the DDD layer order from the plan:',
|
|
129
|
+
'Follow the layer order from the plan (see backend-standards.mdc for project layers):'],
|
|
130
|
+
[/\d+\. \*\*Domain Layer\*\*: Entities, value objects, repository interfaces, domain errors\n/,
|
|
131
|
+
'1. **Data Layer**: Models, database operations, data access\n'],
|
|
132
|
+
[/\d+\. \*\*Application Layer\*\*: Services, validators, DTOs\n/,
|
|
133
|
+
'2. **Business Logic Layer**: Controllers, services, external integrations\n'],
|
|
134
|
+
[/\d+\. \*\*Infrastructure Layer\*\*: Repository implementations \([^)]*\), external integrations\n/,
|
|
135
|
+
'3. **Presentation Layer**: Routes, handlers, middleware\n'],
|
|
136
|
+
[/\d+\. \*\*Presentation Layer\*\*: Controllers, routes, middleware\n/,
|
|
137
|
+
'4. **Integration Layer**: Wiring, configuration, server registration\n'],
|
|
138
|
+
['Follow DDD layer order: Domain > Application > Infrastructure > Presentation.',
|
|
139
|
+
'Follow the layer order defined in backend-standards.mdc.'],
|
|
140
|
+
['**ALWAYS** follow DDD layer separation',
|
|
141
|
+
'**ALWAYS** follow the layer separation defined in backend-standards.mdc'],
|
|
142
|
+
['**ALWAYS** handle errors with custom domain error classes',
|
|
143
|
+
'**ALWAYS** handle errors following the patterns in backend-standards.mdc'],
|
|
144
|
+
['ALWAYS handle errors with domain error classes',
|
|
145
|
+
'ALWAYS handle errors following the patterns in backend-standards.mdc'],
|
|
146
|
+
[/- (?:\*\*MANDATORY\*\*: )?If modifying a DB schema → update .* schemas in `shared\/src\/schemas\/` BEFORE continuing\n/, ''],
|
|
147
|
+
] : [];
|
|
148
|
+
|
|
149
|
+
// Dispatch table keyed by the file's POSIX path suffix.
|
|
150
|
+
const isBackendAgent =
|
|
151
|
+
posixRelativePath.endsWith('/agents/backend-developer.md') ||
|
|
152
|
+
posixRelativePath.endsWith('/agents/backend-planner.md');
|
|
153
|
+
const isMultiPurposeAgent =
|
|
154
|
+
posixRelativePath.endsWith('/agents/spec-creator.md') ||
|
|
155
|
+
posixRelativePath.endsWith('/agents/production-code-validator.md') ||
|
|
156
|
+
posixRelativePath.endsWith('/agents/database-architect.md');
|
|
157
|
+
const isWorkflowSkill =
|
|
158
|
+
posixRelativePath.endsWith('/skills/development-workflow/SKILL.md') ||
|
|
159
|
+
posixRelativePath.endsWith('/skills/development-workflow/references/ticket-template.md');
|
|
160
|
+
|
|
161
|
+
// Accumulate rules for this file in the correct order.
|
|
162
|
+
const rules = [];
|
|
163
|
+
|
|
164
|
+
if (isBackendAgent) {
|
|
165
|
+
if (validation !== 'Zod') {
|
|
166
|
+
rules.push(...zodReplacements);
|
|
167
|
+
rules.push(...ormReplacements);
|
|
168
|
+
rules.push(...schemaPathReplacements);
|
|
169
|
+
} else if (ormReplacements.length > 0) {
|
|
170
|
+
rules.push(...ormReplacements);
|
|
171
|
+
}
|
|
172
|
+
// Architecture adaptations run after ORM/Zod.
|
|
173
|
+
if (posixRelativePath.endsWith('/agents/backend-planner.md')) {
|
|
174
|
+
rules.push(...archReplacementsBackendPlanner);
|
|
175
|
+
} else if (posixRelativePath.endsWith('/agents/backend-developer.md')) {
|
|
176
|
+
rules.push(...archReplacementsBackendDeveloper);
|
|
177
|
+
}
|
|
178
|
+
} else if (isMultiPurposeAgent) {
|
|
179
|
+
if (validation !== 'Zod') {
|
|
180
|
+
rules.push(...zodReplacements);
|
|
181
|
+
rules.push(...schemaPathReplacements);
|
|
182
|
+
}
|
|
183
|
+
} else if (isWorkflowSkill) {
|
|
184
|
+
if (validation !== 'Zod') {
|
|
185
|
+
rules.push(...zodReplacements);
|
|
186
|
+
rules.push(...schemaPathReplacements);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return rules.length > 0 ? rules : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Apply an ordered list of [from, to] rules to a content string.
|
|
195
|
+
* Strings are replaced with `.replaceAll` (all occurrences). Regexes are
|
|
196
|
+
* replaced with `.replace` (respects the regex's own flags — `g` for
|
|
197
|
+
* global, absent for first-occurrence; the current rule set uses regexes
|
|
198
|
+
* without `g` because they target unique structural lines).
|
|
199
|
+
*/
|
|
200
|
+
function applyRulesToContent(content, rules) {
|
|
201
|
+
let result = content;
|
|
202
|
+
for (const [from, to] of rules) {
|
|
203
|
+
if (from instanceof RegExp) {
|
|
204
|
+
result = result.replace(from, to);
|
|
205
|
+
} else {
|
|
206
|
+
result = result.replaceAll(from, to);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Pure, in-memory stack adaptation. Returns the adapted content.
|
|
214
|
+
* Zero filesystem I/O. Safe to call repeatedly on the same input
|
|
215
|
+
* (idempotent by rule design).
|
|
216
|
+
*
|
|
217
|
+
* @param {string} content - Raw file content
|
|
218
|
+
* @param {string} posixRelativePath - e.g. ".claude/agents/backend-developer.md"
|
|
219
|
+
* @param {object} scan
|
|
220
|
+
* @param {object} config
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
function applyStackAdaptationsToContent(content, posixRelativePath, scan, config) {
|
|
224
|
+
const rules = computeRulesFor(posixRelativePath, scan, config);
|
|
225
|
+
if (!rules) return content;
|
|
226
|
+
return applyRulesToContent(content, rules);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Candidate file list for stack adaptations. Mirrors the files touched
|
|
231
|
+
* by the original adaptCopiedFiles. Only files that exist on disk are
|
|
232
|
+
* returned.
|
|
233
|
+
*/
|
|
234
|
+
function candidateFilesFor(dest, aiTools, projectType) {
|
|
235
|
+
const toolDirs = [];
|
|
236
|
+
if (aiTools !== 'gemini') toolDirs.push('.claude');
|
|
237
|
+
if (aiTools !== 'claude') toolDirs.push('.gemini');
|
|
238
|
+
|
|
239
|
+
const results = [];
|
|
240
|
+
|
|
241
|
+
for (const dir of toolDirs) {
|
|
242
|
+
// Backend agents
|
|
243
|
+
results.push(`${dir}/agents/backend-developer.md`);
|
|
244
|
+
results.push(`${dir}/agents/backend-planner.md`);
|
|
245
|
+
// Multi-purpose agents
|
|
246
|
+
results.push(`${dir}/agents/spec-creator.md`);
|
|
247
|
+
results.push(`${dir}/agents/production-code-validator.md`);
|
|
248
|
+
results.push(`${dir}/agents/database-architect.md`);
|
|
249
|
+
// Workflow skill files
|
|
250
|
+
results.push(`${dir}/skills/development-workflow/SKILL.md`);
|
|
251
|
+
results.push(`${dir}/skills/development-workflow/references/ticket-template.md`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Filter by on-disk presence AND by project-type (single-stack
|
|
255
|
+
// projects may have pruned backend-* files).
|
|
256
|
+
return results.filter((posixPath) => {
|
|
257
|
+
const absPath = path.join(dest, ...posixPath.split('/'));
|
|
258
|
+
return fs.existsSync(absPath);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Apply stack adaptations to files on disk.
|
|
264
|
+
*
|
|
265
|
+
* @param {string} dest - Project root
|
|
266
|
+
* @param {object} scan - scan() result
|
|
267
|
+
* @param {object} config - { projectType, aiTools, ... }
|
|
268
|
+
* @param {Set<string>|null} allowlist - POSIX paths permitted to be
|
|
269
|
+
* touched. If null, all candidate files are touched (install path).
|
|
270
|
+
* If a Set, only files whose POSIX path is IN the Set are touched
|
|
271
|
+
* (upgrade path — prevents running adaptations on preserved user
|
|
272
|
+
* files).
|
|
273
|
+
* @returns {string[]} POSIX relative paths that were touched (whether
|
|
274
|
+
* their content actually changed or not — callers should re-hash them)
|
|
275
|
+
*/
|
|
276
|
+
function applyStackAdaptations(dest, scan, config, allowlist = null) {
|
|
277
|
+
const touched = [];
|
|
278
|
+
const candidates = candidateFilesFor(dest, config.aiTools, config.projectType);
|
|
279
|
+
|
|
280
|
+
for (const posixPath of candidates) {
|
|
281
|
+
if (allowlist !== null && !allowlist.has(posixPath)) continue;
|
|
282
|
+
const absPath = path.join(dest, ...posixPath.split('/'));
|
|
283
|
+
let content;
|
|
284
|
+
try {
|
|
285
|
+
content = fs.readFileSync(absPath, 'utf8');
|
|
286
|
+
} catch {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const adapted = applyStackAdaptationsToContent(content, posixPath, scan, config);
|
|
290
|
+
if (adapted !== content) {
|
|
291
|
+
try {
|
|
292
|
+
fs.writeFileSync(absPath, adapted, 'utf8');
|
|
293
|
+
} catch (e) {
|
|
294
|
+
console.warn(` ⚠ Failed to write stack-adapted ${posixPath}: ${e.code || e.message}`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
touched.push(posixPath);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Non-agent adaptations: documentation-standards.mdc is project-type-
|
|
302
|
+
// driven, not stack-driven. Keeps its own imperative branch here.
|
|
303
|
+
const docStdRelative = 'ai-specs/specs/documentation-standards.mdc';
|
|
304
|
+
const docStdPath = path.join(dest, docStdRelative);
|
|
305
|
+
if (
|
|
306
|
+
fs.existsSync(docStdPath) &&
|
|
307
|
+
(allowlist === null || allowlist.has(docStdRelative))
|
|
308
|
+
) {
|
|
309
|
+
try {
|
|
310
|
+
let content = fs.readFileSync(docStdPath, 'utf8');
|
|
311
|
+
if (config.projectType === 'backend') {
|
|
312
|
+
content = content.replace(/\| `ai-specs\/specs\/frontend-standards\.mdc` \|[^\n]*\n/, '');
|
|
313
|
+
content = content.replace(/\| `docs\/specs\/ui-components\.md` \|[^\n]*\n/, '');
|
|
314
|
+
content = content.replace(/ - UI component changes → `docs\/specs\/ui-components\.md`\n/, '');
|
|
315
|
+
} else if (config.projectType === 'frontend') {
|
|
316
|
+
content = content.replace(/\| `ai-specs\/specs\/backend-standards\.mdc` \|[^\n]*\n/, '');
|
|
317
|
+
content = content.replace(/\| `docs\/specs\/api-spec\.yaml` \|[^\n]*\n/, '');
|
|
318
|
+
}
|
|
319
|
+
fs.writeFileSync(docStdPath, content, 'utf8');
|
|
320
|
+
touched.push(docStdRelative);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
console.warn(` ⚠ Failed to adapt documentation-standards.mdc: ${e.code || e.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return touched;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = {
|
|
330
|
+
applyStackAdaptations,
|
|
331
|
+
applyStackAdaptationsToContent,
|
|
332
|
+
computeRulesFor,
|
|
333
|
+
applyRulesToContent,
|
|
334
|
+
candidateFilesFor,
|
|
335
|
+
};
|