@jant/core 0.3.45 → 0.3.47

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.
Files changed (114) hide show
  1. package/bin/commands/db/execute-file.js +12 -4
  2. package/bin/commands/db/rehearse.js +2 -2
  3. package/bin/commands/export.js +12 -4
  4. package/bin/commands/import-site.js +99 -305
  5. package/bin/commands/migrate.js +36 -69
  6. package/bin/commands/reset-password.js +10 -4
  7. package/bin/commands/site/export.js +59 -248
  8. package/bin/commands/site/snapshot/export.js +58 -45
  9. package/bin/commands/site/snapshot/import.js +104 -52
  10. package/bin/lib/node-env.js +100 -0
  11. package/bin/lib/runtime-target.js +64 -0
  12. package/bin/lib/site-snapshot.js +185 -54
  13. package/bin/lib/sql-export.js +19 -2
  14. package/dist/{app-C-L7wL6o.js → app-3REcR-3U.js} +332 -190
  15. package/dist/app-B67XOEyo.js +6 -0
  16. package/dist/client/.vite/manifest.json +2 -2
  17. package/dist/client/_assets/{client-auth-Dcon89Av.js → client-auth-Ce5WEAVS.js} +236 -183
  18. package/dist/client/_assets/client-s71Js1Cu.css +2 -0
  19. package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
  20. package/dist/github-sync-C593r22F.js +4 -0
  21. package/dist/github-sync-bL1hnx3Q.js +428 -0
  22. package/dist/index.js +3 -2
  23. package/dist/node.js +5 -4
  24. package/package.json +3 -2
  25. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  26. package/src/__tests__/import-site-command.test.ts +18 -0
  27. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
  28. package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
  29. package/src/client/components/jant-compose-dialog.ts +7 -6
  30. package/src/client/components/jant-compose-editor.ts +6 -5
  31. package/src/client/components/jant-settings-general.ts +164 -22
  32. package/src/client/components/settings-types.ts +4 -6
  33. package/src/client/random-uuid.ts +23 -0
  34. package/src/client-auth.ts +1 -1
  35. package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
  36. package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
  37. package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
  38. package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
  39. package/src/db/migrations/meta/0021_snapshot.json +2121 -0
  40. package/src/db/migrations/meta/_journal.json +7 -0
  41. package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
  42. package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
  43. package/src/db/migrations/pg/meta/_journal.json +7 -0
  44. package/src/db/pg/schema.ts +21 -26
  45. package/src/db/rehearsal-fixtures/demo-current.json +1 -1
  46. package/src/db/schema.ts +16 -20
  47. package/src/i18n/__tests__/middleware.test.ts +43 -1
  48. package/src/i18n/coverage.generated.ts +17 -0
  49. package/src/i18n/i18n.ts +18 -2
  50. package/src/i18n/index.ts +3 -0
  51. package/src/i18n/locales/settings/en.po +16 -11
  52. package/src/i18n/locales/settings/en.ts +1 -1
  53. package/src/i18n/locales/settings/zh-Hans.po +17 -12
  54. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  55. package/src/i18n/locales/settings/zh-Hant.po +16 -11
  56. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  57. package/src/i18n/locales.ts +84 -2
  58. package/src/i18n/middleware.ts +25 -16
  59. package/src/i18n/supported-locales.ts +153 -0
  60. package/src/lib/__tests__/csp-builder.test.ts +19 -2
  61. package/src/lib/__tests__/feed.test.ts +242 -1
  62. package/src/lib/__tests__/post-meta.test.ts +0 -1
  63. package/src/lib/__tests__/view.test.ts +0 -1
  64. package/src/lib/csp-builder.ts +28 -10
  65. package/src/lib/feed.ts +153 -3
  66. package/src/middleware/__tests__/secure-headers.test.ts +89 -0
  67. package/src/middleware/auth.ts +1 -1
  68. package/src/middleware/secure-headers.ts +47 -1
  69. package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
  70. package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
  71. package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
  72. package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
  73. package/src/node/__tests__/cli-sql-export.test.ts +49 -0
  74. package/src/node/index.ts +1 -0
  75. package/src/preset.css +8 -2
  76. package/src/routes/api/__tests__/settings.test.ts +3 -2
  77. package/src/routes/api/github-sync.tsx +1 -1
  78. package/src/routes/api/settings.ts +4 -1
  79. package/src/routes/auth/signin.tsx +6 -0
  80. package/src/routes/pages/archive.tsx +4 -2
  81. package/src/services/__tests__/post.test.ts +19 -19
  82. package/src/services/__tests__/search.test.ts +0 -1
  83. package/src/services/__tests__/settings.test.ts +22 -3
  84. package/src/services/bootstrap.ts +7 -3
  85. package/src/services/collection.ts +3 -3
  86. package/src/services/export.ts +0 -3
  87. package/src/services/navigation.ts +0 -2
  88. package/src/services/path.ts +1 -38
  89. package/src/services/post.ts +32 -66
  90. package/src/services/search.ts +0 -6
  91. package/src/services/settings.ts +47 -6
  92. package/src/services/site-admin.ts +6 -1
  93. package/src/styles/ui.css +12 -23
  94. package/src/types/entities.ts +0 -1
  95. package/src/ui/color-themes.ts +1 -1
  96. package/src/ui/dash/settings/GeneralContent.tsx +17 -19
  97. package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
  98. package/src/ui/feed/NoteCard.tsx +1 -11
  99. package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
  100. package/src/ui/pages/HomePage.tsx +1 -4
  101. package/src/ui/pages/PostPage.tsx +2 -0
  102. package/bin/commands/collections.js +0 -268
  103. package/bin/commands/media.js +0 -302
  104. package/bin/commands/posts.js +0 -262
  105. package/bin/commands/search.js +0 -53
  106. package/bin/commands/settings.js +0 -93
  107. package/bin/lib/http-api.js +0 -223
  108. package/bin/lib/media-upload.js +0 -206
  109. package/dist/app-Hvqe7Ks_.js +0 -5
  110. package/dist/client/_assets/client-DDs6NzB3.css +0 -2
  111. package/src/__tests__/bin/content-cli.test.ts +0 -179
  112. package/src/__tests__/bin/media-cli.test.ts +0 -192
  113. /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
  114. /package/dist/{github-app-WeadXMb8.js → github-app-D0GvNnqp.js} +0 -0
