swallowkit 1.1.2 → 2.0.0-alpha.10

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 (262) hide show
  1. package/README.ja.md +61 -7
  2. package/README.md +61 -7
  3. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  4. package/dist/cli/commands/add-auth.js +103 -27
  5. package/dist/cli/commands/add-auth.js.map +1 -1
  6. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  7. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  8. package/dist/cli/commands/configure-mcp.js +42 -0
  9. package/dist/cli/commands/configure-mcp.js.map +1 -0
  10. package/dist/cli/commands/dev.d.ts +5 -2
  11. package/dist/cli/commands/dev.d.ts.map +1 -1
  12. package/dist/cli/commands/dev.js +143 -67
  13. package/dist/cli/commands/dev.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts +14 -7
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/init.js +149 -170
  17. package/dist/cli/commands/init.js.map +1 -1
  18. package/dist/cli/commands/migrations.d.ts.map +1 -1
  19. package/dist/cli/commands/migrations.js +25 -0
  20. package/dist/cli/commands/migrations.js.map +1 -1
  21. package/dist/cli/commands/provision.d.ts +5 -0
  22. package/dist/cli/commands/provision.d.ts.map +1 -1
  23. package/dist/cli/commands/provision.js +278 -55
  24. package/dist/cli/commands/provision.js.map +1 -1
  25. package/dist/cli/commands/scaffold.d.ts +5 -1
  26. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  27. package/dist/cli/commands/scaffold.js +123 -18
  28. package/dist/cli/commands/scaffold.js.map +1 -1
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +49 -8
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/core/config.d.ts +2 -1
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +19 -0
  36. package/dist/core/config.js.map +1 -1
  37. package/dist/core/operations/create-model.d.ts +2 -0
  38. package/dist/core/operations/create-model.d.ts.map +1 -1
  39. package/dist/core/operations/create-model.js +60 -8
  40. package/dist/core/operations/create-model.js.map +1 -1
  41. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  42. package/dist/core/operations/migration-generator.js +12 -1
  43. package/dist/core/operations/migration-generator.js.map +1 -1
  44. package/dist/core/operations/model-migration.d.ts +22 -0
  45. package/dist/core/operations/model-migration.d.ts.map +1 -0
  46. package/dist/core/operations/model-migration.js +36 -0
  47. package/dist/core/operations/model-migration.js.map +1 -0
  48. package/dist/core/operations/provision-deployment.d.ts +52 -0
  49. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  50. package/dist/core/operations/provision-deployment.js +164 -0
  51. package/dist/core/operations/provision-deployment.js.map +1 -0
  52. package/dist/core/operations/provision-operations.d.ts +62 -6
  53. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  54. package/dist/core/operations/provision-operations.js +653 -115
  55. package/dist/core/operations/provision-operations.js.map +1 -1
  56. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  57. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  58. package/dist/core/operations/scaffold-apply.js +1 -0
  59. package/dist/core/operations/scaffold-apply.js.map +1 -1
  60. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  61. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  62. package/dist/core/operations/scaffold-machine.js.map +1 -1
  63. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  64. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  65. package/dist/core/operations/scaffold-plan.js +3 -1
  66. package/dist/core/operations/scaffold-plan.js.map +1 -1
  67. package/dist/core/project/agent-instructions.d.ts +2 -1
  68. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  69. package/dist/core/project/agent-instructions.js +15 -0
  70. package/dist/core/project/agent-instructions.js.map +1 -1
  71. package/dist/core/project/boundaries.d.ts +5 -2
  72. package/dist/core/project/boundaries.d.ts.map +1 -1
  73. package/dist/core/project/boundaries.js +51 -20
  74. package/dist/core/project/boundaries.js.map +1 -1
  75. package/dist/core/project/capabilities.d.ts +17 -0
  76. package/dist/core/project/capabilities.d.ts.map +1 -1
  77. package/dist/core/project/capabilities.js +45 -6
  78. package/dist/core/project/capabilities.js.map +1 -1
  79. package/dist/core/project/home-page.d.ts +4 -0
  80. package/dist/core/project/home-page.d.ts.map +1 -0
  81. package/dist/core/project/home-page.js +113 -0
  82. package/dist/core/project/home-page.js.map +1 -0
  83. package/dist/core/project/manifest.d.ts +3 -1
  84. package/dist/core/project/manifest.d.ts.map +1 -1
  85. package/dist/core/project/manifest.js +47 -25
  86. package/dist/core/project/manifest.js.map +1 -1
  87. package/dist/core/project/mcp-config.d.ts +26 -0
  88. package/dist/core/project/mcp-config.d.ts.map +1 -0
  89. package/dist/core/project/mcp-config.js +117 -0
  90. package/dist/core/project/mcp-config.js.map +1 -0
  91. package/dist/core/project/react-project.d.ts +26 -0
  92. package/dist/core/project/react-project.d.ts.map +1 -0
  93. package/dist/core/project/react-project.js +443 -0
  94. package/dist/core/project/react-project.js.map +1 -0
  95. package/dist/core/project/state.d.ts +7 -0
  96. package/dist/core/project/state.d.ts.map +1 -1
  97. package/dist/core/project/state.js +86 -0
  98. package/dist/core/project/state.js.map +1 -1
  99. package/dist/core/project/validation.d.ts.map +1 -1
  100. package/dist/core/project/validation.js +32 -10
  101. package/dist/core/project/validation.js.map +1 -1
  102. package/dist/core/project/workflows.d.ts.map +1 -1
  103. package/dist/core/project/workflows.js +53 -7
  104. package/dist/core/project/workflows.js.map +1 -1
  105. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  106. package/dist/core/scaffold/auth-generator.js +26 -27
  107. package/dist/core/scaffold/auth-generator.js.map +1 -1
  108. package/dist/core/scaffold/model-parser.d.ts +23 -0
  109. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  110. package/dist/core/scaffold/model-parser.js +407 -137
  111. package/dist/core/scaffold/model-parser.js.map +1 -1
  112. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  113. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  114. package/dist/core/scaffold/native-schema-generator.js +294 -91
  115. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  116. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  117. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  118. package/dist/core/scaffold/openapi-generator.js +53 -3
  119. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  120. package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
  121. package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
  122. package/dist/core/scaffold/react-bff-generator.js +252 -0
  123. package/dist/core/scaffold/react-bff-generator.js.map +1 -0
  124. package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
  125. package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
  126. package/dist/core/scaffold/react-ui-generator.js +41 -0
  127. package/dist/core/scaffold/react-ui-generator.js.map +1 -0
  128. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  129. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  130. package/dist/core/scaffold/ui-generator.js +68 -7
  131. package/dist/core/scaffold/ui-generator.js.map +1 -1
  132. package/dist/core/verify/index.d.ts +4 -3
  133. package/dist/core/verify/index.d.ts.map +1 -1
  134. package/dist/core/verify/index.js +122 -5
  135. package/dist/core/verify/index.js.map +1 -1
  136. package/dist/index.d.ts +3 -3
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +3 -2
  139. package/dist/index.js.map +1 -1
  140. package/dist/machine/contracts.d.ts +1 -0
  141. package/dist/machine/contracts.d.ts.map +1 -1
  142. package/dist/machine/errors.d.ts +3 -2
  143. package/dist/machine/errors.d.ts.map +1 -1
  144. package/dist/machine/errors.js +2 -1
  145. package/dist/machine/errors.js.map +1 -1
  146. package/dist/machine/index.d.ts.map +1 -1
  147. package/dist/machine/index.js +61 -30
  148. package/dist/machine/index.js.map +1 -1
  149. package/dist/mcp/index.d.ts +6 -4
  150. package/dist/mcp/index.d.ts.map +1 -1
  151. package/dist/mcp/index.js +210 -25
  152. package/dist/mcp/index.js.map +1 -1
  153. package/dist/mcp/version-selection.d.ts +55 -0
  154. package/dist/mcp/version-selection.d.ts.map +1 -0
  155. package/dist/mcp/version-selection.js +152 -0
  156. package/dist/mcp/version-selection.js.map +1 -0
  157. package/dist/types/index.d.ts +4 -0
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/utils/azure-cli.d.ts +56 -0
  160. package/dist/utils/azure-cli.d.ts.map +1 -0
  161. package/dist/utils/azure-cli.js +326 -0
  162. package/dist/utils/azure-cli.js.map +1 -0
  163. package/dist/utils/azure-diagnostics.d.ts +16 -0
  164. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  165. package/dist/utils/azure-diagnostics.js +68 -0
  166. package/dist/utils/azure-diagnostics.js.map +1 -0
  167. package/dist/utils/package-manager.d.ts +2 -2
  168. package/dist/utils/package-manager.d.ts.map +1 -1
  169. package/dist/utils/package-manager.js +6 -3
  170. package/dist/utils/package-manager.js.map +1 -1
  171. package/dist/utils/python-uv.d.ts +5 -7
  172. package/dist/utils/python-uv.d.ts.map +1 -1
  173. package/dist/utils/python-uv.js +50 -24
  174. package/dist/utils/python-uv.js.map +1 -1
  175. package/dist/utils/react-dev.d.ts +4 -0
  176. package/dist/utils/react-dev.d.ts.map +1 -0
  177. package/dist/utils/react-dev.js +35 -0
  178. package/dist/utils/react-dev.js.map +1 -0
  179. package/dist/utils/semver.d.ts +14 -0
  180. package/dist/utils/semver.d.ts.map +1 -0
  181. package/dist/utils/semver.js +61 -0
  182. package/dist/utils/semver.js.map +1 -0
  183. package/package.json +7 -4
  184. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  185. package/src/__tests__/agent-instructions.test.ts +6 -1
  186. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  187. package/src/__tests__/auth.test.ts +9 -0
  188. package/src/__tests__/azure-cli.test.ts +202 -0
  189. package/src/__tests__/boundaries.test.ts +42 -0
  190. package/src/__tests__/csharp-schema.test.ts +261 -0
  191. package/src/__tests__/dev.test.ts +13 -6
  192. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  193. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  194. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  195. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  196. package/src/__tests__/home-page.test.ts +50 -0
  197. package/src/__tests__/init.test.ts +145 -5
  198. package/src/__tests__/mcp-version.test.ts +262 -0
  199. package/src/__tests__/mcp.test.ts +120 -3
  200. package/src/__tests__/multi-auth.test.ts +115 -4
  201. package/src/__tests__/package-manager.test.ts +17 -2
  202. package/src/__tests__/plan-apply.test.ts +179 -0
  203. package/src/__tests__/provision-runtime.test.ts +722 -0
  204. package/src/__tests__/python-uv.test.ts +50 -14
  205. package/src/__tests__/python-venv.test.ts +48 -0
  206. package/src/__tests__/react-dev.test.ts +72 -0
  207. package/src/__tests__/react.test.ts +838 -0
  208. package/src/__tests__/release-metadata.test.ts +121 -0
  209. package/src/__tests__/schema-analysis.test.ts +329 -0
  210. package/src/cli/commands/add-auth.ts +103 -29
  211. package/src/cli/commands/configure-mcp.ts +54 -0
  212. package/src/cli/commands/dev.ts +145 -73
  213. package/src/cli/commands/init.ts +147 -172
  214. package/src/cli/commands/migrations.ts +22 -0
  215. package/src/cli/commands/provision.ts +294 -56
  216. package/src/cli/commands/scaffold.ts +145 -22
  217. package/src/cli/index.ts +46 -10
  218. package/src/core/config.ts +21 -1
  219. package/src/core/operations/create-model.ts +51 -13
  220. package/src/core/operations/migration-generator.ts +14 -1
  221. package/src/core/operations/model-migration.ts +52 -0
  222. package/src/core/operations/provision-deployment.ts +183 -0
  223. package/src/core/operations/provision-operations.ts +662 -154
  224. package/src/core/operations/scaffold-apply.ts +2 -0
  225. package/src/core/operations/scaffold-machine.ts +1 -0
  226. package/src/core/operations/scaffold-plan.ts +5 -1
  227. package/src/core/project/agent-instructions.ts +17 -1
  228. package/src/core/project/agent-templates/AGENTS.md +113 -15
  229. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  230. package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
  231. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  232. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  233. package/src/core/project/boundaries.ts +62 -20
  234. package/src/core/project/capabilities.ts +63 -6
  235. package/src/core/project/home-page.ts +114 -0
  236. package/src/core/project/manifest.ts +52 -27
  237. package/src/core/project/mcp-config.ts +84 -0
  238. package/src/core/project/react-project.ts +529 -0
  239. package/src/core/project/state.ts +70 -0
  240. package/src/core/project/validation.ts +32 -10
  241. package/src/core/project/workflows.ts +53 -7
  242. package/src/core/scaffold/auth-generator.ts +26 -27
  243. package/src/core/scaffold/model-parser.ts +413 -129
  244. package/src/core/scaffold/native-schema-generator.ts +319 -103
  245. package/src/core/scaffold/openapi-generator.ts +63 -5
  246. package/src/core/scaffold/react-bff-generator.ts +292 -0
  247. package/src/core/scaffold/react-ui-generator.ts +51 -0
  248. package/src/core/scaffold/ui-generator.ts +70 -8
  249. package/src/core/verify/index.ts +118 -6
  250. package/src/index.ts +3 -1
  251. package/src/machine/contracts.ts +1 -0
  252. package/src/machine/errors.ts +4 -2
  253. package/src/machine/index.ts +67 -36
  254. package/src/mcp/index.ts +221 -31
  255. package/src/mcp/version-selection.ts +163 -0
  256. package/src/types/index.ts +4 -0
  257. package/src/utils/azure-cli.ts +315 -0
  258. package/src/utils/azure-diagnostics.ts +75 -0
  259. package/src/utils/package-manager.ts +7 -4
  260. package/src/utils/python-uv.ts +53 -33
  261. package/src/utils/react-dev.ts +31 -0
  262. package/src/utils/semver.ts +59 -0
