mirror-mind 0.39.1 → 0.39.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/.pi/skills/mm-identity/SKILL.md +9 -2
- package/.pi/skills/mm-journey/SKILL.md +3 -1
- package/docs/releases/index.md +1 -0
- package/docs/releases/v0.39.2.md +113 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/plugins/mirror-mind/plugin.json +1 -1
- package/plugins/mirror-mind/skills/mm-identity/SKILL.md +9 -2
- package/plugins/mirror-mind/skills/mm-journey/SKILL.md +9 -1
- package/ts/src/frontDoor/cli.ts +53 -11
- package/ts/src/frontDoor/textInput.ts +55 -0
- package/ts/src/runtime/diagnose.ts +11 -3
|
@@ -59,9 +59,15 @@ Update identity content directly in the database.
|
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
mirror identity set <layer> <key> --content "..." [--mirror-home PATH]
|
|
62
|
+
mirror identity set <layer> <key> --file <path> [--mirror-home PATH]
|
|
62
63
|
```
|
|
63
64
|
|
|
64
|
-
If `--content` is
|
|
65
|
+
If neither `--content` nor `--file` is given, content is read from stdin.
|
|
66
|
+
|
|
67
|
+
For anything longer than a line, write the text to a file and pass `--file`:
|
|
68
|
+
Mirror reads it as bytes, so the shell never splits, substitutes, or re-encodes
|
|
69
|
+
it. On Windows PowerShell 5.1 it is the only safe route: the pipe is ASCII by
|
|
70
|
+
default and turns every accented letter into `?`. The pipe stays for one-liners.
|
|
65
71
|
|
|
66
72
|
A new `journey` or `persona` key must be kebab-case: lowercase letters, digits,
|
|
67
73
|
and single hyphens, up to 80 characters, such as `product-launch`. Agents put
|
|
@@ -70,7 +76,8 @@ one that would work. A key created before this rule keeps working.
|
|
|
70
76
|
|
|
71
77
|
**Examples:**
|
|
72
78
|
- `mirror identity set ego behavior --content "Be direct."`
|
|
73
|
-
- `
|
|
79
|
+
- `mirror identity set self soul --file new-soul.md`
|
|
80
|
+
- `echo "Be direct." | mirror identity set ego behavior` (a one-liner; not on PowerShell 5.1)
|
|
74
81
|
|
|
75
82
|
---
|
|
76
83
|
|
|
@@ -16,7 +16,9 @@ When receiving `/mm-journey update <slug> <content>`: answered by the TS core (C
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
mirror journey update <slug> "<content>"
|
|
19
|
-
#
|
|
19
|
+
# longer than a line: write it to a file, read as bytes, untouched by the shell
|
|
20
|
+
mirror journey update <slug> --file <path>
|
|
21
|
+
# a one-liner through stdin (not on Windows PowerShell 5.1, whose pipe is ASCII):
|
|
20
22
|
echo "<content>" | mirror journey update <slug> -
|
|
21
23
|
```
|
|
22
24
|
|
package/docs/releases/index.md
CHANGED
|
@@ -12,6 +12,7 @@ Future release notes should use the structure defined in [Release Notes](../proc
|
|
|
12
12
|
|
|
13
13
|
## Releases
|
|
14
14
|
|
|
15
|
+
- [v0.39.2 — Input, Read the Platform's Way](v0.39.2.md) — patch release of PR #49 and PR #48 (@rodrigoimmaginario), CR194, and CR193: `runtime diagnose` stops warning every Windows install about Node; a PowerShell byte-order mark is not stored; stdin is decoded by its mark, UTF-8 or UTF-16, and `identity set` and `journey update` gain `--file`, the route around PowerShell 5.1's ASCII pipe; `package-lock.json` names the package at its version, and the release doctor checks it. No migration.
|
|
15
16
|
- [v0.39.1 — Pi Finds Its Node, or Says So](v0.39.1.md) — patch release of PR #47 (@rodrigoimmaginario) and CR191: a Pi opened where `node` is not on the PATH no longer exits; the Pi extension resolves Node where the hook wrappers look, so a global install needs nothing configured; when no Node resolves, one warning names the fix and `runtime diagnose` reports the `hooks.log` line. No migration.
|
|
16
17
|
- [v0.39.0 — The Core as a Library](v0.39.0.md) — minor release of CV24's first eight Delivery Stories: `mirror-mind/desktop`, a library entry through which a host such as Mirror Desktop 0.3 embeds this core; calls for a home's migration state, Conversations, sidebar reads, home creation, migration on request, Mirror's context, learning from open Conversations, and Journey administration, each with an explicit path and no environment. The new `exports` field closes every other deep import. No migration.
|
|
17
18
|
- [v0.38.1 — The Refusal Names the Way Out](v0.38.1.md) — patch release of three Change Requests, CR185, CR186, and CR187: a package with no home configured names the homes it finds and the `mirror init <name>` that selects one, and `init` adopts a home that already exists; a missing or refused OpenRouter key is one line that names the file, before anything is touched; a `stable` clone ahead only by `main`'s commits is told that `stable` is behind it, not to push or reset. No migration.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.39.2 is a patch release about input read the platform's way, most of it on Windows.
|
|
4
|
+
`runtime diagnose` stops warning every Windows install that Node is unresolvable, and a
|
|
5
|
+
PowerShell byte-order mark is no longer stored as identity content (PR #49, Rodrigo
|
|
6
|
+
Immaginario). From that review, CR194: text on stdin is decoded by its mark, UTF-8 or
|
|
7
|
+
UTF-16, and `identity set` and `journey update` gain `--file`, the route for any text
|
|
8
|
+
longer than a line and the only safe one on Windows PowerShell 5.1, whose pipe is ASCII.
|
|
9
|
+
`package-lock.json` names the package at its version after nine releases that left it
|
|
10
|
+
behind (PR #48), and the release doctor now checks it (CR193). No migration, and no model
|
|
11
|
+
or prompt change.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# v0.39.2 — Input, Read the Platform's Way
|
|
15
|
+
|
|
16
|
+
**Date:** 2026-11-03
|
|
17
|
+
|
|
18
|
+
## Highlights
|
|
19
|
+
|
|
20
|
+
- **`runtime diagnose` no longer warns every Windows install that Node is unresolvable**
|
|
21
|
+
([PR #49](https://github.com/mirror-mind-ai/mirror/pull/49), by @rodrigoimmaginario).
|
|
22
|
+
It split the `PATH` on `:` and looked for `node`; on Windows that is `;` and `node.exe`,
|
|
23
|
+
so nothing ever matched. It now looks the platform's way, and the test proves the Windows
|
|
24
|
+
branch on every host.
|
|
25
|
+
- **A PowerShell byte-order mark is not content** (PR #49). `identity set` and
|
|
26
|
+
`journey update -` drop the one at the head of stdin instead of storing it as the first
|
|
27
|
+
character of an entry.
|
|
28
|
+
- **Text on stdin is decoded by its mark, and `--file` reads a file as bytes**
|
|
29
|
+
([CR194](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr194-powershell-stdin-reaches-identity-set-as-utf-16-or-ascii.md)).
|
|
30
|
+
UTF-16, as Windows PowerShell 5.1's `>` writes it, is read as UTF-16. The pipe's ASCII
|
|
31
|
+
default, which turns every accented letter into `?`, cannot be detected, so
|
|
32
|
+
`identity set <layer> <key> --file <path>` and `journey update <slug> --file <path>` read
|
|
33
|
+
the file themselves: the route for anything longer than a line on every platform, and
|
|
34
|
+
the only safe one on PowerShell 5.1. The troubleshooting page names the three PowerShell
|
|
35
|
+
behaviors and the way around each.
|
|
36
|
+
- **`package-lock.json` names `mirror-mind` at the package's version**
|
|
37
|
+
([PR #48](https://github.com/mirror-mind-ai/mirror/pull/48), by @rodrigoimmaginario),
|
|
38
|
+
with a test that keeps it so. Its root had said `mirror-core 0.0.0` through nine releases,
|
|
39
|
+
and the first `npm install` on any clone left the tree dirty.
|
|
40
|
+
- **The release doctor fails on a lock left behind**
|
|
41
|
+
([CR193](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr193-the-release-doctor-does-not-check-the-lockfile.md)),
|
|
42
|
+
before anything is tagged, naming the command that levels it. This is the first release it
|
|
43
|
+
checked.
|
|
44
|
+
- No migration, and no model or prompt change.
|
|
45
|
+
|
|
46
|
+
## Where We Started
|
|
47
|
+
|
|
48
|
+
Rodrigo Immaginario's PRs #48 and #49 had waited since 2026-10-19 and 2026-10-21. One
|
|
49
|
+
described a lockfile that nine releases in a row had left behind, each rebase of the PR
|
|
50
|
+
proving it again, the last of them v0.39.1 the afternoon before it merged. The other
|
|
51
|
+
described two inputs misread on Windows: a `PATH` split as if it were POSIX, so `runtime
|
|
52
|
+
diagnose` prescribed `MIRROR_NODE` to every healthy Windows install, and a byte-order mark
|
|
53
|
+
stored as the first character of an identity entry, repaired by hand on 2026-10-17.
|
|
54
|
+
|
|
55
|
+
Both were merged as written, each with one maintainer commit: the lock brought to the
|
|
56
|
+
current version so the PR's own test was green at merge, and `hookNodeFindings` given the
|
|
57
|
+
platform as a parameter so the branch the PR exists for is proved on macOS and Ubuntu,
|
|
58
|
+
where CI runs the suite, and not only on a Windows machine.
|
|
59
|
+
|
|
60
|
+
The review of PR #49 found the two hazards the BOM fix sat beside. In Windows PowerShell
|
|
61
|
+
5.1, `>` writes UTF-16LE, and the pipe to a native command goes through `$OutputEncoding`,
|
|
62
|
+
whose default is ASCII: Portuguese text arrives with every accent as `?`, and Mirror stored
|
|
63
|
+
what arrived. The second is not detectable — a `?` is a character — which is what shaped
|
|
64
|
+
CR194.
|
|
65
|
+
|
|
66
|
+
## What Changed
|
|
67
|
+
|
|
68
|
+
**One decoder for human text** (`ts/src/frontDoor/textInput.ts`). A UTF-8 or UTF-16 mark
|
|
69
|
+
at the head of the bytes chooses the decoding and is dropped; nothing else about the text
|
|
70
|
+
is touched. UTF-16 without a mark is not guessed at. `identity set` and `journey update -`
|
|
71
|
+
read stdin through it.
|
|
72
|
+
|
|
73
|
+
**`--file`, the route around the pipe.** Mirror reads the file as bytes, so the shell never
|
|
74
|
+
splits, substitutes, or re-encodes the text. `--file` beside `--content`, or beside `-`, is
|
|
75
|
+
refused in one line; a path that cannot be read is refused naming the path and nothing from
|
|
76
|
+
the error; nothing is written in either case. The skills present `--file` for anything
|
|
77
|
+
longer than a line, and keep the pipe for one-liners.
|
|
78
|
+
|
|
79
|
+
**`runtime diagnose` on Windows** looks for `node.exe` on a `;`-separated `PATH`, and only
|
|
80
|
+
`node.exe`: on Windows the executable check is an existence check, and an extensionless
|
|
81
|
+
`node` is nothing the system would run.
|
|
82
|
+
|
|
83
|
+
**The lockfile.** Regenerated once with `npm install --package-lock-only --ignore-scripts`;
|
|
84
|
+
`packageLock.test.ts` fails CI when its root and `package.json` disagree; the release doctor
|
|
85
|
+
makes the same four comparisons before the tag exists, and the versioning page names the
|
|
86
|
+
lock among what a release commit changes.
|
|
87
|
+
|
|
88
|
+
Validated on 2026-11-03 against a scratch copy of the maintainer's database: UTF-8, UTF-8
|
|
89
|
+
with a mark, and UTF-16LE as PowerShell 5.1 writes it, through `--file` and through stdin;
|
|
90
|
+
the refusals writing nothing; the one-liner pipe unchanged.
|
|
91
|
+
|
|
92
|
+
## Conscious Exclusions
|
|
93
|
+
|
|
94
|
+
- Windows PowerShell 5.1 itself was not run: there is no Windows machine on the maintainer's
|
|
95
|
+
side. The decoder's cases are pinned byte for byte; a measurement on the OS is asked of
|
|
96
|
+
the reporter.
|
|
97
|
+
- Detecting text the ASCII pipe damaged: not possible without guessing.
|
|
98
|
+
- The hook wrappers and the MCP locator still accept a relative `MIRROR_NODE`
|
|
99
|
+
([CR192](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr192-the-hook-wrappers-and-the-mcp-locator-accept-a-relative-mirror-node.md),
|
|
100
|
+
captured).
|
|
101
|
+
- The JSON stdin readers (`conversations append`, the hooks' payloads) are unchanged: their
|
|
102
|
+
input comes from runtimes, which write UTF-8 without a mark.
|
|
103
|
+
|
|
104
|
+
## Upgrading
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
mirror runtime update
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Next Horizon
|
|
111
|
+
|
|
112
|
+
Rodrigo Immaginario's PRs #50, #53, and #54, all Windows and packaging; CR190 and CR192 in
|
|
113
|
+
RS010's backlog. CV24 continues on its branch.
|
package/package.json
CHANGED
|
@@ -59,9 +59,15 @@ Update identity content directly in the database.
|
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
mirror identity set <layer> <key> --content "..." [--mirror-home PATH]
|
|
62
|
+
mirror identity set <layer> <key> --file <path> [--mirror-home PATH]
|
|
62
63
|
```
|
|
63
64
|
|
|
64
|
-
If `--content` is
|
|
65
|
+
If neither `--content` nor `--file` is given, content is read from stdin.
|
|
66
|
+
|
|
67
|
+
For anything longer than a line, write the text to a file and pass `--file`:
|
|
68
|
+
Mirror reads it as bytes, so the shell never splits, substitutes, or re-encodes
|
|
69
|
+
it. On Windows PowerShell 5.1 it is the only safe route: the pipe is ASCII by
|
|
70
|
+
default and turns every accented letter into `?`. The pipe stays for one-liners.
|
|
65
71
|
|
|
66
72
|
A new `journey` or `persona` key must be kebab-case: lowercase letters, digits,
|
|
67
73
|
and single hyphens, up to 80 characters, such as `product-launch`. Agents put
|
|
@@ -70,7 +76,8 @@ one that would work. A key created before this rule keeps working.
|
|
|
70
76
|
|
|
71
77
|
**Examples:**
|
|
72
78
|
- `mirror identity set ego behavior --content "Be direct."`
|
|
73
|
-
- `
|
|
79
|
+
- `mirror identity set self soul --file new-soul.md`
|
|
80
|
+
- `echo "Be direct." | mirror identity set ego behavior` (a one-liner; not on PowerShell 5.1)
|
|
74
81
|
|
|
75
82
|
---
|
|
76
83
|
|
|
@@ -30,7 +30,15 @@ If the journey path appears outdated relative to recent conversations and memori
|
|
|
30
30
|
mirror journey update JOURNEY "UPDATED_CONTENT"
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
For
|
|
33
|
+
For anything longer than a line, write it to a file and pass `--file`: Mirror
|
|
34
|
+
reads it as bytes, untouched by the shell (on Windows PowerShell 5.1 the pipe
|
|
35
|
+
is ASCII by default, and this is the only safe route):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
mirror journey update JOURNEY --file PATH
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A one-liner can go through stdin:
|
|
34
42
|
|
|
35
43
|
```bash
|
|
36
44
|
echo "CONTENT" | mirror journey update JOURNEY -
|
package/ts/src/frontDoor/cli.ts
CHANGED
|
@@ -80,6 +80,7 @@ import { configFilePath, loadConfiguration, silenceExperimentalWarnings } from "
|
|
|
80
80
|
import { runSeed } from "#seed/seed.ts";
|
|
81
81
|
import { getTasksForWeek, listTasks } from "#tasks/taskStore.ts";
|
|
82
82
|
import { computeWeekRange } from "#tasks/weekView.ts";
|
|
83
|
+
import { expandHome } from "#util/paths.ts";
|
|
83
84
|
import { PROGRAM } from "#util/program.ts";
|
|
84
85
|
import { newId, nowIso } from "#util/pyGenerators.ts";
|
|
85
86
|
import { hasOption, optionValue, stripOptionWithValue } from "./args.ts";
|
|
@@ -187,6 +188,7 @@ import {
|
|
|
187
188
|
applyTasksStatusChange,
|
|
188
189
|
type TaskStatusTarget,
|
|
189
190
|
} from "./tasksWriteRoute.ts";
|
|
191
|
+
import { readStdinText, readTextFile } from "./textInput.ts";
|
|
190
192
|
import { answerUsage } from "./usage.ts";
|
|
191
193
|
|
|
192
194
|
/**
|
|
@@ -522,12 +524,19 @@ function runShadowRead(db: Database, args: readonly string[]): number {
|
|
|
522
524
|
return 0;
|
|
523
525
|
}
|
|
524
526
|
|
|
527
|
+
/** Stdin as human text, decoded by its mark (PR #49, CR194: `textInput.ts`). */
|
|
525
528
|
function readStdinContent(): string {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
529
|
+
return readStdinText();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* CR194: `--file <path>`, the text read by Mirror as bytes, so the shell's pipe
|
|
534
|
+
* never touches it (PowerShell 5.1's pipe is ASCII by default, and its `>` is
|
|
535
|
+
* UTF-16LE). `~` expands as it does for `--mirror-home`; a relative path is
|
|
536
|
+
* the cwd's. The refusal names the path and nothing from the error.
|
|
537
|
+
*/
|
|
538
|
+
function readFileContent(path: string): { text: string } | { error: string } {
|
|
539
|
+
return readTextFile(expandHome(path));
|
|
531
540
|
}
|
|
532
541
|
|
|
533
542
|
/** `conversations append` (CV22.DS7.US10 slice B′). */
|
|
@@ -840,7 +849,10 @@ function refusedNewIdentityKey(args: readonly string[], layer: string, key: stri
|
|
|
840
849
|
function runIdentityWrite(argv: readonly string[]): number {
|
|
841
850
|
const args = argv.slice(2);
|
|
842
851
|
const positionals = stripOptionWithValue(
|
|
843
|
-
stripOptionWithValue(
|
|
852
|
+
stripOptionWithValue(
|
|
853
|
+
stripOptionWithValue(stripOptionWithValue(args, "--content"), "--file"),
|
|
854
|
+
"--db-path",
|
|
855
|
+
),
|
|
844
856
|
"--mirror-home",
|
|
845
857
|
);
|
|
846
858
|
const layer = positionals[0];
|
|
@@ -854,7 +866,23 @@ function runIdentityWrite(argv: readonly string[]): number {
|
|
|
854
866
|
console.error(`Error: ${refused}`);
|
|
855
867
|
return 1;
|
|
856
868
|
}
|
|
857
|
-
const
|
|
869
|
+
const inlineContent = optionValue(args, "--content");
|
|
870
|
+
const filePath = optionValue(args, "--file");
|
|
871
|
+
if (inlineContent !== null && filePath !== null) {
|
|
872
|
+
console.error("Error: --file and --content name two sources for one content; pass one.");
|
|
873
|
+
return 1;
|
|
874
|
+
}
|
|
875
|
+
let content: string;
|
|
876
|
+
if (filePath !== null) {
|
|
877
|
+
const read = readFileContent(filePath);
|
|
878
|
+
if ("error" in read) {
|
|
879
|
+
console.error(`Error: ${read.error}`);
|
|
880
|
+
return 1;
|
|
881
|
+
}
|
|
882
|
+
content = read.text;
|
|
883
|
+
} else {
|
|
884
|
+
content = inlineContent ?? readStdinContent();
|
|
885
|
+
}
|
|
858
886
|
if (!content.trim()) {
|
|
859
887
|
console.error("Error: content is empty.");
|
|
860
888
|
return 1;
|
|
@@ -1007,22 +1035,36 @@ function runJourneyWrite(argv: readonly string[]): number {
|
|
|
1007
1035
|
*/
|
|
1008
1036
|
const JOURNEY_UPDATE_USAGE =
|
|
1009
1037
|
`Usage: ${PROGRAM} journey update <slug> <content>\n` +
|
|
1010
|
-
" Pass '-' as <content> to read it from stdin.";
|
|
1038
|
+
" Pass '-' as <content> to read it from stdin, or --file <path> to read a file.";
|
|
1011
1039
|
const FLAG_SHAPED_CONTENT = /^--?[A-Za-z]/;
|
|
1012
1040
|
|
|
1013
1041
|
function runJourneyUpdateWrite(argv: readonly string[]): number {
|
|
1014
1042
|
const args = argv.slice(2);
|
|
1015
1043
|
const positionals = stripOptionWithValue(
|
|
1016
|
-
stripOptionWithValue(args, "--db-path"),
|
|
1044
|
+
stripOptionWithValue(stripOptionWithValue(args, "--file"), "--db-path"),
|
|
1017
1045
|
"--mirror-home",
|
|
1018
1046
|
);
|
|
1019
1047
|
const slug = positionals[0];
|
|
1020
1048
|
let content = positionals[1];
|
|
1021
|
-
|
|
1049
|
+
const filePath = optionValue(args, "--file");
|
|
1050
|
+
if (!slug || (content === undefined && filePath === null)) {
|
|
1022
1051
|
console.error(JOURNEY_UPDATE_USAGE);
|
|
1023
1052
|
return 1;
|
|
1024
1053
|
}
|
|
1025
|
-
if (
|
|
1054
|
+
if (filePath !== null) {
|
|
1055
|
+
if (content !== undefined) {
|
|
1056
|
+
console.error(
|
|
1057
|
+
`Error: --file and ${content === "-" ? "'-'" : "<content>"} name two sources for one content; pass one.`,
|
|
1058
|
+
);
|
|
1059
|
+
return 1;
|
|
1060
|
+
}
|
|
1061
|
+
const read = readFileContent(filePath);
|
|
1062
|
+
if ("error" in read) {
|
|
1063
|
+
console.error(`Error: ${read.error}`);
|
|
1064
|
+
return 1;
|
|
1065
|
+
}
|
|
1066
|
+
content = read.text;
|
|
1067
|
+
} else if (content === "-") {
|
|
1026
1068
|
content = readStdinContent();
|
|
1027
1069
|
} else if (FLAG_SHAPED_CONTENT.test(content)) {
|
|
1028
1070
|
console.error(
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Human text that reaches the front door as bytes: stdin behind `identity set`
|
|
2
|
+
// and `journey update -`, and the file behind `--file` (CR194).
|
|
3
|
+
//
|
|
4
|
+
// The bytes carry no encoding but what their first bytes say. PowerShell 5.1
|
|
5
|
+
// writes a UTF-8 mark at the head of what it pipes (PR #49) and UTF-16LE from
|
|
6
|
+
// `>`; an editor writes UTF-8 with or without a mark. The mark is an encoding's
|
|
7
|
+
// signature, not content: it chooses the decoding and is dropped. UTF-16
|
|
8
|
+
// without a mark is not detectable and is not attempted: a guess about bytes
|
|
9
|
+
// would be wrong about some file, silently.
|
|
10
|
+
//
|
|
11
|
+
// What the ASCII pipe does to a text (`$OutputEncoding` in PowerShell 5.1
|
|
12
|
+
// turns every accented letter into `?`) is not detectable either: a `?` is a
|
|
13
|
+
// character. `--file` is the route that never meets the pipe.
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
|
|
17
|
+
/** The text the bytes carry, by their mark; the mark itself is never returned. */
|
|
18
|
+
export function decodeTextInput(bytes: Buffer): string {
|
|
19
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
20
|
+
return bytes.subarray(2).toString("utf16le");
|
|
21
|
+
}
|
|
22
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
23
|
+
const payload = Buffer.from(bytes.subarray(2));
|
|
24
|
+
// An odd trailing byte is not a code unit; `swap16` refuses it.
|
|
25
|
+
return (payload.length % 2 === 0 ? payload : payload.subarray(0, payload.length - 1))
|
|
26
|
+
.swap16()
|
|
27
|
+
.toString("utf16le");
|
|
28
|
+
}
|
|
29
|
+
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
30
|
+
return bytes.subarray(3).toString("utf8");
|
|
31
|
+
}
|
|
32
|
+
return bytes.toString("utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Stdin to its end, decoded; "" when there is none. */
|
|
36
|
+
export function readStdinText(): string {
|
|
37
|
+
try {
|
|
38
|
+
return decodeTextInput(readFileSync(0));
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The file behind `--file`, decoded; or the one-line refusal. The line names
|
|
46
|
+
* the path and nothing from the error: a directory, a missing file, and a
|
|
47
|
+
* permission refused all read the same to the person, and `errno` is noise.
|
|
48
|
+
*/
|
|
49
|
+
export function readTextFile(path: string): { text: string } | { error: string } {
|
|
50
|
+
try {
|
|
51
|
+
return { text: decodeTextInput(readFileSync(path)) };
|
|
52
|
+
} catch {
|
|
53
|
+
return { error: `cannot read ${path}` };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// inconclusive without a key; the live fetch belongs to DS8.
|
|
26
26
|
|
|
27
27
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
28
|
-
import { basename, dirname, join } from "node:path";
|
|
28
|
+
import { basename, dirname, join, posix, win32 } from "node:path";
|
|
29
29
|
import { openDatabaseReadOnly } from "#db/database.ts";
|
|
30
30
|
import {
|
|
31
31
|
type ExtensionPluginReport,
|
|
@@ -913,16 +913,24 @@ export function hookFailureFindings(
|
|
|
913
913
|
export function hookNodeFindings(
|
|
914
914
|
env: NodeJS.ProcessEnv,
|
|
915
915
|
isExecutable: (path: string) => boolean,
|
|
916
|
+
platform: NodeJS.Platform = process.platform,
|
|
916
917
|
): DriftFinding[] {
|
|
917
918
|
const explicit = env.MIRROR_NODE ?? "";
|
|
918
919
|
if (explicit && isExecutable(explicit)) return [];
|
|
919
920
|
const candidates = HOOK_NODE_CANDIDATES.map((candidate) =>
|
|
920
921
|
candidate.replace("$HOME", env.HOME ?? ""),
|
|
921
922
|
);
|
|
923
|
+
// The platform's PATH separator and its executable name: `;` and `node.exe`
|
|
924
|
+
// on Windows, where splitting on `:` cut `C:\Program Files\nodejs` into `C`
|
|
925
|
+
// and a path and the finding fired on every install that had Node (PR #49).
|
|
926
|
+
// `platform` is a parameter so the test runs both branches on every host:
|
|
927
|
+
// CI's Windows job runs two files and this one is not among them.
|
|
928
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
929
|
+
const name = platform === "win32" ? "node.exe" : "node";
|
|
922
930
|
const onPath = (env.PATH ?? "")
|
|
923
|
-
.split(
|
|
931
|
+
.split(paths.delimiter)
|
|
924
932
|
.filter(Boolean)
|
|
925
|
-
.some((entry) => isExecutable(
|
|
933
|
+
.some((entry) => isExecutable(paths.join(entry, name)));
|
|
926
934
|
if (onPath || candidates.some((candidate) => isExecutable(candidate))) return [];
|
|
927
935
|
return [
|
|
928
936
|
{
|