mirror-mind 0.33.1 → 0.33.3
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.2.md +124 -0
- package/docs/releases/v0.33.3.md +158 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/ts/src/frontDoor/runtimeRoute.ts +50 -33
- package/ts/src/guards/retiredSurfaces.ts +18 -0
- package/ts/src/runtime/channel.ts +18 -0
- package/ts/src/runtime/git.ts +46 -7
- package/ts/src/runtime/installKind.ts +95 -20
- package/ts/src/runtime/status.ts +26 -32
- package/ts/src/runtime/strategies/package.ts +38 -2
package/docs/releases/index.md
CHANGED
|
@@ -12,6 +12,8 @@ Future release notes should use the structure defined in [Release Notes](../proc
|
|
|
12
12
|
|
|
13
13
|
## Releases
|
|
14
14
|
|
|
15
|
+
- [v0.33.3 — Mirror Finds Itself Under Any Path](v0.33.3.md) — patch release of one Change Request, CR140, carried by a contributor's PR #42: Mirror read its own install path from `URL.pathname`, so a checkout or package under a path with a space or a non-ASCII character read as `Install: unknown`, the welcome card showed no update, and `runtime update` refused. The path is now a filesystem path, a guard keeps the idiom out, and an `unknown` install names the path it looked up from. On Windows, npm's global layout is recognized and npm runs with no shell, verified on Windows in CI for the two files they touch; there is still no Windows product. Affected installs update once by hand. No migration.
|
|
16
|
+
- [v0.33.2 — Git Runs Where Mirror Lives](v0.33.2.md) — patch release of one Change Request, CR138: on a clone install, `runtime status`, `version`, and `diagnose` describe the install from any directory, and `diagnose` grades Mirror's own tree. Git now runs only in the install's own checkout, so a checkout someone else made can no longer run a `core.fsmonitor` command through these commands; `version --start PATH` is the one explicit exception. Package installs were never exposed. No migration.
|
|
15
17
|
- [v0.33.1 — The Install Answers For Itself](v0.33.1.md) — patch release of six RS010 Change Requests: the welcome card, the status line, and both `release-notes` readers answer for the install that is running, never for the cwd or the repository around the install. A package asks its own tree and the npm registry, a clone its own checkout and remote; `pending` says `unknown` when it cannot tell, and a `--ref` value can no longer run a command through git. No migration.
|
|
16
18
|
- [v0.33.0 — One Plugin, Three Runtimes](v0.33.0.md) — minor release closing CV21.DS2: the one plugin works as it is on Claude Code, Codex, and Grok. Its MCP server starts on all three, the explicit Mirror form is recognized in every runtime's spelling, and each session is logged under its runtime with every reply beside its prompt. Also: a faster Pi session start, and notice that Gemini CLI is retired in favor of Antigravity.
|
|
17
19
|
- [v0.32.1 — The Release That Shipped](v0.32.1.md) — patch release that is the published CV22 release: `v0.32.0` was tagged and its publication refused by the retired-surface tripwire over one sentence in Getting Started, now rephrased. No code, behavior, or schema differs from `v0.32.0`; read that note for the release itself.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.33.2 is a maintenance release of one Change Request, CR138, which closes
|
|
4
|
+
the family v0.33.1 began. For a clone install, `runtime status`, `version`,
|
|
5
|
+
and `diagnose` described the repository the person was standing in, not the
|
|
6
|
+
install: a healthy clone read `attention needed` outside its checkout, and
|
|
7
|
+
`diagnose` reported another project's uncommitted files as Mirror drift.
|
|
8
|
+
Because git reads a repository's own config, running these commands inside a
|
|
9
|
+
checkout someone else made could also run a command that checkout chose. Git
|
|
10
|
+
now runs only in the install's own checkout, and `version --start PATH` is
|
|
11
|
+
the one explicit exception. Package installs, the default, were never
|
|
12
|
+
exposed. Upgrade with `mirror runtime update`; there is no migration.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# v0.33.2 — Git Runs Where Mirror Lives
|
|
16
|
+
|
|
17
|
+
**Date:** 2026-10-15
|
|
18
|
+
|
|
19
|
+
## Highlights
|
|
20
|
+
|
|
21
|
+
- **On a clone install, `runtime status`, `version`, and `diagnose` describe the install
|
|
22
|
+
from any directory** (CR138). A healthy clone is `ready` everywhere. The channel row is
|
|
23
|
+
what `runtime channel` reports, and the clone role is the checkout's own.
|
|
24
|
+
- **`diagnose` grades Mirror's own tree.** Run inside another project, it reported that
|
|
25
|
+
project's uncommitted files as Mirror drift and advised committing, stashing, or
|
|
26
|
+
discarding them. It no longer looks there.
|
|
27
|
+
- **A checkout you did not make can no longer run a command through these commands.**
|
|
28
|
+
Git reads a repository's `.git/config`, where `core.fsmonitor` names a command git runs
|
|
29
|
+
on `status`. Run inside such a checkout, `runtime status`, `version`, and `diagnose` ran
|
|
30
|
+
it. They now run git only in the install's own checkout. `version --start PATH` is the
|
|
31
|
+
one documented way to inspect another repository.
|
|
32
|
+
- **Package installs were never exposed.** A package inspects no repository; its
|
|
33
|
+
clone-role row now reads `none (package install)`.
|
|
34
|
+
- **An install that cannot be identified inspects no repository**, and says why;
|
|
35
|
+
`update --check` answers `unknown` with the reason.
|
|
36
|
+
|
|
37
|
+
## Where We Started
|
|
38
|
+
|
|
39
|
+
`v0.33.1` made the welcome card, the status line, and both release-notes readers answer
|
|
40
|
+
for the install instead of the cwd. CR138, captured during that work, named the same
|
|
41
|
+
defect in `runtime status` and `runtime version` for a clone: both named the install on
|
|
42
|
+
their first line, then reported the repository, git state, and channel of whatever
|
|
43
|
+
directory they were run in. REFERENCE.md's recommended update flow begins with `runtime
|
|
44
|
+
status`, so a clone user who ran it from a project was told `attention needed` about a
|
|
45
|
+
healthy install.
|
|
46
|
+
|
|
47
|
+
Selection found more than the capture said. The clone-role row read the cwd too.
|
|
48
|
+
`runtime diagnose` built the same report, so from inside another project it listed that
|
|
49
|
+
project's uncommitted files as Mirror's and recommended discarding work in a repository
|
|
50
|
+
that was not Mirror's. The plan's review then measured the third face. Git runs a
|
|
51
|
+
repository's own `core.fsmonitor` command on `status`, so these commands, run inside a
|
|
52
|
+
checkout someone else made, ran what that checkout chose. No skill runs these commands,
|
|
53
|
+
so it took a person running them there. The exposure was clone installs, and installs
|
|
54
|
+
that cannot be identified; a package never inspected a repository.
|
|
55
|
+
|
|
56
|
+
## What Changed
|
|
57
|
+
|
|
58
|
+
**One rule says where the repository rows come from.** `inspectionRootFor(install,
|
|
59
|
+
start)`: `--start` when given, else a clone's own checkout, else nowhere. The report
|
|
60
|
+
builder and `runtime version` both use it. The builder no longer defaults to the cwd, and
|
|
61
|
+
`status`, `diagnose`, and the update gate pass none.
|
|
62
|
+
|
|
63
|
+
**The rows are the install's.** The channel row is the answer `runtime channel` gives,
|
|
64
|
+
and the clone role is the checkout's marker. `diagnose`'s worktree findings come from the
|
|
65
|
+
install's tree, so a dirty Mirror checkout is still reported and another project's work
|
|
66
|
+
never is.
|
|
67
|
+
|
|
68
|
+
**`--start` is the one explicit exception.** `runtime version --start PATH` keeps its
|
|
69
|
+
documented meaning, to inspect the repository at PATH, and is now named as the one way
|
|
70
|
+
these commands run git in a repository the install does not own.
|
|
71
|
+
|
|
72
|
+
**An install that cannot be identified asks nothing.** Its git rows say `install kind
|
|
73
|
+
unknown: <reason>`, and its verdict is `attention needed`, which is true: `runtime
|
|
74
|
+
update` refuses such an install. `update --check` answers `unknown` with the same reason
|
|
75
|
+
instead of asking the cwd's git.
|
|
76
|
+
|
|
77
|
+
The tests run every command from a directory in no repository and from a hostile project:
|
|
78
|
+
uncommitted work, its own channel and role markers, and a `core.fsmonitor` that creates a
|
|
79
|
+
marker file. The answers are byte-identical, and the marker never appears, except through
|
|
80
|
+
`--start`, which asks for it. No golden changed.
|
|
81
|
+
|
|
82
|
+
### What you will notice
|
|
83
|
+
|
|
84
|
+
- On a clone, `runtime status` gives the same answer from any directory as from the
|
|
85
|
+
checkout, and `runtime version`'s `Update channel:` matches `runtime channel`.
|
|
86
|
+
- On a package, `Clone role: none (package install)`.
|
|
87
|
+
- `runtime diagnose` may still report `front_door_errors`. Every command that exits 1 to
|
|
88
|
+
report a verdict, `diagnose` included, is logged as an error and counted. That is
|
|
89
|
+
CR139, captured and not in this release.
|
|
90
|
+
|
|
91
|
+
## Upgrading
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
mirror runtime update # or: npm install -g mirror-mind
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
There is no database migration, and no runtime wiring changes. The update itself is run
|
|
98
|
+
by the version you have now; the fix applies from the first command after it.
|
|
99
|
+
|
|
100
|
+
## Conscious Exclusions
|
|
101
|
+
|
|
102
|
+
- **Counting verdicts as front-door errors** (CR139). `runtime status`'s `attention
|
|
103
|
+
needed`, `diagnose`'s findings, and `release-notes pending`'s `unknown` exit 1, and the
|
|
104
|
+
front door logs every non-zero exit as an error that `diagnose` counts. This release
|
|
105
|
+
removes the false verdicts that fed it, not the loop.
|
|
106
|
+
- **The `release` environment's ref check** (CR136).
|
|
107
|
+
- **Retiring `version --start`.** It was kept, as the explicit override it always was.
|
|
108
|
+
|
|
109
|
+
## What We Learned
|
|
110
|
+
|
|
111
|
+
In CR133 to CR135, "never the cwd" was a correctness rule: the person's project is not
|
|
112
|
+
Mirror, so it cannot answer Mirror's questions. CR138 showed it is also a trust boundary.
|
|
113
|
+
The cwd's repository configures the very git that inspects it, so a wrong answer and an
|
|
114
|
+
executed command had the same cause and the same fix, which is where a command decides to
|
|
115
|
+
look.
|
|
116
|
+
|
|
117
|
+
A test can pin a defect as faithfully as it pins a feature. One CLI test asserted, with a
|
|
118
|
+
comment saying so: "Git inspection still describes the cwd." It was right about the
|
|
119
|
+
oracle and wrong about the product, and it now pins the fix.
|
|
120
|
+
|
|
121
|
+
## Next Horizon
|
|
122
|
+
|
|
123
|
+
CR139, the verdict loop, is the visible next step in RS010. CR136 would harden the
|
|
124
|
+
publish route.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.33.3 is a maintenance release of one Change Request, CR140, carried by a
|
|
4
|
+
contributor's pull request. Mirror read its own install path from
|
|
5
|
+
`URL.pathname`, which spells a space as `%20`, `Código` as `C%C3%B3digo`, and
|
|
6
|
+
`C:\` as `/C:/`. Under such a path a checkout or a package read as `Install:
|
|
7
|
+
unknown`, the welcome card showed no update, and `runtime update` refused for
|
|
8
|
+
a reason that was not true. The path is now a filesystem path, a guard keeps
|
|
9
|
+
the idiom out of the code, and an `unknown` install names the path it looked
|
|
10
|
+
up from. On Windows, npm's global layout is recognized and npm runs with no
|
|
11
|
+
shell, verified on Windows in CI for the two files they touch; there is still
|
|
12
|
+
no Windows product. People under such a path update once by hand; everyone
|
|
13
|
+
else as usual. No migration.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# v0.33.3 — Mirror Finds Itself Under Any Path
|
|
17
|
+
|
|
18
|
+
**Date:** 2026-10-16
|
|
19
|
+
|
|
20
|
+
## Highlights
|
|
21
|
+
|
|
22
|
+
- **Mirror recognizes its own install under any path** (CR140, PR #42 by
|
|
23
|
+
@rodrigoimmaginario, with the macOS reproduction by @lgarciasbr). A checkout or a
|
|
24
|
+
package under `My Projects`, `Código`, or `C:\Users\John Doe` read as `Install:
|
|
25
|
+
unknown`; it reads as what it is.
|
|
26
|
+
- **An `unknown` install names the path it looked up from.** `Install: unknown (<reason>;
|
|
27
|
+
looked up from <path>)`, with the path spelled as the detector saw it, so a wrong
|
|
28
|
+
spelling is wrong in the report instead of hidden behind `not a git checkout`.
|
|
29
|
+
- **The idiom cannot come back.** A retired-surface row, `url-pathname-as-path`, sweeps
|
|
30
|
+
`ts/src/` and `ts/scripts/` for a URL's pathname read as a filesystem path. Widening it
|
|
31
|
+
to the scripts caught a second instance on the first run.
|
|
32
|
+
- **On Windows, the install answers too.** npm's Windows global layout
|
|
33
|
+
(`<prefix>\node_modules\<name>` and a `<prefix>\<bin>.cmd` shim) is recognized, and npm
|
|
34
|
+
runs with no shell, as `node.exe` running `npm-cli.js`. Both run on Windows in CI, for
|
|
35
|
+
the two test files they touch. There is still no Windows product.
|
|
36
|
+
- **People this release is for cannot take it the usual way.** Their `runtime update`
|
|
37
|
+
refuses them; see Upgrading.
|
|
38
|
+
|
|
39
|
+
## Where We Started
|
|
40
|
+
|
|
41
|
+
Every route that asks "how is this Mirror installed" hands the detector the front door's
|
|
42
|
+
own path, and two of them computed it as `new URL("…/cli.ts", import.meta.url).pathname`.
|
|
43
|
+
A URL's pathname is not a filesystem path: it percent-encodes every reserved or non-ASCII
|
|
44
|
+
character, and on Windows it spells a drive as `/C:/`. None of those paths exist, so no
|
|
45
|
+
`.git` was found above the front door, no manifest either, and the install was `unknown`
|
|
46
|
+
with a reason that was false for a checkout whose branch the same command named a few
|
|
47
|
+
lines down.
|
|
48
|
+
|
|
49
|
+
Three surfaces failed together. `runtime status` printed `Install: unknown`, and since
|
|
50
|
+
`v0.33.2` the repository rows went blank with it. The welcome card skips the remote update
|
|
51
|
+
check for an `unknown` install, so nobody under such a path was told an update existed.
|
|
52
|
+
And `runtime update` refused at its status gate, so the updater could not ship its own
|
|
53
|
+
fix to the people it affected.
|
|
54
|
+
|
|
55
|
+
It was reported twice within a day, independently: on Windows 11, where it first looked
|
|
56
|
+
Windows-specific, and on macOS under `~/Documents/Código/mirror` at `0.32.1` and `0.33.2`.
|
|
57
|
+
Review found the common case neither report named: a space. `/tmp/space only` read as
|
|
58
|
+
`unknown` the same way, and on Windows that is most home directories.
|
|
59
|
+
|
|
60
|
+
## What Changed
|
|
61
|
+
|
|
62
|
+
**The path never enters URL form.** One `thisFrontDoorPath()` in
|
|
63
|
+
`ts/src/runtime/installKind.ts` joins from `import.meta.dirname`, and every route hands
|
|
64
|
+
the detector that. The first version of the fix converted out of the URL with
|
|
65
|
+
`fileURLToPath`; the review asked for the form in which the class of bug is not possible,
|
|
66
|
+
which is the idiom `ts/src/runtime/treeRoot.ts` already used.
|
|
67
|
+
|
|
68
|
+
**A guard, not a memory.** The retired-surface table gained its first row that retires an
|
|
69
|
+
idiom rather than a deleted file: `url-pathname-as-path`, with the code shapes as patterns
|
|
70
|
+
so prose may still name `URL.pathname`. Its live-tree sweep runs in the suite. We checked
|
|
71
|
+
the pin by putting `.pathname` back locally and watching it fail.
|
|
72
|
+
|
|
73
|
+
**The `unknown` reason carries evidence.** It ends with `; looked up from <path>`. Had
|
|
74
|
+
that line existed, the first report would have carried its own diagnosis.
|
|
75
|
+
|
|
76
|
+
**Windows, honestly scoped.** The layout detector accepts npm's Windows shape, with the
|
|
77
|
+
shim's own text as the evidence that the bin reaches into this package, since nothing
|
|
78
|
+
resolves. npm is spawned as `npm.cmd` itself spawns it, `npm-cli.js` beside `node.exe`
|
|
79
|
+
through the running node: no shell, so the welcome card's timeout kills npm itself rather
|
|
80
|
+
than a shell above it (CVE-2024-27980 is why Node refuses `.cmd` without one). When no
|
|
81
|
+
`npm-cli.js` sits beside the node, the answer is a named absence reported as "npm
|
|
82
|
+
unavailable", not a module-not-found stack. One accepted difference from `npm.cmd`: it
|
|
83
|
+
runs the npm bundled with the running node, not one the person upgraded under the
|
|
84
|
+
prefix.
|
|
85
|
+
|
|
86
|
+
A `windows-install-kind` job on `windows-latest` runs `installKind.test.ts` and
|
|
87
|
+
`packageStrategy.test.ts`, about 25 seconds. On its first run on `main` it failed: on a
|
|
88
|
+
test written on macOS the same day, by the reviewer who asked for the job, comparing a
|
|
89
|
+
fixture against its POSIX spelling where Windows adds a drive. Fixed the way the PR had
|
|
90
|
+
fixed six others.
|
|
91
|
+
|
|
92
|
+
**Verified where it matters.** The install-from-nothing smoke, which packs this tree and
|
|
93
|
+
installs it into a scratch prefix, was run under `/tmp/mirror pkg Código`: 76 checks, all
|
|
94
|
+
passing, through `init`, `seed`, `status`, every runtime's hooks, and the MCP launcher.
|
|
95
|
+
The smoke itself needed one fix first: its Gemini section split the work directory on
|
|
96
|
+
the space, the same assumption this release removes from the product.
|
|
97
|
+
|
|
98
|
+
### What you will notice
|
|
99
|
+
|
|
100
|
+
- Under a path with a space or an accent, `runtime status` reads `Install: clone (…)` or
|
|
101
|
+
`Install: package (…)`, the welcome card checks for updates again, and `runtime update`
|
|
102
|
+
works, after the one manual update below.
|
|
103
|
+
- Under any other path, nothing. The POSIX layout is checked first and is unchanged.
|
|
104
|
+
- An install that is still `unknown` tells you where it looked.
|
|
105
|
+
|
|
106
|
+
## Upgrading
|
|
107
|
+
|
|
108
|
+
If your Mirror lives under a path with a space or a non-ASCII character anywhere above
|
|
109
|
+
it, the version you have cannot update itself: `runtime update` refuses it. Once, by
|
|
110
|
+
hand:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# a clone: in the checkout
|
|
114
|
+
git pull --ff-only
|
|
115
|
+
# a package
|
|
116
|
+
npm install -g mirror-mind@latest
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
From then on `mirror runtime update` works as documented. Everyone else:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
mirror runtime update # or: npm install -g mirror-mind
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
There is no database migration and no runtime wiring change.
|
|
126
|
+
|
|
127
|
+
## Conscious Exclusions
|
|
128
|
+
|
|
129
|
+
- **A Windows product.** REFERENCE.md's platform envelope stands: POSIX. The Windows
|
|
130
|
+
changes here are correct where they run and verified on Windows for their two files;
|
|
131
|
+
the hook wrappers, the plugin's MCP entry, `init`'s wiring, and the suite as a whole
|
|
132
|
+
are not. The contributor describes a series toward that; its shape is not decided.
|
|
133
|
+
- **The `frame-installer` guard row's `absentSuffixes`** (`.cmd`, `.ps1`) will meet that
|
|
134
|
+
series and is to be raised there, not here.
|
|
135
|
+
- **The guard's `identifier.pathname` pattern** would also flag a legitimate
|
|
136
|
+
`request.pathname` if `ts/src` ever grows HTTP routing. The row's `exemptions` field is
|
|
137
|
+
for that day.
|
|
138
|
+
- **Counting verdicts as front-door errors** (CR139), unchanged from `v0.33.2`.
|
|
139
|
+
|
|
140
|
+
## What We Learned
|
|
141
|
+
|
|
142
|
+
A diagnostic that names a conclusion is a diagnostic that lies when the conclusion is
|
|
143
|
+
wrong. `not a git checkout` was false, and nothing in the line let anyone see why. A
|
|
144
|
+
diagnostic that names evidence, the path it walked, is true even when the detector is
|
|
145
|
+
wrong, and it turns a two-platform debugging session into one line read once.
|
|
146
|
+
|
|
147
|
+
The strong form of a fix removes the class, not the instance. `fileURLToPath` fixed every
|
|
148
|
+
path that exists; `import.meta.dirname` made the question of URL spelling unaskable, and
|
|
149
|
+
the guard made the answer durable. The review asked for the second, and the contributor
|
|
150
|
+
delivered it with a guard that found a second instance on its own.
|
|
151
|
+
|
|
152
|
+
A CI job earns its place on its first catch, and this one's first catch was the person
|
|
153
|
+
who asked for it.
|
|
154
|
+
|
|
155
|
+
## Next Horizon
|
|
156
|
+
|
|
157
|
+
CR139, the verdict loop, is the visible next step in RS010. The contributor's Windows
|
|
158
|
+
series, if it comes, begins with the suite on `windows-latest`.
|
package/package.json
CHANGED
|
@@ -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,16 +33,21 @@ 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 {
|
|
45
|
+
import {
|
|
46
|
+
detectInstallKind,
|
|
47
|
+
type InstallKind,
|
|
48
|
+
inspectionRootFor,
|
|
49
|
+
thisFrontDoorPath,
|
|
50
|
+
} from "#runtime/installKind.ts";
|
|
46
51
|
import { MIGRATE_DECLINED_EXIT, renderMigrate, runMigrate } from "#runtime/migrate.ts";
|
|
47
52
|
import {
|
|
48
53
|
buildPendingReleaseNotes,
|
|
@@ -156,6 +161,11 @@ function resolveWelcomeHome(explicit: string | null, env: NodeJS.ProcessEnv): st
|
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
export interface RuntimeRouteIo {
|
|
164
|
+
/**
|
|
165
|
+
* Where the command was run. No `runtime` read route consults it: since
|
|
166
|
+
* CR134 and CR138 they answer for the install, wherever they run. Tests pass
|
|
167
|
+
* it to show that it does not matter.
|
|
168
|
+
*/
|
|
159
169
|
cwd?: string;
|
|
160
170
|
env?: NodeJS.ProcessEnv;
|
|
161
171
|
stdout?: (text: string) => void;
|
|
@@ -244,7 +254,6 @@ export async function runRuntimeReadRoute(
|
|
|
244
254
|
io: InstallRouteIo = {},
|
|
245
255
|
): Promise<number> {
|
|
246
256
|
const env = io.env ?? process.env;
|
|
247
|
-
const cwd = io.cwd ?? process.cwd();
|
|
248
257
|
const args = argv.slice(1);
|
|
249
258
|
const subcommand = args[0] ?? "";
|
|
250
259
|
// The version is the running tree's, never the cwd's: for a skill the cwd
|
|
@@ -254,19 +263,24 @@ export async function runRuntimeReadRoute(
|
|
|
254
263
|
|
|
255
264
|
if (subcommand === "version") {
|
|
256
265
|
const startArg = optionValue(args, "--start");
|
|
257
|
-
const
|
|
258
|
-
|
|
266
|
+
const install = io.install ?? detectThisInstall();
|
|
267
|
+
// The install's own repository, or the one `--start` names: the one
|
|
268
|
+
// explicit way these commands run git outside the install (CR138).
|
|
269
|
+
const root = inspectionRootFor(install, startArg ? expandHome(startArg) : null);
|
|
259
270
|
writeOut(
|
|
260
271
|
io,
|
|
261
272
|
renderRuntimeVersion({
|
|
262
273
|
version,
|
|
263
274
|
install,
|
|
264
|
-
git: inspectGitFor(install,
|
|
265
|
-
cloneRole:
|
|
266
|
-
updateChannel:
|
|
267
|
-
install
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
git: inspectGitFor(install, root),
|
|
276
|
+
cloneRole: cloneRoleFor(install, root),
|
|
277
|
+
updateChannel: channelForReport(
|
|
278
|
+
install,
|
|
279
|
+
root,
|
|
280
|
+
env,
|
|
281
|
+
io.osHome ?? homedir(),
|
|
282
|
+
optionValue(args, "--channel"),
|
|
283
|
+
),
|
|
270
284
|
}),
|
|
271
285
|
);
|
|
272
286
|
return 0;
|
|
@@ -274,24 +288,24 @@ export async function runRuntimeReadRoute(
|
|
|
274
288
|
|
|
275
289
|
if (subcommand === "status") {
|
|
276
290
|
const report = buildRuntimeStatus({
|
|
277
|
-
start: cwd,
|
|
278
291
|
mirrorHome: optionValue(args, "--mirror-home"),
|
|
279
292
|
channel: optionValue(args, "--channel"),
|
|
280
293
|
env,
|
|
281
294
|
version,
|
|
282
|
-
install: detectThisInstall(),
|
|
295
|
+
install: io.install ?? detectThisInstall(),
|
|
283
296
|
});
|
|
284
297
|
writeOut(io, renderRuntimeStatus(report, env));
|
|
285
298
|
return statusVerdict(report) === "ready" ? 0 : 1;
|
|
286
299
|
}
|
|
287
300
|
|
|
288
301
|
if (subcommand === "diagnose") {
|
|
302
|
+
// The install's own tree, so its worktree findings are Mirror's and never
|
|
303
|
+
// the cwd project's (CR138).
|
|
289
304
|
const report = buildRuntimeStatus({
|
|
290
|
-
start: cwd,
|
|
291
305
|
mirrorHome: optionValue(args, "--mirror-home"),
|
|
292
306
|
env,
|
|
293
307
|
version,
|
|
294
|
-
install: detectThisInstall(),
|
|
308
|
+
install: io.install ?? detectThisInstall(),
|
|
295
309
|
});
|
|
296
310
|
const findings = [
|
|
297
311
|
...diagnoseRuntime(report, inspectGitWorktree(report.git.repository)),
|
|
@@ -340,7 +354,7 @@ export async function runRuntimeReadRoute(
|
|
|
340
354
|
|
|
341
355
|
if (subcommand === "backup") return runRuntimeBackup(args.slice(1), io, env);
|
|
342
356
|
if (subcommand === "migrate") return runRuntimeMigrate(args.slice(1), io, env);
|
|
343
|
-
if (subcommand === "update") return runRuntimeUpdate(args.slice(1), io, env
|
|
357
|
+
if (subcommand === "update") return runRuntimeUpdate(args.slice(1), io, env);
|
|
344
358
|
if (subcommand === "channel") return runRuntimeChannel(args.slice(1), io, env);
|
|
345
359
|
|
|
346
360
|
// Routing answers every name it does not recognize before dispatch (the
|
|
@@ -486,9 +500,8 @@ function runRuntimeChannel(
|
|
|
486
500
|
*/
|
|
487
501
|
function runRuntimeUpdate(
|
|
488
502
|
args: readonly string[],
|
|
489
|
-
io:
|
|
503
|
+
io: InstallRouteIo,
|
|
490
504
|
env: NodeJS.ProcessEnv,
|
|
491
|
-
cwd: string,
|
|
492
505
|
): number {
|
|
493
506
|
const version = packageVersion(runningTreeRoot()) ?? "unknown";
|
|
494
507
|
const channelOverride = optionValue(args, "--channel");
|
|
@@ -499,11 +512,16 @@ function runRuntimeUpdate(
|
|
|
499
512
|
// npm runs in the tree the front door runs from -- for a package, the
|
|
500
513
|
// install's own root -- and never in the cwd, whose `.npmrc` could name
|
|
501
514
|
// another registry or prefix (CR135). The strategy's own calls do the same.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
515
|
+
//
|
|
516
|
+
// Git runs in the clone's own checkout and nowhere else: the gate's report
|
|
517
|
+
// and `--check` name no cwd (CR138), whose `.git/config` could name a
|
|
518
|
+
// command for git to run.
|
|
519
|
+
const install =
|
|
520
|
+
io.install ??
|
|
521
|
+
detectInstallKind({
|
|
522
|
+
frontDoorPath: thisFrontDoorPath(),
|
|
523
|
+
npmRootGlobal: npmRootGlobal(runningTreeRoot()),
|
|
524
|
+
});
|
|
507
525
|
const channel = channelFor(install, env, homedir(), channelOverride);
|
|
508
526
|
|
|
509
527
|
if (args.includes("--check")) {
|
|
@@ -517,7 +535,12 @@ function runRuntimeUpdate(
|
|
|
517
535
|
? 0
|
|
518
536
|
: 1;
|
|
519
537
|
}
|
|
520
|
-
|
|
538
|
+
// An install that cannot be identified has no checkout to ask, and the
|
|
539
|
+
// cwd is not one (CR138, decision 2). `runtime update` refuses it too.
|
|
540
|
+
const availability =
|
|
541
|
+
install.kind === "clone"
|
|
542
|
+
? checkUpdateAvailability(install.repository, channelOverride, version)
|
|
543
|
+
: unavailableUpdateCheck(version, channel, `install kind unknown: ${install.reason}`);
|
|
521
544
|
writeOut(io, renderRuntimeUpdateAvailability(availability));
|
|
522
545
|
return availability.status === "up_to_date" || availability.status === "update_available"
|
|
523
546
|
? 0
|
|
@@ -541,7 +564,6 @@ function runRuntimeUpdate(
|
|
|
541
564
|
|
|
542
565
|
const gate = (): { ready: boolean; allowed: boolean; detail: string } => {
|
|
543
566
|
const report = buildRuntimeStatus({
|
|
544
|
-
start,
|
|
545
567
|
mirrorHome,
|
|
546
568
|
channel: channelOverride,
|
|
547
569
|
env,
|
|
@@ -631,11 +653,6 @@ function spawner(env: NodeJS.ProcessEnv): UpdateSpawn {
|
|
|
631
653
|
return frontDoorSpawner(join(runningTreeRoot(), "bin", "mirror.js"), env);
|
|
632
654
|
}
|
|
633
655
|
|
|
634
|
-
/** This front door's own path, which is also how the install kind is read. */
|
|
635
|
-
function frontDoorSelfPath(): string {
|
|
636
|
-
return new URL("./cli.ts", import.meta.url).pathname;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
656
|
/**
|
|
640
657
|
* How THIS front door is installed, by what the filesystem shows and nothing
|
|
641
658
|
* spawned: a checkout by its `.git`, a package by npm's global layout and the
|
|
@@ -643,7 +660,7 @@ function frontDoorSelfPath(): string {
|
|
|
643
660
|
* lane, probes `npm root -g` itself.
|
|
644
661
|
*/
|
|
645
662
|
function detectThisInstall(): InstallKind {
|
|
646
|
-
return detectInstallKind({ frontDoorPath:
|
|
663
|
+
return detectInstallKind({ frontDoorPath: thisFrontDoorPath() });
|
|
647
664
|
}
|
|
648
665
|
|
|
649
666
|
/**
|
|
@@ -460,6 +460,24 @@ export const RETIRED: readonly RetiredSurface[] = [
|
|
|
460
460
|
// exploration's record is `docs/project/exploration/es-004-*.md`.
|
|
461
461
|
exemptions: {},
|
|
462
462
|
},
|
|
463
|
+
{
|
|
464
|
+
// Not a deleted surface: a retired IDIOM. A URL's pathname is not a
|
|
465
|
+
// filesystem path -- it percent-encodes `Código` and `John Doe`, and on
|
|
466
|
+
// Windows spells `C:\...` as `/C:/...` -- and read as one it made every
|
|
467
|
+
// install under such a directory `unknown` (PR #42, CR140). The fix is
|
|
468
|
+
// `import.meta.dirname` or `fileURLToPath`; this row keeps the idiom from
|
|
469
|
+
// coming back in code. Tests may spell the bug to pin it, so the scope is
|
|
470
|
+
// the product and its scripts.
|
|
471
|
+
surfaceId: "url-pathname-as-path",
|
|
472
|
+
story: "PR #42 (CR140)",
|
|
473
|
+
absentPaths: [],
|
|
474
|
+
// The code shapes: a property read off a call (`new URL(...).pathname`) or
|
|
475
|
+
// off a variable (`url.pathname`). Prose writes `URL.pathname`, capital U,
|
|
476
|
+
// which neither pattern matches, so a comment may still name the cause.
|
|
477
|
+
forbiddenPatterns: ["\\)\\.pathname\\b", "\\b[a-z_$][\\w$]*\\.pathname\\b"],
|
|
478
|
+
residueScope: ["ts/src/", "ts/scripts/"],
|
|
479
|
+
exemptions: {},
|
|
480
|
+
},
|
|
463
481
|
];
|
|
464
482
|
|
|
465
483
|
export interface SurfaceProblem {
|
|
@@ -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
|
@@ -436,12 +436,52 @@ export const NO_REPOSITORY: GitStatus = {
|
|
|
436
436
|
};
|
|
437
437
|
|
|
438
438
|
/**
|
|
439
|
-
* The git facts of
|
|
440
|
-
*
|
|
441
|
-
*
|
|
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).
|
|
442
444
|
*/
|
|
443
|
-
export function inspectGitFor(install: InstallKind,
|
|
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.
|
|
478
|
+
*/
|
|
479
|
+
export function unavailableUpdateCheck(
|
|
480
|
+
version: string,
|
|
481
|
+
updateChannel: MarkerValue,
|
|
482
|
+
note: string,
|
|
483
|
+
): UpdateAvailability {
|
|
484
|
+
return availability(version, updateChannel, { note });
|
|
445
485
|
}
|
|
446
486
|
|
|
447
487
|
/**
|
|
@@ -470,8 +510,7 @@ export function renderRuntimeVersion(report: RuntimeVersionReport): string {
|
|
|
470
510
|
lines.push(`Version: ${report.version}`);
|
|
471
511
|
lines.push(...renderInstallLines(report.install));
|
|
472
512
|
lines.push(...renderRepositoryLines(report.install, report.git, false));
|
|
473
|
-
lines.push(
|
|
474
|
-
if (report.cloneRole.note) lines.push(`Clone role note: ${report.cloneRole.note}`);
|
|
513
|
+
lines.push(...renderCloneRoleLines(report.install, report.cloneRole));
|
|
475
514
|
lines.push(`Update channel: ${report.updateChannel.value}`);
|
|
476
515
|
if (report.updateChannel.note) lines.push(`Update channel note: ${report.updateChannel.note}`);
|
|
477
516
|
return `${lines.join("\n")}\n`;
|
|
@@ -139,13 +139,19 @@ function packageUnderNpmRoot(
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* The installed package holding this front door, by its layout, with no
|
|
142
|
-
* subprocess: the
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* the
|
|
142
|
+
* subprocess (CV22.DS10.US3 plateau 4: the read-only routes run where npm may
|
|
143
|
+
* be off the PATH and must still say what this is). npm has two global
|
|
144
|
+
* layouts, and in both the bin is what makes a global install global -- a
|
|
145
|
+
* project that happens to live at `<x>/lib` has the manifest half and not the
|
|
146
|
+
* bin half:
|
|
147
|
+
*
|
|
148
|
+
* - POSIX: the manifest at `<prefix>/lib/node_modules/<name>`, and one of its
|
|
149
|
+
* bins at `<prefix>/bin/<bin>`, a symlink that resolves back into the
|
|
150
|
+
* package.
|
|
151
|
+
* - Windows: the manifest at `<prefix>\node_modules\<name>`, and one of its
|
|
152
|
+
* bins at `<prefix>\<bin>.cmd`, a shim file that runs
|
|
153
|
+
* `%dp0%\node_modules\<name>\...`. Nothing resolves, so the shim's own
|
|
154
|
+
* text is the evidence that the bin reaches into THIS package.
|
|
149
155
|
*/
|
|
150
156
|
function packageByLayout(
|
|
151
157
|
frontDoorPath: string,
|
|
@@ -156,15 +162,52 @@ function packageByLayout(
|
|
|
156
162
|
const manifest = nearestManifest(frontDoorPath, null, exists, readFile);
|
|
157
163
|
if (manifest === null) return null;
|
|
158
164
|
const nodeModules = dirname(manifest.root);
|
|
165
|
+
if (basename(nodeModules) !== "node_modules") return null;
|
|
166
|
+
const linked =
|
|
167
|
+
binLinkedUnderPosixPrefix(manifest, nodeModules, exists, realpath) ||
|
|
168
|
+
binShimmedUnderWindowsPrefix(manifest, nodeModules, exists, readFile);
|
|
169
|
+
if (!linked) return null;
|
|
170
|
+
return { kind: "package", root: manifest.root, name: manifest.name, version: manifest.version };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function binLinkedUnderPosixPrefix(
|
|
174
|
+
manifest: Manifest,
|
|
175
|
+
nodeModules: string,
|
|
176
|
+
exists: (path: string) => boolean,
|
|
177
|
+
realpath: (path: string) => string,
|
|
178
|
+
): boolean {
|
|
159
179
|
const lib = dirname(nodeModules);
|
|
160
|
-
if (basename(
|
|
180
|
+
if (basename(lib) !== "lib") return false;
|
|
161
181
|
const prefix = dirname(lib);
|
|
162
|
-
|
|
182
|
+
return manifest.bins.some((bin) => {
|
|
163
183
|
const link = join(prefix, "bin", bin);
|
|
164
184
|
return exists(link) && isInside(manifest.root, realpath(link));
|
|
165
185
|
});
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Not gated by platform, on purpose: the layout is evidence wherever it is
|
|
190
|
+
* found, and a Windows-only branch could be exercised only on Windows. Kept
|
|
191
|
+
* open, macOS and Linux run the Windows tests with fixture paths, and the
|
|
192
|
+
* `windows-latest` job runs the same tests on the real thing.
|
|
193
|
+
*/
|
|
194
|
+
function binShimmedUnderWindowsPrefix(
|
|
195
|
+
manifest: Manifest,
|
|
196
|
+
nodeModules: string,
|
|
197
|
+
exists: (path: string) => boolean,
|
|
198
|
+
readFile: (path: string) => string,
|
|
199
|
+
): boolean {
|
|
200
|
+
const prefix = dirname(nodeModules);
|
|
201
|
+
const runsThisPackage = `node_modules/${manifest.name}/`;
|
|
202
|
+
return manifest.bins.some((bin) => {
|
|
203
|
+
const shim = join(prefix, `${bin}.cmd`);
|
|
204
|
+
if (!exists(shim)) return false;
|
|
205
|
+
try {
|
|
206
|
+
return readFile(shim).replace(/\\/g, "/").includes(runsThisPackage);
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
168
211
|
}
|
|
169
212
|
|
|
170
213
|
/** `realpathSync`, except that a path that does not exist is returned as given. */
|
|
@@ -176,6 +219,21 @@ function safeRealpath(path: string): string {
|
|
|
176
219
|
}
|
|
177
220
|
}
|
|
178
221
|
|
|
222
|
+
/**
|
|
223
|
+
* This front door's own path, as the filesystem spells it: what every route
|
|
224
|
+
* hands `detectInstallKind`. From `import.meta.dirname`, so the path never
|
|
225
|
+
* passes through URL form: a URL's pathname is not a filesystem path. It
|
|
226
|
+
* percent-encodes every reserved character -- a directory named `Código` or
|
|
227
|
+
* `John Doe` becomes `C%C3%B3digo`, `John%20Doe` -- and on Windows it spells
|
|
228
|
+
* `C:\...` as `/C:/...`. Read as a path, none of those exist, so no `.git`
|
|
229
|
+
* was found above the front door and every such install was `unknown`: the
|
|
230
|
+
* updater refused, and after CR138 the git rows went blank. The guard row
|
|
231
|
+
* `url-pathname-as-path` keeps the idiom out of `ts/src` and `ts/scripts`.
|
|
232
|
+
*/
|
|
233
|
+
export function thisFrontDoorPath(): string {
|
|
234
|
+
return join(import.meta.dirname, "..", "frontDoor", "cli.ts");
|
|
235
|
+
}
|
|
236
|
+
|
|
179
237
|
export function detectInstallKind(probe: InstallKindProbe): InstallKind {
|
|
180
238
|
const exists = probe.exists ?? existsSync;
|
|
181
239
|
const readFile = probe.readFile ?? ((path: string) => readFileSync(path, "utf8"));
|
|
@@ -190,15 +248,32 @@ export function detectInstallKind(probe: InstallKindProbe): InstallKind {
|
|
|
190
248
|
const repository = cloneRootFor(dirname(frontDoorPath), exists);
|
|
191
249
|
if (repository !== null) return { kind: "clone", repository };
|
|
192
250
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
251
|
+
// The reason carries the path the walk started from, spelled as the detector
|
|
252
|
+
// saw it (CR140): a path that is wrong -- a URL's `%C3%B3` for `ó`, a `/C:/`
|
|
253
|
+
// for `C:\` -- is then wrong in the report, where `not a git checkout` alone
|
|
254
|
+
// sent two contributors on two platforms debugging what this line shows.
|
|
255
|
+
const because =
|
|
256
|
+
probe.npmRootGlobal === null
|
|
257
|
+
? "not a git checkout, and npm is unavailable to identify a package install"
|
|
258
|
+
: probe.npmRootGlobal === undefined
|
|
259
|
+
? "not a git checkout, and not laid out as a global npm install"
|
|
260
|
+
: "not a git checkout, and not under the global npm root";
|
|
261
|
+
return { kind: "unknown", reason: `${because}; looked up from ${frontDoorPath}` };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Where the repository rows of `status`, `version`, and `diagnose` come from
|
|
266
|
+
* (CR138): `start` when one is given -- `version --start PATH`, or a test --
|
|
267
|
+
* else a clone's own checkout, else nowhere. Never the cwd. For a skill the
|
|
268
|
+
* cwd is the person's project, and git run there reads that project's config,
|
|
269
|
+
* where `core.fsmonitor` names a command git runs on `status`.
|
|
270
|
+
*/
|
|
271
|
+
export function inspectionRootFor(
|
|
272
|
+
install: InstallKind,
|
|
273
|
+
start: string | null | undefined,
|
|
274
|
+
): string | null {
|
|
275
|
+
if (start !== null && start !== undefined) return resolve(start);
|
|
276
|
+
return install.kind === "clone" ? install.repository : null;
|
|
202
277
|
}
|
|
203
278
|
|
|
204
279
|
/** One line for a render or a log entry. */
|
package/ts/src/runtime/status.ts
CHANGED
|
@@ -26,16 +26,22 @@ import { inspectMigrationFiles } from "#extensions/migrations.ts";
|
|
|
26
26
|
import { type DbPathEnv, dbNameForEnv, resolveMirrorHome } from "#frontDoor/dbPath.ts";
|
|
27
27
|
import { DEFAULT_EMBEDDING_MODEL, DEFAULT_EXTRACTION_MODEL } from "#providers/config.ts";
|
|
28
28
|
import { sortByCodePoint } from "#util/pythonText.ts";
|
|
29
|
-
import {
|
|
29
|
+
import { channelForReport } from "./channel.ts";
|
|
30
30
|
import type { MarkerValue } from "./git.ts";
|
|
31
31
|
import {
|
|
32
|
+
cloneRoleFor,
|
|
32
33
|
type GitStatus,
|
|
33
|
-
inspectCloneRole,
|
|
34
34
|
inspectGitFor,
|
|
35
|
-
|
|
35
|
+
renderCloneRoleLines,
|
|
36
36
|
renderRepositoryLines,
|
|
37
37
|
} from "./git.ts";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
detectInstallKind,
|
|
40
|
+
type InstallKind,
|
|
41
|
+
inspectionRootFor,
|
|
42
|
+
renderInstallLines,
|
|
43
|
+
thisFrontDoorPath,
|
|
44
|
+
} from "./installKind.ts";
|
|
39
45
|
|
|
40
46
|
export interface CoreMigrationHealth {
|
|
41
47
|
ready: boolean;
|
|
@@ -339,6 +345,11 @@ export function detectNodeVersion(): string | null {
|
|
|
339
345
|
}
|
|
340
346
|
|
|
341
347
|
export interface BuildStatusOptions {
|
|
348
|
+
/**
|
|
349
|
+
* An explicit repository to inspect. Absent, the install's own: a clone's
|
|
350
|
+
* checkout, and for anything else none (CR138). It used to default to the
|
|
351
|
+
* cwd, which for a skill is the person's project.
|
|
352
|
+
*/
|
|
342
353
|
start?: string;
|
|
343
354
|
mirrorHome?: string | null;
|
|
344
355
|
channel?: string | null;
|
|
@@ -354,21 +365,6 @@ export interface BuildStatusOptions {
|
|
|
354
365
|
install?: InstallKind;
|
|
355
366
|
}
|
|
356
367
|
|
|
357
|
-
/**
|
|
358
|
-
* The channel the report shows. A clone is inspected at `start`, as the
|
|
359
|
-
* oracle did and `version --start PATH` still means; a package has no tree at
|
|
360
|
-
* `start` and keeps its channel in the user's config file (US3 D9).
|
|
361
|
-
*/
|
|
362
|
-
function channelForStatus(
|
|
363
|
-
install: InstallKind,
|
|
364
|
-
startPath: string,
|
|
365
|
-
override: string | null,
|
|
366
|
-
env: NodeJS.ProcessEnv,
|
|
367
|
-
): MarkerValue {
|
|
368
|
-
if (install.kind === "package") return channelFor(install, env, undefined, override);
|
|
369
|
-
return inspectUpdateChannel(startPath, override);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
368
|
/**
|
|
373
369
|
* Python's `resolve_mirror_home` raises `ValueError`, and
|
|
374
370
|
* `build_runtime_status` prints `str(exc)` as `Mirror home note:`. The front
|
|
@@ -394,16 +390,15 @@ function mirrorHomeForStatus(env: DbPathEnv): { home: string | null; error: stri
|
|
|
394
390
|
|
|
395
391
|
/** Port of `build_runtime_status`. */
|
|
396
392
|
export function buildRuntimeStatus(options: BuildStatusOptions = {}): RuntimeStatusReport {
|
|
397
|
-
const startPath = resolve(options.start ?? process.cwd());
|
|
398
393
|
const env = options.env ?? process.env;
|
|
399
|
-
const install =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
const git = inspectGitFor(install,
|
|
394
|
+
const install = options.install ?? detectInstallKind({ frontDoorPath: thisFrontDoorPath() });
|
|
395
|
+
// The repository rows are the install's own, or the one `start` names; never
|
|
396
|
+
// the cwd's (CR138). The git facts are a clone's: a package has no
|
|
397
|
+
// repository, and grading the cwd as one read "not a git repository" as a git
|
|
398
|
+
// error, said `attention needed`, and so refused every package update at the
|
|
399
|
+
// gate (CV22.DS10.US3 plateau 4, found by the package lane of the update smoke).
|
|
400
|
+
const root = inspectionRootFor(install, options.start);
|
|
401
|
+
const git = inspectGitFor(install, root);
|
|
407
402
|
|
|
408
403
|
let mirrorHome: string | null;
|
|
409
404
|
let mirrorHomeError: string | null;
|
|
@@ -430,9 +425,9 @@ export function buildRuntimeStatus(options: BuildStatusOptions = {}): RuntimeSta
|
|
|
430
425
|
core_migrations: inspectCoreMigrations(dbPath, dbExists),
|
|
431
426
|
extensions: listInstalledExtensions(mirrorHome),
|
|
432
427
|
extension_health: inspectExtensionHealth(mirrorHome, dbPath, dbExists),
|
|
433
|
-
clone_role:
|
|
428
|
+
clone_role: cloneRoleFor(install, root),
|
|
434
429
|
memory_env: memoryEnv,
|
|
435
|
-
update_channel:
|
|
430
|
+
update_channel: channelForReport(install, root, env, undefined, options.channel ?? null),
|
|
436
431
|
node_version: options.nodeVersion === undefined ? detectNodeVersion() : options.nodeVersion,
|
|
437
432
|
};
|
|
438
433
|
}
|
|
@@ -533,8 +528,7 @@ export function renderRuntimeStatus(
|
|
|
533
528
|
lines.push("Installed extensions: 0");
|
|
534
529
|
}
|
|
535
530
|
lines.push(...renderExtensionHealth(report.extension_health));
|
|
536
|
-
lines.push(
|
|
537
|
-
if (report.clone_role.note) lines.push(`Clone role note: ${report.clone_role.note}`);
|
|
531
|
+
lines.push(...renderCloneRoleLines(report.install, report.clone_role));
|
|
538
532
|
lines.push(`Update channel: ${report.update_channel.value}`);
|
|
539
533
|
if (report.update_channel.note) lines.push(`Update channel note: ${report.update_channel.note}`);
|
|
540
534
|
lines.push(`Node: ${report.node_version || "not found (TS front door requires Node >= 24)"}`);
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// the pipeline's decisions are testable without a registry.
|
|
12
12
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
|
-
import {
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { basename, dirname, join } from "node:path";
|
|
15
16
|
import { type ApplyStrategy, KNOWN_CHANNELS } from "#runtime/updatePipeline.ts";
|
|
16
17
|
import { compareSemver, isPlainSemver } from "#util/semver.ts";
|
|
17
18
|
|
|
@@ -40,9 +41,44 @@ export type NpmRunner = (
|
|
|
40
41
|
stderr: string;
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* How npm is spawned on this platform.
|
|
46
|
+
*
|
|
47
|
+
* Elsewhere `npm` is a file on the PATH, spawned directly. On Windows it is
|
|
48
|
+
* `npm.cmd`, which Node runs only through a shell (since CVE-2024-27980 it
|
|
49
|
+
* refuses otherwise) -- and a shell's timeout ends the shell, not the npm
|
|
50
|
+
* under it: an orphan that outlives the welcome card's budget and holds the
|
|
51
|
+
* directory it ran in. So on Windows Mirror runs npm the way `npm.cmd` itself
|
|
52
|
+
* does: the `npm-cli.js` every Windows distribution of Node ships beside
|
|
53
|
+
* `node.exe`, through the node already running. No shell, and the budget's
|
|
54
|
+
* kill reaches npm itself. Without this every probe answered "npm is
|
|
55
|
+
* unavailable" on a machine with npm on its PATH, and the update lane refused
|
|
56
|
+
* for a reason that was not true.
|
|
57
|
+
*
|
|
58
|
+
* One difference from `npm.cmd`, accepted: the shim prefers an npm the person
|
|
59
|
+
* upgraded under `<prefix>\node_modules\npm` when there is one; this runs the
|
|
60
|
+
* npm bundled with the node that is running. And when no `npm-cli.js` sits
|
|
61
|
+
* beside that node -- a node from a manager that ships npm elsewhere -- the
|
|
62
|
+
* answer is a named absence, so the status line can say "npm is unavailable"
|
|
63
|
+
* and where it looked, rather than a module-not-found stack.
|
|
64
|
+
*/
|
|
65
|
+
export function npmInvocation(
|
|
66
|
+
args: readonly string[],
|
|
67
|
+
platform: NodeJS.Platform,
|
|
68
|
+
execPath: string,
|
|
69
|
+
exists: (path: string) => boolean = existsSync,
|
|
70
|
+
): { file: string; args: string[] } | { unavailable: string } {
|
|
71
|
+
if (platform !== "win32") return { file: "npm", args: [...args] };
|
|
72
|
+
const cli = join(dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
73
|
+
if (!exists(cli)) return { unavailable: `npm-cli.js not found beside node: ${cli}` };
|
|
74
|
+
return { file: execPath, args: [cli, ...args] };
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
export const defaultNpmRunner: NpmRunner = (args, call = {}) => {
|
|
78
|
+
const invocation = npmInvocation(args, process.platform, process.execPath);
|
|
79
|
+
if ("unavailable" in invocation) return { code: -1, stdout: "", stderr: invocation.unavailable };
|
|
44
80
|
try {
|
|
45
|
-
const stdout = execFileSync(
|
|
81
|
+
const stdout = execFileSync(invocation.file, invocation.args, {
|
|
46
82
|
encoding: "utf8",
|
|
47
83
|
stdio: ["ignore", "pipe", "pipe"],
|
|
48
84
|
...(call.cwd === undefined ? {} : { cwd: call.cwd }),
|