swallowkit 1.0.0-beta.55 → 1.0.0-beta.56

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 (135) hide show
  1. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  2. package/dist/cli/commands/add-auth.js +154 -157
  3. package/dist/cli/commands/add-auth.js.map +1 -1
  4. package/dist/cli/commands/init.d.ts +5 -0
  5. package/dist/cli/commands/init.d.ts.map +1 -1
  6. package/dist/cli/commands/init.js +178 -25
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/scaffold.d.ts +7 -2
  9. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  10. package/dist/cli/commands/scaffold.js +111 -123
  11. package/dist/cli/commands/scaffold.js.map +1 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +127 -2
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/core/config.d.ts +1 -1
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js +8 -4
  18. package/dist/core/config.js.map +1 -1
  19. package/dist/core/operations/auth-operations.d.ts +57 -0
  20. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  21. package/dist/core/operations/auth-operations.js +269 -0
  22. package/dist/core/operations/auth-operations.js.map +1 -0
  23. package/dist/core/operations/create-model.d.ts.map +1 -1
  24. package/dist/core/operations/create-model.js +23 -13
  25. package/dist/core/operations/create-model.js.map +1 -1
  26. package/dist/core/operations/file-session.d.ts +53 -0
  27. package/dist/core/operations/file-session.d.ts.map +1 -0
  28. package/dist/core/operations/file-session.js +195 -0
  29. package/dist/core/operations/file-session.js.map +1 -0
  30. package/dist/core/operations/provision-operations.d.ts +52 -0
  31. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  32. package/dist/core/operations/provision-operations.js +211 -0
  33. package/dist/core/operations/provision-operations.js.map +1 -0
  34. package/dist/core/operations/runtime.d.ts +5 -0
  35. package/dist/core/operations/runtime.d.ts.map +1 -1
  36. package/dist/core/operations/runtime.js +23 -0
  37. package/dist/core/operations/runtime.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  40. package/dist/core/operations/scaffold-apply.js +57 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -0
  42. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  43. package/dist/core/operations/scaffold-machine.js +1 -13
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  47. package/dist/core/operations/scaffold-plan.js +195 -0
  48. package/dist/core/operations/scaffold-plan.js.map +1 -0
  49. package/dist/core/project/artifacts.d.ts +58 -0
  50. package/dist/core/project/artifacts.d.ts.map +1 -0
  51. package/dist/core/project/artifacts.js +179 -0
  52. package/dist/core/project/artifacts.js.map +1 -0
  53. package/dist/core/project/boundaries.d.ts +50 -0
  54. package/dist/core/project/boundaries.d.ts.map +1 -0
  55. package/dist/core/project/boundaries.js +117 -0
  56. package/dist/core/project/boundaries.js.map +1 -0
  57. package/dist/core/project/drift.d.ts +29 -0
  58. package/dist/core/project/drift.d.ts.map +1 -0
  59. package/dist/core/project/drift.js +188 -0
  60. package/dist/core/project/drift.js.map +1 -0
  61. package/dist/core/project/infra.d.ts +49 -0
  62. package/dist/core/project/infra.d.ts.map +1 -0
  63. package/dist/core/project/infra.js +167 -0
  64. package/dist/core/project/infra.js.map +1 -0
  65. package/dist/core/project/state.d.ts +15 -0
  66. package/dist/core/project/state.d.ts.map +1 -0
  67. package/dist/core/project/state.js +106 -0
  68. package/dist/core/project/state.js.map +1 -0
  69. package/dist/core/project/workflows.d.ts +43 -0
  70. package/dist/core/project/workflows.d.ts.map +1 -0
  71. package/dist/core/project/workflows.js +255 -0
  72. package/dist/core/project/workflows.js.map +1 -0
  73. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  74. package/dist/core/scaffold/auth-generator.js +24 -27
  75. package/dist/core/scaffold/auth-generator.js.map +1 -1
  76. package/dist/core/scaffold/functions-generator.js +9 -9
  77. package/dist/core/scaffold/nextjs-generator.js +6 -6
  78. package/dist/core/scaffold/ui-generator.js +19 -19
  79. package/dist/core/scaffold/ui-generator.js.map +1 -1
  80. package/dist/core/verify/index.d.ts +62 -0
  81. package/dist/core/verify/index.d.ts.map +1 -0
  82. package/dist/core/verify/index.js +343 -0
  83. package/dist/core/verify/index.js.map +1 -0
  84. package/dist/machine/contracts.d.ts +8 -0
  85. package/dist/machine/contracts.d.ts.map +1 -1
  86. package/dist/machine/errors.d.ts +4 -1
  87. package/dist/machine/errors.d.ts.map +1 -1
  88. package/dist/machine/errors.js +9 -1
  89. package/dist/machine/errors.js.map +1 -1
  90. package/dist/machine/index.d.ts.map +1 -1
  91. package/dist/machine/index.js +350 -16
  92. package/dist/machine/index.js.map +1 -1
  93. package/dist/mcp/index.d.ts.map +1 -1
  94. package/dist/mcp/index.js +202 -1
  95. package/dist/mcp/index.js.map +1 -1
  96. package/dist/types/index.d.ts +14 -0
  97. package/dist/types/index.d.ts.map +1 -1
  98. package/package.json +1 -1
  99. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +17 -17
  100. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +6 -6
  101. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +11 -11
  102. package/src/__tests__/agent-loop-phase34.test.ts +449 -0
  103. package/src/__tests__/file-session.test.ts +98 -0
  104. package/src/__tests__/init.test.ts +50 -0
  105. package/src/__tests__/mcp.test.ts +113 -5
  106. package/src/__tests__/plan-apply.test.ts +294 -0
  107. package/src/cli/commands/add-auth.ts +164 -164
  108. package/src/cli/commands/init.ts +186 -24
  109. package/src/cli/commands/scaffold.ts +128 -129
  110. package/src/cli/index.ts +148 -2
  111. package/src/core/config.ts +5 -4
  112. package/src/core/operations/auth-operations.ts +353 -0
  113. package/src/core/operations/create-model.ts +25 -13
  114. package/src/core/operations/file-session.ts +210 -0
  115. package/src/core/operations/provision-operations.ts +297 -0
  116. package/src/core/operations/runtime.ts +23 -0
  117. package/src/core/operations/scaffold-apply.ts +106 -0
  118. package/src/core/operations/scaffold-machine.ts +2 -16
  119. package/src/core/operations/scaffold-plan.ts +224 -0
  120. package/src/core/project/artifacts.ts +205 -0
  121. package/src/core/project/boundaries.ts +170 -0
  122. package/src/core/project/drift.ts +202 -0
  123. package/src/core/project/infra.ts +196 -0
  124. package/src/core/project/state.ts +77 -0
  125. package/src/core/project/workflows.ts +249 -0
  126. package/src/core/scaffold/auth-generator.ts +24 -27
  127. package/src/core/scaffold/functions-generator.ts +9 -9
  128. package/src/core/scaffold/nextjs-generator.ts +6 -6
  129. package/src/core/scaffold/ui-generator.ts +19 -19
  130. package/src/core/verify/index.ts +392 -0
  131. package/src/machine/contracts.ts +16 -0
  132. package/src/machine/errors.ts +12 -1
  133. package/src/machine/index.ts +444 -15
  134. package/src/mcp/index.ts +198 -2
  135. package/src/types/index.ts +17 -0
