@jant/core 0.3.46 → 0.3.48

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 (110) 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 +60 -267
  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-DU7dpJID.js +6 -0
  15. package/dist/{app-DB-P66E5.js → app-DdnIoX7y.js} +333 -191
  16. package/dist/client/.vite/manifest.json +2 -2
  17. package/dist/client/_assets/client-BoUn7xBo.css +2 -0
  18. package/dist/client/_assets/{client-auth-BLCUje4M.js → client-auth-Ce5WEAVS.js} +102 -49
  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/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
  27. package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
  28. package/src/client/components/jant-settings-general.ts +164 -22
  29. package/src/client/components/settings-types.ts +4 -6
  30. package/src/client-auth.ts +1 -1
  31. package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
  32. package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
  33. package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
  34. package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
  35. package/src/db/migrations/meta/0021_snapshot.json +2121 -0
  36. package/src/db/migrations/meta/_journal.json +7 -0
  37. package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
  38. package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
  39. package/src/db/migrations/pg/meta/_journal.json +7 -0
  40. package/src/db/pg/schema.ts +21 -26
  41. package/src/db/rehearsal-fixtures/demo-current.json +1 -1
  42. package/src/db/schema.ts +16 -20
  43. package/src/i18n/__tests__/middleware.test.ts +43 -1
  44. package/src/i18n/coverage.generated.ts +17 -0
  45. package/src/i18n/i18n.ts +18 -2
  46. package/src/i18n/index.ts +3 -0
  47. package/src/i18n/locales/settings/en.po +16 -11
  48. package/src/i18n/locales/settings/en.ts +1 -1
  49. package/src/i18n/locales/settings/zh-Hans.po +17 -12
  50. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  51. package/src/i18n/locales/settings/zh-Hant.po +16 -11
  52. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  53. package/src/i18n/locales.ts +84 -2
  54. package/src/i18n/middleware.ts +25 -16
  55. package/src/i18n/supported-locales.ts +153 -0
  56. package/src/lib/__tests__/csp-builder.test.ts +19 -2
  57. package/src/lib/__tests__/feed.test.ts +242 -1
  58. package/src/lib/__tests__/post-meta.test.ts +0 -1
  59. package/src/lib/__tests__/view.test.ts +0 -1
  60. package/src/lib/api-posts.ts +9 -7
  61. package/src/lib/csp-builder.ts +28 -10
  62. package/src/lib/feed.ts +153 -3
  63. package/src/middleware/__tests__/secure-headers.test.ts +89 -0
  64. package/src/middleware/auth.ts +1 -1
  65. package/src/middleware/secure-headers.ts +47 -1
  66. package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
  67. package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
  68. package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
  69. package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
  70. package/src/node/__tests__/cli-sql-export.test.ts +49 -0
  71. package/src/node/index.ts +1 -0
  72. package/src/preset.css +8 -2
  73. package/src/routes/api/__tests__/settings.test.ts +3 -2
  74. package/src/routes/api/github-sync.tsx +1 -1
  75. package/src/routes/api/settings.ts +4 -1
  76. package/src/routes/auth/signin.tsx +6 -0
  77. package/src/routes/pages/archive.tsx +4 -2
  78. package/src/services/__tests__/post.test.ts +19 -19
  79. package/src/services/__tests__/search.test.ts +0 -1
  80. package/src/services/__tests__/settings.test.ts +22 -3
  81. package/src/services/bootstrap.ts +7 -3
  82. package/src/services/collection.ts +3 -3
  83. package/src/services/export.ts +0 -3
  84. package/src/services/navigation.ts +0 -2
  85. package/src/services/path.ts +1 -38
  86. package/src/services/post.ts +32 -66
  87. package/src/services/search.ts +0 -6
  88. package/src/services/settings.ts +47 -6
  89. package/src/services/site-admin.ts +6 -1
  90. package/src/styles/ui.css +14 -25
  91. package/src/types/entities.ts +0 -1
  92. package/src/ui/color-themes.ts +1 -1
  93. package/src/ui/dash/settings/GeneralContent.tsx +17 -19
  94. package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
  95. package/src/ui/feed/NoteCard.tsx +1 -11
  96. package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
  97. package/src/ui/pages/PostPage.tsx +2 -0
  98. package/bin/commands/collections.js +0 -268
  99. package/bin/commands/media.js +0 -302
  100. package/bin/commands/posts.js +0 -262
  101. package/bin/commands/search.js +0 -53
  102. package/bin/commands/settings.js +0 -93
  103. package/bin/lib/http-api.js +0 -223
  104. package/bin/lib/media-upload.js +0 -206
  105. package/dist/app-CM7sb3xO.js +0 -5
  106. package/dist/client/_assets/client-DDs6NzB3.css +0 -2
  107. package/src/__tests__/bin/content-cli.test.ts +0 -179
  108. package/src/__tests__/bin/media-cli.test.ts +0 -192
  109. /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
  110. /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
  /**
@@ -1483,223 +1481,6 @@ function createRemoteTarget(apiUrl, token) {
1483
1481
  };
1484
1482
  }
1485
1483
 
1486
- async function createLocalTarget(env = process.env) {
1487
- const nodeDatabase = await openNodeDatabase(env);
1488
- const { createNodeCliRuntime, resolveConfig } = await loadNodeRuntime();
1489
- const bindings = nodeDatabase.bindings;
1490
- const runtime = await createNodeCliRuntime(bindings);
1491
- const allSettings = await runtime.services.settings.getAll();
1492
- const appConfig = resolveConfig(bindings, allSettings);
1493
- const summaryConfig = {
1494
- maxParagraphs: appConfig.summaryMaxParagraphs,
1495
- maxChars: appConfig.summaryMaxChars,
1496
- };
1497
-
1498
- return {
1499
- async close() {
1500
- await nodeDatabase.close();
1501
- },
1502
- async getSetupStatus() {
1503
- return runtime.services.settings.isOnboardingComplete();
1504
- },
1505
- async updateSettings(updates) {
1506
- await runtime.services.settings.setMany(updates);
1507
- return { settings: updates };
1508
- },
1509
- async updateImportSettings(updates) {
1510
- await runtime.services.settings.setMany(updates);
1511
- return { success: true };
1512
- },
1513
- async listNavItems() {
1514
- return runtime.services.navItems.list();
1515
- },
1516
- async createNavItem(data) {
1517
- return runtime.services.navItems.create(data);
1518
- },
1519
- async deleteNavItem(id) {
1520
- return runtime.services.navItems.delete(id);
1521
- },
1522
- async removeSiteAvatar() {
1523
- return runtime.services.settings.removeAvatar(runtime.storage);
1524
- },
1525
- async uploadSiteAvatar(data) {
1526
- if (!runtime.storage) {
1527
- throw new Error("Local import requires configured storage.");
1528
- }
1529
-
1530
- const avatarAsset = await readImportAsset({
1531
- sourceUrl: data.avatarUrl,
1532
- sourceFilePath: data.avatarFilePath,
1533
- });
1534
- if (!avatarAsset) {
1535
- throw new Error(`Failed to read site avatar: ${data.avatarUrl}`);
1536
- }
1537
-
1538
- let faviconIco;
1539
- if (data.faviconUrl || data.faviconFilePath) {
1540
- const faviconAsset = await readImportAsset({
1541
- sourceUrl: data.faviconUrl,
1542
- sourceFilePath: data.faviconFilePath,
1543
- mimeType: "image/x-icon",
1544
- originalName: "favicon.ico",
1545
- });
1546
- if (faviconAsset) {
1547
- faviconIco = toArrayBuffer(faviconAsset.bytes);
1548
- }
1549
- }
1550
-
1551
- let appleTouchIcon;
1552
- if (data.appleTouchUrl) {
1553
- const appleTouchAsset = await readImportAsset({
1554
- sourceUrl: data.appleTouchUrl,
1555
- sourceFilePath: data.appleTouchFilePath,
1556
- });
1557
- if (appleTouchAsset) {
1558
- appleTouchIcon = toArrayBuffer(appleTouchAsset.bytes);
1559
- }
1560
- }
1561
-
1562
- await runtime.services.settings.uploadAvatar(
1563
- {
1564
- file: createUploadFile(
1565
- avatarAsset.filename,
1566
- avatarAsset.contentType,
1567
- avatarAsset.bytes,
1568
- ),
1569
- faviconIco,
1570
- appleTouchIcon,
1571
- },
1572
- {
1573
- media: runtime.services.media,
1574
- storage: runtime.storage,
1575
- storageProvider: appConfig.storageDriver,
1576
- maxFileSizeMB: appConfig.uploadMaxFileSize,
1577
- },
1578
- );
1579
-
1580
- return { success: true };
1581
- },
1582
- async syncSiteAvatar(data) {
1583
- await this.removeSiteAvatar();
1584
- if (!data) {
1585
- return { success: true };
1586
- }
1587
- return this.uploadSiteAvatar(data);
1588
- },
1589
- async listCollections() {
1590
- return runtime.services.collections.list();
1591
- },
1592
- async listCollectionDirectoryItems() {
1593
- return runtime.services.collections.listDirectoryItems();
1594
- },
1595
- async createCollection(data) {
1596
- return runtime.services.collections.create(data);
1597
- },
1598
- async createCollectionDirectoryItem(data) {
1599
- return runtime.services.collections.createDirectoryItem(data);
1600
- },
1601
- async moveCollectionDirectoryItem(id, after, before) {
1602
- return runtime.services.collections.moveDirectoryItem(id, after, before);
1603
- },
1604
- async deleteCollectionDirectoryItem(id) {
1605
- return runtime.services.collections.deleteDirectoryItem(id);
1606
- },
1607
- async createPost(data) {
1608
- const { attachments, ...postData } = data;
1609
- return runtime.services.posts.createWithAttachments(
1610
- postData,
1611
- attachments,
1612
- {
1613
- media: runtime.services.media,
1614
- storage: runtime.storage,
1615
- storageDriver: appConfig.storageDriver,
1616
- maxFileSizeMB: appConfig.uploadMaxFileSize,
1617
- },
1618
- summaryConfig,
1619
- );
1620
- },
1621
- async createAlias(path, targetSlug) {
1622
- const post = await runtime.services.posts.getBySlug(targetSlug);
1623
- if (!post) {
1624
- throw new Error(`Post with slug "${targetSlug}" not found`);
1625
- }
1626
- return runtime.services.customUrls.create({
1627
- path,
1628
- targetType: "post",
1629
- targetId: post.id,
1630
- });
1631
- },
1632
- async uploadMedia(mediaSpec) {
1633
- if (!runtime.storage) {
1634
- throw new Error("Local import requires configured storage.");
1635
- }
1636
-
1637
- const asset = await readMediaSpecAsset(mediaSpec);
1638
- if (!asset) return null;
1639
-
1640
- const originalName =
1641
- mediaSpec.originalName ||
1642
- asset.filename ||
1643
- getFilenameFromUrl(mediaSpec.src) ||
1644
- "file";
1645
- const bytes = asset.bytes;
1646
- const { id, filename, storageKey } =
1647
- generateImportedStorageKey(originalName);
1648
- const mimeType =
1649
- mediaSpec.mimeType || asset.contentType || guessMimeType(originalName);
1650
- let posterKey;
1651
-
1652
- if (mediaSpec.poster) {
1653
- const posterAsset = await readMediaSpecAsset(mediaSpec, "poster");
1654
- if (posterAsset) {
1655
- const posterName = posterAsset.filename || "poster.webp";
1656
- const posterExt = extname(posterName) || ".webp";
1657
- posterKey = storageKey.replace(/(\.[^.]+)?$/, `-poster${posterExt}`);
1658
- await runtime.storage.put(posterKey, posterAsset.bytes, {
1659
- contentType: posterAsset.contentType || guessMimeType(posterName),
1660
- });
1661
- }
1662
- }
1663
-
1664
- await runtime.storage.put(storageKey, bytes, {
1665
- contentType: mimeType,
1666
- });
1667
-
1668
- const createdMedia = await runtime.services.media.create({
1669
- id,
1670
- filename,
1671
- originalName,
1672
- mimeType,
1673
- size: mediaSpec.size ?? bytes.byteLength,
1674
- storageKey,
1675
- provider: appConfig.storageDriver,
1676
- width: mediaSpec.width ?? undefined,
1677
- height: mediaSpec.height ?? undefined,
1678
- alt: mediaSpec.alt ?? undefined,
1679
- position: mediaSpec.position ?? undefined,
1680
- blurhash: mediaSpec.blurhash ?? undefined,
1681
- waveform: mediaSpec.waveform ?? undefined,
1682
- posterKey,
1683
- summary: mediaSpec.summary ?? undefined,
1684
- chars: mediaSpec.chars ?? undefined,
1685
- mediaKind: mediaSpec.kind ?? undefined,
1686
- });
1687
-
1688
- return {
1689
- id: createdMedia.id,
1690
- url: getMediaPublicUrl(
1691
- createdMedia.storageKey,
1692
- createdMedia.provider,
1693
- appConfig,
1694
- ),
1695
- };
1696
- },
1697
- async checkPostSlugAvailability(slug) {
1698
- return runtime.services.posts.checkSlugAvailability(slug);
1699
- },
1700
- };
1701
- }
1702
-
1703
1484
  /**
1704
1485
  * Walk `content/` and classify each `_index.md` / `index.md` bundle by its
1705
1486
  * front-matter `type`. Returns ordered root-post bundles (with child reply
@@ -1998,11 +1779,48 @@ export const __test__ = {
1998
1779
  buildPostPayloadFromBundle,
1999
1780
  };
2000
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
+
2001
1819
  export async function run(argv) {
2002
- const { values } = parseArgs({
1820
+ const { values, positionals } = parseArgs({
2003
1821
  args: argv,
1822
+ allowPositionals: true,
2004
1823
  options: {
2005
- url: { type: "string" },
2006
1824
  token: { type: "string" },
2007
1825
  path: { type: "string", default: "." },
2008
1826
  "dry-run": { type: "boolean", default: false },
@@ -2012,63 +1830,38 @@ export async function run(argv) {
2012
1830
  });
2013
1831
 
2014
1832
  if (values.help) {
2015
- console.log("Usage: jant site import [--url <url>] [options]");
2016
- console.log("");
2017
- console.log("Import a Hugo export directory or ZIP into a Jant instance.");
2018
- console.log("");
2019
- console.log("Modes:");
2020
- console.log(
2021
- " Local No --url; imports into the local Node database runtime",
2022
- );
2023
- console.log(
2024
- ` Remote --url requires ${CLI_API_TOKEN_ENV_VAR} or --token`,
2025
- );
2026
- console.log("");
2027
- console.log("Options:");
2028
- console.log(" --url Target remote Jant instance URL");
2029
- console.log(
2030
- " --path Path to export directory or ZIP file (default: .)",
2031
- );
2032
- console.log(" --dry-run Parse and validate without making API calls");
2033
- console.log(
2034
- " --skip-remote-media Skip uploading absolute-URL images found in body (relative paths and declared media still import)",
2035
- );
2036
- console.log("");
2037
- console.log(
2038
- "Import expects an empty target site and fails on slug or alias conflicts.",
2039
- );
2040
- console.log("");
2041
- console.log("Authentication:");
2042
- console.log(` Set ${CLI_API_TOKEN_ENV_VAR} env var (recommended):`);
2043
- console.log(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
2044
- console.log(" jant site import --url https://your-site.com");
2045
- console.log("");
2046
- console.log("Examples:");
2047
- console.log(" jant site import --path ./jant-site");
2048
- console.log(" jant site import --path ./jant-site-export.zip");
2049
- console.log("");
2050
- console.log("Compatibility alias: jant import-site");
1833
+ printImportUsage();
2051
1834
  process.exit(0);
2052
1835
  }
2053
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"];
2054
1852
  const token = getCliApiToken(process.env, values.token);
2055
- if (values.url && !token && !values["dry-run"]) {
1853
+ if (!token && !dryRun) {
2056
1854
  console.error(
2057
- `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)`,
2058
1856
  );
2059
1857
  console.error("");
2060
1858
  console.error(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
2061
1859
  process.exit(1);
2062
1860
  }
2063
1861
 
2064
- const apiUrl = values.url?.replace(/\/$/, "");
2065
- const dryRun = values["dry-run"];
1862
+ const apiUrl = url.replace(/\/$/, "");
2066
1863
  const skipRemoteMedia = values["skip-remote-media"];
2067
- const target = dryRun
2068
- ? null
2069
- : values.url
2070
- ? createRemoteTarget(apiUrl, token)
2071
- : await createLocalTarget(process.env);
1864
+ const target = dryRun ? null : createRemoteTarget(apiUrl, token);
2072
1865
 
2073
1866
  // 1. Read source — directory or ZIP
2074
1867
  const inputPath = resolve(process.cwd(), values.path);
@@ -2118,7 +1911,7 @@ export async function run(argv) {
2118
1911
  if (target) {
2119
1912
  const setupError = await getIncompleteSetupError(
2120
1913
  target,
2121
- values.url ? `Target site at ${apiUrl}` : "Local target site",
1914
+ `Target site at ${apiUrl}`,
2122
1915
  );
2123
1916
  if (setupError) {
2124
1917
  console.error("");
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, readdirSync } from "node:fs";
1
+ import { existsSync, readdirSync } from "node:fs";
2
2
  import { resolve, dirname } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { parseArgs } from "node:util";
@@ -10,12 +10,15 @@ import {
10
10
  applyPgBackfills,
11
11
  } from "../lib/migration-runner.js";
12
12
  import { loadNodeRuntime } from "../lib/load-node-runtime.js";
13
+ import { loadNodeEnvFile } from "../lib/node-env.js";
13
14
  import { openNodeSqlite, resolveDatabaseDialect } from "../lib/node-sqlite.js";
14
15
  import {
16
+ bootstrapCliRuntime,
15
17
  getCliRuntimeLabel,
16
- resolveCliRuntime,
17
18
  } from "../lib/runtime-target.js";
18
19
 
20
+ export { loadNodeEnvFile };
21
+
19
22
  export function isMigrationDebugEnabled(env = process.env) {
20
23
  return env.JANT_DEBUG_MIGRATE === "1";
21
24
  }
@@ -42,38 +45,6 @@ export function describeNodeDatabaseTarget(databaseUrl) {
42
45
  }
43
46
  }
44
47
 
45
- export function loadNodeEnvFile(envPath, env = process.env) {
46
- const result = {
47
- envPath,
48
- found: false,
49
- assignedKeys: [],
50
- skippedKeys: [],
51
- };
52
-
53
- try {
54
- const content = readFileSync(envPath, "utf8");
55
- result.found = true;
56
- for (const line of content.split("\n")) {
57
- const trimmed = line.trim();
58
- if (!trimmed || trimmed.startsWith("#")) continue;
59
- const eqIdx = trimmed.indexOf("=");
60
- if (eqIdx < 1) continue;
61
- const key = trimmed.slice(0, eqIdx).trim();
62
- const value = trimmed.slice(eqIdx + 1).trim();
63
- if (key in env) {
64
- result.skippedKeys.push(key);
65
- continue;
66
- }
67
- env[key] = value;
68
- result.assignedKeys.push(key);
69
- }
70
- } catch {
71
- // .env.node not found
72
- }
73
-
74
- return result;
75
- }
76
-
77
48
  function logMigrationDebug(message) {
78
49
  console.log(`[jant:migrate] ${message}`);
79
50
  }
@@ -222,7 +193,7 @@ export async function run(argv) {
222
193
  console.log(" --local Force local D1 instead of DATABASE_URL");
223
194
  console.log(" --remote Run against remote D1");
224
195
  console.log(
225
- " --node Force Node runtime (loads .env.node for DATABASE_URL)",
196
+ " --node Force Node runtime even if DATABASE_URL is unset",
226
197
  );
227
198
  console.log(
228
199
  " --config Wrangler config file (default: wrangler.toml)",
@@ -232,20 +203,20 @@ export async function run(argv) {
232
203
  console.log(" --persist-to Local D1 state directory override");
233
204
  console.log("");
234
205
  console.log(
235
- "If DATABASE_URL or DATA_DIR is set and no runtime flag is passed, this command uses the Node database runtime.",
206
+ "`.env.node` next to your project (or in packages/core/) is auto-loaded.",
207
+ );
208
+ console.log(
209
+ "If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
236
210
  );
211
+ console.log("this command uses the Node database runtime.");
237
212
  process.exit(0);
238
213
  }
239
214
 
240
- // --node: load .env.node and force node runtime
241
- let nodeEnvLoadResult;
242
- if (values.node) {
243
- const __dir = dirname(fileURLToPath(import.meta.url));
244
- const envPath = resolve(__dir, "../../.env.node");
245
- nodeEnvLoadResult = loadNodeEnvFile(envPath);
246
- }
247
-
248
- const runtime = resolveCliRuntime(values);
215
+ // bootstrapCliRuntime auto-loads `.env.node` (so DATABASE_URL/DATA_DIR
216
+ // resolve without sourcing the file) and prints a one-line banner with
217
+ // the chosen target.
218
+ const { runtime, envLoad } = bootstrapCliRuntime(values);
219
+ const nodeEnvLoadResult = envLoad;
249
220
  const debugMigrate = isMigrationDebugEnabled();
250
221
  const databaseUrl = process.env.DATABASE_URL ?? "";
251
222
  const databaseDialect =
@@ -255,30 +226,26 @@ export async function run(argv) {
255
226
 
256
227
  if (debugMigrate) {
257
228
  logMigrationDebug(`cli.runtime=${runtime}`);
258
- if (values.node) {
259
- const databaseUrlSource = nodeEnvLoadResult?.assignedKeys.includes(
260
- "DATABASE_URL",
261
- )
262
- ? ".env.node"
263
- : process.env.DATABASE_URL
264
- ? "process.env"
265
- : "<unset>";
266
- const dataDirSource = nodeEnvLoadResult?.assignedKeys.includes("DATA_DIR")
267
- ? ".env.node"
268
- : process.env.DATA_DIR
269
- ? "process.env"
270
- : "<unset>";
271
- const envPath = nodeEnvLoadResult?.envPath ?? "<unknown>";
272
- const envState = nodeEnvLoadResult?.found ? "loaded" : "missing";
273
- const skippedKeys = nodeEnvLoadResult?.skippedKeys.join(", ") || "<none>";
274
- logMigrationDebug(`cli.node_env.path=${envPath}`);
275
- logMigrationDebug(`cli.node_env.state=${envState}`);
276
- logMigrationDebug(`cli.node_env.skipped_keys=${skippedKeys}`);
277
- logMigrationDebug(
278
- `cli.node_env.database_url_source=${databaseUrlSource}`,
279
- );
280
- logMigrationDebug(`cli.node_env.data_dir_source=${dataDirSource}`);
281
- }
229
+ const databaseUrlSource = nodeEnvLoadResult?.assignedKeys.includes(
230
+ "DATABASE_URL",
231
+ )
232
+ ? ".env.node"
233
+ : process.env.DATABASE_URL
234
+ ? "process.env"
235
+ : "<unset>";
236
+ const dataDirSource = nodeEnvLoadResult?.assignedKeys.includes("DATA_DIR")
237
+ ? ".env.node"
238
+ : process.env.DATA_DIR
239
+ ? "process.env"
240
+ : "<unset>";
241
+ const envPath = nodeEnvLoadResult?.envPath ?? "<unknown>";
242
+ const envState = nodeEnvLoadResult?.found ? "loaded" : "missing";
243
+ const skippedKeys = nodeEnvLoadResult?.skippedKeys.join(", ") || "<none>";
244
+ logMigrationDebug(`cli.node_env.path=${envPath}`);
245
+ logMigrationDebug(`cli.node_env.state=${envState}`);
246
+ logMigrationDebug(`cli.node_env.skipped_keys=${skippedKeys}`);
247
+ logMigrationDebug(`cli.node_env.database_url_source=${databaseUrlSource}`);
248
+ logMigrationDebug(`cli.node_env.data_dir_source=${dataDirSource}`);
282
249
 
283
250
  if (runtime === "node") {
284
251
  logMigrationDebug(`cli.node.dialect=${databaseDialect ?? "<unset>"}`);