@m-kopa/launchpad-cli 0.57.0 → 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 +76 -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 +632 -145
  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.57.0";
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") {
@@ -3399,19 +3402,14 @@ var SpecSchema = z2.object({
3399
3402
  message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
3400
3403
  });
3401
3404
  }
3402
- if (s.env_vars !== undefined) {
3403
- ctx.addIssue({
3404
- code: z2.ZodIssueCode.custom,
3405
- path: ["env_vars"],
3406
- 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."
3407
- });
3408
- }
3409
- if (s.build !== undefined) {
3410
- ctx.addIssue({
3411
- code: z2.ZodIssueCode.custom,
3412
- path: ["build"],
3413
- 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."
3414
- });
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
+ }
3415
3413
  }
3416
3414
  }
3417
3415
  if (!isContainer && s.containers !== undefined) {
@@ -4403,6 +4401,21 @@ function membersForSecret(set, secret) {
4403
4401
  }
4404
4402
  // ../launchpad-engine/dist/redact-status.js
4405
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
+ }
4406
4419
  // src/bundle/boundary.ts
4407
4420
  var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
4408
4421
  function applyBoundaryToFiles(args) {
@@ -4550,7 +4563,7 @@ async function bundleAndDeploy(args) {
4550
4563
  }
4551
4564
 
4552
4565
  // src/deploy/verify-deploy.ts
4553
- import { readFileSync as readFileSync9, existsSync as existsSync4 } from "node:fs";
4566
+ import { readFileSync as readFileSync10, existsSync as existsSync4 } from "node:fs";
4554
4567
  import { join as join9 } from "node:path";
4555
4568
 
4556
4569
  // src/deploy/redact-diagnostic.ts
@@ -4596,11 +4609,67 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
4596
4609
  return {
4597
4610
  slug: typeof raw.slug === "string" ? raw.slug : slug,
4598
4611
  supported: raw.supported,
4612
+ ...raw.origin === "pages" || raw.origin === "dispatch" ? { origin: raw.origin } : {},
4613
+ ...projectV2(raw.v2),
4599
4614
  liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
4600
4615
  lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
4601
4616
  ...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
4602
4617
  };
4603
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
+ }
4604
4673
  function projectLastSuccessful(value) {
4605
4674
  if (value === null || typeof value !== "object")
4606
4675
  return null;
@@ -4650,20 +4719,27 @@ function shaMatches(a, b) {
4650
4719
  return x === y;
4651
4720
  return x.slice(0, n) === y.slice(0, n);
4652
4721
  }
4653
- async function readDeploymentBaseline(deps, slug) {
4722
+ async function readDeploymentContext(deps, slug) {
4654
4723
  let status;
4655
4724
  try {
4656
4725
  status = await deps.fetchStatus(slug);
4657
4726
  } catch {
4658
- 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 } : {} };
4659
4734
  }
4660
- if (status === null || !status.supported || status.liveDeployment === null)
4661
- return null;
4662
4735
  const live = status.liveDeployment;
4663
4736
  return {
4664
- id: live.id,
4665
- commitHash: live.commit?.hash ?? null,
4666
- createdOn: live.createdOn
4737
+ baseline: {
4738
+ id: live.id,
4739
+ commitHash: live.commit?.hash ?? null,
4740
+ createdOn: live.createdOn
4741
+ },
4742
+ ...origin !== undefined ? { origin } : {}
4667
4743
  };
4668
4744
  }
