azcodr 2.0.0 → 2.2.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 (73) hide show
  1. package/.agents/hooks.json +3 -3
  2. package/.github/workflows/ci.yml +1 -0
  3. package/.github/workflows/publish.yml +0 -4
  4. package/AGENTS.md +1 -1
  5. package/bin/azcodr.js +11 -4
  6. package/lib/cli-parse.d.ts +32 -0
  7. package/lib/cli-parse.d.ts.map +1 -0
  8. package/lib/cli-parse.js +45 -41
  9. package/lib/cli-parse.js.map +1 -0
  10. package/lib/cli-target.d.ts +66 -0
  11. package/lib/cli-target.d.ts.map +1 -0
  12. package/lib/cli-target.js +89 -96
  13. package/lib/cli-target.js.map +1 -0
  14. package/lib/cli.d.ts +40 -0
  15. package/lib/cli.d.ts.map +1 -0
  16. package/lib/cli.js +122 -136
  17. package/lib/cli.js.map +1 -0
  18. package/lib/errors.d.ts +54 -0
  19. package/lib/errors.d.ts.map +1 -0
  20. package/lib/errors.js +25 -18
  21. package/lib/errors.js.map +1 -0
  22. package/lib/git.d.ts +23 -0
  23. package/lib/git.d.ts.map +1 -0
  24. package/lib/git.js +33 -22
  25. package/lib/git.js.map +1 -0
  26. package/lib/guards.d.ts +50 -0
  27. package/lib/guards.d.ts.map +1 -0
  28. package/lib/guards.js +75 -64
  29. package/lib/guards.js.map +1 -0
  30. package/lib/index.d.ts +14 -188
  31. package/lib/index.d.ts.map +1 -0
  32. package/lib/index.js +17 -5
  33. package/lib/index.js.map +1 -0
  34. package/lib/links.d.ts +42 -0
  35. package/lib/links.d.ts.map +1 -0
  36. package/lib/links.js +116 -99
  37. package/lib/links.js.map +1 -0
  38. package/lib/permissions.d.ts +13 -0
  39. package/lib/permissions.d.ts.map +1 -0
  40. package/lib/permissions.js +40 -36
  41. package/lib/permissions.js.map +1 -0
  42. package/lib/repo.d.ts +30 -0
  43. package/lib/repo.d.ts.map +1 -0
  44. package/lib/repo.js +80 -71
  45. package/lib/repo.js.map +1 -0
  46. package/lib/scaffold.d.ts +109 -0
  47. package/lib/scaffold.d.ts.map +1 -0
  48. package/lib/scaffold.js +165 -197
  49. package/lib/scaffold.js.map +1 -0
  50. package/memory.md +24 -0
  51. package/package.json +14 -5
  52. package/scripts/validate/adr.js +11 -7
  53. package/scripts/validate/io.js +7 -9
  54. package/scripts/validate/links.js +6 -7
  55. package/scripts/validate/parity.js +5 -7
  56. package/scripts/validate/root.js +6 -7
  57. package/scripts/validate/rules.js +7 -9
  58. package/scripts/validate/skills.js +8 -10
  59. package/scripts/validate/text.js +3 -5
  60. package/scripts/validate-cli.js +1 -2
  61. package/scripts/validate.js +37 -19
  62. package/src/cli-parse.ts +77 -0
  63. package/src/cli-target.ts +167 -0
  64. package/src/cli.ts +240 -0
  65. package/src/errors.ts +50 -0
  66. package/src/git.ts +42 -0
  67. package/src/guards.ts +116 -0
  68. package/src/index.ts +60 -0
  69. package/src/links.ts +153 -0
  70. package/src/permissions.ts +46 -0
  71. package/src/repo.ts +104 -0
  72. package/src/scaffold.ts +282 -0
  73. package/scripts/test_coverage.js +0 -66