@@ -1,19 +1,44 @@
1
1
  import { Command, CommanderError } from "commander";
2
+ import { isSwallowKitProject } from "../core/config";
2
3
  import { createModelOperation } from "../core/operations/create-model";
4
+ import { applyScaffoldOperation } from "../core/operations/scaffold-apply";
5
+ import { applyAuthOperation, planAuthOperation } from "../core/operations/auth-operations";
6
+ import { applyProvisionOperation, planProvisionOperation } from "../core/operations/provision-operations";
7
+ import { ProcessExitInterceptError } from "../core/operations/runtime";
8
+ import { inspectInfra } from "../core/project/infra";
3
9
  import { runMachineScaffoldOperation } from "../core/operations/scaffold-machine";
10
+ import { planScaffoldOperation } from "../core/operations/scaffold-plan";
11
+ import { inspectArtifacts } from "../core/project/artifacts";
12
+ import { inspectBoundaries } from "../core/project/boundaries";
13
+ import { detectDrift } from "../core/project/drift";
4
14
  import { loadProjectManifest } from "../core/project/manifest";
15
+ import { loadLastVerifyState } from "../core/project/state";
5
16
  import { validateProject } from "../core/project/validation";
6
- import { MachineErrorResponse, MachineResponse, MachineSuccessResponse } from "./contracts";
7
- import { MachineCommandError, toMachineError } from "./errors";
17
+ import { explainVerifyFailure, runVerify, VerifyResult } from "../core/verify";
18
+ import {
19
+ MachineErrorResponse,
20
+ MachineNextAction,
21
+ MachineOperationStatus,
22
+ MachineResponse,
23
+ MachineSuccessResponse,
24
+ } from "./contracts";
25
+ import { MachineCommandError, resolveMachineErrorStatus, toMachineError } from "./errors";
8
26
 