4669
4745
  async function waitForCommitDeployment(args, deps) {
@@ -4671,6 +4747,7 @@ async function waitForCommitDeployment(args, deps) {
4671
4747
  const start = deps.now();
4672
4748
  const baselineId = args.baseline?.id ?? null;
4673
4749
  let matchedInProgress = false;
4750
+ let lastV2Stage = null;
4674
4751
  for (;; ) {
4675
4752
  let status;
4676
4753
  try {
@@ -4684,6 +4761,38 @@ async function waitForCommitDeployment(args, deps) {
4684
4761
  return { kind: "unsupported" };
4685
4762
  const live = status.liveDeployment;
4686
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
+ }
4687
4796
  if (live !== null && shaMatches(live.commit?.hash, args.shippedSha)) {
4688
4797
  if (live.buildStatus === "success") {
4689
4798
  return { kind: "success", deploymentUrl: live.url };
@@ -4795,7 +4904,7 @@ async function verifyServed(args, deps = { fetch }) {
4795
4904
  }
4796
4905
 
4797
4906
  // src/commands/status.ts
4798
- import { readFileSync as readFileSync8 } from "node:fs";
4907
+ import { readFileSync as readFileSync9 } from "node:fs";
4799
4908
 
4800
4909
  // src/deploy/manifest-state.ts
4801
4910
  async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
@@ -5604,6 +5713,88 @@ still provisioning after the watch window — re-run \`launchpad watch ${slug}\`
5604
5713
  }
5605
5714
  }
5606
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
+
5607
5798
  // src/commands/status.ts
5608
5799
  var statusCommand = {
5609
5800
  name: "status",
@@ -5699,7 +5890,7 @@ async function runStatus(args, io) {
5699
5890
  }
5700
5891
  let localYaml = null;
5701
5892
  try {
5702
- localYaml = readFileSync8(parsed.file, "utf8");
5893
+ localYaml = readFileSync9(parsed.file, "utf8");
5703
5894
  } catch (e) {
5704
5895
  if (!isEnoent(e)) {
5705
5896
  io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe12(e)}`);
@@ -5708,24 +5899,24 @@ async function runStatus(args, io) {
5708
5899
  }
5709
5900
  let local = null;
5710
5901
  if (localYaml !== null) {
5711
- let localObj;
5712
- try {
5713
- const { parse: parseYaml4 } = await import("yaml");
5714
- localObj = parseYaml4(localYaml);
5715
- } catch (e) {
5716
- io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe12(e)}`);
5717
- return 2;
5718
- }
5719
- const localParse = parseManifest(localObj);
5902
+ const localParse = parseManifestAnyVersion(localYaml, parsed.file);
5720
5903
  if (localParse.kind !== "ok") {
5721
- io.err(`launchpad status: ${parsed.file} failed schema validation:`);
5722
- for (const issue of localParse.issues) {
5723
- 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}`);
5724
5913
  }
5725
5914
  return 2;
5726
5915
  }
5727
5916
  local = localParse.manifest;
5728
- warnSecretShape(local.production_env, io);
5917
+ if (local.apiVersion === "launchpad.m-kopa.us/v1alpha1") {
5918
+ warnSecretShape(local.production_env, io);
5919
+ }
5729
5920
  }
5730
5921
  let state;
5731
5922
  try {
@@ -5755,9 +5946,9 @@ async function runStatus(args, io) {
5755
5946
  }
5756
5947
  if (state.manifestYaml === null || state.manifestYaml === undefined) {
5757
5948
  const live = deployment?.liveDeployment ?? null;
5758
- 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);
5759
5950
  const liveButEmpty = lifecycle !== null && lifecycle.state === "live";
5760
- const platformTracked = state.hasAppFile === true || live?.trigger === "bot";
5951
+ const platformTracked = state.hasAppFile === true || live?.trigger === "bot" || deployment?.origin === "dispatch";
5761
5952
  const out = {
5762
5953
  state: contentIsLive ? platformTracked ? "live_drift_unknown" : "live_content_untracked" : platformTracked && !deploymentKnown ? "live_drift_unknown" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
5763
5954
  ...local === null ? { drift: null } : {},
@@ -5790,7 +5981,7 @@ async function runStatus(args, io) {
5790
5981
  };
5791
5982
  emit4(out, parsed.json, io);
5792
5983
  if (parsed.strict) {
5793
- if (deployment?.liveDeployment?.buildStatus === "failure") {
5984
+ if (deployment?.liveDeployment?.buildStatus === "failure" || deployment?.v2?.state === "refused") {
5794
5985
  io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
5795
5986
  return 1;
5796
5987
  }
@@ -5798,26 +5989,40 @@ async function runStatus(args, io) {
5798
5989
  }
5799
5990
  return 0;
5800
5991
  }
5801
- let deployedObj;
5802
- try {
5803
- const { parse: parseYaml4 } = await import("yaml");
5804
- deployedObj = parseYaml4(state.manifestYaml);
5805
- } catch (e) {
5806
- io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${describe12(e)}`);
5807
- return 2;
5808
- }
5809
- const deployedParse = parseManifest(deployedObj);
5992
+ const deployedParse = parseManifestAnyVersion(state.manifestYaml, `deployed manifest at ${state.deployedManifestSha}`);
5810
5993
  if (deployedParse.kind !== "ok") {
5811
- io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} failed schema validation:`);
5812
- for (const issue of deployedParse.issues) {
5813
- 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
+ }
5814
6001
  }
5815
6002
  return 2;
5816
6003
  }
5817
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
+ }
5818
6023
  const localGroups = await resolveStatusGroups({
5819
6024
  cfg,
5820
- refs: allowedEntraGroups(local.access),
6025
+ refs: allowedEntraGroups(manifestAccess(local)),
5821
6026
  source: "local manifest",
5822
6027
  manifestSha: null
5823
6028
  });
@@ -5826,14 +6031,17 @@ async function runStatus(args, io) {
5826
6031
  }
5827
6032
  const deployedGroups = await resolveStatusGroups({
5828
6033
  cfg,
5829
- refs: allowedEntraGroups(deployed.access),
6034
+ refs: allowedEntraGroups(manifestAccess(deployed)),
5830
6035
  source: "deployed manifest",
5831
6036
  manifestSha: state.deployedManifestSha
5832
6037
  });
5833
6038
  if (deployedGroups.kind === "error") {
5834
6039
  return renderStatusGroupError(deployedGroups.envelope, parsed.json, io);
5835
6040
  }
5836
- 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, {
5837
6045
  local: localGroups.ids,
5838
6046
  deployed: deployedGroups.ids
5839
6047
  });
@@ -5850,8 +6058,8 @@ async function runStatus(args, io) {
5850
6058
  }
5851
6059
  accessDrift = detectCanonicalAccessDrift(localGroups.ids, enforcedGroups.ids);
5852
6060
  }
