azcodr 2.0.0 → 2.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.
Files changed (71) hide show
  1. package/.agents/hooks.json +3 -3
  2. package/.github/workflows/publish.yml +0 -4
  3. package/bin/azcodr.js +9 -4
  4. package/lib/cli-parse.d.ts +32 -0
  5. package/lib/cli-parse.d.ts.map +1 -0
  6. package/lib/cli-parse.js +45 -41
  7. package/lib/cli-parse.js.map +1 -0
  8. package/lib/cli-target.d.ts +66 -0
  9. package/lib/cli-target.d.ts.map +1 -0
  10. package/lib/cli-target.js +89 -96
  11. package/lib/cli-target.js.map +1 -0
  12. package/lib/cli.d.ts +40 -0
  13. package/lib/cli.d.ts.map +1 -0
  14. package/lib/cli.js +122 -136
  15. package/lib/cli.js.map +1 -0
  16. package/lib/errors.d.ts +39 -0
  17. package/lib/errors.d.ts.map +1 -0
  18. package/lib/errors.js +16 -18
  19. package/lib/errors.js.map +1 -0
  20. package/lib/git.d.ts +15 -0
  21. package/lib/git.d.ts.map +1 -0
  22. package/lib/git.js +25 -22
  23. package/lib/git.js.map +1 -0
  24. package/lib/guards.d.ts +35 -0
  25. package/lib/guards.d.ts.map +1 -0
  26. package/lib/guards.js +62 -63
  27. package/lib/guards.js.map +1 -0
  28. package/lib/index.d.ts +6 -199
  29. package/lib/index.d.ts.map +1 -0
  30. package/lib/index.js +4 -5
  31. package/lib/index.js.map +1 -0
  32. package/lib/links.d.ts +28 -0
  33. package/lib/links.d.ts.map +1 -0
  34. package/lib/links.js +105 -99
  35. package/lib/links.js.map +1 -0
  36. package/lib/permissions.d.ts +9 -0
  37. package/lib/permissions.d.ts.map +1 -0
  38. package/lib/permissions.js +36 -36
  39. package/lib/permissions.js.map +1 -0
  40. package/lib/repo.d.ts +20 -0
  41. package/lib/repo.d.ts.map +1 -0
  42. package/lib/repo.js +73 -71
  43. package/lib/repo.js.map +1 -0
  44. package/lib/scaffold.d.ts +80 -0
  45. package/lib/scaffold.d.ts.map +1 -0
  46. package/lib/scaffold.js +159 -196
  47. package/lib/scaffold.js.map +1 -0
  48. package/memory.md +16 -0
  49. package/package.json +13 -3
  50. package/scripts/test_coverage.js +2 -2
  51. package/scripts/validate/adr.js +11 -7
  52. package/scripts/validate/io.js +7 -9
  53. package/scripts/validate/links.js +6 -7
  54. package/scripts/validate/parity.js +5 -7
  55. package/scripts/validate/root.js +6 -7
  56. package/scripts/validate/rules.js +7 -9
  57. package/scripts/validate/skills.js +8 -10
  58. package/scripts/validate/text.js +3 -5
  59. package/scripts/validate-cli.js +1 -2
  60. package/scripts/validate.js +37 -19
  61. package/src/cli-parse.ts +77 -0
  62. package/src/cli-target.ts +167 -0
  63. package/src/cli.ts +240 -0
  64. package/src/errors.ts +35 -0
  65. package/src/git.ts +34 -0
  66. package/src/guards.ts +101 -0
  67. package/src/index.ts +39 -0
  68. package/src/links.ts +139 -0
  69. package/src/permissions.ts +42 -0
  70. package/src/repo.ts +94 -0
  71. package/src/scaffold.ts +273 -0
@@ -1,6 +1,4 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
1
+ import fs from 'node:fs';
4
2
 
5
3
  /**
6
4
  * Harness-parity verdicts, kept free of filesystem I/O.
@@ -15,7 +13,7 @@ const fs = require('node:fs');
15
13
  * The three target spellings a harness-parity file may legitimately use.
16
14
  * Pure so it can be unit-tested without touching the filesystem.
17
15
  */