@@ -3,13 +3,13 @@ import { parseArgs } from "node:util";
3
3
  import { executeD1 } from "../../lib/d1-query.js";
4
4
  import { openNodeDatabase } from "../../lib/node-database.js";
5
5
  import {
6
+ bootstrapCliRuntime,
6
7
  getCliRuntimeLabel,
7
- resolveCliRuntime,
8
8
  } from "../../lib/runtime-target.js";
9
9
 
10
10
  function formatUsage() {
11
11
  console.log(
12
- "Usage: jant db execute-file --file <path> [--local | --remote] [--config <file>] [--env <name>] [--database <binding>]",
12
+ "Usage: jant db execute-file --file <path> [--local | --remote | --node] [--config <file>] [--env <name>] [--database <binding>]",
13
13
  );
14
14
  console.log("");
15
15
  console.log(
@@ -20,6 +20,9 @@ function formatUsage() {
20
20
  console.log(" --file SQL file to execute");
21
21
  console.log(" --local Force local D1 instead of DATABASE_URL");
22
22
  console.log(" --remote Run against remote D1");
23
+ console.log(
24
+ " --node Force Node runtime even if DATABASE_URL is unset",
25
+ );
23
26
  console.log(
24
27
  " --config Wrangler config file (default: wrangler.toml)",
25
28
  );
@@ -28,8 +31,12 @@ function formatUsage() {
28
31
  console.log(" --persist-to Local D1 state directory override");
29
32
  console.log("");
30
33
  console.log(
31
- "If DATABASE_URL or DATA_DIR is set and no runtime flag is passed, this command uses the Node database runtime.",
34
+ "`.env.node` next to your project (or in packages/core/) is auto-loaded.",
35
+ );
36
+ console.log(
37
+ "If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
32
38
  );
39
+ console.log("this command uses the Node database runtime.");
33
40
  }
34
41
 
35
42
  async function loadSqlFile(filePath) {
@@ -59,6 +66,7 @@ export async function run(argv) {
59
66
  file: { type: "string" },
60
67
  help: { type: "boolean", short: "h" },
61
68
  local: { type: "boolean", default: false },
69
+ node: { type: "boolean", default: false },
62
70
  "persist-to": { type: "string" },
63
71
  remote: { type: "boolean", default: false },
64
72
  },
@@ -73,7 +81,7 @@ export async function run(argv) {
73
81
  throw new Error("Missing required --file <path> argument.");
74
82
  }
75
83
 
76
- const runtime = resolveCliRuntime(values);
84
+ const { runtime } = bootstrapCliRuntime(values);
77
85
  const sql = await loadSqlFile(values.file);
78
86
 
79
87
  if (runtime === "node") {
@@ -1,8 +1,8 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { rehearseD1Migrations } from "../../lib/migration-rehearsal.js";
3
3
  import {
4
+ bootstrapCliRuntime,
4
5
  getCliRuntimeLabel,
5
- resolveCliRuntime,
6
6
  } from "../../lib/runtime-target.js";
7
7
 
8
8
  export async function run(argv) {
@@ -46,7 +46,7 @@ export async function run(argv) {
46
46
  throw new Error("Missing required --fixture option.");
47
47
  }
48
48
 
49
- const runtime = resolveCliRuntime(values);
49
+ const { runtime } = bootstrapCliRuntime(values);
50
50
  if (runtime === "node") {
51
51
  throw new Error(
52
52
  "Migration rehearsal only supports D1. Pass --local or --remote.",
@@ -5,8 +5,8 @@ import { queryD1 } from "../lib/d1-query.js";
5
5
  import { openNodeDatabase } from "../lib/node-database.js";
6
6
  import { dumpDatabaseToSql } from "../lib/sql-export.js";
7
7
  import {
8
+ bootstrapCliRuntime,
8
9
  getCliRuntimeLabel,
9
- resolveCliRuntime,
10
10
  } from "../lib/runtime-target.js";
11
11
 
12
12
  function createD1QueryRunner(runtime) {
@@ -25,6 +25,7 @@ export async function run(argv) {
25
25
  database: { type: "string", default: "DB" },
26
26
  env: { type: "string" },
27
27
  local: { type: "boolean", default: false },
28
+ node: { type: "boolean", default: false },
28
29
  remote: { type: "boolean", default: false },
29
30
  output: { type: "string", short: "o", default: "jant-export.sql" },
30
31
  help: { type: "boolean", short: "h" },
@@ -34,7 +35,7 @@ export async function run(argv) {
34
35
 
35
36
  if (values.help) {
36
37
  console.log(
37
- "Usage: jant db export [--local | --remote] [--output <file>] [--config <file>] [--env <name>] [--database <binding>]",
38
+ "Usage: jant db export [--local | --remote | --node] [--output <file>] [--config <file>] [--env <name>] [--database <binding>]",
38
39
  );
39
40
  console.log("");
40
41
  console.log("Export the current database to a SQL file.");
@@ -44,6 +45,9 @@ export async function run(argv) {
44
45
  console.log(
45
46
  " --remote Export from remote D1 database (default: local)",
46
47
  );
48
+ console.log(
49
+ " --node Force Node runtime even if DATABASE_URL is unset",
50
+ );
47
51
  console.log(
48
52
  " --output, -o Output file path (default: jant-export.sql)",
49
53
  );
@@ -55,14 +59,18 @@ export async function run(argv) {
55
59
  console.log(" --persist-to Local D1 state directory override");
56
60
  console.log("");
57
61
  console.log(
58
- "If DATABASE_URL or DATA_DIR is set and no runtime flag is passed, this command uses the Node database runtime.",
62
+ "`.env.node` next to your project (or in packages/core/) is auto-loaded.",
63
+ );
64
+ console.log(
65
+ "If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
59
66
  );
67
+ console.log("this command uses the Node database runtime.");
60
68
  console.log("");
61
69
  console.log("Compatibility alias: jant export");
62
70
  process.exit(0);
63
71
  }
64
72
 
65
- const runtime = resolveCliRuntime(values);
73
+ const { runtime } = bootstrapCliRuntime(values);
66
74
  const output = values.output;
67
75
  let sql;
68
76
 
@@ -18,8 +18,6 @@ import {
18
18
  normalizeImportedBody,
19
19
  rewriteMediaReferences,
20
20
  } from "../lib/site-media-parser.js";
21
- import { openNodeDatabase } from "../lib/node-database.js";
22
- import { loadNodeRuntime } from "../lib/load-node-runtime.js";
23
21
  import { parseFrontMatter as parseFrontMatterShared } from "../lib/hugo-markdown.js";
24
22
 
25
23
  /**
@@ -437,6 +435,18 @@ function isAbsoluteUrl(value) {
437
435
  return typeof value === "string" && /^https?:\/\//i.test(value);
438
436
  }
439
437
 
438
+ /**
439
+ * True when `url` points to a different location than the import target —
440
+ * i.e. has a scheme (`https://`, `data:`) or is protocol-relative (`//cdn…`).
441
+ * False for relative paths (`/media/x`, `./x`, `x`), which always belong to
442
+ * the source site and must be rehosted. Used to filter the body-fallback
443
+ * upload when `--skip-remote-media` is set.
444
+ */
445
+ function isAbsoluteImportUrl(value) {
446
+ if (typeof value !== "string") return false;
447
+ return /^([a-z][a-z0-9+.\-]*:|\/\/)/i.test(value);
448
+ }
449
+
440
450
  /**
441
451
  * Resolve a `media:` entry's `src` or `poster` reference to a local disk
442
452
  * path when the export bundled the bytes under `static/`. Absolute URLs
@@ -641,19 +651,7 @@ async function buildImportedAttachments(
641
651
  target,
642
652
  siteConfig,
643
653
  sourceRootDir,
644
- options = {},
645
654
  ) {
646
- if (
647
- sourceRootDir &&
648
- typeof sourceRootDir === "object" &&
649
- !Array.isArray(sourceRootDir) &&
650
- options &&
651
- Object.keys(options).length === 0
652
- ) {
653
- options = sourceRootDir;
654
- sourceRootDir = null;
655
- }
656
-
657
655
  const attachments = [];
658
656
  let uploaded = 0;
659
657
 
@@ -668,10 +666,6 @@ async function buildImportedAttachments(
668
666
  continue;
669
667
  }
670
668
 
671
- if (options.skipUploads) {
672
- continue;
673
- }
674
-
675
669
  const normalized = await normalizeMediaSpec(
676
670
  spec,
677
671
  siteConfig,
@@ -1487,223 +1481,6 @@ function createRemoteTarget(apiUrl, token) {
1487
1481
  };
1488
1482
  }
1489
1483
 
1490
- async function createLocalTarget(env = process.env) {
1491
- const nodeDatabase = await openNodeDatabase(env);
1492
- const { createNodeCliRuntime, resolveConfig } = await loadNodeRuntime();
1493
- const bindings = nodeDatabase.bindings;
1494
- const runtime = await createNodeCliRuntime(bindings);
1495
- const allSettings = await runtime.services.settings.getAll();
1496
- const appConfig = resolveConfig(bindings, allSettings);
1497
- const summaryConfig = {
1498
- maxParagraphs: appConfig.summaryMaxParagraphs,
1499
- maxChars: appConfig.summaryMaxChars,
1500
- };
1501
-
1502
- return {
1503
- async close() {
1504
- await nodeDatabase.close();
1505
- },
1506
- async getSetupStatus() {
1507
- return runtime.services.settings.isOnboardingComplete();
1508
- },
1509
- async updateSettings(updates) {
1510
- await runtime.services.settings.setMany(updates);
1511
- return { settings: updates };
1512
- },
1513
- async updateImportSettings(updates) {
1514
- await runtime.services.settings.setMany(updates);
1515
- return { success: true };
1516
- },
1517
- async listNavItems() {
1518
- return runtime.services.navItems.list();
1519
- },
1520
- async createNavItem(data) {
1521
- return runtime.services.navItems.create(data);
1522
- },
1523
- async deleteNavItem(id) {
1524
- return runtime.services.navItems.delete(id);
1525
- },
1526
- async removeSiteAvatar() {
1527
- return runtime.services.settings.removeAvatar(runtime.storage);
1528
- },
1529
- async uploadSiteAvatar(data) {
1530
- if (!runtime.storage) {
1531
- throw new Error("Local import requires configured storage.");
1532
- }
1533
-
1534
- const avatarAsset = await readImportAsset({
1535
- sourceUrl: data.avatarUrl,
1536
- sourceFilePath: data.avatarFilePath,
1537
- });
1538
- if (!avatarAsset) {
1539
- throw new Error(`Failed to read site avatar: ${data.avatarUrl}`);
1540
- }
1541
-
1542
- let faviconIco;
1543
- if (data.faviconUrl || data.faviconFilePath) {
1544
- const faviconAsset = await readImportAsset({
1545
- sourceUrl: data.faviconUrl,
1546
- sourceFilePath: data.faviconFilePath,
1547
- mimeType: "image/x-icon",
1548
- originalName: "favicon.ico",
1549
- });
1550
- if (faviconAsset) {
1551
- faviconIco = toArrayBuffer(faviconAsset.bytes);
1552
- }
1553
- }
1554
-
1555
- let appleTouchIcon;
1556
- if (data.appleTouchUrl) {
1557
- const appleTouchAsset = await readImportAsset({
1558
- sourceUrl: data.appleTouchUrl,
1559
- sourceFilePath: data.appleTouchFilePath,
1560
- });
1561
- if (appleTouchAsset) {
1562
- appleTouchIcon = toArrayBuffer(appleTouchAsset.bytes);
1563
- }
1564
- }
1565
-
1566
- await runtime.services.settings.uploadAvatar(
1567
- {
1568
- file: createUploadFile(
1569
- avatarAsset.filename,
1570
- avatarAsset.contentType,
1571
- avatarAsset.bytes,
1572
- ),
1573
- faviconIco,
1574
- appleTouchIcon,
1575
- },
1576
- {
1577
- media: runtime.services.media,
1578
- storage: runtime.storage,
1579
- storageProvider: appConfig.storageDriver,
1580
- maxFileSizeMB: appConfig.uploadMaxFileSize,
1581
- },
1582
- );
1583
-
1584
- return { success: true };
1585
- },
1586
- async syncSiteAvatar(data) {
1587
- await this.removeSiteAvatar();
1588
- if (!data) {
1589
- return { success: true };
1590
- }
1591
- return this.uploadSiteAvatar(data);
1592
- },
1593
- async listCollections() {
1594
- return runtime.services.collections.list();
1595
- },
1596
- async listCollectionDirectoryItems() {
1597
- return runtime.services.collections.listDirectoryItems();
1598
- },
1599
- async createCollection(data) {
1600
- return runtime.services.collections.create(data);
1601
- },
1602
- async createCollectionDirectoryItem(data) {
1603
- return runtime.services.collections.createDirectoryItem(data);
1604
- },
1605
- async moveCollectionDirectoryItem(id, after, before) {
1606
- return runtime.services.collections.moveDirectoryItem(id, after, before);
1607
- },
1608
- async deleteCollectionDirectoryItem(id) {
1609
- return runtime.services.collections.deleteDirectoryItem(id);
1610
- },
1611
- async createPost(data) {
1612
- const { attachments, ...postData } = data;
1613
- return runtime.services.posts.createWithAttachments(
1614
- postData,
1615
- attachments,
1616
- {
1617
- media: runtime.services.media,
1618
- storage: runtime.storage,
1619
- storageDriver: appConfig.storageDriver,
1620
- maxFileSizeMB: appConfig.uploadMaxFileSize,
1621
- },
1622
- summaryConfig,
1623
- );
1624
- },
1625
- async createAlias(path, targetSlug) {
1626
- const post = await runtime.services.posts.getBySlug(targetSlug);
1627
- if (!post) {
1628
- throw new Error(`Post with slug "${targetSlug}" not found`);
1629
- }
1630
- return runtime.services.customUrls.create({
1631
- path,
1632
- targetType: "post",
1633
- targetId: post.id,
1634
- });
1635
- },
1636
- async uploadMedia(mediaSpec) {
1637
- if (!runtime.storage) {
1638
- throw new Error("Local import requires configured storage.");
1639
- }
1640
-
1641
- const asset = await readMediaSpecAsset(mediaSpec);
1642
- if (!asset) return null;
1643
-
1644
- const originalName =
1645
- mediaSpec.originalName ||
1646
- asset.filename ||
1647
- getFilenameFromUrl(mediaSpec.src) ||
1648
- "file";
1649
- const bytes = asset.bytes;
1650
- const { id, filename, storageKey } =
1651
- generateImportedStorageKey(originalName);
1652
- const mimeType =
1653
- mediaSpec.mimeType || asset.contentType || guessMimeType(originalName);
1654
- let posterKey;
1655
-
1656
- if (mediaSpec.poster) {
1657
- const posterAsset = await readMediaSpecAsset(mediaSpec, "poster");
1658
- if (posterAsset) {
1659
- const posterName = posterAsset.filename || "poster.webp";
1660
- const posterExt = extname(posterName) || ".webp";
1661
- posterKey = storageKey.replace(/(\.[^.]+)?$/, `-poster${posterExt}`);
1662
- await runtime.storage.put(posterKey, posterAsset.bytes, {
1663
- contentType: posterAsset.contentType || guessMimeType(posterName),
1664
- });
1665
- }
1666
- }
1667
-
1668
- await runtime.storage.put(storageKey, bytes, {
1669
- contentType: mimeType,
1670
- });
1671
-
1672
- const createdMedia = await runtime.services.media.create({
1673
- id,
1674
- filename,
1675
- originalName,
1676
- mimeType,
1677
- size: mediaSpec.size ?? bytes.byteLength,
1678
- storageKey,
1679
- provider: appConfig.storageDriver,
1680
- width: mediaSpec.width ?? undefined,
1681
- height: mediaSpec.height ?? undefined,
1682
- alt: mediaSpec.alt ?? undefined,
1683
- position: mediaSpec.position ?? undefined,
1684
- blurhash: mediaSpec.blurhash ?? undefined,
1685
- waveform: mediaSpec.waveform ?? undefined,
1686
- posterKey,
1687
- summary: mediaSpec.summary ?? undefined,
1688
- chars: mediaSpec.chars ?? undefined,
1689
- mediaKind: mediaSpec.kind ?? undefined,
1690
- });
1691
-
1692
- return {
1693
- id: createdMedia.id,
1694
- url: getMediaPublicUrl(
1695
- createdMedia.storageKey,
1696
- createdMedia.provider,
1697
- appConfig,
1698
- ),
1699
- };
1700
- },
1701
- async checkPostSlugAvailability(slug) {
1702
- return runtime.services.posts.checkSlugAvailability(slug);
1703
- },
1704
- };
1705
- }
1706
-
1707
1484
  /**
1708
1485
  * Walk `content/` and classify each `_index.md` / `index.md` bundle by its
1709
1486
  * front-matter `type`. Returns ordered root-post bundles (with child reply
@@ -1976,6 +1753,7 @@ function buildPostPayloadFromBundle(bundle, options) {
1976
1753
  }
1977
1754
 
1978
1755
  export const __test__ = {
1756
+ isAbsoluteImportUrl,
1979
1757
  resolveImportUrl,
1980
1758
  readMediaSpecAsset,
1981
1759
  normalizeMediaSpec,
@@ -2001,77 +1779,89 @@ export const __test__ = {
2001
1779
  buildPostPayloadFromBundle,
2002
1780
  };
2003
1781
 
1782
+ function printImportUsage() {
1783
+ console.log("Usage: jant site import <url> [options]");
1784
+ console.log("");
1785
+ console.log("Import a Hugo export directory or ZIP into a Jant site.");
1786
+ console.log("");
1787
+ console.log("Arguments:");
1788
+ console.log(" <url> Jant site URL (required)");
1789
+ console.log("");
1790
+ console.log("Options:");
1791
+ console.log(
1792
+ " --path Path to export directory or ZIP file (default: .)",
1793
+ );
1794
+ console.log(" --dry-run Parse and validate without making API calls");
1795
+ console.log(
1796
+ " --skip-remote-media Skip uploading absolute-URL images found in body (relative paths and declared media still import)",
1797
+ );
1798
+ console.log(" --token API token (overrides JANT_API_TOKEN)");
1799
+ console.log("");
1800
+ console.log(
1801
+ "Import expects an empty target site and fails on slug or alias conflicts.",
1802
+ );
1803
+ console.log("");
1804
+ console.log("Authentication:");
1805
+ console.log(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
1806
+ console.log(" jant site import https://your-site.example --path ./export");
1807
+ console.log("");
1808
+ console.log("Examples:");
1809
+ console.log(
1810
+ " jant site import https://your-site.example --path ./jant-site",
1811
+ );
1812
+ console.log(
1813
+ " jant site import https://your-site.example --path ./jant-site-export.zip",
1814
+ );
1815
+ console.log("");
1816
+ console.log("Compatibility alias: jant import-site");
1817
+ }
1818
+
2004
1819
  export async function run(argv) {
2005
- const { values } = parseArgs({
1820
+ const { values, positionals } = parseArgs({
2006
1821
  args: argv,
1822
+ allowPositionals: true,
2007
1823
  options: {
2008
- url: { type: "string" },
2009
1824
  token: { type: "string" },
2010
1825
  path: { type: "string", default: "." },
2011
1826
  "dry-run": { type: "boolean", default: false },
2012
- "skip-media": { type: "boolean", default: false },
1827
+ "skip-remote-media": { type: "boolean", default: false },
2013
1828
  help: { type: "boolean", short: "h" },
2014
1829
  },
2015
1830
  });
2016
1831
 
2017
1832
  if (values.help) {
2018
- console.log("Usage: jant site import [--url <url>] [options]");
2019
- console.log("");
2020
- console.log("Import a Hugo export directory or ZIP into a Jant instance.");
2021
- console.log("");
2022
- console.log("Modes:");
2023
- console.log(
2024
- " Local No --url; imports into the local Node database runtime",
2025
- );
2026
- console.log(
2027
- ` Remote --url requires ${CLI_API_TOKEN_ENV_VAR} or --token`,
2028
- );
2029
- console.log("");
2030
- console.log("Options:");
2031
- console.log(" --url Target remote Jant instance URL");
2032
- console.log(
2033
- " --path Path to export directory or ZIP file (default: .)",
2034
- );
2035
- console.log(" --dry-run Parse and validate without making API calls");
2036
- console.log(
2037
- " --skip-media Skip remote media download/upload (embedded text attachments still import)",
2038
- );
2039
- console.log("");
2040
- console.log(
2041
- "Import expects an empty target site and fails on slug or alias conflicts.",
2042
- );
2043
- console.log("");
2044
- console.log("Authentication:");
2045
- console.log(` Set ${CLI_API_TOKEN_ENV_VAR} env var (recommended):`);
2046
- console.log(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
2047
- console.log(" jant site import --url https://your-site.com");
2048
- console.log("");
2049
- console.log("Examples:");
2050
- console.log(" jant site import --path ./jant-site");
2051
- console.log(" jant site import --path ./jant-site-export.zip");
2052
- console.log("");
2053
- console.log("Compatibility alias: jant import-site");
1833
+ printImportUsage();
2054
1834
  process.exit(0);
2055
1835
  }
2056
1836
 
1837
+ const url = positionals[0];
1838
+ if (!url) {
1839
+ console.error("Error: site URL is required");
1840
+ console.error("");
1841
+ printImportUsage();
1842
+ process.exit(1);
1843
+ }
1844
+ if (positionals.length > 1) {
1845
+ console.error(
1846
+ `Error: unexpected extra arguments: ${positionals.slice(1).join(" ")}`,
1847
+ );
1848
+ process.exit(1);
1849
+ }
1850
+
1851
+ const dryRun = values["dry-run"];
2057
1852
  const token = getCliApiToken(process.env, values.token);
2058
- if (values.url && !token && !values["dry-run"]) {
1853
+ if (!token && !dryRun) {
2059
1854
  console.error(
2060
- `Error: remote import requires ${CLI_API_TOKEN_ENV_VAR} or --token (unless using --dry-run)`,
1855
+ `Error: site import requires ${CLI_API_TOKEN_ENV_VAR} or --token (unless using --dry-run)`,
2061
1856
  );
2062
1857
  console.error("");
2063
1858
  console.error(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
2064
1859
  process.exit(1);
2065
1860
  }
2066
1861
 
2067
- const apiUrl = values.url?.replace(/\/$/, "");
2068
- const dryRun = values["dry-run"];
2069
- const skipMedia = values["skip-media"];
2070
- const target = dryRun
2071
- ? null
2072
- : values.url
2073
- ? createRemoteTarget(apiUrl, token)
2074
- : await createLocalTarget(process.env);
1862
+ const apiUrl = url.replace(/\/$/, "");
1863
+ const skipRemoteMedia = values["skip-remote-media"];
1864
+ const target = dryRun ? null : createRemoteTarget(apiUrl, token);
2075
1865
 
2076
1866
  // 1. Read source — directory or ZIP
2077
1867
  const inputPath = resolve(process.cwd(), values.path);
@@ -2121,7 +1911,7 @@ export async function run(argv) {
2121
1911
  if (target) {
2122
1912
  const setupError = await getIncompleteSetupError(
2123
1913
  target,
2124
- values.url ? `Target site at ${apiUrl}` : "Local target site",
1914
+ `Target site at ${apiUrl}`,
2125
1915
  );
2126
1916
  if (setupError) {
2127
1917
  console.error("");
@@ -2149,7 +1939,7 @@ export async function run(argv) {
2149
1939
  `[dry-run] Would replace navigation with ${importedNav.items.length} items`,
2150
1940
  );
2151
1941
  }
2152
- if (avatarImport && !skipMedia) {
1942
+ if (avatarImport) {
2153
1943
  if (avatarImport.mode === "remove") {
2154
1944
  console.log("[dry-run] Would remove existing site avatar");
2155
1945
  } else {
@@ -2204,7 +1994,7 @@ export async function run(argv) {
2204
1994
  }
2205
1995
  }
2206
1996
 
2207
- if (avatarImport && !skipMedia) {
1997
+ if (avatarImport) {
2208
1998
  try {
2209
1999
  await target.syncSiteAvatar(
2210
2000
  avatarImport.mode === "set" ? avatarImport : null,
@@ -2349,7 +2139,7 @@ export async function run(argv) {
2349
2139
  }
2350
2140
 
2351
2141
  const rootResourceIds = [];
2352
- if (!skipMedia && !dryRun && rootResourceSpecs.length > 0) {
2142
+ if (!dryRun && rootResourceSpecs.length > 0) {
2353
2143
  const result = await uploadBundleResources(rootResourceSpecs, target);
2354
2144
  mediaUploaded += result.uploaded;
2355
2145
  if (result.urlMap.size > 0) {
@@ -2360,8 +2150,13 @@ export async function run(argv) {
2360
2150
 
2361
2151
  // Fallback: rewrite any leftover in-body image URLs (covers hand-
2362
2152
  // authored Hugo content where the exporter didn't declare resources).
2363
- if (!skipMedia && !dryRun) {
2364
- const imageMedia = findImageUrls(rootBody).map((src) => ({ src }));
2153
+ // `--skip-remote-media` filters out absolute URLs here so we only
2154
+ // rehost relative paths (the source site's own files).
2155
+ if (!dryRun) {
2156
+ const fallbackUrls = findImageUrls(rootBody).filter(
2157
+ (url) => !skipRemoteMedia || !isAbsoluteImportUrl(url),
2158
+ );
2159
+ const imageMedia = fallbackUrls.map((src) => ({ src }));
2365
2160
  const uploadResult = await uploadMediaList(
2366
2161
  imageMedia,
2367
2162
  target,
@@ -2380,7 +2175,6 @@ export async function run(argv) {
2380
2175
  target,
2381
2176
  siteConfig,
2382
2177
  sourceRootDir,
2383
- { skipUploads: skipMedia },
2384
2178
  );
2385
2179
  importedAttachments = attachmentResult.attachments;
2386
2180
  mediaUploaded += attachmentResult.uploaded;
@@ -2396,7 +2190,7 @@ export async function run(argv) {
2396
2190
  // These reference a `.md` artifact that holds the full body; the
2397
2191
  // normalizer fetches the bytes (local disk first, then remote URL)
2398
2192
  // and decodes them.
2399
- if (!skipMedia && !dryRun) {
2193
+ if (!dryRun) {
2400
2194
  for (const textEntry of rootTextAttachmentEntries) {
2401
2195
  const textAttachment = await normalizeTextAttachmentSpec(
2402
2196
  textEntry,
@@ -2496,7 +2290,7 @@ export async function run(argv) {
2496
2290
  }
2497
2291
 
2498
2292
  const replyResourceIds = [];
2499
- if (!skipMedia && replyResourceSpecs.length > 0) {
2293
+ if (replyResourceSpecs.length > 0) {
2500
2294
  const result = await uploadBundleResources(
2501
2295
  replyResourceSpecs,
2502
2296
  target,
@@ -2508,8 +2302,11 @@ export async function run(argv) {
2508
2302
  replyResourceIds.push(...result.mediaIds);
2509
2303
  }
2510
2304
 
2511
- if (!skipMedia) {
2512
- const imageMedia = findImageUrls(replyBody).map((src) => ({ src }));
2305
+ {
2306
+ const fallbackUrls = findImageUrls(replyBody).filter(
2307
+ (url) => !skipRemoteMedia || !isAbsoluteImportUrl(url),
2308
+ );
2309
+ const imageMedia = fallbackUrls.map((src) => ({ src }));
2513
2310
  const uploadResult = await uploadMediaList(
2514
2311
  imageMedia,
2515
2312
  target,
@@ -2527,7 +2324,6 @@ export async function run(argv) {
2527
2324
  target,
2528
2325
  siteConfig,
2529
2326
  sourceRootDir,
2530
- { skipUploads: skipMedia },
2531
2327
  );
2532
2328
  replyAttachments = attachmentResult.attachments;
2533
2329
  mediaUploaded += attachmentResult.uploaded;
@@ -2535,15 +2331,13 @@ export async function run(argv) {
2535
2331
  replyAttachments.push({ type: "media", mediaId });
2536
2332
  }
2537
2333
 
2538
- if (!skipMedia) {
2539
- for (const textEntry of replyTextAttachmentEntries) {
2540
- const textAttachment = await normalizeTextAttachmentSpec(
2541
- textEntry,
2542
- siteConfig,
2543
- sourceRootDir,
2544
- );
2545
- if (textAttachment) replyAttachments.push(textAttachment);
2546
- }
2334
+ for (const textEntry of replyTextAttachmentEntries) {
2335
+ const textAttachment = await normalizeTextAttachmentSpec(
2336
+ textEntry,
2337
+ siteConfig,
2338
+ sourceRootDir,
2339
+ );
2340
+ if (textAttachment) replyAttachments.push(textAttachment);
2547
2341
  }
2548
2342
 
2549
2343
  const replyMemberships = resolveCollectionMemberships(