5853
- const localAssignmentGroups = (local.auth ?? "access") === "access" ? localGroups : null;
5854
- const deployedAssignmentGroups = (deployed.auth ?? "access") === "access" ? deployedGroups : null;
6061
+ const localAssignmentGroups = manifestAuth(local) === "access" ? localGroups : null;
6062
+ const deployedAssignmentGroups = manifestAuth(deployed) === "access" ? deployedGroups : null;
5855
6063
  const assignmentCheck = localAssignmentGroups === null && deployedAssignmentGroups === null ? { kind: "ok", issues: [] } : await findAssignmentIssues(cfg, localAssignmentGroups, deployedAssignmentGroups, state.deployedManifestSha);
5856
6064
  if (assignmentCheck.kind === "error") {
5857
6065
  return renderStatusGroupError(assignmentCheck.envelope, parsed.json, io);
@@ -6052,6 +6260,58 @@ function computeDrift(local, deployed, canonicalGroups) {
6052
6260
  }
6053
6261
  return diffs;
6054
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
+ }
6055
6315
  function lifecycleOutput(slug, lc) {
6056
6316
  const state = lc.state === "failed" ? "provisioning_failed" : lc.state === "live" ? "in_sync" : lc.state;
6057
6317
  return {
@@ -6119,7 +6379,7 @@ function emit4(out, asJson, io) {
6119
6379
  surfaceExceptions(out, io);
6120
6380
  return;
6121
6381
  case "live_content_untracked":
6122
- 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\`).`);
6123
6383
  surfaceNoLocalManifestNote(out, io);
6124
6384
  surfaceHeadVsDeployed(out, io);
6125
6385
  surfaceDeployment(out, io);
@@ -6181,6 +6441,10 @@ function surfaceGroupIssues(out, io) {
6181
6441
  function surfaceNoLocalManifestNote(out, io) {
6182
6442
  if (out.drift !== null)
6183
6443
  return;
6444
+ if (out.driftUnavailableReason !== undefined) {
6445
+ io.out(` ${out.driftUnavailableReason}.`);
6446
+ return;
6447
+ }
6184
6448
  io.out(" no local launchpad.yaml here — drift not checked " + "(cd into the app directory or pass --file to compare)");
6185
6449
  }
6186
6450
  function triggerLabel(trigger) {
@@ -6206,7 +6470,13 @@ function surfaceFailedLifecycleDeployment(out, io) {
6206
6470
  }
6207
6471
  function surfaceDeployment(out, io) {
6208
6472
  const dep = out.deployment;
6209
- 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)
6210
6480
  return;
6211
6481
  if (dep.reconciledToLive === true) {
6212
6482
  io.out(" ✓ recovered: this app was in a failed state; its verified-live build reconciled the record failed→live (no `launchpad recover` needed).");
@@ -6246,6 +6516,29 @@ function surfaceDeployment(out, io) {
6246
6516
  }
6247
6517
  }
6248
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
+ }
6249
6542
  function surfaceExceptions(out, io) {
6250
6543
  const exceptions = out.standingExceptions;
6251
6544
  if (exceptions === undefined || exceptions.length === 0)
@@ -6262,7 +6555,7 @@ function surfaceHeadVsDeployed(out, io) {
6262
6555
  if (out.hasOpenPr) {
6263
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})`);
6264
6557
  } else {
6265
- 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)`);
6266
6559
  }
6267
6560
  }
6268
6561
  }
@@ -6283,7 +6576,7 @@ function warnSecretShape(env, io) {
6283
6576
  const hasPrefix = prefixes.some((p) => v.startsWith(p));
6284
6577
  const longBase64Shape = v.length >= 32 && /^[A-Za-z0-9+/=_-]+$/.test(v);
6285
6578
  if (hasPrefix && longBase64Shape) {
6286
- 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.)`);
6287
6580
  }
6288
6581
  }
6289
6582
  }
@@ -6342,7 +6635,7 @@ function parseArgs2(args, cwd = process.cwd(), warn) {
6342
6635
  if (slug === null) {
6343
6636
  const inferred = inferSlug({ cwd, file, warn });
6344
6637
  if (inferred === null) {
6345
- 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>.`;
6346
6639
  }
