@m-kopa/launchpad-cli 0.63.0 → 0.65.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/CHANGELOG.md +44 -1
- package/dist/cli.js +457 -119
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/migrate.d.ts +4 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/secrets-template.d.ts.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/manifest/container-runtime-warnings.d.ts +13 -0
- package/dist/manifest/container-runtime-warnings.d.ts.map +1 -0
- package/dist/manifest/migrate-dispatch.d.ts +13 -3
- package/dist/manifest/migrate-dispatch.d.ts.map +1 -1
- package/dist/manifest/wrangler-config.d.ts +3 -0
- package/dist/manifest/wrangler-config.d.ts.map +1 -0
- package/dist/secrets/declared.d.ts +12 -0
- package/dist/secrets/declared.d.ts.map +1 -0
- package/dist/secrets/push.d.ts +0 -8
- package/dist/secrets/push.d.ts.map +1 -1
- package/dist/secrets/status.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +3 -3
- package/skills/launchpad-content-pr/SKILL.md +1 -1
- package/skills/launchpad-deploy/SKILL.md +50 -7
- package/skills/launchpad-deploy-status/SKILL.md +4 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-identity/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +5 -5
- package/skills/launchpad-report/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +4 -1
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
19
19
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
20
|
|
|
21
21
|
// src/version.ts
|
|
22
|
-
var CLI_VERSION = "0.
|
|
22
|
+
var CLI_VERSION = "0.65.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import { readFileSync } from "node:fs";
|
|
@@ -3363,7 +3363,10 @@ var SpecSchema = z3.object({
|
|
|
3363
3363
|
env_vars: z3.record(z3.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
|
|
3364
3364
|
containers: z3.array(ContainerSchema).min(1).optional(),
|
|
3365
3365
|
secrets: SecretsSchema.optional(),
|
|
3366
|
-
fronted_by_concierge: z3.boolean().optional()
|
|
3366
|
+
fronted_by_concierge: z3.boolean().optional(),
|
|
3367
|
+
bindings: z3.object({
|
|
3368
|
+
d1: z3.string().min(1).max(64).regex(ENV_NAME_REGEX, "bindings.d1 must be an UPPER_SNAKE_CASE binding name").optional()
|
|
3369
|
+
}).strict().optional()
|
|
3367
3370
|
}).strict().superRefine((s, ctx) => {
|
|
3368
3371
|
if (s.access.identity_types !== undefined && (s.auth ?? "access") !== "gateway") {
|
|
3369
3372
|
ctx.addIssue({
|
|
@@ -3432,14 +3435,27 @@ var SpecSchema = z3.object({
|
|
|
3432
3435
|
message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
|
|
3433
3436
|
});
|
|
3434
3437
|
}
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
}
|
|
3438
|
+
}
|
|
3439
|
+
if (s.bindings?.d1 !== undefined) {
|
|
3440
|
+
if (s.origin !== "dispatch") {
|
|
3441
|
+
ctx.addIssue({
|
|
3442
|
+
code: z3.ZodIssueCode.custom,
|
|
3443
|
+
path: ["bindings", "d1"],
|
|
3444
|
+
message: "bindings.d1 requires origin: dispatch. A pages-origin app declares its database with a v1 pages target d1_binding."
|
|
3445
|
+
});
|
|
3446
|
+
} else if (s.appType !== "react+api") {
|
|
3447
|
+
ctx.addIssue({
|
|
3448
|
+
code: z3.ZodIssueCode.custom,
|
|
3449
|
+
path: ["bindings", "d1"],
|
|
3450
|
+
message: `bindings.d1 requires appType 'react+api' (current appType: '${s.appType}'); only a react+api app ships the Worker code that can read a database.`
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
if (s.env_vars?.[s.bindings.d1] !== undefined) {
|
|
3454
|
+
ctx.addIssue({
|
|
3455
|
+
code: z3.ZodIssueCode.custom,
|
|
3456
|
+
path: ["env_vars", s.bindings.d1],
|
|
3457
|
+
message: `env var '${s.bindings.d1}' has the same name as bindings.d1. Rename one of them.`
|
|
3458
|
+
});
|
|
3443
3459
|
}
|
|
3444
3460
|
}
|
|
3445
3461
|
if (!isContainer && s.containers !== undefined) {
|
|
@@ -4449,7 +4465,7 @@ function deploymentAuthIssue(appType, auth) {
|
|
|
4449
4465
|
function newAppOrigin(appType, requested) {
|
|
4450
4466
|
if (appType === "container")
|
|
4451
4467
|
return;
|
|
4452
|
-
return requested ??
|
|
4468
|
+
return requested ?? "dispatch";
|
|
4453
4469
|
}
|
|
4454
4470
|
// src/bundle/boundary.ts
|
|
4455
4471
|
var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
|
|
@@ -8620,24 +8636,32 @@ function rejectUnsupportedCliRuntime(args) {
|
|
|
8620
8636
|
}
|
|
8621
8637
|
|
|
8622
8638
|
// src/commands/migrate.ts
|
|
8623
|
-
import { closeSync, existsSync as existsSync7, lstatSync as lstatSync4, mkdirSync as mkdirSync2, mkdtempSync, openSync, readFileSync as readFileSync11, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8639
|
+
import { closeSync, existsSync as existsSync7, lstatSync as lstatSync4, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync as readdirSync2, readFileSync as readFileSync11, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8624
8640
|
import path8 from "node:path";
|
|
8625
8641
|
import { stringify } from "yaml";
|
|
8626
8642
|
|
|
8627
8643
|
// src/manifest/migrate-dispatch.ts
|
|
8628
|
-
|
|
8644
|
+
var PLATFORM_ENV_NAMES = new Set([
|
|
8645
|
+
"GATEWAY_ISSUER",
|
|
8646
|
+
"GATEWAY_AUD",
|
|
8647
|
+
"GATEWAY_JWKS_URL",
|
|
8648
|
+
"CF_ACCESS_TEAM_DOMAIN",
|
|
8649
|
+
"CF_ACCESS_AUD",
|
|
8650
|
+
"JWKS_URL"
|
|
8651
|
+
]);
|
|
8652
|
+
function dispatchManifest(manifest, wranglerD1) {
|
|
8629
8653
|
const v2 = manifest.apiVersion === "launchpad.m-kopa.us/v2";
|
|
8630
8654
|
const source = v2 ? manifest.spec : manifest;
|
|
8631
8655
|
const appType = v2 ? manifest.spec.appType : manifest.deployment.type;
|
|
8632
|
-
if (appType
|
|
8633
|
-
throw new Error("
|
|
8634
|
-
}
|
|
8635
|
-
if (source.secrets !== undefined || source.containers !== undefined) {
|
|
8636
|
-
throw new Error("Secret and container bindings cannot be converted automatically.");
|
|
8656
|
+
if (appType === "container" || source.containers !== undefined) {
|
|
8657
|
+
throw new Error("Containers use their own runtime and cannot convert to dispatch.");
|
|
8637
8658
|
}
|
|
8638
8659
|
if (!v2)
|
|
8639
8660
|
assertConvertibleV1(manifest);
|
|
8640
|
-
const
|
|
8661
|
+
const d1 = d1Binding(v2 ? manifest.spec.bindings?.d1 : pagesTargetD1(manifest), wranglerD1);
|
|
8662
|
+
const plainEnv = v2 ? manifest.spec.env_vars : plainTextEnv(manifest.production_env);
|
|
8663
|
+
const envVars = { ...plainEnv, ...secretEnv(source.secrets, plainEnv) };
|
|
8664
|
+
const spec = v2 ? { ...manifest.spec, secrets: undefined } : {
|
|
8641
8665
|
appType,
|
|
8642
8666
|
display: {
|
|
8643
8667
|
name: manifest.metadata.name,
|
|
@@ -8655,16 +8679,20 @@ function dispatchManifest(manifest) {
|
|
|
8655
8679
|
app: manifest.app,
|
|
8656
8680
|
verify: manifest.verify,
|
|
8657
8681
|
sensitive: manifest.sensitive,
|
|
8658
|
-
catalogue: manifest.catalogue
|
|
8659
|
-
...manifest.production_env === undefined ? {} : {
|
|
8660
|
-
env_vars: Object.fromEntries(Object.entries(manifest.production_env).map(([name, value]) => [name, { type: "plain_text", value }]))
|
|
8661
|
-
}
|
|
8682
|
+
catalogue: manifest.catalogue
|
|
8662
8683
|
};
|
|
8663
8684
|
const candidate = {
|
|
8664
8685
|
apiVersion: "launchpad.m-kopa.us/v2",
|
|
8665
8686
|
kind: "App",
|
|
8666
8687
|
metadata: v2 ? manifest.metadata : { slug: manifest.metadata.name, schemaVersion: 2 },
|
|
8667
|
-
spec: {
|
|
8688
|
+
spec: {
|
|
8689
|
+
...spec,
|
|
8690
|
+
...d1 === undefined ? {} : { bindings: { d1 } },
|
|
8691
|
+
env_vars: Object.keys(envVars).length === 0 ? undefined : envVars,
|
|
8692
|
+
origin: "dispatch",
|
|
8693
|
+
auth: "gateway",
|
|
8694
|
+
confine_origin: undefined
|
|
8695
|
+
}
|
|
8668
8696
|
};
|
|
8669
8697
|
const parsed = parseManifestV2(candidate);
|
|
8670
8698
|
if (parsed.kind !== "ok") {
|
|
@@ -8675,7 +8703,7 @@ function dispatchManifest(manifest) {
|
|
|
8675
8703
|
function assertConvertibleV1(manifest) {
|
|
8676
8704
|
const unsupported2 = [
|
|
8677
8705
|
manifest.hostnames !== undefined && "hostnames",
|
|
8678
|
-
manifest.targets !== undefined && "targets",
|
|
8706
|
+
manifest.targets !== undefined && pagesTargetD1(manifest) === undefined && "targets",
|
|
8679
8707
|
manifest.access.session_duration !== undefined && "access.session_duration",
|
|
8680
8708
|
manifest.deployment.runtime !== undefined && "deployment.runtime"
|
|
8681
8709
|
].filter(Boolean);
|
|
@@ -8683,12 +8711,224 @@ function assertConvertibleV1(manifest) {
|
|
|
8683
8711
|
throw new Error(`Cannot preserve ${unsupported2.join(", ")} on dispatch. Review these settings with the platform team before migrating.`);
|
|
8684
8712
|
}
|
|
8685
8713
|
}
|
|
8714
|
+
function d1Binding(declared, wranglerD1) {
|
|
8715
|
+
if (declared !== undefined && wranglerD1 !== undefined && declared !== wranglerD1) {
|
|
8716
|
+
throw new Error(`The manifest binds D1 as ${declared} but the wrangler config binds ${wranglerD1}. Use one name for both.`);
|
|
8717
|
+
}
|
|
8718
|
+
return declared ?? wranglerD1;
|
|
8719
|
+
}
|
|
8720
|
+
function plainTextEnv(env) {
|
|
8721
|
+
if (env === undefined)
|
|
8722
|
+
return;
|
|
8723
|
+
return Object.fromEntries(Object.entries(env).map(([name, value]) => [name, { type: "plain_text", value }]));
|
|
8724
|
+
}
|
|
8725
|
+
function secretEnv(secrets, env) {
|
|
8726
|
+
const out = {};
|
|
8727
|
+
for (const binding of secrets?.bindings ?? []) {
|
|
8728
|
+
const refusal = secretRefusal(binding, env);
|
|
8729
|
+
if (refusal !== undefined)
|
|
8730
|
+
throw new Error(`Secret ${binding.name} ${refusal}`);
|
|
8731
|
+
out[binding.name] = { type: "secret_text", secret_ref: binding.name };
|
|
8732
|
+
}
|
|
8733
|
+
return out;
|
|
8734
|
+
}
|
|
8735
|
+
function secretRefusal(binding, env) {
|
|
8736
|
+
const targets = binding.targets === undefined || binding.targets === "all" ? [] : binding.targets;
|
|
8737
|
+
const checks = [
|
|
8738
|
+
[binding.source !== "env-file", "is platform-managed and cannot be converted automatically. Ask the platform team to migrate it."],
|
|
8739
|
+
[targets.some((target) => target !== "primary"), "targets another surface, which dispatch does not have."],
|
|
8740
|
+
[PLATFORM_ENV_NAMES.has(binding.name), "is set by the platform on dispatch apps. Rename it before converting."],
|
|
8741
|
+
[env?.[binding.name] !== undefined, "is also declared as an environment variable. Keep one."]
|
|
8742
|
+
];
|
|
8743
|
+
return checks.find(([refused]) => refused)?.[1];
|
|
8744
|
+
}
|
|
8745
|
+
function pagesTargetD1(manifest) {
|
|
8746
|
+
const [target, ...rest] = manifest.targets ?? [];
|
|
8747
|
+
if (rest.length > 0 || target?.kind !== "pages")
|
|
8748
|
+
return;
|
|
8749
|
+
return target.d1_binding;
|
|
8750
|
+
}
|
|
8751
|
+
|
|
8752
|
+
// src/manifest/wrangler-config.ts
|
|
8753
|
+
var ALLOWED_KEYS = new Set([
|
|
8754
|
+
"name",
|
|
8755
|
+
"compatibility_date",
|
|
8756
|
+
"compatibility_flags",
|
|
8757
|
+
"pages_build_output_dir",
|
|
8758
|
+
"d1_databases"
|
|
8759
|
+
]);
|
|
8760
|
+
var PLATFORM_FLAGS = new Set(["nodejs_compat"]);
|
|
8761
|
+
|
|
8762
|
+
class UnsupportedWranglerConfigError extends Error {
|
|
8763
|
+
}
|
|
8764
|
+
function wranglerD1Binding(file, text) {
|
|
8765
|
+
const toml = file.endsWith(".toml");
|
|
8766
|
+
const config = toml ? readToml(file, text) : readJsonc(file, text);
|
|
8767
|
+
for (const key of Object.keys(config)) {
|
|
8768
|
+
if (!ALLOWED_KEYS.has(key) && (toml || key !== "$schema"))
|
|
8769
|
+
throw unsupportedKey(file, key);
|
|
8770
|
+
}
|
|
8771
|
+
if (config.compatibility_flags !== undefined) {
|
|
8772
|
+
const extra = asArray(file, "compatibility_flags", config.compatibility_flags).find((flag) => !PLATFORM_FLAGS.has(String(flag)));
|
|
8773
|
+
if (extra !== undefined) {
|
|
8774
|
+
throw new UnsupportedWranglerConfigError(`${file} sets compatibility flag \`${String(extra)}\`; dispatch runs with nodejs_compat only`);
|
|
8775
|
+
}
|
|
8776
|
+
}
|
|
8777
|
+
if (config.d1_databases === undefined)
|
|
8778
|
+
return;
|
|
8779
|
+
const databases = asArray(file, "d1_databases", config.d1_databases);
|
|
8780
|
+
if (databases.length !== 1) {
|
|
8781
|
+
throw new UnsupportedWranglerConfigError(`${file} declares ${databases.length} \`d1_databases\` entries; dispatch binds exactly one D1 database`);
|
|
8782
|
+
}
|
|
8783
|
+
const binding = databases[0]?.binding;
|
|
8784
|
+
if (typeof binding !== "string" || binding === "") {
|
|
8785
|
+
throw new UnsupportedWranglerConfigError(`${file} has a \`d1_databases\` entry without a binding name`);
|
|
8786
|
+
}
|
|
8787
|
+
return binding;
|
|
8788
|
+
}
|
|
8789
|
+
function unsupportedKey(file, key) {
|
|
8790
|
+
return new UnsupportedWranglerConfigError(`${file} sets \`${key}\`, which dispatch cannot preserve`);
|
|
8791
|
+
}
|
|
8792
|
+
function asArray(file, key, value) {
|
|
8793
|
+
if (!Array.isArray(value))
|
|
8794
|
+
throw new UnsupportedWranglerConfigError(`${file} sets \`${key}\` to something other than a list`);
|
|
8795
|
+
return value;
|
|
8796
|
+
}
|
|
8797
|
+
function readJsonc(file, text) {
|
|
8798
|
+
const stripped = stripOutsideStrings(text, '"', (at) => {
|
|
8799
|
+
if (text.startsWith("//", at))
|
|
8800
|
+
return lineEnd(text, at) - at;
|
|
8801
|
+
if (!text.startsWith("/*", at))
|
|
8802
|
+
return 0;
|
|
8803
|
+
const end = text.indexOf("*/", at + 2);
|
|
8804
|
+
return (end === -1 ? text.length : end + 2) - at;
|
|
8805
|
+
}).replace(/,(\s*[}\]])/g, "$1");
|
|
8806
|
+
let parsed;
|
|
8807
|
+
try {
|
|
8808
|
+
parsed = JSON.parse(stripped);
|
|
8809
|
+
} catch {
|
|
8810
|
+
throw new UnsupportedWranglerConfigError(`${file} could not be read as JSON`);
|
|
8811
|
+
}
|
|
8812
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8813
|
+
throw new UnsupportedWranglerConfigError(`${file} is not a JSON object`);
|
|
8814
|
+
}
|
|
8815
|
+
return parsed;
|
|
8816
|
+
}
|
|
8817
|
+
function readToml(file, text) {
|
|
8818
|
+
const config = {};
|
|
8819
|
+
const databases = [];
|
|
8820
|
+
let table = config;
|
|
8821
|
+
for (const line of logicalTomlLines(text)) {
|
|
8822
|
+
if (line.startsWith("[")) {
|
|
8823
|
+
table = d1Table(file, line);
|
|
8824
|
+
databases.push(table);
|
|
8825
|
+
} else {
|
|
8826
|
+
assign(file, table, line, table === config);
|
|
8827
|
+
}
|
|
8828
|
+
}
|
|
8829
|
+
if (databases.length === 0)
|
|
8830
|
+
return config;
|
|
8831
|
+
if (config.d1_databases !== undefined)
|
|
8832
|
+
throw new UnsupportedWranglerConfigError(`${file} declares d1_databases twice`);
|
|
8833
|
+
return { ...config, d1_databases: databases };
|
|
8834
|
+
}
|
|
8835
|
+
function d1Table(file, line) {
|
|
8836
|
+
const name = /^\[\[?\s*([^\]]*?)\s*\]\]?$/.exec(line)?.[1] ?? line;
|
|
8837
|
+
if (!line.startsWith("[[") || name !== "d1_databases")
|
|
8838
|
+
throw unsupportedKey(file, topKey(name));
|
|
8839
|
+
return {};
|
|
8840
|
+
}
|
|
8841
|
+
function assign(file, table, line, topLevel) {
|
|
8842
|
+
const assignment = /^([\w\-."]+)\s*=\s*(.*)$/.exec(line);
|
|
8843
|
+
if (assignment === null)
|
|
8844
|
+
throw new UnsupportedWranglerConfigError(`${file} could not be read as TOML near \`${line.slice(0, 40)}\``);
|
|
8845
|
+
const key = topKey(assignment[1]);
|
|
8846
|
+
if (topLevel && assignment[1].includes("."))
|
|
8847
|
+
throw unsupportedKey(file, key);
|
|
8848
|
+
table[key] = tomlValue(assignment[2].trim());
|
|
8849
|
+
}
|
|
8850
|
+
function topKey(dotted) {
|
|
8851
|
+
return dotted.split(".")[0].replace(/"/g, "").trim();
|
|
8852
|
+
}
|
|
8853
|
+
function tomlValue(raw) {
|
|
8854
|
+
const quoted = /^"((?:[^"\\]|\\.)*)"$|^'([^']*)'$/.exec(raw);
|
|
8855
|
+
if (quoted !== null)
|
|
8856
|
+
return quoted[1] ?? quoted[2];
|
|
8857
|
+
if (!raw.startsWith("["))
|
|
8858
|
+
return raw;
|
|
8859
|
+
const inner = raw.slice(1, -1);
|
|
8860
|
+
if (inner.includes("{")) {
|
|
8861
|
+
return [...inner.matchAll(/\{([^}]*)\}/g)].map((table) => ({
|
|
8862
|
+
binding: /(?:^|[\s,])binding\s*=\s*["']([^"']*)["']/.exec(table[1])?.[1]
|
|
8863
|
+
}));
|
|
8864
|
+
}
|
|
8865
|
+
return [...inner.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)].map((item) => item[1] ?? item[2]);
|
|
8866
|
+
}
|
|
8867
|
+
function logicalTomlLines(text) {
|
|
8868
|
+
const stripped = stripOutsideStrings(text, `"'`, (at) => text[at] === "#" ? lineEnd(text, at) - at : 0);
|
|
8869
|
+
const lines = [];
|
|
8870
|
+
let pending = "";
|
|
8871
|
+
for (const raw of stripped.split(/\r?\n/)) {
|
|
8872
|
+
pending = `${pending} ${raw.trim()}`.trim();
|
|
8873
|
+
if (pending === "" || openBrackets(pending) > 0)
|
|
8874
|
+
continue;
|
|
8875
|
+
lines.push(pending);
|
|
8876
|
+
pending = "";
|
|
8877
|
+
}
|
|
8878
|
+
if (pending !== "")
|
|
8879
|
+
lines.push(pending);
|
|
8880
|
+
return lines;
|
|
8881
|
+
}
|
|
8882
|
+
function lineEnd(text, at) {
|
|
8883
|
+
const end = text.indexOf(`
|
|
8884
|
+
`, at);
|
|
8885
|
+
return end === -1 ? text.length : end;
|
|
8886
|
+
}
|
|
8887
|
+
function openBrackets(line) {
|
|
8888
|
+
if (/^\[\[?[^\]=]*\]\]?$/.test(line))
|
|
8889
|
+
return 0;
|
|
8890
|
+
let open = 0;
|
|
8891
|
+
stripOutsideStrings(line, `"'`, (at) => {
|
|
8892
|
+
if (line[at] === "[" || line[at] === "{")
|
|
8893
|
+
open++;
|
|
8894
|
+
if (line[at] === "]" || line[at] === "}")
|
|
8895
|
+
open--;
|
|
8896
|
+
return 0;
|
|
8897
|
+
});
|
|
8898
|
+
return open;
|
|
8899
|
+
}
|
|
8900
|
+
function stripOutsideStrings(text, quotes, skip) {
|
|
8901
|
+
let out = "";
|
|
8902
|
+
for (let i = 0;i < text.length; ) {
|
|
8903
|
+
if (quotes.includes(text[i])) {
|
|
8904
|
+
const end = stringEnd(text, i);
|
|
8905
|
+
out += text.slice(i, end);
|
|
8906
|
+
i = end;
|
|
8907
|
+
continue;
|
|
8908
|
+
}
|
|
8909
|
+
const skipped = skip(i);
|
|
8910
|
+
if (skipped === 0)
|
|
8911
|
+
out += text[i];
|
|
8912
|
+
i += Math.max(skipped, 1);
|
|
8913
|
+
}
|
|
8914
|
+
return out;
|
|
8915
|
+
}
|
|
8916
|
+
function stringEnd(text, start) {
|
|
8917
|
+
const quote = text[start];
|
|
8918
|
+
for (let i = start + 1;i < text.length; i++) {
|
|
8919
|
+
if (text[i] === "\\" && quote === '"')
|
|
8920
|
+
i++;
|
|
8921
|
+
else if (text[i] === quote)
|
|
8922
|
+
return i + 1;
|
|
8923
|
+
}
|
|
8924
|
+
return text.length;
|
|
8925
|
+
}
|
|
8686
8926
|
|
|
8687
8927
|
// src/commands/migrate.ts
|
|
8688
8928
|
var USAGE = "usage: launchpad migrate --origin dispatch [--dry-run]";
|
|
8689
8929
|
var migrateCommand = {
|
|
8690
8930
|
name: "migrate",
|
|
8691
|
-
summary: "convert an unprovisioned static or React app to Workers dispatch",
|
|
8931
|
+
summary: "convert an unprovisioned static, React or React API app to Workers dispatch",
|
|
8692
8932
|
run: runMigrate
|
|
8693
8933
|
};
|
|
8694
8934
|
async function runMigrate(args, io) {
|
|
@@ -8711,6 +8951,9 @@ async function runMigrate(args, io) {
|
|
|
8711
8951
|
io.out(`${result.slug}: launchpad.yaml now uses v2 Workers dispatch with gateway authentication.`);
|
|
8712
8952
|
io.out(`Audience, owner and application settings are preserved. Original: ${BACKUP_PATH}`);
|
|
8713
8953
|
io.out("Run `launchpad deploy`. No Pages project is needed.");
|
|
8954
|
+
const next = secretsNextStep(result.secrets);
|
|
8955
|
+
if (next !== null)
|
|
8956
|
+
io.out(next);
|
|
8714
8957
|
}
|
|
8715
8958
|
return 0;
|
|
8716
8959
|
} catch (error) {
|
|
@@ -8719,6 +8962,11 @@ async function runMigrate(args, io) {
|
|
|
8719
8962
|
}
|
|
8720
8963
|
}
|
|
8721
8964
|
var BACKUP_PATH = ".launchpad/manifest-before-dispatch.yaml";
|
|
8965
|
+
function secretsNextStep(secrets) {
|
|
8966
|
+
if (secrets.length === 0)
|
|
8967
|
+
return null;
|
|
8968
|
+
return `Secrets (${secrets.join(", ")}) carry over as secret_text env vars. Once \`launchpad status\` shows the first deploy delivered, run \`launchpad secrets push\` to set them.`;
|
|
8969
|
+
}
|
|
8722
8970
|
async function convertToDispatch(cwd, dryRun) {
|
|
8723
8971
|
const file = path8.join(cwd, "launchpad.yaml");
|
|
8724
8972
|
if (!lstatSync4(file).isFile())
|
|
@@ -8730,18 +8978,38 @@ async function convertToDispatch(cwd, dryRun) {
|
|
|
8730
8978
|
const manifest = loaded.manifest;
|
|
8731
8979
|
if (manifest.apiVersion === "launchpad.m-kopa.us/v2" && manifest.spec.origin === "dispatch")
|
|
8732
8980
|
return { kind: "already" };
|
|
8733
|
-
const
|
|
8734
|
-
|
|
8735
|
-
|
|
8981
|
+
const slug = manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.metadata.slug : manifest.metadata.name;
|
|
8982
|
+
let converted;
|
|
8983
|
+
try {
|
|
8984
|
+
converted = convertLocally(cwd, manifest);
|
|
8985
|
+
} catch (error) {
|
|
8986
|
+
await refuseIfRegistered(slug);
|
|
8987
|
+
throw error;
|
|
8988
|
+
}
|
|
8736
8989
|
await assertUnregistered(slug);
|
|
8990
|
+
const secrets = Object.entries(converted.spec.env_vars ?? {}).filter(([, value]) => value.type === "secret_text").map(([name]) => name);
|
|
8737
8991
|
if (dryRun)
|
|
8738
|
-
return { kind: "eligible", slug };
|
|
8992
|
+
return { kind: "eligible", slug, secrets };
|
|
8739
8993
|
try {
|
|
8740
8994
|
saveMigration(file, original, converted);
|
|
8741
8995
|
} catch (error) {
|
|
8742
8996
|
throw new MigrationSaveError(error);
|
|
8743
8997
|
}
|
|
8744
|
-
return { kind: "converted", slug };
|
|
8998
|
+
return { kind: "converted", slug, secrets };
|
|
8999
|
+
}
|
|
9000
|
+
function convertLocally(cwd, manifest) {
|
|
9001
|
+
const v2 = manifest.apiVersion === "launchpad.m-kopa.us/v2";
|
|
9002
|
+
const spec = v2 ? manifest.spec : manifest;
|
|
9003
|
+
const appType = v2 ? manifest.spec.appType : manifest.deployment.type;
|
|
9004
|
+
return dispatchManifest(manifest, inspectRuntime(cwd, appType, spec.app?.root, spec.build));
|
|
9005
|
+
}
|
|
9006
|
+
async function refuseIfRegistered(slug) {
|
|
9007
|
+
try {
|
|
9008
|
+
await assertUnregistered(slug);
|
|
9009
|
+
} catch (error) {
|
|
9010
|
+
if (error instanceof RegisteredAppError)
|
|
9011
|
+
throw error;
|
|
9012
|
+
}
|
|
8745
9013
|
}
|
|
8746
9014
|
async function assertUnregistered(slug) {
|
|
8747
9015
|
try {
|
|
@@ -8773,23 +9041,54 @@ class RegisteredAppError extends Error {
|
|
|
8773
9041
|
super("This app is already registered. This command only converts apps that have never provisioned; ask the platform team to migrate an existing app.");
|
|
8774
9042
|
}
|
|
8775
9043
|
}
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
const
|
|
8779
|
-
const
|
|
9044
|
+
var WRANGLER_FILES = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"];
|
|
9045
|
+
function inspectRuntime(cwd, appType, appRootDir, build) {
|
|
9046
|
+
const d1 = new Set;
|
|
9047
|
+
for (const root of runtimeRoots(cwd, appRootDir, build)) {
|
|
9048
|
+
assertNoUncompiledCode(root, appType);
|
|
9049
|
+
for (const binding2 of wranglerD1Bindings(cwd, root))
|
|
9050
|
+
d1.add(binding2);
|
|
9051
|
+
}
|
|
9052
|
+
if (d1.size > 1)
|
|
9053
|
+
throw new Error(`The wrangler configs bind D1 under different names (${[...d1].join(", ")}). Use one name.`);
|
|
9054
|
+
const [binding] = d1;
|
|
9055
|
+
if (binding !== undefined && appType !== "react+api") {
|
|
9056
|
+
throw new Error("The wrangler config declares a D1 database, which only react+api apps can use on dispatch.");
|
|
9057
|
+
}
|
|
9058
|
+
return binding;
|
|
9059
|
+
}
|
|
9060
|
+
function runtimeRoots(cwd, appRootDir, build) {
|
|
9061
|
+
const appRoot = path8.resolve(cwd, appRootDir ?? ".");
|
|
9062
|
+
const buildRoot = path8.resolve(appRoot, build?.root_dir ?? ".");
|
|
9063
|
+
return new Set([
|
|
8780
9064
|
cwd,
|
|
8781
9065
|
appRoot,
|
|
8782
9066
|
buildRoot,
|
|
8783
|
-
path8.resolve(cwd,
|
|
8784
|
-
path8.resolve(buildRoot,
|
|
9067
|
+
path8.resolve(cwd, build?.root_dir ?? "."),
|
|
9068
|
+
path8.resolve(buildRoot, build?.destination_dir ?? "dist")
|
|
8785
9069
|
]);
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
9070
|
+
}
|
|
9071
|
+
function assertNoUncompiledCode(root, appType) {
|
|
9072
|
+
const api = appType === "react+api";
|
|
9073
|
+
for (const name of api ? ["_worker.js"] : ["_worker.js", "functions"]) {
|
|
9074
|
+
if (existsSync7(path8.join(root, name))) {
|
|
9075
|
+
throw new Error(`${name} needs a runtime review before conversion. Pages Functions and custom Worker code must not be dropped. Ask the platform team to review this app.`);
|
|
8791
9076
|
}
|
|
8792
9077
|
}
|
|
9078
|
+
if (api)
|
|
9079
|
+
assertWorkerRoutesFit(root);
|
|
9080
|
+
}
|
|
9081
|
+
function wranglerD1Bindings(cwd, root) {
|
|
9082
|
+
return WRANGLER_FILES.map((name) => path8.join(root, name)).filter((file) => existsSync7(file)).map((file) => wranglerD1Binding(path8.relative(cwd, file), readFileSync11(file, "utf8"))).filter((binding) => binding !== undefined);
|
|
9083
|
+
}
|
|
9084
|
+
function assertWorkerRoutesFit(root) {
|
|
9085
|
+
const functions = path8.join(root, "functions");
|
|
9086
|
+
if (!existsSync7(functions) || !lstatSync4(functions).isDirectory())
|
|
9087
|
+
return;
|
|
9088
|
+
const catchAll = readdirSync2(functions).find((name) => /^_middleware\.[cm]?[jt]sx?$/.test(name) || name.startsWith("["));
|
|
9089
|
+
if (catchAll !== undefined) {
|
|
9090
|
+
throw new Error(`functions/${catchAll} would run the Worker for every path, including static assets, which dispatch does not support. Move it under a prefix such as functions/api/.`);
|
|
9091
|
+
}
|
|
8793
9092
|
}
|
|
8794
9093
|
function saveMigration(file, original, converted) {
|
|
8795
9094
|
if (readFileSync11(file, "utf8") !== original)
|
|
@@ -9602,24 +9901,12 @@ function isIneligible(e) {
|
|
|
9602
9901
|
return !(e instanceof RegistryUnavailableError || e instanceof MigrationSaveError || e instanceof UpgradeRequiredError || e instanceof UnauthenticatedError);
|
|
9603
9902
|
}
|
|
9604
9903
|
async function defaultToDispatch(cwd, manifestPath, io) {
|
|
9605
|
-
|
|
9606
|
-
try {
|
|
9607
|
-
raw = parseYaml6(readFileSync12(manifestPath, "utf8"));
|
|
9608
|
-
} catch {
|
|
9609
|
-
return null;
|
|
9610
|
-
}
|
|
9611
|
-
const isV22 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
|
|
9612
|
-
const appType = isV22 ? raw?.spec?.appType : raw?.deployment?.type;
|
|
9613
|
-
const origin = isV22 ? raw?.spec?.origin : raw?.origin;
|
|
9614
|
-
const auth = isV22 ? raw?.spec?.auth : raw?.auth;
|
|
9615
|
-
if (appType !== "static" && appType !== "react" || origin !== undefined || auth !== "gateway")
|
|
9904
|
+
if (!impliesPagesForGateway(manifestPath))
|
|
9616
9905
|
return null;
|
|
9617
9906
|
try {
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
io
|
|
9621
|
-
io.out(" To stay on Pages instead, set `spec.origin: pages` and deploy again.");
|
|
9622
|
-
}
|
|
9907
|
+
const result = await convertToDispatch(cwd, false);
|
|
9908
|
+
if (result.kind === "converted")
|
|
9909
|
+
reportConversion(io, result.secrets);
|
|
9623
9910
|
return null;
|
|
9624
9911
|
} catch (e) {
|
|
9625
9912
|
if (e instanceof RegisteredAppError)
|
|
@@ -9629,6 +9916,30 @@ async function defaultToDispatch(cwd, manifestPath, io) {
|
|
|
9629
9916
|
return `Not converted to Workers dispatch: ${describe17(e)}`;
|
|
9630
9917
|
}
|
|
9631
9918
|
}
|
|
9919
|
+
var CONVERTIBLE_APP_TYPES = new Set(["static", "react", "react+api"]);
|
|
9920
|
+
function impliesPagesForGateway(manifestPath) {
|
|
9921
|
+
const fields = originFields(manifestPath);
|
|
9922
|
+
return CONVERTIBLE_APP_TYPES.has(fields.appType) && fields.origin === undefined && fields.auth === "gateway";
|
|
9923
|
+
}
|
|
9924
|
+
function originFields(manifestPath) {
|
|
9925
|
+
let raw;
|
|
9926
|
+
try {
|
|
9927
|
+
raw = parseYaml6(readFileSync12(manifestPath, "utf8")) ?? {};
|
|
9928
|
+
} catch {
|
|
9929
|
+
return {};
|
|
9930
|
+
}
|
|
9931
|
+
if (raw.apiVersion === "launchpad.m-kopa.us/v2")
|
|
9932
|
+
return raw.spec ?? {};
|
|
9933
|
+
return { origin: raw.origin, auth: raw.auth, appType: raw.deployment?.type };
|
|
9934
|
+
}
|
|
9935
|
+
function reportConversion(io, secrets) {
|
|
9936
|
+
io.out("New apps deploy on Workers dispatch by default, so launchpad.yaml was converted (no Pages project needed).");
|
|
9937
|
+
io.out(` Audience, owner and build settings are preserved. Original: ${BACKUP_PATH}`);
|
|
9938
|
+
io.out(` To stay on Pages instead, restore ${BACKUP_PATH} as launchpad.yaml, set \`origin: pages\` (v1) or \`spec.origin: pages\` (v2) and deploy again.`);
|
|
9939
|
+
const next = secretsNextStep(secrets);
|
|
9940
|
+
if (next !== null)
|
|
9941
|
+
io.out(` ${next}`);
|
|
9942
|
+
}
|
|
9632
9943
|
function declaredOriginOf(isV22, origin) {
|
|
9633
9944
|
if (!isV22)
|
|
9634
9945
|
return "pages";
|
|
@@ -11084,10 +11395,6 @@ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(),
|
|
|
11084
11395
|
return 64;
|
|
11085
11396
|
}
|
|
11086
11397
|
const dispatch = newAppOrigin(fillResult.inputs.type, inputs.origin) === "dispatch";
|
|
11087
|
-
if (dispatch && fillResult.inputs.type === "react+api") {
|
|
11088
|
-
io.err("launchpad init: API starter dispatch is not available yet; use --origin pages");
|
|
11089
|
-
return 64;
|
|
11090
|
-
}
|
|
11091
11398
|
if (dispatch && (inputs.hostnames.length > 0 || inputs.sessionDuration !== undefined)) {
|
|
11092
11399
|
io.err("launchpad init: dispatch does not support --hostname or --session-duration; use --origin pages for a Pages manifest");
|
|
11093
11400
|
return 64;
|
|
@@ -11159,11 +11466,10 @@ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(),
|
|
|
11159
11466
|
io.out("");
|
|
11160
11467
|
io.out("Next steps:");
|
|
11161
11468
|
io.out(" launchpad validate # confirm the manifest");
|
|
11162
|
-
|
|
11163
|
-
io.out(" launchpad secrets template # scaffold .env.example");
|
|
11469
|
+
io.out(" launchpad secrets template # scaffold .env.example");
|
|
11164
11470
|
io.out(" launchpad deploy # build and deploy the app");
|
|
11165
11471
|
if (dispatch)
|
|
11166
|
-
io.out("
|
|
11472
|
+
io.out(" Push secrets with `launchpad secrets push` once the first deploy has been delivered.");
|
|
11167
11473
|
return 0;
|
|
11168
11474
|
}
|
|
11169
11475
|
function parseArgs6(args) {
|
|
@@ -11215,11 +11521,11 @@ function parseArgs6(args) {
|
|
|
11215
11521
|
i++;
|
|
11216
11522
|
return next;
|
|
11217
11523
|
};
|
|
11218
|
-
const setFromNext = (
|
|
11524
|
+
const setFromNext = (assign2) => {
|
|
11219
11525
|
const got = needsValue();
|
|
11220
11526
|
if (typeof got !== "string")
|
|
11221
11527
|
return got;
|
|
11222
|
-
|
|
11528
|
+
assign2(got);
|
|
11223
11529
|
return;
|
|
11224
11530
|
};
|
|
11225
11531
|
if (a === "--name") {
|
|
@@ -12096,6 +12402,20 @@ function describe25(e) {
|
|
|
12096
12402
|
|
|
12097
12403
|
// src/commands/plan.ts
|
|
12098
12404
|
import { resolve as resolve12 } from "node:path";
|
|
12405
|
+
|
|
12406
|
+
// src/manifest/container-runtime-warnings.ts
|
|
12407
|
+
var CONTAINER_RUNTIME_GATE_WARNING = "Container apps are gated by the platform runtime switch; `launchpad deploy` may return " + "503 container_runtime_off until the platform team enables your app. See docs/concepts/containers.md.";
|
|
12408
|
+
var RUNTIME_ACA_RESERVED_WARNING = "deployment.runtime: aca is reserved and currently ignored — the app runs on cloudflare-containers " + "regardless. See docs/concepts/containers.md.";
|
|
12409
|
+
function containerRuntimeWarnings(view) {
|
|
12410
|
+
if (view.deploymentType !== "container")
|
|
12411
|
+
return [];
|
|
12412
|
+
const warnings = [CONTAINER_RUNTIME_GATE_WARNING];
|
|
12413
|
+
if (view.runtime === "aca")
|
|
12414
|
+
warnings.push(RUNTIME_ACA_RESERVED_WARNING);
|
|
12415
|
+
return warnings;
|
|
12416
|
+
}
|
|
12417
|
+
|
|
12418
|
+
// src/commands/plan.ts
|
|
12099
12419
|
var planCommand = {
|
|
12100
12420
|
name: "plan",
|
|
12101
12421
|
summary: "summarise what the manifest would deploy (offline)",
|
|
@@ -12127,7 +12447,15 @@ function renderJson(result, io) {
|
|
|
12127
12447
|
switch (result.kind) {
|
|
12128
12448
|
case "ok": {
|
|
12129
12449
|
const plan = derivePlan(result.manifest);
|
|
12130
|
-
|
|
12450
|
+
const warnings = containerRuntimeWarnings({
|
|
12451
|
+
deploymentType: plan.desired.app.deploymentType,
|
|
12452
|
+
runtime: plan.desired.app.runtime
|
|
12453
|
+
});
|
|
12454
|
+
io.out(JSON.stringify({
|
|
12455
|
+
ok: true,
|
|
12456
|
+
...plan,
|
|
12457
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
12458
|
+
}));
|
|
12131
12459
|
return 0;
|
|
12132
12460
|
}
|
|
12133
12461
|
case "file-not-found":
|
|
@@ -12237,6 +12565,12 @@ function renderPlan(m, io) {
|
|
|
12237
12565
|
}
|
|
12238
12566
|
io.out("");
|
|
12239
12567
|
}
|
|
12568
|
+
for (const w of containerRuntimeWarnings({
|
|
12569
|
+
deploymentType: desired.app.deploymentType,
|
|
12570
|
+
runtime: desired.app.runtime
|
|
12571
|
+
})) {
|
|
12572
|
+
io.out(`⚠ ${w}`);
|
|
12573
|
+
}
|
|
12240
12574
|
const totalBindings = desired.secrets.envFile.length + desired.secrets.platformManaged.length;
|
|
12241
12575
|
io.out(`Secrets (${totalBindings})`);
|
|
12242
12576
|
if (totalBindings === 0) {
|
|
@@ -13974,6 +14308,22 @@ function parseEnv(text) {
|
|
|
13974
14308
|
return { entries, invalidNames, malformedLines };
|
|
13975
14309
|
}
|
|
13976
14310
|
|
|
14311
|
+
// src/secrets/declared.ts
|
|
14312
|
+
function declaredSecrets(loaded) {
|
|
14313
|
+
if (loaded.schema === "launchpad.m-kopa.us/v1alpha1") {
|
|
14314
|
+
const m2 = loaded.manifest;
|
|
14315
|
+
return { slug: m2.metadata.name, apiVersion: m2.apiVersion, bindings: m2.secrets?.bindings ?? [] };
|
|
14316
|
+
}
|
|
14317
|
+
const m = loaded.manifest;
|
|
14318
|
+
const envVars = m.spec.appType === "container" ? {} : m.spec.env_vars ?? {};
|
|
14319
|
+
const fromEnvVars = Object.entries(envVars).filter(([, v]) => v.type === "secret_text").map(([name]) => ({ name, source: "env-file" }));
|
|
14320
|
+
return {
|
|
14321
|
+
slug: m.metadata.slug,
|
|
14322
|
+
apiVersion: m.apiVersion,
|
|
14323
|
+
bindings: [...m.spec.secrets?.bindings ?? [], ...fromEnvVars]
|
|
14324
|
+
};
|
|
14325
|
+
}
|
|
14326
|
+
|
|
13977
14327
|
// src/secrets/push.ts
|
|
13978
14328
|
async function runSecretsPush(opts, io, deps = {}) {
|
|
13979
14329
|
const cfg = deps.config ?? loadConfig();
|
|
@@ -13983,14 +14333,10 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
13983
14333
|
if (loaded.kind !== "ok") {
|
|
13984
14334
|
return renderManifestError4(loaded, io);
|
|
13985
14335
|
}
|
|
13986
|
-
const
|
|
13987
|
-
if (v2Refusal !== null)
|
|
13988
|
-
return v2Refusal;
|
|
13989
|
-
const manifest = loaded.manifest;
|
|
13990
|
-
const bindings = manifest.secrets?.bindings ?? [];
|
|
14336
|
+
const { slug, bindings } = declaredSecrets(loaded);
|
|
13991
14337
|
const envFileBindings = bindings.filter((b) => b.source === "env-file");
|
|
13992
14338
|
if (envFileBindings.length === 0) {
|
|
13993
|
-
io.out("No `env-file`
|
|
14339
|
+
io.out("No secrets to push: the manifest declares no `env-file` bindings or `secret_text` env vars.");
|
|
13994
14340
|
io.out("(platform-managed bindings are pushed via the platform-team ops path.)");
|
|
13995
14341
|
return 0;
|
|
13996
14342
|
}
|
|
@@ -14069,11 +14415,11 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
14069
14415
|
io.out("Re-run without --dry-run to push.");
|
|
14070
14416
|
return 0;
|
|
14071
14417
|
}
|
|
14072
|
-
const slug = manifest.metadata.name;
|
|
14073
14418
|
let pushed = 0;
|
|
14074
14419
|
let partial = 0;
|
|
14075
14420
|
let anyPagesUpdated = false;
|
|
14076
14421
|
let anyWorkerUpdated = false;
|
|
14422
|
+
let anyDispatchUpdated = false;
|
|
14077
14423
|
for (const b of envFileBindings) {
|
|
14078
14424
|
const value = parsed.entries.get(b.name);
|
|
14079
14425
|
io.out(`Pushing ${b.name} …`);
|
|
@@ -14094,6 +14440,11 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
14094
14440
|
io.err(`✗ secrets push ${b.name}: invalid bot response (results must be an array)`);
|
|
14095
14441
|
return 2;
|
|
14096
14442
|
}
|
|
14443
|
+
if (resp.error === "dispatch_script_not_delivered") {
|
|
14444
|
+
io.err(`✗ secrets push ${b.name}: ${resp.message ?? "the dispatch script is not delivered yet"}`);
|
|
14445
|
+
io.err(`${pushed} of ${envFileBindings.length} pushed before failure.`);
|
|
14446
|
+
return 1;
|
|
14447
|
+
}
|
|
14097
14448
|
const results = resp.results ?? [];
|
|
14098
14449
|
for (const r of results) {
|
|
14099
14450
|
if (r.status !== "updated")
|
|
@@ -14102,6 +14453,8 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
14102
14453
|
anyPagesUpdated = true;
|
|
14103
14454
|
else if (r.kind === "worker")
|
|
14104
14455
|
anyWorkerUpdated = true;
|
|
14456
|
+
else if (r.kind === "dispatch")
|
|
14457
|
+
anyDispatchUpdated = true;
|
|
14105
14458
|
}
|
|
14106
14459
|
const notUpdated = results.filter((r) => r.status !== "updated");
|
|
14107
14460
|
if (resp.updated === true && notUpdated.length === 0) {
|
|
@@ -14125,7 +14478,9 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
14125
14478
|
if (anyPagesUpdated) {
|
|
14126
14479
|
return offerRedeploy(slug, opts, io, deps);
|
|
14127
14480
|
}
|
|
14128
|
-
if (
|
|
14481
|
+
if (anyDispatchUpdated) {
|
|
14482
|
+
io.out("Set on the app's dispatch Worker and live now - no redeploy needed.");
|
|
14483
|
+
} else if (anyWorkerUpdated) {
|
|
14129
14484
|
io.out("These secrets bind on the app's next invocation — no redeploy needed.");
|
|
14130
14485
|
}
|
|
14131
14486
|
return 0;
|
|
@@ -14194,20 +14549,6 @@ function mapPushError(e, slug, name, io) {
|
|
|
14194
14549
|
function describe30(e) {
|
|
14195
14550
|
return e instanceof Error ? e.message : String(e);
|
|
14196
14551
|
}
|
|
14197
|
-
function refuseV2Manifest(loaded, command, io) {
|
|
14198
|
-
if (loaded.schema !== "launchpad.m-kopa.us/v2")
|
|
14199
|
-
return null;
|
|
14200
|
-
const origin = loaded.manifest.spec?.origin;
|
|
14201
|
-
if (origin === "dispatch") {
|
|
14202
|
-
io.err(`${command}: this is a dispatch-origin (v2) app.`);
|
|
14203
|
-
const outcome = command.endsWith("push") ? "nothing was pushed" : "nothing was read";
|
|
14204
|
-
io.err(` Secrets are not delivered to the dispatch namespace yet (PS-2740); ${outcome}.`);
|
|
14205
|
-
return 1;
|
|
14206
|
-
}
|
|
14207
|
-
io.err(`${command}: this is a launchpad.m-kopa.us/v2 manifest.`);
|
|
14208
|
-
io.err(" This command reads v1alpha1 `secrets.bindings`; v2 declares secrets as `spec.env_vars` `secret_text` entries, which it does not push yet.");
|
|
14209
|
-
return 1;
|
|
14210
|
-
}
|
|
14211
14552
|
function renderManifestError4(result, io) {
|
|
14212
14553
|
switch (result.kind) {
|
|
14213
14554
|
case "ok":
|
|
@@ -14248,17 +14589,12 @@ async function runSecretsStatus(opts, io, deps = {}) {
|
|
|
14248
14589
|
const loaded = loadManifestAnyVersion(manifestPath);
|
|
14249
14590
|
if (loaded.kind !== "ok")
|
|
14250
14591
|
return renderManifestError5(loaded, io);
|
|
14251
|
-
const
|
|
14252
|
-
if (v2Refusal !== null)
|
|
14253
|
-
return v2Refusal;
|
|
14254
|
-
const manifest = loaded.manifest;
|
|
14255
|
-
const slug = manifest.metadata.name;
|
|
14592
|
+
const { slug, bindings } = declaredSecrets(loaded);
|
|
14256
14593
|
if (opts.slug != null && opts.slug !== slug) {
|
|
14257
14594
|
io.err(`launchpad secrets status: slug "${opts.slug}" does not match this directory's ` + `manifest (metadata.name: "${slug}").`);
|
|
14258
14595
|
io.err(" secrets status audits the manifest in the current directory — omit the slug, " + "or run from the app's directory.");
|
|
14259
14596
|
return 2;
|
|
14260
14597
|
}
|
|
14261
|
-
const bindings = manifest.secrets?.bindings ?? [];
|
|
14262
14598
|
if (bindings.length === 0) {
|
|
14263
14599
|
io.out("No secret bindings declared in the manifest.");
|
|
14264
14600
|
return 0;
|
|
@@ -14608,12 +14944,12 @@ async function runSecretsTemplate(args, io) {
|
|
|
14608
14944
|
return 64;
|
|
14609
14945
|
}
|
|
14610
14946
|
const manifestPath = resolve14(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
14611
|
-
const
|
|
14612
|
-
const renderResult = renderManifest(result, io);
|
|
14947
|
+
const renderResult = renderManifest(loadManifestAnyVersion(manifestPath), io);
|
|
14613
14948
|
if (renderResult.kind !== "ok") {
|
|
14614
14949
|
return renderResult.code;
|
|
14615
14950
|
}
|
|
14616
|
-
const
|
|
14951
|
+
const declared = renderResult.declared;
|
|
14952
|
+
const body = buildTemplate(declared, flags.includePlatformManaged);
|
|
14617
14953
|
if (flags.toStdout) {
|
|
14618
14954
|
for (const line of body.split(`
|
|
14619
14955
|
`)) {
|
|
@@ -14636,14 +14972,14 @@ async function runSecretsTemplate(args, io) {
|
|
|
14636
14972
|
return 2;
|
|
14637
14973
|
}
|
|
14638
14974
|
io.out(`✓ wrote ${outPath}`);
|
|
14639
|
-
const
|
|
14640
|
-
io.out(` ${
|
|
14975
|
+
const written = declared.bindings.filter((b) => b.source === "env-file").length;
|
|
14976
|
+
io.out(` ${written} binding${written === 1 ? "" : "s"} scaffolded`);
|
|
14641
14977
|
return 0;
|
|
14642
14978
|
}
|
|
14643
14979
|
function renderManifest(result, io) {
|
|
14644
14980
|
switch (result.kind) {
|
|
14645
14981
|
case "ok":
|
|
14646
|
-
return { kind: "ok",
|
|
14982
|
+
return { kind: "ok", declared: declaredSecrets(result) };
|
|
14647
14983
|
case "file-not-found":
|
|
14648
14984
|
io.err(`launchpad secrets template: ${result.path}`);
|
|
14649
14985
|
io.err(" manifest not found");
|
|
@@ -14665,19 +15001,19 @@ function renderManifest(result, io) {
|
|
|
14665
15001
|
return { kind: "fail", code: 1 };
|
|
14666
15002
|
}
|
|
14667
15003
|
}
|
|
14668
|
-
function buildTemplate(
|
|
14669
|
-
const bindings =
|
|
15004
|
+
function buildTemplate(declared, includePlatformManaged) {
|
|
15005
|
+
const { bindings } = declared;
|
|
14670
15006
|
const lines = [];
|
|
14671
15007
|
lines.push("# Auto-generated by `launchpad secrets template` — do not commit values.");
|
|
14672
|
-
lines.push(`# Source: launchpad.yaml (apiVersion: ${
|
|
14673
|
-
lines.push(`# App: ${
|
|
15008
|
+
lines.push(`# Source: launchpad.yaml (apiVersion: ${declared.apiVersion})`);
|
|
15009
|
+
lines.push(`# App: ${declared.slug}`);
|
|
14674
15010
|
lines.push("#");
|
|
14675
15011
|
lines.push("# Fill in values, then run `launchpad secrets push`.");
|
|
14676
15012
|
lines.push("# This file is meant to be committed; the real `.env` must be gitignored.");
|
|
14677
15013
|
lines.push("");
|
|
14678
15014
|
if (bindings.length === 0) {
|
|
14679
15015
|
lines.push("# No secret bindings declared in launchpad.yaml.");
|
|
14680
|
-
lines.push("# Add entries under `secrets.bindings[]` and re-run this command.");
|
|
15016
|
+
lines.push("# Add entries under `secrets.bindings[]` (v1alpha1) or `spec.env_vars` with " + "`type: secret_text` (v2) and re-run this command.");
|
|
14681
15017
|
lines.push("");
|
|
14682
15018
|
return lines.join(`
|
|
14683
15019
|
`);
|
|
@@ -14711,11 +15047,6 @@ function buildTemplate(manifest, includePlatformManaged) {
|
|
|
14711
15047
|
return lines.join(`
|
|
14712
15048
|
`);
|
|
14713
15049
|
}
|
|
14714
|
-
function countBindings(manifest) {
|
|
14715
|
-
const bindings = manifest.secrets?.bindings ?? [];
|
|
14716
|
-
const written = bindings.filter((b) => b.source === "env-file").length;
|
|
14717
|
-
return { total: bindings.length, written };
|
|
14718
|
-
}
|
|
14719
15050
|
function parseFlags4(args) {
|
|
14720
15051
|
let file;
|
|
14721
15052
|
let out = ".env.example";
|
|
@@ -14924,7 +15255,7 @@ import { promises as fs7, existsSync as existsSync16 } from "node:fs";
|
|
|
14924
15255
|
// src/skills-bundle.ts
|
|
14925
15256
|
import { homedir as homedir2 } from "node:os";
|
|
14926
15257
|
import { join as join15 } from "node:path";
|
|
14927
|
-
import { readFileSync as readFileSync20, readdirSync as
|
|
15258
|
+
import { readFileSync as readFileSync20, readdirSync as readdirSync3 } from "node:fs";
|
|
14928
15259
|
var SKILL_PREFIX = "launchpad-";
|
|
14929
15260
|
function skillsTargetDir() {
|
|
14930
15261
|
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir2(), ".claude", "skills");
|
|
@@ -14932,7 +15263,7 @@ function skillsTargetDir() {
|
|
|
14932
15263
|
function readInstalledSkills() {
|
|
14933
15264
|
try {
|
|
14934
15265
|
const dir = skillsTargetDir();
|
|
14935
|
-
const bundle =
|
|
15266
|
+
const bundle = readdirSync3(dir, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.startsWith(SKILL_PREFIX));
|
|
14936
15267
|
if (!bundle)
|
|
14937
15268
|
return { present: false, version: null };
|
|
14938
15269
|
return {
|
|
@@ -15622,7 +15953,7 @@ function printHelp5(io) {
|
|
|
15622
15953
|
}
|
|
15623
15954
|
|
|
15624
15955
|
// src/commands/validate.ts
|
|
15625
|
-
import { existsSync as existsSync17, readFileSync as readFileSync22, readdirSync as
|
|
15956
|
+
import { existsSync as existsSync17, readFileSync as readFileSync22, readdirSync as readdirSync4, statSync as statSync2 } from "node:fs";
|
|
15626
15957
|
import { dirname as dirname12, resolve as resolve17 } from "node:path";
|
|
15627
15958
|
var validateCommand = {
|
|
15628
15959
|
name: "validate",
|
|
@@ -15713,7 +16044,7 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
15713
16044
|
}
|
|
15714
16045
|
let entries;
|
|
15715
16046
|
try {
|
|
15716
|
-
entries =
|
|
16047
|
+
entries = readdirSync4(abs);
|
|
15717
16048
|
} catch (e) {
|
|
15718
16049
|
return [`failed to read static served directory '${serveDir}/': ${describe32(e)}`];
|
|
15719
16050
|
}
|
|
@@ -15803,6 +16134,9 @@ function renderHumanOk(result, group, boundary, io) {
|
|
|
15803
16134
|
if (!boundary.declared) {
|
|
15804
16135
|
io.out(`⚠ ${DECLARE_APP_HINT}`);
|
|
15805
16136
|
}
|
|
16137
|
+
for (const w of containerRuntimeWarnings(view)) {
|
|
16138
|
+
io.out(`⚠ ${w}`);
|
|
16139
|
+
}
|
|
15806
16140
|
switch (group.kind) {
|
|
15807
16141
|
case "skipped":
|
|
15808
16142
|
return 0;
|
|
@@ -15869,6 +16203,7 @@ function renderHumanError(result, io) {
|
|
|
15869
16203
|
}
|
|
15870
16204
|
function renderJsonOk(result, group, boundary, io) {
|
|
15871
16205
|
const view = manifestView(result.manifest);
|
|
16206
|
+
const runtimeWarnings = containerRuntimeWarnings(view);
|
|
15872
16207
|
const base = {
|
|
15873
16208
|
manifest_valid: true,
|
|
15874
16209
|
runtime_supported: true,
|
|
@@ -15878,7 +16213,8 @@ function renderJsonOk(result, group, boundary, io) {
|
|
|
15878
16213
|
declared: boundary.declared,
|
|
15879
16214
|
errors: boundary.errors,
|
|
15880
16215
|
warnings: boundary.warnings
|
|
15881
|
-
}
|
|
16216
|
+
},
|
|
16217
|
+
...runtimeWarnings.length > 0 ? { warnings: runtimeWarnings } : {}
|
|
15882
16218
|
};
|
|
15883
16219
|
if (boundary.errors.length > 0) {
|
|
15884
16220
|
io.out(JSON.stringify({ ok: false, ...base }));
|
|
@@ -15967,7 +16303,8 @@ function manifestView(manifest) {
|
|
|
15967
16303
|
access: manifest.spec.access,
|
|
15968
16304
|
buildCommand: manifest.spec.build?.command,
|
|
15969
16305
|
destinationDir: manifest.spec.build?.destination_dir,
|
|
15970
|
-
appDeclared: manifest.spec.app !== undefined
|
|
16306
|
+
appDeclared: manifest.spec.app !== undefined,
|
|
16307
|
+
runtime: undefined
|
|
15971
16308
|
};
|
|
15972
16309
|
}
|
|
15973
16310
|
return {
|
|
@@ -15976,7 +16313,8 @@ function manifestView(manifest) {
|
|
|
15976
16313
|
access: manifest.access,
|
|
15977
16314
|
buildCommand: manifest.build?.command,
|
|
15978
16315
|
destinationDir: manifest.build?.destination_dir,
|
|
15979
|
-
appDeclared: manifest.app !== undefined
|
|
16316
|
+
appDeclared: manifest.app !== undefined,
|
|
16317
|
+
runtime: manifest.deployment.runtime
|
|
15980
16318
|
};
|
|
15981
16319
|
}
|
|
15982
16320
|
function parseArgs13(args) {
|