@monster-orange/den 0.2.6 → 0.2.8
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 +225 -0
- package/bin/den +9 -0
- package/docs/distributions.md +205 -6
- package/package.json +1 -1
- package/service/install.sh +41 -2
- package/src/shared/agent-actor.ts +38 -3
- package/src/shared/config-error.ts +33 -0
- package/src/shared/den-config.ts +84 -5
- package/src/shared/environment-manager/catalog.ts +61 -3
- package/src/shared/environment-manager/default-workspace.ts +53 -14
- package/src/shared/environment-manager/git-remote.ts +49 -0
- package/src/shared/environment-manager/health.ts +51 -8
- package/src/shared/environment-manager/service-runner.ts +51 -5
- package/src/shared/environment-manager/workspace-manager.ts +69 -26
- package/src/shared/environment-manager/workspace-org.ts +279 -0
- package/src/shared/manifest/service-links.ts +20 -8
- package/src/shared/org-sources.ts +309 -0
- package/src/shared/types/index.ts +19 -2
- package/src/ui/cli/clone-plan.ts +374 -0
- package/src/ui/cli/flag-parser.ts +57 -3
- package/src/ui/cli/index.ts +346 -48
- package/src/ui/cli/org-command.ts +172 -0
- package/src/ui/cli/results-viewer.ts +5 -5
- package/src/ui/cli/table.ts +20 -0
- package/src/ui/cli/up-args.ts +15 -0
- package/src/ui/cli/validate-all.ts +171 -0
- package/src/ui/mcp/scope.ts +277 -0
- package/src/ui/mcp/server.ts +94 -9
- package/src/ui/mcp/tools.ts +101 -17
- package/src/ui/server/audit.ts +90 -3
- package/src/ui/server/daemon-reaper.ts +22 -4
- package/src/ui/server/github-service.ts +6 -1
- package/src/ui/server/index.ts +96 -5
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/readonly-routes.ts +12 -3
- package/src/ui/server/release-service.ts +40 -5
- package/src/ui/server/workspace-routes.ts +13 -6
package/src/ui/server/index.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
getWorkspaceByName,
|
|
51
51
|
isWorkspaceActive,
|
|
52
52
|
listWorkspaceServices,
|
|
53
|
+
readWorkspacesFile,
|
|
53
54
|
resolveDepAddress,
|
|
54
55
|
parseAddress,
|
|
55
56
|
readManifestAnyBranch,
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
checkServiceHealth,
|
|
81
82
|
loadDaemonsFromFile,
|
|
82
83
|
pruneDeadDaemonsAsync,
|
|
84
|
+
daemonRootOf,
|
|
83
85
|
isInteractiveServiceCommand,
|
|
84
86
|
rewriteLegacyOpctlKillCommandForContainer,
|
|
85
87
|
shouldDetachServiceCommand,
|
|
@@ -95,6 +97,7 @@ import {
|
|
|
95
97
|
getServiceLogPath,
|
|
96
98
|
} from "../../shared/process/spawn-streaming.js";
|
|
97
99
|
import {
|
|
100
|
+
assertDenConfigValid,
|
|
98
101
|
readDenConfig,
|
|
99
102
|
isProductionEnv,
|
|
100
103
|
isLocalEnabled,
|
|
@@ -105,6 +108,11 @@ import {
|
|
|
105
108
|
writeUserReleaseConfig,
|
|
106
109
|
type ReleaseConfig,
|
|
107
110
|
} from "../../shared/den-config.js";
|
|
111
|
+
import {
|
|
112
|
+
CONFIG_ERROR_STATUS,
|
|
113
|
+
configErrorBody,
|
|
114
|
+
isConfigError,
|
|
115
|
+
} from "../../shared/config-error.js";
|
|
108
116
|
import { readOrg } from "../../shared/org.js";
|
|
109
117
|
import { engineVersion, getDistRoot } from "../../shared/paths.js";
|
|
110
118
|
import { enginePath } from "../../shared/engine.js";
|
|
@@ -142,6 +150,7 @@ import {
|
|
|
142
150
|
actorSuffix,
|
|
143
151
|
recordAction,
|
|
144
152
|
recordAgentRefusal,
|
|
153
|
+
repoAtPath,
|
|
145
154
|
setAuditSink,
|
|
146
155
|
} from "./audit.js";
|
|
147
156
|
import { handleReadonlyRoutes } from "./readonly-routes.js";
|
|
@@ -416,6 +425,26 @@ const STATIC_EXTS = new Set([
|
|
|
416
425
|
".woff2",
|
|
417
426
|
]);
|
|
418
427
|
|
|
428
|
+
/** Every workspace's checkouts (active or not), for repoAtPath. */
|
|
429
|
+
function allWorkspaceCheckouts(): ResolvedService[] {
|
|
430
|
+
const catalog = readCatalog();
|
|
431
|
+
return Object.entries(readWorkspacesFile().workspaces).flatMap(
|
|
432
|
+
([name, config]) => listWorkspaceServices(config, catalog, name),
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* API routes that answer whatever the config holds: the identity probe the
|
|
438
|
+
* CLI and MCP use to find this server, its own log (where a config error is
|
|
439
|
+
* also written), the changelog, and restarting it.
|
|
440
|
+
*/
|
|
441
|
+
const CONFIG_FREE_ROUTES = new Set([
|
|
442
|
+
"/api/server-info",
|
|
443
|
+
"/api/server-log",
|
|
444
|
+
"/api/changelog",
|
|
445
|
+
"/api/web/restart",
|
|
446
|
+
]);
|
|
447
|
+
|
|
419
448
|
// ── Sudo password store ───────────────────────────────────────────────────────
|
|
420
449
|
import {
|
|
421
450
|
setSudoPassword,
|
|
@@ -436,6 +465,11 @@ import {
|
|
|
436
465
|
send404,
|
|
437
466
|
} from "./http-helpers.js";
|
|
438
467
|
import { orgOf, listOrgs } from "../../shared/manifest/service-links.js";
|
|
468
|
+
import {
|
|
469
|
+
readOrgContext,
|
|
470
|
+
resolveAllWorkspaceOrgs,
|
|
471
|
+
withOrgMembership,
|
|
472
|
+
} from "../../shared/environment-manager/workspace-org.js";
|
|
439
473
|
import {
|
|
440
474
|
readLatestSummary,
|
|
441
475
|
securityReportsDir,
|
|
@@ -603,6 +637,13 @@ async function handleRequest(
|
|
|
603
637
|
}
|
|
604
638
|
}
|
|
605
639
|
|
|
640
|
+
// ── Config validation ──────────────────────────────────────────────────────
|
|
641
|
+
// A config that fails validation answers every API route that reads it as
|
|
642
|
+
// the ConfigError the handler below maps to 422 JSON, before any route's own
|
|
643
|
+
// catch can turn it into a 400, a 500 or an empty list. Pages still load.
|
|
644
|
+
if (pathname.startsWith("/api/") && !CONFIG_FREE_ROUTES.has(pathname))
|
|
645
|
+
assertDenConfigValid();
|
|
646
|
+
|
|
606
647
|
// ── Static UI ──────────────────────────────────────────────────────────────
|
|
607
648
|
// Local Dev is the default page when local environment is enabled.
|
|
608
649
|
if (
|
|
@@ -684,12 +725,24 @@ async function handleRequest(
|
|
|
684
725
|
|
|
685
726
|
// ── API: environments ──────────────────────────────────────────────────────
|
|
686
727
|
// Orgs the catalog's repos belong to, with config/den.yml display names.
|
|
728
|
+
// ORG-1d: a repo's org is an org listing it, else one whose source claims
|
|
729
|
+
// its owner, else its GitHub owner; a claimed owner is not listed. Service
|
|
730
|
+
// rows keep `org` as the repo's GitHub owner (C3).
|
|
731
|
+
// Each entry also says whether it is the default org (`github_org`) and
|
|
732
|
+
// which workspaces on this machine belong to it (ORG-1a). Nothing else
|
|
733
|
+
// changes: same entries, order and fields (ORG-SPEC C3).
|
|
687
734
|
if (pathname === "/api/orgs") {
|
|
688
|
-
const
|
|
689
|
-
const
|
|
735
|
+
const ctx = readOrgContext();
|
|
736
|
+
const catalog = ctx.catalog;
|
|
737
|
+
const orgs = withOrgMembership(
|
|
738
|
+
listOrgs(catalog, ctx.config.orgs ?? {}),
|
|
739
|
+
resolveAllWorkspaceOrgs(),
|
|
740
|
+
ctx.file,
|
|
741
|
+
ctx.config,
|
|
742
|
+
);
|
|
690
743
|
if (url.searchParams.get("repos") !== "1") return sendJson(res, orgs);
|
|
691
744
|
const repos = Object.fromEntries(
|
|
692
|
-
Object.keys(catalog).map((r) => [r, orgOf(r, catalog)]),
|
|
745
|
+
Object.keys(catalog).map((r) => [r, orgOf(r, catalog, ctx.config.orgs)]),
|
|
693
746
|
);
|
|
694
747
|
return sendJson(res, { orgs, repos });
|
|
695
748
|
}
|
|
@@ -794,6 +847,17 @@ async function handleRequest(
|
|
|
794
847
|
} catch {
|
|
795
848
|
return send400(res, "invalid JSON");
|
|
796
849
|
}
|
|
850
|
+
// A call that named a path (manifest_validate) is recorded under the
|
|
851
|
+
// catalog repo that path is inside, across every workspace's checkouts.
|
|
852
|
+
if (typeof report.repo !== "string" && report.path !== undefined) {
|
|
853
|
+
let repo: string | null = null;
|
|
854
|
+
try {
|
|
855
|
+
repo = repoAtPath(report.path, allWorkspaceCheckouts());
|
|
856
|
+
} catch {
|
|
857
|
+
/* unresolvable workspaces: recorded under the tool only */
|
|
858
|
+
}
|
|
859
|
+
if (repo) report = { ...report, repo };
|
|
860
|
+
}
|
|
797
861
|
const entry = recordAgentRefusal(actor, report);
|
|
798
862
|
return sendJson(res, { recorded: true, ts: entry.ts });
|
|
799
863
|
}
|
|
@@ -2375,7 +2439,14 @@ async function handleRequest(
|
|
|
2375
2439
|
// ── API: den config ────────────────────────────────────────────────────
|
|
2376
2440
|
if (pathname === `/api/${product().cli}/config` && req.method === "GET") {
|
|
2377
2441
|
// `org` resolved (id defaults to edge.org) for the header's org label.
|
|
2378
|
-
|
|
2442
|
+
const config = readDenConfig();
|
|
2443
|
+
// `default_org` (ORG-1a) is the default work org, `github_org`; `org`
|
|
2444
|
+
// stays the instance org (readOrg).
|
|
2445
|
+
return sendJson(res, {
|
|
2446
|
+
...config,
|
|
2447
|
+
org: readOrg(),
|
|
2448
|
+
default_org: config.github_org?.trim() || null,
|
|
2449
|
+
});
|
|
2379
2450
|
}
|
|
2380
2451
|
|
|
2381
2452
|
// ── API: server info + staleness (drives the header "stale server" pill) ────
|
|
@@ -3130,6 +3201,21 @@ const server = http.createServer((req, res) => {
|
|
|
3130
3201
|
// rejection — which CRASHES the whole server on modern Node (freezing every
|
|
3131
3202
|
// health dot) and leaves the request hanging. Fail the one request instead.
|
|
3132
3203
|
handleRequest(req, res).catch((err) => {
|
|
3204
|
+
// A config that fails validation is the user's to fix: name it to the
|
|
3205
|
+
// caller as JSON (the {ok, error, code} frame), not a bare 500.
|
|
3206
|
+
if (isConfigError(err)) {
|
|
3207
|
+
serverLog(
|
|
3208
|
+
` ⚠ ${req.method} ${(req.url ?? "").split("?")[0]}: ${err.message}`,
|
|
3209
|
+
);
|
|
3210
|
+
try {
|
|
3211
|
+
if (!res.headersSent)
|
|
3212
|
+
return sendJson(res, configErrorBody(err), CONFIG_ERROR_STATUS);
|
|
3213
|
+
res.end();
|
|
3214
|
+
} catch {
|
|
3215
|
+
/* socket already gone */
|
|
3216
|
+
}
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3133
3219
|
console.error(`[server] ${req.method} ${req.url} failed:`, err);
|
|
3134
3220
|
try {
|
|
3135
3221
|
if (!res.headersSent) {
|
|
@@ -3184,7 +3270,7 @@ void (async () => {
|
|
|
3184
3270
|
await new Promise<void>((resolve) =>
|
|
3185
3271
|
setTimeout(resolve, DAEMON_SHUTDOWN_GRACE_MS),
|
|
3186
3272
|
);
|
|
3187
|
-
await reapDeadDaemons({
|
|
3273
|
+
const reaped = await reapDeadDaemons({
|
|
3188
3274
|
listDaemons,
|
|
3189
3275
|
resolveService,
|
|
3190
3276
|
getActiveWorkspaces,
|
|
@@ -3192,7 +3278,12 @@ void (async () => {
|
|
|
3192
3278
|
unregisterDaemon,
|
|
3193
3279
|
delay: (ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
|
|
3194
3280
|
retryDelayMs: DAEMON_RETRY_DELAY_MS,
|
|
3281
|
+
rootOf: daemonRootOf,
|
|
3195
3282
|
});
|
|
3283
|
+
if (reaped.removed.length > 0)
|
|
3284
|
+
console.error(
|
|
3285
|
+
`Reaped ${reaped.removed.length} stopped daemon(s): ${reaped.removed.join(", ")}`,
|
|
3286
|
+
);
|
|
3196
3287
|
})();
|
|
3197
3288
|
|
|
3198
3289
|
const prunedSnapshots = pruneDeadSnapshots();
|
|
@@ -781,7 +781,7 @@ function activityList(x) {
|
|
|
781
781
|
.then(({ entries = [] }) => {
|
|
782
782
|
if (!live) return;
|
|
783
783
|
el.innerHTML = entries.length
|
|
784
|
-
? `<ol class="act">${entries.map((a) => `<li><time datetime="${esc(a.ts)}">${esc(hhmm(a.ts))}</time><span class="dg-who ${a.actor === "agent" ? "agent" : ""}">${esc(a.actor === "agent" ? "[agent]" + (a.client ? " " + a.client : "") : "you")}</span><span>${esc(a.verb)} ${esc(a.detail)}${a.outcome === "refused" ? ` <em class="refused">refused: ${esc(a.reason || "")}</em>` : ""}</span></li>`).join("")}</ol>`
|
|
784
|
+
? `<ol class="act">${entries.map((a) => `<li><time datetime="${esc(a.ts)}">${esc(hhmm(a.ts))}</time><span class="dg-who ${a.actor === "agent" ? "agent" : ""}">${esc(a.actor === "agent" ? (a.agent ? "[" + a.agent + "]" : "[agent]") + (a.client ? " " + a.client : "") : "you")}</span><span>${esc(a.verb)} ${esc(a.detail)}${a.outcome === "refused" ? ` <em class="refused">refused: ${esc(a.reason || "")}</em>` : ""}</span></li>`).join("")}</ol>`
|
|
785
785
|
: none(
|
|
786
786
|
"No actions yet. Starts, restarts and syncs, yours and agents', show here.",
|
|
787
787
|
);
|
|
@@ -96,13 +96,22 @@ export function activityFor(
|
|
|
96
96
|
// and for each of its services.
|
|
97
97
|
const repoLevel = (e: AuditEntry, repo: string): boolean =>
|
|
98
98
|
e.verb === "exec" && e.detail.startsWith(repo + " ");
|
|
99
|
+
// A refused non-lifecycle call (logs_tail, logs_search, …) is listed under
|
|
100
|
+
// each service or repo it named.
|
|
101
|
+
const named = (e: AuditEntry, hit: (a: string) => boolean): boolean =>
|
|
102
|
+
(e.addresses ?? []).some(hit);
|
|
99
103
|
const matches = (e: AuditEntry): boolean => {
|
|
100
|
-
if (target.address)
|
|
104
|
+
if (target.address) {
|
|
105
|
+
const address = target.address;
|
|
106
|
+
const repo = address.split("/")[0];
|
|
101
107
|
return (
|
|
102
|
-
(e.verb === "exec" && e.detail.startsWith(
|
|
103
|
-
repoLevel(e,
|
|
108
|
+
(e.verb === "exec" && e.detail.startsWith(address + " ")) ||
|
|
109
|
+
repoLevel(e, repo) ||
|
|
110
|
+
named(e, (a) => a === address || a === repo)
|
|
104
111
|
);
|
|
112
|
+
}
|
|
105
113
|
const repo = target.repo ?? "";
|
|
114
|
+
if (named(e, (a) => a === repo || a.startsWith(repo + "/"))) return true;
|
|
106
115
|
if (e.verb === "exec")
|
|
107
116
|
return e.detail.startsWith(repo + "/") || repoLevel(e, repo);
|
|
108
117
|
if (e.verb === "sync") return e.detail.split(" ").includes(`repo=${repo}`);
|
|
@@ -19,13 +19,22 @@ import { execFile } from "child_process";
|
|
|
19
19
|
import { promisify } from "util";
|
|
20
20
|
import { GH_API_TIMEOUT_MS } from "../../shared/constants.js";
|
|
21
21
|
import {
|
|
22
|
+
readDenConfig,
|
|
23
|
+
readExplicitReleaseOrg,
|
|
22
24
|
readReleaseConfig,
|
|
25
|
+
releaseOwnerFor,
|
|
23
26
|
type ReleaseConfig,
|
|
24
27
|
type ReleaseRepo,
|
|
25
28
|
type ResolvedReleaseTarget,
|
|
26
29
|
resolveReleaseTarget,
|
|
27
30
|
} from "../../shared/den-config.js";
|
|
28
31
|
import { readCatalog } from "../../shared/environment-manager/catalog.js";
|
|
32
|
+
import { orgOf } from "../../shared/manifest/service-links.js";
|
|
33
|
+
import {
|
|
34
|
+
ghOwnerOf,
|
|
35
|
+
isSourcedOrg,
|
|
36
|
+
orgHasGithub,
|
|
37
|
+
} from "../../shared/org-sources.js";
|
|
29
38
|
import { listReposWithManifests } from "../../shared/manifest/workspace-manifests.js";
|
|
30
39
|
|
|
31
40
|
import { product } from "../../shared/product.js";
|
|
@@ -255,20 +264,31 @@ export class ReleaseService {
|
|
|
255
264
|
const known = new Set(cfg.repos.map((r) => r.name));
|
|
256
265
|
return {
|
|
257
266
|
...cfg,
|
|
258
|
-
repos: [
|
|
267
|
+
repos: releasableOnGithub([
|
|
259
268
|
...cfg.repos,
|
|
260
269
|
...manifestReleaseRepos().filter((r) => !known.has(r.name)),
|
|
261
|
-
],
|
|
270
|
+
]),
|
|
262
271
|
};
|
|
263
272
|
}
|
|
264
273
|
|
|
265
|
-
|
|
266
|
-
|
|
274
|
+
/**
|
|
275
|
+
* The owner a repo is released from (REL-1): an explicit `release.org`
|
|
276
|
+
* (den.yml or user.yml) wins; otherwise the repo's own non-empty catalog
|
|
277
|
+
* owner, so a client org's repo releases from its own org; `github_org`
|
|
278
|
+
* (readReleaseConfig's fallback for `cfg.org`) last.
|
|
279
|
+
*/
|
|
280
|
+
private ownerFor(repo: string, cfg: ReleaseConfig): string {
|
|
281
|
+
return releaseOwnerFor({
|
|
282
|
+
explicitOrg: readExplicitReleaseOrg(),
|
|
283
|
+
// The repo's GitHub owner, never its ORG-1d org id (which needn't be one).
|
|
284
|
+
catalogOwner: ghOwnerOf(readCatalog()[repo]),
|
|
285
|
+
githubOrg: cfg.org,
|
|
286
|
+
});
|
|
267
287
|
}
|
|
268
288
|
|
|
269
289
|
slug(repo: string, cfg = this.config()): string {
|
|
270
290
|
const gh = cfg.repos.find((r) => r.name === repo)?.gh;
|
|
271
|
-
return gh && gh.includes("/") ? gh : `${this.
|
|
291
|
+
return gh && gh.includes("/") ? gh : `${this.ownerFor(repo, cfg)}/${repo}`;
|
|
272
292
|
}
|
|
273
293
|
|
|
274
294
|
/**
|
|
@@ -1109,6 +1129,21 @@ export async function readVersionSha(
|
|
|
1109
1129
|
}
|
|
1110
1130
|
}
|
|
1111
1131
|
|
|
1132
|
+
/**
|
|
1133
|
+
* Release repos less those of an org with no GitHub source (ORG-1d): their
|
|
1134
|
+
* release flow needs GitHub, so the Release page hides them rather than
|
|
1135
|
+
* failing. With no ORG-1d keys every repo stays.
|
|
1136
|
+
*/
|
|
1137
|
+
export function releasableOnGithub(repos: ReleaseRepo[]): ReleaseRepo[] {
|
|
1138
|
+
const orgs = readDenConfig().orgs ?? {};
|
|
1139
|
+
if (!Object.values(orgs).some(isSourcedOrg)) return repos;
|
|
1140
|
+
const catalog = readCatalog();
|
|
1141
|
+
return repos.filter((r) => {
|
|
1142
|
+
const org = orgOf(r.name, catalog, orgs);
|
|
1143
|
+
return !org || orgHasGithub(org, orgs);
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1112
1147
|
/** Workspace repos whose manifest has a `release:` block, as release repos. */
|
|
1113
1148
|
export function manifestReleaseRepos(): ReleaseRepo[] {
|
|
1114
1149
|
const catalog = readCatalog();
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
getWaitDeps,
|
|
66
66
|
} from "../../shared/environment-manager/dag.js";
|
|
67
67
|
import { deriveGraph } from "../../shared/environment-manager/derivation/derive-graph.js";
|
|
68
|
+
import { githubOrgFor } from "../../shared/environment-manager/workspace-org.js";
|
|
68
69
|
import {
|
|
69
70
|
deriveStartTargets,
|
|
70
71
|
filterClosureForStart,
|
|
@@ -85,10 +86,12 @@ import { depService } from "../../shared/manifest/deps.js";
|
|
|
85
86
|
import { waitForReady } from "../../shared/environment-manager/readiness.js";
|
|
86
87
|
import {
|
|
87
88
|
readCatalog,
|
|
88
|
-
|
|
89
|
+
needsGhRepair,
|
|
90
|
+
resolveRepoCloneSource,
|
|
89
91
|
upsertCatalogEntry,
|
|
90
92
|
} from "../../shared/environment-manager/catalog.js";
|
|
91
93
|
import { getReposRoot, readUserConfig } from "../../shared/den-config.js";
|
|
94
|
+
import { redactRemote } from "../../shared/environment-manager/git-remote.js";
|
|
92
95
|
import { getDistRoot } from "../../shared/paths.js";
|
|
93
96
|
import { recordRemovedFromWorkspace } from "../../shared/environment-manager/default-workspace.js";
|
|
94
97
|
import { getSudoEnv, prewarmSudo } from "./sudo-store.js";
|
|
@@ -319,7 +322,7 @@ async function buildAvailableRepos(wsOverride: string | null) {
|
|
|
319
322
|
hasManifest: false,
|
|
320
323
|
alreadyAdded,
|
|
321
324
|
source: "catalog",
|
|
322
|
-
gh: entry.gh,
|
|
325
|
+
gh: entry.gh ?? null,
|
|
323
326
|
});
|
|
324
327
|
}
|
|
325
328
|
|
|
@@ -2158,14 +2161,18 @@ export async function handleWorkspaceRoutes(
|
|
|
2158
2161
|
// flow writes one), which used to read as "not in the catalog" and
|
|
2159
2162
|
// skip the clone entirely. Derive the conventional slug instead, and
|
|
2160
2163
|
// persist it so every other flow (CLI up/setup) stops hitting this.
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
+
// ORG-1d: a repo with no source and a `remote:` clones from it.
|
|
2165
|
+
const ghSlug = resolveRepoCloneSource(svc.repoName, readCatalog(), () =>
|
|
2166
|
+
githubOrgFor(ws.name),
|
|
2167
|
+
);
|
|
2168
|
+
const entry = readCatalog()[svc.repoName];
|
|
2169
|
+
if (needsGhRepair(entry, ghSlug)) {
|
|
2170
|
+
upsertCatalogEntry(svc.repoName, { ...entry, gh: ghSlug });
|
|
2164
2171
|
}
|
|
2165
2172
|
const branch = (svc as { branch?: string | null }).branch ?? "main";
|
|
2166
2173
|
emit({
|
|
2167
2174
|
type: "line",
|
|
2168
|
-
text: ` ${svc.repoName}: cloning ${ghSlug} (${branch})…`,
|
|
2175
|
+
text: ` ${svc.repoName}: cloning ${redactRemote(ghSlug)} (${branch})…`,
|
|
2169
2176
|
});
|
|
2170
2177
|
try {
|
|
2171
2178
|
// Synchronous (same helper the workspace-add flow uses) — the server
|