@m-kopa/launchpad-cli 0.39.0 → 0.41.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 +84 -0
- package/dist/cli.js +400 -159
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/status.d.ts +8 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/deploy/manifest-state.d.ts +7 -0
- package/dist/deploy/manifest-state.d.ts.map +1 -1
- package/dist/http/api-client.d.ts.map +1 -1
- package/dist/report/classify-fault.d.ts +28 -0
- package/dist/report/classify-fault.d.ts.map +1 -0
- package/dist/report/fault-signal.d.ts +23 -0
- package/dist/report/fault-signal.d.ts.map +1 -0
- package/dist/report/report-nudge.d.ts +33 -0
- package/dist/report/report-nudge.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +3 -1
- package/skills/launchpad-content-pr/SKILL.md +6 -1
- package/skills/launchpad-deploy/SKILL.md +26 -1
- package/skills/launchpad-deploy-status/SKILL.md +14 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-identity/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-report/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +10 -1
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
19
19
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
20
|
|
|
21
21
|
// src/version.ts
|
|
22
|
-
var CLI_VERSION = "0.
|
|
22
|
+
var CLI_VERSION = "0.41.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
@@ -75,6 +75,28 @@ class TransportError extends Error {
|
|
|
75
75
|
code = "transport_error";
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// src/report/fault-signal.ts
|
|
79
|
+
var last = null;
|
|
80
|
+
function recordFault(err) {
|
|
81
|
+
if (err === null || typeof err !== "object")
|
|
82
|
+
return;
|
|
83
|
+
const code = err.code;
|
|
84
|
+
if (typeof code !== "string")
|
|
85
|
+
return;
|
|
86
|
+
const status = err.status;
|
|
87
|
+
last = typeof status === "number" ? { code, status } : { code };
|
|
88
|
+
}
|
|
89
|
+
function recordedFault(err) {
|
|
90
|
+
recordFault(err);
|
|
91
|
+
return err;
|
|
92
|
+
}
|
|
93
|
+
function clearFault() {
|
|
94
|
+
last = null;
|
|
95
|
+
}
|
|
96
|
+
function peekFault() {
|
|
97
|
+
return last;
|
|
98
|
+
}
|
|
99
|
+
|
|
78
100
|
// src/auth/flow.ts
|
|
79
101
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
80
102
|
|
|
@@ -838,7 +860,7 @@ async function apiJson(cfg, opts, fetcher = fetch) {
|
|
|
838
860
|
try {
|
|
839
861
|
parsed = await res.json();
|
|
840
862
|
} catch (e) {
|
|
841
|
-
throw new TransportError(`bot returned non-JSON body for ${opts.path}: ${describe7(e)}`);
|
|
863
|
+
throw recordedFault(new TransportError(`bot returned non-JSON body for ${opts.path}: ${describe7(e)}`));
|
|
842
864
|
}
|
|
843
865
|
return parsed;
|
|
844
866
|
}
|
|
@@ -880,27 +902,28 @@ async function apiRaw(cfg, opts, fetcher = fetch) {
|
|
|
880
902
|
try {
|
|
881
903
|
res = await fetcher(url, init);
|
|
882
904
|
} catch (e) {
|
|
883
|
-
throw new TransportError(`network error calling ${url}: ${describe7(e)}`);
|
|
905
|
+
throw recordedFault(new TransportError(`network error calling ${url}: ${describe7(e)}`));
|
|
884
906
|
}
|
|
885
907
|
if (res.status === 401) {
|
|
886
908
|
const detail = await peek(res);
|
|
887
|
-
throw new UnauthenticatedError(`bot returned 401 for ${opts.path}: ${detail} — run \`launchpad login\``);
|
|
909
|
+
throw recordedFault(new UnauthenticatedError(`bot returned 401 for ${opts.path}: ${detail} — run \`launchpad login\``));
|
|
888
910
|
}
|
|
889
911
|
if (res.status === 403) {
|
|
890
912
|
const detail = await peek(res);
|
|
891
|
-
throw new ForbiddenError(`bot returned 403 for ${opts.path}: ${detail}`);
|
|
913
|
+
throw recordedFault(new ForbiddenError(`bot returned 403 for ${opts.path}: ${detail}`));
|
|
892
914
|
}
|
|
893
915
|
if (res.status === 404) {
|
|
894
916
|
const detail = await peek(res);
|
|
895
|
-
throw new NotFoundError(`bot returned 404 for ${opts.path}: ${detail}`);
|
|
917
|
+
throw recordedFault(new NotFoundError(`bot returned 404 for ${opts.path}: ${detail}`));
|
|
896
918
|
}
|
|
897
919
|
if (!res.ok) {
|
|
898
920
|
if (opts.nonThrowingStatuses?.includes(res.status) === true) {
|
|
899
921
|
return res;
|
|
900
922
|
}
|
|
901
923
|
const detail = await peek(res);
|
|
902
|
-
throw new ApiError(`bot returned HTTP ${res.status} for ${opts.path}: ${detail}`, res.status);
|
|
924
|
+
throw recordedFault(new ApiError(`bot returned HTTP ${res.status} for ${opts.path}: ${detail}`, res.status));
|
|
903
925
|
}
|
|
926
|
+
clearFault();
|
|
904
927
|
return res;
|
|
905
928
|
}
|
|
906
929
|
async function peek(res) {
|
|
@@ -2183,6 +2206,9 @@ var AppBoundarySchema = z.object({
|
|
|
2183
2206
|
exclude: z.array(z.string().min(1)).optional()
|
|
2184
2207
|
}).strict();
|
|
2185
2208
|
var ProductionEnvSchema = z.record(z.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z.string());
|
|
2209
|
+
var VerifySchema = z.object({
|
|
2210
|
+
protected_path: z.string().min(1).regex(/^\//, "verify.protected_path must be a root-relative path beginning with '/'")
|
|
2211
|
+
}).strict();
|
|
2186
2212
|
var ManifestSchema = z.object({
|
|
2187
2213
|
apiVersion: z.literal("launchpad.m-kopa.us/v1alpha1"),
|
|
2188
2214
|
kind: z.literal("App"),
|
|
@@ -2196,9 +2222,33 @@ var ManifestSchema = z.object({
|
|
|
2196
2222
|
targets: z.array(TargetSchema).optional(),
|
|
2197
2223
|
build: BuildSchema.optional(),
|
|
2198
2224
|
production_env: ProductionEnvSchema.optional(),
|
|
2199
|
-
app: AppBoundarySchema.optional()
|
|
2225
|
+
app: AppBoundarySchema.optional(),
|
|
2226
|
+
verify: VerifySchema.optional(),
|
|
2227
|
+
confine_origin: z.boolean().optional(),
|
|
2228
|
+
catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional()
|
|
2200
2229
|
}).strict().superRefine((m, ctx) => {
|
|
2201
2230
|
const isContainer = m.deployment.type === "container";
|
|
2231
|
+
if (m.confine_origin !== undefined) {
|
|
2232
|
+
if (isContainer) {
|
|
2233
|
+
ctx.addIssue({
|
|
2234
|
+
code: z.ZodIssueCode.custom,
|
|
2235
|
+
path: ["confine_origin"],
|
|
2236
|
+
message: "confine_origin is only valid for gateway pages apps (static/react/react+api); container apps have no pages.dev surface."
|
|
2237
|
+
});
|
|
2238
|
+
} else if (m.auth === "access") {
|
|
2239
|
+
ctx.addIssue({
|
|
2240
|
+
code: z.ZodIssueCode.custom,
|
|
2241
|
+
path: ["confine_origin"],
|
|
2242
|
+
message: "confine_origin applies to gateway-fronted apps (auth: gateway); it has no meaning under Cloudflare Access."
|
|
2243
|
+
});
|
|
2244
|
+
} else if (m.deployment.type === "static" && m.confine_origin === false) {
|
|
2245
|
+
ctx.addIssue({
|
|
2246
|
+
code: z.ZodIssueCode.custom,
|
|
2247
|
+
path: ["confine_origin"],
|
|
2248
|
+
message: "confine_origin: false is not allowed for static apps — they have no verifier, so an unconfined apex would be world-readable (ADR 0031)."
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2202
2252
|
if (isContainer && m.auth === "gateway") {
|
|
2203
2253
|
ctx.addIssue({
|
|
2204
2254
|
code: z.ZodIssueCode.custom,
|
|
@@ -2790,6 +2840,9 @@ var SpecSchema = z2.object({
|
|
|
2790
2840
|
auth: z2.enum(AUTH_MODES).optional(),
|
|
2791
2841
|
build: BuildSchema.optional(),
|
|
2792
2842
|
app: AppBoundarySchema.optional(),
|
|
2843
|
+
verify: VerifySchema.optional(),
|
|
2844
|
+
confine_origin: z2.boolean().optional(),
|
|
2845
|
+
catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
|
|
2793
2846
|
env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
|
|
2794
2847
|
containers: z2.array(ContainerSchema).min(1).optional(),
|
|
2795
2848
|
secrets: SecretsSchema.optional(),
|
|
@@ -2812,6 +2865,27 @@ var SpecSchema = z2.object({
|
|
|
2812
2865
|
message: "containers[] is required when appType is 'container'"
|
|
2813
2866
|
});
|
|
2814
2867
|
}
|
|
2868
|
+
if (s.confine_origin !== undefined) {
|
|
2869
|
+
if (isContainer) {
|
|
2870
|
+
ctx.addIssue({
|
|
2871
|
+
code: z2.ZodIssueCode.custom,
|
|
2872
|
+
path: ["confine_origin"],
|
|
2873
|
+
message: "confine_origin is only valid for gateway pages apps; container apps have no pages.dev surface."
|
|
2874
|
+
});
|
|
2875
|
+
} else if (s.auth === "access") {
|
|
2876
|
+
ctx.addIssue({
|
|
2877
|
+
code: z2.ZodIssueCode.custom,
|
|
2878
|
+
path: ["confine_origin"],
|
|
2879
|
+
message: "confine_origin applies to gateway-fronted apps (auth: gateway); it has no meaning under Cloudflare Access."
|
|
2880
|
+
});
|
|
2881
|
+
} else if (s.appType === "static" && s.confine_origin === false) {
|
|
2882
|
+
ctx.addIssue({
|
|
2883
|
+
code: z2.ZodIssueCode.custom,
|
|
2884
|
+
path: ["confine_origin"],
|
|
2885
|
+
message: "confine_origin: false is not allowed for static apps — an unconfined apex with no verifier is world-readable (ADR 0031)."
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2815
2889
|
if (!isContainer && s.containers !== undefined) {
|
|
2816
2890
|
ctx.addIssue({
|
|
2817
2891
|
code: z2.ZodIssueCode.custom,
|
|
@@ -3320,9 +3394,45 @@ function emitSecretStep(lines, binding) {
|
|
|
3320
3394
|
// ../launchpad-engine/dist/per-app-workspace.js
|
|
3321
3395
|
var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
|
|
3322
3396
|
var R2_S3_ENDPOINT = `https://${CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
|
3397
|
+
// ../launchpad-engine/dist/access-drift.js
|
|
3398
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3399
|
+
function canonicalUuid(s) {
|
|
3400
|
+
const t = s.trim();
|
|
3401
|
+
return UUID_RE.test(t) ? t.toLowerCase() : null;
|
|
3402
|
+
}
|
|
3403
|
+
function normalise(tokens, side, resolve6) {
|
|
3404
|
+
const uuids = new Set;
|
|
3405
|
+
const unresolved = [];
|
|
3406
|
+
for (const raw of tokens) {
|
|
3407
|
+
const direct = canonicalUuid(raw);
|
|
3408
|
+
const uuid = direct ?? (() => {
|
|
3409
|
+
const resolved = resolve6(raw);
|
|
3410
|
+
return resolved === null ? null : canonicalUuid(resolved);
|
|
3411
|
+
})();
|
|
3412
|
+
if (uuid === null) {
|
|
3413
|
+
unresolved.push({ side, token: raw });
|
|
3414
|
+
} else {
|
|
3415
|
+
uuids.add(uuid);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
return { uuids, unresolved };
|
|
3419
|
+
}
|
|
3420
|
+
function detectAccessDrift(declaredTokens, enforcedTokens, resolve6) {
|
|
3421
|
+
const declared = normalise(declaredTokens, "manifest", resolve6);
|
|
3422
|
+
const enforced = normalise(enforcedTokens, "enforced", resolve6);
|
|
3423
|
+
const manifestOnly = [...declared.uuids].filter((u) => !enforced.uuids.has(u)).sort();
|
|
3424
|
+
const liveOnly = [...enforced.uuids].filter((u) => !declared.uuids.has(u)).sort();
|
|
3425
|
+
const unresolved = [...declared.unresolved, ...enforced.unresolved];
|
|
3426
|
+
return {
|
|
3427
|
+
manifestOnly,
|
|
3428
|
+
liveOnly,
|
|
3429
|
+
unresolved,
|
|
3430
|
+
inSync: manifestOnly.length === 0 && liveOnly.length === 0 && unresolved.length === 0
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3323
3433
|
// ../launchpad-engine/dist/fleet-manifest.js
|
|
3324
3434
|
import { z as z3 } from "zod";
|
|
3325
|
-
var
|
|
3435
|
+
var UUID_RE2 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
3326
3436
|
var TF_RESIDENCIES = ["per-app-workspace", "main-tf"];
|
|
3327
3437
|
var APP_SHAPES = [
|
|
3328
3438
|
"pages",
|
|
@@ -3349,7 +3459,7 @@ var FLEET_TIERS = [
|
|
|
3349
3459
|
"out-of-scope"
|
|
3350
3460
|
];
|
|
3351
3461
|
var DEVICE_POSTURE = ["gateway-eligible", "retained-access"];
|
|
3352
|
-
var GroupId = z3.string().regex(
|
|
3462
|
+
var GroupId = z3.string().regex(UUID_RE2, "Entra group ids are emitted as canonical UUIDs in the JWT groups claim — display names will not match policies.");
|
|
3353
3463
|
var FleetAppSchema = z3.object({
|
|
3354
3464
|
slug: z3.string().regex(SLUG_REGEX),
|
|
3355
3465
|
residency: z3.enum(TF_RESIDENCIES),
|
|
@@ -3621,8 +3731,8 @@ async function bundleAndDeploy(args) {
|
|
|
3621
3731
|
}
|
|
3622
3732
|
|
|
3623
3733
|
// src/deploy/verify-deploy.ts
|
|
3624
|
-
import { readFileSync as
|
|
3625
|
-
import { join as
|
|
3734
|
+
import { readFileSync as readFileSync8, existsSync as existsSync4 } from "node:fs";
|
|
3735
|
+
import { join as join9 } from "node:path";
|
|
3626
3736
|
|
|
3627
3737
|
// src/deploy/deployment-status.ts
|
|
3628
3738
|
async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
|
|
@@ -3837,7 +3947,7 @@ function redactDiagnostic(text, maxLen = MAX_DIAGNOSTIC_LEN) {
|
|
|
3837
3947
|
}
|
|
3838
3948
|
|
|
3839
3949
|
// src/commands/status.ts
|
|
3840
|
-
import { readFileSync as
|
|
3950
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
3841
3951
|
|
|
3842
3952
|
// src/deploy/manifest-state.ts
|
|
3843
3953
|
async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
@@ -3849,6 +3959,7 @@ async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
|
3849
3959
|
lastAppliedManifestSha: raw.lastAppliedManifestSha,
|
|
3850
3960
|
appRepoHeadSha: raw.appRepoHeadSha ?? null,
|
|
3851
3961
|
openPr: raw.openPr,
|
|
3962
|
+
enforcedGroups: raw.enforcedGroups ?? null,
|
|
3852
3963
|
...raw.manifestYaml !== undefined ? { manifestYaml: raw.manifestYaml } : {}
|
|
3853
3964
|
};
|
|
3854
3965
|
}
|
|
@@ -3893,6 +4004,110 @@ function inferSlug(opts) {
|
|
|
3893
4004
|
return fromManifest ?? fromDir;
|
|
3894
4005
|
}
|
|
3895
4006
|
|
|
4007
|
+
// src/groups/client.ts
|
|
4008
|
+
import { mkdirSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
|
|
4009
|
+
import { dirname as dirname5, join as join8 } from "node:path";
|
|
4010
|
+
var CACHE_TTL_MS = 60 * 60 * 1000;
|
|
4011
|
+
var CACHE_FILENAME = "groups.json";
|
|
4012
|
+
async function fetchGroups(cfg, opts = {}) {
|
|
4013
|
+
const now = opts.now ?? Date.now;
|
|
4014
|
+
const cachePath = join8(cfg.cacheDir, CACHE_FILENAME);
|
|
4015
|
+
if (opts.forceRefresh !== true) {
|
|
4016
|
+
const cached = readCache(cachePath);
|
|
4017
|
+
if (cached !== null) {
|
|
4018
|
+
const ageMs = now() - Date.parse(cached.fetchedAt);
|
|
4019
|
+
if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < CACHE_TTL_MS) {
|
|
4020
|
+
return {
|
|
4021
|
+
kind: "ok",
|
|
4022
|
+
source: "cache",
|
|
4023
|
+
fetchedAt: cached.fetchedAt,
|
|
4024
|
+
groups: cached.groups
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
let response;
|
|
4030
|
+
try {
|
|
4031
|
+
response = await apiJson(cfg, { path: "/groups" }, opts.fetcher);
|
|
4032
|
+
} catch (e) {
|
|
4033
|
+
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
4034
|
+
throw e;
|
|
4035
|
+
}
|
|
4036
|
+
return { kind: "error", message: describe11(e) };
|
|
4037
|
+
}
|
|
4038
|
+
if (typeof response !== "object" || response === null || !Array.isArray(response.groups) || !response.groups.every(isEntraGroup)) {
|
|
4039
|
+
return {
|
|
4040
|
+
kind: "error",
|
|
4041
|
+
message: "bot returned a malformed groups response"
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
const groups = response.groups;
|
|
4045
|
+
const fetchedAt = new Date(now()).toISOString();
|
|
4046
|
+
writeCache(cachePath, { fetchedAt, groups });
|
|
4047
|
+
return { kind: "ok", source: "fresh", fetchedAt, groups };
|
|
4048
|
+
}
|
|
4049
|
+
function readCache(path8) {
|
|
4050
|
+
let raw;
|
|
4051
|
+
try {
|
|
4052
|
+
raw = readFileSync6(path8, "utf8");
|
|
4053
|
+
} catch {
|
|
4054
|
+
return null;
|
|
4055
|
+
}
|
|
4056
|
+
let parsed;
|
|
4057
|
+
try {
|
|
4058
|
+
parsed = JSON.parse(raw);
|
|
4059
|
+
} catch {
|
|
4060
|
+
return null;
|
|
4061
|
+
}
|
|
4062
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
4063
|
+
return null;
|
|
4064
|
+
const p = parsed;
|
|
4065
|
+
if (typeof p.fetchedAt !== "string")
|
|
4066
|
+
return null;
|
|
4067
|
+
if (!Array.isArray(p.groups))
|
|
4068
|
+
return null;
|
|
4069
|
+
for (const g of p.groups)
|
|
4070
|
+
if (!isEntraGroup(g))
|
|
4071
|
+
return null;
|
|
4072
|
+
return p;
|
|
4073
|
+
}
|
|
4074
|
+
function isEntraGroup(value) {
|
|
4075
|
+
if (typeof value !== "object" || value === null)
|
|
4076
|
+
return false;
|
|
4077
|
+
const g = value;
|
|
4078
|
+
return typeof g.id === "string" && typeof g.displayName === "string" && (typeof g.mailNickname === "string" || g.mailNickname === null);
|
|
4079
|
+
}
|
|
4080
|
+
function writeCache(path8, envelope) {
|
|
4081
|
+
try {
|
|
4082
|
+
mkdirSync(dirname5(path8), { recursive: true });
|
|
4083
|
+
writeFileSync(path8, JSON.stringify(envelope), "utf8");
|
|
4084
|
+
} catch {}
|
|
4085
|
+
}
|
|
4086
|
+
function describe11(e) {
|
|
4087
|
+
return e instanceof Error ? e.message : String(e);
|
|
4088
|
+
}
|
|
4089
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4090
|
+
function isUuid2(value) {
|
|
4091
|
+
return UUID_PATTERN.test(value);
|
|
4092
|
+
}
|
|
4093
|
+
function findGroup(groups, nameOrId) {
|
|
4094
|
+
if (isUuid2(nameOrId)) {
|
|
4095
|
+
const lower = nameOrId.toLowerCase();
|
|
4096
|
+
const hit = groups.find((g) => g.id.toLowerCase() === lower);
|
|
4097
|
+
return hit ? { kind: "found", group: hit } : { kind: "not-found" };
|
|
4098
|
+
}
|
|
4099
|
+
const matches = groups.filter((g) => g.displayName === nameOrId || g.mailNickname !== null && g.mailNickname === nameOrId);
|
|
4100
|
+
if (matches.length === 0)
|
|
4101
|
+
return { kind: "not-found" };
|
|
4102
|
+
if (matches.length === 1)
|
|
4103
|
+
return { kind: "found", group: matches[0] };
|
|
4104
|
+
return { kind: "ambiguous", matches };
|
|
4105
|
+
}
|
|
4106
|
+
function searchGroups(groups, query) {
|
|
4107
|
+
const q = query.toLowerCase();
|
|
4108
|
+
return groups.filter((g) => g.displayName.toLowerCase().includes(q) || g.mailNickname !== null && g.mailNickname.toLowerCase().includes(q) || g.id.toLowerCase().includes(q));
|
|
4109
|
+
}
|
|
4110
|
+
|
|
3896
4111
|
// src/watch/capabilities.ts
|
|
3897
4112
|
function isUtf8(env) {
|
|
3898
4113
|
const v = `${env.LC_ALL ?? ""}|${env.LC_CTYPE ?? ""}|${env.LANG ?? ""}`.toLowerCase();
|
|
@@ -4525,7 +4740,7 @@ function mapBotError(e, slug, io) {
|
|
|
4525
4740
|
io.err(`launchpad status: ${e.message}`);
|
|
4526
4741
|
return 2;
|
|
4527
4742
|
}
|
|
4528
|
-
io.err(`launchpad status failed: ${
|
|
4743
|
+
io.err(`launchpad status failed: ${describe12(e)}`);
|
|
4529
4744
|
return 2;
|
|
4530
4745
|
}
|
|
4531
4746
|
async function runStatus(args, io) {
|
|
@@ -4551,10 +4766,10 @@ async function runStatus(args, io) {
|
|
|
4551
4766
|
}
|
|
4552
4767
|
let localYaml = null;
|
|
4553
4768
|
try {
|
|
4554
|
-
localYaml =
|
|
4769
|
+
localYaml = readFileSync7(parsed.file, "utf8");
|
|
4555
4770
|
} catch (e) {
|
|
4556
4771
|
if (!isEnoent(e)) {
|
|
4557
|
-
io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${
|
|
4772
|
+
io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe12(e)}`);
|
|
4558
4773
|
return 2;
|
|
4559
4774
|
}
|
|
4560
4775
|
}
|
|
@@ -4565,7 +4780,7 @@ async function runStatus(args, io) {
|
|
|
4565
4780
|
const { parse: parseYaml4 } = await import("yaml");
|
|
4566
4781
|
localObj = parseYaml4(localYaml);
|
|
4567
4782
|
} catch (e) {
|
|
4568
|
-
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${
|
|
4783
|
+
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe12(e)}`);
|
|
4569
4784
|
return 2;
|
|
4570
4785
|
}
|
|
4571
4786
|
const localParse = parseManifest(localObj);
|
|
@@ -4594,7 +4809,7 @@ async function runStatus(args, io) {
|
|
|
4594
4809
|
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
4595
4810
|
return mapBotError(e, parsed.slug, io);
|
|
4596
4811
|
}
|
|
4597
|
-
io.err(`launchpad status: live deployment state unavailable (${
|
|
4812
|
+
io.err(`launchpad status: live deployment state unavailable (${describe12(e)}) — ` + `the report below is from the platform manifest view only.`);
|
|
4598
4813
|
}
|
|
4599
4814
|
let standingExceptions = null;
|
|
4600
4815
|
try {
|
|
@@ -4603,7 +4818,7 @@ async function runStatus(args, io) {
|
|
|
4603
4818
|
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
4604
4819
|
return mapBotError(e, parsed.slug, io);
|
|
4605
4820
|
}
|
|
4606
|
-
io.err(`launchpad status: standing-exception inventory unavailable (${
|
|
4821
|
+
io.err(`launchpad status: standing-exception inventory unavailable (${describe12(e)}).`);
|
|
4607
4822
|
}
|
|
4608
4823
|
if (state.manifestYaml === null || state.manifestYaml === undefined) {
|
|
4609
4824
|
const live = deployment?.liveDeployment ?? null;
|
|
@@ -4655,7 +4870,7 @@ async function runStatus(args, io) {
|
|
|
4655
4870
|
const { parse: parseYaml4 } = await import("yaml");
|
|
4656
4871
|
deployedObj = parseYaml4(state.manifestYaml);
|
|
4657
4872
|
} catch (e) {
|
|
4658
|
-
io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${
|
|
4873
|
+
io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe12(e)}`);
|
|
4659
4874
|
return 2;
|
|
4660
4875
|
}
|
|
4661
4876
|
const deployedParse = parseManifest(deployedObj);
|
|
@@ -4668,6 +4883,16 @@ async function runStatus(args, io) {
|
|
|
4668
4883
|
}
|
|
4669
4884
|
const deployed = deployedParse.manifest;
|
|
4670
4885
|
const drift = computeDrift(local, deployed);
|
|
4886
|
+
let accessDrift;
|
|
4887
|
+
if (state.enforcedGroups !== null) {
|
|
4888
|
+
let resolver;
|
|
4889
|
+
try {
|
|
4890
|
+
resolver = await buildGroupResolver(cfg);
|
|
4891
|
+
} catch (e) {
|
|
4892
|
+
return mapBotError(e, parsed.slug, io);
|
|
4893
|
+
}
|
|
4894
|
+
accessDrift = detectAccessDrift(allowedEntraGroups(local.access), state.enforcedGroups, resolver);
|
|
4895
|
+
}
|
|
4671
4896
|
const result = {
|
|
4672
4897
|
state: drift.length === 0 ? "in_sync" : "drift",
|
|
4673
4898
|
slug: parsed.slug,
|
|
@@ -4678,14 +4903,30 @@ async function runStatus(args, io) {
|
|
|
4678
4903
|
driftFields: drift.map((d) => d.path),
|
|
4679
4904
|
driftDetails: drift,
|
|
4680
4905
|
...deploymentKnown ? { deployment } : {},
|
|
4681
|
-
...standingExceptions !== null ? { standingExceptions } : {}
|
|
4906
|
+
...standingExceptions !== null ? { standingExceptions } : {},
|
|
4907
|
+
...accessDrift !== undefined ? { accessDrift } : {}
|
|
4682
4908
|
};
|
|
4683
4909
|
emit3(result, parsed.json, io);
|
|
4684
|
-
if (result.state === "drift"
|
|
4910
|
+
if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false)) {
|
|
4685
4911
|
return 1;
|
|
4686
4912
|
}
|
|
4687
4913
|
return 0;
|
|
4688
4914
|
}
|
|
4915
|
+
async function buildGroupResolver(cfg) {
|
|
4916
|
+
try {
|
|
4917
|
+
const res = await fetchGroups(cfg);
|
|
4918
|
+
if (res.kind !== "ok")
|
|
4919
|
+
return () => null;
|
|
4920
|
+
return (nameOrId) => {
|
|
4921
|
+
const hit = findGroup(res.groups, nameOrId);
|
|
4922
|
+
return hit.kind === "found" ? hit.group.id : null;
|
|
4923
|
+
};
|
|
4924
|
+
} catch (e) {
|
|
4925
|
+
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError)
|
|
4926
|
+
throw e;
|
|
4927
|
+
return () => null;
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4689
4930
|
function computeDrift(local, deployed) {
|
|
4690
4931
|
const diffs = [];
|
|
4691
4932
|
const cmp = (path8, l, d) => {
|
|
@@ -4797,6 +5038,7 @@ function emit3(out, asJson, io) {
|
|
|
4797
5038
|
case "in_sync":
|
|
4798
5039
|
io.out(`${out.slug}: live, in sync` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
|
|
4799
5040
|
surfaceHeadVsDeployed(out, io);
|
|
5041
|
+
surfaceAccessDrift(out, io);
|
|
4800
5042
|
surfaceDeployment(out, io);
|
|
4801
5043
|
surfaceExceptions(out, io);
|
|
4802
5044
|
return;
|
|
@@ -4808,11 +5050,28 @@ function emit3(out, asJson, io) {
|
|
|
4808
5050
|
io.out(` deployed: ${formatValue(d.deployed)}`);
|
|
4809
5051
|
}
|
|
4810
5052
|
surfaceHeadVsDeployed(out, io);
|
|
5053
|
+
surfaceAccessDrift(out, io);
|
|
4811
5054
|
surfaceDeployment(out, io);
|
|
4812
5055
|
surfaceExceptions(out, io);
|
|
4813
5056
|
return;
|
|
4814
5057
|
}
|
|
4815
5058
|
}
|
|
5059
|
+
function surfaceAccessDrift(out, io) {
|
|
5060
|
+
const ad = out.accessDrift;
|
|
5061
|
+
if (ad === undefined || ad.inSync)
|
|
5062
|
+
return;
|
|
5063
|
+
io.out(" ⚠ ACCESS DRIFT — your declared groups are not what the gateway enforces:");
|
|
5064
|
+
if (ad.liveOnly.length > 0) {
|
|
5065
|
+
io.out(` enforced but NOT declared (over-grant — still reachable): ${ad.liveOnly.join(", ")}`);
|
|
5066
|
+
}
|
|
5067
|
+
if (ad.manifestOnly.length > 0) {
|
|
5068
|
+
io.out(` declared but NOT enforced (locked out despite the manifest): ${ad.manifestOnly.join(", ")}`);
|
|
5069
|
+
}
|
|
5070
|
+
for (const u of ad.unresolved) {
|
|
5071
|
+
io.out(` unresolved group (${u.side}): ${u.token}`);
|
|
5072
|
+
}
|
|
5073
|
+
io.out(" `launchpad deploy` will NOT fix this — access changes go via the gated TF route.");
|
|
5074
|
+
}
|
|
4816
5075
|
function surfaceNoLocalManifestNote(out, io) {
|
|
4817
5076
|
if (out.drift !== null)
|
|
4818
5077
|
return;
|
|
@@ -5005,7 +5264,7 @@ function printUsage2(io) {
|
|
|
5005
5264
|
].join(`
|
|
5006
5265
|
`));
|
|
5007
5266
|
}
|
|
5008
|
-
function
|
|
5267
|
+
function describe12(e) {
|
|
5009
5268
|
return e instanceof Error ? e.message : String(e);
|
|
5010
5269
|
}
|
|
5011
5270
|
function isEnoent(e) {
|
|
@@ -5016,10 +5275,10 @@ function isEnoent(e) {
|
|
|
5016
5275
|
var VERIFY_MISMATCH_EXIT = 65;
|
|
5017
5276
|
var NO_DEPLOYMENT_EXIT = 69;
|
|
5018
5277
|
function readLocalEntrypoint(cwd) {
|
|
5019
|
-
for (const rel of ["index.html",
|
|
5020
|
-
const p =
|
|
5278
|
+
for (const rel of ["index.html", join9("static", "index.html")]) {
|
|
5279
|
+
const p = join9(cwd, rel);
|
|
5021
5280
|
if (existsSync4(p))
|
|
5022
|
-
return new Uint8Array(
|
|
5281
|
+
return new Uint8Array(readFileSync8(p));
|
|
5023
5282
|
}
|
|
5024
5283
|
return null;
|
|
5025
5284
|
}
|
|
@@ -5253,7 +5512,7 @@ async function verifyFirstDeployServed(args, liveUrl, io, deps) {
|
|
|
5253
5512
|
|
|
5254
5513
|
// src/commands/deploy.ts
|
|
5255
5514
|
import { parse as parseYaml5 } from "yaml";
|
|
5256
|
-
import { readFileSync as
|
|
5515
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
5257
5516
|
|
|
5258
5517
|
// src/deploy/git-files.ts
|
|
5259
5518
|
import { spawn as spawn3 } from "node:child_process";
|
|
@@ -5694,15 +5953,15 @@ function parseDeployFlags(args) {
|
|
|
5694
5953
|
|
|
5695
5954
|
// src/deploy/apply.ts
|
|
5696
5955
|
import { existsSync as existsSync5, rmSync } from "node:fs";
|
|
5697
|
-
import { resolve as resolvePath, join as
|
|
5956
|
+
import { resolve as resolvePath, join as join10 } from "node:path";
|
|
5698
5957
|
|
|
5699
5958
|
// src/manifest/load.ts
|
|
5700
|
-
import { readFileSync as
|
|
5959
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
5701
5960
|
import { parse as parseYaml4, YAMLParseError } from "yaml";
|
|
5702
5961
|
function loadManifest(path8) {
|
|
5703
5962
|
let raw;
|
|
5704
5963
|
try {
|
|
5705
|
-
raw =
|
|
5964
|
+
raw = readFileSync9(path8, "utf8");
|
|
5706
5965
|
} catch (err) {
|
|
5707
5966
|
const e = err;
|
|
5708
5967
|
if (e.code === "ENOENT") {
|
|
@@ -5871,7 +6130,7 @@ async function pollUntilApplied(args) {
|
|
|
5871
6130
|
path: `/apps/${args.slug}/manifest/state`
|
|
5872
6131
|
}, args.fetcher);
|
|
5873
6132
|
} catch (e) {
|
|
5874
|
-
args.io.err(`! state fetch failed (will retry): ${
|
|
6133
|
+
args.io.err(`! state fetch failed (will retry): ${describe13(e)}`);
|
|
5875
6134
|
await sleep(args.pollIntervalSec * 1000);
|
|
5876
6135
|
continue;
|
|
5877
6136
|
}
|
|
@@ -5924,14 +6183,14 @@ function sleep(ms) {
|
|
|
5924
6183
|
return new Promise((res) => setTimeout(res, ms));
|
|
5925
6184
|
}
|
|
5926
6185
|
function deletePinIfPresent(cfg, slug, io) {
|
|
5927
|
-
const pinPath =
|
|
6186
|
+
const pinPath = join10(cfg.stateDir, slug, "group.json");
|
|
5928
6187
|
if (!existsSync5(pinPath))
|
|
5929
6188
|
return;
|
|
5930
6189
|
try {
|
|
5931
6190
|
rmSync(pinPath, { force: true });
|
|
5932
6191
|
io.out(` (cleaned up obsolete pin file ${pinPath})`);
|
|
5933
6192
|
} catch (e) {
|
|
5934
|
-
io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${
|
|
6193
|
+
io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${describe13(e)}`);
|
|
5935
6194
|
}
|
|
5936
6195
|
}
|
|
5937
6196
|
async function loadSlugForResume(opts, io) {
|
|
@@ -5955,7 +6214,7 @@ async function defaultPrompt(question) {
|
|
|
5955
6214
|
rl.close();
|
|
5956
6215
|
}
|
|
5957
6216
|
}
|
|
5958
|
-
function
|
|
6217
|
+
function describe13(e) {
|
|
5959
6218
|
return e instanceof Error ? e.message : String(e);
|
|
5960
6219
|
}
|
|
5961
6220
|
function mapHttpError(e, slug, io) {
|
|
@@ -5979,7 +6238,7 @@ function mapHttpError(e, slug, io) {
|
|
|
5979
6238
|
io.err(`launchpad deploy --apply: ${e.message}`);
|
|
5980
6239
|
return 1;
|
|
5981
6240
|
}
|
|
5982
|
-
io.err(`launchpad deploy --apply failed: ${
|
|
6241
|
+
io.err(`launchpad deploy --apply failed: ${describe13(e)}`);
|
|
5983
6242
|
return 2;
|
|
5984
6243
|
}
|
|
5985
6244
|
function renderManifestError(loaded, io) {
|
|
@@ -6021,7 +6280,7 @@ async function runDeployDryRun(opts, io, deps = {}) {
|
|
|
6021
6280
|
try {
|
|
6022
6281
|
manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha)(process.cwd());
|
|
6023
6282
|
} catch (e) {
|
|
6024
|
-
const msg = `failed to resolve git HEAD in ${process.cwd()}: ${
|
|
6283
|
+
const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe14(e)}`;
|
|
6025
6284
|
if (opts.json) {
|
|
6026
6285
|
io.out(JSON.stringify({ ok: false, kind: "git-error", message: msg }));
|
|
6027
6286
|
} else {
|
|
@@ -6070,7 +6329,7 @@ function defaultGitHeadSha(cwd) {
|
|
|
6070
6329
|
});
|
|
6071
6330
|
return out.trim();
|
|
6072
6331
|
}
|
|
6073
|
-
function
|
|
6332
|
+
function describe14(e) {
|
|
6074
6333
|
return e instanceof Error ? e.message : String(e);
|
|
6075
6334
|
}
|
|
6076
6335
|
function mapHttpError2(e, slug, json, io) {
|
|
@@ -6454,7 +6713,7 @@ async function runDeploy(args, io) {
|
|
|
6454
6713
|
const contentManifestPath = path8.join(process.cwd(), "launchpad.yaml");
|
|
6455
6714
|
if (existsSync6(contentManifestPath)) {
|
|
6456
6715
|
try {
|
|
6457
|
-
contentManifestYaml =
|
|
6716
|
+
contentManifestYaml = readFileSync10(contentManifestPath, "utf8");
|
|
6458
6717
|
} catch {
|
|
6459
6718
|
contentManifestYaml = null;
|
|
6460
6719
|
}
|
|
@@ -6528,7 +6787,7 @@ async function runDeploy(args, io) {
|
|
|
6528
6787
|
io.err(`launchpad deploy: ${e.message}`);
|
|
6529
6788
|
return 1;
|
|
6530
6789
|
}
|
|
6531
|
-
io.err(`launchpad deploy failed: ${
|
|
6790
|
+
io.err(`launchpad deploy failed: ${describe15(e)}`);
|
|
6532
6791
|
return 1;
|
|
6533
6792
|
}
|
|
6534
6793
|
}
|
|
@@ -6600,7 +6859,7 @@ function formatBytes2(n) {
|
|
|
6600
6859
|
return `${(n / 1024).toFixed(1)}KB`;
|
|
6601
6860
|
return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
6602
6861
|
}
|
|
6603
|
-
function
|
|
6862
|
+
function describe15(e) {
|
|
6604
6863
|
return e instanceof Error ? e.message : String(e);
|
|
6605
6864
|
}
|
|
6606
6865
|
function surfaceDeployExtras(body, io, slug, allowStale = false) {
|
|
@@ -6633,6 +6892,17 @@ function surfaceDeployExtras(body, io, slug, allowStale = false) {
|
|
|
6633
6892
|
}
|
|
6634
6893
|
io.out(` Full list: \`launchpad status ${slug}\`.`);
|
|
6635
6894
|
}
|
|
6895
|
+
const adw = body.access_drift_warning;
|
|
6896
|
+
if (adw !== undefined) {
|
|
6897
|
+
io.err("warning: ACCESS DRIFT — declared groups don't match what the gateway enforces:");
|
|
6898
|
+
if (adw.liveOnly !== undefined && adw.liveOnly.length > 0) {
|
|
6899
|
+
io.err(` enforced but NOT declared (over-grant): ${adw.liveOnly.join(", ")}`);
|
|
6900
|
+
}
|
|
6901
|
+
if (adw.manifestOnly !== undefined && adw.manifestOnly.length > 0) {
|
|
6902
|
+
io.err(` declared but NOT enforced: ${adw.manifestOnly.join(", ")}`);
|
|
6903
|
+
}
|
|
6904
|
+
io.err(` reconcile access via the gated TF route — \`launchpad deploy\` can't (it ships content only).`);
|
|
6905
|
+
}
|
|
6636
6906
|
}
|
|
6637
6907
|
function isStaleBlock(body) {
|
|
6638
6908
|
const code = body?.error;
|
|
@@ -6665,7 +6935,7 @@ async function runModelADeploy(args) {
|
|
|
6665
6935
|
let slug;
|
|
6666
6936
|
let appType = "static";
|
|
6667
6937
|
try {
|
|
6668
|
-
const manifestObj = parseYaml5(
|
|
6938
|
+
const manifestObj = parseYaml5(readFileSync10(manifestPath, "utf8"));
|
|
6669
6939
|
const metaSlug = resolveManifestSlug(manifestObj);
|
|
6670
6940
|
if (metaSlug === null) {
|
|
6671
6941
|
io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
|
|
@@ -6677,7 +6947,7 @@ async function runModelADeploy(args) {
|
|
|
6677
6947
|
appType = dtype;
|
|
6678
6948
|
}
|
|
6679
6949
|
} catch (e) {
|
|
6680
|
-
io.err(`launchpad deploy: failed to read ${manifestPath}: ${
|
|
6950
|
+
io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe15(e)}`);
|
|
6681
6951
|
return 1;
|
|
6682
6952
|
}
|
|
6683
6953
|
const noVerify = args.argv.includes("--no-verify");
|
|
@@ -6707,7 +6977,7 @@ async function runModelADeploy(args) {
|
|
|
6707
6977
|
try {
|
|
6708
6978
|
cfg = loadConfig();
|
|
6709
6979
|
} catch (e) {
|
|
6710
|
-
io.err(`launchpad deploy: ${
|
|
6980
|
+
io.err(`launchpad deploy: ${describe15(e)}`);
|
|
6711
6981
|
return 1;
|
|
6712
6982
|
}
|
|
6713
6983
|
const deployBaseline = noVerify ? null : await readDeploymentBaseline(realCommitWaitDeps(cfg), slug);
|
|
@@ -6736,7 +7006,7 @@ async function runModelADeploy(args) {
|
|
|
6736
7006
|
io.err(" run `launchpad login` to refresh your session.");
|
|
6737
7007
|
return 1;
|
|
6738
7008
|
}
|
|
6739
|
-
io.err(`launchpad deploy: unexpected error: ${
|
|
7009
|
+
io.err(`launchpad deploy: unexpected error: ${describe15(e)}`);
|
|
6740
7010
|
return 1;
|
|
6741
7011
|
}
|
|
6742
7012
|
switch (result.kind) {
|
|
@@ -6769,6 +7039,23 @@ async function runModelADeploy(args) {
|
|
|
6769
7039
|
surfaceStaleBlock(body, io);
|
|
6770
7040
|
return 1;
|
|
6771
7041
|
}
|
|
7042
|
+
if (result.status === 409 && errorCode === "access_drift") {
|
|
7043
|
+
io.err(`launchpad deploy: refused — ACCESS DRIFT on "${slug}".`);
|
|
7044
|
+
const liveOnly = Array.isArray(body.live_only) ? body.live_only : [];
|
|
7045
|
+
const manifestOnly = Array.isArray(body.manifest_only) ? body.manifest_only : [];
|
|
7046
|
+
if (liveOnly.length > 0) {
|
|
7047
|
+
io.err(` enforced but NOT declared (over-grant): ${liveOnly.map(String).join(", ")}`);
|
|
7048
|
+
}
|
|
7049
|
+
if (manifestOnly.length > 0) {
|
|
7050
|
+
io.err(` declared but NOT enforced: ${manifestOnly.map(String).join(", ")}`);
|
|
7051
|
+
}
|
|
7052
|
+
io.err("");
|
|
7053
|
+
io.err(" Your manifest's access groups don't match what the gateway enforces.");
|
|
7054
|
+
io.err(" `launchpad deploy` ships content only and cannot change access — reconcile");
|
|
7055
|
+
io.err(" via the gated TF route (see the access-change runbook), then redeploy.");
|
|
7056
|
+
io.err(" Nothing was committed by this attempt.");
|
|
7057
|
+
return 1;
|
|
7058
|
+
}
|
|
6772
7059
|
io.err(`launchpad deploy: bot rejected the upload (HTTP ${result.status}, ${errorCode}).`);
|
|
6773
7060
|
if (typeof body.message === "string") {
|
|
6774
7061
|
io.err(` ${body.message}`);
|
|
@@ -6864,7 +7151,7 @@ async function runModelADeploy(args) {
|
|
|
6864
7151
|
}
|
|
6865
7152
|
|
|
6866
7153
|
// src/commands/redeploy.ts
|
|
6867
|
-
import { existsSync as existsSync7, readFileSync as
|
|
7154
|
+
import { existsSync as existsSync7, readFileSync as readFileSync11 } from "node:fs";
|
|
6868
7155
|
import { resolve as resolvePath2 } from "node:path";
|
|
6869
7156
|
import { parse as parseYaml6 } from "yaml";
|
|
6870
7157
|
var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
@@ -6945,7 +7232,7 @@ function readLocalAppType(cwd, file) {
|
|
|
6945
7232
|
if (!existsSync7(manifestPath))
|
|
6946
7233
|
return null;
|
|
6947
7234
|
try {
|
|
6948
|
-
const obj = parseYaml6(
|
|
7235
|
+
const obj = parseYaml6(readFileSync11(manifestPath, "utf8"));
|
|
6949
7236
|
return coerceAppType(obj?.deployment?.type);
|
|
6950
7237
|
} catch {
|
|
6951
7238
|
return null;
|
|
@@ -7127,7 +7414,7 @@ async function runEnvvars(args, io) {
|
|
|
7127
7414
|
io.err(`launchpad envvars: ${e.message}`);
|
|
7128
7415
|
return 1;
|
|
7129
7416
|
}
|
|
7130
|
-
io.err(`launchpad envvars failed: ${
|
|
7417
|
+
io.err(`launchpad envvars failed: ${describe16(e)}`);
|
|
7131
7418
|
return 1;
|
|
7132
7419
|
}
|
|
7133
7420
|
}
|
|
@@ -7224,13 +7511,13 @@ function renderList(envVars, io) {
|
|
|
7224
7511
|
io.out(fmt(row));
|
|
7225
7512
|
}
|
|
7226
7513
|
}
|
|
7227
|
-
function
|
|
7514
|
+
function describe16(e) {
|
|
7228
7515
|
return e instanceof Error ? e.message : String(e);
|
|
7229
7516
|
}
|
|
7230
7517
|
|
|
7231
7518
|
// src/commands/generate.ts
|
|
7232
|
-
import { mkdirSync, readFileSync as
|
|
7233
|
-
import { dirname as
|
|
7519
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7520
|
+
import { dirname as dirname6, resolve as resolve8, relative as relative3 } from "node:path";
|
|
7234
7521
|
var generateCommand = {
|
|
7235
7522
|
name: "generate",
|
|
7236
7523
|
summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
|
|
@@ -7248,7 +7535,7 @@ async function runGenerate(args, io) {
|
|
|
7248
7535
|
if (result.kind !== "ok") {
|
|
7249
7536
|
return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
|
|
7250
7537
|
}
|
|
7251
|
-
const appRoot =
|
|
7538
|
+
const appRoot = dirname6(manifestPath);
|
|
7252
7539
|
const wranglerPath = resolve8(appRoot, "container", "wrangler.toml");
|
|
7253
7540
|
const workflowPath = resolve8(appRoot, ".github", "workflows", "deploy.yml");
|
|
7254
7541
|
const wranglerOut = generateWranglerToml(result.manifest);
|
|
@@ -7292,8 +7579,8 @@ function applyOne(artefact, path10, out, force) {
|
|
|
7292
7579
|
};
|
|
7293
7580
|
}
|
|
7294
7581
|
try {
|
|
7295
|
-
|
|
7296
|
-
|
|
7582
|
+
mkdirSync2(dirname6(path10), { recursive: true });
|
|
7583
|
+
writeFileSync2(path10, out.content, "utf8");
|
|
7297
7584
|
return {
|
|
7298
7585
|
artefact,
|
|
7299
7586
|
action: {
|
|
@@ -7317,7 +7604,7 @@ function applyOne(artefact, path10, out, force) {
|
|
|
7317
7604
|
}
|
|
7318
7605
|
function readIfExists(path10) {
|
|
7319
7606
|
try {
|
|
7320
|
-
return { kind: "ok", content:
|
|
7607
|
+
return { kind: "ok", content: readFileSync12(path10, "utf8") };
|
|
7321
7608
|
} catch (err) {
|
|
7322
7609
|
const e = err;
|
|
7323
7610
|
if (e.code === "ENOENT")
|
|
@@ -7527,110 +7814,6 @@ function parseFlags(args) {
|
|
|
7527
7814
|
return { kind: "ok", file, dryRun, force, json };
|
|
7528
7815
|
}
|
|
7529
7816
|
|
|
7530
|
-
// src/groups/client.ts
|
|
7531
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7532
|
-
import { dirname as dirname6, join as join11 } from "node:path";
|
|
7533
|
-
var CACHE_TTL_MS = 60 * 60 * 1000;
|
|
7534
|
-
var CACHE_FILENAME = "groups.json";
|
|
7535
|
-
async function fetchGroups(cfg, opts = {}) {
|
|
7536
|
-
const now = opts.now ?? Date.now;
|
|
7537
|
-
const cachePath = join11(cfg.cacheDir, CACHE_FILENAME);
|
|
7538
|
-
if (opts.forceRefresh !== true) {
|
|
7539
|
-
const cached = readCache(cachePath);
|
|
7540
|
-
if (cached !== null) {
|
|
7541
|
-
const ageMs = now() - Date.parse(cached.fetchedAt);
|
|
7542
|
-
if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < CACHE_TTL_MS) {
|
|
7543
|
-
return {
|
|
7544
|
-
kind: "ok",
|
|
7545
|
-
source: "cache",
|
|
7546
|
-
fetchedAt: cached.fetchedAt,
|
|
7547
|
-
groups: cached.groups
|
|
7548
|
-
};
|
|
7549
|
-
}
|
|
7550
|
-
}
|
|
7551
|
-
}
|
|
7552
|
-
let response;
|
|
7553
|
-
try {
|
|
7554
|
-
response = await apiJson(cfg, { path: "/groups" }, opts.fetcher);
|
|
7555
|
-
} catch (e) {
|
|
7556
|
-
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
7557
|
-
throw e;
|
|
7558
|
-
}
|
|
7559
|
-
return { kind: "error", message: describe16(e) };
|
|
7560
|
-
}
|
|
7561
|
-
if (typeof response !== "object" || response === null || !Array.isArray(response.groups) || !response.groups.every(isEntraGroup)) {
|
|
7562
|
-
return {
|
|
7563
|
-
kind: "error",
|
|
7564
|
-
message: "bot returned a malformed groups response"
|
|
7565
|
-
};
|
|
7566
|
-
}
|
|
7567
|
-
const groups = response.groups;
|
|
7568
|
-
const fetchedAt = new Date(now()).toISOString();
|
|
7569
|
-
writeCache(cachePath, { fetchedAt, groups });
|
|
7570
|
-
return { kind: "ok", source: "fresh", fetchedAt, groups };
|
|
7571
|
-
}
|
|
7572
|
-
function readCache(path10) {
|
|
7573
|
-
let raw;
|
|
7574
|
-
try {
|
|
7575
|
-
raw = readFileSync12(path10, "utf8");
|
|
7576
|
-
} catch {
|
|
7577
|
-
return null;
|
|
7578
|
-
}
|
|
7579
|
-
let parsed;
|
|
7580
|
-
try {
|
|
7581
|
-
parsed = JSON.parse(raw);
|
|
7582
|
-
} catch {
|
|
7583
|
-
return null;
|
|
7584
|
-
}
|
|
7585
|
-
if (typeof parsed !== "object" || parsed === null)
|
|
7586
|
-
return null;
|
|
7587
|
-
const p = parsed;
|
|
7588
|
-
if (typeof p.fetchedAt !== "string")
|
|
7589
|
-
return null;
|
|
7590
|
-
if (!Array.isArray(p.groups))
|
|
7591
|
-
return null;
|
|
7592
|
-
for (const g of p.groups)
|
|
7593
|
-
if (!isEntraGroup(g))
|
|
7594
|
-
return null;
|
|
7595
|
-
return p;
|
|
7596
|
-
}
|
|
7597
|
-
function isEntraGroup(value) {
|
|
7598
|
-
if (typeof value !== "object" || value === null)
|
|
7599
|
-
return false;
|
|
7600
|
-
const g = value;
|
|
7601
|
-
return typeof g.id === "string" && typeof g.displayName === "string" && (typeof g.mailNickname === "string" || g.mailNickname === null);
|
|
7602
|
-
}
|
|
7603
|
-
function writeCache(path10, envelope) {
|
|
7604
|
-
try {
|
|
7605
|
-
mkdirSync2(dirname6(path10), { recursive: true });
|
|
7606
|
-
writeFileSync2(path10, JSON.stringify(envelope), "utf8");
|
|
7607
|
-
} catch {}
|
|
7608
|
-
}
|
|
7609
|
-
function describe16(e) {
|
|
7610
|
-
return e instanceof Error ? e.message : String(e);
|
|
7611
|
-
}
|
|
7612
|
-
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7613
|
-
function isUuid(value) {
|
|
7614
|
-
return UUID_PATTERN.test(value);
|
|
7615
|
-
}
|
|
7616
|
-
function findGroup(groups, nameOrId) {
|
|
7617
|
-
if (isUuid(nameOrId)) {
|
|
7618
|
-
const lower = nameOrId.toLowerCase();
|
|
7619
|
-
const hit = groups.find((g) => g.id.toLowerCase() === lower);
|
|
7620
|
-
return hit ? { kind: "found", group: hit } : { kind: "not-found" };
|
|
7621
|
-
}
|
|
7622
|
-
const matches = groups.filter((g) => g.displayName === nameOrId || g.mailNickname !== null && g.mailNickname === nameOrId);
|
|
7623
|
-
if (matches.length === 0)
|
|
7624
|
-
return { kind: "not-found" };
|
|
7625
|
-
if (matches.length === 1)
|
|
7626
|
-
return { kind: "found", group: matches[0] };
|
|
7627
|
-
return { kind: "ambiguous", matches };
|
|
7628
|
-
}
|
|
7629
|
-
function searchGroups(groups, query) {
|
|
7630
|
-
const q = query.toLowerCase();
|
|
7631
|
-
return groups.filter((g) => g.displayName.toLowerCase().includes(q) || g.mailNickname !== null && g.mailNickname.toLowerCase().includes(q) || g.id.toLowerCase().includes(q));
|
|
7632
|
-
}
|
|
7633
|
-
|
|
7634
7817
|
// src/auth/jwt.ts
|
|
7635
7818
|
class JwtParseError extends Error {
|
|
7636
7819
|
code = "jwt_parse_error";
|
|
@@ -8048,7 +8231,7 @@ async function runResolve(args, io) {
|
|
|
8048
8231
|
return 1;
|
|
8049
8232
|
}
|
|
8050
8233
|
io.out(hit.group.id);
|
|
8051
|
-
if (!
|
|
8234
|
+
if (!isUuid2(key)) {
|
|
8052
8235
|
io.err(`# resolved "${key}" → ${hit.group.displayName}`);
|
|
8053
8236
|
}
|
|
8054
8237
|
return 0;
|
|
@@ -13720,6 +13903,52 @@ function printHelp6(io, commands) {
|
|
|
13720
13903
|
io.out("silent until the grant session expires.");
|
|
13721
13904
|
}
|
|
13722
13905
|
|
|
13906
|
+
// src/report/classify-fault.ts
|
|
13907
|
+
var PLATFORM_FAULT_EXIT_CODES = [69];
|
|
13908
|
+
function classifyFault(input3) {
|
|
13909
|
+
if (input3.exitCode === 0)
|
|
13910
|
+
return false;
|
|
13911
|
+
if (PLATFORM_FAULT_EXIT_CODES.includes(input3.exitCode))
|
|
13912
|
+
return true;
|
|
13913
|
+
const f = input3.lastFault;
|
|
13914
|
+
if (f === null)
|
|
13915
|
+
return false;
|
|
13916
|
+
if (f.code === "transport_error")
|
|
13917
|
+
return true;
|
|
13918
|
+
if (f.code === "api_error")
|
|
13919
|
+
return (f.status ?? 0) >= 500;
|
|
13920
|
+
return false;
|
|
13921
|
+
}
|
|
13922
|
+
|
|
13923
|
+
// src/report/report-nudge.ts
|
|
13924
|
+
function nudgeSuppressed(ctx) {
|
|
13925
|
+
if (ctx.env.CI)
|
|
13926
|
+
return true;
|
|
13927
|
+
if (!ctx.stderrIsTTY)
|
|
13928
|
+
return true;
|
|
13929
|
+
if (ctx.argv.includes("--json"))
|
|
13930
|
+
return true;
|
|
13931
|
+
const verb = ctx.argv[0];
|
|
13932
|
+
if (verb === "bug" || verb === "feature")
|
|
13933
|
+
return true;
|
|
13934
|
+
return false;
|
|
13935
|
+
}
|
|
13936
|
+
function printNudge(io) {
|
|
13937
|
+
io.err('This looks like a platform issue, not something you did — you can report it with `launchpad bug "<what you were trying to do>"` (the last command is attached automatically; don\'t paste secrets).');
|
|
13938
|
+
}
|
|
13939
|
+
function maybeReportNudge(io, ctx) {
|
|
13940
|
+
if (nudgeSuppressed(ctx))
|
|
13941
|
+
return;
|
|
13942
|
+
if (!classifyFault({ exitCode: ctx.exitCode, lastFault: ctx.lastFault }))
|
|
13943
|
+
return;
|
|
13944
|
+
printNudge(io);
|
|
13945
|
+
}
|
|
13946
|
+
function maybeCrashReportNudge(io, ctx) {
|
|
13947
|
+
if (nudgeSuppressed(ctx))
|
|
13948
|
+
return;
|
|
13949
|
+
printNudge(io);
|
|
13950
|
+
}
|
|
13951
|
+
|
|
13723
13952
|
// src/cli.ts
|
|
13724
13953
|
var io = {
|
|
13725
13954
|
out: (line) => {
|
|
@@ -13737,6 +13966,13 @@ dispatch(args, io).then((code) => {
|
|
|
13737
13966
|
const elapsedMs = Date.now() - startedAt;
|
|
13738
13967
|
process.exitCode = code;
|
|
13739
13968
|
notifyAfterCommand(io, args);
|
|
13969
|
+
maybeReportNudge(io, {
|
|
13970
|
+
argv: args,
|
|
13971
|
+
env: process.env,
|
|
13972
|
+
stderrIsTTY: Boolean(process.stderr.isTTY),
|
|
13973
|
+
exitCode: code,
|
|
13974
|
+
lastFault: peekFault()
|
|
13975
|
+
});
|
|
13740
13976
|
maybeFirstRunNoticeReal(io, args);
|
|
13741
13977
|
recordAfterCommandReal(args, code, elapsedMs);
|
|
13742
13978
|
}).catch((err) => {
|
|
@@ -13745,6 +13981,11 @@ dispatch(args, io).then((code) => {
|
|
|
13745
13981
|
process.stderr.write(`launchpad: unexpected error: ${msg}
|
|
13746
13982
|
`);
|
|
13747
13983
|
process.exitCode = 70;
|
|
13984
|
+
maybeCrashReportNudge(io, {
|
|
13985
|
+
argv: args,
|
|
13986
|
+
env: process.env,
|
|
13987
|
+
stderrIsTTY: Boolean(process.stderr.isTTY)
|
|
13988
|
+
});
|
|
13748
13989
|
maybeFirstRunNoticeReal(io, args);
|
|
13749
13990
|
recordAfterCommandReal(args, 70, elapsedMs);
|
|
13750
13991
|
});
|