@mean-weasel/lineage 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +152 -56
- package/dist/cli/lineage-channel.js +303 -0
- package/dist/cli/lineage-channel.js.map +7 -0
- package/dist/cli/lineage-dev.js +900 -220
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage-preview.js +6182 -0
- package/dist/cli/lineage-preview.js.map +7 -0
- package/dist/cli/lineage.js +891 -216
- package/dist/cli/lineage.js.map +4 -4
- package/dist/cli/managed-service.js +430 -0
- package/dist/cli/managed-service.js.map +7 -0
- package/dist/runtime-build.json +9 -0
- package/dist/server.js +452 -164
- package/dist/server.js.map +4 -4
- package/dist/web/assets/{index-OkymIyPO.js → index-DJ5UR_oa.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +22 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.14
|
|
4
|
+
|
|
5
|
+
- Fix isolated channel installs against npm clients that return registry integrity as a flat `dist.integrity` field, while rejecting missing or conflicting identity metadata.
|
|
6
|
+
- Run promotion claim verification through an exact receipt-bound registry install, named production profile, external asset root, and managed writer.
|
|
7
|
+
- Supersede the `0.1.13` candidate, whose packaged channel installer cannot bootstrap registry installs with the affected npm metadata shape.
|
|
8
|
+
|
|
9
|
+
## 0.1.13
|
|
10
|
+
|
|
11
|
+
- Isolate stable, preview, and checkout-only development code into attested channel-specific roots with runtime doctor and tamper detection.
|
|
12
|
+
- Require fingerprint-bound named profiles, opened-handle SQLite identity checks, one managed writer, consistent database clones, and referenced-asset-only migration receipts for persistent writes.
|
|
13
|
+
- Ship profile-aware managed service lifecycles, a three-runtime adversarial coexistence oracle, and an atomic version-locked Codex plugin release/install path.
|
|
14
|
+
|
|
3
15
|
## 0.1.12
|
|
4
16
|
|
|
5
17
|
- Add named runtime profiles that bind environment, SQLite, media root, service origin, and expected runtime identity without allowing silent path drift.
|
package/README.md
CHANGED
|
@@ -4,41 +4,62 @@ Lineage is a local-first workspace for reviewing creative assets, branching vari
|
|
|
4
4
|
|
|
5
5
|
## Package Channels
|
|
6
6
|
|
|
7
|
-
Lineage is packaged as `@mean-weasel/lineage
|
|
7
|
+
Lineage is packaged as `@mean-weasel/lineage`, but stable and preview must not
|
|
8
|
+
be installed into the same global npm prefix. A global `npm install -g ...@next`
|
|
9
|
+
would replace the files and bins used by `latest`.
|
|
10
|
+
|
|
11
|
+
Use the stable package only as a bootstrap for the channel installer, then
|
|
12
|
+
resolve `latest` and `next` into separate content-addressed roots:
|
|
8
13
|
|
|
9
14
|
```bash
|
|
10
15
|
npm install -g @mean-weasel/lineage@latest
|
|
11
|
-
|
|
16
|
+
lineage-channel install stable
|
|
17
|
+
lineage-channel install preview
|
|
12
18
|
```
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
By default the installer writes immutable version/integrity roots beneath
|
|
21
|
+
`~/Library/Application Support/Lineage/runtimes`, plus channel-qualified
|
|
22
|
+
launchers in its `bin` directory. Every launcher is pinned to an absolute
|
|
23
|
+
package root and install receipt; it does not select code from `PATH` at runtime.
|
|
24
|
+
|
|
25
|
+
The three exact channel meanings are:
|
|
15
26
|
|
|
16
|
-
- `
|
|
17
|
-
- `
|
|
18
|
-
- `dev` is the
|
|
19
|
-
|
|
20
|
-
- `lineage-dev` runs with dev runtime defaults.
|
|
27
|
+
- `stable` is npm `latest`, launched as `lineage-stable` from its receipt-bound root.
|
|
28
|
+
- `preview` is npm `next`, launched as `lineage-preview` from a different root.
|
|
29
|
+
- `dev` is the current Git checkout/worktree, launched from source with
|
|
30
|
+
`npm run lineage:dev -- <command>`.
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
Verify code identity before inspecting data or starting a service:
|
|
23
33
|
|
|
24
34
|
```bash
|
|
25
|
-
lineage --
|
|
26
|
-
lineage --
|
|
27
|
-
lineage
|
|
28
|
-
lineage-dev --version
|
|
35
|
+
lineage-stable runtime doctor --json
|
|
36
|
+
lineage-preview runtime doctor --json
|
|
37
|
+
npm run lineage:dev -- runtime doctor --json
|
|
29
38
|
```
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
`runtime doctor` fails unless channel, canonical package/checkout root, exact
|
|
41
|
+
version, embedded Git/build fingerprint, install receipt, and installed package
|
|
42
|
+
tree agree. `runtime info` prints the same identity without requiring it to pass
|
|
43
|
+
and is intended for repair diagnostics.
|
|
44
|
+
|
|
45
|
+
A published package still carries a `lineage-dev` migration stub so old global
|
|
46
|
+
links fail with checkout guidance. It never runs package code as dev.
|
|
47
|
+
|
|
48
|
+
Start each foreground runtime explicitly:
|
|
32
49
|
|
|
33
50
|
```bash
|
|
34
|
-
lineage start
|
|
35
|
-
lineage-
|
|
51
|
+
lineage-stable start --profile team-production
|
|
52
|
+
lineage-preview start --profile team-preview
|
|
53
|
+
npm run lineage:dev -- start --profile team-development
|
|
36
54
|
```
|
|
37
55
|
|
|
38
|
-
|
|
56
|
+
Stable defaults to `lineage.localhost:5197`, dev to
|
|
57
|
+
`lineage-dev.localhost:5198`, and preview to
|
|
58
|
+
`lineage-preview.localhost:5199`. Override those defaults with `--port`,
|
|
59
|
+
`--host`, `--db`, or `LINEAGE_HOME`:
|
|
39
60
|
|
|
40
61
|
```bash
|
|
41
|
-
lineage start --
|
|
62
|
+
lineage-stable start --profile team-production --port 6123
|
|
42
63
|
```
|
|
43
64
|
|
|
44
65
|
`--db` selects the SQLite state file. `--asset-root` (or
|
|
@@ -50,17 +71,19 @@ web assets, and public demo fixtures.
|
|
|
50
71
|
|
|
51
72
|
## Runtime Channels and SQLite
|
|
52
73
|
|
|
53
|
-
Lineage treats
|
|
74
|
+
Lineage treats attested code identity and local SQLite data identity as separate,
|
|
75
|
+
simultaneously required concerns:
|
|
54
76
|
|
|
55
|
-
- `stable` is npm `latest` and
|
|
56
|
-
- `preview` is the
|
|
57
|
-
- `dev` is
|
|
77
|
+
- `stable` is the isolated npm `latest` install and daily-use channel.
|
|
78
|
+
- `preview` is the isolated npm `next` candidate.
|
|
79
|
+
- `dev` is the current Git checkout/worktree and may be dirty.
|
|
58
80
|
|
|
59
|
-
Each channel
|
|
60
|
-
Do not point preview or dev code at the stable database
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
Each channel must use its own code root, runtime directory, and SQLite database.
|
|
82
|
+
Do not point preview or dev code at the stable database. Use the profile clone
|
|
83
|
+
command when a realistic preview/dev dataset is needed; it reads the source
|
|
84
|
+
through SQLite's online backup API and gives the target a fresh non-production
|
|
85
|
+
identity. Direct `--db` and `LINEAGE_DB` selection is diagnostic/read-only
|
|
86
|
+
unless a matching named profile has been selected and its writer lease is held.
|
|
64
87
|
|
|
65
88
|
For repeatable or multi-session work, prefer a named profile over those legacy
|
|
66
89
|
path defaults. A profile is an immutable identity contract for one Lineage
|
|
@@ -71,6 +94,11 @@ service and its data:
|
|
|
71
94
|
"schema_version": "lineage.profile.v1",
|
|
72
95
|
"profile_id": "team-production",
|
|
73
96
|
"environment": "production",
|
|
97
|
+
"expected_runtime": {
|
|
98
|
+
"channel": "stable",
|
|
99
|
+
"code_origin": "package",
|
|
100
|
+
"code_fingerprint": "<64-character fingerprint from runtime doctor>"
|
|
101
|
+
},
|
|
74
102
|
"database_path": "./lineage.sqlite",
|
|
75
103
|
"asset_root": "./media",
|
|
76
104
|
"service_origin": "http://lineage.localhost:5197"
|
|
@@ -83,47 +111,101 @@ and asset paths resolve from the manifest directory. Inspect a profile without
|
|
|
83
111
|
creating or migrating anything:
|
|
84
112
|
|
|
85
113
|
```bash
|
|
86
|
-
lineage profile doctor --profile team-production --json
|
|
87
|
-
lineage
|
|
88
|
-
lineage start --profile team-production
|
|
114
|
+
lineage-stable profile doctor --profile team-production --json
|
|
115
|
+
lineage-stable start --profile team-production
|
|
89
116
|
```
|
|
90
117
|
|
|
91
118
|
`LINEAGE_PROFILE` is equivalent to `--profile`. Profile commands reject direct
|
|
92
119
|
`--db` and `--asset-root` overrides, and a dev or preview runtime refuses to
|
|
93
120
|
open a production profile. Doctor also requires the existing SQLite database
|
|
94
|
-
to have one matching `lineage_profile_identity` binding
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
to have one matching `lineage_profile_identity` binding, including the profile
|
|
122
|
+
manifest fingerprint. Binding is an explicit migration operation:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
lineage-stable profile bind --profile team-production --confirm-write --json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The profile must pin the exact verified code origin and fingerprint before it
|
|
129
|
+
can be bound or used for writes. `profile bind` may add a fingerprint to a
|
|
130
|
+
matching legacy identity, but refuses a conflicting identity. An unprofiled
|
|
131
|
+
CLI or service is `legacy-unbound` and read-only; mutating CLI commands and HTTP
|
|
132
|
+
methods fail with a profile-required error instead of creating a database.
|
|
133
|
+
|
|
134
|
+
To create a realistic preview or development database without copying a live
|
|
135
|
+
SQLite file directly, define a new non-production target profile whose database
|
|
136
|
+
does not exist, then run:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
lineage-preview profile clone --source-db /path/to/source.sqlite \
|
|
140
|
+
--target-profile team-preview --confirm-write --json
|
|
141
|
+
npm run lineage:dev -- profile clone --source-db /path/to/source.sqlite \
|
|
142
|
+
--target-profile team-development --confirm-write --json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Clone refuses production targets, existing target files, source/target path
|
|
146
|
+
reuse, unverified code, and missing confirmation. It writes a local receipt
|
|
147
|
+
under the target profile directory after SQLite integrity and identity checks.
|
|
148
|
+
|
|
149
|
+
When migrating a legacy installation whose production media still lives under
|
|
150
|
+
a development checkout, stage only database-referenced local files into the
|
|
151
|
+
new profile's nonexistent, dedicated asset root before binding or starting it:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
lineage-stable profile clone-assets --source-asset-root /path/to/legacy/checkout \
|
|
155
|
+
--target-profile team-production --confirm-write --json
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Asset clone holds the target profile's writer lease, refuses reused or nested
|
|
159
|
+
roots and escaping references, reserves the target without clobbering, verifies
|
|
160
|
+
every copied file by SHA-256, uses owner-only permissions, and writes a compact
|
|
161
|
+
receipt. Missing legacy references are counted rather than silently replaced;
|
|
162
|
+
unreferenced checkout scratch files are not copied. Inspect that count before
|
|
163
|
+
binding the database. The source database and asset tree remain read-only.
|
|
99
164
|
|
|
100
165
|
A named profile has one cross-process writer lease at
|
|
101
|
-
`<
|
|
102
|
-
lifetime, so a second service is refused while it is
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
WAL journaling and a five-second busy timeout. If an owner was killed, the next
|
|
112
|
-
writer may reclaim the lock only after the recorded PID is no longer alive;
|
|
113
|
-
malformed or mismatched lock metadata is left in place for manual inspection.
|
|
114
|
-
`profile doctor` never acquires or modifies the lease. Legacy-unbound databases
|
|
115
|
-
retain their existing compatibility behavior and also use WAL.
|
|
166
|
+
`<profile-directory>/writer.lock`. The managed service holds that lease for its
|
|
167
|
+
lifetime, so a second service or one-shot CLI writer is refused while it is
|
|
168
|
+
running. A CLI data command may hold the same lease only while the service is
|
|
169
|
+
offline. Lineage verifies lease ownership before opening a named-profile
|
|
170
|
+
database for writes and configures SQLite with WAL journaling and a five-second
|
|
171
|
+
busy timeout. If an owner was killed, the next writer may reclaim the lock only
|
|
172
|
+
after the recorded PID is no longer alive; malformed or mismatched lock metadata
|
|
173
|
+
is left in place for manual inspection. `profile doctor` never acquires or
|
|
174
|
+
modifies the lease. The lease records both the immutable profile ID and manifest
|
|
175
|
+
fingerprint; legacy-unbound access never acquires a writer lease.
|
|
116
176
|
|
|
117
177
|
Check the active runtime before making changes:
|
|
118
178
|
|
|
119
179
|
```bash
|
|
120
|
-
lineage db info --json
|
|
121
|
-
lineage-
|
|
180
|
+
lineage-stable db info --profile team-production --json
|
|
181
|
+
lineage-preview db info --profile team-preview --json
|
|
182
|
+
npm run lineage:dev -- db info --profile team-development --json
|
|
122
183
|
```
|
|
123
184
|
|
|
185
|
+
Managed services are profile-scoped. Their receipts record the launcher PID
|
|
186
|
+
and start token, unique service instance, code root/fingerprint, profile and
|
|
187
|
+
database fingerprints, origin, and log path. Start waits for `/api/runtime` to
|
|
188
|
+
match every field before reporting success or opening a browser. Status exits
|
|
189
|
+
nonzero for a stale PID, unreachable service, current-code drift, wrong port,
|
|
190
|
+
or any code/profile/database/instance mismatch:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
make start-prod-bg LINEAGE_PROD_PROFILE=team-production
|
|
194
|
+
make status-prod LINEAGE_PROD_PROFILE=team-production
|
|
195
|
+
make start-preview-bg LINEAGE_PREVIEW_PROFILE=team-preview
|
|
196
|
+
make start-dev-bg LINEAGE_DEV_PROFILE=team-development
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Use the matching `status-*`, `logs-*`, and `stop-*` target with the same profile
|
|
200
|
+
variable. Stable and preview targets execute the service controller from that
|
|
201
|
+
channel's attested, content-addressed package root; only dev executes the
|
|
202
|
+
checkout controller. Stop signals only the process identity recorded for that profile.
|
|
203
|
+
Checkout-backed `start-local-prod` and registration-only launchd/tmux status
|
|
204
|
+
paths are intentionally not supported.
|
|
205
|
+
|
|
124
206
|
The app shows a persistent environment/profile badge, and Settings includes the
|
|
125
|
-
profile binding, channel, version, Git
|
|
126
|
-
SQLite path.
|
|
207
|
+
profile binding, code origin/root/fingerprint, channel, version, embedded Git
|
|
208
|
+
revision, schema markers, and SQLite path.
|
|
127
209
|
|
|
128
210
|
## Agent Claims
|
|
129
211
|
|
|
@@ -242,15 +324,27 @@ whose version or `lineage.version` does not exactly match the resolved Lineage
|
|
|
242
324
|
package version. The plugin artifact and installer package are released from
|
|
243
325
|
this public repository.
|
|
244
326
|
|
|
327
|
+
Every Lineage release preflights the current operator skill, packs and checksums
|
|
328
|
+
the exact matching plugin, installs that artifact into a temporary Codex plugin
|
|
329
|
+
root, and attaches both files to the versioned GitHub release before npm publish
|
|
330
|
+
or dist-tag mutation is allowed. Release status reports the local version lock
|
|
331
|
+
and GitHub assets. The installed skill requires isolated channel launchers,
|
|
332
|
+
named profiles, managed service identity status, and profile-only writes.
|
|
333
|
+
|
|
245
334
|
## Local Development
|
|
246
335
|
|
|
247
336
|
```bash
|
|
248
337
|
npm ci
|
|
249
338
|
npm run dev
|
|
250
339
|
npm run ci
|
|
340
|
+
npm run runtime:oracle
|
|
251
341
|
```
|
|
252
342
|
|
|
253
343
|
`npm run dev` starts the local development server from source. `npm run ci` runs the full local verification gate.
|
|
344
|
+
`npm run runtime:oracle` creates three temporary code roots, profiles,
|
|
345
|
+
databases, ports, and managed services, proves their identities are distinct
|
|
346
|
+
while all are live, attacks every cross-channel and stale-identity boundary,
|
|
347
|
+
and then cleans up.
|
|
254
348
|
|
|
255
349
|
## Command Shortcuts
|
|
256
350
|
|
|
@@ -260,7 +354,9 @@ verification commands. Run `make` or `make help` to list the available targets.
|
|
|
260
354
|
```bash
|
|
261
355
|
make install-prod
|
|
262
356
|
make install-plugin-prod
|
|
263
|
-
make start-prod
|
|
357
|
+
make start-prod LINEAGE_PROD_PROFILE=team-production
|
|
358
|
+
make start-prod-bg LINEAGE_PROD_PROFILE=team-production
|
|
359
|
+
make status-prod LINEAGE_PROD_PROFILE=team-production
|
|
264
360
|
make check
|
|
265
361
|
make smoke
|
|
266
362
|
```
|
|
@@ -287,7 +383,7 @@ npm run release:dry-run -- --tag latest
|
|
|
287
383
|
npm run release:latest
|
|
288
384
|
```
|
|
289
385
|
|
|
290
|
-
The release script verifies package metadata, changelog version coverage, public-readiness scans, install smoke, browser smoke, audit,
|
|
386
|
+
The release script verifies package metadata, changelog version coverage, public-readiness scans, install smoke, browser smoke, audit, package contents, and the version-locked Codex plugin before publishing. The GitHub plugin artifact and checksum must exist before npm publish or dist-tag mutation. Promotion also installs the candidate package and runs a claim lifecycle smoke that creates a target claim, proves missing-token writes fail, proves matching-token writes succeed, verifies read surfaces do not expose the raw token, and proves release invalidates the token. GitHub Actions runs CI on pull requests and `main`; publishing is manual through the Release workflow.
|
|
291
387
|
|
|
292
388
|
Use the Release workflow operations this way:
|
|
293
389
|
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/lineage-channel.ts
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
readlinkSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
writeFileSync
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { homedir, platform } from "node:os";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
// src/shared/runtimeInfoTypes.ts
|
|
24
|
+
var lineageRuntimeBuildSchemaVersion = "lineage.runtime_build.v1";
|
|
25
|
+
var lineageRuntimeInstallSchemaVersion = "lineage.runtime_install.v1";
|
|
26
|
+
|
|
27
|
+
// src/cli/lineage-channel.ts
|
|
28
|
+
var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
29
|
+
var packageInfo = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
30
|
+
function sha256(value) {
|
|
31
|
+
return createHash("sha256").update(value).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
function readOption(args, name) {
|
|
34
|
+
const inline = args.find((arg) => arg.startsWith(`${name}=`));
|
|
35
|
+
if (inline) return inline.slice(name.length + 1);
|
|
36
|
+
const index = args.indexOf(name);
|
|
37
|
+
return index >= 0 ? args[index + 1] : void 0;
|
|
38
|
+
}
|
|
39
|
+
function runtimeRoot() {
|
|
40
|
+
if (process.env.LINEAGE_RUNTIME_ROOT) return resolve(process.env.LINEAGE_RUNTIME_ROOT);
|
|
41
|
+
if (platform() === "darwin") return join(homedir(), "Library", "Application Support", "Lineage", "runtimes");
|
|
42
|
+
if (platform() === "win32") return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Lineage", "runtimes");
|
|
43
|
+
return join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "lineage", "runtimes");
|
|
44
|
+
}
|
|
45
|
+
function parseChannel(value) {
|
|
46
|
+
if (value === "stable" || value === "preview") return value;
|
|
47
|
+
throw new Error("Channel must be stable or preview; dev is checkout-only");
|
|
48
|
+
}
|
|
49
|
+
function packageTreeSha256(root) {
|
|
50
|
+
const hash = createHash("sha256");
|
|
51
|
+
const visit = (directory, relativeDirectory = "") => {
|
|
52
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
53
|
+
const relativePath = relativeDirectory ? join(relativeDirectory, entry.name) : entry.name;
|
|
54
|
+
const path = join(directory, entry.name);
|
|
55
|
+
hash.update(relativePath.replaceAll("\\", "/"));
|
|
56
|
+
hash.update("\0");
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
hash.update("directory\0");
|
|
59
|
+
visit(path, relativePath);
|
|
60
|
+
} else if (entry.isSymbolicLink()) {
|
|
61
|
+
hash.update("symlink\0");
|
|
62
|
+
hash.update(readlinkSync(path));
|
|
63
|
+
} else if (entry.isFile()) {
|
|
64
|
+
hash.update("file\0");
|
|
65
|
+
hash.update(readFileSync(path));
|
|
66
|
+
} else {
|
|
67
|
+
hash.update("other\0");
|
|
68
|
+
}
|
|
69
|
+
hash.update("\0");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
visit(root);
|
|
73
|
+
return hash.digest("hex");
|
|
74
|
+
}
|
|
75
|
+
function expectedBuildFingerprint(build) {
|
|
76
|
+
return sha256(JSON.stringify({
|
|
77
|
+
package_name: build.package_name,
|
|
78
|
+
package_version: build.package_version,
|
|
79
|
+
schema_version: build.schema_version,
|
|
80
|
+
source_dirty: build.source_dirty,
|
|
81
|
+
source_fingerprint: build.source_fingerprint,
|
|
82
|
+
source_git_sha: build.source_git_sha
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
function validateBuild(root, installed) {
|
|
86
|
+
const path = join(root, "dist", "runtime-build.json");
|
|
87
|
+
const build = JSON.parse(readFileSync(path, "utf8"));
|
|
88
|
+
if (build.schema_version !== lineageRuntimeBuildSchemaVersion) throw new Error(`Unsupported build identity schema in ${path}`);
|
|
89
|
+
if (build.package_name !== installed.name || build.package_version !== installed.version) throw new Error("Embedded build identity does not match installed package.json");
|
|
90
|
+
if (!/^[a-f0-9]{40}$/i.test(build.source_git_sha)) throw new Error("Embedded build Git revision is invalid");
|
|
91
|
+
if (!/^[a-f0-9]{64}$/i.test(build.source_fingerprint)) throw new Error("Embedded source fingerprint is invalid");
|
|
92
|
+
if (build.build_fingerprint !== expectedBuildFingerprint(build)) throw new Error("Embedded build fingerprint does not match its contents");
|
|
93
|
+
if (build.source_dirty) throw new Error("Refusing stable/preview installation of a dirty-source build");
|
|
94
|
+
return build;
|
|
95
|
+
}
|
|
96
|
+
function parseRegistryPackageMetadata(value) {
|
|
97
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
98
|
+
throw new Error("npm metadata was not a JSON object");
|
|
99
|
+
}
|
|
100
|
+
const metadata = value;
|
|
101
|
+
const nestedIntegrity = metadata.dist?.integrity;
|
|
102
|
+
const flatIntegrity = metadata["dist.integrity"];
|
|
103
|
+
if (typeof nestedIntegrity === "string" && typeof flatIntegrity === "string" && nestedIntegrity !== flatIntegrity) {
|
|
104
|
+
throw new Error("npm metadata returned conflicting integrity values");
|
|
105
|
+
}
|
|
106
|
+
const integrity = typeof nestedIntegrity === "string" ? nestedIntegrity : flatIntegrity;
|
|
107
|
+
if (typeof metadata.version !== "string" || !metadata.version || typeof integrity !== "string" || !integrity) {
|
|
108
|
+
throw new Error("npm metadata did not include exact version and integrity");
|
|
109
|
+
}
|
|
110
|
+
return { integrity, version: metadata.version };
|
|
111
|
+
}
|
|
112
|
+
function resolveSpec(spec, allowLocalPackage) {
|
|
113
|
+
const localPath = resolve(spec.replace(/^file:/, ""));
|
|
114
|
+
if (existsSync(localPath)) {
|
|
115
|
+
if (!allowLocalPackage) {
|
|
116
|
+
throw new Error("Local package paths require --allow-local-package; normal stable/preview installs must resolve from the npm registry");
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
installSpec: localPath,
|
|
120
|
+
integrity: `sha512-${createHash("sha512").update(readFileSync(localPath)).digest("base64")}`,
|
|
121
|
+
requestedSpec: spec,
|
|
122
|
+
source: "local"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
let metadata;
|
|
126
|
+
try {
|
|
127
|
+
metadata = parseRegistryPackageMetadata(JSON.parse(execFileSync("npm", ["view", spec, "version", "dist.integrity", "--json"], { encoding: "utf8" })));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new Error(`npm metadata for ${spec} did not include exact version and integrity`, { cause: error });
|
|
130
|
+
}
|
|
131
|
+
const name = spec.startsWith("@") ? spec.slice(0, spec.indexOf("@", 1)) : spec.split("@")[0];
|
|
132
|
+
return {
|
|
133
|
+
installSpec: `${name}@${metadata.version}`,
|
|
134
|
+
integrity: metadata.integrity,
|
|
135
|
+
expectedVersion: metadata.version,
|
|
136
|
+
requestedSpec: spec,
|
|
137
|
+
source: "registry"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function shellQuote(value) {
|
|
141
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
142
|
+
}
|
|
143
|
+
function writeShim(path, channel, receiptPath, entrypoint) {
|
|
144
|
+
const tempPath = `${path}.tmp-${process.pid}`;
|
|
145
|
+
const script = `#!/bin/sh
|
|
146
|
+
LINEAGE_RUNTIME_RECEIPT=${shellQuote(receiptPath)} LINEAGE_RELEASE_CHANNEL=${shellQuote(channel)} exec ${shellQuote(process.execPath)} ${shellQuote(entrypoint)} "$@"
|
|
147
|
+
`;
|
|
148
|
+
writeFileSync(tempPath, script, { mode: 493 });
|
|
149
|
+
chmodSync(tempPath, 493);
|
|
150
|
+
renameSync(tempPath, path);
|
|
151
|
+
}
|
|
152
|
+
function validateExistingReceipt(receiptPath, channel, expected) {
|
|
153
|
+
const receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
154
|
+
if (receipt.schema_version !== lineageRuntimeInstallSchemaVersion || receipt.channel !== channel) throw new Error(`Invalid existing ${channel} runtime receipt`);
|
|
155
|
+
const installed = JSON.parse(readFileSync(join(receipt.package_root, "package.json"), "utf8"));
|
|
156
|
+
const build = validateBuild(receipt.package_root, installed);
|
|
157
|
+
if (receipt.package_name !== installed.name || receipt.package_version !== installed.version) throw new Error(`Existing ${channel} receipt does not match package.json`);
|
|
158
|
+
if (receipt.build_fingerprint !== build.build_fingerprint) throw new Error(`Existing ${channel} receipt does not match embedded build identity`);
|
|
159
|
+
if (packageTreeSha256(receipt.package_root) !== receipt.package_tree_sha256) throw new Error(`Existing ${channel} runtime package tree has changed`);
|
|
160
|
+
if (expected && (receipt.package_integrity !== expected.integrity || receipt.package_root !== expected.packageRoot || receipt.package_source !== expected.source || receipt.package_version !== expected.version || receipt.build_fingerprint !== expected.build.build_fingerprint)) {
|
|
161
|
+
throw new Error(`Existing ${channel} runtime receipt does not match the freshly resolved package`);
|
|
162
|
+
}
|
|
163
|
+
return receipt;
|
|
164
|
+
}
|
|
165
|
+
function install(channel, args) {
|
|
166
|
+
const root = resolve(readOption(args, "--root") || runtimeRoot());
|
|
167
|
+
const shimDir = resolve(readOption(args, "--shim-dir") || join(root, "bin"));
|
|
168
|
+
const requestedSpec = readOption(args, "--package") || `${packageInfo.name}@${channel === "stable" ? "latest" : "next"}`;
|
|
169
|
+
const resolvedSpec = resolveSpec(requestedSpec, args.includes("--allow-local-package"));
|
|
170
|
+
const channelRoot = join(root, "installs", channel);
|
|
171
|
+
mkdirSync(channelRoot, { recursive: true });
|
|
172
|
+
const stagingRoot = mkdtempSync(join(channelRoot, ".staging-"));
|
|
173
|
+
let keepStaging = false;
|
|
174
|
+
try {
|
|
175
|
+
execFileSync("npm", [
|
|
176
|
+
"install",
|
|
177
|
+
"--prefix",
|
|
178
|
+
stagingRoot,
|
|
179
|
+
"--ignore-scripts",
|
|
180
|
+
"--no-audit",
|
|
181
|
+
"--no-fund",
|
|
182
|
+
"--package-lock=false",
|
|
183
|
+
resolvedSpec.installSpec
|
|
184
|
+
], { stdio: "ignore" });
|
|
185
|
+
const stagingPackageRoot = join(stagingRoot, "node_modules", ...packageInfo.name.split("/"));
|
|
186
|
+
const installed = JSON.parse(readFileSync(join(stagingPackageRoot, "package.json"), "utf8"));
|
|
187
|
+
if (installed.name !== packageInfo.name) throw new Error(`Installed unexpected package ${installed.name}`);
|
|
188
|
+
if (resolvedSpec.expectedVersion && installed.version !== resolvedSpec.expectedVersion) throw new Error(`Installed ${installed.version}, expected ${resolvedSpec.expectedVersion}`);
|
|
189
|
+
const build = validateBuild(stagingPackageRoot, installed);
|
|
190
|
+
const installId = `${installed.version}-${sha256(resolvedSpec.integrity).slice(0, 16)}`;
|
|
191
|
+
const finalRoot = join(channelRoot, installId);
|
|
192
|
+
const finalPackageRoot = join(finalRoot, "node_modules", ...packageInfo.name.split("/"));
|
|
193
|
+
const receiptPath = join(finalRoot, "lineage-runtime-receipt.json");
|
|
194
|
+
let receipt;
|
|
195
|
+
if (existsSync(finalRoot)) {
|
|
196
|
+
receipt = validateExistingReceipt(receiptPath, channel, {
|
|
197
|
+
build,
|
|
198
|
+
integrity: resolvedSpec.integrity,
|
|
199
|
+
packageRoot: finalPackageRoot,
|
|
200
|
+
source: resolvedSpec.source,
|
|
201
|
+
version: installed.version
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
const packageTree = packageTreeSha256(stagingPackageRoot);
|
|
205
|
+
renameSync(stagingRoot, finalRoot);
|
|
206
|
+
keepStaging = true;
|
|
207
|
+
receipt = {
|
|
208
|
+
build_fingerprint: build.build_fingerprint,
|
|
209
|
+
channel,
|
|
210
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
211
|
+
package_integrity: resolvedSpec.integrity,
|
|
212
|
+
package_name: installed.name,
|
|
213
|
+
package_root: finalPackageRoot,
|
|
214
|
+
package_source: resolvedSpec.source,
|
|
215
|
+
package_spec: resolvedSpec.requestedSpec,
|
|
216
|
+
package_tree_sha256: packageTree,
|
|
217
|
+
package_version: installed.version,
|
|
218
|
+
schema_version: lineageRuntimeInstallSchemaVersion
|
|
219
|
+
};
|
|
220
|
+
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}
|
|
221
|
+
`, { mode: 384 });
|
|
222
|
+
}
|
|
223
|
+
mkdirSync(shimDir, { recursive: true });
|
|
224
|
+
const shim = join(shimDir, channel === "stable" ? "lineage-stable" : "lineage-preview");
|
|
225
|
+
const entrypoint = join(finalPackageRoot, "dist", "cli", channel === "stable" ? "lineage.js" : "lineage-preview.js");
|
|
226
|
+
if (!existsSync(entrypoint)) throw new Error(`Installed package is missing ${entrypoint}`);
|
|
227
|
+
writeShim(shim, channel, receiptPath, entrypoint);
|
|
228
|
+
const serviceShim = join(shimDir, channel === "stable" ? "lineage-stable-service" : "lineage-preview-service");
|
|
229
|
+
const serviceEntrypoint = join(finalPackageRoot, "dist", "cli", "managed-service.js");
|
|
230
|
+
if (!existsSync(serviceEntrypoint)) throw new Error(`Installed package is missing ${serviceEntrypoint}`);
|
|
231
|
+
writeShim(serviceShim, channel, receiptPath, serviceEntrypoint);
|
|
232
|
+
const pointerDir = join(root, "channels");
|
|
233
|
+
mkdirSync(pointerDir, { recursive: true });
|
|
234
|
+
writeFileSync(join(pointerDir, `${channel}.json`), `${JSON.stringify({ channel, receipt_path: receiptPath, service_shim: serviceShim, shim }, null, 2)}
|
|
235
|
+
`, { mode: 384 });
|
|
236
|
+
return { ...receipt, receipt_path: receiptPath, service_shim: serviceShim, shim };
|
|
237
|
+
} finally {
|
|
238
|
+
if (!keepStaging) rmSync(stagingRoot, { force: true, recursive: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function status(args) {
|
|
242
|
+
const root = resolve(readOption(args, "--root") || runtimeRoot());
|
|
243
|
+
return Object.fromEntries(["stable", "preview"].map((channel) => {
|
|
244
|
+
const pointerPath = join(root, "channels", `${channel}.json`);
|
|
245
|
+
if (!existsSync(pointerPath)) return [channel, { installed: false }];
|
|
246
|
+
try {
|
|
247
|
+
const pointer = JSON.parse(readFileSync(pointerPath, "utf8"));
|
|
248
|
+
const receipt = validateExistingReceipt(pointer.receipt_path, channel);
|
|
249
|
+
if (!existsSync(pointer.shim) || !existsSync(pointer.service_shim)) throw new Error(`Existing ${channel} runtime shims are missing`);
|
|
250
|
+
return [channel, { installed: true, receipt, receipt_path: pointer.receipt_path, service_shim: pointer.service_shim, shim: pointer.shim }];
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return [channel, { error: error instanceof Error ? error.message : String(error), installed: false }];
|
|
253
|
+
}
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
function usage() {
|
|
257
|
+
return `lineage-channel ${packageInfo.version}
|
|
258
|
+
|
|
259
|
+
Usage:
|
|
260
|
+
lineage-channel install stable [--root <path>] [--shim-dir <path>] [--package <npm-spec>] [--json]
|
|
261
|
+
lineage-channel install preview [--root <path>] [--shim-dir <path>] [--package <npm-spec>] [--json]
|
|
262
|
+
lineage-channel status [--root <path>] [--json]
|
|
263
|
+
|
|
264
|
+
Stable and preview are installed into separate content-addressed roots. Dev is
|
|
265
|
+
checkout-only and is started with npm run lineage:dev -- <command>. Local
|
|
266
|
+
tarballs are refused unless --allow-local-package is supplied explicitly.`;
|
|
267
|
+
}
|
|
268
|
+
function print(value, json) {
|
|
269
|
+
if (json) console.log(JSON.stringify(value, null, 2));
|
|
270
|
+
else if (value && typeof value === "object" && "shim" in value) {
|
|
271
|
+
const installed = value;
|
|
272
|
+
console.log(`Installed Lineage ${installed.channel} ${installed.package_version}`);
|
|
273
|
+
console.log(`Launcher: ${installed.shim}`);
|
|
274
|
+
console.log(`Service manager: ${installed.service_shim}`);
|
|
275
|
+
} else console.log(JSON.stringify(value, null, 2));
|
|
276
|
+
}
|
|
277
|
+
function runLineageChannel(args = process.argv.slice(2)) {
|
|
278
|
+
const json = args.includes("--json");
|
|
279
|
+
try {
|
|
280
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
281
|
+
console.log(usage());
|
|
282
|
+
} else if (args.includes("--version") || args.includes("-v")) {
|
|
283
|
+
console.log(packageInfo.version);
|
|
284
|
+
} else if (args[0] === "install") {
|
|
285
|
+
print(install(parseChannel(args[1] || readOption(args, "--channel")), args.slice(2)), json);
|
|
286
|
+
} else if (args[0] === "status") {
|
|
287
|
+
print(status(args.slice(1)), json);
|
|
288
|
+
} else {
|
|
289
|
+
throw new Error(`Unknown lineage-channel command: ${args[0]}`);
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
293
|
+
if (json) console.error(JSON.stringify({ error: message, ok: false }, null, 2));
|
|
294
|
+
else console.error(`lineage-channel: ${message}`);
|
|
295
|
+
process.exitCode = 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) runLineageChannel();
|
|
299
|
+
export {
|
|
300
|
+
parseRegistryPackageMetadata,
|
|
301
|
+
runLineageChannel
|
|
302
|
+
};
|
|
303
|
+
//# sourceMappingURL=lineage-channel.js.map
|