@miosa/cli 1.1.4 → 1.1.6

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 (55) hide show
  1. package/README.md +118 -0
  2. package/dist/app-contract.d.ts +92 -0
  3. package/dist/app-contract.d.ts.map +1 -0
  4. package/dist/app-contract.js +219 -0
  5. package/dist/app-contract.js.map +1 -0
  6. package/dist/app-manifest.d.ts +50 -0
  7. package/dist/app-manifest.d.ts.map +1 -1
  8. package/dist/app-manifest.js +106 -0
  9. package/dist/app-manifest.js.map +1 -1
  10. package/dist/app-operation.d.ts +23 -0
  11. package/dist/app-operation.d.ts.map +1 -0
  12. package/dist/app-operation.js +49 -0
  13. package/dist/app-operation.js.map +1 -0
  14. package/dist/app-release.d.ts +132 -0
  15. package/dist/app-release.d.ts.map +1 -0
  16. package/dist/app-release.js +294 -0
  17. package/dist/app-release.js.map +1 -0
  18. package/dist/bin/miosa.js +13 -0
  19. package/dist/bin/miosa.js.map +1 -1
  20. package/dist/commands/app.d.ts +35 -0
  21. package/dist/commands/app.d.ts.map +1 -1
  22. package/dist/commands/app.js +726 -1
  23. package/dist/commands/app.js.map +1 -1
  24. package/dist/commands/capabilities.d.ts.map +1 -1
  25. package/dist/commands/capabilities.js +82 -7
  26. package/dist/commands/capabilities.js.map +1 -1
  27. package/dist/commands/context.d.ts.map +1 -1
  28. package/dist/commands/context.js +3 -1
  29. package/dist/commands/context.js.map +1 -1
  30. package/dist/commands/docker-deploy.d.ts.map +1 -1
  31. package/dist/commands/docker-deploy.js +174 -10
  32. package/dist/commands/docker-deploy.js.map +1 -1
  33. package/dist/commands/operating-contract.d.ts +3 -0
  34. package/dist/commands/operating-contract.d.ts.map +1 -0
  35. package/dist/commands/operating-contract.js +651 -0
  36. package/dist/commands/operating-contract.js.map +1 -0
  37. package/dist/commands/pull.d.ts +11 -0
  38. package/dist/commands/pull.d.ts.map +1 -1
  39. package/dist/commands/pull.js +3 -3
  40. package/dist/commands/pull.js.map +1 -1
  41. package/dist/commands/releases.d.ts.map +1 -1
  42. package/dist/commands/releases.js +12 -4
  43. package/dist/commands/releases.js.map +1 -1
  44. package/dist/commands/sandbox.d.ts +28 -0
  45. package/dist/commands/sandbox.d.ts.map +1 -1
  46. package/dist/commands/sandbox.js +1 -1
  47. package/dist/commands/sandbox.js.map +1 -1
  48. package/dist/config.d.ts +2 -1
  49. package/dist/config.d.ts.map +1 -1
  50. package/dist/config.js +27 -5
  51. package/dist/config.js.map +1 -1
  52. package/dist/types.d.ts +7 -1
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js.map +1 -1
  55. package/package.json +1 -1
