@launchsecure/launch-kit 0.0.33 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/chart-serve.js +167 -2
- package/dist/server/cli.js +286 -50
- package/dist/server/course-entry.js +1 -1
- package/dist/server/graph-mcp-entry.js +180 -4
- package/dist/server/init-entry.js +563 -60
- package/dist/server/launch-bot-entry.js +4078 -0
- package/dist/server/launch-radar-entry.js +45 -0
- package/dist/server/orbit-entry.js +123 -26
- package/dist/server/parse-worker-entry.js +167 -2
- package/dist/server/radar-docker-init-entry.js +496 -39
- package/dist/server/radar-teardown-entry.js +23 -22
- package/dist/server/rover-entry.js +20555 -0
- package/package.json +8 -5
- package/scaffolds/ls-marketplace/plugins/kit/commands/standup.md +6 -6
- package/scaffolds/ls-marketplace/plugins/kit/skills/analyse/SKILL.md +6 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/SKILL.md +72 -49
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/briefs.mjs +152 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/debug/SKILL.md +45 -20
- package/scaffolds/ls-marketplace/plugins/kit/skills/deploy-check/SKILL.md +76 -67
- package/scaffolds/ls-marketplace/plugins/kit/skills/handoff/SKILL.md +132 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/kickoff/SKILL.md +151 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/orbit/SKILL.md +14 -2
- package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +149 -133
|
@@ -468,8 +468,281 @@ var init_mcp = __esm({
|
|
|
468
468
|
}
|
|
469
469
|
});
|
|
470
470
|
|
|
471
|
+
// src/server/launch-kit-services.ts
|
|
472
|
+
function defaultServices() {
|
|
473
|
+
return [expandShorthand("radar")];
|
|
474
|
+
}
|
|
475
|
+
function expandShorthand(name) {
|
|
476
|
+
if (name === "preview") {
|
|
477
|
+
const raw = process.env.PREVIEW_PORT;
|
|
478
|
+
const port = raw && Number.isFinite(Number.parseInt(raw, 10)) ? Number.parseInt(raw, 10) : 3e3;
|
|
479
|
+
return { name: "preview", port, bin: "", args: [], skipSpawn: true };
|
|
480
|
+
}
|
|
481
|
+
const def = SHORTHANDS[name];
|
|
482
|
+
if (!def) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${[...Object.keys(SHORTHANDS), "preview"].join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return { name, port: def.port, bin: def.bin, args: [...def.args] };
|
|
488
|
+
}
|
|
489
|
+
function coerceEntry(raw, index) {
|
|
490
|
+
if (typeof raw === "string") {
|
|
491
|
+
return expandShorthand(raw);
|
|
492
|
+
}
|
|
493
|
+
if (typeof raw !== "object" || raw === null) {
|
|
494
|
+
throw new Error(`[launch-kit-services] entry #${index} must be a string shorthand or an object`);
|
|
495
|
+
}
|
|
496
|
+
const r = raw;
|
|
497
|
+
if (typeof r.name !== "string" || typeof r.port !== "number" || typeof r.bin !== "string") {
|
|
498
|
+
throw new Error(`[launch-kit-services] entry #${index}: { name:string, port:number, bin:string } required`);
|
|
499
|
+
}
|
|
500
|
+
if (r.args !== void 0 && (!Array.isArray(r.args) || r.args.some((a) => typeof a !== "string"))) {
|
|
501
|
+
throw new Error(`[launch-kit-services] entry #${index}: args must be a string[]`);
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
name: r.name,
|
|
505
|
+
port: r.port,
|
|
506
|
+
bin: r.bin,
|
|
507
|
+
args: r.args ?? []
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function validate(services) {
|
|
511
|
+
if (services.length === 0) {
|
|
512
|
+
throw new Error(`[launch-kit-services] resolved an empty service list`);
|
|
513
|
+
}
|
|
514
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
515
|
+
const seenPorts = /* @__PURE__ */ new Set();
|
|
516
|
+
for (const s of services) {
|
|
517
|
+
if (!DNS_NAME_RE.test(s.name)) {
|
|
518
|
+
throw new Error(`[launch-kit-services] service name "${s.name}" is not DNS-safe (lowercase letters/digits/hyphens, \u226463 chars, no leading/trailing hyphen)`);
|
|
519
|
+
}
|
|
520
|
+
if (seenNames.has(s.name)) {
|
|
521
|
+
throw new Error(`[launch-kit-services] duplicate service name "${s.name}"`);
|
|
522
|
+
}
|
|
523
|
+
seenNames.add(s.name);
|
|
524
|
+
if (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535) {
|
|
525
|
+
throw new Error(`[launch-kit-services] service "${s.name}" has invalid port ${s.port}`);
|
|
526
|
+
}
|
|
527
|
+
if (seenPorts.has(s.port)) {
|
|
528
|
+
throw new Error(`[launch-kit-services] duplicate port ${s.port} (services must each listen on a unique port)`);
|
|
529
|
+
}
|
|
530
|
+
seenPorts.add(s.port);
|
|
531
|
+
}
|
|
532
|
+
return services;
|
|
533
|
+
}
|
|
534
|
+
function resolveServices(opts = {}) {
|
|
535
|
+
const env = opts.env ?? process.env;
|
|
536
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
537
|
+
const rawEnv = env.LAUNCHKIT_SERVICES?.trim();
|
|
538
|
+
if (rawEnv) {
|
|
539
|
+
let parsed;
|
|
540
|
+
try {
|
|
541
|
+
parsed = JSON.parse(rawEnv);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
544
|
+
}
|
|
545
|
+
if (!Array.isArray(parsed)) {
|
|
546
|
+
throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES must be a JSON array`);
|
|
547
|
+
}
|
|
548
|
+
return validate(parsed.map(coerceEntry));
|
|
549
|
+
}
|
|
550
|
+
const filePath = (0, import_node_path.join)(cwd, ".launchpod", "services.json");
|
|
551
|
+
if ((0, import_node_fs.existsSync)(filePath)) {
|
|
552
|
+
let parsed;
|
|
553
|
+
try {
|
|
554
|
+
parsed = JSON.parse((0, import_node_fs.readFileSync)(filePath, "utf8"));
|
|
555
|
+
} catch (err) {
|
|
556
|
+
throw new Error(`[launch-kit-services] ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
557
|
+
}
|
|
558
|
+
if (!Array.isArray(parsed)) {
|
|
559
|
+
throw new Error(`[launch-kit-services] ${filePath} must be a JSON array`);
|
|
560
|
+
}
|
|
561
|
+
return validate(parsed.map(coerceEntry));
|
|
562
|
+
}
|
|
563
|
+
return validate(defaultServices());
|
|
564
|
+
}
|
|
565
|
+
var import_node_fs, import_node_path, SHORTHANDS, DNS_NAME_RE, SHORTHAND_NAMES;
|
|
566
|
+
var init_launch_kit_services = __esm({
|
|
567
|
+
"src/server/launch-kit-services.ts"() {
|
|
568
|
+
"use strict";
|
|
569
|
+
import_node_fs = require("node:fs");
|
|
570
|
+
import_node_path = require("node:path");
|
|
571
|
+
SHORTHANDS = {
|
|
572
|
+
radar: { port: 3517, bin: "launch-radar", args: [] },
|
|
573
|
+
sequencer: { port: 3517, bin: "launch-sequencer", args: [] },
|
|
574
|
+
chart: { port: 52819, bin: "launch-chart", args: ["serve"] },
|
|
575
|
+
deck: { port: 52829, bin: "launch-deck", args: ["serve"] },
|
|
576
|
+
council: { port: 52839, bin: "launch-council", args: ["serve"] },
|
|
577
|
+
// Claude web terminal — exposes a viewable/drivable `claude` session at
|
|
578
|
+
// `bot.<baseDomain>`. NOTE: no auth gate yet (tracked as a separate
|
|
579
|
+
// high-priority rover-security work item); ships behind a plain link first.
|
|
580
|
+
bot: { port: 52849, bin: "launch-bot", args: ["serve"] }
|
|
581
|
+
};
|
|
582
|
+
DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
583
|
+
SHORTHAND_NAMES = [...Object.keys(SHORTHANDS), "preview"];
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// src/server/cf-ingress.ts
|
|
588
|
+
async function cf(opts) {
|
|
589
|
+
const res = await fetch(`${CF_API_BASE}${opts.path}`, {
|
|
590
|
+
method: opts.method,
|
|
591
|
+
headers: {
|
|
592
|
+
Authorization: `Bearer ${opts.apiToken}`,
|
|
593
|
+
"Content-Type": "application/json",
|
|
594
|
+
Accept: "application/json",
|
|
595
|
+
"User-Agent": "launch-kit/cf-ingress"
|
|
596
|
+
},
|
|
597
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
598
|
+
signal: AbortSignal.timeout(15e3)
|
|
599
|
+
});
|
|
600
|
+
const text = await res.text();
|
|
601
|
+
let parsed;
|
|
602
|
+
try {
|
|
603
|
+
parsed = text ? JSON.parse(text) : { success: false };
|
|
604
|
+
} catch {
|
|
605
|
+
throw new Error(`[cf] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON body: ${text.slice(0, 200)}`);
|
|
606
|
+
}
|
|
607
|
+
return parsed;
|
|
608
|
+
}
|
|
609
|
+
function isNotFound(env) {
|
|
610
|
+
return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
|
|
611
|
+
}
|
|
612
|
+
function loadState(path6) {
|
|
613
|
+
if (!(0, import_node_fs2.existsSync)(path6)) return null;
|
|
614
|
+
try {
|
|
615
|
+
const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path6, "utf8"));
|
|
616
|
+
if (typeof parsed?.tunnelId === "string" && typeof parsed?.accountId === "string") {
|
|
617
|
+
return parsed;
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
} catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function saveState(path6, state) {
|
|
625
|
+
const dir = (0, import_node_path2.dirname)(path6);
|
|
626
|
+
if (!(0, import_node_fs2.existsSync)(dir)) (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
627
|
+
(0, import_node_fs2.writeFileSync)(path6, JSON.stringify(state, null, 2));
|
|
628
|
+
}
|
|
629
|
+
async function ensureTunnel(input, knownTunnelId) {
|
|
630
|
+
if (knownTunnelId) {
|
|
631
|
+
const got = await cf({
|
|
632
|
+
apiToken: input.apiToken,
|
|
633
|
+
method: "GET",
|
|
634
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${knownTunnelId}`
|
|
635
|
+
});
|
|
636
|
+
if (got.success && got.result && !got.result.deleted_at) {
|
|
637
|
+
return knownTunnelId;
|
|
638
|
+
}
|
|
639
|
+
if (!isNotFound(got) && !got.success) {
|
|
640
|
+
throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const created = await cf({
|
|
644
|
+
apiToken: input.apiToken,
|
|
645
|
+
method: "POST",
|
|
646
|
+
path: `/accounts/${input.accountId}/cfd_tunnel`,
|
|
647
|
+
body: { name: input.tunnelName, config_src: "cloudflare" }
|
|
648
|
+
});
|
|
649
|
+
if (!created.success || !created.result) {
|
|
650
|
+
throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
|
|
651
|
+
}
|
|
652
|
+
return created.result.id;
|
|
653
|
+
}
|
|
654
|
+
async function fetchConnectorToken(input, tunnelId) {
|
|
655
|
+
const res = await cf({
|
|
656
|
+
apiToken: input.apiToken,
|
|
657
|
+
method: "GET",
|
|
658
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/token`
|
|
659
|
+
});
|
|
660
|
+
if (!res.success || typeof res.result !== "string") {
|
|
661
|
+
throw new Error(`[cf] connector-token fetch failed: ${JSON.stringify(res.errors)}`);
|
|
662
|
+
}
|
|
663
|
+
return res.result;
|
|
664
|
+
}
|
|
665
|
+
async function setIngressConfig(input, tunnelId) {
|
|
666
|
+
const ingress = input.services.map((s) => ({
|
|
667
|
+
hostname: `${s.name}.${input.zone.name}`,
|
|
668
|
+
service: `http://localhost:${s.port}`
|
|
669
|
+
}));
|
|
670
|
+
ingress.push({ service: "http_status:404" });
|
|
671
|
+
const res = await cf({
|
|
672
|
+
apiToken: input.apiToken,
|
|
673
|
+
method: "PUT",
|
|
674
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/configurations`,
|
|
675
|
+
body: { config: { ingress } }
|
|
676
|
+
});
|
|
677
|
+
if (!res.success) {
|
|
678
|
+
throw new Error(`[cf] ingress config PUT failed: ${JSON.stringify(res.errors)}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
async function ensureDnsRecord(input, tunnelId, service) {
|
|
682
|
+
const fqdn = `${service.name}.${input.zone.name}`;
|
|
683
|
+
const target = `${tunnelId}.cfargotunnel.com`;
|
|
684
|
+
const existing = await cf({
|
|
685
|
+
apiToken: input.apiToken,
|
|
686
|
+
method: "GET",
|
|
687
|
+
path: `/zones/${input.zone.id}/dns_records?name=${encodeURIComponent(fqdn)}&type=CNAME`
|
|
688
|
+
});
|
|
689
|
+
if (existing.success && Array.isArray(existing.result) && existing.result.length > 0) {
|
|
690
|
+
const rec = existing.result[0];
|
|
691
|
+
if (rec.content === target) return;
|
|
692
|
+
const upd = await cf({
|
|
693
|
+
apiToken: input.apiToken,
|
|
694
|
+
method: "PUT",
|
|
695
|
+
path: `/zones/${input.zone.id}/dns_records/${rec.id}`,
|
|
696
|
+
body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
|
|
697
|
+
});
|
|
698
|
+
if (!upd.success) {
|
|
699
|
+
throw new Error(`[cf] DNS record update for ${fqdn} failed: ${JSON.stringify(upd.errors)}`);
|
|
700
|
+
}
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const created = await cf({
|
|
704
|
+
apiToken: input.apiToken,
|
|
705
|
+
method: "POST",
|
|
706
|
+
path: `/zones/${input.zone.id}/dns_records`,
|
|
707
|
+
body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
|
|
708
|
+
});
|
|
709
|
+
if (created.success) return;
|
|
710
|
+
if ((created.errors ?? []).some((e) => e.code === CF_ERR_DNS_RECORD_EXISTS)) return;
|
|
711
|
+
throw new Error(`[cf] DNS record create for ${fqdn} failed: ${JSON.stringify(created.errors)}`);
|
|
712
|
+
}
|
|
713
|
+
async function provisionIngress(input) {
|
|
714
|
+
const prior = loadState(input.stateFile);
|
|
715
|
+
const tunnelId = await ensureTunnel(input, prior?.tunnelId ?? null);
|
|
716
|
+
saveState(input.stateFile, {
|
|
717
|
+
tunnelId,
|
|
718
|
+
accountId: input.accountId,
|
|
719
|
+
tunnelName: input.tunnelName,
|
|
720
|
+
zoneId: input.zone.id
|
|
721
|
+
});
|
|
722
|
+
const connectorToken = await fetchConnectorToken(input, tunnelId);
|
|
723
|
+
await setIngressConfig(input, tunnelId);
|
|
724
|
+
await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
|
|
725
|
+
const hostnames = {};
|
|
726
|
+
for (const s of input.services) hostnames[s.name] = `${s.name}.${input.zone.name}`;
|
|
727
|
+
return { tunnelId, connectorToken, hostnames };
|
|
728
|
+
}
|
|
729
|
+
var import_node_fs2, import_node_path2, CF_API_BASE, CF_ERR_DNS_RECORD_EXISTS;
|
|
730
|
+
var init_cf_ingress = __esm({
|
|
731
|
+
"src/server/cf-ingress.ts"() {
|
|
732
|
+
"use strict";
|
|
733
|
+
import_node_fs2 = require("node:fs");
|
|
734
|
+
import_node_path2 = require("node:path");
|
|
735
|
+
CF_API_BASE = "https://api.cloudflare.com/client/v4";
|
|
736
|
+
CF_ERR_DNS_RECORD_EXISTS = 81053;
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
471
740
|
// src/server/radar-docker-init-entry.ts
|
|
472
741
|
var radar_docker_init_entry_exports = {};
|
|
742
|
+
__export(radar_docker_init_entry_exports, {
|
|
743
|
+
maybeProvisionIngress: () => maybeProvisionIngress,
|
|
744
|
+
spawnServiceGroup: () => spawnServiceGroup
|
|
745
|
+
});
|
|
473
746
|
function fail(message) {
|
|
474
747
|
console.error(message);
|
|
475
748
|
process.exit(1);
|
|
@@ -485,39 +758,39 @@ function run2(cmd, args, stdio = "inherit") {
|
|
|
485
758
|
}
|
|
486
759
|
async function setupFromCloud() {
|
|
487
760
|
const pat = requireEnv("LS_PAT");
|
|
761
|
+
const orgSlug = requireEnv("LS_ORG_SLUG");
|
|
762
|
+
const projectSlug = requireEnv("LS_PROJECT_SLUG");
|
|
488
763
|
const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
|
|
489
|
-
const orgSlug = process.env.LS_ORG_SLUG;
|
|
490
|
-
const projectSlug = process.env.LS_PROJECT_SLUG;
|
|
491
764
|
const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
|
|
492
765
|
let bundle;
|
|
493
766
|
try {
|
|
494
767
|
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
495
768
|
} catch (err) {
|
|
496
|
-
fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and
|
|
769
|
+
fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and LS_ORG_SLUG/LS_PROJECT_SLUG point at a project the user has access to.`);
|
|
497
770
|
}
|
|
498
|
-
if (!process.env.LS_ORG_SLUG) process.env.LS_ORG_SLUG = bundle.orgSlug;
|
|
499
|
-
if (!process.env.LS_PROJECT_SLUG) process.env.LS_PROJECT_SLUG = bundle.projectSlug;
|
|
500
771
|
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
501
772
|
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
502
773
|
if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
|
|
503
774
|
if (!process.env.GH_TOKEN) {
|
|
504
775
|
fail(`[entrypoint] no GH_TOKEN available \u2014 user has not connected GitHub (githubTokenStatus=${bundle.githubTokenStatus}). Connect GitHub in LS or pre-set GH_TOKEN in the container env.`);
|
|
505
776
|
}
|
|
506
|
-
|
|
777
|
+
const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
|
|
778
|
+
console.log(`[entrypoint] bundle from cloud: org=${orgSlug} project=${projectSlug} git=${process.env.GIT_USER_NAME} <${process.env.GIT_USER_EMAIL}> github=${bundle.githubTokenStatus.toLowerCase()} ${cfNote}`);
|
|
779
|
+
return bundle;
|
|
507
780
|
}
|
|
508
781
|
function setupClaudeCredentials() {
|
|
509
782
|
const home = process.env.HOME ?? "/home/launchpod";
|
|
510
|
-
const claudeDir = (0,
|
|
511
|
-
(0,
|
|
783
|
+
const claudeDir = (0, import_node_path3.join)(home, ".claude");
|
|
784
|
+
(0, import_node_fs3.mkdirSync)(claudeDir, { recursive: true });
|
|
512
785
|
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
513
|
-
const credsPath = (0,
|
|
514
|
-
(0,
|
|
515
|
-
(0,
|
|
516
|
-
const configPath = (0,
|
|
786
|
+
const credsPath = (0, import_node_path3.join)(claudeDir, ".credentials.json");
|
|
787
|
+
(0, import_node_fs3.writeFileSync)(credsPath, decoded);
|
|
788
|
+
(0, import_node_fs3.chmodSync)(credsPath, 384);
|
|
789
|
+
const configPath = (0, import_node_path3.join)(home, ".claude.json");
|
|
517
790
|
let cfg = {};
|
|
518
|
-
if ((0,
|
|
791
|
+
if ((0, import_node_fs3.existsSync)(configPath)) {
|
|
519
792
|
try {
|
|
520
|
-
cfg = JSON.parse((0,
|
|
793
|
+
cfg = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
|
|
521
794
|
} catch {
|
|
522
795
|
cfg = {};
|
|
523
796
|
}
|
|
@@ -526,8 +799,25 @@ function setupClaudeCredentials() {
|
|
|
526
799
|
cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
|
|
527
800
|
cfg.numStartups = (cfg.numStartups ?? 0) + 1;
|
|
528
801
|
cfg.installMethod = cfg.installMethod ?? "global";
|
|
529
|
-
|
|
530
|
-
|
|
802
|
+
const PREAPPROVED_MCPS = [
|
|
803
|
+
"launch-secure",
|
|
804
|
+
"launch-chart",
|
|
805
|
+
"launch-deck",
|
|
806
|
+
"launch-orbit",
|
|
807
|
+
"launch-recall",
|
|
808
|
+
"launch-beacon",
|
|
809
|
+
"launch-sequencer"
|
|
810
|
+
];
|
|
811
|
+
const projects = cfg.projects ?? {};
|
|
812
|
+
const wsKey = "/workspace";
|
|
813
|
+
const wsProject = projects[wsKey] ?? {};
|
|
814
|
+
const existingEnabled = Array.isArray(wsProject.enabledMcpjsonServers) ? wsProject.enabledMcpjsonServers : [];
|
|
815
|
+
const mergedEnabled = Array.from(/* @__PURE__ */ new Set([...existingEnabled, ...PREAPPROVED_MCPS]));
|
|
816
|
+
wsProject.enabledMcpjsonServers = mergedEnabled;
|
|
817
|
+
projects[wsKey] = wsProject;
|
|
818
|
+
cfg.projects = projects;
|
|
819
|
+
(0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
820
|
+
(0, import_node_fs3.chmodSync)(configPath, 384);
|
|
531
821
|
}
|
|
532
822
|
function setupGitAndGh() {
|
|
533
823
|
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
@@ -535,9 +825,30 @@ function setupGitAndGh() {
|
|
|
535
825
|
const status = run2("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
536
826
|
if (status !== 0) fail(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
537
827
|
}
|
|
828
|
+
function detectAndSetPreviewPort() {
|
|
829
|
+
if (process.env.PREVIEW_PORT) return;
|
|
830
|
+
try {
|
|
831
|
+
const pkgPath = "/workspace/package.json";
|
|
832
|
+
if (!(0, import_node_fs3.existsSync)(pkgPath)) return;
|
|
833
|
+
const pkg = JSON.parse((0, import_node_fs3.readFileSync)(pkgPath, "utf-8"));
|
|
834
|
+
const scripts = pkg.scripts ?? {};
|
|
835
|
+
const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
|
|
836
|
+
for (const name of ["dev", "start", "serve"]) {
|
|
837
|
+
const script = scripts[name];
|
|
838
|
+
if (typeof script !== "string") continue;
|
|
839
|
+
const m = script.match(portRe);
|
|
840
|
+
if (m) {
|
|
841
|
+
process.env.PREVIEW_PORT = m[1];
|
|
842
|
+
console.log(`[entrypoint] preview port detected from package.json scripts.${name}: ${m[1]}`);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} catch {
|
|
847
|
+
}
|
|
848
|
+
}
|
|
538
849
|
function initWorkspaceIfEmpty() {
|
|
539
850
|
process.chdir("/workspace");
|
|
540
|
-
if ((0,
|
|
851
|
+
if ((0, import_node_fs3.existsSync)(".git")) {
|
|
541
852
|
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
542
853
|
return;
|
|
543
854
|
}
|
|
@@ -552,47 +863,183 @@ function initWorkspaceIfEmpty() {
|
|
|
552
863
|
]);
|
|
553
864
|
if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
554
865
|
}
|
|
555
|
-
function
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
866
|
+
async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
867
|
+
const token = bundle.cloudflareToken ?? null;
|
|
868
|
+
const accountId = bundle.cloudflareAccountId ?? null;
|
|
869
|
+
const zones = bundle.cloudflareZones ?? [];
|
|
870
|
+
if (!token && !accountId && zones.length === 0) return null;
|
|
871
|
+
if (!token || !accountId) {
|
|
872
|
+
fail(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
|
|
873
|
+
}
|
|
874
|
+
const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
|
|
875
|
+
let chosen = null;
|
|
876
|
+
if (baseDomain) {
|
|
877
|
+
chosen = zones.find((z) => z.name === baseDomain) ?? null;
|
|
878
|
+
if (!chosen) {
|
|
879
|
+
fail(`[entrypoint] LAUNCHKIT_CF_BASE_DOMAIN="${baseDomain}" is not among the connected CF token's zones (${zones.map((z) => z.name).join(", ") || "none"}). Either change the env or grant Zone:Read on that zone in the CF token.`);
|
|
562
880
|
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
881
|
+
} else if (zones.length === 1) {
|
|
882
|
+
chosen = { id: zones[0].id, name: zones[0].name };
|
|
883
|
+
} else {
|
|
884
|
+
fail(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
|
|
885
|
+
}
|
|
886
|
+
const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
|
|
887
|
+
console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => s.name).join(",")}`);
|
|
888
|
+
const result = await provisionIngress({
|
|
889
|
+
apiToken: token,
|
|
890
|
+
accountId,
|
|
891
|
+
zone: chosen,
|
|
892
|
+
tunnelName: `launch-kit-${projectSlug}`,
|
|
893
|
+
services: services.map((s) => ({ name: s.name, port: s.port })),
|
|
894
|
+
stateFile
|
|
570
895
|
});
|
|
896
|
+
for (const [name, fqdn] of Object.entries(result.hostnames)) {
|
|
897
|
+
console.log(`[entrypoint] ${name} \u2192 https://${fqdn}`);
|
|
898
|
+
}
|
|
899
|
+
return result;
|
|
900
|
+
}
|
|
901
|
+
function spawnServiceGroup(services) {
|
|
902
|
+
const children = [];
|
|
903
|
+
let shuttingDown = false;
|
|
904
|
+
const killAll = (signal = "SIGTERM") => {
|
|
905
|
+
if (shuttingDown) return;
|
|
906
|
+
shuttingDown = true;
|
|
907
|
+
for (const c2 of children) {
|
|
908
|
+
try {
|
|
909
|
+
c2.proc.kill(signal);
|
|
910
|
+
} catch {
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
const prefixStream = (name, stream, sink) => {
|
|
915
|
+
let buf = "";
|
|
916
|
+
stream.setEncoding("utf8");
|
|
917
|
+
stream.on("data", (chunk) => {
|
|
918
|
+
buf += chunk;
|
|
919
|
+
const lines = buf.split("\n");
|
|
920
|
+
buf = lines.pop() ?? "";
|
|
921
|
+
for (const line of lines) sink.write(`[${name}] ${line}
|
|
922
|
+
`);
|
|
923
|
+
});
|
|
924
|
+
stream.on("end", () => {
|
|
925
|
+
if (buf) sink.write(`[${name}] ${buf}
|
|
926
|
+
`);
|
|
927
|
+
});
|
|
928
|
+
};
|
|
929
|
+
const signalHandlers = [];
|
|
930
|
+
const installSignals = () => {
|
|
931
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
932
|
+
const fn = () => {
|
|
933
|
+
console.log(`[entrypoint] received ${sig} \u2014 forwarding to ${children.length} child process(es)`);
|
|
934
|
+
killAll(sig);
|
|
935
|
+
};
|
|
936
|
+
process.on(sig, fn);
|
|
937
|
+
signalHandlers.push({ sig, fn });
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
const removeSignals = () => {
|
|
941
|
+
for (const h of signalHandlers) process.off(h.sig, h.fn);
|
|
942
|
+
signalHandlers.length = 0;
|
|
943
|
+
};
|
|
944
|
+
return new Promise((resolve3, reject) => {
|
|
945
|
+
let exitedCount = 0;
|
|
946
|
+
let firstFailure = null;
|
|
947
|
+
for (const spec of services) {
|
|
948
|
+
if (spec.skipSpawn) {
|
|
949
|
+
console.log(`[entrypoint] ${spec.name} \u2192 ingress-only on port ${spec.port} (no spawn; user starts dev server here)`);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const args = [...spec.args, "--port", String(spec.port)];
|
|
953
|
+
console.log(`[entrypoint] starting ${spec.name}: ${spec.bin} ${args.join(" ")}`);
|
|
954
|
+
const proc = (0, import_node_child_process2.spawn)(spec.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
955
|
+
children.push({ spec, proc });
|
|
956
|
+
if (proc.stdout) prefixStream(spec.name, proc.stdout, process.stdout);
|
|
957
|
+
if (proc.stderr) prefixStream(spec.name, proc.stderr, process.stderr);
|
|
958
|
+
proc.on("exit", (code, signal) => {
|
|
959
|
+
exitedCount += 1;
|
|
960
|
+
const label = `[${spec.name}] exited code=${code ?? "?"} signal=${signal ?? "-"}`;
|
|
961
|
+
if (!shuttingDown && code !== 0) {
|
|
962
|
+
console.error(`[entrypoint] ${label} \u2014 bringing the group down`);
|
|
963
|
+
if (!firstFailure) firstFailure = { name: spec.name, code, signal };
|
|
964
|
+
killAll();
|
|
965
|
+
} else {
|
|
966
|
+
console.log(`[entrypoint] ${label}`);
|
|
967
|
+
}
|
|
968
|
+
if (exitedCount === children.length) {
|
|
969
|
+
if (firstFailure) reject(new Error(`service "${firstFailure.name}" exited code=${firstFailure.code ?? "?"}`));
|
|
970
|
+
else resolve3();
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
proc.on("error", (err) => {
|
|
974
|
+
console.error(`[entrypoint] [${spec.name}] spawn error: ${err.message}`);
|
|
975
|
+
if (!firstFailure) firstFailure = { name: spec.name, code: null, signal: null };
|
|
976
|
+
killAll();
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
installSignals();
|
|
980
|
+
}).finally(removeSignals);
|
|
571
981
|
}
|
|
572
982
|
async function main() {
|
|
573
983
|
for (const k of REQUIRED_ENV) requireEnv(k);
|
|
574
|
-
await setupFromCloud();
|
|
984
|
+
const bundle = await setupFromCloud();
|
|
575
985
|
setupClaudeCredentials();
|
|
576
986
|
setupGitAndGh();
|
|
577
987
|
initWorkspaceIfEmpty();
|
|
578
|
-
|
|
988
|
+
detectAndSetPreviewPort();
|
|
989
|
+
let services;
|
|
990
|
+
try {
|
|
991
|
+
services = resolveServices();
|
|
992
|
+
} catch (err) {
|
|
993
|
+
fail(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
994
|
+
}
|
|
995
|
+
console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
|
|
996
|
+
const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
|
|
997
|
+
if (ingress) {
|
|
998
|
+
process.env.RADAR_CF_TUNNEL_TOKEN = ingress.connectorToken;
|
|
999
|
+
const radarFqdn = ingress.hostnames.radar;
|
|
1000
|
+
if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
|
|
1001
|
+
else if (services.some((s) => s.name === "radar")) {
|
|
1002
|
+
fail(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
|
|
1003
|
+
}
|
|
1004
|
+
} else if (services.length > 1) {
|
|
1005
|
+
const first = services[0];
|
|
1006
|
+
console.warn(
|
|
1007
|
+
`[entrypoint] \u26A0 quick mode \u2014 only the first service "${first.name}" (port ${first.port}) will be exposed via the ephemeral *.trycloudflare.com URL. Other service(s) [${services.slice(1).map((s) => s.name).join(", ")}] will run on localhost inside the container only. Connect a Cloudflare provider in LS and set LAUNCHKIT_CF_BASE_DOMAIN to expose all services with stable subdomains.`
|
|
1008
|
+
);
|
|
1009
|
+
if (first.name !== "radar") {
|
|
1010
|
+
console.warn(`[entrypoint] \u26A0 first service is "${first.name}", not "radar" \u2014 quick tunneling is owned by the radar agent today, so NO external URL will be available.`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
await spawnServiceGroup(services);
|
|
1015
|
+
process.exit(0);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
579
1020
|
}
|
|
580
|
-
var import_node_child_process2,
|
|
1021
|
+
var import_node_child_process2, import_node_fs3, import_node_path3, REQUIRED_ENV;
|
|
581
1022
|
var init_radar_docker_init_entry = __esm({
|
|
582
1023
|
"src/server/radar-docker-init-entry.ts"() {
|
|
583
1024
|
"use strict";
|
|
584
1025
|
import_node_child_process2 = require("node:child_process");
|
|
585
|
-
|
|
586
|
-
|
|
1026
|
+
import_node_fs3 = require("node:fs");
|
|
1027
|
+
import_node_path3 = require("node:path");
|
|
587
1028
|
init_mcp();
|
|
1029
|
+
init_launch_kit_services();
|
|
1030
|
+
init_cf_ingress();
|
|
588
1031
|
REQUIRED_ENV = [
|
|
589
1032
|
"CLAUDE_CREDENTIALS_B64",
|
|
590
|
-
"LS_PAT"
|
|
1033
|
+
"LS_PAT",
|
|
1034
|
+
"LS_ORG_SLUG",
|
|
1035
|
+
"LS_PROJECT_SLUG"
|
|
591
1036
|
];
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
1037
|
+
if (!process.env.VITEST) {
|
|
1038
|
+
main().catch((err) => {
|
|
1039
|
+
console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
596
1043
|
}
|
|
597
1044
|
});
|
|
598
1045
|
|
|
@@ -612,10 +1059,28 @@ var import_node_child_process = require("node:child_process");
|
|
|
612
1059
|
function run(cmd, args, stdio = "inherit") {
|
|
613
1060
|
return (0, import_node_child_process.spawnSync)(cmd, args, { stdio }).status ?? 1;
|
|
614
1061
|
}
|
|
615
|
-
function
|
|
616
|
-
|
|
1062
|
+
function hasGh() {
|
|
1063
|
+
return (0, import_node_child_process.spawnSync)("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
1064
|
+
}
|
|
1065
|
+
function wireGitHubAuth() {
|
|
1066
|
+
if (!process.env.GH_TOKEN) return false;
|
|
1067
|
+
if (hasGh()) {
|
|
617
1068
|
run("gh", ["auth", "setup-git"]);
|
|
1069
|
+
return true;
|
|
618
1070
|
}
|
|
1071
|
+
const key = "credential.https://github.com.helper";
|
|
1072
|
+
run("git", ["config", "--global", "--replace-all", key, ""]);
|
|
1073
|
+
run("git", [
|
|
1074
|
+
"config",
|
|
1075
|
+
"--global",
|
|
1076
|
+
"--add",
|
|
1077
|
+
key,
|
|
1078
|
+
`!f() { echo username=x-access-token; echo "password=$GH_TOKEN"; }; f`
|
|
1079
|
+
]);
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
function configureGitForBot(identity) {
|
|
1083
|
+
wireGitHubAuth();
|
|
619
1084
|
run("git", ["config", "--global", "user.name", identity.name]);
|
|
620
1085
|
run("git", ["config", "--global", "user.email", identity.email]);
|
|
621
1086
|
run("git", ["config", "--global", "init.defaultBranch", "main"]);
|
|
@@ -771,8 +1236,8 @@ Wired in Claude Code (.mcp.json):
|
|
|
771
1236
|
launch-recall \u2014 restore deleted/modified files from shadow git
|
|
772
1237
|
|
|
773
1238
|
Other tools (run on demand via npx):
|
|
774
|
-
npx launch-
|
|
775
|
-
npx launch-
|
|
1239
|
+
npx launch-radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
|
|
1240
|
+
npx launch-sequencer \u2014 full pipeline UI (separate sequencer login)
|
|
776
1241
|
npx launch-beacon monitor \u2014 local HTTP receiver for the launch-kit-beacon
|
|
777
1242
|
in-browser monitor. Paste the printed URL into
|
|
778
1243
|
the beacon debug panel; events stream to
|
|
@@ -994,9 +1459,23 @@ What it does:
|
|
|
994
1459
|
Does NOT clone, re-install deps, re-prompt for PAT, or re-run the onboard
|
|
995
1460
|
script. Use \`init\` for those.
|
|
996
1461
|
|
|
1462
|
+
No cred yet? If the repo already exists here but was never init'd (e.g. the
|
|
1463
|
+
repo was created by hand and connected in LS afterward), pass
|
|
1464
|
+
--token/--org/--project and refresh will SEED the cred file, then proceed \u2014
|
|
1465
|
+
no clone, no init required.
|
|
1466
|
+
|
|
997
1467
|
Options:
|
|
998
1468
|
--dir=<path> Target directory (default: cwd). Must contain a
|
|
999
|
-
valid .launch-secure.cred.config
|
|
1469
|
+
valid .launch-secure.cred.config, OR pass the
|
|
1470
|
+
--token/--org/--project flags below to seed one.
|
|
1471
|
+
--token=<ls_pat_\u2026> LaunchSecure PAT. Only needed to seed a cred when
|
|
1472
|
+
none exists yet (otherwise read from the cred file).
|
|
1473
|
+
--org=<orgSlug> Org slug for the seeded cred (with --token).
|
|
1474
|
+
--project=<projectSlug> Project slug for the seeded cred (with --token).
|
|
1475
|
+
--url=<serverUrl> LaunchSecure base URL for the seeded cred
|
|
1476
|
+
(default: ${DEFAULT_SERVER_URL}).
|
|
1477
|
+
--course=<name> Course/profile name for the seeded cred
|
|
1478
|
+
(default: inferred from --url).
|
|
1000
1479
|
--no-migrate-safety Skip refreshing the migrate-safety scaffold.
|
|
1001
1480
|
--no-ls-marketplace Skip refreshing the launch-secure marketplace.
|
|
1002
1481
|
--no-recall-hook Skip refreshing the recall-hook scaffold.
|
|
@@ -1058,9 +1537,13 @@ Options:
|
|
|
1058
1537
|
--git-identity="N <e>" Non-interactive git identity for service-account /
|
|
1059
1538
|
CI / Docker runs. Configures git user.name, user.email,
|
|
1060
1539
|
init.defaultBranch=main, pull.rebase=false; also
|
|
1061
|
-
wires GH_TOKEN into git's credential helper via
|
|
1062
|
-
\`gh auth setup-git
|
|
1540
|
+
wires GH_TOKEN into git's credential helper (via
|
|
1541
|
+
\`gh auth setup-git\`, or a gh-less helper when the
|
|
1542
|
+
GitHub CLI is absent) when GH_TOKEN is set. Example:
|
|
1063
1543
|
--git-identity="Radar Bot <radar@launchpod.local>".
|
|
1544
|
+
Note: GH_TOKEN is wired for the clone even without
|
|
1545
|
+
--git-identity, so private-repo clones work on hosts
|
|
1546
|
+
with no \`gh\` as long as GH_TOKEN is set.
|
|
1064
1547
|
--no-install Skip dependency install step (also skips the onboard
|
|
1065
1548
|
script \u2014 install is its prerequisite).
|
|
1066
1549
|
--no-onboard Skip the onboard script even when install runs.
|
|
@@ -1162,9 +1645,9 @@ function preflight() {
|
|
|
1162
1645
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
1163
1646
|
if (nodeMajor < 18) fail2(`Node.js >= 18 required (current: ${process.versions.node}).`);
|
|
1164
1647
|
if (!which("git")) fail2("git not found in PATH. Install git: https://git-scm.com/downloads");
|
|
1165
|
-
const
|
|
1166
|
-
ok(`preflight ok \u2014 node ${process.versions.node}, git present${
|
|
1167
|
-
return { hasGh };
|
|
1648
|
+
const hasGh2 = which("gh") !== null;
|
|
1649
|
+
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh2 ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
1650
|
+
return { hasGh: hasGh2 };
|
|
1168
1651
|
}
|
|
1169
1652
|
var PROJECT_INFO_TIMEOUT_MS = 3e4;
|
|
1170
1653
|
var PROJECT_INFO_MAX_ATTEMPTS = 3;
|
|
@@ -1317,11 +1800,11 @@ function dirIsEmpty(dir) {
|
|
|
1317
1800
|
if (!fs5.existsSync(dir)) return true;
|
|
1318
1801
|
return fs5.readdirSync(dir).length === 0;
|
|
1319
1802
|
}
|
|
1320
|
-
function cloneRepo(repoUrl, targetDir,
|
|
1803
|
+
function cloneRepo(repoUrl, targetDir, hasGh2) {
|
|
1321
1804
|
const isGithub = /github\.com/i.test(repoUrl);
|
|
1322
1805
|
let cmd;
|
|
1323
1806
|
let args;
|
|
1324
|
-
if (
|
|
1807
|
+
if (hasGh2 && isGithub) {
|
|
1325
1808
|
cmd = "gh";
|
|
1326
1809
|
args = ["repo", "clone", repoUrl, targetDir];
|
|
1327
1810
|
info(`cloning via gh: ${repoUrl} \u2192 ${targetDir}`);
|
|
@@ -1337,7 +1820,7 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
|
1337
1820
|
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
1338
1821
|
if (res.status !== 0) {
|
|
1339
1822
|
fail2(
|
|
1340
|
-
`Clone failed (${cmd} exited ${res.status}). For private repos make sure your GitHub auth is set up: \`gh auth login
|
|
1823
|
+
`Clone failed (${cmd} exited ${res.status}). For private repos make sure your GitHub auth is set up: \`gh auth login\`, set GH_TOKEN=<a PAT with repo scope> before re-running (works without the GitHub CLI), or add an SSH key to your GitHub account.`
|
|
1341
1824
|
);
|
|
1342
1825
|
}
|
|
1343
1826
|
if (!fs5.existsSync(path5.join(targetDir, ".git"))) {
|
|
@@ -1988,9 +2471,26 @@ async function mainRefresh(args) {
|
|
|
1988
2471
|
ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
|
|
1989
2472
|
}
|
|
1990
2473
|
}
|
|
2474
|
+
if (!cred && args.token && args.orgSlug && args.projectSlug) {
|
|
2475
|
+
const courseName = args.course ?? inferCourseName(args.serverUrl);
|
|
2476
|
+
const seedCfg = {
|
|
2477
|
+
pat: args.token,
|
|
2478
|
+
orgSlug: args.orgSlug,
|
|
2479
|
+
projectSlug: args.projectSlug,
|
|
2480
|
+
serverUrl: args.serverUrl
|
|
2481
|
+
};
|
|
2482
|
+
info(`no ${CONFIG_FILENAME} found \u2014 seeding it from --token/--org/--project (course: ${courseName})`);
|
|
2483
|
+
writeConfigFile(targetDir, seedCfg, courseName);
|
|
2484
|
+
cred = { active: courseName, profiles: { [courseName]: seedCfg } };
|
|
2485
|
+
}
|
|
1991
2486
|
if (!cred) {
|
|
1992
2487
|
fail2(
|
|
1993
|
-
`no ${CONFIG_FILENAME} found at ${targetDir}, and could not recover from .mcp.json.
|
|
2488
|
+
`no ${CONFIG_FILENAME} found at ${targetDir}, and could not recover from .mcp.json.
|
|
2489
|
+
Refresh re-applies configs to an already-wired checkout, so it needs a cred. Two ways forward:
|
|
2490
|
+
\u2022 Seed it inline (you already have the repo): re-run refresh with credentials \u2014
|
|
2491
|
+
launch-kit refresh --dir=${path5.relative(cwd, targetDir) || "."} --token=<pat> --org=<org> --project=<project>
|
|
2492
|
+
\u2022 Or run a full init in place \u2014 it detects this existing repo and skips the clone:
|
|
2493
|
+
launch-kit init --token=<pat> --org=<org> --project=<project> --dir=${path5.relative(cwd, targetDir) || "."}`
|
|
1994
2494
|
);
|
|
1995
2495
|
}
|
|
1996
2496
|
const nested = toNested(cred);
|
|
@@ -2076,11 +2576,13 @@ async function mainInit(args) {
|
|
|
2076
2576
|
["project", args.projectSlug],
|
|
2077
2577
|
["server", args.serverUrl]
|
|
2078
2578
|
]);
|
|
2079
|
-
const { hasGh } = preflight();
|
|
2080
|
-
phase("preflight", { status: "ok", summary: `node ${process.versions.node}${
|
|
2579
|
+
const { hasGh: hasGh2 } = preflight();
|
|
2580
|
+
phase("preflight", { status: "ok", summary: `node ${process.versions.node}${hasGh2 ? " \xB7 git \xB7 gh" : " \xB7 git (gh not found \u2014 will use git for clone)"}` });
|
|
2081
2581
|
if (args.gitIdentity) {
|
|
2082
2582
|
configureGitForBot(args.gitIdentity);
|
|
2083
|
-
phase("git identity", { status: "ok", summary: `${args.gitIdentity.name} <${args.gitIdentity.email}>${process.env.GH_TOKEN ? " \xB7
|
|
2583
|
+
phase("git identity", { status: "ok", summary: `${args.gitIdentity.name} <${args.gitIdentity.email}>${process.env.GH_TOKEN ? " \xB7 git credential helper wired" : ""}` });
|
|
2584
|
+
} else if (wireGitHubAuth()) {
|
|
2585
|
+
phase("git auth", { status: "ok", summary: hasGh2 ? "GH_TOKEN \u2192 gh credential helper" : "GH_TOKEN \u2192 git credential helper (gh not found)" });
|
|
2084
2586
|
}
|
|
2085
2587
|
info(`resolving project ${args.orgSlug}/${args.projectSlug} on ${args.serverUrl} \u2026`);
|
|
2086
2588
|
let resolved;
|
|
@@ -2105,7 +2607,8 @@ async function mainInit(args) {
|
|
|
2105
2607
|
if (isGitRepo(targetDir)) {
|
|
2106
2608
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
2107
2609
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
2108
|
-
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 skipping clone,
|
|
2610
|
+
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 wiring this existing repo (skipping clone, no GitHub auth needed)`);
|
|
2611
|
+
info(`tip: once wired, use \`launch-kit refresh\` here to re-apply configs without re-running init`);
|
|
2109
2612
|
skipClone = true;
|
|
2110
2613
|
} else {
|
|
2111
2614
|
fail2(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
|
|
@@ -2117,7 +2620,7 @@ async function mainInit(args) {
|
|
|
2117
2620
|
const relTarget = path5.relative(cwd, targetDir) || ".";
|
|
2118
2621
|
if (!skipClone) {
|
|
2119
2622
|
section(`Cloning ${repoUrl}`);
|
|
2120
|
-
cloneRepo(repoUrl, targetDir,
|
|
2623
|
+
cloneRepo(repoUrl, targetDir, hasGh2);
|
|
2121
2624
|
phase("clone", { status: "ok", summary: `\u2192 ${relTarget}` });
|
|
2122
2625
|
} else {
|
|
2123
2626
|
phase("clone", { status: "in-sync", summary: `${relTarget} (already a clone of this repo)` });
|