@@ -2,10 +2,10 @@
2
2
  /**
3
3
  * Plan / Apply Provision — Azure プロビジョニングの事前計画と承認付き実行。
4
4
  *
5
- * - plan provision: ローカルの決定論的プリフライト(Bicep 解析・az CLI 有無)のみ。
6
- * ネットワークアクセスや az login を必要とする操作は行わない。
7
- * --what-if を明示した場合のみ az deployment group what-if を実行する。
5
+ * - plan provision: Bicep と Azure の現在の状態を確認する。リソースは変更しない。
8
6
  * - apply provision: 常に承認(--approve)必須。承認なしでは requires-human で停止する。
7
+ * - 対話的な `swallowkit provision` は同じ計画をメモリ上で作成・表示し、確認後に
8
+ * executeProvisionPlan で同じ安全確認を経て実行する(計画ファイルは保存しない)。
9
9
  */
10
10
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
11
  if (k2 === undefined) k2 = k;
@@ -41,17 +41,23 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  };
42
42
  })();
43
43
  Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.INTERACTIVE_PROVISION_RETRY_HINT = void 0;
44
45
  exports.planProvisionOperation = planProvisionOperation;
46
+ exports.provisionNextActions = provisionNextActions;
47
+ exports.inspectProvisionOperation = inspectProvisionOperation;
45
48
  exports.applyProvisionOperation = applyProvisionOperation;
49
+ exports.executeProvisionPlan = executeProvisionPlan;
46
50
  const crypto = __importStar(require("crypto"));
47
51
  const fs = __importStar(require("fs"));
52
+ const azure_cli_1 = require("../../utils/azure-cli");
53
+ const azure_diagnostics_1 = require("../../utils/azure-diagnostics");
48
54
  const path = __importStar(require("path"));
49
- const child_process_1 = require("child_process");
50
55
  const errors_1 = require("../../machine/errors");
51
56
  const version_1 = require("../../version");
52
57
  const infra_1 = require("../project/infra");
53
58
  const state_1 = require("../project/state");
54
59
  const file_session_1 = require("./file-session");
60
+ const provision_deployment_1 = require("./provision-deployment");
55
61
  const migrations_1 = require("../project/migrations");
56
62
  const RESOURCE_GROUP_PATTERN = /^[A-Za-z0-9._()-]{1,90}$/;
57
63
  const LOCATION_PATTERN = /^[a-z0-9]{3,30}$/;
@@ -66,36 +72,38 @@ function validateProvisionInputs(options) {
66
72
  if (options.subscription && !SUBSCRIPTION_PATTERN.test(options.subscription)) {
67
73
  throw new errors_1.MachineCommandError("invalid-arguments", "Subscription must be a GUID.", undefined, "failed");
68
74
  }
69
- }
70
- function isAzCliAvailable() {
71
- try {
72
- const result = (0, child_process_1.spawnSync)("az", ["--version"], { shell: false, stdio: "ignore", timeout: 30000 });
73
- return result.status === 0;
74
- }
75
- catch {
76
- return false;
75
+ if (options.resumeDeployment && (!/^[A-Za-z0-9._()-]{1,64}$/.test(options.resumeDeployment) || options.reconcile || options.adoptExisting)) {
76
+ throw new errors_1.MachineCommandError("invalid-arguments", "--resume-deployment requires a valid deployment name and cannot be combined with adoption or reconcile.", undefined, "blocked");
77
77
  }
78
78
  }
79
+ function azCliUnavailableWarning(availability) {
80
+ return availability.reason === "not-found"
81
+ ? "az-cli-not-found: Azure CLI was not found on PATH. Install it (https://aka.ms/azure-cli) or add it to PATH, then try again."
82
+ : `az-cli-not-runnable: Azure CLI was found but could not be started. ${availability.detail}`;
83
+ }
79
84
  function displayCommand(command) {
80
85
  return [command.file, ...command.args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg)].join(" ");
81
86
  }
