@m-kopa/launchpad-cli 0.25.0 → 0.27.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 +116 -0
- package/dist/auth/flow.d.ts +7 -3
- package/dist/auth/flow.d.ts.map +1 -1
- package/dist/auth/gateway-flow.d.ts +76 -0
- package/dist/auth/gateway-flow.d.ts.map +1 -0
- package/dist/auth/session.d.ts +35 -2
- package/dist/auth/session.d.ts.map +1 -1
- package/dist/cli.js +871 -331
- package/dist/commands/deploy.d.ts +10 -13
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/destroy.d.ts +1 -1
- package/dist/commands/destroy.d.ts.map +1 -1
- package/dist/commands/envvars.d.ts +2 -2
- package/dist/commands/envvars.d.ts.map +1 -1
- package/dist/commands/infer-slug.d.ts +37 -0
- package/dist/commands/infer-slug.d.ts.map +1 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/logout.d.ts +7 -0
- package/dist/commands/logout.d.ts.map +1 -1
- package/dist/commands/logs.d.ts +2 -2
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/merge.d.ts +2 -2
- package/dist/commands/merge.d.ts.map +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/recover.d.ts +12 -0
- package/dist/commands/recover.d.ts.map +1 -0
- package/dist/commands/review.d.ts +2 -2
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/status.d.ts +13 -3
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/launchpad-content-pr/SKILL.md +1 -1
- package/skills/launchpad-deploy/SKILL.md +1 -1
- package/skills/launchpad-deploy-status/SKILL.md +6 -4
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +13 -6
package/dist/cli.js
CHANGED
|
@@ -19,21 +19,33 @@ 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.27.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
26
26
|
import * as path from "node:path";
|
|
27
27
|
var DEFAULT_BOT_URL = "https://launchpad-portal-bot.mkopa-launchpad.workers.dev";
|
|
28
|
+
var DEFAULT_AUTH_GATEWAY_URL = "https://auth.launchpad.m-kopa.us";
|
|
28
29
|
function loadConfig(env = process.env) {
|
|
29
30
|
const rawBotUrl = env.LAUNCHPAD_BOT_URL ?? DEFAULT_BOT_URL;
|
|
30
31
|
const botUrl = rawBotUrl.replace(/\/+$/, "");
|
|
32
|
+
const rawGatewayUrl = env.LAUNCHPAD_AUTH_GATEWAY_URL ?? DEFAULT_AUTH_GATEWAY_URL;
|
|
33
|
+
const authGatewayUrl = rawGatewayUrl.replace(/\/+$/, "");
|
|
34
|
+
const authLegacy = env.LAUNCHPAD_AUTH_LEGACY === "1";
|
|
31
35
|
const sessionPath = env.LAUNCHPAD_SESSION_PATH ?? path.join(os.homedir(), ".launchpad", "session.json");
|
|
32
36
|
const cacheDir = env.LAUNCHPAD_CACHE_DIR ?? path.join(os.homedir(), ".launchpad", "cache");
|
|
33
37
|
const stateDir = env.LAUNCHPAD_STATE_DIR ?? path.join(os.homedir(), ".launchpad", "state");
|
|
34
38
|
const rawPlatformRepo = env.LAUNCHPAD_PLATFORM_REPO;
|
|
35
39
|
const platformRepoPath = rawPlatformRepo !== undefined && rawPlatformRepo.length > 0 ? path.resolve(rawPlatformRepo) : null;
|
|
36
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
botUrl,
|
|
42
|
+
authGatewayUrl,
|
|
43
|
+
authLegacy,
|
|
44
|
+
sessionPath,
|
|
45
|
+
cacheDir,
|
|
46
|
+
stateDir,
|
|
47
|
+
platformRepoPath
|
|
48
|
+
};
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
// src/http/errors.ts
|
|
@@ -64,7 +76,7 @@ class TransportError extends Error {
|
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
// src/auth/flow.ts
|
|
67
|
-
import { randomBytes as
|
|
79
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
68
80
|
|
|
69
81
|
// src/auth/callback-server.ts
|
|
70
82
|
import { createServer } from "node:http";
|
|
@@ -371,6 +383,10 @@ import * as fs from "node:fs/promises";
|
|
|
371
383
|
import * as path2 from "node:path";
|
|
372
384
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
373
385
|
var SESSION_VERSION = 1;
|
|
386
|
+
var GATEWAY_SESSION_VERSION = 2;
|
|
387
|
+
function isGatewaySession(s) {
|
|
388
|
+
return s.version === GATEWAY_SESSION_VERSION;
|
|
389
|
+
}
|
|
374
390
|
|
|
375
391
|
class SessionParseError extends Error {
|
|
376
392
|
code = "session_parse_error";
|
|
@@ -394,8 +410,11 @@ async function readSession(sessionPath) {
|
|
|
394
410
|
throw new SessionParseError(`session file at ${sessionPath} is not an object`);
|
|
395
411
|
}
|
|
396
412
|
const obj = parsed;
|
|
413
|
+
if (obj.version === GATEWAY_SESSION_VERSION) {
|
|
414
|
+
return parseGatewaySession(obj, sessionPath);
|
|
415
|
+
}
|
|
397
416
|
if (obj.version !== SESSION_VERSION) {
|
|
398
|
-
throw new SessionParseError(`unsupported session version ${String(obj.version)} at ${sessionPath}; expected ${SESSION_VERSION}`);
|
|
417
|
+
throw new SessionParseError(`unsupported session version ${String(obj.version)} at ${sessionPath}; expected ${SESSION_VERSION} or ${GATEWAY_SESSION_VERSION}`);
|
|
399
418
|
}
|
|
400
419
|
for (const k of [
|
|
401
420
|
"accessToken",
|
|
@@ -416,6 +435,20 @@ async function readSession(sessionPath) {
|
|
|
416
435
|
}
|
|
417
436
|
return obj;
|
|
418
437
|
}
|
|
438
|
+
function parseGatewaySession(obj, sessionPath) {
|
|
439
|
+
if (obj.kind !== "gateway") {
|
|
440
|
+
throw new SessionParseError(`session file at ${sessionPath}: version 2 with unknown kind ${String(obj.kind)}`);
|
|
441
|
+
}
|
|
442
|
+
for (const k of ["accessToken", "refreshToken", "gatewayUrl", "issuedAt"]) {
|
|
443
|
+
if (typeof obj[k] !== "string") {
|
|
444
|
+
throw new SessionParseError(`session file at ${sessionPath}: missing or non-string ${k}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (typeof obj.accessTokenExpiresAt !== "number") {
|
|
448
|
+
throw new SessionParseError(`session file at ${sessionPath}: missing or non-number accessTokenExpiresAt`);
|
|
449
|
+
}
|
|
450
|
+
return obj;
|
|
451
|
+
}
|
|
419
452
|
async function writeSession(sessionPath, session) {
|
|
420
453
|
const dir = path2.dirname(sessionPath);
|
|
421
454
|
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
@@ -453,6 +486,9 @@ function describe4(e) {
|
|
|
453
486
|
return e instanceof Error ? e.message : String(e);
|
|
454
487
|
}
|
|
455
488
|
|
|
489
|
+
// src/auth/gateway-flow.ts
|
|
490
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
491
|
+
|
|
456
492
|
// src/auth/browser.ts
|
|
457
493
|
import { spawn } from "node:child_process";
|
|
458
494
|
|
|
@@ -487,16 +523,185 @@ function chooseOpener(platform, url) {
|
|
|
487
523
|
}
|
|
488
524
|
}
|
|
489
525
|
|
|
526
|
+
// src/auth/gateway-flow.ts
|
|
527
|
+
var CLI_SESSION_AUTH_PATH = "/__cli_session_auth";
|
|
528
|
+
var CLI_SESSION_TOKEN_PATH = "/__cli_session_token";
|
|
529
|
+
var CLI_LOGOUT_PATH = "/__cli_logout";
|
|
530
|
+
|
|
531
|
+
class GatewayUnavailableError extends Error {
|
|
532
|
+
code = "gateway_unavailable";
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
class GatewayTokenError extends Error {
|
|
536
|
+
code = "gateway_token_error";
|
|
537
|
+
httpStatus;
|
|
538
|
+
constructor(message, httpStatus) {
|
|
539
|
+
super(message);
|
|
540
|
+
this.name = "GatewayTokenError";
|
|
541
|
+
if (httpStatus !== undefined)
|
|
542
|
+
this.httpStatus = httpStatus;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
class GatewayRateLimitError extends Error {
|
|
547
|
+
code = "gateway_rate_limited";
|
|
548
|
+
retryAfterSec;
|
|
549
|
+
constructor(retryAfterSec) {
|
|
550
|
+
super(`gateway rate-limited the request — retry in ${retryAfterSec}s (your session is intact)`);
|
|
551
|
+
this.name = "GatewayRateLimitError";
|
|
552
|
+
this.retryAfterSec = retryAfterSec;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async function gatewayLogin(opts) {
|
|
556
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
557
|
+
const opener = opts.browserOpener ?? ((url) => openBrowser(url));
|
|
558
|
+
await probeGateway(opts.gatewayUrl, fetcher);
|
|
559
|
+
const state = randomBytes3(16).toString("hex");
|
|
560
|
+
const server = await bindCallbackServer(state);
|
|
561
|
+
try {
|
|
562
|
+
const redirectUri = `http://127.0.0.1:${server.port}/callback`;
|
|
563
|
+
const pkce = generatePkcePair();
|
|
564
|
+
const u = new URL(`${opts.gatewayUrl}${CLI_SESSION_AUTH_PATH}`);
|
|
565
|
+
u.searchParams.set("cb", redirectUri);
|
|
566
|
+
u.searchParams.set("code_challenge", pkce.challenge);
|
|
567
|
+
u.searchParams.set("state", state);
|
|
568
|
+
const authUrl = u.toString();
|
|
569
|
+
opts.onAuthUrl?.(authUrl);
|
|
570
|
+
try {
|
|
571
|
+
await opener(authUrl);
|
|
572
|
+
} catch (e) {
|
|
573
|
+
opts.onAuthUrl?.(`(could not auto-open browser: ${describe5(e)} — copy the URL above into a browser instead)`);
|
|
574
|
+
}
|
|
575
|
+
const callback = await server.result;
|
|
576
|
+
const pair = await postSessionTokenForm(opts.gatewayUrl, new URLSearchParams({
|
|
577
|
+
grant_type: "authorization_code",
|
|
578
|
+
code: callback.code,
|
|
579
|
+
code_verifier: pkce.verifier
|
|
580
|
+
}), fetcher);
|
|
581
|
+
const session = {
|
|
582
|
+
version: GATEWAY_SESSION_VERSION,
|
|
583
|
+
kind: "gateway",
|
|
584
|
+
accessToken: pair.accessToken,
|
|
585
|
+
refreshToken: pair.refreshToken,
|
|
586
|
+
accessTokenExpiresAt: Date.now() + pair.expiresInSec * 1000,
|
|
587
|
+
gatewayUrl: opts.gatewayUrl,
|
|
588
|
+
issuedAt: new Date().toISOString()
|
|
589
|
+
};
|
|
590
|
+
await writeSession(opts.sessionPath, session);
|
|
591
|
+
return session;
|
|
592
|
+
} finally {
|
|
593
|
+
await server.close();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function refreshGatewayTokens(params, fetcher = fetch) {
|
|
597
|
+
return postSessionTokenForm(params.gatewayUrl, new URLSearchParams({
|
|
598
|
+
grant_type: "refresh_token",
|
|
599
|
+
refresh_token: params.refreshToken
|
|
600
|
+
}), fetcher);
|
|
601
|
+
}
|
|
602
|
+
async function revokeGatewaySession(params, fetcher = fetch) {
|
|
603
|
+
const url = `${params.gatewayUrl}${CLI_LOGOUT_PATH}`;
|
|
604
|
+
let res;
|
|
605
|
+
try {
|
|
606
|
+
res = await fetcher(url, {
|
|
607
|
+
method: "POST",
|
|
608
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
609
|
+
body: new URLSearchParams({ refresh_token: params.refreshToken }).toString()
|
|
610
|
+
});
|
|
611
|
+
} catch (e) {
|
|
612
|
+
throw new GatewayTokenError(`logout endpoint: network error: ${describe5(e)}`);
|
|
613
|
+
}
|
|
614
|
+
if (res.status === 429) {
|
|
615
|
+
throw new GatewayRateLimitError(readRetryAfter(res));
|
|
616
|
+
}
|
|
617
|
+
if (res.status !== 204 && !res.ok) {
|
|
618
|
+
const detail = await res.text().catch(() => "");
|
|
619
|
+
throw new GatewayTokenError(`logout endpoint returned HTTP ${res.status}: ${detail.slice(0, 200)}`, res.status);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function probeGateway(gatewayUrl, fetcher) {
|
|
623
|
+
const url = `${gatewayUrl}${CLI_SESSION_AUTH_PATH}`;
|
|
624
|
+
let res;
|
|
625
|
+
try {
|
|
626
|
+
res = await fetcher(url, { method: "GET" });
|
|
627
|
+
} catch (e) {
|
|
628
|
+
throw new GatewayUnavailableError(`auth gateway unreachable at ${gatewayUrl}: ${describe5(e)}`);
|
|
629
|
+
}
|
|
630
|
+
if (res.status === 429) {
|
|
631
|
+
throw new GatewayRateLimitError(readRetryAfter(res));
|
|
632
|
+
}
|
|
633
|
+
if (res.status !== 400) {
|
|
634
|
+
throw new GatewayUnavailableError(`auth gateway at ${gatewayUrl} is not serving the cli-session grant (HTTP ${res.status} from ${CLI_SESSION_AUTH_PATH})`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async function postSessionTokenForm(gatewayUrl, form, fetcher) {
|
|
638
|
+
const url = `${gatewayUrl}${CLI_SESSION_TOKEN_PATH}`;
|
|
639
|
+
let res;
|
|
640
|
+
try {
|
|
641
|
+
res = await fetcher(url, {
|
|
642
|
+
method: "POST",
|
|
643
|
+
headers: {
|
|
644
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
645
|
+
accept: "application/json"
|
|
646
|
+
},
|
|
647
|
+
body: form.toString()
|
|
648
|
+
});
|
|
649
|
+
} catch (e) {
|
|
650
|
+
throw new GatewayTokenError(`session token endpoint: network error: ${describe5(e)}`);
|
|
651
|
+
}
|
|
652
|
+
if (res.status === 429) {
|
|
653
|
+
throw new GatewayRateLimitError(readRetryAfter(res));
|
|
654
|
+
}
|
|
655
|
+
if (!res.ok) {
|
|
656
|
+
const detail = await res.text().catch(() => "");
|
|
657
|
+
throw new GatewayTokenError(`session token endpoint returned HTTP ${res.status}: ${detail.slice(0, 200)}`, res.status);
|
|
658
|
+
}
|
|
659
|
+
let parsed;
|
|
660
|
+
try {
|
|
661
|
+
parsed = await res.json();
|
|
662
|
+
} catch (e) {
|
|
663
|
+
throw new GatewayTokenError(`session token endpoint: non-JSON response: ${describe5(e)}`);
|
|
664
|
+
}
|
|
665
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
666
|
+
throw new GatewayTokenError(`session token endpoint: response is not an object`);
|
|
667
|
+
}
|
|
668
|
+
const obj = parsed;
|
|
669
|
+
if (typeof obj.access_token !== "string") {
|
|
670
|
+
throw new GatewayTokenError(`session token endpoint: response missing access_token`);
|
|
671
|
+
}
|
|
672
|
+
if (typeof obj.refresh_token !== "string") {
|
|
673
|
+
throw new GatewayTokenError(`session token endpoint: response missing refresh_token`);
|
|
674
|
+
}
|
|
675
|
+
const expiresIn = obj.expires_in;
|
|
676
|
+
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
677
|
+
throw new GatewayTokenError(`session token endpoint: response missing positive finite numeric expires_in (got ${String(expiresIn)})`);
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
accessToken: obj.access_token,
|
|
681
|
+
refreshToken: obj.refresh_token,
|
|
682
|
+
expiresInSec: expiresIn
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function readRetryAfter(res) {
|
|
686
|
+
const raw = res.headers.get("retry-after");
|
|
687
|
+
const n = raw === null ? NaN : Number.parseInt(raw, 10);
|
|
688
|
+
return Number.isFinite(n) && n > 0 ? n : 60;
|
|
689
|
+
}
|
|
690
|
+
function describe5(e) {
|
|
691
|
+
return e instanceof Error ? e.message : String(e);
|
|
692
|
+
}
|
|
693
|
+
|
|
490
694
|
// src/auth/flow.ts
|
|
491
695
|
class LoginRequiredError extends Error {
|
|
492
696
|
code = "login_required";
|
|
493
697
|
}
|
|
494
698
|
var REFRESH_SKEW_MS = 30000;
|
|
699
|
+
var MAX_ABSORBED_RETRY_AFTER_SEC = 10;
|
|
495
700
|
async function login(opts) {
|
|
496
701
|
const fetcher = opts.fetcher ?? fetch;
|
|
497
702
|
const opener = opts.browserOpener ?? ((url) => openBrowser(url));
|
|
498
703
|
const endpoints = await discoverOauthEndpoints(opts.botUrl, fetcher);
|
|
499
|
-
const state =
|
|
704
|
+
const state = randomBytes4(16).toString("hex");
|
|
500
705
|
const server = await bindCallbackServer(state);
|
|
501
706
|
try {
|
|
502
707
|
const redirectUri = `http://127.0.0.1:${server.port}/callback`;
|
|
@@ -514,7 +719,7 @@ async function login(opts) {
|
|
|
514
719
|
try {
|
|
515
720
|
await opener(authUrl);
|
|
516
721
|
} catch (e) {
|
|
517
|
-
opts.onAuthUrl?.(`(could not auto-open browser: ${
|
|
722
|
+
opts.onAuthUrl?.(`(could not auto-open browser: ${describe6(e)} — copy the URL above into a browser instead)`);
|
|
518
723
|
}
|
|
519
724
|
const callback = await server.result;
|
|
520
725
|
const tokens = await exchangeCodeForTokens({
|
|
@@ -541,7 +746,7 @@ async function login(opts) {
|
|
|
541
746
|
await server.close();
|
|
542
747
|
}
|
|
543
748
|
}
|
|
544
|
-
async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now) {
|
|
749
|
+
async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {
|
|
545
750
|
const session = await readSession(sessionPath);
|
|
546
751
|
if (session === null) {
|
|
547
752
|
throw new LoginRequiredError("no session — run `launchpad login`");
|
|
@@ -549,6 +754,9 @@ async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now)
|
|
|
549
754
|
if (session.accessTokenExpiresAt - REFRESH_SKEW_MS > now()) {
|
|
550
755
|
return { accessToken: session.accessToken, session };
|
|
551
756
|
}
|
|
757
|
+
if (isGatewaySession(session)) {
|
|
758
|
+
return refreshGatewaySession(session, sessionPath, fetcher, now, sleep);
|
|
759
|
+
}
|
|
552
760
|
if (session.resource === undefined) {
|
|
553
761
|
throw new LoginRequiredError("session was written by a pre-0.7.1 CLI and is missing the resource indicator — run `launchpad login`");
|
|
554
762
|
}
|
|
@@ -576,7 +784,39 @@ async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now)
|
|
|
576
784
|
await writeSession(sessionPath, refreshed);
|
|
577
785
|
return { accessToken: refreshed.accessToken, session: refreshed };
|
|
578
786
|
}
|
|
579
|
-
function
|
|
787
|
+
async function refreshGatewaySession(session, sessionPath, fetcher, now, sleep) {
|
|
788
|
+
let pair;
|
|
789
|
+
try {
|
|
790
|
+
try {
|
|
791
|
+
pair = await refreshGatewayTokens({ gatewayUrl: session.gatewayUrl, refreshToken: session.refreshToken }, fetcher);
|
|
792
|
+
} catch (e) {
|
|
793
|
+
if (e instanceof GatewayRateLimitError && e.retryAfterSec <= MAX_ABSORBED_RETRY_AFTER_SEC) {
|
|
794
|
+
await sleep(e.retryAfterSec * 1000);
|
|
795
|
+
pair = await refreshGatewayTokens({ gatewayUrl: session.gatewayUrl, refreshToken: session.refreshToken }, fetcher);
|
|
796
|
+
} else {
|
|
797
|
+
throw e;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
} catch (e) {
|
|
801
|
+
if (e instanceof GatewayTokenError && (e.httpStatus === 400 || e.httpStatus === 401)) {
|
|
802
|
+
await clearSession(sessionPath).catch(() => {
|
|
803
|
+
return;
|
|
804
|
+
});
|
|
805
|
+
throw new LoginRequiredError(`session revoked or expired — run \`launchpad login\` (gateway said: ${e.message})`);
|
|
806
|
+
}
|
|
807
|
+
throw e;
|
|
808
|
+
}
|
|
809
|
+
const refreshed = {
|
|
810
|
+
...session,
|
|
811
|
+
accessToken: pair.accessToken,
|
|
812
|
+
refreshToken: pair.refreshToken,
|
|
813
|
+
accessTokenExpiresAt: now() + pair.expiresInSec * 1000,
|
|
814
|
+
issuedAt: new Date(now()).toISOString()
|
|
815
|
+
};
|
|
816
|
+
await writeSession(sessionPath, refreshed);
|
|
817
|
+
return { accessToken: refreshed.accessToken, session: refreshed };
|
|
818
|
+
}
|
|
819
|
+
function describe6(e) {
|
|
580
820
|
return e instanceof Error ? e.message : String(e);
|
|
581
821
|
}
|
|
582
822
|
function buildAuthorizationUrl(params) {
|
|
@@ -598,7 +838,7 @@ async function apiJson(cfg, opts, fetcher = fetch) {
|
|
|
598
838
|
try {
|
|
599
839
|
parsed = await res.json();
|
|
600
840
|
} catch (e) {
|
|
601
|
-
throw new TransportError(`bot returned non-JSON body for ${opts.path}: ${
|
|
841
|
+
throw new TransportError(`bot returned non-JSON body for ${opts.path}: ${describe7(e)}`);
|
|
602
842
|
}
|
|
603
843
|
return parsed;
|
|
604
844
|
}
|
|
@@ -640,7 +880,7 @@ async function apiRaw(cfg, opts, fetcher = fetch) {
|
|
|
640
880
|
try {
|
|
641
881
|
res = await fetcher(url, init);
|
|
642
882
|
} catch (e) {
|
|
643
|
-
throw new TransportError(`network error calling ${url}: ${
|
|
883
|
+
throw new TransportError(`network error calling ${url}: ${describe7(e)}`);
|
|
644
884
|
}
|
|
645
885
|
if (res.status === 401) {
|
|
646
886
|
const detail = await peek(res);
|
|
@@ -671,7 +911,7 @@ async function peek(res) {
|
|
|
671
911
|
return "";
|
|
672
912
|
}
|
|
673
913
|
}
|
|
674
|
-
function
|
|
914
|
+
function describe7(e) {
|
|
675
915
|
return e instanceof Error ? e.message : String(e);
|
|
676
916
|
}
|
|
677
917
|
|
|
@@ -701,7 +941,7 @@ async function runApps(_args, io) {
|
|
|
701
941
|
io.err(e.message);
|
|
702
942
|
return 1;
|
|
703
943
|
}
|
|
704
|
-
io.err(`launchpad apps failed: ${
|
|
944
|
+
io.err(`launchpad apps failed: ${describe8(e)}`);
|
|
705
945
|
return 1;
|
|
706
946
|
}
|
|
707
947
|
}
|
|
@@ -749,7 +989,7 @@ function formatRelative(iso) {
|
|
|
749
989
|
return `${hr}h ago`;
|
|
750
990
|
return `${Math.floor(hr / 24)}d ago`;
|
|
751
991
|
}
|
|
752
|
-
function
|
|
992
|
+
function describe8(e) {
|
|
753
993
|
return e instanceof Error ? e.message : String(e);
|
|
754
994
|
}
|
|
755
995
|
|
|
@@ -1046,7 +1286,7 @@ async function runClone(args, io) {
|
|
|
1046
1286
|
io.err(`launchpad clone: ${e.message}`);
|
|
1047
1287
|
return 1;
|
|
1048
1288
|
}
|
|
1049
|
-
io.err(`launchpad clone failed: ${
|
|
1289
|
+
io.err(`launchpad clone failed: ${describe9(e)}`);
|
|
1050
1290
|
return 1;
|
|
1051
1291
|
}
|
|
1052
1292
|
}
|
|
@@ -1063,7 +1303,7 @@ function formatBytes(n) {
|
|
|
1063
1303
|
return `${(n / 1024).toFixed(1)}KB`;
|
|
1064
1304
|
return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
1065
1305
|
}
|
|
1066
|
-
function
|
|
1306
|
+
function describe9(e) {
|
|
1067
1307
|
return e instanceof Error ? e.message : String(e);
|
|
1068
1308
|
}
|
|
1069
1309
|
|
|
@@ -1129,7 +1369,7 @@ async function runCreate(args, io) {
|
|
|
1129
1369
|
io.err(`launchpad create: ${e.message}`);
|
|
1130
1370
|
return 1;
|
|
1131
1371
|
}
|
|
1132
|
-
io.err(`launchpad create failed: ${
|
|
1372
|
+
io.err(`launchpad create failed: ${describe10(e)}`);
|
|
1133
1373
|
return 1;
|
|
1134
1374
|
}
|
|
1135
1375
|
}
|
|
@@ -1214,13 +1454,13 @@ function printUsage(io) {
|
|
|
1214
1454
|
].join(`
|
|
1215
1455
|
`));
|
|
1216
1456
|
}
|
|
1217
|
-
function
|
|
1457
|
+
function describe10(e) {
|
|
1218
1458
|
return e instanceof Error ? e.message : String(e);
|
|
1219
1459
|
}
|
|
1220
1460
|
|
|
1221
1461
|
// src/commands/deploy.ts
|
|
1222
|
-
import { existsSync as
|
|
1223
|
-
import * as
|
|
1462
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
1463
|
+
import * as path7 from "node:path";
|
|
1224
1464
|
|
|
1225
1465
|
// src/bundle/orchestrate.ts
|
|
1226
1466
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
@@ -3306,8 +3546,8 @@ async function bundleAndDeploy(args) {
|
|
|
3306
3546
|
}
|
|
3307
3547
|
|
|
3308
3548
|
// src/commands/deploy.ts
|
|
3309
|
-
import { parse as
|
|
3310
|
-
import { readFileSync as
|
|
3549
|
+
import { parse as parseYaml5 } from "yaml";
|
|
3550
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
3311
3551
|
|
|
3312
3552
|
// src/deploy/git-files.ts
|
|
3313
3553
|
import { spawn as spawn3 } from "node:child_process";
|
|
@@ -3882,7 +4122,7 @@ async function pollUntilApplied(args) {
|
|
|
3882
4122
|
path: `/apps/${args.slug}/manifest/state`
|
|
3883
4123
|
}, args.fetcher);
|
|
3884
4124
|
} catch (e) {
|
|
3885
|
-
args.io.err(`! state fetch failed (will retry): ${
|
|
4125
|
+
args.io.err(`! state fetch failed (will retry): ${describe11(e)}`);
|
|
3886
4126
|
await sleep(args.pollIntervalSec * 1000);
|
|
3887
4127
|
continue;
|
|
3888
4128
|
}
|
|
@@ -3942,7 +4182,7 @@ function deletePinIfPresent(cfg, slug, io) {
|
|
|
3942
4182
|
rmSync(pinPath, { force: true });
|
|
3943
4183
|
io.out(` (cleaned up obsolete pin file ${pinPath})`);
|
|
3944
4184
|
} catch (e) {
|
|
3945
|
-
io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${
|
|
4185
|
+
io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${describe11(e)}`);
|
|
3946
4186
|
}
|
|
3947
4187
|
}
|
|
3948
4188
|
async function loadSlugForResume(opts, io) {
|
|
@@ -3966,7 +4206,7 @@ async function defaultPrompt(question) {
|
|
|
3966
4206
|
rl.close();
|
|
3967
4207
|
}
|
|
3968
4208
|
}
|
|
3969
|
-
function
|
|
4209
|
+
function describe11(e) {
|
|
3970
4210
|
return e instanceof Error ? e.message : String(e);
|
|
3971
4211
|
}
|
|
3972
4212
|
function mapHttpError(e, slug, io) {
|
|
@@ -3990,7 +4230,7 @@ function mapHttpError(e, slug, io) {
|
|
|
3990
4230
|
io.err(`launchpad deploy --apply: ${e.message}`);
|
|
3991
4231
|
return 1;
|
|
3992
4232
|
}
|
|
3993
|
-
io.err(`launchpad deploy --apply failed: ${
|
|
4233
|
+
io.err(`launchpad deploy --apply failed: ${describe11(e)}`);
|
|
3994
4234
|
return 2;
|
|
3995
4235
|
}
|
|
3996
4236
|
function renderManifestError(loaded, io) {
|
|
@@ -4032,7 +4272,7 @@ async function runDeployDryRun(opts, io, deps = {}) {
|
|
|
4032
4272
|
try {
|
|
4033
4273
|
manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha)(process.cwd());
|
|
4034
4274
|
} catch (e) {
|
|
4035
|
-
const msg = `failed to resolve git HEAD in ${process.cwd()}: ${
|
|
4275
|
+
const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe12(e)}`;
|
|
4036
4276
|
if (opts.json) {
|
|
4037
4277
|
io.out(JSON.stringify({ ok: false, kind: "git-error", message: msg }));
|
|
4038
4278
|
} else {
|
|
@@ -4081,7 +4321,7 @@ function defaultGitHeadSha(cwd) {
|
|
|
4081
4321
|
});
|
|
4082
4322
|
return out.trim();
|
|
4083
4323
|
}
|
|
4084
|
-
function
|
|
4324
|
+
function describe12(e) {
|
|
4085
4325
|
return e instanceof Error ? e.message : String(e);
|
|
4086
4326
|
}
|
|
4087
4327
|
function mapHttpError2(e, slug, json, io) {
|
|
@@ -4367,6 +4607,46 @@ function handleNetworkError(e, io, slug, verb) {
|
|
|
4367
4607
|
return 1;
|
|
4368
4608
|
}
|
|
4369
4609
|
|
|
4610
|
+
// src/commands/infer-slug.ts
|
|
4611
|
+
import * as path6 from "node:path";
|
|
4612
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
4613
|
+
import { parse as parseYaml4 } from "yaml";
|
|
4614
|
+
var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
4615
|
+
function inferSlugFromCwd(cwd) {
|
|
4616
|
+
const base = path6.basename(cwd);
|
|
4617
|
+
const m = base.match(DIRNAME_RE);
|
|
4618
|
+
return m === null ? null : m[1];
|
|
4619
|
+
}
|
|
4620
|
+
function resolveManifestSlug(parsed) {
|
|
4621
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.metadata !== "object" || parsed.metadata === null) {
|
|
4622
|
+
return null;
|
|
4623
|
+
}
|
|
4624
|
+
const meta = parsed.metadata;
|
|
4625
|
+
if (typeof meta.slug === "string")
|
|
4626
|
+
return meta.slug;
|
|
4627
|
+
if (typeof meta.name === "string")
|
|
4628
|
+
return meta.name;
|
|
4629
|
+
return null;
|
|
4630
|
+
}
|
|
4631
|
+
function inferSlugFromManifestFile(manifestPath) {
|
|
4632
|
+
if (!existsSync4(manifestPath))
|
|
4633
|
+
return null;
|
|
4634
|
+
try {
|
|
4635
|
+
return resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
|
|
4636
|
+
} catch {
|
|
4637
|
+
return null;
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
function inferSlug(opts) {
|
|
4641
|
+
const manifestPath = path6.resolve(opts.cwd, opts.file ?? "launchpad.yaml");
|
|
4642
|
+
const fromManifest = inferSlugFromManifestFile(manifestPath);
|
|
4643
|
+
const fromDir = inferSlugFromCwd(opts.cwd);
|
|
4644
|
+
if (fromManifest !== null && fromDir !== null && fromManifest !== fromDir) {
|
|
4645
|
+
opts.warn?.(`note: directory name suggests "${fromDir}" but ${path6.basename(manifestPath)} ` + `declares "${fromManifest}" — using the manifest. ` + `(Pass <slug> or --slug to override.)`);
|
|
4646
|
+
}
|
|
4647
|
+
return fromManifest ?? fromDir;
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4370
4650
|
// src/commands/deploy.ts
|
|
4371
4651
|
var deployCommand = {
|
|
4372
4652
|
name: "deploy",
|
|
@@ -4374,27 +4654,17 @@ var deployCommand = {
|
|
|
4374
4654
|
run: runDeploy
|
|
4375
4655
|
};
|
|
4376
4656
|
var SLUG_RE4 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
4377
|
-
var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
4378
4657
|
async function runDeploy(args, io) {
|
|
4379
4658
|
const flags = parseDeployFlags(args);
|
|
4380
4659
|
if (typeof flags === "string") {
|
|
4381
4660
|
io.err(`launchpad deploy: ${flags}`);
|
|
4661
|
+
const isFlagMisuse = flags.includes("is only valid with") || flags.includes("does not accept") || flags.includes("mutually exclusive");
|
|
4662
|
+
if (isFlagMisuse) {
|
|
4663
|
+
io.err(" (run `launchpad deploy` with no flags from your app directory for a content deploy)");
|
|
4664
|
+
return 64;
|
|
4665
|
+
}
|
|
4382
4666
|
io.err("");
|
|
4383
|
-
io.err(
|
|
4384
|
-
` + " (slug defaults to the current directory's `launchpad-app-<slug>` suffix)\n" + `
|
|
4385
|
-
` + `M-892 modes:
|
|
4386
|
-
` + ` launchpad deploy --resume <slug>
|
|
4387
|
-
` + ` launchpad deploy --abandon <slug>
|
|
4388
|
-
` + ` launchpad deploy --new --slug <slug> --display-name <name>
|
|
4389
|
-
` + ` --app-type <${APP_TYPES.join("|")}>
|
|
4390
|
-
` + ` --allowed-group <G_KEY> [--allowed-group ...]
|
|
4391
|
-
` + `
|
|
4392
|
-
` + `Scope 6 dry-run (manifest-driven preview, read-only):
|
|
4393
|
-
` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
|
|
4394
|
-
` + `
|
|
4395
|
-
` + `Scope 6 apply (manifest-driven, writes TF + runs terraform apply):
|
|
4396
|
-
` + ` launchpad deploy --apply --platform-repo <path>
|
|
4397
|
-
` + " [--file <manifest>] [--re-pin] [--yes]");
|
|
4667
|
+
io.err(deployUsage());
|
|
4398
4668
|
return 64;
|
|
4399
4669
|
}
|
|
4400
4670
|
if (flags.mode.kind === "dry-run") {
|
|
@@ -4428,8 +4698,8 @@ async function runDeploy(args, io) {
|
|
|
4428
4698
|
}, io);
|
|
4429
4699
|
}
|
|
4430
4700
|
const cwd = process.cwd();
|
|
4431
|
-
const manifestPath =
|
|
4432
|
-
if (
|
|
4701
|
+
const manifestPath = path7.join(cwd, "launchpad.yaml");
|
|
4702
|
+
if (existsSync5(manifestPath)) {
|
|
4433
4703
|
return runModelADeploy({ cwd, manifestPath, argv: args, io });
|
|
4434
4704
|
}
|
|
4435
4705
|
const parsed = parseArgs2(args);
|
|
@@ -4441,9 +4711,9 @@ async function runDeploy(args, io) {
|
|
|
4441
4711
|
if (parsed.slug !== null) {
|
|
4442
4712
|
slug = parsed.slug;
|
|
4443
4713
|
} else {
|
|
4444
|
-
const inferred =
|
|
4714
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
4445
4715
|
if (inferred === null) {
|
|
4446
|
-
io.err(`launchpad deploy: could not infer slug from cwd (${
|
|
4716
|
+
io.err(`launchpad deploy: could not infer slug from cwd (${path7.basename(process.cwd())});
|
|
4447
4717
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
4448
4718
|
return 64;
|
|
4449
4719
|
}
|
|
@@ -4463,10 +4733,10 @@ async function runDeploy(args, io) {
|
|
|
4463
4733
|
return 1;
|
|
4464
4734
|
}
|
|
4465
4735
|
let contentManifestYaml = null;
|
|
4466
|
-
const contentManifestPath =
|
|
4467
|
-
if (
|
|
4736
|
+
const contentManifestPath = path7.join(process.cwd(), "launchpad.yaml");
|
|
4737
|
+
if (existsSync5(contentManifestPath)) {
|
|
4468
4738
|
try {
|
|
4469
|
-
contentManifestYaml =
|
|
4739
|
+
contentManifestYaml = readFileSync7(contentManifestPath, "utf8");
|
|
4470
4740
|
} catch {
|
|
4471
4741
|
contentManifestYaml = null;
|
|
4472
4742
|
}
|
|
@@ -4540,10 +4810,28 @@ async function runDeploy(args, io) {
|
|
|
4540
4810
|
io.err(`launchpad deploy: ${e.message}`);
|
|
4541
4811
|
return 1;
|
|
4542
4812
|
}
|
|
4543
|
-
io.err(`launchpad deploy failed: ${
|
|
4813
|
+
io.err(`launchpad deploy failed: ${describe13(e)}`);
|
|
4544
4814
|
return 1;
|
|
4545
4815
|
}
|
|
4546
4816
|
}
|
|
4817
|
+
function deployUsage() {
|
|
4818
|
+
return `usage: launchpad deploy [--message <text>] [--slug <slug>]
|
|
4819
|
+
` + ` (slug comes from launchpad.yaml at cwd when present — Model A —
|
|
4820
|
+
` + " else from the current directory's `launchpad-app-<slug>` suffix)\n" + `
|
|
4821
|
+
` + `provisioning modes:
|
|
4822
|
+
` + ` launchpad deploy --resume <slug>
|
|
4823
|
+
` + ` launchpad deploy --abandon <slug>
|
|
4824
|
+
` + ` launchpad deploy --new --slug <slug> --display-name <name>
|
|
4825
|
+
` + ` --app-type <${APP_TYPES.join("|")}>
|
|
4826
|
+
` + ` --allowed-group <G_KEY> [--allowed-group ...]
|
|
4827
|
+
` + `
|
|
4828
|
+
` + `manifest-driven dry-run (read-only preview):
|
|
4829
|
+
` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
|
|
4830
|
+
` + `
|
|
4831
|
+
` + `manifest-driven apply (runs server-side via portal-bot):
|
|
4832
|
+
` + ` launchpad deploy --apply [--file <manifest>] [--at <sha>] [--re-pin]
|
|
4833
|
+
` + " [--yes] [--resume-pr <n>] [--timeout-minutes <n>]";
|
|
4834
|
+
}
|
|
4547
4835
|
function parseArgs2(args) {
|
|
4548
4836
|
let message = null;
|
|
4549
4837
|
let slug = null;
|
|
@@ -4570,11 +4858,6 @@ function parseArgs2(args) {
|
|
|
4570
4858
|
}
|
|
4571
4859
|
return { slug, message };
|
|
4572
4860
|
}
|
|
4573
|
-
function inferSlugFromCwd(cwd) {
|
|
4574
|
-
const base = path6.basename(cwd);
|
|
4575
|
-
const m = base.match(DIRNAME_RE);
|
|
4576
|
-
return m === null ? null : m[1];
|
|
4577
|
-
}
|
|
4578
4861
|
function makeBytesFetcher(bytes, pathSuffix) {
|
|
4579
4862
|
return async (input, init) => {
|
|
4580
4863
|
const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
@@ -4599,7 +4882,7 @@ function formatBytes2(n) {
|
|
|
4599
4882
|
return `${(n / 1024).toFixed(1)}KB`;
|
|
4600
4883
|
return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
4601
4884
|
}
|
|
4602
|
-
function
|
|
4885
|
+
function describe13(e) {
|
|
4603
4886
|
return e instanceof Error ? e.message : String(e);
|
|
4604
4887
|
}
|
|
4605
4888
|
function surfaceDeployExtras(body, io, slug) {
|
|
@@ -4624,29 +4907,18 @@ function surfaceDeployExtras(body, io, slug) {
|
|
|
4624
4907
|
io.out(` Full list: \`launchpad status ${slug}\`.`);
|
|
4625
4908
|
}
|
|
4626
4909
|
}
|
|
4627
|
-
function resolveManifestSlug(parsed) {
|
|
4628
|
-
if (parsed === null || typeof parsed !== "object" || typeof parsed.metadata !== "object" || parsed.metadata === null) {
|
|
4629
|
-
return null;
|
|
4630
|
-
}
|
|
4631
|
-
const meta = parsed.metadata;
|
|
4632
|
-
if (typeof meta.slug === "string")
|
|
4633
|
-
return meta.slug;
|
|
4634
|
-
if (typeof meta.name === "string")
|
|
4635
|
-
return meta.name;
|
|
4636
|
-
return null;
|
|
4637
|
-
}
|
|
4638
4910
|
async function runModelADeploy(args) {
|
|
4639
4911
|
const { cwd, manifestPath, io } = args;
|
|
4640
4912
|
let slug;
|
|
4641
4913
|
try {
|
|
4642
|
-
const metaSlug = resolveManifestSlug(
|
|
4914
|
+
const metaSlug = resolveManifestSlug(parseYaml5(readFileSync7(manifestPath, "utf8")));
|
|
4643
4915
|
if (metaSlug === null) {
|
|
4644
4916
|
io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
|
|
4645
4917
|
return 64;
|
|
4646
4918
|
}
|
|
4647
4919
|
slug = metaSlug;
|
|
4648
4920
|
} catch (e) {
|
|
4649
|
-
io.err(`launchpad deploy: failed to read ${manifestPath}: ${
|
|
4921
|
+
io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe13(e)}`);
|
|
4650
4922
|
return 1;
|
|
4651
4923
|
}
|
|
4652
4924
|
if (!SLUG_RE4.test(slug)) {
|
|
@@ -4658,7 +4930,7 @@ async function runModelADeploy(args) {
|
|
|
4658
4930
|
try {
|
|
4659
4931
|
cfg = loadConfig();
|
|
4660
4932
|
} catch (e) {
|
|
4661
|
-
io.err(`launchpad deploy: ${
|
|
4933
|
+
io.err(`launchpad deploy: ${describe13(e)}`);
|
|
4662
4934
|
return 1;
|
|
4663
4935
|
}
|
|
4664
4936
|
let result;
|
|
@@ -4670,7 +4942,7 @@ async function runModelADeploy(args) {
|
|
|
4670
4942
|
io.err(" run `launchpad login` to refresh your session.");
|
|
4671
4943
|
return 1;
|
|
4672
4944
|
}
|
|
4673
|
-
io.err(`launchpad deploy: unexpected error: ${
|
|
4945
|
+
io.err(`launchpad deploy: unexpected error: ${describe13(e)}`);
|
|
4674
4946
|
return 1;
|
|
4675
4947
|
}
|
|
4676
4948
|
switch (result.kind) {
|
|
@@ -4715,6 +4987,11 @@ async function runModelADeploy(args) {
|
|
|
4715
4987
|
io.err(` - ${String(f.path)} [${String(f.rule)}]`);
|
|
4716
4988
|
}
|
|
4717
4989
|
}
|
|
4990
|
+
if (errorCode === "bundle_policy_violation" || errorCode === "app_boundary_violation" || errorCode === "bad_build_command") {
|
|
4991
|
+
io.err("");
|
|
4992
|
+
io.err(" Nothing was committed or claimed by this attempt — fix the");
|
|
4993
|
+
io.err(" listed file(s) and re-run `launchpad deploy`; the retry is clean.");
|
|
4994
|
+
}
|
|
4718
4995
|
return 1;
|
|
4719
4996
|
}
|
|
4720
4997
|
case "ok": {
|
|
@@ -4740,9 +5017,12 @@ async function runModelADeploy(args) {
|
|
|
4740
5017
|
io.out("");
|
|
4741
5018
|
io.out(message);
|
|
4742
5019
|
io.out("");
|
|
5020
|
+
io.out(`Your bundle ships with this provisioning run — when lifecycle reaches "live",`);
|
|
5021
|
+
io.out("this deploy's content is what's serving. No second deploy needed.");
|
|
5022
|
+
io.out("");
|
|
4743
5023
|
io.out("Next steps:");
|
|
4744
5024
|
io.out(` launchpad status ${slug} # watch lifecycle (provisioning → live)`);
|
|
4745
|
-
io.out(` launchpad deploy #
|
|
5025
|
+
io.out(` launchpad deploy # only if the app comes up live WITHOUT your content (rare)`);
|
|
4746
5026
|
return 0;
|
|
4747
5027
|
}
|
|
4748
5028
|
if (typeof success.commit_sha !== "string" || typeof success.repo !== "string") {
|
|
@@ -4771,14 +5051,13 @@ async function runModelADeploy(args) {
|
|
|
4771
5051
|
}
|
|
4772
5052
|
|
|
4773
5053
|
// src/commands/envvars.ts
|
|
4774
|
-
import * as
|
|
5054
|
+
import * as path8 from "node:path";
|
|
4775
5055
|
var envvarsCommand = {
|
|
4776
5056
|
name: "envvars",
|
|
4777
5057
|
summary: "list / set / remove production env vars (slug-scoped)",
|
|
4778
5058
|
run: runEnvvars
|
|
4779
5059
|
};
|
|
4780
5060
|
var SLUG_RE5 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
4781
|
-
var DIRNAME_RE2 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
4782
5061
|
var ENV_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
4783
5062
|
async function runEnvvars(args, io) {
|
|
4784
5063
|
const parsed = parseArgs3(args);
|
|
@@ -4790,9 +5069,9 @@ async function runEnvvars(args, io) {
|
|
|
4790
5069
|
if (parsed.slug !== null) {
|
|
4791
5070
|
slug = parsed.slug;
|
|
4792
5071
|
} else {
|
|
4793
|
-
const inferred =
|
|
5072
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
4794
5073
|
if (inferred === null) {
|
|
4795
|
-
io.err(`launchpad envvars: could not infer slug from cwd (${
|
|
5074
|
+
io.err(`launchpad envvars: could not infer slug from cwd (${path8.basename(process.cwd())});
|
|
4796
5075
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
4797
5076
|
return 64;
|
|
4798
5077
|
}
|
|
@@ -4860,7 +5139,7 @@ async function runEnvvars(args, io) {
|
|
|
4860
5139
|
io.err(`launchpad envvars: ${e.message}`);
|
|
4861
5140
|
return 1;
|
|
4862
5141
|
}
|
|
4863
|
-
io.err(`launchpad envvars failed: ${
|
|
5142
|
+
io.err(`launchpad envvars failed: ${describe14(e)}`);
|
|
4864
5143
|
return 1;
|
|
4865
5144
|
}
|
|
4866
5145
|
}
|
|
@@ -4938,11 +5217,6 @@ function parseArgs3(args) {
|
|
|
4938
5217
|
}
|
|
4939
5218
|
return null;
|
|
4940
5219
|
}
|
|
4941
|
-
function inferSlugFromCwd2(cwd) {
|
|
4942
|
-
const base = path7.basename(cwd);
|
|
4943
|
-
const m = base.match(DIRNAME_RE2);
|
|
4944
|
-
return m === null ? null : m[1];
|
|
4945
|
-
}
|
|
4946
5220
|
function renderList(envVars, io) {
|
|
4947
5221
|
if (envVars.length === 0) {
|
|
4948
5222
|
io.out("(no env vars set)");
|
|
@@ -4962,13 +5236,13 @@ function renderList(envVars, io) {
|
|
|
4962
5236
|
io.out(fmt(row));
|
|
4963
5237
|
}
|
|
4964
5238
|
}
|
|
4965
|
-
function
|
|
5239
|
+
function describe14(e) {
|
|
4966
5240
|
return e instanceof Error ? e.message : String(e);
|
|
4967
5241
|
}
|
|
4968
5242
|
|
|
4969
5243
|
// src/commands/generate.ts
|
|
4970
|
-
import { mkdirSync, readFileSync as
|
|
4971
|
-
import { dirname as dirname4, resolve as
|
|
5244
|
+
import { mkdirSync, readFileSync as readFileSync8, writeFileSync } from "node:fs";
|
|
5245
|
+
import { dirname as dirname4, resolve as resolve7, relative as relative3 } from "node:path";
|
|
4972
5246
|
var generateCommand = {
|
|
4973
5247
|
name: "generate",
|
|
4974
5248
|
summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
|
|
@@ -4981,14 +5255,14 @@ async function runGenerate(args, io) {
|
|
|
4981
5255
|
io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
|
|
4982
5256
|
return 64;
|
|
4983
5257
|
}
|
|
4984
|
-
const manifestPath =
|
|
5258
|
+
const manifestPath = resolve7(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
4985
5259
|
const result = loadManifest(manifestPath);
|
|
4986
5260
|
if (result.kind !== "ok") {
|
|
4987
5261
|
return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
|
|
4988
5262
|
}
|
|
4989
5263
|
const appRoot = dirname4(manifestPath);
|
|
4990
|
-
const wranglerPath =
|
|
4991
|
-
const workflowPath =
|
|
5264
|
+
const wranglerPath = resolve7(appRoot, "container", "wrangler.toml");
|
|
5265
|
+
const workflowPath = resolve7(appRoot, ".github", "workflows", "deploy.yml");
|
|
4992
5266
|
const wranglerOut = generateWranglerToml(result.manifest);
|
|
4993
5267
|
const workflowOut = generateGithubDeployWorkflow(result.manifest);
|
|
4994
5268
|
if (flags.dryRun) {
|
|
@@ -5003,7 +5277,7 @@ async function runGenerate(args, io) {
|
|
|
5003
5277
|
];
|
|
5004
5278
|
return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
|
|
5005
5279
|
}
|
|
5006
|
-
function applyOne(artefact,
|
|
5280
|
+
function applyOne(artefact, path9, out, force) {
|
|
5007
5281
|
if (out.kind === "not-applicable") {
|
|
5008
5282
|
return {
|
|
5009
5283
|
artefact,
|
|
@@ -5011,32 +5285,32 @@ function applyOne(artefact, path8, out, force) {
|
|
|
5011
5285
|
warnings: []
|
|
5012
5286
|
};
|
|
5013
5287
|
}
|
|
5014
|
-
const existing = readIfExists(
|
|
5288
|
+
const existing = readIfExists(path9);
|
|
5015
5289
|
if (existing.kind === "read-error") {
|
|
5016
5290
|
return {
|
|
5017
5291
|
artefact,
|
|
5018
|
-
action: { kind: "write-error", path:
|
|
5292
|
+
action: { kind: "write-error", path: path9, message: existing.message },
|
|
5019
5293
|
warnings: out.warnings
|
|
5020
5294
|
};
|
|
5021
5295
|
}
|
|
5022
5296
|
if (existing.kind === "ok" && existing.content === out.content) {
|
|
5023
|
-
return { artefact, action: { kind: "unchanged", path:
|
|
5297
|
+
return { artefact, action: { kind: "unchanged", path: path9 }, warnings: out.warnings };
|
|
5024
5298
|
}
|
|
5025
5299
|
if (existing.kind === "ok" && existing.content !== out.content && !force) {
|
|
5026
5300
|
return {
|
|
5027
5301
|
artefact,
|
|
5028
|
-
action: { kind: "would-overwrite", path:
|
|
5302
|
+
action: { kind: "would-overwrite", path: path9 },
|
|
5029
5303
|
warnings: out.warnings
|
|
5030
5304
|
};
|
|
5031
5305
|
}
|
|
5032
5306
|
try {
|
|
5033
|
-
mkdirSync(dirname4(
|
|
5034
|
-
writeFileSync(
|
|
5307
|
+
mkdirSync(dirname4(path9), { recursive: true });
|
|
5308
|
+
writeFileSync(path9, out.content, "utf8");
|
|
5035
5309
|
return {
|
|
5036
5310
|
artefact,
|
|
5037
5311
|
action: {
|
|
5038
5312
|
kind: "written",
|
|
5039
|
-
path:
|
|
5313
|
+
path: path9,
|
|
5040
5314
|
bytes: Buffer.byteLength(out.content, "utf8")
|
|
5041
5315
|
},
|
|
5042
5316
|
warnings: out.warnings
|
|
@@ -5046,16 +5320,16 @@ function applyOne(artefact, path8, out, force) {
|
|
|
5046
5320
|
artefact,
|
|
5047
5321
|
action: {
|
|
5048
5322
|
kind: "write-error",
|
|
5049
|
-
path:
|
|
5323
|
+
path: path9,
|
|
5050
5324
|
message: err.message ?? String(err)
|
|
5051
5325
|
},
|
|
5052
5326
|
warnings: out.warnings
|
|
5053
5327
|
};
|
|
5054
5328
|
}
|
|
5055
5329
|
}
|
|
5056
|
-
function readIfExists(
|
|
5330
|
+
function readIfExists(path9) {
|
|
5057
5331
|
try {
|
|
5058
|
-
return { kind: "ok", content:
|
|
5332
|
+
return { kind: "ok", content: readFileSync8(path9, "utf8") };
|
|
5059
5333
|
} catch (err) {
|
|
5060
5334
|
const e = err;
|
|
5061
5335
|
if (e.code === "ENOENT")
|
|
@@ -5266,7 +5540,7 @@ function parseFlags(args) {
|
|
|
5266
5540
|
}
|
|
5267
5541
|
|
|
5268
5542
|
// src/groups/client.ts
|
|
5269
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
5543
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5270
5544
|
import { dirname as dirname5, join as join9 } from "node:path";
|
|
5271
5545
|
var CACHE_TTL_MS = 60 * 60 * 1000;
|
|
5272
5546
|
var CACHE_FILENAME = "groups.json";
|
|
@@ -5294,7 +5568,7 @@ async function fetchGroups(cfg, opts = {}) {
|
|
|
5294
5568
|
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
5295
5569
|
throw e;
|
|
5296
5570
|
}
|
|
5297
|
-
return { kind: "error", message:
|
|
5571
|
+
return { kind: "error", message: describe15(e) };
|
|
5298
5572
|
}
|
|
5299
5573
|
if (typeof response !== "object" || response === null || !Array.isArray(response.groups) || !response.groups.every(isEntraGroup)) {
|
|
5300
5574
|
return {
|
|
@@ -5307,10 +5581,10 @@ async function fetchGroups(cfg, opts = {}) {
|
|
|
5307
5581
|
writeCache(cachePath, { fetchedAt, groups });
|
|
5308
5582
|
return { kind: "ok", source: "fresh", fetchedAt, groups };
|
|
5309
5583
|
}
|
|
5310
|
-
function readCache(
|
|
5584
|
+
function readCache(path9) {
|
|
5311
5585
|
let raw;
|
|
5312
5586
|
try {
|
|
5313
|
-
raw =
|
|
5587
|
+
raw = readFileSync9(path9, "utf8");
|
|
5314
5588
|
} catch {
|
|
5315
5589
|
return null;
|
|
5316
5590
|
}
|
|
@@ -5338,13 +5612,13 @@ function isEntraGroup(value) {
|
|
|
5338
5612
|
const g = value;
|
|
5339
5613
|
return typeof g.id === "string" && typeof g.displayName === "string" && (typeof g.mailNickname === "string" || g.mailNickname === null);
|
|
5340
5614
|
}
|
|
5341
|
-
function writeCache(
|
|
5615
|
+
function writeCache(path9, envelope) {
|
|
5342
5616
|
try {
|
|
5343
|
-
mkdirSync2(dirname5(
|
|
5344
|
-
writeFileSync2(
|
|
5617
|
+
mkdirSync2(dirname5(path9), { recursive: true });
|
|
5618
|
+
writeFileSync2(path9, JSON.stringify(envelope), "utf8");
|
|
5345
5619
|
} catch {}
|
|
5346
5620
|
}
|
|
5347
|
-
function
|
|
5621
|
+
function describe15(e) {
|
|
5348
5622
|
return e instanceof Error ? e.message : String(e);
|
|
5349
5623
|
}
|
|
5350
5624
|
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -5382,13 +5656,13 @@ function decodeJwtPayload(token) {
|
|
|
5382
5656
|
try {
|
|
5383
5657
|
json = Buffer.from(b64UrlToB64(payload), "base64").toString("utf8");
|
|
5384
5658
|
} catch (e) {
|
|
5385
|
-
throw new JwtParseError(`could not base64-decode JWT payload: ${
|
|
5659
|
+
throw new JwtParseError(`could not base64-decode JWT payload: ${describe16(e)}`);
|
|
5386
5660
|
}
|
|
5387
5661
|
let parsed;
|
|
5388
5662
|
try {
|
|
5389
5663
|
parsed = JSON.parse(json);
|
|
5390
5664
|
} catch (e) {
|
|
5391
|
-
throw new JwtParseError(`JWT payload is not JSON: ${
|
|
5665
|
+
throw new JwtParseError(`JWT payload is not JSON: ${describe16(e)}`);
|
|
5392
5666
|
}
|
|
5393
5667
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5394
5668
|
throw new JwtParseError(`JWT payload is not an object`);
|
|
@@ -5399,7 +5673,7 @@ function b64UrlToB64(s) {
|
|
|
5399
5673
|
const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
|
|
5400
5674
|
return padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
5401
5675
|
}
|
|
5402
|
-
function
|
|
5676
|
+
function describe16(e) {
|
|
5403
5677
|
return e instanceof Error ? e.message : String(e);
|
|
5404
5678
|
}
|
|
5405
5679
|
|
|
@@ -5426,7 +5700,7 @@ async function runGroupsWhoami(args, io) {
|
|
|
5426
5700
|
try {
|
|
5427
5701
|
payload = decodeJwtPayload(session.accessToken);
|
|
5428
5702
|
} catch (e) {
|
|
5429
|
-
const message = e instanceof JwtParseError ? e.message :
|
|
5703
|
+
const message = e instanceof JwtParseError ? e.message : describe17(e);
|
|
5430
5704
|
if (json) {
|
|
5431
5705
|
io.out(JSON.stringify({ ok: false, kind: "jwt-parse-error", message }));
|
|
5432
5706
|
} else {
|
|
@@ -5507,7 +5781,7 @@ function parseFlags2(args) {
|
|
|
5507
5781
|
}
|
|
5508
5782
|
return { kind: "ok", json };
|
|
5509
5783
|
}
|
|
5510
|
-
function
|
|
5784
|
+
function describe17(e) {
|
|
5511
5785
|
return e instanceof Error ? e.message : String(e);
|
|
5512
5786
|
}
|
|
5513
5787
|
|
|
@@ -5610,7 +5884,7 @@ async function loadGroups(io, refresh, json) {
|
|
|
5610
5884
|
}
|
|
5611
5885
|
return { kind: "error", code: 3 };
|
|
5612
5886
|
}
|
|
5613
|
-
renderFetchError(io,
|
|
5887
|
+
renderFetchError(io, describe18(e), json);
|
|
5614
5888
|
return { kind: "error", code: 2 };
|
|
5615
5889
|
}
|
|
5616
5890
|
}
|
|
@@ -5805,23 +6079,23 @@ function renderTable2(io, groups) {
|
|
|
5805
6079
|
for (const r of rows)
|
|
5806
6080
|
io.out(fmt(r));
|
|
5807
6081
|
}
|
|
5808
|
-
function
|
|
6082
|
+
function describe18(e) {
|
|
5809
6083
|
return e instanceof Error ? e.message : String(e);
|
|
5810
6084
|
}
|
|
5811
6085
|
|
|
5812
6086
|
// src/commands/init.ts
|
|
5813
6087
|
import { createInterface } from "node:readline/promises";
|
|
5814
|
-
import { existsSync as
|
|
5815
|
-
import { resolve as
|
|
6088
|
+
import { existsSync as existsSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6089
|
+
import { resolve as resolve8 } from "node:path";
|
|
5816
6090
|
import { stringify as yamlStringify } from "yaml";
|
|
5817
6091
|
|
|
5818
6092
|
// src/detect/index.ts
|
|
5819
|
-
import { existsSync as
|
|
6093
|
+
import { existsSync as existsSync6, readFileSync as readFileSync10, statSync } from "node:fs";
|
|
5820
6094
|
import { join as join10 } from "node:path";
|
|
5821
6095
|
function detectAppShape(cwd) {
|
|
5822
|
-
const hasPackageJson =
|
|
5823
|
-
const hasAnyLockfile = LOCKFILES.some(({ file }) =>
|
|
5824
|
-
const hasViteConfig = VITE_CONFIG_NAMES.some((n) =>
|
|
6096
|
+
const hasPackageJson = existsSync6(join10(cwd, "package.json"));
|
|
6097
|
+
const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync6(join10(cwd, file)));
|
|
6098
|
+
const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync6(join10(cwd, n)));
|
|
5825
6099
|
if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
|
|
5826
6100
|
return {
|
|
5827
6101
|
kind: "not-applicable",
|
|
@@ -5867,7 +6141,7 @@ var LOCKFILES = [
|
|
|
5867
6141
|
{ file: "yarn.lock", pm: "yarn" }
|
|
5868
6142
|
];
|
|
5869
6143
|
function detectPackageManager(cwd) {
|
|
5870
|
-
const present = LOCKFILES.filter(({ file }) =>
|
|
6144
|
+
const present = LOCKFILES.filter(({ file }) => existsSync6(join10(cwd, file)));
|
|
5871
6145
|
if (present.length === 0) {
|
|
5872
6146
|
return {
|
|
5873
6147
|
kind: "ambiguous",
|
|
@@ -5890,7 +6164,7 @@ function detectPackageManager(cwd) {
|
|
|
5890
6164
|
function detectVitePresence(cwd) {
|
|
5891
6165
|
for (const name of VITE_CONFIG_NAMES) {
|
|
5892
6166
|
const p = join10(cwd, name);
|
|
5893
|
-
if (
|
|
6167
|
+
if (existsSync6(p)) {
|
|
5894
6168
|
return { kind: "ok", value: { path: p } };
|
|
5895
6169
|
}
|
|
5896
6170
|
}
|
|
@@ -5902,7 +6176,7 @@ function detectVitePresence(cwd) {
|
|
|
5902
6176
|
function detectAppType(cwd) {
|
|
5903
6177
|
const fnDir = join10(cwd, "functions");
|
|
5904
6178
|
let hasFunctionsDir = false;
|
|
5905
|
-
if (
|
|
6179
|
+
if (existsSync6(fnDir)) {
|
|
5906
6180
|
try {
|
|
5907
6181
|
hasFunctionsDir = statSync(fnDir).isDirectory();
|
|
5908
6182
|
} catch {
|
|
@@ -5913,7 +6187,7 @@ function detectAppType(cwd) {
|
|
|
5913
6187
|
}
|
|
5914
6188
|
function detectBuildCommand(cwd, pm) {
|
|
5915
6189
|
const pkgJsonPath = join10(cwd, "package.json");
|
|
5916
|
-
if (!
|
|
6190
|
+
if (!existsSync6(pkgJsonPath)) {
|
|
5917
6191
|
return {
|
|
5918
6192
|
kind: "ambiguous",
|
|
5919
6193
|
reason: "no package.json at repo root. Run your package manager's `init` first."
|
|
@@ -5921,7 +6195,7 @@ function detectBuildCommand(cwd, pm) {
|
|
|
5921
6195
|
}
|
|
5922
6196
|
let pkgJson;
|
|
5923
6197
|
try {
|
|
5924
|
-
pkgJson = JSON.parse(
|
|
6198
|
+
pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
|
|
5925
6199
|
} catch (e) {
|
|
5926
6200
|
return {
|
|
5927
6201
|
kind: "ambiguous",
|
|
@@ -5956,7 +6230,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
|
|
|
5956
6230
|
function detectDestinationDir(cwd, vite) {
|
|
5957
6231
|
let text;
|
|
5958
6232
|
try {
|
|
5959
|
-
text =
|
|
6233
|
+
text = readFileSync10(vite.path, "utf8");
|
|
5960
6234
|
} catch (e) {
|
|
5961
6235
|
return {
|
|
5962
6236
|
kind: "ambiguous",
|
|
@@ -5985,8 +6259,8 @@ async function runInit(args, io, prompt) {
|
|
|
5985
6259
|
return 64;
|
|
5986
6260
|
}
|
|
5987
6261
|
const { inputs, options } = parsed;
|
|
5988
|
-
const outPath =
|
|
5989
|
-
if (
|
|
6262
|
+
const outPath = resolve8(process.cwd(), options.out);
|
|
6263
|
+
if (existsSync7(outPath) && !options.force) {
|
|
5990
6264
|
io.err(`launchpad init: ${outPath} already exists`);
|
|
5991
6265
|
io.err("Pass --force to overwrite.");
|
|
5992
6266
|
return 64;
|
|
@@ -6033,7 +6307,7 @@ async function runInit(args, io, prompt) {
|
|
|
6033
6307
|
}
|
|
6034
6308
|
io.out(`✓ wrote ${outPath}`);
|
|
6035
6309
|
if (options.gitignore) {
|
|
6036
|
-
const gitignorePath =
|
|
6310
|
+
const gitignorePath = resolve8(process.cwd(), ".gitignore");
|
|
6037
6311
|
try {
|
|
6038
6312
|
const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
|
|
6039
6313
|
if (changed.length > 0) {
|
|
@@ -6402,10 +6676,10 @@ function buildManifest(inputs, detected) {
|
|
|
6402
6676
|
function renderYaml(manifest) {
|
|
6403
6677
|
return yamlStringify(manifest, { lineWidth: 0 });
|
|
6404
6678
|
}
|
|
6405
|
-
function ensureGitignoreEntries(
|
|
6679
|
+
function ensureGitignoreEntries(path9, entries) {
|
|
6406
6680
|
let current = "";
|
|
6407
|
-
if (
|
|
6408
|
-
current =
|
|
6681
|
+
if (existsSync7(path9)) {
|
|
6682
|
+
current = readFileSync11(path9, "utf8");
|
|
6409
6683
|
}
|
|
6410
6684
|
const lines = current.split(/\r?\n/);
|
|
6411
6685
|
const present = new Set(lines.map((l) => l.trim()));
|
|
@@ -6423,7 +6697,7 @@ function ensureGitignoreEntries(path8, entries) {
|
|
|
6423
6697
|
}
|
|
6424
6698
|
}
|
|
6425
6699
|
if (added.length > 0) {
|
|
6426
|
-
writeFileSync3(
|
|
6700
|
+
writeFileSync3(path9, out, { encoding: "utf8" });
|
|
6427
6701
|
}
|
|
6428
6702
|
return added;
|
|
6429
6703
|
}
|
|
@@ -6441,68 +6715,122 @@ async function defaultPrompt2(question, fallback) {
|
|
|
6441
6715
|
}
|
|
6442
6716
|
|
|
6443
6717
|
// src/commands/login.ts
|
|
6444
|
-
var
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6718
|
+
var REAL_DEPS = { gatewayLogin, legacyLogin: login };
|
|
6719
|
+
var loginCommand = makeLoginCommand();
|
|
6720
|
+
function makeLoginCommand(deps = REAL_DEPS) {
|
|
6721
|
+
return {
|
|
6722
|
+
name: "login",
|
|
6723
|
+
summary: "authenticate (browser-based PKCE) and store a session",
|
|
6724
|
+
run: (args, io) => runLogin(args, io, deps)
|
|
6725
|
+
};
|
|
6726
|
+
}
|
|
6727
|
+
async function runLogin(_args, io, deps) {
|
|
6450
6728
|
try {
|
|
6451
6729
|
const cfg = loadConfig();
|
|
6452
|
-
|
|
6730
|
+
if (cfg.authLegacy) {
|
|
6731
|
+
io.out("LAUNCHPAD_AUTH_LEGACY=1 — using the legacy Cloudflare Access flow (deprecated; this path will be removed when the dual-auth window closes).");
|
|
6732
|
+
return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
|
|
6733
|
+
}
|
|
6734
|
+
io.out("Opening browser to sign in via the Launchpad auth gateway…");
|
|
6453
6735
|
io.out("(if it doesn't open automatically, copy the URL below)");
|
|
6454
6736
|
io.out("");
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6737
|
+
try {
|
|
6738
|
+
const session = await deps.gatewayLogin({
|
|
6739
|
+
gatewayUrl: cfg.authGatewayUrl,
|
|
6740
|
+
sessionPath: cfg.sessionPath,
|
|
6741
|
+
onAuthUrl: (url) => {
|
|
6742
|
+
io.out(url);
|
|
6743
|
+
io.out("");
|
|
6744
|
+
}
|
|
6745
|
+
});
|
|
6746
|
+
const expiresIn = Math.max(0, Math.round((session.accessTokenExpiresAt - Date.now()) / 1000));
|
|
6747
|
+
io.out(`Logged in. Session stored at ${cfg.sessionPath}`);
|
|
6748
|
+
io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
|
|
6749
|
+
return 0;
|
|
6750
|
+
} catch (e) {
|
|
6751
|
+
io.err(`Gateway login failed: ${describe19(e)}`);
|
|
6752
|
+
io.err("Falling back to the legacy Cloudflare Access flow (DEPRECATED — this fallback will be removed when the dual-auth window closes).");
|
|
6753
|
+
io.err("");
|
|
6754
|
+
return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
|
|
6755
|
+
}
|
|
6467
6756
|
} catch (e) {
|
|
6468
|
-
io.err(`launchpad login failed: ${
|
|
6757
|
+
io.err(`launchpad login failed: ${describe19(e)}`);
|
|
6469
6758
|
return 1;
|
|
6470
6759
|
}
|
|
6471
6760
|
}
|
|
6472
|
-
function
|
|
6761
|
+
async function runLegacyLogin(io, botUrl, sessionPath, deps) {
|
|
6762
|
+
io.out("Opening browser to authenticate with Cloudflare Access…");
|
|
6763
|
+
io.out("(if it doesn't open automatically, copy the URL below)");
|
|
6764
|
+
io.out("");
|
|
6765
|
+
const session = await deps.legacyLogin({
|
|
6766
|
+
botUrl,
|
|
6767
|
+
sessionPath,
|
|
6768
|
+
onAuthUrl: (url) => {
|
|
6769
|
+
io.out(url);
|
|
6770
|
+
io.out("");
|
|
6771
|
+
}
|
|
6772
|
+
});
|
|
6773
|
+
const expiresIn = Math.max(0, Math.round((session.accessTokenExpiresAt - Date.now()) / 1000));
|
|
6774
|
+
io.out(`Logged in. Session stored at ${sessionPath}`);
|
|
6775
|
+
io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
|
|
6776
|
+
return 0;
|
|
6777
|
+
}
|
|
6778
|
+
function describe19(e) {
|
|
6473
6779
|
return e instanceof Error ? e.message : String(e);
|
|
6474
6780
|
}
|
|
6475
6781
|
|
|
6476
6782
|
// src/commands/logout.ts
|
|
6477
|
-
var
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6783
|
+
var REAL_DEPS2 = { revoke: revokeGatewaySession };
|
|
6784
|
+
var logoutCommand = makeLogoutCommand();
|
|
6785
|
+
function makeLogoutCommand(deps = REAL_DEPS2) {
|
|
6786
|
+
return {
|
|
6787
|
+
name: "logout",
|
|
6788
|
+
summary: "revoke the session server-side and clear it locally",
|
|
6789
|
+
run: (args, io) => runLogout(args, io, deps)
|
|
6790
|
+
};
|
|
6791
|
+
}
|
|
6792
|
+
async function runLogout(_args, io, deps) {
|
|
6483
6793
|
try {
|
|
6484
6794
|
const cfg = loadConfig();
|
|
6795
|
+
let session = null;
|
|
6796
|
+
try {
|
|
6797
|
+
session = await readSession(cfg.sessionPath);
|
|
6798
|
+
} catch (e) {
|
|
6799
|
+
if (!(e instanceof SessionParseError))
|
|
6800
|
+
throw e;
|
|
6801
|
+
io.err(`warning: session file is corrupt (${e.message}) — clearing it without a server-side revoke.`);
|
|
6802
|
+
}
|
|
6803
|
+
if (session !== null && isGatewaySession(session)) {
|
|
6804
|
+
try {
|
|
6805
|
+
await deps.revoke({
|
|
6806
|
+
gatewayUrl: session.gatewayUrl,
|
|
6807
|
+
refreshToken: session.refreshToken
|
|
6808
|
+
});
|
|
6809
|
+
io.out("Server-side session revoked.");
|
|
6810
|
+
} catch (e) {
|
|
6811
|
+
io.err(`warning: could not revoke the session server-side (${describe20(e)}) — clearing the local session anyway. Any in-flight access token expires within 15 minutes.`);
|
|
6812
|
+
}
|
|
6813
|
+
}
|
|
6485
6814
|
const had = await clearSession(cfg.sessionPath);
|
|
6486
6815
|
io.out(had ? `Logged out. Session cleared at ${cfg.sessionPath}` : "Already logged out.");
|
|
6487
6816
|
return 0;
|
|
6488
6817
|
} catch (e) {
|
|
6489
|
-
io.err(`launchpad logout failed: ${
|
|
6818
|
+
io.err(`launchpad logout failed: ${describe20(e)}`);
|
|
6490
6819
|
return 1;
|
|
6491
6820
|
}
|
|
6492
6821
|
}
|
|
6493
|
-
function
|
|
6822
|
+
function describe20(e) {
|
|
6494
6823
|
return e instanceof Error ? e.message : String(e);
|
|
6495
6824
|
}
|
|
6496
6825
|
|
|
6497
6826
|
// src/commands/logs.ts
|
|
6498
|
-
import * as
|
|
6827
|
+
import * as path9 from "node:path";
|
|
6499
6828
|
var logsCommand = {
|
|
6500
6829
|
name: "logs",
|
|
6501
6830
|
summary: "show recent Pages deployment history (slug-scoped)",
|
|
6502
6831
|
run: runLogs
|
|
6503
6832
|
};
|
|
6504
6833
|
var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
6505
|
-
var DIRNAME_RE3 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
6506
6834
|
var DEFAULT_LINES = 10;
|
|
6507
6835
|
var MAX_LINES = 25;
|
|
6508
6836
|
async function runLogs(args, io) {
|
|
@@ -6516,9 +6844,9 @@ async function runLogs(args, io) {
|
|
|
6516
6844
|
if (parsed.slug !== null) {
|
|
6517
6845
|
slug = parsed.slug;
|
|
6518
6846
|
} else {
|
|
6519
|
-
const inferred =
|
|
6847
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
6520
6848
|
if (inferred === null) {
|
|
6521
|
-
io.err(`launchpad logs: could not infer slug from cwd (${
|
|
6849
|
+
io.err(`launchpad logs: could not infer slug from cwd (${path9.basename(process.cwd())});
|
|
6522
6850
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
6523
6851
|
return 64;
|
|
6524
6852
|
}
|
|
@@ -6561,7 +6889,7 @@ async function runLogs(args, io) {
|
|
|
6561
6889
|
io.err(`launchpad logs: ${e.message}`);
|
|
6562
6890
|
return 1;
|
|
6563
6891
|
}
|
|
6564
|
-
io.err(`launchpad logs failed: ${
|
|
6892
|
+
io.err(`launchpad logs failed: ${describe21(e)}`);
|
|
6565
6893
|
return 1;
|
|
6566
6894
|
}
|
|
6567
6895
|
}
|
|
@@ -6596,11 +6924,6 @@ function parseArgs5(args) {
|
|
|
6596
6924
|
}
|
|
6597
6925
|
return { slug, lines };
|
|
6598
6926
|
}
|
|
6599
|
-
function inferSlugFromCwd3(cwd) {
|
|
6600
|
-
const base = path8.basename(cwd);
|
|
6601
|
-
const m = base.match(DIRNAME_RE3);
|
|
6602
|
-
return m === null ? null : m[1];
|
|
6603
|
-
}
|
|
6604
6927
|
function renderDeployments(deployments, io) {
|
|
6605
6928
|
if (deployments.length === 0) {
|
|
6606
6929
|
io.out("(no deployments yet)");
|
|
@@ -6646,19 +6969,18 @@ function formatRelative2(iso) {
|
|
|
6646
6969
|
return `${hr}h ago`;
|
|
6647
6970
|
return `${Math.floor(hr / 24)}d ago`;
|
|
6648
6971
|
}
|
|
6649
|
-
function
|
|
6972
|
+
function describe21(e) {
|
|
6650
6973
|
return e instanceof Error ? e.message : String(e);
|
|
6651
6974
|
}
|
|
6652
6975
|
|
|
6653
6976
|
// src/commands/merge.ts
|
|
6654
|
-
import * as
|
|
6977
|
+
import * as path10 from "node:path";
|
|
6655
6978
|
var mergeCommand = {
|
|
6656
6979
|
name: "merge",
|
|
6657
6980
|
summary: "squash-merge a review-passed PR (slug-scoped)",
|
|
6658
6981
|
run: runMerge
|
|
6659
6982
|
};
|
|
6660
6983
|
var SLUG_RE7 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
6661
|
-
var DIRNAME_RE4 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
6662
6984
|
var HANDLED_STATUSES = [409, 422, 502, 503];
|
|
6663
6985
|
async function runMerge(args, io) {
|
|
6664
6986
|
const parsed = parseArgs6(args);
|
|
@@ -6671,9 +6993,9 @@ async function runMerge(args, io) {
|
|
|
6671
6993
|
if (parsed.slug !== null) {
|
|
6672
6994
|
slug = parsed.slug;
|
|
6673
6995
|
} else {
|
|
6674
|
-
const inferred =
|
|
6996
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
6675
6997
|
if (inferred === null) {
|
|
6676
|
-
io.err(`launchpad merge: could not infer slug from cwd (${
|
|
6998
|
+
io.err(`launchpad merge: could not infer slug from cwd (${path10.basename(process.cwd())});
|
|
6677
6999
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
6678
7000
|
return 64;
|
|
6679
7001
|
}
|
|
@@ -6696,7 +7018,7 @@ async function runMerge(args, io) {
|
|
|
6696
7018
|
try {
|
|
6697
7019
|
body = await res.json();
|
|
6698
7020
|
} catch (e) {
|
|
6699
|
-
io.err(`launchpad merge: bot returned 2xx with malformed body: ${
|
|
7021
|
+
io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe22(e)}`);
|
|
6700
7022
|
return 1;
|
|
6701
7023
|
}
|
|
6702
7024
|
io.out("");
|
|
@@ -6736,7 +7058,7 @@ async function runMerge(args, io) {
|
|
|
6736
7058
|
io.err(`launchpad merge: ${e.message}`);
|
|
6737
7059
|
return 1;
|
|
6738
7060
|
}
|
|
6739
|
-
io.err(`launchpad merge failed: ${
|
|
7061
|
+
io.err(`launchpad merge failed: ${describe22(e)}`);
|
|
6740
7062
|
return 1;
|
|
6741
7063
|
}
|
|
6742
7064
|
}
|
|
@@ -6770,11 +7092,6 @@ function parseArgs6(args) {
|
|
|
6770
7092
|
return null;
|
|
6771
7093
|
return { slug, prNumber };
|
|
6772
7094
|
}
|
|
6773
|
-
function inferSlugFromCwd4(cwd) {
|
|
6774
|
-
const base = path9.basename(cwd);
|
|
6775
|
-
const m = base.match(DIRNAME_RE4);
|
|
6776
|
-
return m === null ? null : m[1];
|
|
6777
|
-
}
|
|
6778
7095
|
function renderBotError(status, env, io, prNumber = null) {
|
|
6779
7096
|
const code = env?.error ?? "unknown";
|
|
6780
7097
|
const detail = env?.message;
|
|
@@ -6825,12 +7142,12 @@ function renderBotError(status, env, io, prNumber = null) {
|
|
|
6825
7142
|
io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
|
|
6826
7143
|
}
|
|
6827
7144
|
}
|
|
6828
|
-
function
|
|
7145
|
+
function describe22(e) {
|
|
6829
7146
|
return e instanceof Error ? e.message : String(e);
|
|
6830
7147
|
}
|
|
6831
7148
|
|
|
6832
7149
|
// src/commands/plan.ts
|
|
6833
|
-
import { resolve as
|
|
7150
|
+
import { resolve as resolve9 } from "node:path";
|
|
6834
7151
|
var planCommand = {
|
|
6835
7152
|
name: "plan",
|
|
6836
7153
|
summary: "summarise what the manifest would deploy (offline)",
|
|
@@ -6843,7 +7160,7 @@ async function runPlan(args, io) {
|
|
|
6843
7160
|
io.err("Usage: launchpad plan [--file <path>] [--json]");
|
|
6844
7161
|
return 64;
|
|
6845
7162
|
}
|
|
6846
|
-
const manifestPath =
|
|
7163
|
+
const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
6847
7164
|
const result = loadManifest(manifestPath);
|
|
6848
7165
|
return flags.json ? renderJson(result, io) : renderHuman(result, io);
|
|
6849
7166
|
}
|
|
@@ -7026,7 +7343,7 @@ var SLUG_RE8 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
|
7026
7343
|
var SLUG_MIN_LENGTH = 3;
|
|
7027
7344
|
var SLUG_MAX_LENGTH = 58;
|
|
7028
7345
|
async function runDestroy(args, io, prompt, isTty) {
|
|
7029
|
-
const parsed = parseArgs7(args);
|
|
7346
|
+
const parsed = parseArgs7(args, process.cwd(), (l) => io.err(l));
|
|
7030
7347
|
if (typeof parsed === "string") {
|
|
7031
7348
|
io.err(`launchpad destroy: ${parsed}`);
|
|
7032
7349
|
printUsage3(io);
|
|
@@ -7077,11 +7394,11 @@ async function runDestroy(args, io, prompt, isTty) {
|
|
|
7077
7394
|
io.err(`launchpad destroy: ${e.message}`);
|
|
7078
7395
|
return 1;
|
|
7079
7396
|
}
|
|
7080
|
-
io.err(`launchpad destroy failed: ${
|
|
7397
|
+
io.err(`launchpad destroy failed: ${describe23(e)}`);
|
|
7081
7398
|
return 1;
|
|
7082
7399
|
}
|
|
7083
7400
|
}
|
|
7084
|
-
function parseArgs7(args, cwd = process.cwd()) {
|
|
7401
|
+
function parseArgs7(args, cwd = process.cwd(), warn) {
|
|
7085
7402
|
let slug = null;
|
|
7086
7403
|
let confirmSlug = null;
|
|
7087
7404
|
let yes = false;
|
|
@@ -7128,7 +7445,7 @@ function parseArgs7(args, cwd = process.cwd()) {
|
|
|
7128
7445
|
i += 1;
|
|
7129
7446
|
}
|
|
7130
7447
|
if (slug === null) {
|
|
7131
|
-
const inferred =
|
|
7448
|
+
const inferred = inferSlug({ cwd, warn });
|
|
7132
7449
|
if (inferred === null) {
|
|
7133
7450
|
return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
7134
7451
|
}
|
|
@@ -7278,7 +7595,7 @@ function printUsage3(io) {
|
|
|
7278
7595
|
].join(`
|
|
7279
7596
|
`));
|
|
7280
7597
|
}
|
|
7281
|
-
function
|
|
7598
|
+
function describe23(e) {
|
|
7282
7599
|
return e instanceof Error ? e.message : String(e);
|
|
7283
7600
|
}
|
|
7284
7601
|
|
|
@@ -7288,8 +7605,8 @@ import { stringify as stringifyYaml } from "yaml";
|
|
|
7288
7605
|
|
|
7289
7606
|
// src/deploy/manifest-state.ts
|
|
7290
7607
|
async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
7291
|
-
const
|
|
7292
|
-
const raw = await apiJson(cfg, { path:
|
|
7608
|
+
const path11 = opts.includeManifest === true ? `/apps/${encodeURIComponent(slug)}/manifest/state?include=manifest` : `/apps/${encodeURIComponent(slug)}/manifest/state`;
|
|
7609
|
+
const raw = await apiJson(cfg, { path: path11 }, fetcher);
|
|
7293
7610
|
return {
|
|
7294
7611
|
slug: raw.slug,
|
|
7295
7612
|
hasAppFile: raw.hasAppFile,
|
|
@@ -7302,8 +7619,8 @@ async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
|
7302
7619
|
|
|
7303
7620
|
// src/deploy/manifest-status.ts
|
|
7304
7621
|
async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
|
|
7305
|
-
const
|
|
7306
|
-
return apiJson(cfg, { path:
|
|
7622
|
+
const path11 = `/apps/${encodeURIComponent(slug)}/manifest/status`;
|
|
7623
|
+
return apiJson(cfg, { path: path11 }, fetcher);
|
|
7307
7624
|
}
|
|
7308
7625
|
|
|
7309
7626
|
// src/deploy/deployment-status.ts
|
|
@@ -7347,7 +7664,7 @@ var pullCommand = {
|
|
|
7347
7664
|
};
|
|
7348
7665
|
var SLUG_RE9 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
7349
7666
|
async function runPull(args, io) {
|
|
7350
|
-
const parsed = parseArgs8(args);
|
|
7667
|
+
const parsed = parseArgs8(args, process.cwd(), (l) => io.err(l));
|
|
7351
7668
|
if (typeof parsed === "string") {
|
|
7352
7669
|
io.err(`launchpad pull: ${parsed}`);
|
|
7353
7670
|
printUsage4(io);
|
|
@@ -7422,7 +7739,7 @@ async function runPull(args, io) {
|
|
|
7422
7739
|
io.err(`launchpad pull: ${e.message}`);
|
|
7423
7740
|
return 1;
|
|
7424
7741
|
}
|
|
7425
|
-
io.err(`launchpad pull failed: ${
|
|
7742
|
+
io.err(`launchpad pull failed: ${describe24(e)}`);
|
|
7426
7743
|
return 1;
|
|
7427
7744
|
}
|
|
7428
7745
|
}
|
|
@@ -7433,7 +7750,7 @@ async function fetchLiveDeploymentBestEffort(cfg, slug) {
|
|
|
7433
7750
|
return null;
|
|
7434
7751
|
}
|
|
7435
7752
|
}
|
|
7436
|
-
function parseArgs8(args, cwd = process.cwd()) {
|
|
7753
|
+
function parseArgs8(args, cwd = process.cwd(), warn) {
|
|
7437
7754
|
let slug = null;
|
|
7438
7755
|
let out = null;
|
|
7439
7756
|
let status = false;
|
|
@@ -7474,7 +7791,7 @@ function parseArgs8(args, cwd = process.cwd()) {
|
|
|
7474
7791
|
i += 1;
|
|
7475
7792
|
}
|
|
7476
7793
|
if (slug === null) {
|
|
7477
|
-
const inferred =
|
|
7794
|
+
const inferred = inferSlug({ cwd, warn });
|
|
7478
7795
|
if (inferred === null) {
|
|
7479
7796
|
return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
7480
7797
|
}
|
|
@@ -7492,8 +7809,9 @@ function printUsage4(io) {
|
|
|
7492
7809
|
" Reads the deployed launchpad.yaml for an app via the bot.",
|
|
7493
7810
|
" No local platform-repo or terraform required.",
|
|
7494
7811
|
"",
|
|
7495
|
-
"
|
|
7496
|
-
"
|
|
7812
|
+
" With no slug, it is inferred from the local launchpad.yaml's",
|
|
7813
|
+
" declared slug first, then from a launchpad-app-<slug>/",
|
|
7814
|
+
" directory name. Explicit --slug or positional override.",
|
|
7497
7815
|
"",
|
|
7498
7816
|
" --status read the role-redacted status block (what your",
|
|
7499
7817
|
" role may see) instead of the spec manifest.",
|
|
@@ -7502,18 +7820,188 @@ function printUsage4(io) {
|
|
|
7502
7820
|
].join(`
|
|
7503
7821
|
`));
|
|
7504
7822
|
}
|
|
7505
|
-
function
|
|
7823
|
+
function describe24(e) {
|
|
7506
7824
|
return e instanceof Error ? e.message : String(e);
|
|
7507
7825
|
}
|
|
7508
7826
|
|
|
7827
|
+
// src/commands/recover.ts
|
|
7828
|
+
var recoverCommand = {
|
|
7829
|
+
name: "recover",
|
|
7830
|
+
summary: "repair a terminal-failed app record by reconciling against live state",
|
|
7831
|
+
run: runRecover
|
|
7832
|
+
};
|
|
7833
|
+
var SLUG_RE10 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
7834
|
+
async function runRecover(args, io) {
|
|
7835
|
+
const parsed = parseRecoverArgs(args, process.cwd(), (l) => io.err(l));
|
|
7836
|
+
if (typeof parsed === "string") {
|
|
7837
|
+
io.err(`launchpad recover: ${parsed}`);
|
|
7838
|
+
printUsage5(io);
|
|
7839
|
+
return 64;
|
|
7840
|
+
}
|
|
7841
|
+
const cfg = loadConfig();
|
|
7842
|
+
try {
|
|
7843
|
+
io.out(`Reconciling "${parsed.slug}" against live Cloudflare state …`);
|
|
7844
|
+
const res = await apiRaw(cfg, {
|
|
7845
|
+
method: "POST",
|
|
7846
|
+
path: `/apps/${parsed.slug}/recover`,
|
|
7847
|
+
jsonBody: undefined,
|
|
7848
|
+
nonThrowingStatuses: [409, 503]
|
|
7849
|
+
});
|
|
7850
|
+
const body = await res.json().catch(() => null);
|
|
7851
|
+
if (parsed.json) {
|
|
7852
|
+
io.out(JSON.stringify({ httpStatus: res.status, ...body ?? {} }, null, 2));
|
|
7853
|
+
return res.status === 200 ? 0 : 1;
|
|
7854
|
+
}
|
|
7855
|
+
if (res.status === 200 && body !== null && "outcome" in body) {
|
|
7856
|
+
renderSuccess2(body, io);
|
|
7857
|
+
return 0;
|
|
7858
|
+
}
|
|
7859
|
+
if (res.status === 503) {
|
|
7860
|
+
const msg = body !== null && "message" in body && typeof body.message === "string" ? body.message : "live state unavailable — nothing was changed; retry shortly.";
|
|
7861
|
+
io.err(`launchpad recover: ${msg}`);
|
|
7862
|
+
return 1;
|
|
7863
|
+
}
|
|
7864
|
+
if (res.status === 409 && body !== null && "error" in body) {
|
|
7865
|
+
renderRefusal(parsed.slug, body, io);
|
|
7866
|
+
return 1;
|
|
7867
|
+
}
|
|
7868
|
+
io.err(`launchpad recover: bot returned an unexpected HTTP ${res.status} response.`);
|
|
7869
|
+
return 1;
|
|
7870
|
+
} catch (e) {
|
|
7871
|
+
return mapError(e, parsed.slug, io);
|
|
7872
|
+
}
|
|
7873
|
+
}
|
|
7874
|
+
function renderSuccess2(body, io) {
|
|
7875
|
+
if (body.outcome === "noop_already_healthy") {
|
|
7876
|
+
io.out(`${body.slug}: already healthy — nothing to recover.`);
|
|
7877
|
+
io.out(` ${body.message}`);
|
|
7878
|
+
return;
|
|
7879
|
+
}
|
|
7880
|
+
io.out(`${body.slug}: REPAIRED — registry record reconciled to live.`);
|
|
7881
|
+
if (body.before !== undefined) {
|
|
7882
|
+
io.out(` before: ${body.before.lifecycle}` + (body.before.reason !== null ? ` (${body.before.reason})` : ""));
|
|
7883
|
+
}
|
|
7884
|
+
if (body.after !== undefined) {
|
|
7885
|
+
io.out(` after: ${body.after.lifecycle}`);
|
|
7886
|
+
}
|
|
7887
|
+
const checked = body.checked;
|
|
7888
|
+
if (checked !== undefined) {
|
|
7889
|
+
io.out(` verified: Pages project "${checked.pagesProject}" exists` + (checked.latestDeployment !== null ? `; latest production deployment ${checked.latestDeployment.id} ` + `(${checked.latestDeployment.buildStatus}, ${checked.latestDeployment.createdOn})` : ""));
|
|
7890
|
+
if (checked.olderContentServing) {
|
|
7891
|
+
io.out(" note: the LATEST build failed — an older successful deployment is what's serving.");
|
|
7892
|
+
}
|
|
7893
|
+
}
|
|
7894
|
+
io.out("");
|
|
7895
|
+
io.out(`Run \`launchpad status ${body.slug}\` — it now reports the live deployment truth.`);
|
|
7896
|
+
}
|
|
7897
|
+
function renderRefusal(slug, body, io) {
|
|
7898
|
+
io.err(`launchpad recover: refused — "${slug}" was NOT repaired.`);
|
|
7899
|
+
if (typeof body.message === "string") {
|
|
7900
|
+
io.err(` ${body.message}`);
|
|
7901
|
+
}
|
|
7902
|
+
const checked = body.checked;
|
|
7903
|
+
if (checked !== undefined) {
|
|
7904
|
+
io.err(" checked:");
|
|
7905
|
+
io.err(` Pages project "${checked.pagesProject}": ${checked.projectExists ? "exists" : "MISSING"}`);
|
|
7906
|
+
if (checked.latestDeployment !== null) {
|
|
7907
|
+
io.err(` latest production deployment: ${checked.latestDeployment.id} (${checked.latestDeployment.buildStatus})`);
|
|
7908
|
+
} else if (checked.projectExists) {
|
|
7909
|
+
io.err(" latest production deployment: none");
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
function mapError(e, slug, io) {
|
|
7914
|
+
if (e instanceof UnauthenticatedError) {
|
|
7915
|
+
io.err(`launchpad recover: ${e.message}`);
|
|
7916
|
+
io.err(" session expired, run `launchpad login`");
|
|
7917
|
+
return 1;
|
|
7918
|
+
}
|
|
7919
|
+
if (e instanceof ForbiddenError) {
|
|
7920
|
+
io.err(`launchpad recover: not authorised for app "${slug}" (you must be an owner or editor).`);
|
|
7921
|
+
return 1;
|
|
7922
|
+
}
|
|
7923
|
+
if (e instanceof NotFoundError) {
|
|
7924
|
+
io.err(`launchpad recover: app "${slug}" not found.`);
|
|
7925
|
+
return 1;
|
|
7926
|
+
}
|
|
7927
|
+
if (e instanceof ApiError || e instanceof TransportError) {
|
|
7928
|
+
io.err(`launchpad recover: ${e.message}`);
|
|
7929
|
+
return 1;
|
|
7930
|
+
}
|
|
7931
|
+
io.err(`launchpad recover failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
7932
|
+
return 1;
|
|
7933
|
+
}
|
|
7934
|
+
function parseRecoverArgs(args, cwd = process.cwd(), warn) {
|
|
7935
|
+
let slug = null;
|
|
7936
|
+
let json = false;
|
|
7937
|
+
let i = 0;
|
|
7938
|
+
while (i < args.length) {
|
|
7939
|
+
const a = args[i] ?? "";
|
|
7940
|
+
if (a === "--slug") {
|
|
7941
|
+
const v = args[i + 1];
|
|
7942
|
+
if (v === undefined)
|
|
7943
|
+
return "missing value for --slug";
|
|
7944
|
+
if (slug !== null)
|
|
7945
|
+
return "cannot mix --slug with positional slug (or pass --slug twice)";
|
|
7946
|
+
slug = v;
|
|
7947
|
+
i += 2;
|
|
7948
|
+
continue;
|
|
7949
|
+
}
|
|
7950
|
+
if (a === "--json") {
|
|
7951
|
+
json = true;
|
|
7952
|
+
i += 1;
|
|
7953
|
+
continue;
|
|
7954
|
+
}
|
|
7955
|
+
if (a.startsWith("--")) {
|
|
7956
|
+
return `unknown flag "${a}"`;
|
|
7957
|
+
}
|
|
7958
|
+
if (slug !== null) {
|
|
7959
|
+
return "cannot mix --slug with positional slug (or pass two positional slugs)";
|
|
7960
|
+
}
|
|
7961
|
+
slug = a;
|
|
7962
|
+
i += 1;
|
|
7963
|
+
}
|
|
7964
|
+
if (slug === null) {
|
|
7965
|
+
slug = inferSlug({ cwd, warn });
|
|
7966
|
+
}
|
|
7967
|
+
if (slug === null) {
|
|
7968
|
+
return "slug not provided + cannot infer from cwd. " + "Pass <slug> or --slug <slug>, cd into launchpad-app-<slug>/, or run from a directory with launchpad.yaml.";
|
|
7969
|
+
}
|
|
7970
|
+
if (!SLUG_RE10.test(slug)) {
|
|
7971
|
+
return `invalid slug "${slug}" — expected ${SLUG_RE10.source}`;
|
|
7972
|
+
}
|
|
7973
|
+
return { slug, json };
|
|
7974
|
+
}
|
|
7975
|
+
function printUsage5(io) {
|
|
7976
|
+
io.err([
|
|
7977
|
+
"usage: launchpad recover [<slug>] [--slug <slug>] [--json]",
|
|
7978
|
+
"",
|
|
7979
|
+
" Repair an app whose registry record is stuck at a terminal",
|
|
7980
|
+
" provisioning failure although the app is actually live (e.g. a",
|
|
7981
|
+
" since-fixed platform bug failed the record after content shipped).",
|
|
7982
|
+
"",
|
|
7983
|
+
" The bot verifies LIVE Cloudflare state first: the record is only",
|
|
7984
|
+
" repaired when the Pages project exists and a successful production",
|
|
7985
|
+
" deployment is serving. A not-live app is refused with what was",
|
|
7986
|
+
" checked — recover never fabricates a live state.",
|
|
7987
|
+
"",
|
|
7988
|
+
" Recovering an already-healthy app is a no-op success.",
|
|
7989
|
+
"",
|
|
7990
|
+
"Flags:",
|
|
7991
|
+
" --slug <slug> override cwd inference.",
|
|
7992
|
+
" --json emit machine-readable JSON to stdout."
|
|
7993
|
+
].join(`
|
|
7994
|
+
`));
|
|
7995
|
+
}
|
|
7996
|
+
|
|
7509
7997
|
// src/commands/status.ts
|
|
7510
|
-
import { readFileSync as
|
|
7998
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
7511
7999
|
var statusCommand = {
|
|
7512
8000
|
name: "status",
|
|
7513
8001
|
summary: "show drift between local launchpad.yaml and deployed state",
|
|
7514
8002
|
run: runStatus
|
|
7515
8003
|
};
|
|
7516
|
-
var
|
|
8004
|
+
var SLUG_RE11 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
7517
8005
|
async function fetchLifecycle(cfg, slug) {
|
|
7518
8006
|
try {
|
|
7519
8007
|
return await apiJson(cfg, { path: `/apps/${slug}/lifecycle` });
|
|
@@ -7541,14 +8029,14 @@ function mapBotError(e, slug, io) {
|
|
|
7541
8029
|
io.err(`launchpad status: ${e.message}`);
|
|
7542
8030
|
return 2;
|
|
7543
8031
|
}
|
|
7544
|
-
io.err(`launchpad status failed: ${
|
|
8032
|
+
io.err(`launchpad status failed: ${describe25(e)}`);
|
|
7545
8033
|
return 2;
|
|
7546
8034
|
}
|
|
7547
8035
|
async function runStatus(args, io) {
|
|
7548
|
-
const parsed = parseArgs9(args);
|
|
8036
|
+
const parsed = parseArgs9(args, process.cwd(), (l) => io.err(l));
|
|
7549
8037
|
if (typeof parsed === "string") {
|
|
7550
8038
|
io.err(`launchpad status: ${parsed}`);
|
|
7551
|
-
|
|
8039
|
+
printUsage6(io);
|
|
7552
8040
|
return 64;
|
|
7553
8041
|
}
|
|
7554
8042
|
const cfg = loadConfig();
|
|
@@ -7562,34 +8050,36 @@ async function runStatus(args, io) {
|
|
|
7562
8050
|
emit3(lifecycleOutput(parsed.slug, lifecycle), parsed.json, io);
|
|
7563
8051
|
return 0;
|
|
7564
8052
|
}
|
|
7565
|
-
let localYaml;
|
|
8053
|
+
let localYaml = null;
|
|
7566
8054
|
try {
|
|
7567
|
-
localYaml =
|
|
8055
|
+
localYaml = readFileSync12(parsed.file, "utf8");
|
|
7568
8056
|
} catch (e) {
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
8057
|
+
if (!isEnoent(e)) {
|
|
8058
|
+
io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe25(e)}`);
|
|
8059
|
+
return 2;
|
|
7572
8060
|
}
|
|
7573
|
-
return 2;
|
|
7574
8061
|
}
|
|
7575
|
-
let
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
if (localParse.kind !== "ok") {
|
|
7585
|
-
io.err(`launchpad status: ${parsed.file} failed schema validation:`);
|
|
7586
|
-
for (const issue of localParse.issues) {
|
|
7587
|
-
io.err(` - ${issue.path}: ${issue.message}`);
|
|
8062
|
+
let local = null;
|
|
8063
|
+
if (localYaml !== null) {
|
|
8064
|
+
let localObj;
|
|
8065
|
+
try {
|
|
8066
|
+
const { parse: parseYaml6 } = await import("yaml");
|
|
8067
|
+
localObj = parseYaml6(localYaml);
|
|
8068
|
+
} catch (e) {
|
|
8069
|
+
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe25(e)}`);
|
|
8070
|
+
return 2;
|
|
7588
8071
|
}
|
|
7589
|
-
|
|
8072
|
+
const localParse = parseManifest(localObj);
|
|
8073
|
+
if (localParse.kind !== "ok") {
|
|
8074
|
+
io.err(`launchpad status: ${parsed.file} failed schema validation:`);
|
|
8075
|
+
for (const issue of localParse.issues) {
|
|
8076
|
+
io.err(` - ${issue.path}: ${issue.message}`);
|
|
8077
|
+
}
|
|
8078
|
+
return 2;
|
|
8079
|
+
}
|
|
8080
|
+
local = localParse.manifest;
|
|
8081
|
+
warnSecretShape(local.production_env, io);
|
|
7590
8082
|
}
|
|
7591
|
-
const local = localParse.manifest;
|
|
7592
|
-
warnSecretShape(local.production_env, io);
|
|
7593
8083
|
let state;
|
|
7594
8084
|
try {
|
|
7595
8085
|
state = await fetchManifestState(cfg, parsed.slug, { includeManifest: true });
|
|
@@ -7605,7 +8095,7 @@ async function runStatus(args, io) {
|
|
|
7605
8095
|
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
7606
8096
|
return mapBotError(e, parsed.slug, io);
|
|
7607
8097
|
}
|
|
7608
|
-
io.err(`launchpad status: live deployment state unavailable (${
|
|
8098
|
+
io.err(`launchpad status: live deployment state unavailable (${describe25(e)}) — ` + `the report below is from the platform manifest view only.`);
|
|
7609
8099
|
}
|
|
7610
8100
|
let standingExceptions = null;
|
|
7611
8101
|
try {
|
|
@@ -7614,7 +8104,7 @@ async function runStatus(args, io) {
|
|
|
7614
8104
|
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
7615
8105
|
return mapBotError(e, parsed.slug, io);
|
|
7616
8106
|
}
|
|
7617
|
-
io.err(`launchpad status: standing-exception inventory unavailable (${
|
|
8107
|
+
io.err(`launchpad status: standing-exception inventory unavailable (${describe25(e)}).`);
|
|
7618
8108
|
}
|
|
7619
8109
|
if (state.manifestYaml === null || state.manifestYaml === undefined) {
|
|
7620
8110
|
const live = deployment?.liveDeployment ?? null;
|
|
@@ -7623,6 +8113,7 @@ async function runStatus(args, io) {
|
|
|
7623
8113
|
const out = {
|
|
7624
8114
|
state: contentIsLive ? "live_content_untracked" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
|
|
7625
8115
|
slug: parsed.slug,
|
|
8116
|
+
...local === null ? { drift: null } : {},
|
|
7626
8117
|
deployedSha: state.lastAppliedManifestSha,
|
|
7627
8118
|
headSha: state.appRepoHeadSha,
|
|
7628
8119
|
hasOpenPr: state.openPr !== null,
|
|
@@ -7635,12 +8126,36 @@ async function runStatus(args, io) {
|
|
|
7635
8126
|
emit3(out, parsed.json, io);
|
|
7636
8127
|
return 0;
|
|
7637
8128
|
}
|
|
8129
|
+
if (local === null) {
|
|
8130
|
+
const out = {
|
|
8131
|
+
state: "live_drift_unknown",
|
|
8132
|
+
slug: parsed.slug,
|
|
8133
|
+
drift: null,
|
|
8134
|
+
deployedSha: state.lastAppliedManifestSha,
|
|
8135
|
+
headSha: state.appRepoHeadSha,
|
|
8136
|
+
hasOpenPr: state.openPr !== null,
|
|
8137
|
+
openPrNumber: state.openPr?.number ?? null,
|
|
8138
|
+
driftFields: [],
|
|
8139
|
+
driftDetails: [],
|
|
8140
|
+
...deploymentKnown ? { deployment } : {},
|
|
8141
|
+
...standingExceptions !== null ? { standingExceptions } : {}
|
|
8142
|
+
};
|
|
8143
|
+
emit3(out, parsed.json, io);
|
|
8144
|
+
if (parsed.strict) {
|
|
8145
|
+
if (deployment?.liveDeployment?.buildStatus === "failure") {
|
|
8146
|
+
io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
|
|
8147
|
+
return 1;
|
|
8148
|
+
}
|
|
8149
|
+
io.err(`launchpad status: --strict: drift not evaluated — no local ` + `launchpad.yaml here; live state looks healthy, exiting 0.`);
|
|
8150
|
+
}
|
|
8151
|
+
return 0;
|
|
8152
|
+
}
|
|
7638
8153
|
let deployedObj;
|
|
7639
8154
|
try {
|
|
7640
|
-
const { parse:
|
|
7641
|
-
deployedObj =
|
|
8155
|
+
const { parse: parseYaml6 } = await import("yaml");
|
|
8156
|
+
deployedObj = parseYaml6(state.manifestYaml);
|
|
7642
8157
|
} catch (e) {
|
|
7643
|
-
io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${
|
|
8158
|
+
io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe25(e)}`);
|
|
7644
8159
|
return 2;
|
|
7645
8160
|
}
|
|
7646
8161
|
const deployedParse = parseManifest(deployedObj);
|
|
@@ -7673,17 +8188,17 @@ async function runStatus(args, io) {
|
|
|
7673
8188
|
}
|
|
7674
8189
|
function computeDrift(local, deployed) {
|
|
7675
8190
|
const diffs = [];
|
|
7676
|
-
const cmp = (
|
|
8191
|
+
const cmp = (path11, l, d) => {
|
|
7677
8192
|
const li = l ?? null;
|
|
7678
8193
|
const di = d ?? null;
|
|
7679
8194
|
if (typeof li === "object" || typeof di === "object") {
|
|
7680
8195
|
if (JSON.stringify(li) !== JSON.stringify(di)) {
|
|
7681
|
-
diffs.push({ path:
|
|
8196
|
+
diffs.push({ path: path11, local: l, deployed: d });
|
|
7682
8197
|
}
|
|
7683
8198
|
return;
|
|
7684
8199
|
}
|
|
7685
8200
|
if (li !== di) {
|
|
7686
|
-
diffs.push({ path:
|
|
8201
|
+
diffs.push({ path: path11, local: l, deployed: d });
|
|
7687
8202
|
}
|
|
7688
8203
|
};
|
|
7689
8204
|
cmp("metadata.name", local.metadata.name, deployed.metadata.name);
|
|
@@ -7748,18 +8263,28 @@ function emit3(out, asJson, io) {
|
|
|
7748
8263
|
return;
|
|
7749
8264
|
case "live_no_content":
|
|
7750
8265
|
io.out(`${out.slug}: live — no content deployed yet. Run \`launchpad deploy\`.`);
|
|
8266
|
+
surfaceNoLocalManifestNote(out, io);
|
|
7751
8267
|
surfaceHeadVsDeployed(out, io);
|
|
7752
8268
|
surfaceDeployment(out, io);
|
|
7753
8269
|
surfaceExceptions(out, io);
|
|
7754
8270
|
return;
|
|
7755
8271
|
case "no_deployed_manifest":
|
|
7756
8272
|
io.out(`${out.slug}: no deployed manifest yet — run \`launchpad deploy\`.`);
|
|
8273
|
+
surfaceNoLocalManifestNote(out, io);
|
|
7757
8274
|
surfaceHeadVsDeployed(out, io);
|
|
7758
8275
|
surfaceDeployment(out, io);
|
|
7759
8276
|
surfaceExceptions(out, io);
|
|
7760
8277
|
return;
|
|
7761
8278
|
case "live_content_untracked":
|
|
7762
8279
|
io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
|
|
8280
|
+
surfaceNoLocalManifestNote(out, io);
|
|
8281
|
+
surfaceHeadVsDeployed(out, io);
|
|
8282
|
+
surfaceDeployment(out, io);
|
|
8283
|
+
surfaceExceptions(out, io);
|
|
8284
|
+
return;
|
|
8285
|
+
case "live_drift_unknown":
|
|
8286
|
+
io.out(`${out.slug}: live` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
|
|
8287
|
+
surfaceNoLocalManifestNote(out, io);
|
|
7763
8288
|
surfaceHeadVsDeployed(out, io);
|
|
7764
8289
|
surfaceDeployment(out, io);
|
|
7765
8290
|
surfaceExceptions(out, io);
|
|
@@ -7783,6 +8308,11 @@ function emit3(out, asJson, io) {
|
|
|
7783
8308
|
return;
|
|
7784
8309
|
}
|
|
7785
8310
|
}
|
|
8311
|
+
function surfaceNoLocalManifestNote(out, io) {
|
|
8312
|
+
if (out.drift !== null)
|
|
8313
|
+
return;
|
|
8314
|
+
io.out(" no local launchpad.yaml here — drift not checked " + "(cd into the app directory or pass --file to compare)");
|
|
8315
|
+
}
|
|
7786
8316
|
function triggerLabel(trigger) {
|
|
7787
8317
|
if (trigger === "git-push")
|
|
7788
8318
|
return "git push";
|
|
@@ -7870,7 +8400,7 @@ function warnSecretShape(env, io) {
|
|
|
7870
8400
|
}
|
|
7871
8401
|
}
|
|
7872
8402
|
}
|
|
7873
|
-
function parseArgs9(args, cwd = process.cwd()) {
|
|
8403
|
+
function parseArgs9(args, cwd = process.cwd(), warn) {
|
|
7874
8404
|
let slug = null;
|
|
7875
8405
|
let file = "./launchpad.yaml";
|
|
7876
8406
|
let json = false;
|
|
@@ -7917,54 +8447,68 @@ function parseArgs9(args, cwd = process.cwd()) {
|
|
|
7917
8447
|
i += 1;
|
|
7918
8448
|
}
|
|
7919
8449
|
if (slug === null) {
|
|
7920
|
-
const inferred =
|
|
8450
|
+
const inferred = inferSlug({ cwd, file, warn });
|
|
7921
8451
|
if (inferred === null) {
|
|
7922
8452
|
return `slug not provided + cannot infer from cwd. Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
7923
8453
|
}
|
|
7924
8454
|
slug = inferred;
|
|
7925
8455
|
}
|
|
7926
|
-
if (!
|
|
7927
|
-
return `invalid slug "${slug}" — expected ${
|
|
8456
|
+
if (!SLUG_RE11.test(slug)) {
|
|
8457
|
+
return `invalid slug "${slug}" — expected ${SLUG_RE11.source}`;
|
|
7928
8458
|
}
|
|
7929
8459
|
return { slug, file, json, strict };
|
|
7930
8460
|
}
|
|
7931
|
-
function
|
|
8461
|
+
function printUsage6(io) {
|
|
7932
8462
|
io.err([
|
|
7933
8463
|
"usage: launchpad status [<slug>] [--slug <slug>] [--file <path>] [--json] [--strict]",
|
|
7934
8464
|
"",
|
|
7935
8465
|
" Compare local launchpad.yaml against the deployed state.",
|
|
7936
8466
|
" No local platform-repo or terraform required.",
|
|
7937
8467
|
"",
|
|
7938
|
-
"
|
|
7939
|
-
"
|
|
8468
|
+
" With no explicit slug, it is inferred from (in order): the",
|
|
8469
|
+
" local manifest's declared slug (./launchpad.yaml or --file),",
|
|
8470
|
+
" then a launchpad-app-<slug>/ directory name. An explicit",
|
|
8471
|
+
" --slug or positional always overrides; when the manifest and",
|
|
8472
|
+
" the directory name disagree, the manifest wins (with a note).",
|
|
8473
|
+
"",
|
|
8474
|
+
" With a known slug but NO local manifest, status degrades to",
|
|
8475
|
+
" the live-truth-only view (lifecycle + deployment; drift not",
|
|
8476
|
+
" checked) and exits 0.",
|
|
7940
8477
|
"",
|
|
7941
8478
|
"Flags:",
|
|
7942
8479
|
" --file <path> local manifest path (default: ./launchpad.yaml).",
|
|
7943
|
-
"
|
|
7944
|
-
"
|
|
8480
|
+
" Also consulted for slug inference; --slug or a",
|
|
8481
|
+
" positional slug still overrides.",
|
|
8482
|
+
" --slug <slug> override inference.",
|
|
7945
8483
|
" --json emit machine-readable JSON to stdout.",
|
|
7946
8484
|
" --strict exit 1 on drift. Default is report-only (exit 0).",
|
|
8485
|
+
" With no local manifest, drift can't be evaluated:",
|
|
8486
|
+
" exit 0 unless the live build itself failed.",
|
|
7947
8487
|
"",
|
|
7948
8488
|
"Exit codes:",
|
|
7949
|
-
" 0 = in sync, OR drift in default (report-only) mode
|
|
7950
|
-
"
|
|
7951
|
-
"
|
|
8489
|
+
" 0 = in sync, OR drift in default (report-only) mode, OR the",
|
|
8490
|
+
" live-truth-only view (no local manifest).",
|
|
8491
|
+
" 1 = drift, when --strict is set (or a failed live build under",
|
|
8492
|
+
" --strict with no local manifest).",
|
|
8493
|
+
" 2 = error (network, auth, unreadable/invalid local manifest, etc.)."
|
|
7952
8494
|
].join(`
|
|
7953
8495
|
`));
|
|
7954
8496
|
}
|
|
7955
|
-
function
|
|
8497
|
+
function describe25(e) {
|
|
7956
8498
|
return e instanceof Error ? e.message : String(e);
|
|
7957
8499
|
}
|
|
8500
|
+
function isEnoent(e) {
|
|
8501
|
+
return typeof e === "object" && e !== null && e.code === "ENOENT";
|
|
8502
|
+
}
|
|
7958
8503
|
|
|
7959
8504
|
// src/commands/review.ts
|
|
7960
|
-
import * as
|
|
8505
|
+
import * as path11 from "node:path";
|
|
7961
8506
|
var reviewCommand = {
|
|
7962
8507
|
name: "review",
|
|
7963
8508
|
summary: "show the review state for a PR (slug-scoped)",
|
|
7964
8509
|
run: runReview
|
|
7965
8510
|
};
|
|
7966
|
-
var
|
|
7967
|
-
var DIRNAME_RE5 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
8511
|
+
var SLUG_RE12 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
7968
8512
|
async function runReview(args, io) {
|
|
7969
8513
|
const parsed = parseArgs10(args);
|
|
7970
8514
|
if (parsed === null) {
|
|
@@ -7976,16 +8520,16 @@ async function runReview(args, io) {
|
|
|
7976
8520
|
if (parsed.slug !== null) {
|
|
7977
8521
|
slug = parsed.slug;
|
|
7978
8522
|
} else {
|
|
7979
|
-
const inferred =
|
|
8523
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
7980
8524
|
if (inferred === null) {
|
|
7981
|
-
io.err(`launchpad review: could not infer slug from cwd (${
|
|
8525
|
+
io.err(`launchpad review: could not infer slug from cwd (${path11.basename(process.cwd())});
|
|
7982
8526
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
7983
8527
|
return 64;
|
|
7984
8528
|
}
|
|
7985
8529
|
slug = inferred;
|
|
7986
8530
|
}
|
|
7987
|
-
if (!
|
|
7988
|
-
io.err(`launchpad review: invalid slug "${slug}" — expected ${
|
|
8531
|
+
if (!SLUG_RE12.test(slug)) {
|
|
8532
|
+
io.err(`launchpad review: invalid slug "${slug}" — expected ${SLUG_RE12.source}`);
|
|
7989
8533
|
return 64;
|
|
7990
8534
|
}
|
|
7991
8535
|
try {
|
|
@@ -8028,7 +8572,7 @@ async function runReview(args, io) {
|
|
|
8028
8572
|
io.err(`launchpad review: ${e.message}`);
|
|
8029
8573
|
return 1;
|
|
8030
8574
|
}
|
|
8031
|
-
io.err(`launchpad review failed: ${
|
|
8575
|
+
io.err(`launchpad review failed: ${describe26(e)}`);
|
|
8032
8576
|
return 1;
|
|
8033
8577
|
}
|
|
8034
8578
|
}
|
|
@@ -8060,11 +8604,6 @@ function parseArgs10(args) {
|
|
|
8060
8604
|
}
|
|
8061
8605
|
return { slug, prNumber };
|
|
8062
8606
|
}
|
|
8063
|
-
function inferSlugFromCwd5(cwd) {
|
|
8064
|
-
const base = path10.basename(cwd);
|
|
8065
|
-
const m = base.match(DIRNAME_RE5);
|
|
8066
|
-
return m === null ? null : m[1];
|
|
8067
|
-
}
|
|
8068
8607
|
function renderReview(r, io) {
|
|
8069
8608
|
const rv = r.review;
|
|
8070
8609
|
io.out(`Review of PR #${rv.prNumber} (${rv.slug})`);
|
|
@@ -8111,12 +8650,12 @@ function severityRank(s) {
|
|
|
8111
8650
|
return 3;
|
|
8112
8651
|
}
|
|
8113
8652
|
}
|
|
8114
|
-
function
|
|
8653
|
+
function describe26(e) {
|
|
8115
8654
|
return e instanceof Error ? e.message : String(e);
|
|
8116
8655
|
}
|
|
8117
8656
|
|
|
8118
8657
|
// src/deploy/rollback.ts
|
|
8119
|
-
import { existsSync as
|
|
8658
|
+
import { existsSync as existsSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8120
8659
|
import { resolve as resolvePath2 } from "node:path";
|
|
8121
8660
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
8122
8661
|
|
|
@@ -8171,7 +8710,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
8171
8710
|
cwd: process.cwd()
|
|
8172
8711
|
});
|
|
8173
8712
|
} catch (e) {
|
|
8174
|
-
io.err(`launchpad rollback: git rev-parse failed to start: ${
|
|
8713
|
+
io.err(`launchpad rollback: git rev-parse failed to start: ${describe27(e)}`);
|
|
8175
8714
|
return 2;
|
|
8176
8715
|
}
|
|
8177
8716
|
if (revParse.exitCode !== 0) {
|
|
@@ -8186,7 +8725,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
8186
8725
|
try {
|
|
8187
8726
|
show = await runner.run("git", ["show", `${verifiedSha}:${manifestRelpath}`], { cwd: process.cwd() });
|
|
8188
8727
|
} catch (e) {
|
|
8189
|
-
io.err(`launchpad rollback: git show failed to start: ${
|
|
8728
|
+
io.err(`launchpad rollback: git show failed to start: ${describe27(e)}`);
|
|
8190
8729
|
return 2;
|
|
8191
8730
|
}
|
|
8192
8731
|
if (show.exitCode !== 0) {
|
|
@@ -8220,7 +8759,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
8220
8759
|
try {
|
|
8221
8760
|
writeFileSync5(manifestPath, historicalYaml, "utf8");
|
|
8222
8761
|
} catch (e) {
|
|
8223
|
-
io.err(`launchpad rollback: failed to write ${manifestPath}: ${
|
|
8762
|
+
io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe27(e)}`);
|
|
8224
8763
|
return 2;
|
|
8225
8764
|
}
|
|
8226
8765
|
io.out(`Restored ${manifestPath} from ${verifiedSha.slice(0, 12)}.`);
|
|
@@ -8240,16 +8779,16 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
8240
8779
|
yes: true
|
|
8241
8780
|
}, io, applyDeps);
|
|
8242
8781
|
}
|
|
8243
|
-
function readCurrentManifest(
|
|
8244
|
-
if (!
|
|
8782
|
+
function readCurrentManifest(path12) {
|
|
8783
|
+
if (!existsSync8(path12))
|
|
8245
8784
|
return null;
|
|
8246
8785
|
let raw;
|
|
8247
8786
|
try {
|
|
8248
|
-
raw =
|
|
8787
|
+
raw = readFileSync13(path12, "utf8");
|
|
8249
8788
|
} catch {
|
|
8250
8789
|
return null;
|
|
8251
8790
|
}
|
|
8252
|
-
const parsed = parseManifest2(raw,
|
|
8791
|
+
const parsed = parseManifest2(raw, path12);
|
|
8253
8792
|
if (parsed.kind !== "ok")
|
|
8254
8793
|
return null;
|
|
8255
8794
|
return summarise(parsed.manifest);
|
|
@@ -8302,7 +8841,7 @@ async function defaultPrompt4(question) {
|
|
|
8302
8841
|
rl.close();
|
|
8303
8842
|
}
|
|
8304
8843
|
}
|
|
8305
|
-
function
|
|
8844
|
+
function describe27(e) {
|
|
8306
8845
|
return e instanceof Error ? e.message : String(e);
|
|
8307
8846
|
}
|
|
8308
8847
|
function renderManifestError3(result, io) {
|
|
@@ -8401,7 +8940,7 @@ function parseArgs11(args) {
|
|
|
8401
8940
|
}
|
|
8402
8941
|
|
|
8403
8942
|
// src/secrets/push.ts
|
|
8404
|
-
import { existsSync as
|
|
8943
|
+
import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
|
|
8405
8944
|
import { resolve as resolvePath3 } from "node:path";
|
|
8406
8945
|
|
|
8407
8946
|
// src/secrets/env-parse.ts
|
|
@@ -8456,16 +8995,16 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
8456
8995
|
return 0;
|
|
8457
8996
|
}
|
|
8458
8997
|
const envPath = resolvePath3(process.cwd(), opts.env ?? ".env");
|
|
8459
|
-
if (!
|
|
8998
|
+
if (!existsSync9(envPath)) {
|
|
8460
8999
|
io.err(`launchpad secrets push: ${envPath}`);
|
|
8461
9000
|
io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
|
|
8462
9001
|
return 2;
|
|
8463
9002
|
}
|
|
8464
9003
|
let envText;
|
|
8465
9004
|
try {
|
|
8466
|
-
envText =
|
|
9005
|
+
envText = readFileSync14(envPath, "utf8");
|
|
8467
9006
|
} catch (e) {
|
|
8468
|
-
io.err(`launchpad secrets push: failed to read ${envPath}: ${
|
|
9007
|
+
io.err(`launchpad secrets push: failed to read ${envPath}: ${describe28(e)}`);
|
|
8469
9008
|
return 2;
|
|
8470
9009
|
}
|
|
8471
9010
|
const parsed = parseEnv(envText);
|
|
@@ -8596,10 +9135,10 @@ function mapPushError(e, slug, name, io) {
|
|
|
8596
9135
|
io.err(`✗ secrets push ${name}: ${e.message}`);
|
|
8597
9136
|
return 1;
|
|
8598
9137
|
}
|
|
8599
|
-
io.err(`✗ secrets push ${name} failed: ${
|
|
9138
|
+
io.err(`✗ secrets push ${name} failed: ${describe28(e)}`);
|
|
8600
9139
|
return 2;
|
|
8601
9140
|
}
|
|
8602
|
-
function
|
|
9141
|
+
function describe28(e) {
|
|
8603
9142
|
return e instanceof Error ? e.message : String(e);
|
|
8604
9143
|
}
|
|
8605
9144
|
function renderManifestError4(result, io) {
|
|
@@ -8735,32 +9274,32 @@ function renderManifestError5(result, io) {
|
|
|
8735
9274
|
}
|
|
8736
9275
|
|
|
8737
9276
|
// src/secrets/set.ts
|
|
8738
|
-
import { existsSync as
|
|
9277
|
+
import { existsSync as existsSync10, readFileSync as readFileSync15 } from "node:fs";
|
|
8739
9278
|
import { resolve as resolvePath5 } from "node:path";
|
|
8740
|
-
import { parse as
|
|
9279
|
+
import { parse as parseYaml6 } from "yaml";
|
|
8741
9280
|
var CELL_LABEL2 = {
|
|
8742
9281
|
present: "PRESENT",
|
|
8743
9282
|
missing: "MISSING",
|
|
8744
9283
|
not_deployed: "NOT_DEPLOYED"
|
|
8745
9284
|
};
|
|
8746
9285
|
function loadSet(fleetFile, setName, io) {
|
|
8747
|
-
const
|
|
8748
|
-
if (!
|
|
8749
|
-
io.err(`✗ ${
|
|
9286
|
+
const path12 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
|
|
9287
|
+
if (!existsSync10(path12)) {
|
|
9288
|
+
io.err(`✗ ${path12}`);
|
|
8750
9289
|
io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
|
|
8751
9290
|
return 2;
|
|
8752
9291
|
}
|
|
8753
9292
|
let obj;
|
|
8754
9293
|
try {
|
|
8755
|
-
obj =
|
|
9294
|
+
obj = parseYaml6(readFileSync15(path12, "utf8"));
|
|
8756
9295
|
} catch (e) {
|
|
8757
|
-
io.err(`✗ ${
|
|
9296
|
+
io.err(`✗ ${path12}`);
|
|
8758
9297
|
io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
8759
9298
|
return 1;
|
|
8760
9299
|
}
|
|
8761
9300
|
const parsed = parseFleetSecretSets(obj);
|
|
8762
9301
|
if (!parsed.ok) {
|
|
8763
|
-
io.err(`✗ ${
|
|
9302
|
+
io.err(`✗ ${path12}`);
|
|
8764
9303
|
io.err(` ${parsed.issues.length} schema issue(s):`);
|
|
8765
9304
|
for (const i of parsed.issues)
|
|
8766
9305
|
io.err(` ${i.path}: ${i.message}`);
|
|
@@ -8769,7 +9308,7 @@ function loadSet(fleetFile, setName, io) {
|
|
|
8769
9308
|
const set = parsed.manifest.secretSets.find((s) => s.name === setName);
|
|
8770
9309
|
if (set === undefined) {
|
|
8771
9310
|
const names = parsed.manifest.secretSets.map((s) => s.name).join(", ");
|
|
8772
|
-
io.err(`✗ secret-set "${setName}" not found in ${
|
|
9311
|
+
io.err(`✗ secret-set "${setName}" not found in ${path12}`);
|
|
8773
9312
|
io.err(` declared sets: ${names}`);
|
|
8774
9313
|
return 1;
|
|
8775
9314
|
}
|
|
@@ -8837,14 +9376,14 @@ async function runSecretsPushSet(opts, io, deps = {}) {
|
|
|
8837
9376
|
if (typeof set === "number")
|
|
8838
9377
|
return set;
|
|
8839
9378
|
const envPath = resolvePath5(process.cwd(), opts.env ?? ".env");
|
|
8840
|
-
if (!
|
|
9379
|
+
if (!existsSync10(envPath)) {
|
|
8841
9380
|
io.err(`launchpad secrets push --set: ${envPath} not found.`);
|
|
8842
9381
|
io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
|
|
8843
9382
|
return 2;
|
|
8844
9383
|
}
|
|
8845
9384
|
let envText;
|
|
8846
9385
|
try {
|
|
8847
|
-
envText =
|
|
9386
|
+
envText = readFileSync15(envPath, "utf8");
|
|
8848
9387
|
} catch (e) {
|
|
8849
9388
|
io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
8850
9389
|
return 2;
|
|
@@ -8988,8 +9527,8 @@ function setPushExit(e) {
|
|
|
8988
9527
|
}
|
|
8989
9528
|
|
|
8990
9529
|
// src/commands/secrets-template.ts
|
|
8991
|
-
import { existsSync as
|
|
8992
|
-
import { resolve as
|
|
9530
|
+
import { existsSync as existsSync11, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9531
|
+
import { resolve as resolve10 } from "node:path";
|
|
8993
9532
|
async function runSecretsTemplate(args, io) {
|
|
8994
9533
|
const flags = parseFlags4(args);
|
|
8995
9534
|
if (flags.kind === "usage-error") {
|
|
@@ -8997,7 +9536,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
8997
9536
|
io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
|
|
8998
9537
|
return 64;
|
|
8999
9538
|
}
|
|
9000
|
-
const manifestPath =
|
|
9539
|
+
const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
9001
9540
|
const result = loadManifest(manifestPath);
|
|
9002
9541
|
const renderResult = renderManifest(result, io);
|
|
9003
9542
|
if (renderResult.kind !== "ok") {
|
|
@@ -9011,8 +9550,8 @@ async function runSecretsTemplate(args, io) {
|
|
|
9011
9550
|
}
|
|
9012
9551
|
return 0;
|
|
9013
9552
|
}
|
|
9014
|
-
const outPath =
|
|
9015
|
-
if (
|
|
9553
|
+
const outPath = resolve10(process.cwd(), flags.out);
|
|
9554
|
+
if (existsSync11(outPath) && !flags.force) {
|
|
9016
9555
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
9017
9556
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
9018
9557
|
return 64;
|
|
@@ -9300,8 +9839,8 @@ function printHelp2(io) {
|
|
|
9300
9839
|
|
|
9301
9840
|
// src/commands/skills.ts
|
|
9302
9841
|
import { fileURLToPath } from "node:url";
|
|
9303
|
-
import { dirname as dirname6, join as join11, resolve as
|
|
9304
|
-
import { promises as fs5, existsSync as
|
|
9842
|
+
import { dirname as dirname6, join as join11, resolve as resolve11 } from "node:path";
|
|
9843
|
+
import { promises as fs5, existsSync as existsSync12 } from "node:fs";
|
|
9305
9844
|
import { homedir as homedir2 } from "node:os";
|
|
9306
9845
|
var BUNDLE_PREFIX = "launchpad-";
|
|
9307
9846
|
var BUNDLED_SKILLS = [
|
|
@@ -9343,7 +9882,7 @@ async function runSkills(args, io) {
|
|
|
9343
9882
|
return 64;
|
|
9344
9883
|
}
|
|
9345
9884
|
} catch (e) {
|
|
9346
|
-
io.err(`launchpad skills ${action}: ${
|
|
9885
|
+
io.err(`launchpad skills ${action}: ${describe29(e)}`);
|
|
9347
9886
|
return 1;
|
|
9348
9887
|
}
|
|
9349
9888
|
}
|
|
@@ -9368,11 +9907,11 @@ function resolveInstallEnv() {
|
|
|
9368
9907
|
function defaultBundleDir() {
|
|
9369
9908
|
const here = dirname6(fileURLToPath(import.meta.url));
|
|
9370
9909
|
const candidates = [
|
|
9371
|
-
|
|
9372
|
-
|
|
9910
|
+
resolve11(here, "..", "skills"),
|
|
9911
|
+
resolve11(here, "..", "..", "skills")
|
|
9373
9912
|
];
|
|
9374
9913
|
for (const c of candidates) {
|
|
9375
|
-
if (
|
|
9914
|
+
if (existsSync12(join11(c, "launchpad-onboard", "SKILL.md"))) {
|
|
9376
9915
|
return c;
|
|
9377
9916
|
}
|
|
9378
9917
|
}
|
|
@@ -9452,10 +9991,10 @@ async function doList(io) {
|
|
|
9452
9991
|
}
|
|
9453
9992
|
return 0;
|
|
9454
9993
|
}
|
|
9455
|
-
async function readVersion(
|
|
9994
|
+
async function readVersion(path12) {
|
|
9456
9995
|
let text;
|
|
9457
9996
|
try {
|
|
9458
|
-
text = await fs5.readFile(
|
|
9997
|
+
text = await fs5.readFile(path12, "utf8");
|
|
9459
9998
|
} catch {
|
|
9460
9999
|
return null;
|
|
9461
10000
|
}
|
|
@@ -9465,15 +10004,15 @@ async function readVersion(path11) {
|
|
|
9465
10004
|
const m = /^version:\s*(.+?)\s*$/m.exec(front);
|
|
9466
10005
|
return m === null ? null : m[1] ?? null;
|
|
9467
10006
|
}
|
|
9468
|
-
async function isDir(
|
|
10007
|
+
async function isDir(path12) {
|
|
9469
10008
|
try {
|
|
9470
|
-
const stat = await fs5.stat(
|
|
10009
|
+
const stat = await fs5.stat(path12);
|
|
9471
10010
|
return stat.isDirectory();
|
|
9472
10011
|
} catch {
|
|
9473
10012
|
return false;
|
|
9474
10013
|
}
|
|
9475
10014
|
}
|
|
9476
|
-
function
|
|
10015
|
+
function describe29(e) {
|
|
9477
10016
|
return e instanceof Error ? e.message : String(e);
|
|
9478
10017
|
}
|
|
9479
10018
|
|
|
@@ -9481,13 +10020,13 @@ function describe28(e) {
|
|
|
9481
10020
|
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
9482
10021
|
import { promisify } from "node:util";
|
|
9483
10022
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9484
|
-
import { dirname as dirname7, resolve as
|
|
10023
|
+
import { dirname as dirname7, resolve as resolve12, relative as relative4, isAbsolute as isAbsolute2, join as join12 } from "node:path";
|
|
9485
10024
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
9486
|
-
import { readFileSync as
|
|
10025
|
+
import { readFileSync as readFileSync16, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
9487
10026
|
|
|
9488
10027
|
// src/commands/channel-auth.ts
|
|
9489
10028
|
import { createServer as createServer2 } from "node:http";
|
|
9490
|
-
import { createHash as createHash2, randomBytes as
|
|
10029
|
+
import { createHash as createHash2, randomBytes as randomBytes5 } from "node:crypto";
|
|
9491
10030
|
var CHANNEL_BASE = "https://get.launchpad.m-kopa.us";
|
|
9492
10031
|
var CLI_AUTH_URL = `${CHANNEL_BASE}/__cli_auth`;
|
|
9493
10032
|
var CLI_TOKEN_URL = `${CHANNEL_BASE}/__cli_token`;
|
|
@@ -9497,7 +10036,7 @@ function base64url(b) {
|
|
|
9497
10036
|
return b.toString("base64url");
|
|
9498
10037
|
}
|
|
9499
10038
|
function pkcePair() {
|
|
9500
|
-
const verifier = base64url(
|
|
10039
|
+
const verifier = base64url(randomBytes5(32));
|
|
9501
10040
|
const challenge = base64url(createHash2("sha256").update(verifier).digest());
|
|
9502
10041
|
return { verifier, challenge };
|
|
9503
10042
|
}
|
|
@@ -9526,9 +10065,9 @@ async function startLoopback(state, timeoutMs) {
|
|
|
9526
10065
|
resolveCode(code);
|
|
9527
10066
|
}
|
|
9528
10067
|
});
|
|
9529
|
-
const bound = await new Promise((
|
|
9530
|
-
server.once("error", () =>
|
|
9531
|
-
server.listen(0, "127.0.0.1", () =>
|
|
10068
|
+
const bound = await new Promise((resolve12) => {
|
|
10069
|
+
server.once("error", () => resolve12(false));
|
|
10070
|
+
server.listen(0, "127.0.0.1", () => resolve12(true));
|
|
9532
10071
|
});
|
|
9533
10072
|
if (!bound)
|
|
9534
10073
|
return null;
|
|
@@ -9543,7 +10082,7 @@ async function startLoopback(state, timeoutMs) {
|
|
|
9543
10082
|
async function runChannelLoopbackUpdate(deps) {
|
|
9544
10083
|
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
9545
10084
|
const { verifier, challenge } = pkcePair();
|
|
9546
|
-
const state = base64url(
|
|
10085
|
+
const state = base64url(randomBytes5(16));
|
|
9547
10086
|
const loopback = await startLoopback(state, timeoutMs);
|
|
9548
10087
|
if (!loopback)
|
|
9549
10088
|
return { ok: false, reason: "no-browser", detail: "could not bind a loopback port" };
|
|
@@ -9712,7 +10251,7 @@ function resolveLatestVersion() {
|
|
|
9712
10251
|
}
|
|
9713
10252
|
function detectInstallChannel() {
|
|
9714
10253
|
try {
|
|
9715
|
-
return
|
|
10254
|
+
return readFileSync16(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
|
|
9716
10255
|
} catch {
|
|
9717
10256
|
return "github";
|
|
9718
10257
|
}
|
|
@@ -9775,7 +10314,7 @@ function errStderr(e) {
|
|
|
9775
10314
|
return "";
|
|
9776
10315
|
}
|
|
9777
10316
|
async function detectPackageManager2() {
|
|
9778
|
-
const pkgRoot =
|
|
10317
|
+
const pkgRoot = resolve12(dirname7(fileURLToPath2(import.meta.url)), "..", "..");
|
|
9779
10318
|
const candidates = [];
|
|
9780
10319
|
const npmRoot = await pmRoot("npm");
|
|
9781
10320
|
if (npmRoot !== null)
|
|
@@ -9783,7 +10322,7 @@ async function detectPackageManager2() {
|
|
|
9783
10322
|
const pnpmRoot = await pmRoot("pnpm");
|
|
9784
10323
|
if (pnpmRoot !== null)
|
|
9785
10324
|
candidates.push(["pnpm", pnpmRoot]);
|
|
9786
|
-
candidates.push(["bun",
|
|
10325
|
+
candidates.push(["bun", resolve12(homedir3(), ".bun/install/global/node_modules")]);
|
|
9787
10326
|
const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
|
|
9788
10327
|
return matches.length === 1 ? matches[0][0] : null;
|
|
9789
10328
|
}
|
|
@@ -9919,8 +10458,8 @@ function printHelp4(io) {
|
|
|
9919
10458
|
}
|
|
9920
10459
|
|
|
9921
10460
|
// src/commands/validate.ts
|
|
9922
|
-
import { readFileSync as
|
|
9923
|
-
import { dirname as dirname8, resolve as
|
|
10461
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
10462
|
+
import { dirname as dirname8, resolve as resolve13 } from "node:path";
|
|
9924
10463
|
var validateCommand = {
|
|
9925
10464
|
name: "validate",
|
|
9926
10465
|
summary: "validate launchpad.yaml against the v1alpha1 schema",
|
|
@@ -9934,12 +10473,12 @@ async function runValidate(args, io) {
|
|
|
9934
10473
|
return 64;
|
|
9935
10474
|
}
|
|
9936
10475
|
const { file, json, strictGroups } = parseResult;
|
|
9937
|
-
const
|
|
9938
|
-
const result = loadManifest(
|
|
10476
|
+
const path12 = resolve13(process.cwd(), file ?? "launchpad.yaml");
|
|
10477
|
+
const result = loadManifest(path12);
|
|
9939
10478
|
if (result.kind !== "ok") {
|
|
9940
10479
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
9941
10480
|
}
|
|
9942
|
-
const boundary = checkBoundary(
|
|
10481
|
+
const boundary = checkBoundary(path12, result.manifest.app !== undefined);
|
|
9943
10482
|
const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
|
|
9944
10483
|
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
9945
10484
|
}
|
|
@@ -9953,7 +10492,7 @@ function checkBoundary(manifestPath, declared) {
|
|
|
9953
10492
|
}
|
|
9954
10493
|
let manifestYaml;
|
|
9955
10494
|
try {
|
|
9956
|
-
manifestYaml =
|
|
10495
|
+
manifestYaml = readFileSync17(manifestPath, "utf8");
|
|
9957
10496
|
} catch {
|
|
9958
10497
|
manifestYaml = null;
|
|
9959
10498
|
}
|
|
@@ -9994,7 +10533,7 @@ async function checkGroup(allowedGroup, cfg) {
|
|
|
9994
10533
|
if (e instanceof ForbiddenError) {
|
|
9995
10534
|
return { kind: "forbidden", message: e.message };
|
|
9996
10535
|
}
|
|
9997
|
-
return { kind: "network", message:
|
|
10536
|
+
return { kind: "network", message: describe30(e) };
|
|
9998
10537
|
}
|
|
9999
10538
|
}
|
|
10000
10539
|
async function checkGroups(groups) {
|
|
@@ -10207,7 +10746,7 @@ function parseArgs12(args) {
|
|
|
10207
10746
|
}
|
|
10208
10747
|
return { kind: "ok", file, json, strictGroups };
|
|
10209
10748
|
}
|
|
10210
|
-
function
|
|
10749
|
+
function describe30(e) {
|
|
10211
10750
|
return e instanceof Error ? e.message : String(e);
|
|
10212
10751
|
}
|
|
10213
10752
|
|
|
@@ -10256,11 +10795,11 @@ async function runWhoami(_args, io) {
|
|
|
10256
10795
|
io.err(e.message);
|
|
10257
10796
|
return 1;
|
|
10258
10797
|
}
|
|
10259
|
-
io.err(`launchpad whoami failed: ${
|
|
10798
|
+
io.err(`launchpad whoami failed: ${describe31(e)}`);
|
|
10260
10799
|
return 1;
|
|
10261
10800
|
}
|
|
10262
10801
|
}
|
|
10263
|
-
function
|
|
10802
|
+
function describe31(e) {
|
|
10264
10803
|
return e instanceof Error ? e.message : String(e);
|
|
10265
10804
|
}
|
|
10266
10805
|
|
|
@@ -10268,14 +10807,14 @@ function describe30(e) {
|
|
|
10268
10807
|
import { spawn as spawn6 } from "node:child_process";
|
|
10269
10808
|
import { homedir as homedir4 } from "node:os";
|
|
10270
10809
|
import { join as join13 } from "node:path";
|
|
10271
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
10810
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "node:fs";
|
|
10272
10811
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
10273
10812
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
10274
10813
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
10275
10814
|
var CACHE_FILE = join13(homedir4(), ".launchpad", "update-check.json");
|
|
10276
10815
|
function readCache2() {
|
|
10277
10816
|
try {
|
|
10278
|
-
const raw = JSON.parse(
|
|
10817
|
+
const raw = JSON.parse(readFileSync18(CACHE_FILE, "utf8"));
|
|
10279
10818
|
if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
|
|
10280
10819
|
const latest = raw.latest;
|
|
10281
10820
|
return {
|
|
@@ -10391,6 +10930,7 @@ var COMMANDS = [
|
|
|
10391
10930
|
generateCommand,
|
|
10392
10931
|
pullCommand,
|
|
10393
10932
|
statusCommand,
|
|
10933
|
+
recoverCommand,
|
|
10394
10934
|
destroyCommand,
|
|
10395
10935
|
rollbackCommand,
|
|
10396
10936
|
groupsCommand,
|