@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.
@@ -1,19 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/lineageCli.ts
4
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
4
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
5
+ import { randomUUID as randomUUID4 } from "node:crypto";
5
6
  import { homedir as homedir2, platform as platform2 } from "node:os";
6
- import { dirname as dirname5, join as join10, resolve as resolve8 } from "node:path";
7
+ import { dirname as dirname6, join as join10, resolve as resolve8 } from "node:path";
7
8
  import { spawn } from "node:child_process";
8
- import { fileURLToPath as fileURLToPath2 } from "node:url";
9
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
9
10
 
10
11
  // src/server/agentClaims.ts
11
- import { createHash as createHash3, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
12
+ import { createHash as createHash4, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
12
13
 
13
14
  // src/server/assetLineageDb.ts
14
15
  import { createRequire as createRequire2 } from "node:module";
15
16
  import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
16
- import { join as join6, resolve as resolve5 } from "node:path";
17
+ import { join as join6 } from "node:path";
17
18
 
18
19
  // src/server/assetCore.ts
19
20
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
@@ -587,15 +588,35 @@ function validateProject(project = defaultProject) {
587
588
  return { project: catalog.project, product: catalog.product, catalogPath: resolvedCatalogPath(project), default_bucket: catalog.default_bucket, default_region: catalog.default_region, asset_count: catalog.assets.length };
588
589
  }
589
590
 
591
+ // src/server/profileWriterLease.ts
592
+ import { createHash as createHash3, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
593
+ import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
594
+ import { dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
595
+
590
596
  // src/server/lineageProfiles.ts
597
+ import { createHash as createHash2, randomUUID } from "node:crypto";
591
598
  import { createRequire } from "node:module";
592
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
599
+ import {
600
+ chmodSync,
601
+ constants as fsConstants,
602
+ copyFileSync as copyFileSync2,
603
+ existsSync as existsSync4,
604
+ linkSync,
605
+ mkdirSync as mkdirSync3,
606
+ readFileSync as readFileSync3,
607
+ realpathSync,
608
+ rmSync,
609
+ statSync as statSync3,
610
+ writeFileSync as writeFileSync2
611
+ } from "node:fs";
593
612
  import { homedir, platform } from "node:os";
594
- import { dirname as dirname3, isAbsolute, join as join4, resolve as resolve3 } from "node:path";
613
+ import { dirname as dirname3, isAbsolute, join as join4, relative as relative2, resolve as resolve3 } from "node:path";
595
614
 
596
615
  // src/shared/lineageProfileTypes.ts
597
616
  var lineageProfileSchemaVersion = "lineage.profile.v1";
598
617
  var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
618
+ var lineageProfileCloneReceiptSchemaVersion = "lineage.profile_clone_receipt.v1";
619
+ var lineageProfileAssetsCloneReceiptSchemaVersion = "lineage.profile_assets_clone_receipt.v1";
599
620
 
600
621
  // src/server/lineageProfiles.ts
601
622
  var require2 = createRequire(import.meta.url);
@@ -647,6 +668,16 @@ function validateChannel(value) {
647
668
  if (value === "stable" || value === "preview" || value === "dev") return value;
648
669
  throw new Error(`Invalid expected runtime channel: ${String(value)}`);
649
670
  }
671
+ function validateCodeOrigin(value) {
672
+ if (value === void 0) return void 0;
673
+ if (value === "checkout" || value === "package") return value;
674
+ throw new Error(`Invalid expected runtime code_origin: ${String(value)}`);
675
+ }
676
+ function validateFingerprint(value, field) {
677
+ if (value === void 0) return void 0;
678
+ if (!/^[a-f0-9]{64}$/i.test(value)) throw new Error(`Profile ${field} must be a 64-character SHA-256 fingerprint`);
679
+ return value.toLowerCase();
680
+ }
650
681
  function resolveManifestPath(manifestPath, value) {
651
682
  return resolve3(dirname3(manifestPath), value);
652
683
  }
@@ -662,9 +693,6 @@ function validateServiceOrigin(value) {
662
693
  if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
663
694
  throw new Error("Profile service_origin must contain only scheme, host, and port");
664
695
  }
665
- const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
666
- const isLocal = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
667
- if (!isLocal) throw new Error("Profile service_origin must use a loopback or localhost host");
668
696
  return parsed.origin;
669
697
  }
670
698
  function resolveLineageProfile(selector) {
@@ -692,6 +720,8 @@ function resolveLineageProfile(selector) {
692
720
  const expected = expectedRaw;
693
721
  const expectedGitSha = expected ? optionalString(expected, "git_sha") : void 0;
694
722
  const expectedVersion = expected ? optionalString(expected, "version") : void 0;
723
+ const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
724
+ const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
695
725
  const migrationsRaw = record.required_schema_migrations;
696
726
  if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
697
727
  throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
@@ -708,6 +738,8 @@ function resolveLineageProfile(selector) {
708
738
  ...expected ? {
709
739
  expected_runtime: {
710
740
  ...expectedChannel ? { channel: expectedChannel } : {},
741
+ ...expectedCodeFingerprint ? { code_fingerprint: expectedCodeFingerprint } : {},
742
+ ...expectedCodeOrigin ? { code_origin: expectedCodeOrigin } : {},
711
743
  ...expectedGitSha ? { git_sha: expectedGitSha } : {},
712
744
  ...expectedVersion ? { version: expectedVersion } : {}
713
745
  }
@@ -717,48 +749,30 @@ function resolveLineageProfile(selector) {
717
749
  schema_version: lineageProfileSchemaVersion,
718
750
  service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
719
751
  };
720
- return { ...manifest, manifest_path: manifestPath };
752
+ return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
721
753
  }
722
- function bindLineageProfileDatabase(profile, confirmWrite) {
723
- const result = {
724
- database_path: profile.database_path,
754
+ function lineageProfileFingerprint(profile) {
755
+ return createHash2("sha256").update(JSON.stringify({
756
+ asset_root: resolve3(profile.asset_root),
757
+ database_path: resolve3(profile.database_path),
725
758
  environment: profile.environment,
726
- ok: true,
727
759
  profile_id: profile.profile_id,
728
- schema_version: "lineage.profile_bind.v1"
729
- };
730
- if (!confirmWrite) return { ...result, dryRun: true };
731
- mkdirSync3(dirname3(profile.database_path), { recursive: true });
732
- const { DatabaseSync } = require2("node:sqlite");
733
- const database = new DatabaseSync(profile.database_path);
734
- try {
735
- database.exec("BEGIN IMMEDIATE");
736
- try {
737
- database.exec(`
738
- create table if not exists lineage_profile_identity (
739
- profile_id text primary key,
740
- environment text not null,
741
- bound_at text not null
742
- )
743
- `);
744
- const rows = database.prepare("select profile_id, environment, bound_at from lineage_profile_identity").all();
745
- if (rows.length > 0) {
746
- if (rows.length !== 1 || rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
747
- throw new Error(`Database ${profile.database_path} is already bound to a different Lineage profile identity`);
748
- }
749
- database.exec("COMMIT");
750
- return { ...result, already_bound: true, bound_at: rows[0].bound_at };
751
- }
752
- const boundAt = (/* @__PURE__ */ new Date()).toISOString();
753
- database.prepare("insert into lineage_profile_identity (profile_id, environment, bound_at) values (?, ?, ?)").run(profile.profile_id, profile.environment, boundAt);
754
- database.exec("COMMIT");
755
- return { ...result, bound_at: boundAt };
756
- } catch (error) {
757
- database.exec("ROLLBACK");
758
- throw error;
759
- }
760
- } finally {
761
- database.close();
760
+ schema_version: profile.schema_version,
761
+ service_origin: profile.service_origin
762
+ })).digest("hex");
763
+ }
764
+ function assertProfileRuntimePin(profile, runtime) {
765
+ if (!profile.expected_runtime?.code_fingerprint || !profile.expected_runtime.code_origin) {
766
+ throw new Error(`Profile ${profile.profile_id} must pin expected_runtime.code_fingerprint and expected_runtime.code_origin before binding or writing`);
767
+ }
768
+ if (!runtime?.code?.verified) {
769
+ throw new Error(`Profile ${profile.profile_id} requires a verified runtime code identity before binding or writing`);
770
+ }
771
+ if (runtime.code.fingerprint !== profile.expected_runtime.code_fingerprint) {
772
+ throw new Error(`Profile ${profile.profile_id} expects code fingerprint ${profile.expected_runtime.code_fingerprint}, found ${runtime.code.fingerprint}`);
773
+ }
774
+ if (runtime.code.origin !== profile.expected_runtime.code_origin) {
775
+ throw new Error(`Profile ${profile.profile_id} expects code origin ${profile.expected_runtime.code_origin}, found ${runtime.code.origin}`);
762
776
  }
763
777
  }
764
778
  function assertProfileChannel(profile, channel) {
@@ -772,12 +786,9 @@ function assertProfileChannel(profile, channel) {
772
786
  function tableExists(database, table) {
773
787
  return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
774
788
  }
775
- function gitShasMatch(expected, actual) {
776
- if (!actual) return false;
777
- const normalizedExpected = expected.toLowerCase();
778
- const normalizedActual = actual.toLowerCase();
779
- const shorterLength = Math.min(normalizedExpected.length, normalizedActual.length);
780
- return shorterLength >= 12 && normalizedExpected.slice(0, shorterLength) === normalizedActual.slice(0, shorterLength);
789
+ function tableColumns(database, table) {
790
+ if (!tableExists(database, table)) return /* @__PURE__ */ new Set();
791
+ return new Set(database.prepare(`pragma table_info(${table})`).all().map((row) => row.name));
781
792
  }
782
793
  function inspectDatabase(profile) {
783
794
  const result = {
@@ -791,11 +802,14 @@ function inspectDatabase(profile) {
791
802
  const database = new DatabaseSync(profile.database_path, { readOnly: true });
792
803
  try {
793
804
  if (tableExists(database, "lineage_profile_identity")) {
794
- const rows = database.prepare("select profile_id, environment, bound_at from lineage_profile_identity").all();
805
+ const columns = tableColumns(database, "lineage_profile_identity");
806
+ const fingerprintExpression = columns.has("profile_fingerprint") ? "profile_fingerprint" : "'' as profile_fingerprint";
807
+ const rows = database.prepare(`select profile_id, environment, bound_at, ${fingerprintExpression} from lineage_profile_identity`).all();
795
808
  if (rows.length === 1) {
796
809
  const identity = {
797
810
  bound_at: typeof rows[0].bound_at === "string" ? rows[0].bound_at : void 0,
798
811
  environment: String(rows[0].environment),
812
+ profile_fingerprint: typeof rows[0].profile_fingerprint === "string" ? rows[0].profile_fingerprint : "",
799
813
  profile_id: String(rows[0].profile_id)
800
814
  };
801
815
  result.identity = identity;
@@ -819,7 +833,14 @@ function doctorLineageProfile(selector, runtime) {
819
833
  const result = {
820
834
  checks,
821
835
  ok: false,
822
- runtime: { channel: runtime.channel, git_sha: runtime.gitSha, version: runtime.version },
836
+ runtime: {
837
+ channel: runtime.channel,
838
+ code_fingerprint: runtime.code?.fingerprint,
839
+ code_origin: runtime.code?.origin,
840
+ code_verified: runtime.code?.verified,
841
+ git_sha: runtime.gitSha,
842
+ version: runtime.version
843
+ },
823
844
  schema_version: lineageProfileDoctorSchemaVersion
824
845
  };
825
846
  let profile;
@@ -838,12 +859,18 @@ function doctorLineageProfile(selector, runtime) {
838
859
  } catch (error) {
839
860
  checks.push({ id: "runtime_channel", message: error instanceof Error ? error.message : String(error), status: "fail" });
840
861
  }
862
+ try {
863
+ assertProfileRuntimePin(profile, runtime);
864
+ checks.push({ id: "runtime_code", message: `Verified ${runtime.code.origin} code ${runtime.code.fingerprint}`, status: "pass" });
865
+ } catch (error) {
866
+ checks.push({ id: "runtime_code", message: error instanceof Error ? error.message : String(error), status: "fail" });
867
+ }
841
868
  if (profile.expected_runtime?.version && profile.expected_runtime.version !== runtime.version) {
842
869
  checks.push({ id: "runtime_version", message: `Expected version ${profile.expected_runtime.version}, found ${runtime.version}`, status: "fail" });
843
870
  } else {
844
871
  checks.push({ id: "runtime_version", message: `Runtime version ${runtime.version}`, status: "pass" });
845
872
  }
846
- if (profile.expected_runtime?.git_sha && !gitShasMatch(profile.expected_runtime.git_sha, runtime.gitSha)) {
873
+ if (profile.expected_runtime?.git_sha && profile.expected_runtime.git_sha !== runtime.gitSha) {
847
874
  checks.push({ id: "runtime_git_sha", message: `Expected Git SHA ${profile.expected_runtime.git_sha}, found ${runtime.gitSha || "unavailable"}`, status: "fail" });
848
875
  } else if (profile.expected_runtime?.git_sha) {
849
876
  checks.push({ id: "runtime_git_sha", message: `Git SHA ${runtime.gitSha}`, status: "pass" });
@@ -866,10 +893,10 @@ function doctorLineageProfile(selector, runtime) {
866
893
  if (database.error) checks.push({ id: "database_read", message: database.error, status: "fail" });
867
894
  if (!database.identity) {
868
895
  checks.push({ id: "database_identity", message: "Database is not bound to a Lineage profile", status: "fail" });
869
- } else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment) {
896
+ } else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment || database.identity.profile_fingerprint !== profile.profile_fingerprint) {
870
897
  checks.push({
871
898
  id: "database_identity",
872
- message: `Database identity ${database.identity.profile_id}/${database.identity.environment} does not match ${profile.profile_id}/${profile.environment}`,
899
+ message: `Database identity ${database.identity.profile_id}/${database.identity.environment}/${database.identity.profile_fingerprint || "no-fingerprint"} does not match ${profile.profile_id}/${profile.environment}/${profile.profile_fingerprint}`,
873
900
  status: "fail"
874
901
  });
875
902
  } else {
@@ -892,6 +919,7 @@ function runtimeProfileIdentity(channel) {
892
919
  return {
893
920
  bound: true,
894
921
  environment,
922
+ fingerprint: process.env.LINEAGE_PROFILE_FINGERPRINT,
895
923
  id,
896
924
  manifest_path: process.env.LINEAGE_PROFILE_MANIFEST,
897
925
  service_origin: process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
@@ -901,31 +929,308 @@ function runtimeProfileIdentity(channel) {
901
929
  bound: false,
902
930
  environment: channelEnvironment(channel),
903
931
  id: "legacy-unbound",
904
- warning: id || environment || process.env.LINEAGE_PROFILE_MANIFEST ? "Invalid unbound runtime: derived profile identity was supplied without a resolved LINEAGE_PROFILE selector." : "Legacy unbound runtime: database and asset paths are not protected by a named profile."
932
+ warning: id || environment || process.env.LINEAGE_PROFILE_FINGERPRINT || process.env.LINEAGE_PROFILE_MANIFEST ? "Invalid unbound runtime: derived profile identity was supplied without a resolved LINEAGE_PROFILE selector." : "Legacy unbound runtime: database and asset paths are not protected by a named profile."
905
933
  };
906
934
  }
907
935
  function assertUnselectedDatabaseIsUnbound(runtime) {
908
- if (runtime.database.exists && runtime.database.error) {
909
- throw new Error(
910
- `Database ${runtime.database.path} identity could not be verified: ${runtime.database.error}; refusing unselected access`
911
- );
912
- }
913
- if (runtime.schema.profile_identity_rows !== void 0 && runtime.schema.profile_identity_rows !== 1) {
914
- throw new Error(
915
- `Database ${runtime.database.path} has invalid Lineage profile identity row count ${runtime.schema.profile_identity_rows}; refusing unselected access`
916
- );
917
- }
918
936
  if (!runtime.schema.profile_id) return;
919
937
  const environment = runtime.schema.profile_environment || "unknown";
920
938
  throw new Error(
921
939
  `Database ${runtime.database.path} is bound to Lineage profile ${runtime.schema.profile_id}/${environment}; select that profile with --profile`
922
940
  );
923
941
  }
942
+ function ensureProfileIdentityTable(database) {
943
+ if (!tableExists(database, "lineage_profile_identity")) {
944
+ database.exec(`
945
+ create table lineage_profile_identity (
946
+ profile_id text primary key,
947
+ environment text not null,
948
+ profile_fingerprint text not null,
949
+ bound_at text not null
950
+ )
951
+ `);
952
+ return;
953
+ }
954
+ const columns = tableColumns(database, "lineage_profile_identity");
955
+ if (!columns.has("profile_id") || !columns.has("environment") || !columns.has("bound_at")) {
956
+ throw new Error("Existing lineage_profile_identity table has an unsupported shape");
957
+ }
958
+ if (!columns.has("profile_fingerprint")) {
959
+ database.exec("alter table lineage_profile_identity add column profile_fingerprint text not null default ''");
960
+ }
961
+ }
962
+ function assertRequiredMigrations(database, profile) {
963
+ const required = profile.required_schema_migrations || [];
964
+ if (required.length === 0) return;
965
+ if (!tableExists(database, "lineage_schema_migrations")) {
966
+ throw new Error(`Database is missing required schema migrations: ${required.join(", ")}`);
967
+ }
968
+ const available = new Set(database.prepare("select key from lineage_schema_migrations").all().map((row) => row.key));
969
+ const missing = required.filter((key) => !available.has(key));
970
+ if (missing.length > 0) throw new Error(`Database is missing required schema migrations: ${missing.join(", ")}`);
971
+ }
972
+ function profileIdentity(profile) {
973
+ return {
974
+ bound_at: (/* @__PURE__ */ new Date()).toISOString(),
975
+ environment: profile.environment,
976
+ profile_fingerprint: profile.profile_fingerprint,
977
+ profile_id: profile.profile_id
978
+ };
979
+ }
980
+ function bindOpenDatabase(database, profile, replace) {
981
+ database.exec("begin immediate");
982
+ try {
983
+ ensureProfileIdentityTable(database);
984
+ assertRequiredMigrations(database, profile);
985
+ const rows = database.prepare("select profile_id, environment, profile_fingerprint, bound_at from lineage_profile_identity").all();
986
+ if (!replace && rows.length > 0) {
987
+ if (rows.length !== 1) throw new Error(`Expected at most one lineage_profile_identity row, found ${rows.length}`);
988
+ const existing = rows[0];
989
+ const sameBaseIdentity = existing.profile_id === profile.profile_id && existing.environment === profile.environment;
990
+ const migratableLegacyIdentity = sameBaseIdentity && !existing.profile_fingerprint;
991
+ if (!migratableLegacyIdentity && existing.profile_fingerprint !== profile.profile_fingerprint) {
992
+ throw new Error(`Database is already bound to ${existing.profile_id}/${existing.environment}/${existing.profile_fingerprint || "no-fingerprint"}`);
993
+ }
994
+ if (!migratableLegacyIdentity) {
995
+ database.exec("commit");
996
+ return existing;
997
+ }
998
+ }
999
+ const identity = profileIdentity(profile);
1000
+ database.exec("delete from lineage_profile_identity");
1001
+ database.prepare("insert into lineage_profile_identity (profile_id, environment, profile_fingerprint, bound_at) values (?, ?, ?, ?)").run(identity.profile_id, identity.environment, identity.profile_fingerprint, identity.bound_at);
1002
+ const integrity = database.prepare("pragma integrity_check").get();
1003
+ if (integrity?.integrity_check !== "ok") throw new Error(`SQLite integrity check failed: ${integrity?.integrity_check || "unknown result"}`);
1004
+ database.exec("commit");
1005
+ return identity;
1006
+ } catch (error) {
1007
+ try {
1008
+ database.exec("rollback");
1009
+ } catch {
1010
+ }
1011
+ throw error;
1012
+ }
1013
+ }
1014
+ function bindLineageProfileDatabase(selector, runtime, confirmWrite) {
1015
+ if (!confirmWrite) throw new Error("Profile bind requires --confirm-write");
1016
+ const profile = resolveLineageProfile(selector);
1017
+ assertProfileChannel(profile, runtime.channel);
1018
+ assertProfileRuntimePin(profile, runtime);
1019
+ if (!existsSync4(profile.database_path)) throw new Error(`Profile database does not exist: ${profile.database_path}`);
1020
+ const { DatabaseSync } = require2("node:sqlite");
1021
+ const database = new DatabaseSync(profile.database_path);
1022
+ try {
1023
+ const before = inspectDatabase(profile).identity;
1024
+ const identity = bindOpenDatabase(database, profile, false);
1025
+ return {
1026
+ already_bound: Boolean(before?.profile_fingerprint === profile.profile_fingerprint),
1027
+ database_path: profile.database_path,
1028
+ identity,
1029
+ schema_version: "lineage.profile_bind.v1"
1030
+ };
1031
+ } finally {
1032
+ database.close();
1033
+ }
1034
+ }
1035
+ async function cloneLineageProfileDatabase(sourceDatabasePath, targetSelector, runtime, confirmWrite) {
1036
+ if (!confirmWrite) throw new Error("Profile clone requires --confirm-write");
1037
+ const profile = resolveLineageProfile(targetSelector);
1038
+ assertProfileChannel(profile, runtime.channel);
1039
+ assertProfileRuntimePin(profile, runtime);
1040
+ if (profile.environment === "production") throw new Error("Profile clone target must be preview or development, never production");
1041
+ const sourcePath = resolve3(sourceDatabasePath);
1042
+ const targetPath = resolve3(profile.database_path);
1043
+ if (sourcePath === targetPath) throw new Error("Profile clone source and target database paths must be different");
1044
+ if (!existsSync4(sourcePath)) throw new Error(`Profile clone source database does not exist: ${sourcePath}`);
1045
+ if (existsSync4(targetPath)) throw new Error(`Profile clone target database already exists: ${targetPath}`);
1046
+ mkdirSync3(dirname3(targetPath), { recursive: true });
1047
+ const temporaryPath = `${targetPath}.clone-${randomUUID()}.tmp`;
1048
+ const receiptDirectory = join4(dirname3(profile.manifest_path), "clone-receipts");
1049
+ let targetCreated = false;
1050
+ const { DatabaseSync, backup } = require2("node:sqlite");
1051
+ const source = new DatabaseSync(sourcePath, { readOnly: true });
1052
+ try {
1053
+ const pagesCopied = await backup(source, temporaryPath);
1054
+ const cloned = new DatabaseSync(temporaryPath);
1055
+ let identity;
1056
+ try {
1057
+ identity = bindOpenDatabase(cloned, profile, true);
1058
+ } finally {
1059
+ cloned.close();
1060
+ }
1061
+ linkSync(temporaryPath, targetPath);
1062
+ targetCreated = true;
1063
+ rmSync(temporaryPath, { force: true });
1064
+ mkdirSync3(receiptDirectory, { recursive: true, mode: 448 });
1065
+ const receiptPath = join4(receiptDirectory, `${Date.now()}-${randomUUID()}.json`);
1066
+ const result = {
1067
+ database_path: targetPath,
1068
+ pages_copied: pagesCopied,
1069
+ receipt_path: receiptPath,
1070
+ schema_version: lineageProfileCloneReceiptSchemaVersion,
1071
+ source_database_path: sourcePath,
1072
+ target_identity: identity
1073
+ };
1074
+ writeFileSync2(receiptPath, `${JSON.stringify({ ...result, created_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
1075
+ `, { encoding: "utf8", flag: "wx", mode: 384 });
1076
+ return result;
1077
+ } catch (error) {
1078
+ rmSync(temporaryPath, { force: true });
1079
+ if (targetCreated) rmSync(targetPath, { force: true });
1080
+ throw error;
1081
+ } finally {
1082
+ source.close();
1083
+ }
1084
+ }
1085
+ function pathIsInside(path, parent) {
1086
+ const child = relative2(parent, path);
1087
+ return child !== "" && !child.startsWith("..") && !isAbsolute(child);
1088
+ }
1089
+ function fileSha2562(path) {
1090
+ return createHash2("sha256").update(readFileSync3(path)).digest("hex");
1091
+ }
1092
+ function profileAssetReferences(database) {
1093
+ const references = [];
1094
+ const addColumn = (table, column, kind) => {
1095
+ if (!tableColumns(database, table).has(column)) return;
1096
+ const rows = database.prepare(`select ${column} value from ${table} where ${column} is not null and ${column} <> ''`).all();
1097
+ for (const row of rows) references.push({ kind, value: row.value });
1098
+ };
1099
+ addColumn("assets", "local_path", "scratch");
1100
+ addColumn("asset_attempts", "file_path", "scratch");
1101
+ addColumn("generation_job_outputs", "file_path", "scratch");
1102
+ addColumn("asset_ledger_sources", "local_path", "scratch");
1103
+ addColumn("content_posts", "source_path", "root");
1104
+ if (tableColumns(database, "projects").has("id")) {
1105
+ const projects = database.prepare("select id from projects where id is not null and id <> ''").all();
1106
+ for (const project of projects) {
1107
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(project.id)) throw new Error("A project ID cannot be mapped safely into the source asset root");
1108
+ references.push({ kind: "root", value: join4(project.id, "assets", "catalog.json") });
1109
+ }
1110
+ }
1111
+ return references;
1112
+ }
1113
+ function resolveProfileAssetReference(sourceRoot, reference) {
1114
+ if (reference.value.includes("\0")) throw new Error("A referenced asset path contains a null byte");
1115
+ const sourceScratch = join4(sourceRoot, ".asset-scratch");
1116
+ const base = reference.kind === "scratch" ? sourceScratch : sourceRoot;
1117
+ const value = reference.kind === "scratch" && reference.value.startsWith(".asset-scratch/") ? reference.value.slice(".asset-scratch/".length) : reference.value;
1118
+ const candidate = isAbsolute(value) ? resolve3(value) : resolve3(base, value);
1119
+ if (!pathIsInside(candidate, base)) throw new Error("A referenced asset path escapes the declared source asset root");
1120
+ return { path: candidate, relative_path: relative2(sourceRoot, candidate) };
1121
+ }
1122
+ function cloneLineageProfileAssets(sourceAssetRoot, targetSelector, runtime, confirmWrite) {
1123
+ if (!confirmWrite) throw new Error("Profile asset clone requires --confirm-write");
1124
+ const profile = resolveLineageProfile(targetSelector);
1125
+ assertProfileChannel(profile, runtime.channel);
1126
+ assertProfileRuntimePin(profile, runtime);
1127
+ if (!existsSync4(profile.database_path)) throw new Error(`Profile database does not exist: ${profile.database_path}`);
1128
+ const sourceRoot = resolve3(sourceAssetRoot);
1129
+ const targetRoot = resolve3(profile.asset_root);
1130
+ if (!existsSync4(sourceRoot) || !statSync3(sourceRoot).isDirectory()) throw new Error(`Source asset root does not exist: ${sourceRoot}`);
1131
+ if (sourceRoot === targetRoot || pathIsInside(targetRoot, sourceRoot) || pathIsInside(sourceRoot, targetRoot)) {
1132
+ throw new Error("Source and target asset roots must be disjoint");
1133
+ }
1134
+ if (pathIsInside(profile.database_path, targetRoot)) throw new Error("Profile database must not be stored inside its asset root");
1135
+ if (existsSync4(targetRoot)) throw new Error(`Profile asset clone target already exists: ${targetRoot}`);
1136
+ const databaseIdentity = inspectDatabase(profile).identity;
1137
+ if (databaseIdentity && (databaseIdentity.profile_id !== profile.profile_id || databaseIdentity.environment !== profile.environment || databaseIdentity.profile_fingerprint && databaseIdentity.profile_fingerprint !== profile.profile_fingerprint)) {
1138
+ throw new Error(`Profile database is already bound to ${databaseIdentity.profile_id}/${databaseIdentity.environment}`);
1139
+ }
1140
+ const { DatabaseSync } = require2("node:sqlite");
1141
+ const database = new DatabaseSync(profile.database_path, { readOnly: true });
1142
+ let references;
1143
+ try {
1144
+ database.exec("begin");
1145
+ references = profileAssetReferences(database);
1146
+ database.exec("commit");
1147
+ } catch (error) {
1148
+ try {
1149
+ database.exec("rollback");
1150
+ } catch {
1151
+ }
1152
+ throw error;
1153
+ } finally {
1154
+ database.close();
1155
+ }
1156
+ const sourceRealRoot = realpathSync(sourceRoot);
1157
+ const files = /* @__PURE__ */ new Map();
1158
+ let missingReferences = 0;
1159
+ let duplicateReferences = 0;
1160
+ for (const reference of references) {
1161
+ const resolved = resolveProfileAssetReference(sourceRoot, reference);
1162
+ if (!existsSync4(resolved.path)) {
1163
+ missingReferences += 1;
1164
+ continue;
1165
+ }
1166
+ const stats = statSync3(resolved.path);
1167
+ if (!stats.isFile()) {
1168
+ missingReferences += 1;
1169
+ continue;
1170
+ }
1171
+ const realSource = realpathSync(resolved.path);
1172
+ if (!pathIsInside(realSource, sourceRealRoot)) throw new Error("A referenced asset symlink escapes the declared source asset root");
1173
+ const existing = files.get(resolved.relative_path);
1174
+ if (existing) {
1175
+ if (realpathSync(existing) !== realSource) throw new Error("Two source assets map to the same target path");
1176
+ duplicateReferences += 1;
1177
+ continue;
1178
+ }
1179
+ files.set(resolved.relative_path, resolved.path);
1180
+ }
1181
+ mkdirSync3(dirname3(targetRoot), { recursive: true, mode: 448 });
1182
+ mkdirSync3(targetRoot, { mode: 448 });
1183
+ try {
1184
+ let bytesCopied = 0;
1185
+ const treeHash = createHash2("sha256");
1186
+ for (const relativePath of [...files.keys()].sort()) {
1187
+ const sourcePath = files.get(relativePath);
1188
+ const destinationPath = join4(targetRoot, relativePath);
1189
+ mkdirSync3(dirname3(destinationPath), { recursive: true, mode: 448 });
1190
+ copyFileSync2(sourcePath, destinationPath, fsConstants.COPYFILE_EXCL);
1191
+ chmodSync(destinationPath, 384);
1192
+ const sourceHash = fileSha2562(sourcePath);
1193
+ const destinationHash = fileSha2562(destinationPath);
1194
+ if (sourceHash !== destinationHash) throw new Error("A source asset changed or failed checksum verification during clone");
1195
+ bytesCopied += statSync3(destinationPath).size;
1196
+ treeHash.update(relativePath.replaceAll("\\", "/"));
1197
+ treeHash.update("\0");
1198
+ treeHash.update(destinationHash);
1199
+ treeHash.update("\0");
1200
+ }
1201
+ chmodSync(targetRoot, 448);
1202
+ const receiptPath = join4(targetRoot, ".lineage-profile-assets.json");
1203
+ const result = {
1204
+ asset_root: targetRoot,
1205
+ bytes_copied: bytesCopied,
1206
+ code_fingerprint: runtime.code.fingerprint,
1207
+ database_path: profile.database_path,
1208
+ duplicate_references: duplicateReferences,
1209
+ files_copied: files.size,
1210
+ missing_references: missingReferences,
1211
+ profile_fingerprint: profile.profile_fingerprint,
1212
+ profile_id: profile.profile_id,
1213
+ receipt_path: receiptPath,
1214
+ references_discovered: references.length,
1215
+ runtime_channel: runtime.channel,
1216
+ schema_version: lineageProfileAssetsCloneReceiptSchemaVersion,
1217
+ source_asset_root: sourceRoot,
1218
+ tree_sha256: treeHash.digest("hex")
1219
+ };
1220
+ writeFileSync2(receiptPath, `${JSON.stringify({ ...result, created_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
1221
+ `, {
1222
+ encoding: "utf8",
1223
+ flag: "wx",
1224
+ mode: 384
1225
+ });
1226
+ return result;
1227
+ } catch (error) {
1228
+ rmSync(targetRoot, { force: true, recursive: true });
1229
+ throw error;
1230
+ }
1231
+ }
924
1232
 
925
1233
  // src/server/profileWriterLease.ts
926
- import { createHash as createHash2, randomUUID, timingSafeEqual } from "node:crypto";
927
- import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, realpathSync, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
928
- import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
929
1234
  var writerLeaseSchemaVersion = "lineage.profile_writer_lease.v1";
930
1235
  var ownerFileName = "owner.json";
931
1236
  var ProfileWriterLeaseConflictError = class extends Error {
@@ -936,24 +1241,8 @@ var ProfileWriterLeaseConflictError = class extends Error {
936
1241
  }
937
1242
  owner;
938
1243
  };
939
- function canonicalDatabasePath(databasePath) {
940
- const resolved = resolve4(databasePath);
941
- const missingSegments = [];
942
- let candidate = resolved;
943
- while (!existsSync5(candidate)) {
944
- const parent = dirname4(candidate);
945
- if (parent === candidate) return resolved;
946
- missingSegments.unshift(basename3(candidate));
947
- candidate = parent;
948
- }
949
- try {
950
- return join5(realpathSync(candidate), ...missingSegments);
951
- } catch {
952
- return resolved;
953
- }
954
- }
955
1244
  function profileWriterLockPath(profile) {
956
- return `${canonicalDatabasePath(profile.database_path)}.writer.lock`;
1245
+ return join5(dirname4(profile.manifest_path), "writer.lock");
957
1246
  }
958
1247
  function ownerPath(lockPath) {
959
1248
  return join5(lockPath, ownerFileName);
@@ -976,7 +1265,7 @@ function readOwner(lockPath) {
976
1265
  throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
977
1266
  }
978
1267
  const owner = raw;
979
- if (owner.schema_version !== writerLeaseSchemaVersion || typeof owner.profile_id !== "string" || owner.environment !== "production" && owner.environment !== "preview" && owner.environment !== "development" || owner.role !== "service" && owner.role !== "cli" || !Number.isSafeInteger(owner.pid) || Number(owner.pid) <= 0 || typeof owner.acquired_at !== "string" || Number.isNaN(Date.parse(owner.acquired_at)) || owner.role === "service" && (typeof owner.service_origin !== "string" || !owner.service_origin) || owner.role === "cli" && owner.service_origin !== void 0 || typeof owner.token !== "string" || owner.token.length < 16) {
1268
+ if (owner.schema_version !== writerLeaseSchemaVersion || typeof owner.profile_id !== "string" || typeof owner.profile_fingerprint !== "string" || !/^[a-f0-9]{64}$/i.test(owner.profile_fingerprint) || owner.environment !== "production" && owner.environment !== "preview" && owner.environment !== "development" || owner.role !== "service" && owner.role !== "cli" || !Number.isSafeInteger(owner.pid) || Number(owner.pid) <= 0 || typeof owner.acquired_at !== "string" || Number.isNaN(Date.parse(owner.acquired_at)) || owner.role === "service" && (typeof owner.service_origin !== "string" || !owner.service_origin) || owner.role === "cli" && owner.service_origin !== void 0 || typeof owner.token !== "string" || owner.token.length < 16) {
980
1269
  throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
981
1270
  }
982
1271
  return owner;
@@ -993,10 +1282,10 @@ function processIsAlive(pid) {
993
1282
  function createLeaseDirectory(lockPath, owner) {
994
1283
  mkdirSync4(lockPath, { mode: 448 });
995
1284
  try {
996
- writeFileSync2(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
1285
+ writeFileSync3(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
997
1286
  `, { encoding: "utf8", flag: "wx", mode: 384 });
998
1287
  } catch (error) {
999
- rmSync(lockPath, { force: true, recursive: true });
1288
+ rmSync2(lockPath, { force: true, recursive: true });
1000
1289
  throw error;
1001
1290
  }
1002
1291
  }
@@ -1008,14 +1297,15 @@ function reclaimDeadOwner(lockPath, owner) {
1008
1297
  publicOwner
1009
1298
  );
1010
1299
  }
1011
- const tokenFence = createHash2("sha256").update(owner.token).digest("hex").slice(0, 24);
1300
+ const tokenFence = createHash3("sha256").update(owner.token).digest("hex").slice(0, 24);
1012
1301
  const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
1013
1302
  try {
1014
1303
  renameSync(lockPath, tombstone);
1015
1304
  } catch (error) {
1016
- if (errorCode(error) === "ENOENT" || errorCode(error) === "EEXIST" || errorCode(error) === "ENOTEMPTY") return;
1305
+ if (errorCode(error) === "ENOENT") return;
1017
1306
  throw error;
1018
1307
  }
1308
+ rmSync2(tombstone, { force: true, recursive: true });
1019
1309
  }
1020
1310
  function acquireProfileWriterLease(profile, channel, role = "service") {
1021
1311
  assertProfileChannel(profile, channel);
@@ -1024,11 +1314,12 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
1024
1314
  acquired_at: (/* @__PURE__ */ new Date()).toISOString(),
1025
1315
  environment: profile.environment,
1026
1316
  pid: process.pid,
1317
+ profile_fingerprint: profile.profile_fingerprint,
1027
1318
  profile_id: profile.profile_id,
1028
1319
  role,
1029
1320
  schema_version: writerLeaseSchemaVersion,
1030
1321
  ...role === "service" ? { service_origin: profile.service_origin } : {},
1031
- token: randomUUID()
1322
+ token: randomUUID2()
1032
1323
  };
1033
1324
  for (let attempt = 0; attempt < 4; attempt += 1) {
1034
1325
  try {
@@ -1051,7 +1342,7 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
1051
1342
  released = true;
1052
1343
  try {
1053
1344
  const current = readOwner(lockPath);
1054
- if (current.token === owner.token && current.pid === owner.pid) rmSync(lockPath, { force: true, recursive: true });
1345
+ if (current.token === owner.token && current.pid === owner.pid) rmSync2(lockPath, { force: true, recursive: true });
1055
1346
  } catch {
1056
1347
  }
1057
1348
  if (process.env.LINEAGE_WRITER_LEASE_TOKEN === owner.token) delete process.env.LINEAGE_WRITER_LEASE_TOKEN;
@@ -1067,7 +1358,7 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
1067
1358
  if (errorCode(readError) === "ENOENT") continue;
1068
1359
  throw readError;
1069
1360
  }
1070
- if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment) {
1361
+ if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment || existing.profile_fingerprint !== profile.profile_fingerprint) {
1071
1362
  throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; manual inspection is required`, { cause: error });
1072
1363
  }
1073
1364
  reclaimDeadOwner(lockPath, existing);
@@ -1076,23 +1367,47 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
1076
1367
  throw new Error(`Could not acquire writer lease for Lineage profile ${profile.profile_id}; another writer is racing for ownership`);
1077
1368
  }
1078
1369
  function assertProfileWriterLeaseHeld() {
1079
- if (!process.env.LINEAGE_PROFILE) return;
1370
+ if (!process.env.LINEAGE_PROFILE) {
1371
+ throw new Error("Persistent writes require a selected named Lineage profile and its writer lease; legacy-unbound access is read-only");
1372
+ }
1080
1373
  const profileId = process.env.LINEAGE_PROFILE_ID;
1374
+ const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
1081
1375
  const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
1082
1376
  const manifestPath = process.env.LINEAGE_PROFILE_MANIFEST;
1083
- const databasePath = process.env.LINEAGE_DB;
1084
1377
  const lockPath = process.env.LINEAGE_WRITER_LOCK_PATH;
1085
1378
  const token = process.env.LINEAGE_WRITER_LEASE_TOKEN;
1086
- if (!profileId || !environment || !manifestPath || !databasePath || !lockPath || !token) {
1379
+ if (!profileId || !profileFingerprint || !environment || !manifestPath || !lockPath || !token) {
1087
1380
  throw new Error("Named Lineage profiles may write only through a process holding the profile writer lease");
1088
1381
  }
1089
- const expectedLockPath = `${canonicalDatabasePath(databasePath)}.writer.lock`;
1090
- if (resolve4(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile database");
1382
+ const expectedLockPath = join5(dirname4(resolve4(manifestPath)), "writer.lock");
1383
+ if (resolve4(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile manifest");
1091
1384
  const owner = readOwner(lockPath);
1092
- if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.environment !== environment) {
1385
+ if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.profile_fingerprint !== profileFingerprint || owner.environment !== environment) {
1093
1386
  throw new Error("Current process does not own the selected Lineage profile writer lease");
1094
1387
  }
1095
1388
  }
1389
+ function assertSelectedProfileDatabaseIdentity(database) {
1390
+ const profileId = process.env.LINEAGE_PROFILE_ID;
1391
+ const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
1392
+ const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
1393
+ if (!process.env.LINEAGE_PROFILE || !profileId || !environment || !profileFingerprint) {
1394
+ throw new Error("Writable SQLite identity validation requires a fully resolved named Lineage profile");
1395
+ }
1396
+ const table = database.prepare("select name from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
1397
+ if (!table) throw new Error(`Refusing writable open: database is not bound to Lineage profile ${profileId}`);
1398
+ const columns = new Set(database.prepare("pragma table_info(lineage_profile_identity)").all().map((row) => row.name));
1399
+ if (!columns.has("profile_id") || !columns.has("environment") || !columns.has("profile_fingerprint")) {
1400
+ throw new Error(`Refusing writable open: database identity for ${profileId} is missing required fingerprint fields`);
1401
+ }
1402
+ const rows = database.prepare("select profile_id, environment, profile_fingerprint from lineage_profile_identity").all();
1403
+ if (rows.length !== 1) throw new Error(`Refusing writable open: expected exactly one database profile identity, found ${rows.length}`);
1404
+ const identity = rows[0];
1405
+ if (identity.profile_id !== profileId || identity.environment !== environment || identity.profile_fingerprint !== profileFingerprint) {
1406
+ throw new Error(
1407
+ `Refusing writable open: database identity ${String(identity.profile_id)}/${String(identity.environment)}/${String(identity.profile_fingerprint)} does not match selected profile ${profileId}/${environment}/${profileFingerprint}`
1408
+ );
1409
+ }
1410
+ }
1096
1411
  function getProfileWriterDelegation(profile) {
1097
1412
  const lockPath = profileWriterLockPath(profile);
1098
1413
  if (!existsSync5(lockPath)) {
@@ -1101,7 +1416,7 @@ function getProfileWriterDelegation(profile) {
1101
1416
  );
1102
1417
  }
1103
1418
  const owner = readOwner(lockPath);
1104
- if (owner.profile_id !== profile.profile_id || owner.environment !== profile.environment) {
1419
+ if (owner.profile_id !== profile.profile_id || owner.environment !== profile.environment || owner.profile_fingerprint !== profile.profile_fingerprint) {
1105
1420
  throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; refusing delegation`);
1106
1421
  }
1107
1422
  if (owner.role !== "service" || owner.service_origin !== profile.service_origin) {
@@ -1122,44 +1437,19 @@ function nowIso() {
1122
1437
  function lineageDbPath() {
1123
1438
  return process.env.LINEAGE_DB || join6(packageRoot, ".lineage", "asset-lineage.sqlite");
1124
1439
  }
1125
- function assertOpenedProfileIdentity(database, databasePath) {
1126
- const selector = process.env.LINEAGE_PROFILE;
1127
- if (!selector) return;
1128
- const profile = resolveLineageProfile(selector);
1129
- if (resolve5(databasePath) !== profile.database_path) {
1130
- throw new Error(`Profile ${profile.profile_id} requires database ${profile.database_path}, not ${databasePath}`);
1131
- }
1132
- const table = database.prepare("select 1 from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
1133
- if (!table) throw new Error(`Profile database ${databasePath} is not bound to a Lineage profile identity`);
1134
- const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
1135
- if (rows.length !== 1) {
1136
- throw new Error(`Profile database ${databasePath} has invalid Lineage profile identity row count ${rows.length}`);
1137
- }
1138
- if (rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
1139
- throw new Error(
1140
- `Profile database ${databasePath} is bound to ${rows[0].profile_id}/${rows[0].environment}, not ${profile.profile_id}/${profile.environment}`
1141
- );
1142
- }
1143
- }
1144
1440
  function lineageDb() {
1145
1441
  const readOnly = process.env.LINEAGE_DB_ACCESS === "read-only";
1146
- const databasePath = lineageDbPath();
1147
1442
  if (!readOnly) assertProfileWriterLeaseHeld();
1148
- if (process.env.LINEAGE_PROFILE && !existsSync6(databasePath)) {
1149
- throw new Error(`Profile database does not exist: ${databasePath}; bind the profile before opening it`);
1443
+ if (!readOnly && !existsSync6(lineageDbPath())) {
1444
+ throw new Error(`Refusing writable open of missing profile database ${lineageDbPath()}; bind an existing database or create a non-production clone first`);
1150
1445
  }
1151
- if (!readOnly) mkdirSync5(join6(databasePath, ".."), { recursive: true });
1446
+ if (!readOnly) mkdirSync5(join6(lineageDbPath(), ".."), { recursive: true });
1152
1447
  const { DatabaseSync } = require3("node:sqlite");
1153
- const database = readOnly ? new DatabaseSync(databasePath, { readOnly: true }) : new DatabaseSync(databasePath);
1154
- try {
1155
- assertOpenedProfileIdentity(database, databasePath);
1156
- database.exec("PRAGMA foreign_keys = ON");
1157
- database.exec("PRAGMA busy_timeout = 5000");
1158
- if (readOnly) return database;
1159
- } catch (error) {
1160
- database.close();
1161
- throw error;
1162
- }
1448
+ const database = readOnly ? new DatabaseSync(lineageDbPath(), { readOnly: true }) : new DatabaseSync(lineageDbPath());
1449
+ if (process.env.LINEAGE_PROFILE) assertSelectedProfileDatabaseIdentity(database);
1450
+ database.exec("PRAGMA foreign_keys = ON");
1451
+ database.exec("PRAGMA busy_timeout = 5000");
1452
+ if (readOnly) return database;
1163
1453
  database.exec("PRAGMA journal_mode = WAL");
1164
1454
  database.exec(`
1165
1455
  create table if not exists projects (
@@ -1876,7 +2166,7 @@ function randomId(prefix) {
1876
2166
  return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
1877
2167
  }
1878
2168
  function tokenHash(token) {
1879
- return createHash3("sha256").update(token).digest("hex");
2169
+ return createHash4("sha256").update(token).digest("hex");
1880
2170
  }
1881
2171
  function safeEqual(a, b) {
1882
2172
  const left = Buffer.from(a);
@@ -2998,8 +3288,8 @@ function rootFor(database, project, assetId) {
2998
3288
  }
2999
3289
  function assertCanonicalRoot(database, project, root) {
3000
3290
  requireAsset2(database, project, root);
3001
- const canonicalRoot = rootFor(database, project, root);
3002
- if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
3291
+ const canonicalRoot2 = rootFor(database, project, root);
3292
+ if (canonicalRoot2 !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
3003
3293
  }
3004
3294
  function explicitWorkspaceRoot(database, project, assetId) {
3005
3295
  const row = database.prepare(`
@@ -3688,8 +3978,8 @@ function linkSelectedLineageChild(project, fields) {
3688
3978
 
3689
3979
  // src/server/generationReceipts.ts
3690
3980
  import { existsSync as existsSync8, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
3691
- import { relative as relative2, resolve as resolve6 } from "node:path";
3692
- import { randomUUID as randomUUID2 } from "node:crypto";
3981
+ import { relative as relative3, resolve as resolve5 } from "node:path";
3982
+ import { randomUUID as randomUUID3 } from "node:crypto";
3693
3983
  var adapterVersion = "generation-receipts-v1";
3694
3984
  var provider = "codex-handoff";
3695
3985
  var GenerationReceiptError = class extends Error {
@@ -3700,7 +3990,7 @@ var GenerationReceiptError = class extends Error {
3700
3990
  status;
3701
3991
  };
3702
3992
  function jobId() {
3703
- return `gen-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`;
3993
+ return `gen-${Date.now().toString(36)}-${randomUUID3().slice(0, 8)}`;
3704
3994
  }
3705
3995
  function quote(value) {
3706
3996
  return JSON.stringify(value);
@@ -3896,12 +4186,12 @@ function planImageReroll(project = defaultProject, fields) {
3896
4186
  }
3897
4187
  }
3898
4188
  function isPathInside(child, parent) {
3899
- const rel = relative2(parent, child);
4189
+ const rel = relative3(parent, child);
3900
4190
  return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
3901
4191
  }
3902
4192
  function resolveScratchFile(file) {
3903
- const scratchRoot = resolve6(repoRoot, ".asset-scratch");
3904
- const candidate = file.startsWith(".asset-scratch/") || resolve6(file).startsWith(scratchRoot) ? resolve6(repoRoot, file) : resolve6(scratchRoot, file);
4193
+ const scratchRoot = resolve5(repoRoot, ".asset-scratch");
4194
+ const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
3905
4195
  if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
3906
4196
  if (!existsSync8(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
3907
4197
  const realScratchRoot = realpathSync2(scratchRoot);
@@ -3911,7 +4201,7 @@ function resolveScratchFile(file) {
3911
4201
  if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
3912
4202
  const checksum = fileSha256(candidate);
3913
4203
  return {
3914
- relativePath: relative2(scratchRoot, candidate),
4204
+ relativePath: relative3(scratchRoot, candidate),
3915
4205
  checksum,
3916
4206
  size: stats.size,
3917
4207
  contentType: contentTypeFor(candidate),
@@ -3987,9 +4277,9 @@ function importImageRerollOutput(project = defaultProject, fields) {
3987
4277
  }
3988
4278
 
3989
4279
  // src/server/lineageSelectionPacket.ts
3990
- import { createHash as createHash4 } from "node:crypto";
4280
+ import { createHash as createHash5 } from "node:crypto";
3991
4281
  import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
3992
- import { isAbsolute as isAbsolute2, resolve as resolve7 } from "node:path";
4282
+ import { isAbsolute as isAbsolute2, resolve as resolve6 } from "node:path";
3993
4283
  var LineageSelectionPacketError = class extends Error {
3994
4284
  constructor(message, warnings = [], errors = [], diagnostics = []) {
3995
4285
  super(message);
@@ -4036,9 +4326,9 @@ function resolveWorkspace(project, options) {
4036
4326
  function resolveLocalReference(reference) {
4037
4327
  if (!reference) return void 0;
4038
4328
  if (isAbsolute2(reference)) return reference;
4039
- const repoRelative = resolve7(repoRoot, reference);
4329
+ const repoRelative = resolve6(repoRoot, reference);
4040
4330
  if (existsSync9(repoRelative)) return repoRelative;
4041
- return resolve7(repoRoot, ".asset-scratch", reference);
4331
+ return resolve6(repoRoot, ".asset-scratch", reference);
4042
4332
  }
4043
4333
  function fileSize(path) {
4044
4334
  if (!path || !existsSync9(path)) return void 0;
@@ -4114,7 +4404,7 @@ function assetForNode(node, catalogAsset, warnings) {
4114
4404
  };
4115
4405
  }
4116
4406
  function packetId(packetIdentity) {
4117
- const digest = createHash4("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
4407
+ const digest = createHash5("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
4118
4408
  return `lineage_packet_${digest}`;
4119
4409
  }
4120
4410
  var SHA256_PATTERN = /^[a-f0-9]{64}$/;
@@ -4185,7 +4475,7 @@ function lineageSelectionPacketV2IdentityProjection(packet) {
4185
4475
  }
4186
4476
  function lineageSelectionPacketV2IdentitySha256(packet) {
4187
4477
  const projection = lineageSelectionPacketV2IdentityProjection(packet);
4188
- return createHash4("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
4478
+ return createHash5("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
4189
4479
  }
4190
4480
  function addDiagnostic(diagnostics, diagnostic) {
4191
4481
  if (diagnostics.some((item) => item.code === diagnostic.code && item.severity === diagnostic.severity && item.asset_id === diagnostic.asset_id)) return;
@@ -4490,14 +4780,49 @@ function getLineageSelectionPacket(project, options = {}) {
4490
4780
  }
4491
4781
 
4492
4782
  // src/server/runtimeInfo.ts
4783
+ import { createHash as createHash6 } from "node:crypto";
4493
4784
  import { spawnSync as spawnSync2 } from "node:child_process";
4494
4785
  import { createRequire as createRequire4 } from "node:module";
4495
- import { existsSync as existsSync10, readFileSync as readFileSync5, statSync as statSync6 } from "node:fs";
4496
- import { join as join9 } from "node:path";
4786
+ import { existsSync as existsSync10, lstatSync as lstatSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, readlinkSync, realpathSync as realpathSync3, statSync as statSync6 } from "node:fs";
4787
+ import { dirname as dirname5, isAbsolute as isAbsolute3, join as join9, resolve as resolve7 } from "node:path";
4788
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
4789
+
4790
+ // src/shared/runtimeInfoTypes.ts
4791
+ var lineageRuntimeBuildSchemaVersion = "lineage.runtime_build.v1";
4792
+ var lineageRuntimeInstallSchemaVersion = "lineage.runtime_install.v1";
4793
+
4794
+ // src/server/runtimeInfo.ts
4497
4795
  var require5 = createRequire4(import.meta.url);
4498
- function packageInfo() {
4796
+ var processStartedAt = (/* @__PURE__ */ new Date()).toISOString();
4797
+ function isLineagePackageRoot(root) {
4499
4798
  try {
4500
- const info = JSON.parse(readFileSync5(join9(packageRoot, "package.json"), "utf8"));
4799
+ const info = JSON.parse(readFileSync5(join9(root, "package.json"), "utf8"));
4800
+ return info.name === "@mean-weasel/lineage";
4801
+ } catch {
4802
+ return false;
4803
+ }
4804
+ }
4805
+ function executingCodeRoot() {
4806
+ const moduleDirectory = dirname5(fileURLToPath2(import.meta.url));
4807
+ const candidates = [resolve7(moduleDirectory, "../.."), resolve7(moduleDirectory, "..")];
4808
+ const root = candidates.find(isLineagePackageRoot);
4809
+ if (!root) throw new Error(`Unable to derive Lineage code root from executing module ${fileURLToPath2(import.meta.url)}`);
4810
+ return canonicalRoot(root);
4811
+ }
4812
+ var codeRoot = executingCodeRoot();
4813
+ function sha256(value) {
4814
+ return createHash6("sha256").update(value).digest("hex");
4815
+ }
4816
+ function canonicalRoot(root) {
4817
+ try {
4818
+ return realpathSync3(root);
4819
+ } catch {
4820
+ return resolve7(root);
4821
+ }
4822
+ }
4823
+ function packageInfo(root = codeRoot) {
4824
+ try {
4825
+ const info = JSON.parse(readFileSync5(join9(root, "package.json"), "utf8"));
4501
4826
  return { name: info.name || "@mean-weasel/lineage", version: info.version || "0.0.0" };
4502
4827
  } catch {
4503
4828
  return { name: "@mean-weasel/lineage", version: "0.0.0" };
@@ -4510,12 +4835,199 @@ function normalizeRuntimeChannel(value) {
4510
4835
  if (value === "development") return "dev";
4511
4836
  return process.env.NODE_ENV === "production" ? "stable" : "dev";
4512
4837
  }
4513
- function gitSha() {
4514
- const envSha = process.env.LINEAGE_GIT_SHA || process.env.GITHUB_SHA;
4515
- if (envSha) return envSha.slice(0, 40);
4516
- if (!existsSync10(join9(packageRoot, ".git"))) return void 0;
4517
- const result = spawnSync2("git", ["rev-parse", "--short=12", "HEAD"], { cwd: packageRoot, encoding: "utf8" });
4518
- return result.status === 0 ? result.stdout.trim() || void 0 : void 0;
4838
+ function git(root, args) {
4839
+ const result = spawnSync2("git", args, { cwd: root, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
4840
+ return result.status === 0 ? result.stdout : void 0;
4841
+ }
4842
+ function untrackedFingerprint(root, status) {
4843
+ const hash = createHash6("sha256");
4844
+ hash.update(status);
4845
+ const listed = git(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
4846
+ if (listed === void 0) return hash.digest("hex");
4847
+ for (const relativePath of listed.split("\0").filter(Boolean).sort()) {
4848
+ hash.update("\0");
4849
+ hash.update(relativePath);
4850
+ const path = join9(root, relativePath);
4851
+ try {
4852
+ const stat = lstatSync2(path);
4853
+ if (stat.isSymbolicLink()) hash.update(readlinkSync(path));
4854
+ else if (stat.isFile()) hash.update(readFileSync5(path));
4855
+ else hash.update(`[${stat.mode}:${stat.size}]`);
4856
+ } catch (error) {
4857
+ hash.update(`[unreadable:${error instanceof Error ? error.message : String(error)}]`);
4858
+ }
4859
+ }
4860
+ return hash.digest("hex");
4861
+ }
4862
+ function checkoutCodeIdentity(root, channel, version) {
4863
+ const errors = [];
4864
+ const gitRootRaw = git(root, ["rev-parse", "--show-toplevel"])?.trim();
4865
+ const gitSha = git(root, ["rev-parse", "HEAD"])?.trim();
4866
+ const status = git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
4867
+ const diff = git(root, ["diff", "--binary", "HEAD", "--"]);
4868
+ const canonicalGitRoot = gitRootRaw ? canonicalRoot(gitRootRaw) : void 0;
4869
+ if (!canonicalGitRoot || canonicalGitRoot !== root) errors.push("Code root is not the canonical root of a Git checkout/worktree");
4870
+ if (!gitSha || !/^[a-f0-9]{40}$/i.test(gitSha)) errors.push("Checkout Git revision is unavailable");
4871
+ if (status === void 0 || diff === void 0) errors.push("Checkout dirty state could not be inspected");
4872
+ if (channel !== "dev") errors.push(`Checkout code may run only as dev, not ${channel}`);
4873
+ const dirty = Boolean(status);
4874
+ const sourceFingerprint = sha256(`${gitSha || "unknown"}\0${sha256(diff || "")}\0${untrackedFingerprint(root, status || "")}`);
4875
+ return {
4876
+ channel,
4877
+ dirty,
4878
+ errors,
4879
+ fingerprint: sha256(JSON.stringify({ channel, dirty, git_sha: gitSha, origin: "checkout", root, source_fingerprint: sourceFingerprint, version })),
4880
+ git_sha: gitSha,
4881
+ origin: "checkout",
4882
+ package_version: version,
4883
+ root,
4884
+ source_fingerprint: sourceFingerprint,
4885
+ verified: errors.length === 0
4886
+ };
4887
+ }
4888
+ function buildFingerprint(build) {
4889
+ return sha256(JSON.stringify({
4890
+ package_name: build.package_name,
4891
+ package_version: build.package_version,
4892
+ schema_version: build.schema_version,
4893
+ source_dirty: build.source_dirty,
4894
+ source_fingerprint: build.source_fingerprint,
4895
+ source_git_sha: build.source_git_sha
4896
+ }));
4897
+ }
4898
+ function readBuildIdentity(root, errors) {
4899
+ const path = join9(root, "dist", "runtime-build.json");
4900
+ let build;
4901
+ try {
4902
+ build = JSON.parse(readFileSync5(path, "utf8"));
4903
+ } catch (error) {
4904
+ errors.push(`Embedded build identity is missing or unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`);
4905
+ return void 0;
4906
+ }
4907
+ if (build.schema_version !== lineageRuntimeBuildSchemaVersion) errors.push(`Unsupported build identity schema: ${String(build.schema_version)}`);
4908
+ if (!/^[a-f0-9]{40}$/i.test(build.source_git_sha || "")) errors.push("Embedded build Git revision is invalid");
4909
+ if (!/^[a-f0-9]{64}$/i.test(build.source_fingerprint || "")) errors.push("Embedded source fingerprint is invalid");
4910
+ const expected = buildFingerprint(build);
4911
+ if (build.build_fingerprint !== expected) errors.push("Embedded build fingerprint does not match its contents");
4912
+ return build;
4913
+ }
4914
+ function readInstallReceipt(path, errors) {
4915
+ if (!path) {
4916
+ errors.push("Packaged runtime was not launched through a channel install receipt");
4917
+ return void 0;
4918
+ }
4919
+ let receipt;
4920
+ const receiptPath = resolve7(path);
4921
+ try {
4922
+ receipt = JSON.parse(readFileSync5(receiptPath, "utf8"));
4923
+ } catch (error) {
4924
+ errors.push(`Runtime install receipt is missing or unreadable at ${receiptPath}: ${error instanceof Error ? error.message : String(error)}`);
4925
+ return void 0;
4926
+ }
4927
+ if (receipt.schema_version !== lineageRuntimeInstallSchemaVersion) errors.push(`Unsupported runtime install receipt schema: ${String(receipt.schema_version)}`);
4928
+ if (!isAbsolute3(receipt.package_root || "")) errors.push("Runtime install receipt package_root must be absolute");
4929
+ if (!receipt.package_integrity?.startsWith("sha512-")) errors.push("Runtime install receipt package integrity is invalid");
4930
+ if (receipt.package_source !== "registry" && receipt.package_source !== "local") errors.push("Runtime install receipt package source is invalid");
4931
+ if (typeof receipt.package_spec !== "string" || !receipt.package_spec) errors.push("Runtime install receipt package spec is invalid");
4932
+ if (!/^[a-f0-9]{64}$/i.test(receipt.package_tree_sha256 || "")) errors.push("Runtime install receipt package tree hash is invalid");
4933
+ if (Number.isNaN(Date.parse(receipt.installed_at))) errors.push("Runtime install receipt timestamp is invalid");
4934
+ return { ...receipt, receipt_path: receiptPath };
4935
+ }
4936
+ function lineagePackageTreeSha256(root) {
4937
+ const hash = createHash6("sha256");
4938
+ const visit = (directory, relativeDirectory = "") => {
4939
+ for (const entry of readdirSync3(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
4940
+ const relativePath = relativeDirectory ? join9(relativeDirectory, entry.name) : entry.name;
4941
+ const path = join9(directory, entry.name);
4942
+ hash.update(relativePath.replaceAll("\\", "/"));
4943
+ hash.update("\0");
4944
+ if (entry.isDirectory()) {
4945
+ hash.update("directory\0");
4946
+ visit(path, relativePath);
4947
+ } else if (entry.isSymbolicLink()) {
4948
+ hash.update("symlink\0");
4949
+ hash.update(readlinkSync(path));
4950
+ } else if (entry.isFile()) {
4951
+ hash.update("file\0");
4952
+ hash.update(readFileSync5(path));
4953
+ } else {
4954
+ hash.update("other\0");
4955
+ }
4956
+ hash.update("\0");
4957
+ }
4958
+ };
4959
+ visit(root);
4960
+ return hash.digest("hex");
4961
+ }
4962
+ function packageCodeIdentity(root, channel, info, receiptPath) {
4963
+ const errors = [];
4964
+ const build = readBuildIdentity(root, errors);
4965
+ const install = readInstallReceipt(receiptPath, errors);
4966
+ if (channel === "dev") errors.push("Published package code cannot run as dev; use a Git checkout/worktree");
4967
+ if (build?.package_name !== info.name || build?.package_version !== info.version) {
4968
+ errors.push("Embedded build package identity does not match package.json");
4969
+ }
4970
+ if (build?.source_dirty) errors.push("Stable and preview package installs require a clean-source build");
4971
+ if (install) {
4972
+ if (install.channel !== channel) errors.push(`Install receipt channel ${install.channel} does not match requested ${channel}`);
4973
+ if (canonicalRoot(install.package_root) !== root) errors.push("Install receipt package root does not match the executing package root");
4974
+ if (install.package_name !== info.name || install.package_version !== info.version) errors.push("Install receipt package identity does not match package.json");
4975
+ if (build && install.build_fingerprint !== build.build_fingerprint) errors.push("Install receipt build fingerprint does not match the embedded build");
4976
+ try {
4977
+ if (lineagePackageTreeSha256(root) !== install.package_tree_sha256) errors.push("Installed package tree does not match the channel install receipt");
4978
+ } catch (error) {
4979
+ errors.push(`Installed package tree could not be verified: ${error instanceof Error ? error.message : String(error)}`);
4980
+ }
4981
+ }
4982
+ const gitSha = build?.source_git_sha;
4983
+ const sourceFingerprint = build?.source_fingerprint;
4984
+ return {
4985
+ build,
4986
+ channel,
4987
+ dirty: build?.source_dirty,
4988
+ errors,
4989
+ fingerprint: sha256(JSON.stringify({
4990
+ build_fingerprint: build?.build_fingerprint,
4991
+ channel,
4992
+ install_integrity: install?.package_integrity,
4993
+ origin: "package",
4994
+ root,
4995
+ version: info.version
4996
+ })),
4997
+ git_sha: gitSha,
4998
+ install,
4999
+ origin: "package",
5000
+ package_version: info.version,
5001
+ root,
5002
+ source_fingerprint: sourceFingerprint,
5003
+ verified: errors.length === 0
5004
+ };
5005
+ }
5006
+ function getLineageCodeIdentity(channel, options = {}) {
5007
+ const root = canonicalRoot(options.root || codeRoot);
5008
+ const info = packageInfo(root);
5009
+ if (existsSync10(join9(root, ".git"))) return checkoutCodeIdentity(root, channel, info.version);
5010
+ if (existsSync10(join9(root, "package.json"))) {
5011
+ return packageCodeIdentity(root, channel, info, options.receiptPath ?? process.env.LINEAGE_RUNTIME_RECEIPT);
5012
+ }
5013
+ const errors = [`Code root has neither checkout metadata nor package.json: ${root}`];
5014
+ return {
5015
+ channel,
5016
+ errors,
5017
+ fingerprint: sha256(JSON.stringify({ channel, origin: "unknown", root, version: info.version })),
5018
+ origin: "unknown",
5019
+ package_version: info.version,
5020
+ root,
5021
+ verified: false
5022
+ };
5023
+ }
5024
+ function assertLineageCodeOrigin(channel) {
5025
+ const identity = getLineageCodeIdentity(channel);
5026
+ if (!identity.verified) {
5027
+ const migration = channel === "dev" ? "Run dev from a Git checkout with npm run lineage:dev -- <command>." : `Install and launch an isolated ${channel} runtime with lineage-channel install ${channel}.`;
5028
+ throw new Error(`Unverified ${channel} code origin: ${identity.errors.join("; ")}. ${migration}`);
5029
+ }
5030
+ return identity;
4519
5031
  }
4520
5032
  function tableExists2(database, table) {
4521
5033
  return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
@@ -4533,6 +5045,7 @@ function getLineageRuntimeInfo(options = {}) {
4533
5045
  const info = packageInfo();
4534
5046
  const dbPath = options.dbPath || lineageDbPath();
4535
5047
  const channel = normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
5048
+ const code = options.code || getLineageCodeIdentity(channel);
4536
5049
  const databaseInfo = { exists: existsSync10(dbPath), path: dbPath };
4537
5050
  const schema = { migration_keys: [] };
4538
5051
  if (databaseInfo.exists) {
@@ -4547,11 +5060,13 @@ function getLineageRuntimeInfo(options = {}) {
4547
5060
  databaseInfo.workspaces = tableCount(database, "lineage_workspaces");
4548
5061
  schema.migration_keys = migrationKeys(database);
4549
5062
  if (tableExists2(database, "lineage_profile_identity")) {
4550
- const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
4551
- schema.profile_identity_rows = rows.length;
5063
+ const columns = new Set(database.prepare("pragma table_info(lineage_profile_identity)").all().map((row) => row.name));
5064
+ const fingerprintExpression = columns.has("profile_fingerprint") ? "profile_fingerprint" : "null as profile_fingerprint";
5065
+ const rows = database.prepare(`select profile_id, environment, ${fingerprintExpression} from lineage_profile_identity`).all();
4552
5066
  if (rows.length === 1) {
4553
5067
  schema.profile_id = rows[0].profile_id;
4554
5068
  schema.profile_environment = rows[0].environment;
5069
+ if (rows[0].profile_fingerprint) schema.profile_fingerprint = rows[0].profile_fingerprint;
4555
5070
  }
4556
5071
  }
4557
5072
  } finally {
@@ -4564,13 +5079,20 @@ function getLineageRuntimeInfo(options = {}) {
4564
5079
  return {
4565
5080
  asset_root: repoRoot,
4566
5081
  channel,
5082
+ code,
4567
5083
  database: databaseInfo,
4568
5084
  fetchedAt: nowIso(),
4569
- git_sha: gitSha(),
5085
+ git_sha: code.git_sha,
4570
5086
  node_env: process.env.NODE_ENV,
4571
5087
  package_name: info.name,
4572
5088
  profile: runtimeProfileIdentity(channel),
4573
5089
  schema,
5090
+ service: {
5091
+ instance_id: process.env.LINEAGE_SERVICE_INSTANCE_ID,
5092
+ launcher_pid: process.env.LINEAGE_LAUNCHER_PID ? Number(process.env.LINEAGE_LAUNCHER_PID) : void 0,
5093
+ pid: process.pid,
5094
+ started_at: processStartedAt
5095
+ },
4574
5096
  version: info.version
4575
5097
  };
4576
5098
  }
@@ -4665,7 +5187,7 @@ var signalExitCodes = {
4665
5187
  SIGTERM: 143
4666
5188
  };
4667
5189
  function packageRoot2() {
4668
- return resolve8(dirname5(fileURLToPath2(import.meta.url)), "..", "..");
5190
+ return resolve8(dirname6(fileURLToPath3(import.meta.url)), "..", "..");
4669
5191
  }
4670
5192
  function packageVersion() {
4671
5193
  try {
@@ -4710,9 +5232,8 @@ function resolveStartOptions(config, args) {
4710
5232
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
4711
5233
  throw new Error(`Invalid port: ${rawPort}`);
4712
5234
  }
4713
- const profileHost = serviceUrl ? normalizeListenHost(serviceUrl.hostname) : void 0;
4714
- const host = normalizeListenHost(readOption(args, "--host") || process.env.HOST || profileHost || config.defaultHost);
4715
- if (profile && serviceUrl && (host !== profileHost || port !== Number(serviceUrl.port || 80))) {
5235
+ const host = readOption(args, "--host") || process.env.HOST || serviceUrl?.hostname || config.defaultHost;
5236
+ if (profile && serviceUrl && (host !== serviceUrl.hostname || port !== Number(serviceUrl.port || 80))) {
4716
5237
  throw new Error(`Profile ${profile.profile_id} service_origin ${profile.service_origin} conflicts with requested host/port ${host}:${port}`);
4717
5238
  }
4718
5239
  return {
@@ -4725,13 +5246,6 @@ function resolveStartOptions(config, args) {
4725
5246
  profile
4726
5247
  };
4727
5248
  }
4728
- function normalizeListenHost(host) {
4729
- return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
4730
- }
4731
- function lineageServiceUrl(host, port) {
4732
- const urlHost = host.includes(":") ? `[${normalizeListenHost(host)}]` : host;
4733
- return `http://${urlHost}:${port}`;
4734
- }
4735
5249
  function resolveCliAssetRoot(args) {
4736
5250
  return resolve8(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
4737
5251
  }
@@ -4748,15 +5262,8 @@ function hasOption(args, name) {
4748
5262
  }
4749
5263
  function profileSelector(args) {
4750
5264
  const option = readOption(args, "--profile");
4751
- if (hasOption(args, "--profile") && !option?.trim()) {
4752
- throw new Error("--profile requires a non-empty profile ID or manifest path");
4753
- }
4754
- if (option && process.env.LINEAGE_PROFILE) {
4755
- const optionProfile = resolveLineageProfile(option);
4756
- const environmentProfile = resolveLineageProfile(process.env.LINEAGE_PROFILE);
4757
- if (optionProfile.manifest_path !== environmentProfile.manifest_path) {
4758
- throw new Error(`--profile ${option} conflicts with LINEAGE_PROFILE ${process.env.LINEAGE_PROFILE}`);
4759
- }
5265
+ if (option && process.env.LINEAGE_PROFILE && option !== process.env.LINEAGE_PROFILE) {
5266
+ throw new Error(`--profile ${option} conflicts with LINEAGE_PROFILE ${process.env.LINEAGE_PROFILE}`);
4760
5267
  }
4761
5268
  return option || process.env.LINEAGE_PROFILE;
4762
5269
  }
@@ -4780,12 +5287,13 @@ function prepareCliProfile(config, args) {
4780
5287
  }
4781
5288
  assertProfileChannel(profile, config.channel);
4782
5289
  const runtime = getLineageRuntimeInfo({ channel: config.channel, dbPath: profile.database_path });
4783
- const doctor = doctorLineageProfile(selector, { channel: config.channel, gitSha: runtime.git_sha, version: runtime.version });
5290
+ const doctor = doctorLineageProfile(selector, { channel: config.channel, code: runtime.code, gitSha: runtime.git_sha, version: runtime.version });
4784
5291
  if (!doctor.ok) throw new Error(`Profile ${profile.profile_id} failed doctor: ${doctorFailures(doctor)}`);
4785
5292
  process.env.LINEAGE_ASSET_ROOT = profile.asset_root;
4786
5293
  process.env.LINEAGE_DB = profile.database_path;
4787
5294
  process.env.LINEAGE_PROFILE = selector;
4788
5295
  process.env.LINEAGE_PROFILE_ENVIRONMENT = profile.environment;
5296
+ process.env.LINEAGE_PROFILE_FINGERPRINT = profile.profile_fingerprint;
4789
5297
  process.env.LINEAGE_PROFILE_ID = profile.profile_id;
4790
5298
  process.env.LINEAGE_PROFILE_MANIFEST = profile.manifest_path;
4791
5299
  process.env.LINEAGE_PROFILE_SERVICE_ORIGIN = profile.service_origin;
@@ -4798,7 +5306,11 @@ function formatLineageHelp(config) {
4798
5306
  Usage:
4799
5307
  ${config.binName} start [--profile <id-or-manifest>] [--port <port>] [--host <host>] [--db <path>] [--asset-root <path>] [--open] [--json]
4800
5308
  ${config.binName} profile doctor --profile <id-or-manifest> [--json]
4801
- ${config.binName} profile bind --profile <id-or-manifest> [--confirm-write] [--json]
5309
+ ${config.binName} profile bind --profile <id-or-manifest> --confirm-write [--json]
5310
+ ${config.binName} profile clone --source-db <snapshot-source> --target-profile <id-or-manifest> --confirm-write [--json]
5311
+ ${config.binName} profile clone-assets --source-asset-root <path> --target-profile <id-or-manifest> --confirm-write [--json]
5312
+ ${config.binName} runtime info [--json]
5313
+ ${config.binName} runtime doctor [--json]
4802
5314
  ${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
4803
5315
  ${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
4804
5316
  ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
@@ -4837,7 +5349,15 @@ Asset catalogs and local media default to the installed package root. Pass
4837
5349
  All commands accept --profile <id-or-manifest> or LINEAGE_PROFILE. Named
4838
5350
  profiles are authoritative for database, asset root, environment, and origin;
4839
5351
  they cannot be combined with direct --db or --asset-root overrides. Commands
4840
- without a profile run in legacy-unbound compatibility mode.
5352
+ without a profile run in legacy-unbound diagnostic/read-only mode.
5353
+
5354
+ Operational commands also require an attested code origin. Stable and preview
5355
+ run from separate lineage-channel install receipts. Dev runs only from a Git
5356
+ checkout/worktree through npm run lineage:dev.
5357
+
5358
+ Server startup independently verifies code origin. When --open is requested,
5359
+ the browser opens only after /api/runtime matches the expected code, profile,
5360
+ database, and unique service instance.
4841
5361
 
4842
5362
  Variation vs re-roll:
4843
5363
  link-child creates a new visible child variation edge.
@@ -4852,6 +5372,48 @@ function openBrowser(url) {
4852
5372
  const opener = spawn(command, args, { detached: true, stdio: "ignore" });
4853
5373
  opener.unref();
4854
5374
  }
5375
+ function lineageServiceIdentityErrors(runtime, expected) {
5376
+ const errors = [];
5377
+ if (runtime.channel !== expected.channel) errors.push(`channel ${runtime.channel} != ${expected.channel}`);
5378
+ if (!runtime.code?.verified) errors.push("service code identity is not verified");
5379
+ if (runtime.code?.fingerprint !== expected.code_fingerprint) errors.push(`code fingerprint ${runtime.code?.fingerprint || "missing"} != ${expected.code_fingerprint}`);
5380
+ if (resolve8(runtime.database.path) !== resolve8(expected.database_path)) errors.push(`database ${runtime.database.path} != ${expected.database_path}`);
5381
+ if (runtime.service?.instance_id !== expected.instance_id) errors.push(`service instance ${runtime.service?.instance_id || "missing"} != ${expected.instance_id}`);
5382
+ if (runtime.service?.launcher_pid !== expected.launcher_pid) errors.push(`launcher pid ${runtime.service?.launcher_pid || "missing"} != ${expected.launcher_pid}`);
5383
+ if (expected.profile) {
5384
+ if (!runtime.profile.bound) errors.push("service profile is unbound");
5385
+ if (runtime.profile.id !== expected.profile.profile_id) errors.push(`profile ${runtime.profile.id} != ${expected.profile.profile_id}`);
5386
+ if (runtime.profile.environment !== expected.profile.environment) errors.push(`environment ${runtime.profile.environment} != ${expected.profile.environment}`);
5387
+ if (runtime.profile.fingerprint !== expected.profile.profile_fingerprint) errors.push(`profile fingerprint ${runtime.profile.fingerprint || "missing"} != ${expected.profile.profile_fingerprint}`);
5388
+ if (runtime.schema.profile_id !== expected.profile.profile_id) errors.push(`database profile ${runtime.schema.profile_id || "missing"} != ${expected.profile.profile_id}`);
5389
+ if (runtime.schema.profile_fingerprint !== expected.profile.profile_fingerprint) errors.push(`database fingerprint ${runtime.schema.profile_fingerprint || "missing"} != ${expected.profile.profile_fingerprint}`);
5390
+ }
5391
+ return errors;
5392
+ }
5393
+ async function openBrowserAfterReadiness(url, expected, child) {
5394
+ const deadline = Date.now() + 15e3;
5395
+ let lastError = "service did not respond";
5396
+ while (Date.now() < deadline && child.exitCode === null) {
5397
+ try {
5398
+ const response = await fetch(`${url}/api/runtime`, { signal: AbortSignal.timeout(1e3) });
5399
+ if (response.ok) {
5400
+ const body = await response.json();
5401
+ if (body.runtime) {
5402
+ const errors = lineageServiceIdentityErrors(body.runtime, expected);
5403
+ if (errors.length === 0) {
5404
+ openBrowser(url);
5405
+ return;
5406
+ }
5407
+ lastError = errors.join("; ");
5408
+ }
5409
+ } else lastError = `HTTP ${response.status}`;
5410
+ } catch (error) {
5411
+ lastError = error instanceof Error ? error.message : String(error);
5412
+ }
5413
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 200));
5414
+ }
5415
+ throw new Error(`Service readiness failed; browser was not opened: ${lastError}`);
5416
+ }
4855
5417
  function positionalArgs(args) {
4856
5418
  const values = [];
4857
5419
  for (let index = 0; index < args.length; index += 1) {
@@ -4910,8 +5472,8 @@ function runLineageDataCommand(command, args) {
4910
5472
  const out = readOption(args, "--out");
4911
5473
  if (out) {
4912
5474
  const outPath = resolve8(out);
4913
- mkdirSync6(dirname5(outPath), { recursive: true });
4914
- writeFileSync3(outPath, `${JSON.stringify(packet, null, 2)}
5475
+ mkdirSync6(dirname6(outPath), { recursive: true });
5476
+ writeFileSync4(outPath, `${JSON.stringify(packet, null, 2)}
4915
5477
  `);
4916
5478
  }
4917
5479
  return packet;
@@ -5122,41 +5684,90 @@ function runLineageDbCommand(config, command, args) {
5122
5684
  throw new Error(`Unknown db command: ${command}`);
5123
5685
  }
5124
5686
  function runLineageProfileCommand(config, command, args) {
5125
- if (command !== "doctor" && command !== "bind") throw new Error(`Unknown profile command: ${command}`);
5687
+ const runtime = getLineageRuntimeInfo({ channel: config.channel });
5688
+ const runtimeIdentity = { channel: config.channel, code: runtime.code, gitSha: runtime.git_sha, version: runtime.version };
5689
+ if (command === "clone") {
5690
+ const source = readOption(args, "--source-db");
5691
+ const target = readOption(args, "--target-profile");
5692
+ if (!source || !target) throw new Error("lineage profile clone requires --source-db and --target-profile");
5693
+ if (hasOption(args, "--profile")) throw new Error("Profile clone uses --target-profile, not --profile");
5694
+ if (!args.includes("--confirm-write")) throw new Error("Profile clone requires --confirm-write");
5695
+ const targetProfile = resolveLineageProfile(target);
5696
+ const writerLease = acquireProfileWriterLease(targetProfile, config.channel, "cli");
5697
+ return cloneLineageProfileDatabase(source, target, runtimeIdentity, true).finally(writerLease.release);
5698
+ }
5699
+ if (command === "clone-assets") {
5700
+ const source = readOption(args, "--source-asset-root");
5701
+ const target = readOption(args, "--target-profile");
5702
+ if (!source || !target) throw new Error("lineage profile clone-assets requires --source-asset-root and --target-profile");
5703
+ if (hasOption(args, "--profile")) throw new Error("Profile asset clone uses --target-profile, not --profile");
5704
+ if (!args.includes("--confirm-write")) throw new Error("Profile asset clone requires --confirm-write");
5705
+ const targetProfile = resolveLineageProfile(target);
5706
+ const writerLease = acquireProfileWriterLease(targetProfile, config.channel, "cli");
5707
+ try {
5708
+ return cloneLineageProfileAssets(source, target, runtimeIdentity, true);
5709
+ } finally {
5710
+ writerLease.release();
5711
+ }
5712
+ }
5126
5713
  if (hasOption(args, "--db")) throw new Error(`Profile ${command} cannot be combined with --db`);
5127
5714
  if (hasOption(args, "--asset-root")) throw new Error(`Profile ${command} cannot be combined with --asset-root`);
5128
5715
  const selector = profileSelector(args);
5129
5716
  if (!selector) throw new Error(`lineage profile ${command} requires --profile or LINEAGE_PROFILE`);
5717
+ if (command === "doctor") return doctorLineageProfile(selector, runtimeIdentity);
5130
5718
  if (command === "bind") {
5719
+ if (!args.includes("--confirm-write")) throw new Error("Profile bind requires --confirm-write");
5131
5720
  const profile = resolveLineageProfile(selector);
5132
- assertProfileChannel(profile, config.channel);
5133
- if (!args.includes("--confirm-write")) return bindLineageProfileDatabase(profile, false);
5134
- mkdirSync6(dirname5(profile.database_path), { recursive: true });
5135
5721
  const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
5136
5722
  try {
5137
- return bindLineageProfileDatabase(profile, true);
5723
+ return bindLineageProfileDatabase(selector, runtimeIdentity, true);
5138
5724
  } finally {
5139
5725
  writerLease.release();
5140
5726
  }
5141
5727
  }
5142
- const runtime = getLineageRuntimeInfo({ channel: config.channel });
5143
- return doctorLineageProfile(selector, { channel: config.channel, gitSha: runtime.git_sha, version: runtime.version });
5728
+ throw new Error(`Unknown profile command: ${command}`);
5144
5729
  }
5145
5730
  function printProfileResult(result, json) {
5146
5731
  if (json) {
5147
5732
  console.log(JSON.stringify(result, null, 2));
5148
5733
  return;
5149
5734
  }
5150
- if (result.schema_version === "lineage.profile_bind.v1") {
5151
- console.log(`${result.dryRun ? "Would bind" : result.already_bound ? "Already bound" : "Bound"} ${result.profile_id} to ${result.database_path}`);
5152
- return;
5735
+ if (result.schema_version === "lineage.profile_doctor.v1") {
5736
+ console.log(`Profile doctor: ${result.ok ? "ok" : "failed"}`);
5737
+ for (const check of result.checks) console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`);
5738
+ } else if (result.schema_version === "lineage.profile_bind.v1") {
5739
+ console.log(`${result.already_bound ? "Already bound" : "Bound"} ${result.database_path} to ${result.identity.profile_id}`);
5740
+ } else if (result.schema_version === "lineage.profile_clone_receipt.v1") {
5741
+ console.log(`Cloned ${result.source_database_path} to ${result.database_path} for ${result.target_identity.profile_id}`);
5742
+ console.log(`Receipt: ${result.receipt_path}`);
5743
+ } else {
5744
+ console.log(`Cloned ${result.files_copied} referenced asset file(s) into ${result.asset_root}`);
5745
+ console.log(`Receipt: ${result.receipt_path}`);
5153
5746
  }
5154
- console.log(`Profile doctor: ${result.ok ? "ok" : "failed"}`);
5155
- for (const check of result.checks) console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`);
5156
5747
  }
5157
5748
  function lineageProfileDoctorExitCode(result) {
5158
5749
  return result.ok ? 0 : 1;
5159
5750
  }
5751
+ function runLineageRuntimeCommand(config, command) {
5752
+ if (command === "info") return getLineageCodeIdentity(config.channel);
5753
+ if (command === "doctor") return assertLineageCodeOrigin(config.channel);
5754
+ throw new Error(`Unknown runtime command: ${command}`);
5755
+ }
5756
+ function printRuntimeResult(result, json) {
5757
+ if (json) {
5758
+ console.log(JSON.stringify(result, null, 2));
5759
+ return;
5760
+ }
5761
+ console.log(`Code origin: ${result.origin}`);
5762
+ console.log(`Code root: ${result.root}`);
5763
+ console.log(`Channel: ${result.channel}`);
5764
+ console.log(`Version: ${result.package_version}`);
5765
+ if (result.git_sha) console.log(`Git: ${result.git_sha}`);
5766
+ if (result.dirty !== void 0) console.log(`Dirty: ${result.dirty ? "yes" : "no"}`);
5767
+ console.log(`Fingerprint: ${result.fingerprint}`);
5768
+ console.log(`Verified: ${result.verified ? "yes" : "no"}`);
5769
+ for (const error of result.errors) console.log(`FAIL: ${error}`);
5770
+ }
5160
5771
  function lineageCliRequiresWriterLease(command, args) {
5161
5772
  if (command === "next" || command === "brief" || command === "inspect" || command === "selection") return false;
5162
5773
  const subcommand = positionalArgs(args)[0] || "";
@@ -5188,6 +5799,10 @@ function printDbResult(command, result, json) {
5188
5799
  const runtime = result;
5189
5800
  console.log(`Channel: ${runtime.channel}`);
5190
5801
  console.log(`Version: ${runtime.version}`);
5802
+ if (runtime.code) {
5803
+ console.log(`Code: ${runtime.code.origin} ${runtime.code.verified ? "verified" : "unverified"} ${runtime.code.fingerprint}`);
5804
+ console.log(`Code root: ${runtime.code.root}`);
5805
+ }
5191
5806
  if (runtime.git_sha) console.log(`Git: ${runtime.git_sha}`);
5192
5807
  if (runtime.node_env) console.log(`Node env: ${runtime.node_env}`);
5193
5808
  console.log(`Assets: ${runtime.asset_root}`);
@@ -5336,8 +5951,8 @@ function start(config, args) {
5336
5951
  else console.error(`${config.binName}: ${message}`);
5337
5952
  process.exit(1);
5338
5953
  }
5339
- mkdirSync6(dirname5(options.dbPath), { recursive: true });
5340
- const url = lineageServiceUrl(options.host, options.port);
5954
+ mkdirSync6(dirname6(options.dbPath), { recursive: true });
5955
+ const url = `http://${options.host}:${options.port}`;
5341
5956
  if (options.json) {
5342
5957
  console.log(JSON.stringify({ assetRoot: options.assetRoot, channel: config.channel, dbPath: options.dbPath, host: options.host, port: options.port, profile: options.profile ? { environment: options.profile.environment, id: options.profile.profile_id } : { bound: false, id: "legacy-unbound" }, status: "starting", url }, null, 2));
5343
5958
  } else {
@@ -5347,7 +5962,8 @@ function start(config, args) {
5347
5962
  console.log(`SQLite: ${options.dbPath}`);
5348
5963
  console.log(`Assets: ${options.assetRoot}`);
5349
5964
  }
5350
- if (options.open) openBrowser(url);
5965
+ const code = getLineageCodeIdentity(config.channel);
5966
+ const serviceInstanceId = process.env.LINEAGE_SERVICE_INSTANCE_ID || randomUUID4();
5351
5967
  const child = spawn(process.execPath, [serverPath], {
5352
5968
  env: {
5353
5969
  ...process.env,
@@ -5357,15 +5973,31 @@ function start(config, args) {
5357
5973
  LINEAGE_DB: options.dbPath,
5358
5974
  LINEAGE_PROFILE: options.profile ? process.env.LINEAGE_PROFILE : void 0,
5359
5975
  LINEAGE_PROFILE_ENVIRONMENT: options.profile?.environment,
5976
+ LINEAGE_PROFILE_FINGERPRINT: options.profile?.profile_fingerprint,
5360
5977
  LINEAGE_PROFILE_ID: options.profile?.profile_id,
5361
5978
  LINEAGE_PROFILE_MANIFEST: options.profile?.manifest_path,
5362
5979
  LINEAGE_PROFILE_SERVICE_ORIGIN: options.profile?.service_origin,
5363
- LINEAGE_DB_ACCESS: void 0,
5980
+ LINEAGE_LAUNCHER_PID: String(process.pid),
5981
+ LINEAGE_SERVICE_INSTANCE_ID: serviceInstanceId,
5982
+ LINEAGE_DB_ACCESS: options.profile ? void 0 : "read-only",
5364
5983
  NODE_ENV: "production",
5365
5984
  PORT: String(options.port)
5366
5985
  },
5367
5986
  stdio: "inherit"
5368
5987
  });
5988
+ if (options.open) {
5989
+ void openBrowserAfterReadiness(url, {
5990
+ channel: config.channel,
5991
+ code_fingerprint: code.fingerprint,
5992
+ database_path: options.dbPath,
5993
+ instance_id: serviceInstanceId,
5994
+ launcher_pid: process.pid,
5995
+ profile: options.profile
5996
+ }, child).catch((error) => {
5997
+ console.error(`${config.binName}: ${error instanceof Error ? error.message : String(error)}`);
5998
+ child.kill("SIGTERM");
5999
+ });
6000
+ }
5369
6001
  let forwardedSignal;
5370
6002
  const stop = (signal) => {
5371
6003
  forwardedSignal = signal;
@@ -5373,7 +6005,7 @@ function start(config, args) {
5373
6005
  };
5374
6006
  process.once("SIGINT", stop);
5375
6007
  process.once("SIGTERM", stop);
5376
- child.on("exit", (code) => process.exit(code ?? (forwardedSignal ? signalExitCodes[forwardedSignal] || 1 : 0)));
6008
+ child.on("exit", (code2) => process.exit(code2 ?? (forwardedSignal ? signalExitCodes[forwardedSignal] || 1 : 0)));
5377
6009
  child.on("error", (error) => {
5378
6010
  console.error(`${config.binName}: failed to start server: ${error.message}`);
5379
6011
  process.exit(1);
@@ -5391,18 +6023,26 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
5391
6023
  }
5392
6024
  const normalizedArgs = args[0] === "lineage" ? args.slice(1) : args;
5393
6025
  const [command] = normalizedArgs;
5394
- if (command === "start") {
5395
- start(config, normalizedArgs.slice(1));
5396
- return;
5397
- }
5398
6026
  if (command === "profile") {
5399
6027
  const commandArgs = normalizedArgs.slice(2);
5400
6028
  const profileCommand = normalizedArgs[1] || "";
5401
6029
  const json2 = commandArgs.includes("--json");
5402
6030
  try {
5403
6031
  const result = runLineageProfileCommand(config, profileCommand, commandArgs);
6032
+ if (result instanceof Promise) {
6033
+ result.then((value) => {
6034
+ printProfileResult(value, json2);
6035
+ process.exit(0);
6036
+ }).catch((error) => {
6037
+ const message2 = error instanceof Error ? error.message : String(error);
6038
+ if (json2) console.error(JSON.stringify({ ok: false, command: `profile ${profileCommand}`, error: message2 }, null, 2));
6039
+ else console.error(`${config.binName}: ${message2}`);
6040
+ process.exit(1);
6041
+ });
6042
+ return;
6043
+ }
5404
6044
  printProfileResult(result, json2);
5405
- process.exit(lineageProfileDoctorExitCode(result));
6045
+ process.exit(result.schema_version === "lineage.profile_doctor.v1" ? lineageProfileDoctorExitCode(result) : 0);
5406
6046
  } catch (error) {
5407
6047
  const message2 = error instanceof Error ? error.message : String(error);
5408
6048
  if (json2) console.error(JSON.stringify({ ok: false, command: `profile ${profileCommand}`, error: message2 }, null, 2));
@@ -5410,11 +6050,42 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
5410
6050
  process.exit(1);
5411
6051
  }
5412
6052
  }
6053
+ if (command === "runtime") {
6054
+ const runtimeCommand = normalizedArgs[1] || "";
6055
+ const commandArgs = normalizedArgs.slice(2);
6056
+ const json2 = commandArgs.includes("--json");
6057
+ try {
6058
+ printRuntimeResult(runLineageRuntimeCommand(config, runtimeCommand), json2);
6059
+ process.exit(0);
6060
+ } catch (error) {
6061
+ const message2 = error instanceof Error ? error.message : String(error);
6062
+ if (json2) console.error(JSON.stringify({ ok: false, command: `runtime ${runtimeCommand}`, error: message2 }, null, 2));
6063
+ else console.error(`${config.binName}: ${message2}`);
6064
+ process.exit(1);
6065
+ }
6066
+ }
6067
+ const originDiagnosticOnly = command === "db" && normalizedArgs[1] === "info";
6068
+ if (!originDiagnosticOnly) {
6069
+ try {
6070
+ assertLineageCodeOrigin(config.channel);
6071
+ } catch (error) {
6072
+ const message2 = error instanceof Error ? error.message : String(error);
6073
+ const json2 = normalizedArgs.includes("--json");
6074
+ if (json2) console.error(JSON.stringify({ ok: false, command, error: message2 }, null, 2));
6075
+ else console.error(`${config.binName}: ${message2}`);
6076
+ process.exit(1);
6077
+ }
6078
+ }
6079
+ if (command === "start") {
6080
+ start(config, normalizedArgs.slice(1));
6081
+ return;
6082
+ }
5413
6083
  let managedWriterProfile;
5414
6084
  try {
5415
6085
  const profile = prepareCliProfile(config, normalizedArgs.slice(1));
6086
+ const requiresWriter = lineageCliRequiresWriterLease(command, normalizedArgs.slice(1));
5416
6087
  if (profile) {
5417
- if (lineageCliRequiresWriterLease(command, normalizedArgs.slice(1))) {
6088
+ if (requiresWriter) {
5418
6089
  delete process.env.LINEAGE_DB_ACCESS;
5419
6090
  try {
5420
6091
  const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
@@ -5427,6 +6098,10 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
5427
6098
  } else {
5428
6099
  process.env.LINEAGE_DB_ACCESS = "read-only";
5429
6100
  }
6101
+ } else if (requiresWriter) {
6102
+ throw new Error("Persistent writes require --profile; legacy-unbound access is read-only");
6103
+ } else {
6104
+ process.env.LINEAGE_DB_ACCESS = "read-only";
5430
6105
  }
5431
6106
  } catch (error) {
5432
6107
  const message2 = error instanceof Error ? error.message : String(error);