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.
Files changed (93) hide show
  1. package/.agents/hooks.json +42 -42
  2. package/.agents/hooks.json.example +42 -42
  3. package/.agents/mcp_config.json.example +29 -29
  4. package/.agents/scripts/safety_guard.sh +143 -34
  5. package/.agents/scripts/verify_completion.sh +90 -27
  6. package/.agents/skills/agentic-architect/SKILL.md +125 -125
  7. package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
  8. package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
  9. package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
  10. package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
  11. package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +402 -402
  12. package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
  13. package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
  14. package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
  15. package/.agents/skills/compliance-audit/SKILL.md +120 -120
  16. package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
  17. package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
  18. package/.agents/skills/lets-build/SKILL.md +173 -173
  19. package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
  20. package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
  21. package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
  22. package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +419 -255
  23. package/.agents/skills/product-analyst/SKILL.md +154 -154
  24. package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
  25. package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
  26. package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
  27. package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
  28. package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
  29. package/.agents/skills/relentless-questioner/SKILL.md +128 -128
  30. package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
  31. package/.editorconfig +19 -19
  32. package/.github/workflows/ci.yml +167 -78
  33. package/.github/workflows/publish.yml +200 -0
  34. package/.gitignore +40 -25
  35. package/AGENTS.md +103 -102
  36. package/LICENSE +21 -21
  37. package/README.md +168 -165
  38. package/bin/azcodr.js +14 -228
  39. package/docs/knowledge/ubiquitous_language.md +31 -18
  40. package/docs/rules/agentic_configuration.md +259 -259
  41. package/docs/rules/api_architecture.md +179 -179
  42. package/docs/rules/authentication.md +76 -76
  43. package/docs/rules/authorization.md +75 -75
  44. package/docs/rules/caching.md +69 -69
  45. package/docs/rules/clean_code.md +62 -62
  46. package/docs/rules/cloud_native.md +41 -41
  47. package/docs/rules/cqrs.md +203 -203
  48. package/docs/rules/database_design.md +125 -125
  49. package/docs/rules/database_operations.md +69 -69
  50. package/docs/rules/design_patterns.md +98 -98
  51. package/docs/rules/devops_ci_cd.md +76 -76
  52. package/docs/rules/domain_driven_design.md +122 -122
  53. package/docs/rules/error_handling.md +54 -52
  54. package/docs/rules/feature_flags.md +59 -59
  55. package/docs/rules/frontend_architecture.md +157 -157
  56. package/docs/rules/multitenancy_architecture.md +98 -98
  57. package/docs/rules/product_ownership.md +127 -127
  58. package/docs/rules/project_management.md +49 -49
  59. package/docs/rules/relentless_questioning.md +52 -52
  60. package/docs/rules/requirements_engineering.md +98 -98
  61. package/docs/rules/security_compliance.md +53 -53
  62. package/docs/rules/server_driven_ui.md +88 -88
  63. package/docs/rules/test_driven_development.md +185 -185
  64. package/docs/rules/transactional_email.md +27 -27
  65. package/docs/rules/type_safety.md +65 -65
  66. package/docs/rules/ui_ux_architecture.md +150 -150
  67. package/docs/rules/workflow_state_machines.md +117 -117
  68. package/lib/cli-parse.js +51 -0
  69. package/lib/cli-target.js +109 -0
  70. package/lib/cli.js +180 -0
  71. package/lib/errors.js +28 -0
  72. package/lib/git.js +29 -0
  73. package/lib/guards.js +96 -0
  74. package/lib/index.d.ts +199 -134
  75. package/lib/index.js +5 -5
  76. package/lib/links.js +123 -0
  77. package/lib/permissions.js +44 -0
  78. package/lib/repo.js +90 -0
  79. package/lib/scaffold.js +238 -448
  80. package/memory.md +119 -36
  81. package/package.json +65 -62
  82. package/scripts/test_coverage.js +66 -38
  83. package/scripts/validate/adr.js +151 -0
  84. package/scripts/validate/io.js +84 -0
  85. package/scripts/validate/links.js +167 -0
  86. package/scripts/validate/parity.js +124 -0
  87. package/scripts/validate/root.js +184 -0
  88. package/scripts/validate/rules.js +44 -0
  89. package/scripts/validate/skills.js +96 -0
  90. package/scripts/validate/text.js +29 -0
  91. package/scripts/validate-cli.js +13 -0
  92. package/scripts/validate.js +140 -258
  93. 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 cp = require('node:child_process');
