@mean-weasel/lineage 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +198 -33
- package/dist/cli/lineage-channel.js +276 -0
- package/dist/cli/lineage-channel.js.map +7 -0
- package/dist/cli/lineage-dev.js +2008 -141
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage-preview.js +6182 -0
- package/dist/cli/lineage-preview.js.map +7 -0
- package/dist/cli/lineage.js +1991 -136
- package/dist/cli/lineage.js.map +4 -4
- package/dist/cli/managed-service.js +430 -0
- package/dist/cli/managed-service.js.map +7 -0
- package/dist/runtime-build.json +9 -0
- package/dist/server.js +2357 -182
- package/dist/server.js.map +4 -4
- package/dist/web/assets/{index-CKMlZT43.css → index-3uJbXqPS.css} +1 -1
- package/dist/web/assets/index-BrGIkUus.js +23 -0
- package/dist/web/index.html +2 -2
- package/package.json +22 -9
- package/dist/web/assets/index-ADWhXN3-.js +0 -23
package/dist/cli/lineage-dev.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/lineageCli.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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";
|
|
6
|
+
import { homedir as homedir2, platform as platform2 } from "node:os";
|
|
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
|
|
9
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
9
10
|
|
|
10
11
|
// src/server/agentClaims.ts
|
|
11
|
-
import { createHash as
|
|
12
|
+
import { createHash as createHash4, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
12
13
|
|
|
13
14
|
// src/server/assetLineageDb.ts
|
|
14
|
-
import { createRequire } from "node:module";
|
|
15
|
-
import { mkdirSync as
|
|
16
|
-
import { join as
|
|
15
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
16
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
|
|
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,20 +588,869 @@ 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
|
|
|
590
|
-
// src/server/
|
|
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
|
+
|
|
596
|
+
// src/server/lineageProfiles.ts
|
|
597
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
598
|
+
import { createRequire } from "node:module";
|
|
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";
|
|
612
|
+
import { homedir, platform } from "node:os";
|
|
613
|
+
import { dirname as dirname3, isAbsolute, join as join4, relative as relative2, resolve as resolve3 } from "node:path";
|
|
614
|
+
|
|
615
|
+
// src/shared/lineageProfileTypes.ts
|
|
616
|
+
var lineageProfileSchemaVersion = "lineage.profile.v1";
|
|
617
|
+
var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
|
|
618
|
+
var lineageProfileCloneReceiptSchemaVersion = "lineage.profile_clone_receipt.v1";
|
|
619
|
+
var lineageProfileAssetsCloneReceiptSchemaVersion = "lineage.profile_assets_clone_receipt.v1";
|
|
620
|
+
|
|
621
|
+
// src/server/lineageProfiles.ts
|
|
591
622
|
var require2 = createRequire(import.meta.url);
|
|
623
|
+
var profileIdPattern = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
624
|
+
function lineageDataRoot() {
|
|
625
|
+
if (process.env.LINEAGE_HOME) return resolve3(process.env.LINEAGE_HOME);
|
|
626
|
+
if (platform() === "darwin") return join4(homedir(), "Library", "Application Support", "Lineage");
|
|
627
|
+
if (platform() === "win32") return join4(process.env.APPDATA || join4(homedir(), "AppData", "Roaming"), "Lineage");
|
|
628
|
+
return join4(process.env.XDG_DATA_HOME || join4(homedir(), ".local", "share"), "lineage");
|
|
629
|
+
}
|
|
630
|
+
function lineageProfileRoot() {
|
|
631
|
+
return resolve3(process.env.LINEAGE_PROFILE_ROOT || join4(lineageDataRoot(), "profiles"));
|
|
632
|
+
}
|
|
633
|
+
function environmentChannel(environment) {
|
|
634
|
+
if (environment === "production") return "stable";
|
|
635
|
+
if (environment === "preview") return "preview";
|
|
636
|
+
return "dev";
|
|
637
|
+
}
|
|
638
|
+
function channelEnvironment(channel) {
|
|
639
|
+
if (channel === "stable") return "production";
|
|
640
|
+
if (channel === "preview") return "preview";
|
|
641
|
+
return "development";
|
|
642
|
+
}
|
|
643
|
+
function profileManifestPath(selector) {
|
|
644
|
+
const value = selector.trim();
|
|
645
|
+
if (!value) throw new Error("Profile selector must not be empty");
|
|
646
|
+
const looksLikePath = isAbsolute(value) || value.includes("/") || value.includes("\\") || value.endsWith(".json");
|
|
647
|
+
if (looksLikePath) return { manifestPath: resolve3(value) };
|
|
648
|
+
if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
|
|
649
|
+
return { manifestPath: join4(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
|
|
650
|
+
}
|
|
651
|
+
function requiredString(record, key) {
|
|
652
|
+
const value = record[key];
|
|
653
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
|
|
654
|
+
return value.trim();
|
|
655
|
+
}
|
|
656
|
+
function optionalString(record, key) {
|
|
657
|
+
const value = record[key];
|
|
658
|
+
if (value === void 0) return void 0;
|
|
659
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
|
|
660
|
+
return value.trim();
|
|
661
|
+
}
|
|
662
|
+
function validateEnvironment(value) {
|
|
663
|
+
if (value === "production" || value === "preview" || value === "development") return value;
|
|
664
|
+
throw new Error(`Invalid profile environment: ${value}`);
|
|
665
|
+
}
|
|
666
|
+
function validateChannel(value) {
|
|
667
|
+
if (value === void 0) return void 0;
|
|
668
|
+
if (value === "stable" || value === "preview" || value === "dev") return value;
|
|
669
|
+
throw new Error(`Invalid expected runtime channel: ${String(value)}`);
|
|
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
|
+
}
|
|
681
|
+
function resolveManifestPath(manifestPath, value) {
|
|
682
|
+
return resolve3(dirname3(manifestPath), value);
|
|
683
|
+
}
|
|
684
|
+
function validateServiceOrigin(value) {
|
|
685
|
+
let parsed;
|
|
686
|
+
try {
|
|
687
|
+
parsed = new URL(value);
|
|
688
|
+
} catch {
|
|
689
|
+
throw new Error(`Invalid profile service_origin: ${value}`);
|
|
690
|
+
}
|
|
691
|
+
if (parsed.protocol !== "http:") throw new Error("Profile service_origin must use http for the local Lineage service");
|
|
692
|
+
if (!parsed.port) throw new Error("Profile service_origin must include an explicit port");
|
|
693
|
+
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
694
|
+
throw new Error("Profile service_origin must contain only scheme, host, and port");
|
|
695
|
+
}
|
|
696
|
+
return parsed.origin;
|
|
697
|
+
}
|
|
698
|
+
function resolveLineageProfile(selector) {
|
|
699
|
+
const { manifestPath, namedProfileId } = profileManifestPath(selector);
|
|
700
|
+
if (!existsSync4(manifestPath)) throw new Error(`Profile manifest does not exist: ${manifestPath}`);
|
|
701
|
+
let raw;
|
|
702
|
+
try {
|
|
703
|
+
raw = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
704
|
+
} catch (error) {
|
|
705
|
+
throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
706
|
+
}
|
|
707
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
|
|
708
|
+
const record = raw;
|
|
709
|
+
const schemaVersion = requiredString(record, "schema_version");
|
|
710
|
+
if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
|
|
711
|
+
const profileId = requiredString(record, "profile_id");
|
|
712
|
+
if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
|
|
713
|
+
if (namedProfileId && namedProfileId !== profileId) {
|
|
714
|
+
throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
|
|
715
|
+
}
|
|
716
|
+
const expectedRaw = record.expected_runtime;
|
|
717
|
+
if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
|
|
718
|
+
throw new Error("Profile expected_runtime must be an object");
|
|
719
|
+
}
|
|
720
|
+
const expected = expectedRaw;
|
|
721
|
+
const expectedGitSha = expected ? optionalString(expected, "git_sha") : void 0;
|
|
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);
|
|
725
|
+
const migrationsRaw = record.required_schema_migrations;
|
|
726
|
+
if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
|
|
727
|
+
throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
|
|
728
|
+
}
|
|
729
|
+
const environment = validateEnvironment(requiredString(record, "environment"));
|
|
730
|
+
const expectedChannel = validateChannel(expected?.channel);
|
|
731
|
+
if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
|
|
732
|
+
throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
|
|
733
|
+
}
|
|
734
|
+
const manifest = {
|
|
735
|
+
asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
|
|
736
|
+
database_path: resolveManifestPath(manifestPath, requiredString(record, "database_path")),
|
|
737
|
+
environment,
|
|
738
|
+
...expected ? {
|
|
739
|
+
expected_runtime: {
|
|
740
|
+
...expectedChannel ? { channel: expectedChannel } : {},
|
|
741
|
+
...expectedCodeFingerprint ? { code_fingerprint: expectedCodeFingerprint } : {},
|
|
742
|
+
...expectedCodeOrigin ? { code_origin: expectedCodeOrigin } : {},
|
|
743
|
+
...expectedGitSha ? { git_sha: expectedGitSha } : {},
|
|
744
|
+
...expectedVersion ? { version: expectedVersion } : {}
|
|
745
|
+
}
|
|
746
|
+
} : {},
|
|
747
|
+
profile_id: profileId,
|
|
748
|
+
...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
|
|
749
|
+
schema_version: lineageProfileSchemaVersion,
|
|
750
|
+
service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
|
|
751
|
+
};
|
|
752
|
+
return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
|
|
753
|
+
}
|
|
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),
|
|
758
|
+
environment: profile.environment,
|
|
759
|
+
profile_id: profile.profile_id,
|
|
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}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function assertProfileChannel(profile, channel) {
|
|
779
|
+
const requiredChannel = environmentChannel(profile.environment);
|
|
780
|
+
if (channel === requiredChannel) return;
|
|
781
|
+
if (profile.environment === "production") {
|
|
782
|
+
throw new Error(`Refusing to open production profile ${profile.profile_id} from ${channel} code; use the stable channel`);
|
|
783
|
+
}
|
|
784
|
+
throw new Error(`Profile ${profile.profile_id} requires the ${requiredChannel} channel, not ${channel}`);
|
|
785
|
+
}
|
|
786
|
+
function tableExists(database, table) {
|
|
787
|
+
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
788
|
+
}
|
|
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));
|
|
792
|
+
}
|
|
793
|
+
function inspectDatabase(profile) {
|
|
794
|
+
const result = {
|
|
795
|
+
exists: existsSync4(profile.database_path),
|
|
796
|
+
migration_keys: [],
|
|
797
|
+
path: profile.database_path
|
|
798
|
+
};
|
|
799
|
+
if (!result.exists) return result;
|
|
800
|
+
try {
|
|
801
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
802
|
+
const database = new DatabaseSync(profile.database_path, { readOnly: true });
|
|
803
|
+
try {
|
|
804
|
+
if (tableExists(database, "lineage_profile_identity")) {
|
|
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();
|
|
808
|
+
if (rows.length === 1) {
|
|
809
|
+
const identity2 = {
|
|
810
|
+
bound_at: typeof rows[0].bound_at === "string" ? rows[0].bound_at : void 0,
|
|
811
|
+
environment: String(rows[0].environment),
|
|
812
|
+
profile_fingerprint: typeof rows[0].profile_fingerprint === "string" ? rows[0].profile_fingerprint : "",
|
|
813
|
+
profile_id: String(rows[0].profile_id)
|
|
814
|
+
};
|
|
815
|
+
result.identity = identity2;
|
|
816
|
+
} else {
|
|
817
|
+
result.error = `Expected exactly one lineage_profile_identity row, found ${rows.length}`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (tableExists(database, "lineage_schema_migrations")) {
|
|
821
|
+
result.migration_keys = database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
|
|
822
|
+
}
|
|
823
|
+
} finally {
|
|
824
|
+
database.close();
|
|
825
|
+
}
|
|
826
|
+
} catch (error) {
|
|
827
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
828
|
+
}
|
|
829
|
+
return result;
|
|
830
|
+
}
|
|
831
|
+
function doctorLineageProfile(selector, runtime) {
|
|
832
|
+
const checks = [];
|
|
833
|
+
const result = {
|
|
834
|
+
checks,
|
|
835
|
+
ok: false,
|
|
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
|
+
},
|
|
844
|
+
schema_version: lineageProfileDoctorSchemaVersion
|
|
845
|
+
};
|
|
846
|
+
let profile;
|
|
847
|
+
try {
|
|
848
|
+
profile = resolveLineageProfile(selector);
|
|
849
|
+
result.profile = profile;
|
|
850
|
+
result.manifest_path = profile.manifest_path;
|
|
851
|
+
checks.push({ id: "manifest", message: `Loaded ${profile.profile_id}`, status: "pass" });
|
|
852
|
+
} catch (error) {
|
|
853
|
+
checks.push({ id: "manifest", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
854
|
+
return result;
|
|
855
|
+
}
|
|
856
|
+
try {
|
|
857
|
+
assertProfileChannel(profile, runtime.channel);
|
|
858
|
+
checks.push({ id: "runtime_channel", message: `${runtime.channel} code matches ${profile.environment}`, status: "pass" });
|
|
859
|
+
} catch (error) {
|
|
860
|
+
checks.push({ id: "runtime_channel", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
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
|
+
}
|
|
868
|
+
if (profile.expected_runtime?.version && profile.expected_runtime.version !== runtime.version) {
|
|
869
|
+
checks.push({ id: "runtime_version", message: `Expected version ${profile.expected_runtime.version}, found ${runtime.version}`, status: "fail" });
|
|
870
|
+
} else {
|
|
871
|
+
checks.push({ id: "runtime_version", message: `Runtime version ${runtime.version}`, status: "pass" });
|
|
872
|
+
}
|
|
873
|
+
if (profile.expected_runtime?.git_sha && profile.expected_runtime.git_sha !== runtime.gitSha) {
|
|
874
|
+
checks.push({ id: "runtime_git_sha", message: `Expected Git SHA ${profile.expected_runtime.git_sha}, found ${runtime.gitSha || "unavailable"}`, status: "fail" });
|
|
875
|
+
} else if (profile.expected_runtime?.git_sha) {
|
|
876
|
+
checks.push({ id: "runtime_git_sha", message: `Git SHA ${runtime.gitSha}`, status: "pass" });
|
|
877
|
+
}
|
|
878
|
+
const assetExists = existsSync4(profile.asset_root);
|
|
879
|
+
const assetIsDirectory = assetExists && statSync3(profile.asset_root).isDirectory();
|
|
880
|
+
result.asset_root = { exists: assetExists, is_directory: assetIsDirectory, path: profile.asset_root };
|
|
881
|
+
checks.push({
|
|
882
|
+
id: "asset_root",
|
|
883
|
+
message: assetIsDirectory ? `Asset root exists: ${profile.asset_root}` : `Asset root is missing or not a directory: ${profile.asset_root}`,
|
|
884
|
+
status: assetIsDirectory ? "pass" : "fail"
|
|
885
|
+
});
|
|
886
|
+
const database = inspectDatabase(profile);
|
|
887
|
+
result.database = database;
|
|
888
|
+
checks.push({
|
|
889
|
+
id: "database_exists",
|
|
890
|
+
message: database.exists ? `Database exists: ${profile.database_path}` : `Database does not exist: ${profile.database_path}`,
|
|
891
|
+
status: database.exists ? "pass" : "fail"
|
|
892
|
+
});
|
|
893
|
+
if (database.error) checks.push({ id: "database_read", message: database.error, status: "fail" });
|
|
894
|
+
if (!database.identity) {
|
|
895
|
+
checks.push({ id: "database_identity", message: "Database is not bound to a Lineage profile", status: "fail" });
|
|
896
|
+
} else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment || database.identity.profile_fingerprint !== profile.profile_fingerprint) {
|
|
897
|
+
checks.push({
|
|
898
|
+
id: "database_identity",
|
|
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}`,
|
|
900
|
+
status: "fail"
|
|
901
|
+
});
|
|
902
|
+
} else {
|
|
903
|
+
checks.push({ id: "database_identity", message: `Database is bound to ${profile.profile_id}`, status: "pass" });
|
|
904
|
+
}
|
|
905
|
+
const missingMigrations = (profile.required_schema_migrations || []).filter((key) => !database.migration_keys.includes(key));
|
|
906
|
+
checks.push({
|
|
907
|
+
id: "database_schema",
|
|
908
|
+
message: missingMigrations.length ? `Missing required schema migrations: ${missingMigrations.join(", ")}` : `${database.migration_keys.length} schema migration marker(s) available`,
|
|
909
|
+
status: missingMigrations.length ? "fail" : "pass"
|
|
910
|
+
});
|
|
911
|
+
result.ok = checks.every((check) => check.status !== "fail");
|
|
912
|
+
return result;
|
|
913
|
+
}
|
|
914
|
+
function runtimeProfileIdentity(channel) {
|
|
915
|
+
const selector = process.env.LINEAGE_PROFILE;
|
|
916
|
+
const id = process.env.LINEAGE_PROFILE_ID;
|
|
917
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
918
|
+
if (selector && id && environment) {
|
|
919
|
+
return {
|
|
920
|
+
bound: true,
|
|
921
|
+
environment,
|
|
922
|
+
fingerprint: process.env.LINEAGE_PROFILE_FINGERPRINT,
|
|
923
|
+
id,
|
|
924
|
+
manifest_path: process.env.LINEAGE_PROFILE_MANIFEST,
|
|
925
|
+
service_origin: process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
bound: false,
|
|
930
|
+
environment: channelEnvironment(channel),
|
|
931
|
+
id: "legacy-unbound",
|
|
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."
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function assertUnselectedDatabaseIsUnbound(runtime) {
|
|
936
|
+
if (!runtime.schema.profile_id) return;
|
|
937
|
+
const environment = runtime.schema.profile_environment || "unknown";
|
|
938
|
+
throw new Error(
|
|
939
|
+
`Database ${runtime.database.path} is bound to Lineage profile ${runtime.schema.profile_id}/${environment}; select that profile with --profile`
|
|
940
|
+
);
|
|
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 identity2 = 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(identity2.profile_id, identity2.environment, identity2.profile_fingerprint, identity2.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 identity2;
|
|
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 identity2 = bindOpenDatabase(database, profile, false);
|
|
1025
|
+
return {
|
|
1026
|
+
already_bound: Boolean(before?.profile_fingerprint === profile.profile_fingerprint),
|
|
1027
|
+
database_path: profile.database_path,
|
|
1028
|
+
identity: identity2,
|
|
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 identity2;
|
|
1056
|
+
try {
|
|
1057
|
+
identity2 = 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: identity2
|
|
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
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/server/profileWriterLease.ts
|
|
1234
|
+
var writerLeaseSchemaVersion = "lineage.profile_writer_lease.v1";
|
|
1235
|
+
var ownerFileName = "owner.json";
|
|
1236
|
+
var ProfileWriterLeaseConflictError = class extends Error {
|
|
1237
|
+
constructor(message, owner) {
|
|
1238
|
+
super(message);
|
|
1239
|
+
this.owner = owner;
|
|
1240
|
+
this.name = "ProfileWriterLeaseConflictError";
|
|
1241
|
+
}
|
|
1242
|
+
owner;
|
|
1243
|
+
};
|
|
1244
|
+
function profileWriterLockPath(profile) {
|
|
1245
|
+
return join5(dirname4(profile.manifest_path), "writer.lock");
|
|
1246
|
+
}
|
|
1247
|
+
function ownerPath(lockPath) {
|
|
1248
|
+
return join5(lockPath, ownerFileName);
|
|
1249
|
+
}
|
|
1250
|
+
function errorCode(error) {
|
|
1251
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
1252
|
+
}
|
|
1253
|
+
function readOwner(lockPath) {
|
|
1254
|
+
const stat = lstatSync(lockPath);
|
|
1255
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
1256
|
+
throw new Error(`Writer lease path is not a safe directory: ${lockPath}; manual inspection is required`);
|
|
1257
|
+
}
|
|
1258
|
+
let raw;
|
|
1259
|
+
try {
|
|
1260
|
+
raw = JSON.parse(readFileSync4(ownerPath(lockPath), "utf8"));
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
throw new Error(`Writer lease metadata is unreadable at ${lockPath}; refusing automatic recovery`, { cause: error });
|
|
1263
|
+
}
|
|
1264
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1265
|
+
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
1266
|
+
}
|
|
1267
|
+
const owner = raw;
|
|
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) {
|
|
1269
|
+
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
1270
|
+
}
|
|
1271
|
+
return owner;
|
|
1272
|
+
}
|
|
1273
|
+
function processIsAlive(pid) {
|
|
1274
|
+
try {
|
|
1275
|
+
process.kill(pid, 0);
|
|
1276
|
+
return true;
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
if (errorCode(error) === "ESRCH") return false;
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function createLeaseDirectory(lockPath, owner) {
|
|
1283
|
+
mkdirSync4(lockPath, { mode: 448 });
|
|
1284
|
+
try {
|
|
1285
|
+
writeFileSync3(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
|
|
1286
|
+
`, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
rmSync2(lockPath, { force: true, recursive: true });
|
|
1289
|
+
throw error;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function reclaimDeadOwner(lockPath, owner) {
|
|
1293
|
+
if (processIsAlive(owner.pid)) {
|
|
1294
|
+
const { token: _token, ...publicOwner } = owner;
|
|
1295
|
+
throw new ProfileWriterLeaseConflictError(
|
|
1296
|
+
`Lineage profile ${owner.profile_id} already has an active ${owner.role} writer (pid ${owner.pid}); use that managed service or stop it before starting another writer`,
|
|
1297
|
+
publicOwner
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
const tokenFence = createHash3("sha256").update(owner.token).digest("hex").slice(0, 24);
|
|
1301
|
+
const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
|
|
1302
|
+
try {
|
|
1303
|
+
renameSync(lockPath, tombstone);
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (errorCode(error) === "ENOENT") return;
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
rmSync2(tombstone, { force: true, recursive: true });
|
|
1309
|
+
}
|
|
1310
|
+
function acquireProfileWriterLease(profile, channel, role = "service") {
|
|
1311
|
+
assertProfileChannel(profile, channel);
|
|
1312
|
+
const lockPath = profileWriterLockPath(profile);
|
|
1313
|
+
const owner = {
|
|
1314
|
+
acquired_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1315
|
+
environment: profile.environment,
|
|
1316
|
+
pid: process.pid,
|
|
1317
|
+
profile_fingerprint: profile.profile_fingerprint,
|
|
1318
|
+
profile_id: profile.profile_id,
|
|
1319
|
+
role,
|
|
1320
|
+
schema_version: writerLeaseSchemaVersion,
|
|
1321
|
+
...role === "service" ? { service_origin: profile.service_origin } : {},
|
|
1322
|
+
token: randomUUID2()
|
|
1323
|
+
};
|
|
1324
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1325
|
+
try {
|
|
1326
|
+
createLeaseDirectory(lockPath, owner);
|
|
1327
|
+
process.env.LINEAGE_WRITER_LEASE_TOKEN = owner.token;
|
|
1328
|
+
process.env.LINEAGE_WRITER_LOCK_PATH = lockPath;
|
|
1329
|
+
const { token: _token, ...publicOwner } = owner;
|
|
1330
|
+
let released = false;
|
|
1331
|
+
return {
|
|
1332
|
+
authenticate: (candidate) => {
|
|
1333
|
+
if (!candidate) return false;
|
|
1334
|
+
const expected = Buffer.from(owner.token);
|
|
1335
|
+
const actual = Buffer.from(candidate);
|
|
1336
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
1337
|
+
},
|
|
1338
|
+
lock_path: lockPath,
|
|
1339
|
+
owner: publicOwner,
|
|
1340
|
+
release: () => {
|
|
1341
|
+
if (released) return;
|
|
1342
|
+
released = true;
|
|
1343
|
+
try {
|
|
1344
|
+
const current = readOwner(lockPath);
|
|
1345
|
+
if (current.token === owner.token && current.pid === owner.pid) rmSync2(lockPath, { force: true, recursive: true });
|
|
1346
|
+
} catch {
|
|
1347
|
+
}
|
|
1348
|
+
if (process.env.LINEAGE_WRITER_LEASE_TOKEN === owner.token) delete process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
1349
|
+
if (process.env.LINEAGE_WRITER_LOCK_PATH === lockPath) delete process.env.LINEAGE_WRITER_LOCK_PATH;
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
1354
|
+
let existing;
|
|
1355
|
+
try {
|
|
1356
|
+
existing = readOwner(lockPath);
|
|
1357
|
+
} catch (readError) {
|
|
1358
|
+
if (errorCode(readError) === "ENOENT") continue;
|
|
1359
|
+
throw readError;
|
|
1360
|
+
}
|
|
1361
|
+
if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment || existing.profile_fingerprint !== profile.profile_fingerprint) {
|
|
1362
|
+
throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; manual inspection is required`, { cause: error });
|
|
1363
|
+
}
|
|
1364
|
+
reclaimDeadOwner(lockPath, existing);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
throw new Error(`Could not acquire writer lease for Lineage profile ${profile.profile_id}; another writer is racing for ownership`);
|
|
1368
|
+
}
|
|
1369
|
+
function assertProfileWriterLeaseHeld() {
|
|
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
|
+
}
|
|
1373
|
+
const profileId = process.env.LINEAGE_PROFILE_ID;
|
|
1374
|
+
const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
|
|
1375
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
1376
|
+
const manifestPath = process.env.LINEAGE_PROFILE_MANIFEST;
|
|
1377
|
+
const lockPath = process.env.LINEAGE_WRITER_LOCK_PATH;
|
|
1378
|
+
const token = process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
1379
|
+
if (!profileId || !profileFingerprint || !environment || !manifestPath || !lockPath || !token) {
|
|
1380
|
+
throw new Error("Named Lineage profiles may write only through a process holding the profile writer lease");
|
|
1381
|
+
}
|
|
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");
|
|
1384
|
+
const owner = readOwner(lockPath);
|
|
1385
|
+
if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.profile_fingerprint !== profileFingerprint || owner.environment !== environment) {
|
|
1386
|
+
throw new Error("Current process does not own the selected Lineage profile writer lease");
|
|
1387
|
+
}
|
|
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 identity2 = rows[0];
|
|
1405
|
+
if (identity2.profile_id !== profileId || identity2.environment !== environment || identity2.profile_fingerprint !== profileFingerprint) {
|
|
1406
|
+
throw new Error(
|
|
1407
|
+
`Refusing writable open: database identity ${String(identity2.profile_id)}/${String(identity2.environment)}/${String(identity2.profile_fingerprint)} does not match selected profile ${profileId}/${environment}/${profileFingerprint}`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
function getProfileWriterDelegation(profile) {
|
|
1412
|
+
const lockPath = profileWriterLockPath(profile);
|
|
1413
|
+
if (!existsSync5(lockPath)) {
|
|
1414
|
+
throw new Error(
|
|
1415
|
+
`Lineage profile ${profile.profile_id} mutating commands require its managed service at ${profile.service_origin}; no active service writer lease was found`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
const owner = readOwner(lockPath);
|
|
1419
|
+
if (owner.profile_id !== profile.profile_id || owner.environment !== profile.environment || owner.profile_fingerprint !== profile.profile_fingerprint) {
|
|
1420
|
+
throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; refusing delegation`);
|
|
1421
|
+
}
|
|
1422
|
+
if (owner.role !== "service" || owner.service_origin !== profile.service_origin) {
|
|
1423
|
+
throw new Error(`Writer lease for Lineage profile ${profile.profile_id} is not the configured managed service at ${profile.service_origin}`);
|
|
1424
|
+
}
|
|
1425
|
+
if (!processIsAlive(owner.pid)) {
|
|
1426
|
+
throw new Error(`Managed service writer lease for Lineage profile ${profile.profile_id} is stale; refusing delegation`);
|
|
1427
|
+
}
|
|
1428
|
+
const { token, ...publicOwner } = owner;
|
|
1429
|
+
return { owner: publicOwner, service_origin: owner.service_origin, token };
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/server/assetLineageDb.ts
|
|
1433
|
+
var require3 = createRequire2(import.meta.url);
|
|
592
1434
|
function nowIso() {
|
|
593
1435
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
594
1436
|
}
|
|
595
1437
|
function lineageDbPath() {
|
|
596
|
-
return process.env.LINEAGE_DB ||
|
|
1438
|
+
return process.env.LINEAGE_DB || join6(packageRoot, ".lineage", "asset-lineage.sqlite");
|
|
597
1439
|
}
|
|
598
1440
|
function lineageDb() {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1441
|
+
const readOnly = process.env.LINEAGE_DB_ACCESS === "read-only";
|
|
1442
|
+
if (!readOnly) assertProfileWriterLeaseHeld();
|
|
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`);
|
|
1445
|
+
}
|
|
1446
|
+
if (!readOnly) mkdirSync5(join6(lineageDbPath(), ".."), { recursive: true });
|
|
1447
|
+
const { DatabaseSync } = require3("node:sqlite");
|
|
1448
|
+
const database = readOnly ? new DatabaseSync(lineageDbPath(), { readOnly: true }) : new DatabaseSync(lineageDbPath());
|
|
1449
|
+
if (process.env.LINEAGE_PROFILE) assertSelectedProfileDatabaseIdentity(database);
|
|
602
1450
|
database.exec("PRAGMA foreign_keys = ON");
|
|
603
1451
|
database.exec("PRAGMA busy_timeout = 5000");
|
|
1452
|
+
if (readOnly) return database;
|
|
1453
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
604
1454
|
database.exec(`
|
|
605
1455
|
create table if not exists projects (
|
|
606
1456
|
id text primary key,
|
|
@@ -1316,12 +2166,12 @@ function randomId(prefix) {
|
|
|
1316
2166
|
return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
|
|
1317
2167
|
}
|
|
1318
2168
|
function tokenHash(token) {
|
|
1319
|
-
return
|
|
2169
|
+
return createHash4("sha256").update(token).digest("hex");
|
|
1320
2170
|
}
|
|
1321
2171
|
function safeEqual(a, b) {
|
|
1322
2172
|
const left = Buffer.from(a);
|
|
1323
2173
|
const right = Buffer.from(b);
|
|
1324
|
-
return left.length === right.length &&
|
|
2174
|
+
return left.length === right.length && timingSafeEqual2(left, right);
|
|
1325
2175
|
}
|
|
1326
2176
|
function expiresAtFrom(timestamp, ttlSeconds) {
|
|
1327
2177
|
return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
|
|
@@ -1348,6 +2198,12 @@ function derivedState(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1348
2198
|
}
|
|
1349
2199
|
function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
1350
2200
|
const heartbeatAt = String(row.heartbeat_at);
|
|
2201
|
+
const persistedStatus = String(row.status);
|
|
2202
|
+
const state = derivedState({
|
|
2203
|
+
expires_at: String(row.expires_at),
|
|
2204
|
+
heartbeat_at: heartbeatAt,
|
|
2205
|
+
status: persistedStatus
|
|
2206
|
+
}, now);
|
|
1351
2207
|
return {
|
|
1352
2208
|
id: String(row.id),
|
|
1353
2209
|
project: String(row.project_id),
|
|
@@ -1359,7 +2215,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1359
2215
|
agent_name: String(row.agent_name),
|
|
1360
2216
|
agent_kind: String(row.agent_kind),
|
|
1361
2217
|
thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
|
|
1362
|
-
status:
|
|
2218
|
+
status: persistedStatus === "active" && state === "expired" ? "expired" : persistedStatus,
|
|
1363
2219
|
created_at: String(row.created_at),
|
|
1364
2220
|
heartbeat_at: heartbeatAt,
|
|
1365
2221
|
expires_at: String(row.expires_at),
|
|
@@ -1369,11 +2225,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1369
2225
|
override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
|
|
1370
2226
|
metadata: parseMetadata(row.metadata_json),
|
|
1371
2227
|
heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
|
|
1372
|
-
derived_state:
|
|
1373
|
-
expires_at: String(row.expires_at),
|
|
1374
|
-
heartbeat_at: heartbeatAt,
|
|
1375
|
-
status: String(row.status)
|
|
1376
|
-
}, now)
|
|
2228
|
+
derived_state: state
|
|
1377
2229
|
};
|
|
1378
2230
|
}
|
|
1379
2231
|
function eventToRow(row) {
|
|
@@ -1515,7 +2367,7 @@ function createAgentClaim(fields) {
|
|
|
1515
2367
|
function listAgentClaims(project) {
|
|
1516
2368
|
const database = lineageDb();
|
|
1517
2369
|
try {
|
|
1518
|
-
expireActiveClaims(database);
|
|
2370
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
|
|
1519
2371
|
const rows = project ? database.prepare("select * from agent_claims where project_id = ? order by status, heartbeat_at desc").all(project) : database.prepare("select * from agent_claims order by project_id, status, heartbeat_at desc").all();
|
|
1520
2372
|
return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
|
|
1521
2373
|
} finally {
|
|
@@ -1525,7 +2377,7 @@ function listAgentClaims(project) {
|
|
|
1525
2377
|
function inspectAgentClaim(claimId, project) {
|
|
1526
2378
|
const database = lineageDb();
|
|
1527
2379
|
try {
|
|
1528
|
-
expireActiveClaims(database);
|
|
2380
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
|
|
1529
2381
|
const claim = findClaimById(database, claimId, project);
|
|
1530
2382
|
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1531
2383
|
const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
|
|
@@ -1662,7 +2514,7 @@ function validateAgentClaimForWrite(fields) {
|
|
|
1662
2514
|
}
|
|
1663
2515
|
|
|
1664
2516
|
// src/server/assetLineage.ts
|
|
1665
|
-
import { join as
|
|
2517
|
+
import { join as join7 } from "node:path";
|
|
1666
2518
|
|
|
1667
2519
|
// src/server/assetLineageSelection.ts
|
|
1668
2520
|
function selectedRows(database, project, root) {
|
|
@@ -2213,9 +3065,18 @@ function knownRoots(database, project) {
|
|
|
2213
3065
|
and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
|
|
2214
3066
|
group by parent_asset_id
|
|
2215
3067
|
`).all(project, project, project, project);
|
|
2216
|
-
|
|
3068
|
+
const roots = /* @__PURE__ */ new Map();
|
|
3069
|
+
for (const row of rows) {
|
|
3070
|
+
const existing = roots.get(row.root_asset_id);
|
|
3071
|
+
roots.set(row.root_asset_id, {
|
|
3072
|
+
root_asset_id: row.root_asset_id,
|
|
3073
|
+
selected_at: row.selected_at || existing?.selected_at
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
return [...roots.values()];
|
|
2217
3077
|
}
|
|
2218
3078
|
function seedLegacyWorkspaces(database, project) {
|
|
3079
|
+
if (process.env.LINEAGE_DB_ACCESS === "read-only") return;
|
|
2219
3080
|
ensureProject2(database, project);
|
|
2220
3081
|
const timestamp = nowIso();
|
|
2221
3082
|
const statement = database.prepare(`
|
|
@@ -2239,6 +3100,34 @@ function seedLegacyWorkspaces(database, project) {
|
|
|
2239
3100
|
);
|
|
2240
3101
|
}
|
|
2241
3102
|
}
|
|
3103
|
+
function inferredLegacyWorkspaces(database, project) {
|
|
3104
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") return [];
|
|
3105
|
+
const persistedRoots = new Set(
|
|
3106
|
+
database.prepare("select root_asset_id from lineage_workspaces where project_id = ?").all(project).map((row) => row.root_asset_id)
|
|
3107
|
+
);
|
|
3108
|
+
const asset = database.prepare("select id, title from assets where project_id = ? and id = ?");
|
|
3109
|
+
return knownRoots(database, project).flatMap((root) => {
|
|
3110
|
+
if (persistedRoots.has(root.root_asset_id)) return [];
|
|
3111
|
+
const row = asset.get(project, root.root_asset_id);
|
|
3112
|
+
if (!row) return [];
|
|
3113
|
+
const timestamp = root.selected_at || nowIso();
|
|
3114
|
+
return [{
|
|
3115
|
+
active_at: root.selected_at,
|
|
3116
|
+
created_at: timestamp,
|
|
3117
|
+
created_by: "system",
|
|
3118
|
+
id: lineageWorkspaceId(project, root.root_asset_id),
|
|
3119
|
+
project,
|
|
3120
|
+
root_asset_id: root.root_asset_id,
|
|
3121
|
+
status: "active",
|
|
3122
|
+
title: `${row.title} lineage`,
|
|
3123
|
+
updated_at: timestamp
|
|
3124
|
+
}];
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
function sortWorkspaces(workspaces) {
|
|
3128
|
+
const statusRank = { active: 0, paused: 1, archived: 2 };
|
|
3129
|
+
return workspaces.sort((left, right) => statusRank[left.status] - statusRank[right.status] || (right.active_at || "").localeCompare(left.active_at || "") || right.updated_at.localeCompare(left.updated_at) || left.title.localeCompare(right.title));
|
|
3130
|
+
}
|
|
2242
3131
|
function listRows(database, project) {
|
|
2243
3132
|
return database.prepare(`
|
|
2244
3133
|
select * from lineage_workspaces
|
|
@@ -2250,23 +3139,15 @@ function listRows(database, project) {
|
|
|
2250
3139
|
title
|
|
2251
3140
|
`).all(project).map(rowToWorkspace);
|
|
2252
3141
|
}
|
|
2253
|
-
function activeWorkspace(database, project) {
|
|
2254
|
-
const row = database.prepare(`
|
|
2255
|
-
select * from lineage_workspaces
|
|
2256
|
-
where project_id = ? and status != 'archived'
|
|
2257
|
-
order by active_at desc nulls last, updated_at desc
|
|
2258
|
-
limit 1
|
|
2259
|
-
`).get(project);
|
|
2260
|
-
return row ? rowToWorkspace(row) : null;
|
|
2261
|
-
}
|
|
2262
3142
|
function listLineageWorkspaces(project) {
|
|
2263
3143
|
const database = lineageDb();
|
|
2264
3144
|
try {
|
|
2265
3145
|
seedLegacyWorkspaces(database, project);
|
|
3146
|
+
const workspaces = sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]);
|
|
2266
3147
|
return {
|
|
2267
3148
|
project,
|
|
2268
|
-
active_workspace:
|
|
2269
|
-
workspaces
|
|
3149
|
+
active_workspace: workspaces.find((workspace) => workspace.status !== "archived") || null,
|
|
3150
|
+
workspaces,
|
|
2270
3151
|
fetchedAt: nowIso()
|
|
2271
3152
|
};
|
|
2272
3153
|
} finally {
|
|
@@ -2277,7 +3158,7 @@ function activeLineageWorkspaceRoot(project) {
|
|
|
2277
3158
|
const database = lineageDb();
|
|
2278
3159
|
try {
|
|
2279
3160
|
seedLegacyWorkspaces(database, project);
|
|
2280
|
-
return
|
|
3161
|
+
return sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]).find((workspace) => workspace.status !== "archived")?.root_asset_id;
|
|
2281
3162
|
} finally {
|
|
2282
3163
|
database.close();
|
|
2283
3164
|
}
|
|
@@ -2336,7 +3217,7 @@ function upsertProject(database, project) {
|
|
|
2336
3217
|
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
2337
3218
|
values (?, ?, ?, ?, ?)
|
|
2338
3219
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
2339
|
-
`).run(project, project,
|
|
3220
|
+
`).run(project, project, join7(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
2340
3221
|
}
|
|
2341
3222
|
function upsertAsset(database, project, asset) {
|
|
2342
3223
|
const timestamp = nowIso();
|
|
@@ -2407,8 +3288,8 @@ function rootFor(database, project, assetId) {
|
|
|
2407
3288
|
}
|
|
2408
3289
|
function assertCanonicalRoot(database, project, root) {
|
|
2409
3290
|
requireAsset2(database, project, root);
|
|
2410
|
-
const
|
|
2411
|
-
if (
|
|
3291
|
+
const canonicalRoot2 = rootFor(database, project, root);
|
|
3292
|
+
if (canonicalRoot2 !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
|
|
2412
3293
|
}
|
|
2413
3294
|
function explicitWorkspaceRoot(database, project, assetId) {
|
|
2414
3295
|
const row = database.prepare(`
|
|
@@ -2975,16 +3856,34 @@ function clearLineageRerollRequest(project, fields) {
|
|
|
2975
3856
|
}
|
|
2976
3857
|
}
|
|
2977
3858
|
|
|
2978
|
-
// src/server/
|
|
2979
|
-
|
|
3859
|
+
// src/server/lineageRuntimeCommand.ts
|
|
3860
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
3861
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
3862
|
+
import { join as join8 } from "node:path";
|
|
3863
|
+
var require4 = createRequire3(import.meta.url);
|
|
3864
|
+
function lineagePublicPackageCommand() {
|
|
3865
|
+
if (process.env.LINEAGE_CHANNEL === "dev") {
|
|
3866
|
+
const sourceCli = join8(packageRoot, "src", "cli", "lineage-dev.ts");
|
|
3867
|
+
const builtCli = join8(packageRoot, "dist", "cli", "lineage-dev.js");
|
|
3868
|
+
return existsSync7(sourceCli) ? `${shellQuote(process.execPath)} --import ${shellQuote(require4.resolve("tsx"))} ${shellQuote(sourceCli)}` : `${shellQuote(process.execPath)} ${shellQuote(builtCli)}`;
|
|
3869
|
+
}
|
|
3870
|
+
if (process.env.LINEAGE_CHANNEL === "preview") return "LINEAGE_CHANNEL=preview npx --package @mean-weasel/lineage@next lineage-dev";
|
|
3871
|
+
return "npx @mean-weasel/lineage";
|
|
3872
|
+
}
|
|
2980
3873
|
function shellQuote(value) {
|
|
2981
3874
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
2982
3875
|
}
|
|
3876
|
+
function lineageRuntimeSelector() {
|
|
3877
|
+
const manifest = process.env.LINEAGE_PROFILE_MANIFEST?.trim();
|
|
3878
|
+
return manifest ? `--profile ${shellQuote(manifest)}` : `--db ${shellQuote(lineageDbPath())}`;
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
// src/server/assetLineageHandoff.ts
|
|
2983
3882
|
function lineageCommand(command, project, rootAssetId) {
|
|
2984
|
-
return `${
|
|
3883
|
+
return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
|
|
2985
3884
|
}
|
|
2986
3885
|
function linkChildCommand(project, rootAssetId) {
|
|
2987
|
-
return `${
|
|
3886
|
+
return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
2988
3887
|
}
|
|
2989
3888
|
function rerollImportGuidance(rootAssetId, targetAssetId) {
|
|
2990
3889
|
return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
|
|
@@ -3026,7 +3925,7 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
3026
3925
|
},
|
|
3027
3926
|
handoff: {
|
|
3028
3927
|
next_command: lineageCommand("next", project, next.root_asset_id),
|
|
3029
|
-
inspect_command: asset ? `${
|
|
3928
|
+
inspect_command: asset ? `${lineagePublicPackageCommand()} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} ${lineageRuntimeSelector()} --json` : void 0,
|
|
3030
3929
|
link_child_command: asset ? linkChildCommand(project, next.root_asset_id) : void 0
|
|
3031
3930
|
},
|
|
3032
3931
|
fetchedAt: nowIso()
|
|
@@ -3078,9 +3977,9 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
3078
3977
|
}
|
|
3079
3978
|
|
|
3080
3979
|
// src/server/generationReceipts.ts
|
|
3081
|
-
import { existsSync as
|
|
3082
|
-
import { relative as
|
|
3083
|
-
import { randomUUID } from "node:crypto";
|
|
3980
|
+
import { existsSync as existsSync8, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
|
|
3981
|
+
import { relative as relative3, resolve as resolve5 } from "node:path";
|
|
3982
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3084
3983
|
var adapterVersion = "generation-receipts-v1";
|
|
3085
3984
|
var provider = "codex-handoff";
|
|
3086
3985
|
var GenerationReceiptError = class extends Error {
|
|
@@ -3091,7 +3990,7 @@ var GenerationReceiptError = class extends Error {
|
|
|
3091
3990
|
status;
|
|
3092
3991
|
};
|
|
3093
3992
|
function jobId() {
|
|
3094
|
-
return `gen-${Date.now().toString(36)}-${
|
|
3993
|
+
return `gen-${Date.now().toString(36)}-${randomUUID3().slice(0, 8)}`;
|
|
3095
3994
|
}
|
|
3096
3995
|
function quote(value) {
|
|
3097
3996
|
return JSON.stringify(value);
|
|
@@ -3101,7 +4000,7 @@ function parseJson(value, fallback) {
|
|
|
3101
4000
|
return JSON.parse(value);
|
|
3102
4001
|
}
|
|
3103
4002
|
function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
|
|
3104
|
-
const importCommand =
|
|
4003
|
+
const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
3105
4004
|
return {
|
|
3106
4005
|
schema_version: "lineage.generation_handoff.v1",
|
|
3107
4006
|
provider,
|
|
@@ -3237,7 +4136,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
3237
4136
|
fetchedAt: timestamp
|
|
3238
4137
|
}
|
|
3239
4138
|
};
|
|
3240
|
-
const
|
|
4139
|
+
const preview2 = {
|
|
3241
4140
|
id,
|
|
3242
4141
|
project_id: project,
|
|
3243
4142
|
provider,
|
|
@@ -3263,7 +4162,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
3263
4162
|
created_at: timestamp
|
|
3264
4163
|
}]
|
|
3265
4164
|
};
|
|
3266
|
-
if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job:
|
|
4165
|
+
if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview2 };
|
|
3267
4166
|
const database = lineageDb();
|
|
3268
4167
|
try {
|
|
3269
4168
|
database.exec("BEGIN IMMEDIATE");
|
|
@@ -3275,7 +4174,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
3275
4174
|
) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
|
|
3276
4175
|
`).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
|
|
3277
4176
|
database.prepare("insert into generation_job_inputs (id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json) values (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(input.id, id, project, input.asset_id, input.root_asset_id, input.role, input.position, input.selection_strategy, JSON.stringify(input.selection_snapshot));
|
|
3278
|
-
insertReceipt(database, id, "plan", "reroll plan",
|
|
4177
|
+
insertReceipt(database, id, "plan", "reroll plan", preview2.receipts[0].payload);
|
|
3279
4178
|
database.exec("COMMIT");
|
|
3280
4179
|
} catch (error) {
|
|
3281
4180
|
database.exec("ROLLBACK");
|
|
@@ -3287,22 +4186,22 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
3287
4186
|
}
|
|
3288
4187
|
}
|
|
3289
4188
|
function isPathInside(child, parent) {
|
|
3290
|
-
const rel =
|
|
4189
|
+
const rel = relative3(parent, child);
|
|
3291
4190
|
return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
|
|
3292
4191
|
}
|
|
3293
4192
|
function resolveScratchFile(file) {
|
|
3294
|
-
const scratchRoot =
|
|
3295
|
-
const candidate = file.startsWith(".asset-scratch/") ||
|
|
4193
|
+
const scratchRoot = resolve5(repoRoot, ".asset-scratch");
|
|
4194
|
+
const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
|
|
3296
4195
|
if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
3297
|
-
if (!
|
|
3298
|
-
const realScratchRoot =
|
|
3299
|
-
const realCandidate =
|
|
4196
|
+
if (!existsSync8(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
|
|
4197
|
+
const realScratchRoot = realpathSync2(scratchRoot);
|
|
4198
|
+
const realCandidate = realpathSync2(candidate);
|
|
3300
4199
|
if (!isPathInside(realCandidate, realScratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
3301
|
-
const stats =
|
|
4200
|
+
const stats = statSync4(candidate);
|
|
3302
4201
|
if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
|
|
3303
4202
|
const checksum = fileSha256(candidate);
|
|
3304
4203
|
return {
|
|
3305
|
-
relativePath:
|
|
4204
|
+
relativePath: relative3(scratchRoot, candidate),
|
|
3306
4205
|
checksum,
|
|
3307
4206
|
size: stats.size,
|
|
3308
4207
|
contentType: contentTypeFor(candidate),
|
|
@@ -3378,17 +4277,19 @@ function importImageRerollOutput(project = defaultProject, fields) {
|
|
|
3378
4277
|
}
|
|
3379
4278
|
|
|
3380
4279
|
// src/server/lineageSelectionPacket.ts
|
|
3381
|
-
import { createHash as
|
|
3382
|
-
import { existsSync as
|
|
3383
|
-
import { isAbsolute, resolve as
|
|
4280
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
4281
|
+
import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
|
|
4282
|
+
import { isAbsolute as isAbsolute2, resolve as resolve6 } from "node:path";
|
|
3384
4283
|
var LineageSelectionPacketError = class extends Error {
|
|
3385
|
-
constructor(message, warnings = [], errors = []) {
|
|
4284
|
+
constructor(message, warnings = [], errors = [], diagnostics = []) {
|
|
3386
4285
|
super(message);
|
|
3387
4286
|
this.warnings = warnings;
|
|
3388
4287
|
this.errors = errors;
|
|
4288
|
+
this.diagnostics = diagnostics;
|
|
3389
4289
|
}
|
|
3390
4290
|
warnings;
|
|
3391
4291
|
errors;
|
|
4292
|
+
diagnostics;
|
|
3392
4293
|
};
|
|
3393
4294
|
function listAllAssets(project) {
|
|
3394
4295
|
const pageSize = 100;
|
|
@@ -3424,15 +4325,15 @@ function resolveWorkspace(project, options) {
|
|
|
3424
4325
|
}
|
|
3425
4326
|
function resolveLocalReference(reference) {
|
|
3426
4327
|
if (!reference) return void 0;
|
|
3427
|
-
if (
|
|
3428
|
-
const repoRelative =
|
|
3429
|
-
if (
|
|
3430
|
-
return
|
|
4328
|
+
if (isAbsolute2(reference)) return reference;
|
|
4329
|
+
const repoRelative = resolve6(repoRoot, reference);
|
|
4330
|
+
if (existsSync9(repoRelative)) return repoRelative;
|
|
4331
|
+
return resolve6(repoRoot, ".asset-scratch", reference);
|
|
3431
4332
|
}
|
|
3432
4333
|
function fileSize(path) {
|
|
3433
|
-
if (!path || !
|
|
4334
|
+
if (!path || !existsSync9(path)) return void 0;
|
|
3434
4335
|
try {
|
|
3435
|
-
return
|
|
4336
|
+
return statSync5(path).size;
|
|
3436
4337
|
} catch {
|
|
3437
4338
|
return void 0;
|
|
3438
4339
|
}
|
|
@@ -3458,7 +4359,7 @@ function currentAttemptFor(node) {
|
|
|
3458
4359
|
function assetForNode(node, catalogAsset, warnings) {
|
|
3459
4360
|
const localReference = catalogAsset?.local?.absolute_path || node.current_attempt?.file_path || node.local_path || catalogAsset?.local?.relative_path;
|
|
3460
4361
|
const absolutePath = catalogAsset?.local?.absolute_path || resolveLocalReference(localReference);
|
|
3461
|
-
const localExists = absolutePath ?
|
|
4362
|
+
const localExists = absolutePath ? existsSync9(absolutePath) : false;
|
|
3462
4363
|
const hasLocalClaim = Boolean(absolutePath || node.local_path || catalogAsset?.local?.relative_path);
|
|
3463
4364
|
const s3Key = catalogAsset?.s3?.key || node.s3_key;
|
|
3464
4365
|
const hasS3 = Boolean(s3Key);
|
|
@@ -3503,27 +4404,209 @@ function assetForNode(node, catalogAsset, warnings) {
|
|
|
3503
4404
|
};
|
|
3504
4405
|
}
|
|
3505
4406
|
function packetId(packetIdentity) {
|
|
3506
|
-
const digest =
|
|
4407
|
+
const digest = createHash5("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
|
|
3507
4408
|
return `lineage_packet_${digest}`;
|
|
3508
4409
|
}
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
4410
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
4411
|
+
function canonicalValue(value) {
|
|
4412
|
+
if (Array.isArray(value)) return value.map((item) => canonicalValue(item));
|
|
4413
|
+
if (value && typeof value === "object") {
|
|
4414
|
+
return Object.fromEntries(
|
|
4415
|
+
Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, canonicalValue(item)])
|
|
4416
|
+
);
|
|
4417
|
+
}
|
|
4418
|
+
return value;
|
|
4419
|
+
}
|
|
4420
|
+
function canonicalLineageSelectionPacketIdentityJson(projection) {
|
|
4421
|
+
return JSON.stringify(canonicalValue(projection));
|
|
4422
|
+
}
|
|
4423
|
+
function lineageSelectionPacketV2IdentityProjection(packet) {
|
|
4424
|
+
const assetsById = new Map(packet.assets.map((asset) => [asset.asset_id, asset]));
|
|
4425
|
+
const selection = packet.selection.items.map((item) => {
|
|
4426
|
+
const asset = assetsById.get(item.asset_id);
|
|
4427
|
+
if (!asset) {
|
|
4428
|
+
const diagnostics = [{ asset_id: item.asset_id, code: "selected_asset_missing", severity: "error" }];
|
|
4429
|
+
throw new LineageSelectionPacketError(
|
|
4430
|
+
`Selected asset ${item.asset_id} is absent from the v2 packet asset envelope.`,
|
|
4431
|
+
[],
|
|
4432
|
+
["selected_asset_missing"],
|
|
4433
|
+
diagnostics
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
return {
|
|
4437
|
+
asset_id: item.asset_id,
|
|
4438
|
+
campaign: asset.campaign,
|
|
4439
|
+
channel: asset.channel,
|
|
4440
|
+
current_attempt: {
|
|
4441
|
+
asset_id: asset.current_attempt.asset_id,
|
|
4442
|
+
attempt_index: asset.current_attempt.attempt_index,
|
|
4443
|
+
checksum_sha256: asset.current_attempt.checksum_sha256,
|
|
4444
|
+
id: asset.current_attempt.id,
|
|
4445
|
+
source: asset.current_attempt.source
|
|
4446
|
+
},
|
|
4447
|
+
media_type: asset.media_type,
|
|
4448
|
+
mime_type: asset.mime_type,
|
|
4449
|
+
position: item.position,
|
|
4450
|
+
selection_note: item.selection_note,
|
|
4451
|
+
title: asset.title
|
|
4452
|
+
};
|
|
4453
|
+
});
|
|
4454
|
+
return {
|
|
4455
|
+
schema_version: "lineage.selection_packet.v2",
|
|
4456
|
+
project: packet.project,
|
|
4457
|
+
product: packet.product,
|
|
4458
|
+
workspace: {
|
|
4459
|
+
id: packet.workspace.id,
|
|
4460
|
+
root_asset_id: packet.workspace.root_asset_id
|
|
4461
|
+
},
|
|
4462
|
+
context: {
|
|
4463
|
+
campaign: packet.context.campaign,
|
|
4464
|
+
channel: packet.context.channel,
|
|
4465
|
+
labels: packet.context.labels,
|
|
4466
|
+
notes: packet.context.notes
|
|
4467
|
+
},
|
|
4468
|
+
selection,
|
|
4469
|
+
diagnostics: packet.diagnostics.map((diagnostic) => ({
|
|
4470
|
+
code: diagnostic.code,
|
|
4471
|
+
severity: diagnostic.severity,
|
|
4472
|
+
...diagnostic.asset_id ? { asset_id: diagnostic.asset_id } : {}
|
|
4473
|
+
}))
|
|
4474
|
+
};
|
|
4475
|
+
}
|
|
4476
|
+
function lineageSelectionPacketV2IdentitySha256(packet) {
|
|
4477
|
+
const projection = lineageSelectionPacketV2IdentityProjection(packet);
|
|
4478
|
+
return createHash5("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
|
|
4479
|
+
}
|
|
4480
|
+
function addDiagnostic(diagnostics, diagnostic) {
|
|
4481
|
+
if (diagnostics.some((item) => item.code === diagnostic.code && item.severity === diagnostic.severity && item.asset_id === diagnostic.asset_id)) return;
|
|
4482
|
+
diagnostics.push(diagnostic);
|
|
4483
|
+
}
|
|
4484
|
+
function requireV2CurrentAttempt(node, warnings, diagnostics) {
|
|
4485
|
+
const attempt = node.current_attempt;
|
|
4486
|
+
if (!attempt) {
|
|
4487
|
+
const message = `Selected asset ${node.asset_id} has no current attempt identity.`;
|
|
4488
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_missing", severity: "error" };
|
|
4489
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_missing"], [...diagnostics, diagnostic]);
|
|
4490
|
+
}
|
|
4491
|
+
if (!attempt.id || !attempt.asset_id || !Number.isInteger(attempt.attempt_index) || attempt.attempt_index <= 0 || !attempt.source || attempt.is_current !== true) {
|
|
4492
|
+
const message = `Selected asset ${node.asset_id} has malformed current attempt identity.`;
|
|
4493
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_identity", severity: "error" };
|
|
4494
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_identity"], [...diagnostics, diagnostic]);
|
|
4495
|
+
}
|
|
4496
|
+
if (!attempt.checksum_sha256 || !SHA256_PATTERN.test(attempt.checksum_sha256)) {
|
|
4497
|
+
const message = `Selected asset ${node.asset_id} current attempt does not have a valid lowercase SHA-256 checksum.`;
|
|
4498
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_checksum", severity: "error" };
|
|
4499
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_checksum"], [...diagnostics, diagnostic]);
|
|
4500
|
+
}
|
|
4501
|
+
return {
|
|
4502
|
+
asset_id: attempt.asset_id,
|
|
4503
|
+
attempt_index: attempt.attempt_index,
|
|
4504
|
+
checksum_sha256: attempt.checksum_sha256,
|
|
4505
|
+
file_path: attempt.file_path,
|
|
4506
|
+
generation_job_id: attempt.generation_job_id,
|
|
4507
|
+
id: attempt.id,
|
|
4508
|
+
is_current: attempt.is_current,
|
|
4509
|
+
source: attempt.source
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
function assetForNodeV2(node, visibleCatalogAsset, catalogById, warnings, errors, diagnostics) {
|
|
4513
|
+
const currentAttempt = requireV2CurrentAttempt(node, warnings, diagnostics);
|
|
4514
|
+
const currentAttemptCatalogAsset = catalogById.get(currentAttempt.asset_id);
|
|
4515
|
+
const isVisibleNodeAttempt = currentAttempt.asset_id === node.asset_id;
|
|
4516
|
+
const mediaCatalogAsset = currentAttemptCatalogAsset || (isVisibleNodeAttempt ? visibleCatalogAsset : void 0);
|
|
4517
|
+
const localReference = currentAttempt.file_path || mediaCatalogAsset?.local?.absolute_path || mediaCatalogAsset?.local?.relative_path || (isVisibleNodeAttempt ? node.local_path : void 0);
|
|
4518
|
+
const absolutePath = resolveLocalReference(localReference);
|
|
4519
|
+
const localExists = absolutePath ? existsSync9(absolutePath) : false;
|
|
4520
|
+
const hasLocalClaim = Boolean(localReference);
|
|
4521
|
+
const s3Key = mediaCatalogAsset?.s3?.key || (isVisibleNodeAttempt ? node.s3_key : void 0);
|
|
4522
|
+
const hasS3 = Boolean(s3Key);
|
|
4523
|
+
const mimeType = mediaCatalogAsset?.local?.content_type || mediaCatalogAsset?.s3?.content_type || visibleCatalogAsset?.local?.content_type || visibleCatalogAsset?.s3?.content_type || (localReference ? contentTypeFor(localReference) : void 0);
|
|
4524
|
+
const mediaType = mediaCatalogAsset?.content_type || visibleCatalogAsset?.content_type || node.media_type;
|
|
4525
|
+
if (localExists && absolutePath && fileSha256(absolutePath) !== currentAttempt.checksum_sha256) {
|
|
4526
|
+
const message = `Selected asset ${node.asset_id} current attempt checksum does not match its local file.`;
|
|
4527
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
|
|
4528
|
+
throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
|
|
4529
|
+
}
|
|
4530
|
+
const s3Checksum = mediaCatalogAsset?.s3?.checksum_sha256;
|
|
4531
|
+
if (hasS3 && s3Checksum && s3Checksum !== currentAttempt.checksum_sha256) {
|
|
4532
|
+
const message = `Selected asset ${node.asset_id} current attempt checksum does not match its S3 media envelope.`;
|
|
4533
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
|
|
4534
|
+
throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
|
|
4535
|
+
}
|
|
4536
|
+
if (hasLocalClaim && !localExists) {
|
|
4537
|
+
warnings.push(`Selected asset ${node.asset_id} current attempt has a local path but the file is missing: ${absolutePath || localReference}`);
|
|
4538
|
+
}
|
|
4539
|
+
if (!hasLocalClaim && !hasS3) {
|
|
4540
|
+
const message = `Selected asset ${node.asset_id} current attempt has neither a local file nor S3 key.`;
|
|
4541
|
+
warnings.push(message);
|
|
4542
|
+
errors.push(message);
|
|
4543
|
+
}
|
|
4544
|
+
if (mediaType && mediaType !== "image" && mediaType !== "gif") {
|
|
4545
|
+
warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
|
|
4546
|
+
addDiagnostic(diagnostics, { asset_id: node.asset_id, code: "unsupported_media_type", severity: "warning" });
|
|
4547
|
+
}
|
|
4548
|
+
const conflictingChecksums = [
|
|
4549
|
+
currentAttemptCatalogAsset?.local?.checksum_sha256,
|
|
4550
|
+
currentAttemptCatalogAsset?.s3?.checksum_sha256,
|
|
4551
|
+
visibleCatalogAsset?.local?.checksum_sha256,
|
|
4552
|
+
visibleCatalogAsset?.s3?.checksum_sha256,
|
|
4553
|
+
node.checksum_sha256
|
|
4554
|
+
].filter((checksum) => Boolean(checksum && checksum !== currentAttempt.checksum_sha256));
|
|
4555
|
+
if (conflictingChecksums.length > 0) {
|
|
4556
|
+
warnings.push(`Selected asset ${node.asset_id} catalog or visible-node checksum differs from its current attempt; current attempt checksum is authoritative.`);
|
|
4557
|
+
}
|
|
4558
|
+
return {
|
|
4559
|
+
asset_id: node.asset_id,
|
|
4560
|
+
campaign: visibleCatalogAsset?.campaign || node.campaign,
|
|
4561
|
+
channel: visibleCatalogAsset?.channel || node.channel,
|
|
4562
|
+
checksum_sha256: currentAttempt.checksum_sha256,
|
|
4563
|
+
current_attempt: currentAttempt,
|
|
4564
|
+
local: {
|
|
4565
|
+
absolute_path: absolutePath,
|
|
4566
|
+
content_type: mediaCatalogAsset?.local?.content_type,
|
|
4567
|
+
exists: localExists,
|
|
4568
|
+
relative_path: mediaCatalogAsset?.local?.relative_path || currentAttempt.file_path || (isVisibleNodeAttempt ? node.local_path : void 0),
|
|
4569
|
+
size_bytes: mediaCatalogAsset?.local?.size_bytes || fileSize(absolutePath)
|
|
4570
|
+
},
|
|
4571
|
+
media_type: mediaType,
|
|
4572
|
+
mime_type: mimeType,
|
|
4573
|
+
review_notes: node.review_notes,
|
|
4574
|
+
review_state: node.review_state,
|
|
4575
|
+
s3: {
|
|
4576
|
+
bucket: mediaCatalogAsset?.s3?.bucket,
|
|
4577
|
+
checksum_sha256: mediaCatalogAsset?.s3?.checksum_sha256,
|
|
4578
|
+
content_type: mediaCatalogAsset?.s3?.content_type,
|
|
4579
|
+
etag: mediaCatalogAsset?.s3?.etag,
|
|
4580
|
+
key: s3Key,
|
|
4581
|
+
region: mediaCatalogAsset?.s3?.region,
|
|
4582
|
+
size_bytes: mediaCatalogAsset?.s3?.size_bytes,
|
|
4583
|
+
version_id: mediaCatalogAsset?.s3?.version_id
|
|
4584
|
+
},
|
|
4585
|
+
selection_note: node.selection_note,
|
|
4586
|
+
source: mediaCatalogAsset?.source || visibleCatalogAsset?.source || node.source,
|
|
4587
|
+
status: visibleCatalogAsset?.status || node.status,
|
|
4588
|
+
storage_state: storageState(hasLocalClaim, hasS3),
|
|
4589
|
+
title: visibleCatalogAsset?.title || node.title || node.asset_id
|
|
4590
|
+
};
|
|
4591
|
+
}
|
|
4592
|
+
function getLineageSelectionPacketV1(project, options = {}) {
|
|
4593
|
+
const workspace = resolveWorkspace(project, options);
|
|
4594
|
+
const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
|
|
4595
|
+
const catalogSummary = validateProject(project);
|
|
4596
|
+
const catalogAssets = listAllAssets(project);
|
|
4597
|
+
const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
|
|
4598
|
+
const warnings = [];
|
|
4599
|
+
const errors = [];
|
|
4600
|
+
const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
|
|
4601
|
+
const selectedItems = snapshot.selections.map((selection) => ({
|
|
4602
|
+
asset_id: selection.asset_id,
|
|
4603
|
+
position: selection.position,
|
|
4604
|
+
selected_at: selection.selected_at,
|
|
4605
|
+
selection_note: selection.notes
|
|
4606
|
+
}));
|
|
4607
|
+
if (selectedItems.length === 0) warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
|
|
4608
|
+
const assets = selectedItems.flatMap((item) => {
|
|
4609
|
+
const node = nodeById.get(item.asset_id);
|
|
3527
4610
|
if (!node) {
|
|
3528
4611
|
const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
|
|
3529
4612
|
warnings.push(message);
|
|
@@ -3549,7 +4632,7 @@ function getLineageSelectionPacket(project, options = {}) {
|
|
|
3549
4632
|
labels: options.labels || [],
|
|
3550
4633
|
notes: options.contextNotes
|
|
3551
4634
|
};
|
|
3552
|
-
const
|
|
4635
|
+
const identity2 = {
|
|
3553
4636
|
assets: assets.map((asset) => ({
|
|
3554
4637
|
asset_id: asset.asset_id,
|
|
3555
4638
|
checksum_sha256: asset.checksum_sha256,
|
|
@@ -3570,7 +4653,7 @@ function getLineageSelectionPacket(project, options = {}) {
|
|
|
3570
4653
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3571
4654
|
errors,
|
|
3572
4655
|
kind: "lineage.selection_packet",
|
|
3573
|
-
packet_id: packetId(
|
|
4656
|
+
packet_id: packetId(identity2),
|
|
3574
4657
|
product: catalogSummary.product,
|
|
3575
4658
|
project,
|
|
3576
4659
|
schema_version: "lineage.selection_packet.v1",
|
|
@@ -3597,16 +4680,149 @@ function getLineageSelectionPacket(project, options = {}) {
|
|
|
3597
4680
|
}
|
|
3598
4681
|
};
|
|
3599
4682
|
}
|
|
4683
|
+
function getLineageSelectionPacketV2(project, options) {
|
|
4684
|
+
const workspace = resolveWorkspace(project, options);
|
|
4685
|
+
const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
|
|
4686
|
+
const catalogSummary = validateProject(project);
|
|
4687
|
+
const catalogAssets = listAllAssets(project);
|
|
4688
|
+
const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
|
|
4689
|
+
const warnings = [];
|
|
4690
|
+
const errors = [];
|
|
4691
|
+
const diagnostics = [];
|
|
4692
|
+
const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
|
|
4693
|
+
const selectedItems = snapshot.selections.map((selection) => ({
|
|
4694
|
+
asset_id: selection.asset_id,
|
|
4695
|
+
position: selection.position,
|
|
4696
|
+
selected_at: selection.selected_at,
|
|
4697
|
+
selection_note: selection.notes
|
|
4698
|
+
}));
|
|
4699
|
+
if (selectedItems.length === 0) {
|
|
4700
|
+
warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
|
|
4701
|
+
addDiagnostic(diagnostics, { code: "empty_selection", severity: "warning" });
|
|
4702
|
+
}
|
|
4703
|
+
const assets = [];
|
|
4704
|
+
for (const item of selectedItems) {
|
|
4705
|
+
const node = nodeById.get(item.asset_id);
|
|
4706
|
+
if (!node) {
|
|
4707
|
+
const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
|
|
4708
|
+
warnings.push(message);
|
|
4709
|
+
errors.push(message);
|
|
4710
|
+
const diagnostic = { asset_id: item.asset_id, code: "selected_asset_outside_workspace", severity: "error" };
|
|
4711
|
+
addDiagnostic(diagnostics, diagnostic);
|
|
4712
|
+
throw new LineageSelectionPacketError(message, warnings, ["selected_asset_outside_workspace"], diagnostics);
|
|
4713
|
+
}
|
|
4714
|
+
assets.push(assetForNodeV2(node, catalogById.get(item.asset_id), catalogById, warnings, errors, diagnostics));
|
|
4715
|
+
}
|
|
4716
|
+
if (assets.some((asset) => !asset.dimensions)) {
|
|
4717
|
+
warnings.push("Image dimensions are unavailable for one or more selected assets.");
|
|
4718
|
+
}
|
|
4719
|
+
if (options.strict) {
|
|
4720
|
+
const strictErrors = [
|
|
4721
|
+
...selectedItems.length === 0 ? ["empty_selection"] : [],
|
|
4722
|
+
...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
|
|
4723
|
+
...assets.filter((asset) => asset.storage_state === "unresolved").map((asset) => `unresolved_current_attempt_media:${asset.asset_id}`),
|
|
4724
|
+
...diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => `${diagnostic.code}${diagnostic.asset_id ? `:${diagnostic.asset_id}` : ""}`)
|
|
4725
|
+
];
|
|
4726
|
+
if (strictErrors.length > 0) {
|
|
4727
|
+
throw new LineageSelectionPacketError(
|
|
4728
|
+
`Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`,
|
|
4729
|
+
warnings,
|
|
4730
|
+
strictErrors,
|
|
4731
|
+
diagnostics
|
|
4732
|
+
);
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
const packet = {
|
|
4736
|
+
assets,
|
|
4737
|
+
context: {
|
|
4738
|
+
campaign: options.campaign,
|
|
4739
|
+
channel: options.channel,
|
|
4740
|
+
labels: options.labels || [],
|
|
4741
|
+
notes: options.contextNotes
|
|
4742
|
+
},
|
|
4743
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4744
|
+
diagnostics,
|
|
4745
|
+
errors: [...new Set(errors)],
|
|
4746
|
+
identity_sha256: "",
|
|
4747
|
+
kind: "lineage.selection_packet",
|
|
4748
|
+
packet_id: "",
|
|
4749
|
+
product: catalogSummary.product,
|
|
4750
|
+
project,
|
|
4751
|
+
schema_version: "lineage.selection_packet.v2",
|
|
4752
|
+
selection: {
|
|
4753
|
+
asset_ids: selectedItems.map((item) => item.asset_id),
|
|
4754
|
+
count: selectedItems.length,
|
|
4755
|
+
items: selectedItems,
|
|
4756
|
+
root_asset_id: workspace.root_asset_id
|
|
4757
|
+
},
|
|
4758
|
+
source: {
|
|
4759
|
+
app: "lineage",
|
|
4760
|
+
command: options.command,
|
|
4761
|
+
db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
|
|
4762
|
+
package: options.packageVersion
|
|
4763
|
+
},
|
|
4764
|
+
warnings: [...new Set(warnings)],
|
|
4765
|
+
workspace: {
|
|
4766
|
+
active_at: workspace.active_at,
|
|
4767
|
+
id: workspace.id,
|
|
4768
|
+
notes: workspace.notes,
|
|
4769
|
+
root_asset_id: workspace.root_asset_id,
|
|
4770
|
+
status: workspace.status,
|
|
4771
|
+
title: workspace.title
|
|
4772
|
+
}
|
|
4773
|
+
};
|
|
4774
|
+
packet.identity_sha256 = lineageSelectionPacketV2IdentitySha256(packet);
|
|
4775
|
+
packet.packet_id = `lineage_packet_${packet.identity_sha256.slice(0, 24)}`;
|
|
4776
|
+
return packet;
|
|
4777
|
+
}
|
|
4778
|
+
function getLineageSelectionPacket(project, options = {}) {
|
|
4779
|
+
return options.schema === "v2" ? getLineageSelectionPacketV2(project, options) : getLineageSelectionPacketV1(project, options);
|
|
4780
|
+
}
|
|
3600
4781
|
|
|
3601
4782
|
// src/server/runtimeInfo.ts
|
|
4783
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
3602
4784
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
3603
|
-
import { createRequire as
|
|
3604
|
-
import { existsSync as
|
|
3605
|
-
import { join as
|
|
3606
|
-
|
|
3607
|
-
|
|
4785
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
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
|
|
4795
|
+
var require5 = createRequire4(import.meta.url);
|
|
4796
|
+
var processStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4797
|
+
function isLineagePackageRoot(root) {
|
|
4798
|
+
try {
|
|
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) {
|
|
3608
4817
|
try {
|
|
3609
|
-
|
|
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"));
|
|
3610
4826
|
return { name: info.name || "@mean-weasel/lineage", version: info.version || "0.0.0" };
|
|
3611
4827
|
} catch {
|
|
3612
4828
|
return { name: "@mean-weasel/lineage", version: "0.0.0" };
|
|
@@ -3619,35 +4835,240 @@ function normalizeRuntimeChannel(value) {
|
|
|
3619
4835
|
if (value === "development") return "dev";
|
|
3620
4836
|
return process.env.NODE_ENV === "production" ? "stable" : "dev";
|
|
3621
4837
|
}
|
|
3622
|
-
function
|
|
3623
|
-
const
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
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");
|
|
3628
4861
|
}
|
|
3629
|
-
function
|
|
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 identity2 = getLineageCodeIdentity(channel);
|
|
5026
|
+
if (!identity2.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: ${identity2.errors.join("; ")}. ${migration}`);
|
|
5029
|
+
}
|
|
5030
|
+
return identity2;
|
|
5031
|
+
}
|
|
5032
|
+
function tableExists2(database, table) {
|
|
3630
5033
|
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
3631
5034
|
}
|
|
3632
5035
|
function tableCount(database, table) {
|
|
3633
|
-
if (!
|
|
5036
|
+
if (!tableExists2(database, table)) return void 0;
|
|
3634
5037
|
const row = database.prepare(`select count(*) count from ${table}`).get();
|
|
3635
5038
|
return typeof row?.count === "number" ? row.count : void 0;
|
|
3636
5039
|
}
|
|
5040
|
+
function migrationKeys(database) {
|
|
5041
|
+
if (!tableExists2(database, "lineage_schema_migrations")) return [];
|
|
5042
|
+
return database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
|
|
5043
|
+
}
|
|
3637
5044
|
function getLineageRuntimeInfo(options = {}) {
|
|
3638
5045
|
const info = packageInfo();
|
|
3639
5046
|
const dbPath = options.dbPath || lineageDbPath();
|
|
3640
|
-
const
|
|
5047
|
+
const channel = normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
|
|
5048
|
+
const code = options.code || getLineageCodeIdentity(channel);
|
|
5049
|
+
const databaseInfo = { exists: existsSync10(dbPath), path: dbPath };
|
|
5050
|
+
const schema = { migration_keys: [] };
|
|
3641
5051
|
if (databaseInfo.exists) {
|
|
3642
5052
|
try {
|
|
3643
|
-
const stat =
|
|
5053
|
+
const stat = statSync6(dbPath);
|
|
3644
5054
|
databaseInfo.modified_at = stat.mtime.toISOString();
|
|
3645
5055
|
databaseInfo.size_bytes = stat.size;
|
|
3646
|
-
const { DatabaseSync } =
|
|
5056
|
+
const { DatabaseSync } = require5("node:sqlite");
|
|
3647
5057
|
const database = new DatabaseSync(dbPath, { readOnly: true });
|
|
3648
5058
|
try {
|
|
3649
5059
|
databaseInfo.projects = tableCount(database, "projects");
|
|
3650
5060
|
databaseInfo.workspaces = tableCount(database, "lineage_workspaces");
|
|
5061
|
+
schema.migration_keys = migrationKeys(database);
|
|
5062
|
+
if (tableExists2(database, "lineage_profile_identity")) {
|
|
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();
|
|
5066
|
+
if (rows.length === 1) {
|
|
5067
|
+
schema.profile_id = rows[0].profile_id;
|
|
5068
|
+
schema.profile_environment = rows[0].environment;
|
|
5069
|
+
if (rows[0].profile_fingerprint) schema.profile_fingerprint = rows[0].profile_fingerprint;
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
3651
5072
|
} finally {
|
|
3652
5073
|
database.close();
|
|
3653
5074
|
}
|
|
@@ -3657,27 +5078,120 @@ function getLineageRuntimeInfo(options = {}) {
|
|
|
3657
5078
|
}
|
|
3658
5079
|
return {
|
|
3659
5080
|
asset_root: repoRoot,
|
|
3660
|
-
channel
|
|
5081
|
+
channel,
|
|
5082
|
+
code,
|
|
3661
5083
|
database: databaseInfo,
|
|
3662
5084
|
fetchedAt: nowIso(),
|
|
3663
|
-
git_sha:
|
|
5085
|
+
git_sha: code.git_sha,
|
|
3664
5086
|
node_env: process.env.NODE_ENV,
|
|
3665
5087
|
package_name: info.name,
|
|
5088
|
+
profile: runtimeProfileIdentity(channel),
|
|
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
|
+
},
|
|
3666
5096
|
version: info.version
|
|
3667
5097
|
};
|
|
3668
5098
|
}
|
|
3669
5099
|
|
|
5100
|
+
// src/server/managedWriterRouting.ts
|
|
5101
|
+
var managedWriterRequestSchemaVersion = "lineage.managed_writer_request.v1";
|
|
5102
|
+
var managedWriterResponseSchemaVersion = "lineage.managed_writer_response.v1";
|
|
5103
|
+
var managedWriterRoute = "/api/managed-writer/execute";
|
|
5104
|
+
var delegationHeader = "X-Lineage-Writer-Delegation";
|
|
5105
|
+
var ManagedWriterRoutingError = class extends Error {
|
|
5106
|
+
constructor(message, status = 409, options) {
|
|
5107
|
+
super(message, options);
|
|
5108
|
+
this.status = status;
|
|
5109
|
+
this.name = "ManagedWriterRoutingError";
|
|
5110
|
+
}
|
|
5111
|
+
status;
|
|
5112
|
+
};
|
|
5113
|
+
function managedWriterTimeoutMs(command, args) {
|
|
5114
|
+
const subcommand = args.find((arg) => !arg.startsWith("-")) || "";
|
|
5115
|
+
return command === "reroll" && subcommand === "import" ? 5 * 6e4 : 3e4;
|
|
5116
|
+
}
|
|
5117
|
+
function errorMessage(body, fallback) {
|
|
5118
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return fallback;
|
|
5119
|
+
const record = body;
|
|
5120
|
+
if (typeof record.message === "string") return record.message;
|
|
5121
|
+
if (typeof record.error === "string") return record.error;
|
|
5122
|
+
return fallback;
|
|
5123
|
+
}
|
|
5124
|
+
function agentClaimError(body, status) {
|
|
5125
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
5126
|
+
const record = body;
|
|
5127
|
+
if (typeof record.message !== "string" || typeof record.error !== "string") return void 0;
|
|
5128
|
+
const conflicts = Array.isArray(record.conflicts) ? record.conflicts : [];
|
|
5129
|
+
return new AgentClaimError(record.message, status, record.error, conflicts);
|
|
5130
|
+
}
|
|
5131
|
+
function assertResponseIdentity(body, profile, channel) {
|
|
5132
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
5133
|
+
throw new ManagedWriterRoutingError("Managed writer service returned a non-object response", 502);
|
|
5134
|
+
}
|
|
5135
|
+
const response = body;
|
|
5136
|
+
if (response.ok !== true || response.schema_version !== managedWriterResponseSchemaVersion || !response.service || response.service.profile_id !== profile.profile_id || response.service.environment !== profile.environment || response.service.service_origin !== profile.service_origin || response.service.channel !== channel) {
|
|
5137
|
+
throw new ManagedWriterRoutingError("Managed writer response identity does not match the selected profile/service", 502);
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
async function executeManagedWriterCommand(profile, channel, command, args) {
|
|
5141
|
+
const delegation = getProfileWriterDelegation(profile);
|
|
5142
|
+
let response;
|
|
5143
|
+
try {
|
|
5144
|
+
response = await fetch(new URL(managedWriterRoute, delegation.service_origin), {
|
|
5145
|
+
body: JSON.stringify({
|
|
5146
|
+
args,
|
|
5147
|
+
channel,
|
|
5148
|
+
command,
|
|
5149
|
+
environment: profile.environment,
|
|
5150
|
+
profile_id: profile.profile_id,
|
|
5151
|
+
schema_version: managedWriterRequestSchemaVersion,
|
|
5152
|
+
service_origin: profile.service_origin
|
|
5153
|
+
}),
|
|
5154
|
+
headers: {
|
|
5155
|
+
"Content-Type": "application/json",
|
|
5156
|
+
[delegationHeader]: delegation.token
|
|
5157
|
+
},
|
|
5158
|
+
method: "POST",
|
|
5159
|
+
redirect: "error",
|
|
5160
|
+
signal: AbortSignal.timeout(managedWriterTimeoutMs(command, args))
|
|
5161
|
+
});
|
|
5162
|
+
} catch (error) {
|
|
5163
|
+
throw new ManagedWriterRoutingError(
|
|
5164
|
+
`Managed service for Lineage profile ${profile.profile_id} is unavailable or did not respond at ${profile.service_origin}; no direct fallback was attempted and the mutation outcome is unknown`,
|
|
5165
|
+
503,
|
|
5166
|
+
{ cause: error }
|
|
5167
|
+
);
|
|
5168
|
+
}
|
|
5169
|
+
let body;
|
|
5170
|
+
try {
|
|
5171
|
+
body = await response.json();
|
|
5172
|
+
} catch (error) {
|
|
5173
|
+
throw new ManagedWriterRoutingError("Managed writer service returned invalid JSON; mutation result is unknown", 502, { cause: error });
|
|
5174
|
+
}
|
|
5175
|
+
if (!response.ok) {
|
|
5176
|
+
const claimError = agentClaimError(body, response.status);
|
|
5177
|
+
if (claimError) throw claimError;
|
|
5178
|
+
throw new ManagedWriterRoutingError(errorMessage(body, `Managed writer service returned HTTP ${response.status}`), response.status);
|
|
5179
|
+
}
|
|
5180
|
+
assertResponseIdentity(body, profile, channel);
|
|
5181
|
+
return body.result;
|
|
5182
|
+
}
|
|
5183
|
+
|
|
3670
5184
|
// src/cli/lineageCli.ts
|
|
3671
5185
|
var signalExitCodes = {
|
|
3672
5186
|
SIGINT: 130,
|
|
3673
5187
|
SIGTERM: 143
|
|
3674
5188
|
};
|
|
3675
5189
|
function packageRoot2() {
|
|
3676
|
-
return
|
|
5190
|
+
return resolve8(dirname6(fileURLToPath3(import.meta.url)), "..", "..");
|
|
3677
5191
|
}
|
|
3678
5192
|
function packageVersion() {
|
|
3679
5193
|
try {
|
|
3680
|
-
const packageInfo2 = JSON.parse(
|
|
5194
|
+
const packageInfo2 = JSON.parse(readFileSync6(join10(packageRoot2(), "package.json"), "utf8"));
|
|
3681
5195
|
return packageInfo2.version || "0.0.0";
|
|
3682
5196
|
} catch {
|
|
3683
5197
|
return "0.0.0";
|
|
@@ -3685,9 +5199,9 @@ function packageVersion() {
|
|
|
3685
5199
|
}
|
|
3686
5200
|
function dataRoot(displayName) {
|
|
3687
5201
|
if (process.env.LINEAGE_HOME) return process.env.LINEAGE_HOME;
|
|
3688
|
-
if (
|
|
3689
|
-
if (
|
|
3690
|
-
return
|
|
5202
|
+
if (platform2() === "darwin") return join10(homedir2(), "Library", "Application Support", displayName);
|
|
5203
|
+
if (platform2() === "win32") return join10(process.env.APPDATA || join10(homedir2(), "AppData", "Roaming"), displayName);
|
|
5204
|
+
return join10(process.env.XDG_DATA_HOME || join10(homedir2(), ".local", "share"), displayName.toLowerCase().replace(/\s+/g, "-"));
|
|
3691
5205
|
}
|
|
3692
5206
|
function readOption(args, name) {
|
|
3693
5207
|
const prefix = `${name}=`;
|
|
@@ -3711,22 +5225,29 @@ function readOptions(args, name) {
|
|
|
3711
5225
|
return values;
|
|
3712
5226
|
}
|
|
3713
5227
|
function resolveStartOptions(config, args) {
|
|
3714
|
-
const
|
|
5228
|
+
const profile = prepareCliProfile(config, args);
|
|
5229
|
+
const serviceUrl = profile ? new URL(profile.service_origin) : void 0;
|
|
5230
|
+
const rawPort = readOption(args, "--port") || process.env.PORT || serviceUrl?.port || String(config.defaultPort);
|
|
3715
5231
|
const port = Number(rawPort);
|
|
3716
5232
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
3717
5233
|
throw new Error(`Invalid port: ${rawPort}`);
|
|
3718
5234
|
}
|
|
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))) {
|
|
5237
|
+
throw new Error(`Profile ${profile.profile_id} service_origin ${profile.service_origin} conflicts with requested host/port ${host}:${port}`);
|
|
5238
|
+
}
|
|
3719
5239
|
return {
|
|
3720
|
-
assetRoot: resolveCliAssetRoot(args),
|
|
3721
|
-
dbPath: resolveCliDbPath(config, args),
|
|
3722
|
-
host
|
|
5240
|
+
assetRoot: profile?.asset_root || resolveCliAssetRoot(args),
|
|
5241
|
+
dbPath: profile?.database_path || resolveCliDbPath(config, args),
|
|
5242
|
+
host,
|
|
3723
5243
|
json: args.includes("--json"),
|
|
3724
5244
|
open: args.includes("--open"),
|
|
3725
|
-
port
|
|
5245
|
+
port,
|
|
5246
|
+
profile
|
|
3726
5247
|
};
|
|
3727
5248
|
}
|
|
3728
5249
|
function resolveCliAssetRoot(args) {
|
|
3729
|
-
return
|
|
5250
|
+
return resolve8(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
|
|
3730
5251
|
}
|
|
3731
5252
|
function configureCliAssetRoot(args) {
|
|
3732
5253
|
const assetRoot = resolveCliAssetRoot(args);
|
|
@@ -3734,17 +5255,66 @@ function configureCliAssetRoot(args) {
|
|
|
3734
5255
|
return setLineageAssetRoot(assetRoot);
|
|
3735
5256
|
}
|
|
3736
5257
|
function resolveCliDbPath(config, args) {
|
|
3737
|
-
return readOption(args, "--db") || process.env.LINEAGE_DB ||
|
|
5258
|
+
return readOption(args, "--db") || process.env.LINEAGE_DB || join10(dataRoot(config.displayName), `${config.binName}.sqlite`);
|
|
5259
|
+
}
|
|
5260
|
+
function hasOption(args, name) {
|
|
5261
|
+
return args.includes(name) || args.some((arg) => arg.startsWith(`${name}=`));
|
|
5262
|
+
}
|
|
5263
|
+
function profileSelector(args) {
|
|
5264
|
+
const option = readOption(args, "--profile");
|
|
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}`);
|
|
5267
|
+
}
|
|
5268
|
+
return option || process.env.LINEAGE_PROFILE;
|
|
5269
|
+
}
|
|
5270
|
+
function doctorFailures(result) {
|
|
5271
|
+
return result.checks.filter((check) => check.status === "fail").map((check) => `${check.id}: ${check.message}`).join("; ");
|
|
5272
|
+
}
|
|
5273
|
+
function prepareCliProfile(config, args) {
|
|
5274
|
+
const selector = profileSelector(args);
|
|
5275
|
+
if (!selector) {
|
|
5276
|
+
assertUnselectedDatabaseIsUnbound(getLineageRuntimeInfo({ channel: config.channel, dbPath: resolveCliDbPath(config, args) }));
|
|
5277
|
+
return void 0;
|
|
5278
|
+
}
|
|
5279
|
+
if (hasOption(args, "--db")) throw new Error("A named profile cannot be combined with --db");
|
|
5280
|
+
if (hasOption(args, "--asset-root")) throw new Error("A named profile cannot be combined with --asset-root");
|
|
5281
|
+
const profile = resolveLineageProfile(selector);
|
|
5282
|
+
if (process.env.LINEAGE_DB && resolve8(process.env.LINEAGE_DB) !== profile.database_path) {
|
|
5283
|
+
throw new Error(`Profile ${profile.profile_id} database_path conflicts with LINEAGE_DB`);
|
|
5284
|
+
}
|
|
5285
|
+
if (process.env.LINEAGE_ASSET_ROOT && resolve8(process.env.LINEAGE_ASSET_ROOT) !== profile.asset_root) {
|
|
5286
|
+
throw new Error(`Profile ${profile.profile_id} asset_root conflicts with LINEAGE_ASSET_ROOT`);
|
|
5287
|
+
}
|
|
5288
|
+
assertProfileChannel(profile, config.channel);
|
|
5289
|
+
const runtime = getLineageRuntimeInfo({ channel: config.channel, dbPath: profile.database_path });
|
|
5290
|
+
const doctor = doctorLineageProfile(selector, { channel: config.channel, code: runtime.code, gitSha: runtime.git_sha, version: runtime.version });
|
|
5291
|
+
if (!doctor.ok) throw new Error(`Profile ${profile.profile_id} failed doctor: ${doctorFailures(doctor)}`);
|
|
5292
|
+
process.env.LINEAGE_ASSET_ROOT = profile.asset_root;
|
|
5293
|
+
process.env.LINEAGE_DB = profile.database_path;
|
|
5294
|
+
process.env.LINEAGE_PROFILE = selector;
|
|
5295
|
+
process.env.LINEAGE_PROFILE_ENVIRONMENT = profile.environment;
|
|
5296
|
+
process.env.LINEAGE_PROFILE_FINGERPRINT = profile.profile_fingerprint;
|
|
5297
|
+
process.env.LINEAGE_PROFILE_ID = profile.profile_id;
|
|
5298
|
+
process.env.LINEAGE_PROFILE_MANIFEST = profile.manifest_path;
|
|
5299
|
+
process.env.LINEAGE_PROFILE_SERVICE_ORIGIN = profile.service_origin;
|
|
5300
|
+
setLineageAssetRoot(profile.asset_root);
|
|
5301
|
+
return profile;
|
|
3738
5302
|
}
|
|
3739
5303
|
function formatLineageHelp(config) {
|
|
3740
5304
|
return `${config.binName} ${packageVersion()}
|
|
3741
5305
|
|
|
3742
5306
|
Usage:
|
|
3743
|
-
${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--asset-root <path>] [--open] [--json]
|
|
5307
|
+
${config.binName} start [--profile <id-or-manifest>] [--port <port>] [--host <host>] [--db <path>] [--asset-root <path>] [--open] [--json]
|
|
5308
|
+
${config.binName} profile doctor --profile <id-or-manifest> [--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]
|
|
3744
5314
|
${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
3745
5315
|
${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
3746
5316
|
${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
|
|
3747
|
-
${config.binName} selection packet [--project <project>] [--workspace <id-or-root>|--root <asset-id>] [--channel <channel>] [--campaign <campaign>] [--context-notes <text>] [--label <label>] [--out <path>] [--strict] [--db <path>] [--json]
|
|
5317
|
+
${config.binName} selection packet [--project <project>] [--workspace <id-or-root>|--root <asset-id>] [--channel <channel>] [--campaign <campaign>] [--context-notes <text>] [--label <label>] [--schema v2] [--out <path>] [--strict] [--db <path>] [--json]
|
|
3748
5318
|
${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
|
|
3749
5319
|
${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
3750
5320
|
${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
@@ -3776,6 +5346,19 @@ ${config.displayName} runs the bundled Lineage server for the ${config.channel}
|
|
|
3776
5346
|
Asset catalogs and local media default to the installed package root. Pass
|
|
3777
5347
|
--asset-root <path> or LINEAGE_ASSET_ROOT to use an external project root.
|
|
3778
5348
|
|
|
5349
|
+
All commands accept --profile <id-or-manifest> or LINEAGE_PROFILE. Named
|
|
5350
|
+
profiles are authoritative for database, asset root, environment, and origin;
|
|
5351
|
+
they cannot be combined with direct --db or --asset-root overrides. Commands
|
|
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.
|
|
5361
|
+
|
|
3779
5362
|
Variation vs re-roll:
|
|
3780
5363
|
link-child creates a new visible child variation edge.
|
|
3781
5364
|
reroll mark -> reroll plan -> reroll import updates the same node with a new attempt.`;
|
|
@@ -3784,11 +5367,53 @@ function printHelp(config) {
|
|
|
3784
5367
|
console.log(formatLineageHelp(config));
|
|
3785
5368
|
}
|
|
3786
5369
|
function openBrowser(url) {
|
|
3787
|
-
const command =
|
|
3788
|
-
const args =
|
|
5370
|
+
const command = platform2() === "darwin" ? "open" : platform2() === "win32" ? "cmd" : "xdg-open";
|
|
5371
|
+
const args = platform2() === "win32" ? ["/c", "start", "", url] : [url];
|
|
3789
5372
|
const opener = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
3790
5373
|
opener.unref();
|
|
3791
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
|
+
}
|
|
3792
5417
|
function positionalArgs(args) {
|
|
3793
5418
|
const values = [];
|
|
3794
5419
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -3828,6 +5453,8 @@ function runLineageDataCommand(command, args) {
|
|
|
3828
5453
|
if (command === "selection") {
|
|
3829
5454
|
const subcommand = positionalArgs(args)[0] || "";
|
|
3830
5455
|
if (subcommand !== "packet") throw new Error(`Unknown selection command: ${subcommand}`);
|
|
5456
|
+
const schema = readOption(args, "--schema");
|
|
5457
|
+
if (schema && schema !== "v2") throw new Error(`Unsupported selection packet schema: ${schema}. Omit --schema for v1 or pass --schema v2.`);
|
|
3831
5458
|
const labels = readOptions(args, "--label").flatMap((label) => label.split(",")).map((label) => label.trim()).filter(Boolean);
|
|
3832
5459
|
const packet = getLineageSelectionPacket(options.project, {
|
|
3833
5460
|
campaign: readOption(args, "--campaign"),
|
|
@@ -3838,14 +5465,15 @@ function runLineageDataCommand(command, args) {
|
|
|
3838
5465
|
labels,
|
|
3839
5466
|
packageVersion: packageVersion(),
|
|
3840
5467
|
rootAssetId: options.rootAssetId,
|
|
5468
|
+
schema: schema === "v2" ? "v2" : void 0,
|
|
3841
5469
|
strict: args.includes("--strict"),
|
|
3842
5470
|
workspaceId: readOption(args, "--workspace") || readOption(args, "--workspace-id")
|
|
3843
5471
|
});
|
|
3844
5472
|
const out = readOption(args, "--out");
|
|
3845
5473
|
if (out) {
|
|
3846
|
-
const outPath =
|
|
3847
|
-
|
|
3848
|
-
|
|
5474
|
+
const outPath = resolve8(out);
|
|
5475
|
+
mkdirSync6(dirname6(outPath), { recursive: true });
|
|
5476
|
+
writeFileSync4(outPath, `${JSON.stringify(packet, null, 2)}
|
|
3849
5477
|
`);
|
|
3850
5478
|
}
|
|
3851
5479
|
return packet;
|
|
@@ -4055,6 +5683,113 @@ function runLineageDbCommand(config, command, args) {
|
|
|
4055
5683
|
if (command === "info") return getLineageRuntimeInfo({ channel: config.channel, dbPath });
|
|
4056
5684
|
throw new Error(`Unknown db command: ${command}`);
|
|
4057
5685
|
}
|
|
5686
|
+
function runLineageProfileCommand(config, command, args) {
|
|
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
|
+
}
|
|
5713
|
+
if (hasOption(args, "--db")) throw new Error(`Profile ${command} cannot be combined with --db`);
|
|
5714
|
+
if (hasOption(args, "--asset-root")) throw new Error(`Profile ${command} cannot be combined with --asset-root`);
|
|
5715
|
+
const selector = profileSelector(args);
|
|
5716
|
+
if (!selector) throw new Error(`lineage profile ${command} requires --profile or LINEAGE_PROFILE`);
|
|
5717
|
+
if (command === "doctor") return doctorLineageProfile(selector, runtimeIdentity);
|
|
5718
|
+
if (command === "bind") {
|
|
5719
|
+
if (!args.includes("--confirm-write")) throw new Error("Profile bind requires --confirm-write");
|
|
5720
|
+
const profile = resolveLineageProfile(selector);
|
|
5721
|
+
const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
|
|
5722
|
+
try {
|
|
5723
|
+
return bindLineageProfileDatabase(selector, runtimeIdentity, true);
|
|
5724
|
+
} finally {
|
|
5725
|
+
writerLease.release();
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
throw new Error(`Unknown profile command: ${command}`);
|
|
5729
|
+
}
|
|
5730
|
+
function printProfileResult(result, json) {
|
|
5731
|
+
if (json) {
|
|
5732
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5733
|
+
return;
|
|
5734
|
+
}
|
|
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}`);
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
function lineageProfileDoctorExitCode(result) {
|
|
5749
|
+
return result.ok ? 0 : 1;
|
|
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
|
+
}
|
|
5771
|
+
function lineageCliRequiresWriterLease(command, args) {
|
|
5772
|
+
if (command === "next" || command === "brief" || command === "inspect" || command === "selection") return false;
|
|
5773
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
5774
|
+
if (command === "reroll") return subcommand !== "list";
|
|
5775
|
+
if (command === "tasks") return subcommand !== "list" && subcommand !== "inspect";
|
|
5776
|
+
if (command === "db") return subcommand !== "info";
|
|
5777
|
+
if (command === "agent") return subcommand !== "status" && subcommand !== "graph" && subcommand !== "inspect";
|
|
5778
|
+
return true;
|
|
5779
|
+
}
|
|
5780
|
+
function argsWithoutProfileSelector(args) {
|
|
5781
|
+
const filtered = [];
|
|
5782
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
5783
|
+
const arg = args[index];
|
|
5784
|
+
if (arg === "--profile") {
|
|
5785
|
+
index += 1;
|
|
5786
|
+
continue;
|
|
5787
|
+
}
|
|
5788
|
+
if (arg.startsWith("--profile=")) continue;
|
|
5789
|
+
filtered.push(arg);
|
|
5790
|
+
}
|
|
5791
|
+
return filtered;
|
|
5792
|
+
}
|
|
4058
5793
|
function printDbResult(command, result, json) {
|
|
4059
5794
|
if (json) {
|
|
4060
5795
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -4064,6 +5799,10 @@ function printDbResult(command, result, json) {
|
|
|
4064
5799
|
const runtime = result;
|
|
4065
5800
|
console.log(`Channel: ${runtime.channel}`);
|
|
4066
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
|
+
}
|
|
4067
5806
|
if (runtime.git_sha) console.log(`Git: ${runtime.git_sha}`);
|
|
4068
5807
|
if (runtime.node_env) console.log(`Node env: ${runtime.node_env}`);
|
|
4069
5808
|
console.log(`Assets: ${runtime.asset_root}`);
|
|
@@ -4205,23 +5944,26 @@ function start(config, args) {
|
|
|
4205
5944
|
else console.error(`${config.binName}: ${message}`);
|
|
4206
5945
|
process.exit(1);
|
|
4207
5946
|
}
|
|
4208
|
-
const serverPath =
|
|
4209
|
-
if (!
|
|
5947
|
+
const serverPath = join10(packageRoot2(), "dist", "server.js");
|
|
5948
|
+
if (!existsSync11(serverPath)) {
|
|
4210
5949
|
const message = `Missing bundled server at ${serverPath}. Run npm run build before using ${config.binName} start from a source checkout.`;
|
|
4211
5950
|
if (options.json) console.error(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
4212
5951
|
else console.error(`${config.binName}: ${message}`);
|
|
4213
5952
|
process.exit(1);
|
|
4214
5953
|
}
|
|
4215
|
-
|
|
5954
|
+
mkdirSync6(dirname6(options.dbPath), { recursive: true });
|
|
4216
5955
|
const url = `http://${options.host}:${options.port}`;
|
|
4217
5956
|
if (options.json) {
|
|
4218
|
-
console.log(JSON.stringify({ assetRoot: options.assetRoot, channel: config.channel, dbPath: options.dbPath, host: options.host, port: options.port, status: "starting", url }, null, 2));
|
|
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));
|
|
4219
5958
|
} else {
|
|
4220
5959
|
console.log(`${config.displayName} starting at ${url}`);
|
|
5960
|
+
if (options.profile) console.log(`Profile: ${options.profile.profile_id} (${options.profile.environment})`);
|
|
5961
|
+
else console.warn("Warning: legacy-unbound runtime; database and asset paths are not protected by a named profile.");
|
|
4221
5962
|
console.log(`SQLite: ${options.dbPath}`);
|
|
4222
5963
|
console.log(`Assets: ${options.assetRoot}`);
|
|
4223
5964
|
}
|
|
4224
|
-
|
|
5965
|
+
const code = getLineageCodeIdentity(config.channel);
|
|
5966
|
+
const serviceInstanceId = process.env.LINEAGE_SERVICE_INSTANCE_ID || randomUUID4();
|
|
4225
5967
|
const child = spawn(process.execPath, [serverPath], {
|
|
4226
5968
|
env: {
|
|
4227
5969
|
...process.env,
|
|
@@ -4229,11 +5971,33 @@ function start(config, args) {
|
|
|
4229
5971
|
LINEAGE_ASSET_ROOT: options.assetRoot,
|
|
4230
5972
|
LINEAGE_CHANNEL: config.channel,
|
|
4231
5973
|
LINEAGE_DB: options.dbPath,
|
|
5974
|
+
LINEAGE_PROFILE: options.profile ? process.env.LINEAGE_PROFILE : void 0,
|
|
5975
|
+
LINEAGE_PROFILE_ENVIRONMENT: options.profile?.environment,
|
|
5976
|
+
LINEAGE_PROFILE_FINGERPRINT: options.profile?.profile_fingerprint,
|
|
5977
|
+
LINEAGE_PROFILE_ID: options.profile?.profile_id,
|
|
5978
|
+
LINEAGE_PROFILE_MANIFEST: options.profile?.manifest_path,
|
|
5979
|
+
LINEAGE_PROFILE_SERVICE_ORIGIN: options.profile?.service_origin,
|
|
5980
|
+
LINEAGE_LAUNCHER_PID: String(process.pid),
|
|
5981
|
+
LINEAGE_SERVICE_INSTANCE_ID: serviceInstanceId,
|
|
5982
|
+
LINEAGE_DB_ACCESS: options.profile ? void 0 : "read-only",
|
|
4232
5983
|
NODE_ENV: "production",
|
|
4233
5984
|
PORT: String(options.port)
|
|
4234
5985
|
},
|
|
4235
5986
|
stdio: "inherit"
|
|
4236
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
|
+
}
|
|
4237
6001
|
let forwardedSignal;
|
|
4238
6002
|
const stop = (signal) => {
|
|
4239
6003
|
forwardedSignal = signal;
|
|
@@ -4241,13 +6005,14 @@ function start(config, args) {
|
|
|
4241
6005
|
};
|
|
4242
6006
|
process.once("SIGINT", stop);
|
|
4243
6007
|
process.once("SIGTERM", stop);
|
|
4244
|
-
child.on("exit", (
|
|
6008
|
+
child.on("exit", (code2) => process.exit(code2 ?? (forwardedSignal ? signalExitCodes[forwardedSignal] || 1 : 0)));
|
|
4245
6009
|
child.on("error", (error) => {
|
|
4246
6010
|
console.error(`${config.binName}: failed to start server: ${error.message}`);
|
|
4247
6011
|
process.exit(1);
|
|
4248
6012
|
});
|
|
4249
6013
|
}
|
|
4250
|
-
function runLineageCli(config, args = process.argv.slice(2)) {
|
|
6014
|
+
async function runLineageCli(config, args = process.argv.slice(2)) {
|
|
6015
|
+
process.env.LINEAGE_CHANNEL = config.channel;
|
|
4251
6016
|
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
4252
6017
|
printHelp(config);
|
|
4253
6018
|
process.exit(0);
|
|
@@ -4258,15 +6023,99 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
4258
6023
|
}
|
|
4259
6024
|
const normalizedArgs = args[0] === "lineage" ? args.slice(1) : args;
|
|
4260
6025
|
const [command] = normalizedArgs;
|
|
6026
|
+
if (command === "profile") {
|
|
6027
|
+
const commandArgs = normalizedArgs.slice(2);
|
|
6028
|
+
const profileCommand = normalizedArgs[1] || "";
|
|
6029
|
+
const json2 = commandArgs.includes("--json");
|
|
6030
|
+
try {
|
|
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
|
+
}
|
|
6044
|
+
printProfileResult(result, json2);
|
|
6045
|
+
process.exit(result.schema_version === "lineage.profile_doctor.v1" ? lineageProfileDoctorExitCode(result) : 0);
|
|
6046
|
+
} catch (error) {
|
|
6047
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
6048
|
+
if (json2) console.error(JSON.stringify({ ok: false, command: `profile ${profileCommand}`, error: message2 }, null, 2));
|
|
6049
|
+
else console.error(`${config.binName}: ${message2}`);
|
|
6050
|
+
process.exit(1);
|
|
6051
|
+
}
|
|
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
|
+
}
|
|
4261
6079
|
if (command === "start") {
|
|
4262
6080
|
start(config, normalizedArgs.slice(1));
|
|
4263
6081
|
return;
|
|
4264
6082
|
}
|
|
6083
|
+
let managedWriterProfile;
|
|
6084
|
+
try {
|
|
6085
|
+
const profile = prepareCliProfile(config, normalizedArgs.slice(1));
|
|
6086
|
+
const requiresWriter = lineageCliRequiresWriterLease(command, normalizedArgs.slice(1));
|
|
6087
|
+
if (profile) {
|
|
6088
|
+
if (requiresWriter) {
|
|
6089
|
+
delete process.env.LINEAGE_DB_ACCESS;
|
|
6090
|
+
try {
|
|
6091
|
+
const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
|
|
6092
|
+
process.once("exit", writerLease.release);
|
|
6093
|
+
} catch (error) {
|
|
6094
|
+
if (!(error instanceof ProfileWriterLeaseConflictError) || error.owner.role !== "service") throw error;
|
|
6095
|
+
process.env.LINEAGE_DB_ACCESS = "read-only";
|
|
6096
|
+
managedWriterProfile = profile;
|
|
6097
|
+
}
|
|
6098
|
+
} else {
|
|
6099
|
+
process.env.LINEAGE_DB_ACCESS = "read-only";
|
|
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";
|
|
6105
|
+
}
|
|
6106
|
+
} catch (error) {
|
|
6107
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
6108
|
+
const json2 = normalizedArgs.includes("--json");
|
|
6109
|
+
if (json2) console.error(JSON.stringify({ ok: false, command, error: message2 }, null, 2));
|
|
6110
|
+
else console.error(`${config.binName}: ${message2}`);
|
|
6111
|
+
process.exit(1);
|
|
6112
|
+
}
|
|
4265
6113
|
if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
|
|
4266
6114
|
const commandArgs = normalizedArgs.slice(1);
|
|
4267
6115
|
const json2 = commandArgs.includes("--json");
|
|
4268
6116
|
try {
|
|
4269
|
-
|
|
6117
|
+
const result = managedWriterProfile ? await executeManagedWriterCommand(managedWriterProfile, config.channel, command, argsWithoutProfileSelector(commandArgs)) : runLineageDataCommand(command, commandArgs);
|
|
6118
|
+
printDataResult(command, result, json2);
|
|
4270
6119
|
} catch (error) {
|
|
4271
6120
|
const message2 = redactAgentClaimTokens(error instanceof Error ? error.message : String(error));
|
|
4272
6121
|
if (json2) {
|
|
@@ -4296,7 +6145,13 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
4296
6145
|
const agentCommand = normalizedArgs[1] || "";
|
|
4297
6146
|
const json2 = commandArgs.includes("--json");
|
|
4298
6147
|
try {
|
|
4299
|
-
|
|
6148
|
+
const result = managedWriterProfile ? await executeManagedWriterCommand(
|
|
6149
|
+
managedWriterProfile,
|
|
6150
|
+
config.channel,
|
|
6151
|
+
"agent",
|
|
6152
|
+
argsWithoutProfileSelector([agentCommand, ...commandArgs])
|
|
6153
|
+
) : runLineageAgentCommand(agentCommand, commandArgs);
|
|
6154
|
+
printAgentResult(agentCommand, result, json2);
|
|
4300
6155
|
} catch (error) {
|
|
4301
6156
|
const message2 = redactAgentClaimTokens(error instanceof Error ? error.message : String(error));
|
|
4302
6157
|
if (json2) {
|
|
@@ -4317,5 +6172,17 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
4317
6172
|
}
|
|
4318
6173
|
|
|
4319
6174
|
// src/cli/lineage-dev.ts
|
|
4320
|
-
|
|
6175
|
+
var identity = getLineageCodeIdentity("dev");
|
|
6176
|
+
if (!identity.verified) {
|
|
6177
|
+
console.error(`lineage-dev: published package execution is disabled. ${identity.errors.join("; ")}. Run dev from a Git checkout with npm run lineage:dev -- <command>.`);
|
|
6178
|
+
process.exit(1);
|
|
6179
|
+
}
|
|
6180
|
+
var preview = process.env.LINEAGE_CHANNEL === "preview";
|
|
6181
|
+
await runLineageCli({
|
|
6182
|
+
binName: "lineage-dev",
|
|
6183
|
+
channel: preview ? "preview" : "dev",
|
|
6184
|
+
defaultHost: preview ? "lineage-preview.localhost" : "lineage-dev.localhost",
|
|
6185
|
+
defaultPort: preview ? 5199 : 5198,
|
|
6186
|
+
displayName: preview ? "Lineage Preview" : "Lineage Dev"
|
|
6187
|
+
});
|
|
4321
6188
|
//# sourceMappingURL=lineage-dev.js.map
|