9
27
  function writeMachineResponse<TData>(response: MachineResponse<TData>): void {
10
28
  process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
11
29
  }
12
30
 
13
- function writeMachineSuccess<TData>(command: string, data: TData): void {
31
+ interface MachineResponseMeta {
32
+ status?: MachineOperationStatus;
33
+ nextActions?: MachineNextAction[];
34
+ }
35
+
36
+ function writeMachineSuccess<TData>(command: string, data: TData, meta?: MachineResponseMeta): void {
14
37
  const response: MachineSuccessResponse<TData> = {
15
38
  ok: true,
16
39
  command,
40
+ status: meta?.status ?? "complete",
41
+ ...(meta?.nextActions && meta.nextActions.length > 0 ? { nextActions: meta.nextActions } : {}),
17
42
  data,
18
43
  };
19
44
  writeMachineResponse(response);
@@ -23,17 +48,75 @@ function writeMachineError(command: string, error: unknown): void {
23
48
  const response: MachineErrorResponse = {
24
49
  ok: false,
25
50
  command,
51
+ status: resolveMachineErrorStatus(error),
26
52
  error: toMachineError(error),
27
53
  };
28
54
  writeMachineResponse(response);
29
55
  }
30
56
 
57
+ /** 書き込み系操作の前提チェック。プロジェクト外なら明確な blocked エラーを返す。 */
58
+ function ensureProjectForMachine(): void {
59
+ if (!isSwallowKitProject()) {
60
+ throw new MachineCommandError(
61
+ "not-a-swallowkit-project",
62
+ `No SwallowKit project found in ${process.cwd()}. Expected swallowkit.config.js, swallowkit.config.json, or .swallowkitrc.json in the current directory. Run this command from the project root, or create a project with "swallowkit init".`,
63
+ { cwd: process.cwd() },
64
+ "blocked"
65
+ );
66
+ }
67
+ }
68
+
69
+ /**
70
+ * レガシーな人間向けコードパスが process.exit を直接呼んでも、
71
+ * JSON envelope なしにプロセスが死なないようにする安全網。
72
+ * console.error の出力を集めてエラーメッセージとして返す。
73
+ */
74
+ async function runActionWithExitGuard<TData>(action: () => Promise<TData>): Promise<TData> {
75
+ const capturedErrors: string[] = [];
76
+ const originalError = console.error;
77
+ console.error = (...args: unknown[]) => {
78
+ const text = args
79
+ .map((arg) => (typeof arg === "string" ? arg : arg instanceof Error ? arg.stack || arg.message : JSON.stringify(arg)))
80
+ .join(" ");
81
+ capturedErrors.push(text);
82
+ process.stderr.write(`${text}\n`);
83
+ };
84
+ const originalExit = process.exit;
85
+ process.exit = ((code?: number | string | null) => {
86
+ const normalized = typeof code === "number" ? code : typeof process.exitCode === "number" ? process.exitCode : 1;
87
+ throw new ProcessExitInterceptError(normalized);
88
+ }) as typeof process.exit;
89
+
90
+ try {
91
+ return await action();
92
+ } catch (error) {
93
+ if (error instanceof ProcessExitInterceptError) {
94
+ const message = capturedErrors
95
+ .map((entry) => entry.replace(/[❌💡]\s*/gu, "").trim())
96
+ .filter((entry) => entry.length > 0)
97
+ .join("\n");
98
+ throw new MachineCommandError(
99
+ "operation-aborted",
100
+ message || `Operation aborted with exit code ${error.exitCode}.`,
101
+ undefined,
102
+ "failed"
103
+ );
104
+ }
105
+ throw error;
106
+ } finally {
107
+ console.error = originalError;
108
+ process.exit = originalExit;
109
+ }
110
+ }
111
+
31
112
  async function handleMachineAction<TData>(
32
113
  command: string,
33
- action: () => Promise<TData>
114
+ action: () => Promise<TData>,
115
+ meta?: (data: TData) => MachineResponseMeta
34
116
  ): Promise<void> {
35
117
  try {
36
- writeMachineSuccess(command, await action());
118
+ const data = await runActionWithExitGuard(action);
119
+ writeMachineSuccess(command, data, meta?.(data));
37
120
  } catch (error) {
38
121
  writeMachineError(command, error);
39
122
  process.exitCode = 1;
@@ -45,11 +128,7 @@ function createMachineProgram(): Command {
45
128
  program
46
129
  .name("swallowkit machine")
47
130
  .description("SwallowKit machine-readable CLI for AI and MCP integrations")
48
- .showHelpAfterError(false)
49
- .configureOutput({
50
- writeErr: () => undefined,
51
- writeOut: () => undefined,
52
- });
131
+ .showHelpAfterError(false);
53
132
 
54
133
  const inspect = new Command("inspect");
55
134
  inspect
@@ -94,6 +173,49 @@ function createMachineProgram(): Command {
94
173
  });
95
174
  });
96
175
 
176
+ inspect
177
+ .command("artifacts")
178
+ .description("Inspect generated artifacts and their ownership from the artifact ledger")
179
+ .action(async () => {
180
+ await handleMachineAction("inspect-artifacts", async () => inspectArtifacts());
181
+ });
182
+
183
+ inspect
184
+ .command("boundaries")
185
+ .description("Return the responsibility boundary contract between AI free-form authoring and deterministic generation")
186
+ .action(async () => {
187
+ await handleMachineAction("inspect-boundaries", async () => inspectBoundaries());
188
+ });
189
+
190
+ inspect
191
+ .command("infra")
192
+ .description("Inspect Bicep infrastructure assets (params, modules, outputs, container wiring) without calling Azure")
193
+ .action(async () => {
194
+ await handleMachineAction("inspect-infra", async () => inspectInfra());
195
+ });
196
+
197
+ inspect
198
+ .command("drift")
199
+ .description("Detect drift between generated artifacts and the current project state")
200
+ .action(async () => {
201
+ await handleMachineAction(
202
+ "inspect-drift",
203
+ async () => detectDrift(),
204
+ (result) => ({
205
+ status: "complete",
206
+ nextActions:
207
+ result.findings.length > 0
208
+ ? [
209
+ {
210
+ command: "swallowkit machine verify project",
211
+ description: "Run full verification to confirm the project is in a consistent state.",
212
+ },
213
+ ]
214
+ : [],
215
+ })
216
+ );
217
+ });
218
+
97
219
  const validate = new Command("validate");
98
220
  validate
99
221
  .command("project")
@@ -112,6 +234,7 @@ function createMachineProgram(): Command {
112
234
  .option("--overwrite <mode>", "Overwrite policy: always | never", "never")
113
235
  .action(async (names: string[], options: { modelsDir?: string; connector?: string; overwrite?: string }) => {
114
236
  await handleMachineAction("generate-model", async () => {
237
+ ensureProjectForMachine();
115
238
  if (options.overwrite !== "always" && options.overwrite !== "never") {
116
239
  throw new MachineCommandError(
117
240
  "invalid-overwrite-mode",
@@ -136,17 +259,273 @@ function createMachineProgram(): Command {
136
259
  .option("--api-dir <dir>", "API routes directory", "app/api")
137
260
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
138
261
  .action(async (model: string, options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
139
- await handleMachineAction("generate-scaffold", async () => runMachineScaffoldOperation({
262
+ await handleMachineAction("generate-scaffold", async () => {
263
+ ensureProjectForMachine();
264
+ return runMachineScaffoldOperation({
140
265
  model,
141
266
  functionsDir: options.functionsDir,
142
267
  apiDir: options.apiDir,
143
268
  apiOnly: options.apiOnly,
144
- }));
269
+ });
270
+ });
271
+ });
272
+
273
+ const plan = new Command("plan");
274
+ plan
275
+ .command("scaffold")
276
+ .description("Compute the scaffold change plan without writing any files")
277
+ .argument("<model>", "Model file or model name")
278
+ .option("--functions-dir <dir>", "Functions directory", "functions")
279
+ .option("--api-dir <dir>", "API routes directory", "app/api")
280
+ .option("--api-only", "Skip UI components in the plan", false)
281
+ .action(async (model: string, options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
282
+ await handleMachineAction(
283
+ "plan-scaffold",
284
+ async () => {
285
+ ensureProjectForMachine();
286
+ return planScaffoldOperation({
287
+ model,
288
+ functionsDir: options.functionsDir,
289
+ apiDir: options.apiDir,
290
+ apiOnly: options.apiOnly,
291
+ });
292
+ },
293
+ (planData) => ({
294
+ status: planData.requiresApproval ? "requires-human" : "complete",
295
+ nextActions: [
296
+ {
297
+ command: `swallowkit machine apply scaffold --plan ${planData.planId}${planData.requiresApproval ? " --approve" : ""}`,
298
+ description: planData.requiresApproval
299
+ ? "Apply the plan after reviewing the listed conflicts (requires --approve)."
300
+ : "Apply the plan.",
301
+ },
302
+ ],
303
+ })
304
+ );
305
+ });
306
+
307
+ plan
308
+ .command("auth")
309
+ .description("Compute the add-auth change plan without writing any files")
310
+ .requiredOption("--provider <provider>", "Auth provider: custom-jwt | swa | external-token | none")
311
+ .option("--scheme <name>", "Add as a named authentication scheme")
312
+ .action(async (options: { provider: string; scheme?: string }) => {
313
+ await handleMachineAction(
314
+ "plan-auth",
315
+ async () => {
316
+ ensureProjectForMachine();
317
+ return planAuthOperation({ provider: options.provider, scheme: options.scheme });
318
+ },
319
+ (planData) => ({
320
+ status: planData.requiresApproval ? "requires-human" : "complete",
321
+ nextActions: [
322
+ {
323
+ command: `swallowkit machine apply auth --plan ${planData.planId}${planData.requiresApproval ? " --approve" : ""}`,
324
+ description: planData.requiresApproval
325
+ ? "Apply the plan after reviewing the listed conflicts (requires --approve)."
326
+ : "Apply the plan.",
327
+ },
328
+ ],
329
+ })
330
+ );
331
+ });
332
+
333
+ plan
334
+ .command("provision")
335
+ .description("Preflight Azure provisioning locally; use --what-if for an az what-if analysis (requires az login)")
336
+ .requiredOption("-g, --resource-group <name>", "Resource group name")
337
+ .requiredOption("--location <region>", "Primary location for Functions and Cosmos DB")
338
+ .requiredOption("--swa-location <region>", "Static Web App location")
339
+ .option("--subscription <id>", "Azure subscription GUID")
340
+ .option("--what-if", "Run az deployment group what-if (requires az login)", false)
341
+ .action(async (options: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean }) => {
342
+ await handleMachineAction(
343
+ "plan-provision",
344
+ async () => {
345
+ ensureProjectForMachine();
346
+ return planProvisionOperation({
347
+ resourceGroup: options.resourceGroup,
348
+ location: options.location,
349
+ swaLocation: options.swaLocation,
350
+ subscription: options.subscription,
351
+ whatIf: options.whatIf,
352
+ });
353
+ },
354
+ (planData) => ({
355
+ status: "requires-human",
356
+ nextActions: [
357
+ {
358
+ command: `swallowkit machine apply provision --plan ${planData.planId} --approve`,
359
+ description:
360
+ "Provisioning creates billable Azure resources. A human must review plan.commands and approve before applying.",
361
+ },
362
+ ],
363
+ })
364
+ );
365
+ });
366
+
367
+ const apply = new Command("apply");
368
+ apply
369
+ .command("scaffold")
370
+ .description("Apply scaffold changes, verifying plan freshness and overwrite approval")
371
+ .argument("[model]", "Model file or model name (optional when --plan is given)")
372
+ .option("--plan <planId>", "Apply a previously computed plan")
373
+ .option("--approve", "Approve overwriting files that were modified after generation", false)
374
+ .option("--functions-dir <dir>", "Functions directory")
375
+ .option("--api-dir <dir>", "API routes directory")
376
+ .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets")
377
+ .action(
378
+ async (
379
+ model: string | undefined,
380
+ options: { plan?: string; approve?: boolean; functionsDir?: string; apiDir?: string; apiOnly?: boolean }
381
+ ) => {
382
+ await handleMachineAction(
383
+ "apply-scaffold",
384
+ async () => {
385
+ ensureProjectForMachine();
386
+ return applyScaffoldOperation({
387
+ model,
388
+ planId: options.plan,
389
+ approve: options.approve,
390
+ functionsDir: options.functionsDir,
391
+ apiDir: options.apiDir,
392
+ apiOnly: options.apiOnly,
393
+ });
394
+ },
395
+ () => ({
396
+ status: "complete",
397
+ nextActions: [
398
+ {
399
+ command: "swallowkit machine verify project",
400
+ description: "Verify the project after applying scaffold changes.",
401
+ },
402
+ ],
403
+ })
404
+ );
405
+ }
406
+ );
407
+
408
+ apply
409
+ .command("auth")
410
+ .description("Apply add-auth changes, verifying plan freshness and overwrite approval")
411
+ .option("--plan <planId>", "Apply a previously computed plan")
412
+ .option("--provider <provider>", "Auth provider (optional when --plan is given)")
413
+ .option("--scheme <name>", "Add as a named authentication scheme")
414
+ .option("--approve", "Approve overwriting files that were modified after generation", false)
415
+ .action(async (options: { plan?: string; provider?: string; scheme?: string; approve?: boolean }) => {
416
+ await handleMachineAction(
417
+ "apply-auth",
418
+ async () => {
419
+ ensureProjectForMachine();
420
+ return applyAuthOperation({
421
+ provider: options.provider,
422
+ scheme: options.scheme,
423
+ planId: options.plan,
424
+ approve: options.approve,
425
+ });
426
+ },
427
+ () => ({
428
+ status: "complete",
429
+ nextActions: [
430
+ {
431
+ command: "swallowkit machine verify project",
432
+ description: "Verify the project after applying auth changes.",
433
+ },
434
+ ],
435
+ })
436
+ );
437
+ });
438
+
439
+ apply
440
+ .command("provision")
441
+ .description("Apply an approved provisioning plan (always requires --approve; creates billable Azure resources)")
442
+ .requiredOption("--plan <planId>", "Provision plan to apply")
443
+ .option("--approve", "Confirm human approval of the provisioning plan", false)
444
+ .action(async (options: { plan: string; approve?: boolean }) => {
445
+ await handleMachineAction(
446
+ "apply-provision",
447
+ async () => {
448
+ ensureProjectForMachine();
449
+ return applyProvisionOperation({ planId: options.plan, approve: options.approve });
450
+ },
451
+ () => ({
452
+ status: "complete",
453
+ nextActions: [
454
+ {
455
+ command: "swallowkit machine inspect infra",
456
+ description: "Re-inspect infra assets after provisioning.",
457
+ },
458
+ ],
459
+ })
460
+ );
461
+ });
462
+
463
+ const verify = new Command("verify");
464
+ verify
465
+ .command("project")
466
+ .description("Run verification checks (structure, drift, typecheck) with machine-readable evidence")
467
+ .option(
468
+ "--checks <ids>",
469
+ "Comma-separated check ids to run (structure,drift,typecheck,build,lint,test, plus custom ids from swallowkit.config verify.checks)"
470
+ )
471
+ .action(async (options: { checks?: string }) => {
472
+ await handleMachineAction(
473
+ "verify-project",
474
+ async () => runVerify(options.checks ? options.checks.split(",") : undefined),
475
+ (result) => ({
476
+ status: result.summary.done ? "complete" : "in-progress",
477
+ nextActions: result.summary.done
478
+ ? []
479
+ : [
480
+ {
481
+ command: "swallowkit machine explain failure",
482
+ description: "Get evidence and suggested actions for the failed checks.",
483
+ },
484
+ ],
485
+ })
486
+ );
487
+ });
488
+
489
+ const explain = new Command("explain");
490
+ explain
491
+ .command("failure")
492
+ .description("Explain failures from the most recent verify run")
493
+ .option("--check <id>", "Explain a specific check id")
494
+ .action(async (options: { check?: string }) => {
495
+ await handleMachineAction("explain-failure", async () => {
496
+ const lastVerify = loadLastVerifyState<VerifyResult>();
497
+ if (!lastVerify) {
498
+ throw new MachineCommandError(
499
+ "no-verification-result",
500
+ 'No verify result found. Run "swallowkit machine verify project" first.',
501
+ undefined,
502
+ "blocked"
503
+ );
504
+ }
505
+
506
+ try {
507
+ return {
508
+ verifiedAt: lastVerify.verifiedAt,
509
+ failures: explainVerifyFailure(lastVerify, options.check),
510
+ };
511
+ } catch (error) {
512
+ throw new MachineCommandError(
513
+ "unknown-check",
514
+ error instanceof Error ? error.message : String(error),
515
+ { check: options.check },
516
+ "failed"
517
+ );
518
+ }
519
+ });
145
520
  });
146
521
 
147
522
  program.addCommand(inspect);
148
523
  program.addCommand(validate);
149
524
  program.addCommand(generate);
525
+ program.addCommand(plan);
526
+ program.addCommand(apply);
527
+ program.addCommand(verify);
528
+ program.addCommand(explain);
150
529
  return program;
151
530
  }
152
531
 
@@ -154,20 +533,70 @@ export function isMachineCommand(argv: string[]): boolean {
154
533
  return argv[2] === "machine";
155
534
  }
156
535
 
536
+ /**
537
+ * すべてのコマンド階層に exitOverride と出力キャプチャを適用する。
538
+ * commander の addCommand は親の出力設定を継承しないため、再帰的に設定する。
539
+ */
540
+ function applyMachineOutputSettings(command: Command, sink: { text: string }): void {
541
+ command.exitOverride();
542
+ command.showHelpAfterError(false);
543
+ command.configureOutput({
544
+ writeErr: (str) => {
545
+ sink.text += str;
546
+ },
547
+ writeOut: (str) => {
548
+ sink.text += str;
549
+ },
550
+ });
551
+ for (const sub of command.commands) {
552
+ applyMachineOutputSettings(sub, sink);
553
+ }
554
+ }
555
+
157
556
  export async function runMachineCli(argv: string[] = process.argv): Promise<void> {
158
557
  const program = createMachineProgram();
159
- program.exitOverride();
558
+ const commanderOutput = { text: "" };
559
+ applyMachineOutputSettings(program, commanderOutput);
560
+
561
+ // Keep stdout reserved for the single JSON envelope: any console output
562
+ // produced by underlying operations (e.g. config-load logs) goes to stderr.
563
+ const originalLog = console.log;
564
+ const originalInfo = console.info;
565
+ const originalWarn = console.warn;
566
+ const toStderr = (...args: unknown[]) => {
567
+ process.stderr.write(`${args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ")}\n`);
568
+ };
569
+ console.log = toStderr;
570
+ console.info = toStderr;
571
+ console.warn = toStderr;
160
572
 
161
573
  try {
162
574
  await program.parseAsync(argv.slice(3), { from: "user" });
163
575
  } catch (error) {
164
576
  if (error instanceof CommanderError) {
165
- writeMachineError("machine-parse", new MachineCommandError("invalid-command", error.message));
166
- process.exitCode = Number.isFinite(error.exitCode) ? error.exitCode : 1;
577
+ const capturedText = commanderOutput.text.trim();
578
+ if (error.code === "commander.helpDisplayed" || error.code === "commander.help") {
579
+ // --help / help は正常応答として usage を JSON envelope で返す
580
+ writeMachineSuccess("machine-help", { help: capturedText });
581
+ return;
582
+ }
583
+ writeMachineError(
584
+ "machine-parse",
585
+ new MachineCommandError(
586
+ "invalid-command",
587
+ error.message,
588
+ capturedText ? { usage: capturedText } : undefined
589
+ )
590
+ );
591
+ process.exitCode = Number.isFinite(error.exitCode) && error.exitCode !== 0 ? error.exitCode : 1;
167
592
  return;
168
593
  }
169
594
 
170
595
  writeMachineError("machine-parse", error);
171
596
  process.exitCode = 1;
597
+ } finally {
598
+ console.log = originalLog;
599
+ console.info = originalInfo;
600
+ console.warn = originalWarn;
172
601
  }
173
602
  }