@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
@@ -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>"}`);
@@ -4,8 +4,8 @@ import { executeD1, queryD1 } from "../lib/d1-query.js";
4
4
  import { openNodeDatabase } from "../lib/node-database.js";
5
5
  import { loadNodeRuntime } from "../lib/load-node-runtime.js";
6
6
  import {
7
+ bootstrapCliRuntime,
7
8
  getCliRuntimeLabel,
8
- resolveCliRuntime,
9
9
  } from "../lib/runtime-target.js";
10
10
  import { resolveCliSite } from "../lib/site-selection.js";
11
11
 
@@ -18,6 +18,7 @@ export async function run(argv) {
18
18
  env: { type: "string" },
19
19
  host: { type: "string" },
20
20
  local: { type: "boolean", default: false },
21
+ node: { type: "boolean", default: false },
21
22
  "path-prefix": { type: "string" },
22
23
  "persist-to": { type: "string" },
23
24
  remote: { type: "boolean", default: false },
@@ -28,13 +29,14 @@ export async function run(argv) {
28
29
  });
29
30
 
30
31
  if (values.help) {
31
- console.log("Usage: jant reset-password [--local | --remote]");
32
+ console.log("Usage: jant reset-password [--local | --remote | --node]");
32
33
  console.log("");
33
34
  console.log("Generate a password reset token (expires in 15 minutes).");
34
35
  console.log("");
35
36
  console.log("Options:");
36
37
  console.log(" --local Force local D1 instead of DATABASE_URL");
37
38
  console.log(" --remote Run against remote D1 database (default: local)");
39
+ console.log(" --node Force Node runtime even if DATABASE_URL is unset");
38
40
  console.log(" --site Target site id");
39
41
  console.log(" --host Target site host");
40
42
  console.log(" --url Target site URL");
@@ -45,12 +47,16 @@ export async function run(argv) {
45
47
  console.log(" --persist-to Local D1 state directory override");
46
48
  console.log("");
47
49
  console.log(
48
- "If DATABASE_URL or DATA_DIR is set and no runtime flag is passed, this command uses the Node database runtime.",
50
+ "`.env.node` next to your project (or in packages/core/) is auto-loaded.",
49
51
  );
52
+ console.log(
53
+ "If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
54
+ );
55
+ console.log("this command uses the Node database runtime.");
50
56
  process.exit(0);
51
57
  }
52
58
 
53
- const runtime = resolveCliRuntime(values);
59
+ const { runtime } = bootstrapCliRuntime(values);
54
60
 
55
61
  const token = randomBytes(32).toString("hex");
56
62
  const hash = createHash("sha256").update(token).digest("hex");
@@ -6,22 +6,8 @@ import {
6
6
  CLI_API_TOKEN_ENV_VAR,
7
7
  getCliApiToken,
8
8
  } from "../../lib/cli-api-token.js";
9
- import { openNodeDatabase } from "../../lib/node-database.js";
10
- import { loadNodeRuntime } from "../../lib/load-node-runtime.js";
11
9
  import { pullSiteExportZipBytes } from "../../lib/site-pull-media.js";
12
10
 
13
- function describeLocalExportSource(input) {
14
- if (input.siteUrl) {
15
- return input.siteUrl;
16
- }
17
-
18
- if (input.siteDomain?.host) {
19
- return `https://${input.siteDomain.host}${input.siteDomain.pathPrefix || ""}`;
20
- }
21
-
22
- return `site "${input.site.key}"`;
23
- }
24
-
25
11
  async function exportRemoteSite(url, token) {
26
12
  const response = await fetch(`${url.replace(/\/$/, "")}/api/export/hugo`, {
27
13
  method: "POST",
@@ -83,189 +69,46 @@ function logPullProgress(event) {
83
69
  }
84
70
  }
85
71
 
86
- function getStorageKeyFromUrl(url, appConfig) {
87
- try {
88
- const resolvedUrl = new URL(url, appConfig.siteUrl);
89
- let pathname = resolvedUrl.pathname;
90
- const publicPathPrefixes = [
91
- appConfig.r2PublicUrl,
92
- appConfig.s3PublicUrl,
93
- appConfig.localPublicUrl,
94
- ]
95
- .filter(Boolean)
96
- .map((value) => {
97
- try {
98
- const parsed = new URL(value);
99
- return parsed.pathname.replace(/\/+$/, "");
100
- } catch {
101
- return "";
102
- }
103
- })
104
- .filter(Boolean);
105
-
106
- for (const prefix of publicPathPrefixes) {
107
- if (pathname.startsWith(`${prefix}/`)) {
108
- pathname = pathname.slice(prefix.length + 1);
109
- break;
110
- }
111
- if (pathname === prefix) {
112
- pathname = "";
113
- break;
114
- }
115
- }
116
-
117
- const sitePathPrefix = appConfig.sitePathPrefix || "";
118
- if (sitePathPrefix && pathname.startsWith(`${sitePathPrefix}/`)) {
119
- pathname = pathname.slice(sitePathPrefix.length + 1);
120
- } else {
121
- pathname = pathname.replace(/^\/+/, "");
122
- }
123
-
124
- if (!pathname.startsWith("media/") && !pathname.startsWith("favicon/")) {
125
- return null;
126
- }
127
-
128
- return pathname;
129
- } catch {
130
- return null;
131
- }
132
- }
133
-
134
- async function readStorageBody(body) {
135
- const reader = body.getReader();
136
- const chunks = [];
137
- let totalLength = 0;
138
-
139
- for (;;) {
140
- const { done, value } = await reader.read();
141
- if (done) break;
142
- chunks.push(value);
143
- totalLength += value.length;
144
- }
145
-
146
- const bytes = new Uint8Array(totalLength);
147
- let offset = 0;
148
- for (const chunk of chunks) {
149
- bytes.set(chunk, offset);
150
- offset += chunk.length;
151
- }
152
-
153
- return bytes;
154
- }
155
-
156
- function createLocalAssetLoader(storage, appConfig) {
157
- if (!storage) {
158
- return null;
159
- }
160
-
161
- return async ({ resolvedUrl }) => {
162
- const storageKey = getStorageKeyFromUrl(resolvedUrl, appConfig);
163
- if (!storageKey) {
164
- return null;
165
- }
166
-
167
- const object = await storage.get(storageKey);
168
- if (!object?.body) {
169
- return null;
170
- }
171
-
172
- return {
173
- bytes: await readStorageBody(object.body),
174
- contentType: object.contentType || "",
175
- };
176
- };
177
- }
178
-
179
- async function exportLocalSite(env = process.env) {
180
- const nodeDatabase = await openNodeDatabase(env);
181
-
182
- try {
183
- const {
184
- createExportService,
185
- createNodeCliRuntime,
186
- resolveConfig,
187
- buildThemeStyle,
188
- BUILTIN_COLOR_THEMES,
189
- BUILTIN_FONT_THEMES,
190
- getCjkSerifCssVariables,
191
- getFontThemeCssVariables,
192
- } = await loadNodeRuntime();
193
- const runtime = await createNodeCliRuntime(nodeDatabase.bindings);
194
- const allSettings = await runtime.services.settings.getAll();
195
- const navItems = await runtime.services.navItems.list();
196
- const appConfig = resolveConfig(nodeDatabase.bindings, allSettings);
197
- const activeTheme = BUILTIN_COLOR_THEMES.find(
198
- (theme) => theme.id === (appConfig.themeId || appConfig.defaultThemeId),
199
- );
200
- const fontTheme = appConfig.fontThemeId
201
- ? BUILTIN_FONT_THEMES.find((theme) => theme.id === appConfig.fontThemeId)
202
- : undefined;
203
- const fontOverrides = {
204
- ...getCjkSerifCssVariables(appConfig.siteLanguage),
205
- ...(fontTheme ? getFontThemeCssVariables(fontTheme) : {}),
206
- };
207
- const themeCss = buildThemeStyle(
208
- activeTheme,
209
- appConfig.themeMode,
210
- fontOverrides,
211
- );
212
- const appleTouchKey = allSettings.SITE_FAVICON_APPLE_TOUCH || "";
213
- const exportService = createExportService(
214
- runtime.services,
215
- {
216
- siteName: appConfig.siteName,
217
- siteUrl: appConfig.siteUrl,
218
- siteDescription: appConfig.siteDescription,
219
- siteLanguage: appConfig.siteLanguage,
220
- showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
221
- homeDefaultView: appConfig.homeDefaultView,
222
- mainRssFeed: appConfig.mainRssFeed,
223
- siteFooter: appConfig.siteFooter,
224
- showHeaderAvatar: appConfig.showHeaderAvatar,
225
- siteAvatarUrl: appConfig.siteAvatarUrl,
226
- faviconIcoBase64: allSettings.SITE_FAVICON_ICO || undefined,
227
- appleTouchIconStorageKey: appleTouchKey || undefined,
228
- faviconVersion: appConfig.faviconVersion,
229
- themeId: appConfig.themeId,
230
- defaultThemeId: appConfig.defaultThemeId,
231
- fontThemeId: appConfig.fontThemeId,
232
- themeMode: appConfig.themeMode,
233
- noindex: appConfig.noindex,
234
- themeCss,
235
- customCss: appConfig.customCSS,
236
- r2PublicUrl: appConfig.r2PublicUrl,
237
- s3PublicUrl: appConfig.s3PublicUrl,
238
- localPublicUrl: appConfig.localPublicUrl,
239
- imageTransformUrl: appConfig.imageTransformUrl,
240
- sitePathPrefix: appConfig.sitePathPrefix,
241
- navItems,
242
- pageSize: appConfig.pageSize,
243
- archivePageSize: appConfig.archivePageSize,
244
- rssFeedLimit: appConfig.rssFeedLimit,
245
- },
246
- {
247
- storage: runtime.storage,
248
- },
249
- );
250
-
251
- return {
252
- zip: await exportService.generateHugoSite(),
253
- assetLoader: createLocalAssetLoader(runtime.storage, appConfig),
254
- source: describeLocalExportSource({
255
- site: runtime.currentSite,
256
- siteDomain: runtime.currentSiteDomain,
257
- siteUrl: appConfig.siteUrl,
258
- }),
259
- };
260
- } finally {
261
- await nodeDatabase.close();
262
- }
72
+ function printUsage() {
73
+ console.log("Usage: jant site export <url> [options]");
74
+ console.log("");
75
+ console.log("Export a Jant site as a Hugo ZIP archive or directory.");
76
+ console.log("");
77
+ console.log("Arguments:");
78
+ console.log(" <url> Jant site URL (required)");
79
+ console.log("");
80
+ console.log("Options:");
81
+ console.log(
82
+ " --output, -o Output ZIP path (default: jant-site-export.zip)",
83
+ );
84
+ console.log(
85
+ " --directory, -d Export directly to a directory for hugo serve/debugging",
86
+ );
87
+ console.log(
88
+ " --pull-media Download referenced media into static/media/ (default: on)",
89
+ );
90
+ console.log(" --no-pull-media Skip the media pull and keep original URLs");
91
+ console.log(" --token API token (overrides JANT_API_TOKEN)");
92
+ console.log("");
93
+ console.log("Authentication:");
94
+ console.log(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
95
+ console.log(" jant site export https://your-site.example");
96
+ console.log("");
97
+ console.log("Examples:");
98
+ console.log(
99
+ " jant site export https://your-site.example -o ./export.zip",
100
+ );
101
+ console.log(
102
+ " jant site export https://your-site.example -d ./jant-site && cd ./jant-site && hugo serve",
103
+ );
263
104
  }
264
105
 
265
106
  export async function run(argv) {
266
107
  const noPullMedia = argv.includes("--no-pull-media");
267
- const { values } = parseArgs({
268
- args: argv.filter((arg) => arg !== "--no-pull-media"),
108
+ const filteredArgv = argv.filter((arg) => arg !== "--no-pull-media");
109
+ const { values, positionals } = parseArgs({
110
+ args: filteredArgv,
111
+ allowPositionals: true,
269
112
  options: {
270
113
  directory: {
271
114
  type: "string",
@@ -279,49 +122,28 @@ export async function run(argv) {
279
122
  default: "jant-site-export.zip",
280
123
  },
281
124
  token: { type: "string" },
282
- url: { type: "string" },
283
125
  },
284
126
  });
285
127
 
286
128
  if (values.help) {
287
- console.log("Usage: jant site export [--url <url>] [options]");
288
- console.log("");
289
- console.log("Export a Jant site as a Hugo ZIP archive or directory.");
290
- console.log("");
291
- console.log("Modes:");
292
- console.log(
293
- " Local No --url; exports from the local Node database runtime",
294
- );
295
- console.log(
296
- ` Remote --url requires ${CLI_API_TOKEN_ENV_VAR} or --token`,
297
- );
298
- console.log("");
299
- console.log("Options:");
300
- console.log(" --url Remote Jant site URL");
301
- console.log(
302
- " --output, -o Output ZIP path (default: jant-site-export.zip)",
303
- );
304
- console.log(
305
- " --directory, -d Export directly to a directory for hugo serve/debugging",
306
- );
307
- console.log(
308
- " --pull-media Download referenced media into static/media/ (default: on)",
309
- );
310
- console.log(
311
- " --no-pull-media Skip the media pull and keep original URLs",
312
- );
313
- console.log(" --token API token for remote export");
314
- console.log("");
315
- console.log("Authentication:");
316
- console.log(` export ${CLI_API_TOKEN_ENV_VAR}=jnt_your_token`);
317
- console.log(" jant site export --url https://your-site.com");
318
- console.log("");
319
- console.log("Examples:");
320
- console.log(" jant site export --directory ./jant-site");
321
- console.log(" cd ./jant-site && hugo serve");
129
+ printUsage();
322
130
  process.exit(0);
323
131
  }
324
132
 
133
+ const url = positionals[0];
134
+ if (!url) {
135
+ console.error("Error: site URL is required");
136
+ console.error("");
137
+ printUsage();
138
+ process.exit(1);
139
+ }
140
+ if (positionals.length > 1) {
141
+ console.error(
142
+ `Error: unexpected extra arguments: ${positionals.slice(1).join(" ")}`,
143
+ );
144
+ process.exit(1);
145
+ }
146
+
325
147
  if (values.directory && values.output !== "jant-site-export.zip") {
326
148
  console.error("Error: use either --output or --directory, not both");
327
149
  process.exit(1);
@@ -334,40 +156,29 @@ export async function run(argv) {
334
156
  const token = getCliApiToken(process.env, values.token);
335
157
  const pullMedia = values["pull-media"] ?? !noPullMedia;
336
158
 
337
- if (values.url && !token) {
159
+ if (!token) {
338
160
  console.error(
339
- `Error: remote export requires ${CLI_API_TOKEN_ENV_VAR} or --token`,
161
+ `Error: site export requires ${CLI_API_TOKEN_ENV_VAR} or --token`,
340
162
  );
341
163
  process.exit(1);
342
164
  }
343
165
 
344
- console.log(
345
- values.url
346
- ? `Exporting site from ${values.url}...`
347
- : "Exporting site from the local runtime...",
348
- );
166
+ console.log(`Exporting site from ${url}...`);
349
167
 
350
- const exported = values.url
351
- ? {
352
- zip: await exportRemoteSite(values.url, token),
353
- assetLoader: null,
354
- source: values.url,
355
- }
356
- : await exportLocalSite(process.env);
357
- let zip = exported.zip;
168
+ const zipBytes = await exportRemoteSite(url, token);
169
+ let zip = zipBytes;
358
170
  let pullStats = null;
359
171
 
360
172
  if (pullMedia) {
361
173
  console.log("Preparing pull-media export ZIP...");
362
174
  const pulled = await pullSiteExportZipBytes(zip, {
363
- assetLoader: exported.assetLoader,
175
+ assetLoader: null,
364
176
  logger: logPullProgress,
365
177
  });
366
178
  zip = pulled.zipBytes;
367
179
  pullStats = pulled.stats;
368
180
  }
369
181
 
370
- const source = exported.source;
371
182
  if (outputDirectory) {
372
183
  let existingEntries = [];
373
184
  try {
@@ -393,12 +204,12 @@ export async function run(argv) {
393
204
  mkdirSync(dirname(fullPath), { recursive: true });
394
205
  writeFileSync(fullPath, Buffer.from(bytes));
395
206
  }
396
- console.log(`Exported site from ${source} to ${values.directory}`);
207
+ console.log(`Exported site from ${url} to ${values.directory}`);
397
208
  console.log(`Preview with: cd ${values.directory} && hugo serve`);
398
209
  } else {
399
210
  console.log(`Writing ${values.output}...`);
400
211
  writeFileSync(output, Buffer.from(zip));
401
- console.log(`Exported site from ${source} to ${values.output}`);
212
+ console.log(`Exported site from ${url} to ${values.output}`);
402
213
  }
403
214
 
404
215
  if (pullStats) {