6347
6640
  slug = inferred;
6348
6641
  }
@@ -6401,7 +6694,7 @@ function readLocalEntrypoint(cwd) {
6401
6694
  for (const rel of ["index.html", join9("static", "index.html")]) {
6402
6695
  const p = join9(cwd, rel);
6403
6696
  if (existsSync4(p))
6404
- return new Uint8Array(readFileSync9(p));
6697
+ return new Uint8Array(readFileSync10(p));
6405
6698
  }
6406
6699
  return null;
6407
6700
  }
@@ -6416,13 +6709,25 @@ function realVerifyDeployDeps(cfg) {
6416
6709
  }
6417
6710
  async function runDeployVerification(args, io, deps) {
6418
6711
  io.out("");
6419
- 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
+ };
6420
6724
  const settle = await deps.wait({
6421
6725
  slug: args.slug,
6422
6726
  shippedSha: args.shippedSha,
6423
6727
  baseline: args.baseline,
6424
6728
  registrationBoundMs: args.registrationBoundMs,
6425
- settleTimeoutMs: args.timeoutMs
6729
+ settleTimeoutMs: args.timeoutMs,
6730
+ onV2Stage
6426
6731
  }, deps.waitDeps);
6427
6732
  switch (settle.kind) {
6428
6733
  case "unsupported":
@@ -6451,10 +6756,25 @@ async function runDeployVerification(args, io, deps) {
6451
6756
  io.out(` It may still be queued. Re-check the outcome: \`launchpad status ${args.slug}\``);
6452
6757
  return 0;
6453
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
+ }
6454
6764
  io.out(`⏳ Your commit's build is still in progress after ${Math.round(args.timeoutMs / 1000)}s — couldn't verify yet.`);
6455
6765
  io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
6456
6766
  return 0;
6457
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
+ }
6458
6778
  io.err("✗ The Cloudflare Pages build for your commit FAILED — your content is NOT live.");
6459
6779
  if (settle.failedStage)
6460
6780
  io.err(` failed stage: ${redactDiagnostic(settle.failedStage)}`);
@@ -6466,9 +6786,30 @@ async function runDeployVerification(args, io, deps) {
6466
6786
  io.err(` Full logs: \`launchpad status ${args.slug}\``);
6467
6787
  return 1;
6468
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
+ }
6469
6794
  return verifySuccess(args, settle.deploymentUrl, io, deps);
6470
6795
  }