6
-
7
- /**
8
- * Socket-hardened process boundary (Supply Chain: shell access).
9
- * Scaffolder must spawn `git`, but never via a shell string.
10
- * Uses execFileSync with argv (shell:false) and an explicit subcommand allowlist.
11
- * No network, no env exfiltration; cwd is constrained to targetDir callers.
12
- */
13
- const GIT_ALLOWED_SUBCOMMANDS = new Set(['rev-parse', 'init', 'branch', 'add', 'commit']);
14
-
15
- function runGit(args, options = {}) {
16
- if (!Array.isArray(args) || args.length === 0) {
17
- throw new Error('runGit requires a non-empty argv array');
18
- }
19
- if (!GIT_ALLOWED_SUBCOMMANDS.has(args[0])) {
20
- throw new Error(`Blocked git subcommand: ${String(args[0])}`);
21
- }
22
- return cp.execFileSync('git', args, {
23
- encoding: 'utf-8',
24
- stdio: 'pipe',
25
- shell: false,
26
- ...options
27
- });
28
- }
29
-
30
- /**
31
- * Filesystem scope guard (Supply Chain: filesystem access).
32
- * Constrains all reads/writes to targetDir / templateDir.
33
- */
34
- function assertInside(root, candidate, message) {
35
- const resolvedRoot = path.resolve(root);
36
- const resolvedCandidate = path.resolve(root, candidate);
37
- const relative = path.relative(resolvedRoot, resolvedCandidate);
38
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
39
- throw new Error(message || `Path escapes allowed root: ${candidate}`);
40
- }
41
- }
42
-
43
- const TEMPLATE_ITEMS = [
44
- 'AGENTS.md',
45
- 'memory.md',
46
- 'README.md',
47
- 'docs',
48
- '.agents',
49
- '.github',
50
- 'scripts',
51
- '.gitignore',
52
- '.editorconfig',
53
- 'LICENSE'
54
- ];
55
-
56
- /**
57
- * Returns the root path to the azcodr template files.
58
- */
59
- function getTemplateDir() {
60
- return path.resolve(__dirname, '..');
61
- }
62
-
63
- /**
64
- * Validates the target directory to ensure it is suitable for scaffolding.
65
- */
66
- function validateTarget(targetDir, options = {}) {
67
- const { templateDir = getTemplateDir(), force = false, dryRun = false } = options;
68
- const resolvedTarget = path.resolve(targetDir);
69
- const resolvedTemplate = path.resolve(templateDir);
70
-
71
- if (resolvedTarget === resolvedTemplate) {
72
- throw new Error(`Cannot scaffold into the azcodr template directory itself: ${resolvedTarget}`);
73
- }
74
-
75
- if (!fs.existsSync(resolvedTarget)) {
76
- if (!dryRun) {
77
- fs.mkdirSync(resolvedTarget, { recursive: true });
78
- }
79
- return;
80
- }
81
-
82
- const entries = fs.readdirSync(resolvedTarget);
83
- if (entries.length > 0 && !force) {
84
- throw new Error(
85
- `Target directory is not empty (${entries.length} items found: ${resolvedTarget}). ` +
86
- `Use --force to overwrite existing files.`
87
- );
88
- }
89
- }
90
-
91
- /**
92
- * Detects whether linkName and targetFileName refer to the same entry on a case-insensitive filesystem.
93
- * Uses exact directory listing (not existsSync, which lies on case-insensitive systems).
94
- */
95
- function isSameCaseInsensitiveFile(targetDir, linkName, targetFileName) {
96
- if (linkName.toLowerCase() !== targetFileName.toLowerCase()) {
97
- return false;
98
- }
99
- if (linkName === targetFileName) {
100
- return true;
101
- }
102
- let entries;
103
- try {
104
- entries = fs.readdirSync(targetDir);
105
- } catch {
106
- return false;
107
- }
108
- const targetPath = path.join(targetDir, targetFileName);
109
- const linkPath = path.join(targetDir, linkName);
110
- const hasTargetExact = entries.includes(targetFileName);
111
- const hasLinkExact = entries.includes(linkName);
112
- try {
113
- if (hasTargetExact && hasLinkExact) {
114
- if (fs.existsSync(targetPath) && fs.existsSync(linkPath)) {
115
- return !fs.lstatSync(linkPath).isSymbolicLink();
116
- }
117
- return false;
118
- }
119
- if (fs.existsSync(targetPath) && fs.existsSync(linkPath)) {
120
- // Case-insensitive collision: verify entries are accessible (throws on disk error).
121
- fs.lstatSync(targetPath);
122
- fs.lstatSync(linkPath);
123
- return true;
124
- }
125
- } catch {
126
- return false;
127
- }
128
- return false;
129
- }
130
-
131
- /**
132
- * Safely creates or updates a symbolic link, falling back to a file copy if symlinks are unsupported.
133
- */
134
- function ensureSymlink(targetDir, linkName, targetFileName, dryRun = false) {
135
- if (dryRun) return true;
136
-
137
- if (isSameCaseInsensitiveFile(targetDir, linkName, targetFileName)) {
138
- return true;
139
- }
140
-
141
- const linkPath = path.join(targetDir, linkName);
142
- try {
143
- fs.lstatSync(linkPath);
144
- fs.rmSync(linkPath, { force: true });
145
- } catch {
146
- // Path does not exist, proceed
147
- }
148
-
149
- try {
150
- fs.symlinkSync(targetFileName, linkPath, 'file');
151
- return true;
152
- } catch {
153
- // Fallback if environment (e.g., certain Windows configs) prevents symlink creation
154
- const sourceFile = path.resolve(targetDir, targetFileName);
155
- if (fs.existsSync(sourceFile)) {
156
- fs.copyFileSync(sourceFile, linkPath);
157
- }
158
- return true;
159
- }
160
- }
161
-
162
- /**
163
- * Creates a symlink, falling back to a text pointer (not a full copy).
164
- * Used for .github/copilot-instructions.md where a full AGENTS.md copy would
165
- * break relative markdown links (they resolve from .github/, not root).
166
- */
167
- function ensureSymlinkOrPointer(targetDir, linkName, targetFileName, dryRun = false) {
168
- if (dryRun) return true;
169
-
170
- const linkPath = path.join(targetDir, linkName);
171
- try {
172
- try {
173
- fs.lstatSync(linkPath);
174
- const existing = fs.existsSync(linkPath) ? fs.readFileSync(linkPath, 'utf-8') : '';
175
- if (existing.trim() === targetFileName) return true;
176
- fs.rmSync(linkPath, { force: true });
177
- } catch {
178
- // Path does not exist, proceed
179
- }
180
- fs.symlinkSync(targetFileName, linkPath, 'file');
181
- return true;
182
- } catch {
183
- fs.writeFileSync(linkPath, `${targetFileName}\n`, 'utf-8');
184
- return true;
185
- }
186
- }
187
-
188
- /**
189
- * Ensures all bash scripts in agent and skill directories have executable permissions (0o755).
190
- */
191
- function makeScriptsExecutable(targetDir, dryRun = false) {
192
- const modified = [];
193
-
194
- const agentScriptsDir = path.join(targetDir, '.agents', 'scripts');
195
- if (fs.existsSync(agentScriptsDir) && fs.statSync(agentScriptsDir).isDirectory()) {
196
- const files = fs.readdirSync(agentScriptsDir);
197
- for (const file of files) {
198
- if (file.endsWith('.sh')) {
199
- const filePath = path.join(agentScriptsDir, file);
200
- modified.push(filePath);
201
- if (!dryRun) {
202
- try {
203
- fs.chmodSync(filePath, 0o755);
204
- } catch {
205
- // Non-critical if filesystem does not support POSIX permissions
206
- }
207
- }
208
- }
209
- }
210
- }
211
-
212
- const skillsDir = path.join(targetDir, '.agents', 'skills');
213
- if (!fs.existsSync(skillsDir)) return modified;
214
-
215
- const skills = fs.readdirSync(skillsDir);
216
- for (const skill of skills) {
217
- const scriptsDir = path.join(skillsDir, skill, 'scripts');
218
- if (fs.existsSync(scriptsDir) && fs.statSync(scriptsDir).isDirectory()) {
219
- const files = fs.readdirSync(scriptsDir);
220
- for (const file of files) {
221
- if (file.endsWith('.sh')) {
222
- const filePath = path.join(scriptsDir, file);
223
- modified.push(filePath);
224
- if (!dryRun) {
225
- try {
226
- fs.chmodSync(filePath, 0o755);
227
- } catch {
228
- // Non-critical if filesystem does not support POSIX permissions
229
- }
230
- }
231
- }
232
- }
233
- }
234
- }
235
- return modified;
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
+ };