@mean-weasel/lineage 0.1.10 → 0.1.12
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 +82 -4
- package/dist/cli/lineage-dev.js +1325 -114
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +1315 -111
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +2039 -150
- package/dist/server.js.map +4 -4
- package/dist/web/assets/{index-CKMlZT43.css → index-3uJbXqPS.css} +1 -1
- package/dist/web/assets/index-OkymIyPO.js +23 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CICrfv4C.js +0 -23
package/dist/cli/lineage-dev.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/lineageCli.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { homedir, platform } from "node:os";
|
|
6
|
-
import { dirname as
|
|
4
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5
|
+
import { homedir as homedir2, platform as platform2 } from "node:os";
|
|
6
|
+
import { dirname as dirname5, join as join10, resolve as resolve8 } from "node:path";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/server/agentClaims.ts
|
|
11
|
-
import { createHash as
|
|
11
|
+
import { createHash as createHash3, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
12
12
|
|
|
13
13
|
// src/server/assetLineageDb.ts
|
|
14
|
-
import { createRequire } from "node:module";
|
|
15
|
-
import { mkdirSync as
|
|
16
|
-
import { join as
|
|
14
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
15
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
|
|
16
|
+
import { join as join6, resolve as resolve5 } from "node:path";
|
|
17
17
|
|
|
18
18
|
// src/server/assetCore.ts
|
|
19
19
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -289,7 +289,7 @@ function isPackageRoot(path) {
|
|
|
289
289
|
return false;
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
-
function
|
|
292
|
+
function resolvePackageRoot() {
|
|
293
293
|
const moduleDir = dirname2(fileURLToPath(import.meta.url));
|
|
294
294
|
const candidates = [
|
|
295
295
|
process.env.LINEAGE_REPO_ROOT,
|
|
@@ -301,7 +301,12 @@ function resolveRepoRoot() {
|
|
|
301
301
|
if (!root) throw new Error("Unable to locate Lineage package root");
|
|
302
302
|
return root;
|
|
303
303
|
}
|
|
304
|
-
var
|
|
304
|
+
var packageRoot = resolvePackageRoot();
|
|
305
|
+
var repoRoot = resolve2(process.env.LINEAGE_ASSET_ROOT || packageRoot);
|
|
306
|
+
function setLineageAssetRoot(path) {
|
|
307
|
+
repoRoot = resolve2(path || packageRoot);
|
|
308
|
+
return repoRoot;
|
|
309
|
+
}
|
|
305
310
|
var defaultProject = "demo-project";
|
|
306
311
|
var defaultProduct = process.env.LINEAGE_DEFAULT_PRODUCT || defaultProject;
|
|
307
312
|
var publicFallbackBucket = "lineage-demo-assets";
|
|
@@ -326,7 +331,7 @@ function catalogPath(project = defaultProject) {
|
|
|
326
331
|
return join3(repoRoot, cleanProject(project), "assets", "catalog.json");
|
|
327
332
|
}
|
|
328
333
|
function fixtureCatalogPath(project = defaultProject) {
|
|
329
|
-
return join3(
|
|
334
|
+
return join3(packageRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
|
|
330
335
|
}
|
|
331
336
|
function resolvedCatalogPath(project = defaultProject) {
|
|
332
337
|
const path = catalogPath(project);
|
|
@@ -582,20 +587,580 @@ function validateProject(project = defaultProject) {
|
|
|
582
587
|
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 };
|
|
583
588
|
}
|
|
584
589
|
|
|
585
|
-
// src/server/
|
|
590
|
+
// src/server/lineageProfiles.ts
|
|
591
|
+
import { createRequire } from "node:module";
|
|
592
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
|
|
593
|
+
import { homedir, platform } from "node:os";
|
|
594
|
+
import { dirname as dirname3, isAbsolute, join as join4, resolve as resolve3 } from "node:path";
|
|
595
|
+
|
|
596
|
+
// src/shared/lineageProfileTypes.ts
|
|
597
|
+
var lineageProfileSchemaVersion = "lineage.profile.v1";
|
|
598
|
+
var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
|
|
599
|
+
|
|
600
|
+
// src/server/lineageProfiles.ts
|
|
586
601
|
var require2 = createRequire(import.meta.url);
|
|
602
|
+
var profileIdPattern = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
603
|
+
function lineageDataRoot() {
|
|
604
|
+
if (process.env.LINEAGE_HOME) return resolve3(process.env.LINEAGE_HOME);
|
|
605
|
+
if (platform() === "darwin") return join4(homedir(), "Library", "Application Support", "Lineage");
|
|
606
|
+
if (platform() === "win32") return join4(process.env.APPDATA || join4(homedir(), "AppData", "Roaming"), "Lineage");
|
|
607
|
+
return join4(process.env.XDG_DATA_HOME || join4(homedir(), ".local", "share"), "lineage");
|
|
608
|
+
}
|
|
609
|
+
function lineageProfileRoot() {
|
|
610
|
+
return resolve3(process.env.LINEAGE_PROFILE_ROOT || join4(lineageDataRoot(), "profiles"));
|
|
611
|
+
}
|
|
612
|
+
function environmentChannel(environment) {
|
|
613
|
+
if (environment === "production") return "stable";
|
|
614
|
+
if (environment === "preview") return "preview";
|
|
615
|
+
return "dev";
|
|
616
|
+
}
|
|
617
|
+
function channelEnvironment(channel) {
|
|
618
|
+
if (channel === "stable") return "production";
|
|
619
|
+
if (channel === "preview") return "preview";
|
|
620
|
+
return "development";
|
|
621
|
+
}
|
|
622
|
+
function profileManifestPath(selector) {
|
|
623
|
+
const value = selector.trim();
|
|
624
|
+
if (!value) throw new Error("Profile selector must not be empty");
|
|
625
|
+
const looksLikePath = isAbsolute(value) || value.includes("/") || value.includes("\\") || value.endsWith(".json");
|
|
626
|
+
if (looksLikePath) return { manifestPath: resolve3(value) };
|
|
627
|
+
if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
|
|
628
|
+
return { manifestPath: join4(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
|
|
629
|
+
}
|
|
630
|
+
function requiredString(record, key) {
|
|
631
|
+
const value = record[key];
|
|
632
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
|
|
633
|
+
return value.trim();
|
|
634
|
+
}
|
|
635
|
+
function optionalString(record, key) {
|
|
636
|
+
const value = record[key];
|
|
637
|
+
if (value === void 0) return void 0;
|
|
638
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
|
|
639
|
+
return value.trim();
|
|
640
|
+
}
|
|
641
|
+
function validateEnvironment(value) {
|
|
642
|
+
if (value === "production" || value === "preview" || value === "development") return value;
|
|
643
|
+
throw new Error(`Invalid profile environment: ${value}`);
|
|
644
|
+
}
|
|
645
|
+
function validateChannel(value) {
|
|
646
|
+
if (value === void 0) return void 0;
|
|
647
|
+
if (value === "stable" || value === "preview" || value === "dev") return value;
|
|
648
|
+
throw new Error(`Invalid expected runtime channel: ${String(value)}`);
|
|
649
|
+
}
|
|
650
|
+
function resolveManifestPath(manifestPath, value) {
|
|
651
|
+
return resolve3(dirname3(manifestPath), value);
|
|
652
|
+
}
|
|
653
|
+
function validateServiceOrigin(value) {
|
|
654
|
+
let parsed;
|
|
655
|
+
try {
|
|
656
|
+
parsed = new URL(value);
|
|
657
|
+
} catch {
|
|
658
|
+
throw new Error(`Invalid profile service_origin: ${value}`);
|
|
659
|
+
}
|
|
660
|
+
if (parsed.protocol !== "http:") throw new Error("Profile service_origin must use http for the local Lineage service");
|
|
661
|
+
if (!parsed.port) throw new Error("Profile service_origin must include an explicit port");
|
|
662
|
+
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
663
|
+
throw new Error("Profile service_origin must contain only scheme, host, and port");
|
|
664
|
+
}
|
|
665
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
666
|
+
const isLocal = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
667
|
+
if (!isLocal) throw new Error("Profile service_origin must use a loopback or localhost host");
|
|
668
|
+
return parsed.origin;
|
|
669
|
+
}
|
|
670
|
+
function resolveLineageProfile(selector) {
|
|
671
|
+
const { manifestPath, namedProfileId } = profileManifestPath(selector);
|
|
672
|
+
if (!existsSync4(manifestPath)) throw new Error(`Profile manifest does not exist: ${manifestPath}`);
|
|
673
|
+
let raw;
|
|
674
|
+
try {
|
|
675
|
+
raw = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
676
|
+
} catch (error) {
|
|
677
|
+
throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
678
|
+
}
|
|
679
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
|
|
680
|
+
const record = raw;
|
|
681
|
+
const schemaVersion = requiredString(record, "schema_version");
|
|
682
|
+
if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
|
|
683
|
+
const profileId = requiredString(record, "profile_id");
|
|
684
|
+
if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
|
|
685
|
+
if (namedProfileId && namedProfileId !== profileId) {
|
|
686
|
+
throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
|
|
687
|
+
}
|
|
688
|
+
const expectedRaw = record.expected_runtime;
|
|
689
|
+
if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
|
|
690
|
+
throw new Error("Profile expected_runtime must be an object");
|
|
691
|
+
}
|
|
692
|
+
const expected = expectedRaw;
|
|
693
|
+
const expectedGitSha = expected ? optionalString(expected, "git_sha") : void 0;
|
|
694
|
+
const expectedVersion = expected ? optionalString(expected, "version") : void 0;
|
|
695
|
+
const migrationsRaw = record.required_schema_migrations;
|
|
696
|
+
if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
|
|
697
|
+
throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
|
|
698
|
+
}
|
|
699
|
+
const environment = validateEnvironment(requiredString(record, "environment"));
|
|
700
|
+
const expectedChannel = validateChannel(expected?.channel);
|
|
701
|
+
if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
|
|
702
|
+
throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
|
|
703
|
+
}
|
|
704
|
+
const manifest = {
|
|
705
|
+
asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
|
|
706
|
+
database_path: resolveManifestPath(manifestPath, requiredString(record, "database_path")),
|
|
707
|
+
environment,
|
|
708
|
+
...expected ? {
|
|
709
|
+
expected_runtime: {
|
|
710
|
+
...expectedChannel ? { channel: expectedChannel } : {},
|
|
711
|
+
...expectedGitSha ? { git_sha: expectedGitSha } : {},
|
|
712
|
+
...expectedVersion ? { version: expectedVersion } : {}
|
|
713
|
+
}
|
|
714
|
+
} : {},
|
|
715
|
+
profile_id: profileId,
|
|
716
|
+
...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
|
|
717
|
+
schema_version: lineageProfileSchemaVersion,
|
|
718
|
+
service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
|
|
719
|
+
};
|
|
720
|
+
return { ...manifest, manifest_path: manifestPath };
|
|
721
|
+
}
|
|
722
|
+
function bindLineageProfileDatabase(profile, confirmWrite) {
|
|
723
|
+
const result = {
|
|
724
|
+
database_path: profile.database_path,
|
|
725
|
+
environment: profile.environment,
|
|
726
|
+
ok: true,
|
|
727
|
+
profile_id: profile.profile_id,
|
|
728
|
+
schema_version: "lineage.profile_bind.v1"
|
|
729
|
+
};
|
|
730
|
+
if (!confirmWrite) return { ...result, dryRun: true };
|
|
731
|
+
mkdirSync3(dirname3(profile.database_path), { recursive: true });
|
|
732
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
733
|
+
const database = new DatabaseSync(profile.database_path);
|
|
734
|
+
try {
|
|
735
|
+
database.exec("BEGIN IMMEDIATE");
|
|
736
|
+
try {
|
|
737
|
+
database.exec(`
|
|
738
|
+
create table if not exists lineage_profile_identity (
|
|
739
|
+
profile_id text primary key,
|
|
740
|
+
environment text not null,
|
|
741
|
+
bound_at text not null
|
|
742
|
+
)
|
|
743
|
+
`);
|
|
744
|
+
const rows = database.prepare("select profile_id, environment, bound_at from lineage_profile_identity").all();
|
|
745
|
+
if (rows.length > 0) {
|
|
746
|
+
if (rows.length !== 1 || rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
|
|
747
|
+
throw new Error(`Database ${profile.database_path} is already bound to a different Lineage profile identity`);
|
|
748
|
+
}
|
|
749
|
+
database.exec("COMMIT");
|
|
750
|
+
return { ...result, already_bound: true, bound_at: rows[0].bound_at };
|
|
751
|
+
}
|
|
752
|
+
const boundAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
753
|
+
database.prepare("insert into lineage_profile_identity (profile_id, environment, bound_at) values (?, ?, ?)").run(profile.profile_id, profile.environment, boundAt);
|
|
754
|
+
database.exec("COMMIT");
|
|
755
|
+
return { ...result, bound_at: boundAt };
|
|
756
|
+
} catch (error) {
|
|
757
|
+
database.exec("ROLLBACK");
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
} finally {
|
|
761
|
+
database.close();
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function assertProfileChannel(profile, channel) {
|
|
765
|
+
const requiredChannel = environmentChannel(profile.environment);
|
|
766
|
+
if (channel === requiredChannel) return;
|
|
767
|
+
if (profile.environment === "production") {
|
|
768
|
+
throw new Error(`Refusing to open production profile ${profile.profile_id} from ${channel} code; use the stable channel`);
|
|
769
|
+
}
|
|
770
|
+
throw new Error(`Profile ${profile.profile_id} requires the ${requiredChannel} channel, not ${channel}`);
|
|
771
|
+
}
|
|
772
|
+
function tableExists(database, table) {
|
|
773
|
+
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
774
|
+
}
|
|
775
|
+
function gitShasMatch(expected, actual) {
|
|
776
|
+
if (!actual) return false;
|
|
777
|
+
const normalizedExpected = expected.toLowerCase();
|
|
778
|
+
const normalizedActual = actual.toLowerCase();
|
|
779
|
+
const shorterLength = Math.min(normalizedExpected.length, normalizedActual.length);
|
|
780
|
+
return shorterLength >= 12 && normalizedExpected.slice(0, shorterLength) === normalizedActual.slice(0, shorterLength);
|
|
781
|
+
}
|
|
782
|
+
function inspectDatabase(profile) {
|
|
783
|
+
const result = {
|
|
784
|
+
exists: existsSync4(profile.database_path),
|
|
785
|
+
migration_keys: [],
|
|
786
|
+
path: profile.database_path
|
|
787
|
+
};
|
|
788
|
+
if (!result.exists) return result;
|
|
789
|
+
try {
|
|
790
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
791
|
+
const database = new DatabaseSync(profile.database_path, { readOnly: true });
|
|
792
|
+
try {
|
|
793
|
+
if (tableExists(database, "lineage_profile_identity")) {
|
|
794
|
+
const rows = database.prepare("select profile_id, environment, bound_at from lineage_profile_identity").all();
|
|
795
|
+
if (rows.length === 1) {
|
|
796
|
+
const identity = {
|
|
797
|
+
bound_at: typeof rows[0].bound_at === "string" ? rows[0].bound_at : void 0,
|
|
798
|
+
environment: String(rows[0].environment),
|
|
799
|
+
profile_id: String(rows[0].profile_id)
|
|
800
|
+
};
|
|
801
|
+
result.identity = identity;
|
|
802
|
+
} else {
|
|
803
|
+
result.error = `Expected exactly one lineage_profile_identity row, found ${rows.length}`;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (tableExists(database, "lineage_schema_migrations")) {
|
|
807
|
+
result.migration_keys = database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
|
|
808
|
+
}
|
|
809
|
+
} finally {
|
|
810
|
+
database.close();
|
|
811
|
+
}
|
|
812
|
+
} catch (error) {
|
|
813
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
814
|
+
}
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
function doctorLineageProfile(selector, runtime) {
|
|
818
|
+
const checks = [];
|
|
819
|
+
const result = {
|
|
820
|
+
checks,
|
|
821
|
+
ok: false,
|
|
822
|
+
runtime: { channel: runtime.channel, git_sha: runtime.gitSha, version: runtime.version },
|
|
823
|
+
schema_version: lineageProfileDoctorSchemaVersion
|
|
824
|
+
};
|
|
825
|
+
let profile;
|
|
826
|
+
try {
|
|
827
|
+
profile = resolveLineageProfile(selector);
|
|
828
|
+
result.profile = profile;
|
|
829
|
+
result.manifest_path = profile.manifest_path;
|
|
830
|
+
checks.push({ id: "manifest", message: `Loaded ${profile.profile_id}`, status: "pass" });
|
|
831
|
+
} catch (error) {
|
|
832
|
+
checks.push({ id: "manifest", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
assertProfileChannel(profile, runtime.channel);
|
|
837
|
+
checks.push({ id: "runtime_channel", message: `${runtime.channel} code matches ${profile.environment}`, status: "pass" });
|
|
838
|
+
} catch (error) {
|
|
839
|
+
checks.push({ id: "runtime_channel", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
840
|
+
}
|
|
841
|
+
if (profile.expected_runtime?.version && profile.expected_runtime.version !== runtime.version) {
|
|
842
|
+
checks.push({ id: "runtime_version", message: `Expected version ${profile.expected_runtime.version}, found ${runtime.version}`, status: "fail" });
|
|
843
|
+
} else {
|
|
844
|
+
checks.push({ id: "runtime_version", message: `Runtime version ${runtime.version}`, status: "pass" });
|
|
845
|
+
}
|
|
846
|
+
if (profile.expected_runtime?.git_sha && !gitShasMatch(profile.expected_runtime.git_sha, runtime.gitSha)) {
|
|
847
|
+
checks.push({ id: "runtime_git_sha", message: `Expected Git SHA ${profile.expected_runtime.git_sha}, found ${runtime.gitSha || "unavailable"}`, status: "fail" });
|
|
848
|
+
} else if (profile.expected_runtime?.git_sha) {
|
|
849
|
+
checks.push({ id: "runtime_git_sha", message: `Git SHA ${runtime.gitSha}`, status: "pass" });
|
|
850
|
+
}
|
|
851
|
+
const assetExists = existsSync4(profile.asset_root);
|
|
852
|
+
const assetIsDirectory = assetExists && statSync3(profile.asset_root).isDirectory();
|
|
853
|
+
result.asset_root = { exists: assetExists, is_directory: assetIsDirectory, path: profile.asset_root };
|
|
854
|
+
checks.push({
|
|
855
|
+
id: "asset_root",
|
|
856
|
+
message: assetIsDirectory ? `Asset root exists: ${profile.asset_root}` : `Asset root is missing or not a directory: ${profile.asset_root}`,
|
|
857
|
+
status: assetIsDirectory ? "pass" : "fail"
|
|
858
|
+
});
|
|
859
|
+
const database = inspectDatabase(profile);
|
|
860
|
+
result.database = database;
|
|
861
|
+
checks.push({
|
|
862
|
+
id: "database_exists",
|
|
863
|
+
message: database.exists ? `Database exists: ${profile.database_path}` : `Database does not exist: ${profile.database_path}`,
|
|
864
|
+
status: database.exists ? "pass" : "fail"
|
|
865
|
+
});
|
|
866
|
+
if (database.error) checks.push({ id: "database_read", message: database.error, status: "fail" });
|
|
867
|
+
if (!database.identity) {
|
|
868
|
+
checks.push({ id: "database_identity", message: "Database is not bound to a Lineage profile", status: "fail" });
|
|
869
|
+
} else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment) {
|
|
870
|
+
checks.push({
|
|
871
|
+
id: "database_identity",
|
|
872
|
+
message: `Database identity ${database.identity.profile_id}/${database.identity.environment} does not match ${profile.profile_id}/${profile.environment}`,
|
|
873
|
+
status: "fail"
|
|
874
|
+
});
|
|
875
|
+
} else {
|
|
876
|
+
checks.push({ id: "database_identity", message: `Database is bound to ${profile.profile_id}`, status: "pass" });
|
|
877
|
+
}
|
|
878
|
+
const missingMigrations = (profile.required_schema_migrations || []).filter((key) => !database.migration_keys.includes(key));
|
|
879
|
+
checks.push({
|
|
880
|
+
id: "database_schema",
|
|
881
|
+
message: missingMigrations.length ? `Missing required schema migrations: ${missingMigrations.join(", ")}` : `${database.migration_keys.length} schema migration marker(s) available`,
|
|
882
|
+
status: missingMigrations.length ? "fail" : "pass"
|
|
883
|
+
});
|
|
884
|
+
result.ok = checks.every((check) => check.status !== "fail");
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
function runtimeProfileIdentity(channel) {
|
|
888
|
+
const selector = process.env.LINEAGE_PROFILE;
|
|
889
|
+
const id = process.env.LINEAGE_PROFILE_ID;
|
|
890
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
891
|
+
if (selector && id && environment) {
|
|
892
|
+
return {
|
|
893
|
+
bound: true,
|
|
894
|
+
environment,
|
|
895
|
+
id,
|
|
896
|
+
manifest_path: process.env.LINEAGE_PROFILE_MANIFEST,
|
|
897
|
+
service_origin: process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
bound: false,
|
|
902
|
+
environment: channelEnvironment(channel),
|
|
903
|
+
id: "legacy-unbound",
|
|
904
|
+
warning: id || environment || process.env.LINEAGE_PROFILE_MANIFEST ? "Invalid unbound runtime: derived profile identity was supplied without a resolved LINEAGE_PROFILE selector." : "Legacy unbound runtime: database and asset paths are not protected by a named profile."
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
function assertUnselectedDatabaseIsUnbound(runtime) {
|
|
908
|
+
if (runtime.database.exists && runtime.database.error) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`Database ${runtime.database.path} identity could not be verified: ${runtime.database.error}; refusing unselected access`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
if (runtime.schema.profile_identity_rows !== void 0 && runtime.schema.profile_identity_rows !== 1) {
|
|
914
|
+
throw new Error(
|
|
915
|
+
`Database ${runtime.database.path} has invalid Lineage profile identity row count ${runtime.schema.profile_identity_rows}; refusing unselected access`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (!runtime.schema.profile_id) return;
|
|
919
|
+
const environment = runtime.schema.profile_environment || "unknown";
|
|
920
|
+
throw new Error(
|
|
921
|
+
`Database ${runtime.database.path} is bound to Lineage profile ${runtime.schema.profile_id}/${environment}; select that profile with --profile`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/server/profileWriterLease.ts
|
|
926
|
+
import { createHash as createHash2, randomUUID, timingSafeEqual } from "node:crypto";
|
|
927
|
+
import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, realpathSync, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
928
|
+
import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
|
|
929
|
+
var writerLeaseSchemaVersion = "lineage.profile_writer_lease.v1";
|
|
930
|
+
var ownerFileName = "owner.json";
|
|
931
|
+
var ProfileWriterLeaseConflictError = class extends Error {
|
|
932
|
+
constructor(message, owner) {
|
|
933
|
+
super(message);
|
|
934
|
+
this.owner = owner;
|
|
935
|
+
this.name = "ProfileWriterLeaseConflictError";
|
|
936
|
+
}
|
|
937
|
+
owner;
|
|
938
|
+
};
|
|
939
|
+
function canonicalDatabasePath(databasePath) {
|
|
940
|
+
const resolved = resolve4(databasePath);
|
|
941
|
+
const missingSegments = [];
|
|
942
|
+
let candidate = resolved;
|
|
943
|
+
while (!existsSync5(candidate)) {
|
|
944
|
+
const parent = dirname4(candidate);
|
|
945
|
+
if (parent === candidate) return resolved;
|
|
946
|
+
missingSegments.unshift(basename3(candidate));
|
|
947
|
+
candidate = parent;
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
return join5(realpathSync(candidate), ...missingSegments);
|
|
951
|
+
} catch {
|
|
952
|
+
return resolved;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function profileWriterLockPath(profile) {
|
|
956
|
+
return `${canonicalDatabasePath(profile.database_path)}.writer.lock`;
|
|
957
|
+
}
|
|
958
|
+
function ownerPath(lockPath) {
|
|
959
|
+
return join5(lockPath, ownerFileName);
|
|
960
|
+
}
|
|
961
|
+
function errorCode(error) {
|
|
962
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
963
|
+
}
|
|
964
|
+
function readOwner(lockPath) {
|
|
965
|
+
const stat = lstatSync(lockPath);
|
|
966
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
967
|
+
throw new Error(`Writer lease path is not a safe directory: ${lockPath}; manual inspection is required`);
|
|
968
|
+
}
|
|
969
|
+
let raw;
|
|
970
|
+
try {
|
|
971
|
+
raw = JSON.parse(readFileSync4(ownerPath(lockPath), "utf8"));
|
|
972
|
+
} catch (error) {
|
|
973
|
+
throw new Error(`Writer lease metadata is unreadable at ${lockPath}; refusing automatic recovery`, { cause: error });
|
|
974
|
+
}
|
|
975
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
976
|
+
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
977
|
+
}
|
|
978
|
+
const owner = raw;
|
|
979
|
+
if (owner.schema_version !== writerLeaseSchemaVersion || typeof owner.profile_id !== "string" || owner.environment !== "production" && owner.environment !== "preview" && owner.environment !== "development" || owner.role !== "service" && owner.role !== "cli" || !Number.isSafeInteger(owner.pid) || Number(owner.pid) <= 0 || typeof owner.acquired_at !== "string" || Number.isNaN(Date.parse(owner.acquired_at)) || owner.role === "service" && (typeof owner.service_origin !== "string" || !owner.service_origin) || owner.role === "cli" && owner.service_origin !== void 0 || typeof owner.token !== "string" || owner.token.length < 16) {
|
|
980
|
+
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
981
|
+
}
|
|
982
|
+
return owner;
|
|
983
|
+
}
|
|
984
|
+
function processIsAlive(pid) {
|
|
985
|
+
try {
|
|
986
|
+
process.kill(pid, 0);
|
|
987
|
+
return true;
|
|
988
|
+
} catch (error) {
|
|
989
|
+
if (errorCode(error) === "ESRCH") return false;
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function createLeaseDirectory(lockPath, owner) {
|
|
994
|
+
mkdirSync4(lockPath, { mode: 448 });
|
|
995
|
+
try {
|
|
996
|
+
writeFileSync2(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
|
|
997
|
+
`, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
998
|
+
} catch (error) {
|
|
999
|
+
rmSync(lockPath, { force: true, recursive: true });
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function reclaimDeadOwner(lockPath, owner) {
|
|
1004
|
+
if (processIsAlive(owner.pid)) {
|
|
1005
|
+
const { token: _token, ...publicOwner } = owner;
|
|
1006
|
+
throw new ProfileWriterLeaseConflictError(
|
|
1007
|
+
`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`,
|
|
1008
|
+
publicOwner
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
const tokenFence = createHash2("sha256").update(owner.token).digest("hex").slice(0, 24);
|
|
1012
|
+
const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
|
|
1013
|
+
try {
|
|
1014
|
+
renameSync(lockPath, tombstone);
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
if (errorCode(error) === "ENOENT" || errorCode(error) === "EEXIST" || errorCode(error) === "ENOTEMPTY") return;
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function acquireProfileWriterLease(profile, channel, role = "service") {
|
|
1021
|
+
assertProfileChannel(profile, channel);
|
|
1022
|
+
const lockPath = profileWriterLockPath(profile);
|
|
1023
|
+
const owner = {
|
|
1024
|
+
acquired_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1025
|
+
environment: profile.environment,
|
|
1026
|
+
pid: process.pid,
|
|
1027
|
+
profile_id: profile.profile_id,
|
|
1028
|
+
role,
|
|
1029
|
+
schema_version: writerLeaseSchemaVersion,
|
|
1030
|
+
...role === "service" ? { service_origin: profile.service_origin } : {},
|
|
1031
|
+
token: randomUUID()
|
|
1032
|
+
};
|
|
1033
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1034
|
+
try {
|
|
1035
|
+
createLeaseDirectory(lockPath, owner);
|
|
1036
|
+
process.env.LINEAGE_WRITER_LEASE_TOKEN = owner.token;
|
|
1037
|
+
process.env.LINEAGE_WRITER_LOCK_PATH = lockPath;
|
|
1038
|
+
const { token: _token, ...publicOwner } = owner;
|
|
1039
|
+
let released = false;
|
|
1040
|
+
return {
|
|
1041
|
+
authenticate: (candidate) => {
|
|
1042
|
+
if (!candidate) return false;
|
|
1043
|
+
const expected = Buffer.from(owner.token);
|
|
1044
|
+
const actual = Buffer.from(candidate);
|
|
1045
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
1046
|
+
},
|
|
1047
|
+
lock_path: lockPath,
|
|
1048
|
+
owner: publicOwner,
|
|
1049
|
+
release: () => {
|
|
1050
|
+
if (released) return;
|
|
1051
|
+
released = true;
|
|
1052
|
+
try {
|
|
1053
|
+
const current = readOwner(lockPath);
|
|
1054
|
+
if (current.token === owner.token && current.pid === owner.pid) rmSync(lockPath, { force: true, recursive: true });
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
if (process.env.LINEAGE_WRITER_LEASE_TOKEN === owner.token) delete process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
1058
|
+
if (process.env.LINEAGE_WRITER_LOCK_PATH === lockPath) delete process.env.LINEAGE_WRITER_LOCK_PATH;
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
1063
|
+
let existing;
|
|
1064
|
+
try {
|
|
1065
|
+
existing = readOwner(lockPath);
|
|
1066
|
+
} catch (readError) {
|
|
1067
|
+
if (errorCode(readError) === "ENOENT") continue;
|
|
1068
|
+
throw readError;
|
|
1069
|
+
}
|
|
1070
|
+
if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment) {
|
|
1071
|
+
throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; manual inspection is required`, { cause: error });
|
|
1072
|
+
}
|
|
1073
|
+
reclaimDeadOwner(lockPath, existing);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
throw new Error(`Could not acquire writer lease for Lineage profile ${profile.profile_id}; another writer is racing for ownership`);
|
|
1077
|
+
}
|
|
1078
|
+
function assertProfileWriterLeaseHeld() {
|
|
1079
|
+
if (!process.env.LINEAGE_PROFILE) return;
|
|
1080
|
+
const profileId = process.env.LINEAGE_PROFILE_ID;
|
|
1081
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
1082
|
+
const manifestPath = process.env.LINEAGE_PROFILE_MANIFEST;
|
|
1083
|
+
const databasePath = process.env.LINEAGE_DB;
|
|
1084
|
+
const lockPath = process.env.LINEAGE_WRITER_LOCK_PATH;
|
|
1085
|
+
const token = process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
1086
|
+
if (!profileId || !environment || !manifestPath || !databasePath || !lockPath || !token) {
|
|
1087
|
+
throw new Error("Named Lineage profiles may write only through a process holding the profile writer lease");
|
|
1088
|
+
}
|
|
1089
|
+
const expectedLockPath = `${canonicalDatabasePath(databasePath)}.writer.lock`;
|
|
1090
|
+
if (resolve4(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile database");
|
|
1091
|
+
const owner = readOwner(lockPath);
|
|
1092
|
+
if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.environment !== environment) {
|
|
1093
|
+
throw new Error("Current process does not own the selected Lineage profile writer lease");
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function getProfileWriterDelegation(profile) {
|
|
1097
|
+
const lockPath = profileWriterLockPath(profile);
|
|
1098
|
+
if (!existsSync5(lockPath)) {
|
|
1099
|
+
throw new Error(
|
|
1100
|
+
`Lineage profile ${profile.profile_id} mutating commands require its managed service at ${profile.service_origin}; no active service writer lease was found`
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
const owner = readOwner(lockPath);
|
|
1104
|
+
if (owner.profile_id !== profile.profile_id || owner.environment !== profile.environment) {
|
|
1105
|
+
throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; refusing delegation`);
|
|
1106
|
+
}
|
|
1107
|
+
if (owner.role !== "service" || owner.service_origin !== profile.service_origin) {
|
|
1108
|
+
throw new Error(`Writer lease for Lineage profile ${profile.profile_id} is not the configured managed service at ${profile.service_origin}`);
|
|
1109
|
+
}
|
|
1110
|
+
if (!processIsAlive(owner.pid)) {
|
|
1111
|
+
throw new Error(`Managed service writer lease for Lineage profile ${profile.profile_id} is stale; refusing delegation`);
|
|
1112
|
+
}
|
|
1113
|
+
const { token, ...publicOwner } = owner;
|
|
1114
|
+
return { owner: publicOwner, service_origin: owner.service_origin, token };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/server/assetLineageDb.ts
|
|
1118
|
+
var require3 = createRequire2(import.meta.url);
|
|
587
1119
|
function nowIso() {
|
|
588
1120
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
589
1121
|
}
|
|
590
1122
|
function lineageDbPath() {
|
|
591
|
-
return process.env.LINEAGE_DB ||
|
|
1123
|
+
return process.env.LINEAGE_DB || join6(packageRoot, ".lineage", "asset-lineage.sqlite");
|
|
1124
|
+
}
|
|
1125
|
+
function assertOpenedProfileIdentity(database, databasePath) {
|
|
1126
|
+
const selector = process.env.LINEAGE_PROFILE;
|
|
1127
|
+
if (!selector) return;
|
|
1128
|
+
const profile = resolveLineageProfile(selector);
|
|
1129
|
+
if (resolve5(databasePath) !== profile.database_path) {
|
|
1130
|
+
throw new Error(`Profile ${profile.profile_id} requires database ${profile.database_path}, not ${databasePath}`);
|
|
1131
|
+
}
|
|
1132
|
+
const table = database.prepare("select 1 from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
|
|
1133
|
+
if (!table) throw new Error(`Profile database ${databasePath} is not bound to a Lineage profile identity`);
|
|
1134
|
+
const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
|
|
1135
|
+
if (rows.length !== 1) {
|
|
1136
|
+
throw new Error(`Profile database ${databasePath} has invalid Lineage profile identity row count ${rows.length}`);
|
|
1137
|
+
}
|
|
1138
|
+
if (rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
`Profile database ${databasePath} is bound to ${rows[0].profile_id}/${rows[0].environment}, not ${profile.profile_id}/${profile.environment}`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
592
1143
|
}
|
|
593
1144
|
function lineageDb() {
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1145
|
+
const readOnly = process.env.LINEAGE_DB_ACCESS === "read-only";
|
|
1146
|
+
const databasePath = lineageDbPath();
|
|
1147
|
+
if (!readOnly) assertProfileWriterLeaseHeld();
|
|
1148
|
+
if (process.env.LINEAGE_PROFILE && !existsSync6(databasePath)) {
|
|
1149
|
+
throw new Error(`Profile database does not exist: ${databasePath}; bind the profile before opening it`);
|
|
1150
|
+
}
|
|
1151
|
+
if (!readOnly) mkdirSync5(join6(databasePath, ".."), { recursive: true });
|
|
1152
|
+
const { DatabaseSync } = require3("node:sqlite");
|
|
1153
|
+
const database = readOnly ? new DatabaseSync(databasePath, { readOnly: true }) : new DatabaseSync(databasePath);
|
|
1154
|
+
try {
|
|
1155
|
+
assertOpenedProfileIdentity(database, databasePath);
|
|
1156
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
1157
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
1158
|
+
if (readOnly) return database;
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
database.close();
|
|
1161
|
+
throw error;
|
|
1162
|
+
}
|
|
1163
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
599
1164
|
database.exec(`
|
|
600
1165
|
create table if not exists projects (
|
|
601
1166
|
id text primary key,
|
|
@@ -1311,12 +1876,12 @@ function randomId(prefix) {
|
|
|
1311
1876
|
return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
|
|
1312
1877
|
}
|
|
1313
1878
|
function tokenHash(token) {
|
|
1314
|
-
return
|
|
1879
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
1315
1880
|
}
|
|
1316
1881
|
function safeEqual(a, b) {
|
|
1317
1882
|
const left = Buffer.from(a);
|
|
1318
1883
|
const right = Buffer.from(b);
|
|
1319
|
-
return left.length === right.length &&
|
|
1884
|
+
return left.length === right.length && timingSafeEqual2(left, right);
|
|
1320
1885
|
}
|
|
1321
1886
|
function expiresAtFrom(timestamp, ttlSeconds) {
|
|
1322
1887
|
return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
|
|
@@ -1343,6 +1908,12 @@ function derivedState(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1343
1908
|
}
|
|
1344
1909
|
function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
1345
1910
|
const heartbeatAt = String(row.heartbeat_at);
|
|
1911
|
+
const persistedStatus = String(row.status);
|
|
1912
|
+
const state = derivedState({
|
|
1913
|
+
expires_at: String(row.expires_at),
|
|
1914
|
+
heartbeat_at: heartbeatAt,
|
|
1915
|
+
status: persistedStatus
|
|
1916
|
+
}, now);
|
|
1346
1917
|
return {
|
|
1347
1918
|
id: String(row.id),
|
|
1348
1919
|
project: String(row.project_id),
|
|
@@ -1354,7 +1925,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1354
1925
|
agent_name: String(row.agent_name),
|
|
1355
1926
|
agent_kind: String(row.agent_kind),
|
|
1356
1927
|
thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
|
|
1357
|
-
status:
|
|
1928
|
+
status: persistedStatus === "active" && state === "expired" ? "expired" : persistedStatus,
|
|
1358
1929
|
created_at: String(row.created_at),
|
|
1359
1930
|
heartbeat_at: heartbeatAt,
|
|
1360
1931
|
expires_at: String(row.expires_at),
|
|
@@ -1364,11 +1935,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1364
1935
|
override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
|
|
1365
1936
|
metadata: parseMetadata(row.metadata_json),
|
|
1366
1937
|
heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
|
|
1367
|
-
derived_state:
|
|
1368
|
-
expires_at: String(row.expires_at),
|
|
1369
|
-
heartbeat_at: heartbeatAt,
|
|
1370
|
-
status: String(row.status)
|
|
1371
|
-
}, now)
|
|
1938
|
+
derived_state: state
|
|
1372
1939
|
};
|
|
1373
1940
|
}
|
|
1374
1941
|
function eventToRow(row) {
|
|
@@ -1510,7 +2077,7 @@ function createAgentClaim(fields) {
|
|
|
1510
2077
|
function listAgentClaims(project) {
|
|
1511
2078
|
const database = lineageDb();
|
|
1512
2079
|
try {
|
|
1513
|
-
expireActiveClaims(database);
|
|
2080
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
|
|
1514
2081
|
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();
|
|
1515
2082
|
return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
|
|
1516
2083
|
} finally {
|
|
@@ -1520,7 +2087,7 @@ function listAgentClaims(project) {
|
|
|
1520
2087
|
function inspectAgentClaim(claimId, project) {
|
|
1521
2088
|
const database = lineageDb();
|
|
1522
2089
|
try {
|
|
1523
|
-
expireActiveClaims(database);
|
|
2090
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
|
|
1524
2091
|
const claim = findClaimById(database, claimId, project);
|
|
1525
2092
|
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1526
2093
|
const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
|
|
@@ -1657,7 +2224,7 @@ function validateAgentClaimForWrite(fields) {
|
|
|
1657
2224
|
}
|
|
1658
2225
|
|
|
1659
2226
|
// src/server/assetLineage.ts
|
|
1660
|
-
import { join as
|
|
2227
|
+
import { join as join7 } from "node:path";
|
|
1661
2228
|
|
|
1662
2229
|
// src/server/assetLineageSelection.ts
|
|
1663
2230
|
function selectedRows(database, project, root) {
|
|
@@ -2208,9 +2775,18 @@ function knownRoots(database, project) {
|
|
|
2208
2775
|
and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
|
|
2209
2776
|
group by parent_asset_id
|
|
2210
2777
|
`).all(project, project, project, project);
|
|
2211
|
-
|
|
2778
|
+
const roots = /* @__PURE__ */ new Map();
|
|
2779
|
+
for (const row of rows) {
|
|
2780
|
+
const existing = roots.get(row.root_asset_id);
|
|
2781
|
+
roots.set(row.root_asset_id, {
|
|
2782
|
+
root_asset_id: row.root_asset_id,
|
|
2783
|
+
selected_at: row.selected_at || existing?.selected_at
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
return [...roots.values()];
|
|
2212
2787
|
}
|
|
2213
2788
|
function seedLegacyWorkspaces(database, project) {
|
|
2789
|
+
if (process.env.LINEAGE_DB_ACCESS === "read-only") return;
|
|
2214
2790
|
ensureProject2(database, project);
|
|
2215
2791
|
const timestamp = nowIso();
|
|
2216
2792
|
const statement = database.prepare(`
|
|
@@ -2234,6 +2810,34 @@ function seedLegacyWorkspaces(database, project) {
|
|
|
2234
2810
|
);
|
|
2235
2811
|
}
|
|
2236
2812
|
}
|
|
2813
|
+
function inferredLegacyWorkspaces(database, project) {
|
|
2814
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") return [];
|
|
2815
|
+
const persistedRoots = new Set(
|
|
2816
|
+
database.prepare("select root_asset_id from lineage_workspaces where project_id = ?").all(project).map((row) => row.root_asset_id)
|
|
2817
|
+
);
|
|
2818
|
+
const asset = database.prepare("select id, title from assets where project_id = ? and id = ?");
|
|
2819
|
+
return knownRoots(database, project).flatMap((root) => {
|
|
2820
|
+
if (persistedRoots.has(root.root_asset_id)) return [];
|
|
2821
|
+
const row = asset.get(project, root.root_asset_id);
|
|
2822
|
+
if (!row) return [];
|
|
2823
|
+
const timestamp = root.selected_at || nowIso();
|
|
2824
|
+
return [{
|
|
2825
|
+
active_at: root.selected_at,
|
|
2826
|
+
created_at: timestamp,
|
|
2827
|
+
created_by: "system",
|
|
2828
|
+
id: lineageWorkspaceId(project, root.root_asset_id),
|
|
2829
|
+
project,
|
|
2830
|
+
root_asset_id: root.root_asset_id,
|
|
2831
|
+
status: "active",
|
|
2832
|
+
title: `${row.title} lineage`,
|
|
2833
|
+
updated_at: timestamp
|
|
2834
|
+
}];
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
function sortWorkspaces(workspaces) {
|
|
2838
|
+
const statusRank = { active: 0, paused: 1, archived: 2 };
|
|
2839
|
+
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));
|
|
2840
|
+
}
|
|
2237
2841
|
function listRows(database, project) {
|
|
2238
2842
|
return database.prepare(`
|
|
2239
2843
|
select * from lineage_workspaces
|
|
@@ -2245,23 +2849,15 @@ function listRows(database, project) {
|
|
|
2245
2849
|
title
|
|
2246
2850
|
`).all(project).map(rowToWorkspace);
|
|
2247
2851
|
}
|
|
2248
|
-
function activeWorkspace(database, project) {
|
|
2249
|
-
const row = database.prepare(`
|
|
2250
|
-
select * from lineage_workspaces
|
|
2251
|
-
where project_id = ? and status != 'archived'
|
|
2252
|
-
order by active_at desc nulls last, updated_at desc
|
|
2253
|
-
limit 1
|
|
2254
|
-
`).get(project);
|
|
2255
|
-
return row ? rowToWorkspace(row) : null;
|
|
2256
|
-
}
|
|
2257
2852
|
function listLineageWorkspaces(project) {
|
|
2258
2853
|
const database = lineageDb();
|
|
2259
2854
|
try {
|
|
2260
2855
|
seedLegacyWorkspaces(database, project);
|
|
2856
|
+
const workspaces = sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]);
|
|
2261
2857
|
return {
|
|
2262
2858
|
project,
|
|
2263
|
-
active_workspace:
|
|
2264
|
-
workspaces
|
|
2859
|
+
active_workspace: workspaces.find((workspace) => workspace.status !== "archived") || null,
|
|
2860
|
+
workspaces,
|
|
2265
2861
|
fetchedAt: nowIso()
|
|
2266
2862
|
};
|
|
2267
2863
|
} finally {
|
|
@@ -2272,7 +2868,7 @@ function activeLineageWorkspaceRoot(project) {
|
|
|
2272
2868
|
const database = lineageDb();
|
|
2273
2869
|
try {
|
|
2274
2870
|
seedLegacyWorkspaces(database, project);
|
|
2275
|
-
return
|
|
2871
|
+
return sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]).find((workspace) => workspace.status !== "archived")?.root_asset_id;
|
|
2276
2872
|
} finally {
|
|
2277
2873
|
database.close();
|
|
2278
2874
|
}
|
|
@@ -2331,7 +2927,7 @@ function upsertProject(database, project) {
|
|
|
2331
2927
|
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
2332
2928
|
values (?, ?, ?, ?, ?)
|
|
2333
2929
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
2334
|
-
`).run(project, project,
|
|
2930
|
+
`).run(project, project, join7(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
2335
2931
|
}
|
|
2336
2932
|
function upsertAsset(database, project, asset) {
|
|
2337
2933
|
const timestamp = nowIso();
|
|
@@ -2970,16 +3566,34 @@ function clearLineageRerollRequest(project, fields) {
|
|
|
2970
3566
|
}
|
|
2971
3567
|
}
|
|
2972
3568
|
|
|
2973
|
-
// src/server/
|
|
2974
|
-
|
|
3569
|
+
// src/server/lineageRuntimeCommand.ts
|
|
3570
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
3571
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
3572
|
+
import { join as join8 } from "node:path";
|
|
3573
|
+
var require4 = createRequire3(import.meta.url);
|
|
3574
|
+
function lineagePublicPackageCommand() {
|
|
3575
|
+
if (process.env.LINEAGE_CHANNEL === "dev") {
|
|
3576
|
+
const sourceCli = join8(packageRoot, "src", "cli", "lineage-dev.ts");
|
|
3577
|
+
const builtCli = join8(packageRoot, "dist", "cli", "lineage-dev.js");
|
|
3578
|
+
return existsSync7(sourceCli) ? `${shellQuote(process.execPath)} --import ${shellQuote(require4.resolve("tsx"))} ${shellQuote(sourceCli)}` : `${shellQuote(process.execPath)} ${shellQuote(builtCli)}`;
|
|
3579
|
+
}
|
|
3580
|
+
if (process.env.LINEAGE_CHANNEL === "preview") return "LINEAGE_CHANNEL=preview npx --package @mean-weasel/lineage@next lineage-dev";
|
|
3581
|
+
return "npx @mean-weasel/lineage";
|
|
3582
|
+
}
|
|
2975
3583
|
function shellQuote(value) {
|
|
2976
3584
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
2977
3585
|
}
|
|
3586
|
+
function lineageRuntimeSelector() {
|
|
3587
|
+
const manifest = process.env.LINEAGE_PROFILE_MANIFEST?.trim();
|
|
3588
|
+
return manifest ? `--profile ${shellQuote(manifest)}` : `--db ${shellQuote(lineageDbPath())}`;
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
// src/server/assetLineageHandoff.ts
|
|
2978
3592
|
function lineageCommand(command, project, rootAssetId) {
|
|
2979
|
-
return `${
|
|
3593
|
+
return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
|
|
2980
3594
|
}
|
|
2981
3595
|
function linkChildCommand(project, rootAssetId) {
|
|
2982
|
-
return `${
|
|
3596
|
+
return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
2983
3597
|
}
|
|
2984
3598
|
function rerollImportGuidance(rootAssetId, targetAssetId) {
|
|
2985
3599
|
return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
|
|
@@ -3021,7 +3635,7 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
3021
3635
|
},
|
|
3022
3636
|
handoff: {
|
|
3023
3637
|
next_command: lineageCommand("next", project, next.root_asset_id),
|
|
3024
|
-
inspect_command: asset ? `${
|
|
3638
|
+
inspect_command: asset ? `${lineagePublicPackageCommand()} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} ${lineageRuntimeSelector()} --json` : void 0,
|
|
3025
3639
|
link_child_command: asset ? linkChildCommand(project, next.root_asset_id) : void 0
|
|
3026
3640
|
},
|
|
3027
3641
|
fetchedAt: nowIso()
|
|
@@ -3073,9 +3687,9 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
3073
3687
|
}
|
|
3074
3688
|
|
|
3075
3689
|
// src/server/generationReceipts.ts
|
|
3076
|
-
import { existsSync as
|
|
3077
|
-
import { relative as relative2, resolve as
|
|
3078
|
-
import { randomUUID } from "node:crypto";
|
|
3690
|
+
import { existsSync as existsSync8, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
|
|
3691
|
+
import { relative as relative2, resolve as resolve6 } from "node:path";
|
|
3692
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3079
3693
|
var adapterVersion = "generation-receipts-v1";
|
|
3080
3694
|
var provider = "codex-handoff";
|
|
3081
3695
|
var GenerationReceiptError = class extends Error {
|
|
@@ -3086,7 +3700,7 @@ var GenerationReceiptError = class extends Error {
|
|
|
3086
3700
|
status;
|
|
3087
3701
|
};
|
|
3088
3702
|
function jobId() {
|
|
3089
|
-
return `gen-${Date.now().toString(36)}-${
|
|
3703
|
+
return `gen-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`;
|
|
3090
3704
|
}
|
|
3091
3705
|
function quote(value) {
|
|
3092
3706
|
return JSON.stringify(value);
|
|
@@ -3096,7 +3710,7 @@ function parseJson(value, fallback) {
|
|
|
3096
3710
|
return JSON.parse(value);
|
|
3097
3711
|
}
|
|
3098
3712
|
function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
|
|
3099
|
-
const importCommand =
|
|
3713
|
+
const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
3100
3714
|
return {
|
|
3101
3715
|
schema_version: "lineage.generation_handoff.v1",
|
|
3102
3716
|
provider,
|
|
@@ -3232,7 +3846,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
3232
3846
|
fetchedAt: timestamp
|
|
3233
3847
|
}
|
|
3234
3848
|
};
|
|
3235
|
-
const
|
|
3849
|
+
const preview2 = {
|
|
3236
3850
|
id,
|
|
3237
3851
|
project_id: project,
|
|
3238
3852
|
provider,
|
|
@@ -3258,7 +3872,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
3258
3872
|
created_at: timestamp
|
|
3259
3873
|
}]
|
|
3260
3874
|
};
|
|
3261
|
-
if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job:
|
|
3875
|
+
if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview2 };
|
|
3262
3876
|
const database = lineageDb();
|
|
3263
3877
|
try {
|
|
3264
3878
|
database.exec("BEGIN IMMEDIATE");
|
|
@@ -3270,7 +3884,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
3270
3884
|
) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
|
|
3271
3885
|
`).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
|
|
3272
3886
|
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));
|
|
3273
|
-
insertReceipt(database, id, "plan", "reroll plan",
|
|
3887
|
+
insertReceipt(database, id, "plan", "reroll plan", preview2.receipts[0].payload);
|
|
3274
3888
|
database.exec("COMMIT");
|
|
3275
3889
|
} catch (error) {
|
|
3276
3890
|
database.exec("ROLLBACK");
|
|
@@ -3286,14 +3900,14 @@ function isPathInside(child, parent) {
|
|
|
3286
3900
|
return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
|
|
3287
3901
|
}
|
|
3288
3902
|
function resolveScratchFile(file) {
|
|
3289
|
-
const scratchRoot =
|
|
3290
|
-
const candidate = file.startsWith(".asset-scratch/") ||
|
|
3903
|
+
const scratchRoot = resolve6(repoRoot, ".asset-scratch");
|
|
3904
|
+
const candidate = file.startsWith(".asset-scratch/") || resolve6(file).startsWith(scratchRoot) ? resolve6(repoRoot, file) : resolve6(scratchRoot, file);
|
|
3291
3905
|
if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
3292
|
-
if (!
|
|
3293
|
-
const realScratchRoot =
|
|
3294
|
-
const realCandidate =
|
|
3906
|
+
if (!existsSync8(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
|
|
3907
|
+
const realScratchRoot = realpathSync2(scratchRoot);
|
|
3908
|
+
const realCandidate = realpathSync2(candidate);
|
|
3295
3909
|
if (!isPathInside(realCandidate, realScratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
3296
|
-
const stats =
|
|
3910
|
+
const stats = statSync4(candidate);
|
|
3297
3911
|
if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
|
|
3298
3912
|
const checksum = fileSha256(candidate);
|
|
3299
3913
|
return {
|
|
@@ -3373,17 +3987,19 @@ function importImageRerollOutput(project = defaultProject, fields) {
|
|
|
3373
3987
|
}
|
|
3374
3988
|
|
|
3375
3989
|
// src/server/lineageSelectionPacket.ts
|
|
3376
|
-
import { createHash as
|
|
3377
|
-
import { existsSync as
|
|
3378
|
-
import { isAbsolute, resolve as
|
|
3990
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3991
|
+
import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
|
|
3992
|
+
import { isAbsolute as isAbsolute2, resolve as resolve7 } from "node:path";
|
|
3379
3993
|
var LineageSelectionPacketError = class extends Error {
|
|
3380
|
-
constructor(message, warnings = [], errors = []) {
|
|
3994
|
+
constructor(message, warnings = [], errors = [], diagnostics = []) {
|
|
3381
3995
|
super(message);
|
|
3382
3996
|
this.warnings = warnings;
|
|
3383
3997
|
this.errors = errors;
|
|
3998
|
+
this.diagnostics = diagnostics;
|
|
3384
3999
|
}
|
|
3385
4000
|
warnings;
|
|
3386
4001
|
errors;
|
|
4002
|
+
diagnostics;
|
|
3387
4003
|
};
|
|
3388
4004
|
function listAllAssets(project) {
|
|
3389
4005
|
const pageSize = 100;
|
|
@@ -3419,15 +4035,15 @@ function resolveWorkspace(project, options) {
|
|
|
3419
4035
|
}
|
|
3420
4036
|
function resolveLocalReference(reference) {
|
|
3421
4037
|
if (!reference) return void 0;
|
|
3422
|
-
if (
|
|
3423
|
-
const repoRelative =
|
|
3424
|
-
if (
|
|
3425
|
-
return
|
|
4038
|
+
if (isAbsolute2(reference)) return reference;
|
|
4039
|
+
const repoRelative = resolve7(repoRoot, reference);
|
|
4040
|
+
if (existsSync9(repoRelative)) return repoRelative;
|
|
4041
|
+
return resolve7(repoRoot, ".asset-scratch", reference);
|
|
3426
4042
|
}
|
|
3427
4043
|
function fileSize(path) {
|
|
3428
|
-
if (!path || !
|
|
4044
|
+
if (!path || !existsSync9(path)) return void 0;
|
|
3429
4045
|
try {
|
|
3430
|
-
return
|
|
4046
|
+
return statSync5(path).size;
|
|
3431
4047
|
} catch {
|
|
3432
4048
|
return void 0;
|
|
3433
4049
|
}
|
|
@@ -3453,7 +4069,7 @@ function currentAttemptFor(node) {
|
|
|
3453
4069
|
function assetForNode(node, catalogAsset, warnings) {
|
|
3454
4070
|
const localReference = catalogAsset?.local?.absolute_path || node.current_attempt?.file_path || node.local_path || catalogAsset?.local?.relative_path;
|
|
3455
4071
|
const absolutePath = catalogAsset?.local?.absolute_path || resolveLocalReference(localReference);
|
|
3456
|
-
const localExists = absolutePath ?
|
|
4072
|
+
const localExists = absolutePath ? existsSync9(absolutePath) : false;
|
|
3457
4073
|
const hasLocalClaim = Boolean(absolutePath || node.local_path || catalogAsset?.local?.relative_path);
|
|
3458
4074
|
const s3Key = catalogAsset?.s3?.key || node.s3_key;
|
|
3459
4075
|
const hasS3 = Boolean(s3Key);
|
|
@@ -3498,10 +4114,192 @@ function assetForNode(node, catalogAsset, warnings) {
|
|
|
3498
4114
|
};
|
|
3499
4115
|
}
|
|
3500
4116
|
function packetId(packetIdentity) {
|
|
3501
|
-
const digest =
|
|
4117
|
+
const digest = createHash4("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
|
|
3502
4118
|
return `lineage_packet_${digest}`;
|
|
3503
4119
|
}
|
|
3504
|
-
|
|
4120
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
4121
|
+
function canonicalValue(value) {
|
|
4122
|
+
if (Array.isArray(value)) return value.map((item) => canonicalValue(item));
|
|
4123
|
+
if (value && typeof value === "object") {
|
|
4124
|
+
return Object.fromEntries(
|
|
4125
|
+
Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, canonicalValue(item)])
|
|
4126
|
+
);
|
|
4127
|
+
}
|
|
4128
|
+
return value;
|
|
4129
|
+
}
|
|
4130
|
+
function canonicalLineageSelectionPacketIdentityJson(projection) {
|
|
4131
|
+
return JSON.stringify(canonicalValue(projection));
|
|
4132
|
+
}
|
|
4133
|
+
function lineageSelectionPacketV2IdentityProjection(packet) {
|
|
4134
|
+
const assetsById = new Map(packet.assets.map((asset) => [asset.asset_id, asset]));
|
|
4135
|
+
const selection = packet.selection.items.map((item) => {
|
|
4136
|
+
const asset = assetsById.get(item.asset_id);
|
|
4137
|
+
if (!asset) {
|
|
4138
|
+
const diagnostics = [{ asset_id: item.asset_id, code: "selected_asset_missing", severity: "error" }];
|
|
4139
|
+
throw new LineageSelectionPacketError(
|
|
4140
|
+
`Selected asset ${item.asset_id} is absent from the v2 packet asset envelope.`,
|
|
4141
|
+
[],
|
|
4142
|
+
["selected_asset_missing"],
|
|
4143
|
+
diagnostics
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
return {
|
|
4147
|
+
asset_id: item.asset_id,
|
|
4148
|
+
campaign: asset.campaign,
|
|
4149
|
+
channel: asset.channel,
|
|
4150
|
+
current_attempt: {
|
|
4151
|
+
asset_id: asset.current_attempt.asset_id,
|
|
4152
|
+
attempt_index: asset.current_attempt.attempt_index,
|
|
4153
|
+
checksum_sha256: asset.current_attempt.checksum_sha256,
|
|
4154
|
+
id: asset.current_attempt.id,
|
|
4155
|
+
source: asset.current_attempt.source
|
|
4156
|
+
},
|
|
4157
|
+
media_type: asset.media_type,
|
|
4158
|
+
mime_type: asset.mime_type,
|
|
4159
|
+
position: item.position,
|
|
4160
|
+
selection_note: item.selection_note,
|
|
4161
|
+
title: asset.title
|
|
4162
|
+
};
|
|
4163
|
+
});
|
|
4164
|
+
return {
|
|
4165
|
+
schema_version: "lineage.selection_packet.v2",
|
|
4166
|
+
project: packet.project,
|
|
4167
|
+
product: packet.product,
|
|
4168
|
+
workspace: {
|
|
4169
|
+
id: packet.workspace.id,
|
|
4170
|
+
root_asset_id: packet.workspace.root_asset_id
|
|
4171
|
+
},
|
|
4172
|
+
context: {
|
|
4173
|
+
campaign: packet.context.campaign,
|
|
4174
|
+
channel: packet.context.channel,
|
|
4175
|
+
labels: packet.context.labels,
|
|
4176
|
+
notes: packet.context.notes
|
|
4177
|
+
},
|
|
4178
|
+
selection,
|
|
4179
|
+
diagnostics: packet.diagnostics.map((diagnostic) => ({
|
|
4180
|
+
code: diagnostic.code,
|
|
4181
|
+
severity: diagnostic.severity,
|
|
4182
|
+
...diagnostic.asset_id ? { asset_id: diagnostic.asset_id } : {}
|
|
4183
|
+
}))
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
function lineageSelectionPacketV2IdentitySha256(packet) {
|
|
4187
|
+
const projection = lineageSelectionPacketV2IdentityProjection(packet);
|
|
4188
|
+
return createHash4("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
|
|
4189
|
+
}
|
|
4190
|
+
function addDiagnostic(diagnostics, diagnostic) {
|
|
4191
|
+
if (diagnostics.some((item) => item.code === diagnostic.code && item.severity === diagnostic.severity && item.asset_id === diagnostic.asset_id)) return;
|
|
4192
|
+
diagnostics.push(diagnostic);
|
|
4193
|
+
}
|
|
4194
|
+
function requireV2CurrentAttempt(node, warnings, diagnostics) {
|
|
4195
|
+
const attempt = node.current_attempt;
|
|
4196
|
+
if (!attempt) {
|
|
4197
|
+
const message = `Selected asset ${node.asset_id} has no current attempt identity.`;
|
|
4198
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_missing", severity: "error" };
|
|
4199
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_missing"], [...diagnostics, diagnostic]);
|
|
4200
|
+
}
|
|
4201
|
+
if (!attempt.id || !attempt.asset_id || !Number.isInteger(attempt.attempt_index) || attempt.attempt_index <= 0 || !attempt.source || attempt.is_current !== true) {
|
|
4202
|
+
const message = `Selected asset ${node.asset_id} has malformed current attempt identity.`;
|
|
4203
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_identity", severity: "error" };
|
|
4204
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_identity"], [...diagnostics, diagnostic]);
|
|
4205
|
+
}
|
|
4206
|
+
if (!attempt.checksum_sha256 || !SHA256_PATTERN.test(attempt.checksum_sha256)) {
|
|
4207
|
+
const message = `Selected asset ${node.asset_id} current attempt does not have a valid lowercase SHA-256 checksum.`;
|
|
4208
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_checksum", severity: "error" };
|
|
4209
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_checksum"], [...diagnostics, diagnostic]);
|
|
4210
|
+
}
|
|
4211
|
+
return {
|
|
4212
|
+
asset_id: attempt.asset_id,
|
|
4213
|
+
attempt_index: attempt.attempt_index,
|
|
4214
|
+
checksum_sha256: attempt.checksum_sha256,
|
|
4215
|
+
file_path: attempt.file_path,
|
|
4216
|
+
generation_job_id: attempt.generation_job_id,
|
|
4217
|
+
id: attempt.id,
|
|
4218
|
+
is_current: attempt.is_current,
|
|
4219
|
+
source: attempt.source
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
function assetForNodeV2(node, visibleCatalogAsset, catalogById, warnings, errors, diagnostics) {
|
|
4223
|
+
const currentAttempt = requireV2CurrentAttempt(node, warnings, diagnostics);
|
|
4224
|
+
const currentAttemptCatalogAsset = catalogById.get(currentAttempt.asset_id);
|
|
4225
|
+
const isVisibleNodeAttempt = currentAttempt.asset_id === node.asset_id;
|
|
4226
|
+
const mediaCatalogAsset = currentAttemptCatalogAsset || (isVisibleNodeAttempt ? visibleCatalogAsset : void 0);
|
|
4227
|
+
const localReference = currentAttempt.file_path || mediaCatalogAsset?.local?.absolute_path || mediaCatalogAsset?.local?.relative_path || (isVisibleNodeAttempt ? node.local_path : void 0);
|
|
4228
|
+
const absolutePath = resolveLocalReference(localReference);
|
|
4229
|
+
const localExists = absolutePath ? existsSync9(absolutePath) : false;
|
|
4230
|
+
const hasLocalClaim = Boolean(localReference);
|
|
4231
|
+
const s3Key = mediaCatalogAsset?.s3?.key || (isVisibleNodeAttempt ? node.s3_key : void 0);
|
|
4232
|
+
const hasS3 = Boolean(s3Key);
|
|
4233
|
+
const mimeType = mediaCatalogAsset?.local?.content_type || mediaCatalogAsset?.s3?.content_type || visibleCatalogAsset?.local?.content_type || visibleCatalogAsset?.s3?.content_type || (localReference ? contentTypeFor(localReference) : void 0);
|
|
4234
|
+
const mediaType = mediaCatalogAsset?.content_type || visibleCatalogAsset?.content_type || node.media_type;
|
|
4235
|
+
if (localExists && absolutePath && fileSha256(absolutePath) !== currentAttempt.checksum_sha256) {
|
|
4236
|
+
const message = `Selected asset ${node.asset_id} current attempt checksum does not match its local file.`;
|
|
4237
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
|
|
4238
|
+
throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
|
|
4239
|
+
}
|
|
4240
|
+
const s3Checksum = mediaCatalogAsset?.s3?.checksum_sha256;
|
|
4241
|
+
if (hasS3 && s3Checksum && s3Checksum !== currentAttempt.checksum_sha256) {
|
|
4242
|
+
const message = `Selected asset ${node.asset_id} current attempt checksum does not match its S3 media envelope.`;
|
|
4243
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
|
|
4244
|
+
throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
|
|
4245
|
+
}
|
|
4246
|
+
if (hasLocalClaim && !localExists) {
|
|
4247
|
+
warnings.push(`Selected asset ${node.asset_id} current attempt has a local path but the file is missing: ${absolutePath || localReference}`);
|
|
4248
|
+
}
|
|
4249
|
+
if (!hasLocalClaim && !hasS3) {
|
|
4250
|
+
const message = `Selected asset ${node.asset_id} current attempt has neither a local file nor S3 key.`;
|
|
4251
|
+
warnings.push(message);
|
|
4252
|
+
errors.push(message);
|
|
4253
|
+
}
|
|
4254
|
+
if (mediaType && mediaType !== "image" && mediaType !== "gif") {
|
|
4255
|
+
warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
|
|
4256
|
+
addDiagnostic(diagnostics, { asset_id: node.asset_id, code: "unsupported_media_type", severity: "warning" });
|
|
4257
|
+
}
|
|
4258
|
+
const conflictingChecksums = [
|
|
4259
|
+
currentAttemptCatalogAsset?.local?.checksum_sha256,
|
|
4260
|
+
currentAttemptCatalogAsset?.s3?.checksum_sha256,
|
|
4261
|
+
visibleCatalogAsset?.local?.checksum_sha256,
|
|
4262
|
+
visibleCatalogAsset?.s3?.checksum_sha256,
|
|
4263
|
+
node.checksum_sha256
|
|
4264
|
+
].filter((checksum) => Boolean(checksum && checksum !== currentAttempt.checksum_sha256));
|
|
4265
|
+
if (conflictingChecksums.length > 0) {
|
|
4266
|
+
warnings.push(`Selected asset ${node.asset_id} catalog or visible-node checksum differs from its current attempt; current attempt checksum is authoritative.`);
|
|
4267
|
+
}
|
|
4268
|
+
return {
|
|
4269
|
+
asset_id: node.asset_id,
|
|
4270
|
+
campaign: visibleCatalogAsset?.campaign || node.campaign,
|
|
4271
|
+
channel: visibleCatalogAsset?.channel || node.channel,
|
|
4272
|
+
checksum_sha256: currentAttempt.checksum_sha256,
|
|
4273
|
+
current_attempt: currentAttempt,
|
|
4274
|
+
local: {
|
|
4275
|
+
absolute_path: absolutePath,
|
|
4276
|
+
content_type: mediaCatalogAsset?.local?.content_type,
|
|
4277
|
+
exists: localExists,
|
|
4278
|
+
relative_path: mediaCatalogAsset?.local?.relative_path || currentAttempt.file_path || (isVisibleNodeAttempt ? node.local_path : void 0),
|
|
4279
|
+
size_bytes: mediaCatalogAsset?.local?.size_bytes || fileSize(absolutePath)
|
|
4280
|
+
},
|
|
4281
|
+
media_type: mediaType,
|
|
4282
|
+
mime_type: mimeType,
|
|
4283
|
+
review_notes: node.review_notes,
|
|
4284
|
+
review_state: node.review_state,
|
|
4285
|
+
s3: {
|
|
4286
|
+
bucket: mediaCatalogAsset?.s3?.bucket,
|
|
4287
|
+
checksum_sha256: mediaCatalogAsset?.s3?.checksum_sha256,
|
|
4288
|
+
content_type: mediaCatalogAsset?.s3?.content_type,
|
|
4289
|
+
etag: mediaCatalogAsset?.s3?.etag,
|
|
4290
|
+
key: s3Key,
|
|
4291
|
+
region: mediaCatalogAsset?.s3?.region,
|
|
4292
|
+
size_bytes: mediaCatalogAsset?.s3?.size_bytes,
|
|
4293
|
+
version_id: mediaCatalogAsset?.s3?.version_id
|
|
4294
|
+
},
|
|
4295
|
+
selection_note: node.selection_note,
|
|
4296
|
+
source: mediaCatalogAsset?.source || visibleCatalogAsset?.source || node.source,
|
|
4297
|
+
status: visibleCatalogAsset?.status || node.status,
|
|
4298
|
+
storage_state: storageState(hasLocalClaim, hasS3),
|
|
4299
|
+
title: visibleCatalogAsset?.title || node.title || node.asset_id
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
function getLineageSelectionPacketV1(project, options = {}) {
|
|
3505
4303
|
const workspace = resolveWorkspace(project, options);
|
|
3506
4304
|
const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
|
|
3507
4305
|
const catalogSummary = validateProject(project);
|
|
@@ -3592,16 +4390,114 @@ function getLineageSelectionPacket(project, options = {}) {
|
|
|
3592
4390
|
}
|
|
3593
4391
|
};
|
|
3594
4392
|
}
|
|
4393
|
+
function getLineageSelectionPacketV2(project, options) {
|
|
4394
|
+
const workspace = resolveWorkspace(project, options);
|
|
4395
|
+
const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
|
|
4396
|
+
const catalogSummary = validateProject(project);
|
|
4397
|
+
const catalogAssets = listAllAssets(project);
|
|
4398
|
+
const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
|
|
4399
|
+
const warnings = [];
|
|
4400
|
+
const errors = [];
|
|
4401
|
+
const diagnostics = [];
|
|
4402
|
+
const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
|
|
4403
|
+
const selectedItems = snapshot.selections.map((selection) => ({
|
|
4404
|
+
asset_id: selection.asset_id,
|
|
4405
|
+
position: selection.position,
|
|
4406
|
+
selected_at: selection.selected_at,
|
|
4407
|
+
selection_note: selection.notes
|
|
4408
|
+
}));
|
|
4409
|
+
if (selectedItems.length === 0) {
|
|
4410
|
+
warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
|
|
4411
|
+
addDiagnostic(diagnostics, { code: "empty_selection", severity: "warning" });
|
|
4412
|
+
}
|
|
4413
|
+
const assets = [];
|
|
4414
|
+
for (const item of selectedItems) {
|
|
4415
|
+
const node = nodeById.get(item.asset_id);
|
|
4416
|
+
if (!node) {
|
|
4417
|
+
const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
|
|
4418
|
+
warnings.push(message);
|
|
4419
|
+
errors.push(message);
|
|
4420
|
+
const diagnostic = { asset_id: item.asset_id, code: "selected_asset_outside_workspace", severity: "error" };
|
|
4421
|
+
addDiagnostic(diagnostics, diagnostic);
|
|
4422
|
+
throw new LineageSelectionPacketError(message, warnings, ["selected_asset_outside_workspace"], diagnostics);
|
|
4423
|
+
}
|
|
4424
|
+
assets.push(assetForNodeV2(node, catalogById.get(item.asset_id), catalogById, warnings, errors, diagnostics));
|
|
4425
|
+
}
|
|
4426
|
+
if (assets.some((asset) => !asset.dimensions)) {
|
|
4427
|
+
warnings.push("Image dimensions are unavailable for one or more selected assets.");
|
|
4428
|
+
}
|
|
4429
|
+
if (options.strict) {
|
|
4430
|
+
const strictErrors = [
|
|
4431
|
+
...selectedItems.length === 0 ? ["empty_selection"] : [],
|
|
4432
|
+
...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
|
|
4433
|
+
...assets.filter((asset) => asset.storage_state === "unresolved").map((asset) => `unresolved_current_attempt_media:${asset.asset_id}`),
|
|
4434
|
+
...diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => `${diagnostic.code}${diagnostic.asset_id ? `:${diagnostic.asset_id}` : ""}`)
|
|
4435
|
+
];
|
|
4436
|
+
if (strictErrors.length > 0) {
|
|
4437
|
+
throw new LineageSelectionPacketError(
|
|
4438
|
+
`Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`,
|
|
4439
|
+
warnings,
|
|
4440
|
+
strictErrors,
|
|
4441
|
+
diagnostics
|
|
4442
|
+
);
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
const packet = {
|
|
4446
|
+
assets,
|
|
4447
|
+
context: {
|
|
4448
|
+
campaign: options.campaign,
|
|
4449
|
+
channel: options.channel,
|
|
4450
|
+
labels: options.labels || [],
|
|
4451
|
+
notes: options.contextNotes
|
|
4452
|
+
},
|
|
4453
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4454
|
+
diagnostics,
|
|
4455
|
+
errors: [...new Set(errors)],
|
|
4456
|
+
identity_sha256: "",
|
|
4457
|
+
kind: "lineage.selection_packet",
|
|
4458
|
+
packet_id: "",
|
|
4459
|
+
product: catalogSummary.product,
|
|
4460
|
+
project,
|
|
4461
|
+
schema_version: "lineage.selection_packet.v2",
|
|
4462
|
+
selection: {
|
|
4463
|
+
asset_ids: selectedItems.map((item) => item.asset_id),
|
|
4464
|
+
count: selectedItems.length,
|
|
4465
|
+
items: selectedItems,
|
|
4466
|
+
root_asset_id: workspace.root_asset_id
|
|
4467
|
+
},
|
|
4468
|
+
source: {
|
|
4469
|
+
app: "lineage",
|
|
4470
|
+
command: options.command,
|
|
4471
|
+
db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
|
|
4472
|
+
package: options.packageVersion
|
|
4473
|
+
},
|
|
4474
|
+
warnings: [...new Set(warnings)],
|
|
4475
|
+
workspace: {
|
|
4476
|
+
active_at: workspace.active_at,
|
|
4477
|
+
id: workspace.id,
|
|
4478
|
+
notes: workspace.notes,
|
|
4479
|
+
root_asset_id: workspace.root_asset_id,
|
|
4480
|
+
status: workspace.status,
|
|
4481
|
+
title: workspace.title
|
|
4482
|
+
}
|
|
4483
|
+
};
|
|
4484
|
+
packet.identity_sha256 = lineageSelectionPacketV2IdentitySha256(packet);
|
|
4485
|
+
packet.packet_id = `lineage_packet_${packet.identity_sha256.slice(0, 24)}`;
|
|
4486
|
+
return packet;
|
|
4487
|
+
}
|
|
4488
|
+
function getLineageSelectionPacket(project, options = {}) {
|
|
4489
|
+
return options.schema === "v2" ? getLineageSelectionPacketV2(project, options) : getLineageSelectionPacketV1(project, options);
|
|
4490
|
+
}
|
|
3595
4491
|
|
|
3596
4492
|
// src/server/runtimeInfo.ts
|
|
3597
4493
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
3598
|
-
import { createRequire as
|
|
3599
|
-
import { existsSync as
|
|
3600
|
-
import { join as
|
|
3601
|
-
var
|
|
4494
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
4495
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, statSync as statSync6 } from "node:fs";
|
|
4496
|
+
import { join as join9 } from "node:path";
|
|
4497
|
+
var require5 = createRequire4(import.meta.url);
|
|
3602
4498
|
function packageInfo() {
|
|
3603
4499
|
try {
|
|
3604
|
-
const info = JSON.parse(
|
|
4500
|
+
const info = JSON.parse(readFileSync5(join9(packageRoot, "package.json"), "utf8"));
|
|
3605
4501
|
return { name: info.name || "@mean-weasel/lineage", version: info.version || "0.0.0" };
|
|
3606
4502
|
} catch {
|
|
3607
4503
|
return { name: "@mean-weasel/lineage", version: "0.0.0" };
|
|
@@ -3617,32 +4513,47 @@ function normalizeRuntimeChannel(value) {
|
|
|
3617
4513
|
function gitSha() {
|
|
3618
4514
|
const envSha = process.env.LINEAGE_GIT_SHA || process.env.GITHUB_SHA;
|
|
3619
4515
|
if (envSha) return envSha.slice(0, 40);
|
|
3620
|
-
if (!
|
|
3621
|
-
const result = spawnSync2("git", ["rev-parse", "--short=12", "HEAD"], { cwd:
|
|
4516
|
+
if (!existsSync10(join9(packageRoot, ".git"))) return void 0;
|
|
4517
|
+
const result = spawnSync2("git", ["rev-parse", "--short=12", "HEAD"], { cwd: packageRoot, encoding: "utf8" });
|
|
3622
4518
|
return result.status === 0 ? result.stdout.trim() || void 0 : void 0;
|
|
3623
4519
|
}
|
|
3624
|
-
function
|
|
4520
|
+
function tableExists2(database, table) {
|
|
3625
4521
|
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
3626
4522
|
}
|
|
3627
4523
|
function tableCount(database, table) {
|
|
3628
|
-
if (!
|
|
4524
|
+
if (!tableExists2(database, table)) return void 0;
|
|
3629
4525
|
const row = database.prepare(`select count(*) count from ${table}`).get();
|
|
3630
4526
|
return typeof row?.count === "number" ? row.count : void 0;
|
|
3631
4527
|
}
|
|
4528
|
+
function migrationKeys(database) {
|
|
4529
|
+
if (!tableExists2(database, "lineage_schema_migrations")) return [];
|
|
4530
|
+
return database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
|
|
4531
|
+
}
|
|
3632
4532
|
function getLineageRuntimeInfo(options = {}) {
|
|
3633
4533
|
const info = packageInfo();
|
|
3634
4534
|
const dbPath = options.dbPath || lineageDbPath();
|
|
3635
|
-
const
|
|
4535
|
+
const channel = normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
|
|
4536
|
+
const databaseInfo = { exists: existsSync10(dbPath), path: dbPath };
|
|
4537
|
+
const schema = { migration_keys: [] };
|
|
3636
4538
|
if (databaseInfo.exists) {
|
|
3637
4539
|
try {
|
|
3638
|
-
const stat =
|
|
4540
|
+
const stat = statSync6(dbPath);
|
|
3639
4541
|
databaseInfo.modified_at = stat.mtime.toISOString();
|
|
3640
4542
|
databaseInfo.size_bytes = stat.size;
|
|
3641
|
-
const { DatabaseSync } =
|
|
4543
|
+
const { DatabaseSync } = require5("node:sqlite");
|
|
3642
4544
|
const database = new DatabaseSync(dbPath, { readOnly: true });
|
|
3643
4545
|
try {
|
|
3644
4546
|
databaseInfo.projects = tableCount(database, "projects");
|
|
3645
4547
|
databaseInfo.workspaces = tableCount(database, "lineage_workspaces");
|
|
4548
|
+
schema.migration_keys = migrationKeys(database);
|
|
4549
|
+
if (tableExists2(database, "lineage_profile_identity")) {
|
|
4550
|
+
const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
|
|
4551
|
+
schema.profile_identity_rows = rows.length;
|
|
4552
|
+
if (rows.length === 1) {
|
|
4553
|
+
schema.profile_id = rows[0].profile_id;
|
|
4554
|
+
schema.profile_environment = rows[0].environment;
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
3646
4557
|
} finally {
|
|
3647
4558
|
database.close();
|
|
3648
4559
|
}
|
|
@@ -3651,27 +4562,114 @@ function getLineageRuntimeInfo(options = {}) {
|
|
|
3651
4562
|
}
|
|
3652
4563
|
}
|
|
3653
4564
|
return {
|
|
3654
|
-
|
|
4565
|
+
asset_root: repoRoot,
|
|
4566
|
+
channel,
|
|
3655
4567
|
database: databaseInfo,
|
|
3656
4568
|
fetchedAt: nowIso(),
|
|
3657
4569
|
git_sha: gitSha(),
|
|
3658
4570
|
node_env: process.env.NODE_ENV,
|
|
3659
4571
|
package_name: info.name,
|
|
4572
|
+
profile: runtimeProfileIdentity(channel),
|
|
4573
|
+
schema,
|
|
3660
4574
|
version: info.version
|
|
3661
4575
|
};
|
|
3662
4576
|
}
|
|
3663
4577
|
|
|
4578
|
+
// src/server/managedWriterRouting.ts
|
|
4579
|
+
var managedWriterRequestSchemaVersion = "lineage.managed_writer_request.v1";
|
|
4580
|
+
var managedWriterResponseSchemaVersion = "lineage.managed_writer_response.v1";
|
|
4581
|
+
var managedWriterRoute = "/api/managed-writer/execute";
|
|
4582
|
+
var delegationHeader = "X-Lineage-Writer-Delegation";
|
|
4583
|
+
var ManagedWriterRoutingError = class extends Error {
|
|
4584
|
+
constructor(message, status = 409, options) {
|
|
4585
|
+
super(message, options);
|
|
4586
|
+
this.status = status;
|
|
4587
|
+
this.name = "ManagedWriterRoutingError";
|
|
4588
|
+
}
|
|
4589
|
+
status;
|
|
4590
|
+
};
|
|
4591
|
+
function managedWriterTimeoutMs(command, args) {
|
|
4592
|
+
const subcommand = args.find((arg) => !arg.startsWith("-")) || "";
|
|
4593
|
+
return command === "reroll" && subcommand === "import" ? 5 * 6e4 : 3e4;
|
|
4594
|
+
}
|
|
4595
|
+
function errorMessage(body, fallback) {
|
|
4596
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return fallback;
|
|
4597
|
+
const record = body;
|
|
4598
|
+
if (typeof record.message === "string") return record.message;
|
|
4599
|
+
if (typeof record.error === "string") return record.error;
|
|
4600
|
+
return fallback;
|
|
4601
|
+
}
|
|
4602
|
+
function agentClaimError(body, status) {
|
|
4603
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
4604
|
+
const record = body;
|
|
4605
|
+
if (typeof record.message !== "string" || typeof record.error !== "string") return void 0;
|
|
4606
|
+
const conflicts = Array.isArray(record.conflicts) ? record.conflicts : [];
|
|
4607
|
+
return new AgentClaimError(record.message, status, record.error, conflicts);
|
|
4608
|
+
}
|
|
4609
|
+
function assertResponseIdentity(body, profile, channel) {
|
|
4610
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
4611
|
+
throw new ManagedWriterRoutingError("Managed writer service returned a non-object response", 502);
|
|
4612
|
+
}
|
|
4613
|
+
const response = body;
|
|
4614
|
+
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) {
|
|
4615
|
+
throw new ManagedWriterRoutingError("Managed writer response identity does not match the selected profile/service", 502);
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
async function executeManagedWriterCommand(profile, channel, command, args) {
|
|
4619
|
+
const delegation = getProfileWriterDelegation(profile);
|
|
4620
|
+
let response;
|
|
4621
|
+
try {
|
|
4622
|
+
response = await fetch(new URL(managedWriterRoute, delegation.service_origin), {
|
|
4623
|
+
body: JSON.stringify({
|
|
4624
|
+
args,
|
|
4625
|
+
channel,
|
|
4626
|
+
command,
|
|
4627
|
+
environment: profile.environment,
|
|
4628
|
+
profile_id: profile.profile_id,
|
|
4629
|
+
schema_version: managedWriterRequestSchemaVersion,
|
|
4630
|
+
service_origin: profile.service_origin
|
|
4631
|
+
}),
|
|
4632
|
+
headers: {
|
|
4633
|
+
"Content-Type": "application/json",
|
|
4634
|
+
[delegationHeader]: delegation.token
|
|
4635
|
+
},
|
|
4636
|
+
method: "POST",
|
|
4637
|
+
redirect: "error",
|
|
4638
|
+
signal: AbortSignal.timeout(managedWriterTimeoutMs(command, args))
|
|
4639
|
+
});
|
|
4640
|
+
} catch (error) {
|
|
4641
|
+
throw new ManagedWriterRoutingError(
|
|
4642
|
+
`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`,
|
|
4643
|
+
503,
|
|
4644
|
+
{ cause: error }
|
|
4645
|
+
);
|
|
4646
|
+
}
|
|
4647
|
+
let body;
|
|
4648
|
+
try {
|
|
4649
|
+
body = await response.json();
|
|
4650
|
+
} catch (error) {
|
|
4651
|
+
throw new ManagedWriterRoutingError("Managed writer service returned invalid JSON; mutation result is unknown", 502, { cause: error });
|
|
4652
|
+
}
|
|
4653
|
+
if (!response.ok) {
|
|
4654
|
+
const claimError = agentClaimError(body, response.status);
|
|
4655
|
+
if (claimError) throw claimError;
|
|
4656
|
+
throw new ManagedWriterRoutingError(errorMessage(body, `Managed writer service returned HTTP ${response.status}`), response.status);
|
|
4657
|
+
}
|
|
4658
|
+
assertResponseIdentity(body, profile, channel);
|
|
4659
|
+
return body.result;
|
|
4660
|
+
}
|
|
4661
|
+
|
|
3664
4662
|
// src/cli/lineageCli.ts
|
|
3665
4663
|
var signalExitCodes = {
|
|
3666
4664
|
SIGINT: 130,
|
|
3667
4665
|
SIGTERM: 143
|
|
3668
4666
|
};
|
|
3669
|
-
function
|
|
3670
|
-
return
|
|
4667
|
+
function packageRoot2() {
|
|
4668
|
+
return resolve8(dirname5(fileURLToPath2(import.meta.url)), "..", "..");
|
|
3671
4669
|
}
|
|
3672
4670
|
function packageVersion() {
|
|
3673
4671
|
try {
|
|
3674
|
-
const packageInfo2 = JSON.parse(
|
|
4672
|
+
const packageInfo2 = JSON.parse(readFileSync6(join10(packageRoot2(), "package.json"), "utf8"));
|
|
3675
4673
|
return packageInfo2.version || "0.0.0";
|
|
3676
4674
|
} catch {
|
|
3677
4675
|
return "0.0.0";
|
|
@@ -3679,9 +4677,9 @@ function packageVersion() {
|
|
|
3679
4677
|
}
|
|
3680
4678
|
function dataRoot(displayName) {
|
|
3681
4679
|
if (process.env.LINEAGE_HOME) return process.env.LINEAGE_HOME;
|
|
3682
|
-
if (
|
|
3683
|
-
if (
|
|
3684
|
-
return
|
|
4680
|
+
if (platform2() === "darwin") return join10(homedir2(), "Library", "Application Support", displayName);
|
|
4681
|
+
if (platform2() === "win32") return join10(process.env.APPDATA || join10(homedir2(), "AppData", "Roaming"), displayName);
|
|
4682
|
+
return join10(process.env.XDG_DATA_HOME || join10(homedir2(), ".local", "share"), displayName.toLowerCase().replace(/\s+/g, "-"));
|
|
3685
4683
|
}
|
|
3686
4684
|
function readOption(args, name) {
|
|
3687
4685
|
const prefix = `${name}=`;
|
|
@@ -3705,31 +4703,106 @@ function readOptions(args, name) {
|
|
|
3705
4703
|
return values;
|
|
3706
4704
|
}
|
|
3707
4705
|
function resolveStartOptions(config, args) {
|
|
3708
|
-
const
|
|
4706
|
+
const profile = prepareCliProfile(config, args);
|
|
4707
|
+
const serviceUrl = profile ? new URL(profile.service_origin) : void 0;
|
|
4708
|
+
const rawPort = readOption(args, "--port") || process.env.PORT || serviceUrl?.port || String(config.defaultPort);
|
|
3709
4709
|
const port = Number(rawPort);
|
|
3710
4710
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
3711
4711
|
throw new Error(`Invalid port: ${rawPort}`);
|
|
3712
4712
|
}
|
|
4713
|
+
const profileHost = serviceUrl ? normalizeListenHost(serviceUrl.hostname) : void 0;
|
|
4714
|
+
const host = normalizeListenHost(readOption(args, "--host") || process.env.HOST || profileHost || config.defaultHost);
|
|
4715
|
+
if (profile && serviceUrl && (host !== profileHost || port !== Number(serviceUrl.port || 80))) {
|
|
4716
|
+
throw new Error(`Profile ${profile.profile_id} service_origin ${profile.service_origin} conflicts with requested host/port ${host}:${port}`);
|
|
4717
|
+
}
|
|
3713
4718
|
return {
|
|
3714
|
-
|
|
3715
|
-
|
|
4719
|
+
assetRoot: profile?.asset_root || resolveCliAssetRoot(args),
|
|
4720
|
+
dbPath: profile?.database_path || resolveCliDbPath(config, args),
|
|
4721
|
+
host,
|
|
3716
4722
|
json: args.includes("--json"),
|
|
3717
4723
|
open: args.includes("--open"),
|
|
3718
|
-
port
|
|
4724
|
+
port,
|
|
4725
|
+
profile
|
|
3719
4726
|
};
|
|
3720
4727
|
}
|
|
4728
|
+
function normalizeListenHost(host) {
|
|
4729
|
+
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
4730
|
+
}
|
|
4731
|
+
function lineageServiceUrl(host, port) {
|
|
4732
|
+
const urlHost = host.includes(":") ? `[${normalizeListenHost(host)}]` : host;
|
|
4733
|
+
return `http://${urlHost}:${port}`;
|
|
4734
|
+
}
|
|
4735
|
+
function resolveCliAssetRoot(args) {
|
|
4736
|
+
return resolve8(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
|
|
4737
|
+
}
|
|
4738
|
+
function configureCliAssetRoot(args) {
|
|
4739
|
+
const assetRoot = resolveCliAssetRoot(args);
|
|
4740
|
+
process.env.LINEAGE_ASSET_ROOT = assetRoot;
|
|
4741
|
+
return setLineageAssetRoot(assetRoot);
|
|
4742
|
+
}
|
|
3721
4743
|
function resolveCliDbPath(config, args) {
|
|
3722
|
-
return readOption(args, "--db") || process.env.LINEAGE_DB ||
|
|
4744
|
+
return readOption(args, "--db") || process.env.LINEAGE_DB || join10(dataRoot(config.displayName), `${config.binName}.sqlite`);
|
|
4745
|
+
}
|
|
4746
|
+
function hasOption(args, name) {
|
|
4747
|
+
return args.includes(name) || args.some((arg) => arg.startsWith(`${name}=`));
|
|
4748
|
+
}
|
|
4749
|
+
function profileSelector(args) {
|
|
4750
|
+
const option = readOption(args, "--profile");
|
|
4751
|
+
if (hasOption(args, "--profile") && !option?.trim()) {
|
|
4752
|
+
throw new Error("--profile requires a non-empty profile ID or manifest path");
|
|
4753
|
+
}
|
|
4754
|
+
if (option && process.env.LINEAGE_PROFILE) {
|
|
4755
|
+
const optionProfile = resolveLineageProfile(option);
|
|
4756
|
+
const environmentProfile = resolveLineageProfile(process.env.LINEAGE_PROFILE);
|
|
4757
|
+
if (optionProfile.manifest_path !== environmentProfile.manifest_path) {
|
|
4758
|
+
throw new Error(`--profile ${option} conflicts with LINEAGE_PROFILE ${process.env.LINEAGE_PROFILE}`);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
return option || process.env.LINEAGE_PROFILE;
|
|
4762
|
+
}
|
|
4763
|
+
function doctorFailures(result) {
|
|
4764
|
+
return result.checks.filter((check) => check.status === "fail").map((check) => `${check.id}: ${check.message}`).join("; ");
|
|
4765
|
+
}
|
|
4766
|
+
function prepareCliProfile(config, args) {
|
|
4767
|
+
const selector = profileSelector(args);
|
|
4768
|
+
if (!selector) {
|
|
4769
|
+
assertUnselectedDatabaseIsUnbound(getLineageRuntimeInfo({ channel: config.channel, dbPath: resolveCliDbPath(config, args) }));
|
|
4770
|
+
return void 0;
|
|
4771
|
+
}
|
|
4772
|
+
if (hasOption(args, "--db")) throw new Error("A named profile cannot be combined with --db");
|
|
4773
|
+
if (hasOption(args, "--asset-root")) throw new Error("A named profile cannot be combined with --asset-root");
|
|
4774
|
+
const profile = resolveLineageProfile(selector);
|
|
4775
|
+
if (process.env.LINEAGE_DB && resolve8(process.env.LINEAGE_DB) !== profile.database_path) {
|
|
4776
|
+
throw new Error(`Profile ${profile.profile_id} database_path conflicts with LINEAGE_DB`);
|
|
4777
|
+
}
|
|
4778
|
+
if (process.env.LINEAGE_ASSET_ROOT && resolve8(process.env.LINEAGE_ASSET_ROOT) !== profile.asset_root) {
|
|
4779
|
+
throw new Error(`Profile ${profile.profile_id} asset_root conflicts with LINEAGE_ASSET_ROOT`);
|
|
4780
|
+
}
|
|
4781
|
+
assertProfileChannel(profile, config.channel);
|
|
4782
|
+
const runtime = getLineageRuntimeInfo({ channel: config.channel, dbPath: profile.database_path });
|
|
4783
|
+
const doctor = doctorLineageProfile(selector, { channel: config.channel, gitSha: runtime.git_sha, version: runtime.version });
|
|
4784
|
+
if (!doctor.ok) throw new Error(`Profile ${profile.profile_id} failed doctor: ${doctorFailures(doctor)}`);
|
|
4785
|
+
process.env.LINEAGE_ASSET_ROOT = profile.asset_root;
|
|
4786
|
+
process.env.LINEAGE_DB = profile.database_path;
|
|
4787
|
+
process.env.LINEAGE_PROFILE = selector;
|
|
4788
|
+
process.env.LINEAGE_PROFILE_ENVIRONMENT = profile.environment;
|
|
4789
|
+
process.env.LINEAGE_PROFILE_ID = profile.profile_id;
|
|
4790
|
+
process.env.LINEAGE_PROFILE_MANIFEST = profile.manifest_path;
|
|
4791
|
+
process.env.LINEAGE_PROFILE_SERVICE_ORIGIN = profile.service_origin;
|
|
4792
|
+
setLineageAssetRoot(profile.asset_root);
|
|
4793
|
+
return profile;
|
|
3723
4794
|
}
|
|
3724
4795
|
function formatLineageHelp(config) {
|
|
3725
4796
|
return `${config.binName} ${packageVersion()}
|
|
3726
4797
|
|
|
3727
4798
|
Usage:
|
|
3728
|
-
${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--open] [--json]
|
|
4799
|
+
${config.binName} start [--profile <id-or-manifest>] [--port <port>] [--host <host>] [--db <path>] [--asset-root <path>] [--open] [--json]
|
|
4800
|
+
${config.binName} profile doctor --profile <id-or-manifest> [--json]
|
|
4801
|
+
${config.binName} profile bind --profile <id-or-manifest> [--confirm-write] [--json]
|
|
3729
4802
|
${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
3730
4803
|
${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
3731
4804
|
${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
|
|
3732
|
-
${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]
|
|
4805
|
+
${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]
|
|
3733
4806
|
${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
|
|
3734
4807
|
${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
3735
4808
|
${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
@@ -3758,6 +4831,14 @@ Usage:
|
|
|
3758
4831
|
|
|
3759
4832
|
${config.displayName} runs the bundled Lineage server for the ${config.channel} channel.
|
|
3760
4833
|
|
|
4834
|
+
Asset catalogs and local media default to the installed package root. Pass
|
|
4835
|
+
--asset-root <path> or LINEAGE_ASSET_ROOT to use an external project root.
|
|
4836
|
+
|
|
4837
|
+
All commands accept --profile <id-or-manifest> or LINEAGE_PROFILE. Named
|
|
4838
|
+
profiles are authoritative for database, asset root, environment, and origin;
|
|
4839
|
+
they cannot be combined with direct --db or --asset-root overrides. Commands
|
|
4840
|
+
without a profile run in legacy-unbound compatibility mode.
|
|
4841
|
+
|
|
3761
4842
|
Variation vs re-roll:
|
|
3762
4843
|
link-child creates a new visible child variation edge.
|
|
3763
4844
|
reroll mark -> reroll plan -> reroll import updates the same node with a new attempt.`;
|
|
@@ -3766,8 +4847,8 @@ function printHelp(config) {
|
|
|
3766
4847
|
console.log(formatLineageHelp(config));
|
|
3767
4848
|
}
|
|
3768
4849
|
function openBrowser(url) {
|
|
3769
|
-
const command =
|
|
3770
|
-
const args =
|
|
4850
|
+
const command = platform2() === "darwin" ? "open" : platform2() === "win32" ? "cmd" : "xdg-open";
|
|
4851
|
+
const args = platform2() === "win32" ? ["/c", "start", "", url] : [url];
|
|
3771
4852
|
const opener = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
3772
4853
|
opener.unref();
|
|
3773
4854
|
}
|
|
@@ -3784,6 +4865,7 @@ function positionalArgs(args) {
|
|
|
3784
4865
|
return values;
|
|
3785
4866
|
}
|
|
3786
4867
|
function resolveDataCommandOptions(args) {
|
|
4868
|
+
configureCliAssetRoot(args);
|
|
3787
4869
|
const positions = positionalArgs(args);
|
|
3788
4870
|
const options = {
|
|
3789
4871
|
assetId: readOption(args, "--asset-id") || positions[0],
|
|
@@ -3809,6 +4891,8 @@ function runLineageDataCommand(command, args) {
|
|
|
3809
4891
|
if (command === "selection") {
|
|
3810
4892
|
const subcommand = positionalArgs(args)[0] || "";
|
|
3811
4893
|
if (subcommand !== "packet") throw new Error(`Unknown selection command: ${subcommand}`);
|
|
4894
|
+
const schema = readOption(args, "--schema");
|
|
4895
|
+
if (schema && schema !== "v2") throw new Error(`Unsupported selection packet schema: ${schema}. Omit --schema for v1 or pass --schema v2.`);
|
|
3812
4896
|
const labels = readOptions(args, "--label").flatMap((label) => label.split(",")).map((label) => label.trim()).filter(Boolean);
|
|
3813
4897
|
const packet = getLineageSelectionPacket(options.project, {
|
|
3814
4898
|
campaign: readOption(args, "--campaign"),
|
|
@@ -3819,14 +4903,15 @@ function runLineageDataCommand(command, args) {
|
|
|
3819
4903
|
labels,
|
|
3820
4904
|
packageVersion: packageVersion(),
|
|
3821
4905
|
rootAssetId: options.rootAssetId,
|
|
4906
|
+
schema: schema === "v2" ? "v2" : void 0,
|
|
3822
4907
|
strict: args.includes("--strict"),
|
|
3823
4908
|
workspaceId: readOption(args, "--workspace") || readOption(args, "--workspace-id")
|
|
3824
4909
|
});
|
|
3825
4910
|
const out = readOption(args, "--out");
|
|
3826
4911
|
if (out) {
|
|
3827
|
-
const outPath =
|
|
3828
|
-
|
|
3829
|
-
|
|
4912
|
+
const outPath = resolve8(out);
|
|
4913
|
+
mkdirSync6(dirname5(outPath), { recursive: true });
|
|
4914
|
+
writeFileSync3(outPath, `${JSON.stringify(packet, null, 2)}
|
|
3830
4915
|
`);
|
|
3831
4916
|
}
|
|
3832
4917
|
return packet;
|
|
@@ -4030,11 +5115,70 @@ function printDataResult(command, result, json) {
|
|
|
4030
5115
|
console.log(String(result));
|
|
4031
5116
|
}
|
|
4032
5117
|
function runLineageDbCommand(config, command, args) {
|
|
5118
|
+
configureCliAssetRoot(args);
|
|
4033
5119
|
const dbPath = resolveCliDbPath(config, args);
|
|
4034
5120
|
process.env.LINEAGE_DB = dbPath;
|
|
4035
5121
|
if (command === "info") return getLineageRuntimeInfo({ channel: config.channel, dbPath });
|
|
4036
5122
|
throw new Error(`Unknown db command: ${command}`);
|
|
4037
5123
|
}
|
|
5124
|
+
function runLineageProfileCommand(config, command, args) {
|
|
5125
|
+
if (command !== "doctor" && command !== "bind") throw new Error(`Unknown profile command: ${command}`);
|
|
5126
|
+
if (hasOption(args, "--db")) throw new Error(`Profile ${command} cannot be combined with --db`);
|
|
5127
|
+
if (hasOption(args, "--asset-root")) throw new Error(`Profile ${command} cannot be combined with --asset-root`);
|
|
5128
|
+
const selector = profileSelector(args);
|
|
5129
|
+
if (!selector) throw new Error(`lineage profile ${command} requires --profile or LINEAGE_PROFILE`);
|
|
5130
|
+
if (command === "bind") {
|
|
5131
|
+
const profile = resolveLineageProfile(selector);
|
|
5132
|
+
assertProfileChannel(profile, config.channel);
|
|
5133
|
+
if (!args.includes("--confirm-write")) return bindLineageProfileDatabase(profile, false);
|
|
5134
|
+
mkdirSync6(dirname5(profile.database_path), { recursive: true });
|
|
5135
|
+
const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
|
|
5136
|
+
try {
|
|
5137
|
+
return bindLineageProfileDatabase(profile, true);
|
|
5138
|
+
} finally {
|
|
5139
|
+
writerLease.release();
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
const runtime = getLineageRuntimeInfo({ channel: config.channel });
|
|
5143
|
+
return doctorLineageProfile(selector, { channel: config.channel, gitSha: runtime.git_sha, version: runtime.version });
|
|
5144
|
+
}
|
|
5145
|
+
function printProfileResult(result, json) {
|
|
5146
|
+
if (json) {
|
|
5147
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5148
|
+
return;
|
|
5149
|
+
}
|
|
5150
|
+
if (result.schema_version === "lineage.profile_bind.v1") {
|
|
5151
|
+
console.log(`${result.dryRun ? "Would bind" : result.already_bound ? "Already bound" : "Bound"} ${result.profile_id} to ${result.database_path}`);
|
|
5152
|
+
return;
|
|
5153
|
+
}
|
|
5154
|
+
console.log(`Profile doctor: ${result.ok ? "ok" : "failed"}`);
|
|
5155
|
+
for (const check of result.checks) console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`);
|
|
5156
|
+
}
|
|
5157
|
+
function lineageProfileDoctorExitCode(result) {
|
|
5158
|
+
return result.ok ? 0 : 1;
|
|
5159
|
+
}
|
|
5160
|
+
function lineageCliRequiresWriterLease(command, args) {
|
|
5161
|
+
if (command === "next" || command === "brief" || command === "inspect" || command === "selection") return false;
|
|
5162
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
5163
|
+
if (command === "reroll") return subcommand !== "list";
|
|
5164
|
+
if (command === "tasks") return subcommand !== "list" && subcommand !== "inspect";
|
|
5165
|
+
if (command === "db") return subcommand !== "info";
|
|
5166
|
+
if (command === "agent") return subcommand !== "status" && subcommand !== "graph" && subcommand !== "inspect";
|
|
5167
|
+
return true;
|
|
5168
|
+
}
|
|
5169
|
+
function argsWithoutProfileSelector(args) {
|
|
5170
|
+
const filtered = [];
|
|
5171
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
5172
|
+
const arg = args[index];
|
|
5173
|
+
if (arg === "--profile") {
|
|
5174
|
+
index += 1;
|
|
5175
|
+
continue;
|
|
5176
|
+
}
|
|
5177
|
+
if (arg.startsWith("--profile=")) continue;
|
|
5178
|
+
filtered.push(arg);
|
|
5179
|
+
}
|
|
5180
|
+
return filtered;
|
|
5181
|
+
}
|
|
4038
5182
|
function printDbResult(command, result, json) {
|
|
4039
5183
|
if (json) {
|
|
4040
5184
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -4046,6 +5190,7 @@ function printDbResult(command, result, json) {
|
|
|
4046
5190
|
console.log(`Version: ${runtime.version}`);
|
|
4047
5191
|
if (runtime.git_sha) console.log(`Git: ${runtime.git_sha}`);
|
|
4048
5192
|
if (runtime.node_env) console.log(`Node env: ${runtime.node_env}`);
|
|
5193
|
+
console.log(`Assets: ${runtime.asset_root}`);
|
|
4049
5194
|
console.log(`SQLite: ${runtime.database.path}`);
|
|
4050
5195
|
console.log(`Exists: ${runtime.database.exists ? "yes" : "no"}`);
|
|
4051
5196
|
if (runtime.database.size_bytes !== void 0) console.log(`Size: ${runtime.database.size_bytes} bytes`);
|
|
@@ -4058,6 +5203,7 @@ function printDbResult(command, result, json) {
|
|
|
4058
5203
|
console.log(String(result));
|
|
4059
5204
|
}
|
|
4060
5205
|
function runLineageAgentCommand(command, args) {
|
|
5206
|
+
configureCliAssetRoot(args);
|
|
4061
5207
|
const dbPath = readOption(args, "--db");
|
|
4062
5208
|
if (dbPath) process.env.LINEAGE_DB = dbPath;
|
|
4063
5209
|
const project = readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct;
|
|
@@ -4183,28 +5329,38 @@ function start(config, args) {
|
|
|
4183
5329
|
else console.error(`${config.binName}: ${message}`);
|
|
4184
5330
|
process.exit(1);
|
|
4185
5331
|
}
|
|
4186
|
-
const serverPath =
|
|
4187
|
-
if (!
|
|
5332
|
+
const serverPath = join10(packageRoot2(), "dist", "server.js");
|
|
5333
|
+
if (!existsSync11(serverPath)) {
|
|
4188
5334
|
const message = `Missing bundled server at ${serverPath}. Run npm run build before using ${config.binName} start from a source checkout.`;
|
|
4189
5335
|
if (options.json) console.error(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
4190
5336
|
else console.error(`${config.binName}: ${message}`);
|
|
4191
5337
|
process.exit(1);
|
|
4192
5338
|
}
|
|
4193
|
-
|
|
4194
|
-
const url =
|
|
5339
|
+
mkdirSync6(dirname5(options.dbPath), { recursive: true });
|
|
5340
|
+
const url = lineageServiceUrl(options.host, options.port);
|
|
4195
5341
|
if (options.json) {
|
|
4196
|
-
console.log(JSON.stringify({ channel: config.channel, dbPath: options.dbPath, host: options.host, port: options.port, status: "starting", url }, null, 2));
|
|
5342
|
+
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));
|
|
4197
5343
|
} else {
|
|
4198
5344
|
console.log(`${config.displayName} starting at ${url}`);
|
|
5345
|
+
if (options.profile) console.log(`Profile: ${options.profile.profile_id} (${options.profile.environment})`);
|
|
5346
|
+
else console.warn("Warning: legacy-unbound runtime; database and asset paths are not protected by a named profile.");
|
|
4199
5347
|
console.log(`SQLite: ${options.dbPath}`);
|
|
5348
|
+
console.log(`Assets: ${options.assetRoot}`);
|
|
4200
5349
|
}
|
|
4201
5350
|
if (options.open) openBrowser(url);
|
|
4202
5351
|
const child = spawn(process.execPath, [serverPath], {
|
|
4203
5352
|
env: {
|
|
4204
5353
|
...process.env,
|
|
4205
5354
|
HOST: options.host,
|
|
5355
|
+
LINEAGE_ASSET_ROOT: options.assetRoot,
|
|
4206
5356
|
LINEAGE_CHANNEL: config.channel,
|
|
4207
5357
|
LINEAGE_DB: options.dbPath,
|
|
5358
|
+
LINEAGE_PROFILE: options.profile ? process.env.LINEAGE_PROFILE : void 0,
|
|
5359
|
+
LINEAGE_PROFILE_ENVIRONMENT: options.profile?.environment,
|
|
5360
|
+
LINEAGE_PROFILE_ID: options.profile?.profile_id,
|
|
5361
|
+
LINEAGE_PROFILE_MANIFEST: options.profile?.manifest_path,
|
|
5362
|
+
LINEAGE_PROFILE_SERVICE_ORIGIN: options.profile?.service_origin,
|
|
5363
|
+
LINEAGE_DB_ACCESS: void 0,
|
|
4208
5364
|
NODE_ENV: "production",
|
|
4209
5365
|
PORT: String(options.port)
|
|
4210
5366
|
},
|
|
@@ -4223,7 +5379,8 @@ function start(config, args) {
|
|
|
4223
5379
|
process.exit(1);
|
|
4224
5380
|
});
|
|
4225
5381
|
}
|
|
4226
|
-
function runLineageCli(config, args = process.argv.slice(2)) {
|
|
5382
|
+
async function runLineageCli(config, args = process.argv.slice(2)) {
|
|
5383
|
+
process.env.LINEAGE_CHANNEL = config.channel;
|
|
4227
5384
|
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
4228
5385
|
printHelp(config);
|
|
4229
5386
|
process.exit(0);
|
|
@@ -4238,11 +5395,52 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
4238
5395
|
start(config, normalizedArgs.slice(1));
|
|
4239
5396
|
return;
|
|
4240
5397
|
}
|
|
5398
|
+
if (command === "profile") {
|
|
5399
|
+
const commandArgs = normalizedArgs.slice(2);
|
|
5400
|
+
const profileCommand = normalizedArgs[1] || "";
|
|
5401
|
+
const json2 = commandArgs.includes("--json");
|
|
5402
|
+
try {
|
|
5403
|
+
const result = runLineageProfileCommand(config, profileCommand, commandArgs);
|
|
5404
|
+
printProfileResult(result, json2);
|
|
5405
|
+
process.exit(lineageProfileDoctorExitCode(result));
|
|
5406
|
+
} catch (error) {
|
|
5407
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
5408
|
+
if (json2) console.error(JSON.stringify({ ok: false, command: `profile ${profileCommand}`, error: message2 }, null, 2));
|
|
5409
|
+
else console.error(`${config.binName}: ${message2}`);
|
|
5410
|
+
process.exit(1);
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
let managedWriterProfile;
|
|
5414
|
+
try {
|
|
5415
|
+
const profile = prepareCliProfile(config, normalizedArgs.slice(1));
|
|
5416
|
+
if (profile) {
|
|
5417
|
+
if (lineageCliRequiresWriterLease(command, normalizedArgs.slice(1))) {
|
|
5418
|
+
delete process.env.LINEAGE_DB_ACCESS;
|
|
5419
|
+
try {
|
|
5420
|
+
const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
|
|
5421
|
+
process.once("exit", writerLease.release);
|
|
5422
|
+
} catch (error) {
|
|
5423
|
+
if (!(error instanceof ProfileWriterLeaseConflictError) || error.owner.role !== "service") throw error;
|
|
5424
|
+
process.env.LINEAGE_DB_ACCESS = "read-only";
|
|
5425
|
+
managedWriterProfile = profile;
|
|
5426
|
+
}
|
|
5427
|
+
} else {
|
|
5428
|
+
process.env.LINEAGE_DB_ACCESS = "read-only";
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
} catch (error) {
|
|
5432
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
5433
|
+
const json2 = normalizedArgs.includes("--json");
|
|
5434
|
+
if (json2) console.error(JSON.stringify({ ok: false, command, error: message2 }, null, 2));
|
|
5435
|
+
else console.error(`${config.binName}: ${message2}`);
|
|
5436
|
+
process.exit(1);
|
|
5437
|
+
}
|
|
4241
5438
|
if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
|
|
4242
5439
|
const commandArgs = normalizedArgs.slice(1);
|
|
4243
5440
|
const json2 = commandArgs.includes("--json");
|
|
4244
5441
|
try {
|
|
4245
|
-
|
|
5442
|
+
const result = managedWriterProfile ? await executeManagedWriterCommand(managedWriterProfile, config.channel, command, argsWithoutProfileSelector(commandArgs)) : runLineageDataCommand(command, commandArgs);
|
|
5443
|
+
printDataResult(command, result, json2);
|
|
4246
5444
|
} catch (error) {
|
|
4247
5445
|
const message2 = redactAgentClaimTokens(error instanceof Error ? error.message : String(error));
|
|
4248
5446
|
if (json2) {
|
|
@@ -4272,7 +5470,13 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
4272
5470
|
const agentCommand = normalizedArgs[1] || "";
|
|
4273
5471
|
const json2 = commandArgs.includes("--json");
|
|
4274
5472
|
try {
|
|
4275
|
-
|
|
5473
|
+
const result = managedWriterProfile ? await executeManagedWriterCommand(
|
|
5474
|
+
managedWriterProfile,
|
|
5475
|
+
config.channel,
|
|
5476
|
+
"agent",
|
|
5477
|
+
argsWithoutProfileSelector([agentCommand, ...commandArgs])
|
|
5478
|
+
) : runLineageAgentCommand(agentCommand, commandArgs);
|
|
5479
|
+
printAgentResult(agentCommand, result, json2);
|
|
4276
5480
|
} catch (error) {
|
|
4277
5481
|
const message2 = redactAgentClaimTokens(error instanceof Error ? error.message : String(error));
|
|
4278
5482
|
if (json2) {
|
|
@@ -4293,5 +5497,12 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
4293
5497
|
}
|
|
4294
5498
|
|
|
4295
5499
|
// src/cli/lineage-dev.ts
|
|
4296
|
-
|
|
5500
|
+
var preview = process.env.LINEAGE_CHANNEL === "preview";
|
|
5501
|
+
await runLineageCli({
|
|
5502
|
+
binName: "lineage-dev",
|
|
5503
|
+
channel: preview ? "preview" : "dev",
|
|
5504
|
+
defaultHost: preview ? "lineage-preview.localhost" : "lineage-dev.localhost",
|
|
5505
|
+
defaultPort: preview ? 5199 : 5198,
|
|
5506
|
+
displayName: preview ? "Lineage Preview" : "Lineage Dev"
|
|
5507
|
+
});
|
|
4297
5508
|
//# sourceMappingURL=lineage-dev.js.map
|