@m-kopa/launchpad-cli 0.56.1 → 0.60.1

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 (36) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/bundle/upload.d.ts +2 -0
  3. package/dist/bundle/upload.d.ts.map +1 -1
  4. package/dist/cli.js +656 -147
  5. package/dist/commands/deploy.d.ts +15 -0
  6. package/dist/commands/deploy.d.ts.map +1 -1
  7. package/dist/commands/init.d.ts +3 -0
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/status.d.ts +14 -1
  10. package/dist/commands/status.d.ts.map +1 -1
  11. package/dist/commands/validate.d.ts.map +1 -1
  12. package/dist/deploy/apply.d.ts +6 -0
  13. package/dist/deploy/apply.d.ts.map +1 -1
  14. package/dist/deploy/commit-match-settle.d.ts +14 -1
  15. package/dist/deploy/commit-match-settle.d.ts.map +1 -1
  16. package/dist/deploy/deployment-status.d.ts +19 -0
  17. package/dist/deploy/deployment-status.d.ts.map +1 -1
  18. package/dist/deploy/manifest-diff.d.ts +2 -0
  19. package/dist/deploy/manifest-diff.d.ts.map +1 -1
  20. package/dist/deploy/verify-deploy.d.ts +2 -0
  21. package/dist/deploy/verify-deploy.d.ts.map +1 -1
  22. package/dist/manifest/load.d.ts +27 -0
  23. package/dist/manifest/load.d.ts.map +1 -1
  24. package/dist/secrets/push.d.ts +8 -0
  25. package/dist/secrets/push.d.ts.map +1 -1
  26. package/dist/secrets/status.d.ts.map +1 -1
  27. package/dist/version.d.ts +1 -1
  28. package/package.json +1 -1
  29. package/skills/launchpad-content-pr/SKILL.md +1 -1
  30. package/skills/launchpad-deploy/SKILL.md +32 -10
  31. package/skills/launchpad-deploy-status/SKILL.md +10 -1
  32. package/skills/launchpad-destroy/SKILL.md +1 -1
  33. package/skills/launchpad-identity/SKILL.md +1 -1
  34. package/skills/launchpad-onboard/SKILL.md +13 -1
  35. package/skills/launchpad-report/SKILL.md +1 -1
  36. package/skills/launchpad-status/SKILL.md +10 -1
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // src/version.ts
22
- var CLI_VERSION = "0.56.1";
22
+ var CLI_VERSION = "0.60.1";
23
23
 
24
24
  // src/config.ts
25
25
  import { readFileSync } from "node:fs";
@@ -2138,6 +2138,9 @@ function parse202(body, fallbackSlug) {
2138
2138
  slug: typeof body.slug === "string" ? body.slug : fallbackSlug,
2139
2139
  message: typeof body.message === "string" ? body.message : "bundle committed; deployment is pending"
2140
2140
  };
2141
+ if (body.v2Delivery !== undefined && body.v2Delivery !== "pending" && body.v2Delivery !== "deferred") {
2142
+ return null;
2143
+ }
2141
2144
  return reconcile === undefined ? accepted : { ...accepted, reconcile };
2142
2145
  }
