@laitszkin/apollo-toolkit 4.1.4 → 5.0.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 (205) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/apollo-toolkit.ts +4 -0
  3. package/dist/bin/apollo-toolkit.js +4 -0
  4. package/package.json +4 -2
  5. package/packages/cli/dist/help-text-builder.d.ts +23 -0
  6. package/packages/cli/dist/help-text-builder.js +166 -0
  7. package/packages/cli/dist/index.d.ts +6 -17
  8. package/packages/cli/dist/index.js +52 -246
  9. package/packages/cli/dist/installer.d.ts +1 -0
  10. package/packages/cli/dist/installer.js +20 -7
  11. package/packages/cli/dist/parsers/install-parser.d.ts +15 -0
  12. package/packages/cli/dist/parsers/install-parser.js +87 -0
  13. package/packages/cli/dist/parsers/parser-utils.d.ts +9 -0
  14. package/packages/cli/dist/parsers/parser-utils.js +16 -0
  15. package/packages/cli/dist/parsers/tool-parser.d.ts +16 -0
  16. package/packages/cli/dist/parsers/tool-parser.js +58 -0
  17. package/packages/cli/dist/parsers/types.d.ts +50 -0
  18. package/packages/cli/dist/parsers/types.js +1 -0
  19. package/packages/cli/dist/parsers/uninstall-parser.d.ts +15 -0
  20. package/packages/cli/dist/parsers/uninstall-parser.js +67 -0
  21. package/packages/cli/dist/tool-registration.d.ts +2 -0
  22. package/packages/cli/dist/tool-registration.js +2 -0
  23. package/packages/cli/dist/tsconfig.tsbuildinfo +1 -1
  24. package/packages/cli/dist/types.d.ts +3 -1
  25. package/packages/cli/dist/updater.js +11 -5
  26. package/packages/cli/help-text-builder.ts +180 -0
  27. package/packages/cli/index.ts +59 -251
  28. package/packages/cli/installer.ts +19 -7
  29. package/packages/cli/package.json +6 -3
  30. package/packages/cli/parsers/install-parser.ts +94 -0
  31. package/packages/cli/parsers/parser-utils.ts +17 -0
  32. package/packages/cli/parsers/tool-parser.ts +65 -0
  33. package/packages/cli/parsers/types.ts +56 -0
  34. package/packages/cli/parsers/uninstall-parser.ts +75 -0
  35. package/packages/cli/tool-registration.ts +3 -0
  36. package/packages/cli/types.ts +6 -1
  37. package/packages/cli/updater.ts +11 -5
  38. package/packages/tool-registry/dist/registry.js +3 -4
  39. package/packages/tool-registry/dist/tsconfig.tsbuildinfo +1 -1
  40. package/packages/tool-registry/dist/types.d.ts +2 -9
  41. package/packages/tool-registry/package.json +3 -3
  42. package/packages/tool-registry/registry.ts +3 -4
  43. package/packages/tool-registry/tsconfig.json +6 -2
  44. package/packages/tool-registry/types.ts +3 -9
  45. package/packages/tool-utils/app-error.ts +97 -0
  46. package/packages/tool-utils/dist/app-error.d.ts +49 -0
  47. package/packages/tool-utils/dist/app-error.js +80 -0
  48. package/packages/tool-utils/dist/index.d.ts +5 -0
  49. package/packages/tool-utils/dist/index.js +3 -0
  50. package/packages/tool-utils/dist/platform-adapter.d.ts +48 -0
  51. package/packages/tool-utils/dist/platform-adapter.js +73 -0
  52. package/packages/tool-utils/dist/schema.d.ts +68 -0
  53. package/packages/tool-utils/dist/schema.js +67 -0
  54. package/packages/tool-utils/dist/tsconfig.tsbuildinfo +1 -1
  55. package/packages/tool-utils/index.ts +12 -0
  56. package/packages/tool-utils/package.json +3 -3
  57. package/packages/tool-utils/platform-adapter.ts +112 -0
  58. package/packages/tool-utils/schema.ts +122 -0
  59. package/packages/tools/architecture/dist/index.d.ts +13 -0
  60. package/packages/tools/architecture/dist/index.js +55 -57
  61. package/packages/tools/architecture/dist/index.test.js +17 -4
  62. package/packages/tools/architecture/dist/tsconfig.tsbuildinfo +1 -1
  63. package/packages/tools/architecture/index.test.ts +27 -14
  64. package/packages/tools/architecture/index.ts +85 -88
  65. package/packages/tools/architecture/package.json +3 -3
  66. package/packages/tools/codegraph/dist/index.js +12 -22
  67. package/packages/tools/codegraph/dist/tsconfig.tsbuildinfo +1 -1
  68. package/packages/tools/codegraph/index.ts +13 -22
  69. package/packages/tools/codegraph/package.json +3 -3
  70. package/packages/tools/create-review-report/dist/index.d.ts +1 -2
  71. package/packages/tools/create-review-report/dist/index.js +46 -77
  72. package/packages/tools/create-review-report/dist/tsconfig.tsbuildinfo +1 -1
  73. package/packages/tools/create-review-report/index.ts +52 -81
  74. package/packages/tools/create-review-report/package.json +3 -3
  75. package/packages/tools/create-specs/dist/index.d.ts +1 -2
  76. package/packages/tools/create-specs/dist/index.js +70 -123
  77. package/packages/tools/create-specs/dist/tsconfig.tsbuildinfo +1 -1
  78. package/packages/tools/create-specs/index.ts +82 -128
  79. package/packages/tools/create-specs/package.json +3 -3
  80. package/packages/tools/docs-to-voice/dist/index.d.ts +1 -2
  81. package/packages/tools/docs-to-voice/dist/index.js +116 -219
  82. package/packages/tools/docs-to-voice/dist/tsconfig.tsbuildinfo +1 -1
  83. package/packages/tools/docs-to-voice/index.ts +265 -385
  84. package/packages/tools/docs-to-voice/package.json +3 -3
  85. package/packages/tools/enforce-video-aspect-ratio/dist/index.d.ts +1 -2
  86. package/packages/tools/enforce-video-aspect-ratio/dist/index.js +77 -154
  87. package/packages/tools/enforce-video-aspect-ratio/dist/tsconfig.tsbuildinfo +1 -1
  88. package/packages/tools/enforce-video-aspect-ratio/index.ts +87 -172
  89. package/packages/tools/enforce-video-aspect-ratio/package.json +3 -3
  90. package/packages/tools/eval/dist/index.js +7 -0
  91. package/packages/tools/eval/dist/tsconfig.tsbuildinfo +1 -1
  92. package/packages/tools/eval/index.ts +8 -0
  93. package/packages/tools/eval/package.json +3 -3
  94. package/packages/tools/extract-conversations/dist/index.d.ts +1 -2
  95. package/packages/tools/extract-conversations/dist/index.js +31 -29
  96. package/packages/tools/extract-conversations/dist/tsconfig.tsbuildinfo +1 -1
  97. package/packages/tools/extract-conversations/index.ts +37 -30
  98. package/packages/tools/extract-conversations/package.json +3 -3
  99. package/packages/tools/extract-pdf-text/dist/index.d.ts +1 -2
  100. package/packages/tools/extract-pdf-text/dist/index.js +44 -65
  101. package/packages/tools/extract-pdf-text/dist/tsconfig.tsbuildinfo +1 -1
  102. package/packages/tools/extract-pdf-text/index.ts +55 -74
  103. package/packages/tools/extract-pdf-text/package.json +3 -3
  104. package/packages/tools/filter-logs/dist/index.js +60 -84
  105. package/packages/tools/filter-logs/dist/tsconfig.tsbuildinfo +1 -1
  106. package/packages/tools/filter-logs/index.ts +67 -97
  107. package/packages/tools/filter-logs/package.json +3 -3
  108. package/packages/tools/find-github-issues/dist/index.d.ts +10 -0
  109. package/packages/tools/find-github-issues/dist/index.js +34 -5
  110. package/packages/tools/find-github-issues/dist/tsconfig.tsbuildinfo +1 -1
  111. package/packages/tools/find-github-issues/index.ts +37 -5
  112. package/packages/tools/find-github-issues/package.json +3 -3
  113. package/packages/tools/generate-storyboard-images/dist/index.d.ts +1 -2
  114. package/packages/tools/generate-storyboard-images/dist/index.js +98 -173
  115. package/packages/tools/generate-storyboard-images/dist/tsconfig.tsbuildinfo +1 -1
  116. package/packages/tools/generate-storyboard-images/index.ts +100 -188
  117. package/packages/tools/generate-storyboard-images/package.json +3 -3
  118. package/packages/tools/open-github-issue/dist/index.d.ts +13 -0
  119. package/packages/tools/open-github-issue/dist/index.js +67 -68
  120. package/packages/tools/open-github-issue/dist/tsconfig.tsbuildinfo +1 -1
  121. package/packages/tools/open-github-issue/index.ts +71 -72
  122. package/packages/tools/open-github-issue/package.json +3 -3
  123. package/packages/tools/read-github-issue/dist/index.d.ts +16 -1
  124. package/packages/tools/read-github-issue/dist/index.js +32 -40
  125. package/packages/tools/read-github-issue/dist/tsconfig.tsbuildinfo +1 -1
  126. package/packages/tools/read-github-issue/index.ts +32 -45
  127. package/packages/tools/read-github-issue/package.json +3 -3
  128. package/packages/tools/render-error-book/dist/index.d.ts +1 -2
  129. package/packages/tools/render-error-book/dist/index.js +74 -95
  130. package/packages/tools/render-error-book/dist/tsconfig.tsbuildinfo +1 -1
  131. package/packages/tools/render-error-book/index.ts +88 -103
  132. package/packages/tools/render-error-book/package.json +3 -3
  133. package/packages/tools/render-katex/dist/index.d.ts +1 -2
  134. package/packages/tools/render-katex/dist/index.js +70 -157
  135. package/packages/tools/render-katex/dist/tsconfig.tsbuildinfo +1 -1
  136. package/packages/tools/render-katex/index.ts +138 -222
  137. package/packages/tools/render-katex/package.json +3 -3
  138. package/packages/tools/review-threads/dist/index.d.ts +12 -0
  139. package/packages/tools/review-threads/dist/index.js +83 -86
  140. package/packages/tools/review-threads/dist/tsconfig.tsbuildinfo +1 -1
  141. package/packages/tools/review-threads/index.ts +90 -84
  142. package/packages/tools/review-threads/package.json +3 -3
  143. package/packages/tools/search-logs/dist/index.js +100 -136
  144. package/packages/tools/search-logs/dist/tsconfig.tsbuildinfo +1 -1
  145. package/packages/tools/search-logs/index.ts +113 -145
  146. package/packages/tools/search-logs/package.json +3 -3
  147. package/packages/tools/sync-memory-index/dist/index.js +34 -28
  148. package/packages/tools/sync-memory-index/dist/tsconfig.tsbuildinfo +1 -1
  149. package/packages/tools/sync-memory-index/index.ts +37 -28
  150. package/packages/tools/sync-memory-index/package.json +3 -3
  151. package/packages/tools/validate-openai-agent-config/dist/index.js +13 -7
  152. package/packages/tools/validate-openai-agent-config/dist/tsconfig.tsbuildinfo +1 -1
  153. package/packages/tools/validate-openai-agent-config/index.ts +13 -7
  154. package/packages/tools/validate-openai-agent-config/package.json +3 -3
  155. package/packages/tools/validate-skill-frontmatter/dist/index.js +12 -6
  156. package/packages/tools/validate-skill-frontmatter/dist/tsconfig.tsbuildinfo +1 -1
  157. package/packages/tools/validate-skill-frontmatter/index.ts +12 -6
  158. package/packages/tools/validate-skill-frontmatter/package.json +3 -3
  159. package/packages/tui/dist/index.d.ts +2 -1
  160. package/packages/tui/dist/index.js +1 -0
  161. package/packages/tui/dist/stdio-adapter.d.ts +36 -0
  162. package/packages/tui/dist/stdio-adapter.js +69 -0
  163. package/packages/tui/dist/terminal.js +3 -1
  164. package/packages/tui/dist/tsconfig.tsbuildinfo +1 -1
  165. package/packages/tui/dist/types.d.ts +17 -0
  166. package/packages/tui/index.ts +2 -1
  167. package/packages/tui/package.json +6 -5
  168. package/packages/tui/stdio-adapter.ts +85 -0
  169. package/packages/tui/terminal.ts +3 -1
  170. package/packages/tui/tsconfig.json +5 -2
  171. package/packages/tui/types.ts +19 -0
  172. package/resources/project-architecture/assets/architecture.css +2 -1
  173. package/resources/project-architecture/atlas/atlas.history.log +1 -0
  174. package/resources/project-architecture/atlas/atlas.history.undo.json +13 -2
  175. package/resources/project-architecture/atlas/atlas.history.undo.stack.json +610 -0
  176. package/resources/project-architecture/atlas/atlas.index.yaml +81 -5
  177. package/resources/project-architecture/atlas/features/cli-dispatch.yaml +43 -0
  178. package/resources/project-architecture/atlas/features/terminal-ui.yaml +29 -0
  179. package/resources/project-architecture/atlas/features/tool-registry.yaml +22 -0
  180. package/resources/project-architecture/atlas/features/tool-utils.yaml +22 -0
  181. package/resources/project-architecture/features/cli-dispatch/arg-parser.html +40 -0
  182. package/resources/project-architecture/features/cli-dispatch/help-builder.html +40 -0
  183. package/resources/project-architecture/features/cli-dispatch/index.html +64 -0
  184. package/resources/project-architecture/features/cli-dispatch/installer-core.html +40 -0
  185. package/resources/project-architecture/features/cli-dispatch/tool-discovery.html +40 -0
  186. package/resources/project-architecture/features/cli-dispatch/update-checker.html +40 -0
  187. package/resources/project-architecture/features/terminal-ui/banner-display.html +40 -0
  188. package/resources/project-architecture/features/terminal-ui/index.html +50 -0
  189. package/resources/project-architecture/features/terminal-ui/interactive-prompts.html +40 -0
  190. package/resources/project-architecture/features/terminal-ui/terminal-detection.html +40 -0
  191. package/resources/project-architecture/features/tool-registry/formatter.html +40 -0
  192. package/resources/project-architecture/features/tool-registry/index.html +43 -0
  193. package/resources/project-architecture/features/tool-registry/registry-core.html +40 -0
  194. package/resources/project-architecture/features/tool-utils/index.html +43 -0
  195. package/resources/project-architecture/features/tool-utils/log-utils.html +40 -0
  196. package/resources/project-architecture/features/tool-utils/skill-discovery.html +40 -0
  197. package/resources/project-architecture/index.html +365 -121
  198. package/scripts/rewrite-imports.mjs +2 -2
  199. package/scripts/test.sh +144 -8
  200. package/skills/design/SKILL.md +57 -64
  201. package/skills/design/assets/templates/DESIGN.md +12 -0
  202. package/skills/design/references/code-smells.md +94 -0
  203. package/skills/design/references/module-boundary-adjustment.md +126 -0
  204. package/skills/design/references/module-internal-restructuring.md +132 -0
  205. package/skills/design/references/module-internal-simplification.md +164 -0
