azcodr 1.0.1 → 1.1.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/skills/agentic-architect/scripts/validate_agentic_configs.sh +24 -8
- package/.editorconfig +19 -0
- package/.gitignore +3 -0
- package/bin/azcodr.js +170 -97
- package/changes.md +14 -0
- package/lib/index.d.ts +153 -0
- package/lib/scaffold.js +86 -24
- package/package.json +14 -4
|
@@ -45,21 +45,37 @@ if [[ -L "${CLAUDE_FILE}" ]]; then
|
|
|
45
45
|
else
|
|
46
46
|
log_fail "CLAUDE.md points to '${TARGET}' instead of 'AGENTS.md'."
|
|
47
47
|
fi
|
|
48
|
+
elif [[ -f "${CLAUDE_FILE}" ]] && [[ "$(< "${CLAUDE_FILE}")" == "AGENTS.md" ]]; then
|
|
49
|
+
log_pass "CLAUDE.md is a text pointer to AGENTS.md (symlink fallback)."
|
|
48
50
|
else
|
|
49
51
|
log_fail "CLAUDE.md is not a symbolic link."
|
|
50
52
|
fi
|
|
51
53
|
|
|
52
|
-
# Check agents.md symlink
|
|
54
|
+
# Check agents.md symlink (case-insensitive filesystem aware)
|
|
53
55
|
AGENTS_LOWER="${WORKSPACE_ROOT}/agents.md"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
IS_CASE_INSENSITIVE=false
|
|
57
|
+
if [[ "$(uname -s)" == "Darwin" ]] || [[ "$(uname -s)" =~ (MINGW|MSYS|CYGWIN) ]]; then
|
|
58
|
+
IS_CASE_INSENSITIVE=true
|
|
59
|
+
elif [[ -f "${AGENTS_FILE}" ]] && [[ -f "${AGENTS_LOWER}" ]] && [[ ! -L "${AGENTS_LOWER}" ]]; then
|
|
60
|
+
IS_CASE_INSENSITIVE=true
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if [[ "${IS_CASE_INSENSITIVE}" == "true" ]]; then
|
|
64
|
+
log_pass "agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem)."
|
|
65
|
+
else
|
|
66
|
+
if [[ ! -L "${AGENTS_LOWER}" ]] && [[ ! -e "${AGENTS_LOWER}" ]] && [[ -f "${AGENTS_FILE}" ]]; then
|
|
67
|
+
ln -sf "AGENTS.md" "${AGENTS_LOWER}"
|
|
68
|
+
fi
|
|
69
|
+
if [[ -L "${AGENTS_LOWER}" ]]; then
|
|
70
|
+
TARGET=$(readlink "${AGENTS_LOWER}")
|
|
71
|
+
if [[ "${TARGET}" == "AGENTS.md" ]]; then
|
|
72
|
+
log_pass "agents.md is a valid symlink to AGENTS.md."
|
|
73
|
+
else
|
|
74
|
+
log_fail "agents.md points to '${TARGET}' instead of 'AGENTS.md'."
|
|
75
|
+
fi
|
|
58
76
|
else
|
|
59
|
-
log_fail "agents.md
|
|
77
|
+
log_fail "agents.md is not a symbolic link."
|
|
60
78
|
fi
|
|
61
|
-
else
|
|
62
|
-
log_fail "agents.md is not a symbolic link."
|
|
63
79
|
fi
|
|
64
80
|
|
|
65
81
|
# 2. Checking Progressive Disclosure Rules (docs/rules)
|
package/.editorconfig
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# http://editorconfig.org
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
indent_style = space
|
|
6
|
+
indent_size = 2
|
|
7
|
+
end_of_line = lf
|
|
8
|
+
charset = utf-8
|
|
9
|
+
trim_trailing_whitespace = true
|
|
10
|
+
insert_final_newline = true
|
|
11
|
+
|
|
12
|
+
[*.md]
|
|
13
|
+
trim_trailing_whitespace = false
|
|
14
|
+
|
|
15
|
+
[Makefile]
|
|
16
|
+
indent_style = tab
|
|
17
|
+
|
|
18
|
+
[*.go]
|
|
19
|
+
indent_style = tab
|
package/.gitignore
CHANGED
package/bin/azcodr.js
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const readline = require('node:readline');
|
|
6
|
+
const fs = require('node:fs');
|
|
6
7
|
const { scaffold, logChange, getTemplateDir } = require('../lib/scaffold.js');
|
|
7
8
|
const pkg = require('../package.json');
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function printHelp() {
|
|
12
|
-
console.log(`
|
|
10
|
+
function printHelp(out = console.log) {
|
|
11
|
+
out(`
|
|
13
12
|
azcodr v${pkg.version}
|
|
14
13
|
Enterprise Multi-Tenant Architecture & Agentic Engineering Starter Template
|
|
15
14
|
|
|
@@ -22,208 +21,282 @@ Commands:
|
|
|
22
21
|
change <title> Log a generic architectural change to changes.md
|
|
23
22
|
|
|
24
23
|
Scaffold Options:
|
|
24
|
+
-d, --dry-run Simulate scaffolding without modifying filesystem
|
|
25
|
+
-s, --silent Suppress console output messages
|
|
25
26
|
-f, --force Overwrite existing files in target directory without confirmation
|
|
26
27
|
--no-git Do not initialize a git repository
|
|
27
28
|
-v, --version Display version number
|
|
28
29
|
-h, --help Display this help message
|
|
29
30
|
|
|
30
31
|
Change Options:
|
|
31
|
-
-c, --category Category (Rule | Skill | Infrastructure | CLI | Knowledge Hub)
|
|
32
|
+
-c, --category Category (Architecture | Rule | Skill | Infrastructure | CLI | Knowledge Hub)
|
|
32
33
|
-f, --files Target file(s) affected (e.g. "docs/rules/caching.md")
|
|
33
34
|
-r, --rationale Rationale for upstream template incorporation
|
|
34
35
|
-d, --desc Detailed description of the change
|
|
35
36
|
|
|
36
37
|
Examples:
|
|
37
38
|
npx azcodr my-project
|
|
39
|
+
npx azcodr . --dry-run
|
|
38
40
|
npx azcodr . --force
|
|
39
41
|
npx azcodr change "Add Wasm plugin interface" -c Architecture
|
|
40
42
|
`);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
function printVersion() {
|
|
44
|
-
|
|
45
|
+
function printVersion(out = console.log) {
|
|
46
|
+
out(pkg.version);
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
function askQuestion(query) {
|
|
48
|
-
const rl = readline.createInterface({
|
|
49
|
-
input: process.stdin,
|
|
50
|
-
output: process.stdout
|
|
51
|
-
});
|
|
49
|
+
function askQuestion(query, { input = process.stdin, output = process.stdout } = {}) {
|
|
50
|
+
const rl = readline.createInterface({ input, output });
|
|
52
51
|
|
|
53
52
|
return new Promise((resolve) => {
|
|
53
|
+
let resolved = false;
|
|
54
54
|
rl.question(query, (answer) => {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
if (!resolved) {
|
|
56
|
+
resolved = true;
|
|
57
|
+
rl.close();
|
|
58
|
+
resolve(answer.trim());
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
rl.on('close', () => {
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
resolved = true;
|
|
64
|
+
resolve('');
|
|
65
|
+
}
|
|
57
66
|
});
|
|
58
67
|
});
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
async function handleLogChange() {
|
|
70
|
+
async function handleLogChange(rawArgs = process.argv.slice(2), io = {}) {
|
|
71
|
+
const {
|
|
72
|
+
out = console.log,
|
|
73
|
+
err = console.error,
|
|
74
|
+
exit = process.exit,
|
|
75
|
+
stdin = process.stdin,
|
|
76
|
+
stdout = process.stdout,
|
|
77
|
+
cwd = process.cwd(),
|
|
78
|
+
logChange: logChangeFn = logChange
|
|
79
|
+
} = io;
|
|
80
|
+
|
|
62
81
|
let title = null;
|
|
63
82
|
let category = 'Architecture';
|
|
64
83
|
let targetFiles = 'docs/rules/';
|
|
65
84
|
let rationale = 'Generic architectural enhancement';
|
|
66
85
|
let description = '';
|
|
67
86
|
|
|
68
|
-
for (let i = 1; i <
|
|
69
|
-
const a =
|
|
87
|
+
for (let i = 1; i < rawArgs.length; i++) {
|
|
88
|
+
const a = rawArgs[i];
|
|
70
89
|
if (a === '-c' || a === '--category') {
|
|
71
|
-
category =
|
|
90
|
+
category = rawArgs[++i] || category;
|
|
72
91
|
} else if (a === '-f' || a === '--files') {
|
|
73
|
-
targetFiles =
|
|
92
|
+
targetFiles = rawArgs[++i] || targetFiles;
|
|
74
93
|
} else if (a === '-r' || a === '--rationale') {
|
|
75
|
-
rationale =
|
|
94
|
+
rationale = rawArgs[++i] || rationale;
|
|
76
95
|
} else if (a === '-d' || a === '--desc' || a === '--description') {
|
|
77
|
-
description =
|
|
78
|
-
} else if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
description = rawArgs[++i] || description;
|
|
97
|
+
} else if (a.startsWith('-')) {
|
|
98
|
+
err(`❌ Error: Unknown argument '${a}'. Run 'npx azcodr --help' for available options.`);
|
|
99
|
+
return exit(1);
|
|
100
|
+
} else if (!title) {
|
|
101
|
+
title = a;
|
|
82
102
|
}
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
if (!title) {
|
|
86
|
-
if (
|
|
87
|
-
title = await askQuestion('? Change title: ');
|
|
106
|
+
if (stdin.isTTY) {
|
|
107
|
+
title = await askQuestion('? Change title: ', { input: stdin, output: stdout });
|
|
88
108
|
}
|
|
89
109
|
}
|
|
90
110
|
|
|
91
111
|
if (!title) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
err('❌ Error: A title is required to log an upstream change.');
|
|
113
|
+
err('Usage: npx azcodr change "<title>" [-c Category] [-f Files] [-r Rationale] [-d Description]');
|
|
114
|
+
return exit(1);
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
try {
|
|
98
|
-
const res =
|
|
118
|
+
const res = logChangeFn({
|
|
99
119
|
title,
|
|
100
120
|
category,
|
|
101
121
|
targetFiles,
|
|
102
122
|
rationale,
|
|
103
123
|
description,
|
|
104
|
-
targetDir:
|
|
124
|
+
targetDir: cwd
|
|
105
125
|
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} catch (
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
out(`\n✅ Upstream change logged to ${res.filePath}\n`);
|
|
127
|
+
return exit(0);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
err(`\n❌ Failed to log change: ${error.message}\n`);
|
|
130
|
+
return exit(1);
|
|
111
131
|
}
|
|
112
132
|
}
|
|
113
133
|
|
|
114
|
-
async function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
async function runCli(rawArgs = process.argv.slice(2), io = {}) {
|
|
135
|
+
const {
|
|
136
|
+
out = console.log,
|
|
137
|
+
err = console.error,
|
|
138
|
+
exit = process.exit,
|
|
139
|
+
stdin = process.stdin,
|
|
140
|
+
stdout = process.stdout,
|
|
141
|
+
cwd = process.cwd(),
|
|
142
|
+
templateDir = getTemplateDir(),
|
|
143
|
+
scaffold: scaffoldFn = scaffold
|
|
144
|
+
} = io;
|
|
124
145
|
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
return;
|
|
146
|
+
if (rawArgs[0] === 'change' || rawArgs[0] === 'log-change') {
|
|
147
|
+
return handleLogChange(rawArgs, io);
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
let targetDir = null;
|
|
131
151
|
let force = false;
|
|
132
152
|
let noGit = false;
|
|
153
|
+
let dryRun = false;
|
|
154
|
+
let silent = false;
|
|
133
155
|
|
|
134
|
-
for (let i = 0; i <
|
|
135
|
-
const arg =
|
|
156
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
157
|
+
const arg = rawArgs[i];
|
|
136
158
|
if (arg === '-h' || arg === '--help') {
|
|
137
|
-
printHelp();
|
|
138
|
-
|
|
159
|
+
printHelp(out);
|
|
160
|
+
return exit(0);
|
|
139
161
|
} else if (arg === '-v' || arg === '--version') {
|
|
140
|
-
printVersion();
|
|
141
|
-
|
|
162
|
+
printVersion(out);
|
|
163
|
+
return exit(0);
|
|
142
164
|
} else if (arg === '-f' || arg === '--force') {
|
|
143
165
|
force = true;
|
|
144
166
|
} else if (arg === '--no-git') {
|
|
145
167
|
noGit = true;
|
|
146
|
-
} else if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
} else if (arg === '-d' || arg === '--dry-run') {
|
|
169
|
+
dryRun = true;
|
|
170
|
+
} else if (arg === '-s' || arg === '--silent') {
|
|
171
|
+
silent = true;
|
|
172
|
+
} else if (arg.startsWith('-')) {
|
|
173
|
+
err(`❌ Error: Unknown argument '${arg}'. Run 'npx azcodr --help' for available options.`);
|
|
174
|
+
return exit(1);
|
|
175
|
+
} else if (!targetDir) {
|
|
176
|
+
targetDir = arg;
|
|
150
177
|
}
|
|
151
178
|
}
|
|
152
179
|
|
|
153
|
-
|
|
180
|
+
if (!silent) {
|
|
181
|
+
out('\n🚀 azcodr - Enterprise Multi-Tenant Architecture & Agentic Engineering\n');
|
|
182
|
+
}
|
|
154
183
|
|
|
155
184
|
if (!targetDir) {
|
|
156
|
-
if (
|
|
157
|
-
const answer = await askQuestion('? Where would you like to initialize your project? (./) '
|
|
185
|
+
if (stdin.isTTY) {
|
|
186
|
+
const answer = await askQuestion('? Where would you like to initialize your project? (./) ', {
|
|
187
|
+
input: stdin,
|
|
188
|
+
output: stdout
|
|
189
|
+
});
|
|
158
190
|
targetDir = answer || '.';
|
|
159
191
|
} else {
|
|
160
192
|
targetDir = '.';
|
|
161
193
|
}
|
|
162
194
|
}
|
|
163
195
|
|
|
164
|
-
const resolvedTarget = path.resolve(
|
|
165
|
-
const templateDir = getTemplateDir();
|
|
196
|
+
const resolvedTarget = path.resolve(cwd, targetDir);
|
|
166
197
|
|
|
167
198
|
if (resolvedTarget === templateDir) {
|
|
168
|
-
|
|
169
|
-
|
|
199
|
+
err(`❌ Error: Cannot scaffold into the template directory itself: ${resolvedTarget}`);
|
|
200
|
+
return exit(1);
|
|
170
201
|
}
|
|
171
202
|
|
|
172
|
-
const fs = require('node:fs');
|
|
173
203
|
if (fs.existsSync(resolvedTarget)) {
|
|
204
|
+
const stat = fs.statSync(resolvedTarget);
|
|
205
|
+
if (!stat.isDirectory()) {
|
|
206
|
+
err(`❌ Error: Target '${resolvedTarget}' already exists and is not a directory.`);
|
|
207
|
+
return exit(1);
|
|
208
|
+
}
|
|
174
209
|
const entries = fs.readdirSync(resolvedTarget);
|
|
175
210
|
if (entries.length > 0 && !force) {
|
|
176
|
-
if (
|
|
211
|
+
if (stdin.isTTY) {
|
|
177
212
|
const confirm = await askQuestion(
|
|
178
|
-
`⚠️ Target directory '${targetDir}' is not empty (${entries.length} items). Continue? (y/N)
|
|
213
|
+
`⚠️ Target directory '${targetDir}' is not empty (${entries.length} items). Continue? (y/N) `,
|
|
214
|
+
{ input: stdin, output: stdout }
|
|
179
215
|
);
|
|
180
216
|
if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
|
|
181
|
-
|
|
182
|
-
|
|
217
|
+
out('Scaffolding aborted.');
|
|
218
|
+
return exit(0);
|
|
183
219
|
}
|
|
184
220
|
force = true;
|
|
185
221
|
} else {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
);
|
|
189
|
-
process.exit(1);
|
|
222
|
+
err(`❌ Error: Target directory '${resolvedTarget}' is not empty. Use --force to proceed.`);
|
|
223
|
+
return exit(1);
|
|
190
224
|
}
|
|
191
225
|
}
|
|
192
226
|
}
|
|
193
227
|
|
|
194
|
-
|
|
228
|
+
if (dryRun) {
|
|
229
|
+
if (!silent) {
|
|
230
|
+
out(`🔍 DRY RUN: Simulating azcodr scaffolding into: ${resolvedTarget}\n`);
|
|
231
|
+
}
|
|
232
|
+
} else if (!silent) {
|
|
233
|
+
out(`📦 Scaffolding azcodr into: ${resolvedTarget}`);
|
|
234
|
+
}
|
|
195
235
|
|
|
196
236
|
try {
|
|
197
|
-
const result =
|
|
237
|
+
const result = scaffoldFn({
|
|
198
238
|
targetDir: resolvedTarget,
|
|
199
239
|
force,
|
|
200
240
|
noGit,
|
|
201
|
-
templateDir
|
|
241
|
+
templateDir,
|
|
242
|
+
dryRun,
|
|
243
|
+
silent
|
|
202
244
|
});
|
|
203
245
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
246
|
+
if (dryRun) {
|
|
247
|
+
if (!silent) {
|
|
248
|
+
for (const action of result.actions) {
|
|
249
|
+
out(` [preview] ${action}`);
|
|
250
|
+
}
|
|
251
|
+
out('\n🎉 Dry run completed. 0 files modified on disk.\n');
|
|
252
|
+
}
|
|
253
|
+
return exit(0);
|
|
211
254
|
}
|
|
212
255
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
256
|
+
if (!silent) {
|
|
257
|
+
out(' ✅ Progressive disclosure rules copied (docs/rules/)');
|
|
258
|
+
out(' ✅ Workspace knowledge hub and ADR ledger copied (docs/knowledge/, memory.md)');
|
|
259
|
+
out(' ✅ Specialized agentic skills copied (.agents/skills/)');
|
|
260
|
+
out(' ✅ Upstream changes ledger initialized (changes.md)');
|
|
261
|
+
out(' ✅ Editor formatting standards initialized (.editorconfig)');
|
|
262
|
+
out(' ✅ Agent directives and harness symlinks established (AGENTS.md, CLAUDE.md, agents.md)');
|
|
263
|
+
if (result.gitInitialized) {
|
|
264
|
+
out(' ✅ Git repository initialized');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
out('\n🎉 azcodr initialized successfully!\n');
|
|
268
|
+
out('Next steps:');
|
|
269
|
+
if (targetDir !== '.' && targetDir !== './') {
|
|
270
|
+
out(` 1. cd ${targetDir}`);
|
|
271
|
+
}
|
|
272
|
+
out(' 2. Open the project in your AI coding assistant (Antigravity, Claude Code, Cursor, OpenHands)');
|
|
273
|
+
out(' 3. Run /lets-build to start the architectural interview and scaffold your application stack!\n');
|
|
217
274
|
}
|
|
218
|
-
|
|
219
|
-
|
|
275
|
+
return exit(0);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
err(`\n❌ Scaffolding failed: ${error.message}\n`);
|
|
278
|
+
return exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function main() {
|
|
283
|
+
try {
|
|
284
|
+
await runCli(process.argv.slice(2));
|
|
220
285
|
} catch (err) {
|
|
221
|
-
console.error(
|
|
286
|
+
console.error('Unexpected error:', err);
|
|
222
287
|
process.exit(1);
|
|
223
288
|
}
|
|
224
289
|
}
|
|
225
290
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
291
|
+
if (require.main === module) {
|
|
292
|
+
main();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = {
|
|
296
|
+
runCli,
|
|
297
|
+
handleLogChange,
|
|
298
|
+
askQuestion,
|
|
299
|
+
printHelp,
|
|
300
|
+
printVersion,
|
|
301
|
+
main
|
|
302
|
+
};
|
package/changes.md
CHANGED
|
@@ -34,3 +34,17 @@ When an AI agent or engineer discovers a generic architectural improvement, bug
|
|
|
34
34
|
- **Rationale:** Remove fragile git repo resolution and merge scripts; replace with atomic change logging in `changes.md`.
|
|
35
35
|
- **Description:** Retired `merge-ai` skill and removed machine-specific hardcoded paths. All upstream improvements are now recorded atomically in `changes.md`.
|
|
36
36
|
- **Domain Filter Verification:** Verified 100% generic.
|
|
37
|
+
|
|
38
|
+
### [2026-09-25] Harden CLI, achieve 100% test coverage gates, add multi-OS CI workflow, and TypeScript declarations
|
|
39
|
+
- **Category:** CLI
|
|
40
|
+
- **Target File(s):** bin/azcodr.js, lib/scaffold.js, lib/index.d.ts, .github/workflows/ci.yml
|
|
41
|
+
- **Rationale:** Fulfill 100.00% test coverage mandate, cross-platform CI matrix, and library type safety
|
|
42
|
+
- **Description:** Remediate gap assessment findings: add --dry-run and --silent flags, enforce 100.00% line/branch/function coverage gates, add GitHub Actions CI matrix across Node 18/20/22/24 and Linux/macOS/Windows, add .editorconfig template item, and export ambient TypeScript typings.
|
|
43
|
+
- **Domain Filter Verification:** Verified 100% generic; purged of all project-specific business entities and models.
|
|
44
|
+
|
|
45
|
+
### [2026-09-25] Resolve macOS/Windows Git Case-Collision and Cross-Version CI Matrix Coverage
|
|
46
|
+
- **Category:** Infrastructure & CI
|
|
47
|
+
- **Target File(s):** .gitignore, lib/scaffold.js, scripts/test_coverage.js, validate_agentic_configs.sh
|
|
48
|
+
- **Rationale:** Ensure flawless cross-platform and multi-version Node execution across macOS, Windows, and Linux on Node 18, 20, 22, 24.
|
|
49
|
+
- **Description:** Untracked agents.md from Git to prevent cyclic symlink overwrite on case-insensitive filesystems; hardened ensureSymlink with isSameCaseInsensitiveFile check; added cross-version test coverage runner script; updated npm test runner to use native discovery.
|
|
50
|
+
- **Domain Filter Verification:** Verified 100% generic; purged of all project-specific business entities and models.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Multi-Tenant Architecture & Agentic Engineering Starter Template
|
|
3
|
+
* Programmatic API Definitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ScaffoldOptions {
|
|
7
|
+
/** Target directory path where template should be scaffolded (default: process.cwd()) */
|
|
8
|
+
targetDir?: string;
|
|
9
|
+
/** Force overwrite if target directory is non-empty (default: false) */
|
|
10
|
+
force?: boolean;
|
|
11
|
+
/** Skip git repository initialization (default: false) */
|
|
12
|
+
noGit?: boolean;
|
|
13
|
+
/** Custom template root directory (default: azcodr root) */
|
|
14
|
+
templateDir?: string;
|
|
15
|
+
/** Simulate scaffolding without writing files or running git (default: false) */
|
|
16
|
+
dryRun?: boolean;
|
|
17
|
+
/** Suppress console output messages (default: false) */
|
|
18
|
+
silent?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ScaffoldResult {
|
|
22
|
+
/** Whether the scaffolding succeeded */
|
|
23
|
+
success: boolean;
|
|
24
|
+
/** Absolute resolved path of the target directory */
|
|
25
|
+
targetDir: string;
|
|
26
|
+
/** Whether git init was successfully executed */
|
|
27
|
+
gitInitialized: boolean;
|
|
28
|
+
/** Whether execution ran in dry-run simulation mode */
|
|
29
|
+
dryRun: boolean;
|
|
30
|
+
/** List of files, directories, and symlinks created or simulated */
|
|
31
|
+
actions: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LogChangeOptions {
|
|
35
|
+
/** Short title describing the architectural change */
|
|
36
|
+
title: string;
|
|
37
|
+
/** Category of change (Architecture | Rule | Skill | Infrastructure | CLI | Knowledge Hub) */
|
|
38
|
+
category?: string;
|
|
39
|
+
/** Target file(s) affected by the change */
|
|
40
|
+
targetFiles?: string;
|
|
41
|
+
/** Architectural rationale for upstream template incorporation */
|
|
42
|
+
rationale?: string;
|
|
43
|
+
/** Detailed description of the change */
|
|
44
|
+
description?: string;
|
|
45
|
+
/** Working directory containing changes.md (default: process.cwd()) */
|
|
46
|
+
targetDir?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LogChangeResult {
|
|
50
|
+
/** Whether the log entry was successfully recorded */
|
|
51
|
+
success: boolean;
|
|
52
|
+
/** Absolute path to changes.md */
|
|
53
|
+
filePath: string;
|
|
54
|
+
/** Markdown entry text that was appended */
|
|
55
|
+
entry: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ValidateTargetOptions {
|
|
59
|
+
/** Custom template root directory */
|
|
60
|
+
templateDir?: string;
|
|
61
|
+
/** Force allow non-empty directory */
|
|
62
|
+
force?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface InitGitOptions {
|
|
66
|
+
/** If true, skips git initialization */
|
|
67
|
+
noGit?: boolean;
|
|
68
|
+
/** If true, simulates git initialization without executing */
|
|
69
|
+
dryRun?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CopyTemplateOptions {
|
|
73
|
+
/** If true, simulates copy without writing to disk */
|
|
74
|
+
dryRun?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* High-level orchestration function to scaffold the azcodr workspace into targetDir.
|
|
79
|
+
*/
|
|
80
|
+
export function scaffold(options?: ScaffoldOptions): ScaffoldResult;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Appends a standardized upstream change entry to changes.md.
|
|
84
|
+
*/
|
|
85
|
+
export function logChange(options: LogChangeOptions): LogChangeResult;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validates the target directory to ensure it is suitable for scaffolding.
|
|
89
|
+
*/
|
|
90
|
+
export function validateTarget(targetDir: string, options?: ValidateTargetOptions): void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Copies template items into target directory and sets up symlinks and permissions.
|
|
94
|
+
*/
|
|
95
|
+
export function copyTemplate(
|
|
96
|
+
targetDir: string,
|
|
97
|
+
templateDir?: string,
|
|
98
|
+
options?: CopyTemplateOptions
|
|
99
|
+
): string[];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Safely creates or updates a symbolic link, falling back to a file copy if symlinks are unsupported.
|
|
103
|
+
*/
|
|
104
|
+
export function ensureSymlink(
|
|
105
|
+
targetDir: string,
|
|
106
|
+
linkName: string,
|
|
107
|
+
targetFileName: string,
|
|
108
|
+
dryRun?: boolean
|
|
109
|
+
): boolean;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Detects whether linkName and targetFileName refer to the same entry on a case-insensitive filesystem.
|
|
113
|
+
*/
|
|
114
|
+
export function isSameCaseInsensitiveFile(
|
|
115
|
+
targetDir: string,
|
|
116
|
+
linkName: string,
|
|
117
|
+
targetFileName: string
|
|
118
|
+
): boolean;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ensures all bash scripts in skill directories have executable permissions (0o755).
|
|
122
|
+
*/
|
|
123
|
+
export function makeScriptsExecutable(targetDir: string, dryRun?: boolean): string[];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Initializes a git repository in the target directory if not already inside one.
|
|
127
|
+
*/
|
|
128
|
+
export function initGit(targetDir: string, options?: InitGitOptions): boolean;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns the root path to the azcodr template files.
|
|
132
|
+
*/
|
|
133
|
+
export function getTemplateDir(): string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Array of essential template files and directories copied during scaffolding.
|
|
137
|
+
*/
|
|
138
|
+
export const TEMPLATE_ITEMS: readonly string[];
|
|
139
|
+
|
|
140
|
+
declare const defaultExport: {
|
|
141
|
+
scaffold: typeof scaffold;
|
|
142
|
+
logChange: typeof logChange;
|
|
143
|
+
validateTarget: typeof validateTarget;
|
|
144
|
+
copyTemplate: typeof copyTemplate;
|
|
145
|
+
ensureSymlink: typeof ensureSymlink;
|
|
146
|
+
isSameCaseInsensitiveFile: typeof isSameCaseInsensitiveFile;
|
|
147
|
+
makeScriptsExecutable: typeof makeScriptsExecutable;
|
|
148
|
+
initGit: typeof initGit;
|
|
149
|
+
getTemplateDir: typeof getTemplateDir;
|
|
150
|
+
TEMPLATE_ITEMS: typeof TEMPLATE_ITEMS;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export default defaultExport;
|
package/lib/scaffold.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
-
const
|
|
5
|
+
const cp = require('node:child_process');
|
|
6
6
|
|
|
7
7
|
const TEMPLATE_ITEMS = [
|
|
8
8
|
'AGENTS.md',
|
|
@@ -11,7 +11,8 @@ const TEMPLATE_ITEMS = [
|
|
|
11
11
|
'README.md',
|
|
12
12
|
'docs',
|
|
13
13
|
'.agents',
|
|
14
|
-
'.gitignore'
|
|
14
|
+
'.gitignore',
|
|
15
|
+
'.editorconfig'
|
|
15
16
|
];
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -25,7 +26,7 @@ function getTemplateDir() {
|
|
|
25
26
|
* Validates the target directory to ensure it is suitable for scaffolding.
|
|
26
27
|
*/
|
|
27
28
|
function validateTarget(targetDir, options = {}) {
|
|
28
|
-
const { templateDir = getTemplateDir(), force = false } = options;
|
|
29
|
+
const { templateDir = getTemplateDir(), force = false, dryRun = false } = options;
|
|
29
30
|
const resolvedTarget = path.resolve(targetDir);
|
|
30
31
|
const resolvedTemplate = path.resolve(templateDir);
|
|
31
32
|
|
|
@@ -34,7 +35,9 @@ function validateTarget(targetDir, options = {}) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
if (!fs.existsSync(resolvedTarget)) {
|
|
37
|
-
|
|
38
|
+
if (!dryRun) {
|
|
39
|
+
fs.mkdirSync(resolvedTarget, { recursive: true });
|
|
40
|
+
}
|
|
38
41
|
return;
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -47,10 +50,35 @@ function validateTarget(targetDir, options = {}) {
|
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Detects whether linkName and targetFileName refer to the same entry on a case-insensitive filesystem.
|
|
55
|
+
*/
|
|
56
|
+
function isSameCaseInsensitiveFile(targetDir, linkName, targetFileName) {
|
|
57
|
+
if (linkName.toLowerCase() !== targetFileName.toLowerCase()) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const targetPath = path.join(targetDir, targetFileName);
|
|
61
|
+
const linkPath = path.join(targetDir, linkName);
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(targetPath) && fs.existsSync(linkPath)) {
|
|
64
|
+
return !fs.lstatSync(linkPath).isSymbolicLink();
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
/**
|
|
51
73
|
* Safely creates or updates a symbolic link, falling back to a file copy if symlinks are unsupported.
|
|
52
74
|
*/
|
|
53
|
-
function ensureSymlink(targetDir, linkName, targetFileName) {
|
|
75
|
+
function ensureSymlink(targetDir, linkName, targetFileName, dryRun = false) {
|
|
76
|
+
if (dryRun) return true;
|
|
77
|
+
|
|
78
|
+
if (isSameCaseInsensitiveFile(targetDir, linkName, targetFileName)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
const linkPath = path.join(targetDir, linkName);
|
|
55
83
|
try {
|
|
56
84
|
const stat = fs.lstatSync(linkPath);
|
|
@@ -63,21 +91,24 @@ function ensureSymlink(targetDir, linkName, targetFileName) {
|
|
|
63
91
|
|
|
64
92
|
try {
|
|
65
93
|
fs.symlinkSync(targetFileName, linkPath);
|
|
94
|
+
return true;
|
|
66
95
|
} catch {
|
|
67
96
|
// Fallback if environment (e.g., certain Windows configs) prevents symlink creation
|
|
68
97
|
const sourceFile = path.join(targetDir, targetFileName);
|
|
69
98
|
if (fs.existsSync(sourceFile)) {
|
|
70
99
|
fs.copyFileSync(sourceFile, linkPath);
|
|
71
100
|
}
|
|
101
|
+
return true;
|
|
72
102
|
}
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
/**
|
|
76
106
|
* Ensures all bash scripts in skill directories have executable permissions (0o755).
|
|
77
107
|
*/
|
|
78
|
-
function makeScriptsExecutable(targetDir) {
|
|
108
|
+
function makeScriptsExecutable(targetDir, dryRun = false) {
|
|
109
|
+
const modified = [];
|
|
79
110
|
const skillsDir = path.join(targetDir, '.agents', 'skills');
|
|
80
|
-
if (!fs.existsSync(skillsDir)) return;
|
|
111
|
+
if (!fs.existsSync(skillsDir)) return modified;
|
|
81
112
|
|
|
82
113
|
const skills = fs.readdirSync(skillsDir);
|
|
83
114
|
for (const skill of skills) {
|
|
@@ -87,25 +118,31 @@ function makeScriptsExecutable(targetDir) {
|
|
|
87
118
|
for (const file of files) {
|
|
88
119
|
if (file.endsWith('.sh')) {
|
|
89
120
|
const filePath = path.join(scriptsDir, file);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
modified.push(filePath);
|
|
122
|
+
if (!dryRun) {
|
|
123
|
+
try {
|
|
124
|
+
fs.chmodSync(filePath, 0o755);
|
|
125
|
+
} catch {
|
|
126
|
+
// Non-critical if filesystem does not support POSIX permissions
|
|
127
|
+
}
|
|
94
128
|
}
|
|
95
129
|
}
|
|
96
130
|
}
|
|
97
131
|
}
|
|
98
132
|
}
|
|
133
|
+
return modified;
|
|
99
134
|
}
|
|
100
135
|
|
|
101
136
|
/**
|
|
102
137
|
* Recursively copies template files into the target directory and sets up symlinks and permissions.
|
|
103
138
|
*/
|
|
104
|
-
function copyTemplate(targetDir, templateDir = getTemplateDir()) {
|
|
139
|
+
function copyTemplate(targetDir, templateDir = getTemplateDir(), options = {}) {
|
|
140
|
+
const { dryRun = false } = options;
|
|
105
141
|
const resolvedTarget = path.resolve(targetDir);
|
|
106
142
|
const resolvedTemplate = path.resolve(templateDir);
|
|
143
|
+
const actions = [];
|
|
107
144
|
|
|
108
|
-
if (!fs.existsSync(resolvedTarget)) {
|
|
145
|
+
if (!dryRun && !fs.existsSync(resolvedTarget)) {
|
|
109
146
|
fs.mkdirSync(resolvedTarget, { recursive: true });
|
|
110
147
|
}
|
|
111
148
|
|
|
@@ -114,29 +151,46 @@ function copyTemplate(targetDir, templateDir = getTemplateDir()) {
|
|
|
114
151
|
if (!fs.existsSync(srcPath)) continue;
|
|
115
152
|
|
|
116
153
|
const destPath = path.join(resolvedTarget, item);
|
|
117
|
-
|
|
154
|
+
actions.push(`copy: ${item} -> ${destPath}`);
|
|
155
|
+
|
|
156
|
+
if (!dryRun) {
|
|
157
|
+
fs.cpSync(srcPath, destPath, { recursive: true, force: true, dereference: false });
|
|
158
|
+
}
|
|
118
159
|
}
|
|
119
160
|
|
|
120
161
|
// Ensure harness parity symlinks per AGENTS.md mandate
|
|
121
|
-
|
|
122
|
-
ensureSymlink(resolvedTarget, '
|
|
162
|
+
actions.push(`symlink: CLAUDE.md -> AGENTS.md`);
|
|
163
|
+
ensureSymlink(resolvedTarget, 'CLAUDE.md', 'AGENTS.md', dryRun);
|
|
164
|
+
|
|
165
|
+
actions.push(`symlink: agents.md -> AGENTS.md`);
|
|
166
|
+
ensureSymlink(resolvedTarget, 'agents.md', 'AGENTS.md', dryRun);
|
|
123
167
|
|
|
124
168
|
// Ensure scripts are executable
|
|
125
|
-
|
|
169
|
+
const inspectDir = dryRun ? resolvedTemplate : resolvedTarget;
|
|
170
|
+
const scripts = makeScriptsExecutable(inspectDir, dryRun);
|
|
171
|
+
for (const script of scripts) {
|
|
172
|
+
actions.push(`chmod: +x ${path.relative(inspectDir, script)}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return actions;
|
|
126
176
|
}
|
|
127
177
|
|
|
128
178
|
/**
|
|
129
179
|
* Initializes a git repository in the target directory if not already inside one.
|
|
130
180
|
*/
|
|
131
181
|
function initGit(targetDir, options = {}) {
|
|
132
|
-
const { noGit = false } = options;
|
|
182
|
+
const { noGit = false, dryRun = false } = options;
|
|
133
183
|
if (noGit) return false;
|
|
134
184
|
|
|
135
185
|
const gitDir = path.join(targetDir, '.git');
|
|
136
186
|
if (fs.existsSync(gitDir)) return false;
|
|
137
187
|
|
|
188
|
+
if (dryRun) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
138
192
|
try {
|
|
139
|
-
execSync('git init -q', { cwd: targetDir, stdio: 'ignore' });
|
|
193
|
+
cp.execSync('git init -q', { cwd: targetDir, stdio: 'ignore' });
|
|
140
194
|
return true;
|
|
141
195
|
} catch {
|
|
142
196
|
return false;
|
|
@@ -151,18 +205,25 @@ function scaffold(options = {}) {
|
|
|
151
205
|
targetDir = process.cwd(),
|
|
152
206
|
force = false,
|
|
153
207
|
noGit = false,
|
|
154
|
-
templateDir = getTemplateDir()
|
|
208
|
+
templateDir = getTemplateDir(),
|
|
209
|
+
dryRun = false,
|
|
210
|
+
silent = false
|
|
155
211
|
} = options;
|
|
156
212
|
|
|
157
213
|
const resolvedTarget = path.resolve(targetDir);
|
|
158
|
-
validateTarget(resolvedTarget, { templateDir, force });
|
|
159
|
-
copyTemplate(resolvedTarget, templateDir);
|
|
160
|
-
const gitInitialized = initGit(resolvedTarget, { noGit });
|
|
214
|
+
validateTarget(resolvedTarget, { templateDir, force, dryRun });
|
|
215
|
+
const actions = copyTemplate(resolvedTarget, templateDir, { dryRun });
|
|
216
|
+
const gitInitialized = initGit(resolvedTarget, { noGit, dryRun });
|
|
217
|
+
if (gitInitialized) {
|
|
218
|
+
actions.push('git: initialize repository');
|
|
219
|
+
}
|
|
161
220
|
|
|
162
221
|
return {
|
|
163
222
|
success: true,
|
|
164
223
|
targetDir: resolvedTarget,
|
|
165
|
-
gitInitialized
|
|
224
|
+
gitInitialized,
|
|
225
|
+
dryRun,
|
|
226
|
+
actions
|
|
166
227
|
};
|
|
167
228
|
}
|
|
168
229
|
|
|
@@ -216,6 +277,7 @@ module.exports = {
|
|
|
216
277
|
validateTarget,
|
|
217
278
|
copyTemplate,
|
|
218
279
|
ensureSymlink,
|
|
280
|
+
isSameCaseInsensitiveFile,
|
|
219
281
|
makeScriptsExecutable,
|
|
220
282
|
initGit,
|
|
221
283
|
getTemplateDir,
|
package/package.json
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azcodr",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Enterprise Multi-Tenant Architecture & Agentic Engineering Starter Template",
|
|
5
5
|
"bin": {
|
|
6
6
|
"azcodr": "bin/azcodr.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "./lib/index.js",
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./lib/index.d.ts",
|
|
13
|
+
"default": "./lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
9
17
|
"files": [
|
|
10
18
|
"bin",
|
|
11
19
|
"lib",
|
|
@@ -16,6 +24,7 @@
|
|
|
16
24
|
"docs",
|
|
17
25
|
".agents",
|
|
18
26
|
".gitignore",
|
|
27
|
+
".editorconfig",
|
|
19
28
|
"LICENSE"
|
|
20
29
|
],
|
|
21
30
|
"keywords": [
|
|
@@ -42,9 +51,10 @@
|
|
|
42
51
|
"node": ">=18.0.0"
|
|
43
52
|
},
|
|
44
53
|
"scripts": {
|
|
45
|
-
"test": "node --test
|
|
46
|
-
"test:coverage": "node
|
|
54
|
+
"test": "node --test",
|
|
55
|
+
"test:coverage": "node scripts/test_coverage.js",
|
|
56
|
+
"lint": "node --check bin/azcodr.js lib/index.js lib/scaffold.js tests/cli.test.js tests/scaffold.test.js scripts/test_coverage.js",
|
|
47
57
|
"validate": "bash .agents/skills/agentic-architect/scripts/validate_agentic_configs.sh",
|
|
48
|
-
"prepublishOnly": "npm test && npm run validate"
|
|
58
|
+
"prepublishOnly": "npm run test:coverage && npm run validate"
|
|
49
59
|
}
|
|
50
60
|
}
|