azcodr 1.4.0 → 1.5.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/.agents/hooks.json +42 -0
- package/.agents/hooks.json.example +42 -42
- package/.agents/mcp_config.json.example +6 -1
- package/.agents/scripts/safety_guard.sh +34 -0
- package/.agents/scripts/verify_completion.sh +27 -0
- 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 +401 -331
- 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 -165
- package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -109
- package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -113
- package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
- package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +255 -136
- package/.agents/skills/product-analyst/SKILL.md +154 -143
- 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 -125
- package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -84
- package/.editorconfig +19 -19
- package/.github/copilot-instructions.md +1 -0
- package/.github/workflows/ci.yml +56 -0
- package/.gitignore +25 -23
- package/AGENTS.md +102 -102
- package/LICENSE +21 -21
- package/README.md +154 -154
- package/bin/azcodr.js +228 -223
- package/data/.gitkeep +0 -0
- package/docs/knowledge/ubiquitous_language.md +18 -18
- package/docs/rules/agentic_configuration.md +259 -256
- 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 +52 -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 -48
- 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 -184
- 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/index.d.ts +134 -123
- package/lib/index.js +5 -5
- package/lib/scaffold.js +399 -248
- package/memory.md +36 -289
- package/package.json +62 -59
- package/scripts/test_coverage.js +38 -0
- package/scripts/validate.js +246 -0
package/lib/scaffold.js
CHANGED
|
@@ -1,248 +1,399 @@
|
|
|
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
|
-
const TEMPLATE_ITEMS = [
|
|
8
|
-
'AGENTS.md',
|
|
9
|
-
'memory.md',
|
|
10
|
-
'README.md',
|
|
11
|
-
'docs',
|
|
12
|
-
'.agents',
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
fs.
|
|
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
|
-
return
|
|
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
|
-
actions.push(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
const TEMPLATE_ITEMS = [
|
|
8
|
+
'AGENTS.md',
|
|
9
|
+
'memory.md',
|
|
10
|
+
'README.md',
|
|
11
|
+
'docs',
|
|
12
|
+
'.agents',
|
|
13
|
+
'.github',
|
|
14
|
+
'scripts',
|
|
15
|
+
'.gitignore',
|
|
16
|
+
'.editorconfig',
|
|
17
|
+
'LICENSE'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the root path to the azcodr template files.
|
|
22
|
+
*/
|
|
23
|
+
function getTemplateDir() {
|
|
24
|
+
return path.resolve(__dirname, '..');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validates the target directory to ensure it is suitable for scaffolding.
|
|
29
|
+
*/
|
|
30
|
+
function validateTarget(targetDir, options = {}) {
|
|
31
|
+
const { templateDir = getTemplateDir(), force = false, dryRun = false } = options;
|
|
32
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
33
|
+
const resolvedTemplate = path.resolve(templateDir);
|
|
34
|
+
|
|
35
|
+
if (resolvedTarget === resolvedTemplate) {
|
|
36
|
+
throw new Error(`Cannot scaffold into the azcodr template directory itself: ${resolvedTarget}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(resolvedTarget)) {
|
|
40
|
+
if (!dryRun) {
|
|
41
|
+
fs.mkdirSync(resolvedTarget, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entries = fs.readdirSync(resolvedTarget);
|
|
47
|
+
if (entries.length > 0 && !force) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Target directory is not empty (${entries.length} items found: ${resolvedTarget}). ` +
|
|
50
|
+
`Use --force to overwrite existing files.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detects whether linkName and targetFileName refer to the same entry on a case-insensitive filesystem.
|
|
57
|
+
* Uses exact directory listing (not existsSync, which lies on case-insensitive systems).
|
|
58
|
+
*/
|
|
59
|
+
function isSameCaseInsensitiveFile(targetDir, linkName, targetFileName) {
|
|
60
|
+
if (linkName.toLowerCase() !== targetFileName.toLowerCase()) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (linkName === targetFileName) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = fs.readdirSync(targetDir);
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const targetPath = path.join(targetDir, targetFileName);
|
|
73
|
+
const linkPath = path.join(targetDir, linkName);
|
|
74
|
+
const hasTargetExact = entries.includes(targetFileName);
|
|
75
|
+
const hasLinkExact = entries.includes(linkName);
|
|
76
|
+
try {
|
|
77
|
+
if (hasTargetExact && hasLinkExact) {
|
|
78
|
+
if (fs.existsSync(targetPath) && fs.existsSync(linkPath)) {
|
|
79
|
+
return !fs.lstatSync(linkPath).isSymbolicLink();
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (fs.existsSync(targetPath) && fs.existsSync(linkPath)) {
|
|
84
|
+
// Case-insensitive collision: verify entries are accessible (throws on disk error).
|
|
85
|
+
fs.lstatSync(targetPath);
|
|
86
|
+
fs.lstatSync(linkPath);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Safely creates or updates a symbolic link, falling back to a file copy if symlinks are unsupported.
|
|
97
|
+
*/
|
|
98
|
+
function ensureSymlink(targetDir, linkName, targetFileName, dryRun = false) {
|
|
99
|
+
if (dryRun) return true;
|
|
100
|
+
|
|
101
|
+
if (isSameCaseInsensitiveFile(targetDir, linkName, targetFileName)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const linkPath = path.join(targetDir, linkName);
|
|
106
|
+
try {
|
|
107
|
+
fs.lstatSync(linkPath);
|
|
108
|
+
fs.rmSync(linkPath, { force: true });
|
|
109
|
+
} catch {
|
|
110
|
+
// Path does not exist, proceed
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
fs.symlinkSync(targetFileName, linkPath, 'file');
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
// Fallback if environment (e.g., certain Windows configs) prevents symlink creation
|
|
118
|
+
const sourceFile = path.resolve(targetDir, targetFileName);
|
|
119
|
+
if (fs.existsSync(sourceFile)) {
|
|
120
|
+
fs.copyFileSync(sourceFile, linkPath);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates a symlink, falling back to a text pointer (not a full copy).
|
|
128
|
+
* Used for .github/copilot-instructions.md where a full AGENTS.md copy would
|
|
129
|
+
* break relative markdown links (they resolve from .github/, not root).
|
|
130
|
+
*/
|
|
131
|
+
function ensureSymlinkOrPointer(targetDir, linkName, targetFileName, dryRun = false) {
|
|
132
|
+
if (dryRun) return true;
|
|
133
|
+
|
|
134
|
+
const linkPath = path.join(targetDir, linkName);
|
|
135
|
+
try {
|
|
136
|
+
try {
|
|
137
|
+
fs.lstatSync(linkPath);
|
|
138
|
+
const existing = fs.existsSync(linkPath) ? fs.readFileSync(linkPath, 'utf-8') : '';
|
|
139
|
+
if (existing.trim() === targetFileName) return true;
|
|
140
|
+
fs.rmSync(linkPath, { force: true });
|
|
141
|
+
} catch {
|
|
142
|
+
// Path does not exist, proceed
|
|
143
|
+
}
|
|
144
|
+
fs.symlinkSync(targetFileName, linkPath, 'file');
|
|
145
|
+
return true;
|
|
146
|
+
} catch {
|
|
147
|
+
fs.writeFileSync(linkPath, `${targetFileName}\n`, 'utf-8');
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Ensures all bash scripts in agent and skill directories have executable permissions (0o755).
|
|
154
|
+
*/
|
|
155
|
+
function makeScriptsExecutable(targetDir, dryRun = false) {
|
|
156
|
+
const modified = [];
|
|
157
|
+
|
|
158
|
+
const agentScriptsDir = path.join(targetDir, '.agents', 'scripts');
|
|
159
|
+
if (fs.existsSync(agentScriptsDir) && fs.statSync(agentScriptsDir).isDirectory()) {
|
|
160
|
+
const files = fs.readdirSync(agentScriptsDir);
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
if (file.endsWith('.sh')) {
|
|
163
|
+
const filePath = path.join(agentScriptsDir, file);
|
|
164
|
+
modified.push(filePath);
|
|
165
|
+
if (!dryRun) {
|
|
166
|
+
try {
|
|
167
|
+
fs.chmodSync(filePath, 0o755);
|
|
168
|
+
} catch {
|
|
169
|
+
// Non-critical if filesystem does not support POSIX permissions
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const skillsDir = path.join(targetDir, '.agents', 'skills');
|
|
177
|
+
if (!fs.existsSync(skillsDir)) return modified;
|
|
178
|
+
|
|
179
|
+
const skills = fs.readdirSync(skillsDir);
|
|
180
|
+
for (const skill of skills) {
|
|
181
|
+
const scriptsDir = path.join(skillsDir, skill, 'scripts');
|
|
182
|
+
if (fs.existsSync(scriptsDir) && fs.statSync(scriptsDir).isDirectory()) {
|
|
183
|
+
const files = fs.readdirSync(scriptsDir);
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
if (file.endsWith('.sh')) {
|
|
186
|
+
const filePath = path.join(scriptsDir, file);
|
|
187
|
+
modified.push(filePath);
|
|
188
|
+
if (!dryRun) {
|
|
189
|
+
try {
|
|
190
|
+
fs.chmodSync(filePath, 0o755);
|
|
191
|
+
} catch {
|
|
192
|
+
// Non-critical if filesystem does not support POSIX permissions
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return modified;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Recursively copies template files into the target directory and sets up symlinks and permissions.
|
|
204
|
+
*/
|
|
205
|
+
function copyTemplate(targetDir, templateDir = getTemplateDir(), options = {}) {
|
|
206
|
+
const { dryRun = false } = options;
|
|
207
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
208
|
+
const resolvedTemplate = path.resolve(templateDir);
|
|
209
|
+
const actions = [];
|
|
210
|
+
|
|
211
|
+
if (!dryRun && !fs.existsSync(resolvedTarget)) {
|
|
212
|
+
fs.mkdirSync(resolvedTarget, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const item of TEMPLATE_ITEMS) {
|
|
216
|
+
let srcPath = path.join(resolvedTemplate, item);
|
|
217
|
+
if (item === '.gitignore' && !fs.existsSync(srcPath)) {
|
|
218
|
+
const npmIgnorePath = path.join(resolvedTemplate, '.npmignore');
|
|
219
|
+
if (fs.existsSync(npmIgnorePath)) {
|
|
220
|
+
srcPath = npmIgnorePath;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
224
|
+
|
|
225
|
+
const destPath = path.join(resolvedTarget, item);
|
|
226
|
+
actions.push(`copy: ${item} -> ${destPath}`);
|
|
227
|
+
|
|
228
|
+
if (!dryRun) {
|
|
229
|
+
fs.cpSync(srcPath, destPath, { recursive: true, force: true, dereference: false });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Ensure harness parity symlinks per AGENTS.md mandate
|
|
234
|
+
actions.push(`symlink: CLAUDE.md -> AGENTS.md`);
|
|
235
|
+
ensureSymlink(resolvedTarget, 'CLAUDE.md', 'AGENTS.md', dryRun);
|
|
236
|
+
|
|
237
|
+
actions.push(`symlink: agents.md -> AGENTS.md`);
|
|
238
|
+
ensureSymlink(resolvedTarget, 'agents.md', 'AGENTS.md', dryRun);
|
|
239
|
+
|
|
240
|
+
actions.push(`symlink: GEMINI.md -> AGENTS.md`);
|
|
241
|
+
ensureSymlink(resolvedTarget, 'GEMINI.md', 'AGENTS.md', dryRun);
|
|
242
|
+
|
|
243
|
+
actions.push(`symlink: .cursorrules -> AGENTS.md`);
|
|
244
|
+
ensureSymlink(resolvedTarget, '.cursorrules', 'AGENTS.md', dryRun);
|
|
245
|
+
|
|
246
|
+
actions.push(`symlink: .windsurfrules -> AGENTS.md`);
|
|
247
|
+
ensureSymlink(resolvedTarget, '.windsurfrules', 'AGENTS.md', dryRun);
|
|
248
|
+
|
|
249
|
+
// GitHub Copilot harness parity (text-pointer fallback preserves relative links)
|
|
250
|
+
const githubDir = path.join(resolvedTarget, '.github');
|
|
251
|
+
if (!dryRun && !fs.existsSync(githubDir)) {
|
|
252
|
+
fs.mkdirSync(githubDir, { recursive: true });
|
|
253
|
+
}
|
|
254
|
+
actions.push(`symlink: .github/copilot-instructions.md -> ../AGENTS.md`);
|
|
255
|
+
ensureSymlinkOrPointer(githubDir, 'copilot-instructions.md', '../AGENTS.md', dryRun);
|
|
256
|
+
|
|
257
|
+
// Starter package.json for project scripts validation
|
|
258
|
+
const pkgJsonPath = path.join(resolvedTarget, 'package.json');
|
|
259
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
260
|
+
actions.push('create: package.json');
|
|
261
|
+
if (!dryRun) {
|
|
262
|
+
const projectName = path.basename(resolvedTarget) || 'my-project';
|
|
263
|
+
const starterPkg = {
|
|
264
|
+
name: projectName,
|
|
265
|
+
version: '0.1.0',
|
|
266
|
+
private: true,
|
|
267
|
+
description: 'Scaffolded with azcodr enterprise architecture template',
|
|
268
|
+
scripts: {
|
|
269
|
+
test: 'node --test',
|
|
270
|
+
'test:coverage': 'node scripts/test_coverage.js',
|
|
271
|
+
lint: 'echo "No linter configured yet. Run /lets-build to configure toolchain."',
|
|
272
|
+
validate: 'node scripts/validate.js'
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
fs.writeFileSync(pkgJsonPath, JSON.stringify(starterPkg, null, 2) + '\n', 'utf-8');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Ensure scripts are executable
|
|
280
|
+
const inspectDir = dryRun ? resolvedTemplate : resolvedTarget;
|
|
281
|
+
const scripts = makeScriptsExecutable(inspectDir, dryRun);
|
|
282
|
+
for (const script of scripts) {
|
|
283
|
+
actions.push(`chmod: +x ${path.relative(inspectDir, script)}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return actions;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Detects whether targetDir is already inside an existing Git worktree.
|
|
291
|
+
*/
|
|
292
|
+
function isInsideGitWorkTree(targetDir) {
|
|
293
|
+
try {
|
|
294
|
+
const checkDir = fs.existsSync(targetDir) ? targetDir : path.dirname(targetDir);
|
|
295
|
+
const out = cp.execSync('git rev-parse --is-inside-work-tree', {
|
|
296
|
+
cwd: checkDir,
|
|
297
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
298
|
+
encoding: 'utf-8'
|
|
299
|
+
});
|
|
300
|
+
return out.trim() === 'true';
|
|
301
|
+
} catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Initializes a git repository in the target directory if not already inside one.
|
|
308
|
+
*/
|
|
309
|
+
function initGit(targetDir, options = {}) {
|
|
310
|
+
const { noGit = false, dryRun = false } = options;
|
|
311
|
+
if (noGit) return false;
|
|
312
|
+
|
|
313
|
+
const gitDir = path.join(targetDir, '.git');
|
|
314
|
+
if (fs.existsSync(gitDir)) return false;
|
|
315
|
+
|
|
316
|
+
if (isInsideGitWorkTree(targetDir)) return false;
|
|
317
|
+
|
|
318
|
+
if (dryRun) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
try {
|
|
324
|
+
cp.execSync('git init -b main -q', { cwd: targetDir, stdio: 'ignore' });
|
|
325
|
+
} catch {
|
|
326
|
+
cp.execSync('git init -q', { cwd: targetDir, stdio: 'ignore' });
|
|
327
|
+
try {
|
|
328
|
+
cp.execSync('git branch -m main', { cwd: targetDir, stdio: 'ignore' });
|
|
329
|
+
} catch {
|
|
330
|
+
// Non-critical if branch rename fails
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
cp.execSync('git add -A', { cwd: targetDir, stdio: 'ignore' });
|
|
336
|
+
try {
|
|
337
|
+
cp.execSync('git commit -q -m "chore: initial scaffold from azcodr template"', {
|
|
338
|
+
cwd: targetDir,
|
|
339
|
+
stdio: 'ignore'
|
|
340
|
+
});
|
|
341
|
+
} catch {
|
|
342
|
+
cp.execSync('git -c user.name="azcodr" -c user.email="azcodr@local" commit -q -m "chore: initial scaffold from azcodr template"', {
|
|
343
|
+
cwd: targetDir,
|
|
344
|
+
stdio: 'ignore'
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
// Non-critical if initial commit fails
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return true;
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* High-level orchestration function to scaffold the azcodr workspace into targetDir.
|
|
359
|
+
*/
|
|
360
|
+
function scaffold(options = {}) {
|
|
361
|
+
const {
|
|
362
|
+
targetDir = process.cwd(),
|
|
363
|
+
force = false,
|
|
364
|
+
noGit = false,
|
|
365
|
+
templateDir = getTemplateDir(),
|
|
366
|
+
dryRun = false,
|
|
367
|
+
silent = false
|
|
368
|
+
} = options;
|
|
369
|
+
|
|
370
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
371
|
+
validateTarget(resolvedTarget, { templateDir, force, dryRun });
|
|
372
|
+
const actions = copyTemplate(resolvedTarget, templateDir, { dryRun });
|
|
373
|
+
const gitInitialized = initGit(resolvedTarget, { noGit, dryRun });
|
|
374
|
+
if (gitInitialized) {
|
|
375
|
+
actions.push('git: initialize repository');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
success: true,
|
|
380
|
+
targetDir: resolvedTarget,
|
|
381
|
+
gitInitialized,
|
|
382
|
+
dryRun,
|
|
383
|
+
actions
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
module.exports = {
|
|
388
|
+
scaffold,
|
|
389
|
+
validateTarget,
|
|
390
|
+
copyTemplate,
|
|
391
|
+
ensureSymlink,
|
|
392
|
+
ensureSymlinkOrPointer,
|
|
393
|
+
isSameCaseInsensitiveFile,
|
|
394
|
+
makeScriptsExecutable,
|
|
395
|
+
isInsideGitWorkTree,
|
|
396
|
+
initGit,
|
|
397
|
+
getTemplateDir,
|
|
398
|
+
TEMPLATE_ITEMS
|
|
399
|
+
};
|