6471
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
+ }
6472
6813
  function realRedeployVerifyDeps(cfg) {
6473
6814
  return {
6474
6815
  wait: waitForDeployment,
@@ -6695,7 +7036,7 @@ function runGit2(sp, cwd, args) {
6695
7036
  }
6696
7037
 
6697
7038
  // src/deploy/manifest-diff.ts
6698
- import { parse as parseYaml4 } from "yaml";
7039
+ import { parse as parseYaml5 } from "yaml";
6699
7040
  var MASK = "«redacted»";
6700
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;
6701
7042
  function isSecretPath(path8) {
@@ -6786,7 +7127,7 @@ function diffManifests(localYaml, deployedYaml) {
6786
7127
  let local;
6787
7128
  let deployed;
6788
7129
  try {
6789
- local = normalize(parseYaml4(localYaml));
7130
+ local = parseYaml5(localYaml);
6790
7131
  } catch (e) {
6791
7132
  return {
6792
7133
  inSync: false,
@@ -6795,7 +7136,7 @@ function diffManifests(localYaml, deployedYaml) {
6795
7136
  };
6796
7137
  }
6797
7138
  try {
6798
- deployed = normalize(parseYaml4(deployedYaml));
7139
+ deployed = parseYaml5(deployedYaml);
6799
7140
  } catch (e) {
6800
7141
  return {
6801
7142
  inSync: false,
@@ -6803,6 +7144,22 @@ function diffManifests(localYaml, deployedYaml) {
6803
7144
  parseError: `deployed manifest did not parse: ${e.message}`
6804
7145
  };
6805
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);
6806
7163
  if (stableStringify(local) === stableStringify(deployed)) {
6807
7164
  return { inSync: true, changes: [], parseError: null };
6808
7165
  }
@@ -6849,6 +7206,18 @@ function diffManifests(localYaml, deployedYaml) {
6849
7206
  }
6850
7207
  return { inSync: false, changes, parseError: null };
6851
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
+ }
6852
7221
  function renderManifestDiffLines(diff, max = 20) {
6853
7222
  const lines = [];
6854
7223
  for (const c of diff.changes.slice(0, max)) {
@@ -7277,47 +7646,6 @@ function parseDeployFlags(args) {
7277
7646
  import { existsSync as existsSync6, rmSync } from "node:fs";
7278
7647
  import { resolve as resolvePath, join as join10 } from "node:path";
7279
7648
 
7280
- // src/manifest/load.ts
7281
- import { readFileSync as readFileSync10 } from "node:fs";
7282
- import { parse as parseYaml5, YAMLParseError } from "yaml";
7283
- function loadManifest(path8) {
7284
- let raw;
7285
- try {
7286
- raw = readFileSync10(path8, "utf8");
7287
- } catch (err) {
7288
- const e = err;
7289
- if (e.code === "ENOENT") {
7290
- return { kind: "file-not-found", path: path8 };
7291
- }
7292
- return { kind: "read-error", path: path8, message: e.message ?? String(err) };
7293
- }
7294
- return parseManifest2(raw, path8);
7295
- }
7296
- function parseManifest2(yamlText, path8) {
7297
- let parsed;
7298
- try {
7299
- parsed = parseYaml5(yamlText);
7300
- } catch (err) {
7301
- const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
7302
- return { kind: "yaml-parse-error", path: path8, message };
7303
- }
7304
- const result = ManifestSchema.safeParse(parsed);
7305
- if (result.success) {
7306
- return { kind: "ok", manifest: result.data };
7307
- }
7308
- return {
7309
- kind: "schema-error",
7310
- path: path8,
7311
- issues: formatIssues(result.error)
7312
- };
7313
- }
7314
- function formatIssues(error) {
7315
- return error.issues.map((issue) => ({
7316
- path: issue.path.map(String).join(".") || "(root)",
7317
- message: issue.message
7318
- }));
7319
- }
7320
-
7321
7649
  // src/status-page-output.ts
7322
7650
  async function surfaceStatusPageUrl(url, io, options) {
7323
7651
  if (typeof url !== "string" || url.length === 0)
@@ -7392,13 +7720,13 @@ async function runDeployApply(opts, io, deps = {}) {
7392
7720
  return 0;
7393
7721
  }
7394
7722
  const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
7395
- const loaded = loadManifest(manifestPath);
7723
+ const loaded = loadManifestAnyVersion(manifestPath);
7396
7724
  if (loaded.kind !== "ok") {
7397
7725
  return renderManifestError(loaded, io);
7398
7726
  }
7399
- const slug = loaded.manifest.metadata.name;
7727
+ const slug = manifestSlug(loaded);
7400
7728
  if (!SLUG_REGEX2.test(slug)) {
7401
- 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`);
7402
7730
  return 1;
7403
7731
  }
7404
7732
  if (opts.atSha !== undefined && !MANIFEST_SHA_REGEX.test(opts.atSha)) {
@@ -7689,12 +8017,12 @@ function deletePinIfPresent(cfg, slug, io) {
7689
8017
  }
7690
8018
  async function loadSlugForResume(opts, io) {
7691
8019
  const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
7692
- const loaded = loadManifest(manifestPath);
8020
+ const loaded = loadManifestAnyVersion(manifestPath);
7693
8021
  if (loaded.kind !== "ok") {
7694
8022
  renderManifestError(loaded, io);
7695
8023
  return null;
7696
8024
  }
7697
- return loaded.manifest.metadata.name;
8025
+ return manifestSlug(loaded);
7698
8026
  }
7699
8027
  async function defaultPrompt(question) {
7700
8028
  const { createInterface } = await import("node:readline/promises");
@@ -7735,6 +8063,10 @@ function mapHttpError(e, slug, io) {
7735
8063
  io.err(`launchpad deploy --apply failed: ${describe14(e)}`);
7736
8064
  return 2;
7737
8065
  }
8066
+ function manifestSlug(loaded) {
8067
+ const { metadata } = loaded.manifest;
8068
+ return "slug" in metadata ? metadata.slug : metadata.name;
8069
+ }
7738
8070
  function renderManifestError(loaded, io) {
7739
8071
  switch (loaded.kind) {
7740
8072
  case "ok":
@@ -8630,6 +8962,10 @@ async function surfaceManifestDiff(cfg, slug, manifestPath, io) {
8630
8962
  io.out(` config diff: skipped (${diff.parseError}).`);
8631
8963
  return;
8632
8964
  }
8965
+ if (diff.shapeMismatch !== undefined) {
8966
+ io.out(` config diff: manifest shapes differ (${diff.shapeMismatch}); drift was not evaluated.`);
8967
+ return;
8968
+ }
8633
8969
  if (diff.inSync) {
8634
8970
  io.out(" config: launchpad.yaml matches the deployed manifest (in sync).");
8635
8971
  return;
@@ -8653,6 +8989,7 @@ async function runModelADeploy(args) {
8653
8989
  const { cwd, manifestPath, io } = args;
8654
8990
  let slug;
8655
8991
  let appType = "static";
8992
+ let declaredOrigin;
8656
8993
  try {
8657
8994
  const manifestObj = parseYaml6(readFileSync11(manifestPath, "utf8"));
8658
8995
  const metaSlug = resolveManifestSlug(manifestObj);
@@ -8662,13 +8999,22 @@ async function runModelADeploy(args) {
8662
8999
  }
8663
9000
  slug = metaSlug;
8664
9001
  const raw = manifestObj;
8665
- const isV2 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
8666
- 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;
8667
9005
  if (deploymentType === "static" || deploymentType === "react" || deploymentType === "react+api" || deploymentType === "container") {
8668
9006
  appType = deploymentType;
8669
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
+ }
8670
9016
  if (deploymentType === "container") {
8671
- const parsed = isV2 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
9017
+ const parsed = isV22 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
8672
9018
  if (parsed.kind !== "ok") {
8673
9019
  io.err("launchpad deploy: manifest is structurally invalid:");
8674
9020
  for (const issue of parsed.issues) {
@@ -8720,7 +9066,8 @@ async function runModelADeploy(args) {
8720
9066
  try {
8721
9067
  await surfaceManifestDiff(cfg, slug, manifestPath, io);
8722
9068
  } catch {}
8723
- const deployBaseline = noVerify ? null : await readDeploymentBaseline(realCommitWaitDeps(cfg), slug);
9069
+ const deployContext = await readDeploymentContext(realCommitWaitDeps(cfg), slug);
9070
+ const deployBaseline = noVerify ? null : deployContext.baseline;
8724
9071
  let result;
8725
9072
  try {
8726
9073
  if (args.allowUnguarded) {
@@ -8850,7 +9197,11 @@ async function runModelADeploy(args) {
8850
9197
  if (accessExit !== 0)
8851
9198
  return accessExit;
8852
9199
  io.out("");
8853
- 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");
8854
9205
  if (noVerify)
8855
9206
  return 0;
8856
9207
  return runDeployVerification({
@@ -8860,7 +9211,8 @@ async function runModelADeploy(args) {
8860
9211
  shippedSha: outcome.manifest_sha,
8861
9212
  baseline: deployBaseline,
8862
9213
  registrationBoundMs,
8863
- timeoutMs: verifyTimeoutMs
9214
+ timeoutMs: verifyTimeoutMs,
9215
+ dispatchOrigin
8864
9216
  }, io, realVerifyDeployDeps(cfg));
8865
9217
  }
8866
9218
  io.err(`launchpad deploy: ${formatErrorEnvelope(body)}`);
@@ -8956,9 +9308,18 @@ async function runModelADeploy(args) {
8956
9308
  }
8957
9309
  surfaceDeployExtras(success, io, slug, args.allowStale);
8958
9310
  io.out("");
8959
- 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);
8960
9317
  if (noVerify) {
8961
- 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
+ }
8962
9323
  return 0;
8963
9324
  }
8964
9325
  return runDeployVerification({
@@ -8968,11 +9329,33 @@ async function runModelADeploy(args) {
8968
9329
  shippedSha: success.commit_sha,
8969
9330
  baseline: deployBaseline,
8970
9331
  registrationBoundMs,
8971
- timeoutMs: verifyTimeoutMs
9332
+ timeoutMs: verifyTimeoutMs,
9333
+ dispatchOrigin
8972
9334
  }, io, realVerifyDeployDeps(cfg));
8973
9335
  }
8974
9336
  }
8975
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
+ }
8976
9359
  function pendingReconcileOutcome(body) {
8977
9360
  const manifestSha = body.manifestSha;
8978
9361
  const id = body.reconcileId;
@@ -10400,8 +10783,26 @@ async function runInit(args, io, prompt) {
10400
10783
  io.err(`launchpad init: ${fillResult.message}`);
10401
10784
  return 64;
10402
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
+ }
10403
10804
  const manifest = buildManifest(fillResult.inputs, detected);
10404
- const validation = ManifestSchema.safeParse(manifest);
10805
+ const validation = manifest.apiVersion === "launchpad.m-kopa.us/v2" ? ManifestV2Schema.safeParse(manifest) : ManifestSchema.safeParse(manifest);
10405
10806
  if (!validation.success) {
10406
10807
  io.err("launchpad init: invalid manifest input");
10407
10808
  for (const issue of validation.error.issues) {
@@ -10454,8 +10855,11 @@ async function runInit(args, io, prompt) {
10454
10855
  io.out("");
10455
10856
  io.out("Next steps:");
10456
10857
  io.out(" launchpad validate # confirm the manifest");
10457
- io.out(" launchpad secrets template # scaffold .env.example");
10458
- 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.");
10459
10863
  return 0;
10460
10864
  }
10461
10865
  function parseArgs6(args) {
@@ -10466,6 +10870,7 @@ function parseArgs6(args) {
10466
10870
  description: undefined,
10467
10871
  type: undefined,
10468
10872
  auth: undefined,
10873
+ origin: undefined,
10469
10874
  groups: [],
10470
10875
  sessionDuration: undefined,
10471
10876
  hostnames: [],
@@ -10550,6 +10955,16 @@ function parseArgs6(args) {
10550
10955
  inputs.type = got;
10551
10956
  continue;
10552
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
+ }
10553
10968
  if (a === "--auth") {
10554
10969
  const got = needsValue();
10555
10970
  if (typeof got !== "string")
@@ -10744,6 +11159,7 @@ async function fillRequired(inputs, options, io, prompt) {
10744
11159
  description: inputs.description,
10745
11160
  type: typeR,
10746
11161
  auth: inputs.auth,
11162
+ origin: inputs.origin,
10747
11163
  groups: groupsR,
10748
11164
  sessionDuration: inputs.sessionDuration,
10749
11165
  hostnames: inputs.hostnames,
@@ -10803,6 +11219,29 @@ function buildManifest(inputs, detected) {
10803
11219
  root: ".",
10804
11220
  include: [...defaultIncludeForAppType(inputs.type)]
10805
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";
10806
11245
  return manifest;
10807
11246
  }
10808
11247
  function starterIndexHtml(slug) {
@@ -13242,11 +13681,15 @@ async function runSecretsPush(opts, io, deps = {}) {
13242
13681
  const cfg = deps.config ?? loadConfig();
13243
13682
  const fetcher = deps.fetcher ?? fetch;
13244
13683
  const manifestPath = resolvePath4(process.cwd(), opts.file ?? "launchpad.yaml");
13245
- const loaded = loadManifest(manifestPath);
13684
+ const loaded = loadManifestAnyVersion(manifestPath);
13246
13685
  if (loaded.kind !== "ok") {
13247
13686
  return renderManifestError4(loaded, io);
13248
13687
  }
13249
- 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 ?? [];
13250
13693
  const envFileBindings = bindings.filter((b) => b.source === "env-file");
13251
13694
  if (envFileBindings.length === 0) {
13252
13695
  io.out("No `env-file` secret bindings declared in the manifest.");
@@ -13328,7 +13771,7 @@ async function runSecretsPush(opts, io, deps = {}) {
13328
13771
  io.out("Re-run without --dry-run to push.");
13329
13772
  return 0;
13330
13773
  }
13331
- const slug = loaded.manifest.metadata.name;
13774
+ const slug = manifest.metadata.name;
13332
13775
  let pushed = 0;
13333
13776
  let partial = 0;
13334
13777
  let anyPagesUpdated = false;
@@ -13453,6 +13896,20 @@ function mapPushError(e, slug, name, io) {
13453
13896
  function describe30(e) {
13454
13897
  return e instanceof Error ? e.message : String(e);
13455
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
+ }
13456
13913
  function renderManifestError4(result, io) {
13457
13914
  switch (result.kind) {
13458
13915
  case "ok":
@@ -13490,16 +13947,20 @@ async function runSecretsStatus(opts, io, deps = {}) {
13490
13947
  const cfg = deps.config ?? loadConfig();
13491
13948
  const fetcher = deps.fetcher ?? fetch;
13492
13949
  const manifestPath = resolvePath5(process.cwd(), opts.file ?? "launchpad.yaml");
13493
- const loaded = loadManifest(manifestPath);
13950
+ const loaded = loadManifestAnyVersion(manifestPath);
13494
13951
  if (loaded.kind !== "ok")
13495
13952
  return renderManifestError5(loaded, io);
13496
- 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;
13497
13958
  if (opts.slug != null && opts.slug !== slug) {
13498
13959
  io.err(`launchpad secrets status: slug "${opts.slug}" does not match this directory's ` + `manifest (metadata.name: "${slug}").`);
13499
13960
  io.err(" secrets status audits the manifest in the current directory — omit the slug, " + "or run from the app's directory.");
13500
13961
  return 2;
13501
13962
  }
13502
- const bindings = loaded.manifest.secrets?.bindings ?? [];
13963
+ const bindings = manifest.secrets?.bindings ?? [];
13503
13964
  if (bindings.length === 0) {
13504
13965
  io.out("No secret bindings declared in the manifest.");
13505
13966
  return 0;
@@ -14867,7 +15328,7 @@ import { existsSync as existsSync16, readFileSync as readFileSync21, readdirSync
14867
15328
  import { dirname as dirname12, resolve as resolve17 } from "node:path";
14868
15329
  var validateCommand = {
14869
15330
  name: "validate",
14870
- summary: "validate launchpad.yaml against the v1alpha1 schema",
15331
+ summary: "validate launchpad.yaml against its v1alpha1 or v2 schema",
14871
15332
  run: runValidate
14872
15333
  };
14873
15334
  async function runValidate(args, io) {
@@ -14879,7 +15340,7 @@ async function runValidate(args, io) {
14879
15340
  }
14880
15341
  const { file, json, strictGroups } = parseResult;
14881
15342
  const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
14882
- const result = loadManifest(path14);
15343
+ const result = loadManifestAnyVersion(path14);
14883
15344
  if (result.kind !== "ok") {
14884
15345
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
14885
15346
  }
@@ -14889,16 +15350,17 @@ async function runValidate(args, io) {
14889
15350
  manifest: result.manifest,
14890
15351
  io,
14891
15352
  json,
14892
- structurallyValidName: result.manifest.metadata.name
15353
+ structurallyValidName: manifestView(result.manifest).name
14893
15354
  });
14894
15355
  if (unsupported2 !== null)
14895
15356
  return unsupported2;
14896
- const boundary = checkBoundary(path14, result.manifest.app !== undefined);
15357
+ const view = manifestView(result.manifest);
15358
+ const boundary = checkBoundary(path14, view.appDeclared);
14897
15359
  const serveErrors = checkStaticServeDir(path14, result.manifest);
14898
15360
  if (serveErrors.length > 0) {
14899
15361
  boundary.errors = [...boundary.errors, ...serveErrors];
14900
15362
  }
14901
- const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
15363
+ const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(view.access)) : { kind: "skipped" };
14902
15364
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
14903
15365
  }
14904
15366
  function checkBoundary(manifestPath, declared) {
@@ -14922,11 +15384,12 @@ function checkBoundary(manifestPath, declared) {
14922
15384
  return { declared, errors: [], warnings: r.warnings };
14923
15385
  }
14924
15386
  function checkStaticServeDir(manifestPath, manifest) {
14925
- if (manifest.deployment.type !== "static")
15387
+ const view = manifestView(manifest);
15388
+ if (view.deploymentType !== "static")
14926
15389
  return [];
14927
- if (manifest.build?.command !== undefined)
15390
+ if (view.buildCommand !== undefined)
14928
15391
  return [];
14929
- const serveDir = manifest.build?.destination_dir ?? STATIC_SERVE_DIR;
15392
+ const serveDir = view.destinationDir ?? STATIC_SERVE_DIR;
14930
15393
  const normalized = serveDir.replace(/\\/g, "/");
14931
15394
  const segments = normalized.split("/");
14932
15395
  if (normalized === "" || normalized === "." || normalized === "./" || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || segments.some((s) => s === "..")) {
@@ -15026,9 +15489,10 @@ async function checkGroups(groups) {
15026
15489
  return ok ? { kind: "all-ok", perGroup } : { kind: "some-failed", perGroup };
15027
15490
  }
15028
15491
  function renderHumanOk(result, group, boundary, io) {
15029
- io.out(`✓ ${result.manifest.metadata.name} — manifest is valid`);
15030
- io.out(` apiVersion: launchpad.m-kopa.us/v1alpha1`);
15031
- 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}`);
15032
15496
  if (boundary.errors.length > 0) {
15033
15497
  for (const e of boundary.errors) {
15034
15498
  io.err(`✗ app boundary: ${e}`);
@@ -15097,6 +15561,7 @@ function renderHumanError(result, io) {
15097
15561
  return 1;
15098
15562
  case "schema-error":
15099
15563
  io.err(`✗ ${result.path}`);
15564
+ io.err(` validated against schema: ${result.schema}`);
15100
15565
  io.err(` ${result.issues.length} schema issue${result.issues.length === 1 ? "" : "s"}:`);
15101
15566
  for (const issue of result.issues) {
15102
15567
  io.err(` ${issue.path}: ${issue.message}`);
@@ -15105,11 +15570,12 @@ function renderHumanError(result, io) {
15105
15570
  }
15106
15571
  }
15107
15572
  function renderJsonOk(result, group, boundary, io) {
15573
+ const view = manifestView(result.manifest);
15108
15574
  const base = {
15109
15575
  manifest_valid: true,
15110
15576
  runtime_supported: true,
15111
- name: result.manifest.metadata.name,
15112
- deploymentType: result.manifest.deployment.type,
15577
+ name: view.name,
15578
+ deploymentType: view.deploymentType,
15113
15579
  appBoundary: {
15114
15580
  declared: boundary.declared,
15115
15581
  errors: boundary.errors,
@@ -15189,11 +15655,32 @@ function renderJsonError(result, io) {
15189
15655
  ok: false,
15190
15656
  kind: "schema-error",
15191
15657
  path: result.path,
15658
+ schema: result.schema,
15192
15659
  issues: result.issues
15193
15660
  }));
15194
15661
  return 1;
15195
15662
  }
15196
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
+ }
15197
15684
  function parseArgs13(args) {
15198
15685
  let file;
15199
15686
  let json = false;