azcodr 1.5.2 → 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/.agents/hooks.json +42 -42
- package/.agents/hooks.json.example +42 -42
- package/.agents/mcp_config.json.example +29 -29
- package/.agents/scripts/safety_guard.sh +143 -34
- package/.agents/scripts/verify_completion.sh +90 -27
- package/.agents/skills/agentic-architect/SKILL.md +125 -125
- package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
- package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
- package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
- package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
- package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +402 -402
- package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
- package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
- package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
- package/.agents/skills/compliance-audit/SKILL.md +120 -120
- package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
- package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
- package/.agents/skills/lets-build/SKILL.md +173 -173
- package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
- package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
- package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
- package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +419 -255
- package/.agents/skills/product-analyst/SKILL.md +154 -154
- package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
- package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
- package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
- package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
- package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
- package/.agents/skills/relentless-questioner/SKILL.md +128 -128
- package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
- package/.editorconfig +19 -19
- package/.github/workflows/ci.yml +167 -78
- package/.github/workflows/publish.yml +200 -0
- package/.gitignore +40 -25
- package/AGENTS.md +103 -102
- package/LICENSE +21 -21
- package/README.md +168 -165
- package/bin/azcodr.js +14 -228
- package/docs/knowledge/ubiquitous_language.md +31 -18
- package/docs/rules/agentic_configuration.md +259 -259
- package/docs/rules/api_architecture.md +179 -179
- package/docs/rules/authentication.md +76 -76
- package/docs/rules/authorization.md +75 -75
- package/docs/rules/caching.md +69 -69
- package/docs/rules/clean_code.md +62 -62
- package/docs/rules/cloud_native.md +41 -41
- package/docs/rules/cqrs.md +203 -203
- package/docs/rules/database_design.md +125 -125
- package/docs/rules/database_operations.md +69 -69
- package/docs/rules/design_patterns.md +98 -98
- package/docs/rules/devops_ci_cd.md +76 -76
- package/docs/rules/domain_driven_design.md +122 -122
- package/docs/rules/error_handling.md +54 -52
- package/docs/rules/feature_flags.md +59 -59
- package/docs/rules/frontend_architecture.md +157 -157
- package/docs/rules/multitenancy_architecture.md +98 -98
- package/docs/rules/product_ownership.md +127 -127
- package/docs/rules/project_management.md +49 -49
- package/docs/rules/relentless_questioning.md +52 -52
- package/docs/rules/requirements_engineering.md +98 -98
- package/docs/rules/security_compliance.md +53 -53
- package/docs/rules/server_driven_ui.md +88 -88
- package/docs/rules/test_driven_development.md +185 -185
- package/docs/rules/transactional_email.md +27 -27
- package/docs/rules/type_safety.md +65 -65
- package/docs/rules/ui_ux_architecture.md +150 -150
- package/docs/rules/workflow_state_machines.md +117 -117
- package/lib/cli-parse.js +51 -0
- package/lib/cli-target.js +109 -0
- package/lib/cli.js +180 -0
- package/lib/errors.js +28 -0
- package/lib/git.js +29 -0
- package/lib/guards.js +96 -0
- package/lib/index.d.ts +199 -134
- package/lib/index.js +5 -5
- package/lib/links.js +123 -0
- package/lib/permissions.js +44 -0
- package/lib/repo.js +90 -0
- package/lib/scaffold.js +238 -448
- package/memory.md +119 -36
- package/package.json +65 -62
- package/scripts/test_coverage.js +66 -38
- package/scripts/validate/adr.js +151 -0
- package/scripts/validate/io.js +84 -0
- package/scripts/validate/links.js +167 -0
- package/scripts/validate/parity.js +124 -0
- package/scripts/validate/root.js +184 -0
- package/scripts/validate/rules.js +44 -0
- package/scripts/validate/skills.js +96 -0
- package/scripts/validate/text.js +29 -0
- package/scripts/validate-cli.js +13 -0
- package/scripts/validate.js +140 -258
- package/.github/copilot-instructions.md +0 -1
package/lib/scaffold.js
CHANGED
|
@@ -1,448 +1,238 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
* Recursively copies template files into the target directory and sets up symlinks and permissions.
|
|
240
|
-
* Socket note: filesystem access is scoped to templateDir -> targetDir only (assertInside).
|
|
241
|
-
*/
|
|
242
|
-
function copyTemplate(targetDir, templateDir = getTemplateDir(), options = {}) {
|
|
243
|
-
const { dryRun = false } = options;
|
|
244
|
-
const resolvedTarget = path.resolve(targetDir);
|
|
245
|
-
const resolvedTemplate = path.resolve(templateDir);
|
|
246
|
-
const actions = [];
|
|
247
|
-
|
|
248
|
-
if (!dryRun && !fs.existsSync(resolvedTarget)) {
|
|
249
|
-
fs.mkdirSync(resolvedTarget, { recursive: true });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
for (const item of TEMPLATE_ITEMS) {
|
|
253
|
-
let srcPath = path.join(resolvedTemplate, item);
|
|
254
|
-
if (item === '.gitignore' && !fs.existsSync(srcPath)) {
|
|
255
|
-
const npmIgnorePath = path.join(resolvedTemplate, '.npmignore');
|
|
256
|
-
if (fs.existsSync(npmIgnorePath)) {
|
|
257
|
-
srcPath = npmIgnorePath;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (!fs.existsSync(srcPath)) continue;
|
|
261
|
-
|
|
262
|
-
const destPath = path.join(resolvedTarget, item);
|
|
263
|
-
assertInside(resolvedTemplate, srcPath);
|
|
264
|
-
assertInside(resolvedTarget, destPath);
|
|
265
|
-
actions.push(`copy: ${item} -> ${destPath}`);
|
|
266
|
-
|
|
267
|
-
if (!dryRun) {
|
|
268
|
-
fs.cpSync(srcPath, destPath, { recursive: true, force: true, dereference: false });
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Ensure harness parity symlinks per AGENTS.md mandate
|
|
273
|
-
actions.push(`symlink: CLAUDE.md -> AGENTS.md`);
|
|
274
|
-
ensureSymlink(resolvedTarget, 'CLAUDE.md', 'AGENTS.md', dryRun);
|
|
275
|
-
|
|
276
|
-
actions.push(`symlink: agents.md -> AGENTS.md`);
|
|
277
|
-
ensureSymlink(resolvedTarget, 'agents.md', 'AGENTS.md', dryRun);
|
|
278
|
-
|
|
279
|
-
actions.push(`symlink: GEMINI.md -> AGENTS.md`);
|
|
280
|
-
ensureSymlink(resolvedTarget, 'GEMINI.md', 'AGENTS.md', dryRun);
|
|
281
|
-
|
|
282
|
-
actions.push(`symlink: .cursorrules -> AGENTS.md`);
|
|
283
|
-
ensureSymlink(resolvedTarget, '.cursorrules', 'AGENTS.md', dryRun);
|
|
284
|
-
|
|
285
|
-
actions.push(`symlink: .windsurfrules -> AGENTS.md`);
|
|
286
|
-
ensureSymlink(resolvedTarget, '.windsurfrules', 'AGENTS.md', dryRun);
|
|
287
|
-
|
|
288
|
-
// GitHub Copilot harness parity (text-pointer fallback preserves relative links)
|
|
289
|
-
const githubDir = path.join(resolvedTarget, '.github');
|
|
290
|
-
if (!dryRun && !fs.existsSync(githubDir)) {
|
|
291
|
-
fs.mkdirSync(githubDir, { recursive: true });
|
|
292
|
-
}
|
|
293
|
-
actions.push(`symlink: .github/copilot-instructions.md -> ../AGENTS.md`);
|
|
294
|
-
ensureSymlinkOrPointer(githubDir, 'copilot-instructions.md', '../AGENTS.md', dryRun);
|
|
295
|
-
|
|
296
|
-
// Starter package.json for project scripts validation
|
|
297
|
-
const pkgJsonPath = path.join(resolvedTarget, 'package.json');
|
|
298
|
-
if (!fs.existsSync(pkgJsonPath)) {
|
|
299
|
-
actions.push('create: package.json');
|
|
300
|
-
if (!dryRun) {
|
|
301
|
-
const projectName = path.basename(resolvedTarget) || 'my-project';
|
|
302
|
-
const starterPkg = {
|
|
303
|
-
name: projectName,
|
|
304
|
-
version: '0.1.0',
|
|
305
|
-
private: true,
|
|
306
|
-
description: 'Scaffolded with azcodr enterprise architecture template',
|
|
307
|
-
scripts: {
|
|
308
|
-
test: 'node --test',
|
|
309
|
-
'test:coverage': 'node scripts/test_coverage.js',
|
|
310
|
-
lint: 'echo "No linter configured yet. Run /lets-build to configure toolchain."',
|
|
311
|
-
validate: 'node scripts/validate.js'
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
fs.writeFileSync(pkgJsonPath, JSON.stringify(starterPkg, null, 2) + '\n', 'utf-8');
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Ensure scripts are executable
|
|
319
|
-
const inspectDir = dryRun ? resolvedTemplate : resolvedTarget;
|
|
320
|
-
const scripts = makeScriptsExecutable(inspectDir, dryRun);
|
|
321
|
-
for (const script of scripts) {
|
|
322
|
-
actions.push(`chmod: +x ${path.relative(inspectDir, script)}`);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return actions;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Detects whether targetDir is already inside an existing Git worktree.
|
|
330
|
-
* Socket note: shell access is limited to `git rev-parse` via runGit (no shell).
|
|
331
|
-
*/
|
|
332
|
-
function isInsideGitWorkTree(targetDir) {
|
|
333
|
-
try {
|
|
334
|
-
const checkDir = fs.existsSync(targetDir) ? targetDir : path.dirname(targetDir);
|
|
335
|
-
const out = runGit(['rev-parse', '--is-inside-work-tree'], {
|
|
336
|
-
cwd: checkDir,
|
|
337
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
338
|
-
});
|
|
339
|
-
return String(out).trim() === 'true';
|
|
340
|
-
} catch {
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Initializes a git repository in the target directory if not already inside one.
|
|
347
|
-
* Socket note: only allowlisted `git init/branch/add/commit` via execFileSync, cwd scoped.
|
|
348
|
-
*/
|
|
349
|
-
function initGit(targetDir, options = {}) {
|
|
350
|
-
const { noGit = false, dryRun = false } = options;
|
|
351
|
-
if (noGit) return false;
|
|
352
|
-
|
|
353
|
-
const gitDir = path.join(targetDir, '.git');
|
|
354
|
-
if (fs.existsSync(gitDir)) return false;
|
|
355
|
-
|
|
356
|
-
if (isInsideGitWorkTree(targetDir)) return false;
|
|
357
|
-
|
|
358
|
-
if (dryRun) {
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
try {
|
|
364
|
-
runGit(['init', '-b', 'main', '-q'], { cwd: targetDir, stdio: 'ignore' });
|
|
365
|
-
} catch {
|
|
366
|
-
runGit(['init', '-q'], { cwd: targetDir, stdio: 'ignore' });
|
|
367
|
-
try {
|
|
368
|
-
runGit(['branch', '-m', 'main'], { cwd: targetDir, stdio: 'ignore' });
|
|
369
|
-
} catch {
|
|
370
|
-
// Non-critical if branch rename fails
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
runGit(['add', '-A'], { cwd: targetDir, stdio: 'ignore' });
|
|
376
|
-
try {
|
|
377
|
-
runGit(['commit', '-q', '-m', 'chore: initial scaffold from azcodr template'], {
|
|
378
|
-
cwd: targetDir,
|
|
379
|
-
stdio: 'ignore'
|
|
380
|
-
});
|
|
381
|
-
} catch {
|
|
382
|
-
runGit(['commit', '-q', '-m', 'chore: initial scaffold from azcodr template'], {
|
|
383
|
-
cwd: targetDir,
|
|
384
|
-
stdio: 'ignore',
|
|
385
|
-
env: {
|
|
386
|
-
...process.env,
|
|
387
|
-
GIT_AUTHOR_NAME: 'Subodh Khanal',
|
|
388
|
-
GIT_AUTHOR_EMAIL: 'prosubodh@gmail.com',
|
|
389
|
-
GIT_COMMITTER_NAME: 'Subodh Khanal',
|
|
390
|
-
GIT_COMMITTER_EMAIL: 'prosubodh@gmail.com'
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
} catch {
|
|
395
|
-
// Non-critical if initial commit fails
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return true;
|
|
399
|
-
} catch {
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* High-level orchestration function to scaffold the azcodr workspace into targetDir.
|
|
406
|
-
*/
|
|
407
|
-
function scaffold(options = {}) {
|
|
408
|
-
const {
|
|
409
|
-
targetDir = process.cwd(),
|
|
410
|
-
force = false,
|
|
411
|
-
noGit = false,
|
|
412
|
-
templateDir = getTemplateDir(),
|
|
413
|
-
dryRun = false,
|
|
414
|
-
silent = false
|
|
415
|
-
} = options;
|
|
416
|
-
|
|
417
|
-
const resolvedTarget = path.resolve(targetDir);
|
|
418
|
-
validateTarget(resolvedTarget, { templateDir, force, dryRun });
|
|
419
|
-
const actions = copyTemplate(resolvedTarget, templateDir, { dryRun });
|
|
420
|
-
const gitInitialized = initGit(resolvedTarget, { noGit, dryRun });
|
|
421
|
-
if (gitInitialized) {
|
|
422
|
-
actions.push('git: initialize repository');
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
success: true,
|
|
427
|
-
targetDir: resolvedTarget,
|
|
428
|
-
gitInitialized,
|
|
429
|
-
dryRun,
|
|
430
|
-
actions
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
module.exports = {
|
|
435
|
-
scaffold,
|
|
436
|
-
validateTarget,
|
|
437
|
-
copyTemplate,
|
|
438
|
-
ensureSymlink,
|
|
439
|
-
ensureSymlinkOrPointer,
|
|
440
|
-
isSameCaseInsensitiveFile,
|
|
441
|
-
makeScriptsExecutable,
|
|
442
|
-
isInsideGitWorkTree,
|
|
443
|
-
initGit,
|
|
444
|
-
getTemplateDir,
|
|
445
|
-
runGit,
|
|
446
|
-
assertInside,
|
|
447
|
-
TEMPLATE_ITEMS
|
|
448
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { ScaffoldError, ERROR_CODES } = require('./errors.js');
|
|
6
|
+
const { runGit } = require('./git.js');
|
|
7
|
+
const { assertInside, isProtectedTarget } = require('./guards.js');
|
|
8
|
+
const { ensureSymlink, ensureSymlinkOrPointer, isSameCaseInsensitiveFile } = require('./links.js');
|
|
9
|
+
const { makeScriptsExecutable } = require('./permissions.js');
|
|
10
|
+
const { isInsideGitWorkTree, initGit } = require('./repo.js');
|
|
11
|
+
|
|
12
|
+
const TEMPLATE_ITEMS = [
|
|
13
|
+
'AGENTS.md',
|
|
14
|
+
'memory.md',
|
|
15
|
+
'README.md',
|
|
16
|
+
'docs',
|
|
17
|
+
'.agents',
|
|
18
|
+
'.github',
|
|
19
|
+
'scripts',
|
|
20
|
+
'.gitignore',
|
|
21
|
+
'.editorconfig',
|
|
22
|
+
'LICENSE'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the root path to the azcodr template files.
|
|
27
|
+
*/
|
|
28
|
+
function getTemplateDir() {
|
|
29
|
+
return path.resolve(__dirname, '..');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rejectTemplateSelf(resolvedTarget, resolvedTemplate) {
|
|
33
|
+
if (resolvedTarget !== resolvedTemplate) return;
|
|
34
|
+
throw new ScaffoldError(
|
|
35
|
+
'E_TARGET_IS_TEMPLATE',
|
|
36
|
+
`${ERROR_CODES.E_TARGET_IS_TEMPLATE}: ${resolvedTarget}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rejectProtectedTarget(resolvedTarget, templateDir, allowProtected) {
|
|
41
|
+
if (allowProtected) return;
|
|
42
|
+
if (!isProtectedTarget(resolvedTarget, { templateDir })) return;
|
|
43
|
+
throw new ScaffoldError(
|
|
44
|
+
'E_TARGET_IS_PROTECTED',
|
|
45
|
+
`${ERROR_CODES.E_TARGET_IS_PROTECTED}: ${resolvedTarget}. ` +
|
|
46
|
+
'Scaffolding there would overwrite personal or system files. ' +
|
|
47
|
+
'Choose a project subdirectory instead.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rejectNonEmptyTarget(resolvedTarget, force) {
|
|
52
|
+
const entries = fs.readdirSync(resolvedTarget);
|
|
53
|
+
if (entries.length === 0 || force) return;
|
|
54
|
+
throw new ScaffoldError(
|
|
55
|
+
'E_TARGET_NOT_EMPTY',
|
|
56
|
+
`${ERROR_CODES.E_TARGET_NOT_EMPTY} (${entries.length} items found: ${resolvedTarget}). ` +
|
|
57
|
+
'Use --force to overwrite existing files.'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validates the target directory to ensure it is suitable for scaffolding.
|
|
63
|
+
*/
|
|
64
|
+
function validateTarget(targetDir, options = {}) {
|
|
65
|
+
const { templateDir = getTemplateDir(), force = false, dryRun = false, allowProtected = false } = options;
|
|
66
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
67
|
+
const resolvedTemplate = path.resolve(templateDir);
|
|
68
|
+
rejectTemplateSelf(resolvedTarget, resolvedTemplate);
|
|
69
|
+
rejectProtectedTarget(resolvedTarget, templateDir, allowProtected);
|
|
70
|
+
if (!fs.existsSync(resolvedTarget)) {
|
|
71
|
+
if (!dryRun) {
|
|
72
|
+
fs.mkdirSync(resolvedTarget, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
rejectNonEmptyTarget(resolvedTarget, force);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveTemplateItem(resolvedTemplate, item) {
|
|
80
|
+
let srcPath = path.join(resolvedTemplate, item);
|
|
81
|
+
if (item === '.gitignore' && !fs.existsSync(srcPath)) {
|
|
82
|
+
const npmIgnorePath = path.join(resolvedTemplate, '.npmignore');
|
|
83
|
+
if (fs.existsSync(npmIgnorePath)) {
|
|
84
|
+
srcPath = npmIgnorePath;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return srcPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function copyTemplateItems(resolvedTarget, resolvedTemplate, dryRun) {
|
|
91
|
+
const actions = [];
|
|
92
|
+
for (const item of TEMPLATE_ITEMS) {
|
|
93
|
+
const srcPath = resolveTemplateItem(resolvedTemplate, item);
|
|
94
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
95
|
+
const destPath = path.join(resolvedTarget, item);
|
|
96
|
+
assertInside(resolvedTemplate, srcPath);
|
|
97
|
+
assertInside(resolvedTarget, destPath);
|
|
98
|
+
actions.push(`copy: ${item} -> ${destPath}`);
|
|
99
|
+
if (!dryRun) {
|
|
100
|
+
fs.cpSync(srcPath, destPath, { recursive: true, force: true, dereference: false });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return actions;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ensureHarnessParity(resolvedTarget, dryRun) {
|
|
107
|
+
const links = [
|
|
108
|
+
['CLAUDE.md', 'AGENTS.md'],
|
|
109
|
+
['agents.md', 'AGENTS.md'],
|
|
110
|
+
['GEMINI.md', 'AGENTS.md'],
|
|
111
|
+
['.cursorrules', 'AGENTS.md'],
|
|
112
|
+
['.windsurfrules', 'AGENTS.md']
|
|
113
|
+
];
|
|
114
|
+
const actions = [];
|
|
115
|
+
for (const [linkName, targetFileName] of links) {
|
|
116
|
+
actions.push(`symlink: ${linkName} -> ${targetFileName}`);
|
|
117
|
+
ensureSymlink({ targetDir: resolvedTarget, linkName, targetFileName, dryRun });
|
|
118
|
+
}
|
|
119
|
+
return actions;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ensureCopilotParity(resolvedTarget, dryRun) {
|
|
123
|
+
const githubDir = path.join(resolvedTarget, '.github');
|
|
124
|
+
if (!dryRun && !fs.existsSync(githubDir)) {
|
|
125
|
+
fs.mkdirSync(githubDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
ensureSymlinkOrPointer({
|
|
128
|
+
targetDir: githubDir,
|
|
129
|
+
linkName: 'copilot-instructions.md',
|
|
130
|
+
targetFileName: '../AGENTS.md',
|
|
131
|
+
dryRun
|
|
132
|
+
});
|
|
133
|
+
return ['symlink: .github/copilot-instructions.md -> ../AGENTS.md'];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function starterPackageJson(resolvedTarget) {
|
|
137
|
+
const projectName = path.basename(resolvedTarget) || 'my-project';
|
|
138
|
+
return {
|
|
139
|
+
name: projectName,
|
|
140
|
+
version: '0.1.0',
|
|
141
|
+
private: true,
|
|
142
|
+
description: 'Scaffolded with azcodr enterprise architecture template',
|
|
143
|
+
scripts: {
|
|
144
|
+
test: 'node --test',
|
|
145
|
+
'test:coverage': 'node scripts/test_coverage.js',
|
|
146
|
+
lint: 'echo "No linter configured yet. Run /lets-build to configure toolchain."',
|
|
147
|
+
validate: 'node scripts/validate-cli.js'
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function ensureStarterPackageJson(resolvedTarget, dryRun) {
|
|
153
|
+
const pkgJsonPath = path.join(resolvedTarget, 'package.json');
|
|
154
|
+
if (fs.existsSync(pkgJsonPath)) return [];
|
|
155
|
+
if (dryRun) return ['create: package.json'];
|
|
156
|
+
const starterPkg = starterPackageJson(resolvedTarget);
|
|
157
|
+
fs.writeFileSync(pkgJsonPath, JSON.stringify(starterPkg, null, 2) + '\n', 'utf-8');
|
|
158
|
+
return ['create: package.json'];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function chmodTemplateScripts(inspectDir, dryRun) {
|
|
162
|
+
const actions = [];
|
|
163
|
+
const scripts = makeScriptsExecutable(inspectDir, dryRun);
|
|
164
|
+
for (const script of scripts) {
|
|
165
|
+
actions.push(`chmod: +x ${path.relative(inspectDir, script)}`);
|
|
166
|
+
}
|
|
167
|
+
return actions;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Recursively copies template files into the target directory and sets up symlinks and permissions.
|
|
172
|
+
* Socket note: filesystem access is scoped to templateDir -> targetDir only (assertInside).
|
|
173
|
+
*/
|
|
174
|
+
function copyTemplate(targetDir, templateDir = getTemplateDir(), options = {}) {
|
|
175
|
+
const { dryRun = false } = options;
|
|
176
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
177
|
+
const resolvedTemplate = path.resolve(templateDir);
|
|
178
|
+
if (!dryRun && !fs.existsSync(resolvedTarget)) {
|
|
179
|
+
fs.mkdirSync(resolvedTarget, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
const actions = copyTemplateItems(resolvedTarget, resolvedTemplate, dryRun);
|
|
182
|
+
actions.push(...ensureHarnessParity(resolvedTarget, dryRun));
|
|
183
|
+
// GitHub Copilot harness parity (text-pointer fallback preserves relative links)
|
|
184
|
+
actions.push(...ensureCopilotParity(resolvedTarget, dryRun));
|
|
185
|
+
actions.push(...ensureStarterPackageJson(resolvedTarget, dryRun));
|
|
186
|
+
// Ensure scripts are executable
|
|
187
|
+
const inspectDir = dryRun ? resolvedTemplate : resolvedTarget;
|
|
188
|
+
actions.push(...chmodTemplateScripts(inspectDir, dryRun));
|
|
189
|
+
return actions;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* High-level orchestration function to scaffold the azcodr workspace into targetDir.
|
|
194
|
+
*/
|
|
195
|
+
function scaffold(options = {}) {
|
|
196
|
+
const {
|
|
197
|
+
targetDir = process.cwd(),
|
|
198
|
+
force = false,
|
|
199
|
+
noGit = false,
|
|
200
|
+
templateDir = getTemplateDir(),
|
|
201
|
+
dryRun = false
|
|
202
|
+
} = options;
|
|
203
|
+
|
|
204
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
205
|
+
validateTarget(resolvedTarget, { templateDir, force, dryRun });
|
|
206
|
+
const actions = copyTemplate(resolvedTarget, templateDir, { dryRun });
|
|
207
|
+
const gitInitialized = initGit(resolvedTarget, { noGit, dryRun });
|
|
208
|
+
if (gitInitialized) {
|
|
209
|
+
actions.push('git: initialize repository');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
success: true,
|
|
214
|
+
targetDir: resolvedTarget,
|
|
215
|
+
gitInitialized,
|
|
216
|
+
dryRun,
|
|
217
|
+
actions
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
scaffold,
|
|
223
|
+
validateTarget,
|
|
224
|
+
copyTemplate,
|
|
225
|
+
ensureSymlink,
|
|
226
|
+
ensureSymlinkOrPointer,
|
|
227
|
+
isSameCaseInsensitiveFile,
|
|
228
|
+
makeScriptsExecutable,
|
|
229
|
+
isInsideGitWorkTree,
|
|
230
|
+
initGit,
|
|
231
|
+
getTemplateDir,
|
|
232
|
+
runGit,
|
|
233
|
+
assertInside,
|
|
234
|
+
TEMPLATE_ITEMS,
|
|
235
|
+
ScaffoldError,
|
|
236
|
+
ERROR_CODES,
|
|
237
|
+
isProtectedTarget
|
|
238
|
+
};
|