@@ -0,0 +1,651 @@
1
+ import chalk from "chalk";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { approvePlan, assertPlanApplicable, contractFingerprint, createSavedPlan, detectContractDrift, loadPlan, resolveApplicationContract, savePlan, } from "../app-contract.js";
6
+ import { loadAppManifest, validateProjectManifest } from "../app-manifest.js";
7
+ import { MiosaClient } from "../client.js";
8
+ import { loadConfig } from "../config.js";
9
+ import { UserError } from "../errors.js";
10
+ import { activateLinkedRelease, getRelease, requireApplicationLink, } from "./app.js";
11
+ import { handleError, isJsonMode, printJson } from "./util.js";
12
+ function record(value) {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value)
14
+ ? value
15
+ : {};
16
+ }
17
+ function payload(value) {
18
+ const outer = record(value);
19
+ const data = record(outer["data"]);
20
+ return Object.keys(data).length > 0 ? data : outer;
21
+ }
22
+ function stringValue(value, key) {
23
+ const found = record(value)[key];
24
+ return typeof found === "string" && found.trim() ? found.trim() : undefined;
25
+ }
26
+ function output(opts, data, message) {
27
+ if (isJsonMode(opts)) {
28
+ printJson({ ok: true, data, error: null });
29
+ }
30
+ else if (message) {
31
+ console.log(message);
32
+ }
33
+ else {
34
+ console.log(JSON.stringify(data, null, 2));
35
+ }
36
+ }
37
+ function releaseIdentity(release) {
38
+ const metadata = record(release["metadata"]);
39
+ const versionId = stringValue(release, "deployment_version_id") ??
40
+ stringValue(release, "version_id");
41
+ const digest = stringValue(release, "artifact_sha256") ??
42
+ stringValue(release, "archive_sha256") ??
43
+ stringValue(metadata, "artifact_sha256");
44
+ if (!versionId || !digest) {
45
+ throw new UserError("The candidate is missing an immutable version ID or artifact digest.");
46
+ }
47
+ return { versionId, digest };
48
+ }
49
+ function actorName(value) {
50
+ return (value ??
51
+ process.env["MIOSA_APPROVER"] ??
52
+ process.env["USER"] ??
53
+ os.userInfo().username);
54
+ }
55
+ function listJsonFiles(dir) {
56
+ if (!fs.existsSync(dir))
57
+ return [];
58
+ return fs
59
+ .readdirSync(dir)
60
+ .filter((file) => file.endsWith(".json"))
61
+ .sort();
62
+ }
63
+ function planPath(appDir, plan) {
64
+ return path.join(appDir, ".miosa", "plans", `${plan.plan_id}.json`);
65
+ }
66
+ async function assertPrePromotionGates(client, plan) {
67
+ const [release, deployment, environment] = await Promise.all([
68
+ getRelease(client, plan.scope.deployment_id, plan.release.id),
69
+ deploymentState(client, plan.scope.deployment_id),
70
+ client.apiGet(`/api/v1/deployments/${encodeURIComponent(plan.scope.deployment_id)}/env`),
71
+ ]);
72
+ const identity = releaseIdentity(release);
73
+ const releaseMetadata = record(release["metadata"]);
74
+ const deploymentMetadata = record(deployment["metadata"]);
75
+ const failures = [];
76
+ if (identity.versionId !== plan.release.version_id ||
77
+ identity.digest !== plan.release.artifact_sha256) {
78
+ failures.push("candidate release identity no longer matches the saved plan");
79
+ }
80
+ const environmentNames = new Set((environment.data ?? [])
81
+ .map((item) => item.name)
82
+ .filter((name) => Boolean(name)));
83
+ for (const secret of plan.capabilities.secrets) {
84
+ if (secret.required !== false && !environmentNames.has(secret.name)) {
85
+ failures.push(`required secret ${secret.name} is not bound`);
86
+ }
87
+ }
88
+ if (plan.capabilities.database?.required &&
89
+ !environmentNames.has("DATABASE_URL") &&
90
+ !stringValue(deployment, "database_id") &&
91
+ !stringValue(deploymentMetadata, "database_id")) {
92
+ failures.push("required database is not attached");
93
+ }
94
+ if (plan.capabilities.database?.migration?.required &&
95
+ releaseMetadata["migration_verified"] !== true &&
96
+ stringValue(releaseMetadata, "migration_status") !== "succeeded") {
97
+ failures.push("required database migration evidence is missing");
98
+ }
99
+ for (const connector of plan.capabilities.connectors) {
100
+ if (connector.required === false)
101
+ continue;
102
+ const response = payload(await client
103
+ .apiPost(`/api/v1/deployments/${encodeURIComponent(plan.scope.deployment_id)}/connectors/preflight`, { connector: connector.id })
104
+ .catch(() => ({})));
105
+ if (record(response["status"])["bound"] !== true) {
106
+ failures.push(`required connector ${connector.id} is not bound`);
107
+ }
108
+ }
109
+ const jobs = [
110
+ deployment["scheduled_jobs"],
111
+ deploymentMetadata["scheduled_jobs"],
112
+ ].flatMap((value) => (Array.isArray(value) ? value : []));
113
+ const healthyJobs = new Set(jobs.flatMap((candidate) => {
114
+ const job = record(candidate);
115
+ const id = stringValue(job, "id") ?? stringValue(job, "name");
116
+ const healthy = job["enabled"] !== false &&
117
+ job["paused"] !== true &&
118
+ stringValue(job, "status") !== "failed" &&
119
+ stringValue(job, "last_run_status") !== "failed";
120
+ return id && healthy ? [id] : [];
121
+ }));
122
+ for (const job of plan.capabilities.jobs) {
123
+ if (job.required !== false && !healthyJobs.has(job.id)) {
124
+ failures.push(`required job ${job.id} is not registered and healthy`);
125
+ }
126
+ }
127
+ const suppliedBusinessEvidence = new Set(Array.isArray(releaseMetadata["business_capability_evidence"])
128
+ ? releaseMetadata["business_capability_evidence"].filter((value) => typeof value === "string")
129
+ : []);
130
+ for (const capability of plan.capabilities.business) {
131
+ if (capability.required !== false &&
132
+ !suppliedBusinessEvidence.has(capability.id)) {
133
+ failures.push(`business capability ${capability.id} lacks candidate evidence`);
134
+ }
135
+ }
136
+ if (failures.length > 0) {
137
+ throw new UserError(`Promotion gates blocked ${plan.release.id}: ${failures.join("; ")}.`, "Repair the declared binding or attach candidate evidence, then create a new immutable plan.");
138
+ }
139
+ }
140
+ async function deploymentState(client, deploymentId) {
141
+ return payload(await client.apiGet(`/api/v1/deployments/${encodeURIComponent(deploymentId)}`));
142
+ }
143
+ export function register(program) {
144
+ const blueprint = program
145
+ .command("blueprint")
146
+ .description("Validate and inspect miosa.app.yml capability blueprints");
147
+ blueprint
148
+ .command("validate")
149
+ .argument("[path]", "Application directory", ".")
150
+ .option("--json", "Output stable JSON")
151
+ .action((inputPath, opts) => {
152
+ try {
153
+ const dir = path.resolve(inputPath);
154
+ const loaded = loadAppManifest(dir);
155
+ if (!loaded)
156
+ throw new UserError("miosa.app.yml was not found.");
157
+ const issues = validateProjectManifest(loaded.manifest);
158
+ const result = {
159
+ path: loaded.path,
160
+ valid: issues.length === 0,
161
+ issues,
162
+ capabilities: loaded.manifest.capabilities ?? null,
163
+ policy: loaded.manifest.policy ?? null,
164
+ };
165
+ if (issues.length > 0)
166
+ process.exitCode = 1;
167
+ output(opts, result);
168
+ }
169
+ catch (error) {
170
+ handleError(error);
171
+ }
172
+ });
173
+ blueprint
174
+ .command("show")
175
+ .argument("[path]", "Application directory", ".")
176
+ .option("--json", "Output stable JSON")
177
+ .action((inputPath, opts) => {
178
+ try {
179
+ const dir = path.resolve(inputPath);
180
+ const link = requireApplicationLink(dir);
181
+ output(opts, resolveApplicationContract(dir, link));
182
+ }
183
+ catch (error) {
184
+ handleError(error);
185
+ }
186
+ });
187
+ const changes = program
188
+ .command("changes")
189
+ .description("Create, approve, inspect, and exactly apply immutable release plans");
190
+ changes
191
+ .command("plan <release-id>")
192
+ .argument("[path]", "Application directory", ".")
193
+ .option("--json", "Output stable JSON")
194
+ .action(async (releaseId, inputPath, opts) => {
195
+ try {
196
+ const dir = path.resolve(inputPath);
197
+ const link = requireApplicationLink(dir);
198
+ const contract = resolveApplicationContract(dir, link);
199
+ const client = new MiosaClient(loadConfig());
200
+ const [release, deployment] = await Promise.all([
201
+ getRelease(client, link.deploymentId, releaseId),
202
+ deploymentState(client, link.deploymentId),
203
+ ]);
204
+ const identity = releaseIdentity(release);
205
+ let plan = createSavedPlan(dir, {
206
+ scope: contract.scope,
207
+ release: {
208
+ id: releaseId,
209
+ version_id: identity.versionId,
210
+ artifact_sha256: identity.digest,
211
+ rollback_version_id: stringValue(deployment, "active_version_id") ?? null,
212
+ },
213
+ desired_state: {
214
+ route: {
215
+ public_url: stringValue(deployment, "public_url") ??
216
+ stringValue(record(deployment["docker_deploy_app"]), "public_url") ??
217
+ stringValue(deployment, "auto_subdomain") ??
218
+ null,
219
+ },
220
+ archive: { artifact_sha256: identity.digest },
221
+ host: {
222
+ id: stringValue(deployment, "docker_deploy_host_id") ??
223
+ stringValue(record(deployment["docker_deploy_app"]), "docker_deploy_host_id") ??
224
+ null,
225
+ status: "active",
226
+ appliance_status: "healthy",
227
+ },
228
+ database: {
229
+ attached: contract.capabilities.database?.required === true,
230
+ },
231
+ connectors: {
232
+ ids: contract.capabilities.connectors
233
+ .filter((connector) => connector.required !== false)
234
+ .map((connector) => connector.id)
235
+ .sort(),
236
+ },
237
+ jobs: {
238
+ ids: contract.capabilities.jobs
239
+ .filter((job) => job.required !== false)
240
+ .map((job) => job.id)
241
+ .sort(),
242
+ },
243
+ policy: { fingerprint: contractFingerprint(contract.policy) },
244
+ },
245
+ capabilities: contract.capabilities,
246
+ policy: contract.policy,
247
+ });
248
+ const durable = payload(await client.apiPost(`/api/v1/deployments/${encodeURIComponent(link.deploymentId)}/plans`, { plan }, { "Idempotency-Key": `plan:${plan.fingerprint}` }));
249
+ const durableId = stringValue(durable, "id");
250
+ if (!durableId) {
251
+ throw new UserError("The control plane did not persist the immutable application plan.");
252
+ }
253
+ plan = { ...plan, control_plane_plan_id: durableId };
254
+ savePlan(dir, plan);
255
+ output(opts, { plan, plan_path: planPath(dir, plan) });
256
+ }
257
+ catch (error) {
258
+ handleError(error);
259
+ }
260
+ });
261
+ changes
262
+ .command("approve <plan-id>")
263
+ .argument("[path]", "Application directory", ".")
264
+ .option("--actor <identity>", "Approval actor identity")
265
+ .option("--json", "Output stable JSON")
266
+ .action(async (planId, inputPath, opts) => {
267
+ try {
268
+ const dir = path.resolve(inputPath);
269
+ let plan = approvePlan(dir, loadPlan(dir, planId), actorName(opts.actor));
270
+ if (!plan.control_plane_plan_id) {
271
+ throw new UserError(`Saved plan ${plan.plan_id} has no durable control-plane record.`, "Create a new plan with `miosa changes plan`.");
272
+ }
273
+ const controlPlanePlanId = plan.control_plane_plan_id;
274
+ const client = new MiosaClient(loadConfig());
275
+ const durable = payload(await client.apiPost(`/api/v1/application-plans/${encodeURIComponent(controlPlanePlanId)}/approve`, {
276
+ fingerprint: plan.fingerprint,
277
+ actor: actorName(opts.actor),
278
+ }, { "Idempotency-Key": `approve:${plan.fingerprint}:${actorName(opts.actor)}` }));
279
+ plan = { ...plan, control_plane_plan_id: stringValue(durable, "id") ?? plan.control_plane_plan_id };
280
+ savePlan(dir, plan);
281
+ output(opts, { plan, plan_path: planPath(dir, plan) });
282
+ }
283
+ catch (error) {
284
+ handleError(error);
285
+ }
286
+ });
287
+ changes
288
+ .command("show <plan-id>")
289
+ .argument("[path]", "Application directory", ".")
290
+ .option("--json", "Output stable JSON")
291
+ .action((planId, inputPath, opts) => {
292
+ try {
293
+ output(opts, loadPlan(path.resolve(inputPath), planId));
294
+ }
295
+ catch (error) {
296
+ handleError(error);
297
+ }
298
+ });
299
+ changes
300
+ .command("apply <plan-id>")
301
+ .argument("[path]", "Application directory", ".")
302
+ .option("--timeout <seconds>", "Activation timeout", Number, 600)
303
+ .option("--json", "Output stable JSON")
304
+ .action(async (planId, inputPath, opts) => {
305
+ try {
306
+ const dir = path.resolve(inputPath);
307
+ const link = requireApplicationLink(dir);
308
+ const contract = resolveApplicationContract(dir, link);
309
+ let plan = loadPlan(dir, planId);
310
+ assertPlanApplicable(plan, contract.scope);
311
+ if (!plan.control_plane_plan_id) {
312
+ throw new UserError(`Saved plan ${plan.plan_id} has no durable control-plane record.`);
313
+ }
314
+ const controlPlanePlanId = plan.control_plane_plan_id;
315
+ const client = new MiosaClient(loadConfig());
316
+ await assertPrePromotionGates(client, plan);
317
+ await client.apiPost(`/api/v1/application-plans/${encodeURIComponent(controlPlanePlanId)}/apply`, { fingerprint: plan.fingerprint }, { "Idempotency-Key": `apply:${plan.fingerprint}` });
318
+ plan = { ...plan, state: "applying", updated_at: new Date().toISOString() };
319
+ savePlan(dir, plan);
320
+ try {
321
+ const result = await activateLinkedRelease({
322
+ action: "promote",
323
+ dir,
324
+ link,
325
+ releaseId: plan.release.id,
326
+ timeout: opts.timeout,
327
+ idempotencyKey: `plan:${plan.fingerprint}`,
328
+ verificationEvidence: {
329
+ migration_verified: true,
330
+ policy_verified: true,
331
+ },
332
+ });
333
+ plan = {
334
+ ...plan,
335
+ state: result.receipt.result === "verified" ? "verified" : "blocked",
336
+ updated_at: new Date().toISOString(),
337
+ };
338
+ savePlan(dir, plan);
339
+ await client.apiPost(`/api/v1/application-plans/${encodeURIComponent(controlPlanePlanId)}/complete`, {
340
+ result: result.receipt.result,
341
+ receipt: result.receipt,
342
+ }, { "Idempotency-Key": `complete:${plan.fingerprint}` });
343
+ output(opts, { plan, ...result });
344
+ }
345
+ catch (error) {
346
+ savePlan(dir, {
347
+ ...plan,
348
+ state: "failed",
349
+ updated_at: new Date().toISOString(),
350
+ });
351
+ await client
352
+ .apiPost(`/api/v1/application-plans/${encodeURIComponent(controlPlanePlanId)}/complete`, {
353
+ result: "failed",
354
+ receipt: {
355
+ error: error instanceof Error ? error.message : String(error),
356
+ },
357
+ }, { "Idempotency-Key": `complete:${plan.fingerprint}:failed` })
358
+ .catch(() => undefined);
359
+ throw error;
360
+ }
361
+ }
362
+ catch (error) {
363
+ handleError(error);
364
+ }
365
+ });
366
+ const placement = program
367
+ .command("placement")
368
+ .description("Inspect the exact runtime placement for a linked application");
369
+ placement
370
+ .command("show")
371
+ .argument("[path]", "Application directory", ".")
372
+ .option("--json", "Output stable JSON")
373
+ .action(async (inputPath, opts) => {
374
+ try {
375
+ const link = requireApplicationLink(path.resolve(inputPath));
376
+ const deployment = await deploymentState(new MiosaClient(loadConfig()), link.deploymentId);
377
+ output(opts, {
378
+ deployment_id: link.deploymentId,
379
+ product: stringValue(deployment, "deployment_product") ?? null,
380
+ host_id: stringValue(deployment, "docker_deploy_host_id") ?? null,
381
+ active_release_id: stringValue(deployment, "active_release_id") ?? null,
382
+ active_version_id: stringValue(deployment, "active_version_id") ?? null,
383
+ public_url: stringValue(deployment, "public_url") ?? null,
384
+ state: stringValue(deployment, "state") ?? "unknown",
385
+ });
386
+ }
387
+ catch (error) {
388
+ handleError(error);
389
+ }
390
+ });
391
+ const drift = program
392
+ .command("drift")
393
+ .description("Detect and report drift from an approved immutable plan");
394
+ drift
395
+ .command("detect <plan-id>")
396
+ .argument("[path]", "Application directory", ".")
397
+ .option("--json", "Output stable JSON")
398
+ .action(async (planId, inputPath, opts) => {
399
+ try {
400
+ const dir = path.resolve(inputPath);
401
+ const plan = loadPlan(dir, planId);
402
+ const client = new MiosaClient(loadConfig());
403
+ const [deployment, environment] = await Promise.all([
404
+ deploymentState(client, plan.scope.deployment_id),
405
+ client.apiGet(`/api/v1/deployments/${encodeURIComponent(plan.scope.deployment_id)}/env`),
406
+ ]);
407
+ const currentContract = resolveApplicationContract(dir, requireApplicationLink(dir));
408
+ const hostId = stringValue(deployment, "docker_deploy_host_id") ??
409
+ stringValue(record(deployment["docker_deploy_app"]), "docker_deploy_host_id");
410
+ const host = hostId
411
+ ? payload(await client
412
+ .apiGet(`/api/v1/docker-deploy/hosts/${encodeURIComponent(hostId)}`)
413
+ .catch(() => ({})))
414
+ : {};
415
+ const connectorIds = (await Promise.all(plan.capabilities.connectors.map(async (connector) => {
416
+ const response = payload(await client
417
+ .apiPost(`/api/v1/deployments/${encodeURIComponent(plan.scope.deployment_id)}/connectors/preflight`, { connector: connector.id })
418
+ .catch(() => ({})));
419
+ return record(response["status"])["bound"] === true
420
+ ? connector.id
421
+ : null;
422
+ })))
423
+ .filter((id) => Boolean(id))
424
+ .sort();
425
+ const deploymentMetadata = record(deployment["metadata"]);
426
+ const dockerApp = record(deployment["docker_deploy_app"]);
427
+ const runningDigest = stringValue(deployment, "running_artifact_sha256") ??
428
+ stringValue(dockerApp, "artifact_sha256") ??
429
+ stringValue(deploymentMetadata, "running_artifact_sha256") ??
430
+ null;
431
+ const actualJobs = [
432
+ deployment["scheduled_jobs"],
433
+ deploymentMetadata["scheduled_jobs"],
434
+ ]
435
+ .flatMap((value) => (Array.isArray(value) ? value : []))
436
+ .flatMap((candidate) => {
437
+ const job = record(candidate);
438
+ const id = stringValue(job, "id") ?? stringValue(job, "name");
439
+ const healthy = job["enabled"] !== false &&
440
+ job["paused"] !== true &&
441
+ stringValue(job, "status") !== "failed" &&
442
+ stringValue(job, "last_run_status") !== "failed";
443
+ return id && healthy ? [id] : [];
444
+ })
445
+ .sort();
446
+ const publicUrl = stringValue(deployment, "public_url") ??
447
+ stringValue(record(deployment["docker_deploy_app"]), "public_url") ??
448
+ stringValue(deployment, "auto_subdomain") ??
449
+ null;
450
+ const envNames = new Set((environment.data ?? [])
451
+ .map((item) => item.name)
452
+ .filter((name) => Boolean(name)));
453
+ const actual = {
454
+ scope: currentContract.scope,
455
+ release: {
456
+ id: stringValue(deployment, "active_release_id") ?? null,
457
+ version_id: stringValue(deployment, "active_version_id") ?? null,
458
+ artifact_sha256: runningDigest,
459
+ },
460
+ desired_state: {
461
+ route: { public_url: publicUrl },
462
+ archive: {
463
+ artifact_sha256: runningDigest,
464
+ },
465
+ host: {
466
+ id: hostId ?? null,
467
+ status: stringValue(host, "status") ?? (hostId ? "unknown" : "active"),
468
+ appliance_status: stringValue(host, "appliance_status") ??
469
+ (hostId ? "unknown" : "healthy"),
470
+ },
471
+ database: {
472
+ attached: envNames.has("DATABASE_URL") ||
473
+ Boolean(stringValue(deployment, "database_id") ??
474
+ stringValue(deploymentMetadata, "database_id")),
475
+ },
476
+ connectors: { ids: connectorIds },
477
+ jobs: { ids: actualJobs },
478
+ policy: {
479
+ fingerprint: contractFingerprint(currentContract.policy),
480
+ },
481
+ },
482
+ };
483
+ const expected = {
484
+ scope: plan.scope,
485
+ release: {
486
+ id: plan.release.id,
487
+ version_id: plan.release.version_id,
488
+ artifact_sha256: plan.release.artifact_sha256,
489
+ },
490
+ desired_state: plan.desired_state,
491
+ };
492
+ const items = detectContractDrift(expected, actual);
493
+ output(opts, {
494
+ plan_id: plan.plan_id,
495
+ drifted: items.length > 0,
496
+ items,
497
+ reconciliation: items.length === 0
498
+ ? null
499
+ : `miosa changes apply ${plan.plan_id} . --json`,
500
+ });
501
+ if (items.length > 0)
502
+ process.exitCode = 2;
503
+ }
504
+ catch (error) {
505
+ handleError(error);
506
+ }
507
+ });
508
+ drift
509
+ .command("reconcile <plan-id>")
510
+ .description("Print the exact approved reconciliation action")
511
+ .argument("[path]", "Application directory", ".")
512
+ .option("--json", "Output stable JSON")
513
+ .action((planId, inputPath, opts) => {
514
+ try {
515
+ const plan = loadPlan(path.resolve(inputPath), planId);
516
+ output(opts, {
517
+ automatic: false,
518
+ reason: "Reconciliation is a production mutation and requires exact apply.",
519
+ command: `miosa changes apply ${plan.plan_id} . --json`,
520
+ });
521
+ }
522
+ catch (error) {
523
+ handleError(error);
524
+ }
525
+ });
526
+ const policy = program
527
+ .command("policy")
528
+ .description("Evaluate capability and approval policy");
529
+ policy
530
+ .command("check")
531
+ .argument("[path]", "Application directory", ".")
532
+ .option("--json", "Output stable JSON")
533
+ .action((inputPath, opts) => {
534
+ try {
535
+ const dir = path.resolve(inputPath);
536
+ const contract = resolveApplicationContract(dir, requireApplicationLink(dir));
537
+ output(opts, {
538
+ allowed: contract.policy.allowed_environments.length === 0 ||
539
+ contract.policy.allowed_environments.includes(contract.scope.environment),
540
+ scope: contract.scope,
541
+ policy: contract.policy,
542
+ });
543
+ }
544
+ catch (error) {
545
+ handleError(error);
546
+ }
547
+ });
548
+ const evidence = program
549
+ .command("evidence")
550
+ .description("Inspect durable release receipts and verification evidence");
551
+ evidence
552
+ .command("list")
553
+ .argument("[path]", "Application directory", ".")
554
+ .option("--json", "Output stable JSON")
555
+ .action((inputPath, opts) => {
556
+ const dir = path.join(path.resolve(inputPath), ".miosa", "receipts");
557
+ output(opts, { receipts: listJsonFiles(dir), path: dir });
558
+ });
559
+ evidence
560
+ .command("show <receipt-id>")
561
+ .argument("[path]", "Application directory", ".")
562
+ .option("--json", "Output stable JSON")
563
+ .action((receiptId, inputPath, opts) => {
564
+ try {
565
+ const file = path.join(path.resolve(inputPath), ".miosa", "receipts", `${receiptId}.json`);
566
+ if (!fs.existsSync(file))
567
+ throw new UserError(`Receipt not found: ${receiptId}`);
568
+ output(opts, JSON.parse(fs.readFileSync(file, "utf8")));
569
+ }
570
+ catch (error) {
571
+ handleError(error);
572
+ }
573
+ });
574
+ const incidents = program
575
+ .command("incidents")
576
+ .description("Inspect failed or blocked local deployment operations");
577
+ incidents
578
+ .command("list")
579
+ .argument("[path]", "Application directory", ".")
580
+ .option("--json", "Output stable JSON")
581
+ .action((inputPath, opts) => {
582
+ const dir = path.join(path.resolve(inputPath), ".miosa", "operations");
583
+ const incidents = listJsonFiles(dir)
584
+ .map((file) => JSON.parse(fs.readFileSync(path.join(dir, file), "utf8")))
585
+ .filter((item) => item.state === "failed" || item.state === "blocked");
586
+ output(opts, { incidents, count: incidents.length });
587
+ });
588
+ const wait = program
589
+ .command("wait")
590
+ .description("Wait for a durable server operation to reach a terminal state");
591
+ wait
592
+ .command("operation <operation-id>")
593
+ .option("--timeout <seconds>", "Wait timeout", Number, 600)
594
+ .option("--json", "Output stable JSON")
595
+ .action(async (operationId, opts) => {
596
+ try {
597
+ const client = new MiosaClient(loadConfig());
598
+ const deadline = Date.now() + opts.timeout * 1_000;
599
+ let operation = {};
600
+ do {
601
+ operation = payload(await client.apiGet(`/api/v1/operations/${encodeURIComponent(operationId)}`));
602
+ const status = stringValue(operation, "status");
603
+ if (["succeeded", "failed", "canceled"].includes(status ?? "")) {
604
+ output(opts, operation);
605
+ return;
606
+ }
607
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
608
+ } while (Date.now() < deadline);
609
+ throw new UserError(`Timed out waiting for operation ${operationId}.`);
610
+ }
611
+ catch (error) {
612
+ handleError(error);
613
+ }
614
+ });
615
+ const memory = program
616
+ .command("memory")
617
+ .description("Record and inspect local operator decisions for an application");
618
+ memory
619
+ .command("record <message>")
620
+ .argument("[path]", "Application directory", ".")
621
+ .option("--json", "Output stable JSON")
622
+ .action((message, inputPath, opts) => {
623
+ const dir = path.join(path.resolve(inputPath), ".miosa");
624
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
625
+ const file = path.join(dir, "memory.jsonl");
626
+ const entry = {
627
+ recorded_at: new Date().toISOString(),
628
+ actor: actorName(),
629
+ message,
630
+ };
631
+ fs.appendFileSync(file, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
632
+ output(opts, entry, chalk.green("Decision recorded."));
633
+ });
634
+ memory
635
+ .command("list")
636
+ .argument("[path]", "Application directory", ".")
637
+ .option("--json", "Output stable JSON")
638
+ .action((inputPath, opts) => {
639
+ const file = path.join(path.resolve(inputPath), ".miosa", "memory.jsonl");
640
+ const entries = fs.existsSync(file)
641
+ ? fs
642
+ .readFileSync(file, "utf8")
643
+ .trim()
644
+ .split("\n")
645
+ .filter(Boolean)
646
+ .map((line) => JSON.parse(line))
647
+ : [];
648
+ output(opts, { entries });
649
+ });
650
+ }
651
+ //# sourceMappingURL=operating-contract.js.map