18
- function isValidAgentsTarget(target, agentsFile) {
16
+ export function isValidAgentsTarget(target, agentsFile) {
19
17
  const normalized = target.replace(/\\/g, '/');
20
18
  const rootNormalized = agentsFile.replace(/\\/g, '/');
21
19
  return normalized === 'AGENTS.md' || normalized === './AGENTS.md' || normalized === rootNormalized;
@@ -82,7 +80,7 @@ function fileVerdict(ctx) {
82
80
  /**
83
81
  * Decides whether a harness-parity entry is healthy, drifted, or outright wrong.
84
82
  */
85
- function evaluateParityTarget(ctx) {
83
+ export function evaluateParityTarget(ctx) {
86
84
  if (ctx.isSymlink) return symlinkVerdict(ctx);
87
85
  if (ctx.isFile) return fileVerdict(ctx);
88
86
  // Anything lstat saw that is neither a symlink nor a regular file (a
@@ -97,7 +95,7 @@ function evaluateParityTarget(ctx) {
97
95
  *
98
96
  * @returns {{created: boolean, strategy: 'symlink'|'pointer'|null, reason: string|null}}
99
97
  */
100
- function createLowercaseParityLink(lowerPath, fsImpl = fs) {
98
+ export function createLowercaseParityLink(lowerPath, fsImpl = fs) {
101
99
  try {
102
100
  fsImpl.symlinkSync('AGENTS.md', lowerPath);
103
101
  return { created: true, strategy: 'symlink', reason: null };
@@ -117,7 +115,7 @@ function createLowercaseParityLink(lowerPath, fsImpl = fs) {
117
115
  }
118
116
  }
119
117
 
120
- module.exports = {
118
+ export default {
121
119
  isValidAgentsTarget,
122
120
  evaluateParityTarget,
123
121
  createLowercaseParityLink
@@ -1,9 +1,7 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const { readTextOrFail } = require('./io.js');
6
- const { evaluateParityTarget, createLowercaseParityLink } = require('./parity.js');
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readTextOrFail } from './io.js';
4
+ import { evaluateParityTarget, createLowercaseParityLink } from './parity.js';
7
5
 
8
6
  function reportAgentsLineBudget(agentsText, ctx) {
9
7
  const lines = agentsText.split('\n').length;
@@ -181,4 +179,5 @@ function phaseRootConfig(ctx) {
181
179
  checkGitignore(ctx);
182
180
  }
183
181
 
184
- module.exports = { phaseRootConfig, checkParity, checkAgentsFile };
182
+ export { phaseRootConfig, checkParity, checkAgentsFile };
183
+ export default { phaseRootConfig, checkParity, checkAgentsFile };
@@ -1,11 +1,9 @@
1
- 'use strict';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readTextOrFail, walkMarkdown } from './io.js';
4
+ import { stripFencedCode } from './text.js';
2
5
 
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const { readTextOrFail, walkMarkdown } = require('./io.js');
6
- const { stripFencedCode } = require('./text.js');
7
-
8
- function checkOneRule(fp, name, ctx) {
6
+ export function checkOneRule(fp, name, ctx) {
9
7
  const content = readTextOrFail(fp, `Rule ${name}`, ctx.fail);
10
8
  if (content === null) return;
11
9
  const stat = fs.statSync(fp);
@@ -17,7 +15,7 @@ function checkOneRule(fp, name, ctx) {
17
15
  if (stat.size > 24000) ctx.warn(`Rule ${name} exceeds 24KB token-economy cap (${stat.size} bytes)`);
18
16
  }
19
17
 
20
- function phaseRules(ctx) {
18
+ export function phaseRules(ctx) {
21
19
  ctx.log('');
22
20
  ctx.heading('2. Checking Progressive Disclosure Rules...');
23
21
  const rulesDir = path.join(ctx.workspaceRoot, 'docs', 'rules');
@@ -41,4 +39,4 @@ function phaseRules(ctx) {
41
39
  return count;
42
40
  }
43
41
 
44
- module.exports = { phaseRules, checkOneRule };
42
+ export default { phaseRules, checkOneRule };
@@ -1,10 +1,8 @@
1
- 'use strict';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readTextOrFail, readSkillFolders } from './io.js';
2
4
 
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const { readTextOrFail, readSkillFolders } = require('./io.js');
6
-
7
- function frontMatterBlock(content, skill, fail) {
5
+ export function frontMatterBlock(content, skill, fail) {
8
6
  const lines = content.split('\n');
9
7
  if (lines[0].trim() !== '---') {
10
8
  fail(`Skill '${skill}' missing opening front matter delimiter (---)`);
@@ -26,7 +24,7 @@ function frontMatterBlock(content, skill, fail) {
26
24
  return frontMatter;
27
25
  }
28
26
 
29
- function extractSkillDescription(frontMatter) {
27
+ export function extractSkillDescription(frontMatter) {
30
28
  const descMatch = frontMatter.match(/^description:\s*(.+)$/m);
31
29
  const desc = descMatch ? descMatch[1].trim() : '';
32
30
  if (!/^[>|][-+]?$/.test(desc)) return desc;
@@ -60,7 +58,7 @@ function checkSkillBody(checked, ctx) {
60
58
  }
61
59
  }
62
60
 
63
- function checkOneSkill(skill, skillsDir, ctx) {
61
+ export function checkOneSkill(skill, skillsDir, ctx) {
64
62
  const skillFile = path.join(skillsDir, skill, 'SKILL.md');
65
63
  if (!fs.existsSync(skillFile)) {
66
64
  ctx.fail(`Skill '${skill}' missing SKILL.md`);
@@ -75,7 +73,7 @@ function checkOneSkill(skill, skillsDir, ctx) {
75
73
  checkSkillBody({ content, desc, skill }, ctx);
76
74
  }
77
75
 
78
- function phaseSkills(ctx) {
76
+ export function phaseSkills(ctx) {
79
77
  ctx.log('');
80
78
  ctx.heading('3. Checking Specialized Skills (.agents/skills)...');
81
79
  const skillsDir = path.join(ctx.workspaceRoot, '.agents', 'skills');
@@ -93,4 +91,4 @@ function phaseSkills(ctx) {
93
91
  return count;
94
92
  }
95
93
 
96
- module.exports = { phaseSkills, checkOneSkill, extractSkillDescription, frontMatterBlock };
94
+ export default { phaseSkills, checkOneSkill, extractSkillDescription, frontMatterBlock };
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  /**
4
2
  * Text helpers shared by the validator phases.
5
3
  *
@@ -8,7 +6,7 @@
8
6
  * would mistake documentation ABOUT the contract for the contract itself.
9
7
  */
10
8
 
11
- function stripHtmlComments(text) {
9
+ export function stripHtmlComments(text) {
12
10
  // Remove HTML comments, tolerating unclosed ones. A stray `<!--` from an
13
11
  // editor's "comment selection" must not silently hide every ADR after it.
14
12
  return text
@@ -22,8 +20,8 @@ function stripHtmlComments(text) {
22
20
  * "Core Mandate" lines sit inside a ```md fence satisfies both checks, and a
23
21
  * `#### ADR-001` shown as an example is counted as a real decision.
24
22
  */
25
- function stripFencedCode(text) {
23
+ export function stripFencedCode(text) {
26
24
  return text.replace(/^([ \t]*)(```|~~~)[\s\S]*?^\1\2[ \t]*$/gm, '');
27
25
  }
28
26
 
29
- module.exports = { stripHtmlComments, stripFencedCode };
27
+ export default { stripHtmlComments, stripFencedCode };
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  /**
5
4
  * CLI entry for the agentic-architecture validator.
@@ -8,6 +7,6 @@
8
7
  * `require.main` guard means requiring it never validates or exits, and it
9
8
  * avoids a permanently-unreachable branch that blocks the 100% coverage gate.
10
9
  */
11
- const { main } = require('./validate.js');
10
+ import { main } from './validate.js';
12
11
 
13
12
  main();
@@ -1,17 +1,16 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
- const { createReporter, readTextOrFail } = require('./validate/io.js');
7
- const textHelpers = require('./validate/text.js');
8
- const parityHelpers = require('./validate/parity.js');
9
- const ioHelpers = require('./validate/io.js');
10
- const { parseAdrLedger, checkAdrIndexConsistency, checkGlossaryPopulated } = require('./validate/adr.js');
11
- const { phaseRootConfig } = require('./validate/root.js');
12
- const { phaseRules } = require('./validate/rules.js');
13
- const { phaseSkills } = require('./validate/skills.js');
14
- const { phaseLinks } = require('./validate/links.js');
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { createReporter, readTextOrFail, walkMarkdown } from './validate/io.js';
7
+ import * as textHelpers from './validate/text.js';
8
+ import * as parityHelpers from './validate/parity.js';
9
+ import { parseAdrLedger, checkAdrIndexConsistency, checkGlossaryPopulated } from './validate/adr.js';
10
+ import { phaseRootConfig } from './validate/root.js';
11
+ import { phaseRules } from './validate/rules.js';
12
+ import { phaseSkills } from './validate/skills.js';
13
+ import { phaseLinks } from './validate/links.js';
15
14
 
16
15
  /**
17
16
  * NOTE: this module is a library. It never self-executes, so importing it from
@@ -123,7 +122,26 @@ function main(argv = process.argv.slice(2), exit = process.exit) {
123
122
  return exit(result.errors === 0 ? 0 : 1);
124
123
  }
125
124
 
126
- module.exports = {
125
+ const { evaluateParityTarget, isValidAgentsTarget, createLowercaseParityLink } = parityHelpers;
126
+ const { stripHtmlComments, stripFencedCode } = textHelpers;
127
+
128
+ export {
129
+ runValidation,
130
+ parseArgs,
131
+ createReporter,
132
+ main,
133
+ parseAdrLedger,
134
+ checkAdrIndexConsistency,
135
+ checkGlossaryPopulated,
136
+ evaluateParityTarget,
137
+ isValidAgentsTarget,
138
+ createLowercaseParityLink,
139
+ stripHtmlComments,
140
+ stripFencedCode,
141
+ walkMarkdown
142
+ };
143
+
144
+ export default {
127
145
  runValidation,
128
146
  parseArgs,
129
147
  createReporter,
@@ -131,10 +149,10 @@ module.exports = {
131
149
  parseAdrLedger,
132
150
  checkAdrIndexConsistency,
133
151
  checkGlossaryPopulated,
134
- evaluateParityTarget: parityHelpers.evaluateParityTarget,
135
- isValidAgentsTarget: parityHelpers.isValidAgentsTarget,
136
- createLowercaseParityLink: parityHelpers.createLowercaseParityLink,
137
- stripHtmlComments: textHelpers.stripHtmlComments,
138
- stripFencedCode: textHelpers.stripFencedCode,
139
- walkMarkdown: ioHelpers.walkMarkdown
152
+ evaluateParityTarget,
153
+ isValidAgentsTarget,
154
+ createLowercaseParityLink,
155
+ stripHtmlComments,
156
+ stripFencedCode,
157
+ walkMarkdown
140
158
  };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Argument parsing for the azcodr CLI.
3
+ *
4
+ * Table-driven: every boolean flag is one Map entry, so adding a flag cannot
5
+ * raise the complexity of the parser itself.
6
+ */
7
+
8
+ export interface CliParsedOptions {
9
+ targetDir: string | null;
10
+ force: boolean;
11
+ noGit: boolean;
12
+ dryRun: boolean;
13
+ silent: boolean;
14
+ terminal: 'help' | 'version' | 'unknown-flag' | 'extra-arg' | null;
15
+ message?: string;
16
+ }
17
+
18
+ export type BooleanFlagKey = 'force' | 'noGit' | 'dryRun' | 'silent';
19
+
20
+ export function emptyOptions(): Omit<CliParsedOptions, 'terminal' | 'message'> {
21
+ return { targetDir: null, force: false, noGit: false, dryRun: false, silent: false };
22
+ }
23
+
24
+ export const BOOLEAN_FLAGS = new Map<string, BooleanFlagKey>([
25
+ ['-f', 'force'],
26
+ ['--force', 'force'],
27
+ ['--no-git', 'noGit'],
28
+ ['-d', 'dryRun'],
29
+ ['--dry-run', 'dryRun'],
30
+ ['-s', 'silent'],
31
+ ['--silent', 'silent']
32
+ ]);
33
+
34
+ export interface ArgOutcome {
35
+ terminal: 'help' | 'version' | 'unknown-flag' | 'extra-arg' | null;
36
+ message?: string;
37
+ }
38
+
39
+ export function applyArg(
40
+ state: Omit<CliParsedOptions, 'terminal' | 'message'>,
41
+ arg: string
42
+ ): ArgOutcome {
43
+ if (arg === '-h' || arg === '--help') return { terminal: 'help' };
44
+ if (arg === '-v' || arg === '--version') return { terminal: 'version' };
45
+ const flag = BOOLEAN_FLAGS.get(arg);
46
+ if (flag !== undefined) {
47
+ state[flag] = true;
48
+ return { terminal: null };
49
+ }
50
+ if (arg.startsWith('-')) {
51
+ return {
52
+ terminal: 'unknown-flag',
53
+ message: `Unknown argument '${arg}'. Run 'npx azcodr --help' for available options.`
54
+ };
55
+ }
56
+ if (!state.targetDir) {
57
+ state.targetDir = arg;
58
+ return { terminal: null };
59
+ }
60
+ return {
61
+ terminal: 'extra-arg',
62
+ message: `Unexpected argument '${arg}'. Run 'npx azcodr --help' for available options.`
63
+ };
64
+ }
65
+
66
+ export function parseArgs(rawArgs: readonly string[]): CliParsedOptions {
67
+ const state = emptyOptions();
68
+ for (const arg of rawArgs) {
69
+ const outcome = applyArg(state, arg);
70
+ if (outcome.terminal !== null) {
71
+ return { ...state, terminal: outcome.terminal, message: outcome.message };
72
+ }
73
+ }
74
+ return { ...state, terminal: null, message: undefined };
75
+ }
76
+
77
+ export default { emptyOptions, BOOLEAN_FLAGS, applyArg, parseArgs };
@@ -0,0 +1,167 @@
1
+ import path from 'node:path';
2
+ import readline from 'node:readline';
3
+ import fs from 'node:fs';
4
+ import process from 'node:process';
5
+ import { isProtectedTarget } from './guards.js';
6
+
7
+ export interface AskQuestionOptions {
8
+ input?: NodeJS.ReadableStream;
9
+ output?: NodeJS.WritableStream;
10
+ }
11
+
12
+ export function askQuestion(
13
+ query: string,
14
+ { input = process.stdin, output = process.stdout }: AskQuestionOptions = {}
15
+ ): Promise<string> {
16
+ const rl = readline.createInterface({ input, output });
17
+
18
+ return new Promise((resolve) => {
19
+ let resolved = false;
20
+ rl.question(query, (answer: string) => {
21
+ if (!resolved) {
22
+ resolved = true;
23
+ rl.close();
24
+ resolve(answer.trim());
25
+ }
26
+ });
27
+ rl.on('close', () => {
28
+ if (!resolved) {
29
+ resolved = true;
30
+ resolve('');
31
+ }
32
+ });
33
+ });
34
+ }
35
+
36
+ export interface PromptIo {
37
+ stdin: NodeJS.ReadStream | (NodeJS.ReadableStream & { isTTY?: boolean });
38
+ stdout: NodeJS.WriteStream | NodeJS.WritableStream;
39
+ }
40
+
41
+ export async function promptForTargetDir(io: PromptIo): Promise<string> {
42
+ const { stdin, stdout } = io;
43
+ if (!stdin.isTTY) return '.';
44
+ const answer = await askQuestion('? Where would you like to initialize your project? (./) ', {
45
+ input: stdin,
46
+ output: stdout
47
+ });
48
+ return answer || '.';
49
+ }
50
+
51
+ export function rejectBadTarget(resolvedTarget: string, templateDir: string): string | null {
52
+ if (resolvedTarget === templateDir) {
53
+ return `Cannot scaffold into the template directory itself: ${resolvedTarget}`;
54
+ }
55
+ if (isProtectedTarget(resolvedTarget, { templateDir })) {
56
+ return `Refusing to scaffold into protected directory: ${resolvedTarget}. Choose a project subdirectory instead.`;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ export interface ResolveTargetIo extends PromptIo {
62
+ cwd: string;
63
+ templateDir: string;
64
+ }
65
+
66
+ export interface ResolveTargetResult {
67
+ resolvedTarget: string;
68
+ chosen: string;
69
+ error: string | null;
70
+ }
71
+
72
+ export async function resolveTargetDir(
73
+ targetDir: string | null,
74
+ io: ResolveTargetIo
75
+ ): Promise<ResolveTargetResult> {
76
+ const { cwd, templateDir } = io;
77
+ const chosen = targetDir || (await promptForTargetDir(io));
78
+ const resolvedTarget = path.resolve(cwd, chosen);
79
+ const rejection = rejectBadTarget(resolvedTarget, templateDir);
80
+ if (rejection !== null) return { resolvedTarget, chosen, error: rejection };
81
+ return { resolvedTarget, chosen, error: null };
82
+ }
83
+
84
+ export type TargetDirectoryStatus = 'missing' | 'not-a-directory' | 'empty' | 'non-empty';
85
+
86
+ export interface TargetStatusResult {
87
+ status: TargetDirectoryStatus;
88
+ count: number;
89
+ }
90
+
91
+ export function targetStatus(resolvedTarget: string): TargetStatusResult {
92
+ if (!fs.existsSync(resolvedTarget)) return { status: 'missing', count: 0 };
93
+ if (!fs.statSync(resolvedTarget).isDirectory()) return { status: 'not-a-directory', count: 0 };
94
+ const count = fs.readdirSync(resolvedTarget).length;
95
+ return { status: count === 0 ? 'empty' : 'non-empty', count };
96
+ }
97
+
98
+ export interface ConfirmOverwriteIo extends PromptIo {
99
+ out: (msg: string) => void;
100
+ }
101
+
102
+ export interface ConfirmOverwriteResult {
103
+ confirmed: boolean;
104
+ aborted: boolean;
105
+ }
106
+
107
+ export async function confirmOverwrite(
108
+ targetDir: string,
109
+ count: number,
110
+ io: ConfirmOverwriteIo
111
+ ): Promise<ConfirmOverwriteResult> {
112
+ const { stdin, stdout, out } = io;
113
+ if (!stdin.isTTY) return { confirmed: false, aborted: false };
114
+ const confirm = await askQuestion(
115
+ `⚠️ Target directory '${targetDir}' is not empty (${count} items). Continue? (y/N) `,
116
+ { input: stdin, output: stdout }
117
+ );
118
+ if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
119
+ out('Scaffolding aborted.');
120
+ return { confirmed: false, aborted: true };
121
+ }
122
+ return { confirmed: true, aborted: false };
123
+ }
124
+
125
+ export interface EnsureWritableOptions {
126
+ chosen: string;
127
+ resolvedTarget: string;
128
+ force: boolean;
129
+ io: ConfirmOverwriteIo & { err: (msg: string) => void };
130
+ }
131
+
132
+ export interface EnsureWritableResult {
133
+ force?: boolean;
134
+ exitCode?: number;
135
+ message?: string | null;
136
+ }
137
+
138
+ /**
139
+ * Ensures the target may be written. Returns { force } on success or
140
+ * { exitCode, message? } when the CLI must stop before scaffolding.
141
+ */
142
+ export async function ensureWritableTarget(
143
+ options: EnsureWritableOptions
144
+ ): Promise<EnsureWritableResult> {
145
+ const { chosen, resolvedTarget, force, io } = options;
146
+ const { err } = io;
147
+ const { status, count } = targetStatus(resolvedTarget);
148
+ if (status === 'missing' || status === 'empty' || force) return { force };
149
+ if (status === 'not-a-directory') {
150
+ return { exitCode: 1, message: `Target '${resolvedTarget}' already exists and is not a directory.` };
151
+ }
152
+ const { confirmed, aborted } = await confirmOverwrite(chosen, count, io);
153
+ if (aborted) return { exitCode: 0, message: null };
154
+ if (confirmed) return { force: true };
155
+ err(`Target directory '${resolvedTarget}' is not empty. Use --force to proceed.`);
156
+ return { exitCode: 1, message: null };
157
+ }
158
+
159
+ export default {
160
+ askQuestion,
161
+ promptForTargetDir,
162
+ rejectBadTarget,
163
+ resolveTargetDir,
164
+ targetStatus,
165
+ confirmOverwrite,
166
+ ensureWritableTarget
167
+ };