@m-kopa/launchpad-cli 0.23.0 → 0.24.0

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.
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.23.0";
22
+ var CLI_VERSION = "0.24.0";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
@@ -1219,12 +1219,12 @@ function describe9(e) {
1219
1219
  }
1220
1220
 
1221
1221
  // src/commands/deploy.ts
1222
- import { existsSync as existsSync3 } from "node:fs";
1222
+ import { existsSync as existsSync4 } from "node:fs";
1223
1223
  import * as path6 from "node:path";
1224
1224
 
1225
1225
  // src/bundle/orchestrate.ts
1226
- import { readFileSync as readFileSync3 } from "node:fs";
1227
- import { join as join5 } from "node:path";
1226
+ import { readFileSync as readFileSync4 } from "node:fs";
1227
+ import { join as join6 } from "node:path";
1228
1228
 
1229
1229
  // src/deploy/tar-pack.ts
1230
1230
  import * as fs4 from "node:fs/promises";
@@ -1777,741 +1777,656 @@ async function buildWorkerArtifact(cwd, manifestYaml, walkFiles) {
1777
1777
  };
1778
1778
  }
1779
1779
 
1780
- // src/bundle/orchestrate.ts
1781
- async function bundleAndDeploy(args) {
1782
- const { cfg, cwd, slug } = args;
1783
- const manifestPath = join5(cwd, "launchpad.yaml");
1784
- let manifestYaml;
1785
- try {
1786
- manifestYaml = readFileSync3(manifestPath, "utf8");
1787
- } catch (e) {
1788
- return {
1789
- kind: "no-manifest",
1790
- message: `no launchpad.yaml at ${manifestPath} — run \`launchpad init\` from inside your app directory first. ` + `(${e.message})`
1791
- };
1780
+ // src/bundle/boundary.ts
1781
+ import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync3 } from "node:fs";
1782
+ import { join as join5 } from "node:path";
1783
+ import { parse as parseYaml2 } from "yaml";
1784
+
1785
+ // ../launchpad-engine/dist/schema.js
1786
+ import { z } from "zod";
1787
+ var APP_TYPES2 = ["static", "react", "react+api", "container"];
1788
+ var AUTH_MODES = ["access", "gateway"];
1789
+ var RUNTIMES = ["cloudflare-containers", "aca"];
1790
+ var SECRET_SOURCES = ["env-file", "platform-managed"];
1791
+ var TARGET_KINDS = ["pages", "worker"];
1792
+ var PRIMARY_TARGET = "primary";
1793
+ var SLUG_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
1794
+ var ENV_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
1795
+ var SESSION_DURATION_REGEX = /^[0-9]+(s|m|h)$/;
1796
+ var MetadataSchema = z.object({
1797
+ name: z.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
1798
+ team: z.string().min(1),
1799
+ owner: z.string().email(),
1800
+ description: z.string().max(280).optional()
1801
+ }).strict();
1802
+ var DeploymentSchema = z.object({
1803
+ type: z.enum(APP_TYPES2),
1804
+ runtime: z.enum(RUNTIMES).optional()
1805
+ }).strict();
1806
+ var AccessSchema = z.object({
1807
+ allowed_entra_group: z.string().min(1).optional(),
1808
+ 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(),
1809
+ session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional()
1810
+ }).strict().superRefine((access2, ctx) => {
1811
+ const hasSingular = access2.allowed_entra_group !== undefined;
1812
+ const hasPlural = access2.allowed_entra_groups !== undefined;
1813
+ if (!hasSingular && !hasPlural) {
1814
+ ctx.addIssue({
1815
+ code: z.ZodIssueCode.custom,
1816
+ message: "either `allowed_entra_group` (singular, legacy) or `allowed_entra_groups` (plural, preferred) must be set",
1817
+ path: ["allowed_entra_groups"]
1818
+ });
1792
1819
  }
1793
- let walk;
1794
- try {
1795
- walk = walkCwd(cwd);
1796
- } catch (e) {
1797
- if (e instanceof WalkError) {
1798
- return { kind: "walk-error", message: e.message };
1799
- }
1800
- return {
1801
- kind: "walk-error",
1802
- message: `unexpected walk failure: ${e.message}`
1803
- };
1820
+ if (hasSingular && hasPlural) {
1821
+ ctx.addIssue({
1822
+ code: z.ZodIssueCode.custom,
1823
+ message: "`allowed_entra_group` (singular) and `allowed_entra_groups` (plural) are mutually exclusive; use the plural form",
1824
+ path: ["allowed_entra_groups"]
1825
+ });
1804
1826
  }
1805
- let packResult;
1806
- try {
1807
- packResult = await packTarGz(cwd, walk.files);
1808
- } catch (e) {
1809
- if (e instanceof TarPackError) {
1810
- return { kind: "pack-error", message: e.message };
1811
- }
1812
- return {
1813
- kind: "pack-error",
1814
- message: `unexpected pack failure: ${e.message}`
1815
- };
1827
+ });
1828
+ function allowedEntraGroups(access2) {
1829
+ if (access2.allowed_entra_groups !== undefined) {
1830
+ return access2.allowed_entra_groups;
1816
1831
  }
1817
- const workerBuild = await buildWorkerArtifact(cwd, manifestYaml, walk.files);
1818
- if (workerBuild.kind === "error") {
1819
- return { kind: "worker-build-error", message: workerBuild.message };
1832
+ return [access2.allowed_entra_group];
1833
+ }
1834
+ var ContainerSchema = z.object({
1835
+ name: z.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
1836
+ image: z.string().min(1),
1837
+ port: z.number().int().min(1).max(65535),
1838
+ ingress: z.boolean().optional(),
1839
+ healthcheck: z.string().default("/health"),
1840
+ env: z.array(z.string().regex(ENV_NAME_REGEX, "env entries must be UPPER_SNAKE_CASE")).optional(),
1841
+ max_instances: z.number().int().min(1).max(100).optional()
1842
+ }).strict();
1843
+ var TargetSchema = z.object({
1844
+ kind: z.enum(TARGET_KINDS),
1845
+ script: z.string().min(2).max(63).regex(SLUG_REGEX, "target script must be lowercase letters, digits, and hyphens").optional(),
1846
+ schedule: z.array(z.string().min(1).max(120)).min(1).max(20).optional(),
1847
+ d1_binding: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "d1_binding must be an UPPER_SNAKE_CASE binding name").optional()
1848
+ }).strict();
1849
+ var SecretBindingSchema = z.object({
1850
+ name: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "secret binding name must be UPPER_SNAKE_CASE"),
1851
+ source: z.enum(SECRET_SOURCES),
1852
+ description: z.string().max(200).optional(),
1853
+ targets: z.union([z.literal("all"), z.array(z.string().min(1)).min(1)]).optional()
1854
+ }).strict();
1855
+ var SecretsSchema = z.object({
1856
+ bindings: z.array(SecretBindingSchema).optional()
1857
+ }).strict();
1858
+ var BuildSchema = z.object({
1859
+ command: z.string().min(1),
1860
+ destination_dir: z.string().min(1),
1861
+ root_dir: z.string()
1862
+ }).strict();
1863
+ var AppBoundarySchema = z.object({
1864
+ root: z.string().min(1).optional(),
1865
+ include: z.array(z.string().min(1)).min(1).optional(),
1866
+ exclude: z.array(z.string().min(1)).optional()
1867
+ }).strict();
1868
+ var ProductionEnvSchema = z.record(z.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z.string());
1869
+ var ManifestSchema = z.object({
1870
+ apiVersion: z.literal("launchpad.m-kopa.us/v1alpha1"),
1871
+ kind: z.literal("App"),
1872
+ metadata: MetadataSchema,
1873
+ deployment: DeploymentSchema,
1874
+ access: AccessSchema,
1875
+ auth: z.enum(AUTH_MODES).optional(),
1876
+ hostnames: z.array(z.string().min(1)).optional(),
1877
+ containers: z.array(ContainerSchema).min(1).optional(),
1878
+ secrets: SecretsSchema.optional(),
1879
+ targets: z.array(TargetSchema).optional(),
1880
+ build: BuildSchema.optional(),
1881
+ production_env: ProductionEnvSchema.optional(),
1882
+ app: AppBoundarySchema.optional()
1883
+ }).strict().superRefine((m, ctx) => {
1884
+ const isContainer = m.deployment.type === "container";
1885
+ if (isContainer && m.auth === "gateway") {
1886
+ ctx.addIssue({
1887
+ code: z.ZodIssueCode.custom,
1888
+ path: ["auth"],
1889
+ message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
1890
+ });
1820
1891
  }
1821
- const workerArtifact = workerBuild.kind === "ok" ? workerBuild.artifact : undefined;
1822
- const uploadResult = await uploadBundle(cfg, slug, manifestYaml, packResult.bytes, workerArtifact);
1823
- if (uploadResult.kind !== "ok") {
1824
- return {
1825
- kind: "upload-error",
1826
- status: uploadResult.status,
1827
- body: uploadResult.response
1828
- };
1892
+ if (isContainer && m.containers === undefined) {
1893
+ ctx.addIssue({
1894
+ code: z.ZodIssueCode.custom,
1895
+ path: ["containers"],
1896
+ message: "containers[] is required when deployment.type is 'container'"
1897
+ });
1829
1898
  }
1830
- return {
1831
- kind: "ok",
1832
- result: uploadResult.response,
1833
- walk,
1834
- fileCount: packResult.fileCount,
1835
- compressedBytes: packResult.bytes.byteLength,
1836
- workerScript: workerArtifact?.script ?? null
1837
- };
1838
- }
1839
-
1840
- // src/commands/deploy.ts
1841
- import { parse as parseYaml3 } from "yaml";
1842
- import { readFileSync as readFileSync5 } from "node:fs";
1843
-
1844
- // src/deploy/git-files.ts
1845
- import { spawn as spawn3 } from "node:child_process";
1846
-
1847
- class GitFilesError extends Error {
1848
- code = "git_files_error";
1849
- }
1850
- var PROTECTED_PATHS = new Set([
1851
- ".github/workflows/launchpad-review.yml",
1852
- ".github/workflows/ci.yml",
1853
- "wrangler.toml",
1854
- ".github/scripts/_gha-cmd.mjs",
1855
- ".github/scripts/parse-eslint.mjs",
1856
- ".github/scripts/parse-tsc.mjs",
1857
- ".github/scripts/parse-vitest.mjs",
1858
- ".github/scripts/parse-gitleaks.mjs",
1859
- ".github/scripts/parse-bun-audit.mjs"
1860
- ]);
1861
- async function listDeployFiles(opts) {
1862
- const sp = opts.spawner ?? spawn3;
1863
- const stdout = await runGit2(sp, opts.cwd, [
1864
- "ls-files",
1865
- "-c",
1866
- "-o",
1867
- "--exclude-standard",
1868
- "-z"
1869
- ]);
1870
- const paths = stdout.split("\x00").filter((p) => p.length > 0 && !PROTECTED_PATHS.has(p));
1871
- return [...paths].sort();
1872
- }
1873
- function runGit2(sp, cwd, args) {
1874
- return new Promise((resolve5, reject) => {
1875
- const spawnOpts = { cwd, stdio: ["ignore", "pipe", "pipe"] };
1876
- const child = sp("git", [...args], spawnOpts);
1877
- let stdout = "";
1878
- let stderr = "";
1879
- child.stdout?.on("data", (chunk) => {
1880
- stdout += chunk.toString();
1899
+ if (!isContainer && m.containers !== undefined) {
1900
+ ctx.addIssue({
1901
+ code: z.ZodIssueCode.custom,
1902
+ path: ["containers"],
1903
+ message: `containers[] is only allowed when deployment.type is 'container' (current type: '${m.deployment.type}')`
1881
1904
  });
1882
- child.stderr?.on("data", (chunk) => {
1883
- stderr += chunk.toString();
1905
+ }
1906
+ if (!isContainer && m.deployment.runtime !== undefined) {
1907
+ ctx.addIssue({
1908
+ code: z.ZodIssueCode.custom,
1909
+ path: ["deployment", "runtime"],
1910
+ message: "deployment.runtime is only meaningful when deployment.type is 'container'"
1884
1911
  });
1885
- child.once("error", (err) => {
1886
- reject(new GitFilesError(`could not run \`git ${args.join(" ")}\`: ${err.message} (is git installed?)`));
1912
+ }
1913
+ if (isContainer && m.build !== undefined) {
1914
+ ctx.addIssue({
1915
+ code: z.ZodIssueCode.custom,
1916
+ path: ["build"],
1917
+ message: "build is only allowed for pages-app deployments (static/react/react+api). Container builds live in the Dockerfile."
1887
1918
  });
1888
- child.once("close", (code) => {
1889
- if (code === 0) {
1890
- resolve5(stdout);
1891
- return;
1892
- }
1893
- reject(new GitFilesError(`\`git ${args.join(" ")}\` exited ${code}: ${stderr.trim()}`));
1919
+ }
1920
+ if (isContainer && m.production_env !== undefined) {
1921
+ ctx.addIssue({
1922
+ code: z.ZodIssueCode.custom,
1923
+ path: ["production_env"],
1924
+ message: "production_env is only allowed for pages-app deployments. Container env vars use containers[].env (names only) + the bot's secrets pipeline for values."
1894
1925
  });
1895
- });
1896
- }
1897
-
1898
- // src/commands/deploy-flags.ts
1899
- var SLUG_RE3 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
1900
- var GROUP_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
1901
- var GROUP_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1902
- function parseDeployFlags(args) {
1903
- let mode = "content";
1904
- let slug = null;
1905
- let displayName = null;
1906
- let description = null;
1907
- let appType = null;
1908
- const allowedGroups = [];
1909
- let message = null;
1910
- let timeoutSeconds = null;
1911
- let manifestFile = null;
1912
- let dryRunJson = false;
1913
- let platformRepo = null;
1914
- let rePin = false;
1915
- let yes = false;
1916
- let resumePrNumber = null;
1917
- let timeoutMinutes = null;
1918
- let modeFlagsSeen = 0;
1919
- let i = 0;
1920
- while (i < args.length) {
1921
- const a = args[i] ?? "";
1922
- if (a === "--new") {
1923
- mode = "new";
1924
- modeFlagsSeen += 1;
1925
- i += 1;
1926
- continue;
1927
- }
1928
- if (a === "--dry-run") {
1929
- mode = "dry-run";
1930
- modeFlagsSeen += 1;
1931
- i += 1;
1932
- continue;
1933
- }
1934
- if (a === "--apply") {
1935
- mode = "apply";
1936
- modeFlagsSeen += 1;
1937
- i += 1;
1938
- continue;
1926
+ }
1927
+ if (isContainer && m.containers !== undefined && m.containers.length > 1) {
1928
+ const ingressContainers = m.containers.filter((c) => c.ingress === true);
1929
+ if (ingressContainers.length !== 1) {
1930
+ ctx.addIssue({
1931
+ code: z.ZodIssueCode.custom,
1932
+ path: ["containers"],
1933
+ message: `multi-container apps must mark exactly one container ingress: true (found ${ingressContainers.length})`
1934
+ });
1939
1935
  }
1940
- if (a === "--platform-repo") {
1941
- const v = args[i + 1];
1942
- if (v === undefined || v.trim().length === 0) {
1943
- return `missing value for ${a}`;
1936
+ }
1937
+ if (isContainer && m.containers !== undefined) {
1938
+ const declaredBindings = new Set((m.secrets?.bindings ?? []).map((b) => b.name));
1939
+ for (let i = 0;i < m.containers.length; i++) {
1940
+ const c = m.containers[i];
1941
+ for (const envName of c.env ?? []) {
1942
+ if (!declaredBindings.has(envName)) {
1943
+ ctx.addIssue({
1944
+ code: z.ZodIssueCode.custom,
1945
+ path: ["containers", i, "env"],
1946
+ message: `env reference '${envName}' has no matching entry in secrets.bindings[]`
1947
+ });
1948
+ }
1944
1949
  }
1945
- platformRepo = v.trim();
1946
- i += 2;
1947
- continue;
1948
1950
  }
1949
- if (a === "--re-pin") {
1950
- rePin = true;
1951
- i += 1;
1952
- continue;
1951
+ }
1952
+ if (isContainer && m.containers !== undefined) {
1953
+ const seen = new Set;
1954
+ for (let i = 0;i < m.containers.length; i++) {
1955
+ const name = m.containers[i]?.name;
1956
+ if (name === undefined)
1957
+ continue;
1958
+ if (seen.has(name)) {
1959
+ ctx.addIssue({
1960
+ code: z.ZodIssueCode.custom,
1961
+ path: ["containers", i, "name"],
1962
+ message: `duplicate container name '${name}' — names become DO class identifiers and must be unique`
1963
+ });
1964
+ }
1965
+ seen.add(name);
1953
1966
  }
1954
- if (a === "--yes" || a === "-y") {
1955
- yes = true;
1956
- i += 1;
1957
- continue;
1967
+ }
1968
+ const bindings = m.secrets?.bindings ?? [];
1969
+ if (bindings.length > 0) {
1970
+ const seen = new Set;
1971
+ for (let i = 0;i < bindings.length; i++) {
1972
+ const name = bindings[i]?.name;
1973
+ if (name === undefined)
1974
+ continue;
1975
+ if (seen.has(name)) {
1976
+ ctx.addIssue({
1977
+ code: z.ZodIssueCode.custom,
1978
+ path: ["secrets", "bindings", i, "name"],
1979
+ message: `duplicate secret binding name '${name}' — names become env-var keys and must be unique`
1980
+ });
1981
+ }
1982
+ seen.add(name);
1958
1983
  }
1959
- if (a === "--resume") {
1960
- const v = args[i + 1];
1961
- if (v === undefined)
1962
- return `missing value for ${a}`;
1963
- mode = "resume";
1964
- slug = v;
1965
- modeFlagsSeen += 1;
1966
- i += 2;
1967
- continue;
1984
+ }
1985
+ const targets = m.targets ?? [];
1986
+ for (let i = 0;i < targets.length; i++) {
1987
+ const t = targets[i];
1988
+ if (t.kind === "worker" && t.script === undefined) {
1989
+ ctx.addIssue({
1990
+ code: z.ZodIssueCode.custom,
1991
+ path: ["targets", i, "script"],
1992
+ message: "a worker target must declare a script (the sibling Workers script name)"
1993
+ });
1968
1994
  }
1969
- if (a === "--abandon") {
1970
- const v = args[i + 1];
1971
- if (v === undefined)
1972
- return `missing value for ${a}`;
1973
- mode = "abandon";
1974
- slug = v;
1975
- modeFlagsSeen += 1;
1976
- i += 2;
1977
- continue;
1995
+ if (t.kind === "pages" && t.script !== undefined) {
1996
+ ctx.addIssue({
1997
+ code: z.ZodIssueCode.custom,
1998
+ path: ["targets", i, "script"],
1999
+ message: "a pages target must not declare a script — the primary Pages project is the app slug"
2000
+ });
1978
2001
  }
1979
- if (a === "--file") {
1980
- const v = args[i + 1];
1981
- if (v === undefined)
1982
- return `missing value for ${a}`;
1983
- manifestFile = v;
1984
- i += 2;
1985
- continue;
2002
+ if (t.kind === "pages" && t.schedule !== undefined) {
2003
+ ctx.addIssue({
2004
+ code: z.ZodIssueCode.custom,
2005
+ path: ["targets", i, "schedule"],
2006
+ message: "schedule is only valid on a worker target (the cron deploy contract — ADR 0022)"
2007
+ });
1986
2008
  }
1987
- if (a === "--json") {
1988
- dryRunJson = true;
1989
- i += 1;
2009
+ }
2010
+ const seenScripts = new Set;
2011
+ let pagesTargets = 0;
2012
+ for (let i = 0;i < targets.length; i++) {
2013
+ const t = targets[i];
2014
+ if (t.kind === "pages") {
2015
+ pagesTargets++;
2016
+ if (pagesTargets > 1) {
2017
+ ctx.addIssue({
2018
+ code: z.ZodIssueCode.custom,
2019
+ path: ["targets", i, "kind"],
2020
+ message: "at most one pages target may be declared (the primary Pages project is singular)"
2021
+ });
2022
+ }
1990
2023
  continue;
1991
2024
  }
1992
- if (a === "--slug") {
1993
- const v = args[i + 1];
1994
- if (v === undefined)
1995
- return `missing value for ${a}`;
1996
- slug = v;
1997
- i += 2;
2025
+ if (t.script === undefined)
1998
2026
  continue;
1999
- }
2000
- if (a === "--display-name" || a === "--name") {
2001
- const v = args[i + 1];
2002
- if (v === undefined)
2003
- return `missing value for ${a}`;
2004
- displayName = v;
2005
- i += 2;
2027
+ if (t.script === PRIMARY_TARGET || t.script === "pages") {
2028
+ ctx.addIssue({
2029
+ code: z.ZodIssueCode.custom,
2030
+ path: ["targets", i, "script"],
2031
+ message: `worker target script '${t.script}' is a reserved selector token — use a different script name`
2032
+ });
2006
2033
  continue;
2007
2034
  }
2008
- if (a === "--description") {
2009
- const v = args[i + 1];
2010
- if (v === undefined)
2011
- return `missing value for ${a}`;
2012
- description = v;
2013
- i += 2;
2014
- continue;
2035
+ if (seenScripts.has(t.script)) {
2036
+ ctx.addIssue({
2037
+ code: z.ZodIssueCode.custom,
2038
+ path: ["targets", i, "script"],
2039
+ message: `duplicate worker target script '${t.script}' — target scripts must be unique`
2040
+ });
2015
2041
  }
2016
- if (a === "--app-type" || a === "--type") {
2017
- const v = args[i + 1];
2018
- if (v === undefined)
2019
- return `missing value for ${a}`;
2020
- if (!APP_TYPES.includes(v)) {
2021
- return `invalid --app-type "${v}" must be one of ${APP_TYPES.join(", ")}`;
2022
- }
2023
- appType = v;
2024
- i += 2;
2042
+ seenScripts.add(t.script);
2043
+ }
2044
+ const validRefs = new Set([PRIMARY_TARGET, ...seenScripts]);
2045
+ if (pagesTargets > 0)
2046
+ validRefs.add("pages");
2047
+ for (let bi = 0;bi < bindings.length; bi++) {
2048
+ const b = bindings[bi];
2049
+ const sel = b?.targets;
2050
+ if (sel === undefined || sel === "all")
2025
2051
  continue;
2026
- }
2027
- if (a === "--allowed-group" || a === "--group") {
2028
- const v = args[i + 1];
2029
- if (v === undefined)
2030
- return `missing value for ${a}`;
2031
- if (!GROUP_KEY_RE2.test(v) && !GROUP_UUID_RE2.test(v)) {
2032
- return `invalid --allowed-group "${v}" — expected a group key (HCL identifier, e.g. G_Product_Security) or an Entra Object-ID UUID`;
2052
+ for (const ref of sel) {
2053
+ if (!validRefs.has(ref)) {
2054
+ ctx.addIssue({
2055
+ code: z.ZodIssueCode.custom,
2056
+ path: ["secrets", "bindings", bi, "targets"],
2057
+ message: `secret binding '${b?.name}' references unknown target '${ref}' — declare it in targets[] or use "primary"`
2058
+ });
2033
2059
  }
2034
- allowedGroups.push(v);
2035
- i += 2;
2036
- continue;
2037
2060
  }
2038
- if (a === "--message" || a === "-m") {
2039
- const v = args[i + 1];
2040
- if (v === undefined)
2041
- return `missing value for ${a}`;
2042
- message = v;
2043
- i += 2;
2044
- continue;
2061
+ }
2062
+ });
2063
+ function parseManifest(input) {
2064
+ const result = ManifestSchema.safeParse(input);
2065
+ if (result.success) {
2066
+ return { kind: "ok", manifest: result.data };
2067
+ }
2068
+ return {
2069
+ kind: "schema-error",
2070
+ issues: result.error.issues.map((issue) => ({
2071
+ path: issue.path.map(String).join(".") || "(root)",
2072
+ message: issue.message
2073
+ }))
2074
+ };
2075
+ }
2076
+ // ../launchpad-engine/dist/app-boundary.js
2077
+ var INCLUDE_EVERYTHING = ["**"];
2078
+ var COMMON_INCLUDE = [
2079
+ "launchpad.yaml",
2080
+ ".gitignore",
2081
+ ".launchpadignore",
2082
+ "package.json",
2083
+ "package-lock.json",
2084
+ "bun.lock",
2085
+ "bun.lockb",
2086
+ "yarn.lock",
2087
+ "pnpm-lock.yaml",
2088
+ "tsconfig.json",
2089
+ "tsconfig.*.json",
2090
+ "README.md",
2091
+ "LICENSE",
2092
+ "index.html",
2093
+ "vite.config.*",
2094
+ "src/**",
2095
+ "public/**",
2096
+ "worker/**"
2097
+ ];
2098
+ function defaultIncludeForAppType(appType) {
2099
+ switch (appType) {
2100
+ case "static":
2101
+ return [...COMMON_INCLUDE, "static/**", "assets/**", "css/**", "js/**", "images/**", "*.html", "*.css", "*.js"];
2102
+ case "react":
2103
+ return COMMON_INCLUDE;
2104
+ case "react+api":
2105
+ return [...COMMON_INCLUDE, "functions/**"];
2106
+ case "container":
2107
+ return [...COMMON_INCLUDE, "container/**", "Dockerfile", "Dockerfile.*", "wrangler.toml"];
2108
+ default:
2109
+ return INCLUDE_EVERYTHING;
2110
+ }
2111
+ }
2112
+ function resolveAppBoundary(input) {
2113
+ if (input.app === undefined) {
2114
+ return { root: ".", include: INCLUDE_EVERYTHING, exclude: [], source: "inferred" };
2115
+ }
2116
+ return {
2117
+ root: normaliseRoot(input.app.root ?? "."),
2118
+ include: input.app.include ?? defaultIncludeForAppType(input.appType),
2119
+ exclude: input.app.exclude ?? [],
2120
+ source: "declared"
2121
+ };
2122
+ }
2123
+ function normaliseRoot(root) {
2124
+ let r = root.replace(/\\/g, "/").trim();
2125
+ if (r.startsWith("./"))
2126
+ r = r.slice(2);
2127
+ while (r.endsWith("/"))
2128
+ r = r.slice(0, -1);
2129
+ return r === "" ? "." : r;
2130
+ }
2131
+ var APP_TYPE_SET = new Set(["static", "react", "react+api", "container"]);
2132
+ function extractBoundaryInput(parsedManifest) {
2133
+ const m = asRecord(parsedManifest);
2134
+ if (m === undefined) {
2135
+ return { kind: "ok", appType: undefined, app: undefined, build: undefined };
2136
+ }
2137
+ const spec = asRecord(m["spec"]);
2138
+ const v2 = spec !== undefined;
2139
+ const rawType = v2 ? spec["appType"] : asRecord(m["deployment"])?.["type"];
2140
+ const appType = typeof rawType === "string" && APP_TYPE_SET.has(rawType) ? rawType : undefined;
2141
+ const rawApp = v2 ? spec["app"] : m["app"];
2142
+ let app;
2143
+ if (rawApp !== undefined) {
2144
+ const parsed = AppBoundarySchema.safeParse(rawApp);
2145
+ if (!parsed.success) {
2146
+ return {
2147
+ kind: "schema-error",
2148
+ issues: parsed.error.issues.map((i) => ({
2149
+ path: `${v2 ? "spec." : ""}app${i.path.length > 0 ? "." + i.path.map(String).join(".") : ""}`,
2150
+ message: i.message
2151
+ }))
2152
+ };
2045
2153
  }
2046
- if (a === "--timeout-seconds") {
2047
- const v = args[i + 1];
2048
- if (v === undefined)
2049
- return `missing value for ${a}`;
2050
- const n = Number(v);
2051
- if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
2052
- return `invalid --timeout-seconds "${v}" — expected positive integer`;
2053
- }
2054
- timeoutSeconds = n;
2055
- i += 2;
2056
- continue;
2057
- }
2058
- if (a === "--timeout-minutes") {
2059
- const v = args[i + 1];
2060
- if (v === undefined)
2061
- return `missing value for ${a}`;
2062
- const n = Number(v);
2063
- if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
2064
- return `invalid --timeout-minutes "${v}" — expected positive integer`;
2065
- }
2066
- timeoutMinutes = n;
2067
- i += 2;
2154
+ app = parsed.data;
2155
+ }
2156
+ const rawBuild = asRecord(v2 ? spec["build"] : m["build"]);
2157
+ const build = rawBuild !== undefined && typeof rawBuild["command"] === "string" && typeof rawBuild["root_dir"] === "string" ? { command: rawBuild["command"], root_dir: rawBuild["root_dir"] } : undefined;
2158
+ return { kind: "ok", appType, app, build };
2159
+ }
2160
+ function asRecord(v) {
2161
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
2162
+ }
2163
+ function parseLaunchpadIgnore(content) {
2164
+ const out = [];
2165
+ for (const raw of content.split(/\r?\n/)) {
2166
+ const line = raw.trim();
2167
+ if (line === "" || line.startsWith("#") || line.startsWith("!"))
2068
2168
  continue;
2069
- }
2070
- if (a === "--resume-pr") {
2071
- const v = args[i + 1];
2072
- if (v === undefined)
2073
- return `missing value for ${a}`;
2074
- const n = Number(v);
2075
- if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
2076
- return `invalid --resume-pr "${v}" — expected positive integer PR number`;
2169
+ out.push(line);
2170
+ }
2171
+ return out;
2172
+ }
2173
+ function globToRegExp2(glob) {
2174
+ let re = "";
2175
+ let i = 0;
2176
+ while (i < glob.length) {
2177
+ const ch = glob[i];
2178
+ if (ch === "*") {
2179
+ if (glob[i + 1] === "*") {
2180
+ if (glob[i + 2] === "/") {
2181
+ re += "(?:.*/)?";
2182
+ i += 3;
2183
+ } else {
2184
+ re += ".*";
2185
+ i += 2;
2186
+ }
2187
+ } else {
2188
+ re += "[^/]*";
2189
+ i += 1;
2077
2190
  }
2078
- resumePrNumber = n;
2079
- i += 2;
2080
- continue;
2191
+ } else if (ch === "?") {
2192
+ re += "[^/]";
2193
+ i += 1;
2194
+ } else if (ch === "/" || /[A-Za-z0-9_-]/.test(ch)) {
2195
+ re += ch;
2196
+ i += 1;
2197
+ } else {
2198
+ re += `\\${ch}`;
2199
+ i += 1;
2081
2200
  }
2082
- return `unknown argument "${a}"`;
2083
- }
2084
- if (modeFlagsSeen > 1) {
2085
- return "--new, --resume, --abandon, --dry-run, and --apply are mutually exclusive";
2086
- }
2087
- if (mode === "dry-run") {
2088
- if (slug !== null)
2089
- return "--dry-run does not accept --slug (slug is read from launchpad.yaml)";
2090
- if (displayName !== null)
2091
- return "--dry-run does not accept --display-name";
2092
- if (description !== null)
2093
- return "--dry-run does not accept --description";
2094
- if (appType !== null)
2095
- return "--dry-run does not accept --app-type";
2096
- if (allowedGroups.length > 0)
2097
- return "--dry-run does not accept --allowed-group (read from launchpad.yaml)";
2098
- if (message !== null)
2099
- return "--dry-run does not accept --message";
2100
- if (timeoutSeconds !== null)
2101
- return "--dry-run does not accept --timeout-seconds";
2102
- if (platformRepo !== null)
2103
- return "--dry-run does not accept --platform-repo (apply-only)";
2104
- if (rePin)
2105
- return "--dry-run does not accept --re-pin (apply-only)";
2106
- if (yes)
2107
- return "--dry-run does not accept --yes (apply-only)";
2108
- return {
2109
- mode: { kind: "dry-run", file: manifestFile, json: dryRunJson },
2110
- message: null,
2111
- timeoutSeconds: null
2112
- };
2113
2201
  }
2114
- if (mode === "apply") {
2115
- if (slug !== null)
2116
- return "--apply does not accept --slug (slug is read from launchpad.yaml)";
2117
- if (displayName !== null)
2118
- return "--apply does not accept --display-name";
2119
- if (description !== null)
2120
- return "--apply does not accept --description";
2121
- if (appType !== null)
2122
- return "--apply does not accept --app-type";
2123
- if (allowedGroups.length > 0)
2124
- return "--apply does not accept --allowed-group (read from launchpad.yaml)";
2125
- if (message !== null)
2126
- return "--apply does not accept --message";
2127
- if (timeoutSeconds !== null)
2128
- return "--apply does not accept --timeout-seconds (use --timeout-minutes instead — apply polling is multi-minute)";
2129
- if (dryRunJson)
2130
- return "--apply does not accept --json";
2131
- return {
2132
- mode: {
2133
- kind: "apply",
2134
- file: manifestFile,
2135
- platformRepo,
2136
- rePin,
2137
- yes,
2138
- resumePrNumber,
2139
- timeoutMinutes
2140
- },
2141
- message: null,
2142
- timeoutSeconds: null
2143
- };
2202
+ return new RegExp(`^${re}$`);
2203
+ }
2204
+ var GLOB_CHARS = /[*?]/;
2205
+ function matchContractPattern(pattern, relPath) {
2206
+ const p = pattern.replace(/\/+$/, "");
2207
+ if (!GLOB_CHARS.test(p)) {
2208
+ return relPath === p || relPath.startsWith(`${p}/`);
2144
2209
  }
2145
- if (resumePrNumber !== null) {
2146
- return "--resume-pr is only valid with --apply";
2210
+ return globToRegExp2(p).test(relPath);
2211
+ }
2212
+ function matchIgnorePattern(pattern, relPath) {
2213
+ let p = pattern;
2214
+ const dirOnly = p.endsWith("/");
2215
+ if (dirOnly)
2216
+ p = p.replace(/\/+$/, "");
2217
+ const anchored = p.startsWith("/");
2218
+ if (anchored)
2219
+ p = p.slice(1);
2220
+ if (!anchored && !p.includes("/")) {
2221
+ const re2 = globToRegExp2(p);
2222
+ const segments = relPath.split("/");
2223
+ const candidates = dirOnly ? segments.slice(0, -1) : segments;
2224
+ return candidates.some((seg) => re2.test(seg));
2147
2225
  }
2148
- if (timeoutMinutes !== null) {
2149
- return "--timeout-minutes is only valid with --apply";
2226
+ if (!GLOB_CHARS.test(p)) {
2227
+ return !dirOnly && relPath === p || relPath.startsWith(`${p}/`);
2150
2228
  }
2151
- if (manifestFile !== null) {
2152
- return "--file is only valid with --dry-run or --apply";
2229
+ const re = globToRegExp2(p);
2230
+ if (dirOnly)
2231
+ return re.test(relPath.split("/").slice(0, -1).join("/"));
2232
+ return re.test(relPath) || re.test(relPath.split("/").slice(0, -1).join("/"));
2233
+ }
2234
+ var PLATFORM_SEEDED_GITHUB_PATHS = new Set([
2235
+ ".github/workflows/launchpad-review.yml",
2236
+ ".github/workflows/ci.yml",
2237
+ ".github/scripts/_gha-cmd.mjs",
2238
+ ".github/scripts/parse-eslint.mjs",
2239
+ ".github/scripts/parse-tsc.mjs",
2240
+ ".github/scripts/parse-vitest.mjs",
2241
+ ".github/scripts/parse-gitleaks.mjs",
2242
+ ".github/scripts/parse-bun-audit.mjs"
2243
+ ]);
2244
+ function checkDenyList(path6) {
2245
+ const segments = path6.split("/");
2246
+ const basename = segments[segments.length - 1];
2247
+ const isExample = basename.endsWith(".example");
2248
+ if (basename.startsWith(".env") && !isExample) {
2249
+ return { path: path6, rule: "env-file", message: `'${path6}' is an environment file (.env*) — never shippable` };
2153
2250
  }
2154
- if (dryRunJson) {
2155
- return "--json is only valid with --dry-run";
2251
+ if (basename === ".npmrc") {
2252
+ return { path: path6, rule: "npmrc", message: `'${path6}' (.npmrc) may carry registry auth — never shippable` };
2156
2253
  }
2157
- if (platformRepo !== null) {
2158
- return "--platform-repo is only valid with --apply";
2254
+ if (basename.startsWith(".dev.vars") && !isExample) {
2255
+ return { path: path6, rule: "dev-vars", message: `'${path6}' is a wrangler dev-vars file — never shippable` };
2159
2256
  }
2160
- if (rePin) {
2161
- return "--re-pin is only valid with --apply";
2257
+ if (segments.includes(".claude")) {
2258
+ return { path: path6, rule: "claude-dir", message: `'${path6}' is under a .claude/ directory (AI-workspace state) — never shippable` };
2162
2259
  }
2163
- if (yes) {
2164
- return "--yes is only valid with --apply";
2260
+ if (segments.includes(".git")) {
2261
+ return { path: path6, rule: "git-dir", message: `'${path6}' is under .git/ never shippable` };
2165
2262
  }
2166
- if (mode === "content") {
2167
- if (slug !== null && !SLUG_RE3.test(slug)) {
2168
- return `invalid slug "${slug}" — expected ${SLUG_RE3.source}`;
2169
- }
2263
+ if (segments[0] === ".github") {
2264
+ if (PLATFORM_SEEDED_GITHUB_PATHS.has(path6))
2265
+ return "seeded";
2170
2266
  return {
2171
- mode: { kind: "content", slug },
2172
- message,
2173
- timeoutSeconds
2267
+ path: path6,
2268
+ rule: "github-dir",
2269
+ message: `'${path6}' — repo-root .github/ is platform-owned (workflows execute under the M-KOPA Actions identity); developer-supplied .github/** is not shippable`
2174
2270
  };
2175
2271
  }
2176
- if (mode === "resume" || mode === "abandon") {
2177
- if (slug === null || !SLUG_RE3.test(slug)) {
2178
- return `--${mode} requires a valid slug (kebab-case)`;
2272
+ return null;
2273
+ }
2274
+ function checkSecretShape(path6, readFileContent) {
2275
+ const segments = path6.split("/");
2276
+ const basename = segments[segments.length - 1];
2277
+ const deny = (what) => ({
2278
+ path: path6,
2279
+ rule: "secret-shape",
2280
+ message: `'${path6}' looks like ${what} — never shippable; if this is a false positive, rename the file`
2281
+ });
2282
+ if (basename.endsWith(".pem"))
2283
+ return deny("PEM key material (*.pem)");
2284
+ if (basename.endsWith(".key"))
2285
+ return deny("private key material (*.key)");
2286
+ if (basename.startsWith("id_rsa"))
2287
+ return deny("an SSH key (id_rsa*)");
2288
+ if (basename.endsWith(".p12") || basename.endsWith(".pfx")) {
2289
+ return deny("a PKCS#12 key store (*.p12 / *.pfx)");
2290
+ }
2291
+ if (basename === "credentials.json")
2292
+ return deny("a credentials file (credentials.json)");
2293
+ if (segments.includes(".aws"))
2294
+ return deny("AWS credentials (.aws/**)");
2295
+ if (basename.endsWith(".json") && readFileContent !== undefined) {
2296
+ const content = readFileContent(path6);
2297
+ if (content !== undefined && /"private_key"\s*:/.test(content)) {
2298
+ return deny('service-account-shaped JSON (carries a "private_key" field)');
2179
2299
  }
2180
- if (displayName !== null || appType !== null || allowedGroups.length > 0) {
2181
- return `--${mode} does not accept create-only flags (--display-name / --app-type / --allowed-group)`;
2300
+ }
2301
+ return null;
2302
+ }
2303
+ var ALWAYS_SHIP = "launchpad.yaml";
2304
+ function applyAppBoundary(contract, files, options = {}) {
2305
+ const ignorePatterns = options.launchpadIgnore ?? [];
2306
+ const included = [];
2307
+ const excluded = [];
2308
+ const denied = [];
2309
+ const rootPrefix = contract.root === "." ? "" : `${contract.root}/`;
2310
+ for (const path6 of [...files].sort()) {
2311
+ if (path6 !== ALWAYS_SHIP) {
2312
+ if (rootPrefix !== "" && !path6.startsWith(rootPrefix)) {
2313
+ excluded.push({ path: path6, reason: "outside-root" });
2314
+ continue;
2315
+ }
2316
+ const rel = rootPrefix === "" ? path6 : path6.slice(rootPrefix.length);
2317
+ if (!contract.include.some((p) => matchContractPattern(p, rel))) {
2318
+ excluded.push({ path: path6, reason: "not-included" });
2319
+ continue;
2320
+ }
2321
+ if (contract.exclude.some((p) => matchContractPattern(p, rel))) {
2322
+ excluded.push({ path: path6, reason: "exclude" });
2323
+ continue;
2324
+ }
2325
+ if (ignorePatterns.some((p) => matchIgnorePattern(p, path6))) {
2326
+ excluded.push({ path: path6, reason: "launchpadignore" });
2327
+ continue;
2328
+ }
2182
2329
  }
2183
- if (message !== null) {
2184
- return `--${mode} does not accept --message`;
2330
+ const denial = checkDenyList(path6);
2331
+ if (denial === "seeded") {
2332
+ excluded.push({ path: path6, reason: "platform-seeded" });
2333
+ continue;
2185
2334
  }
2186
- return {
2187
- mode: { kind: mode, slug },
2188
- message: null,
2189
- timeoutSeconds
2190
- };
2191
- }
2192
- if (slug === null)
2193
- return "--new requires --slug";
2194
- if (!SLUG_RE3.test(slug))
2195
- return `invalid slug "${slug}" — expected ${SLUG_RE3.source}`;
2196
- if (slug.length < 3 || slug.length > 58) {
2197
- return `slug length out of bounds (3–58 chars): "${slug}"`;
2198
- }
2199
- if (displayName === null || displayName.length === 0) {
2200
- return "--new requires --display-name";
2201
- }
2202
- if (appType === null) {
2203
- return `--new requires --app-type (one of ${APP_TYPES.join(", ")})`;
2204
- }
2205
- if (allowedGroups.length === 0) {
2206
- return "--new requires at least one --allowed-group";
2335
+ if (denial !== null) {
2336
+ denied.push(denial);
2337
+ continue;
2338
+ }
2339
+ const secret = checkSecretShape(path6, options.readFileContent);
2340
+ if (secret !== null) {
2341
+ denied.push(secret);
2342
+ continue;
2343
+ }
2344
+ included.push(path6);
2207
2345
  }
2208
- return {
2209
- mode: {
2210
- kind: "new",
2211
- slug,
2212
- displayName,
2213
- description,
2214
- appType,
2215
- allowedGroups
2216
- },
2217
- message,
2218
- timeoutSeconds
2219
- };
2346
+ return { files: included, excluded, denied };
2220
2347
  }
2221
-
2222
- // src/deploy/apply.ts
2223
- import { existsSync as existsSync2, rmSync } from "node:fs";
2224
- import { resolve as resolvePath, join as join6 } from "node:path";
2225
- import { execFileSync } from "node:child_process";
2226
-
2227
- // src/manifest/load.ts
2228
- import { readFileSync as readFileSync4 } from "node:fs";
2229
- import { parse as parseYaml2, YAMLParseError } from "yaml";
2230
-
2231
- // ../launchpad-engine/dist/schema.js
2232
- import { z } from "zod";
2233
- var APP_TYPES2 = ["static", "react", "react+api", "container"];
2234
- var AUTH_MODES = ["access", "gateway"];
2235
- var RUNTIMES = ["cloudflare-containers", "aca"];
2236
- var SECRET_SOURCES = ["env-file", "platform-managed"];
2237
- var TARGET_KINDS = ["pages", "worker"];
2238
- var PRIMARY_TARGET = "primary";
2239
- var SLUG_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
2240
- var ENV_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
2241
- var SESSION_DURATION_REGEX = /^[0-9]+(s|m|h)$/;
2242
- var MetadataSchema = z.object({
2243
- name: z.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
2244
- team: z.string().min(1),
2245
- owner: z.string().email(),
2246
- description: z.string().max(280).optional()
2247
- }).strict();
2248
- var DeploymentSchema = z.object({
2249
- type: z.enum(APP_TYPES2),
2250
- runtime: z.enum(RUNTIMES).optional()
2251
- }).strict();
2252
- var AccessSchema = z.object({
2253
- allowed_entra_group: z.string().min(1).optional(),
2254
- 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(),
2255
- session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional()
2256
- }).strict().superRefine((access2, ctx) => {
2257
- const hasSingular = access2.allowed_entra_group !== undefined;
2258
- const hasPlural = access2.allowed_entra_groups !== undefined;
2259
- if (!hasSingular && !hasPlural) {
2260
- ctx.addIssue({
2261
- code: z.ZodIssueCode.custom,
2262
- message: "either `allowed_entra_group` (singular, legacy) or `allowed_entra_groups` (plural, preferred) must be set",
2263
- path: ["allowed_entra_groups"]
2264
- });
2265
- }
2266
- if (hasSingular && hasPlural) {
2267
- ctx.addIssue({
2268
- code: z.ZodIssueCode.custom,
2269
- message: "`allowed_entra_group` (singular) and `allowed_entra_groups` (plural) are mutually exclusive; use the plural form",
2270
- path: ["allowed_entra_groups"]
2348
+ var PLAIN_PATH = /^(\.\/)?[A-Za-z0-9_@][A-Za-z0-9_@.\/-]*$/;
2349
+ var OPAQUE_SEGMENT = /[$`(){}<]/;
2350
+ function verifyBuildInputs(build, files) {
2351
+ if (build === undefined)
2352
+ return [];
2353
+ const issues = [];
2354
+ const fileSet = new Set(files);
2355
+ const dirSet = new Set;
2356
+ for (const f of files) {
2357
+ const parts = f.split("/");
2358
+ for (let i = 1;i < parts.length; i++) {
2359
+ dirSet.add(parts.slice(0, i).join("/"));
2360
+ }
2361
+ }
2362
+ const exists = (p) => fileSet.has(p) || dirSet.has(p);
2363
+ const rootDir = normaliseRoot(build.root_dir === "" ? "." : build.root_dir);
2364
+ if (rootDir !== "." && !dirSet.has(rootDir)) {
2365
+ issues.push({
2366
+ input: rootDir,
2367
+ message: `build.root_dir '${rootDir}' does not exist in the bundle — the build would run in a missing directory`
2271
2368
  });
2369
+ return issues;
2272
2370
  }
2273
- });
2274
- function allowedEntraGroups(access2) {
2275
- if (access2.allowed_entra_groups !== undefined) {
2276
- return access2.allowed_entra_groups;
2277
- }
2278
- return [access2.allowed_entra_group];
2279
- }
2280
- var ContainerSchema = z.object({
2281
- name: z.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
2282
- image: z.string().min(1),
2283
- port: z.number().int().min(1).max(65535),
2284
- ingress: z.boolean().optional(),
2285
- healthcheck: z.string().default("/health"),
2286
- env: z.array(z.string().regex(ENV_NAME_REGEX, "env entries must be UPPER_SNAKE_CASE")).optional(),
2287
- max_instances: z.number().int().min(1).max(100).optional()
2288
- }).strict();
2289
- var TargetSchema = z.object({
2290
- kind: z.enum(TARGET_KINDS),
2291
- script: z.string().min(2).max(63).regex(SLUG_REGEX, "target script must be lowercase letters, digits, and hyphens").optional(),
2292
- schedule: z.array(z.string().min(1).max(120)).min(1).max(20).optional(),
2293
- d1_binding: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "d1_binding must be an UPPER_SNAKE_CASE binding name").optional()
2294
- }).strict();
2295
- var SecretBindingSchema = z.object({
2296
- name: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "secret binding name must be UPPER_SNAKE_CASE"),
2297
- source: z.enum(SECRET_SOURCES),
2298
- description: z.string().max(200).optional(),
2299
- targets: z.union([z.literal("all"), z.array(z.string().min(1)).min(1)]).optional()
2300
- }).strict();
2301
- var SecretsSchema = z.object({
2302
- bindings: z.array(SecretBindingSchema).optional()
2303
- }).strict();
2304
- var BuildSchema = z.object({
2305
- command: z.string().min(1),
2306
- destination_dir: z.string().min(1),
2307
- root_dir: z.string()
2308
- }).strict();
2309
- var ProductionEnvSchema = z.record(z.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z.string());
2310
- var ManifestSchema = z.object({
2311
- apiVersion: z.literal("launchpad.m-kopa.us/v1alpha1"),
2312
- kind: z.literal("App"),
2313
- metadata: MetadataSchema,
2314
- deployment: DeploymentSchema,
2315
- access: AccessSchema,
2316
- auth: z.enum(AUTH_MODES).optional(),
2317
- hostnames: z.array(z.string().min(1)).optional(),
2318
- containers: z.array(ContainerSchema).min(1).optional(),
2319
- secrets: SecretsSchema.optional(),
2320
- targets: z.array(TargetSchema).optional(),
2321
- build: BuildSchema.optional(),
2322
- production_env: ProductionEnvSchema.optional()
2323
- }).strict().superRefine((m, ctx) => {
2324
- const isContainer = m.deployment.type === "container";
2325
- if (isContainer && m.auth === "gateway") {
2326
- ctx.addIssue({
2327
- code: z.ZodIssueCode.custom,
2328
- path: ["auth"],
2329
- message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
2330
- });
2331
- }
2332
- if (isContainer && m.containers === undefined) {
2333
- ctx.addIssue({
2334
- code: z.ZodIssueCode.custom,
2335
- path: ["containers"],
2336
- message: "containers[] is required when deployment.type is 'container'"
2337
- });
2338
- }
2339
- if (!isContainer && m.containers !== undefined) {
2340
- ctx.addIssue({
2341
- code: z.ZodIssueCode.custom,
2342
- path: ["containers"],
2343
- message: `containers[] is only allowed when deployment.type is 'container' (current type: '${m.deployment.type}')`
2344
- });
2345
- }
2346
- if (!isContainer && m.deployment.runtime !== undefined) {
2347
- ctx.addIssue({
2348
- code: z.ZodIssueCode.custom,
2349
- path: ["deployment", "runtime"],
2350
- message: "deployment.runtime is only meaningful when deployment.type is 'container'"
2351
- });
2352
- }
2353
- if (isContainer && m.build !== undefined) {
2354
- ctx.addIssue({
2355
- code: z.ZodIssueCode.custom,
2356
- path: ["build"],
2357
- message: "build is only allowed for pages-app deployments (static/react/react+api). Container builds live in the Dockerfile."
2358
- });
2359
- }
2360
- if (isContainer && m.production_env !== undefined) {
2361
- ctx.addIssue({
2362
- code: z.ZodIssueCode.custom,
2363
- path: ["production_env"],
2364
- message: "production_env is only allowed for pages-app deployments. Container env vars use containers[].env (names only) + the bot's secrets pipeline for values."
2365
- });
2366
- }
2367
- if (isContainer && m.containers !== undefined && m.containers.length > 1) {
2368
- const ingressContainers = m.containers.filter((c) => c.ingress === true);
2369
- if (ingressContainers.length !== 1) {
2370
- ctx.addIssue({
2371
- code: z.ZodIssueCode.custom,
2372
- path: ["containers"],
2373
- message: `multi-container apps must mark exactly one container ingress: true (found ${ingressContainers.length})`
2374
- });
2375
- }
2376
- }
2377
- if (isContainer && m.containers !== undefined) {
2378
- const declaredBindings = new Set((m.secrets?.bindings ?? []).map((b) => b.name));
2379
- for (let i = 0;i < m.containers.length; i++) {
2380
- const c = m.containers[i];
2381
- for (const envName of c.env ?? []) {
2382
- if (!declaredBindings.has(envName)) {
2383
- ctx.addIssue({
2384
- code: z.ZodIssueCode.custom,
2385
- path: ["containers", i, "env"],
2386
- message: `env reference '${envName}' has no matching entry in secrets.bindings[]`
2387
- });
2388
- }
2389
- }
2390
- }
2391
- }
2392
- if (isContainer && m.containers !== undefined) {
2393
- const seen = new Set;
2394
- for (let i = 0;i < m.containers.length; i++) {
2395
- const name = m.containers[i]?.name;
2396
- if (name === undefined)
2397
- continue;
2398
- if (seen.has(name)) {
2399
- ctx.addIssue({
2400
- code: z.ZodIssueCode.custom,
2401
- path: ["containers", i, "name"],
2402
- message: `duplicate container name '${name}' — names become DO class identifiers and must be unique`
2403
- });
2404
- }
2405
- seen.add(name);
2406
- }
2407
- }
2408
- const bindings = m.secrets?.bindings ?? [];
2409
- if (bindings.length > 0) {
2410
- const seen = new Set;
2411
- for (let i = 0;i < bindings.length; i++) {
2412
- const name = bindings[i]?.name;
2413
- if (name === undefined)
2371
+ let cwd = rootDir === "." ? "" : rootDir;
2372
+ const resolve5 = (p) => {
2373
+ const cleaned = p.startsWith("./") ? p.slice(2) : p;
2374
+ const trimmed = cleaned.replace(/\/+$/, "");
2375
+ return cwd === "" ? trimmed : `${cwd}/${trimmed}`;
2376
+ };
2377
+ const segments = build.command.split(/&&|\|\||[;|\n]/);
2378
+ for (const segment of segments) {
2379
+ if (cwd === null)
2380
+ break;
2381
+ const s = segment.trim();
2382
+ if (s === "" || OPAQUE_SEGMENT.test(s))
2383
+ continue;
2384
+ const tokens = s.split(/\s+/);
2385
+ const cmd = tokens[0];
2386
+ if (cmd === "cd") {
2387
+ const target = tokens[1];
2388
+ if (target === undefined || !PLAIN_PATH.test(target)) {
2389
+ cwd = null;
2414
2390
  continue;
2415
- if (seen.has(name)) {
2416
- ctx.addIssue({
2417
- code: z.ZodIssueCode.custom,
2418
- path: ["secrets", "bindings", i, "name"],
2419
- message: `duplicate secret binding name '${name}' — names become env-var keys and must be unique`
2420
- });
2421
2391
  }
2422
- seen.add(name);
2423
- }
2424
- }
2425
- const targets = m.targets ?? [];
2426
- for (let i = 0;i < targets.length; i++) {
2427
- const t = targets[i];
2428
- if (t.kind === "worker" && t.script === undefined) {
2429
- ctx.addIssue({
2430
- code: z.ZodIssueCode.custom,
2431
- path: ["targets", i, "script"],
2432
- message: "a worker target must declare a script (the sibling Workers script name)"
2433
- });
2434
- }
2435
- if (t.kind === "pages" && t.script !== undefined) {
2436
- ctx.addIssue({
2437
- code: z.ZodIssueCode.custom,
2438
- path: ["targets", i, "script"],
2439
- message: "a pages target must not declare a script — the primary Pages project is the app slug"
2440
- });
2441
- }
2442
- if (t.kind === "pages" && t.schedule !== undefined) {
2443
- ctx.addIssue({
2444
- code: z.ZodIssueCode.custom,
2445
- path: ["targets", i, "schedule"],
2446
- message: "schedule is only valid on a worker target (the cron deploy contract — ADR 0022)"
2447
- });
2448
- }
2449
- }
2450
- const seenScripts = new Set;
2451
- let pagesTargets = 0;
2452
- for (let i = 0;i < targets.length; i++) {
2453
- const t = targets[i];
2454
- if (t.kind === "pages") {
2455
- pagesTargets++;
2456
- if (pagesTargets > 1) {
2457
- ctx.addIssue({
2458
- code: z.ZodIssueCode.custom,
2459
- path: ["targets", i, "kind"],
2460
- message: "at most one pages target may be declared (the primary Pages project is singular)"
2392
+ const resolved = resolve5(target);
2393
+ if (!dirSet.has(resolved)) {
2394
+ issues.push({
2395
+ input: target,
2396
+ message: `build command segment '${s}' changes into '${resolved}', which does not exist in the bundle`
2461
2397
  });
2398
+ cwd = null;
2399
+ continue;
2462
2400
  }
2401
+ cwd = resolved;
2463
2402
  continue;
2464
2403
  }
2465
- if (t.script === undefined)
2466
- continue;
2467
- if (t.script === PRIMARY_TARGET || t.script === "pages") {
2468
- ctx.addIssue({
2469
- code: z.ZodIssueCode.custom,
2470
- path: ["targets", i, "script"],
2471
- message: `worker target script '${t.script}' is a reserved selector token — use a different script name`
2472
- });
2473
- continue;
2474
- }
2475
- if (seenScripts.has(t.script)) {
2476
- ctx.addIssue({
2477
- code: z.ZodIssueCode.custom,
2478
- path: ["targets", i, "script"],
2479
- message: `duplicate worker target script '${t.script}' — target scripts must be unique`
2480
- });
2481
- }
2482
- seenScripts.add(t.script);
2483
- }
2484
- const validRefs = new Set([PRIMARY_TARGET, ...seenScripts]);
2485
- if (pagesTargets > 0)
2486
- validRefs.add("pages");
2487
- for (let bi = 0;bi < bindings.length; bi++) {
2488
- const b = bindings[bi];
2489
- const sel = b?.targets;
2490
- if (sel === undefined || sel === "all")
2491
- continue;
2492
- for (const ref of sel) {
2493
- if (!validRefs.has(ref)) {
2494
- ctx.addIssue({
2495
- code: z.ZodIssueCode.custom,
2496
- path: ["secrets", "bindings", bi, "targets"],
2497
- message: `secret binding '${b?.name}' references unknown target '${ref}' — declare it in targets[] or use "primary"`
2498
- });
2404
+ if (cmd === "cp" || cmd === "cat") {
2405
+ const rawArgs = [];
2406
+ for (const t of tokens.slice(1)) {
2407
+ if (t === ">" || t.startsWith(">"))
2408
+ break;
2409
+ if (t.startsWith("-"))
2410
+ continue;
2411
+ rawArgs.push(t);
2412
+ }
2413
+ const sources = cmd === "cp" ? rawArgs.slice(0, -1) : rawArgs;
2414
+ if (cmd === "cp" && rawArgs.length < 2)
2415
+ continue;
2416
+ for (const src of sources) {
2417
+ if (!PLAIN_PATH.test(src))
2418
+ continue;
2419
+ const resolved = resolve5(src);
2420
+ if (!exists(resolved)) {
2421
+ issues.push({
2422
+ input: src,
2423
+ message: `build command references '${src}' (resolved: '${resolved}'), which does not exist in the bundle — did the content move? (cf. sp-reloc1)`
2424
+ });
2425
+ }
2499
2426
  }
2500
2427
  }
2501
2428
  }
2502
- });
2503
- function parseManifest(input) {
2504
- const result = ManifestSchema.safeParse(input);
2505
- if (result.success) {
2506
- return { kind: "ok", manifest: result.data };
2507
- }
2508
- return {
2509
- kind: "schema-error",
2510
- issues: result.error.issues.map((issue) => ({
2511
- path: issue.path.map(String).join(".") || "(root)",
2512
- message: issue.message
2513
- }))
2514
- };
2429
+ return issues;
2515
2430
  }
2516
2431
  // ../launchpad-engine/dist/status-author.js
2517
2432
  var STATUS_STATUS_KEYS = new Set([
@@ -2557,6 +2472,7 @@ var SpecSchema = z2.object({
2557
2472
  }).strict(),
2558
2473
  auth: z2.enum(AUTH_MODES).optional(),
2559
2474
  build: BuildSchema.optional(),
2475
+ app: AppBoundarySchema.optional(),
2560
2476
  env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
2561
2477
  containers: z2.array(ContainerSchema).min(1).optional(),
2562
2478
  secrets: SecretsSchema.optional(),
@@ -3151,101 +3067,635 @@ var FleetManifestSchema = z3.object({
3151
3067
  });
3152
3068
  }
3153
3069
  }
3154
- });
3155
- // ../launchpad-engine/dist/fleet-secret-sets.js
3156
- import { z as z4 } from "zod";
3157
- var SET_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
3158
- var Slug = z4.string().regex(SLUG_REGEX, "member must be a lowercase-kebab app slug");
3159
- var SecretName = z4.string().regex(ENV_NAME_REGEX, "secret name must be UPPER_SNAKE_CASE");
3160
- var SecretSetExcludeSchema = z4.object({
3161
- member: Slug,
3162
- secret: SecretName
3163
- }).strict();
3164
- var SecretSetSchema = z4.object({
3165
- name: z4.string().min(1).max(64).regex(SET_NAME_REGEX, "set name must be lowercase-kebab"),
3166
- secrets: z4.array(SecretName).min(1),
3167
- members: z4.array(Slug).min(1),
3168
- exclude: z4.array(SecretSetExcludeSchema).optional()
3169
- }).strict();
3170
- var FleetSecretSetsSchema = z4.object({
3171
- schemaVersion: z4.literal(1),
3172
- secretSets: z4.array(SecretSetSchema).min(1)
3173
- }).strict().superRefine((m, ctx) => {
3174
- const seenSets = new Set;
3175
- for (let i = 0;i < m.secretSets.length; i++) {
3176
- const set = m.secretSets[i];
3177
- if (seenSets.has(set.name)) {
3178
- ctx.addIssue({
3179
- code: "custom",
3180
- message: `duplicate secretSet name "${set.name}" — each set name appears once`,
3181
- path: ["secretSets", i, "name"]
3182
- });
3070
+ });
3071
+ // ../launchpad-engine/dist/fleet-secret-sets.js
3072
+ import { z as z4 } from "zod";
3073
+ var SET_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
3074
+ var Slug = z4.string().regex(SLUG_REGEX, "member must be a lowercase-kebab app slug");
3075
+ var SecretName = z4.string().regex(ENV_NAME_REGEX, "secret name must be UPPER_SNAKE_CASE");
3076
+ var SecretSetExcludeSchema = z4.object({
3077
+ member: Slug,
3078
+ secret: SecretName
3079
+ }).strict();
3080
+ var SecretSetSchema = z4.object({
3081
+ name: z4.string().min(1).max(64).regex(SET_NAME_REGEX, "set name must be lowercase-kebab"),
3082
+ secrets: z4.array(SecretName).min(1),
3083
+ members: z4.array(Slug).min(1),
3084
+ exclude: z4.array(SecretSetExcludeSchema).optional()
3085
+ }).strict();
3086
+ var FleetSecretSetsSchema = z4.object({
3087
+ schemaVersion: z4.literal(1),
3088
+ secretSets: z4.array(SecretSetSchema).min(1)
3089
+ }).strict().superRefine((m, ctx) => {
3090
+ const seenSets = new Set;
3091
+ for (let i = 0;i < m.secretSets.length; i++) {
3092
+ const set = m.secretSets[i];
3093
+ if (seenSets.has(set.name)) {
3094
+ ctx.addIssue({
3095
+ code: "custom",
3096
+ message: `duplicate secretSet name "${set.name}" — each set name appears once`,
3097
+ path: ["secretSets", i, "name"]
3098
+ });
3099
+ }
3100
+ seenSets.add(set.name);
3101
+ const seenMembers = new Set;
3102
+ for (const member of set.members) {
3103
+ if (seenMembers.has(member)) {
3104
+ ctx.addIssue({
3105
+ code: "custom",
3106
+ message: `duplicate member "${member}" in set "${set.name}"`,
3107
+ path: ["secretSets", i, "members"]
3108
+ });
3109
+ }
3110
+ seenMembers.add(member);
3111
+ }
3112
+ const seenSecrets = new Set;
3113
+ for (const secret of set.secrets) {
3114
+ if (seenSecrets.has(secret)) {
3115
+ ctx.addIssue({
3116
+ code: "custom",
3117
+ message: `duplicate secret "${secret}" in set "${set.name}"`,
3118
+ path: ["secretSets", i, "secrets"]
3119
+ });
3120
+ }
3121
+ seenSecrets.add(secret);
3122
+ }
3123
+ for (let j = 0;j < (set.exclude ?? []).length; j++) {
3124
+ const ex = set.exclude[j];
3125
+ if (!seenMembers.has(ex.member)) {
3126
+ ctx.addIssue({
3127
+ code: "custom",
3128
+ message: `exclude references member "${ex.member}" which is not in set "${set.name}".members`,
3129
+ path: ["secretSets", i, "exclude", j, "member"]
3130
+ });
3131
+ }
3132
+ if (!seenSecrets.has(ex.secret)) {
3133
+ ctx.addIssue({
3134
+ code: "custom",
3135
+ message: `exclude references secret "${ex.secret}" which is not in set "${set.name}".secrets`,
3136
+ path: ["secretSets", i, "exclude", j, "secret"]
3137
+ });
3138
+ }
3139
+ }
3140
+ }
3141
+ });
3142
+ function parseFleetSecretSets(input) {
3143
+ const res = FleetSecretSetsSchema.safeParse(input);
3144
+ if (res.success)
3145
+ return { ok: true, manifest: res.data };
3146
+ return {
3147
+ ok: false,
3148
+ issues: res.error.issues.map((i) => ({
3149
+ path: i.path.join("."),
3150
+ message: i.message
3151
+ }))
3152
+ };
3153
+ }
3154
+ function membersForSecret(set, secret) {
3155
+ const excluded = new Set((set.exclude ?? []).filter((e) => e.secret === secret).map((e) => e.member));
3156
+ return set.members.filter((m) => !excluded.has(m));
3157
+ }
3158
+ // ../launchpad-engine/dist/redact-status.js
3159
+ var UNSAFE_KEY = new Set(["__proto__", "prototype", "constructor"]);
3160
+ // src/bundle/boundary.ts
3161
+ var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
3162
+ function applyBoundaryToFiles(args) {
3163
+ const { cwd, manifestYaml, files } = args;
3164
+ let parsed;
3165
+ if (manifestYaml !== null) {
3166
+ try {
3167
+ parsed = parseYaml2(manifestYaml);
3168
+ } catch {
3169
+ parsed = undefined;
3170
+ }
3171
+ }
3172
+ const input = extractBoundaryInput(parsed);
3173
+ if (input.kind === "schema-error") {
3174
+ const detail = input.issues.map((i) => `${i.path}: ${i.message}`).join("; ");
3175
+ return { kind: "error", message: `launchpad.yaml app: block is invalid — ${detail}` };
3176
+ }
3177
+ const contract = resolveAppBoundary({ appType: input.appType, app: input.app });
3178
+ let launchpadIgnore = [];
3179
+ const ignorePath = join5(cwd, ".launchpadignore");
3180
+ if (existsSync2(ignorePath)) {
3181
+ try {
3182
+ launchpadIgnore = parseLaunchpadIgnore(readFileSync3(ignorePath, "utf8"));
3183
+ } catch {}
3184
+ }
3185
+ const result = applyAppBoundary(contract, files, {
3186
+ launchpadIgnore,
3187
+ readFileContent: (path6) => {
3188
+ try {
3189
+ const abs = join5(cwd, path6);
3190
+ const stat = lstatSync2(abs);
3191
+ if (!stat.isFile() || stat.size > MAX_CONTENT_PROBE_BYTES)
3192
+ return;
3193
+ return readFileSync3(abs, "utf8");
3194
+ } catch {
3195
+ return;
3196
+ }
3197
+ }
3198
+ });
3199
+ const warnings = [];
3200
+ if (result.denied.length > 0) {
3201
+ if (contract.source === "declared") {
3202
+ const lines = result.denied.map((d) => ` - ${d.message}`).join(`
3203
+ `);
3204
+ return {
3205
+ kind: "error",
3206
+ message: `bundle blocked — ${result.denied.length} file(s) matched by app.include can never ship:
3207
+ ${lines}
3208
+ ` + ` Remove the file(s) or narrow app.include / add app.exclude in launchpad.yaml.`
3209
+ };
3210
+ }
3211
+ for (const d of result.denied) {
3212
+ warnings.push(`stripped ${d.path} — ${d.message}`);
3213
+ }
3214
+ }
3215
+ if (result.files.length === 0) {
3216
+ return {
3217
+ kind: "error",
3218
+ message: "the app boundary excluded every file — nothing to deploy. " + "Check app.root / app.include in launchpad.yaml."
3219
+ };
3220
+ }
3221
+ const buildIssues = verifyBuildInputs(input.build, result.files);
3222
+ if (buildIssues.length > 0) {
3223
+ const lines = buildIssues.map((i) => ` - ${i.message}`).join(`
3224
+ `);
3225
+ return {
3226
+ kind: "error",
3227
+ message: `build-inputs check failed — the build command references missing inputs:
3228
+ ${lines}`
3229
+ };
3230
+ }
3231
+ return {
3232
+ kind: "ok",
3233
+ files: result.files,
3234
+ warnings,
3235
+ inferred: contract.source === "inferred"
3236
+ };
3237
+ }
3238
+
3239
+ // src/bundle/orchestrate.ts
3240
+ async function bundleAndDeploy(args) {
3241
+ const { cfg, cwd, slug } = args;
3242
+ const manifestPath = join6(cwd, "launchpad.yaml");
3243
+ let manifestYaml;
3244
+ try {
3245
+ manifestYaml = readFileSync4(manifestPath, "utf8");
3246
+ } catch (e) {
3247
+ return {
3248
+ kind: "no-manifest",
3249
+ message: `no launchpad.yaml at ${manifestPath} — run \`launchpad init\` from inside your app directory first. ` + `(${e.message})`
3250
+ };
3251
+ }
3252
+ let walk;
3253
+ try {
3254
+ walk = walkCwd(cwd);
3255
+ } catch (e) {
3256
+ if (e instanceof WalkError) {
3257
+ return { kind: "walk-error", message: e.message };
3258
+ }
3259
+ return {
3260
+ kind: "walk-error",
3261
+ message: `unexpected walk failure: ${e.message}`
3262
+ };
3263
+ }
3264
+ const boundary = applyBoundaryToFiles({ cwd, manifestYaml, files: walk.files });
3265
+ if (boundary.kind === "error") {
3266
+ return { kind: "boundary-error", message: boundary.message };
3267
+ }
3268
+ const bundleFiles = boundary.files;
3269
+ let packResult;
3270
+ try {
3271
+ packResult = await packTarGz(cwd, bundleFiles);
3272
+ } catch (e) {
3273
+ if (e instanceof TarPackError) {
3274
+ return { kind: "pack-error", message: e.message };
3275
+ }
3276
+ return {
3277
+ kind: "pack-error",
3278
+ message: `unexpected pack failure: ${e.message}`
3279
+ };
3280
+ }
3281
+ const workerBuild = await buildWorkerArtifact(cwd, manifestYaml, bundleFiles);
3282
+ if (workerBuild.kind === "error") {
3283
+ return { kind: "worker-build-error", message: workerBuild.message };
3284
+ }
3285
+ const workerArtifact = workerBuild.kind === "ok" ? workerBuild.artifact : undefined;
3286
+ const uploadResult = await uploadBundle(cfg, slug, manifestYaml, packResult.bytes, workerArtifact);
3287
+ if (uploadResult.kind !== "ok") {
3288
+ return {
3289
+ kind: "upload-error",
3290
+ status: uploadResult.status,
3291
+ body: uploadResult.response
3292
+ };
3293
+ }
3294
+ return {
3295
+ kind: "ok",
3296
+ result: uploadResult.response,
3297
+ walk,
3298
+ fileCount: packResult.fileCount,
3299
+ compressedBytes: packResult.bytes.byteLength,
3300
+ workerScript: workerArtifact?.script ?? null,
3301
+ boundaryWarnings: boundary.warnings
3302
+ };
3303
+ }
3304
+
3305
+ // src/commands/deploy.ts
3306
+ import { parse as parseYaml4 } from "yaml";
3307
+ import { readFileSync as readFileSync6 } from "node:fs";
3308
+
3309
+ // src/deploy/git-files.ts
3310
+ import { spawn as spawn3 } from "node:child_process";
3311
+
3312
+ class GitFilesError extends Error {
3313
+ code = "git_files_error";
3314
+ }
3315
+ var PROTECTED_PATHS = new Set([
3316
+ ".github/workflows/launchpad-review.yml",
3317
+ ".github/workflows/ci.yml",
3318
+ "wrangler.toml",
3319
+ ".github/scripts/_gha-cmd.mjs",
3320
+ ".github/scripts/parse-eslint.mjs",
3321
+ ".github/scripts/parse-tsc.mjs",
3322
+ ".github/scripts/parse-vitest.mjs",
3323
+ ".github/scripts/parse-gitleaks.mjs",
3324
+ ".github/scripts/parse-bun-audit.mjs"
3325
+ ]);
3326
+ async function listDeployFiles(opts) {
3327
+ const sp = opts.spawner ?? spawn3;
3328
+ const stdout = await runGit2(sp, opts.cwd, [
3329
+ "ls-files",
3330
+ "-c",
3331
+ "-o",
3332
+ "--exclude-standard",
3333
+ "-z"
3334
+ ]);
3335
+ const paths = stdout.split("\x00").filter((p) => p.length > 0 && !PROTECTED_PATHS.has(p));
3336
+ return [...paths].sort();
3337
+ }
3338
+ function runGit2(sp, cwd, args) {
3339
+ return new Promise((resolve5, reject) => {
3340
+ const spawnOpts = { cwd, stdio: ["ignore", "pipe", "pipe"] };
3341
+ const child = sp("git", [...args], spawnOpts);
3342
+ let stdout = "";
3343
+ let stderr = "";
3344
+ child.stdout?.on("data", (chunk) => {
3345
+ stdout += chunk.toString();
3346
+ });
3347
+ child.stderr?.on("data", (chunk) => {
3348
+ stderr += chunk.toString();
3349
+ });
3350
+ child.once("error", (err) => {
3351
+ reject(new GitFilesError(`could not run \`git ${args.join(" ")}\`: ${err.message} (is git installed?)`));
3352
+ });
3353
+ child.once("close", (code) => {
3354
+ if (code === 0) {
3355
+ resolve5(stdout);
3356
+ return;
3357
+ }
3358
+ reject(new GitFilesError(`\`git ${args.join(" ")}\` exited ${code}: ${stderr.trim()}`));
3359
+ });
3360
+ });
3361
+ }
3362
+
3363
+ // src/commands/deploy-flags.ts
3364
+ var SLUG_RE3 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
3365
+ var GROUP_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
3366
+ var GROUP_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3367
+ function parseDeployFlags(args) {
3368
+ let mode = "content";
3369
+ let slug = null;
3370
+ let displayName = null;
3371
+ let description = null;
3372
+ let appType = null;
3373
+ const allowedGroups = [];
3374
+ let message = null;
3375
+ let timeoutSeconds = null;
3376
+ let manifestFile = null;
3377
+ let dryRunJson = false;
3378
+ let platformRepo = null;
3379
+ let rePin = false;
3380
+ let yes = false;
3381
+ let resumePrNumber = null;
3382
+ let timeoutMinutes = null;
3383
+ let modeFlagsSeen = 0;
3384
+ let i = 0;
3385
+ while (i < args.length) {
3386
+ const a = args[i] ?? "";
3387
+ if (a === "--new") {
3388
+ mode = "new";
3389
+ modeFlagsSeen += 1;
3390
+ i += 1;
3391
+ continue;
3392
+ }
3393
+ if (a === "--dry-run") {
3394
+ mode = "dry-run";
3395
+ modeFlagsSeen += 1;
3396
+ i += 1;
3397
+ continue;
3398
+ }
3399
+ if (a === "--apply") {
3400
+ mode = "apply";
3401
+ modeFlagsSeen += 1;
3402
+ i += 1;
3403
+ continue;
3404
+ }
3405
+ if (a === "--platform-repo") {
3406
+ const v = args[i + 1];
3407
+ if (v === undefined || v.trim().length === 0) {
3408
+ return `missing value for ${a}`;
3409
+ }
3410
+ platformRepo = v.trim();
3411
+ i += 2;
3412
+ continue;
3413
+ }
3414
+ if (a === "--re-pin") {
3415
+ rePin = true;
3416
+ i += 1;
3417
+ continue;
3418
+ }
3419
+ if (a === "--yes" || a === "-y") {
3420
+ yes = true;
3421
+ i += 1;
3422
+ continue;
3423
+ }
3424
+ if (a === "--resume") {
3425
+ const v = args[i + 1];
3426
+ if (v === undefined)
3427
+ return `missing value for ${a}`;
3428
+ mode = "resume";
3429
+ slug = v;
3430
+ modeFlagsSeen += 1;
3431
+ i += 2;
3432
+ continue;
3433
+ }
3434
+ if (a === "--abandon") {
3435
+ const v = args[i + 1];
3436
+ if (v === undefined)
3437
+ return `missing value for ${a}`;
3438
+ mode = "abandon";
3439
+ slug = v;
3440
+ modeFlagsSeen += 1;
3441
+ i += 2;
3442
+ continue;
3443
+ }
3444
+ if (a === "--file") {
3445
+ const v = args[i + 1];
3446
+ if (v === undefined)
3447
+ return `missing value for ${a}`;
3448
+ manifestFile = v;
3449
+ i += 2;
3450
+ continue;
3451
+ }
3452
+ if (a === "--json") {
3453
+ dryRunJson = true;
3454
+ i += 1;
3455
+ continue;
3456
+ }
3457
+ if (a === "--slug") {
3458
+ const v = args[i + 1];
3459
+ if (v === undefined)
3460
+ return `missing value for ${a}`;
3461
+ slug = v;
3462
+ i += 2;
3463
+ continue;
3464
+ }
3465
+ if (a === "--display-name" || a === "--name") {
3466
+ const v = args[i + 1];
3467
+ if (v === undefined)
3468
+ return `missing value for ${a}`;
3469
+ displayName = v;
3470
+ i += 2;
3471
+ continue;
3472
+ }
3473
+ if (a === "--description") {
3474
+ const v = args[i + 1];
3475
+ if (v === undefined)
3476
+ return `missing value for ${a}`;
3477
+ description = v;
3478
+ i += 2;
3479
+ continue;
3480
+ }
3481
+ if (a === "--app-type" || a === "--type") {
3482
+ const v = args[i + 1];
3483
+ if (v === undefined)
3484
+ return `missing value for ${a}`;
3485
+ if (!APP_TYPES.includes(v)) {
3486
+ return `invalid --app-type "${v}" — must be one of ${APP_TYPES.join(", ")}`;
3487
+ }
3488
+ appType = v;
3489
+ i += 2;
3490
+ continue;
3491
+ }
3492
+ if (a === "--allowed-group" || a === "--group") {
3493
+ const v = args[i + 1];
3494
+ if (v === undefined)
3495
+ return `missing value for ${a}`;
3496
+ if (!GROUP_KEY_RE2.test(v) && !GROUP_UUID_RE2.test(v)) {
3497
+ return `invalid --allowed-group "${v}" — expected a group key (HCL identifier, e.g. G_Product_Security) or an Entra Object-ID UUID`;
3498
+ }
3499
+ allowedGroups.push(v);
3500
+ i += 2;
3501
+ continue;
3502
+ }
3503
+ if (a === "--message" || a === "-m") {
3504
+ const v = args[i + 1];
3505
+ if (v === undefined)
3506
+ return `missing value for ${a}`;
3507
+ message = v;
3508
+ i += 2;
3509
+ continue;
3510
+ }
3511
+ if (a === "--timeout-seconds") {
3512
+ const v = args[i + 1];
3513
+ if (v === undefined)
3514
+ return `missing value for ${a}`;
3515
+ const n = Number(v);
3516
+ if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
3517
+ return `invalid --timeout-seconds "${v}" — expected positive integer`;
3518
+ }
3519
+ timeoutSeconds = n;
3520
+ i += 2;
3521
+ continue;
3522
+ }
3523
+ if (a === "--timeout-minutes") {
3524
+ const v = args[i + 1];
3525
+ if (v === undefined)
3526
+ return `missing value for ${a}`;
3527
+ const n = Number(v);
3528
+ if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
3529
+ return `invalid --timeout-minutes "${v}" — expected positive integer`;
3530
+ }
3531
+ timeoutMinutes = n;
3532
+ i += 2;
3533
+ continue;
3534
+ }
3535
+ if (a === "--resume-pr") {
3536
+ const v = args[i + 1];
3537
+ if (v === undefined)
3538
+ return `missing value for ${a}`;
3539
+ const n = Number(v);
3540
+ if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
3541
+ return `invalid --resume-pr "${v}" — expected positive integer PR number`;
3542
+ }
3543
+ resumePrNumber = n;
3544
+ i += 2;
3545
+ continue;
3546
+ }
3547
+ return `unknown argument "${a}"`;
3548
+ }
3549
+ if (modeFlagsSeen > 1) {
3550
+ return "--new, --resume, --abandon, --dry-run, and --apply are mutually exclusive";
3551
+ }
3552
+ if (mode === "dry-run") {
3553
+ if (slug !== null)
3554
+ return "--dry-run does not accept --slug (slug is read from launchpad.yaml)";
3555
+ if (displayName !== null)
3556
+ return "--dry-run does not accept --display-name";
3557
+ if (description !== null)
3558
+ return "--dry-run does not accept --description";
3559
+ if (appType !== null)
3560
+ return "--dry-run does not accept --app-type";
3561
+ if (allowedGroups.length > 0)
3562
+ return "--dry-run does not accept --allowed-group (read from launchpad.yaml)";
3563
+ if (message !== null)
3564
+ return "--dry-run does not accept --message";
3565
+ if (timeoutSeconds !== null)
3566
+ return "--dry-run does not accept --timeout-seconds";
3567
+ if (platformRepo !== null)
3568
+ return "--dry-run does not accept --platform-repo (apply-only)";
3569
+ if (rePin)
3570
+ return "--dry-run does not accept --re-pin (apply-only)";
3571
+ if (yes)
3572
+ return "--dry-run does not accept --yes (apply-only)";
3573
+ return {
3574
+ mode: { kind: "dry-run", file: manifestFile, json: dryRunJson },
3575
+ message: null,
3576
+ timeoutSeconds: null
3577
+ };
3578
+ }
3579
+ if (mode === "apply") {
3580
+ if (slug !== null)
3581
+ return "--apply does not accept --slug (slug is read from launchpad.yaml)";
3582
+ if (displayName !== null)
3583
+ return "--apply does not accept --display-name";
3584
+ if (description !== null)
3585
+ return "--apply does not accept --description";
3586
+ if (appType !== null)
3587
+ return "--apply does not accept --app-type";
3588
+ if (allowedGroups.length > 0)
3589
+ return "--apply does not accept --allowed-group (read from launchpad.yaml)";
3590
+ if (message !== null)
3591
+ return "--apply does not accept --message";
3592
+ if (timeoutSeconds !== null)
3593
+ return "--apply does not accept --timeout-seconds (use --timeout-minutes instead — apply polling is multi-minute)";
3594
+ if (dryRunJson)
3595
+ return "--apply does not accept --json";
3596
+ return {
3597
+ mode: {
3598
+ kind: "apply",
3599
+ file: manifestFile,
3600
+ platformRepo,
3601
+ rePin,
3602
+ yes,
3603
+ resumePrNumber,
3604
+ timeoutMinutes
3605
+ },
3606
+ message: null,
3607
+ timeoutSeconds: null
3608
+ };
3609
+ }
3610
+ if (resumePrNumber !== null) {
3611
+ return "--resume-pr is only valid with --apply";
3612
+ }
3613
+ if (timeoutMinutes !== null) {
3614
+ return "--timeout-minutes is only valid with --apply";
3615
+ }
3616
+ if (manifestFile !== null) {
3617
+ return "--file is only valid with --dry-run or --apply";
3618
+ }
3619
+ if (dryRunJson) {
3620
+ return "--json is only valid with --dry-run";
3621
+ }
3622
+ if (platformRepo !== null) {
3623
+ return "--platform-repo is only valid with --apply";
3624
+ }
3625
+ if (rePin) {
3626
+ return "--re-pin is only valid with --apply";
3627
+ }
3628
+ if (yes) {
3629
+ return "--yes is only valid with --apply";
3630
+ }
3631
+ if (mode === "content") {
3632
+ if (slug !== null && !SLUG_RE3.test(slug)) {
3633
+ return `invalid slug "${slug}" — expected ${SLUG_RE3.source}`;
3183
3634
  }
3184
- seenSets.add(set.name);
3185
- const seenMembers = new Set;
3186
- for (const member of set.members) {
3187
- if (seenMembers.has(member)) {
3188
- ctx.addIssue({
3189
- code: "custom",
3190
- message: `duplicate member "${member}" in set "${set.name}"`,
3191
- path: ["secretSets", i, "members"]
3192
- });
3193
- }
3194
- seenMembers.add(member);
3635
+ return {
3636
+ mode: { kind: "content", slug },
3637
+ message,
3638
+ timeoutSeconds
3639
+ };
3640
+ }
3641
+ if (mode === "resume" || mode === "abandon") {
3642
+ if (slug === null || !SLUG_RE3.test(slug)) {
3643
+ return `--${mode} requires a valid slug (kebab-case)`;
3195
3644
  }
3196
- const seenSecrets = new Set;
3197
- for (const secret of set.secrets) {
3198
- if (seenSecrets.has(secret)) {
3199
- ctx.addIssue({
3200
- code: "custom",
3201
- message: `duplicate secret "${secret}" in set "${set.name}"`,
3202
- path: ["secretSets", i, "secrets"]
3203
- });
3204
- }
3205
- seenSecrets.add(secret);
3645
+ if (displayName !== null || appType !== null || allowedGroups.length > 0) {
3646
+ return `--${mode} does not accept create-only flags (--display-name / --app-type / --allowed-group)`;
3206
3647
  }
3207
- for (let j = 0;j < (set.exclude ?? []).length; j++) {
3208
- const ex = set.exclude[j];
3209
- if (!seenMembers.has(ex.member)) {
3210
- ctx.addIssue({
3211
- code: "custom",
3212
- message: `exclude references member "${ex.member}" which is not in set "${set.name}".members`,
3213
- path: ["secretSets", i, "exclude", j, "member"]
3214
- });
3215
- }
3216
- if (!seenSecrets.has(ex.secret)) {
3217
- ctx.addIssue({
3218
- code: "custom",
3219
- message: `exclude references secret "${ex.secret}" which is not in set "${set.name}".secrets`,
3220
- path: ["secretSets", i, "exclude", j, "secret"]
3221
- });
3222
- }
3648
+ if (message !== null) {
3649
+ return `--${mode} does not accept --message`;
3223
3650
  }
3651
+ return {
3652
+ mode: { kind: mode, slug },
3653
+ message: null,
3654
+ timeoutSeconds
3655
+ };
3656
+ }
3657
+ if (slug === null)
3658
+ return "--new requires --slug";
3659
+ if (!SLUG_RE3.test(slug))
3660
+ return `invalid slug "${slug}" — expected ${SLUG_RE3.source}`;
3661
+ if (slug.length < 3 || slug.length > 58) {
3662
+ return `slug length out of bounds (3–58 chars): "${slug}"`;
3663
+ }
3664
+ if (displayName === null || displayName.length === 0) {
3665
+ return "--new requires --display-name";
3666
+ }
3667
+ if (appType === null) {
3668
+ return `--new requires --app-type (one of ${APP_TYPES.join(", ")})`;
3669
+ }
3670
+ if (allowedGroups.length === 0) {
3671
+ return "--new requires at least one --allowed-group";
3224
3672
  }
3225
- });
3226
- function parseFleetSecretSets(input) {
3227
- const res = FleetSecretSetsSchema.safeParse(input);
3228
- if (res.success)
3229
- return { ok: true, manifest: res.data };
3230
3673
  return {
3231
- ok: false,
3232
- issues: res.error.issues.map((i) => ({
3233
- path: i.path.join("."),
3234
- message: i.message
3235
- }))
3674
+ mode: {
3675
+ kind: "new",
3676
+ slug,
3677
+ displayName,
3678
+ description,
3679
+ appType,
3680
+ allowedGroups
3681
+ },
3682
+ message,
3683
+ timeoutSeconds
3236
3684
  };
3237
3685
  }
3238
- function membersForSecret(set, secret) {
3239
- const excluded = new Set((set.exclude ?? []).filter((e) => e.secret === secret).map((e) => e.member));
3240
- return set.members.filter((m) => !excluded.has(m));
3241
- }
3242
- // ../launchpad-engine/dist/redact-status.js
3243
- var UNSAFE_KEY = new Set(["__proto__", "prototype", "constructor"]);
3686
+
3687
+ // src/deploy/apply.ts
3688
+ import { existsSync as existsSync3, rmSync } from "node:fs";
3689
+ import { resolve as resolvePath, join as join7 } from "node:path";
3690
+ import { execFileSync } from "node:child_process";
3691
+
3244
3692
  // src/manifest/load.ts
3693
+ import { readFileSync as readFileSync5 } from "node:fs";
3694
+ import { parse as parseYaml3, YAMLParseError } from "yaml";
3245
3695
  function loadManifest(path6) {
3246
3696
  let raw;
3247
3697
  try {
3248
- raw = readFileSync4(path6, "utf8");
3698
+ raw = readFileSync5(path6, "utf8");
3249
3699
  } catch (err) {
3250
3700
  const e = err;
3251
3701
  if (e.code === "ENOENT") {
@@ -3258,7 +3708,7 @@ function loadManifest(path6) {
3258
3708
  function parseManifest2(yamlText, path6) {
3259
3709
  let parsed;
3260
3710
  try {
3261
- parsed = parseYaml2(yamlText);
3711
+ parsed = parseYaml3(yamlText);
3262
3712
  } catch (err) {
3263
3713
  const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
3264
3714
  return { kind: "yaml-parse-error", path: path6, message };
@@ -3481,8 +3931,8 @@ function defaultGitHeadSha(cwd) {
3481
3931
  return out.trim();
3482
3932
  }
3483
3933
  function deletePinIfPresent(cfg, slug, io) {
3484
- const pinPath = join6(cfg.stateDir, slug, "group.json");
3485
- if (!existsSync2(pinPath))
3934
+ const pinPath = join7(cfg.stateDir, slug, "group.json");
3935
+ if (!existsSync3(pinPath))
3486
3936
  return;
3487
3937
  try {
3488
3938
  rmSync(pinPath, { force: true });
@@ -3974,7 +4424,7 @@ async function runDeploy(args, io) {
3974
4424
  }
3975
4425
  const cwd = process.cwd();
3976
4426
  const manifestPath = path6.join(cwd, "launchpad.yaml");
3977
- if (existsSync3(manifestPath)) {
4427
+ if (existsSync4(manifestPath)) {
3978
4428
  return runModelADeploy({ cwd, manifestPath, argv: args, io });
3979
4429
  }
3980
4430
  const parsed = parseArgs2(args);
@@ -4001,12 +4451,34 @@ async function runDeploy(args, io) {
4001
4451
  try {
4002
4452
  const cfg = loadConfig();
4003
4453
  io.out(`Packaging working tree for ${slug} …`);
4004
- const files = await listDeployFiles({ cwd: process.cwd() });
4005
- if (files.length === 0) {
4454
+ const rawFiles = await listDeployFiles({ cwd: process.cwd() });
4455
+ if (rawFiles.length === 0) {
4006
4456
  io.err("launchpad deploy: no files to deploy (git ls-files returned empty).");
4007
4457
  io.err(" did you run this from inside the cloned working tree?");
4008
4458
  return 1;
4009
4459
  }
4460
+ let contentManifestYaml = null;
4461
+ const contentManifestPath = path6.join(process.cwd(), "launchpad.yaml");
4462
+ if (existsSync4(contentManifestPath)) {
4463
+ try {
4464
+ contentManifestYaml = readFileSync6(contentManifestPath, "utf8");
4465
+ } catch {
4466
+ contentManifestYaml = null;
4467
+ }
4468
+ }
4469
+ const boundary = applyBoundaryToFiles({
4470
+ cwd: process.cwd(),
4471
+ manifestYaml: contentManifestYaml,
4472
+ files: rawFiles
4473
+ });
4474
+ if (boundary.kind === "error") {
4475
+ io.err(`launchpad deploy: ${boundary.message}`);
4476
+ return 1;
4477
+ }
4478
+ for (const w of boundary.warnings) {
4479
+ io.err(`warning: ${w}`);
4480
+ }
4481
+ const files = boundary.files;
4010
4482
  const packed = await packTarGz(process.cwd(), files);
4011
4483
  io.out(`Packed ${packed.fileCount} files (${formatBytes2(packed.uncompressedBytes)}` + ` uncompressed, ${formatBytes2(packed.bytes.length)} gzipped)`);
4012
4484
  io.out(`Uploading to ${cfg.botUrl}/apps/${slug}/deploy-pr …`);
@@ -4026,6 +4498,9 @@ async function runDeploy(args, io) {
4026
4498
  io.out(`PR opened: ${response.reviewUrl}`);
4027
4499
  io.out(`Branch: ${response.branch}`);
4028
4500
  io.out(`Number: #${response.prNumber}`);
4501
+ io.out("");
4502
+ io.out(`Review from the CLI: launchpad review ${response.prNumber} --slug ${slug}`);
4503
+ io.out(`Merge when approved: launchpad merge ${response.prNumber} --slug ${slug}`);
4029
4504
  if (response.bootstrapWorkflow === true) {
4030
4505
  io.out("");
4031
4506
  io.out("(first deploy: included canonical launchpad-review.yml workflow file)");
@@ -4137,7 +4612,7 @@ async function runModelADeploy(args) {
4137
4612
  const { cwd, manifestPath, io } = args;
4138
4613
  let slug;
4139
4614
  try {
4140
- const metaSlug = resolveManifestSlug(parseYaml3(readFileSync5(manifestPath, "utf8")));
4615
+ const metaSlug = resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
4141
4616
  if (metaSlug === null) {
4142
4617
  io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
4143
4618
  return 64;
@@ -4178,6 +4653,9 @@ async function runModelADeploy(args) {
4178
4653
  case "walk-error":
4179
4654
  io.err(`launchpad deploy: walk failed — ${result.message}`);
4180
4655
  return 1;
4656
+ case "boundary-error":
4657
+ io.err(`launchpad deploy: ${result.message}`);
4658
+ return 1;
4181
4659
  case "pack-error":
4182
4660
  io.err(`launchpad deploy: pack failed — ${result.message}`);
4183
4661
  return 1;
@@ -4213,6 +4691,9 @@ async function runModelADeploy(args) {
4213
4691
  return 1;
4214
4692
  }
4215
4693
  case "ok": {
4694
+ for (const w of result.boundaryWarnings) {
4695
+ io.err(`warning: ${w}`);
4696
+ }
4216
4697
  if (result.result.status === "provisioning_started") {
4217
4698
  const submissionId = typeof result.result.submissionId === "string" ? result.result.submissionId : "(missing)";
4218
4699
  const appType = typeof result.result.appType === "string" ? result.result.appType : "(missing)";
@@ -4245,9 +4726,8 @@ async function runModelADeploy(args) {
4245
4726
  io.out(` ${result.result.message}`);
4246
4727
  }
4247
4728
  io.out("");
4248
- io.out("Next steps:");
4249
- io.out(` launchpad status ${slug} # poll for the live URL`);
4250
- io.out(` launchpad apps # see lifecycle`);
4729
+ io.out("Committed; build pending — Cloudflare Pages is building this deploy now.");
4730
+ io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
4251
4731
  return 0;
4252
4732
  }
4253
4733
  }
@@ -4450,7 +4930,7 @@ function describe13(e) {
4450
4930
  }
4451
4931
 
4452
4932
  // src/commands/generate.ts
4453
- import { mkdirSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
4933
+ import { mkdirSync, readFileSync as readFileSync7, writeFileSync } from "node:fs";
4454
4934
  import { dirname as dirname4, resolve as resolve6, relative as relative3 } from "node:path";
4455
4935
  var generateCommand = {
4456
4936
  name: "generate",
@@ -4538,7 +5018,7 @@ function applyOne(artefact, path8, out, force) {
4538
5018
  }
4539
5019
  function readIfExists(path8) {
4540
5020
  try {
4541
- return { kind: "ok", content: readFileSync6(path8, "utf8") };
5021
+ return { kind: "ok", content: readFileSync7(path8, "utf8") };
4542
5022
  } catch (err) {
4543
5023
  const e = err;
4544
5024
  if (e.code === "ENOENT")
@@ -4749,13 +5229,13 @@ function parseFlags(args) {
4749
5229
  }
4750
5230
 
4751
5231
  // src/groups/client.ts
4752
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
4753
- import { dirname as dirname5, join as join8 } from "node:path";
5232
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
5233
+ import { dirname as dirname5, join as join9 } from "node:path";
4754
5234
  var CACHE_TTL_MS = 60 * 60 * 1000;
4755
5235
  var CACHE_FILENAME = "groups.json";
4756
5236
  async function fetchGroups(cfg, opts = {}) {
4757
5237
  const now = opts.now ?? Date.now;
4758
- const cachePath = join8(cfg.cacheDir, CACHE_FILENAME);
5238
+ const cachePath = join9(cfg.cacheDir, CACHE_FILENAME);
4759
5239
  if (opts.forceRefresh !== true) {
4760
5240
  const cached = readCache(cachePath);
4761
5241
  if (cached !== null) {
@@ -4793,7 +5273,7 @@ async function fetchGroups(cfg, opts = {}) {
4793
5273
  function readCache(path8) {
4794
5274
  let raw;
4795
5275
  try {
4796
- raw = readFileSync7(path8, "utf8");
5276
+ raw = readFileSync8(path8, "utf8");
4797
5277
  } catch {
4798
5278
  return null;
4799
5279
  }
@@ -5294,17 +5774,17 @@ function describe17(e) {
5294
5774
 
5295
5775
  // src/commands/init.ts
5296
5776
  import { createInterface } from "node:readline/promises";
5297
- import { existsSync as existsSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "node:fs";
5777
+ import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
5298
5778
  import { resolve as resolve7 } from "node:path";
5299
5779
  import { stringify as yamlStringify } from "yaml";
5300
5780
 
5301
5781
  // src/detect/index.ts
5302
- import { existsSync as existsSync4, readFileSync as readFileSync8, statSync } from "node:fs";
5303
- import { join as join9 } from "node:path";
5782
+ import { existsSync as existsSync5, readFileSync as readFileSync9, statSync } from "node:fs";
5783
+ import { join as join10 } from "node:path";
5304
5784
  function detectAppShape(cwd) {
5305
- const hasPackageJson = existsSync4(join9(cwd, "package.json"));
5306
- const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync4(join9(cwd, file)));
5307
- const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync4(join9(cwd, n)));
5785
+ const hasPackageJson = existsSync5(join10(cwd, "package.json"));
5786
+ const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync5(join10(cwd, file)));
5787
+ const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync5(join10(cwd, n)));
5308
5788
  if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
5309
5789
  return {
5310
5790
  kind: "not-applicable",
@@ -5350,7 +5830,7 @@ var LOCKFILES = [
5350
5830
  { file: "yarn.lock", pm: "yarn" }
5351
5831
  ];
5352
5832
  function detectPackageManager(cwd) {
5353
- const present = LOCKFILES.filter(({ file }) => existsSync4(join9(cwd, file)));
5833
+ const present = LOCKFILES.filter(({ file }) => existsSync5(join10(cwd, file)));
5354
5834
  if (present.length === 0) {
5355
5835
  return {
5356
5836
  kind: "ambiguous",
@@ -5372,8 +5852,8 @@ function detectPackageManager(cwd) {
5372
5852
  }
5373
5853
  function detectVitePresence(cwd) {
5374
5854
  for (const name of VITE_CONFIG_NAMES) {
5375
- const p = join9(cwd, name);
5376
- if (existsSync4(p)) {
5855
+ const p = join10(cwd, name);
5856
+ if (existsSync5(p)) {
5377
5857
  return { kind: "ok", value: { path: p } };
5378
5858
  }
5379
5859
  }
@@ -5383,9 +5863,9 @@ function detectVitePresence(cwd) {
5383
5863
  };
5384
5864
  }
5385
5865
  function detectAppType(cwd) {
5386
- const fnDir = join9(cwd, "functions");
5866
+ const fnDir = join10(cwd, "functions");
5387
5867
  let hasFunctionsDir = false;
5388
- if (existsSync4(fnDir)) {
5868
+ if (existsSync5(fnDir)) {
5389
5869
  try {
5390
5870
  hasFunctionsDir = statSync(fnDir).isDirectory();
5391
5871
  } catch {
@@ -5395,8 +5875,8 @@ function detectAppType(cwd) {
5395
5875
  return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
5396
5876
  }
5397
5877
  function detectBuildCommand(cwd, pm) {
5398
- const pkgJsonPath = join9(cwd, "package.json");
5399
- if (!existsSync4(pkgJsonPath)) {
5878
+ const pkgJsonPath = join10(cwd, "package.json");
5879
+ if (!existsSync5(pkgJsonPath)) {
5400
5880
  return {
5401
5881
  kind: "ambiguous",
5402
5882
  reason: "no package.json at repo root. Run your package manager's `init` first."
@@ -5404,7 +5884,7 @@ function detectBuildCommand(cwd, pm) {
5404
5884
  }
5405
5885
  let pkgJson;
5406
5886
  try {
5407
- pkgJson = JSON.parse(readFileSync8(pkgJsonPath, "utf8"));
5887
+ pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf8"));
5408
5888
  } catch (e) {
5409
5889
  return {
5410
5890
  kind: "ambiguous",
@@ -5439,7 +5919,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
5439
5919
  function detectDestinationDir(cwd, vite) {
5440
5920
  let text;
5441
5921
  try {
5442
- text = readFileSync8(vite.path, "utf8");
5922
+ text = readFileSync9(vite.path, "utf8");
5443
5923
  } catch (e) {
5444
5924
  return {
5445
5925
  kind: "ambiguous",
@@ -5469,7 +5949,7 @@ async function runInit(args, io, prompt) {
5469
5949
  }
5470
5950
  const { inputs, options } = parsed;
5471
5951
  const outPath = resolve7(process.cwd(), options.out);
5472
- if (existsSync5(outPath) && !options.force) {
5952
+ if (existsSync6(outPath) && !options.force) {
5473
5953
  io.err(`launchpad init: ${outPath} already exists`);
5474
5954
  io.err("Pass --force to overwrite.");
5475
5955
  return 64;
@@ -5876,6 +6356,10 @@ function buildManifest(inputs, detected) {
5876
6356
  }
5877
6357
  ];
5878
6358
  }
6359
+ manifest.app = {
6360
+ root: ".",
6361
+ include: [...defaultIncludeForAppType(inputs.type)]
6362
+ };
5879
6363
  return manifest;
5880
6364
  }
5881
6365
  function renderYaml(manifest) {
@@ -5883,8 +6367,8 @@ function renderYaml(manifest) {
5883
6367
  }
5884
6368
  function ensureGitignoreEntries(path8, entries) {
5885
6369
  let current = "";
5886
- if (existsSync5(path8)) {
5887
- current = readFileSync9(path8, "utf8");
6370
+ if (existsSync6(path8)) {
6371
+ current = readFileSync10(path8, "utf8");
5888
6372
  }
5889
6373
  const lines = current.split(/\r?\n/);
5890
6374
  const present = new Set(lines.map((l) => l.trim()));
@@ -6016,7 +6500,7 @@ async function runLogs(args, io) {
6016
6500
  if (r.deployments.length === 0) {
6017
6501
  io.out("");
6018
6502
  io.out("(note: this command shows deploy history, not runtime logs —");
6019
- io.out(" see the portal app detail page for the link to Cf dashboard logs)");
6503
+ io.out(` run \`launchpad status ${slug}\` for the latest build outcome)`);
6020
6504
  }
6021
6505
  return 0;
6022
6506
  } catch (e) {
@@ -6184,6 +6668,8 @@ async function runMerge(args, io) {
6184
6668
  if (body.selfMerge) {
6185
6669
  io.out("Note: self-merge (last commit author == caller)");
6186
6670
  }
6671
+ io.out("");
6672
+ io.out(`Merged; build pending — run \`launchpad status ${slug}\` to confirm the build outcome.`);
6187
6673
  return 0;
6188
6674
  }
6189
6675
  let env = null;
@@ -6296,7 +6782,7 @@ function renderBotError(status, env, io, prNumber = null) {
6296
6782
  io.err(`launchpad merge: GitHub upstream error (${detail ?? code}).`);
6297
6783
  return;
6298
6784
  case "merge_unprocessable":
6299
- io.err(`launchpad merge: GitHub refused the merge (422): ${detail ?? "see GitHub PR page for details"}`);
6785
+ io.err(`launchpad merge: GitHub refused the merge (422): ${detail ?? (prNumber !== null ? `run \`launchpad review ${prNumber}\` for details` : "run `launchpad review <prNumber>` for details")}`);
6300
6786
  return;
6301
6787
  default:
6302
6788
  io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
@@ -6783,6 +7269,26 @@ async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
6783
7269
  return apiJson(cfg, { path: path10 }, fetcher);
6784
7270
  }
6785
7271
 
7272
+ // src/deploy/deployment-status.ts
7273
+ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
7274
+ let raw;
7275
+ try {
7276
+ raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/deployment-status` }, fetcher);
7277
+ } catch (e) {
7278
+ if (e instanceof NotFoundError)
7279
+ return null;
7280
+ throw e;
7281
+ }
7282
+ if (typeof raw.supported !== "boolean")
7283
+ return null;
7284
+ return {
7285
+ slug: typeof raw.slug === "string" ? raw.slug : slug,
7286
+ supported: raw.supported,
7287
+ liveDeployment: raw.liveDeployment ?? null,
7288
+ lastSuccessfulDeployment: raw.lastSuccessfulDeployment ?? null
7289
+ };
7290
+ }
7291
+
6786
7292
  // src/commands/pull.ts
6787
7293
  var pullCommand = {
6788
7294
  name: "pull",
@@ -6828,6 +7334,12 @@ async function runPull(args, io) {
6828
7334
  includeManifest: true
6829
7335
  });
6830
7336
  if (state.manifestYaml === null || state.manifestYaml === undefined) {
7337
+ const live = await fetchLiveDeploymentBestEffort(cfg, parsed.slug);
7338
+ if (live?.liveDeployment != null && (live.liveDeployment.buildStatus === "success" || live.lastSuccessfulDeployment != null)) {
7339
+ io.err(`launchpad pull: no platform-tracked manifest for "${parsed.slug}", ` + `but the app HAS live content (last deployment ${live.liveDeployment.createdOn} ` + `via ${live.liveDeployment.trigger === "git-push" ? "git push" : live.liveDeployment.trigger}, ` + `build ${live.liveDeployment.buildStatus}).`);
7340
+ io.err(` This app deploys outside \`launchpad deploy\`, so there is no manifest to pull. ` + `Run \`launchpad status ${parsed.slug}\` for the live deployment detail.`);
7341
+ return 1;
7342
+ }
6831
7343
  io.err(`launchpad pull: no deployed manifest for "${parsed.slug}" yet — ` + `run \`launchpad deploy\` first, or check \`launchpad status\` for the apply state.`);
6832
7344
  return 1;
6833
7345
  }
@@ -6864,6 +7376,13 @@ async function runPull(args, io) {
6864
7376
  return 1;
6865
7377
  }
6866
7378
  }
7379
+ async function fetchLiveDeploymentBestEffort(cfg, slug) {
7380
+ try {
7381
+ return await fetchDeploymentStatus(cfg, slug);
7382
+ } catch {
7383
+ return null;
7384
+ }
7385
+ }
6867
7386
  function parseArgs8(args, cwd = process.cwd()) {
6868
7387
  let slug = null;
6869
7388
  let out = null;
@@ -6938,7 +7457,7 @@ function describe23(e) {
6938
7457
  }
6939
7458
 
6940
7459
  // src/commands/status.ts
6941
- import { readFileSync as readFileSync10 } from "node:fs";
7460
+ import { readFileSync as readFileSync11 } from "node:fs";
6942
7461
  var statusCommand = {
6943
7462
  name: "status",
6944
7463
  summary: "show drift between local launchpad.yaml and deployed state",
@@ -6995,7 +7514,7 @@ async function runStatus(args, io) {
6995
7514
  }
6996
7515
  let localYaml;
6997
7516
  try {
6998
- localYaml = readFileSync10(parsed.file, "utf8");
7517
+ localYaml = readFileSync11(parsed.file, "utf8");
6999
7518
  } catch (e) {
7000
7519
  io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe24(e)}`);
7001
7520
  if (lifecycle !== null && lifecycle.state === "live") {
@@ -7005,8 +7524,8 @@ async function runStatus(args, io) {
7005
7524
  }
7006
7525
  let localObj;
7007
7526
  try {
7008
- const { parse: parseYaml4 } = await import("yaml");
7009
- localObj = parseYaml4(localYaml);
7527
+ const { parse: parseYaml5 } = await import("yaml");
7528
+ localObj = parseYaml5(localYaml);
7010
7529
  } catch (e) {
7011
7530
  io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe24(e)}`);
7012
7531
  return 2;
@@ -7027,25 +7546,39 @@ async function runStatus(args, io) {
7027
7546
  } catch (e) {
7028
7547
  return mapBotError(e, parsed.slug, io);
7029
7548
  }
7549
+ let deployment = null;
7550
+ let deploymentKnown = false;
7551
+ try {
7552
+ deployment = await fetchDeploymentStatus(cfg, parsed.slug);
7553
+ deploymentKnown = deployment !== null;
7554
+ } catch (e) {
7555
+ if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
7556
+ return mapBotError(e, parsed.slug, io);
7557
+ }
7558
+ io.err(`launchpad status: live deployment state unavailable (${describe24(e)}) — ` + `the report below is from the platform manifest view only.`);
7559
+ }
7030
7560
  if (state.manifestYaml === null || state.manifestYaml === undefined) {
7561
+ const live = deployment?.liveDeployment ?? null;
7562
+ const contentIsLive = live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
7031
7563
  const liveButEmpty = lifecycle !== null && lifecycle.state === "live";
7032
7564
  const out = {
7033
- state: liveButEmpty ? "live_no_content" : "no_deployed_manifest",
7565
+ state: contentIsLive ? "live_content_untracked" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
7034
7566
  slug: parsed.slug,
7035
7567
  deployedSha: state.lastAppliedManifestSha,
7036
7568
  headSha: state.appRepoHeadSha,
7037
7569
  hasOpenPr: state.openPr !== null,
7038
7570
  openPrNumber: state.openPr?.number ?? null,
7039
7571
  driftFields: [],
7040
- driftDetails: []
7572
+ driftDetails: [],
7573
+ ...deploymentKnown ? { deployment } : {}
7041
7574
  };
7042
7575
  emit3(out, parsed.json, io);
7043
7576
  return 0;
7044
7577
  }
7045
7578
  let deployedObj;
7046
7579
  try {
7047
- const { parse: parseYaml4 } = await import("yaml");
7048
- deployedObj = parseYaml4(state.manifestYaml);
7580
+ const { parse: parseYaml5 } = await import("yaml");
7581
+ deployedObj = parseYaml5(state.manifestYaml);
7049
7582
  } catch (e) {
7050
7583
  io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe24(e)}`);
7051
7584
  return 2;
@@ -7068,7 +7601,8 @@ async function runStatus(args, io) {
7068
7601
  hasOpenPr: state.openPr !== null,
7069
7602
  openPrNumber: state.openPr?.number ?? null,
7070
7603
  driftFields: drift.map((d) => d.path),
7071
- driftDetails: drift
7604
+ driftDetails: drift,
7605
+ ...deploymentKnown ? { deployment } : {}
7072
7606
  };
7073
7607
  emit3(result, parsed.json, io);
7074
7608
  if (result.state === "drift" && parsed.strict) {
@@ -7154,14 +7688,22 @@ function emit3(out, asJson, io) {
7154
7688
  case "live_no_content":
7155
7689
  io.out(`${out.slug}: live — no content deployed yet. Run \`launchpad deploy\`.`);
7156
7690
  surfaceHeadVsDeployed(out, io);
7691
+ surfaceDeployment(out, io);
7157
7692
  return;
7158
7693
  case "no_deployed_manifest":
7159
7694
  io.out(`${out.slug}: no deployed manifest yet — run \`launchpad deploy\`.`);
7160
7695
  surfaceHeadVsDeployed(out, io);
7696
+ surfaceDeployment(out, io);
7697
+ return;
7698
+ case "live_content_untracked":
7699
+ io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
7700
+ surfaceHeadVsDeployed(out, io);
7701
+ surfaceDeployment(out, io);
7161
7702
  return;
7162
7703
  case "in_sync":
7163
7704
  io.out(`${out.slug}: live, in sync` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
7164
7705
  surfaceHeadVsDeployed(out, io);
7706
+ surfaceDeployment(out, io);
7165
7707
  return;
7166
7708
  case "drift":
7167
7709
  io.out(`${out.slug}: live, drift: ${out.driftFields.join(", ")}`);
@@ -7171,7 +7713,54 @@ function emit3(out, asJson, io) {
7171
7713
  io.out(` deployed: ${formatValue(d.deployed)}`);
7172
7714
  }
7173
7715
  surfaceHeadVsDeployed(out, io);
7716
+ surfaceDeployment(out, io);
7717
+ return;
7718
+ }
7719
+ }
7720
+ function triggerLabel(trigger) {
7721
+ if (trigger === "git-push")
7722
+ return "git push";
7723
+ if (trigger === "bot")
7724
+ return "launchpad deploy (bot)";
7725
+ return "an unknown mechanism";
7726
+ }
7727
+ function surfaceDeployment(out, io) {
7728
+ const dep = out.deployment;
7729
+ if (dep === undefined || dep === null || !dep.supported)
7730
+ return;
7731
+ const live = dep.liveDeployment;
7732
+ if (live === null) {
7733
+ io.out(" last deployment: none — Cloudflare Pages has no production deployment yet.");
7734
+ return;
7735
+ }
7736
+ const commit = live.commit !== undefined ? `, commit ${live.commit.hash.slice(0, 7)}` : "";
7737
+ const via = triggerLabel(live.trigger);
7738
+ switch (live.buildStatus) {
7739
+ case "success":
7740
+ io.out(` last deployment: build success (${via}${commit}) at ${live.createdOn}`);
7174
7741
  return;
7742
+ case "in_progress":
7743
+ io.out(` last deployment: build IN PROGRESS (${via}${commit}, started ${live.createdOn})`);
7744
+ io.out(" re-run `launchpad status` shortly to confirm the build outcome.");
7745
+ return;
7746
+ case "failure": {
7747
+ const stage = live.failedStage !== undefined ? ` at stage "${live.failedStage}"` : "";
7748
+ io.out(` last deployment: build FAILED${stage} (${via}${commit}) at ${live.createdOn}`);
7749
+ if (live.logExcerpt !== undefined && live.logExcerpt.length > 0) {
7750
+ io.out(" build log (excerpt):");
7751
+ for (const line of live.logExcerpt.split(`
7752
+ `)) {
7753
+ io.out(` ${line}`);
7754
+ }
7755
+ }
7756
+ if (dep.lastSuccessfulDeployment !== null) {
7757
+ io.out(` serving: previous successful deployment from ${dep.lastSuccessfulDeployment.createdOn}`);
7758
+ } else {
7759
+ io.out(" serving: nothing — no successful deployment exists yet.");
7760
+ }
7761
+ io.out(" next: fix the build locally (check build.command / build.root_dir in launchpad.yaml), then run `launchpad deploy`.");
7762
+ return;
7763
+ }
7175
7764
  }
7176
7765
  }
7177
7766
  function surfaceHeadVsDeployed(out, io) {
@@ -7451,7 +8040,7 @@ function describe25(e) {
7451
8040
  }
7452
8041
 
7453
8042
  // src/deploy/rollback.ts
7454
- import { existsSync as existsSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
8043
+ import { existsSync as existsSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
7455
8044
  import { resolve as resolvePath2 } from "node:path";
7456
8045
  import { createInterface as createInterface3 } from "node:readline/promises";
7457
8046
 
@@ -7575,11 +8164,11 @@ async function runRollback(opts, io, deps = {}) {
7575
8164
  }, io, applyDeps);
7576
8165
  }
7577
8166
  function readCurrentManifest(path11) {
7578
- if (!existsSync6(path11))
8167
+ if (!existsSync7(path11))
7579
8168
  return null;
7580
8169
  let raw;
7581
8170
  try {
7582
- raw = readFileSync11(path11, "utf8");
8171
+ raw = readFileSync12(path11, "utf8");
7583
8172
  } catch {
7584
8173
  return null;
7585
8174
  }
@@ -7735,7 +8324,7 @@ function parseArgs11(args) {
7735
8324
  }
7736
8325
 
7737
8326
  // src/secrets/push.ts
7738
- import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
8327
+ import { existsSync as existsSync8, readFileSync as readFileSync13 } from "node:fs";
7739
8328
  import { resolve as resolvePath3 } from "node:path";
7740
8329
 
7741
8330
  // src/secrets/env-parse.ts
@@ -7790,14 +8379,14 @@ async function runSecretsPush(opts, io, deps = {}) {
7790
8379
  return 0;
7791
8380
  }
7792
8381
  const envPath = resolvePath3(process.cwd(), opts.env ?? ".env");
7793
- if (!existsSync7(envPath)) {
8382
+ if (!existsSync8(envPath)) {
7794
8383
  io.err(`launchpad secrets push: ${envPath}`);
7795
8384
  io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
7796
8385
  return 2;
7797
8386
  }
7798
8387
  let envText;
7799
8388
  try {
7800
- envText = readFileSync12(envPath, "utf8");
8389
+ envText = readFileSync13(envPath, "utf8");
7801
8390
  } catch (e) {
7802
8391
  io.err(`launchpad secrets push: failed to read ${envPath}: ${describe27(e)}`);
7803
8392
  return 2;
@@ -8069,9 +8658,9 @@ function renderManifestError5(result, io) {
8069
8658
  }
8070
8659
 
8071
8660
  // src/secrets/set.ts
8072
- import { existsSync as existsSync8, readFileSync as readFileSync13 } from "node:fs";
8661
+ import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
8073
8662
  import { resolve as resolvePath5 } from "node:path";
8074
- import { parse as parseYaml4 } from "yaml";
8663
+ import { parse as parseYaml5 } from "yaml";
8075
8664
  var CELL_LABEL2 = {
8076
8665
  present: "PRESENT",
8077
8666
  missing: "MISSING",
@@ -8079,14 +8668,14 @@ var CELL_LABEL2 = {
8079
8668
  };
8080
8669
  function loadSet(fleetFile, setName, io) {
8081
8670
  const path11 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
8082
- if (!existsSync8(path11)) {
8671
+ if (!existsSync9(path11)) {
8083
8672
  io.err(`✗ ${path11}`);
8084
8673
  io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
8085
8674
  return 2;
8086
8675
  }
8087
8676
  let obj;
8088
8677
  try {
8089
- obj = parseYaml4(readFileSync13(path11, "utf8"));
8678
+ obj = parseYaml5(readFileSync14(path11, "utf8"));
8090
8679
  } catch (e) {
8091
8680
  io.err(`✗ ${path11}`);
8092
8681
  io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
@@ -8171,14 +8760,14 @@ async function runSecretsPushSet(opts, io, deps = {}) {
8171
8760
  if (typeof set === "number")
8172
8761
  return set;
8173
8762
  const envPath = resolvePath5(process.cwd(), opts.env ?? ".env");
8174
- if (!existsSync8(envPath)) {
8763
+ if (!existsSync9(envPath)) {
8175
8764
  io.err(`launchpad secrets push --set: ${envPath} not found.`);
8176
8765
  io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
8177
8766
  return 2;
8178
8767
  }
8179
8768
  let envText;
8180
8769
  try {
8181
- envText = readFileSync13(envPath, "utf8");
8770
+ envText = readFileSync14(envPath, "utf8");
8182
8771
  } catch (e) {
8183
8772
  io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
8184
8773
  return 2;
@@ -8322,7 +8911,7 @@ function setPushExit(e) {
8322
8911
  }
8323
8912
 
8324
8913
  // src/commands/secrets-template.ts
8325
- import { existsSync as existsSync9, writeFileSync as writeFileSync6 } from "node:fs";
8914
+ import { existsSync as existsSync10, writeFileSync as writeFileSync6 } from "node:fs";
8326
8915
  import { resolve as resolve9 } from "node:path";
8327
8916
  async function runSecretsTemplate(args, io) {
8328
8917
  const flags = parseFlags4(args);
@@ -8346,7 +8935,7 @@ async function runSecretsTemplate(args, io) {
8346
8935
  return 0;
8347
8936
  }
8348
8937
  const outPath = resolve9(process.cwd(), flags.out);
8349
- if (existsSync9(outPath) && !flags.force) {
8938
+ if (existsSync10(outPath) && !flags.force) {
8350
8939
  io.err(`launchpad secrets template: ${outPath} already exists`);
8351
8940
  io.err("Pass --force to overwrite, or --stdout to print without writing.");
8352
8941
  return 64;
@@ -8634,8 +9223,8 @@ function printHelp2(io) {
8634
9223
 
8635
9224
  // src/commands/skills.ts
8636
9225
  import { fileURLToPath } from "node:url";
8637
- import { dirname as dirname6, join as join10, resolve as resolve10 } from "node:path";
8638
- import { promises as fs5, existsSync as existsSync10 } from "node:fs";
9226
+ import { dirname as dirname6, join as join11, resolve as resolve10 } from "node:path";
9227
+ import { promises as fs5, existsSync as existsSync11 } from "node:fs";
8639
9228
  import { homedir as homedir2 } from "node:os";
8640
9229
  var BUNDLE_PREFIX = "launchpad-";
8641
9230
  var BUNDLED_SKILLS = [
@@ -8696,7 +9285,7 @@ function printHelp3(io) {
8696
9285
  }
8697
9286
  function resolveInstallEnv() {
8698
9287
  const bundleDir = process.env.LAUNCHPAD_SKILLS_BUNDLE_DIR ?? defaultBundleDir();
8699
- const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join10(homedir2(), ".claude", "skills");
9288
+ const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join11(homedir2(), ".claude", "skills");
8700
9289
  return { bundleDir, userSkillsDir };
8701
9290
  }
8702
9291
  function defaultBundleDir() {
@@ -8706,7 +9295,7 @@ function defaultBundleDir() {
8706
9295
  resolve10(here, "..", "..", "skills")
8707
9296
  ];
8708
9297
  for (const c of candidates) {
8709
- if (existsSync10(join10(c, "launchpad-onboard", "SKILL.md"))) {
9298
+ if (existsSync11(join11(c, "launchpad-onboard", "SKILL.md"))) {
8710
9299
  return c;
8711
9300
  }
8712
9301
  }
@@ -8727,12 +9316,12 @@ async function doInstall(io) {
8727
9316
  }
8728
9317
  let installed = 0;
8729
9318
  for (const skill of BUNDLED_SKILLS) {
8730
- const src = join10(env.bundleDir, skill);
9319
+ const src = join11(env.bundleDir, skill);
8731
9320
  if (!await isDir(src)) {
8732
9321
  io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
8733
9322
  return 1;
8734
9323
  }
8735
- const dest = join10(env.userSkillsDir, skill);
9324
+ const dest = join11(env.userSkillsDir, skill);
8736
9325
  await fs5.rm(dest, { recursive: true, force: true });
8737
9326
  await fs5.cp(src, dest, { recursive: true });
8738
9327
  installed++;
@@ -8757,7 +9346,7 @@ async function doUninstall(io) {
8757
9346
  continue;
8758
9347
  if (!isBundleManaged(entry.name))
8759
9348
  continue;
8760
- const target = join10(env.userSkillsDir, entry.name);
9349
+ const target = join11(env.userSkillsDir, entry.name);
8761
9350
  await fs5.rm(target, { recursive: true, force: true });
8762
9351
  removed++;
8763
9352
  io.out(`✗ removed ${target}`);
@@ -8780,7 +9369,7 @@ async function doList(io) {
8780
9369
  }
8781
9370
  const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
8782
9371
  for (const name of managedDirs) {
8783
- const skillFile = join10(env.userSkillsDir, name, "SKILL.md");
9372
+ const skillFile = join11(env.userSkillsDir, name, "SKILL.md");
8784
9373
  const version = await readVersion(skillFile);
8785
9374
  io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
8786
9375
  }
@@ -8815,9 +9404,9 @@ function describe28(e) {
8815
9404
  import { execFile, spawn as spawn5 } from "node:child_process";
8816
9405
  import { promisify } from "node:util";
8817
9406
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8818
- import { dirname as dirname7, resolve as resolve11, relative as relative4, isAbsolute as isAbsolute2, join as join11 } from "node:path";
9407
+ import { dirname as dirname7, resolve as resolve11, relative as relative4, isAbsolute as isAbsolute2, join as join12 } from "node:path";
8819
9408
  import { homedir as homedir3, tmpdir } from "node:os";
8820
- import { readFileSync as readFileSync14, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
9409
+ import { readFileSync as readFileSync15, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
8821
9410
 
8822
9411
  // src/commands/channel-auth.ts
8823
9412
  import { createServer as createServer2 } from "node:http";
@@ -8938,7 +9527,7 @@ var PKG = "@m-kopa/launchpad-cli";
8938
9527
  var REGISTRY = "https://registry.npmjs.org";
8939
9528
  var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
8940
9529
  var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
8941
- var CHANNEL_MARKER = join11(homedir3(), ".launchpad", "channel");
9530
+ var CHANNEL_MARKER = join12(homedir3(), ".launchpad", "channel");
8942
9531
  var EXIT_UPDATE_AVAILABLE = 10;
8943
9532
  var UPGRADE_ARGS = {
8944
9533
  npm: ["install", "-g", `${PKG}@latest`],
@@ -9021,8 +9610,8 @@ async function openSystemBrowser(url) {
9021
9610
  await execFileAsync(opener, [url]);
9022
9611
  }
9023
9612
  async function runInstallerScript(script) {
9024
- const dir = mkdtempSync(join11(tmpdir(), "launchpad-update-"));
9025
- const file = join11(dir, "install.sh");
9613
+ const dir = mkdtempSync(join12(tmpdir(), "launchpad-update-"));
9614
+ const file = join12(dir, "install.sh");
9026
9615
  try {
9027
9616
  writeFileSync7(file, script, { mode: 448 });
9028
9617
  return await new Promise((resolvePromise) => {
@@ -9046,7 +9635,7 @@ function resolveLatestVersion() {
9046
9635
  }
9047
9636
  function detectInstallChannel() {
9048
9637
  try {
9049
- return readFileSync14(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
9638
+ return readFileSync15(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
9050
9639
  } catch {
9051
9640
  return "github";
9052
9641
  }
@@ -9253,7 +9842,8 @@ function printHelp4(io) {
9253
9842
  }
9254
9843
 
9255
9844
  // src/commands/validate.ts
9256
- import { resolve as resolve12 } from "node:path";
9845
+ import { readFileSync as readFileSync16 } from "node:fs";
9846
+ import { dirname as dirname8, resolve as resolve12 } from "node:path";
9257
9847
  var validateCommand = {
9258
9848
  name: "validate",
9259
9849
  summary: "validate launchpad.yaml against the v1alpha1 schema",
@@ -9272,9 +9862,31 @@ async function runValidate(args, io) {
9272
9862
  if (result.kind !== "ok") {
9273
9863
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
9274
9864
  }
9865
+ const boundary = checkBoundary(path11, result.manifest.app !== undefined);
9275
9866
  const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
9276
- return json ? renderJsonOk(result, groupCheck, io) : renderHumanOk(result, groupCheck, io);
9867
+ return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
9868
+ }
9869
+ function checkBoundary(manifestPath, declared) {
9870
+ const dir = dirname8(manifestPath);
9871
+ let files;
9872
+ try {
9873
+ files = walkCwd(dir).files;
9874
+ } catch {
9875
+ return { declared, errors: [], warnings: [] };
9876
+ }
9877
+ let manifestYaml;
9878
+ try {
9879
+ manifestYaml = readFileSync16(manifestPath, "utf8");
9880
+ } catch {
9881
+ manifestYaml = null;
9882
+ }
9883
+ const r = applyBoundaryToFiles({ cwd: dir, manifestYaml, files });
9884
+ if (r.kind === "error") {
9885
+ return { declared, errors: [r.message], warnings: [] };
9886
+ }
9887
+ return { declared, errors: [], warnings: r.warnings };
9277
9888
  }
9889
+ var DECLARE_APP_HINT = "no app: boundary declared — deploys bundle the whole working tree minus the platform deny-list. " + "Declare app: (root / include / exclude) in launchpad.yaml — see schema/README.md.";
9278
9890
  async function checkGroup(allowedGroup, cfg) {
9279
9891
  try {
9280
9892
  const r = await fetchGroups(cfg);
@@ -9321,10 +9933,22 @@ async function checkGroups(groups) {
9321
9933
  const ok = perGroup.every((r) => r.kind === "ok");
9322
9934
  return ok ? { kind: "all-ok", perGroup } : { kind: "some-failed", perGroup };
9323
9935
  }
9324
- function renderHumanOk(result, group, io) {
9936
+ function renderHumanOk(result, group, boundary, io) {
9325
9937
  io.out(`✓ ${result.manifest.metadata.name} — manifest is valid`);
9326
9938
  io.out(` apiVersion: launchpad.m-kopa.us/v1alpha1`);
9327
9939
  io.out(` type: ${result.manifest.deployment.type}`);
9940
+ if (boundary.errors.length > 0) {
9941
+ for (const e of boundary.errors) {
9942
+ io.err(`✗ app boundary: ${e}`);
9943
+ }
9944
+ return 1;
9945
+ }
9946
+ for (const w of boundary.warnings) {
9947
+ io.out(`⚠ app boundary: ${w}`);
9948
+ }
9949
+ if (!boundary.declared) {
9950
+ io.out(`⚠ ${DECLARE_APP_HINT}`);
9951
+ }
9328
9952
  switch (group.kind) {
9329
9953
  case "skipped":
9330
9954
  return 0;
@@ -9388,11 +10012,20 @@ function renderHumanError(result, io) {
9388
10012
  return 1;
9389
10013
  }
9390
10014
  }
9391
- function renderJsonOk(result, group, io) {
10015
+ function renderJsonOk(result, group, boundary, io) {
9392
10016
  const base = {
9393
10017
  name: result.manifest.metadata.name,
9394
- deploymentType: result.manifest.deployment.type
10018
+ deploymentType: result.manifest.deployment.type,
10019
+ appBoundary: {
10020
+ declared: boundary.declared,
10021
+ errors: boundary.errors,
10022
+ warnings: boundary.warnings
10023
+ }
9395
10024
  };
10025
+ if (boundary.errors.length > 0) {
10026
+ io.out(JSON.stringify({ ok: false, ...base }));
10027
+ return 1;
10028
+ }
9396
10029
  switch (group.kind) {
9397
10030
  case "skipped":
9398
10031
  io.out(JSON.stringify({ ok: true, ...base }));
@@ -9557,15 +10190,15 @@ function describe30(e) {
9557
10190
  // src/update-notifier.ts
9558
10191
  import { spawn as spawn6 } from "node:child_process";
9559
10192
  import { homedir as homedir4 } from "node:os";
9560
- import { join as join12 } from "node:path";
9561
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
10193
+ import { join as join13 } from "node:path";
10194
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "node:fs";
9562
10195
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
9563
10196
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
9564
10197
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
9565
- var CACHE_FILE = join12(homedir4(), ".launchpad", "update-check.json");
10198
+ var CACHE_FILE = join13(homedir4(), ".launchpad", "update-check.json");
9566
10199
  function readCache2() {
9567
10200
  try {
9568
- const raw = JSON.parse(readFileSync15(CACHE_FILE, "utf8"));
10201
+ const raw = JSON.parse(readFileSync17(CACHE_FILE, "utf8"));
9569
10202
  if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
9570
10203
  const latest = raw.latest;
9571
10204
  return {
@@ -9578,7 +10211,7 @@ function readCache2() {
9578
10211
  }
9579
10212
  function writeCache2(state) {
9580
10213
  try {
9581
- mkdirSync3(join12(homedir4(), ".launchpad"), { recursive: true });
10214
+ mkdirSync3(join13(homedir4(), ".launchpad"), { recursive: true });
9582
10215
  writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
9583
10216
  `, { mode: 384 });
9584
10217
  } catch {}