@@ -0,0 +1,122 @@
1
+ import { EOL } from 'node:os';
2
+ import { parseArgs } from 'node:util';
3
+ import type { ParseArgsOptionsConfig } from 'node:util';
4
+ import { formatAppError } from './app-error.js';
5
+
6
+ /**
7
+ * Minimal tool execution context.
8
+ * Defined locally to avoid a circular build dependency:
9
+ * tool-utils → tool-registry → tui → tool-utils.
10
+ */
11
+ export interface ToolContext {
12
+ sourceRoot?: string;
13
+ stdout?: NodeJS.WriteStream;
14
+ stderr?: NodeJS.WriteStream;
15
+ env?: NodeJS.ProcessEnv;
16
+ spawnCommand?: Function;
17
+ cwd?: string;
18
+ /** Structured output adapter — created by createStdioWriter(@laitszkin/tui). */
19
+ stdioWriter?: { info?(msg: string): void; warn?(msg: string): void; error?(msg: string): void };
20
+ }
21
+
22
+ /** Option definition for parseArgs schema. */
23
+ export type SchemaOption =
24
+ | { type: 'string'; default?: string; short?: string; multiple?: boolean; description?: string }
25
+ | { type: 'boolean'; default?: boolean; short?: string; multiple?: boolean; description?: string };
26
+
27
+ /**
28
+ * Complete tool schema — single source of truth for args, help, and validation.
29
+ *
30
+ * Example:
31
+ * ```ts
32
+ * const schema: ToolSchema = {
33
+ * options: {
34
+ * start: { type: 'string', short: 's' },
35
+ * end: { type: 'string', short: 'e' },
36
+ * help: { type: 'boolean', short: 'h' },
37
+ * },
38
+ * allowPositionals: true,
39
+ * usage: 'apltk filter-logs [options] [<file>...]',
40
+ * description: 'Filter log lines by time window.',
41
+ * handler: async (values, positionals, ctx) => { ... },
42
+ * };
43
+ * ```
44
+ */
45
+ export interface ToolSchema {
46
+ options: Record<string, SchemaOption>;
47
+ allowPositionals?: boolean;
48
+ strict?: boolean;
49
+ usage?: string;
50
+ description?: string;
51
+ category?: string;
52
+ handler: (
53
+ values: Record<string, unknown>,
54
+ positionals: string[],
55
+ context: ToolContext,
56
+ ) => Promise<number> | number;
57
+ }
58
+
59
+ function buildHelpText(schema: ToolSchema): string {
60
+ const lines: string[] = [];
61
+ if (schema.usage) {
62
+ lines.push(`Usage: ${schema.usage}`);
63
+ }
64
+ if (schema.description) {
65
+ lines.push('', schema.description);
66
+ }
67
+ lines.push('', 'Options:');
68
+ for (const [key, opt] of Object.entries(schema.options)) {
69
+ if (key === 'help') continue;
70
+ const short = opt.short ? `, -${opt.short}` : '';
71
+ const typeLabel = opt.type === 'string' ? ' <value>' : '';
72
+ const multiLabel = opt.multiple ? ' [...]' : '';
73
+ const def = opt.default !== undefined ? ` (default: ${opt.default})` : '';
74
+ const desc = opt.description ? ` ${opt.description}` : '';
75
+ lines.push(` --${key}${short}${typeLabel}${multiLabel}${def}${desc}`);
76
+ }
77
+ lines.push(' --help, -h Show this help');
78
+ return lines.join(EOL);
79
+ }
80
+
81
+ /**
82
+ * Creates a tool handler function from a ToolSchema declaration.
83
+ * Automatically handles:
84
+ * - Argument parsing via node:util.parseArgs
85
+ * - --help / -h flag (auto-generates help text from options)
86
+ * - Strict mode validation
87
+ */
88
+ export function createToolRunner(schema: ToolSchema) {
89
+ const options: ParseArgsOptionsConfig = {};
90
+ for (const [key, opt] of Object.entries(schema.options)) {
91
+ const entry: { type: 'string' | 'boolean'; default?: string | boolean; short?: string; multiple?: boolean } = { type: opt.type };
92
+ if (opt.default !== undefined) entry.default = opt.default;
93
+ if (opt.short) entry.short = opt.short;
94
+ if (opt.multiple) entry.multiple = true;
95
+ options[key] = entry;
96
+ }
97
+ options.help = { type: 'boolean', short: 'h' };
98
+
99
+ return async (args: string[], context: ToolContext): Promise<number> => {
100
+ const stdout = context.stdout ?? process.stdout;
101
+ const stderr = context.stderr ?? process.stderr;
102
+
103
+ try {
104
+ const { values, positionals } = parseArgs({
105
+ args,
106
+ options,
107
+ allowPositionals: schema.allowPositionals ?? false,
108
+ strict: schema.strict ?? true,
109
+ });
110
+
111
+ if (values.help) {
112
+ stdout.write(buildHelpText(schema) + '\n');
113
+ return 0;
114
+ }
115
+
116
+ return await schema.handler(values as Record<string, unknown>, positionals, context);
117
+ } catch (err) {
118
+ formatAppError(stderr, err);
119
+ return 1;
120
+ }
121
+ };
122
+ }
@@ -1,3 +1,16 @@
1
1
  import type { ToolDefinition, ToolContext } from '@laitszkin/tool-registry';
