@mean-weasel/lineage 0.1.11 → 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,17 @@
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
+
9
+ ## 0.1.12
10
+
11
+ - Add named runtime profiles that bind environment, SQLite, media root, service origin, and expected runtime identity without allowing silent path drift.
12
+ - Enforce a single cross-process writer lease per profile and route profile-bound mutations through the authenticated managed service while keeping inspections read-only.
13
+ - Add opt-in `lineage.selection_packet.v2` exports whose stable semantic identity binds ordered selections to their current attempts and checksums for durable GrowthOps handoff receipts.
14
+
3
15
  ## 0.1.11
4
16
 
5
17
  - Add `--asset-root` / `LINEAGE_ASSET_ROOT` so installed Lineage packages can use external project catalogs and local media independently from the SQLite path.
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,27 +71,141 @@ 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:
76
+
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.
80
+
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.
87
+
88
+ For repeatable or multi-session work, prefer a named profile over those legacy
89
+ path defaults. A profile is an immutable identity contract for one Lineage
90
+ service and its data:
91
+
92
+ ```json
93
+ {
94
+ "schema_version": "lineage.profile.v1",
95
+ "profile_id": "team-production",
96
+ "environment": "production",
97
+ "expected_runtime": {
98
+ "channel": "stable",
99
+ "code_origin": "package",
100
+ "code_fingerprint": "<64-character fingerprint from runtime doctor>"
101
+ },
102
+ "database_path": "./lineage.sqlite",
103
+ "asset_root": "./media",
104
+ "service_origin": "http://lineage.localhost:5197"
105
+ }
106
+ ```
107
+
108
+ Named profiles live at `$LINEAGE_PROFILE_ROOT/<profile-id>/profile.json` by
109
+ default, or can be selected with an explicit manifest path. Relative database
110
+ and asset paths resolve from the manifest directory. Inspect a profile without
111
+ creating or migrating anything:
112
+
113
+ ```bash
114
+ lineage-stable profile doctor --profile team-production --json
115
+ lineage-stable start --profile team-production
116
+ ```
54
117
 
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.
118
+ `LINEAGE_PROFILE` is equivalent to `--profile`. Profile commands reject direct
119
+ `--db` and `--asset-root` overrides, and a dev or preview runtime refuses to
120
+ open a production profile. Doctor also requires the existing SQLite database
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
+ ```
58
144
 
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.
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.
164
+
165
+ A named profile has one cross-process writer lease at
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.
64
176
 
65
177
  Check the active runtime before making changes:
66
178
 
67
179
  ```bash
68
- lineage db info --json
69
- 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
183
+ ```
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
70
197
  ```
71
198
 
72
- The app also shows channel, version, Git SHA when available, and SQLite path in
73
- Settings.
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
+
206
+ The app shows a persistent environment/profile badge, and Settings includes the
207
+ profile binding, code origin/root/fingerprint, channel, version, embedded Git
208
+ revision, schema markers, and SQLite path.
74
209
 
75
210
  ## Agent Claims
76
211
 
@@ -108,6 +243,22 @@ Lineage only exports the packet. GrowthOps or another downstream tool remains
108
243
  responsible for importing it, creating posts, checking media readiness, preparing
109
244
  public URLs, scheduling, and recording placement receipts.
110
245
 
246
+ Version 1 remains the default for compatibility. Opt in to the current-attempt-
247
+ bound contract with `--schema v2`:
248
+
249
+ ```bash
250
+ lineage selection packet --project demo-project --schema v2 \
251
+ --out ./lineage-selection-packet-v2.json --json
252
+ ```
253
+
254
+ The `lineage.selection_packet.v2` packet requires a verifiable current attempt
255
+ and lowercase SHA-256 checksum for every selected asset. Its full
256
+ `identity_sha256` and derived `packet_id` bind only the canonical semantic
257
+ selection: project/product, workspace/root, context, ordered selected assets and
258
+ their current attempts, and stable diagnostics. Timestamps, local paths, storage
259
+ details, source metadata, generation job IDs, and human-readable warning/error
260
+ text stay in the envelope but do not change v2 identity.
261
+
111
262
  Keep the claim fresh and pass it to mutating commands:
112
263
 
113
264
  ```bash
@@ -173,15 +324,27 @@ whose version or `lineage.version` does not exactly match the resolved Lineage
173
324
  package version. The plugin artifact and installer package are released from
174
325
  this public repository.
175
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
+
176
334
  ## Local Development
177
335
 
178
336
  ```bash
179
337
  npm ci
180
338
  npm run dev
181
339
  npm run ci
340
+ npm run runtime:oracle
182
341
  ```
183
342
 
184
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.
185
348
 
186
349
  ## Command Shortcuts
187
350
 
@@ -191,7 +354,9 @@ verification commands. Run `make` or `make help` to list the available targets.
191
354
  ```bash
192
355
  make install-prod
193
356
  make install-plugin-prod
194
- 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
195
360
  make check
196
361
  make smoke
197
362
  ```
@@ -218,7 +383,7 @@ npm run release:dry-run -- --tag latest
218
383
  npm run release:latest
219
384
  ```
220
385
 
221
- 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.
222
387
 
223
388
  Use the Release workflow operations this way:
224
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