2143
2146
  if (body.status === "provisioning_started") {
@@ -2570,6 +2573,12 @@ import { parse as parseYaml2 } from "yaml";
2570
2573
  import { z } from "zod";
2571
2574
  var APP_TYPES2 = ["static", "react", "react+api", "container"];
2572
2575
  var AUTH_MODES = ["access", "gateway"];
2576
+ var IDENTITY_TYPES = ["employee", "dsr"];
2577
+ var IdentityTypesSchema = z.array(z.enum(IDENTITY_TYPES)).min(1).refine((values) => new Set(values).size === values.length, {
2578
+ message: "identity_types must not contain duplicates"
2579
+ }).refine((values) => values.includes("employee"), {
2580
+ message: "identity_types must include employee"
2581
+ });
2573
2582
  var RUNTIMES = ["cloudflare-containers", "aca"];
2574
2583
  var SECRET_SOURCES = ["env-file", "platform-managed"];
2575
2584
  var TARGET_KINDS = ["pages", "worker"];
@@ -2590,7 +2599,8 @@ var DeploymentSchema = z.object({
2590
2599
  var AccessSchema = z.object({
2591
2600
  allowed_entra_group: z.string().min(1).optional(),
2592
2601
  allowed_entra_groups: z.array(z.string().min(1)).min(1).refine((arr) => new Set(arr).size === arr.length, { message: "allowed_entra_groups must not contain duplicates" }).optional(),
2593
- session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional()
2602
+ session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional(),
2603
+ identity_types: IdentityTypesSchema.optional()
2594
2604
  }).strict().superRefine((access, ctx) => {
2595
2605
  const hasSingular = access.allowed_entra_group !== undefined;
2596
2606
  const hasPlural = access.allowed_entra_groups !== undefined;
@@ -2678,6 +2688,13 @@ var ManifestSchema = z.object({
2678
2688
  sensitive: z.boolean().optional()
2679
2689
  }).strict().superRefine((m, ctx) => {
2680
2690
  const isContainer = m.deployment.type === "container";
2691
+ if (m.access.identity_types !== undefined && (m.auth ?? "access") !== "gateway") {
2692
+ ctx.addIssue({
2693
+ code: z.ZodIssueCode.custom,
2694
+ path: ["access", "identity_types"],
2695
+ message: "access.identity_types is only valid with auth: gateway"
2696
+ });
2697
+ }
2681
2698
  if (m.confine_origin !== undefined) {
2682
2699
  if (isContainer) {
2683
2700
  ctx.addIssue({
@@ -3302,7 +3319,8 @@ var SpecSchema = z2.object({
3302
3319
  allowed_entra_groups: z2.array(z2.string().min(1)).min(1).refine((a) => new Set(a).size === a.length, {
3303
3320
  message: "allowed_entra_groups must not contain duplicates"
3304
3321
  }),
3305
- require_email_domain: z2.string().min(1).optional()
3322
+ require_email_domain: z2.string().min(1).optional(),
3323
+ identity_types: IdentityTypesSchema.optional()
3306
3324
  }).strict(),
3307
3325
  auth: z2.enum(AUTH_MODES).optional(),
3308
3326
  build: BuildSchema.optional(),
@@ -3317,6 +3335,13 @@ var SpecSchema = z2.object({
3317
3335
  secrets: SecretsSchema.optional(),
3318
3336
  fronted_by_concierge: z2.boolean().optional()
3319
3337
  }).strict().superRefine((s, ctx) => {
3338
+ if (s.access.identity_types !== undefined && (s.auth ?? "access") !== "gateway") {
3339
+ ctx.addIssue({
3340
+ code: z2.ZodIssueCode.custom,
3341
+ path: ["access", "identity_types"],
3342
+ message: "access.identity_types is only valid with auth: gateway"
3343
+ });
3344
+ }
3320
3345
  for (const [k, v] of Object.entries(s.env_vars ?? {})) {
3321
3346
  if (/_(TOKEN|SECRET)$/.test(k) && v.type !== "secret_text") {
3322
3347
  ctx.addIssue({
@@ -3377,19 +3402,14 @@ var SpecSchema = z2.object({
3377
3402
  message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
3378
3403
  });
3379
3404
  }
3380
- if (s.env_vars !== undefined) {
3381
- ctx.addIssue({
3382
- code: z2.ZodIssueCode.custom,
3383
- path: ["env_vars"],
3384
- message: "env_vars are not yet supported with origin: dispatch — a dispatch workspace has no Pages module to apply them, and accepting them here would silently drop them. Worker-level vars land with the v2 deploy path."
3385
- });
3386
- }
3387
- if (s.build !== undefined) {
3388
- ctx.addIssue({
3389
- code: z2.ZodIssueCode.custom,
3390
- path: ["build"],
3391
- message: "build is not yet supported with origin: dispatch — a dispatch workspace has no Pages module to carry it, and the v2 deploy path that will consume it is not shipped yet. Accepting it here would silently drop it."
3392
- });
3405
+ for (const [name, envVar] of Object.entries(s.env_vars ?? {})) {
3406
+ if (envVar.type === "secret_text") {
3407
+ ctx.addIssue({
3408
+ code: z2.ZodIssueCode.custom,
3409
+ path: ["env_vars", name],
3410
+ message: "secret_text env vars are not supported with origin: dispatch until PS-2740 (secrets on v2) provides their runtime consumer."
3411
+ });
3412
+ }
3393
3413
  }
3394
3414
  }
3395
3415
  if (!isContainer && s.containers !== undefined) {
@@ -4381,6 +4401,21 @@ function membersForSecret(set, secret) {
4381
4401
  }
4382
4402
  // ../launchpad-engine/dist/redact-status.js
4383
4403
  var UNSAFE_KEY = new Set(["__proto__", "prototype", "constructor"]);
4404
+ // ../launchpad-engine/dist/deployment-auth.js
4405
+ function deploymentAuthIssue(appType, auth) {
4406
+ if (appType === "container" || auth === "gateway")
4407
+ return null;
4408
+ return {
4409
+ error: "pages_access_retired",
4410
+ message: "Cloudflare Access login is retired for Pages apps. Set auth: gateway (spec.auth in v2). Containers are unchanged."
4411
+ };
4412
+ }
4413
+ // ../launchpad-engine/dist/new-app-origin.js
4414
+ function newAppOrigin(appType, requested) {
4415
+ if (appType === "container")
4416
+ return;
4417
+ return requested ?? (appType === "react+api" ? "pages" : "dispatch");
4418
+ }
4384
4419
  // src/bundle/boundary.ts
4385
4420
  var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
4386
4421
  function applyBoundaryToFiles(args) {
@@ -4528,7 +4563,7 @@ async function bundleAndDeploy(args) {
4528
4563
  }
4529
4564
 
4530
4565
  // src/deploy/verify-deploy.ts
4531
- import { readFileSync as readFileSync9, existsSync as existsSync4 } from "node:fs";
4566
+ import { readFileSync as readFileSync10, existsSync as existsSync4 } from "node:fs";
4532
4567
  import { join as join9 } from "node:path";
4533
4568
 
4534
4569
  // src/deploy/redact-diagnostic.ts
@@ -4574,11 +4609,67 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
4574
4609
  return {
4575
4610
  slug: typeof raw.slug === "string" ? raw.slug : slug,
4576
4611
  supported: raw.supported,
4612
+ ...raw.origin === "pages" || raw.origin === "dispatch" ? { origin: raw.origin } : {},
4613
+ ...projectV2(raw.v2),
4577
4614
  liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
4578
4615
  lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
4579
4616
  ...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
4580
4617
  };
4581
4618
  }
4619
+ function projectV2(value) {
4620
+ if (value === null || typeof value !== "object")
4621
+ return {};
4622
+ const raw = value;
4623
+ if (!isV2State(raw.state) || !isV2Stage(raw.stage) || typeof raw.commitSha !== "string" || typeof raw.attempts !== "number" || !Number.isFinite(raw.attempts) || typeof raw.updatedAt !== "string") {
4624
+ return {};
4625
+ }
4626
+ if (!isConsistentV2State(raw.state, raw.stage))
4627
+ return {};
4628
+ if (raw.failedAt !== undefined && !isV2FailedStage(raw.failedAt))
4629
+ return {};
4630
+ const v2 = {
4631
+ state: raw.state,
4632
+ stage: raw.stage,
4633
+ commitSha: raw.commitSha,
4634
+ attempts: raw.attempts,
4635
+ updatedAt: raw.updatedAt,
4636
+ ...raw.failedAt !== undefined ? { failedAt: raw.failedAt } : {},
4637
+ ...typeof raw.reason === "string" ? { reason: redactDiagnostic(raw.reason) } : {},
4638
+ ...typeof raw.detail === "string" ? { detail: redactDiagnostic(raw.detail) } : {},
4639
+ ...projectRunUrl(raw.runUrl),
4640
+ ...typeof raw.deliveredAt === "string" ? { deliveredAt: raw.deliveredAt } : {}
4641
+ };
4642
+ return { v2 };
4643
+ }
4644
+ function projectRunUrl(value) {
4645
+ if (typeof value !== "string")
4646
+ return {};
4647
+ try {
4648
+ const url = new URL(value);
4649
+ return url.protocol === "https:" && url.hostname === "github.com" ? { runUrl: value } : {};
4650
+ } catch {
4651
+ return {};
4652
+ }
4653
+ }
4654
+ function isV2State(value) {
4655
+ return value === "pending" || value === "delivered" || value === "refused";
4656
+ }
4657
+ function isV2Stage(value) {
4658
+ return ["committed", "building", "verified", "uploading", "live", "failed"].includes(String(value));
4659
+ }
4660
+ function isConsistentV2State(state, stage) {
4661
+ switch (state) {
4662
+ case "delivered":
4663
+ return stage === "live";
4664
+ case "refused":
4665
+ return stage === "failed";
4666
+ case "pending":
4667
+ return stage !== "live" && stage !== "failed";
4668
+ }
4669
+ }
4670
+ function isV2FailedStage(value) {
4671
+ return ["build", "verify", "download", "expand", "manifest", "plan", "upload"].includes(String(value));
4672
+ }
4582
4673
  function projectLastSuccessful(value) {
4583
4674
  if (value === null || typeof value !== "object")
4584
4675
  return null;
@@ -4628,20 +4719,27 @@ function shaMatches(a, b) {
4628
4719
  return x === y;
4629
4720
  return x.slice(0, n) === y.slice(0, n);
4630
4721
  }
4631
- async function readDeploymentBaseline(deps, slug) {
4722
+ async function readDeploymentContext(deps, slug) {
4632
4723
  let status;
4633
4724
  try {
4634
4725
  status = await deps.fetchStatus(slug);
4635
4726
  } catch {
4636
- return null;
4727
+ return { baseline: null };
4728
+ }
4729
+ if (status === null)
4730
+ return { baseline: null };
4731
+ const origin = status.origin;
4732
+ if (!status.supported || status.liveDeployment === null) {
4733
+ return { baseline: null, ...origin !== undefined ? { origin } : {} };
4637
4734
  }
4638
- if (status === null || !status.supported || status.liveDeployment === null)
4639
- return null;
4640
4735
  const live = status.liveDeployment;
4641
4736
  return {
4642
- id: live.id,
4643
- commitHash: live.commit?.hash ?? null,
4644
- createdOn: live.createdOn
4737
+ baseline: {
4738
+ id: live.id,
4739
+ commitHash: live.commit?.hash ?? null,
4740
+ createdOn: live.createdOn
4741
+ },
4742
+ ...origin !== undefined ? { origin } : {}
4645
4743
  };
4646
4744
  }
4647
4745
  async function waitForCommitDeployment(args, deps) {
@@ -4649,6 +4747,7 @@ async function waitForCommitDeployment(args, deps) {
4649
4747
  const start = deps.now();
4650
4748
  const baselineId = args.baseline?.id ?? null;
4651
4749
  let matchedInProgress = false;
4750
+ let lastV2Stage = null;
4652
4751
  for (;; ) {
4653
4752
  let status;
4654
4753
  try {
@@ -4662,6 +4761,38 @@ async function waitForCommitDeployment(args, deps) {
4662
4761
  return { kind: "unsupported" };
4663
4762
  const live = status.liveDeployment;
4664
4763
  const elapsed = deps.now() - start;
4764
+ if (status.origin === "dispatch" && status.v2 !== undefined && shaMatches(status.v2.commitSha, args.shippedSha)) {
4765
+ const delivery = status.v2;
4766
+ if (delivery.stage !== lastV2Stage) {
4767
+ args.onV2Stage?.(delivery);
4768
+ lastV2Stage = delivery.stage;
4769
+ }
4770
+ if (delivery.state === "refused") {
4771
+ return {
4772
+ kind: "failure",
4773
+ dispatch: true,
4774
+ failedStage: delivery.failedAt ?? delivery.stage,
4775
+ reason: delivery.reason,
4776
+ detail: delivery.detail,
4777
+ runUrl: delivery.runUrl
4778
+ };
4779
+ }
4780
+ if (delivery.state === "delivered" || delivery.stage === "live") {
4781
+ return {
4782
+ kind: "success",
4783
+ deploymentUrl: live?.url ?? null,
4784
+ v2: {
4785
+ commitSha: delivery.commitSha,
4786
+ ...delivery.deliveredAt !== undefined ? { deliveredAt: delivery.deliveredAt } : {}
4787
+ }
4788
+ };
4789
+ }
4790
+ matchedInProgress = true;
4791
+ if (elapsed >= args.settleTimeoutMs)
4792
+ return { kind: "pending" };
4793
+ await deps.sleep(pollIntervalMs);
4794
+ continue;
4795
+ }
4665
4796
  if (live !== null && shaMatches(live.commit?.hash, args.shippedSha)) {
4666
4797
  if (live.buildStatus === "success") {
4667
4798
  return { kind: "success", deploymentUrl: live.url };
@@ -4773,7 +4904,7 @@ async function verifyServed(args, deps = { fetch }) {
4773
4904
  }
4774
4905
 
4775
4906
  // src/commands/status.ts
4776
- import { readFileSync as readFileSync8 } from "node:fs";
4907
+ import { readFileSync as readFileSync9 } from "node:fs";
4777
4908
 
4778
4909
  // src/deploy/manifest-state.ts
4779
4910
  async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
@@ -5582,6 +5713,88 @@ still provisioning after the watch window — re-run \`launchpad watch ${slug}\`
5582
5713
  }
5583
5714
  }
5584
5715
 
5716
+ // src/manifest/load.ts
5717
+ import { readFileSync as readFileSync8 } from "node:fs";
5718
+ import { parse as parseYaml4, YAMLParseError } from "yaml";
5719
+ function loadManifest(path8) {
5720
+ let raw;
5721
+ try {
5722
+ raw = readFileSync8(path8, "utf8");
5723
+ } catch (err) {
5724
+ const e = err;
5725
+ if (e.code === "ENOENT") {
5726
+ return { kind: "file-not-found", path: path8 };
5727
+ }
5728
+ return { kind: "read-error", path: path8, message: e.message ?? String(err) };
5729
+ }
5730
+ return parseManifest2(raw, path8);
5731
+ }
5732
+ function loadManifestAnyVersion(path8) {
5733
+ let raw;
5734
+ try {
5735
+ raw = readFileSync8(path8, "utf8");
5736
+ } catch (err) {
5737
+ const e = err;
5738
+ if (e.code === "ENOENT")
5739
+ return { kind: "file-not-found", path: path8 };
5740
+ return { kind: "read-error", path: path8, message: e.message ?? String(err) };
5741
+ }
5742
+ return parseManifestAnyVersion(raw, path8);
5743
+ }
5744
+ function parseManifestAnyVersion(yamlText, path8) {
5745
+ let parsed;
5746
+ try {
5747
+ parsed = parseYaml4(yamlText);
5748
+ } catch (err) {
5749
+ const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
5750
+ return { kind: "yaml-parse-error", path: path8, message };
5751
+ }
5752
+ const apiVersion = typeof parsed === "object" && parsed !== null ? parsed.apiVersion : undefined;
5753
+ if (apiVersion === "launchpad.m-kopa.us/v2") {
5754
+ const result = parseManifestV2(parsed);
5755
+ return result.kind === "ok" ? { kind: "ok", manifest: result.manifest, schema: apiVersion } : { kind: "schema-error", path: path8, schema: apiVersion, issues: [...result.issues] };
5756
+ }
5757
+ if (apiVersion === "launchpad.m-kopa.us/v1alpha1") {
5758
+ const result = parseManifest(parsed);
5759
+ return result.kind === "ok" ? { kind: "ok", manifest: result.manifest, schema: apiVersion } : { kind: "schema-error", path: path8, schema: apiVersion, issues: [...result.issues] };
5760
+ }
5761
+ return {
5762
+ kind: "schema-error",
5763
+ path: path8,
5764
+ schema: "launchpad.m-kopa.us/v1alpha1 or launchpad.m-kopa.us/v2",
5765
+ issues: [
5766
+ {
5767
+ path: "apiVersion",
5768
+ message: `unsupported apiVersion ${JSON.stringify(apiVersion)}; expected launchpad.m-kopa.us/v1alpha1 or launchpad.m-kopa.us/v2`
5769
+ }
5770
+ ]
5771
+ };
5772
+ }
5773
+ function parseManifest2(yamlText, path8) {
5774
+ let parsed;
5775
+ try {
5776
+ parsed = parseYaml4(yamlText);
5777
+ } catch (err) {
5778
+ const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
5779
+ return { kind: "yaml-parse-error", path: path8, message };
5780
+ }
5781
+ const result = ManifestSchema.safeParse(parsed);
5782
+ if (result.success) {
5783
+ return { kind: "ok", manifest: result.data };
5784
+ }
5785
+ return {
5786
+ kind: "schema-error",
5787
+ path: path8,
5788
+ issues: formatIssues(result.error)
5789
+ };
5790
+ }
5791
+ function formatIssues(error) {
5792
+ return error.issues.map((issue) => ({
5793
+ path: issue.path.map(String).join(".") || "(root)",
5794
+ message: issue.message
5795
+ }));
5796
+ }
5797
+
5585
5798
  // src/commands/status.ts
5586
5799
  var statusCommand = {
5587
5800
  name: "status",
@@ -5677,7 +5890,7 @@ async function runStatus(args, io) {
5677
5890
  }
5678
5891
  let localYaml = null;
5679
5892
  try {
5680
- localYaml = readFileSync8(parsed.file, "utf8");
5893
+ localYaml = readFileSync9(parsed.file, "utf8");
5681
5894
  } catch (e) {
5682
5895
  if (!isEnoent(e)) {
5683
5896
  io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe12(e)}`);
@@ -5686,24 +5899,24 @@ async function runStatus(args, io) {
5686
5899
  }
5687
5900
  let local = null;
5688
5901
  if (localYaml !== null) {
5689
- let localObj;
5690
- try {
5691
- const { parse: parseYaml4 } = await import("yaml");
5692
- localObj = parseYaml4(localYaml);
5693
- } catch (e) {
5694
- io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe12(e)}`);
5695
- return 2;
5696
- }
5697
- const localParse = parseManifest(localObj);
5902
+ const localParse = parseManifestAnyVersion(localYaml, parsed.file);
5698
5903
  if (localParse.kind !== "ok") {
5699
- io.err(`launchpad status: ${parsed.file} failed schema validation:`);
5700
- for (const issue of localParse.issues) {
5701
- io.err(` - ${issue.path}: ${issue.message}`);
5904
+ if (localParse.kind === "yaml-parse-error") {
5905
+ io.err(`launchpad status: ${parsed.file} is not valid YAML: ${localParse.message}`);
5906
+ } else if (localParse.kind === "schema-error") {
5907
+ io.err(`launchpad status: ${parsed.file} failed schema validation against ${localParse.schema}:`);
5908
+ for (const issue of localParse.issues) {
5909
+ io.err(` - ${issue.path}: ${issue.message}`);
5910
+ }
5911
+ } else {
5912
+ io.err(`launchpad status: cannot read local manifest at ${parsed.file}`);
5702
5913
  }
5703
5914
  return 2;
5704
5915
  }
5705
5916
  local = localParse.manifest;
5706
- warnSecretShape(local.production_env, io);
5917
+ if (local.apiVersion === "launchpad.m-kopa.us/v1alpha1") {
5918
+ warnSecretShape(local.production_env, io);
5919
+ }
5707
5920
  }
5708
5921
  let state;
5709
5922
  try {
@@ -5733,9 +5946,9 @@ async function runStatus(args, io) {
5733
5946
  }
5734
5947
  if (state.manifestYaml === null || state.manifestYaml === undefined) {
5735
5948
  const live = deployment?.liveDeployment ?? null;
5736
- const contentIsLive = live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
5949
+ const contentIsLive = deployment?.v2?.state === "delivered" || deployment?.v2?.stage === "live" || live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
5737
5950
  const liveButEmpty = lifecycle !== null && lifecycle.state === "live";
5738
- const platformTracked = state.hasAppFile === true || live?.trigger === "bot";
5951
+ const platformTracked = state.hasAppFile === true || live?.trigger === "bot" || deployment?.origin === "dispatch";
5739
5952
  const out = {
5740
5953
  state: contentIsLive ? platformTracked ? "live_drift_unknown" : "live_content_untracked" : platformTracked && !deploymentKnown ? "live_drift_unknown" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
5741
5954
  ...local === null ? { drift: null } : {},
@@ -5768,7 +5981,7 @@ async function runStatus(args, io) {
5768
5981
  };
5769
5982
  emit4(out, parsed.json, io);
5770
5983
  if (parsed.strict) {
5771
- if (deployment?.liveDeployment?.buildStatus === "failure") {
5984
+ if (deployment?.liveDeployment?.buildStatus === "failure" || deployment?.v2?.state === "refused") {
5772
5985
  io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
5773
5986
  return 1;
5774
5987
  }
@@ -5776,26 +5989,40 @@ async function runStatus(args, io) {
5776
5989
  }
5777
5990
  return 0;
5778
5991
  }
5779
- let deployedObj;
5780
- try {
5781
- const { parse: parseYaml4 } = await import("yaml");
5782
- deployedObj = parseYaml4(state.manifestYaml);
5783
- } catch (e) {
5784
- io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${describe12(e)}`);
5785
- return 2;
5786
- }
5787
- const deployedParse = parseManifest(deployedObj);
5992
+ const deployedParse = parseManifestAnyVersion(state.manifestYaml, `deployed manifest at ${state.deployedManifestSha}`);
5788
5993
  if (deployedParse.kind !== "ok") {
5789
- io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} failed schema validation:`);
5790
- for (const issue of deployedParse.issues) {
5791
- io.err(` - ${issue.path}: ${issue.message}`);
5994
+ if (deployedParse.kind === "yaml-parse-error") {
5995
+ io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${deployedParse.message}`);
5996
+ } else if (deployedParse.kind === "schema-error") {
5997
+ io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} failed schema validation against ${deployedParse.schema}:`);
5998
+ for (const issue of deployedParse.issues) {
5999
+ io.err(` - ${issue.path}: ${issue.message}`);
6000
+ }
5792
6001
  }
5793
6002
  return 2;
5794
6003
  }
5795
6004
  const deployed = deployedParse.manifest;
6005
+ if (local.apiVersion !== deployed.apiVersion) {
6006
+ const reason = `local manifest uses ${local.apiVersion}, while the deployed manifest uses ${deployed.apiVersion}; ` + "the manifest shapes differ, so drift was not evaluated";
6007
+ const out = {
6008
+ state: "live_drift_unknown",
6009
+ drift: null,
6010
+ driftUnavailableReason: reason,
6011
+ ...statusTruthFields({
6012
+ slug: parsed.slug,
6013
+ deployedSha: state.deployedManifestSha,
6014
+ headSha: state.appRepoHeadSha,
6015
+ openPrNumber: state.openPr?.number ?? null,
6016
+ ...deploymentKnown ? { deployment } : {},
6017
+ ...standingExceptions !== null ? { standingExceptions } : {}
6018
+ })
6019
+ };
6020
+ emit4(out, parsed.json, io);
6021
+ return 0;
6022
+ }
5796
6023
  const localGroups = await resolveStatusGroups({
5797
6024
  cfg,
5798
- refs: allowedEntraGroups(local.access),
6025
+ refs: allowedEntraGroups(manifestAccess(local)),
5799
6026
  source: "local manifest",
5800
6027
  manifestSha: null
5801
6028
  });
@@ -5804,14 +6031,17 @@ async function runStatus(args, io) {
5804
6031
  }
5805
6032
  const deployedGroups = await resolveStatusGroups({
5806
6033
  cfg,
5807
- refs: allowedEntraGroups(deployed.access),
6034
+ refs: allowedEntraGroups(manifestAccess(deployed)),
5808
6035
  source: "deployed manifest",
5809
6036
  manifestSha: state.deployedManifestSha
5810
6037
  });
5811
6038
  if (deployedGroups.kind === "error") {
5812
6039
  return renderStatusGroupError(deployedGroups.envelope, parsed.json, io);
5813
6040
  }
5814
- const drift = computeDrift(local, deployed, {
6041
+ const drift = local.apiVersion === "launchpad.m-kopa.us/v2" && deployed.apiVersion === "launchpad.m-kopa.us/v2" ? computeV2SpecDrift(local, deployed, {
6042
+ local: localGroups.ids,
6043
+ deployed: deployedGroups.ids
6044
+ }) : computeDrift(local, deployed, {
5815
6045
  local: localGroups.ids,
5816
6046
  deployed: deployedGroups.ids
5817
6047
  });
@@ -5828,8 +6058,8 @@ async function runStatus(args, io) {
5828
6058
  }
5829
6059
  accessDrift = detectCanonicalAccessDrift(localGroups.ids, enforcedGroups.ids);
5830
6060
  }
5831
- const localAssignmentGroups = (local.auth ?? "access") === "access" ? localGroups : null;
5832
- const deployedAssignmentGroups = (deployed.auth ?? "access") === "access" ? deployedGroups : null;
6061
+ const localAssignmentGroups = manifestAuth(local) === "access" ? localGroups : null;
6062
+ const deployedAssignmentGroups = manifestAuth(deployed) === "access" ? deployedGroups : null;
5833
6063
  const assignmentCheck = localAssignmentGroups === null && deployedAssignmentGroups === null ? { kind: "ok", issues: [] } : await findAssignmentIssues(cfg, localAssignmentGroups, deployedAssignmentGroups, state.deployedManifestSha);
5834
6064
  if (assignmentCheck.kind === "error") {
5835
6065
  return renderStatusGroupError(assignmentCheck.envelope, parsed.json, io);
@@ -6030,6 +6260,58 @@ function computeDrift(local, deployed, canonicalGroups) {
6030
6260
  }
6031
6261
  return diffs;
6032
6262
  }
6263
+ function computeV2SpecDrift(local, deployed, canonicalGroups) {
6264
+ const localSpec = {
6265
+ ...local.spec,
6266
+ access: {
6267
+ ...local.spec.access,
6268
+ allowed_entra_groups: [
6269
+ ...canonicalGroups?.local ?? local.spec.access.allowed_entra_groups
6270
+ ].sort()
6271
+ }
6272
+ };
6273
+ const deployedSpec = {
6274
+ ...deployed.spec,
6275
+ access: {
6276
+ ...deployed.spec.access,
6277
+ allowed_entra_groups: [
6278
+ ...canonicalGroups?.deployed ?? deployed.spec.access.allowed_entra_groups
6279
+ ].sort()
6280
+ }
6281
+ };
6282
+ const diffs = [];
6283
+ compareV2Value("spec", localSpec, deployedSpec, diffs);
6284
+ return diffs;
6285
+ }
6286
+ function compareV2Value(path8, local, deployed, diffs) {
6287
+ if (Object.is(local, deployed))
6288
+ return;
6289
+ if (Array.isArray(local) || Array.isArray(deployed)) {
6290
+ if (JSON.stringify(local) !== JSON.stringify(deployed)) {
6291
+ diffs.push({ path: path8, local, deployed });
6292
+ }
6293
+ return;
6294
+ }
6295
+ if (typeof local === "object" && local !== null && typeof deployed === "object" && deployed !== null) {
6296
+ const localRecord = local;
6297
+ const deployedRecord = deployed;
6298
+ const keys = new Set([
6299
+ ...Object.keys(localRecord),
6300
+ ...Object.keys(deployedRecord)
6301
+ ]);
6302
+ for (const key of [...keys].sort()) {
6303
+ compareV2Value(`${path8}.${key}`, localRecord[key], deployedRecord[key], diffs);
6304
+ }
6305
+ return;
6306
+ }
6307
+ diffs.push({ path: path8, local, deployed });
6308
+ }
6309
+ function manifestAccess(manifest) {
6310
+ return manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.spec.access : manifest.access;
6311
+ }
6312
+ function manifestAuth(manifest) {
6313
+ return manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.spec.auth ?? "access" : manifest.auth ?? "access";
6314
+ }
6033
6315
  function lifecycleOutput(slug, lc) {
6034
6316
  const state = lc.state === "failed" ? "provisioning_failed" : lc.state === "live" ? "in_sync" : lc.state;
6035
6317
  return {
@@ -6097,7 +6379,7 @@ function emit4(out, asJson, io) {
6097
6379
  surfaceExceptions(out, io);
6098
6380
  return;
6099
6381
  case "live_content_untracked":
6100
- io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
6382
+ io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} ` + `(no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
6101
6383
  surfaceNoLocalManifestNote(out, io);
6102
6384
  surfaceHeadVsDeployed(out, io);
6103
6385
  surfaceDeployment(out, io);
@@ -6159,6 +6441,10 @@ function surfaceGroupIssues(out, io) {
6159
6441
  function surfaceNoLocalManifestNote(out, io) {
6160
6442
  if (out.drift !== null)
6161
6443
  return;
6444
+ if (out.driftUnavailableReason !== undefined) {
6445
+ io.out(` ${out.driftUnavailableReason}.`);
6446
+ return;
6447
+ }
6162
6448
  io.out(" no local launchpad.yaml here — drift not checked " + "(cd into the app directory or pass --file to compare)");
6163
6449
  }
6164
6450
  function triggerLabel(trigger) {
@@ -6184,7 +6470,13 @@ function surfaceFailedLifecycleDeployment(out, io) {
6184
6470
  }
6185
6471
  function surfaceDeployment(out, io) {
6186
6472
  const dep = out.deployment;
6187
- if (dep === undefined || dep === null || !dep.supported)
6473
+ if (dep === undefined || dep === null)
6474
+ return;
6475
+ if (dep.origin === "dispatch") {
6476
+ surfaceV2Deployment(dep, io);
6477
+ return;
6478
+ }
6479
+ if (!dep.supported)
6188
6480
  return;
6189
6481
  if (dep.reconciledToLive === true) {
6190
6482
  io.out(" ✓ recovered: this app was in a failed state; its verified-live build reconciled the record failed→live (no `launchpad recover` needed).");
@@ -6224,6 +6516,29 @@ function surfaceDeployment(out, io) {
6224
6516
  }
6225
6517
  }
6226
6518
  }
6519
+ function surfaceV2Deployment(dep, io) {
6520
+ for (const line of renderV2DeploymentLines(dep))
6521
+ io.out(line);
6522
+ }
6523
+ function renderV2DeploymentLines(dep) {
6524
+ const delivery = dep.v2;
6525
+ if (delivery === undefined) {
6526
+ return [" v2 delivery: dispatch origin (delivery state unavailable)."];
6527
+ }
6528
+ const lines = [];
6529
+ const deliveredAt = delivery.state === "delivered" && delivery.deliveredAt !== undefined ? `, delivered ${delivery.deliveredAt}` : "";
6530
+ lines.push(` v2 delivery: state ${delivery.state}, stage ${delivery.stage}, commit ${delivery.commitSha.slice(0, 7)}${deliveredAt}`);
6531
+ if (delivery.state === "refused") {
6532
+ const failedAt = delivery.failedAt ?? delivery.stage;
6533
+ lines.push(` failed at ${failedAt}${delivery.reason ? `: ${delivery.reason}` : ""}`);
6534
+ if (delivery.runUrl)
6535
+ lines.push(` GitHub run: ${delivery.runUrl}`);
6536
+ }
6537
+ if (delivery.state !== "delivered" && dep.lastSuccessfulDeployment !== null) {
6538
+ lines.push(` serving: previous successful deployment from ${dep.lastSuccessfulDeployment.createdOn}`);
6539
+ }
6540
+ return lines;
6541
+ }
6227
6542
  function surfaceExceptions(out, io) {
6228
6543
  const exceptions = out.standingExceptions;
6229
6544
  if (exceptions === undefined || exceptions.length === 0)
@@ -6240,7 +6555,7 @@ function surfaceHeadVsDeployed(out, io) {
6240
6555
  if (out.hasOpenPr) {
6241
6556
  io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)};` + ` an apply may be queued — see PR #${out.openPrNumber})`);
6242
6557
  } else {
6243
- io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)}; main is ahead of deployed)`);
6558
+ io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)};` + ` main is ahead of deployed)`);
6244
6559
  }
6245
6560
  }
6246
6561
  }
@@ -6261,7 +6576,7 @@ function warnSecretShape(env, io) {
6261
6576
  const hasPrefix = prefixes.some((p) => v.startsWith(p));
6262
6577
  const longBase64Shape = v.length >= 32 && /^[A-Za-z0-9+/=_-]+$/.test(v);
6263
6578
  if (hasPrefix && longBase64Shape) {
6264
- io.err(`launchpad status: production_env.${k} looks like a secret. production_env is non-secret by contract; move secrets to the secrets bindings path. (This is informational; status will continue.)`);
6579
+ io.err(`launchpad status: production_env.${k} looks like a secret. ` + `production_env is non-secret by contract; move secrets to the ` + `secrets bindings path. (This is informational; status will continue.)`);
6265
6580
  }
6266
6581
  }
6267
6582
  }
@@ -6320,7 +6635,7 @@ function parseArgs2(args, cwd = process.cwd(), warn) {
6320
6635
  if (slug === null) {
6321
6636
  const inferred = inferSlug({ cwd, file, warn });
6322
6637
  if (inferred === null) {
6323
- return `slug not provided + cannot infer from cwd. Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
6638
+ return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
6324
6639
  }
6325
6640
  slug = inferred;
6326
6641
  }
@@ -6379,7 +6694,7 @@ function readLocalEntrypoint(cwd) {
6379
6694
  for (const rel of ["index.html", join9("static", "index.html")]) {
6380
6695
  const p = join9(cwd, rel);
6381
6696
  if (existsSync4(p))
6382
- return new Uint8Array(readFileSync9(p));
6697
+ return new Uint8Array(readFileSync10(p));
6383
6698
  }
6384
6699
  return null;
6385
6700
  }
@@ -6394,13 +6709,25 @@ function realVerifyDeployDeps(cfg) {
6394
6709
  }
6395
6710
  async function runDeployVerification(args, io, deps) {
6396
6711
  io.out("");
6397
- io.out("Verifying a build of your commit went live …");
6712
+ if (args.dispatchOrigin === true) {
6713
+ io.out("Waiting for your repo build and dispatch delivery …");
6714
+ } else {
6715
+ io.out("Verifying a build of your commit went live …");
6716
+ }
6717
+ const seenV2Stages = new Set;
6718
+ const onV2Stage = (delivery) => {
6719
+ if (seenV2Stages.has(delivery.stage))
6720
+ return;
6721
+ seenV2Stages.add(delivery.stage);
6722
+ io.out(` v2 stage: ${v2StageLabel(delivery.stage)}`);
6723
+ };
6398
6724
  const settle = await deps.wait({
6399
6725
  slug: args.slug,
6400
6726
  shippedSha: args.shippedSha,
6401
6727
  baseline: args.baseline,
6402
6728
  registrationBoundMs: args.registrationBoundMs,
6403
- settleTimeoutMs: args.timeoutMs
6729
+ settleTimeoutMs: args.timeoutMs,
6730
+ onV2Stage
6404
6731
  }, deps.waitDeps);
6405
6732
  switch (settle.kind) {
6406
6733
  case "unsupported":
@@ -6429,10 +6756,25 @@ async function runDeployVerification(args, io, deps) {
6429
6756
  io.out(` It may still be queued. Re-check the outcome: \`launchpad status ${args.slug}\``);
6430
6757
  return 0;
6431
6758
  case "pending":
6759
+ if (args.dispatchOrigin === true || seenV2Stages.size > 0) {
6760
+ io.out(`⏳ Dispatch delivery is still in progress after ${Math.round(args.timeoutMs / 1000)}s - couldn't verify yet.`);
6761
+ io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
6762
+ return 0;
6763
+ }
6432
6764
  io.out(`⏳ Your commit's build is still in progress after ${Math.round(args.timeoutMs / 1000)}s — couldn't verify yet.`);
6433
6765
  io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
6434
6766
  return 0;
6435
6767
  case "failure":
6768
+ if (settle.dispatch === true) {
6769
+ io.err(`✗ Dispatch delivery REFUSED at ${settle.failedStage ?? "unknown"} - your commit is NOT live.`);
6770
+ if (settle.reason)
6771
+ io.err(` reason: ${redactDiagnostic(settle.reason)}`);
6772
+ if (settle.detail)
6773
+ io.err(` detail: ${redactDiagnostic(settle.detail)}`);
6774
+ if (settle.runUrl)
6775
+ io.err(` GitHub run: ${settle.runUrl}`);
6776
+ return 1;
6777
+ }
6436
6778
  io.err("✗ The Cloudflare Pages build for your commit FAILED — your content is NOT live.");
6437
6779
  if (settle.failedStage)
6438
6780
  io.err(` failed stage: ${redactDiagnostic(settle.failedStage)}`);
@@ -6444,9 +6786,30 @@ async function runDeployVerification(args, io, deps) {
6444
6786
  io.err(` Full logs: \`launchpad status ${args.slug}\``);
6445
6787
  return 1;
6446
6788
  case "success":
6789
+ if (settle.v2 !== undefined) {
6790
+ const deliveredAt = settle.v2.deliveredAt ? ` at ${settle.v2.deliveredAt}` : "";
6791
+ io.out(`✓ Live in dispatch namespace - delivered commit ${settle.v2.commitSha}${deliveredAt}.`);
6792
+ return 0;
6793
+ }
6447
6794
  return verifySuccess(args, settle.deploymentUrl, io, deps);
6448
6795
  }
6449
6796
  }
6797
+ function v2StageLabel(stage) {
6798
+ switch (stage) {
6799
+ case "committed":
6800
+ return "source committed";
6801
+ case "building":
6802
+ return "CI building";
6803
+ case "verified":
6804
+ return "run verified";
6805
+ case "uploading":
6806
+ return "artifact uploading";
6807
+ case "live":
6808
+ return "live in dispatch namespace";
6809
+ case "failed":
6810
+ return "failed";
6811
+ }
6812
+ }
6450
6813
  function realRedeployVerifyDeps(cfg) {
6451
6814
  return {
6452
6815
  wait: waitForDeployment,
@@ -6673,7 +7036,7 @@ function runGit2(sp, cwd, args) {
6673
7036
  }
6674
7037
 
6675
7038
  // src/deploy/manifest-diff.ts
6676
- import { parse as parseYaml4 } from "yaml";
7039
+ import { parse as parseYaml5 } from "yaml";
6677
7040
  var MASK = "«redacted»";
6678
7041
  var SECRET_KEY_SEGMENT = /(secret|token|api[_-]?key|access[_-]?key|private[_-]?key|secret[_-]?key|password|passwd|pwd|authorization|auth[_-]?token|bearer|client[_-]?secret|session|credential|value)/i;
6679
7042
  function isSecretPath(path8) {
@@ -6764,7 +7127,7 @@ function diffManifests(localYaml, deployedYaml) {
6764
7127
  let local;
6765
7128
  let deployed;
6766
7129
  try {
6767
- local = normalize(parseYaml4(localYaml));
7130
+ local = parseYaml5(localYaml);
6768
7131
  } catch (e) {
6769
7132
  return {
6770
7133
  inSync: false,
@@ -6773,7 +7136,7 @@ function diffManifests(localYaml, deployedYaml) {
6773
7136
  };
6774
7137
  }
6775
7138
  try {
6776
- deployed = normalize(parseYaml4(deployedYaml));
7139
+ deployed = parseYaml5(deployedYaml);
6777
7140
  } catch (e) {
6778
7141
  return {
6779
7142
  inSync: false,
@@ -6781,6 +7144,22 @@ function diffManifests(localYaml, deployedYaml) {
6781
7144
  parseError: `deployed manifest did not parse: ${e.message}`
6782
7145
  };
6783
7146
  }
7147
+ const localVersion = manifestApiVersion(local);
7148
+ const deployedVersion = manifestApiVersion(deployed);
7149
+ if (localVersion !== deployedVersion && (isV2(localVersion) || isV2(deployedVersion))) {
7150
+ return {
7151
+ inSync: false,
7152
+ changes: [],
7153
+ parseError: null,
7154
+ shapeMismatch: `local uses ${localVersion ?? "an unknown schema"}; deployed uses ${deployedVersion ?? "an unknown schema"}`
7155
+ };
7156
+ }
7157
+ if (isV2(localVersion) && isV2(deployedVersion)) {
7158
+ local = { spec: manifestSpec(local) };
7159
+ deployed = { spec: manifestSpec(deployed) };
7160
+ }
7161
+ local = normalize(local);
7162
+ deployed = normalize(deployed);
6784
7163
  if (stableStringify(local) === stableStringify(deployed)) {
6785
7164
  return { inSync: true, changes: [], parseError: null };
6786
7165
  }
@@ -6827,6 +7206,18 @@ function diffManifests(localYaml, deployedYaml) {
6827
7206
  }
6828
7207
  return { inSync: false, changes, parseError: null };
6829
7208
  }
7209
+ function manifestApiVersion(value) {
7210
+ if (typeof value !== "object" || value === null)
7211
+ return null;
7212
+ const apiVersion = value.apiVersion;
7213
+ return typeof apiVersion === "string" ? apiVersion : null;
7214
+ }
7215
+ function isV2(apiVersion) {
7216
+ return apiVersion === "launchpad.m-kopa.us/v2";
7217
+ }
7218
+ function manifestSpec(value) {
7219
+ return typeof value === "object" && value !== null ? value.spec : undefined;
7220
+ }
6830
7221
  function renderManifestDiffLines(diff, max = 20) {
6831
7222
  const lines = [];
6832
7223
  for (const c of diff.changes.slice(0, max)) {
@@ -7255,47 +7646,6 @@ function parseDeployFlags(args) {
7255
7646
  import { existsSync as existsSync6, rmSync } from "node:fs";
7256
7647
  import { resolve as resolvePath, join as join10 } from "node:path";
7257
7648
 
7258
- // src/manifest/load.ts
7259
- import { readFileSync as readFileSync10 } from "node:fs";
7260
- import { parse as parseYaml5, YAMLParseError } from "yaml";
7261
- function loadManifest(path8) {
7262
- let raw;
7263
- try {
7264
- raw = readFileSync10(path8, "utf8");
7265
- } catch (err) {
7266
- const e = err;
7267
- if (e.code === "ENOENT") {
7268
- return { kind: "file-not-found", path: path8 };
7269
- }
7270
- return { kind: "read-error", path: path8, message: e.message ?? String(err) };
7271
- }
7272
- return parseManifest2(raw, path8);
7273
- }
7274
- function parseManifest2(yamlText, path8) {
7275
- let parsed;
7276
- try {
7277
- parsed = parseYaml5(yamlText);
7278
- } catch (err) {
7279
- const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
7280
- return { kind: "yaml-parse-error", path: path8, message };
7281
- }
7282
- const result = ManifestSchema.safeParse(parsed);
7283
- if (result.success) {
7284
- return { kind: "ok", manifest: result.data };
7285
- }
7286
- return {
7287
- kind: "schema-error",
7288
- path: path8,
7289
- issues: formatIssues(result.error)
7290
- };
7291
- }
7292
- function formatIssues(error) {
7293
- return error.issues.map((issue) => ({
7294
- path: issue.path.map(String).join(".") || "(root)",
7295
- message: issue.message
7296
- }));
7297
- }
7298
-
7299
7649
  // src/status-page-output.ts
7300
7650
  async function surfaceStatusPageUrl(url, io, options) {
7301
7651
  if (typeof url !== "string" || url.length === 0)
@@ -7370,13 +7720,13 @@ async function runDeployApply(opts, io, deps = {}) {
7370
7720
  return 0;
7371
7721
  }
7372
7722
  const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
7373
- const loaded = loadManifest(manifestPath);
7723
+ const loaded = loadManifestAnyVersion(manifestPath);
7374
7724
  if (loaded.kind !== "ok") {
7375
7725
  return renderManifestError(loaded, io);
7376
7726
  }
7377
- const slug = loaded.manifest.metadata.name;
7727
+ const slug = manifestSlug(loaded);
7378
7728
  if (!SLUG_REGEX2.test(slug)) {
7379
- io.err(`launchpad deploy --apply: manifest's metadata.name '${slug}' is not a valid slug`);
7729
+ io.err(`launchpad deploy --apply: manifest's ${loaded.schema === "launchpad.m-kopa.us/v2" ? "metadata.slug" : "metadata.name"} '${slug}' is not a valid slug`);
7380
7730
  return 1;
7381
7731
  }
7382
7732
  if (opts.atSha !== undefined && !MANIFEST_SHA_REGEX.test(opts.atSha)) {
@@ -7667,12 +8017,12 @@ function deletePinIfPresent(cfg, slug, io) {
7667
8017
  }
7668
8018
  async function loadSlugForResume(opts, io) {
7669
8019
  const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
7670
- const loaded = loadManifest(manifestPath);
8020
+ const loaded = loadManifestAnyVersion(manifestPath);
7671
8021
  if (loaded.kind !== "ok") {
7672
8022
  renderManifestError(loaded, io);
7673
8023
  return null;
7674
8024
  }
7675
- return loaded.manifest.metadata.name;
8025
+ return manifestSlug(loaded);
7676
8026
  }
7677
8027
  async function defaultPrompt(question) {
7678
8028
  const { createInterface } = await import("node:readline/promises");
@@ -7713,6 +8063,10 @@ function mapHttpError(e, slug, io) {
7713
8063
  io.err(`launchpad deploy --apply failed: ${describe14(e)}`);
7714
8064
  return 2;
7715
8065
  }
8066
+ function manifestSlug(loaded) {
8067
+ const { metadata } = loaded.manifest;
8068
+ return "slug" in metadata ? metadata.slug : metadata.name;
8069
+ }
7716
8070
  function renderManifestError(loaded, io) {
7717
8071
  switch (loaded.kind) {
7718
8072
  case "ok":
@@ -8608,6 +8962,10 @@ async function surfaceManifestDiff(cfg, slug, manifestPath, io) {
8608
8962
  io.out(` config diff: skipped (${diff.parseError}).`);
8609
8963
  return;
8610
8964
  }
8965
+ if (diff.shapeMismatch !== undefined) {
8966
+ io.out(` config diff: manifest shapes differ (${diff.shapeMismatch}); drift was not evaluated.`);
8967
+ return;
8968
+ }
8611
8969
  if (diff.inSync) {
8612
8970
  io.out(" config: launchpad.yaml matches the deployed manifest (in sync).");
8613
8971
  return;
@@ -8631,6 +8989,7 @@ async function runModelADeploy(args) {
8631
8989
  const { cwd, manifestPath, io } = args;
8632
8990
  let slug;
8633
8991
  let appType = "static";
8992
+ let declaredOrigin;
8634
8993
  try {
8635
8994
  const manifestObj = parseYaml6(readFileSync11(manifestPath, "utf8"));
8636
8995
  const metaSlug = resolveManifestSlug(manifestObj);
@@ -8640,13 +8999,22 @@ async function runModelADeploy(args) {
8640
8999
  }
8641
9000
  slug = metaSlug;
8642
9001
  const raw = manifestObj;
8643
- const isV2 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
8644
- const deploymentType = isV2 ? raw?.spec?.appType : raw?.deployment?.type;
9002
+ const isV22 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
9003
+ declaredOrigin = declaredOriginOf(isV22, raw?.spec?.origin);
9004
+ const deploymentType = isV22 ? raw?.spec?.appType : raw?.deployment?.type;
8645
9005
  if (deploymentType === "static" || deploymentType === "react" || deploymentType === "react+api" || deploymentType === "container") {
8646
9006
  appType = deploymentType;
8647
9007
  }
9008
+ if (appType !== undefined) {
9009
+ const auth = isV22 ? raw?.spec?.auth : raw?.auth;
9010
+ const issue = deploymentAuthIssue(appType, auth === "gateway" ? "gateway" : auth === "access" ? "access" : undefined);
9011
+ if (issue !== null) {
9012
+ io.err(`launchpad deploy: ${issue.message}`);
9013
+ return 1;
9014
+ }
9015
+ }
8648
9016
  if (deploymentType === "container") {
8649
- const parsed = isV2 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
9017
+ const parsed = isV22 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
8650
9018
  if (parsed.kind !== "ok") {
8651
9019
  io.err("launchpad deploy: manifest is structurally invalid:");
8652
9020
  for (const issue of parsed.issues) {
@@ -8698,7 +9066,8 @@ async function runModelADeploy(args) {
8698
9066
  try {
8699
9067
  await surfaceManifestDiff(cfg, slug, manifestPath, io);
8700
9068
  } catch {}
8701
- const deployBaseline = noVerify ? null : await readDeploymentBaseline(realCommitWaitDeps(cfg), slug);
9069
+ const deployContext = await readDeploymentContext(realCommitWaitDeps(cfg), slug);
9070
+ const deployBaseline = noVerify ? null : deployContext.baseline;
8702
9071
  let result;
8703
9072
  try {
8704
9073
  if (args.allowUnguarded) {
@@ -8828,7 +9197,11 @@ async function runModelADeploy(args) {
8828
9197
  if (accessExit !== 0)
8829
9198
  return accessExit;
8830
9199
  io.out("");
8831
- io.out("Committed; build pending - Cloudflare Pages is building this deploy now.");
9200
+ const dispatchOrigin = handoffIsDispatch({
9201
+ declared: declaredOrigin,
9202
+ baseline: deployContext.origin
9203
+ });
9204
+ surfaceCommittedHandoff(dispatchOrigin, io, "ascii");
8832
9205
  if (noVerify)
8833
9206
  return 0;
8834
9207
  return runDeployVerification({
@@ -8838,7 +9211,8 @@ async function runModelADeploy(args) {
8838
9211
  shippedSha: outcome.manifest_sha,
8839
9212
  baseline: deployBaseline,
8840
9213
  registrationBoundMs,
8841
- timeoutMs: verifyTimeoutMs
9214
+ timeoutMs: verifyTimeoutMs,
9215
+ dispatchOrigin
8842
9216
  }, io, realVerifyDeployDeps(cfg));
8843
9217
  }
8844
9218
  io.err(`launchpad deploy: ${formatErrorEnvelope(body)}`);
@@ -8934,9 +9308,18 @@ async function runModelADeploy(args) {
8934
9308
  }
8935
9309
  surfaceDeployExtras(success, io, slug, args.allowStale);
8936
9310
  io.out("");
8937
- io.out("Committed; build pending — Cloudflare Pages is building this deploy now.");
9311
+ const dispatchOrigin = handoffIsDispatch({
9312
+ v2Delivery: success.v2Delivery,
9313
+ declared: declaredOrigin,
9314
+ baseline: deployContext.origin
9315
+ });
9316
+ surfaceCommittedHandoff(dispatchOrigin, io);
8938
9317
  if (noVerify) {
8939
- io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
9318
+ if (dispatchOrigin) {
9319
+ io.out(`Run \`launchpad status ${slug}\` to confirm the delivery stage and outcome.`);
9320
+ } else {
9321
+ io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
9322
+ }
8940
9323
  return 0;
8941
9324
  }
8942
9325
  return runDeployVerification({
@@ -8946,11 +9329,33 @@ async function runModelADeploy(args) {
8946
9329
  shippedSha: success.commit_sha,
8947
9330
  baseline: deployBaseline,
8948
9331
  registrationBoundMs,
8949
- timeoutMs: verifyTimeoutMs
9332
+ timeoutMs: verifyTimeoutMs,
9333
+ dispatchOrigin
8950
9334
  }, io, realVerifyDeployDeps(cfg));
8951
9335
  }
8952
9336
  }
8953
9337
  }
9338
+ function surfaceCommittedHandoff(dispatchOrigin, io, pagesStyle = "unicode") {
9339
+ io.out(committedHandoffMessage(dispatchOrigin, pagesStyle));
9340
+ }
9341
+ function declaredOriginOf(isV22, origin) {
9342
+ if (!isV22)
9343
+ return "pages";
9344
+ return origin === "dispatch" ? "dispatch" : "pages";
9345
+ }
9346
+ function handoffIsDispatch(input) {
9347
+ if (input.v2Delivery === "pending" || input.v2Delivery === "deferred")
9348
+ return true;
9349
+ if (input.declared !== undefined)
9350
+ return input.declared === "dispatch";
9351
+ return input.baseline === "dispatch";
9352
+ }
9353
+ function committedHandoffMessage(dispatchOrigin, pagesStyle = "unicode") {
9354
+ if (dispatchOrigin) {
9355
+ return "Committed; your repo's build workflow is building this commit, then the bot delivers it to the dispatch namespace.";
9356
+ }
9357
+ return pagesStyle === "ascii" ? "Committed; build pending - Cloudflare Pages is building this deploy now." : "Committed; build pending — Cloudflare Pages is building this deploy now.";
9358
+ }
8954
9359
  function pendingReconcileOutcome(body) {
8955
9360
  const manifestSha = body.manifestSha;
8956
9361
  const id = body.reconcileId;
@@ -10378,8 +10783,26 @@ async function runInit(args, io, prompt) {
10378
10783
  io.err(`launchpad init: ${fillResult.message}`);
10379
10784
  return 64;
10380
10785
  }
10786
+ const authIssue = deploymentAuthIssue(fillResult.inputs.type, fillResult.inputs.auth ?? "gateway");
10787
+ if (authIssue !== null) {
10788
+ io.err(`launchpad init: ${authIssue.message}`);
10789
+ return 64;
10790
+ }
10791
+ if (fillResult.inputs.type === "container" && inputs.origin !== undefined) {
10792
+ io.err("launchpad init: --origin is not supported for containers");
10793
+ return 64;
10794
+ }
10795
+ const dispatch = newAppOrigin(fillResult.inputs.type, inputs.origin) === "dispatch";
10796
+ if (dispatch && fillResult.inputs.type === "react+api") {
10797
+ io.err("launchpad init: API starter dispatch is not available yet; use --origin pages");
10798
+ return 64;
10799
+ }
10800
+ if (dispatch && (inputs.hostnames.length > 0 || inputs.sessionDuration !== undefined)) {
10801
+ io.err("launchpad init: dispatch does not support --hostname or --session-duration; use --origin pages for a Pages manifest");
10802
+ return 64;
10803
+ }
10381
10804
  const manifest = buildManifest(fillResult.inputs, detected);
10382
- const validation = ManifestSchema.safeParse(manifest);
10805
+ const validation = manifest.apiVersion === "launchpad.m-kopa.us/v2" ? ManifestV2Schema.safeParse(manifest) : ManifestSchema.safeParse(manifest);
10383
10806
  if (!validation.success) {
10384
10807
  io.err("launchpad init: invalid manifest input");
10385
10808
  for (const issue of validation.error.issues) {
@@ -10432,8 +10855,11 @@ async function runInit(args, io, prompt) {
10432
10855
  io.out("");
10433
10856
  io.out("Next steps:");
10434
10857
  io.out(" launchpad validate # confirm the manifest");
10435
- io.out(" launchpad secrets template # scaffold .env.example");
10436
- io.out(" launchpad deploy # ship it (Scope 6, pending)");
10858
+ if (!dispatch)
10859
+ io.out(" launchpad secrets template # scaffold .env.example");
10860
+ io.out(" launchpad deploy # build and deploy the app");
10861
+ if (dispatch)
10862
+ io.out(" Dispatch currently supports plain-text environment variables, but not secret bindings.");
10437
10863
  return 0;
10438
10864
  }
10439
10865
  function parseArgs6(args) {
@@ -10444,6 +10870,7 @@ function parseArgs6(args) {
10444
10870
  description: undefined,
10445
10871
  type: undefined,
10446
10872
  auth: undefined,
10873
+ origin: undefined,
10447
10874
  groups: [],
10448
10875
  sessionDuration: undefined,
10449
10876
  hostnames: [],
@@ -10528,6 +10955,16 @@ function parseArgs6(args) {
10528
10955
  inputs.type = got;
10529
10956
  continue;
10530
10957
  }
10958
+ if (a === "--origin") {
10959
+ const got = needsValue();
10960
+ if (typeof got !== "string")
10961
+ return got;
10962
+ if (got !== "pages" && got !== "dispatch") {
10963
+ return { kind: "usage-error", message: "--origin must be pages or dispatch" };
10964
+ }
10965
+ inputs.origin = got;
10966
+ continue;
10967
+ }
10531
10968
  if (a === "--auth") {
10532
10969
  const got = needsValue();
10533
10970
  if (typeof got !== "string")
@@ -10722,6 +11159,7 @@ async function fillRequired(inputs, options, io, prompt) {
10722
11159
  description: inputs.description,
10723
11160
  type: typeR,
10724
11161
  auth: inputs.auth,
11162
+ origin: inputs.origin,
10725
11163
  groups: groupsR,
10726
11164
  sessionDuration: inputs.sessionDuration,
10727
11165
  hostnames: inputs.hostnames,
@@ -10781,6 +11219,29 @@ function buildManifest(inputs, detected) {
10781
11219
  root: ".",
10782
11220
  include: [...defaultIncludeForAppType(inputs.type)]
10783
11221
  };
11222
+ if (newAppOrigin(inputs.type, inputs.origin) === "dispatch") {
11223
+ return {
11224
+ apiVersion: "launchpad.m-kopa.us/v2",
11225
+ kind: "App",
11226
+ metadata: { slug: inputs.name, schemaVersion: 2 },
11227
+ spec: {
11228
+ appType: inputs.type,
11229
+ origin: "dispatch",
11230
+ auth: resolvedAuth,
11231
+ display: {
11232
+ name: inputs.name,
11233
+ team: inputs.team,
11234
+ ...inputs.description ? { description: inputs.description } : {}
11235
+ },
11236
+ owner: { email: inputs.owner },
11237
+ access: { allowed_entra_groups: [...inputs.groups] },
11238
+ app: manifest.app,
11239
+ ...manifest.build === undefined ? {} : { build: manifest.build }
11240
+ }
11241
+ };
11242
+ }
11243
+ if (inputs.origin === "pages")
11244
+ manifest.origin = "pages";
10784
11245
  return manifest;
10785
11246
  }
10786
11247
  function starterIndexHtml(slug) {
@@ -13220,11 +13681,15 @@ async function runSecretsPush(opts, io, deps = {}) {
13220
13681
  const cfg = deps.config ?? loadConfig();
13221
13682
  const fetcher = deps.fetcher ?? fetch;
13222
13683
  const manifestPath = resolvePath4(process.cwd(), opts.file ?? "launchpad.yaml");
13223
- const loaded = loadManifest(manifestPath);
13684
+ const loaded = loadManifestAnyVersion(manifestPath);
13224
13685
  if (loaded.kind !== "ok") {
13225
13686
  return renderManifestError4(loaded, io);
13226
13687
  }
13227
- const bindings = loaded.manifest.secrets?.bindings ?? [];
13688
+ const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets push", io);
13689
+ if (v2Refusal !== null)
13690
+ return v2Refusal;
13691
+ const manifest = loaded.manifest;
13692
+ const bindings = manifest.secrets?.bindings ?? [];
13228
13693
  const envFileBindings = bindings.filter((b) => b.source === "env-file");
13229
13694
  if (envFileBindings.length === 0) {
13230
13695
  io.out("No `env-file` secret bindings declared in the manifest.");
@@ -13306,7 +13771,7 @@ async function runSecretsPush(opts, io, deps = {}) {
13306
13771
  io.out("Re-run without --dry-run to push.");
13307
13772
  return 0;
13308
13773
  }
13309
- const slug = loaded.manifest.metadata.name;
13774
+ const slug = manifest.metadata.name;
13310
13775
  let pushed = 0;
13311
13776
  let partial = 0;
13312
13777
  let anyPagesUpdated = false;
@@ -13431,6 +13896,20 @@ function mapPushError(e, slug, name, io) {
13431
13896
  function describe30(e) {
13432
13897
  return e instanceof Error ? e.message : String(e);
13433
13898
  }
13899
+ function refuseV2Manifest(loaded, command, io) {
13900
+ if (loaded.schema !== "launchpad.m-kopa.us/v2")
13901
+ return null;
13902
+ const origin = loaded.manifest.spec?.origin;
13903
+ if (origin === "dispatch") {
13904
+ io.err(`${command}: this is a dispatch-origin (v2) app.`);
13905
+ const outcome = command.endsWith("push") ? "nothing was pushed" : "nothing was read";
13906
+ io.err(` Secrets are not delivered to the dispatch namespace yet (PS-2740); ${outcome}.`);
13907
+ return 1;
13908
+ }
13909
+ io.err(`${command}: this is a launchpad.m-kopa.us/v2 manifest.`);
13910
+ io.err(" This command reads v1alpha1 `secrets.bindings`; v2 declares secrets as `spec.env_vars` `secret_text` entries, which it does not push yet.");
13911
+ return 1;
13912
+ }
13434
13913
  function renderManifestError4(result, io) {
13435
13914
  switch (result.kind) {
13436
13915
  case "ok":
@@ -13468,16 +13947,20 @@ async function runSecretsStatus(opts, io, deps = {}) {
13468
13947
  const cfg = deps.config ?? loadConfig();
13469
13948
  const fetcher = deps.fetcher ?? fetch;
13470
13949
  const manifestPath = resolvePath5(process.cwd(), opts.file ?? "launchpad.yaml");
13471
- const loaded = loadManifest(manifestPath);
13950
+ const loaded = loadManifestAnyVersion(manifestPath);
13472
13951
  if (loaded.kind !== "ok")
13473
13952
  return renderManifestError5(loaded, io);
13474
- const slug = loaded.manifest.metadata.name;
13953
+ const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets status", io);
13954
+ if (v2Refusal !== null)
13955
+ return v2Refusal;
13956
+ const manifest = loaded.manifest;
13957
+ const slug = manifest.metadata.name;
13475
13958
  if (opts.slug != null && opts.slug !== slug) {
13476
13959
  io.err(`launchpad secrets status: slug "${opts.slug}" does not match this directory's ` + `manifest (metadata.name: "${slug}").`);
13477
13960
  io.err(" secrets status audits the manifest in the current directory — omit the slug, " + "or run from the app's directory.");
13478
13961
  return 2;
13479
13962
  }
13480
- const bindings = loaded.manifest.secrets?.bindings ?? [];
13963
+ const bindings = manifest.secrets?.bindings ?? [];
13481
13964
  if (bindings.length === 0) {
13482
13965
  io.out("No secret bindings declared in the manifest.");
13483
13966
  return 0;
@@ -14845,7 +15328,7 @@ import { existsSync as existsSync16, readFileSync as readFileSync21, readdirSync
14845
15328
  import { dirname as dirname12, resolve as resolve17 } from "node:path";
14846
15329
  var validateCommand = {
14847
15330
  name: "validate",
14848
- summary: "validate launchpad.yaml against the v1alpha1 schema",
15331
+ summary: "validate launchpad.yaml against its v1alpha1 or v2 schema",
14849
15332
  run: runValidate
14850
15333
  };
14851
15334
  async function runValidate(args, io) {
@@ -14857,7 +15340,7 @@ async function runValidate(args, io) {
14857
15340
  }
14858
15341
  const { file, json, strictGroups } = parseResult;
14859
15342
  const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
14860
- const result = loadManifest(path14);
15343
+ const result = loadManifestAnyVersion(path14);
14861
15344
  if (result.kind !== "ok") {
14862
15345
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
14863
15346
  }
@@ -14867,16 +15350,17 @@ async function runValidate(args, io) {
14867
15350
  manifest: result.manifest,
14868
15351
  io,
14869
15352
  json,
14870
- structurallyValidName: result.manifest.metadata.name
15353
+ structurallyValidName: manifestView(result.manifest).name
14871
15354
  });
14872
15355
  if (unsupported2 !== null)
14873
15356
  return unsupported2;
14874
- const boundary = checkBoundary(path14, result.manifest.app !== undefined);
15357
+ const view = manifestView(result.manifest);
15358
+ const boundary = checkBoundary(path14, view.appDeclared);
14875
15359
  const serveErrors = checkStaticServeDir(path14, result.manifest);
14876
15360
  if (serveErrors.length > 0) {
14877
15361
  boundary.errors = [...boundary.errors, ...serveErrors];
14878
15362
  }
14879
- const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
15363
+ const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(view.access)) : { kind: "skipped" };
14880
15364
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
14881
15365
  }
14882
15366
  function checkBoundary(manifestPath, declared) {
@@ -14900,11 +15384,12 @@ function checkBoundary(manifestPath, declared) {
14900
15384
  return { declared, errors: [], warnings: r.warnings };
14901
15385
  }
14902
15386
  function checkStaticServeDir(manifestPath, manifest) {
14903
- if (manifest.deployment.type !== "static")
15387
+ const view = manifestView(manifest);
15388
+ if (view.deploymentType !== "static")
14904
15389
  return [];
14905
- if (manifest.build?.command !== undefined)
15390
+ if (view.buildCommand !== undefined)
14906
15391
  return [];
14907
- const serveDir = manifest.build?.destination_dir ?? STATIC_SERVE_DIR;
15392
+ const serveDir = view.destinationDir ?? STATIC_SERVE_DIR;
14908
15393
  const normalized = serveDir.replace(/\\/g, "/");
14909
15394
  const segments = normalized.split("/");
14910
15395
  if (normalized === "" || normalized === "." || normalized === "./" || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || segments.some((s) => s === "..")) {
@@ -15004,9 +15489,10 @@ async function checkGroups(groups) {
15004
15489
  return ok ? { kind: "all-ok", perGroup } : { kind: "some-failed", perGroup };
15005
15490
  }
15006
15491
  function renderHumanOk(result, group, boundary, io) {
15007
- io.out(`✓ ${result.manifest.metadata.name} — manifest is valid`);
15008
- io.out(` apiVersion: launchpad.m-kopa.us/v1alpha1`);
15009
- io.out(` type: ${result.manifest.deployment.type}`);
15492
+ const view = manifestView(result.manifest);
15493
+ io.out(`✓ ${view.name} — manifest is valid`);
15494
+ io.out(` apiVersion: ${result.schema}`);
15495
+ io.out(` type: ${view.deploymentType}`);
15010
15496
  if (boundary.errors.length > 0) {
15011
15497
  for (const e of boundary.errors) {
15012
15498
  io.err(`✗ app boundary: ${e}`);
@@ -15075,6 +15561,7 @@ function renderHumanError(result, io) {
15075
15561
  return 1;
15076
15562
  case "schema-error":
15077
15563
  io.err(`✗ ${result.path}`);
15564
+ io.err(` validated against schema: ${result.schema}`);
15078
15565
  io.err(` ${result.issues.length} schema issue${result.issues.length === 1 ? "" : "s"}:`);
15079
15566
  for (const issue of result.issues) {
15080
15567
  io.err(` ${issue.path}: ${issue.message}`);
@@ -15083,11 +15570,12 @@ function renderHumanError(result, io) {
15083
15570
  }
15084
15571
  }
15085
15572
  function renderJsonOk(result, group, boundary, io) {
15573
+ const view = manifestView(result.manifest);
15086
15574
  const base = {
15087
15575
  manifest_valid: true,
15088
15576
  runtime_supported: true,
15089
- name: result.manifest.metadata.name,
15090
- deploymentType: result.manifest.deployment.type,
15577
+ name: view.name,
15578
+ deploymentType: view.deploymentType,
15091
15579
  appBoundary: {
15092
15580
  declared: boundary.declared,
15093
15581
  errors: boundary.errors,
@@ -15167,11 +15655,32 @@ function renderJsonError(result, io) {
15167
15655
  ok: false,
15168
15656
  kind: "schema-error",
15169
15657
  path: result.path,
15658
+ schema: result.schema,
15170
15659
  issues: result.issues
15171
15660
  }));
15172
15661
  return 1;
15173
15662
  }
15174
15663
  }
15664
+ function manifestView(manifest) {
15665
+ if (manifest.apiVersion === "launchpad.m-kopa.us/v2") {
15666
+ return {
15667
+ name: manifest.metadata.slug,
15668
+ deploymentType: manifest.spec.appType,
15669
+ access: manifest.spec.access,
15670
+ buildCommand: manifest.spec.build?.command,
15671
+ destinationDir: manifest.spec.build?.destination_dir,
15672
+ appDeclared: manifest.spec.app !== undefined
15673
+ };
15674
+ }
15675
+ return {
15676
+ name: manifest.metadata.name,
15677
+ deploymentType: manifest.deployment.type,
15678
+ access: manifest.access,
15679
+ buildCommand: manifest.build?.command,
15680
+ destinationDir: manifest.build?.destination_dir,
15681
+ appDeclared: manifest.app !== undefined
15682
+ };
15683
+ }
15175
15684
  function parseArgs13(args) {
15176
15685
  let file;
15177
15686
  let json = false;