@mean-weasel/lineage 0.1.12 → 0.1.13

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.13
4
+
5
+ - Isolate stable, preview, and checkout-only development code into attested channel-specific roots with runtime doctor and tamper detection.
6
+ - 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.
7
+ - Ship profile-aware managed service lifecycles, a three-runtime adversarial coexistence oracle, and an atomic version-locked Codex plugin release/install path.
8
+
3
9
  ## 0.1.12
4
10
 
5
11
  - 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`. Use `latest` for the stable dogfood or production install, and use `next` when you intentionally want the published preview candidate:
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
- npm install -g @mean-weasel/lineage@next
16
+ lineage-channel install stable
17
+ lineage-channel install preview
12
18
  ```
13
19
 
14
- The stable, preview, and dev channels are intended to coexist conceptually:
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
- - `latest` is the version you should trust for day-to-day dogfooding.
17
- - `next` is the published preview version to test before promotion.
18
- - `dev` is the local GitHub checkout or branch before publication.
19
- - `lineage` runs with stable runtime defaults.
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
- The package includes both CLI bridge bins for help and version checks:
32
+ Verify code identity before inspecting data or starting a service:
23
33
 
24
34
  ```bash
25
- lineage --help
26
- lineage --version
27
- lineage-dev --help
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
- Both bins can also run the bundled production server:
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-dev start
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
- By default, `lineage start` listens on `lineage.localhost:5197` and stores SQLite state in a stable Lineage runtime directory. `lineage-dev start` listens on `lineage-dev.localhost:5198` and uses a separate development SQLite file. Override those defaults with `--port`, `--host`, `--db`, or `LINEAGE_HOME`:
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 --port 6123 --db ~/.lineage/lineage.sqlite --asset-root /absolute/path/to/asset-repo
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 app code channels and local SQLite data as separate concerns:
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 is the daily-use channel.
56
- - `preview` is the published candidate channel, normally npm `next`.
57
- - `dev` is a GitHub checkout or local branch before publication.
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 should use its own default runtime directory and SQLite database.
60
- Do not point preview or dev code at the stable database unless you are doing an
61
- intentional, explicit test. Prefer copying a stable snapshot forward when a
62
- realistic preview/dev dataset is needed. `--db` and `LINEAGE_DB` remain the
63
- escape hatches for intentionally choosing a database.
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 profile bind --profile team-production --confirm-write --json
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; `profile bind` creates
95
- that identity for a new database or explicitly adopts a legacy database, and
96
- requires `--confirm-write`. Profile creation and database binding remain
97
- separate migration operations. A
98
- command without a profile remains compatible but is labeled `legacy-unbound`.
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
- `<resolved-database-path>.writer.lock`. The managed service holds that lease for its
102
- lifetime, so a second service is refused while it is running. Named-profile
103
- mutating CLI commands, including generated claim, heartbeat, link-child, and
104
- re-roll import handoffs, use an authenticated profile-bound route through that
105
- service instead of opening SQLite as a competing writer. The request and
106
- response identities must match the selected profile, environment, channel, and
107
- configured localhost origin; an unavailable or mismatched service fails closed
108
- without a direct-write fallback. A CLI data command may hold the same lease
109
- directly only while the service is offline. Lineage verifies lease ownership
110
- before opening a named-profile database for writes and configures SQLite with
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-dev db info --json
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 SHA when available, schema markers, and
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, and package contents before publishing. 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.
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,276 @@
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
+ renameSync,
15
+ rmSync,
16
+ writeFileSync
17
+ } from "node:fs";
18
+ import { homedir, platform } from "node:os";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ // src/shared/runtimeInfoTypes.ts
23
+ var lineageRuntimeBuildSchemaVersion = "lineage.runtime_build.v1";
24
+ var lineageRuntimeInstallSchemaVersion = "lineage.runtime_install.v1";
25
+
26
+ // src/cli/lineage-channel.ts
27
+ var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
28
+ var packageInfo = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
29
+ function sha256(value) {
30
+ return createHash("sha256").update(value).digest("hex");
31
+ }
32
+ function readOption(args2, name) {
33
+ const inline = args2.find((arg) => arg.startsWith(`${name}=`));
34
+ if (inline) return inline.slice(name.length + 1);
35
+ const index = args2.indexOf(name);
36
+ return index >= 0 ? args2[index + 1] : void 0;
37
+ }
38
+ function runtimeRoot() {
39
+ if (process.env.LINEAGE_RUNTIME_ROOT) return resolve(process.env.LINEAGE_RUNTIME_ROOT);
40
+ if (platform() === "darwin") return join(homedir(), "Library", "Application Support", "Lineage", "runtimes");
41
+ if (platform() === "win32") return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Lineage", "runtimes");
42
+ return join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "lineage", "runtimes");
43
+ }
44
+ function parseChannel(value) {
45
+ if (value === "stable" || value === "preview") return value;
46
+ throw new Error("Channel must be stable or preview; dev is checkout-only");
47
+ }
48
+ function packageTreeSha256(root) {
49
+ const hash = createHash("sha256");
50
+ const visit = (directory, relativeDirectory = "") => {
51
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
52
+ const relativePath = relativeDirectory ? join(relativeDirectory, entry.name) : entry.name;
53
+ const path = join(directory, entry.name);
54
+ hash.update(relativePath.replaceAll("\\", "/"));
55
+ hash.update("\0");
56
+ if (entry.isDirectory()) {
57
+ hash.update("directory\0");
58
+ visit(path, relativePath);
59
+ } else if (entry.isSymbolicLink()) {
60
+ hash.update("symlink\0");
61
+ hash.update(readlinkSync(path));
62
+ } else if (entry.isFile()) {
63
+ hash.update("file\0");
64
+ hash.update(readFileSync(path));
65
+ } else {
66
+ hash.update("other\0");
67
+ }
68
+ hash.update("\0");
69
+ }
70
+ };
71
+ visit(root);
72
+ return hash.digest("hex");
73
+ }
74
+ function expectedBuildFingerprint(build) {
75
+ return sha256(JSON.stringify({
76
+ package_name: build.package_name,
77
+ package_version: build.package_version,
78
+ schema_version: build.schema_version,
79
+ source_dirty: build.source_dirty,
80
+ source_fingerprint: build.source_fingerprint,
81
+ source_git_sha: build.source_git_sha
82
+ }));
83
+ }
84
+ function validateBuild(root, installed) {
85
+ const path = join(root, "dist", "runtime-build.json");
86
+ const build = JSON.parse(readFileSync(path, "utf8"));
87
+ if (build.schema_version !== lineageRuntimeBuildSchemaVersion) throw new Error(`Unsupported build identity schema in ${path}`);
88
+ if (build.package_name !== installed.name || build.package_version !== installed.version) throw new Error("Embedded build identity does not match installed package.json");
89
+ if (!/^[a-f0-9]{40}$/i.test(build.source_git_sha)) throw new Error("Embedded build Git revision is invalid");
90
+ if (!/^[a-f0-9]{64}$/i.test(build.source_fingerprint)) throw new Error("Embedded source fingerprint is invalid");
91
+ if (build.build_fingerprint !== expectedBuildFingerprint(build)) throw new Error("Embedded build fingerprint does not match its contents");
92
+ if (build.source_dirty) throw new Error("Refusing stable/preview installation of a dirty-source build");
93
+ return build;
94
+ }
95
+ function resolveSpec(spec, allowLocalPackage) {
96
+ const localPath = resolve(spec.replace(/^file:/, ""));
97
+ if (existsSync(localPath)) {
98
+ if (!allowLocalPackage) {
99
+ throw new Error("Local package paths require --allow-local-package; normal stable/preview installs must resolve from the npm registry");
100
+ }
101
+ return {
102
+ installSpec: localPath,
103
+ integrity: `sha512-${createHash("sha512").update(readFileSync(localPath)).digest("base64")}`,
104
+ requestedSpec: spec,
105
+ source: "local"
106
+ };
107
+ }
108
+ const metadata = JSON.parse(execFileSync("npm", ["view", spec, "version", "dist.integrity", "--json"], { encoding: "utf8" }));
109
+ if (!metadata.version || !metadata.dist?.integrity) throw new Error(`npm metadata for ${spec} did not include exact version and integrity`);
110
+ const name = spec.startsWith("@") ? spec.slice(0, spec.indexOf("@", 1)) : spec.split("@")[0];
111
+ return {
112
+ installSpec: `${name}@${metadata.version}`,
113
+ integrity: metadata.dist.integrity,
114
+ expectedVersion: metadata.version,
115
+ requestedSpec: spec,
116
+ source: "registry"
117
+ };
118
+ }
119
+ function shellQuote(value) {
120
+ return `'${value.replaceAll("'", "'\\''")}'`;
121
+ }
122
+ function writeShim(path, channel, receiptPath, entrypoint) {
123
+ const tempPath = `${path}.tmp-${process.pid}`;
124
+ const script = `#!/bin/sh
125
+ LINEAGE_RUNTIME_RECEIPT=${shellQuote(receiptPath)} LINEAGE_RELEASE_CHANNEL=${shellQuote(channel)} exec ${shellQuote(process.execPath)} ${shellQuote(entrypoint)} "$@"
126
+ `;
127
+ writeFileSync(tempPath, script, { mode: 493 });
128
+ chmodSync(tempPath, 493);
129
+ renameSync(tempPath, path);
130
+ }
131
+ function validateExistingReceipt(receiptPath, channel, expected) {
132
+ const receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
133
+ if (receipt.schema_version !== lineageRuntimeInstallSchemaVersion || receipt.channel !== channel) throw new Error(`Invalid existing ${channel} runtime receipt`);
134
+ const installed = JSON.parse(readFileSync(join(receipt.package_root, "package.json"), "utf8"));
135
+ const build = validateBuild(receipt.package_root, installed);
136
+ if (receipt.package_name !== installed.name || receipt.package_version !== installed.version) throw new Error(`Existing ${channel} receipt does not match package.json`);
137
+ if (receipt.build_fingerprint !== build.build_fingerprint) throw new Error(`Existing ${channel} receipt does not match embedded build identity`);
138
+ if (packageTreeSha256(receipt.package_root) !== receipt.package_tree_sha256) throw new Error(`Existing ${channel} runtime package tree has changed`);
139
+ 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)) {
140
+ throw new Error(`Existing ${channel} runtime receipt does not match the freshly resolved package`);
141
+ }
142
+ return receipt;
143
+ }
144
+ function install(channel, args2) {
145
+ const root = resolve(readOption(args2, "--root") || runtimeRoot());
146
+ const shimDir = resolve(readOption(args2, "--shim-dir") || join(root, "bin"));
147
+ const requestedSpec = readOption(args2, "--package") || `${packageInfo.name}@${channel === "stable" ? "latest" : "next"}`;
148
+ const resolvedSpec = resolveSpec(requestedSpec, args2.includes("--allow-local-package"));
149
+ const channelRoot = join(root, "installs", channel);
150
+ mkdirSync(channelRoot, { recursive: true });
151
+ const stagingRoot = mkdtempSync(join(channelRoot, ".staging-"));
152
+ let keepStaging = false;
153
+ try {
154
+ execFileSync("npm", [
155
+ "install",
156
+ "--prefix",
157
+ stagingRoot,
158
+ "--ignore-scripts",
159
+ "--no-audit",
160
+ "--no-fund",
161
+ "--package-lock=false",
162
+ resolvedSpec.installSpec
163
+ ], { stdio: "ignore" });
164
+ const stagingPackageRoot = join(stagingRoot, "node_modules", ...packageInfo.name.split("/"));
165
+ const installed = JSON.parse(readFileSync(join(stagingPackageRoot, "package.json"), "utf8"));
166
+ if (installed.name !== packageInfo.name) throw new Error(`Installed unexpected package ${installed.name}`);
167
+ if (resolvedSpec.expectedVersion && installed.version !== resolvedSpec.expectedVersion) throw new Error(`Installed ${installed.version}, expected ${resolvedSpec.expectedVersion}`);
168
+ const build = validateBuild(stagingPackageRoot, installed);
169
+ const installId = `${installed.version}-${sha256(resolvedSpec.integrity).slice(0, 16)}`;
170
+ const finalRoot = join(channelRoot, installId);
171
+ const finalPackageRoot = join(finalRoot, "node_modules", ...packageInfo.name.split("/"));
172
+ const receiptPath = join(finalRoot, "lineage-runtime-receipt.json");
173
+ let receipt;
174
+ if (existsSync(finalRoot)) {
175
+ receipt = validateExistingReceipt(receiptPath, channel, {
176
+ build,
177
+ integrity: resolvedSpec.integrity,
178
+ packageRoot: finalPackageRoot,
179
+ source: resolvedSpec.source,
180
+ version: installed.version
181
+ });
182
+ } else {
183
+ const packageTree = packageTreeSha256(stagingPackageRoot);
184
+ renameSync(stagingRoot, finalRoot);
185
+ keepStaging = true;
186
+ receipt = {
187
+ build_fingerprint: build.build_fingerprint,
188
+ channel,
189
+ installed_at: (/* @__PURE__ */ new Date()).toISOString(),
190
+ package_integrity: resolvedSpec.integrity,
191
+ package_name: installed.name,
192
+ package_root: finalPackageRoot,
193
+ package_source: resolvedSpec.source,
194
+ package_spec: resolvedSpec.requestedSpec,
195
+ package_tree_sha256: packageTree,
196
+ package_version: installed.version,
197
+ schema_version: lineageRuntimeInstallSchemaVersion
198
+ };
199
+ writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}
200
+ `, { mode: 384 });
201
+ }
202
+ mkdirSync(shimDir, { recursive: true });
203
+ const shim = join(shimDir, channel === "stable" ? "lineage-stable" : "lineage-preview");
204
+ const entrypoint = join(finalPackageRoot, "dist", "cli", channel === "stable" ? "lineage.js" : "lineage-preview.js");
205
+ if (!existsSync(entrypoint)) throw new Error(`Installed package is missing ${entrypoint}`);
206
+ writeShim(shim, channel, receiptPath, entrypoint);
207
+ const serviceShim = join(shimDir, channel === "stable" ? "lineage-stable-service" : "lineage-preview-service");
208
+ const serviceEntrypoint = join(finalPackageRoot, "dist", "cli", "managed-service.js");
209
+ if (!existsSync(serviceEntrypoint)) throw new Error(`Installed package is missing ${serviceEntrypoint}`);
210
+ writeShim(serviceShim, channel, receiptPath, serviceEntrypoint);
211
+ const pointerDir = join(root, "channels");
212
+ mkdirSync(pointerDir, { recursive: true });
213
+ writeFileSync(join(pointerDir, `${channel}.json`), `${JSON.stringify({ channel, receipt_path: receiptPath, service_shim: serviceShim, shim }, null, 2)}
214
+ `, { mode: 384 });
215
+ return { ...receipt, receipt_path: receiptPath, service_shim: serviceShim, shim };
216
+ } finally {
217
+ if (!keepStaging) rmSync(stagingRoot, { force: true, recursive: true });
218
+ }
219
+ }
220
+ function status(args2) {
221
+ const root = resolve(readOption(args2, "--root") || runtimeRoot());
222
+ return Object.fromEntries(["stable", "preview"].map((channel) => {
223
+ const pointerPath = join(root, "channels", `${channel}.json`);
224
+ if (!existsSync(pointerPath)) return [channel, { installed: false }];
225
+ try {
226
+ const pointer = JSON.parse(readFileSync(pointerPath, "utf8"));
227
+ const receipt = validateExistingReceipt(pointer.receipt_path, channel);
228
+ if (!existsSync(pointer.shim) || !existsSync(pointer.service_shim)) throw new Error(`Existing ${channel} runtime shims are missing`);
229
+ return [channel, { installed: true, receipt, receipt_path: pointer.receipt_path, service_shim: pointer.service_shim, shim: pointer.shim }];
230
+ } catch (error) {
231
+ return [channel, { error: error instanceof Error ? error.message : String(error), installed: false }];
232
+ }
233
+ }));
234
+ }
235
+ function usage() {
236
+ return `lineage-channel ${packageInfo.version}
237
+
238
+ Usage:
239
+ lineage-channel install stable [--root <path>] [--shim-dir <path>] [--package <npm-spec>] [--json]
240
+ lineage-channel install preview [--root <path>] [--shim-dir <path>] [--package <npm-spec>] [--json]
241
+ lineage-channel status [--root <path>] [--json]
242
+
243
+ Stable and preview are installed into separate content-addressed roots. Dev is
244
+ checkout-only and is started with npm run lineage:dev -- <command>. Local
245
+ tarballs are refused unless --allow-local-package is supplied explicitly.`;
246
+ }
247
+ function print(value, json2) {
248
+ if (json2) console.log(JSON.stringify(value, null, 2));
249
+ else if (value && typeof value === "object" && "shim" in value) {
250
+ const installed = value;
251
+ console.log(`Installed Lineage ${installed.channel} ${installed.package_version}`);
252
+ console.log(`Launcher: ${installed.shim}`);
253
+ console.log(`Service manager: ${installed.service_shim}`);
254
+ } else console.log(JSON.stringify(value, null, 2));
255
+ }
256
+ var args = process.argv.slice(2);
257
+ var json = args.includes("--json");
258
+ try {
259
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
260
+ console.log(usage());
261
+ } else if (args.includes("--version") || args.includes("-v")) {
262
+ console.log(packageInfo.version);
263
+ } else if (args[0] === "install") {
264
+ print(install(parseChannel(args[1] || readOption(args, "--channel")), args.slice(2)), json);
265
+ } else if (args[0] === "status") {
266
+ print(status(args.slice(1)), json);
267
+ } else {
268
+ throw new Error(`Unknown lineage-channel command: ${args[0]}`);
269
+ }
270
+ } catch (error) {
271
+ const message = error instanceof Error ? error.message : String(error);
272
+ if (json) console.error(JSON.stringify({ error: message, ok: false }, null, 2));
273
+ else console.error(`lineage-channel: ${message}`);
274
+ process.exitCode = 1;
275
+ }
276
+ //# sourceMappingURL=lineage-channel.js.map