82
- function runAz(args, timeout = 300000) {
83
- const result = (0, child_process_1.spawnSync)("az", args, { shell: false, encoding: "utf-8", timeout });
84
- return {
85
- status: result.status,
86
- stdout: result.stdout ?? "",
87
- stderr: result.stderr ?? "",
88
- };
87
+ function runAz(args, timeout = 60000) {
88
+ return (0, azure_cli_1.runAzCli)(args, { timeout });
89
+ }
90
+ function azQueryError(code, message, result, secretValues = []) {
91
+ const diagnostic = (0, azure_diagnostics_1.azureCliDiagnostic)(result, "azure-state", secretValues);
92
+ const output = (diagnostic.stderr || diagnostic.stdout).trim().slice(0, 2000);
93
+ if (/az login|not logged in|AADSTS|expired/i.test(output)) {
94
+ return new errors_1.MachineCommandError("az-not-logged-in", `Azure CLI is not signed in (or the session expired). Run "az login", then try again.\n${output.slice(0, 1000)}`, { diagnostic }, "requires-human");
95
+ }
96
+ return new errors_1.MachineCommandError(code, `${message}: ${output || result.errorCode || result.failureKind || `exit ${result.status}`}`, { diagnostic }, "blocked");
89
97
  }
90
98
  function subscriptionArgs(subscription) {
91
99
  return subscription ? ["--subscription", subscription] : [];
92
100
  }
93
101
  function buildBootstrapCommands(options, mainBicepPath, parametersPath) {
94
102
  const commands = [];
95
- commands.push({ file: "az", args: ["group", "create", "--name", options.resourceGroup, "--location", options.location, ...subscriptionArgs(options.subscription)], kind: "resource-group" });
103
+ commands.push({ file: "az", args: ["group", "create", "--name", options.resourceGroup, "--location", options.location, ...subscriptionArgs(options.subscription), "--query", "id", "-o", "json"], kind: "resource-group" });
96
104
  commands.push({
97
105
  file: "az",
98
- args: ["deployment", "group", "create", "--name", "swallowkit-baseline", "--resource-group", options.resourceGroup, "--template-file", mainBicepPath, "--parameters", parametersPath, "--parameters", `location=${options.location}`, `swaLocation=${options.swaLocation}`, ...subscriptionArgs(options.subscription)],
106
+ args: ["deployment", "group", "create", "--name", "swallowkit-baseline", "--resource-group", options.resourceGroup, "--template-file", mainBicepPath, "--parameters", parametersPath, "--parameters", `location=${options.location}`, `swaLocation=${options.swaLocation}`, ...subscriptionArgs(options.subscription), "--no-wait", "--query", provision_deployment_1.DEPLOYMENT_QUERY, "-o", "json"],
99
107
  kind: "deployment",
100
108
  });
101
109
  return commands;
@@ -114,23 +122,32 @@ function readProjectName(parametersPath) {
114
122
  }
115
123
  function inspectRemoteGroup(options) {
116
124
  const existsResult = runAz(["group", "exists", "--name", options.resourceGroup, ...subscriptionArgs(options.subscription)], 60000);
117
- if (existsResult.status !== 0) {
118
- throw new errors_1.MachineCommandError("azure-state-query-failed", `Could not query resource group: ${(existsResult.stderr || existsResult.stdout || "").trim()}`, undefined, "blocked");
125
+ if (existsResult.status !== 0 || existsResult.failureKind || !/^(true|false)$/.test(existsResult.stdout.trim())) {
126
+ throw azQueryError("azure-state-query-failed", "Could not query resource group", existsResult);
119
127
  }
120
128
  if ((existsResult.stdout || "").trim() !== "true")
121
129
  return { exists: false, tags: {} };
122
130
  const showResult = runAz(["group", "show", "--name", options.resourceGroup, ...subscriptionArgs(options.subscription), "--query", "{id:id,tags:tags}", "-o", "json"], 60000);
123
- if (showResult.status !== 0) {
124
- throw new errors_1.MachineCommandError("azure-state-query-failed", `Could not read resource group tags: ${(showResult.stderr || showResult.stdout || "").trim()}`, undefined, "blocked");
131
+ if (showResult.status !== 0 || showResult.failureKind || showResult.stdoutTruncated) {
132
+ throw azQueryError("azure-state-query-failed", "Could not read resource group tags", showResult);
133
+ }
134
+ let parsed;
135
+ try {
136
+ parsed = JSON.parse(showResult.stdout);
137
+ }
138
+ catch {
139
+ throw azQueryError("azure-state-query-failed", "Invalid resource group response", showResult);
140
+ }
141
+ if (!parsed || typeof parsed.id !== "string") {
142
+ throw azQueryError("azure-state-query-failed", "Invalid resource group identity", showResult);
125
143
  }
126
- const parsed = JSON.parse(showResult.stdout || "{}");
127
144
  return { exists: true, id: parsed.id, tags: parsed.tags ?? {} };
128
145
  }
129
146
  function tagCommand(resourceId, key, state, expectedState) {
130
- return { file: "az", args: ["tag", "update", "--resource-id", resourceId, "--operation", "Merge", "--tags", `${key}=${(0, migrations_1.encodeMigrationState)(state)}`], kind: "tag", expectedState };
147
+ return { file: "az", args: ["tag", "update", "--resource-id", resourceId, "--operation", "Merge", "--tags", `${key}=${(0, migrations_1.encodeMigrationState)(state)}`, "-o", "none"], kind: "tag", expectedState };
131
148
  }
132
149
  function bootstrapTagCommand(options, key, state) {
133
- return { file: "az", args: ["group", "update", "--name", options.resourceGroup, "--set", `tags.${key}=${(0, migrations_1.encodeMigrationState)(state)}`, ...subscriptionArgs(options.subscription)], kind: "tag", expectedState: null };
150
+ return { file: "az", args: ["group", "update", "--name", options.resourceGroup, "--set", `tags.${key}=${(0, migrations_1.encodeMigrationState)(state)}`, ...subscriptionArgs(options.subscription), "-o", "none"], kind: "tag", expectedState: null };
134
151
  }
135
152
  function runBaseWhatIf(options, mainBicepPath, parametersPath) {
136
153
  const result = runAz([
@@ -145,7 +162,7 @@ function runBaseWhatIf(options, mainBicepPath, parametersPath) {
145
162
  "-o", "json",
146
163
  ]);
147
164
  if (result.status !== 0) {
148
- throw new errors_1.MachineCommandError("what-if-failed", `az deployment group what-if failed: ${(result.stderr || result.stdout).trim().slice(0, 2000)}`, undefined, "blocked");
165
+ throw azQueryError("what-if-failed", "az deployment group what-if failed", result);
149
166
  }
150
167
  return result.stdout.trim();
151
168
  }
@@ -163,7 +180,93 @@ function assertAdoptionWhatIfIsSafe(whatIfResult) {
163
180
  throw new errors_1.MachineCommandError("what-if-invalid", "Azure what-if did not return valid JSON for legacy adoption.", undefined, "blocked");
164
181
  }
165
182
  }
166
- async function planProvisionOperation(options) {
183
+ function resolveSubscription(options) {
184
+ if (options.subscription)
185
+ return options.subscription;
186
+ const result = runAz(["account", "show", "--query", "id", "-o", "tsv"]);
187
+ const subscription = result.stdout.trim();
188
+ if (result.status !== 0 || !SUBSCRIPTION_PATTERN.test(subscription)) {
189
+ throw azQueryError("azure-subscription-query-failed", "Could not identify the Azure subscription for this provision run", result);
190
+ }
191
+ return subscription;
192
+ }
193
+ function canonicalTemplate(value) {
194
+ const normalize = (item) => {
195
+ if (Array.isArray(item))
196
+ return item.map(normalize);
197
+ if (item && typeof item === "object") {
198
+ return Object.fromEntries(Object.entries(item).filter(([key]) => key !== "_generator")
199
+ .sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalize(entry)]));
200
+ }
201
+ return item;
202
+ };
203
+ return JSON.stringify(normalize(value));
204
+ }
205
+ async function verifyExistingBaseline(options, mainBicepPath, parametersPath) {
206
+ const projectRoot = path.dirname(path.dirname(mainBicepPath));
207
+ const secretValues = provisionSecretValues({
208
+ fingerprints: Object.fromEntries((0, migrations_1.collectLegacyBaselineFiles)(projectRoot).map(file => [file.path, file.checksum])),
209
+ }, projectRoot);
210
+ const reference = {
211
+ subscription: options.subscription, resourceGroup: options.resourceGroup, name: options.resumeDeployment,
212
+ };
213
+ const observation = await (0, provision_deployment_1.queryAzureDeployment)(reference, { secretValues });
214
+ if (observation.state === "unknown") {
215
+ throw new errors_1.MachineCommandError("recovery-deployment-unknown", "The existing deployment could not be identified. No Bicep will be submitted.", { deployment: observation }, "blocked");
216
+ }
217
+ const compiled = await (0, azure_cli_1.runAzCliAsync)(["bicep", "build", "--file", mainBicepPath, "--stdout"], { timeout: 60000, captureBytes: 16777216 });
218
+ const exported = await (0, azure_cli_1.runAzCliAsync)([
219
+ "deployment", "group", "export", "--name", reference.name,
220
+ "--resource-group", reference.resourceGroup, "--subscription", reference.subscription,
221
+ ], { timeout: 60000, captureBytes: 16777216 });
222
+ const parameters = await (0, azure_cli_1.runAzCliAsync)([
223
+ "deployment", "group", "show", "--name", reference.name, "--resource-group", reference.resourceGroup,
224
+ "--subscription", reference.subscription, "--query", "properties.parameters", "-o", "json",
225
+ ], { timeout: 30000 });
226
+ if ([compiled, exported, parameters].some((result) => result.status !== 0 || result.failureKind || result.stdoutTruncated)) {
227
+ throw new errors_1.MachineCommandError("recovery-evidence-unavailable", "Recovery requires the existing deployment template and parameters to match the local baseline. Evidence could not be read; nothing will be redeployed.", {
228
+ diagnostics: [compiled, exported, parameters].map((result, index) => ({
229
+ ...(0, azure_diagnostics_1.azureCliDiagnostic)(result, ["local-bicep-build", "recovery-template", "recovery-parameters"][index], secretValues),
230
+ stdout: "[template/parameter payload omitted]", stderr: "[template/parameter payload omitted]", outputTruncated: true,
231
+ })),
232
+ }, "blocked");
233
+ }
234
+ let template;
235
+ let remoteParameters;
236
+ let localParameters;
237
+ try {
238
+ template = JSON.parse(compiled.stdout);
239
+ if (canonicalTemplate(template) !== canonicalTemplate(JSON.parse(exported.stdout))) {
240
+ throw new errors_1.MachineCommandError("recovery-template-mismatch", "The deployed ARM template differs from the local Bicep. Restore the matching reviewed files before recovery; do not redeploy to repair migration bookkeeping.", undefined, "blocked");
241
+ }
242
+ remoteParameters = JSON.parse(parameters.stdout);
243
+ localParameters = JSON.parse(fs.readFileSync(parametersPath, "utf-8")).parameters;
244
+ if (!remoteParameters || !localParameters || typeof remoteParameters !== "object" || typeof localParameters !== "object")
245
+ throw new Error("Invalid parameters");
246
+ }
247
+ catch (error) {
248
+ if (error instanceof errors_1.MachineCommandError)
249
+ throw error;
250
+ throw new errors_1.MachineCommandError("recovery-invalid-evidence", "Recovery template/parameter evidence is not valid JSON.", undefined, "blocked");
251
+ }
252
+ const declarations = template.parameters;
253
+ if (Object.values(declarations ?? {}).some((parameter) => /^secure/i.test(parameter.type ?? ""))) {
254
+ throw new errors_1.MachineCommandError("recovery-secure-parameters", "This untracked deployment has secure parameters that cannot be verified. Recovery requires the original execution checkpoint; no secret values will be requested or printed.", undefined, "blocked");
255
+ }
256
+ const expected = { ...localParameters, location: { value: options.location }, swaLocation: { value: options.swaLocation } };
257
+ for (const [name, parameter] of Object.entries(expected)) {
258
+ if (!parameter || !Object.prototype.hasOwnProperty.call(parameter, "value")
259
+ || canonicalTemplate(parameter.value) !== canonicalTemplate(remoteParameters[name]?.value)) {
260
+ throw new errors_1.MachineCommandError("recovery-parameters-mismatch", "The deployed parameters differ from the local reviewed parameters/regions. No tag or ledger will be changed.", undefined, "blocked");
261
+ }
262
+ }
263
+ return observation.reference;
264
+ }
265
+ /**
266
+ * Builds the provision plan. Machine/MCP callers persist it for a later `apply provision --plan`;
267
+ * the interactive `swallowkit provision` command keeps it in memory and executes it after confirmation.
268
+ */
269
+ async function planProvisionOperation(options, planOptions = {}) {
167
270
  const projectRoot = process.cwd();
168
271
  validateProvisionInputs(options);
169
272
  const infra = (0, infra_1.inspectInfra)(projectRoot);
@@ -174,9 +277,10 @@ async function planProvisionOperation(options) {
174
277
  if (!infra.parametersFile.exists) {
175
278
  warnings.push("infra/main.parameters.json not found; deployment will fail until it is created.");
176
279
  }
177
- const azCliAvailable = isAzCliAvailable();
178
- if (!azCliAvailable) {
179
- warnings.push("az-cli-not-found: Azure CLI is not installed. Install it before applying: https://aka.ms/azure-cli");
280
+ const azCli = (0, azure_cli_1.checkAzCliAvailability)();
281
+ const azCliAvailable = azCli.available;
282
+ if (!azCli.available) {
283
+ warnings.push(azCliUnavailableWarning(azCli));
180
284
  }
181
285
  const mainBicepPath = path.join(projectRoot, "infra", "main.bicep");
182
286
  const parametersPath = path.join(projectRoot, "infra", "main.parameters.json");
@@ -188,6 +292,7 @@ async function planProvisionOperation(options) {
188
292
  let whatIfResult;
189
293
  let manifestAfterApply;
190
294
  let baselineDrift = [];
295
+ let recoveryDeployment;
191
296
  const manifestPath = path.join(projectRoot, migrations_1.MIGRATION_MANIFEST_PATH);
192
297
  const manifest = fs.existsSync(manifestPath) ? (0, migrations_1.loadMigrationManifest)(projectRoot) : null;
193
298
  if (manifest) {
@@ -202,6 +307,12 @@ async function planProvisionOperation(options) {
202
307
  baselineDrift = (0, migrations_1.findBaselineDrift)(manifest, projectRoot);
203
308
  }
204
309
  if (azCliAvailable) {
310
+ options = { ...options, subscription: resolveSubscription(options) };
311
+ const unfinished = readRuns(projectRoot).find((run) => run.outcome !== "succeeded" && run.outcome !== "failed" && run.plan.resourceGroup === options.resourceGroup
312
+ && run.plan.subscription === options.subscription);
313
+ if (unfinished) {
314
+ throw new errors_1.MachineCommandError("provision-run-pending", `Provision run ${unfinished.runId} is unfinished. Inspect or resume that run instead of submitting Bicep again.`, { runId: unfinished.runId, outcome: unfinished.outcome }, "blocked", provisionNextActions(unfinished.runId));
315
+ }
205
316
  const remoteGroup = inspectRemoteGroup(options);
206
317
  if (!manifest) {
207
318
  if (remoteGroup.exists) {
@@ -210,7 +321,33 @@ async function planProvisionOperation(options) {
210
321
  throw new errors_1.MachineCommandError("migration-manifest-not-found", "Initialize a migration manifest before the first provision.", undefined, "blocked");
211
322
  }
212
323
  migrationTagKey = (0, migrations_1.getMigrationTagKey)(manifest.projectId);
213
- if (!remoteGroup.exists) {
324
+ if (options.resumeDeployment) {
325
+ if (!remoteGroup.exists || manifest.baseline.legacy) {
326
+ throw new errors_1.MachineCommandError("recovery-baseline-required", "--resume-deployment requires an existing resource group and the original non-legacy baseline manifest.", undefined, "blocked");
327
+ }
328
+ const tagValue = remoteGroup.tags[migrationTagKey];
329
+ remoteState = tagValue ? (0, migrations_1.decodeMigrationState)(tagValue) : null;
330
+ if (remoteState && remoteState.version !== 0) {
331
+ throw new errors_1.MachineCommandError("recovery-baseline-only", "Untracked-deployment recovery supports baseline 0 only. Use the original checkpoint to resume migrations.", undefined, "blocked");
332
+ }
333
+ recoveryDeployment = await verifyExistingBaseline(options, mainBicepPath, parametersPath);
334
+ const baselineFiles = (0, migrations_1.collectLegacyBaselineFiles)(projectRoot);
335
+ manifestAfterApply = {
336
+ ...manifest,
337
+ baseline: { ...manifest.baseline, files: baselineFiles, checksum: (0, migrations_1.computeBaselineChecksum)(baselineFiles) },
338
+ };
339
+ if (remoteState && remoteState.checksum !== manifestAfterApply.baseline.checksum) {
340
+ throw new errors_1.MachineCommandError("recovery-checksum-mismatch", "The existing baseline tag does not match this project. Nothing will be overwritten.", undefined, "blocked");
341
+ }
342
+ mode = "recover";
343
+ warnings.push("Recovery only observes the matching existing deployment, then records migration state after Azure Succeeded. It never submits Bicep.");
344
+ commandSpecs = [{
345
+ file: "az",
346
+ args: ["deployment", "group", "show", "--name", recoveryDeployment.name, "--resource-group", options.resourceGroup, ...subscriptionArgs(options.subscription), "--query", provision_deployment_1.DEPLOYMENT_QUERY, "-o", "json"],
347
+ kind: "deployment",
348
+ }, tagCommand(remoteGroup.id, migrationTagKey, { schemaVersion: 1, version: 0, checksum: manifestAfterApply.baseline.checksum }, remoteState)];
349
+ }
350
+ else if (!remoteGroup.exists) {
214
351
  mode = "bootstrap";
215
352
  const baselineFiles = (0, migrations_1.collectLegacyBaselineFiles)(projectRoot);
216
353
  manifestAfterApply = {
@@ -233,7 +370,7 @@ async function planProvisionOperation(options) {
233
370
  commandSpecs = [];
234
371
  if (options.reconcile && !manifest.baseline.legacy) {
235
372
  whatIfResult = runBaseWhatIf(options, mainBicepPath, parametersPath);
236
- warnings.push("Recovering an interrupted bootstrap by reconciling the baseline before recording migration state.");
373
+ warnings.push("Explicit reconcile redeploys the untagged baseline. For an interrupted wait/tag/ledger update use --resume-deployment instead.");
237
374
  mode = "reconcile";
238
375
  const baselineFiles = (0, migrations_1.collectLegacyBaselineFiles)(projectRoot);
239
376
  manifestAfterApply = {
@@ -289,12 +426,13 @@ async function planProvisionOperation(options) {
289
426
  file: "az",
290
427
  args: [
291
428
  "deployment", "group", "create",
292
- "--name", "swallowkit-migration",
429
+ "--name", `swallowkit-migration-${migration.version}-${migration.checksum.slice(0, 12)}`,
293
430
  "--resource-group", options.resourceGroup,
294
431
  "--template-file", path.join(projectRoot, migration.template),
295
432
  ...(migration.parameters ? ["--parameters", path.join(projectRoot, migration.parameters)] : []),
296
433
  "--parameters", `projectName=${projectName}`, `location=${options.location}`,
297
434
  ...subscriptionArgs(options.subscription),
435
+ "--no-wait", "--query", provision_deployment_1.DEPLOYMENT_QUERY, "-o", "json",
298
436
  ],
299
437
  kind: "migration",
300
438
  expectedState,
@@ -306,8 +444,9 @@ async function planProvisionOperation(options) {
306
444
  }
307
445
  }
308
446
  }
309
- else if (options.whatIf || options.adoptExisting || options.reconcile) {
310
- throw new errors_1.MachineCommandError("az-cli-not-found", "Azure CLI is required for --what-if, --adopt-existing, and --reconcile.", undefined, "blocked");
447
+ else if (options.whatIf || options.adoptExisting || options.reconcile || options.resumeDeployment) {
448
+ const detail = azCli.available ? "" : ` ${azCliUnavailableWarning(azCli)}`;
449
+ throw new errors_1.MachineCommandError(!azCli.available && azCli.reason === "not-runnable" ? "az-cli-not-runnable" : "az-cli-not-found", `Azure CLI is required for --what-if, --adopt-existing, --reconcile, and --resume-deployment.${detail}`, undefined, "blocked");
311
450
  }
312
451
  const fingerprints = {
313
452
  "infra/main.bicep": (0, file_session_1.hashFileIfExists)(mainBicepPath),
@@ -337,6 +476,8 @@ async function planProvisionOperation(options) {
337
476
  mode,
338
477
  remoteState,
339
478
  pendingMigrations: pendingMigrations.map((migration) => ({ version: migration.version, checksum: migration.checksum })),
479
+ recoveryDeployment,
480
+ nonce: crypto.randomUUID(),
340
481
  baselineSnapshotChecksum,
341
482
  fingerprints,
342
483
  }))
@@ -365,116 +506,513 @@ async function planProvisionOperation(options) {
365
506
  requiresApproval: true,
366
507
  fingerprints,
367
508
  ...(baselineSnapshotChecksum ? { baselineSnapshotChecksum } : {}),
509
+ ...(recoveryDeployment ? { recoveryDeployment } : {}),
368
510
  };
369
- (0, state_1.savePlanState)(plan, projectRoot);
511
+ if (planOptions.persist !== false)
512
+ (0, state_1.savePlanState)(plan, projectRoot);
370
513
  return plan;
371
514
  }
372
515
  function statesEqual(left, right) {
373
516
  return left?.version === right?.version && left?.checksum === right?.checksum;
374
517
  }
375
- function readRemoteMigrationState(plan) {
376
- const group = inspectRemoteGroup(plan);
377
- if (!group.exists || !plan.migrationTagKey)
378
- return { group, state: null };
379
- const tagValue = group.tags[plan.migrationTagKey];
380
- return { group, state: tagValue ? (0, migrations_1.decodeMigrationState)(tagValue) : null };
518
+ const MACHINE_RETRY_HINT = "Re-run plan provision.";
519
+ exports.INTERACTIVE_PROVISION_RETRY_HINT = 'Run "swallowkit provision" again to review the current state.';
520
+ function provisionNextActions(runId) {
521
+ return [
522
+ { command: `swallowkit machine inspect provision --plan ${runId}`, description: "Read the original Azure deployment state without creating, cancelling, deleting, tagging, or updating the ledger." },
523
+ { command: `swallowkit machine apply provision --plan ${runId} --approve`, description: "After explicit human approval, resume this checkpoint: observe the original deployment and finish tags/ledger; never resubmit an attempted deployment." },
524
+ ];
525
+ }
526
+ function resultForRun(run, outcome = run.outcome, interrupted) {
527
+ run.warnings = run.warnings.slice(-20);
528
+ run.diagnostics = run.diagnostics.slice(-20);
529
+ return {
530
+ status: outcome === "succeeded" ? "complete" : outcome === "in-progress" ? "in-progress" : outcome === "failed" ? "failed" : "blocked",
531
+ outcome, runId: run.runId, planId: run.plan.planId,
532
+ resourceGroup: run.plan.resourceGroup, location: run.plan.location, swaLocation: run.plan.swaLocation,
533
+ executedCommands: run.executedCommands, warnings: run.warnings, diagnostics: run.diagnostics,
534
+ ...(run.observation ? { deployment: run.observation } : {}),
535
+ nextActions: outcome === "succeeded"
536
+ ? [{ command: "swallowkit machine inspect infra", description: "Inspect local infrastructure assets after deployment and migration bookkeeping completed." }]
537
+ : provisionNextActions(run.runId),
538
+ ...(interrupted ? { interrupted } : {}),
539
+ };
540
+ }
541
+ function readRun(runId, projectRoot) {
542
+ let run;
543
+ try {
544
+ run = (0, state_1.loadProvisionRun)(runId, projectRoot);
545
+ }
546
+ catch {
547
+ throw new errors_1.MachineCommandError("invalid-provision-checkpoint", "The execution checkpoint cannot be read. Do not re-submit Bicep; inspect the original deployment first.", { runId }, "blocked");
548
+ }
549
+ if (run && (run.schemaVersion !== 1 || run.runId !== runId || run.plan?.planId !== runId
550
+ || run.projectRoot !== path.resolve(projectRoot) || !Number.isInteger(run.cursor)
551
+ || !Array.isArray(run.plan.commandSpecs) || !run.plan.fingerprints
552
+ || !Array.isArray(run.warnings) || !Array.isArray(run.diagnostics) || !Array.isArray(run.executedCommands)
553
+ || !["succeeded", "in-progress", "unknown", "partial", "failed"].includes(run.outcome)
554
+ || run.cursor < 0 || run.cursor > run.plan.commandSpecs.length)) {
555
+ throw new errors_1.MachineCommandError("invalid-provision-checkpoint", "The execution checkpoint is invalid or belongs to another checkout. Nothing will be re-submitted.", { runId }, "blocked");
556
+ }
557
+ return run;
558
+ }
559
+ function readRuns(projectRoot) {
560
+ try {
561
+ return (0, state_1.listProvisionRuns)(projectRoot).map(record => {
562
+ if (!record || typeof record.runId !== "string")
563
+ throw new Error("Invalid checkpoint identity.");
564
+ const run = readRun(record.runId, projectRoot);
565
+ if (!run)
566
+ throw new Error("Missing execution checkpoint.");
567
+ return run;
568
+ });
569
+ }
570
+ catch (error) {
571
+ if (error instanceof errors_1.MachineCommandError)
572
+ throw error;
573
+ throw new errors_1.MachineCommandError("invalid-provision-checkpoint", "An execution checkpoint is corrupt. Preserve it and inspect the original deployment; nothing will be re-submitted.", undefined, "blocked");
574
+ }
575
+ }
576
+ function validateWaitSeconds(value = provision_deployment_1.DEFAULT_PROVISION_WAIT_SECONDS) {
577
+ if (!Number.isInteger(value) || value < 0 || value > provision_deployment_1.MAX_PROVISION_WAIT_SECONDS) {
578
+ throw new errors_1.MachineCommandError("invalid-arguments", `--wait-seconds must be an integer from 0 to ${provision_deployment_1.MAX_PROVISION_WAIT_SECONDS}.`, undefined, "blocked");
579
+ }
580
+ return value;
581
+ }
582
+ function assertLocalFiles(plan, projectRoot, retryHint, allowRecordedManifest = false) {
583
+ const changedFiles = Object.entries(plan.fingerprints).filter(([relativePath, expectedHash]) => {
584
+ const file = path.join(projectRoot, relativePath);
585
+ if (allowRecordedManifest && relativePath === migrations_1.MIGRATION_MANIFEST_PATH && plan.manifestAfterApply && fs.existsSync(file)
586
+ && fs.readFileSync(file, "utf-8") === `${JSON.stringify(plan.manifestAfterApply, null, 2)}\n`)
587
+ return false;
588
+ return (0, file_session_1.hashFileIfExists)(file) !== expectedHash;
589
+ }).map(([file]) => file);
590
+ if (changedFiles.length || (plan.baselineSnapshotChecksum
591
+ && (0, migrations_1.computeBaselineChecksum)((0, migrations_1.collectLegacyBaselineFiles)(projectRoot)) !== plan.baselineSnapshotChecksum)) {
592
+ throw new errors_1.MachineCommandError("stale-plan", `${changedFiles.join(", ") || "Baseline infrastructure files"} changed after the preview was computed. ${retryHint}`, { planId: plan.planId, changedFiles }, "blocked");
593
+ }
594
+ }
595
+ async function readMigrationState(plan, signal, secretValues = []) {
596
+ const exists = await (0, azure_cli_1.runAzCliAsync)(["group", "exists", "--name", plan.resourceGroup, ...subscriptionArgs(plan.subscription)], { timeout: 30000, signal });
597
+ if (exists.status !== 0 || exists.failureKind || !/^(true|false)$/.test(exists.stdout.trim())) {
598
+ throw azQueryError("azure-state-query-failed", "Could not query resource group", exists, secretValues);
599
+ }
600
+ if (exists.stdout.trim() === "false")
601
+ return { group: { exists: false, tags: {} }, state: null };
602
+ const show = await (0, azure_cli_1.runAzCliAsync)(["group", "show", "--name", plan.resourceGroup, ...subscriptionArgs(plan.subscription), "--query", "{id:id,tags:tags}", "-o", "json"], { timeout: 30000, signal });
603
+ if (show.status !== 0 || show.failureKind || show.stdoutTruncated)
604
+ throw azQueryError("azure-state-query-failed", "Could not read resource group tags", show, secretValues);
605
+ let parsed;
606
+ try {
607
+ parsed = JSON.parse(show.stdout);
608
+ }
609
+ catch {
610
+ throw azQueryError("azure-state-query-failed", "Invalid resource group response", show, secretValues);
611
+ }
612
+ const expectedId = `/subscriptions/${plan.subscription}/resourceGroups/${plan.resourceGroup}`;
613
+ if (typeof parsed.id !== "string" || parsed.id.toLowerCase() !== expectedId.toLowerCase()) {
614
+ throw new errors_1.MachineCommandError("azure-resource-group-mismatch", "Azure returned a different resource group/subscription. Nothing will be updated.", undefined, "blocked");
615
+ }
616
+ const group = { exists: true, id: parsed.id, tags: parsed.tags ?? {} };
617
+ const tag = plan.migrationTagKey ? group.tags[plan.migrationTagKey] : undefined;
618
+ return { group, state: tag ? (0, migrations_1.decodeMigrationState)(tag) : null };
619
+ }
620
+ function desiredTagState(command, key) {
621
+ const value = command.args.find((arg) => arg.startsWith(`${key}=`) || arg.startsWith(`tags.${key}=`));
622
+ if (!value)
623
+ throw new errors_1.MachineCommandError("incompatible-plan", "The reviewed migration tag is missing.", undefined, "blocked");
624
+ return (0, migrations_1.decodeMigrationState)(value.slice(value.indexOf("=") + 1));
381
625
  }
382
- function assertPlanRemoteState(plan, expected) {
383
- if (plan.mode === "offline")
384
- return;
385
- const current = readRemoteMigrationState(plan);
386
- if (plan.mode === "bootstrap") {
387
- if (current.group.exists) {
388
- throw new errors_1.MachineCommandError("stale-azure-state", "The resource group was created after this plan. Re-run plan provision.", undefined, "blocked");
626
+ function provisionSecretValues(plan, projectRoot) {
627
+ const files = Object.keys(plan.fingerprints);
628
+ const secureBicep = files.filter(file => file.endsWith(".bicep")).some(file => /@secure\s*\(/.test(fs.readFileSync(path.join(projectRoot, file), "utf-8")));
629
+ const values = [];
630
+ const collect = (value) => {
631
+ if (typeof value === "string" && value)
632
+ values.push(value);
633
+ else if (Array.isArray(value))
634
+ value.forEach(collect);
635
+ else if (value && typeof value === "object")
636
+ Object.values(value).forEach(collect);
637
+ };
638
+ for (const file of files.filter(file => file.endsWith(".json") && file !== migrations_1.MIGRATION_MANIFEST_PATH)) {
639
+ let parsed;
640
+ try {
641
+ parsed = JSON.parse(fs.readFileSync(path.join(projectRoot, file), "utf-8"));
642
+ }
643
+ catch {
644
+ throw new errors_1.MachineCommandError("invalid-infra-parameters", "Infrastructure JSON is invalid. Correct it before provision; unstructured Azure output cannot safely redact unknown parameter secrets.", undefined, "blocked");
645
+ }
646
+ for (const [name, parameter] of Object.entries(parsed.parameters ?? {})) {
647
+ if (secureBicep || /secret|password|token|key|connection.?string/i.test(name))
648
+ collect(parameter?.value);
389
649
  }
390
- return;
391
650
  }
392
- if (!current.group.exists || !statesEqual(current.state, expected)) {
393
- throw new errors_1.MachineCommandError("stale-azure-state", "Azure migration state changed after this plan was created. Re-run plan provision.", { expected, actual: current.state, resourceGroupExists: current.group.exists }, "blocked");
651
+ return [...new Set(values)].sort((left, right) => right.length - left.length);
652
+ }
653
+ function rememberDiagnostic(run, result, phase, secretValues) {
654
+ run.diagnostics.push((0, azure_diagnostics_1.azureCliDiagnostic)(result, phase, secretValues));
655
+ run.diagnostics = run.diagnostics.slice(-20);
656
+ if (result.stderr.trim()) {
657
+ run.warnings.push(result.stderrTruncated ? "Azure CLI stderr exceeded the capture limit; output omitted." : (0, azure_diagnostics_1.redactAzureText)(result.stderr, secretValues).slice(0, 4000));
658
+ run.warnings = run.warnings.slice(-20);
394
659
  }
395
660
  }
396
- function assertTagExpectedState(plan, expected) {
397
- const current = readRemoteMigrationState(plan);
398
- if (!current.group.exists || !statesEqual(current.state, expected)) {
399
- throw new errors_1.MachineCommandError("stale-azure-state", "Azure migration state changed before its tag could be updated. Re-run plan provision.", { expected, actual: current.state, resourceGroupExists: current.group.exists }, "blocked");
661
+ async function inspectProvisionOperation(options) {
662
+ const run = readRun(options.planId, process.cwd());
663
+ if (!run)
664
+ throw new errors_1.MachineCommandError("provision-run-not-found", "No execution checkpoint exists for this ID. For an older interrupted bootstrap use plan provision --resume-deployment <name> with the original resource group/subscription/regions.", { runId: options.planId }, "blocked");
665
+ const waitSeconds = validateWaitSeconds(options.waitSeconds ?? 0);
666
+ if (run.deployment) {
667
+ let secretValues = [];
668
+ let omitOutput = false;
669
+ try {
670
+ assertLocalFiles(run.plan, process.cwd(), "Reviewed files no longer match.", true);
671
+ secretValues = provisionSecretValues(run.plan, process.cwd());
672
+ }
673
+ catch {
674
+ omitOutput = true;
675
+ run.warnings.push("Reviewed files are unavailable or changed; deployment state is still read, but diagnostic output/causes are omitted to avoid exposing unknown secrets.");
676
+ }
677
+ run.observation = await (0, provision_deployment_1.waitForAzureDeployment)(run.deployment, { deadline: Date.now() + waitSeconds * 1000, secretValues });
678
+ if (omitOutput) {
679
+ run.observation.errors = [];
680
+ run.observation.diagnostic = { ...run.observation.diagnostic, stdout: "[output omitted: reviewed parameters unavailable]", stderr: "", outputTruncated: true };
681
+ }
682
+ run.diagnostics = [...run.diagnostics, run.observation.diagnostic].slice(-20);
683
+ let outcome = run.observation.state === "Failed" || run.observation.state === "Canceled" ? "failed"
684
+ : run.observation.state === "Succeeded" ? run.outcome === "succeeded" ? "succeeded" : "partial"
685
+ : run.observation.state === "unknown" ? "unknown" : "in-progress";
686
+ if (outcome === "succeeded") {
687
+ try {
688
+ assertLocalFiles(run.plan, process.cwd(), "The original reviewed files no longer match.", true);
689
+ const tag = [...run.plan.commandSpecs].reverse().find(command => command.kind === "tag");
690
+ if (tag && !statesEqual((await readMigrationState(run.plan, undefined, secretValues)).state, desiredTagState(tag, run.plan.migrationTagKey))) {
691
+ outcome = "partial";
692
+ run.warnings.push("Azure Succeeded, but the completed run's migration tag no longer matches. No bookkeeping or deployment write was performed.");
693
+ }
694
+ }
695
+ catch {
696
+ outcome = "partial";
697
+ run.warnings.push("Azure Succeeded, but current local files or migration bookkeeping could not be verified. No writes were performed.");
698
+ }
699
+ }
700
+ return resultForRun(run, outcome);
400
701
  }
702
+ return resultForRun(run);
401
703
  }
402
704
  async function applyProvisionOperation(options) {
403
705
  const projectRoot = process.cwd();
404
- const plan = (0, state_1.loadPlanState)(options.planId, projectRoot);
706
+ const run = /^[a-f0-9]{12}$/.test(options.planId) ? readRun(options.planId, projectRoot) : null;
707
+ const plan = run?.plan ?? (0, state_1.loadPlanState)(options.planId, projectRoot);
405
708
  if (!plan) {
406
709
  throw new errors_1.MachineCommandError("plan-not-found", `Plan "${options.planId}" was not found. Run "plan provision" to create a new plan.`, { planId: options.planId }, "blocked");
407
710
  }
408
- // Bicep が plan 後に変更されていないか確認
409
- const changedFiles = [];
410
- for (const [relativePath, expectedHash] of Object.entries(plan.fingerprints)) {
411
- const currentHash = (0, file_session_1.hashFileIfExists)(path.join(projectRoot, relativePath));
412
- if (currentHash !== expectedHash)
413
- changedFiles.push(relativePath);
414
- }
415
- if (changedFiles.length > 0) {
416
- throw new errors_1.MachineCommandError("stale-plan", `Plan "${options.planId}" is stale: ${changedFiles.join(", ")} changed after the plan was created. Re-run "plan provision".`, { planId: options.planId, changedFiles }, "blocked");
417
- }
418
- if (plan.baselineSnapshotChecksum) {
419
- const currentBaselineChecksum = (0, migrations_1.computeBaselineChecksum)((0, migrations_1.collectLegacyBaselineFiles)(projectRoot));
420
- if (currentBaselineChecksum !== plan.baselineSnapshotChecksum) {
421
- throw new errors_1.MachineCommandError("stale-plan", `Plan "${options.planId}" is stale: baseline infrastructure inventory changed after the plan was created.`, { planId: options.planId }, "blocked");
711
+ const result = await executeProvisionPlan(plan, {
712
+ approve: options.approve, waitSeconds: options.waitSeconds, waitUntilComplete: options.waitUntilComplete,
713
+ retryHint: MACHINE_RETRY_HINT, projectRoot,
714
+ });
715
+ if (result.status === "complete") {
716
+ try {
717
+ (0, state_1.deletePlanState)(plan.planId, projectRoot);
718
+ }
719
+ catch {
720
+ result.warnings.push("Azure and migration bookkeeping completed, but the old preview could not be removed. The completed execution checkpoint prevents replay.");
422
721
  }
423
722
  }
723
+ return result;
724
+ }
725
+ /**
726
+ * Runs an approved provision plan with every safety check: local files and Azure state must still
727
+ * match the reviewed preview, and each migration/tag step re-checks Azure state before it runs.
728
+ */
729
+ async function executeProvisionPlan(plan, options = {}) {
730
+ const projectRoot = options.projectRoot ?? process.cwd();
731
+ const retryHint = options.retryHint ?? MACHINE_RETRY_HINT;
732
+ const waitUntilComplete = options.waitUntilComplete === true && options.waitSeconds === undefined;
733
+ const waitSeconds = waitUntilComplete ? 0 : validateWaitSeconds(options.waitSeconds);
424
734
  if (!options.approve) {
425
- throw new errors_1.MachineCommandError("approval-required", "Provisioning creates billable Azure resources and always requires human approval. Review plan.commands and re-run with --approve.", { planId: plan.planId, commands: plan.commands }, "requires-human");
735
+ throw new errors_1.MachineCommandError("approval-required", "Provisioning creates billable Azure resources and always requires human approval. Review plan.commands and re-run with --approve.", { planId: plan.planId, commands: plan.commands }, "requires-human", [{ command: `swallowkit machine apply provision --plan ${plan.planId} --approve`, description: "Review the commands and obtain explicit human consent before applying or resuming." }]);
426
736
  }
427
737
  if (!plan.mode || !plan.commandSpecs) {
428
- throw new errors_1.MachineCommandError("incompatible-plan", "This provision plan was created by an older SwallowKit version. Re-run plan provision.", undefined, "blocked");
738
+ throw new errors_1.MachineCommandError("incompatible-plan", `This provision plan was created by an older SwallowKit version. ${retryHint}`, undefined, "blocked");
429
739
  }
430
740
  if (plan.mode === "offline") {
431
- throw new errors_1.MachineCommandError("online-plan-required", "Provision apply requires a fresh plan created while Azure CLI is available and authenticated.", undefined, "blocked");
741
+ throw new errors_1.MachineCommandError("online-plan-required", `Azure state was not read because Azure CLI was unavailable, so nothing can be applied. Make az available and signed in (az login). ${retryHint}`, { planId: plan.planId, warnings: plan.warnings }, "blocked");
432
742
  }
433
743
  if (plan.mode === "adoption-required") {
434
- throw new errors_1.MachineCommandError("adoption-required", 'This existing resource group has no SwallowKit migration state. Re-plan with --adopt-existing --baseline 0 --what-if.', { planId: plan.planId }, "requires-human");
435
- }
436
- if (!isAzCliAvailable()) {
437
- throw new errors_1.MachineCommandError("az-cli-not-found", "Azure CLI is not installed. Install it first: https://aka.ms/azure-cli", undefined, "blocked");
438
- }
439
- const executedCommands = [];
440
- const warnings = [...plan.warnings];
441
- let deploymentOutputs;
442
- assertPlanRemoteState(plan, plan.remoteState);
443
- for (const command of plan.commandSpecs) {
444
- if (command.kind === "migration") {
445
- assertPlanRemoteState(plan, command.expectedState);
446
- }
447
- if (command.kind === "tag") {
448
- assertTagExpectedState(plan, command.expectedState);
744
+ throw new errors_1.MachineCommandError("adoption-required", 'This resource group has no migration tag. Recover a matching existing baseline with --resume-deployment <name>; adoption applies only to a legacy manifest. Do not reconcile just to repair bookkeeping.', { planId: plan.planId }, "requires-human");
745
+ }
746
+ let run = readRun(plan.planId, projectRoot);
747
+ const resuming = run !== null;
748
+ if (run)
749
+ plan = run.plan;
750
+ try {
751
+ assertLocalFiles(plan, projectRoot, run ? "Restore the original reviewed files before resuming this checkpoint; do not resubmit Bicep." : retryHint, run?.cursor === plan.commandSpecs.length);
752
+ }
753
+ catch (error) {
754
+ if (run && error instanceof errors_1.MachineCommandError) {
755
+ throw new errors_1.MachineCommandError(error.code, error.message, { ...resultForRun(run), cause: error.details }, error.status, provisionNextActions(run.runId));
449
756
  }
450
- const result = runAz(command.args);
451
- const commandText = displayCommand(command);
452
- executedCommands.push(commandText);
453
- if (result.status !== 0) {
454
- throw new errors_1.MachineCommandError("provision-failed", `Command failed: ${commandText}\n${(result.stderr || result.stdout).trim().slice(0, 4000)}`, { executedCommands }, "failed");
757
+ throw error;
758
+ }
759
+ const secretValues = provisionSecretValues(plan, projectRoot);
760
+ if (!plan.subscription || plan.commandSpecs.some((command) => (command.kind === "deployment" || command.kind === "migration")
761
+ && !plan.recoveryDeployment && !command.args.includes("--no-wait"))) {
762
+ throw new errors_1.MachineCommandError("incompatible-plan", `Create a fresh preview with bounded deployment tracking. For an older interrupted deployment use --resume-deployment instead of resubmitting Bicep. ${retryHint}`, undefined, "blocked");
763
+ }
764
+ const azCli = (0, azure_cli_1.checkAzCliAvailability)();
765
+ if (!azCli.available) {
766
+ throw new errors_1.MachineCommandError(azCli.reason === "not-found" ? "az-cli-not-found" : "az-cli-not-runnable", azCliUnavailableWarning(azCli), undefined, "blocked");
767
+ }
768
+ const targetKey = crypto.createHash("sha256").update(`${plan.subscription}/${plan.resourceGroup.toLowerCase()}`).digest("hex");
769
+ let unlock;
770
+ try {
771
+ unlock = (0, state_1.lockProvisionTarget)(targetKey, projectRoot);
772
+ }
773
+ catch (error) {
774
+ throw new errors_1.MachineCommandError("provision-target-locked", error instanceof Error ? error.message : "Provision target is locked.", { runId: plan.planId }, "blocked", provisionNextActions(plan.planId));
775
+ }
776
+ const controller = new AbortController();
777
+ let interrupted;
778
+ const interrupt = (signal) => {
779
+ interrupted = signal;
780
+ controller.abort();
781
+ };
782
+ const sigint = () => interrupt("SIGINT");
783
+ const sigterm = () => interrupt("SIGTERM");
784
+ const externalAbort = () => controller.abort();
785
+ process.on("SIGINT", sigint);
786
+ process.on("SIGTERM", sigterm);
787
+ options.signal?.addEventListener("abort", externalAbort, { once: true });
788
+ if (options.signal?.aborted)
789
+ controller.abort();
790
+ const signal = controller.signal;
791
+ const deadline = waitUntilComplete ? Number.POSITIVE_INFINITY : Date.now() + waitSeconds * 1000;
792
+ const persist = () => {
793
+ if (!run)
794
+ return;
795
+ run.warnings = run.warnings.slice(-20);
796
+ run.diagnostics = run.diagnostics.slice(-20);
797
+ run.updatedAt = new Date().toISOString();
798
+ (0, state_1.saveProvisionRun)(run, projectRoot);
799
+ };
800
+ const pause = (outcome, warning) => {
801
+ run.outcome = outcome;
802
+ run.warnings.push(warning);
803
+ persist();
804
+ return resultForRun(run, outcome, interrupted);
805
+ };
806
+ const observe = async () => {
807
+ const observation = await (0, provision_deployment_1.waitForAzureDeployment)(run.deployment, {
808
+ deadline: signal.aborted ? Date.now() : deadline,
809
+ // A final short read after Ctrl+C must not inherit the aborted wait signal.
810
+ ...(signal.aborted ? {} : { signal }),
811
+ ...(signal.aborted ? { queryTimeoutMs: 5000 } : {}),
812
+ pollIntervalMs: options.pollIntervalMs,
813
+ ...(waitUntilComplete ? { transientRetries: provision_deployment_1.UNBOUNDED_WAIT_TRANSIENT_RETRIES } : {}),
814
+ secretValues,
815
+ onObservation: (value) => {
816
+ run.observation = value;
817
+ if (value.state !== "unknown")
818
+ run.deployment = value.reference;
819
+ persist();
820
+ },
821
+ });
822
+ run.observation = observation;
823
+ run.diagnostics.push(observation.diagnostic);
824
+ run.diagnostics = run.diagnostics.slice(-20);
825
+ return observation;
826
+ };
827
+ try {
828
+ if (!run) {
829
+ const unfinished = readRuns(projectRoot).find((other) => other.outcome !== "succeeded" && other.outcome !== "failed"
830
+ && other.plan.resourceGroup === plan.resourceGroup && other.plan.subscription === plan.subscription);
831
+ if (unfinished)
832
+ throw new errors_1.MachineCommandError("provision-run-pending", "Resume the existing execution checkpoint instead of creating another deployment.", { runId: unfinished.runId }, "blocked", provisionNextActions(unfinished.runId));
833
+ const current = await readMigrationState(plan, signal, secretValues);
834
+ if (plan.mode === "bootstrap" ? current.group.exists : !current.group.exists || !statesEqual(current.state, plan.remoteState)) {
835
+ throw new errors_1.MachineCommandError("stale-azure-state", plan.mode === "bootstrap"
836
+ ? `The resource group was created after the preview was computed. ${retryHint}`
837
+ : `Azure migration state changed after the preview was computed. ${retryHint}`, { expected: plan.remoteState, actual: current.state }, "blocked");
838
+ }
839
+ const checkpointPlan = { ...plan };
840
+ delete checkpointPlan.whatIfResult;
841
+ run = {
842
+ schemaVersion: 1, runId: plan.planId, projectRoot: path.resolve(projectRoot), plan: checkpointPlan,
843
+ cursor: 0, outcome: "in-progress", executedCommands: [], warnings: [...plan.warnings], diagnostics: [],
844
+ updatedAt: new Date().toISOString(),
845
+ };
846
+ persist();
455
847
  }
456
- if (command.kind === "deployment" || command.kind === "migration") {
457
- try {
458
- const deployment = JSON.parse(result.stdout || "{}");
459
- deploymentOutputs = deployment.properties?.outputs;
848
+ for (; run.cursor < plan.commandSpecs.length;) {
849
+ if (signal.aborted)
850
+ return pause(run.observation?.state === "Succeeded" ? "partial" : "unknown", "Local provisioning was interrupted. Azure may continue; use status/resume. No Azure cancel/delete was requested and the ledger is not marked successful.");
851
+ assertLocalFiles(plan, projectRoot, "Restore the original reviewed files before resuming; do not resubmit Bicep.");
852
+ const command = plan.commandSpecs[run.cursor];
853
+ if (command.kind === "deployment" || command.kind === "migration") {
854
+ if (command.kind === "migration" && run.attemptedIndex !== run.cursor && run.observation?.state === "Succeeded" && Date.now() >= deadline) {
855
+ return pause("partial", "The bounded wait budget ended before the next reviewed migration. Resume this checkpoint after approval; completed deployments will not be re-submitted.");
856
+ }
857
+ if (run.attemptedIndex !== run.cursor) {
858
+ let observationOnly = Boolean(plan.recoveryDeployment);
859
+ if (plan.recoveryDeployment) {
860
+ run.deployment = plan.recoveryDeployment;
861
+ }
862
+ else {
863
+ const current = await readMigrationState(plan, signal, secretValues);
864
+ if (!current.group.exists || !statesEqual(current.state, command.expectedState ?? plan.remoteState)) {
865
+ throw new errors_1.MachineCommandError("stale-azure-state", `Azure migration state changed before deployment. ${retryHint}`, { expected: command.expectedState ?? plan.remoteState, actual: current.state }, "blocked");
866
+ }
867
+ const name = command.args[command.args.indexOf("--name") + 1];
868
+ const reference = { name, resourceGroup: plan.resourceGroup, subscription: plan.subscription };
869
+ const previous = await (0, provision_deployment_1.queryAzureDeployment)(reference, { signal, secretValues });
870
+ if (previous.state === "unknown" && previous.reason !== "not-found") {
871
+ run.deployment = reference;
872
+ run.observation = previous;
873
+ return pause("unknown", "The deployment name could not be checked before submission. No create request was sent; inspect the original deployment before proceeding.");
874
+ }
875
+ if (previous.state !== "unknown" && (plan.mode !== "reconcile" || previous.state === "Running" || previous.state === "Accepted")) {
876
+ run.deployment = previous.reference;
877
+ run.observation = previous;
878
+ if (!resuming || command.kind !== "deployment") {
879
+ return pause("unknown", "A deployment already exists under the reviewed name. No create request was sent. Resume only after matching its template/parameters; do not overwrite or re-submit it.");
880
+ }
881
+ run.deployment = await verifyExistingBaseline({ ...plan, resumeDeployment: name }, path.join(projectRoot, "infra", "main.bicep"), path.join(projectRoot, "infra", "main.parameters.json"));
882
+ observationOnly = true;
883
+ }
884
+ else {
885
+ run.deployment = {
886
+ ...reference,
887
+ ...(previous.state !== "unknown" ? { previousCorrelationId: previous.reference.correlationId } : {}),
888
+ };
889
+ }
890
+ }
891
+ // Persist intent before spawn: even a hard process termination must never replay this create.
892
+ run.attemptedIndex = run.cursor;
893
+ persist();
894
+ if (!observationOnly) {
895
+ const result = await (0, azure_cli_1.runAzCliAsync)(command.args, { timeout: 60000, signal });
896
+ run.executedCommands.push(displayCommand(command));
897
+ rememberDiagnostic(run, result, command.kind === "migration" ? "migration-submit" : "baseline-submit", secretValues);
898
+ persist();
899
+ }
900
+ }
901
+ const observation = await observe();
902
+ if (signal.aborted)
903
+ return pause(observation.state === "Succeeded" ? "partial" : observation.state === "Running" || observation.state === "Accepted" ? "in-progress" : "unknown", "Local provisioning was interrupted. Azure may continue. The original deployment is tracked; no cancellation or successful ledger update was performed.");
904
+ if (observation.state === "Failed" || observation.state === "Canceled") {
905
+ const operations = await (0, provision_deployment_1.queryDeploymentOperationErrors)(observation.reference, secretValues, signal);
906
+ observation.errors.push(...operations.errors);
907
+ run.diagnostics.push(operations.diagnostic);
908
+ run.outcome = "failed";
909
+ persist();
910
+ throw new errors_1.MachineCommandError("azure-deployment-failed", `Azure deployment ${observation.reference.name} is ${observation.state}.${observation.errors.length ? `\n${observation.errors.map((error) => `${error.code}: ${error.message}`).join("\n")}` : " Azure did not return a structured cause; inspect deployment operations."}`, resultForRun(run), "failed", provisionNextActions(run.runId));
911
+ }
912
+ if (observation.state !== "Succeeded") {
913
+ return pause(observation.state === "unknown" ? "unknown" : "in-progress", observation.state === "unknown" ? `Azure deployment result is unknown (${observation.reason}); a local az error is not proof that Azure failed. No create will be automatically retried.`
914
+ : `Azure deployment is ${observation.state}. The bounded local wait ended; status/resume tracks the same deployment without submitting Bicep again.`);
915
+ }
460
916
  }
461
- catch {
462
- warnings.push("Could not parse deployment output JSON.");
917
+ else if (command.kind === "tag") {
918
+ if (run.deployment) {
919
+ const observation = await (0, provision_deployment_1.queryAzureDeployment)(run.deployment, { signal, secretValues });
920
+ run.observation = observation;
921
+ if (observation.state !== "Succeeded")
922
+ return pause("partial", "Azure Succeeded was previously observed, but its identity/terminal state could not be confirmed before migration tagging. No tag or ledger update was performed.");
923
+ }
924
+ const current = await readMigrationState(plan, signal, secretValues);
925
+ const desired = desiredTagState(command, plan.migrationTagKey);
926
+ if (!current.group.exists)
927
+ throw new errors_1.MachineCommandError("stale-azure-state", "The resource group disappeared before tagging.", undefined, "blocked");
928
+ if (!statesEqual(current.state, desired)) {
929
+ if (!statesEqual(current.state, command.expectedState)) {
930
+ throw new errors_1.MachineCommandError("stale-azure-state", `Azure migration state changed before its tag could be updated. ${retryHint}`, { expected: command.expectedState, actual: current.state }, "blocked");
931
+ }
932
+ run.attemptedIndex = run.cursor;
933
+ persist();
934
+ const result = await (0, azure_cli_1.runAzCliAsync)(command.args, { timeout: 60000, signal });
935
+ run.executedCommands.push(displayCommand(command));
936
+ rememberDiagnostic(run, result, "migration-tag", secretValues);
937
+ if (result.status !== 0 || result.failureKind || signal.aborted) {
938
+ return pause("partial", "Azure deployment succeeded, but migration tagging did not complete locally. Resume this checkpoint to verify/finish tagging; do not redeploy the baseline.");
939
+ }
940
+ const after = await readMigrationState(plan, signal, secretValues);
941
+ if (!statesEqual(after.state, desired))
942
+ return pause("partial", "The tag update returned successfully but its version/checksum could not be confirmed. Resume bookkeeping without redeploying Bicep.");
943
+ }
463
944
  }
945
+ else {
946
+ const current = await readMigrationState(plan, signal, secretValues);
947
+ if (run.attemptedIndex === run.cursor) {
948
+ if (!current.group.exists)
949
+ return pause("unknown", "Resource group creation was attempted, but its result is unknown. Inspect the target before resuming; nothing will be re-created automatically.");
950
+ }
951
+ else {
952
+ if (current.group.exists)
953
+ throw new errors_1.MachineCommandError("stale-azure-state", `The resource group was created after the preview was computed. ${retryHint}`, undefined, "blocked");
954
+ run.attemptedIndex = run.cursor;
955
+ persist();
956
+ const result = await (0, azure_cli_1.runAzCliAsync)(command.args, { timeout: 60000, signal });
957
+ run.executedCommands.push(displayCommand(command));
958
+ rememberDiagnostic(run, result, "resource-group-create", secretValues);
959
+ if (result.status !== 0 || result.failureKind || signal.aborted)
960
+ return pause("unknown", "Local resource-group creation did not complete. Azure may have accepted it; inspect/resume this checkpoint, not a new bootstrap.");
961
+ }
962
+ }
963
+ run.cursor++;
964
+ delete run.attemptedIndex;
965
+ persist();
966
+ }
967
+ if (signal.aborted)
968
+ return pause("partial", "Local provisioning was interrupted before ledger completion. Resume bookkeeping; no deployment will be submitted again.");
969
+ assertLocalFiles(plan, projectRoot, "Restore the reviewed files before completing the local ledger.", true);
970
+ const finalTag = [...plan.commandSpecs].reverse().find((command) => command.kind === "tag");
971
+ if (finalTag) {
972
+ const current = await readMigrationState(plan, signal, secretValues);
973
+ if (!statesEqual(current.state, desiredTagState(finalTag, plan.migrationTagKey)))
974
+ return pause("partial", "The final remote migration version/checksum changed before local ledger completion. No ledger success was recorded.");
464
975
  }
976
+ if (run.deployment) {
977
+ run.observation = await (0, provision_deployment_1.queryAzureDeployment)(run.deployment, { signal, secretValues });
978
+ if (run.observation.state !== "Succeeded")
979
+ return pause("partial", "The original deployment identity/terminal state could not be confirmed before local ledger completion. No successful ledger update was performed.");
980
+ }
981
+ if (plan.manifestAfterApply && run.outcome !== "succeeded")
982
+ (0, migrations_1.replaceMigrationManifest)(plan.manifestAfterApply, projectRoot);
983
+ run.outcome = "succeeded";
984
+ persist();
985
+ return resultForRun(run);
465
986
  }
466
- if (plan.manifestAfterApply) {
467
- (0, migrations_1.replaceMigrationManifest)(plan.manifestAfterApply, projectRoot);
987
+ catch (error) {
988
+ if (!run)
989
+ throw error;
990
+ if (error instanceof errors_1.MachineCommandError && error.code === "azure-deployment-failed")
991
+ throw error;
992
+ run.outcome = run.observation?.state === "Succeeded" ? "partial" : "unknown";
993
+ run.warnings.push(`Provisioning stopped: ${(0, azure_diagnostics_1.redactAzureText)(error instanceof Error ? error.message : "Unknown local error", secretValues)}. Azure deployment and local bookkeeping are separate; inspect/resume this checkpoint.`);
994
+ try {
995
+ persist();
996
+ }
997
+ catch {
998
+ run.warnings.push("The checkpoint could not be updated. Preserve the existing checkpoint; inspect the original Azure deployment before any further write.");
999
+ }
1000
+ if (error instanceof errors_1.MachineCommandError) {
1001
+ throw new errors_1.MachineCommandError(error.code, error.message, { ...resultForRun(run), cause: error.details }, error.status ?? "blocked", provisionNextActions(run.runId));
1002
+ }
1003
+ return resultForRun(run, run.outcome, interrupted);
1004
+ }
1005
+ finally {
1006
+ process.removeListener("SIGINT", sigint);
1007
+ process.removeListener("SIGTERM", sigterm);
1008
+ options.signal?.removeEventListener("abort", externalAbort);
1009
+ try {
1010
+ unlock();
1011
+ }
1012
+ catch {
1013
+ run?.warnings.push("The local execution lock could not be removed. Inspect its owner before retrying; Azure deployment/ledger results are unchanged.");
1014
+ console.warn("Provision lock cleanup failed; inspect the checkpoint before another apply.");
1015
+ }
468
1016
  }
469
- (0, state_1.deletePlanState)(plan.planId, projectRoot);
470
- return {
471
- planId: plan.planId,
472
- resourceGroup: plan.resourceGroup,
473
- location: plan.location,
474
- swaLocation: plan.swaLocation,
475
- executedCommands,
476
- ...(deploymentOutputs ? { deploymentOutputs } : {}),
477
- warnings,
478
- };
479
1017
  }
480
1018
  //# sourceMappingURL=provision-operations.js.map