@tablation/crew 0.98.0 → 0.99.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +147 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -83,6 +83,25 @@ function routePeriscopeProblems(o) {
|
|
|
83
83
|
function previewHostFinding(message) {
|
|
84
84
|
return /does not resolve/.test(message) ? { severity: "attention", kind: "preview_host_unresolvable", message } : { severity: "blocker", kind: "preview_host", message };
|
|
85
85
|
}
|
|
86
|
+
var refId = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.id === "string" ? v.id : void 0;
|
|
87
|
+
function duplicateLivePeriscopes(rows, tagOf, shipName2, route2) {
|
|
88
|
+
const byTicket = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
const t = refId(r.issue_id);
|
|
91
|
+
if (t) byTicket.set(t, [...byTicket.get(t) ?? [], r]);
|
|
92
|
+
}
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const [t, rs] of byTicket) {
|
|
95
|
+
if (rs.length < 2) continue;
|
|
96
|
+
out.push({
|
|
97
|
+
severity: "attention",
|
|
98
|
+
kind: "periscope_duplicate_live",
|
|
99
|
+
...route2 ? { route: route2 } : {},
|
|
100
|
+
message: `${tagOf(t)} has ${rs.length} live Periscopes rows (${rs.map((r) => `${r.id} on ${shipName2(r.ship_id)}${r.url ? ` ${r.url}` : ""}`).join("; ")}) \u2014 the periscope_url rollup assumes one; end all but one with \`crew periscope down\``
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
86
105
|
|
|
87
106
|
// src/periscope-state.ts
|
|
88
107
|
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -1602,6 +1621,28 @@ import { dirname as dirname3, join as join9 } from "path";
|
|
|
1602
1621
|
var ForgeError = class extends Error {
|
|
1603
1622
|
};
|
|
1604
1623
|
var DEFAULT_FORGE_USERNAME = "x-access-token";
|
|
1624
|
+
var DEFAULT_FORGE_HOST = "github.com";
|
|
1625
|
+
var FORGE_DESCRIPTORS = {
|
|
1626
|
+
github: {
|
|
1627
|
+
kind: "github",
|
|
1628
|
+
httpsUrl: (host, remote) => `https://${host}/${remote}.git`,
|
|
1629
|
+
sshUrl: (host, remote) => `git@${host}:${remote}.git`,
|
|
1630
|
+
credentialUsername: DEFAULT_FORGE_USERNAME
|
|
1631
|
+
},
|
|
1632
|
+
gitlab: {
|
|
1633
|
+
kind: "gitlab",
|
|
1634
|
+
httpsUrl: (host, remote) => `https://${host}/${remote}.git`,
|
|
1635
|
+
sshUrl: (host, remote) => `git@${host}:${remote}.git`,
|
|
1636
|
+
credentialUsername: "oauth2"
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
function normalizeForgeHost(host) {
|
|
1640
|
+
const h = host?.trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/\/+$/, "");
|
|
1641
|
+
return h || DEFAULT_FORGE_HOST;
|
|
1642
|
+
}
|
|
1643
|
+
function forgeDescriptorFor(host) {
|
|
1644
|
+
return normalizeForgeHost(host).includes("gitlab") ? FORGE_DESCRIPTORS.gitlab : FORGE_DESCRIPTORS.github;
|
|
1645
|
+
}
|
|
1605
1646
|
var DEFAULT_KEYCHAIN_SERVICE = "crew-forge";
|
|
1606
1647
|
function defaultTokenFilePath(stateDir, host) {
|
|
1607
1648
|
return join9(stateDir, "forges", `${host.replace(/[^a-z0-9.-]/gi, "_")}.token`);
|
|
@@ -1814,7 +1855,7 @@ function parseForges(raw) {
|
|
|
1814
1855
|
throw new ForgeError(`${where}.token.source must be one of: ${Object.keys(TOKEN_SOURCES).join(", ")}`);
|
|
1815
1856
|
}
|
|
1816
1857
|
out[host.toLowerCase()] = {
|
|
1817
|
-
username: typeof e.username === "string" && e.username ? e.username :
|
|
1858
|
+
username: typeof e.username === "string" && e.username ? e.username : forgeDescriptorFor(host).credentialUsername,
|
|
1818
1859
|
token
|
|
1819
1860
|
};
|
|
1820
1861
|
}
|
|
@@ -2932,6 +2973,7 @@ function parseResolved(raw, m, where) {
|
|
|
2932
2973
|
reposModelId: raw.reposModelId,
|
|
2933
2974
|
repoNames: raw.repoNames,
|
|
2934
2975
|
repoRemotes: raw.repoRemotes,
|
|
2976
|
+
repoForgeHosts: raw.repoForgeHosts,
|
|
2935
2977
|
models: {
|
|
2936
2978
|
issues: m.req(raw.models?.issues, `${where}.resolved.models.issues`),
|
|
2937
2979
|
comments: m.req(raw.models?.comments, `${where}.resolved.models.comments`),
|
|
@@ -3437,23 +3479,29 @@ function reposOf(r) {
|
|
|
3437
3479
|
if (r.dirIsCloneBase) {
|
|
3438
3480
|
const [workspace2] = splitRoute(r.route);
|
|
3439
3481
|
const remotes2 = r.resolved?.repoRemotes ?? {};
|
|
3482
|
+
const hosts = r.resolved?.repoForgeHosts ?? {};
|
|
3440
3483
|
return Object.entries(r.resolved?.repoNames ?? {}).map(([id, name]) => ({
|
|
3441
3484
|
name,
|
|
3442
3485
|
dir: defaultRepoDir(r.reposBasePath, workspace2, name),
|
|
3443
|
-
remote: remotes2[id]
|
|
3486
|
+
remote: remotes2[id],
|
|
3487
|
+
...hosts[id] ? { forgeHost: hosts[id] } : {}
|
|
3444
3488
|
}));
|
|
3445
3489
|
}
|
|
3446
3490
|
if (trackerNames.length <= 1) return [{ name: basename3(r.dir), dir: r.dir }];
|
|
3447
3491
|
const remotes = r.resolved?.repoRemotes ?? {};
|
|
3448
3492
|
const remoteByName = {};
|
|
3493
|
+
const hostByName = {};
|
|
3449
3494
|
for (const [id, name] of Object.entries(r.resolved.repoNames)) {
|
|
3450
3495
|
if (remotes[id]) remoteByName[name] = remotes[id];
|
|
3496
|
+
const h = r.resolved?.repoForgeHosts?.[id];
|
|
3497
|
+
if (h) hostByName[name] = h;
|
|
3451
3498
|
}
|
|
3452
3499
|
const [workspace] = splitRoute(r.route);
|
|
3453
3500
|
return trackerNames.map((name) => ({
|
|
3454
3501
|
name,
|
|
3455
3502
|
dir: defaultRepoDir(r.reposBasePath, workspace, name),
|
|
3456
|
-
remote: remoteByName[name]
|
|
3503
|
+
remote: remoteByName[name],
|
|
3504
|
+
...hostByName[name] ? { forgeHost: hostByName[name] } : {}
|
|
3457
3505
|
}));
|
|
3458
3506
|
}
|
|
3459
3507
|
function passengerRepoTargets(r) {
|
|
@@ -3469,10 +3517,12 @@ function passengerCheckoutTargets(r) {
|
|
|
3469
3517
|
const remotes = r.resolved?.repoRemotes ?? {};
|
|
3470
3518
|
return passengerRepoTargets(r).map((t) => {
|
|
3471
3519
|
const id = repoIdForName(r, t.name);
|
|
3520
|
+
const forgeHost = t.forgeHost ?? (id ? r.resolved?.repoForgeHosts?.[id] : void 0);
|
|
3472
3521
|
return {
|
|
3473
3522
|
name: t.name,
|
|
3474
3523
|
dir: passengerCheckoutDir(r.reposBasePath, workspace, t.name),
|
|
3475
|
-
remote: t.remote ?? (id ? remotes[id] : void 0)
|
|
3524
|
+
remote: t.remote ?? (id ? remotes[id] : void 0),
|
|
3525
|
+
...forgeHost ? { forgeHost } : {}
|
|
3476
3526
|
};
|
|
3477
3527
|
});
|
|
3478
3528
|
}
|
|
@@ -4780,8 +4830,10 @@ function setForgeTokenHosts(hosts) {
|
|
|
4780
4830
|
}
|
|
4781
4831
|
function cloneUrlFor(remote, opts = {}) {
|
|
4782
4832
|
const origin = opts.originUrl?.trim() ?? "";
|
|
4783
|
-
const
|
|
4784
|
-
|
|
4833
|
+
const host = normalizeForgeHost(opts.host);
|
|
4834
|
+
const scheme = tokenHosts.has(host) ? "https" : /^https?:\/\//i.test(origin) ? "https" : /^(git@|ssh:\/\/)/i.test(origin) ? "ssh" : opts.scheme ?? "ssh";
|
|
4835
|
+
const forge = forgeDescriptorFor(host);
|
|
4836
|
+
return scheme === "https" ? forge.httpsUrl(host, remote) : forge.sshUrl(host, remote);
|
|
4785
4837
|
}
|
|
4786
4838
|
function originUrlOf(dir, remoteName) {
|
|
4787
4839
|
try {
|
|
@@ -5244,7 +5296,7 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
|
|
|
5244
5296
|
const raw = (err3.stderr ? String(err3.stderr) : err3.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
|
|
5245
5297
|
const detail = raw || "push dry-run failed";
|
|
5246
5298
|
const f = o.forge;
|
|
5247
|
-
if (!f) return { state: "failed", detail, hint: pushAuthHint(o.remoteUrl) };
|
|
5299
|
+
if (!f) return { state: "failed", detail: hostOfRemote(o.remoteUrl) ? `${hostOfRemote(o.remoteUrl)}: ${detail}` : detail, hint: pushAuthHint(o.remoteUrl) };
|
|
5248
5300
|
if (!f.hasToken) {
|
|
5249
5301
|
return {
|
|
5250
5302
|
state: "failed",
|
|
@@ -7639,6 +7691,30 @@ var Tracker = class {
|
|
|
7639
7691
|
limit: 200
|
|
7640
7692
|
});
|
|
7641
7693
|
}
|
|
7694
|
+
/**
|
|
7695
|
+
* Every `live` Periscopes row on any ship (CREW-2871), for the one-per-issue
|
|
7696
|
+
* guard and doctor. Empty without the table.
|
|
7697
|
+
*/
|
|
7698
|
+
async livePeriscopesAllShips() {
|
|
7699
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
7700
|
+
if (!modelId) return [];
|
|
7701
|
+
return this.client.records.list(modelId, {
|
|
7702
|
+
filters: encodeFilters([{ columnName: "state", operator: "EQ", value: "live" }]),
|
|
7703
|
+
limit: 500
|
|
7704
|
+
});
|
|
7705
|
+
}
|
|
7706
|
+
/** The `live` Periscopes rows of one ticket, on any ship (CREW-2871). */
|
|
7707
|
+
async livePeriscopesForTicket(ticketRecordId) {
|
|
7708
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
7709
|
+
if (!modelId) return [];
|
|
7710
|
+
return this.client.records.list(modelId, {
|
|
7711
|
+
filters: encodeFilters([
|
|
7712
|
+
{ columnName: "issue_id", operator: "EQ", value: ticketRecordId },
|
|
7713
|
+
{ columnName: "state", operator: "EQ", value: "live" }
|
|
7714
|
+
]),
|
|
7715
|
+
limit: 50
|
|
7716
|
+
});
|
|
7717
|
+
}
|
|
7642
7718
|
/** Patches one Periscopes row (EPIC-077). */
|
|
7643
7719
|
async updatePeriscope(rowId, fields) {
|
|
7644
7720
|
const modelId = this.route.resolved?.periscopesModelId;
|
|
@@ -7663,12 +7739,12 @@ var Tracker = class {
|
|
|
7663
7739
|
const conds = [{ columnName: "ship_id", operator: "EQ", value: shipId }];
|
|
7664
7740
|
if (states?.length) conds.push({ columnName: "state", operator: "IN", value: states });
|
|
7665
7741
|
const rows = await this.client.records.list(modelId, { filters: encodeFilters(conds), limit: 200 });
|
|
7666
|
-
const
|
|
7667
|
-
const ids = [...new Set(rows.map((r) =>
|
|
7742
|
+
const refId2 = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.id === "string" ? v.id : void 0;
|
|
7743
|
+
const ids = [...new Set(rows.map((r) => refId2(r.issue_id)).filter((x) => !!x))];
|
|
7668
7744
|
const tickets = new Map((await this.ticketsByIds(ids)).map((t) => [t.id, t]));
|
|
7669
7745
|
const out = [];
|
|
7670
7746
|
for (const row of rows) {
|
|
7671
|
-
const t = tickets.get(
|
|
7747
|
+
const t = tickets.get(refId2(row.issue_id) ?? "");
|
|
7672
7748
|
if (t) out.push({ row, ticket: t });
|
|
7673
7749
|
}
|
|
7674
7750
|
return out.sort((a, b) => String(b.row.started_at ?? "").localeCompare(String(a.row.started_at ?? "")));
|
|
@@ -13156,7 +13232,8 @@ function planRepoClone(i) {
|
|
|
13156
13232
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
13157
13233
|
const target = i.dir ? expandDir(i.dir, i.base) : join29(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
13158
13234
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
13159
|
-
|
|
13235
|
+
const forgeHost = i.resolved?.repoForgeHosts?.[id];
|
|
13236
|
+
return { ...add, remote, target, ...forgeHost ? { forgeHost } : {} };
|
|
13160
13237
|
}
|
|
13161
13238
|
|
|
13162
13239
|
// src/doctor-fix.ts
|
|
@@ -15282,7 +15359,7 @@ function runForge(a, io) {
|
|
|
15282
15359
|
const doc = parseDocument4(io.readFile(io.configFile));
|
|
15283
15360
|
doc.setIn(["ship", "forges", host, "token"], tok);
|
|
15284
15361
|
io.writeFile(io.configFile, doc.toString());
|
|
15285
|
-
io.forges = { ...io.forges ?? {}, [host]: { username:
|
|
15362
|
+
io.forges = { ...io.forges ?? {}, [host]: { username: forgeDescriptorFor(host).credentialUsername, token: tok } };
|
|
15286
15363
|
existing = forgeFor(io.forges, host);
|
|
15287
15364
|
}
|
|
15288
15365
|
if (existing.entry.token.source === "env") {
|
|
@@ -16279,6 +16356,10 @@ async function discover(o) {
|
|
|
16279
16356
|
repos.filter((r) => r.remote).map((r) => [r.id, r.remote])
|
|
16280
16357
|
);
|
|
16281
16358
|
if (Object.keys(remotes).length > 0) out.repoRemotes = remotes;
|
|
16359
|
+
const forgeHosts = Object.fromEntries(
|
|
16360
|
+
repos.filter((r) => r.forge_host?.trim()).map((r) => [r.id, r.forge_host.trim().toLowerCase()])
|
|
16361
|
+
);
|
|
16362
|
+
if (Object.keys(forgeHosts).length > 0) out.repoForgeHosts = forgeHosts;
|
|
16282
16363
|
}
|
|
16283
16364
|
if (o.area) {
|
|
16284
16365
|
if (!out.areaModelId) problems.push(`--area given but this project has no "Projects" table`);
|
|
@@ -17672,6 +17753,13 @@ import { existsSync as existsSync35, mkdirSync as mkdirSync21, readFileSync as r
|
|
|
17672
17753
|
import { join as join37 } from "path";
|
|
17673
17754
|
var PeriscopeError = class extends Error {
|
|
17674
17755
|
};
|
|
17756
|
+
var LivePeriscopeError = class extends PeriscopeError {
|
|
17757
|
+
data;
|
|
17758
|
+
constructor(tag, number, live) {
|
|
17759
|
+
super(`${tag} already has a live periscope on ${live.ship}: ${live.url} (worktree ${live.worktree}) \u2014 run \`crew periscope down ${number} --keep|--discard\` on ${live.ship}, or resume it there with \`crew periscope up --resume ${number}\``);
|
|
17760
|
+
this.data = { refused: "live_periscope_exists", ...live };
|
|
17761
|
+
}
|
|
17762
|
+
};
|
|
17675
17763
|
var priorDir = (stateDir) => join37(stateDir, "periscope-priors");
|
|
17676
17764
|
var priorPath = (stateDir, ticketRecordId) => join37(priorDir(stateDir), `${ticketRecordId}.json`);
|
|
17677
17765
|
function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
@@ -17738,6 +17826,10 @@ function periscopeCaps(l) {
|
|
|
17738
17826
|
idleMinutes: l.periscopeIdleMinutes ?? DEFAULT_PERISCOPE_IDLE_MINUTES2
|
|
17739
17827
|
};
|
|
17740
17828
|
}
|
|
17829
|
+
async function assertNoLivePeriscope(e, t, number) {
|
|
17830
|
+
const live = (await e.liveRows(t.id))[0];
|
|
17831
|
+
if (live) throw new LivePeriscopeError(t.tag, number, live);
|
|
17832
|
+
}
|
|
17741
17833
|
async function existingRefusal(e, t) {
|
|
17742
17834
|
if (t.closed) return `${t.tag} is ${t.status}; a closed ticket cannot be periscoped`;
|
|
17743
17835
|
if (e.repoId && t.repoId !== e.repoId) return `${t.tag} belongs to a different repository than ${e.repoName}`;
|
|
@@ -17796,6 +17888,7 @@ async function runPeriscopeUp(d, args) {
|
|
|
17796
17888
|
if (!ex) throw new PeriscopeError("--issue is not supported here");
|
|
17797
17889
|
prior = await ex.load(args.issue);
|
|
17798
17890
|
if (!prior) throw new PeriscopeError(`no ticket numbered ${args.issue} on this route`);
|
|
17891
|
+
await assertNoLivePeriscope(ex, prior, args.issue);
|
|
17799
17892
|
const refused = await existingRefusal(ex, prior);
|
|
17800
17893
|
if (refused) throw new PeriscopeError(refused);
|
|
17801
17894
|
const unsafe = d.baseProblem();
|
|
@@ -18432,7 +18525,8 @@ function passengerCheckoutSyncSpecs(cfg, workspaceId) {
|
|
|
18432
18525
|
gitRemoteName: effective.branch.remote,
|
|
18433
18526
|
base: effective.branch.base,
|
|
18434
18527
|
originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
|
|
18435
|
-
cloneScheme: cfg.ship.cloneScheme
|
|
18528
|
+
cloneScheme: cfg.ship.cloneScheme,
|
|
18529
|
+
...target.forgeHost ? { forgeHost: target.forgeHost } : {}
|
|
18436
18530
|
});
|
|
18437
18531
|
}
|
|
18438
18532
|
}
|
|
@@ -18442,7 +18536,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
18442
18536
|
return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
|
|
18443
18537
|
name: spec.name,
|
|
18444
18538
|
workspaceId: spec.workspaceId,
|
|
18445
|
-
outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme })
|
|
18539
|
+
outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
|
|
18446
18540
|
}));
|
|
18447
18541
|
}
|
|
18448
18542
|
|
|
@@ -18815,7 +18909,7 @@ function createSyncDaemonServer(targets, secret) {
|
|
|
18815
18909
|
try {
|
|
18816
18910
|
const synced = targets.map((t) => ({
|
|
18817
18911
|
name: t.name,
|
|
18818
|
-
outcome: syncPassengerCheckout(t.dir, t.remote, t.gitRemoteName, t.base, { originUrl: t.originUrl, scheme: t.cloneScheme })
|
|
18912
|
+
outcome: syncPassengerCheckout(t.dir, t.remote, t.gitRemoteName, t.base, { originUrl: t.originUrl, scheme: t.cloneScheme, host: t.forgeHost })
|
|
18819
18913
|
}));
|
|
18820
18914
|
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ synced }));
|
|
18821
18915
|
} catch (error) {
|
|
@@ -19511,7 +19605,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
19511
19605
|
const target = repoTargetFor(route2, result.ticket.repo_id);
|
|
19512
19606
|
ticketRepo = target?.name;
|
|
19513
19607
|
try {
|
|
19514
|
-
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: ship.cloneScheme })) {
|
|
19608
|
+
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: ship.cloneScheme, host: target.forgeHost })) {
|
|
19515
19609
|
emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role });
|
|
19516
19610
|
}
|
|
19517
19611
|
} catch (e) {
|
|
@@ -20272,6 +20366,25 @@ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname22(process.execPath)
|
|
|
20272
20366
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
20273
20367
|
var CYCLE_SECONDS2 = 120;
|
|
20274
20368
|
var STALE_CYCLES = 3;
|
|
20369
|
+
async function duplicateLivePeriscopeProblems(route2, ship) {
|
|
20370
|
+
try {
|
|
20371
|
+
const tr = new Tracker(route2, ship);
|
|
20372
|
+
const rows = await tr.livePeriscopesAllShips();
|
|
20373
|
+
const refOf = (v) => typeof v === "string" ? v : v?.id;
|
|
20374
|
+
const counts = /* @__PURE__ */ new Map();
|
|
20375
|
+
for (const r of rows) {
|
|
20376
|
+
const t = refOf(r.issue_id);
|
|
20377
|
+
if (t) counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
20378
|
+
}
|
|
20379
|
+
const dupIds = [...counts].filter(([, n]) => n > 1).map(([t]) => t);
|
|
20380
|
+
if (dupIds.length === 0) return [];
|
|
20381
|
+
const tags = new Map((await tr.ticketsByIds(dupIds)).map((t) => [t.id, t.issue_tag ?? t.issue_id]));
|
|
20382
|
+
const ships = new Map((await tr.shipRows()).map((x) => [x.id, x.name ?? x.id]));
|
|
20383
|
+
return duplicateLivePeriscopes(rows, (id) => tags.get(id) ?? id, (id) => id ? ships.get(id) ?? id : "an unknown ship", route2.route);
|
|
20384
|
+
} catch {
|
|
20385
|
+
return [];
|
|
20386
|
+
}
|
|
20387
|
+
}
|
|
20275
20388
|
function renderShipLine(s, issueLabelById) {
|
|
20276
20389
|
const name = s.name ?? "(unnamed)";
|
|
20277
20390
|
if (!s.last_seen) return `${name.padEnd(20)} never seen`;
|
|
@@ -21828,7 +21941,7 @@ ${cu.tail.trimEnd()}
|
|
|
21828
21941
|
const target = repoTargetFor(w.route, result.ticket.repo_id);
|
|
21829
21942
|
fleetTicketRepo = target?.name;
|
|
21830
21943
|
try {
|
|
21831
|
-
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme })) {
|
|
21944
|
+
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme, host: target.forgeHost })) {
|
|
21832
21945
|
wemit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: w.role });
|
|
21833
21946
|
}
|
|
21834
21947
|
} catch (e) {
|
|
@@ -22045,7 +22158,7 @@ ${cu.tail.trimEnd()}
|
|
|
22045
22158
|
const target = repoTargetFor(route2, result.ticket.repo_id);
|
|
22046
22159
|
ticketRepo = target?.name;
|
|
22047
22160
|
try {
|
|
22048
|
-
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme })) {
|
|
22161
|
+
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme, host: target.forgeHost })) {
|
|
22049
22162
|
emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: current });
|
|
22050
22163
|
}
|
|
22051
22164
|
} catch (e) {
|
|
@@ -22785,7 +22898,7 @@ ${cu.tail.trimEnd()}
|
|
|
22785
22898
|
}
|
|
22786
22899
|
say("cloning", { remote: plan2.remote, target: plan2.target }, `cloning ${plan2.remote} into ${plan2.target}`);
|
|
22787
22900
|
try {
|
|
22788
|
-
ensureRepoCheckout(plan2.target, plan2.remote, { scheme: cfg.ship.cloneScheme ?? "https" });
|
|
22901
|
+
ensureRepoCheckout(plan2.target, plan2.remote, { scheme: cfg.ship.cloneScheme ?? "https", host: plan2.forgeHost });
|
|
22789
22902
|
} catch (e) {
|
|
22790
22903
|
if (!(e instanceof GitError)) throw e;
|
|
22791
22904
|
const authHint = /authentication failed|could not read (username|password)|permission denied|terminal prompts disabled|repository not found/i.test(e.message) ? "\n hint: git could not authenticate \u2014 run `gh auth login` (or add an SSH key and set ship.cloneScheme: ssh), then re-run" : "";
|
|
@@ -23375,6 +23488,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23375
23488
|
reposModelId: found.reposModelId,
|
|
23376
23489
|
repoNames: found.repoNames,
|
|
23377
23490
|
repoRemotes: found.repoRemotes,
|
|
23491
|
+
repoForgeHosts: found.repoForgeHosts,
|
|
23378
23492
|
models: found.models,
|
|
23379
23493
|
seats: found.seats,
|
|
23380
23494
|
holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
|
|
@@ -24331,6 +24445,10 @@ ${where.join("\n")}`);
|
|
|
24331
24445
|
const row = crewP.find((r) => r.id === assigneeId);
|
|
24332
24446
|
return row?.agent_id ? row.name ?? assigneeId : void 0;
|
|
24333
24447
|
},
|
|
24448
|
+
liveRows: async (ticketRecordId) => {
|
|
24449
|
+
const [rows, ships] = [await trackerP.livePeriscopesForTicket(ticketRecordId), await trackerP.shipRows()];
|
|
24450
|
+
return rows.map((r) => ({ ship: ships.find((x) => x.id === r.ship_id)?.name ?? r.ship_id ?? "an unknown ship", url: r.url ?? "", worktree: r.worktree }));
|
|
24451
|
+
},
|
|
24334
24452
|
hasLiveRun: (ticketRecordId) => liveRuns(cfg.ship.stateDir).some((r) => r.ticketRecordId === ticketRecordId),
|
|
24335
24453
|
hold: async (id) => {
|
|
24336
24454
|
const auth = await trackerP.authorshipProbe();
|
|
@@ -24354,6 +24472,7 @@ ${where.join("\n")}`);
|
|
|
24354
24472
|
if (a.issue) {
|
|
24355
24473
|
const t = await deps.existing.load(a.issue);
|
|
24356
24474
|
if (!t) throw new PeriscopeError(`no ticket numbered ${a.issue} on this route`);
|
|
24475
|
+
await assertNoLivePeriscope(deps.existing, t, a.issue);
|
|
24357
24476
|
const refused = await existingRefusal(deps.existing, t);
|
|
24358
24477
|
if (refused) throw new PeriscopeError(refused);
|
|
24359
24478
|
say("dry-run", `would hold ${t.tag} (status ${t.status} stays) for ${pairId}, cut or reuse its worktree for ${a.repo}, run setup${a.isolate ? ", isolate" : ""}, ports, handoff and preview on ${deps.host}, and record a Periscopes row`);
|
|
@@ -24377,6 +24496,8 @@ ${where.join("\n")}`);
|
|
|
24377
24496
|
} catch (e) {
|
|
24378
24497
|
if (e instanceof PeriscopeError || e instanceof ConfigError) {
|
|
24379
24498
|
process.stderr.write(`crew periscope: ${e.message}
|
|
24499
|
+
`);
|
|
24500
|
+
if (e instanceof LivePeriscopeError && jsonP) process.stdout.write(`${JSON.stringify(e.data)}
|
|
24380
24501
|
`);
|
|
24381
24502
|
process.exit(1);
|
|
24382
24503
|
}
|
|
@@ -25166,6 +25287,7 @@ watermark: ${state.watermark()}
|
|
|
25166
25287
|
const { settled } = await settleRepos(reposOf(route2), (t) => t.name, (t) => resolveOneRepo(route2, t));
|
|
25167
25288
|
const tablePresent = flag("offline") ? void 0 : (await new Tracker(route2, cfg.ship).periscopesStatus().catch(() => ({ present: true }))).present;
|
|
25168
25289
|
problems.push(...routePeriscopeProblems({ route: route2.route, hosts: true, repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview })), tablePresent }));
|
|
25290
|
+
if (!flag("offline")) problems.push(...await duplicateLivePeriscopeProblems(route2, cfg.ship));
|
|
25169
25291
|
}
|
|
25170
25292
|
problems.push(...attentionProblems(openShipAttention(state), problems));
|
|
25171
25293
|
if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
|
|
@@ -25311,6 +25433,12 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25311
25433
|
process.stdout.write(`periscopes: ${f.severity.toUpperCase()} ${f.message}
|
|
25312
25434
|
`);
|
|
25313
25435
|
}
|
|
25436
|
+
if (!flag("offline")) {
|
|
25437
|
+
for (const f of await duplicateLivePeriscopeProblems(route2, cfg.ship)) {
|
|
25438
|
+
process.stdout.write(`periscopes: ${f.severity.toUpperCase()} ${f.message}
|
|
25439
|
+
`);
|
|
25440
|
+
}
|
|
25441
|
+
}
|
|
25314
25442
|
}
|
|
25315
25443
|
process.stdout.write(
|
|
25316
25444
|
!periscopes.present ? "periscopes: Periscopes table absent \u2014 this workspace cannot host periscopes\n" : periscopes.missing.length === 0 ? `periscopes: Periscopes table present (${periscopes.modelId})
|
package/package.json
CHANGED