@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/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 existsSync5 } from "node:fs";
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
- return normalizeCatalog(JSON.parse(readFileSync2(path, "utf8")), project);
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: `npx lineage lineage next --project ${project} --root ${next.root_asset_id} --json`,
1964
- inspect_command: asset ? `npx lineage lineage inspect --project ${project} --asset-id ${asset.asset_id} --json` : void 0,
1965
- link_child_command: asset ? `npx lineage lineage link-child --project ${project} --root ${next.root_asset_id} --child <asset-id> --confirm-write --json` : void 0
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 { dirname as dirname3, join as join8 } from "node:path";
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 demoSeedMediaStatus() {
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: demoAssets.length,
4703
+ present,
4564
4704
  total: demoAssets.length,
4565
- missing: [],
4566
- fixture_present: demoAssets.length,
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 restoreDemoSeedMedia(fields = { confirmWrite: false }) {
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.get("/api/lineage-workspaces/demo/media", asyncRoute2((_req, res) => {
4743
- res.json({ ok: true, status: demoSeedMediaStatus() });
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 || "127.0.0.1";
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 (existsSync5(dist)) {
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
  }