2
+ /**
3
+ * architectureHandler — Known carryover from the createToolRunner migration.
4
+ *
5
+ * Reason for not using createToolRunner:
6
+ * - Mixed TS/JS dispatch: "apply" and "template" subcommands use TypeScript
7
+ * with AppError throws. Other subcommands delegate to the JS atlas CLI
8
+ * (cli.js) which has its own error handling.
9
+ * - Subcommand-level flag parsing: Each subcommand has unique flags; a single
10
+ * ToolSchema can't express this. See DESIGN.md §2.3 for the full picture.
11
+ *
12
+ * Error handling: All TS paths throw UserInputError/SystemError. JS paths are
13
+ * handled by cli.dispatch()'s internal catch.
14
+ */
2
15
  export declare function architectureHandler(args: string[], context: ToolContext): Promise<number>;
3
16
  export declare const tool: ToolDefinition;
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import yaml from 'js-yaml';
6
+ import { UserInputError, SystemError, createPlatformAdapter } from '../../../tool-utils/dist/index.js';
6
7
  // ── Apply & Template helpers (mirrors cli.js internals for the new verbs) ─────
7
8
  function findFeature(state, slug) {
8
9
  return (state.features || []).find((f) => f.slug === slug);
@@ -71,7 +72,7 @@ function removeSubmodule(feature, slug, merged) {
71
72
  function parseEndpoint(value) {
72
73
  const parts = value.split('/').filter(Boolean);
73
74
  if (parts.length === 0)
74
- throw new Error(`Invalid endpoint: "${value}"`);
75
+ throw new UserInputError(`Invalid endpoint: "${value}"`);
75
76
  return parts.length > 1
76
77
  ? { feature: parts[0], submodule: parts[1] }
77
78
  : { feature: parts[0] };
@@ -131,13 +132,11 @@ function yamlStr(value) {
131
132
  // ── apply ────────────────────────────────────────────────────────────────────
132
133
  async function handleApply(applyArgs, context) {
133
134
  const stdout = context.stdout || process.stdout;
134
- const stderr = context.stderr || process.stderr;
135
135
  const sourceRoot = context.sourceRoot ||
136
136
  path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
137
137
  const yamlArg = applyArgs[0];
138
138
  if (!yamlArg || yamlArg.startsWith('--')) {
139
- stderr.write('Usage: apltk architecture apply <yaml-file> [--spec <dir>] [--project <root>] [--no-render]\n');
140
- return 1;
139
+ throw new UserInputError('Missing architecture specification YAML file path. Usage: apltk architecture apply <yaml-file>');
141
140
  }
142
141
  // Simple flag parser for trailing flags (--spec, --project, --no-render)
143
142
  const rest = applyArgs.slice(1);
@@ -160,12 +159,10 @@ async function handleApply(applyArgs, context) {
160
159
  }
161
160
  catch (e) {
162
161
  const location = e.mark ? ` at line ${e.mark.line + 1}` : '';
163
- stderr.write(`Error parsing apply YAML (${yamlArg})${location}: ${e.message}\n`);
164
- return 1;
162
+ throw new UserInputError(`Error parsing apply YAML (${yamlArg})${location}: ${e.message}`);
165
163
  }
166
164
  if (!batch || typeof batch !== 'object') {
167
- stderr.write('Invalid apply YAML: expected an object with "features" / "edges" keys.\n');
168
- return 1;
165
+ throw new UserInputError('Invalid apply YAML: expected an object with "features" / "edges" keys.');
169
166
  }
170
167
  // Import atlas modules (shared with the existing JS CLI)
171
168
  const cliPath = path.join(sourceRoot, 'skills', 'init-project-html', 'lib', 'atlas', 'cli.js');
@@ -182,8 +179,7 @@ async function handleApply(applyArgs, context) {
182
179
  projectRoot = cli.resolveProjectRoot(flags);
183
180
  }
184
181
  catch (e) {
185
- stderr.write(`${e.message}\n`);
186
- return 1;
182
+ throw new UserInputError(e.message);
187
183
  }
188
184
  const isSpec = Boolean(flags.spec);
189
185
  const atlasDir = cli.baseAtlasDir(projectRoot);
@@ -209,7 +205,7 @@ async function handleApply(applyArgs, context) {
209
205
  for (const feat of batch.features || []) {
210
206
  const slug = feat.slug;
211
207
  if (!slug)
212
- throw new Error('"features" entry missing required "slug" field');
208
+ throw new UserInputError('"features" entry missing required "slug" field');
213
209
  switch (feat.action) {
214
210
  case 'add': {
215
211
  const init = {};
@@ -227,7 +223,7 @@ async function handleApply(applyArgs, context) {
227
223
  case 'modify': {
228
224
  const existing = findFeature(merged, slug);
229
225
  if (!existing)
230
- throw new Error(`feature "${slug}" not found for action "modify"`);
226
+ throw new UserInputError(`feature "${slug}" not found for action "modify"`);
231
227
  if (feat.title !== undefined)
232
228
  existing.title = String(feat.title);
233
229
  if (feat.story !== undefined)
@@ -242,7 +238,7 @@ async function handleApply(applyArgs, context) {
242
238
  removeFeature(merged, slug);
243
239
  break;
244
240
  default:
245
- throw new Error(`feature "${slug}": unknown action "${feat.action}"`);
241
+ throw new UserInputError(`feature "${slug}": unknown action "${feat.action}"`);
246
242
  }
247
243
  }
248
244
  // 2) Submodules (add / remove) — skip features that were removed
@@ -251,7 +247,7 @@ async function handleApply(applyArgs, context) {
251
247
  continue;
252
248
  const parent = findFeature(merged, feat.slug);
253
249
  if (!parent)
254
- throw new Error(`feature "${feat.slug}" not found for submodule operations`);
250
+ throw new UserInputError(`feature "${feat.slug}" not found for submodule operations`);
255
251
  for (const sub of feat.submodules || []) {
256
252
  switch (sub.action) {
257
253
  case 'add': {
@@ -269,7 +265,7 @@ async function handleApply(applyArgs, context) {
269
265
  removeSubmodule(parent, sub.slug, merged);
270
266
  break;
271
267
  default:
272
- throw new Error(`submodule "${feat.slug}/${sub.slug}": unknown action "${sub.action}"`);
268
+ throw new UserInputError(`submodule "${feat.slug}/${sub.slug}": unknown action "${sub.action}"`);
273
269
  }
274
270
  }
275
271
  }
@@ -285,7 +281,7 @@ async function handleApply(applyArgs, context) {
285
281
  continue;
286
282
  const subMod = findSubmodule(parent, sub.slug);
287
283
  if (!subMod)
288
- throw new Error(`submodule "${feat.slug}/${sub.slug}" not found for function operations`);
284
+ throw new UserInputError(`submodule "${feat.slug}/${sub.slug}" not found for function operations`);
289
285
  for (const fn of sub.functions || []) {
290
286
  switch (fn.action) {
291
287
  case 'add': {
@@ -308,7 +304,7 @@ async function handleApply(applyArgs, context) {
308
304
  subMod.functions = (subMod.functions || []).filter((f) => f.name !== fn.name);
309
305
  break;
310
306
  default:
311
- throw new Error(`function "${feat.slug}/${sub.slug}/${fn.name}": unknown action "${fn.action}"`);
307
+ throw new UserInputError(`function "${feat.slug}/${sub.slug}/${fn.name}": unknown action "${fn.action}"`);
312
308
  }
313
309
  }
314
310
  }
@@ -322,29 +318,29 @@ async function handleApply(applyArgs, context) {
322
318
  to = parseEndpoint(edge.to);
323
319
  }
324
320
  catch (er) {
325
- throw new Error(`edge: ${er.message}`);
321
+ throw new UserInputError(`edge: ${er.message}`, undefined, { cause: er });
326
322
  }
327
323
  switch (edge.action) {
328
324
  case 'add': {
329
325
  // Referential integrity validation
330
326
  const fromFeature = findFeature(merged, from.feature);
331
327
  if (!fromFeature) {
332
- throw new Error(`edge "${edge.from} → ${edge.to}": source feature "${from.feature}" not found`);
328
+ throw new UserInputError(`edge "${edge.from} → ${edge.to}": source feature "${from.feature}" not found`);
333
329
  }
334
330
  if (from.submodule) {
335
331
  const fromSub = findSubmodule(fromFeature, from.submodule);
336
332
  if (!fromSub) {
337
- throw new Error(`edge "${edge.from} → ${edge.to}": source submodule "${from.submodule}" not found in feature "${from.feature}"`);
333
+ throw new UserInputError(`edge "${edge.from} → ${edge.to}": source submodule "${from.submodule}" not found in feature "${from.feature}"`);
338
334
  }
339
335
  }
340
336
  const toFeature = findFeature(merged, to.feature);
341
337
  if (!toFeature) {
342
- throw new Error(`edge "${edge.from} → ${edge.to}": target feature "${to.feature}" not found`);
338
+ throw new UserInputError(`edge "${edge.from} → ${edge.to}": target feature "${to.feature}" not found`);
343
339
  }
344
340
  if (to.submodule) {
345
341
  const toSub = findSubmodule(toFeature, to.submodule);
346
342
  if (!toSub) {
347
- throw new Error(`edge "${edge.from} → ${edge.to}": target submodule "${to.submodule}" not found in feature "${to.feature}"`);
343
+ throw new UserInputError(`edge "${edge.from} → ${edge.to}": target submodule "${to.submodule}" not found in feature "${to.feature}"`);
348
344
  }
349
345
  }
350
346
  const eid = edge.id || `e-${Math.random().toString(36).slice(2, 8)}`;
@@ -393,13 +389,15 @@ async function handleApply(applyArgs, context) {
393
389
  break;
394
390
  }
395
391
  default:
396
- throw new Error(`edge "${edge.from} → ${edge.to}": unknown action "${edge.action}"`);
392
+ throw new UserInputError(`edge "${edge.from} → ${edge.to}": unknown action "${edge.action}"`);
397
393
  }
398
394
  }
399
395
  }
400
396
  catch (e) {
401
- stderr.write(`Batch aborted: ${e.message}\n`);
402
- return 1;
397
+ if (e instanceof UserInputError || e instanceof SystemError) {
398
+ throw e;
399
+ }
400
+ throw new UserInputError(e.message);
403
401
  }
404
402
  // ── All mutations succeeded — persist ──
405
403
  const saveDir = isSpec ? overlayDir : atlasDir;
@@ -442,7 +440,7 @@ async function handleApply(applyArgs, context) {
442
440
  // ── template ─────────────────────────────────────────────────────────────────
443
441
  async function handleTemplate(templateArgs, context) {
444
442
  const stdout = context.stdout || process.stdout;
445
- const stderr = context.stderr || process.stderr;
443
+ const adapter = createPlatformAdapter();
446
444
  // Parse --spec <dir> --output <dir>
447
445
  let specDir;
448
446
  let outputDir;
@@ -454,8 +452,7 @@ async function handleTemplate(templateArgs, context) {
454
452
  outputDir = templateArgs[++i];
455
453
  }
456
454
  if (!specDir || !outputDir) {
457
- stderr.write('Usage: apltk architecture template --spec <spec-dir> --output <output-dir>\n');
458
- return 1;
455
+ throw new UserInputError('Missing --spec and/or --output arguments. Usage: apltk architecture template --spec <spec-dir> --output <output-dir>');
459
456
  }
460
457
  const specPath = path.resolve(specDir, 'SPEC.md');
461
458
  const outputDirPath = path.resolve(outputDir);
@@ -477,18 +474,13 @@ async function handleTemplate(templateArgs, context) {
477
474
  else {
478
475
  const resolvedSpecDir = path.resolve(specDir);
479
476
  if (!fs.existsSync(resolvedSpecDir)) {
480
- stderr.write(`Spec directory not found: ${resolvedSpecDir}\n`);
477
+ throw new UserInputError(`Spec directory not found: ${resolvedSpecDir}`);
481
478
  }
482
- else {
483
- const mdFiles = fs.readdirSync(resolvedSpecDir).filter((f) => f.endsWith('.md'));
484
- if (mdFiles.length > 0) {
485
- stderr.write(`Spec directory found but no SPEC.md. Found: ${mdFiles.join(', ')}\n`);
486
- }
487
- else {
488
- stderr.write(`Spec directory found but no SPEC.md. No .md files found.\n`);
489
- }
479
+ const mdFiles = fs.readdirSync(resolvedSpecDir).filter((f) => f.endsWith('.md'));
480
+ if (mdFiles.length > 0) {
481
+ throw new UserInputError(`Spec directory found but no SPEC.md. Found: ${mdFiles.join(', ')}`);
490
482
  }
491
- return 1;
483
+ throw new UserInputError('Spec directory found but no SPEC.md. No .md files found.');
492
484
  }
493
485
  // Build proposal.yaml content
494
486
  const lines = [
@@ -506,12 +498,11 @@ async function handleTemplate(templateArgs, context) {
506
498
  lines.push(' submodules: [] # LLM: fill in submodule entries', '', '# Cross-feature edges (leave empty for single-feature proposals)', 'edges: [] # LLM: fill in edge entries', '');
507
499
  try {
508
500
  fs.mkdirSync(outputDirPath, { recursive: true });
509
- fs.writeFileSync(outputPath, lines.join('\n'), 'utf8');
501
+ fs.writeFileSync(outputPath, lines.join(adapter.EOL), 'utf8');
510
502
  stdout.write(`${outputPath}\n`);
511
503
  }
512
504
  catch (e) {
513
- stderr.write(`Error writing proposal.yaml: ${e.message}\n`);
514
- return 1;
505
+ throw new SystemError(`Error writing proposal.yaml: ${e.message}`);
515
506
  }
516
507
  // Try to enrich with CodeGraph API listing
517
508
  try {
@@ -531,7 +522,7 @@ async function handleTemplate(templateArgs, context) {
531
522
  }
532
523
  apiLines.push('#');
533
524
  const existing = fs.readFileSync(outputPath, 'utf8');
534
- fs.writeFileSync(outputPath, apiLines.join('\n') + '\n' + existing);
525
+ fs.writeFileSync(outputPath, apiLines.join(adapter.EOL) + adapter.EOL + existing);
535
526
  cg.close();
536
527
  }
537
528
  }
@@ -541,30 +532,37 @@ async function handleTemplate(templateArgs, context) {
541
532
  return 0;
542
533
  }
543
534
  // ── Handler entrypoint ───────────────────────────────────────────────────────
535
+ /**
536
+ * architectureHandler — Known carryover from the createToolRunner migration.
537
+ *
538
+ * Reason for not using createToolRunner:
539
+ * - Mixed TS/JS dispatch: "apply" and "template" subcommands use TypeScript
540
+ * with AppError throws. Other subcommands delegate to the JS atlas CLI
541
+ * (cli.js) which has its own error handling.
542
+ * - Subcommand-level flag parsing: Each subcommand has unique flags; a single
543
+ * ToolSchema can't express this. See DESIGN.md §2.3 for the full picture.
544
+ *
545
+ * Error handling: All TS paths throw UserInputError/SystemError. JS paths are
546
+ * handled by cli.dispatch()'s internal catch.
547
+ */
544
548
  export async function architectureHandler(args, context) {
545
549
  // Intercept apply / template before passing through to the JS CLI
546
550
  const first = args[0] || '';
547
551
  if (first === 'apply')
548
- return handleApply(args.slice(1), context);
552
+ return await handleApply(args.slice(1), context);
549
553
  if (first === 'template')
550
- return handleTemplate(args.slice(1), context);
554
+ return await handleTemplate(args.slice(1), context);
551
555
  // Delegate to the existing atlas CLI (still in JS)
552
556
  const sourceRoot = context.sourceRoot ||
553
557
  path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
554
558
  const cliPath = path.join(sourceRoot, 'skills', 'init-project-html', 'lib', 'atlas', 'cli.js');
555
- try {
556
- // Use file URL for ESM import compatibility on Windows — import() requires forward slashes.
557
- const cliModule = await import(pathToFileURL(cliPath).href);
558
- const cli = cliModule.default;
559
- return cli.dispatch(args, {
560
- stdout: context.stdout || process.stdout,
561
- stderr: context.stderr || process.stderr,
562
- });
563
- }
564
- catch (error) {
565
- (context.stderr || process.stderr).write(`Error loading atlas CLI: ${error.message}\n`);
566
- return 1;
567
- }
559
+ // Use file URL for ESM import compatibility on Windows — import() requires forward slashes.
560
+ const cliModule = await import(pathToFileURL(cliPath).href);
561
+ const cli = cliModule.default;
562
+ return cli.dispatch(args, {
563
+ stdout: context.stdout || process.stdout,
564
+ stderr: context.stderr || process.stderr,
565
+ });
568
566
  }
569
567
  export const tool = {
570
568
  name: 'architecture',
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import os from 'node:os';
6
+ import { UserInputError } from '../../../tool-utils/dist/index.js';
6
7
  import { tool } from './index.js';
7
8
  function makeIo() {
8
9
  let stdoutBuf = '';
@@ -88,6 +89,11 @@ describe('REGTEST-15: Wrong spec path error', () => {
88
89
  assert.equal(exitCode, 1, 'Expected exit code 1 for missing spec path');
89
90
  assert.ok(io.stderrText.includes('not found'), `stderr should contain "not found": got ${JSON.stringify(io.stderrText)}`);
90
91
  }
92
+ catch (err) {
93
+ // handler may throw — verify error type and message
94
+ assert.ok(err instanceof UserInputError, 'Expected UserInputError');
95
+ assert.ok(err.message.includes('not found'), 'Error message should indicate path not found');
96
+ }
91
97
  finally {
92
98
  mock.restoreAll();
93
99
  }
@@ -221,9 +227,16 @@ describe('REGTEST-17: Edge referential integrity', () => {
221
227
  const handler = tool.handler;
222
228
  if (!handler)
223
229
  throw new Error('tool.handler is undefined');
224
- const exitCode = await handler(['apply', yamlPath, '--no-render'], makeContext(io, { sourceRoot: tmpDir }));
225
- assert.equal(exitCode, 1, 'Expected exit code 1 for edge targeting missing feature');
226
- assert.ok(io.stderrText.includes('non-existent-feature'), `stderr should contain "non-existent-feature": got ${JSON.stringify(io.stderrText)}`);
227
- assert.ok(io.stderrText.includes('Batch aborted'), `stderr should contain "Batch aborted": got ${JSON.stringify(io.stderrText)}`);
230
+ try {
231
+ const exitCode = await handler(['apply', yamlPath, '--no-render'], makeContext(io, { sourceRoot: tmpDir }));
232
+ assert.equal(exitCode, 1, 'Expected exit code 1 for edge targeting missing feature');
233
+ assert.ok(io.stderrText.includes('non-existent-feature'), `stderr should contain "non-existent-feature": got ${JSON.stringify(io.stderrText)}`);
234
+ assert.ok(io.stderrText.length > 0, `stderr should have error text: got ${JSON.stringify(io.stderrText)}`);
235
+ }
236
+ catch (err) {
237
+ // handler may throw instead of returning 1
238
+ assert.ok(err instanceof UserInputError, 'Expected UserInputError');
239
+ assert.ok(err.message.includes('non-existent-feature'), 'Error message should reference the missing feature');
240
+ }
228
241
  });
229
242
  });