@mean-weasel/lineage 0.1.2 → 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
@@ -4527,11 +4527,14 @@ function listImageGenerationJobs(project = defaultProject, fields = {}) {
4527
4527
 
4528
4528
  // src/server/assetLineageDemo.ts
4529
4529
  import { createHash as createHash2 } from "node:crypto";
4530
- import { mkdirSync as mkdirSync5, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
4531
- 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";
4532
4534
  var demoWorkspaceTitle = "Demo: Content iteration tree";
4533
4535
  var demoWorkspaceNotes = "Repeatable sample lineage for demos and onboarding. Archive it when reviewing real work.";
4534
4536
  var demoBasePath = ["lineage-demo", "2026-06-lineage-demo"];
4537
+ var swissifierManifestPath = join8(repoRoot, "fixtures", defaultProject, "lineage", "swissifier-rich-demo.json");
4535
4538
  var demoAssets = [
4536
4539
  { key: "root", channel: "linkedin", file: "demo-root.svg", label: "Initial Demo Concept", fill: "#f6fbfb", stroke: "#0b7f88" },
4537
4540
  { key: "hookA", channel: "tiktok", file: "demo-hook-a-v01.svg", label: "Hook A v01", fill: "#fff8e6", stroke: "#9a6a00" },
@@ -4573,25 +4576,288 @@ function demoProjectDir(project) {
4573
4576
  function demoRelativePath(project, asset) {
4574
4577
  return join8(...demoBasePath, project, asset.channel, asset.file);
4575
4578
  }
4576
- 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;
4577
4700
  return {
4578
4701
  ok: true,
4579
4702
  media_root: join8(repoRoot, ".asset-scratch"),
4580
- present: demoAssets.length,
4703
+ present,
4581
4704
  total: demoAssets.length,
4582
- missing: [],
4583
- fixture_present: demoAssets.length,
4705
+ missing,
4706
+ fixture_present: present,
4584
4707
  fixture_total: demoAssets.length,
4585
- fixture_missing: []
4708
+ fixture_missing: missing
4586
4709
  };
4587
4710
  }
4588
- 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
+ }
4589
4794
  return {
4590
4795
  ok: true,
4591
4796
  dryRun: !fields.confirmWrite,
4592
4797
  media_root: join8(repoRoot, ".asset-scratch"),
4593
- restored: 0,
4594
- 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
4595
4861
  };
4596
4862
  }
4597
4863
  function svg(label, fill, stroke) {
@@ -4609,13 +4875,14 @@ function assetIdFor(asset) {
4609
4875
  function demoAssetIds() {
4610
4876
  return Object.fromEntries(demoAssets.map((asset) => [asset.key, assetIdFor(asset)]));
4611
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
+ }
4612
4883
  function writeDemoFiles(project) {
4613
4884
  const ids = demoAssetIds();
4614
- for (const asset of demoAssets) {
4615
- const path = join8(demoProjectDir(project), asset.channel, asset.file);
4616
- mkdirSync5(dirname3(path), { recursive: true });
4617
- writeFileSync3(path, svg(asset.label, asset.fill, asset.stroke));
4618
- }
4885
+ for (const asset of demoAssets) writeDemoFile(project, asset);
4619
4886
  return ids;
4620
4887
  }
4621
4888
  function upsertDemoAssets(project, ids) {
@@ -4664,6 +4931,55 @@ function upsertDemoAssets(project, ids) {
4664
4931
  }
4665
4932
  return { catalog: 0, local: demoAssets.length, total: demoAssets.length };
4666
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
+ }
4667
4983
  function seedDemoLineageWorkspace(project, fields) {
4668
4984
  const ids = fields.confirmWrite ? writeDemoFiles(project) : demoAssetIds();
4669
4985
  const rootAssetId = ids.root;
@@ -4709,6 +5025,53 @@ function seedDemoLineageWorkspace(project, fields) {
4709
5025
  workspace
4710
5026
  };
4711
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
+ }
4712
5075
  function archiveDemoLineageWorkspace(project, confirmWrite) {
4713
5076
  const ids = demoAssetIds();
4714
5077
  const rootAssetId = ids.root;
@@ -4756,11 +5119,37 @@ function registerLineageWorkspaceRoutes(app2, projectFrom2, asyncRoute2) {
4756
5119
  app2.post("/api/lineage-workspaces/demo/archive", asyncRoute2((req, res) => {
4757
5120
  res.json(archiveDemoLineageWorkspace(projectFrom2(req), req.body.confirmWrite === true));
4758
5121
  }));
4759
- app2.get("/api/lineage-workspaces/demo/media", asyncRoute2((_req, res) => {
4760
- 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)) });
4761
5133
  }));
4762
5134
  app2.post("/api/lineage-workspaces/demo/media/restore", asyncRoute2((req, res) => {
4763
- 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
+ });
4764
5153
  }));
4765
5154
  app2.get("/api/lineage-workspaces/:workspaceId", asyncRoute2((req, res) => {
4766
5155
  res.json({
@@ -4789,7 +5178,7 @@ function registerLineageWorkspaceRoutes(app2, projectFrom2, asyncRoute2) {
4789
5178
  // src/server.ts
4790
5179
  var app = express2();
4791
5180
  var port = Number(process.env.PORT || 5173);
4792
- var host = process.env.HOST || "127.0.0.1";
5181
+ var host = process.env.HOST || "lineage.localhost";
4793
5182
  var isProduction = process.env.NODE_ENV === "production";
4794
5183
  var maxUploadBytes = Number(process.env.LINEAGE_MAX_UPLOAD_MB || 200) * 1024 * 1024;
4795
5184
  var upload = multer({ dest: ensureUploadDir(), limits: { fileSize: maxUploadBytes } });
@@ -5092,7 +5481,7 @@ app.post(
5092
5481
  );
5093
5482
  if (isProduction) {
5094
5483
  const dist = join9(repoRoot, "dist", "web");
5095
- if (existsSync5(dist)) {
5484
+ if (existsSync6(dist)) {
5096
5485
  app.use(express2.static(dist));
5097
5486
  app.get("*", (_req, res) => res.sendFile(join9(dist, "index.html")));
5098
5487
  }