@@ -0,0 +1,46 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ function chmodShFile(filePath: string, dryRun: boolean): void {
5
+ if (dryRun) return;
6
+ try {
7
+ fs.chmodSync(filePath, 0o755);
8
+ } catch {
9
+ // Non-critical if filesystem does not support POSIX permissions
10
+ }
11
+ }
12
+
13
+ function chmodShFilesIn(scriptsDir: string, dryRun: boolean): string[] {
14
+ const modified: string[] = [];
15
+ if (!fs.existsSync(scriptsDir) || !fs.statSync(scriptsDir).isDirectory()) return modified;
16
+ const files = fs.readdirSync(scriptsDir);
17
+ for (const file of files) {
18
+ if (!file.endsWith('.sh')) continue;
19
+ const filePath = path.join(scriptsDir, file);
20
+ modified.push(filePath);
21
+ chmodShFile(filePath, dryRun);
22
+ }
23
+ return modified;
24
+ }
25
+
26
+ /**
27
+ * Ensures all bash scripts in agent and skill directories have executable permissions (0o755).
28
+ *
29
+ * @param targetDir - Root directory containing .agents and skills.
30
+ * @param dryRun - When true, simulates permission changes without modifying disk.
31
+ * @returns Array of file paths that were modified or would be modified.
32
+ */
33
+ export function makeScriptsExecutable(targetDir: string, dryRun = false): string[] {
34
+ const agentScriptsDir = path.join(targetDir, '.agents', 'scripts');
35
+ const modified = chmodShFilesIn(agentScriptsDir, dryRun);
36
+ const skillsDir = path.join(targetDir, '.agents', 'skills');
37
+ if (!fs.existsSync(skillsDir)) return modified;
38
+ const skills = fs.readdirSync(skillsDir);
39
+ for (const skill of skills) {
40
+ const scriptsDir = path.join(skillsDir, skill, 'scripts');
41
+ modified.push(...chmodShFilesIn(scriptsDir, dryRun));
42
+ }
43
+ return modified;
44
+ }
45
+
46
+ export default { makeScriptsExecutable };
package/src/repo.ts ADDED
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { runGit } from './git.js';
5
+
6
+ const INITIAL_COMMIT_MESSAGE = 'chore: initial scaffold from azcodr template';
7
+
8
+ /**
9
+ * Options controlling git repository initialization.
10
+ */
11
+ export interface InitGitOptions {
12
+ noGit?: boolean;
13
+ dryRun?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Detects whether targetDir is already inside an existing Git worktree.
18
+ * Socket note: shell access is limited to `git rev-parse` via runGit (no shell).
19
+ *
20
+ * @param targetDir - Directory path to inspect.
21
+ * @returns True if target directory is within a git worktree, false otherwise.
22
+ */
23
+ export function isInsideGitWorkTree(targetDir: string): boolean {
24
+ try {
25
+ const checkDir = fs.existsSync(targetDir) ? targetDir : path.dirname(targetDir);
26
+ const out = runGit(['rev-parse', '--is-inside-work-tree'], {
27
+ cwd: checkDir,
28
+ stdio: ['ignore', 'pipe', 'ignore']
29
+ });
30
+ return String(out).trim() === 'true';
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function renameBranchMain(targetDir: string): void {
37
+ try {
38
+ runGit(['branch', '-m', 'main'], { cwd: targetDir, stdio: 'ignore' });
39
+ } catch {
40
+ // Non-critical if branch rename fails
41
+ }
42
+ }
43
+
44
+ function initRepository(targetDir: string): void {
45
+ try {
46
+ runGit(['init', '-b', 'main', '-q'], { cwd: targetDir, stdio: 'ignore' });
47
+ } catch {
48
+ runGit(['init', '-q'], { cwd: targetDir, stdio: 'ignore' });
49
+ renameBranchMain(targetDir);
50
+ }
51
+ }
52
+
53
+ function commitWithFallbackIdentity(targetDir: string): void {
54
+ const options = { cwd: targetDir, stdio: 'ignore' as const };
55
+ try {
56
+ runGit(['commit', '-q', '-m', INITIAL_COMMIT_MESSAGE], options);
57
+ } catch {
58
+ runGit(['commit', '-q', '-m', INITIAL_COMMIT_MESSAGE], {
59
+ ...options,
60
+ env: {
61
+ ...process.env,
62
+ GIT_AUTHOR_NAME: 'Subodh Khanal',
63
+ GIT_AUTHOR_EMAIL: 'prosubodh@gmail.com',
64
+ GIT_COMMITTER_NAME: 'Subodh Khanal',
65
+ GIT_COMMITTER_EMAIL: 'prosubodh@gmail.com'
66
+ }
67
+ });
68
+ }
69
+ }
70
+
71
+ function initialCommit(targetDir: string): void {
72
+ try {
73
+ runGit(['add', '-A'], { cwd: targetDir, stdio: 'ignore' });
74
+ commitWithFallbackIdentity(targetDir);
75
+ } catch {
76
+ // Non-critical if initial commit fails
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Initializes a git repository in the target directory if not already inside one.
82
+ * Socket note: only allowlisted `git init/branch/add/commit` via execFileSync, cwd scoped.
83
+ *
84
+ * @param targetDir - Directory where git repository should be initialized.
85
+ * @param options - Configuration options for git initialization.
86
+ * @returns True if repository was initialized and committed, false otherwise.
87
+ */
88
+ export function initGit(targetDir: string, options: InitGitOptions = {}): boolean {
89
+ const { noGit = false, dryRun = false } = options;
90
+ if (noGit) return false;
91
+ const gitDir = path.join(targetDir, '.git');
92
+ if (fs.existsSync(gitDir)) return false;
93
+ if (isInsideGitWorkTree(targetDir)) return false;
94
+ if (dryRun) return true;
95
+ try {
96
+ initRepository(targetDir);
97
+ initialCommit(targetDir);
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ export default { isInsideGitWorkTree, initGit };
@@ -0,0 +1,282 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { ScaffoldError, ERROR_CODES } from './errors.js';
6
+ import type { ScaffoldErrorCode, ScaffoldErrorShape } from './errors.js';
7
+ import { runGit } from './git.js';
8
+ import { assertInside, isProtectedTarget } from './guards.js';
9
+ import { ensureSymlink, ensureSymlinkOrPointer, isSameCaseInsensitiveFile } from './links.js';
10
+ import type { EnsureSymlinkOptions } from './links.js';
11
+ import { makeScriptsExecutable } from './permissions.js';
12
+ import { isInsideGitWorkTree, initGit } from './repo.js';
13
+ import type { InitGitOptions } from './repo.js';
14
+
15
+ /**
16
+ * Canonical list of template files and directories copied into a new workspace.
17
+ */
18
+ export const TEMPLATE_ITEMS: readonly string[] = [
19
+ 'AGENTS.md',
20
+ 'memory.md',
21
+ 'README.md',
22
+ 'docs',
23
+ '.agents',
24
+ '.github',
25
+ 'scripts',
26
+ '.gitignore',
27
+ '.editorconfig',
28
+ 'LICENSE'
29
+ ];
30
+
31
+ /**
32
+ * Configuration options for the primary scaffold orchestration function.
33
+ */
34
+ export interface ScaffoldOptions {
35
+ targetDir?: string;
36
+ force?: boolean;
37
+ noGit?: boolean;
38
+ templateDir?: string;
39
+ dryRun?: boolean;
40
+ silent?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Result object returned upon completion of a scaffold operation.
45
+ */
46
+ export interface ScaffoldResult {
47
+ success: boolean;
48
+ targetDir: string;
49
+ gitInitialized: boolean;
50
+ dryRun: boolean;
51
+ actions: string[];
52
+ }
53
+
54
+ /**
55
+ * Options controlling pre-scaffold target directory validation.
56
+ */
57
+ export interface ValidateTargetOptions {
58
+ templateDir?: string;
59
+ force?: boolean;
60
+ dryRun?: boolean;
61
+ allowProtected?: boolean;
62
+ }
63
+
64
+ /**
65
+ * Options controlling template file copying and file generation.
66
+ */
67
+ export interface CopyTemplateOptions {
68
+ dryRun?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Resolves the root filesystem directory of the azcodr template files.
73
+ *
74
+ * @returns Absolute filesystem path to the template root directory.
75
+ */
76
+ export function getTemplateDir(): string {
77
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
78
+ }
79
+
80
+ function rejectTemplateSelf(resolvedTarget: string, resolvedTemplate: string): void {
81
+ if (resolvedTarget !== resolvedTemplate) return;
82
+ throw new ScaffoldError(
83
+ 'E_TARGET_IS_TEMPLATE',
84
+ `${ERROR_CODES.E_TARGET_IS_TEMPLATE}: ${resolvedTarget}`
85
+ );
86
+ }
87
+
88
+ function rejectProtectedTarget(
89
+ resolvedTarget: string,
90
+ templateDir: string,
91
+ allowProtected: boolean
92
+ ): void {
93
+ if (allowProtected) return;
94
+ if (!isProtectedTarget(resolvedTarget, { templateDir })) return;
95
+ throw new ScaffoldError(
96
+ 'E_TARGET_IS_PROTECTED',
97
+ `${ERROR_CODES.E_TARGET_IS_PROTECTED}: ${resolvedTarget}. ` +
98
+ 'Scaffolding there would overwrite personal or system files. Choose a project subdirectory instead.'
99
+ );
100
+ }
101
+
102
+ function rejectNonEmptyTarget(resolvedTarget: string, force: boolean): void {
103
+ const entries = fs.readdirSync(resolvedTarget);
104
+ if (entries.length === 0 || force) return;
105
+ throw new ScaffoldError(
106
+ 'E_TARGET_NOT_EMPTY',
107
+ `${ERROR_CODES.E_TARGET_NOT_EMPTY} (${entries.length} items found: ${resolvedTarget}). ` +
108
+ 'Use --force to overwrite existing files.'
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Validates the target directory to ensure it is suitable for scaffolding.
114
+ *
115
+ * @param targetDir - Target directory path to validate.
116
+ * @param options - Validation options including force, dry-run, and protected overrides.
117
+ * @throws {ScaffoldError} When target is invalid, protected, or non-empty without force.
118
+ */
119
+ export function validateTarget(targetDir: string, options: ValidateTargetOptions = {}): void {
120
+ const { templateDir = getTemplateDir(), force = false, dryRun = false, allowProtected = false } = options;
121
+ const resolvedTarget = path.resolve(targetDir);
122
+ const resolvedTemplate = path.resolve(templateDir);
123
+ rejectTemplateSelf(resolvedTarget, resolvedTemplate);
124
+ rejectProtectedTarget(resolvedTarget, templateDir, allowProtected);
125
+ if (!fs.existsSync(resolvedTarget)) {
126
+ if (!dryRun) fs.mkdirSync(resolvedTarget, { recursive: true });
127
+ return;
128
+ }
129
+ rejectNonEmptyTarget(resolvedTarget, force);
130
+ }
131
+
132
+ function resolveTemplateItem(resolvedTemplate: string, item: string): string {
133
+ let srcPath = path.join(resolvedTemplate, item);
134
+ if (item === '.gitignore' && !fs.existsSync(srcPath)) {
135
+ const npmIgnorePath = path.join(resolvedTemplate, '.npmignore');
136
+ if (fs.existsSync(npmIgnorePath)) srcPath = npmIgnorePath;
137
+ }
138
+ return srcPath;
139
+ }
140
+
141
+ function copyTemplateItems(resolvedTarget: string, resolvedTemplate: string, dryRun: boolean): string[] {
142
+ const actions: string[] = [];
143
+ for (const item of TEMPLATE_ITEMS) {
144
+ const srcPath = resolveTemplateItem(resolvedTemplate, item);
145
+ if (!fs.existsSync(srcPath)) continue;
146
+ const destPath = path.join(resolvedTarget, item);
147
+ assertInside(resolvedTemplate, srcPath);
148
+ assertInside(resolvedTarget, destPath);
149
+ actions.push(`copy: ${item} -> ${destPath}`);
150
+ if (!dryRun) fs.cpSync(srcPath, destPath, { recursive: true, force: true, dereference: false });
151
+ }
152
+ return actions;
153
+ }
154
+
155
+ function ensureHarnessParity(resolvedTarget: string, dryRun: boolean): string[] {
156
+ const links: readonly [string, string][] = [
157
+ ['CLAUDE.md', 'AGENTS.md'],
158
+ ['agents.md', 'AGENTS.md'],
159
+ ['GEMINI.md', 'AGENTS.md'],
160
+ ['.cursorrules', 'AGENTS.md'],
161
+ ['.windsurfrules', 'AGENTS.md']
162
+ ];
163
+ const actions: string[] = [];
164
+ for (const [linkName, targetFileName] of links) {
165
+ actions.push(`symlink: ${linkName} -> ${targetFileName}`);
166
+ ensureSymlink({ targetDir: resolvedTarget, linkName, targetFileName, dryRun });
167
+ }
168
+ return actions;
169
+ }
170
+
171
+ function ensureCopilotParity(resolvedTarget: string, dryRun: boolean): string[] {
172
+ const githubDir = path.join(resolvedTarget, '.github');
173
+ if (!dryRun && !fs.existsSync(githubDir)) fs.mkdirSync(githubDir, { recursive: true });
174
+ ensureSymlinkOrPointer({
175
+ targetDir: githubDir,
176
+ linkName: 'copilot-instructions.md',
177
+ targetFileName: '../AGENTS.md',
178
+ dryRun
179
+ });
180
+ return ['symlink: .github/copilot-instructions.md -> ../AGENTS.md'];
181
+ }
182
+
183
+ function starterPackageJson(resolvedTarget: string): Record<string, unknown> {
184
+ const projectName = path.basename(resolvedTarget) || 'my-project';
185
+ return {
186
+ name: projectName,
187
+ version: '0.1.0',
188
+ type: 'module',
189
+ private: true,
190
+ description: 'Scaffolded with azcodr enterprise architecture template',
191
+ scripts: {
192
+ test: 'node --test',
193
+ 'test:coverage': 'jest --coverage',
194
+ lint: 'echo "No linter configured yet. Run /lets-build to configure toolchain."',
195
+ validate: 'node scripts/validate-cli.js'
196
+ }
197
+ };
198
+ }
199
+
200
+ function ensureStarterPackageJson(resolvedTarget: string, dryRun: boolean): string[] {
201
+ const pkgJsonPath = path.join(resolvedTarget, 'package.json');
202
+ if (fs.existsSync(pkgJsonPath)) return [];
203
+ if (dryRun) return ['create: package.json'];
204
+ const starterPkg = starterPackageJson(resolvedTarget);
205
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(starterPkg, null, 2) + '\n', 'utf-8');
206
+ return ['create: package.json'];
207
+ }
208
+
209
+ function chmodTemplateScripts(inspectDir: string, dryRun: boolean): string[] {
210
+ const actions: string[] = [];
211
+ const scripts = makeScriptsExecutable(inspectDir, dryRun);
212
+ for (const script of scripts) {
213
+ actions.push(`chmod: +x ${path.relative(inspectDir, script)}`);
214
+ }
215
+ return actions;
216
+ }
217
+
218
+ /**
219
+ * Recursively copies template files into the target directory and sets up symlinks and permissions.
220
+ * Socket note: filesystem access is scoped to templateDir -> targetDir only (assertInside).
221
+ *
222
+ * @param targetDir - Destination directory path.
223
+ * @param templateDir - Source template directory path.
224
+ * @param options - Copy options including dry-run flag.
225
+ * @returns Array of recorded scaffolding action descriptions.
226
+ */
227
+ export function copyTemplate(
228
+ targetDir: string,
229
+ templateDir: string = getTemplateDir(),
230
+ options: CopyTemplateOptions = {}
231
+ ): string[] {
232
+ const { dryRun = false } = options;
233
+ const resolvedTarget = path.resolve(targetDir);
234
+ const resolvedTemplate = path.resolve(templateDir);
235
+ if (!dryRun && !fs.existsSync(resolvedTarget)) {
236
+ fs.mkdirSync(resolvedTarget, { recursive: true });
237
+ }
238
+ const actions = copyTemplateItems(resolvedTarget, resolvedTemplate, dryRun);
239
+ actions.push(...ensureHarnessParity(resolvedTarget, dryRun));
240
+ actions.push(...ensureCopilotParity(resolvedTarget, dryRun));
241
+ actions.push(...ensureStarterPackageJson(resolvedTarget, dryRun));
242
+ const inspectDir = dryRun ? resolvedTemplate : resolvedTarget;
243
+ actions.push(...chmodTemplateScripts(inspectDir, dryRun));
244
+ return actions;
245
+ }
246
+
247
+ /**
248
+ * High-level orchestration function to scaffold the azcodr workspace into targetDir.
249
+ *
250
+ * @param options - Scaffolding options.
251
+ * @returns Result object containing success flag, target directory, and action log.
252
+ */
253
+ export function scaffold(options: ScaffoldOptions = {}): ScaffoldResult {
254
+ const {
255
+ targetDir = process.cwd(),
256
+ force = false,
257
+ noGit = false,
258
+ templateDir = getTemplateDir(),
259
+ dryRun = false
260
+ } = options;
261
+ const resolvedTarget = path.resolve(targetDir);
262
+ validateTarget(resolvedTarget, { templateDir, force, dryRun });
263
+ const actions = copyTemplate(resolvedTarget, templateDir, { dryRun });
264
+ const gitInitialized = initGit(resolvedTarget, { noGit, dryRun });
265
+ if (gitInitialized) actions.push('git: initialize repository');
266
+ return { success: true, targetDir: resolvedTarget, gitInitialized, dryRun, actions };
267
+ }
268
+
269
+ export {
270
+ ensureSymlink, ensureSymlinkOrPointer, isSameCaseInsensitiveFile,
271
+ makeScriptsExecutable, isInsideGitWorkTree, initGit, runGit,
272
+ assertInside, ScaffoldError, ERROR_CODES, isProtectedTarget
273
+ };
274
+
275
+ export type { EnsureSymlinkOptions, InitGitOptions, ScaffoldErrorCode, ScaffoldErrorShape };
276
+
277
+ export default {
278
+ scaffold, validateTarget, copyTemplate, ensureSymlink,
279
+ ensureSymlinkOrPointer, isSameCaseInsensitiveFile, makeScriptsExecutable,
280
+ isInsideGitWorkTree, initGit, getTemplateDir, runGit,
281
+ assertInside, TEMPLATE_ITEMS, ScaffoldError, ERROR_CODES, isProtectedTarget
282
+ };
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const { spawnSync } = require('node:child_process');
5
-
6
- // Native threshold flags and include filtering were added in Node v22.8.0.
7
- // Fail closed on older runtimes instead of silently passing without a gate.
8
- const [major, minor] = process.versions.node.split('.').map(Number);
9
- const supportsThresholds = major > 22 || (major === 22 && minor >= 8);
10
- if (!supportsThresholds) {
11
- console.error(
12
- `test:coverage requires Node >=22.8.0 for threshold enforcement (current: ${process.versions.node}). ` +
13
- `Upgrade Node, or invoke the runner directly: node --test --experimental-test-coverage.`
14
- );
15
- process.exit(1);
16
- }
17
-
18
- const args = [
19
- '--test',
20
- '--experimental-test-coverage',
21
- '--test-coverage-include=bin/**',
22
- '--test-coverage-include=lib/**',
23
- // Explicit file, not scripts/**: validate-cli.js is a two-line process entry
24
- // that only runs when invoked directly, so it cannot be covered in-process.
25
- // Every other script IS gated.
26
- '--test-coverage-include=scripts/validate.js',
27
- '--test-coverage-include=scripts/validate/io.js',
28
- '--test-coverage-include=scripts/validate/text.js',
29
- '--test-coverage-include=scripts/validate/parity.js',
30
- '--test-coverage-include=scripts/validate/adr.js',
31
- '--test-coverage-include=scripts/validate/root.js',
32
- '--test-coverage-include=scripts/validate/rules.js',
33
- '--test-coverage-include=scripts/validate/skills.js',
34
- '--test-coverage-include=scripts/validate/links.js',
35
- '--test-coverage-include=scripts/test_coverage.js',
36
- // bin/, lib/ and test_coverage.js hold no environment-dependent defensive
37
- // branches and are held at 100%. validate.js is a filesystem auditor: it
38
- // contains fault handlers for conditions this host cannot produce (creating
39
- // symlinks requires Developer Mode; a RangeError needs a tree thousands of
40
- // levels deep). Those are exercised by tests/validate-faults.test.js where
41
- // the fault can be injected, so the gate here is deliberately lower rather
42
- // than silently passing.
43
- '--test-coverage-branches=98',
44
- '--test-coverage-functions=97',
45
- '--test-coverage-lines=100'
46
- ];
47
-
48
- const result = spawnSync(process.execPath, args, {
49
- stdio: 'inherit',
50
- env: process.env
51
- });
52
-
53
- if (result.error) {
54
- console.error(result.error);
55
- process.exit(1);
56
- }
57
-
58
- // A null status means the child was killed by a signal (OOM, SIGKILL from a
59
- // runner timeout). `status ?? 0` reported success for a crashed runner, which
60
- // is how a green build can carry zero coverage data.
61
- if (result.signal) {
62
- console.error(`test runner terminated by signal ${result.signal}; coverage gate could not be evaluated.`);
63
- process.exit(1);
64
- }
65
-
66
- process.exit(result.status ?? 1);