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.
@@ -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
- if [[ -L "${AGENTS_LOWER}" ]]; then
55
- TARGET=$(readlink "${AGENTS_LOWER}")
56
- if [[ "${TARGET}" == "AGENTS.md" ]]; then
57
- log_pass "agents.md is a valid symlink to AGENTS.md."
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 points to '${TARGET}' instead of '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
@@ -18,3 +18,6 @@ Thumbs.db
18
18
  .env
19
19
  .env.local
20
20
  .env.*.local
21
+
22
+ # Case-insensitive filesystem parity (agents.md is generated/symlinked on Linux, native on macOS/Windows)
23
+ agents.md
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
- const args = process.argv.slice(2);
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
- console.log(pkg.version);
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
- rl.close();
56
- resolve(answer.trim());
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 < args.length; i++) {
69
- const a = args[i];
87
+ for (let i = 1; i < rawArgs.length; i++) {
88
+ const a = rawArgs[i];
70
89
  if (a === '-c' || a === '--category') {
71
- category = args[++i] || category;
90
+ category = rawArgs[++i] || category;
72
91
  } else if (a === '-f' || a === '--files') {
73
- targetFiles = args[++i] || targetFiles;
92
+ targetFiles = rawArgs[++i] || targetFiles;
74
93
  } else if (a === '-r' || a === '--rationale') {
75
- rationale = args[++i] || rationale;
94
+ rationale = rawArgs[++i] || rationale;
76
95
  } else if (a === '-d' || a === '--desc' || a === '--description') {
77
- description = args[++i] || description;
78
- } else if (!a.startsWith('-')) {
79
- if (!title) {
80
- title = a;
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 (process.stdin.isTTY) {
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
- console.error('❌ Error: A title is required to log an upstream change.');
93
- console.error('Usage: npx azcodr change "<title>" [-c Category] [-f Files] [-r Rationale] [-d Description]');
94
- process.exit(1);
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 = logChange({
118
+ const res = logChangeFn({
99
119
  title,
100
120
  category,
101
121
  targetFiles,
102
122
  rationale,
103
123
  description,
104
- targetDir: process.cwd()
124
+ targetDir: cwd
105
125
  });
106
- console.log(`\n✅ Upstream change logged to ${res.filePath}\n`);
107
- process.exit(0);
108
- } catch (err) {
109
- console.error(`\n❌ Failed to log change: ${err.message}\n`);
110
- process.exit(1);
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 main() {
115
- if (args.length > 0 && (args[0] === '-h' || args[0] === '--help')) {
116
- printHelp();
117
- process.exit(0);
118
- }
119
-
120
- if (args.length > 0 && (args[0] === '-v' || args[0] === '--version')) {
121
- printVersion();
122
- process.exit(0);
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 (args.length > 0 && (args[0] === 'change' || args[0] === 'log-change')) {
126
- await handleLogChange();
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 < args.length; i++) {
135
- const arg = args[i];
156
+ for (let i = 0; i < rawArgs.length; i++) {
157
+ const arg = rawArgs[i];
136
158
  if (arg === '-h' || arg === '--help') {
137
- printHelp();
138
- process.exit(0);
159
+ printHelp(out);
160
+ return exit(0);
139
161
  } else if (arg === '-v' || arg === '--version') {
140
- printVersion();
141
- process.exit(0);
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 (!arg.startsWith('-')) {
147
- if (!targetDir) {
148
- targetDir = arg;
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
- console.log('\n🚀 azcodr - Enterprise Multi-Tenant Architecture & Agentic Engineering\n');
180
+ if (!silent) {
181
+ out('\n🚀 azcodr - Enterprise Multi-Tenant Architecture & Agentic Engineering\n');
182
+ }
154
183
 
155
184
  if (!targetDir) {
156
- if (process.stdin.isTTY) {
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(process.cwd(), targetDir);
165
- const templateDir = getTemplateDir();
196
+ const resolvedTarget = path.resolve(cwd, targetDir);
166
197
 
167
198
  if (resolvedTarget === templateDir) {
168
- console.error(`❌ Error: Cannot scaffold into the template directory itself: ${resolvedTarget}`);
169
- process.exit(1);
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 (process.stdin.isTTY) {
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
- console.log('Scaffolding aborted.');
182
- process.exit(0);
217
+ out('Scaffolding aborted.');
218
+ return exit(0);
183
219
  }
184
220
  force = true;
185
221
  } else {
186
- console.error(
187
- `❌ Error: Target directory '${resolvedTarget}' is not empty. Use --force to proceed.`
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
- console.log(`📦 Scaffolding azcodr into: ${resolvedTarget}`);
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 = scaffold({
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
- console.log(' ✅ Progressive disclosure rules copied (docs/rules/)');
205
- console.log(' ✅ Workspace knowledge hub and ADR ledger copied (docs/knowledge/, memory.md)');
206
- console.log(' ✅ Specialized agentic skills copied (.agents/skills/)');
207
- console.log(' ✅ Upstream changes ledger initialized (changes.md)');
208
- console.log(' ✅ Agent directives and harness symlinks established (AGENTS.md, CLAUDE.md, agents.md)');
209
- if (result.gitInitialized) {
210
- console.log(' ✅ Git repository initialized');
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
- console.log('\n🎉 azcodr initialized successfully!\n');
214
- console.log('Next steps:');
215
- if (targetDir !== '.' && targetDir !== './') {
216
- console.log(` 1. cd ${targetDir}`);
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
- console.log(' 2. Open the project in your AI coding assistant (Antigravity, Claude Code, Cursor, OpenHands)');
219
- console.log(' 3. Run /lets-build to start the architectural interview and scaffold your application stack!\n');
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(`\n❌ Scaffolding failed: ${err.message}\n`);
286
+ console.error('Unexpected error:', err);
222
287
  process.exit(1);
223
288
  }
224
289
  }
225
290
 
226
- main().catch((err) => {
227
- console.error('Unexpected error:', err);
228
- process.exit(1);
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 { execSync } = require('node:child_process');
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
- fs.mkdirSync(resolvedTarget, { recursive: true });
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
- try {
91
- fs.chmodSync(filePath, 0o755);
92
- } catch {
93
- // Non-critical if filesystem does not support POSIX permissions
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
- fs.cpSync(srcPath, destPath, { recursive: true, force: true, dereference: false });
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
- ensureSymlink(resolvedTarget, 'CLAUDE.md', 'AGENTS.md');
122
- ensureSymlink(resolvedTarget, 'agents.md', 'AGENTS.md');
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
- makeScriptsExecutable(resolvedTarget);
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.1",
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 tests/**/*.test.js",
46
- "test:coverage": "node --test --experimental-test-coverage tests/**/*.test.js",
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
  }