@mean-weasel/lineage 0.1.1 → 0.1.3
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 +11 -0
- package/README.md +72 -6
- package/dist/cli/lineage-dev.js +1285 -15
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +1285 -15
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +432 -26
- package/dist/server.js.map +3 -3
- package/dist/web/assets/index-CTsKYXYF.js +19 -0
- package/dist/web/index.html +1 -1
- package/fixtures/demo-project/lineage/swissifier-rich-demo.json +194 -0
- package/package.json +3 -2
- package/dist/web/assets/index-CSnGIWOr.js +0 -19
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
2
|
import express2 from "express";
|
|
3
3
|
import multer from "multer";
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
5
5
|
import { join as join9 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/server/assetCore.ts
|
|
@@ -1015,7 +1015,14 @@ function escapeSvgText2(value) {
|
|
|
1015
1015
|
function loadCatalog(project = defaultProject) {
|
|
1016
1016
|
const path = catalogPath(project);
|
|
1017
1017
|
if (existsSync3(path)) {
|
|
1018
|
-
|
|
1018
|
+
try {
|
|
1019
|
+
return normalizeCatalog(JSON.parse(readFileSync2(path, "utf8")), project);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
1022
|
+
throw new LineageAssetError(`Missing catalog: ${path}`, 404);
|
|
1023
|
+
}
|
|
1024
|
+
throw error;
|
|
1025
|
+
}
|
|
1019
1026
|
}
|
|
1020
1027
|
const clean = cleanProject(project);
|
|
1021
1028
|
if (clean === defaultProject) {
|
|
@@ -1924,6 +1931,16 @@ function updateAssetReview(project, fields) {
|
|
|
1924
1931
|
}
|
|
1925
1932
|
|
|
1926
1933
|
// src/server/assetLineageHandoff.ts
|
|
1934
|
+
var publicPackageCommand = "npx @mean-weasel/lineage";
|
|
1935
|
+
function shellQuote(value) {
|
|
1936
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
1937
|
+
}
|
|
1938
|
+
function lineageCommand(command, project, rootAssetId) {
|
|
1939
|
+
return `${publicPackageCommand} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --db ${shellQuote(lineageDbPath())} --json`;
|
|
1940
|
+
}
|
|
1941
|
+
function linkChildCommand(project, rootAssetId) {
|
|
1942
|
+
return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
|
|
1943
|
+
}
|
|
1927
1944
|
function getLineageBrief(project, rootAssetId) {
|
|
1928
1945
|
const next = getLineageNextAsset(project, rootAssetId);
|
|
1929
1946
|
const assets = next.next_assets;
|
|
@@ -1960,9 +1977,9 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
1960
1977
|
rationale
|
|
1961
1978
|
},
|
|
1962
1979
|
handoff: {
|
|
1963
|
-
next_command:
|
|
1964
|
-
inspect_command: asset ?
|
|
1965
|
-
link_child_command: asset ?
|
|
1980
|
+
next_command: lineageCommand("next", project, next.root_asset_id),
|
|
1981
|
+
inspect_command: asset ? `${publicPackageCommand} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} --db ${shellQuote(lineageDbPath())} --json` : void 0,
|
|
1982
|
+
link_child_command: asset ? linkChildCommand(project, next.root_asset_id) : void 0
|
|
1966
1983
|
},
|
|
1967
1984
|
fetchedAt: nowIso()
|
|
1968
1985
|
};
|
|
@@ -2709,7 +2726,7 @@ function laneFor(channel, localAssets2, catalogAssets, limit) {
|
|
|
2709
2726
|
};
|
|
2710
2727
|
}
|
|
2711
2728
|
function handoff(project) {
|
|
2712
|
-
const prefix = `npx lineage`;
|
|
2729
|
+
const prefix = `npx @mean-weasel/lineage`;
|
|
2713
2730
|
return {
|
|
2714
2731
|
backupTemplate: `${prefix} local backup --project ${project} --asset-id <local-id> --dry-run --json`,
|
|
2715
2732
|
lineageNextTemplate: `${prefix} lineage next --project ${project} --root <root-id> --json`,
|
|
@@ -4510,11 +4527,14 @@ function listImageGenerationJobs(project = defaultProject, fields = {}) {
|
|
|
4510
4527
|
|
|
4511
4528
|
// src/server/assetLineageDemo.ts
|
|
4512
4529
|
import { createHash as createHash2 } from "node:crypto";
|
|
4513
|
-
import { mkdirSync as mkdirSync5, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4514
|
-
import {
|
|
4530
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4531
|
+
import { tmpdir } from "node:os";
|
|
4532
|
+
import { dirname as dirname3, join as join8, posix } from "node:path";
|
|
4533
|
+
import { gunzipSync } from "node:zlib";
|
|
4515
4534
|
var demoWorkspaceTitle = "Demo: Content iteration tree";
|
|
4516
4535
|
var demoWorkspaceNotes = "Repeatable sample lineage for demos and onboarding. Archive it when reviewing real work.";
|
|
4517
4536
|
var demoBasePath = ["lineage-demo", "2026-06-lineage-demo"];
|
|
4537
|
+
var swissifierManifestPath = join8(repoRoot, "fixtures", defaultProject, "lineage", "swissifier-rich-demo.json");
|
|
4518
4538
|
var demoAssets = [
|
|
4519
4539
|
{ key: "root", channel: "linkedin", file: "demo-root.svg", label: "Initial Demo Concept", fill: "#f6fbfb", stroke: "#0b7f88" },
|
|
4520
4540
|
{ key: "hookA", channel: "tiktok", file: "demo-hook-a-v01.svg", label: "Hook A v01", fill: "#fff8e6", stroke: "#9a6a00" },
|
|
@@ -4556,25 +4576,288 @@ function demoProjectDir(project) {
|
|
|
4556
4576
|
function demoRelativePath(project, asset) {
|
|
4557
4577
|
return join8(...demoBasePath, project, asset.channel, asset.file);
|
|
4558
4578
|
}
|
|
4559
|
-
function
|
|
4579
|
+
function demoFilePath(project, asset) {
|
|
4580
|
+
return join8(demoProjectDir(project), asset.channel, asset.file);
|
|
4581
|
+
}
|
|
4582
|
+
function swissifierManifest() {
|
|
4583
|
+
return JSON.parse(readFileSync4(swissifierManifestPath, "utf8"));
|
|
4584
|
+
}
|
|
4585
|
+
function swissifierRelativePath(manifest, asset) {
|
|
4586
|
+
return join8(manifest.media.target_dir, asset.file);
|
|
4587
|
+
}
|
|
4588
|
+
function swissifierFilePath(manifest, asset) {
|
|
4589
|
+
return join8(repoRoot, ".asset-scratch", swissifierRelativePath(manifest, asset));
|
|
4590
|
+
}
|
|
4591
|
+
function swissifierSourcePath(manifest, asset, sourceDir) {
|
|
4592
|
+
const direct = join8(sourceDir, asset.file);
|
|
4593
|
+
if (existsSync5(direct)) return direct;
|
|
4594
|
+
const nested = join8(sourceDir, manifest.media.target_dir, asset.file);
|
|
4595
|
+
return existsSync5(nested) ? nested : null;
|
|
4596
|
+
}
|
|
4597
|
+
function swissifierMediaState(manifest) {
|
|
4598
|
+
const missing = [];
|
|
4599
|
+
const invalid = [];
|
|
4600
|
+
for (const asset of manifest.assets) {
|
|
4601
|
+
const relativePath = swissifierRelativePath(manifest, asset);
|
|
4602
|
+
const path = swissifierFilePath(manifest, asset);
|
|
4603
|
+
if (!existsSync5(path)) {
|
|
4604
|
+
missing.push(relativePath);
|
|
4605
|
+
continue;
|
|
4606
|
+
}
|
|
4607
|
+
if (fileSha256(path) !== asset.checksum_sha256) invalid.push(relativePath);
|
|
4608
|
+
}
|
|
4609
|
+
return { invalid, missing };
|
|
4610
|
+
}
|
|
4611
|
+
function sha256Hex(input) {
|
|
4612
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
4613
|
+
}
|
|
4614
|
+
function safeTarEntryName(rawName) {
|
|
4615
|
+
const stripped = rawName.replace(/\0.*$/, "").replace(/^\.\//, "");
|
|
4616
|
+
if (!stripped || stripped === ".") return "";
|
|
4617
|
+
if (stripped.includes("\\") || stripped.startsWith("/") || /^[A-Za-z]:/.test(stripped)) {
|
|
4618
|
+
throw new Error(`Unsafe Swissifier media archive path: ${rawName}`);
|
|
4619
|
+
}
|
|
4620
|
+
const normalized = posix.normalize(stripped);
|
|
4621
|
+
if (!normalized || normalized === "." || normalized.startsWith("../") || normalized === "..") {
|
|
4622
|
+
throw new Error(`Unsafe Swissifier media archive path: ${rawName}`);
|
|
4623
|
+
}
|
|
4624
|
+
return normalized;
|
|
4625
|
+
}
|
|
4626
|
+
function tarOctal(header, start, length) {
|
|
4627
|
+
const raw = header.toString("utf8", start, start + length).replace(/\0.*$/, "").trim();
|
|
4628
|
+
if (!raw) return 0;
|
|
4629
|
+
if (!/^[0-7]+$/.test(raw)) throw new Error(`Invalid Swissifier media archive size: ${raw}`);
|
|
4630
|
+
return Number.parseInt(raw, 8);
|
|
4631
|
+
}
|
|
4632
|
+
function tarName(header) {
|
|
4633
|
+
const name = header.toString("utf8", 0, 100).replace(/\0.*$/, "");
|
|
4634
|
+
const prefix = header.toString("utf8", 345, 500).replace(/\0.*$/, "");
|
|
4635
|
+
return prefix ? `${prefix}/${name}` : name;
|
|
4636
|
+
}
|
|
4637
|
+
function isAppleDoubleEntry(name) {
|
|
4638
|
+
return name === "._." || name.startsWith("._") || name.includes("/._");
|
|
4639
|
+
}
|
|
4640
|
+
function extractSwissifierMediaArchive(archive, manifest, destination) {
|
|
4641
|
+
const expected = new Map(manifest.assets.map((asset) => [asset.file, asset]));
|
|
4642
|
+
const extracted = /* @__PURE__ */ new Set();
|
|
4643
|
+
const body = gunzipSync(archive);
|
|
4644
|
+
let offset = 0;
|
|
4645
|
+
while (offset + 512 <= body.length) {
|
|
4646
|
+
const header = body.subarray(offset, offset + 512);
|
|
4647
|
+
offset += 512;
|
|
4648
|
+
if (header.every((byte) => byte === 0)) break;
|
|
4649
|
+
const typeflag = header.toString("utf8", 156, 157);
|
|
4650
|
+
const rawName = tarName(header);
|
|
4651
|
+
const name = safeTarEntryName(rawName);
|
|
4652
|
+
const size = tarOctal(header, 124, 12);
|
|
4653
|
+
const nextOffset = offset + Math.ceil(size / 512) * 512;
|
|
4654
|
+
if (nextOffset > body.length) throw new Error(`Truncated Swissifier media archive entry: ${rawName}`);
|
|
4655
|
+
if (!name || typeflag === "5") {
|
|
4656
|
+
offset = nextOffset;
|
|
4657
|
+
continue;
|
|
4658
|
+
}
|
|
4659
|
+
if (typeflag === "x" || typeflag === "g") {
|
|
4660
|
+
offset = nextOffset;
|
|
4661
|
+
continue;
|
|
4662
|
+
}
|
|
4663
|
+
if (isAppleDoubleEntry(name)) {
|
|
4664
|
+
offset = nextOffset;
|
|
4665
|
+
continue;
|
|
4666
|
+
}
|
|
4667
|
+
if (typeflag && typeflag !== "0") throw new Error(`Unsupported Swissifier media archive entry type for ${name}`);
|
|
4668
|
+
const directName = name.startsWith(`${manifest.media.target_dir}/`) ? name.slice(manifest.media.target_dir.length + 1) : name;
|
|
4669
|
+
const asset = expected.get(directName);
|
|
4670
|
+
if (!asset) throw new Error(`Unexpected Swissifier media archive entry: ${name}`);
|
|
4671
|
+
if (size !== asset.size_bytes) throw new Error(`Unexpected Swissifier media archive size for ${name}`);
|
|
4672
|
+
const file = body.subarray(offset, offset + size);
|
|
4673
|
+
const actualSha = sha256Hex(file);
|
|
4674
|
+
if (actualSha !== asset.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier media archive entry: ${name}`);
|
|
4675
|
+
const target = join8(destination, directName);
|
|
4676
|
+
mkdirSync5(dirname3(target), { recursive: true });
|
|
4677
|
+
writeFileSync3(target, file);
|
|
4678
|
+
extracted.add(directName);
|
|
4679
|
+
offset = nextOffset;
|
|
4680
|
+
}
|
|
4681
|
+
const missing = manifest.assets.map((asset) => asset.file).filter((file) => !extracted.has(file));
|
|
4682
|
+
if (missing.length) throw new Error(`Swissifier media archive missing ${missing.length} expected file${missing.length === 1 ? "" : "s"}`);
|
|
4683
|
+
return { extracted: extracted.size };
|
|
4684
|
+
}
|
|
4685
|
+
async function downloadBuffer(url, maxBytes) {
|
|
4686
|
+
const response = await fetch(url);
|
|
4687
|
+
if (!response.ok) throw new Error(`Swissifier media download failed with HTTP ${response.status}`);
|
|
4688
|
+
const contentLength = Number(response.headers.get("content-length") || 0);
|
|
4689
|
+
if (contentLength > maxBytes) throw new Error(`Swissifier media download is too large: ${contentLength} bytes`);
|
|
4690
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
4691
|
+
if (buffer.length > maxBytes) throw new Error(`Swissifier media download is too large: ${buffer.length} bytes`);
|
|
4692
|
+
return buffer;
|
|
4693
|
+
}
|
|
4694
|
+
function missingDemoMedia(project) {
|
|
4695
|
+
return demoAssets.filter((asset) => !existsSync5(demoFilePath(project, asset))).map((asset) => demoRelativePath(project, asset));
|
|
4696
|
+
}
|
|
4697
|
+
function demoSeedMediaStatus(project = defaultProject) {
|
|
4698
|
+
const missing = missingDemoMedia(project);
|
|
4699
|
+
const present = demoAssets.length - missing.length;
|
|
4560
4700
|
return {
|
|
4561
4701
|
ok: true,
|
|
4562
4702
|
media_root: join8(repoRoot, ".asset-scratch"),
|
|
4563
|
-
present
|
|
4703
|
+
present,
|
|
4564
4704
|
total: demoAssets.length,
|
|
4565
|
-
missing
|
|
4566
|
-
fixture_present:
|
|
4705
|
+
missing,
|
|
4706
|
+
fixture_present: present,
|
|
4567
4707
|
fixture_total: demoAssets.length,
|
|
4568
|
-
fixture_missing:
|
|
4708
|
+
fixture_missing: missing
|
|
4569
4709
|
};
|
|
4570
4710
|
}
|
|
4571
|
-
function
|
|
4711
|
+
function swissifierRichDemoMediaStatus(project = defaultProject) {
|
|
4712
|
+
const manifest = swissifierManifest();
|
|
4713
|
+
const { invalid, missing } = swissifierMediaState(manifest);
|
|
4714
|
+
const present = manifest.assets.length - missing.length - invalid.length;
|
|
4715
|
+
return {
|
|
4716
|
+
ok: true,
|
|
4717
|
+
demo_id: manifest.id,
|
|
4718
|
+
project,
|
|
4719
|
+
media_root: join8(repoRoot, ".asset-scratch"),
|
|
4720
|
+
media_target: manifest.media.target_dir,
|
|
4721
|
+
download_available: Boolean(manifest.media.download),
|
|
4722
|
+
download_file: manifest.media.download?.file,
|
|
4723
|
+
download_sha256: manifest.media.download?.sha256,
|
|
4724
|
+
download_url: manifest.media.download?.url,
|
|
4725
|
+
source_env: manifest.media.source_env,
|
|
4726
|
+
source_hint: manifest.media.source_hint,
|
|
4727
|
+
present,
|
|
4728
|
+
total: manifest.assets.length,
|
|
4729
|
+
missing,
|
|
4730
|
+
invalid,
|
|
4731
|
+
fixture_present: present,
|
|
4732
|
+
fixture_total: manifest.assets.length,
|
|
4733
|
+
fixture_missing: [...missing, ...invalid]
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
async function downloadSwissifierRichDemoMedia(project = defaultProject, fields = { confirmWrite: false }) {
|
|
4737
|
+
const manifest = swissifierManifest();
|
|
4738
|
+
const download = manifest.media.download;
|
|
4739
|
+
if (!download) {
|
|
4740
|
+
return {
|
|
4741
|
+
ok: true,
|
|
4742
|
+
demo_id: manifest.id,
|
|
4743
|
+
project,
|
|
4744
|
+
download_available: false,
|
|
4745
|
+
restored: 0,
|
|
4746
|
+
total: manifest.assets.length
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
const sourceUrl = fields.sourceUrl || download.url;
|
|
4750
|
+
const expectedSha256 = fields.expectedSha256 || download.sha256;
|
|
4751
|
+
const maxBytes = Math.max(download.size_bytes + 1024 * 1024, download.size_bytes * 1.1);
|
|
4752
|
+
const archive = await downloadBuffer(sourceUrl, maxBytes);
|
|
4753
|
+
const archiveSha256 = sha256Hex(archive);
|
|
4754
|
+
if (archiveSha256 !== expectedSha256) throw new Error(`Swissifier media download checksum mismatch: expected ${expectedSha256}, got ${archiveSha256}`);
|
|
4755
|
+
if (!fields.confirmWrite) {
|
|
4756
|
+
return {
|
|
4757
|
+
ok: true,
|
|
4758
|
+
demo_id: manifest.id,
|
|
4759
|
+
project,
|
|
4760
|
+
dryRun: true,
|
|
4761
|
+
download_available: true,
|
|
4762
|
+
download_file: download.file,
|
|
4763
|
+
download_url: sourceUrl,
|
|
4764
|
+
archive_sha256: archiveSha256,
|
|
4765
|
+
restored: 0,
|
|
4766
|
+
total: manifest.assets.length,
|
|
4767
|
+
would_restore: manifest.assets.length
|
|
4768
|
+
};
|
|
4769
|
+
}
|
|
4770
|
+
const sourceDir = mkdtempSync(join8(tmpdir(), "lineage-swissifier-media-"));
|
|
4771
|
+
try {
|
|
4772
|
+
const extracted = extractSwissifierMediaArchive(archive, manifest, sourceDir);
|
|
4773
|
+
const restored = restoreSwissifierRichDemoMedia(project, { confirmWrite: true, sourceDir });
|
|
4774
|
+
return {
|
|
4775
|
+
...restored,
|
|
4776
|
+
download_available: true,
|
|
4777
|
+
download_file: download.file,
|
|
4778
|
+
download_url: sourceUrl,
|
|
4779
|
+
archive_sha256: archiveSha256,
|
|
4780
|
+
extracted: extracted.extracted,
|
|
4781
|
+
media_status: swissifierRichDemoMediaStatus(project)
|
|
4782
|
+
};
|
|
4783
|
+
} finally {
|
|
4784
|
+
rmSync(sourceDir, { force: true, recursive: true });
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
function restoreDemoSeedMedia(project = defaultProject, fields = { confirmWrite: false }) {
|
|
4788
|
+
const missing = new Set(missingDemoMedia(project));
|
|
4789
|
+
if (fields.confirmWrite) {
|
|
4790
|
+
for (const asset of demoAssets) {
|
|
4791
|
+
if (missing.has(demoRelativePath(project, asset))) writeDemoFile(project, asset);
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4572
4794
|
return {
|
|
4573
4795
|
ok: true,
|
|
4574
4796
|
dryRun: !fields.confirmWrite,
|
|
4575
4797
|
media_root: join8(repoRoot, ".asset-scratch"),
|
|
4576
|
-
restored: 0,
|
|
4577
|
-
total: demoAssets.length
|
|
4798
|
+
restored: fields.confirmWrite ? missing.size : 0,
|
|
4799
|
+
total: demoAssets.length,
|
|
4800
|
+
would_restore: fields.confirmWrite ? 0 : missing.size,
|
|
4801
|
+
missing: Array.from(missing)
|
|
4802
|
+
};
|
|
4803
|
+
}
|
|
4804
|
+
function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { confirmWrite: false }) {
|
|
4805
|
+
const manifest = swissifierManifest();
|
|
4806
|
+
const before = swissifierMediaState(manifest);
|
|
4807
|
+
const sourceDir = fields.sourceDir || process.env[manifest.media.source_env];
|
|
4808
|
+
if (!sourceDir) {
|
|
4809
|
+
return {
|
|
4810
|
+
ok: true,
|
|
4811
|
+
demo_id: manifest.id,
|
|
4812
|
+
project,
|
|
4813
|
+
dryRun: !fields.confirmWrite,
|
|
4814
|
+
media_root: join8(repoRoot, ".asset-scratch"),
|
|
4815
|
+
restored: 0,
|
|
4816
|
+
total: manifest.assets.length,
|
|
4817
|
+
would_restore: 0,
|
|
4818
|
+
source_required: true,
|
|
4819
|
+
source_env: manifest.media.source_env,
|
|
4820
|
+
source_hint: manifest.media.source_hint,
|
|
4821
|
+
missing: before.missing,
|
|
4822
|
+
invalid: before.invalid,
|
|
4823
|
+
unavailable: [...before.missing, ...before.invalid]
|
|
4824
|
+
};
|
|
4825
|
+
}
|
|
4826
|
+
const unavailable = [];
|
|
4827
|
+
const copyable = [];
|
|
4828
|
+
const wanted = /* @__PURE__ */ new Set([...before.missing, ...before.invalid]);
|
|
4829
|
+
for (const asset of manifest.assets) {
|
|
4830
|
+
const relativePath = swissifierRelativePath(manifest, asset);
|
|
4831
|
+
if (!wanted.has(relativePath)) continue;
|
|
4832
|
+
const source = swissifierSourcePath(manifest, asset, sourceDir);
|
|
4833
|
+
if (!source || fileSha256(source) !== asset.checksum_sha256) {
|
|
4834
|
+
unavailable.push(relativePath);
|
|
4835
|
+
continue;
|
|
4836
|
+
}
|
|
4837
|
+
copyable.push({ asset, source });
|
|
4838
|
+
}
|
|
4839
|
+
if (fields.confirmWrite) {
|
|
4840
|
+
for (const item of copyable) {
|
|
4841
|
+
const target = swissifierFilePath(manifest, item.asset);
|
|
4842
|
+
mkdirSync5(dirname3(target), { recursive: true });
|
|
4843
|
+
copyFileSync2(item.source, target);
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4846
|
+
return {
|
|
4847
|
+
ok: true,
|
|
4848
|
+
demo_id: manifest.id,
|
|
4849
|
+
project,
|
|
4850
|
+
dryRun: !fields.confirmWrite,
|
|
4851
|
+
media_root: join8(repoRoot, ".asset-scratch"),
|
|
4852
|
+
restored: fields.confirmWrite ? copyable.length : 0,
|
|
4853
|
+
total: manifest.assets.length,
|
|
4854
|
+
would_restore: fields.confirmWrite ? 0 : copyable.length,
|
|
4855
|
+
source_required: false,
|
|
4856
|
+
source_env: manifest.media.source_env,
|
|
4857
|
+
source_hint: manifest.media.source_hint,
|
|
4858
|
+
missing: before.missing,
|
|
4859
|
+
invalid: before.invalid,
|
|
4860
|
+
unavailable
|
|
4578
4861
|
};
|
|
4579
4862
|
}
|
|
4580
4863
|
function svg(label, fill, stroke) {
|
|
@@ -4592,13 +4875,14 @@ function assetIdFor(asset) {
|
|
|
4592
4875
|
function demoAssetIds() {
|
|
4593
4876
|
return Object.fromEntries(demoAssets.map((asset) => [asset.key, assetIdFor(asset)]));
|
|
4594
4877
|
}
|
|
4878
|
+
function writeDemoFile(project, asset) {
|
|
4879
|
+
const path = demoFilePath(project, asset);
|
|
4880
|
+
mkdirSync5(dirname3(path), { recursive: true });
|
|
4881
|
+
writeFileSync3(path, svg(asset.label, asset.fill, asset.stroke));
|
|
4882
|
+
}
|
|
4595
4883
|
function writeDemoFiles(project) {
|
|
4596
4884
|
const ids = demoAssetIds();
|
|
4597
|
-
for (const asset of demoAssets)
|
|
4598
|
-
const path = join8(demoProjectDir(project), asset.channel, asset.file);
|
|
4599
|
-
mkdirSync5(dirname3(path), { recursive: true });
|
|
4600
|
-
writeFileSync3(path, svg(asset.label, asset.fill, asset.stroke));
|
|
4601
|
-
}
|
|
4885
|
+
for (const asset of demoAssets) writeDemoFile(project, asset);
|
|
4602
4886
|
return ids;
|
|
4603
4887
|
}
|
|
4604
4888
|
function upsertDemoAssets(project, ids) {
|
|
@@ -4647,6 +4931,55 @@ function upsertDemoAssets(project, ids) {
|
|
|
4647
4931
|
}
|
|
4648
4932
|
return { catalog: 0, local: demoAssets.length, total: demoAssets.length };
|
|
4649
4933
|
}
|
|
4934
|
+
function upsertSwissifierAssets(project, manifest) {
|
|
4935
|
+
const database = lineageDb();
|
|
4936
|
+
const timestamp = nowIso();
|
|
4937
|
+
try {
|
|
4938
|
+
database.prepare(`
|
|
4939
|
+
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
4940
|
+
values (?, ?, ?, ?, ?)
|
|
4941
|
+
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
4942
|
+
`).run(project, project, join8(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
4943
|
+
const assetStatement = database.prepare(`
|
|
4944
|
+
insert into assets (
|
|
4945
|
+
id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
|
|
4946
|
+
channel, campaign, audience, size_bytes, content_type, created_at, updated_at, last_seen_at
|
|
4947
|
+
) values (?, ?, 'local', ?, null, ?, 'image', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4948
|
+
on conflict(id) do update set
|
|
4949
|
+
project_id = excluded.project_id, source = excluded.source, local_path = excluded.local_path, checksum_sha256 = excluded.checksum_sha256,
|
|
4950
|
+
media_type = excluded.media_type, title = excluded.title, status = excluded.status, channel = excluded.channel,
|
|
4951
|
+
campaign = excluded.campaign, audience = excluded.audience, size_bytes = excluded.size_bytes,
|
|
4952
|
+
content_type = excluded.content_type, updated_at = excluded.updated_at, last_seen_at = excluded.last_seen_at
|
|
4953
|
+
`);
|
|
4954
|
+
const reviewStatement = database.prepare(`
|
|
4955
|
+
insert into asset_reviews (asset_id, review_state, updated_at)
|
|
4956
|
+
values (?, 'unreviewed', ?)
|
|
4957
|
+
on conflict(asset_id) do nothing
|
|
4958
|
+
`);
|
|
4959
|
+
for (const asset of manifest.assets) {
|
|
4960
|
+
assetStatement.run(
|
|
4961
|
+
asset.asset_id,
|
|
4962
|
+
project,
|
|
4963
|
+
swissifierRelativePath(manifest, asset),
|
|
4964
|
+
asset.checksum_sha256,
|
|
4965
|
+
asset.title,
|
|
4966
|
+
asset.status,
|
|
4967
|
+
asset.channel,
|
|
4968
|
+
manifest.campaign,
|
|
4969
|
+
manifest.audience,
|
|
4970
|
+
asset.size_bytes,
|
|
4971
|
+
asset.content_type,
|
|
4972
|
+
timestamp,
|
|
4973
|
+
timestamp,
|
|
4974
|
+
timestamp
|
|
4975
|
+
);
|
|
4976
|
+
reviewStatement.run(asset.asset_id, timestamp);
|
|
4977
|
+
}
|
|
4978
|
+
} finally {
|
|
4979
|
+
database.close();
|
|
4980
|
+
}
|
|
4981
|
+
return { catalog: 0, local: manifest.assets.length, total: manifest.assets.length };
|
|
4982
|
+
}
|
|
4650
4983
|
function seedDemoLineageWorkspace(project, fields) {
|
|
4651
4984
|
const ids = fields.confirmWrite ? writeDemoFiles(project) : demoAssetIds();
|
|
4652
4985
|
const rootAssetId = ids.root;
|
|
@@ -4692,6 +5025,53 @@ function seedDemoLineageWorkspace(project, fields) {
|
|
|
4692
5025
|
workspace
|
|
4693
5026
|
};
|
|
4694
5027
|
}
|
|
5028
|
+
function seedSwissifierRichDemoWorkspace(project, fields) {
|
|
5029
|
+
const manifest = swissifierManifest();
|
|
5030
|
+
if (!fields.confirmWrite) {
|
|
5031
|
+
return {
|
|
5032
|
+
ok: true,
|
|
5033
|
+
dryRun: true,
|
|
5034
|
+
demo_id: manifest.id,
|
|
5035
|
+
root_asset_id: manifest.root_asset_id,
|
|
5036
|
+
workspace_id: lineageWorkspaceId(project, manifest.root_asset_id),
|
|
5037
|
+
media_status: swissifierRichDemoMediaStatus(project)
|
|
5038
|
+
};
|
|
5039
|
+
}
|
|
5040
|
+
const summary = upsertSwissifierAssets(project, manifest);
|
|
5041
|
+
for (const edge of manifest.edges) {
|
|
5042
|
+
linkLineageAssets(project, { parentAssetId: edge.parent, childAssetId: edge.child, confirmWrite: true });
|
|
5043
|
+
}
|
|
5044
|
+
updateSelectedAsset(project, {
|
|
5045
|
+
assetIds: manifest.selected_asset_ids,
|
|
5046
|
+
confirmWrite: true,
|
|
5047
|
+
maxSelections: manifest.selected_asset_ids.length,
|
|
5048
|
+
notes: "Swissifier rich demo bases selected for the next variation.",
|
|
5049
|
+
rootAssetId: manifest.root_asset_id
|
|
5050
|
+
});
|
|
5051
|
+
updateLineageLayout(project, {
|
|
5052
|
+
confirmWrite: true,
|
|
5053
|
+
rootAssetId: manifest.root_asset_id,
|
|
5054
|
+
positions: manifest.assets.map((asset) => ({ assetId: asset.asset_id, ...asset.position }))
|
|
5055
|
+
});
|
|
5056
|
+
const workspace = createLineageWorkspace(project, {
|
|
5057
|
+
activate: fields.activate !== false,
|
|
5058
|
+
confirmWrite: true,
|
|
5059
|
+
createdBy: "system",
|
|
5060
|
+
notes: manifest.notes,
|
|
5061
|
+
rootAssetId: manifest.root_asset_id,
|
|
5062
|
+
title: manifest.title
|
|
5063
|
+
}).workspace;
|
|
5064
|
+
return {
|
|
5065
|
+
ok: true,
|
|
5066
|
+
message: `Seeded ${manifest.title}`,
|
|
5067
|
+
demo_id: manifest.id,
|
|
5068
|
+
media_status: swissifierRichDemoMediaStatus(project),
|
|
5069
|
+
root_asset_id: manifest.root_asset_id,
|
|
5070
|
+
selected_asset_ids: manifest.selected_asset_ids,
|
|
5071
|
+
summary,
|
|
5072
|
+
workspace
|
|
5073
|
+
};
|
|
5074
|
+
}
|
|
4695
5075
|
function archiveDemoLineageWorkspace(project, confirmWrite) {
|
|
4696
5076
|
const ids = demoAssetIds();
|
|
4697
5077
|
const rootAssetId = ids.root;
|
|
@@ -4739,11 +5119,37 @@ function registerLineageWorkspaceRoutes(app2, projectFrom2, asyncRoute2) {
|
|
|
4739
5119
|
app2.post("/api/lineage-workspaces/demo/archive", asyncRoute2((req, res) => {
|
|
4740
5120
|
res.json(archiveDemoLineageWorkspace(projectFrom2(req), req.body.confirmWrite === true));
|
|
4741
5121
|
}));
|
|
4742
|
-
app2.
|
|
4743
|
-
res.json(
|
|
5122
|
+
app2.post("/api/lineage-workspaces/demo/swissifier/seed", asyncRoute2((req, res) => {
|
|
5123
|
+
res.json(seedSwissifierRichDemoWorkspace(projectFrom2(req), {
|
|
5124
|
+
activate: req.body.activate !== false,
|
|
5125
|
+
confirmWrite: req.body.confirmWrite === true
|
|
5126
|
+
}));
|
|
5127
|
+
}));
|
|
5128
|
+
app2.get("/api/lineage-workspaces/demo/media", asyncRoute2((req, res) => {
|
|
5129
|
+
res.json({ ok: true, status: demoSeedMediaStatus(projectFrom2(req)) });
|
|
5130
|
+
}));
|
|
5131
|
+
app2.get("/api/lineage-workspaces/demo/swissifier/media", asyncRoute2((req, res) => {
|
|
5132
|
+
res.json({ ok: true, status: swissifierRichDemoMediaStatus(projectFrom2(req)) });
|
|
4744
5133
|
}));
|
|
4745
5134
|
app2.post("/api/lineage-workspaces/demo/media/restore", asyncRoute2((req, res) => {
|
|
4746
|
-
res.json({ ok: true, result: restoreDemoSeedMedia({ confirmWrite: req.body.confirmWrite === true }) });
|
|
5135
|
+
res.json({ ok: true, result: restoreDemoSeedMedia(projectFrom2(req), { confirmWrite: req.body.confirmWrite === true }) });
|
|
5136
|
+
}));
|
|
5137
|
+
app2.post("/api/lineage-workspaces/demo/swissifier/media/restore", asyncRoute2((req, res) => {
|
|
5138
|
+
res.json({
|
|
5139
|
+
ok: true,
|
|
5140
|
+
result: restoreSwissifierRichDemoMedia(projectFrom2(req), {
|
|
5141
|
+
confirmWrite: req.body.confirmWrite === true,
|
|
5142
|
+
sourceDir: typeof req.body.sourceDir === "string" ? req.body.sourceDir : void 0
|
|
5143
|
+
})
|
|
5144
|
+
});
|
|
5145
|
+
}));
|
|
5146
|
+
app2.post("/api/lineage-workspaces/demo/swissifier/media/download", asyncRoute2(async (req, res) => {
|
|
5147
|
+
res.json({
|
|
5148
|
+
ok: true,
|
|
5149
|
+
result: await downloadSwissifierRichDemoMedia(projectFrom2(req), {
|
|
5150
|
+
confirmWrite: req.body.confirmWrite === true
|
|
5151
|
+
})
|
|
5152
|
+
});
|
|
4747
5153
|
}));
|
|
4748
5154
|
app2.get("/api/lineage-workspaces/:workspaceId", asyncRoute2((req, res) => {
|
|
4749
5155
|
res.json({
|
|
@@ -4772,7 +5178,7 @@ function registerLineageWorkspaceRoutes(app2, projectFrom2, asyncRoute2) {
|
|
|
4772
5178
|
// src/server.ts
|
|
4773
5179
|
var app = express2();
|
|
4774
5180
|
var port = Number(process.env.PORT || 5173);
|
|
4775
|
-
var host = process.env.HOST || "
|
|
5181
|
+
var host = process.env.HOST || "lineage.localhost";
|
|
4776
5182
|
var isProduction = process.env.NODE_ENV === "production";
|
|
4777
5183
|
var maxUploadBytes = Number(process.env.LINEAGE_MAX_UPLOAD_MB || 200) * 1024 * 1024;
|
|
4778
5184
|
var upload = multer({ dest: ensureUploadDir(), limits: { fileSize: maxUploadBytes } });
|
|
@@ -5075,7 +5481,7 @@ app.post(
|
|
|
5075
5481
|
);
|
|
5076
5482
|
if (isProduction) {
|
|
5077
5483
|
const dist = join9(repoRoot, "dist", "web");
|
|
5078
|
-
if (
|
|
5484
|
+
if (existsSync6(dist)) {
|
|
5079
5485
|
app.use(express2.static(dist));
|
|
5080
5486
|
app.get("*", (_req, res) => res.sendFile(join9(dist, "index.html")));
|
|
5081
5487
|
}
|