mirror-mind 0.33.0 → 0.33.2
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/docs/releases/index.md +2 -0
- package/docs/releases/v0.33.1.md +174 -0
- package/docs/releases/v0.33.2.md +124 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/ts/src/frontDoor/runtimeRoute.ts +185 -50
- package/ts/src/runtime/channel.ts +18 -0
- package/ts/src/runtime/git.ts +54 -9
- package/ts/src/runtime/installKind.ts +15 -0
- package/ts/src/runtime/releaseNotes.ts +230 -52
- package/ts/src/runtime/status.ts +24 -29
- package/ts/src/runtime/strategies/clone.ts +10 -3
- package/ts/src/runtime/strategies/package.ts +112 -23
- package/ts/src/util/tarEntries.ts +128 -0
- package/ts/src/welcome/card.ts +182 -60
- package/ts/src/welcome/statusLine.ts +12 -2
- package/ts/src/welcome/updateCache.ts +45 -27
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
renderRuntimeBackupCreated,
|
|
22
22
|
verifyBackupArchive,
|
|
23
23
|
} from "#runtime/backup.ts";
|
|
24
|
-
import { channelFor, renderChannel, setChannel } from "#runtime/channel.ts";
|
|
24
|
+
import { channelFor, channelForReport, renderChannel, setChannel } from "#runtime/channel.ts";
|
|
25
25
|
import {
|
|
26
26
|
diagnoseRuntime,
|
|
27
27
|
hookFailureFindings,
|
|
@@ -33,26 +33,31 @@ import {
|
|
|
33
33
|
} from "#runtime/diagnose.ts";
|
|
34
34
|
import {
|
|
35
35
|
checkUpdateAvailability,
|
|
36
|
-
|
|
36
|
+
cloneRoleFor,
|
|
37
37
|
inspectGitFor,
|
|
38
38
|
inspectGitWorktree,
|
|
39
|
-
inspectUpdateChannel,
|
|
40
39
|
packageVersion,
|
|
41
40
|
renderRuntimeUpdateAvailability,
|
|
42
41
|
renderRuntimeVersion,
|
|
42
|
+
unavailableUpdateCheck,
|
|
43
43
|
upstreamFor,
|
|
44
44
|
} from "#runtime/git.ts";
|
|
45
|
-
import { detectInstallKind, type InstallKind } from "#runtime/installKind.ts";
|
|
45
|
+
import { detectInstallKind, type InstallKind, inspectionRootFor } from "#runtime/installKind.ts";
|
|
46
46
|
import { MIGRATE_DECLINED_EXIT, renderMigrate, runMigrate } from "#runtime/migrate.ts";
|
|
47
47
|
import {
|
|
48
48
|
buildPendingReleaseNotes,
|
|
49
|
+
buildPendingReleaseNotesFromRegistry,
|
|
50
|
+
type ReleaseNotesBundle,
|
|
49
51
|
readReleaseNote,
|
|
50
52
|
renderReleaseNote,
|
|
51
53
|
renderReleaseNotesBundle,
|
|
54
|
+
unknownReleaseNotes,
|
|
52
55
|
} from "#runtime/releaseNotes.ts";
|
|
53
56
|
import { buildRuntimeStatus, renderRuntimeStatus, statusVerdict } from "#runtime/status.ts";
|
|
54
57
|
import {
|
|
55
58
|
checkPackageUpdateAvailability,
|
|
59
|
+
defaultNpmRunner,
|
|
60
|
+
type NpmRunner,
|
|
56
61
|
npmRootGlobal,
|
|
57
62
|
renderPackageUpdateAvailability,
|
|
58
63
|
} from "#runtime/strategies/package.ts";
|
|
@@ -61,8 +66,9 @@ import { frontDoorSpawner, postUpdateHints, runUpdate, type UpdateSpawn } from "
|
|
|
61
66
|
import { statusAllowsUpdatePreflight } from "#runtime/updateGate.ts";
|
|
62
67
|
import { renderUpdateResult, updateLogDetail } from "#runtime/updatePipeline.ts";
|
|
63
68
|
import { expandHome } from "#util/paths.ts";
|
|
64
|
-
import { composeWelcome, welcomeDisabled } from "#welcome/card.ts";
|
|
69
|
+
import { composeWelcome, type RemoteCheck, welcomeDisabled } from "#welcome/card.ts";
|
|
65
70
|
import { composeStatusLine } from "#welcome/statusLine.ts";
|
|
71
|
+
import { isoNow, recordUpToDate } from "#welcome/updateCache.ts";
|
|
66
72
|
|
|
67
73
|
/** The `runtime` subcommands this story ports. */
|
|
68
74
|
export const TS_RUNTIME_READ_SUBCOMMANDS = new Set([
|
|
@@ -150,6 +156,11 @@ function resolveWelcomeHome(explicit: string | null, env: NodeJS.ProcessEnv): st
|
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
export interface RuntimeRouteIo {
|
|
159
|
+
/**
|
|
160
|
+
* Where the command was run. No `runtime` read route consults it: since
|
|
161
|
+
* CR134 and CR138 they answer for the install, wherever they run. Tests pass
|
|
162
|
+
* it to show that it does not matter.
|
|
163
|
+
*/
|
|
153
164
|
cwd?: string;
|
|
154
165
|
env?: NodeJS.ProcessEnv;
|
|
155
166
|
stdout?: (text: string) => void;
|
|
@@ -165,18 +176,39 @@ function writeErr(io: RuntimeRouteIo, text: string): void {
|
|
|
165
176
|
(io.stderr ?? ((value: string) => process.stderr.write(value)))(text);
|
|
166
177
|
}
|
|
167
178
|
|
|
168
|
-
/**
|
|
169
|
-
|
|
179
|
+
/**
|
|
180
|
+
* The seams of the routes that answer for the running install, beyond the
|
|
181
|
+
* route's own: the install and its npm, for tests. The welcome (CR135) and
|
|
182
|
+
* `release-notes pending` (CR134) take them.
|
|
183
|
+
*/
|
|
184
|
+
export interface InstallRouteIo extends RuntimeRouteIo {
|
|
185
|
+
install?: InstallKind;
|
|
186
|
+
npm?: NpmRunner;
|
|
187
|
+
/** The OS home whose config directory holds a package's channel file. */
|
|
188
|
+
osHome?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type WelcomeRouteIo = InstallRouteIo;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Port of `welcome.main`. Always exits 0, like the oracle.
|
|
195
|
+
*
|
|
196
|
+
* The card and the status line answer for the install that is running, never
|
|
197
|
+
* for the cwd (CR135): Pi renders both in the person's project.
|
|
198
|
+
*/
|
|
199
|
+
export function runWelcomeRoute(argv: readonly string[], io: WelcomeRouteIo = {}): number {
|
|
170
200
|
const env = io.env ?? process.env;
|
|
171
|
-
const cwd = io.cwd ?? process.cwd();
|
|
172
201
|
const args = argv.slice(1);
|
|
173
202
|
if (welcomeDisabled(env)) return 0;
|
|
174
203
|
|
|
175
204
|
const home = resolveWelcomeHome(optionValue(args, "--mirror-home"), env);
|
|
205
|
+
// The running tree's manifest: a file read, so the status line still spawns nothing.
|
|
206
|
+
const version = packageVersion(runningTreeRoot()) ?? "unknown";
|
|
176
207
|
|
|
177
208
|
if (args.includes("--status-line")) {
|
|
178
209
|
const line = composeStatusLine({
|
|
179
210
|
mirrorHome: home,
|
|
211
|
+
installedVersion: version,
|
|
180
212
|
sessionId: optionValue(args, "--session-id"),
|
|
181
213
|
env,
|
|
182
214
|
});
|
|
@@ -184,26 +216,39 @@ export function runWelcomeRoute(argv: readonly string[], io: RuntimeRouteIo = {}
|
|
|
184
216
|
return 0;
|
|
185
217
|
}
|
|
186
218
|
|
|
187
|
-
const
|
|
188
|
-
const card = composeWelcome({
|
|
219
|
+
const { card, check } = composeWelcome({
|
|
189
220
|
mirrorHome: home,
|
|
190
|
-
|
|
191
|
-
treeRoot: runningTreeRoot(),
|
|
221
|
+
install: io.install ?? detectThisInstall(),
|
|
192
222
|
env,
|
|
193
223
|
version,
|
|
194
|
-
|
|
224
|
+
...(io.npm === undefined ? {} : { npm: io.npm }),
|
|
225
|
+
...(io.osHome === undefined ? {} : { osHome: io.osHome }),
|
|
195
226
|
});
|
|
196
227
|
if (card) writeOut(io, `${card}\n`);
|
|
228
|
+
if (home !== null && check !== null) logWelcomeCheck(home, env, check);
|
|
197
229
|
return 0;
|
|
198
230
|
}
|
|
199
231
|
|
|
232
|
+
/**
|
|
233
|
+
* One front-door line per remote check, so a slow session start can be
|
|
234
|
+
* explained later: what was asked, how it answered, how long it took. Never a
|
|
235
|
+
* path or a version.
|
|
236
|
+
*/
|
|
237
|
+
function logWelcomeCheck(home: string, env: NodeJS.ProcessEnv, check: RemoteCheck): void {
|
|
238
|
+
logFrontDoor(frontDoorLogPath(join(home, dbNameForEnv(env.MEMORY_ENV || "production"))), {
|
|
239
|
+
command: "welcome",
|
|
240
|
+
route: "ts",
|
|
241
|
+
exitCode: 0,
|
|
242
|
+
detail: `update-check source=${check.source} verdict=${check.verdict} ms=${check.ms}`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
200
246
|
/** Port of the read half of `cmd_runtime`. */
|
|
201
247
|
export async function runRuntimeReadRoute(
|
|
202
248
|
argv: readonly string[],
|
|
203
|
-
io:
|
|
249
|
+
io: InstallRouteIo = {},
|
|
204
250
|
): Promise<number> {
|
|
205
251
|
const env = io.env ?? process.env;
|
|
206
|
-
const cwd = io.cwd ?? process.cwd();
|
|
207
252
|
const args = argv.slice(1);
|
|
208
253
|
const subcommand = args[0] ?? "";
|
|
209
254
|
// The version is the running tree's, never the cwd's: for a skill the cwd
|
|
@@ -213,19 +258,24 @@ export async function runRuntimeReadRoute(
|
|
|
213
258
|
|
|
214
259
|
if (subcommand === "version") {
|
|
215
260
|
const startArg = optionValue(args, "--start");
|
|
216
|
-
const
|
|
217
|
-
|
|
261
|
+
const install = io.install ?? detectThisInstall();
|
|
262
|
+
// The install's own repository, or the one `--start` names: the one
|
|
263
|
+
// explicit way these commands run git outside the install (CR138).
|
|
264
|
+
const root = inspectionRootFor(install, startArg ? expandHome(startArg) : null);
|
|
218
265
|
writeOut(
|
|
219
266
|
io,
|
|
220
267
|
renderRuntimeVersion({
|
|
221
268
|
version,
|
|
222
269
|
install,
|
|
223
|
-
git: inspectGitFor(install,
|
|
224
|
-
cloneRole:
|
|
225
|
-
updateChannel:
|
|
226
|
-
install
|
|
227
|
-
|
|
228
|
-
|
|
270
|
+
git: inspectGitFor(install, root),
|
|
271
|
+
cloneRole: cloneRoleFor(install, root),
|
|
272
|
+
updateChannel: channelForReport(
|
|
273
|
+
install,
|
|
274
|
+
root,
|
|
275
|
+
env,
|
|
276
|
+
io.osHome ?? homedir(),
|
|
277
|
+
optionValue(args, "--channel"),
|
|
278
|
+
),
|
|
229
279
|
}),
|
|
230
280
|
);
|
|
231
281
|
return 0;
|
|
@@ -233,24 +283,24 @@ export async function runRuntimeReadRoute(
|
|
|
233
283
|
|
|
234
284
|
if (subcommand === "status") {
|
|
235
285
|
const report = buildRuntimeStatus({
|
|
236
|
-
start: cwd,
|
|
237
286
|
mirrorHome: optionValue(args, "--mirror-home"),
|
|
238
287
|
channel: optionValue(args, "--channel"),
|
|
239
288
|
env,
|
|
240
289
|
version,
|
|
241
|
-
install: detectThisInstall(),
|
|
290
|
+
install: io.install ?? detectThisInstall(),
|
|
242
291
|
});
|
|
243
292
|
writeOut(io, renderRuntimeStatus(report, env));
|
|
244
293
|
return statusVerdict(report) === "ready" ? 0 : 1;
|
|
245
294
|
}
|
|
246
295
|
|
|
247
296
|
if (subcommand === "diagnose") {
|
|
297
|
+
// The install's own tree, so its worktree findings are Mirror's and never
|
|
298
|
+
// the cwd project's (CR138).
|
|
248
299
|
const report = buildRuntimeStatus({
|
|
249
|
-
start: cwd,
|
|
250
300
|
mirrorHome: optionValue(args, "--mirror-home"),
|
|
251
301
|
env,
|
|
252
302
|
version,
|
|
253
|
-
install: detectThisInstall(),
|
|
303
|
+
install: io.install ?? detectThisInstall(),
|
|
254
304
|
});
|
|
255
305
|
const findings = [
|
|
256
306
|
...diagnoseRuntime(report, inspectGitWorktree(report.git.repository)),
|
|
@@ -280,19 +330,14 @@ export async function runRuntimeReadRoute(
|
|
|
280
330
|
if (subcommand === "release-notes") {
|
|
281
331
|
const target = firstPositional(args.slice(1)) ?? "latest";
|
|
282
332
|
if (target === "pending") {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
fetch: !args.includes("--no-fetch"),
|
|
292
|
-
start: cwd,
|
|
293
|
-
}),
|
|
294
|
-
),
|
|
295
|
-
);
|
|
333
|
+
const pending = pendingReleaseNotes(args, io, env, version);
|
|
334
|
+
if ("usage" in pending) {
|
|
335
|
+
writeErr(io, pending.usage);
|
|
336
|
+
return 2;
|
|
337
|
+
}
|
|
338
|
+
writeOut(io, renderReleaseNotesBundle(pending.bundle));
|
|
339
|
+
// `unknown` exits 1, as `update --check` does on `unresolved` (CR134, decision 3).
|
|
340
|
+
return pending.bundle.unknown_reason === null ? 0 : 1;
|
|
296
341
|
} else {
|
|
297
342
|
// The notes ship in the package at the same path as in the checkout
|
|
298
343
|
// (US3 D8), read from the tree that holds the running front door --
|
|
@@ -304,7 +349,7 @@ export async function runRuntimeReadRoute(
|
|
|
304
349
|
|
|
305
350
|
if (subcommand === "backup") return runRuntimeBackup(args.slice(1), io, env);
|
|
306
351
|
if (subcommand === "migrate") return runRuntimeMigrate(args.slice(1), io, env);
|
|
307
|
-
if (subcommand === "update") return runRuntimeUpdate(args.slice(1), io, env
|
|
352
|
+
if (subcommand === "update") return runRuntimeUpdate(args.slice(1), io, env);
|
|
308
353
|
if (subcommand === "channel") return runRuntimeChannel(args.slice(1), io, env);
|
|
309
354
|
|
|
310
355
|
// Routing answers every name it does not recognize before dispatch (the
|
|
@@ -450,20 +495,28 @@ function runRuntimeChannel(
|
|
|
450
495
|
*/
|
|
451
496
|
function runRuntimeUpdate(
|
|
452
497
|
args: readonly string[],
|
|
453
|
-
io:
|
|
498
|
+
io: InstallRouteIo,
|
|
454
499
|
env: NodeJS.ProcessEnv,
|
|
455
|
-
cwd: string,
|
|
456
500
|
): number {
|
|
457
501
|
const version = packageVersion(runningTreeRoot()) ?? "unknown";
|
|
458
502
|
const channelOverride = optionValue(args, "--channel");
|
|
459
503
|
// The update lane asks npm, once, whether `-g` addresses this install: a
|
|
460
504
|
// package outside npm's own global root is not the one `npm install -g`
|
|
461
505
|
// would replace. The read routes do not pay for the probe.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
506
|
+
//
|
|
507
|
+
// npm runs in the tree the front door runs from -- for a package, the
|
|
508
|
+
// install's own root -- and never in the cwd, whose `.npmrc` could name
|
|
509
|
+
// another registry or prefix (CR135). The strategy's own calls do the same.
|
|
510
|
+
//
|
|
511
|
+
// Git runs in the clone's own checkout and nowhere else: the gate's report
|
|
512
|
+
// and `--check` name no cwd (CR138), whose `.git/config` could name a
|
|
513
|
+
// command for git to run.
|
|
514
|
+
const install =
|
|
515
|
+
io.install ??
|
|
516
|
+
detectInstallKind({
|
|
517
|
+
frontDoorPath: frontDoorSelfPath(),
|
|
518
|
+
npmRootGlobal: npmRootGlobal(runningTreeRoot()),
|
|
519
|
+
});
|
|
467
520
|
const channel = channelFor(install, env, homedir(), channelOverride);
|
|
468
521
|
|
|
469
522
|
if (args.includes("--check")) {
|
|
@@ -477,7 +530,12 @@ function runRuntimeUpdate(
|
|
|
477
530
|
? 0
|
|
478
531
|
: 1;
|
|
479
532
|
}
|
|
480
|
-
|
|
533
|
+
// An install that cannot be identified has no checkout to ask, and the
|
|
534
|
+
// cwd is not one (CR138, decision 2). `runtime update` refuses it too.
|
|
535
|
+
const availability =
|
|
536
|
+
install.kind === "clone"
|
|
537
|
+
? checkUpdateAvailability(install.repository, channelOverride, version)
|
|
538
|
+
: unavailableUpdateCheck(version, channel, `install kind unknown: ${install.reason}`);
|
|
481
539
|
writeOut(io, renderRuntimeUpdateAvailability(availability));
|
|
482
540
|
return availability.status === "up_to_date" || availability.status === "update_available"
|
|
483
541
|
? 0
|
|
@@ -501,7 +559,6 @@ function runRuntimeUpdate(
|
|
|
501
559
|
|
|
502
560
|
const gate = (): { ready: boolean; allowed: boolean; detail: string } => {
|
|
503
561
|
const report = buildRuntimeStatus({
|
|
504
|
-
start,
|
|
505
562
|
mirrorHome,
|
|
506
563
|
channel: channelOverride,
|
|
507
564
|
env,
|
|
@@ -548,6 +605,16 @@ function runRuntimeUpdate(
|
|
|
548
605
|
});
|
|
549
606
|
|
|
550
607
|
writeOut(io, renderUpdateResult(result));
|
|
608
|
+
// The install now holds its channel's version: say so where the welcome card
|
|
609
|
+
// and the status line read it, or they keep announcing what was just
|
|
610
|
+
// installed (CR135).
|
|
611
|
+
if (result.success && !args.includes("--dry-run")) {
|
|
612
|
+
recordUpToDate(mirrorHome, {
|
|
613
|
+
channel: channel.value,
|
|
614
|
+
commit: install.kind === "clone" ? result.newRef : null,
|
|
615
|
+
checkedAt: isoNow(),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
551
618
|
if (repair && result.success) {
|
|
552
619
|
// After the stages, not before them: the instruction is what to do NEXT,
|
|
553
620
|
// and it reads as a caption on a result the operator has already seen.
|
|
@@ -596,6 +663,74 @@ function detectThisInstall(): InstallKind {
|
|
|
596
663
|
return detectInstallKind({ frontDoorPath: frontDoorSelfPath() });
|
|
597
664
|
}
|
|
598
665
|
|
|
666
|
+
/**
|
|
667
|
+
* `release-notes pending` for the install that is running, never for the cwd
|
|
668
|
+
* (CR134), which for a skill is the person's project. A clone reads its own
|
|
669
|
+
* channel's ref -- or `--ref` -- in its own repository; a package asks the
|
|
670
|
+
* registry what its channel holds; an install that cannot be identified has no
|
|
671
|
+
* source, and says so.
|
|
672
|
+
*/
|
|
673
|
+
function pendingReleaseNotes(
|
|
674
|
+
args: readonly string[],
|
|
675
|
+
io: InstallRouteIo,
|
|
676
|
+
env: NodeJS.ProcessEnv,
|
|
677
|
+
version: string,
|
|
678
|
+
): { bundle: ReleaseNotesBundle } | { usage: string } {
|
|
679
|
+
const install = io.install ?? detectThisInstall();
|
|
680
|
+
const osHome = io.osHome ?? homedir();
|
|
681
|
+
const ref = optionValue(args, "--ref");
|
|
682
|
+
const fetch = !args.includes("--no-fetch");
|
|
683
|
+
|
|
684
|
+
if (install.kind === "clone") {
|
|
685
|
+
return {
|
|
686
|
+
bundle: buildPendingReleaseNotes({
|
|
687
|
+
// `--from` omitted falls back to the running tree's own version.
|
|
688
|
+
currentVersion: optionValue(args, "--from") ?? version,
|
|
689
|
+
ref: ref ?? upstreamFor(channelFor(install, env, osHome)),
|
|
690
|
+
repository: install.repository,
|
|
691
|
+
fetch,
|
|
692
|
+
}),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (install.kind === "package") {
|
|
697
|
+
if (ref !== null) {
|
|
698
|
+
return {
|
|
699
|
+
usage:
|
|
700
|
+
`release-notes pending: --ref names a git ref, and this install is a package ` +
|
|
701
|
+
`(${install.name}@${install.version}), which reads its channel from the npm registry\n`,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const currentVersion = optionValue(args, "--from") ?? install.version;
|
|
705
|
+
const channel = channelFor(install, env, osHome).value;
|
|
706
|
+
if (!fetch) {
|
|
707
|
+
return {
|
|
708
|
+
bundle: unknownReleaseNotes(
|
|
709
|
+
currentVersion,
|
|
710
|
+
`npm ${install.name}@${channel}`,
|
|
711
|
+
"a package keeps no copy of its channel to read offline; run without --no-fetch",
|
|
712
|
+
),
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
bundle: buildPendingReleaseNotesFromRegistry({
|
|
717
|
+
currentVersion,
|
|
718
|
+
install,
|
|
719
|
+
channel,
|
|
720
|
+
npm: io.npm ?? defaultNpmRunner,
|
|
721
|
+
}),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
bundle: unknownReleaseNotes(
|
|
727
|
+
optionValue(args, "--from") ?? version,
|
|
728
|
+
null,
|
|
729
|
+
`install kind unknown (${install.reason})`,
|
|
730
|
+
),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
599
734
|
/**
|
|
600
735
|
* argparse's positional for `release-notes`: the first bare token that is not
|
|
601
736
|
* consumed as the VALUE of `--from` or `--ref`. Walking the list is the only
|
|
@@ -82,6 +82,24 @@ export function channelFor(
|
|
|
82
82
|
return { value: DEFAULT_CHANNEL, source: null, note: "install kind unknown" };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The channel row of `status` and `version` (CR138). A package's is its
|
|
87
|
+
* config file's. A clone's is the marker in the repository the rows come from
|
|
88
|
+
* (`inspectionRootFor`): its own checkout, or `version --start PATH`, the
|
|
89
|
+
* flag's documented meaning. With no root, the install's own answer, which for
|
|
90
|
+
* a clone without `--start` is the same marker, read the same way.
|
|
91
|
+
*/
|
|
92
|
+
export function channelForReport(
|
|
93
|
+
install: InstallKind,
|
|
94
|
+
root: string | null,
|
|
95
|
+
env: ConfigEnv = process.env,
|
|
96
|
+
home: string = homedir(),
|
|
97
|
+
override: string | null = null,
|
|
98
|
+
): MarkerValue {
|
|
99
|
+
if (install.kind === "package" || root === null) return channelFor(install, env, home, override);
|
|
100
|
+
return inspectUpdateChannel(root, override);
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
export type ChannelWrite =
|
|
86
104
|
| { ok: true; path: string; value: string; previous: string }
|
|
87
105
|
| { ok: false; reason: string; exitCode: 1 | 2 };
|
package/ts/src/runtime/git.ts
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
// Timeouts follow the oracle's two budgets, and for the reason recorded there:
|
|
14
14
|
// local inspections are near-instant, while a single shared 2 s budget once
|
|
15
15
|
// made a network operation report failure after it had already succeeded
|
|
16
|
-
// (the v0.30.1 release incident). Network reads get 120 s
|
|
16
|
+
// (the v0.30.1 release incident). Network reads get 120 s, except where a
|
|
17
|
+
// caller holds a tighter budget of its own: the welcome card's update check
|
|
18
|
+
// runs inside the session start and passes what is left of its 5 s (CR135).
|
|
17
19
|
|
|
18
20
|
import { spawnSync } from "node:child_process";
|
|
19
21
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
@@ -260,11 +262,15 @@ function availability(
|
|
|
260
262
|
* Is the channel's remote branch ahead of HEAD? The remote query is the only
|
|
261
263
|
* authority: remote-tracking refs are only as fresh as the last fetch, and
|
|
262
264
|
* trusting them is how a clone two releases behind gets told it is ahead.
|
|
265
|
+
*
|
|
266
|
+
* `networkTimeoutMs` bounds that query; `runtime update --check` keeps the
|
|
267
|
+
* network budget, and the welcome card passes what is left of its own.
|
|
263
268
|
*/
|
|
264
269
|
export function checkUpdateAvailability(
|
|
265
270
|
start: string,
|
|
266
271
|
channel: string | null,
|
|
267
272
|
version: string,
|
|
273
|
+
networkTimeoutMs: number = GIT_NETWORK_TIMEOUT_MS,
|
|
268
274
|
): UpdateAvailability {
|
|
269
275
|
const startPath = resolve(start);
|
|
270
276
|
const git = inspectGit(startPath);
|
|
@@ -303,7 +309,7 @@ export function checkUpdateAvailability(
|
|
|
303
309
|
const lsRemote = runGit(
|
|
304
310
|
["ls-remote", remote, `refs/heads/${branch}`],
|
|
305
311
|
git.repository,
|
|
306
|
-
|
|
312
|
+
networkTimeoutMs,
|
|
307
313
|
);
|
|
308
314
|
if (lsRemote.code !== 0 || !lsRemote.stdout) {
|
|
309
315
|
return availability(version, updateChannel, {
|
|
@@ -430,12 +436,52 @@ export const NO_REPOSITORY: GitStatus = {
|
|
|
430
436
|
};
|
|
431
437
|
|
|
432
438
|
/**
|
|
433
|
-
* The git facts of
|
|
434
|
-
*
|
|
435
|
-
*
|
|
439
|
+
* The git facts of the install's `root` (`inspectionRootFor`): a package's
|
|
440
|
+
* are `NO_REPOSITORY` (CV22.DS10.US3 plateau 4), since grading the cwd of a
|
|
441
|
+
* package install as a tree read "not a git repository" as a git error. With
|
|
442
|
+
* no root -- an install that cannot be identified, and no `--start` -- no
|
|
443
|
+
* repository is inspected at all, and the rows say why (CR138, decision 2).
|
|
444
|
+
*/
|
|
445
|
+
export function inspectGitFor(install: InstallKind, root: string | null): GitStatus {
|
|
446
|
+
if (install.kind === "package") return NO_REPOSITORY;
|
|
447
|
+
if (root === null) {
|
|
448
|
+
const reason =
|
|
449
|
+
install.kind === "unknown" ? `install kind unknown: ${install.reason}` : "no repository";
|
|
450
|
+
return { ...NO_REPOSITORY, error: reason };
|
|
451
|
+
}
|
|
452
|
+
return inspectGit(root);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The clone role the install's checkout declares (CR138). A package has none
|
|
457
|
+
* to declare. With no root, nothing is read, and the oracle's answer for no
|
|
458
|
+
* repository stands: the default, with the note that says why.
|
|
459
|
+
*/
|
|
460
|
+
export function cloneRoleFor(install: InstallKind, root: string | null): MarkerValue {
|
|
461
|
+
if (install.kind === "package") return { value: "none", source: null, note: "package install" };
|
|
462
|
+
if (root === null) return { value: DEFAULT_CLONE_ROLE, source: null, note: "no repository" };
|
|
463
|
+
return inspectCloneRole(root);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The clone-role rows of `status` and `version`, one renderer for both (CR138). */
|
|
467
|
+
export function renderCloneRoleLines(install: InstallKind, role: MarkerValue): string[] {
|
|
468
|
+
if (install.kind === "package") return ["Clone role: none (package install)"];
|
|
469
|
+
const lines = [`Clone role: ${role.value}`];
|
|
470
|
+
if (role.note) lines.push(`Clone role note: ${role.note}`);
|
|
471
|
+
return lines;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* `update --check` for an install whose repository is not asked: the same
|
|
476
|
+
* shape as the check's own `unknown`, with the reason (CR138). An install that
|
|
477
|
+
* cannot be identified has no checkout to ask, and the cwd is not one.
|
|
436
478
|
*/
|
|
437
|
-
export function
|
|
438
|
-
|
|
479
|
+
export function unavailableUpdateCheck(
|
|
480
|
+
version: string,
|
|
481
|
+
updateChannel: MarkerValue,
|
|
482
|
+
note: string,
|
|
483
|
+
): UpdateAvailability {
|
|
484
|
+
return availability(version, updateChannel, { note });
|
|
439
485
|
}
|
|
440
486
|
|
|
441
487
|
/**
|
|
@@ -464,8 +510,7 @@ export function renderRuntimeVersion(report: RuntimeVersionReport): string {
|
|
|
464
510
|
lines.push(`Version: ${report.version}`);
|
|
465
511
|
lines.push(...renderInstallLines(report.install));
|
|
466
512
|
lines.push(...renderRepositoryLines(report.install, report.git, false));
|
|
467
|
-
lines.push(
|
|
468
|
-
if (report.cloneRole.note) lines.push(`Clone role note: ${report.cloneRole.note}`);
|
|
513
|
+
lines.push(...renderCloneRoleLines(report.install, report.cloneRole));
|
|
469
514
|
lines.push(`Update channel: ${report.updateChannel.value}`);
|
|
470
515
|
if (report.updateChannel.note) lines.push(`Update channel note: ${report.updateChannel.note}`);
|
|
471
516
|
return `${lines.join("\n")}\n`;
|
|
@@ -201,6 +201,21 @@ export function detectInstallKind(probe: InstallKindProbe): InstallKind {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Where the repository rows of `status`, `version`, and `diagnose` come from
|
|
206
|
+
* (CR138): `start` when one is given -- `version --start PATH`, or a test --
|
|
207
|
+
* else a clone's own checkout, else nowhere. Never the cwd. For a skill the
|
|
208
|
+
* cwd is the person's project, and git run there reads that project's config,
|
|
209
|
+
* where `core.fsmonitor` names a command git runs on `status`.
|
|
210
|
+
*/
|
|
211
|
+
export function inspectionRootFor(
|
|
212
|
+
install: InstallKind,
|
|
213
|
+
start: string | null | undefined,
|
|
214
|
+
): string | null {
|
|
215
|
+
if (start !== null && start !== undefined) return resolve(start);
|
|
216
|
+
return install.kind === "clone" ? install.repository : null;
|
|
217
|
+
}
|
|
218
|
+
|
|
204
219
|
/** One line for a render or a log entry. */
|
|
205
220
|
export function describeInstallKind(install: InstallKind): string {
|
|
206
221
|
if (install.kind === "clone") return `clone (${install.repository})`;
|