dineway 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.
Files changed (96) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +89 -0
  3. package/dist/adapters-BlzWJG82.d.mts +106 -0
  4. package/dist/apply-CAPvMfoU.mjs +1339 -0
  5. package/dist/astro/index.d.mts +50 -0
  6. package/dist/astro/index.mjs +1326 -0
  7. package/dist/astro/middleware/auth.d.mts +30 -0
  8. package/dist/astro/middleware/auth.mjs +708 -0
  9. package/dist/astro/middleware/redirect.d.mts +21 -0
  10. package/dist/astro/middleware/redirect.mjs +62 -0
  11. package/dist/astro/middleware/request-context.d.mts +17 -0
  12. package/dist/astro/middleware/request-context.mjs +1371 -0
  13. package/dist/astro/middleware/setup.d.mts +19 -0
  14. package/dist/astro/middleware/setup.mjs +46 -0
  15. package/dist/astro/middleware.d.mts +12 -0
  16. package/dist/astro/middleware.mjs +1716 -0
  17. package/dist/astro/types.d.mts +269 -0
  18. package/dist/astro/types.mjs +1 -0
  19. package/dist/base64-F8-DUraK.mjs +58 -0
  20. package/dist/byline-DeWCMU_i.mjs +234 -0
  21. package/dist/bylines-DyqBV9EQ.mjs +137 -0
  22. package/dist/chunk-ClPoSABd.mjs +21 -0
  23. package/dist/cli/index.d.mts +1 -0
  24. package/dist/cli/index.mjs +3987 -0
  25. package/dist/client/external-auth-headers.d.mts +38 -0
  26. package/dist/client/external-auth-headers.mjs +101 -0
  27. package/dist/client/index.d.mts +397 -0
  28. package/dist/client/index.mjs +345 -0
  29. package/dist/config-Cq8H0SfX.mjs +46 -0
  30. package/dist/connection-C9pxzuag.mjs +52 -0
  31. package/dist/content-zSgdNmnt.mjs +836 -0
  32. package/dist/db/index.d.mts +4 -0
  33. package/dist/db/index.mjs +62 -0
  34. package/dist/db/libsql.d.mts +10 -0
  35. package/dist/db/libsql.mjs +21 -0
  36. package/dist/db/postgres.d.mts +10 -0
  37. package/dist/db/postgres.mjs +29 -0
  38. package/dist/db/sqlite.d.mts +10 -0
  39. package/dist/db/sqlite.mjs +15 -0
  40. package/dist/default-WYlzADZL.mjs +80 -0
  41. package/dist/dialect-helpers-B9uSp2GJ.mjs +89 -0
  42. package/dist/error-DrxtnGPg.mjs +26 -0
  43. package/dist/index-C-jx21qs.d.mts +4771 -0
  44. package/dist/index.d.mts +16 -0
  45. package/dist/index.mjs +30 -0
  46. package/dist/load-C6FCD1FU.mjs +27 -0
  47. package/dist/loader-qKmo0wAY.mjs +446 -0
  48. package/dist/manifest-schema-CTSEyIJ3.mjs +186 -0
  49. package/dist/media/index.d.mts +25 -0
  50. package/dist/media/index.mjs +54 -0
  51. package/dist/media/local-runtime.d.mts +38 -0
  52. package/dist/media/local-runtime.mjs +132 -0
  53. package/dist/media-DMTr80Gv.mjs +199 -0
  54. package/dist/mode-BlyYtIFO.mjs +22 -0
  55. package/dist/page/index.d.mts +148 -0
  56. package/dist/page/index.mjs +419 -0
  57. package/dist/placeholder-B3knXwNc.mjs +267 -0
  58. package/dist/placeholder-bOx1xCTY.d.mts +283 -0
  59. package/dist/plugin-utils.d.mts +57 -0
  60. package/dist/plugin-utils.mjs +77 -0
  61. package/dist/plugins/adapt-sandbox-entry.d.mts +21 -0
  62. package/dist/plugins/adapt-sandbox-entry.mjs +112 -0
  63. package/dist/query-BiaPl_g2.mjs +459 -0
  64. package/dist/redirect-JPqLAbxa.mjs +328 -0
  65. package/dist/registry-DSd1GWB8.mjs +851 -0
  66. package/dist/request-context.d.mts +49 -0
  67. package/dist/request-context.mjs +42 -0
  68. package/dist/runner-B5l1JfOj.d.mts +26 -0
  69. package/dist/runner-BGUGywgG.mjs +1529 -0
  70. package/dist/runtime.d.mts +25 -0
  71. package/dist/runtime.mjs +41 -0
  72. package/dist/search-BNruJHDL.mjs +11054 -0
  73. package/dist/seed/index.d.mts +3 -0
  74. package/dist/seed/index.mjs +15 -0
  75. package/dist/seo/index.d.mts +69 -0
  76. package/dist/seo/index.mjs +69 -0
  77. package/dist/storage/local.d.mts +38 -0
  78. package/dist/storage/local.mjs +165 -0
  79. package/dist/storage/s3.d.mts +31 -0
  80. package/dist/storage/s3.mjs +174 -0
  81. package/dist/tokens-4vgYuXsZ.mjs +170 -0
  82. package/dist/transport-C5FYnid7.mjs +417 -0
  83. package/dist/transport-gIL-e43D.d.mts +41 -0
  84. package/dist/types-BawVha09.mjs +30 -0
  85. package/dist/types-BgQeVaPj.d.mts +192 -0
  86. package/dist/types-CLLdsG3g.d.mts +103 -0
  87. package/dist/types-D38djUXv.d.mts +1196 -0
  88. package/dist/types-DShnjzb6.mjs +15 -0
  89. package/dist/types-DkvMXalq.d.mts +425 -0
  90. package/dist/types-DuNbGKjF.mjs +74 -0
  91. package/dist/types-ju-_ORz7.d.mts +182 -0
  92. package/dist/validate-CXnRKfJK.mjs +327 -0
  93. package/dist/validate-CqRJb_xU.mjs +96 -0
  94. package/dist/validate-DVKJJ-M_.d.mts +377 -0
  95. package/locals.d.ts +47 -0
  96. package/package.json +313 -0
@@ -0,0 +1,3987 @@
1
+ #!/usr/bin/env node
2
+ import { t as __exportAll } from "../chunk-ClPoSABd.mjs";
3
+ import { n as createDatabase } from "../connection-C9pxzuag.mjs";
4
+ import { s as listTablesLike } from "../dialect-helpers-B9uSp2GJ.mjs";
5
+ import { r as runMigrations, t as getMigrationStatus } from "../runner-BGUGywgG.mjs";
6
+ import { t as ContentRepository } from "../content-zSgdNmnt.mjs";
7
+ import { i as encodeBase64url } from "../base64-F8-DUraK.mjs";
8
+ import "../types-BawVha09.mjs";
9
+ import { t as MediaRepository } from "../media-DMTr80Gv.mjs";
10
+ import { f as OptionsRepository, p as TaxonomyRepository, t as applySeed } from "../apply-CAPvMfoU.mjs";
11
+ import { n as SchemaRegistry } from "../registry-DSd1GWB8.mjs";
12
+ import "../redirect-JPqLAbxa.mjs";
13
+ import "../byline-DeWCMU_i.mjs";
14
+ import { r as isI18nEnabled } from "../config-Cq8H0SfX.mjs";
15
+ import "../loader-qKmo0wAY.mjs";
16
+ import { i as pluginManifestSchema } from "../manifest-schema-CTSEyIJ3.mjs";
17
+ import { t as validateSeed } from "../validate-CXnRKfJK.mjs";
18
+ import { LocalStorage } from "../storage/local.mjs";
19
+ import { createHeaderAwareFetch, customHeadersInterceptor, isRedirectResponse, resolveCustomHeaders } from "../client/external-auth-headers.mjs";
20
+ import { DinewayClient } from "../client/index.mjs";
21
+ import { imageSize } from "image-size";
22
+ import { createGzipDecoder, unpackTar } from "modern-tar";
23
+ import { createHash } from "node:crypto";
24
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
25
+ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
26
+ import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from "node:fs/promises";
27
+ import { defineCommand, runCommand, runMain } from "citty";
28
+ import consola, { consola as consola$1 } from "consola";
29
+ import pc from "picocolors";
30
+ import { homedir } from "node:os";
31
+ import { spawn } from "node:child_process";
32
+ import { pipeline } from "node:stream/promises";
33
+ import { packTar } from "modern-tar/fs";
34
+
35
+ //#region src/cli/commands/auth.ts
36
+ /**
37
+ * Auth CLI commands
38
+ */
39
+ /**
40
+ * Generate a cryptographically secure auth secret
41
+ */
42
+ function generateAuthSecret() {
43
+ const bytes = new Uint8Array(32);
44
+ crypto.getRandomValues(bytes);
45
+ return encodeBase64url(bytes);
46
+ }
47
+ const secretCommand = defineCommand({
48
+ meta: {
49
+ name: "secret",
50
+ description: "Generate a secure auth secret"
51
+ },
52
+ run() {
53
+ const secret = generateAuthSecret();
54
+ consola$1.log("");
55
+ consola$1.log(pc.bold("Generated auth secret:"));
56
+ consola$1.log("");
57
+ consola$1.log(` ${pc.cyan("DINEWAY_AUTH_SECRET")}=${pc.green(secret)}`);
58
+ consola$1.log("");
59
+ consola$1.log(pc.dim("Add this to your environment variables."));
60
+ consola$1.log("");
61
+ }
62
+ });
63
+ const authCommand = defineCommand({
64
+ meta: {
65
+ name: "auth",
66
+ description: "Authentication utilities"
67
+ },
68
+ subCommands: { secret: secretCommand }
69
+ });
70
+
71
+ //#endregion
72
+ //#region src/cli/credentials.ts
73
+ /**
74
+ * Credential storage for CLI auth tokens.
75
+ *
76
+ * Stores OAuth tokens in ~/.config/dineway/auth.json.
77
+ * Remote URLs are keyed by origin, local dev by project path.
78
+ */
79
+ function getConfigDir() {
80
+ const xdg = process.env["XDG_CONFIG_HOME"];
81
+ if (xdg) return join(xdg, "dineway");
82
+ return join(homedir(), ".config", "dineway");
83
+ }
84
+ function getCredentialPath() {
85
+ return join(getConfigDir(), "auth.json");
86
+ }
87
+ /**
88
+ * Resolve the credential key for a given URL.
89
+ *
90
+ * Remote URLs are keyed by origin (e.g. "https://my-site.pages.dev").
91
+ * Local dev instances are keyed by project path (e.g. "path:/Users/matt/sites/blog").
92
+ */
93
+ function resolveCredentialKey(baseUrl) {
94
+ try {
95
+ const url = new URL(baseUrl);
96
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]") {
97
+ const projectPath = findProjectRoot(process.cwd());
98
+ if (projectPath) return `path:${projectPath}`;
99
+ return url.origin;
100
+ }
101
+ return url.origin;
102
+ } catch {
103
+ return baseUrl;
104
+ }
105
+ }
106
+ /**
107
+ * Walk up from cwd to find the project root (directory containing astro.config.*).
108
+ */
109
+ function findProjectRoot(from) {
110
+ let dir = resolve(from);
111
+ const root = resolve("/");
112
+ while (dir !== root) {
113
+ for (const name of [
114
+ "astro.config.ts",
115
+ "astro.config.mts",
116
+ "astro.config.js",
117
+ "astro.config.mjs"
118
+ ]) if (existsSync(join(dir, name))) return dir;
119
+ const parent = resolve(dir, "..");
120
+ if (parent === dir) break;
121
+ dir = parent;
122
+ }
123
+ return null;
124
+ }
125
+ function readStore() {
126
+ const path = getCredentialPath();
127
+ try {
128
+ if (existsSync(path)) {
129
+ const content = readFileSync(path, "utf-8");
130
+ return JSON.parse(content);
131
+ }
132
+ } catch {}
133
+ return {};
134
+ }
135
+ function writeStore(store) {
136
+ mkdirSync(getConfigDir(), { recursive: true });
137
+ writeFileSync(getCredentialPath(), JSON.stringify(store, null, " "), {
138
+ encoding: "utf-8",
139
+ mode: 384
140
+ });
141
+ }
142
+ /**
143
+ * Get stored credentials for a URL.
144
+ */
145
+ function getCredentials(baseUrl) {
146
+ const key = resolveCredentialKey(baseUrl);
147
+ const cred = readStore()[key];
148
+ if (!cred || !("accessToken" in cred)) return null;
149
+ return cred;
150
+ }
151
+ /**
152
+ * Save credentials for a URL.
153
+ */
154
+ function saveCredentials(baseUrl, cred) {
155
+ const key = resolveCredentialKey(baseUrl);
156
+ const store = readStore();
157
+ store[key] = cred;
158
+ writeStore(store);
159
+ }
160
+ /**
161
+ * Remove credentials for a URL.
162
+ */
163
+ function removeCredentials(baseUrl) {
164
+ const key = resolveCredentialKey(baseUrl);
165
+ const store = readStore();
166
+ if (key in store) {
167
+ delete store[key];
168
+ writeStore(store);
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+ function marketplaceKey(registryUrl) {
174
+ try {
175
+ return `marketplace:${new URL(registryUrl).origin}`;
176
+ } catch {
177
+ return `marketplace:${registryUrl}`;
178
+ }
179
+ }
180
+ /**
181
+ * Get stored marketplace credential for a registry URL.
182
+ */
183
+ function getMarketplaceCredential(registryUrl) {
184
+ const key = marketplaceKey(registryUrl);
185
+ const cred = readStore()[key];
186
+ if (!cred || !("token" in cred)) return null;
187
+ if (new Date(cred.expiresAt) < /* @__PURE__ */ new Date()) return null;
188
+ return cred;
189
+ }
190
+ /**
191
+ * Save marketplace credential for a registry URL.
192
+ */
193
+ function saveMarketplaceCredential(registryUrl, cred) {
194
+ const key = marketplaceKey(registryUrl);
195
+ const store = readStore();
196
+ store[key] = cred;
197
+ writeStore(store);
198
+ }
199
+ /**
200
+ * Remove marketplace credential for a registry URL.
201
+ */
202
+ function removeMarketplaceCredential(registryUrl) {
203
+ const key = marketplaceKey(registryUrl);
204
+ const store = readStore();
205
+ if (key in store) {
206
+ delete store[key];
207
+ writeStore(store);
208
+ return true;
209
+ }
210
+ return false;
211
+ }
212
+
213
+ //#endregion
214
+ //#region src/cli/client-factory.ts
215
+ var client_factory_exports = /* @__PURE__ */ __exportAll({
216
+ connectionArgs: () => connectionArgs,
217
+ createClientFromArgs: () => createClientFromArgs
218
+ });
219
+ /**
220
+ * Shared connection args for all CLI commands that talk to a Dineway instance.
221
+ * Spread into each command's `args` definition.
222
+ */
223
+ const connectionArgs = {
224
+ url: {
225
+ type: "string",
226
+ alias: "u",
227
+ description: "Dineway instance URL",
228
+ default: "http://localhost:4321"
229
+ },
230
+ token: {
231
+ type: "string",
232
+ alias: "t",
233
+ description: "Auth token"
234
+ },
235
+ header: {
236
+ type: "string",
237
+ alias: "H",
238
+ description: "Custom header \"Name: Value\" (repeatable, or use DINEWAY_HEADERS env)"
239
+ },
240
+ json: {
241
+ type: "boolean",
242
+ description: "Output as JSON"
243
+ }
244
+ };
245
+ /**
246
+ * Create a DinewayClient from CLI args, env vars, and stored credentials.
247
+ *
248
+ * Auth resolution order:
249
+ * 1. --token flag
250
+ * 2. DINEWAY_TOKEN env var
251
+ * 3. Stored credentials (~/.config/dineway/auth.json)
252
+ * 4. Dev bypass (if URL is localhost)
253
+ *
254
+ * Custom headers are merged from (in priority order):
255
+ * 1. Stored credentials (persisted during `dineway login --header`)
256
+ * 2. DINEWAY_HEADERS env var
257
+ * 3. --header CLI flags
258
+ */
259
+ function createClientFromArgs(args) {
260
+ const baseUrl = args.url || process.env["DINEWAY_URL"] || "http://localhost:4321";
261
+ let token = args.token || process.env["DINEWAY_TOKEN"];
262
+ const isLocal = baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
263
+ const cred = !token ? getCredentials(baseUrl) : null;
264
+ const customHeaders = {
265
+ ...cred?.customHeaders,
266
+ ...resolveCustomHeaders()
267
+ };
268
+ const extraInterceptors = [];
269
+ if (Object.keys(customHeaders).length > 0) extraInterceptors.push(customHeadersInterceptor(customHeaders));
270
+ if (!token && cred) if (new Date(cred.expiresAt) > /* @__PURE__ */ new Date()) token = cred.accessToken;
271
+ else return new DinewayClient({
272
+ baseUrl,
273
+ token: cred.accessToken,
274
+ refreshToken: cred.refreshToken,
275
+ onTokenRefresh: (newAccessToken, expiresIn) => {
276
+ saveCredentials(baseUrl, {
277
+ ...cred,
278
+ accessToken: newAccessToken,
279
+ expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString()
280
+ });
281
+ },
282
+ interceptors: extraInterceptors
283
+ });
284
+ return new DinewayClient({
285
+ baseUrl,
286
+ token,
287
+ devBypass: !token && isLocal,
288
+ interceptors: extraInterceptors
289
+ });
290
+ }
291
+
292
+ //#endregion
293
+ //#region src/cli/output.ts
294
+ /**
295
+ * Redirect consola output to stderr so it doesn't pollute JSON on stdout.
296
+ *
297
+ * Call this early in any command that uses `output()` with `--json`.
298
+ * Safe to call multiple times — only applies the redirect once.
299
+ */
300
+ function configureOutputMode(args) {
301
+ if (args.json || !process.stdout.isTTY) {
302
+ consola$1.options.stdout = process.stderr;
303
+ consola$1.options.stderr = process.stderr;
304
+ }
305
+ }
306
+ /**
307
+ * Output data as JSON or pretty-printed.
308
+ *
309
+ * If stdout is not a TTY or --json is set, outputs JSON.
310
+ * Otherwise, outputs a formatted representation.
311
+ */
312
+ function output(data, args) {
313
+ if (args.json || !process.stdout.isTTY) process.stdout.write(JSON.stringify(data, null, 2) + "\n");
314
+ else prettyPrint(data);
315
+ }
316
+ function prettyPrint(data, indent = 0) {
317
+ if (data === null || data === void 0) {
318
+ consola$1.log("(empty)");
319
+ return;
320
+ }
321
+ if (Array.isArray(data)) {
322
+ if (data.length === 0) {
323
+ consola$1.log("(no items)");
324
+ return;
325
+ }
326
+ for (const item of data) {
327
+ prettyPrint(item, indent);
328
+ if (indent === 0) consola$1.log("---");
329
+ }
330
+ return;
331
+ }
332
+ if (typeof data === "object") {
333
+ const obj = Object(data);
334
+ if ("items" in obj && Array.isArray(obj.items)) {
335
+ prettyPrint(obj.items, indent);
336
+ if (typeof obj.nextCursor === "string") consola$1.log(`\nNext cursor: ${obj.nextCursor}`);
337
+ return;
338
+ }
339
+ const prefix = " ".repeat(indent);
340
+ for (const [key, value] of Object.entries(obj)) {
341
+ if (value === null || value === void 0) continue;
342
+ if (typeof value === "object" && !Array.isArray(value)) {
343
+ consola$1.log(`${prefix}${key}:`);
344
+ prettyPrint(value, indent + 1);
345
+ } else if (Array.isArray(value)) consola$1.log(`${prefix}${key}: [${value.length} items]`);
346
+ else {
347
+ const str = typeof value === "string" ? value : JSON.stringify(value);
348
+ const display = str.length > 80 ? str.slice(0, 77) + "..." : str;
349
+ consola$1.log(`${prefix}${key}: ${display}`);
350
+ }
351
+ }
352
+ return;
353
+ }
354
+ consola$1.log(typeof data === "string" ? data : JSON.stringify(data));
355
+ }
356
+
357
+ //#endregion
358
+ //#region src/cli/commands/content.ts
359
+ /**
360
+ * dineway content
361
+ *
362
+ * CRUD commands for managing content items via the Dineway REST API.
363
+ */
364
+ /** Read content data from --data, --file, or --stdin */
365
+ async function readInputData(args) {
366
+ if (args.data) try {
367
+ return JSON.parse(args.data);
368
+ } catch {
369
+ throw new Error("Invalid JSON in --data argument");
370
+ }
371
+ if (args.file) try {
372
+ const content = await readFile(args.file, "utf-8");
373
+ return JSON.parse(content);
374
+ } catch (error) {
375
+ if (error instanceof SyntaxError) throw new Error(`Invalid JSON in file: ${args.file}`, { cause: error });
376
+ throw error;
377
+ }
378
+ if (args.stdin) {
379
+ const chunks = [];
380
+ for await (const chunk of process.stdin) chunks.push(chunk);
381
+ const content = Buffer.concat(chunks).toString("utf-8");
382
+ try {
383
+ return JSON.parse(content);
384
+ } catch {
385
+ throw new Error("Invalid JSON from stdin");
386
+ }
387
+ }
388
+ throw new Error("Provide content data via --data, --file, or --stdin");
389
+ }
390
+ const listCommand$4 = defineCommand({
391
+ meta: {
392
+ name: "list",
393
+ description: "List content items"
394
+ },
395
+ args: {
396
+ collection: {
397
+ type: "positional",
398
+ description: "Collection slug",
399
+ required: true
400
+ },
401
+ status: {
402
+ type: "string",
403
+ description: "Filter by status"
404
+ },
405
+ locale: {
406
+ type: "string",
407
+ description: "Filter by locale"
408
+ },
409
+ limit: {
410
+ type: "string",
411
+ description: "Maximum items to return"
412
+ },
413
+ cursor: {
414
+ type: "string",
415
+ description: "Pagination cursor"
416
+ },
417
+ ...connectionArgs
418
+ },
419
+ async run({ args }) {
420
+ configureOutputMode(args);
421
+ try {
422
+ const result = await createClientFromArgs(args).list(args.collection, {
423
+ status: args.status,
424
+ locale: args.locale,
425
+ limit: args.limit ? parseInt(args.limit, 10) : void 0,
426
+ cursor: args.cursor
427
+ });
428
+ output({
429
+ items: result.items.map((item) => ({
430
+ id: item.id,
431
+ slug: item.slug,
432
+ locale: item.locale,
433
+ status: item.status,
434
+ title: typeof item.data?.title === "string" ? item.data.title : void 0,
435
+ updatedAt: item.updatedAt
436
+ })),
437
+ nextCursor: result.nextCursor
438
+ }, args);
439
+ } catch (error) {
440
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
441
+ process.exit(1);
442
+ }
443
+ }
444
+ });
445
+ const getCommand$3 = defineCommand({
446
+ meta: {
447
+ name: "get",
448
+ description: "Get a single content item"
449
+ },
450
+ args: {
451
+ collection: {
452
+ type: "positional",
453
+ description: "Collection slug",
454
+ required: true
455
+ },
456
+ id: {
457
+ type: "positional",
458
+ description: "Content item ID or slug",
459
+ required: true
460
+ },
461
+ locale: {
462
+ type: "string",
463
+ description: "Locale for slug resolution"
464
+ },
465
+ raw: {
466
+ type: "boolean",
467
+ description: "Return raw Portable Text (skip markdown conversion)"
468
+ },
469
+ published: {
470
+ type: "boolean",
471
+ description: "Return published data only (ignore pending draft)"
472
+ },
473
+ ...connectionArgs
474
+ },
475
+ async run({ args }) {
476
+ configureOutputMode(args);
477
+ try {
478
+ const client = createClientFromArgs(args);
479
+ const item = await client.get(args.collection, args.id, {
480
+ raw: args.raw,
481
+ locale: args.locale
482
+ });
483
+ if (!args.published && item.draftRevisionId) {
484
+ const comparison = await client.compare(args.collection, args.id);
485
+ if (comparison.hasChanges && comparison.draft) item.data = comparison.draft;
486
+ }
487
+ output(item, args);
488
+ } catch (error) {
489
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
490
+ process.exit(1);
491
+ }
492
+ }
493
+ });
494
+ const createCommand$1 = defineCommand({
495
+ meta: {
496
+ name: "create",
497
+ description: "Create a content item"
498
+ },
499
+ args: {
500
+ collection: {
501
+ type: "positional",
502
+ description: "Collection slug",
503
+ required: true
504
+ },
505
+ data: {
506
+ type: "string",
507
+ description: "Content data as JSON string"
508
+ },
509
+ file: {
510
+ type: "string",
511
+ description: "Read content data from a JSON file"
512
+ },
513
+ stdin: {
514
+ type: "boolean",
515
+ description: "Read content data from stdin"
516
+ },
517
+ slug: {
518
+ type: "string",
519
+ description: "Content slug"
520
+ },
521
+ locale: {
522
+ type: "string",
523
+ description: "Content locale"
524
+ },
525
+ "translation-of": {
526
+ type: "string",
527
+ description: "ID of content item to link as translation"
528
+ },
529
+ draft: {
530
+ type: "boolean",
531
+ description: "Keep as draft instead of auto-publishing"
532
+ },
533
+ ...connectionArgs
534
+ },
535
+ async run({ args }) {
536
+ configureOutputMode(args);
537
+ try {
538
+ const data = await readInputData(args);
539
+ const client = createClientFromArgs(args);
540
+ const item = await client.create(args.collection, {
541
+ data,
542
+ slug: args.slug,
543
+ locale: args.locale,
544
+ translationOf: args["translation-of"]
545
+ });
546
+ if (!args.draft) await client.publish(args.collection, item.id);
547
+ output(await client.get(args.collection, item.id), args);
548
+ } catch (error) {
549
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
550
+ process.exit(1);
551
+ }
552
+ }
553
+ });
554
+ const updateCommand = defineCommand({
555
+ meta: {
556
+ name: "update",
557
+ description: "Update a content item"
558
+ },
559
+ args: {
560
+ collection: {
561
+ type: "positional",
562
+ description: "Collection slug",
563
+ required: true
564
+ },
565
+ id: {
566
+ type: "positional",
567
+ description: "Content item ID or slug",
568
+ required: true
569
+ },
570
+ data: {
571
+ type: "string",
572
+ description: "Content data as JSON string"
573
+ },
574
+ file: {
575
+ type: "string",
576
+ description: "Read content data from a JSON file"
577
+ },
578
+ rev: {
579
+ type: "string",
580
+ description: "Revision token from get (prevents overwriting unseen changes)",
581
+ required: true
582
+ },
583
+ draft: {
584
+ type: "boolean",
585
+ description: "Keep as draft instead of auto-publishing"
586
+ },
587
+ ...connectionArgs
588
+ },
589
+ async run({ args }) {
590
+ configureOutputMode(args);
591
+ try {
592
+ const data = await readInputData(args);
593
+ const client = createClientFromArgs(args);
594
+ const updated = await client.update(args.collection, args.id, {
595
+ data,
596
+ _rev: args.rev
597
+ });
598
+ if (!args.draft && updated.draftRevisionId) await client.publish(args.collection, args.id);
599
+ output(await client.get(args.collection, args.id), args);
600
+ } catch (error) {
601
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
602
+ process.exit(1);
603
+ }
604
+ }
605
+ });
606
+ const deleteCommand$2 = defineCommand({
607
+ meta: {
608
+ name: "delete",
609
+ description: "Delete a content item"
610
+ },
611
+ args: {
612
+ collection: {
613
+ type: "positional",
614
+ description: "Collection slug",
615
+ required: true
616
+ },
617
+ id: {
618
+ type: "positional",
619
+ description: "Content item ID or slug",
620
+ required: true
621
+ },
622
+ ...connectionArgs
623
+ },
624
+ async run({ args }) {
625
+ configureOutputMode(args);
626
+ try {
627
+ await createClientFromArgs(args).delete(args.collection, args.id);
628
+ consola$1.success(`Deleted ${args.collection}/${args.id}`);
629
+ } catch (error) {
630
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
631
+ process.exit(1);
632
+ }
633
+ }
634
+ });
635
+ const publishCommand$1 = defineCommand({
636
+ meta: {
637
+ name: "publish",
638
+ description: "Publish a content item"
639
+ },
640
+ args: {
641
+ collection: {
642
+ type: "positional",
643
+ description: "Collection slug",
644
+ required: true
645
+ },
646
+ id: {
647
+ type: "positional",
648
+ description: "Content item ID or slug",
649
+ required: true
650
+ },
651
+ ...connectionArgs
652
+ },
653
+ async run({ args }) {
654
+ configureOutputMode(args);
655
+ try {
656
+ await createClientFromArgs(args).publish(args.collection, args.id);
657
+ consola$1.success(`Published ${args.collection}/${args.id}`);
658
+ } catch (error) {
659
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
660
+ process.exit(1);
661
+ }
662
+ }
663
+ });
664
+ const unpublishCommand = defineCommand({
665
+ meta: {
666
+ name: "unpublish",
667
+ description: "Unpublish a content item"
668
+ },
669
+ args: {
670
+ collection: {
671
+ type: "positional",
672
+ description: "Collection slug",
673
+ required: true
674
+ },
675
+ id: {
676
+ type: "positional",
677
+ description: "Content item ID or slug",
678
+ required: true
679
+ },
680
+ ...connectionArgs
681
+ },
682
+ async run({ args }) {
683
+ configureOutputMode(args);
684
+ try {
685
+ await createClientFromArgs(args).unpublish(args.collection, args.id);
686
+ consola$1.success(`Unpublished ${args.collection}/${args.id}`);
687
+ } catch (error) {
688
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
689
+ process.exit(1);
690
+ }
691
+ }
692
+ });
693
+ const scheduleCommand = defineCommand({
694
+ meta: {
695
+ name: "schedule",
696
+ description: "Schedule content for publishing"
697
+ },
698
+ args: {
699
+ collection: {
700
+ type: "positional",
701
+ description: "Collection slug",
702
+ required: true
703
+ },
704
+ id: {
705
+ type: "positional",
706
+ description: "Content item ID or slug",
707
+ required: true
708
+ },
709
+ at: {
710
+ type: "string",
711
+ description: "ISO 8601 datetime to publish at",
712
+ required: true
713
+ },
714
+ ...connectionArgs
715
+ },
716
+ async run({ args }) {
717
+ configureOutputMode(args);
718
+ try {
719
+ await createClientFromArgs(args).schedule(args.collection, args.id, { at: args.at });
720
+ consola$1.success(`Scheduled ${args.collection}/${args.id} for ${args.at}`);
721
+ } catch (error) {
722
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
723
+ process.exit(1);
724
+ }
725
+ }
726
+ });
727
+ const restoreCommand = defineCommand({
728
+ meta: {
729
+ name: "restore",
730
+ description: "Restore a trashed content item"
731
+ },
732
+ args: {
733
+ collection: {
734
+ type: "positional",
735
+ description: "Collection slug",
736
+ required: true
737
+ },
738
+ id: {
739
+ type: "positional",
740
+ description: "Content item ID or slug",
741
+ required: true
742
+ },
743
+ ...connectionArgs
744
+ },
745
+ async run({ args }) {
746
+ configureOutputMode(args);
747
+ try {
748
+ await createClientFromArgs(args).restore(args.collection, args.id);
749
+ consola$1.success(`Restored ${args.collection}/${args.id}`);
750
+ } catch (error) {
751
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
752
+ process.exit(1);
753
+ }
754
+ }
755
+ });
756
+ const translationsCommand = defineCommand({
757
+ meta: {
758
+ name: "translations",
759
+ description: "List translations for a content item"
760
+ },
761
+ args: {
762
+ collection: {
763
+ type: "positional",
764
+ description: "Collection slug",
765
+ required: true
766
+ },
767
+ id: {
768
+ type: "positional",
769
+ description: "Content item ID or slug",
770
+ required: true
771
+ },
772
+ ...connectionArgs
773
+ },
774
+ async run({ args }) {
775
+ configureOutputMode(args);
776
+ try {
777
+ output(await createClientFromArgs(args).translations(args.collection, args.id), args);
778
+ } catch (error) {
779
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
780
+ process.exit(1);
781
+ }
782
+ }
783
+ });
784
+ const contentCommand = defineCommand({
785
+ meta: {
786
+ name: "content",
787
+ description: "Manage content"
788
+ },
789
+ subCommands: {
790
+ list: listCommand$4,
791
+ get: getCommand$3,
792
+ create: createCommand$1,
793
+ update: updateCommand,
794
+ delete: deleteCommand$2,
795
+ publish: publishCommand$1,
796
+ unpublish: unpublishCommand,
797
+ schedule: scheduleCommand,
798
+ restore: restoreCommand,
799
+ translations: translationsCommand
800
+ }
801
+ });
802
+
803
+ //#endregion
804
+ //#region src/cli/database-url.ts
805
+ const REMOTE_DATABASE_URL_PREFIXES = [
806
+ "libsql:",
807
+ "http://",
808
+ "https://",
809
+ "ws://",
810
+ "wss://"
811
+ ];
812
+ /**
813
+ * Resolve a CLI --database argument into a concrete database URL.
814
+ *
815
+ * - Plain paths become absolute `file:` URLs rooted at `cwd`
816
+ * - Relative `file:` URLs are normalized against `cwd`
817
+ * - `:memory:` and remote URLs are passed through unchanged
818
+ */
819
+ function resolveCliDatabaseTarget(cwd, database) {
820
+ if (database === ":memory:") return {
821
+ url: database,
822
+ display: database
823
+ };
824
+ if (database.startsWith("file:")) {
825
+ const rawPath = database.slice(5);
826
+ const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
827
+ return {
828
+ url: `file:${absolutePath}`,
829
+ display: absolutePath,
830
+ localPath: absolutePath
831
+ };
832
+ }
833
+ if (REMOTE_DATABASE_URL_PREFIXES.some((prefix) => database.startsWith(prefix))) return {
834
+ url: database,
835
+ display: database
836
+ };
837
+ const absolutePath = resolve(cwd, database);
838
+ return {
839
+ url: `file:${absolutePath}`,
840
+ display: absolutePath,
841
+ localPath: absolutePath
842
+ };
843
+ }
844
+
845
+ //#endregion
846
+ //#region src/cli/commands/dev.ts
847
+ /**
848
+ * dineway dev
849
+ *
850
+ * Start development server with optional schema sync from remote
851
+ */
852
+ async function readPackageJson$2(cwd) {
853
+ const pkgPath = resolve(cwd, "package.json");
854
+ try {
855
+ const content = await readFile(pkgPath, "utf-8");
856
+ return JSON.parse(content);
857
+ } catch {
858
+ return null;
859
+ }
860
+ }
861
+ async function fileExists$4(path) {
862
+ try {
863
+ await access(path);
864
+ return true;
865
+ } catch {
866
+ return false;
867
+ }
868
+ }
869
+ const devCommand = defineCommand({
870
+ meta: {
871
+ name: "dev",
872
+ description: "Start dev server with local database"
873
+ },
874
+ args: {
875
+ database: {
876
+ type: "string",
877
+ alias: "d",
878
+ description: "Database path or URL (default: ./data.db)",
879
+ default: "./data.db"
880
+ },
881
+ types: {
882
+ type: "boolean",
883
+ alias: "t",
884
+ description: "Generate types from remote before starting",
885
+ default: false
886
+ },
887
+ port: {
888
+ type: "string",
889
+ alias: "p",
890
+ description: "Port for dev server",
891
+ default: "4321"
892
+ },
893
+ cwd: {
894
+ type: "string",
895
+ description: "Working directory",
896
+ default: process.cwd()
897
+ }
898
+ },
899
+ async run({ args }) {
900
+ const cwd = resolve(args.cwd);
901
+ const pkg = await readPackageJson$2(cwd);
902
+ if (!pkg) {
903
+ consola.error("No package.json found");
904
+ process.exit(1);
905
+ }
906
+ const database = resolveCliDatabaseTarget(cwd, args.database);
907
+ consola.info(`Database: ${database.display}`);
908
+ if (database.localPath && !await fileExists$4(database.localPath)) consola.start("Database not found, initializing...");
909
+ const db = createDatabase({ url: database.url });
910
+ try {
911
+ consola.start("Checking database migrations...");
912
+ const { applied } = await runMigrations(db);
913
+ if (applied.length > 0) consola.success(`Applied ${applied.length} migrations`);
914
+ else consola.info("Database up to date");
915
+ } catch (error) {
916
+ consola.error("Migration failed:", error);
917
+ await db.destroy();
918
+ process.exit(1);
919
+ }
920
+ await db.destroy();
921
+ if (args.types) {
922
+ const remoteUrl = pkg.dineway?.url || process.env.DINEWAY_URL;
923
+ if (!remoteUrl) consola.warn("No remote URL configured. Set DINEWAY_URL or dineway.url in package.json");
924
+ else try {
925
+ const { createClientFromArgs } = await Promise.resolve().then(() => client_factory_exports);
926
+ const client = createClientFromArgs({ url: remoteUrl });
927
+ const schema = await client.schemaExport();
928
+ const types = await client.schemaTypes();
929
+ const { writeFile, mkdir } = await import("node:fs/promises");
930
+ const { resolve: resolvePath, dirname } = await import("node:path");
931
+ const outputPath = resolvePath(cwd, ".dineway/types.ts");
932
+ await mkdir(dirname(outputPath), { recursive: true });
933
+ await writeFile(outputPath, types, "utf-8");
934
+ await writeFile(resolvePath(dirname(outputPath), "schema.json"), JSON.stringify(schema, null, 2), "utf-8");
935
+ consola.success(`Generated types for ${schema.collections.length} collections`);
936
+ } catch (error) {
937
+ consola.warn("Type generation failed:", error instanceof Error ? error.message : error);
938
+ }
939
+ }
940
+ consola.start("Starting Astro dev server...");
941
+ const astroArgs = [
942
+ "astro",
943
+ "dev",
944
+ "--port",
945
+ args.port
946
+ ];
947
+ const pnpmLockExists = await fileExists$4(resolve(cwd, "pnpm-lock.yaml"));
948
+ const yarnLockExists = await fileExists$4(resolve(cwd, "yarn.lock"));
949
+ let cmd;
950
+ let cmdArgs;
951
+ if (pnpmLockExists) {
952
+ cmd = "pnpm";
953
+ cmdArgs = astroArgs;
954
+ } else if (yarnLockExists) {
955
+ cmd = "yarn";
956
+ cmdArgs = astroArgs;
957
+ } else {
958
+ cmd = "npx";
959
+ cmdArgs = astroArgs;
960
+ }
961
+ consola.info(`Running: ${cmd} ${cmdArgs.join(" ")}`);
962
+ const child = spawn(cmd, cmdArgs, {
963
+ cwd,
964
+ stdio: "inherit",
965
+ env: {
966
+ ...process.env,
967
+ DINEWAY_DATABASE_URL: database.url
968
+ }
969
+ });
970
+ child.on("error", (error) => {
971
+ consola.error("Failed to start dev server:", error);
972
+ process.exit(1);
973
+ });
974
+ child.on("exit", (code) => {
975
+ process.exit(code ?? 0);
976
+ });
977
+ const cleanup = () => {
978
+ child.kill("SIGTERM");
979
+ };
980
+ process.on("SIGINT", cleanup);
981
+ process.on("SIGTERM", cleanup);
982
+ }
983
+ });
984
+
985
+ //#endregion
986
+ //#region src/cli/commands/doctor.ts
987
+ /**
988
+ * dineway doctor
989
+ *
990
+ * Diagnose database health: connection, migrations, schema integrity.
991
+ */
992
+ async function fileExists$3(path) {
993
+ try {
994
+ await access(path);
995
+ return true;
996
+ } catch {
997
+ return false;
998
+ }
999
+ }
1000
+ function printResult(result) {
1001
+ (result.status === "pass" ? consola.success : result.status === "warn" ? consola.warn : consola.error)(`${result.name}: ${result.message}`);
1002
+ }
1003
+ async function checkDatabase(database) {
1004
+ const results = [];
1005
+ if (database.localPath && !await fileExists$3(database.localPath)) {
1006
+ results.push({
1007
+ name: "database",
1008
+ status: "fail",
1009
+ message: `not found at ${database.localPath} — run "dineway init"`
1010
+ });
1011
+ return results;
1012
+ }
1013
+ results.push({
1014
+ name: "database",
1015
+ status: "pass",
1016
+ message: database.display
1017
+ });
1018
+ let db;
1019
+ try {
1020
+ db = createDatabase({ url: database.url });
1021
+ const { applied, pending } = await getMigrationStatus(db);
1022
+ if (pending.length === 0) results.push({
1023
+ name: "migrations",
1024
+ status: "pass",
1025
+ message: `${applied.length} applied, none pending`
1026
+ });
1027
+ else results.push({
1028
+ name: "migrations",
1029
+ status: "warn",
1030
+ message: `${applied.length} applied, ${pending.length} pending — run "dineway init"`
1031
+ });
1032
+ const { sql } = await import("kysely");
1033
+ try {
1034
+ const count = (await sql`SELECT COUNT(id) as count FROM _dineway_collections`.execute(db)).rows[0]?.count ?? 0;
1035
+ results.push({
1036
+ name: "collections",
1037
+ status: count > 0 ? "pass" : "warn",
1038
+ message: count > 0 ? `${count} collections defined` : "no collections — seed or create via admin"
1039
+ });
1040
+ } catch {
1041
+ results.push({
1042
+ name: "collections",
1043
+ status: "fail",
1044
+ message: "could not query collections table — migrations may not have run"
1045
+ });
1046
+ }
1047
+ try {
1048
+ const tableNames = await listTablesLike(db, "ec_%");
1049
+ const collectionsResult = await sql`SELECT slug FROM _dineway_collections`.execute(db);
1050
+ const registeredSlugs = new Set(collectionsResult.rows.map((r) => `ec_${r.slug}`));
1051
+ const orphaned = tableNames.filter((name) => !registeredSlugs.has(name));
1052
+ if (orphaned.length > 0) results.push({
1053
+ name: "orphaned tables",
1054
+ status: "warn",
1055
+ message: `found ${orphaned.length}: ${orphaned.join(", ")}`
1056
+ });
1057
+ } catch {}
1058
+ try {
1059
+ const count = (await sql`SELECT COUNT(id) as count FROM _dineway_users`.execute(db)).rows[0]?.count ?? 0;
1060
+ results.push({
1061
+ name: "users",
1062
+ status: count > 0 ? "pass" : "warn",
1063
+ message: count > 0 ? `${count} users` : "no users — complete setup wizard at /_dineway/admin"
1064
+ });
1065
+ } catch {
1066
+ results.push({
1067
+ name: "users",
1068
+ status: "warn",
1069
+ message: "could not query users table"
1070
+ });
1071
+ }
1072
+ } catch (error) {
1073
+ results.push({
1074
+ name: "database connection",
1075
+ status: "fail",
1076
+ message: error instanceof Error ? error.message : "failed to connect"
1077
+ });
1078
+ } finally {
1079
+ if (db) await db.destroy();
1080
+ }
1081
+ return results;
1082
+ }
1083
+ const doctorCommand = defineCommand({
1084
+ meta: {
1085
+ name: "doctor",
1086
+ description: "Check database health and diagnose issues"
1087
+ },
1088
+ args: {
1089
+ database: {
1090
+ type: "string",
1091
+ alias: "d",
1092
+ description: "Database path or URL (default: ./data.db)",
1093
+ default: "./data.db"
1094
+ },
1095
+ cwd: {
1096
+ type: "string",
1097
+ description: "Working directory",
1098
+ default: process.cwd()
1099
+ },
1100
+ json: {
1101
+ type: "boolean",
1102
+ description: "Output results as JSON",
1103
+ default: false
1104
+ }
1105
+ },
1106
+ async run({ args }) {
1107
+ const results = await checkDatabase(resolveCliDatabaseTarget(resolve(args.cwd), args.database));
1108
+ if (args.json) {
1109
+ process.stdout.write(JSON.stringify(results, null, 2) + "\n");
1110
+ return;
1111
+ }
1112
+ consola.start("Dineway Doctor\n");
1113
+ for (const result of results) printResult(result);
1114
+ const fails = results.filter((r) => r.status === "fail");
1115
+ const warns = results.filter((r) => r.status === "warn");
1116
+ consola.log("");
1117
+ if (fails.length === 0 && warns.length === 0) consola.success("All checks passed");
1118
+ else if (fails.length === 0) consola.info(`All critical checks passed (${warns.length} warnings)`);
1119
+ else {
1120
+ consola.error(`${fails.length} issues found`);
1121
+ process.exitCode = 1;
1122
+ }
1123
+ }
1124
+ });
1125
+
1126
+ //#endregion
1127
+ //#region src/cli/commands/export-seed.ts
1128
+ /**
1129
+ * dineway export-seed
1130
+ *
1131
+ * Export current database schema (and optionally content) as a seed file
1132
+ */
1133
+ const SETTINGS_PREFIX = "site:";
1134
+ const exportSeedCommand = defineCommand({
1135
+ meta: {
1136
+ name: "export-seed",
1137
+ description: "Export database schema and content as a seed file"
1138
+ },
1139
+ args: {
1140
+ database: {
1141
+ type: "string",
1142
+ alias: "d",
1143
+ description: "Database path or URL",
1144
+ default: "./data.db"
1145
+ },
1146
+ cwd: {
1147
+ type: "string",
1148
+ description: "Working directory",
1149
+ default: process.cwd()
1150
+ },
1151
+ "with-content": {
1152
+ type: "string",
1153
+ description: "Include content (all or comma-separated collection names)",
1154
+ required: false
1155
+ },
1156
+ pretty: {
1157
+ type: "boolean",
1158
+ description: "Pretty print JSON output",
1159
+ default: true
1160
+ }
1161
+ },
1162
+ async run({ args }) {
1163
+ const database = resolveCliDatabaseTarget(resolve(args.cwd), args.database);
1164
+ consola.info(`Database: ${database.display}`);
1165
+ const db = createDatabase({ url: database.url });
1166
+ try {
1167
+ await runMigrations(db);
1168
+ } catch (error) {
1169
+ consola.error("Migration failed:", error);
1170
+ await db.destroy();
1171
+ process.exit(1);
1172
+ }
1173
+ try {
1174
+ const seed = await exportSeed(db, args["with-content"]);
1175
+ const output = args.pretty ? JSON.stringify(seed, null, " ") : JSON.stringify(seed);
1176
+ console.log(output);
1177
+ } catch (error) {
1178
+ consola.error("Export failed:", error);
1179
+ await db.destroy();
1180
+ process.exit(1);
1181
+ }
1182
+ await db.destroy();
1183
+ }
1184
+ });
1185
+ /**
1186
+ * Export database to seed file format
1187
+ */
1188
+ async function exportSeed(db, withContent) {
1189
+ const seed = {
1190
+ $schema: "https://dineway.foodism.ai/seed.schema.json",
1191
+ version: "1",
1192
+ meta: {
1193
+ name: "Exported Seed",
1194
+ description: "Exported from existing Dineway database"
1195
+ }
1196
+ };
1197
+ seed.settings = await exportSettings(db);
1198
+ seed.collections = await exportCollections(db);
1199
+ seed.taxonomies = await exportTaxonomies(db);
1200
+ seed.menus = await exportMenus(db);
1201
+ seed.widgetAreas = await exportWidgetAreas(db);
1202
+ if (withContent !== void 0) {
1203
+ const collections = withContent === "" || withContent === "true" ? null : withContent.split(",").map((s) => s.trim()).filter(Boolean);
1204
+ seed.content = await exportContent(db, seed.collections || [], collections);
1205
+ }
1206
+ return seed;
1207
+ }
1208
+ /**
1209
+ * Export site settings
1210
+ */
1211
+ async function exportSettings(db) {
1212
+ const allOptions = await new OptionsRepository(db).getByPrefix(SETTINGS_PREFIX);
1213
+ const settings = {};
1214
+ for (const [key, value] of allOptions) {
1215
+ const settingKey = key.replace(SETTINGS_PREFIX, "");
1216
+ settings[settingKey] = value;
1217
+ }
1218
+ return Object.keys(settings).length > 0 ? settings : void 0;
1219
+ }
1220
+ /**
1221
+ * Export collections and their fields
1222
+ */
1223
+ async function exportCollections(db) {
1224
+ const registry = new SchemaRegistry(db);
1225
+ const collections = await registry.listCollections();
1226
+ const result = [];
1227
+ for (const collection of collections) {
1228
+ const fields = await registry.listFields(collection.id);
1229
+ const seedCollection = {
1230
+ slug: collection.slug,
1231
+ label: collection.label,
1232
+ labelSingular: collection.labelSingular || void 0,
1233
+ description: collection.description || void 0,
1234
+ icon: collection.icon || void 0,
1235
+ supports: collection.supports.length > 0 ? collection.supports : void 0,
1236
+ urlPattern: collection.urlPattern || void 0,
1237
+ fields: fields.map((field) => ({
1238
+ slug: field.slug,
1239
+ label: field.label,
1240
+ type: field.type,
1241
+ required: field.required || void 0,
1242
+ unique: field.unique || void 0,
1243
+ searchable: field.searchable || void 0,
1244
+ defaultValue: field.defaultValue,
1245
+ validation: field.validation ? { ...field.validation } : void 0,
1246
+ widget: field.widget || void 0,
1247
+ options: field.options || void 0
1248
+ }))
1249
+ };
1250
+ result.push(seedCollection);
1251
+ }
1252
+ return result;
1253
+ }
1254
+ /**
1255
+ * Export taxonomy definitions and terms
1256
+ */
1257
+ async function exportTaxonomies(db) {
1258
+ const defs = await db.selectFrom("_dineway_taxonomy_defs").selectAll().execute();
1259
+ const result = [];
1260
+ const termRepo = new TaxonomyRepository(db);
1261
+ for (const def of defs) {
1262
+ const terms = await termRepo.findByName(def.name);
1263
+ const seedTerms = [];
1264
+ const idToSlug = /* @__PURE__ */ new Map();
1265
+ for (const term of terms) idToSlug.set(term.id, term.slug);
1266
+ for (const term of terms) {
1267
+ const seedTerm = {
1268
+ slug: term.slug,
1269
+ label: term.label,
1270
+ description: typeof term.data?.description === "string" ? term.data.description : void 0
1271
+ };
1272
+ if (term.parentId) seedTerm.parent = idToSlug.get(term.parentId);
1273
+ seedTerms.push(seedTerm);
1274
+ }
1275
+ const taxonomy = {
1276
+ name: def.name,
1277
+ label: def.label,
1278
+ labelSingular: def.label_singular || void 0,
1279
+ hierarchical: def.hierarchical === 1,
1280
+ collections: def.collections ? JSON.parse(def.collections) : []
1281
+ };
1282
+ if (seedTerms.length > 0) taxonomy.terms = seedTerms;
1283
+ result.push(taxonomy);
1284
+ }
1285
+ return result;
1286
+ }
1287
+ /**
1288
+ * Export menus with their items
1289
+ */
1290
+ async function exportMenus(db) {
1291
+ const menus = await db.selectFrom("_dineway_menus").selectAll().execute();
1292
+ const result = [];
1293
+ for (const menu of menus) {
1294
+ const seedItems = buildMenuItemTree(await db.selectFrom("_dineway_menu_items").selectAll().where("menu_id", "=", menu.id).orderBy("sort_order", "asc").execute());
1295
+ result.push({
1296
+ name: menu.name,
1297
+ label: menu.label,
1298
+ items: seedItems
1299
+ });
1300
+ }
1301
+ return result;
1302
+ }
1303
+ /** Type guard for valid widget types */
1304
+ function isWidgetType(t) {
1305
+ return t === "content" || t === "menu" || t === "component";
1306
+ }
1307
+ /**
1308
+ * Build hierarchical menu item tree from flat array
1309
+ */
1310
+ function buildMenuItemTree(items) {
1311
+ const childMap = /* @__PURE__ */ new Map();
1312
+ for (const item of items) {
1313
+ const parentId = item.parent_id;
1314
+ if (!childMap.has(parentId)) childMap.set(parentId, []);
1315
+ childMap.get(parentId).push(item);
1316
+ }
1317
+ function buildLevel(parentId) {
1318
+ return (childMap.get(parentId) || []).map((item) => {
1319
+ const seedItem = {
1320
+ type: item.type,
1321
+ label: item.label || void 0
1322
+ };
1323
+ if (item.type === "custom") seedItem.url = item.custom_url || void 0;
1324
+ else {
1325
+ seedItem.ref = item.reference_id || void 0;
1326
+ seedItem.collection = item.reference_collection || void 0;
1327
+ }
1328
+ if (item.target === "_blank") seedItem.target = "_blank";
1329
+ if (item.title_attr) seedItem.titleAttr = item.title_attr;
1330
+ if (item.css_classes) seedItem.cssClasses = item.css_classes;
1331
+ const itemChildren = buildLevel(item.id);
1332
+ if (itemChildren.length > 0) seedItem.children = itemChildren;
1333
+ return seedItem;
1334
+ });
1335
+ }
1336
+ return buildLevel(null);
1337
+ }
1338
+ /**
1339
+ * Export widget areas with their widgets
1340
+ */
1341
+ async function exportWidgetAreas(db) {
1342
+ const areas = await db.selectFrom("_dineway_widget_areas").selectAll().execute();
1343
+ const result = [];
1344
+ for (const area of areas) {
1345
+ const seedWidgets = (await db.selectFrom("_dineway_widgets").selectAll().where("area_id", "=", area.id).orderBy("sort_order", "asc").execute()).filter((w) => isWidgetType(w.type)).map((widget) => {
1346
+ const seedWidget = { type: isWidgetType(widget.type) ? widget.type : "content" };
1347
+ if (widget.title) seedWidget.title = widget.title;
1348
+ if (widget.type === "content" && widget.content) seedWidget.content = JSON.parse(widget.content);
1349
+ else if (widget.type === "menu" && widget.menu_name) seedWidget.menuName = widget.menu_name;
1350
+ else if (widget.type === "component") {
1351
+ if (widget.component_id) seedWidget.componentId = widget.component_id;
1352
+ if (widget.component_props) seedWidget.props = JSON.parse(widget.component_props);
1353
+ }
1354
+ return seedWidget;
1355
+ });
1356
+ result.push({
1357
+ name: area.name,
1358
+ label: area.label,
1359
+ description: area.description || void 0,
1360
+ widgets: seedWidgets
1361
+ });
1362
+ }
1363
+ return result;
1364
+ }
1365
+ /**
1366
+ * Export content from collections
1367
+ */
1368
+ async function exportContent(db, collections, includeCollections) {
1369
+ const content = {};
1370
+ const contentRepo = new ContentRepository(db);
1371
+ const taxonomyRepo = new TaxonomyRepository(db);
1372
+ const mediaRepo = new MediaRepository(db);
1373
+ const mediaMap = /* @__PURE__ */ new Map();
1374
+ try {
1375
+ let cursor;
1376
+ do {
1377
+ const result = await mediaRepo.findMany({
1378
+ limit: 100,
1379
+ cursor,
1380
+ status: "all"
1381
+ });
1382
+ for (const media of result.items) mediaMap.set(media.id, {
1383
+ url: `/_dineway/api/media/file/${media.storageKey}`,
1384
+ filename: media.filename,
1385
+ alt: media.alt || void 0,
1386
+ caption: media.caption || void 0
1387
+ });
1388
+ cursor = result.nextCursor;
1389
+ } while (cursor);
1390
+ } catch {}
1391
+ const i18nEnabled = isI18nEnabled();
1392
+ for (const collection of collections) {
1393
+ if (includeCollections && !includeCollections.includes(collection.slug)) continue;
1394
+ const entries = [];
1395
+ let cursor;
1396
+ const translationGroupToSeedId = /* @__PURE__ */ new Map();
1397
+ do {
1398
+ const result = await contentRepo.findMany(collection.slug, {
1399
+ limit: 100,
1400
+ cursor
1401
+ });
1402
+ for (const item of result.items) {
1403
+ const seedId = item.slug ? i18nEnabled && item.locale ? `${collection.slug}:${item.slug}:${item.locale}` : `${collection.slug}:${item.slug}` : item.id;
1404
+ const processedData = processDataForExport(item.data, collection.fields, mediaMap);
1405
+ const entry = {
1406
+ id: seedId,
1407
+ slug: item.slug || item.id,
1408
+ status: item.status === "published" || item.status === "draft" ? item.status : void 0,
1409
+ data: processedData
1410
+ };
1411
+ if (i18nEnabled && item.locale) {
1412
+ entry.locale = item.locale;
1413
+ if (item.translationGroup) {
1414
+ const sourceSeedId = translationGroupToSeedId.get(item.translationGroup);
1415
+ if (sourceSeedId) entry.translationOf = sourceSeedId;
1416
+ else translationGroupToSeedId.set(item.translationGroup, seedId);
1417
+ }
1418
+ }
1419
+ const taxonomies = await getTaxonomyAssignments(taxonomyRepo, collection.slug, item.id);
1420
+ if (Object.keys(taxonomies).length > 0) entry.taxonomies = taxonomies;
1421
+ entries.push(entry);
1422
+ }
1423
+ cursor = result.nextCursor;
1424
+ } while (cursor);
1425
+ if (i18nEnabled && entries.length > 0) entries.sort((a, b) => {
1426
+ if (a.translationOf && !b.translationOf) return 1;
1427
+ if (!a.translationOf && b.translationOf) return -1;
1428
+ return 0;
1429
+ });
1430
+ if (entries.length > 0) content[collection.slug] = entries;
1431
+ }
1432
+ return content;
1433
+ }
1434
+ /**
1435
+ * Process content data for export, converting image fields to $media syntax
1436
+ */
1437
+ function processDataForExport(data, fields, mediaMap) {
1438
+ const result = {};
1439
+ const fieldTypes = /* @__PURE__ */ new Map();
1440
+ for (const field of fields) fieldTypes.set(field.slug, field.type);
1441
+ for (const [key, value] of Object.entries(data)) {
1442
+ const fieldType = fieldTypes.get(key);
1443
+ if (fieldType === "image" && value && typeof value === "object") {
1444
+ const imageValue = value;
1445
+ if (imageValue.id) {
1446
+ const mediaInfo = mediaMap.get(imageValue.id);
1447
+ if (mediaInfo) {
1448
+ result[key] = { $media: {
1449
+ url: mediaInfo.url,
1450
+ filename: mediaInfo.filename,
1451
+ alt: imageValue.alt || mediaInfo.alt,
1452
+ caption: mediaInfo.caption
1453
+ } };
1454
+ continue;
1455
+ }
1456
+ }
1457
+ result[key] = value;
1458
+ } else if (fieldType === "reference" && typeof value === "string") result[key] = `$ref:${value}`;
1459
+ else if (Array.isArray(value)) result[key] = value.map((item) => {
1460
+ if (typeof item === "string" && fieldType === "reference") return `$ref:${item}`;
1461
+ return item;
1462
+ });
1463
+ else result[key] = value;
1464
+ }
1465
+ return result;
1466
+ }
1467
+ /**
1468
+ * Get taxonomy term assignments for a content entry
1469
+ */
1470
+ async function getTaxonomyAssignments(taxonomyRepo, collection, entryId) {
1471
+ const terms = await taxonomyRepo.getTermsForEntry(collection, entryId);
1472
+ const result = {};
1473
+ for (const term of terms) {
1474
+ if (!result[term.name]) result[term.name] = [];
1475
+ result[term.name].push(term.slug);
1476
+ }
1477
+ return result;
1478
+ }
1479
+
1480
+ //#endregion
1481
+ //#region src/cli/commands/init.ts
1482
+ /**
1483
+ * dineway init
1484
+ *
1485
+ * Initialize database from template config in package.json
1486
+ */
1487
+ async function fileExists$2(path) {
1488
+ try {
1489
+ await access(path);
1490
+ return true;
1491
+ } catch {
1492
+ return false;
1493
+ }
1494
+ }
1495
+ async function readPackageJson$1(cwd) {
1496
+ const pkgPath = resolve(cwd, "package.json");
1497
+ try {
1498
+ const content = await readFile(pkgPath, "utf-8");
1499
+ return JSON.parse(content);
1500
+ } catch {
1501
+ return null;
1502
+ }
1503
+ }
1504
+ async function runSqlFile(db, filePath) {
1505
+ const statements = (await readFile(filePath, "utf-8")).split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
1506
+ for (const statement of statements) await db.executeQuery({
1507
+ sql: statement,
1508
+ parameters: [],
1509
+ query: {
1510
+ kind: "RawNode",
1511
+ sqlFragments: [statement],
1512
+ parameters: []
1513
+ }
1514
+ });
1515
+ }
1516
+ /**
1517
+ * Check if database has already been initialized with template schema
1518
+ */
1519
+ async function isAlreadyInitialized(db) {
1520
+ try {
1521
+ const { sql } = await import("kysely");
1522
+ const row = (await sql`SELECT COUNT(id) as count FROM _dineway_collections`.execute(db)).rows[0];
1523
+ return row ? row.count > 0 : false;
1524
+ } catch {
1525
+ return false;
1526
+ }
1527
+ }
1528
+ const initCommand = defineCommand({
1529
+ meta: {
1530
+ name: "init",
1531
+ description: "Initialize database from template config"
1532
+ },
1533
+ args: {
1534
+ database: {
1535
+ type: "string",
1536
+ alias: "d",
1537
+ description: "Database path or URL (default: ./data.db)",
1538
+ default: "./data.db"
1539
+ },
1540
+ cwd: {
1541
+ type: "string",
1542
+ description: "Working directory",
1543
+ default: process.cwd()
1544
+ },
1545
+ force: {
1546
+ type: "boolean",
1547
+ alias: "f",
1548
+ description: "Force re-initialization",
1549
+ default: false
1550
+ }
1551
+ },
1552
+ async run({ args }) {
1553
+ const cwd = resolve(args.cwd);
1554
+ consola.start("Initializing Dineway...");
1555
+ const pkg = await readPackageJson$1(cwd);
1556
+ if (!pkg) {
1557
+ consola.error("No package.json found in", cwd);
1558
+ process.exit(1);
1559
+ }
1560
+ const config = pkg.dineway;
1561
+ consola.info(`Project: ${pkg.name || "unknown"}`);
1562
+ if (config?.label) consola.info(`Template: ${config.label}`);
1563
+ const database = resolveCliDatabaseTarget(cwd, args.database);
1564
+ consola.info(`Database: ${database.display}`);
1565
+ const db = createDatabase({ url: database.url });
1566
+ consola.start("Running migrations...");
1567
+ try {
1568
+ const { applied } = await runMigrations(db);
1569
+ if (applied.length > 0) {
1570
+ consola.success(`Applied ${applied.length} migrations`);
1571
+ for (const name of applied) consola.info(` - ${name}`);
1572
+ } else consola.info("Migrations already up to date");
1573
+ } catch (error) {
1574
+ consola.error("Migration failed:", error);
1575
+ await db.destroy();
1576
+ process.exit(1);
1577
+ }
1578
+ const alreadyInitialized = await isAlreadyInitialized(db);
1579
+ if (alreadyInitialized && !args.force) {
1580
+ await db.destroy();
1581
+ consola.success("Already initialized. Use --force to re-run schema/seed.");
1582
+ return;
1583
+ }
1584
+ if (alreadyInitialized && args.force) consola.warn("Re-initializing (--force)...");
1585
+ if (config?.schema) {
1586
+ const schemaPath = resolve(cwd, config.schema);
1587
+ if (await fileExists$2(schemaPath)) {
1588
+ consola.start(`Running schema: ${config.schema}`);
1589
+ try {
1590
+ await runSqlFile(db, schemaPath);
1591
+ consola.success("Schema applied");
1592
+ } catch (error) {
1593
+ consola.error("Schema failed:", error);
1594
+ await db.destroy();
1595
+ process.exit(1);
1596
+ }
1597
+ } else consola.warn(`Schema file not found: ${config.schema}`);
1598
+ }
1599
+ await db.destroy();
1600
+ consola.success("Dineway initialized successfully!");
1601
+ consola.info("Run `pnpm dev` to start the development server");
1602
+ }
1603
+ });
1604
+
1605
+ //#endregion
1606
+ //#region src/cli/commands/login.ts
1607
+ /**
1608
+ * Login/logout/whoami CLI commands
1609
+ *
1610
+ * Login uses the OAuth Device Flow (RFC 8628):
1611
+ * 1. POST /oauth/device/code → get device_code + user_code
1612
+ * 2. Display URL + code to user
1613
+ * 3. Poll POST /oauth/device/token until authorized
1614
+ * 4. Save tokens to ~/.config/dineway/auth.json
1615
+ *
1616
+ * Custom headers (--header / DINEWAY_HEADERS) are sent with every request
1617
+ * and persisted to credentials so subsequent commands inherit them.
1618
+ * This supports sites behind reverse proxies and external auth gateways.
1619
+ */
1620
+ async function pollForToken(tokenEndpoint, deviceCode, interval, expiresIn, fetchFn) {
1621
+ const deadline = Date.now() + expiresIn * 1e3;
1622
+ let currentInterval = interval;
1623
+ while (Date.now() < deadline) {
1624
+ await new Promise((resolve) => setTimeout(resolve, currentInterval * 1e3));
1625
+ const res = await fetchFn(tokenEndpoint, {
1626
+ method: "POST",
1627
+ headers: { "Content-Type": "application/json" },
1628
+ body: JSON.stringify({
1629
+ device_code: deviceCode,
1630
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1631
+ })
1632
+ });
1633
+ if (res.ok) return (await res.json()).data;
1634
+ const body = await res.json();
1635
+ if (body.error === "authorization_pending") continue;
1636
+ if (body.error === "slow_down") {
1637
+ currentInterval = body.interval ?? currentInterval + 5;
1638
+ continue;
1639
+ }
1640
+ if (body.error === "expired_token") throw new Error("Device code expired. Please try again.");
1641
+ if (body.error === "access_denied") throw new Error("Authorization was denied.");
1642
+ throw new Error(`Token exchange failed: ${body.error || res.statusText}`);
1643
+ }
1644
+ throw new Error("Device code expired (timeout). Please try again.");
1645
+ }
1646
+ function printExternalAuthGuidance(baseUrl) {
1647
+ console.log();
1648
+ consola$1.info("This instance is behind an external auth gateway or reverse proxy.");
1649
+ consola$1.info("Authenticate at the gateway first, or retry with the required forwarded headers.");
1650
+ console.log();
1651
+ consola$1.info(` ${pc.bold("Header-based retry:")}`);
1652
+ console.log(` ${pc.cyan(`dineway login --url ${baseUrl} -H "Header-Name: value"`)}`);
1653
+ console.log(` ${pc.cyan(`DINEWAY_HEADERS="Header-Name: value" dineway login --url ${baseUrl}`)}`);
1654
+ console.log();
1655
+ consola$1.info(` ${pc.bold("API token fallback:")}`);
1656
+ console.log(` ${pc.cyan("Sign in through the gateway in your browser, then create a token in Settings > API Tokens.")}`);
1657
+ console.log(` ${pc.cyan(`dineway --token <token> --url ${baseUrl}`)}`);
1658
+ console.log();
1659
+ }
1660
+ const loginCommand = defineCommand({
1661
+ meta: {
1662
+ name: "login",
1663
+ description: "Log in to a Dineway instance"
1664
+ },
1665
+ args: {
1666
+ url: {
1667
+ type: "string",
1668
+ alias: "u",
1669
+ description: "Dineway instance URL",
1670
+ default: "http://localhost:4321"
1671
+ },
1672
+ header: {
1673
+ type: "string",
1674
+ alias: "H",
1675
+ description: "Custom header \"Name: Value\" (repeatable, or use DINEWAY_HEADERS env)"
1676
+ }
1677
+ },
1678
+ async run({ args }) {
1679
+ const baseUrl = args.url || "http://localhost:4321";
1680
+ consola$1.start(`Connecting to ${baseUrl}...`);
1681
+ const customHeaders = resolveCustomHeaders();
1682
+ let headerFetch = createHeaderAwareFetch(customHeaders);
1683
+ try {
1684
+ let res = await headerFetch(new URL("/_dineway/.well-known/auth", baseUrl), { redirect: "manual" });
1685
+ if (isRedirectResponse(res)) {
1686
+ printExternalAuthGuidance(baseUrl);
1687
+ return;
1688
+ }
1689
+ if (!res.ok) {
1690
+ if (res.status === 404) {
1691
+ if (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1")) {
1692
+ consola$1.info("Auth discovery not available. Trying dev bypass...");
1693
+ const bypassRes = await fetch(new URL("/_dineway/api/auth/dev-bypass", baseUrl), { redirect: "manual" });
1694
+ if (bypassRes.status === 302 || bypassRes.ok) consola$1.success("Dev bypass available. Client will authenticate automatically.");
1695
+ else consola$1.error("Could not authenticate. Is the dev server running?");
1696
+ } else consola$1.error("Auth discovery endpoint not found. Is this a Dineway instance?");
1697
+ return;
1698
+ }
1699
+ consola$1.error(`Discovery failed: ${res.status} ${res.statusText}`);
1700
+ process.exit(2);
1701
+ }
1702
+ const discovery = await res.json();
1703
+ consola$1.success(`Connected to ${discovery.instance?.name || "Dineway"}`);
1704
+ const deviceFlow = discovery.auth?.methods?.device_flow;
1705
+ if (!deviceFlow) {
1706
+ consola$1.info("Device Flow is not available for this instance.");
1707
+ consola$1.info("Generate an API token in Settings > API Tokens");
1708
+ consola$1.info(`Then run: ${pc.cyan(`dineway --token <token> --url ${baseUrl}`)}`);
1709
+ return;
1710
+ }
1711
+ const codeRes = await headerFetch(new URL(deviceFlow.device_authorization_endpoint, baseUrl), {
1712
+ method: "POST",
1713
+ headers: {
1714
+ "Content-Type": "application/json",
1715
+ "X-Dineway-Request": "1"
1716
+ },
1717
+ body: JSON.stringify({
1718
+ client_id: "dineway-cli",
1719
+ scope: "admin"
1720
+ })
1721
+ });
1722
+ if (!codeRes.ok) {
1723
+ consola$1.error(`Failed to request device code: ${codeRes.status}`);
1724
+ process.exit(2);
1725
+ }
1726
+ const deviceCode = (await codeRes.json()).data;
1727
+ console.log();
1728
+ consola$1.info(`Open your browser to:`);
1729
+ console.log(` ${pc.cyan(pc.bold(deviceCode.verification_uri))}`);
1730
+ console.log();
1731
+ consola$1.info(`Enter code: ${pc.yellow(pc.bold(deviceCode.user_code))}`);
1732
+ console.log();
1733
+ try {
1734
+ const { execFile } = await import("node:child_process");
1735
+ if (process.platform === "darwin") execFile("open", [deviceCode.verification_uri]);
1736
+ else if (process.platform === "win32") execFile("cmd", [
1737
+ "/c",
1738
+ "start",
1739
+ "",
1740
+ deviceCode.verification_uri
1741
+ ]);
1742
+ else execFile("xdg-open", [deviceCode.verification_uri]);
1743
+ } catch {}
1744
+ consola$1.start("Waiting for authorization...");
1745
+ const tokenResult = await pollForToken(new URL(deviceFlow.token_endpoint, baseUrl).toString(), deviceCode.device_code, deviceCode.interval, deviceCode.expires_in, headerFetch);
1746
+ let userEmail = "unknown";
1747
+ let userRole = "unknown";
1748
+ try {
1749
+ const meRes = await headerFetch(new URL("/_dineway/api/auth/me", baseUrl), { headers: { Authorization: `Bearer ${tokenResult.access_token}` } });
1750
+ if (meRes.ok) {
1751
+ const me = (await meRes.json()).data;
1752
+ userEmail = me.email || "unknown";
1753
+ userRole = (me.role ? {
1754
+ 10: "subscriber",
1755
+ 20: "contributor",
1756
+ 30: "author",
1757
+ 40: "editor",
1758
+ 50: "admin"
1759
+ }[me.role] : void 0) || "unknown";
1760
+ }
1761
+ } catch {}
1762
+ const expiresAt = new Date(Date.now() + tokenResult.expires_in * 1e3).toISOString();
1763
+ const hasCustomHeaders = Object.keys(customHeaders).length > 0;
1764
+ saveCredentials(baseUrl, {
1765
+ accessToken: tokenResult.access_token,
1766
+ refreshToken: tokenResult.refresh_token,
1767
+ expiresAt,
1768
+ ...hasCustomHeaders ? { customHeaders } : {},
1769
+ user: {
1770
+ email: userEmail,
1771
+ role: userRole
1772
+ }
1773
+ });
1774
+ consola$1.success(`Logged in as ${pc.bold(userEmail)} (${userRole})`);
1775
+ consola$1.info(`Token saved to ${pc.dim(resolveCredentialKey(baseUrl))}`);
1776
+ } catch (error) {
1777
+ consola$1.error(error instanceof Error ? error.message : "Login failed");
1778
+ process.exit(2);
1779
+ }
1780
+ }
1781
+ });
1782
+ const logoutCommand = defineCommand({
1783
+ meta: {
1784
+ name: "logout",
1785
+ description: "Log out of a Dineway instance"
1786
+ },
1787
+ args: { url: {
1788
+ type: "string",
1789
+ alias: "u",
1790
+ description: "Dineway instance URL",
1791
+ default: "http://localhost:4321"
1792
+ } },
1793
+ async run({ args }) {
1794
+ const baseUrl = args.url || "http://localhost:4321";
1795
+ const cred = getCredentials(baseUrl);
1796
+ if (!cred) {
1797
+ consola$1.info("No stored credentials found for this instance.");
1798
+ return;
1799
+ }
1800
+ const headerFetch = createHeaderAwareFetch(cred.customHeaders ?? {});
1801
+ try {
1802
+ await headerFetch(new URL("/_dineway/api/oauth/token/revoke", baseUrl), {
1803
+ method: "POST",
1804
+ headers: { "Content-Type": "application/json" },
1805
+ body: JSON.stringify({ token: cred.refreshToken })
1806
+ });
1807
+ } catch {}
1808
+ removeCredentials(baseUrl);
1809
+ consola$1.success("Logged out successfully.");
1810
+ }
1811
+ });
1812
+ const whoamiCommand = defineCommand({
1813
+ meta: {
1814
+ name: "whoami",
1815
+ description: "Show current user and auth method"
1816
+ },
1817
+ args: {
1818
+ url: {
1819
+ type: "string",
1820
+ alias: "u",
1821
+ description: "Dineway instance URL",
1822
+ default: "http://localhost:4321"
1823
+ },
1824
+ token: {
1825
+ type: "string",
1826
+ alias: "t",
1827
+ description: "Auth token"
1828
+ },
1829
+ json: {
1830
+ type: "boolean",
1831
+ description: "Output as JSON"
1832
+ }
1833
+ },
1834
+ async run({ args }) {
1835
+ configureOutputMode(args);
1836
+ const baseUrl = args.url || "http://localhost:4321";
1837
+ let token = args.token || process.env["DINEWAY_TOKEN"];
1838
+ let authMethod = token ? "token" : "none";
1839
+ let storedHeaders = {};
1840
+ if (!token) {
1841
+ const cred = getCredentials(baseUrl);
1842
+ if (cred) {
1843
+ token = cred.accessToken;
1844
+ authMethod = "stored";
1845
+ storedHeaders = cred.customHeaders ?? {};
1846
+ if (new Date(cred.expiresAt) < /* @__PURE__ */ new Date()) {
1847
+ const headerFetch = createHeaderAwareFetch(storedHeaders);
1848
+ try {
1849
+ const refreshRes = await headerFetch(new URL("/_dineway/api/oauth/token/refresh", baseUrl), {
1850
+ method: "POST",
1851
+ headers: { "Content-Type": "application/json" },
1852
+ body: JSON.stringify({
1853
+ refresh_token: cred.refreshToken,
1854
+ grant_type: "refresh_token"
1855
+ })
1856
+ });
1857
+ if (refreshRes.ok) {
1858
+ const refreshed = await refreshRes.json();
1859
+ token = refreshed.access_token;
1860
+ saveCredentials(baseUrl, {
1861
+ ...cred,
1862
+ accessToken: refreshed.access_token,
1863
+ expiresAt: new Date(Date.now() + refreshed.expires_in * 1e3).toISOString()
1864
+ });
1865
+ } else {
1866
+ consola$1.warn("Stored token expired and refresh failed. Run: dineway login");
1867
+ process.exit(2);
1868
+ }
1869
+ } catch {
1870
+ consola$1.warn("Stored token expired. Run: dineway login");
1871
+ process.exit(2);
1872
+ }
1873
+ }
1874
+ }
1875
+ }
1876
+ if (!token) {
1877
+ if (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1")) {
1878
+ authMethod = "dev-bypass";
1879
+ consola$1.info(`Auth method: ${pc.cyan("dev-bypass")}`);
1880
+ consola$1.info("No stored credentials. Client will use dev bypass for localhost.");
1881
+ return;
1882
+ }
1883
+ consola$1.error("Not logged in. Run: dineway login");
1884
+ process.exit(2);
1885
+ }
1886
+ const headerFetch = createHeaderAwareFetch(storedHeaders);
1887
+ try {
1888
+ const meRes = await headerFetch(new URL("/_dineway/api/auth/me", baseUrl), { headers: { Authorization: `Bearer ${token}` } });
1889
+ if (!meRes.ok) {
1890
+ if (meRes.status === 401) {
1891
+ consola$1.error("Token is invalid or expired. Run: dineway login");
1892
+ process.exit(1);
1893
+ }
1894
+ consola$1.error(`Failed to fetch user info: ${meRes.status}`);
1895
+ process.exit(1);
1896
+ }
1897
+ const me = (await meRes.json()).data;
1898
+ const roleNames = {
1899
+ 10: "subscriber",
1900
+ 20: "contributor",
1901
+ 30: "author",
1902
+ 40: "editor",
1903
+ 50: "admin"
1904
+ };
1905
+ if (args.json) console.log(JSON.stringify({
1906
+ id: me.id,
1907
+ email: me.email,
1908
+ name: me.name,
1909
+ role: roleNames[me.role] || `unknown (${me.role})`,
1910
+ authMethod,
1911
+ url: baseUrl
1912
+ }));
1913
+ else {
1914
+ consola$1.info(`Email: ${pc.bold(me.email)}`);
1915
+ if (me.name) consola$1.info(`Name: ${me.name}`);
1916
+ consola$1.info(`Role: ${pc.cyan(roleNames[me.role] || `unknown (${me.role})`)}`);
1917
+ consola$1.info(`Auth: ${pc.dim(authMethod)}`);
1918
+ consola$1.info(`URL: ${pc.dim(baseUrl)}`);
1919
+ }
1920
+ } catch (error) {
1921
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
1922
+ process.exit(1);
1923
+ }
1924
+ }
1925
+ });
1926
+
1927
+ //#endregion
1928
+ //#region src/cli/commands/media.ts
1929
+ /**
1930
+ * dineway media
1931
+ *
1932
+ * Manage media items via the Dineway API
1933
+ */
1934
+ const listCommand$3 = defineCommand({
1935
+ meta: {
1936
+ name: "list",
1937
+ description: "List media items"
1938
+ },
1939
+ args: {
1940
+ ...connectionArgs,
1941
+ mime: {
1942
+ type: "string",
1943
+ description: "Filter by MIME type (e.g., image/png)"
1944
+ },
1945
+ limit: {
1946
+ type: "string",
1947
+ description: "Number of items to return"
1948
+ },
1949
+ cursor: {
1950
+ type: "string",
1951
+ description: "Pagination cursor"
1952
+ }
1953
+ },
1954
+ async run({ args }) {
1955
+ configureOutputMode(args);
1956
+ const client = createClientFromArgs(args);
1957
+ try {
1958
+ output(await client.mediaList({
1959
+ mimeType: args.mime,
1960
+ limit: args.limit ? Number(args.limit) : void 0,
1961
+ cursor: args.cursor
1962
+ }), args);
1963
+ } catch (error) {
1964
+ consola$1.error("Failed to list media:", error instanceof Error ? error.message : error);
1965
+ process.exit(1);
1966
+ }
1967
+ }
1968
+ });
1969
+ const uploadCommand = defineCommand({
1970
+ meta: {
1971
+ name: "upload",
1972
+ description: "Upload a media file"
1973
+ },
1974
+ args: {
1975
+ file: {
1976
+ type: "positional",
1977
+ description: "Path to the file to upload",
1978
+ required: true
1979
+ },
1980
+ ...connectionArgs,
1981
+ alt: {
1982
+ type: "string",
1983
+ description: "Alt text for the media item"
1984
+ },
1985
+ caption: {
1986
+ type: "string",
1987
+ description: "Caption for the media item"
1988
+ }
1989
+ },
1990
+ async run({ args }) {
1991
+ configureOutputMode(args);
1992
+ const client = createClientFromArgs(args);
1993
+ const filename = basename(args.file);
1994
+ consola$1.start(`Uploading ${filename}...`);
1995
+ try {
1996
+ const buffer = await readFile(args.file);
1997
+ const result = await client.mediaUpload(buffer, filename, {
1998
+ alt: args.alt,
1999
+ caption: args.caption
2000
+ });
2001
+ consola$1.success(`Uploaded ${filename}`);
2002
+ output(result, args);
2003
+ } catch (error) {
2004
+ consola$1.error("Failed to upload:", error instanceof Error ? error.message : error);
2005
+ process.exit(1);
2006
+ }
2007
+ }
2008
+ });
2009
+ const getCommand$2 = defineCommand({
2010
+ meta: {
2011
+ name: "get",
2012
+ description: "Get a media item"
2013
+ },
2014
+ args: {
2015
+ id: {
2016
+ type: "positional",
2017
+ description: "Media item ID",
2018
+ required: true
2019
+ },
2020
+ ...connectionArgs
2021
+ },
2022
+ async run({ args }) {
2023
+ configureOutputMode(args);
2024
+ const client = createClientFromArgs(args);
2025
+ try {
2026
+ output(await client.mediaGet(args.id), args);
2027
+ } catch (error) {
2028
+ consola$1.error("Failed to get media:", error instanceof Error ? error.message : error);
2029
+ process.exit(1);
2030
+ }
2031
+ }
2032
+ });
2033
+ const deleteCommand$1 = defineCommand({
2034
+ meta: {
2035
+ name: "delete",
2036
+ description: "Delete a media item"
2037
+ },
2038
+ args: {
2039
+ id: {
2040
+ type: "positional",
2041
+ description: "Media item ID",
2042
+ required: true
2043
+ },
2044
+ ...connectionArgs
2045
+ },
2046
+ async run({ args }) {
2047
+ configureOutputMode(args);
2048
+ const client = createClientFromArgs(args);
2049
+ try {
2050
+ await client.mediaDelete(args.id);
2051
+ if (args.json) output({ deleted: true }, args);
2052
+ else consola$1.success(`Deleted media item ${args.id}`);
2053
+ } catch (error) {
2054
+ consola$1.error("Failed to delete media:", error instanceof Error ? error.message : error);
2055
+ process.exit(1);
2056
+ }
2057
+ }
2058
+ });
2059
+ const mediaCommand = defineCommand({
2060
+ meta: {
2061
+ name: "media",
2062
+ description: "Manage media items"
2063
+ },
2064
+ subCommands: {
2065
+ list: listCommand$3,
2066
+ upload: uploadCommand,
2067
+ get: getCommand$2,
2068
+ delete: deleteCommand$1
2069
+ }
2070
+ });
2071
+
2072
+ //#endregion
2073
+ //#region src/cli/commands/menu.ts
2074
+ /**
2075
+ * dineway menu
2076
+ *
2077
+ * Manage menus via the Dineway REST API.
2078
+ */
2079
+ const listCommand$2 = defineCommand({
2080
+ meta: {
2081
+ name: "list",
2082
+ description: "List all menus"
2083
+ },
2084
+ args: { ...connectionArgs },
2085
+ async run({ args }) {
2086
+ configureOutputMode(args);
2087
+ try {
2088
+ output(await createClientFromArgs(args).menus(), args);
2089
+ } catch (error) {
2090
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
2091
+ process.exit(1);
2092
+ }
2093
+ }
2094
+ });
2095
+ const getCommand$1 = defineCommand({
2096
+ meta: {
2097
+ name: "get",
2098
+ description: "Get a menu with its items"
2099
+ },
2100
+ args: {
2101
+ name: {
2102
+ type: "positional",
2103
+ description: "Menu name",
2104
+ required: true
2105
+ },
2106
+ ...connectionArgs
2107
+ },
2108
+ async run({ args }) {
2109
+ configureOutputMode(args);
2110
+ try {
2111
+ output(await createClientFromArgs(args).menu(args.name), args);
2112
+ } catch (error) {
2113
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
2114
+ process.exit(1);
2115
+ }
2116
+ }
2117
+ });
2118
+ const menuCommand = defineCommand({
2119
+ meta: {
2120
+ name: "menu",
2121
+ description: "Manage menus"
2122
+ },
2123
+ subCommands: {
2124
+ list: listCommand$2,
2125
+ get: getCommand$1
2126
+ }
2127
+ });
2128
+
2129
+ //#endregion
2130
+ //#region src/cli/commands/bundle-utils.ts
2131
+ /**
2132
+ * Bundle utility functions
2133
+ *
2134
+ * Shared logic extracted from the bundle command so it can be tested
2135
+ * without the CLI harness and tsdown dependency.
2136
+ */
2137
+ const MAX_BUNDLE_SIZE = 5 * 1024 * 1024;
2138
+ const MAX_SCREENSHOTS = 5;
2139
+ const MAX_SCREENSHOT_WIDTH = 1920;
2140
+ const MAX_SCREENSHOT_HEIGHT = 1080;
2141
+ const ICON_SIZE = 256;
2142
+ /** Matches require("node:xxx") / require("xxx") / import("node:xxx") in bundled output */
2143
+ const NODE_BUILTIN_IMPORT_RE = /(?:import|require)\s*\(?["'](?:node:)?([a-z_]+)["']\)?/g;
2144
+ const LEADING_DOT_SLASH_RE = /^\.\//;
2145
+ const DIST_PREFIX_RE = /^dist\//;
2146
+ const MJS_EXT_RE = /\.m?js$/;
2147
+ const BUILD_OUTPUT_EXT_RE = /\.(?:mjs|js|cjs)$/;
2148
+ const TS_TO_TSX_RE = /\.ts$/;
2149
+ /** Node.js built-in modules that shouldn't appear in sandbox code */
2150
+ const NODE_BUILTINS = new Set([
2151
+ "assert",
2152
+ "buffer",
2153
+ "child_process",
2154
+ "cluster",
2155
+ "crypto",
2156
+ "dgram",
2157
+ "dns",
2158
+ "domain",
2159
+ "events",
2160
+ "fs",
2161
+ "http",
2162
+ "http2",
2163
+ "https",
2164
+ "inspector",
2165
+ "module",
2166
+ "net",
2167
+ "os",
2168
+ "path",
2169
+ "perf_hooks",
2170
+ "process",
2171
+ "punycode",
2172
+ "querystring",
2173
+ "readline",
2174
+ "repl",
2175
+ "stream",
2176
+ "string_decoder",
2177
+ "sys",
2178
+ "timers",
2179
+ "tls",
2180
+ "trace_events",
2181
+ "tty",
2182
+ "url",
2183
+ "util",
2184
+ "v8",
2185
+ "vm",
2186
+ "wasi",
2187
+ "worker_threads",
2188
+ "zlib"
2189
+ ]);
2190
+ async function fileExists$1(path) {
2191
+ try {
2192
+ await access(path);
2193
+ return true;
2194
+ } catch {
2195
+ return false;
2196
+ }
2197
+ }
2198
+ /**
2199
+ * Read image dimensions from a buffer.
2200
+ * Returns [width, height] or null if the format is unrecognized.
2201
+ */
2202
+ function readImageDimensions(buf) {
2203
+ try {
2204
+ const result = imageSize(buf);
2205
+ if (result.width != null && result.height != null) return [result.width, result.height];
2206
+ return null;
2207
+ } catch {
2208
+ return null;
2209
+ }
2210
+ }
2211
+ /**
2212
+ * Extract manifest metadata from a ResolvedPlugin.
2213
+ * Strips functions (hooks, route handlers) and keeps only serializable metadata.
2214
+ */
2215
+ function extractManifest(plugin) {
2216
+ const hooks = [];
2217
+ for (const [name, resolved] of Object.entries(plugin.hooks)) {
2218
+ if (!resolved) continue;
2219
+ if (resolved.exclusive || resolved.priority !== 100 || resolved.timeout !== 5e3) {
2220
+ const entry = { name };
2221
+ if (resolved.exclusive) entry.exclusive = true;
2222
+ if (resolved.priority !== 100) entry.priority = resolved.priority;
2223
+ if (resolved.timeout !== 5e3) entry.timeout = resolved.timeout;
2224
+ hooks.push(entry);
2225
+ } else hooks.push(name);
2226
+ }
2227
+ return {
2228
+ id: plugin.id,
2229
+ version: plugin.version,
2230
+ capabilities: plugin.capabilities,
2231
+ allowedHosts: plugin.allowedHosts,
2232
+ storage: plugin.storage,
2233
+ hooks,
2234
+ routes: Object.keys(plugin.routes),
2235
+ admin: {
2236
+ settingsSchema: plugin.admin.settingsSchema,
2237
+ pages: plugin.admin.pages,
2238
+ widgets: plugin.admin.widgets
2239
+ }
2240
+ };
2241
+ }
2242
+ /**
2243
+ * Scan bundled code for Node.js built-in imports.
2244
+ * Matches require("node:xxx"), require("xxx"), import("node:xxx") — the patterns
2245
+ * that appear in bundled ESM/CJS output (not source-level named imports).
2246
+ * Returns deduplicated array of built-in module names found.
2247
+ */
2248
+ function findNodeBuiltinImports(code) {
2249
+ const found = [];
2250
+ NODE_BUILTIN_IMPORT_RE.lastIndex = 0;
2251
+ let match;
2252
+ while ((match = NODE_BUILTIN_IMPORT_RE.exec(code)) !== null) {
2253
+ const mod = match[1];
2254
+ if (NODE_BUILTINS.has(mod)) found.push(mod);
2255
+ }
2256
+ return [...new Set(found)];
2257
+ }
2258
+ /**
2259
+ * Find a build output file by base name, checking common extensions.
2260
+ * tsdown may output .mjs, .js, or .cjs depending on format and config.
2261
+ */
2262
+ async function findBuildOutput(dir, baseName) {
2263
+ const normalizedBaseName = baseName.replace(BUILD_OUTPUT_EXT_RE, "");
2264
+ for (const ext of [
2265
+ ".mjs",
2266
+ ".js",
2267
+ ".cjs"
2268
+ ]) {
2269
+ const candidate = join(dir, `${normalizedBaseName}${ext}`);
2270
+ if (await fileExists$1(candidate)) return candidate;
2271
+ }
2272
+ }
2273
+ /**
2274
+ * Resolve a dist/built path back to its source .ts/.tsx equivalent.
2275
+ * E.g., "./dist/index.mjs" → "src/index.ts"
2276
+ */
2277
+ async function resolveSourceEntry(pluginDir, distPath) {
2278
+ const cleaned = distPath.replace(LEADING_DOT_SLASH_RE, "");
2279
+ const direct = resolve(pluginDir, cleaned);
2280
+ if (await fileExists$1(direct)) return direct;
2281
+ const srcPath = cleaned.replace(DIST_PREFIX_RE, "src/").replace(MJS_EXT_RE, ".ts");
2282
+ const srcFull = resolve(pluginDir, srcPath);
2283
+ if (await fileExists$1(srcFull)) return srcFull;
2284
+ const tsxFull = resolve(pluginDir, srcPath.replace(TS_TO_TSX_RE, ".tsx"));
2285
+ if (await fileExists$1(tsxFull)) return tsxFull;
2286
+ }
2287
+ const TS_SOURCE_EXPORT_RE = /\.(?:ts|tsx|mts|cts|jsx)$/;
2288
+ /**
2289
+ * Find package.json exports that point to source files instead of built output.
2290
+ * Returns an array of `{ exportPath, resolvedPath }` for each offending export.
2291
+ */
2292
+ function findSourceExports(exports) {
2293
+ const issues = [];
2294
+ for (const [exportPath, exportValue] of Object.entries(exports)) {
2295
+ const resolved = typeof exportValue === "string" ? exportValue : exportValue && typeof exportValue === "object" && "import" in exportValue ? exportValue.import : null;
2296
+ if (resolved && TS_SOURCE_EXPORT_RE.test(resolved)) issues.push({
2297
+ exportPath,
2298
+ resolvedPath: resolved
2299
+ });
2300
+ }
2301
+ return issues;
2302
+ }
2303
+ /**
2304
+ * Recursively calculate the total size of all files in a directory.
2305
+ */
2306
+ async function calculateDirectorySize(dir) {
2307
+ let total = 0;
2308
+ const items = await readdir(dir, { withFileTypes: true });
2309
+ for (const item of items) {
2310
+ const fullPath = join(dir, item.name);
2311
+ if (item.isFile()) {
2312
+ const s = await stat(fullPath);
2313
+ total += s.size;
2314
+ } else if (item.isDirectory()) total += await calculateDirectorySize(fullPath);
2315
+ }
2316
+ return total;
2317
+ }
2318
+ /**
2319
+ * Create a gzipped tarball from a directory.
2320
+ */
2321
+ async function createTarball(sourceDir, outputPath) {
2322
+ const { createGzip } = await import("node:zlib");
2323
+ await pipeline(packTar(sourceDir), createGzip({ level: 9 }), createWriteStream(outputPath));
2324
+ }
2325
+
2326
+ //#endregion
2327
+ //#region src/cli/commands/bundle.ts
2328
+ /**
2329
+ * dineway plugin bundle
2330
+ *
2331
+ * Produces a publishable plugin tarball from a plugin source directory.
2332
+ *
2333
+ * Steps:
2334
+ * 1. Resolve plugin entrypoint (finds definePlugin() export)
2335
+ * 2. Bundle backend code with tsdown → backend.js (single ES module, tree-shaken)
2336
+ * 3. Bundle admin code if present → admin.js
2337
+ * 4. Extract manifest from definePlugin() → manifest.json
2338
+ * 5. Collect assets (README.md, icon.png, screenshots/)
2339
+ * 6. Validate bundle (manifest schema, size limits, no Node.js builtins)
2340
+ * 7. Create tarball ({id}-{version}.tar.gz)
2341
+ */
2342
+ var bundle_exports = /* @__PURE__ */ __exportAll({ bundleCommand: () => bundleCommand });
2343
+ const TS_EXT_RE = /\.tsx?$/;
2344
+ const SLASH_RE = /\//g;
2345
+ const LEADING_AT_RE = /^@/;
2346
+ const DINEWAY_SCOPE_RE = /^@dineway-ai\//;
2347
+ const bundleCommand = defineCommand({
2348
+ meta: {
2349
+ name: "bundle",
2350
+ description: "Bundle a plugin for marketplace distribution"
2351
+ },
2352
+ args: {
2353
+ dir: {
2354
+ type: "string",
2355
+ description: "Plugin directory (default: current directory)",
2356
+ default: process.cwd()
2357
+ },
2358
+ outDir: {
2359
+ type: "string",
2360
+ alias: "o",
2361
+ description: "Output directory for the tarball (default: ./dist)",
2362
+ default: "dist"
2363
+ },
2364
+ validateOnly: {
2365
+ type: "boolean",
2366
+ description: "Run validation only, skip tarball creation",
2367
+ default: false
2368
+ }
2369
+ },
2370
+ async run({ args }) {
2371
+ const pluginDir = resolve(args.dir);
2372
+ const outDir = resolve(pluginDir, args.outDir);
2373
+ const validateOnly = args.validateOnly;
2374
+ consola.start(validateOnly ? "Validating plugin..." : "Bundling plugin...");
2375
+ const pkgPath = join(pluginDir, "package.json");
2376
+ if (!await fileExists$1(pkgPath)) {
2377
+ consola.error("No package.json found in", pluginDir);
2378
+ process.exit(1);
2379
+ }
2380
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
2381
+ let backendEntry;
2382
+ let adminEntry;
2383
+ if (pkg.exports) {
2384
+ const sandboxExport = pkg.exports["./sandbox"];
2385
+ if (typeof sandboxExport === "string") backendEntry = await resolveSourceEntry(pluginDir, sandboxExport);
2386
+ else if (sandboxExport && typeof sandboxExport === "object" && "import" in sandboxExport) backendEntry = await resolveSourceEntry(pluginDir, sandboxExport.import);
2387
+ const adminExport = pkg.exports["./admin"];
2388
+ if (typeof adminExport === "string") adminEntry = await resolveSourceEntry(pluginDir, adminExport);
2389
+ else if (adminExport && typeof adminExport === "object" && "import" in adminExport) adminEntry = await resolveSourceEntry(pluginDir, adminExport.import);
2390
+ }
2391
+ if (!backendEntry) {
2392
+ const defaultSandbox = join(pluginDir, "src/sandbox-entry.ts");
2393
+ if (await fileExists$1(defaultSandbox)) backendEntry = defaultSandbox;
2394
+ }
2395
+ let mainEntry;
2396
+ if (pkg.exports?.["."] !== void 0) {
2397
+ const mainExport = pkg.exports["."];
2398
+ if (typeof mainExport === "string") mainEntry = await resolveSourceEntry(pluginDir, mainExport);
2399
+ else if (mainExport && typeof mainExport === "object" && "import" in mainExport) mainEntry = await resolveSourceEntry(pluginDir, mainExport.import);
2400
+ }
2401
+ if (!mainEntry && pkg.main) mainEntry = await resolveSourceEntry(pluginDir, pkg.main);
2402
+ if (!mainEntry) {
2403
+ const defaultMain = join(pluginDir, "src/index.ts");
2404
+ if (await fileExists$1(defaultMain)) mainEntry = defaultMain;
2405
+ }
2406
+ if (!mainEntry) {
2407
+ consola.error("Cannot find plugin entrypoint. Expected src/index.ts or main/exports in package.json");
2408
+ process.exit(1);
2409
+ }
2410
+ consola.info(`Main entry: ${mainEntry}`);
2411
+ if (backendEntry) consola.info(`Backend entry: ${backendEntry}`);
2412
+ if (adminEntry) consola.info(`Admin entry: ${adminEntry}`);
2413
+ consola.start("Extracting plugin manifest...");
2414
+ const { build } = await import("tsdown");
2415
+ const tmpDir = join(pluginDir, ".dineway-bundle-tmp");
2416
+ try {
2417
+ await mkdir(tmpDir, { recursive: true });
2418
+ const mainOutDir = join(tmpDir, "main");
2419
+ await build({
2420
+ config: false,
2421
+ entry: [mainEntry],
2422
+ format: "esm",
2423
+ outDir: mainOutDir,
2424
+ dts: false,
2425
+ platform: "node",
2426
+ external: ["dineway", DINEWAY_SCOPE_RE]
2427
+ });
2428
+ const pluginNodeModules = join(pluginDir, "node_modules");
2429
+ const tmpNodeModules = join(mainOutDir, "node_modules");
2430
+ if (await fileExists$1(pluginNodeModules)) await symlink(pluginNodeModules, tmpNodeModules, "junction");
2431
+ const mainOutputPath = await findBuildOutput(mainOutDir, basename(mainEntry).replace(TS_EXT_RE, ""));
2432
+ if (!mainOutputPath) {
2433
+ consola.error("Failed to build main entry — no output found in", mainOutDir);
2434
+ process.exit(1);
2435
+ }
2436
+ const pluginModule = await import(mainOutputPath);
2437
+ let resolvedPlugin;
2438
+ if (typeof pluginModule.createPlugin === "function") resolvedPlugin = pluginModule.createPlugin();
2439
+ else if (typeof pluginModule.default === "function") resolvedPlugin = pluginModule.default();
2440
+ else if (typeof pluginModule.default === "object" && pluginModule.default !== null) {
2441
+ const defaultExport = pluginModule.default;
2442
+ if ("id" in defaultExport && "version" in defaultExport) resolvedPlugin = defaultExport;
2443
+ }
2444
+ if (!resolvedPlugin) for (const [key, value] of Object.entries(pluginModule)) {
2445
+ if (key === "default" || typeof value !== "function") continue;
2446
+ try {
2447
+ const result = value();
2448
+ if (result && typeof result === "object" && "id" in result && "version" in result) {
2449
+ resolvedPlugin = {
2450
+ id: result.id,
2451
+ version: result.version,
2452
+ capabilities: result.capabilities ?? [],
2453
+ allowedHosts: result.allowedHosts ?? [],
2454
+ storage: result.storage ?? {},
2455
+ hooks: {},
2456
+ routes: {},
2457
+ admin: {
2458
+ pages: result.adminPages,
2459
+ widgets: result.adminWidgets
2460
+ }
2461
+ };
2462
+ if (backendEntry) {
2463
+ const backendProbeDir = join(tmpDir, "backend-probe");
2464
+ const probeShimDir = join(tmpDir, "probe-shims");
2465
+ await mkdir(probeShimDir, { recursive: true });
2466
+ await writeFile(join(probeShimDir, "dineway.mjs"), "export const definePlugin = (d) => d;\n");
2467
+ await build({
2468
+ config: false,
2469
+ entry: [backendEntry],
2470
+ format: "esm",
2471
+ outDir: backendProbeDir,
2472
+ dts: false,
2473
+ platform: "neutral",
2474
+ external: [],
2475
+ alias: { dineway: join(probeShimDir, "dineway.mjs") },
2476
+ treeshake: true
2477
+ });
2478
+ const backendProbePath = await findBuildOutput(backendProbeDir, basename(backendEntry).replace(TS_EXT_RE, ""));
2479
+ if (backendProbePath) {
2480
+ const standardDef = (await import(backendProbePath)).default ?? {};
2481
+ const hooks = standardDef.hooks;
2482
+ const routes = standardDef.routes;
2483
+ if (hooks) for (const hookName of Object.keys(hooks)) {
2484
+ const hookEntry = hooks[hookName];
2485
+ const isConfig = typeof hookEntry === "object" && hookEntry !== null && "handler" in hookEntry;
2486
+ const config = isConfig ? hookEntry : {};
2487
+ resolvedPlugin.hooks[hookName] = {
2488
+ handler: isConfig ? hookEntry.handler : hookEntry,
2489
+ priority: config.priority ?? 100,
2490
+ timeout: config.timeout ?? 5e3,
2491
+ dependencies: config.dependencies ?? [],
2492
+ errorPolicy: config.errorPolicy ?? "abort",
2493
+ exclusive: config.exclusive ?? false,
2494
+ pluginId: result.id
2495
+ };
2496
+ }
2497
+ if (routes) for (const [name, route] of Object.entries(routes)) {
2498
+ const routeObj = route;
2499
+ resolvedPlugin.routes[name] = {
2500
+ handler: routeObj.handler,
2501
+ public: routeObj.public
2502
+ };
2503
+ }
2504
+ }
2505
+ }
2506
+ break;
2507
+ }
2508
+ } catch {}
2509
+ }
2510
+ if (!resolvedPlugin?.id || !resolvedPlugin?.version) {
2511
+ consola.error("Could not extract plugin definition. Expected one of:\n - createPlugin() export (native format)\n - Descriptor factory function returning { id, version, ... } (standard format)");
2512
+ process.exit(1);
2513
+ }
2514
+ const manifest = extractManifest(resolvedPlugin);
2515
+ if (resolvedPlugin.admin?.entry) {
2516
+ consola.error("Plugin declares adminEntry — React admin components require native/trusted mode. Use Block Kit for sandboxed admin pages, or remove adminEntry.");
2517
+ process.exit(1);
2518
+ }
2519
+ if (resolvedPlugin.admin?.portableTextBlocks && resolvedPlugin.admin.portableTextBlocks.length > 0) {
2520
+ consola.error("Plugin declares portableTextBlocks — these require native/trusted mode and cannot be bundled for the marketplace.");
2521
+ process.exit(1);
2522
+ }
2523
+ consola.success(`Plugin: ${manifest.id}@${manifest.version}`);
2524
+ consola.info(` Capabilities: ${manifest.capabilities.length > 0 ? manifest.capabilities.join(", ") : "(none)"}`);
2525
+ consola.info(` Hooks: ${manifest.hooks.length > 0 ? manifest.hooks.map((h) => typeof h === "string" ? h : h.name).join(", ") : "(none)"}`);
2526
+ consola.info(` Routes: ${manifest.routes.length > 0 ? manifest.routes.map((r) => typeof r === "string" ? r : r.name).join(", ") : "(none)"}`);
2527
+ const bundleDir = join(tmpDir, "bundle");
2528
+ await mkdir(bundleDir, { recursive: true });
2529
+ if (backendEntry) {
2530
+ consola.start("Bundling backend...");
2531
+ const shimDir = join(tmpDir, "shims");
2532
+ await mkdir(shimDir, { recursive: true });
2533
+ await writeFile(join(shimDir, "dineway.mjs"), "export const definePlugin = (d) => d;\n");
2534
+ await build({
2535
+ config: false,
2536
+ entry: [backendEntry],
2537
+ format: "esm",
2538
+ outDir: join(tmpDir, "backend"),
2539
+ dts: false,
2540
+ platform: "neutral",
2541
+ external: [],
2542
+ alias: { dineway: join(shimDir, "dineway.mjs") },
2543
+ minify: true,
2544
+ treeshake: true
2545
+ });
2546
+ const backendBaseName = basename(backendEntry).replace(TS_EXT_RE, "");
2547
+ const backendOutputPath = await findBuildOutput(join(tmpDir, "backend"), backendBaseName);
2548
+ if (backendOutputPath) {
2549
+ await copyFile(backendOutputPath, join(bundleDir, "backend.js"));
2550
+ consola.success("Built backend.js");
2551
+ } else {
2552
+ consola.error("Backend build produced no output");
2553
+ process.exit(1);
2554
+ }
2555
+ } else {
2556
+ consola.warn("No sandbox entry found — bundle will have no backend.js");
2557
+ consola.warn(" Add a \"sandbox-entry.ts\" in src/ or a \"./sandbox\" export in package.json");
2558
+ }
2559
+ if (adminEntry) {
2560
+ consola.start("Bundling admin...");
2561
+ await build({
2562
+ config: false,
2563
+ entry: [adminEntry],
2564
+ format: "esm",
2565
+ outDir: join(tmpDir, "admin"),
2566
+ dts: false,
2567
+ platform: "neutral",
2568
+ external: [],
2569
+ minify: true,
2570
+ treeshake: true
2571
+ });
2572
+ const adminBaseName = basename(adminEntry).replace(TS_EXT_RE, "");
2573
+ const adminOutputPath = await findBuildOutput(join(tmpDir, "admin"), adminBaseName);
2574
+ if (adminOutputPath) {
2575
+ await copyFile(adminOutputPath, join(bundleDir, "admin.js"));
2576
+ consola.success("Built admin.js");
2577
+ }
2578
+ }
2579
+ await writeFile(join(bundleDir, "manifest.json"), JSON.stringify(manifest, null, 2));
2580
+ consola.start("Collecting assets...");
2581
+ const readmePath = join(pluginDir, "README.md");
2582
+ if (await fileExists$1(readmePath)) {
2583
+ await copyFile(readmePath, join(bundleDir, "README.md"));
2584
+ consola.success("Included README.md");
2585
+ }
2586
+ const iconPath = join(pluginDir, "icon.png");
2587
+ if (await fileExists$1(iconPath)) {
2588
+ const dims = readImageDimensions(await readFile(iconPath));
2589
+ if (!dims) consola.warn("icon.png is not a valid PNG — skipping");
2590
+ else if (dims[0] !== ICON_SIZE || dims[1] !== ICON_SIZE) {
2591
+ consola.warn(`icon.png is ${dims[0]}x${dims[1]}, expected ${ICON_SIZE}x${ICON_SIZE} — including anyway`);
2592
+ await copyFile(iconPath, join(bundleDir, "icon.png"));
2593
+ } else {
2594
+ await copyFile(iconPath, join(bundleDir, "icon.png"));
2595
+ consola.success("Included icon.png");
2596
+ }
2597
+ }
2598
+ const screenshotsDir = join(pluginDir, "screenshots");
2599
+ if (await fileExists$1(screenshotsDir)) {
2600
+ const screenshotFiles = (await readdir(screenshotsDir)).filter((f) => {
2601
+ const ext = extname(f).toLowerCase();
2602
+ return ext === ".png" || ext === ".jpg" || ext === ".jpeg";
2603
+ }).toSorted().slice(0, MAX_SCREENSHOTS);
2604
+ if (screenshotFiles.length > 0) {
2605
+ await mkdir(join(bundleDir, "screenshots"), { recursive: true });
2606
+ for (const file of screenshotFiles) {
2607
+ const filePath = join(screenshotsDir, file);
2608
+ const dims = readImageDimensions(await readFile(filePath));
2609
+ if (!dims) {
2610
+ consola.warn(`screenshots/${file} — cannot read dimensions, skipping`);
2611
+ continue;
2612
+ }
2613
+ if (dims[0] > MAX_SCREENSHOT_WIDTH || dims[1] > MAX_SCREENSHOT_HEIGHT) consola.warn(`screenshots/${file} is ${dims[0]}x${dims[1]}, max ${MAX_SCREENSHOT_WIDTH}x${MAX_SCREENSHOT_HEIGHT} — including anyway`);
2614
+ await copyFile(filePath, join(bundleDir, "screenshots", file));
2615
+ }
2616
+ consola.success(`Included ${screenshotFiles.length} screenshot(s)`);
2617
+ }
2618
+ }
2619
+ consola.start("Validating bundle...");
2620
+ let hasErrors = false;
2621
+ if (pkg.exports) for (const issue of findSourceExports(pkg.exports)) {
2622
+ consola.error(`Export "${issue.exportPath}" points to source (${issue.resolvedPath}). Package exports must point to built files (e.g. dist/*.mjs). Add a build step and update the exports map.`);
2623
+ hasErrors = true;
2624
+ }
2625
+ const backendPath = join(bundleDir, "backend.js");
2626
+ if (await fileExists$1(backendPath)) {
2627
+ const builtins = findNodeBuiltinImports(await readFile(backendPath, "utf-8"));
2628
+ if (builtins.length > 0) {
2629
+ consola.error(`backend.js imports Node.js built-in modules: ${builtins.join(", ")}`);
2630
+ consola.error("Sandboxed plugins cannot use Node.js APIs");
2631
+ hasErrors = true;
2632
+ }
2633
+ }
2634
+ if (manifest.capabilities.includes("network:fetch:any")) consola.warn("Plugin declares unrestricted network access (network:fetch:any) — it can make requests to any host");
2635
+ else if (manifest.capabilities.includes("network:fetch") && manifest.allowedHosts.length === 0) consola.warn("Plugin declares network:fetch capability but no allowedHosts — all fetch requests will be blocked");
2636
+ if (resolvedPlugin.admin?.portableTextBlocks && resolvedPlugin.admin.portableTextBlocks.length > 0) consola.warn("Plugin declares portableTextBlocks — these require trusted mode and will be ignored in sandboxed plugins");
2637
+ if (resolvedPlugin.admin?.entry) consola.warn("Plugin declares admin.entry — custom React components require trusted mode. Use Block Kit for sandboxed admin pages");
2638
+ if (resolvedPlugin.hooks["page:fragments"]) consola.warn("Plugin declares page:fragments hook — this is trusted-only and will not work in sandboxed mode");
2639
+ const hasAdminPages = (manifest.admin?.pages?.length ?? 0) > 0;
2640
+ const hasAdminWidgets = (manifest.admin?.widgets?.length ?? 0) > 0;
2641
+ if (hasAdminPages || hasAdminWidgets) {
2642
+ if (!manifest.routes.map((r) => typeof r === "string" ? r : r.name).includes("admin")) {
2643
+ consola.error(`Plugin declares ${hasAdminPages ? "adminPages" : ""}${hasAdminPages && hasAdminWidgets ? " and " : ""}${hasAdminWidgets ? "adminWidgets" : ""} but the sandbox entry has no "admin" route. Add an admin route handler to serve Block Kit pages.`);
2644
+ hasErrors = true;
2645
+ }
2646
+ }
2647
+ const totalSize = await calculateDirectorySize(bundleDir);
2648
+ if (totalSize > MAX_BUNDLE_SIZE) {
2649
+ const sizeMB = (totalSize / 1024 / 1024).toFixed(2);
2650
+ consola.error(`Bundle size ${sizeMB}MB exceeds maximum of 5MB`);
2651
+ hasErrors = true;
2652
+ } else {
2653
+ const sizeKB = (totalSize / 1024).toFixed(1);
2654
+ consola.info(`Bundle size: ${sizeKB}KB`);
2655
+ }
2656
+ if (hasErrors) {
2657
+ consola.error("Bundle validation failed");
2658
+ process.exit(1);
2659
+ }
2660
+ consola.success("Validation passed");
2661
+ if (validateOnly) return;
2662
+ await mkdir(outDir, { recursive: true });
2663
+ const tarballName = `${manifest.id.replace(SLASH_RE, "-").replace(LEADING_AT_RE, "")}-${manifest.version}.tar.gz`;
2664
+ const tarballPath = join(outDir, tarballName);
2665
+ consola.start("Creating tarball...");
2666
+ await createTarball(bundleDir, tarballPath);
2667
+ const tarballSizeKB = ((await stat(tarballPath)).size / 1024).toFixed(1);
2668
+ const tarballBuf = await readFile(tarballPath);
2669
+ const checksum = createHash("sha256").update(tarballBuf).digest("hex");
2670
+ consola.success(`Created ${tarballName} (${tarballSizeKB}KB)`);
2671
+ consola.info(` SHA-256: ${checksum}`);
2672
+ consola.info(` Path: ${tarballPath}`);
2673
+ } finally {
2674
+ if (tmpDir.endsWith(".dineway-bundle-tmp")) await rm(tmpDir, {
2675
+ recursive: true,
2676
+ force: true
2677
+ });
2678
+ }
2679
+ }
2680
+ });
2681
+
2682
+ //#endregion
2683
+ //#region src/cli/commands/plugin-init.ts
2684
+ /**
2685
+ * dineway plugin init
2686
+ *
2687
+ * Scaffold a new Dineway plugin. Generates the standard-format boilerplate:
2688
+ * src/index.ts -- descriptor factory
2689
+ * src/sandbox-entry.ts -- definePlugin({ hooks, routes })
2690
+ * package.json
2691
+ * tsconfig.json
2692
+ *
2693
+ * Use --native to generate native-format boilerplate instead (createPlugin + React admin).
2694
+ *
2695
+ */
2696
+ const SLUG_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2697
+ const SCOPE_RE = /^@[^/]+\//;
2698
+ const pluginInitCommand = defineCommand({
2699
+ meta: {
2700
+ name: "init",
2701
+ description: "Scaffold a new plugin"
2702
+ },
2703
+ args: {
2704
+ dir: {
2705
+ type: "string",
2706
+ description: "Directory to create the plugin in (default: current directory)",
2707
+ default: "."
2708
+ },
2709
+ name: {
2710
+ type: "string",
2711
+ description: "Plugin name/id (e.g. my-plugin or @org/my-plugin)"
2712
+ },
2713
+ native: {
2714
+ type: "boolean",
2715
+ description: "Generate native-format plugin (createPlugin + React admin)",
2716
+ default: false
2717
+ }
2718
+ },
2719
+ async run({ args }) {
2720
+ const targetDir = resolve(args.dir);
2721
+ const isNative = args.native;
2722
+ let pluginName = args.name || basename(targetDir);
2723
+ if (!pluginName || pluginName === ".") pluginName = basename(resolve("."));
2724
+ const slug = pluginName.replace(SCOPE_RE, "");
2725
+ if (!SLUG_RE.test(slug)) {
2726
+ consola.error(`Invalid plugin name "${pluginName}". Use lowercase letters, numbers, and hyphens (e.g. my-plugin).`);
2727
+ process.exit(1);
2728
+ }
2729
+ const srcDir = join(targetDir, "src");
2730
+ if (await fileExists$1(join(targetDir, "package.json"))) {
2731
+ consola.error(`package.json already exists in ${targetDir}`);
2732
+ process.exit(1);
2733
+ }
2734
+ consola.start(`Scaffolding ${isNative ? "native" : "standard"} plugin: ${pluginName}`);
2735
+ await mkdir(srcDir, { recursive: true });
2736
+ if (isNative) await scaffoldNative(targetDir, srcDir, pluginName, slug);
2737
+ else await scaffoldStandard(targetDir, srcDir, pluginName, slug);
2738
+ consola.success(`Plugin scaffolded in ${targetDir}`);
2739
+ consola.info("Next steps:");
2740
+ if (args.dir !== ".") consola.info(` 1. cd ${args.dir}`);
2741
+ consola.info(` ${args.dir !== "." ? "2" : "1"}. pnpm install`);
2742
+ if (isNative) consola.info(` ${args.dir !== "." ? "3" : "2"}. Edit src/index.ts to add hooks and routes`);
2743
+ else consola.info(` ${args.dir !== "." ? "3" : "2"}. Edit src/sandbox-entry.ts to add hooks and routes`);
2744
+ consola.info(` ${args.dir !== "." ? "4" : "3"}. dineway plugin validate --dir .`);
2745
+ }
2746
+ });
2747
+ async function scaffoldStandard(targetDir, srcDir, pluginName, slug) {
2748
+ const fnName = slug.split("-").map((s, i) => i === 0 ? s : s[0].toUpperCase() + s.slice(1)).join("");
2749
+ await writeFile(join(targetDir, "package.json"), JSON.stringify({
2750
+ name: pluginName,
2751
+ version: "0.1.0",
2752
+ type: "module",
2753
+ exports: {
2754
+ ".": "./src/index.ts",
2755
+ "./sandbox": "./src/sandbox-entry.ts"
2756
+ },
2757
+ files: ["src"],
2758
+ peerDependencies: { dineway: "*" }
2759
+ }, null, " ") + "\n");
2760
+ await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify({
2761
+ compilerOptions: {
2762
+ target: "ES2022",
2763
+ module: "preserve",
2764
+ moduleResolution: "bundler",
2765
+ strict: true,
2766
+ esModuleInterop: true,
2767
+ declaration: true,
2768
+ outDir: "./dist",
2769
+ rootDir: "./src"
2770
+ },
2771
+ include: ["src/**/*"],
2772
+ exclude: ["node_modules", "dist"]
2773
+ }, null, " ") + "\n");
2774
+ await writeFile(join(srcDir, "index.ts"), `import type { PluginDescriptor } from "dineway";
2775
+
2776
+ export function ${fnName}Plugin(): PluginDescriptor {
2777
+ \treturn {
2778
+ \t\tid: "${pluginName}",
2779
+ \t\tversion: "0.1.0",
2780
+ \t\tformat: "standard",
2781
+ \t\tentrypoint: "${pluginName}/sandbox",
2782
+ \t\tcapabilities: [],
2783
+ \t};
2784
+ }
2785
+ `);
2786
+ await writeFile(join(srcDir, "sandbox-entry.ts"), `import { definePlugin } from "dineway";
2787
+ import type { PluginContext } from "dineway";
2788
+
2789
+ export default definePlugin({
2790
+ \thooks: {
2791
+ \t\t"content:afterSave": {
2792
+ \t\t\thandler: async (event: any, ctx: PluginContext) => {
2793
+ \t\t\t\tctx.log.info("Content saved", {
2794
+ \t\t\t\t\tcollection: event.collection,
2795
+ \t\t\t\t\tid: event.content.id,
2796
+ \t\t\t\t});
2797
+ \t\t\t},
2798
+ \t\t},
2799
+ \t},
2800
+ });
2801
+ `);
2802
+ }
2803
+ async function scaffoldNative(targetDir, srcDir, pluginName, slug) {
2804
+ const fnName = slug.split("-").map((s, i) => i === 0 ? s : s[0].toUpperCase() + s.slice(1)).join("");
2805
+ await writeFile(join(targetDir, "package.json"), JSON.stringify({
2806
+ name: pluginName,
2807
+ version: "0.1.0",
2808
+ type: "module",
2809
+ exports: { ".": "./src/index.ts" },
2810
+ files: ["src"],
2811
+ peerDependencies: { dineway: "*" }
2812
+ }, null, " ") + "\n");
2813
+ await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify({
2814
+ compilerOptions: {
2815
+ target: "ES2022",
2816
+ module: "preserve",
2817
+ moduleResolution: "bundler",
2818
+ strict: true,
2819
+ esModuleInterop: true,
2820
+ declaration: true,
2821
+ outDir: "./dist",
2822
+ rootDir: "./src"
2823
+ },
2824
+ include: ["src/**/*"],
2825
+ exclude: ["node_modules", "dist"]
2826
+ }, null, " ") + "\n");
2827
+ await writeFile(join(srcDir, "index.ts"), `import { definePlugin } from "dineway";
2828
+ import type { PluginDescriptor } from "dineway";
2829
+
2830
+ export function ${fnName}Plugin(): PluginDescriptor {
2831
+ \treturn {
2832
+ \t\tid: "${pluginName}",
2833
+ \t\tversion: "0.1.0",
2834
+ \t\tformat: "native",
2835
+ \t\tentrypoint: "${pluginName}",
2836
+ \t\toptions: {},
2837
+ \t};
2838
+ }
2839
+
2840
+ export function createPlugin() {
2841
+ \treturn definePlugin({
2842
+ \t\tid: "${pluginName}",
2843
+ \t\tversion: "0.1.0",
2844
+
2845
+ \t\thooks: {
2846
+ \t\t\t"content:afterSave": async (event, ctx) => {
2847
+ \t\t\t\tctx.log.info("Content saved", {
2848
+ \t\t\t\t\tcollection: event.collection,
2849
+ \t\t\t\t\tid: event.content.id,
2850
+ \t\t\t\t});
2851
+ \t\t\t},
2852
+ \t\t},
2853
+ \t});
2854
+ }
2855
+
2856
+ export default createPlugin;
2857
+ `);
2858
+ }
2859
+
2860
+ //#endregion
2861
+ //#region src/cli/commands/plugin-validate.ts
2862
+ /**
2863
+ * dineway plugin validate
2864
+ *
2865
+ * Runs bundle validation without producing a tarball.
2866
+ * Thin wrapper around `dineway plugin bundle --validate-only`.
2867
+ *
2868
+ */
2869
+ const pluginValidateCommand = defineCommand({
2870
+ meta: {
2871
+ name: "validate",
2872
+ description: "Validate a plugin without producing a tarball (same checks as bundle)"
2873
+ },
2874
+ args: { dir: {
2875
+ type: "string",
2876
+ description: "Plugin directory (default: current directory)",
2877
+ default: "."
2878
+ } },
2879
+ async run({ args }) {
2880
+ await runCommand(bundleCommand, { rawArgs: [
2881
+ "--dir",
2882
+ args.dir,
2883
+ "--validateOnly"
2884
+ ] });
2885
+ }
2886
+ });
2887
+
2888
+ //#endregion
2889
+ //#region src/cli/commands/publish.ts
2890
+ /**
2891
+ * dineway plugin publish
2892
+ *
2893
+ * Publishes a plugin tarball to the Dineway Marketplace.
2894
+ *
2895
+ * Flow:
2896
+ * 1. Resolve tarball (from --tarball path, or build via `dineway plugin bundle`)
2897
+ * 2. Read manifest.json from tarball to show summary
2898
+ * 3. Authenticate (stored credential or GitHub device flow)
2899
+ * 4. Pre-publish validation (check plugin exists, version not published)
2900
+ * 5. Upload via multipart POST
2901
+ * 6. Display audit results
2902
+ */
2903
+ const DEFAULT_REGISTRY = "https://marketplace.dineway.foodism.ai";
2904
+ /**
2905
+ * Authenticate with the marketplace via GitHub Device Flow.
2906
+ * Returns the marketplace JWT and author info.
2907
+ */
2908
+ async function authenticateViaDeviceFlow(registryUrl) {
2909
+ consola.start("Fetching auth configuration...");
2910
+ const discoveryRes = await fetch(new URL("/api/v1/auth/discovery", registryUrl));
2911
+ if (!discoveryRes.ok) throw new Error(`Marketplace unreachable: ${discoveryRes.status} ${discoveryRes.statusText}`);
2912
+ const discovery = await discoveryRes.json();
2913
+ const deviceRes = await fetch(discovery.github.deviceAuthorizationEndpoint, {
2914
+ method: "POST",
2915
+ headers: {
2916
+ "Content-Type": "application/json",
2917
+ Accept: "application/json"
2918
+ },
2919
+ body: JSON.stringify({
2920
+ client_id: discovery.github.clientId,
2921
+ scope: "read:user user:email"
2922
+ })
2923
+ });
2924
+ if (!deviceRes.ok) throw new Error(`GitHub device flow failed: ${deviceRes.status}`);
2925
+ const deviceCode = await deviceRes.json();
2926
+ console.log();
2927
+ consola.info("Open your browser to:");
2928
+ console.log(` ${pc.cyan(pc.bold(deviceCode.verification_uri))}`);
2929
+ console.log();
2930
+ consola.info(`Enter code: ${pc.yellow(pc.bold(deviceCode.user_code))}`);
2931
+ console.log();
2932
+ try {
2933
+ const { execFile } = await import("node:child_process");
2934
+ if (process.platform === "darwin") execFile("open", [deviceCode.verification_uri]);
2935
+ else if (process.platform === "win32") execFile("cmd", [
2936
+ "/c",
2937
+ "start",
2938
+ "",
2939
+ deviceCode.verification_uri
2940
+ ]);
2941
+ else execFile("xdg-open", [deviceCode.verification_uri]);
2942
+ } catch {}
2943
+ consola.start("Waiting for authorization...");
2944
+ const githubToken = await pollGitHubDeviceFlow(discovery.github.tokenEndpoint, discovery.github.clientId, deviceCode.device_code, deviceCode.interval, deviceCode.expires_in);
2945
+ consola.start("Authenticating with marketplace...");
2946
+ const deviceTokenUrl = new URL(discovery.marketplace.deviceTokenEndpoint, registryUrl);
2947
+ const authRes = await fetch(deviceTokenUrl, {
2948
+ method: "POST",
2949
+ headers: { "Content-Type": "application/json" },
2950
+ body: JSON.stringify({ access_token: githubToken })
2951
+ });
2952
+ if (!authRes.ok) {
2953
+ const body = await authRes.json().catch(() => ({}));
2954
+ throw new Error(`Marketplace auth failed: ${body.error ?? authRes.statusText}`);
2955
+ }
2956
+ return await authRes.json();
2957
+ }
2958
+ async function pollGitHubDeviceFlow(tokenEndpoint, clientId, deviceCode, interval, expiresIn) {
2959
+ const deadline = Date.now() + expiresIn * 1e3;
2960
+ let currentInterval = interval;
2961
+ while (Date.now() < deadline) {
2962
+ await new Promise((r) => setTimeout(r, currentInterval * 1e3));
2963
+ const body = await (await fetch(tokenEndpoint, {
2964
+ method: "POST",
2965
+ headers: {
2966
+ "Content-Type": "application/json",
2967
+ Accept: "application/json"
2968
+ },
2969
+ body: JSON.stringify({
2970
+ client_id: clientId,
2971
+ device_code: deviceCode,
2972
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2973
+ })
2974
+ })).json();
2975
+ if (body.access_token) return body.access_token;
2976
+ if (body.error === "authorization_pending") continue;
2977
+ if (body.error === "slow_down") {
2978
+ currentInterval = body.interval ?? currentInterval + 5;
2979
+ continue;
2980
+ }
2981
+ if (body.error === "expired_token") throw new Error("Device code expired. Please try again.");
2982
+ if (body.error === "access_denied") throw new Error("Authorization was denied.");
2983
+ throw new Error(`GitHub token exchange failed: ${body.error ?? "unknown error"}`);
2984
+ }
2985
+ throw new Error("Device code expired (timeout). Please try again.");
2986
+ }
2987
+ const manifestSummarySchema = pluginManifestSchema.pick({
2988
+ id: true,
2989
+ version: true,
2990
+ capabilities: true,
2991
+ allowedHosts: true
2992
+ });
2993
+ /**
2994
+ * Read manifest.json from a tarball without fully extracting it.
2995
+ */
2996
+ async function readManifestFromTarball(tarballPath) {
2997
+ const data = await readFile(tarballPath);
2998
+ const manifest = (await unpackTar(new ReadableStream({ start(controller) {
2999
+ controller.enqueue(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
3000
+ controller.close();
3001
+ } }).pipeThrough(createGzipDecoder()), { filter: (header) => header.name === "manifest.json" })).find((e) => e.header.name === "manifest.json");
3002
+ if (!manifest?.data) throw new Error("Tarball does not contain manifest.json");
3003
+ const content = new TextDecoder().decode(manifest.data);
3004
+ const parsed = JSON.parse(content);
3005
+ const result = manifestSummarySchema.safeParse(parsed);
3006
+ if (!result.success) throw new Error(`Invalid manifest.json: ${result.error.message}`);
3007
+ return result.data;
3008
+ }
3009
+ const POLL_INTERVAL_MS = 3e3;
3010
+ const POLL_TIMEOUT_MS = 12e4;
3011
+ /**
3012
+ * Poll the version endpoint until status leaves "pending" or timeout.
3013
+ * Returns the final version data, or null on timeout.
3014
+ */
3015
+ async function pollVersionStatus(versionUrl, token) {
3016
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
3017
+ while (Date.now() < deadline) {
3018
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
3019
+ try {
3020
+ const res = await fetch(versionUrl, { headers: { Authorization: `Bearer ${token}` } });
3021
+ if (!res.ok) continue;
3022
+ const data = await res.json();
3023
+ if (data.status !== "pending") return data;
3024
+ } catch {}
3025
+ }
3026
+ return null;
3027
+ }
3028
+ function displayAuditResults(version) {
3029
+ const statusColor = version.status === "published" ? pc.green : version.status === "flagged" ? pc.yellow : pc.red;
3030
+ consola.info(` Status: ${statusColor(version.status)}`);
3031
+ if (version.audit_verdict) {
3032
+ const verdictColor = version.audit_verdict === "pass" ? pc.green : version.audit_verdict === "warn" ? pc.yellow : pc.red;
3033
+ consola.info(` Audit: ${verdictColor(version.audit_verdict)}`);
3034
+ }
3035
+ if (version.image_audit_verdict) {
3036
+ const verdictColor = version.image_audit_verdict === "pass" ? pc.green : version.image_audit_verdict === "warn" ? pc.yellow : pc.red;
3037
+ consola.info(` Image audit: ${verdictColor(version.image_audit_verdict)}`);
3038
+ }
3039
+ }
3040
+ function displayInlineAuditResults(audit, imageAudit) {
3041
+ const verdictColor = audit.verdict === "pass" ? pc.green : audit.verdict === "warn" ? pc.yellow : pc.red;
3042
+ consola.info(` Audit: ${verdictColor(audit.verdict)} (risk: ${audit.riskScore}/100)`);
3043
+ if (audit.findings.length > 0) for (const finding of audit.findings) {
3044
+ const icon = finding.severity === "high" ? pc.red("!") : pc.yellow("~");
3045
+ consola.info(` ${icon} [${finding.category}] ${finding.description}`);
3046
+ }
3047
+ if (imageAudit) {
3048
+ const imgColor = imageAudit.verdict === "pass" ? pc.green : imageAudit.verdict === "warn" ? pc.yellow : pc.red;
3049
+ consola.info(` Image audit: ${imgColor(imageAudit.verdict)}`);
3050
+ }
3051
+ }
3052
+ const publishCommand = defineCommand({
3053
+ meta: {
3054
+ name: "publish",
3055
+ description: "Publish a plugin to the Dineway Marketplace"
3056
+ },
3057
+ args: {
3058
+ tarball: {
3059
+ type: "string",
3060
+ description: "Path to plugin tarball (default: build first via `dineway plugin bundle`)"
3061
+ },
3062
+ dir: {
3063
+ type: "string",
3064
+ description: "Plugin directory (used with --build, default: current directory)",
3065
+ default: process.cwd()
3066
+ },
3067
+ build: {
3068
+ type: "boolean",
3069
+ description: "Build the plugin before publishing",
3070
+ default: false
3071
+ },
3072
+ registry: {
3073
+ type: "string",
3074
+ description: "Marketplace registry URL",
3075
+ default: DEFAULT_REGISTRY
3076
+ },
3077
+ "no-wait": {
3078
+ type: "boolean",
3079
+ description: "Exit immediately after upload without waiting for audit (useful for CI)",
3080
+ default: false
3081
+ }
3082
+ },
3083
+ async run({ args }) {
3084
+ const registryUrl = args.registry;
3085
+ let tarballPath;
3086
+ if (args.tarball) tarballPath = resolve(args.tarball);
3087
+ else if (args.build) {
3088
+ consola.start("Building plugin...");
3089
+ const pluginDir = resolve(args.dir);
3090
+ try {
3091
+ const { runCommand } = await import("citty");
3092
+ const { bundleCommand } = await Promise.resolve().then(() => bundle_exports);
3093
+ await runCommand(bundleCommand, { rawArgs: ["--dir", pluginDir] });
3094
+ } catch {
3095
+ consola.error("Build failed");
3096
+ process.exit(1);
3097
+ }
3098
+ const { readdir } = await import("node:fs/promises");
3099
+ const distDir = resolve(pluginDir, "dist");
3100
+ const tarball = (await readdir(distDir)).find((f) => f.endsWith(".tar.gz"));
3101
+ if (!tarball) {
3102
+ consola.error("Build succeeded but no .tar.gz found in dist/");
3103
+ process.exit(1);
3104
+ }
3105
+ tarballPath = resolve(distDir, tarball);
3106
+ } else {
3107
+ const pluginDir = resolve(args.dir);
3108
+ const { readdir } = await import("node:fs/promises");
3109
+ try {
3110
+ const distDir = resolve(pluginDir, "dist");
3111
+ const tarball = (await readdir(distDir)).find((f) => f.endsWith(".tar.gz"));
3112
+ if (tarball) tarballPath = resolve(distDir, tarball);
3113
+ else {
3114
+ consola.error("No tarball found. Run `dineway plugin bundle` first or use --build.");
3115
+ process.exit(1);
3116
+ }
3117
+ } catch {
3118
+ consola.error("No dist/ directory found. Run `dineway plugin bundle` first or use --build.");
3119
+ process.exit(1);
3120
+ }
3121
+ }
3122
+ const sizeKB = ((await stat(tarballPath)).size / 1024).toFixed(1);
3123
+ consola.info(`Tarball: ${pc.dim(tarballPath)} (${sizeKB}KB)`);
3124
+ const manifest = await readManifestFromTarball(tarballPath);
3125
+ console.log();
3126
+ consola.info(`Plugin: ${pc.bold(`${manifest.id}@${manifest.version}`)}`);
3127
+ if (manifest.capabilities.length > 0) consola.info(`Capabilities: ${manifest.capabilities.join(", ")}`);
3128
+ if (manifest.allowedHosts?.length) consola.info(`Allowed hosts: ${manifest.allowedHosts.join(", ")}`);
3129
+ console.log();
3130
+ let token;
3131
+ const envToken = process.env.DINEWAY_MARKETPLACE_TOKEN;
3132
+ const stored = !envToken ? getMarketplaceCredential(registryUrl) : null;
3133
+ if (envToken) {
3134
+ token = envToken;
3135
+ consola.info("Using DINEWAY_MARKETPLACE_TOKEN for authentication");
3136
+ } else if (stored) {
3137
+ token = stored.token;
3138
+ consola.info(`Authenticated as ${pc.bold(stored.author?.name ?? "unknown")}`);
3139
+ } else {
3140
+ consola.info("Not logged in to marketplace. Starting GitHub authentication...");
3141
+ const result = await authenticateViaDeviceFlow(registryUrl);
3142
+ token = result.token;
3143
+ saveMarketplaceCredential(registryUrl, {
3144
+ token: result.token,
3145
+ expiresAt: new Date(Date.now() + 30 * 86400 * 1e3).toISOString(),
3146
+ author: {
3147
+ id: result.author.id,
3148
+ name: result.author.name
3149
+ }
3150
+ });
3151
+ consola.success(`Authenticated as ${pc.bold(result.author.name)}`);
3152
+ }
3153
+ consola.start("Checking marketplace...");
3154
+ const pluginRes = await fetch(new URL(`/api/v1/plugins/${manifest.id}`, registryUrl));
3155
+ if (pluginRes.status === 404 && !envToken) {
3156
+ consola.info(`Plugin ${pc.bold(manifest.id)} not found in marketplace. Registering...`);
3157
+ const createRes = await fetch(new URL("/api/v1/plugins", registryUrl), {
3158
+ method: "POST",
3159
+ headers: {
3160
+ "Content-Type": "application/json",
3161
+ Authorization: `Bearer ${token}`
3162
+ },
3163
+ body: JSON.stringify({
3164
+ id: manifest.id,
3165
+ name: manifest.id,
3166
+ capabilities: manifest.capabilities
3167
+ })
3168
+ });
3169
+ if (!createRes.ok) {
3170
+ const body = await createRes.json().catch(() => ({}));
3171
+ if (createRes.status === 401) {
3172
+ removeMarketplaceCredential(registryUrl);
3173
+ consola.error("Authentication expired. Please run `dineway plugin publish` again to re-authenticate.");
3174
+ process.exit(1);
3175
+ }
3176
+ consola.error(`Failed to register plugin: ${body.error ?? createRes.statusText}`);
3177
+ process.exit(1);
3178
+ }
3179
+ consola.success(`Registered ${pc.bold(manifest.id)}`);
3180
+ } else if (pluginRes.status === 404 && envToken) consola.info(`Plugin ${pc.bold(manifest.id)} will be auto-registered on publish`);
3181
+ else if (!pluginRes.ok) {
3182
+ consola.error(`Marketplace error: ${pluginRes.status}`);
3183
+ process.exit(1);
3184
+ }
3185
+ consola.start(`Publishing ${manifest.id}@${manifest.version}...`);
3186
+ const tarballData = await readFile(tarballPath);
3187
+ const formData = new FormData();
3188
+ formData.append("bundle", new Blob([tarballData], { type: "application/gzip" }), basename(tarballPath));
3189
+ const uploadUrl = new URL(`/api/v1/plugins/${manifest.id}/versions`, registryUrl);
3190
+ const uploadRes = await fetch(uploadUrl, {
3191
+ method: "POST",
3192
+ headers: { Authorization: `Bearer ${token}` },
3193
+ body: formData
3194
+ });
3195
+ if (!uploadRes.ok && uploadRes.status !== 202) {
3196
+ const body = await uploadRes.json().catch(() => ({}));
3197
+ if (uploadRes.status === 401) {
3198
+ if (envToken) consola.error("DINEWAY_MARKETPLACE_TOKEN was rejected by the marketplace.");
3199
+ else {
3200
+ removeMarketplaceCredential(registryUrl);
3201
+ consola.error("Authentication expired. Please run `dineway plugin publish` again.");
3202
+ }
3203
+ process.exit(1);
3204
+ }
3205
+ if (uploadRes.status === 409) {
3206
+ if (body.latestVersion) consola.error(`Version ${manifest.version} must be greater than ${body.latestVersion}`);
3207
+ else consola.error(body.error ?? "Version conflict");
3208
+ process.exit(1);
3209
+ }
3210
+ if (uploadRes.status === 422 && body.audit) {
3211
+ consola.error("Plugin failed security audit:");
3212
+ consola.error(` Verdict: ${pc.red(body.audit.verdict)}`);
3213
+ consola.error(` Summary: ${body.audit.summary}`);
3214
+ process.exit(1);
3215
+ }
3216
+ consola.error(`Publish failed: ${body.error ?? uploadRes.statusText}`);
3217
+ process.exit(1);
3218
+ }
3219
+ const result = await uploadRes.json();
3220
+ console.log();
3221
+ consola.success(`Uploaded ${pc.bold(`${manifest.id}@${result.version}`)}`);
3222
+ consola.info(` Checksum: ${pc.dim(result.checksum)}`);
3223
+ consola.info(` Size: ${(result.bundleSize / 1024).toFixed(1)}KB`);
3224
+ if (uploadRes.status === 202) {
3225
+ consola.info(` Status: ${pc.yellow("pending")} (audit running in background)`);
3226
+ if (args["no-wait"]) {
3227
+ consola.info("Skipping audit wait (--no-wait). Check status later.");
3228
+ console.log();
3229
+ return;
3230
+ }
3231
+ consola.start("Waiting for security audit to complete...");
3232
+ const versionUrl = new URL(`/api/v1/plugins/${manifest.id}/versions/${manifest.version}`, registryUrl);
3233
+ const finalStatus = await pollVersionStatus(versionUrl.toString(), token);
3234
+ if (finalStatus) displayAuditResults(finalStatus);
3235
+ else {
3236
+ consola.warn("Audit did not complete within timeout. Check status later with:");
3237
+ consola.info(` ${pc.dim(`curl ${versionUrl.toString()}`)}`);
3238
+ }
3239
+ } else {
3240
+ if (result.audit) displayInlineAuditResults(result.audit, result.imageAudit ?? null);
3241
+ consola.info(` Status: ${pc.green(result.status ?? "published")}`);
3242
+ }
3243
+ console.log();
3244
+ }
3245
+ });
3246
+ const marketplaceLoginCommand = defineCommand({
3247
+ meta: {
3248
+ name: "login",
3249
+ description: "Log in to the Dineway Marketplace via GitHub"
3250
+ },
3251
+ args: { registry: {
3252
+ type: "string",
3253
+ description: "Marketplace registry URL",
3254
+ default: DEFAULT_REGISTRY
3255
+ } },
3256
+ async run({ args }) {
3257
+ const registryUrl = args.registry;
3258
+ const existing = getMarketplaceCredential(registryUrl);
3259
+ if (existing) {
3260
+ consola.info(`Already logged in as ${pc.bold(existing.author?.name ?? "unknown")}`);
3261
+ consola.info("Use `dineway plugin logout` to log out first.");
3262
+ return;
3263
+ }
3264
+ const result = await authenticateViaDeviceFlow(registryUrl);
3265
+ saveMarketplaceCredential(registryUrl, {
3266
+ token: result.token,
3267
+ expiresAt: new Date(Date.now() + 30 * 86400 * 1e3).toISOString(),
3268
+ author: {
3269
+ id: result.author.id,
3270
+ name: result.author.name
3271
+ }
3272
+ });
3273
+ consola.success(`Logged in as ${pc.bold(result.author.name)}`);
3274
+ }
3275
+ });
3276
+ const marketplaceLogoutCommand = defineCommand({
3277
+ meta: {
3278
+ name: "logout",
3279
+ description: "Log out of the Dineway Marketplace"
3280
+ },
3281
+ args: { registry: {
3282
+ type: "string",
3283
+ description: "Marketplace registry URL",
3284
+ default: DEFAULT_REGISTRY
3285
+ } },
3286
+ async run({ args }) {
3287
+ if (removeMarketplaceCredential(args.registry)) consola.success("Logged out of marketplace.");
3288
+ else consola.info("No marketplace credentials found.");
3289
+ }
3290
+ });
3291
+
3292
+ //#endregion
3293
+ //#region src/cli/commands/plugin.ts
3294
+ /**
3295
+ * dineway plugin
3296
+ *
3297
+ * Plugin management commands grouped under a single namespace.
3298
+ *
3299
+ * Subcommands:
3300
+ * - init: Scaffold a new plugin
3301
+ * - bundle: Bundle a plugin for marketplace distribution
3302
+ * - validate: Run bundle validation without producing a tarball
3303
+ * - publish: Publish a plugin to the marketplace
3304
+ * - login: Log in to the marketplace via GitHub
3305
+ * - logout: Log out of the marketplace
3306
+ *
3307
+ */
3308
+ const pluginCommand = defineCommand({
3309
+ meta: {
3310
+ name: "plugin",
3311
+ description: "Manage plugins"
3312
+ },
3313
+ subCommands: {
3314
+ init: pluginInitCommand,
3315
+ bundle: bundleCommand,
3316
+ validate: pluginValidateCommand,
3317
+ publish: publishCommand,
3318
+ login: marketplaceLoginCommand,
3319
+ logout: marketplaceLogoutCommand
3320
+ }
3321
+ });
3322
+
3323
+ //#endregion
3324
+ //#region src/cli/commands/schema.ts
3325
+ /**
3326
+ * dineway schema
3327
+ *
3328
+ * Manage collections and fields via the remote API
3329
+ */
3330
+ const listCommand$1 = defineCommand({
3331
+ meta: {
3332
+ name: "list",
3333
+ description: "List all collections"
3334
+ },
3335
+ args: { ...connectionArgs },
3336
+ async run({ args }) {
3337
+ configureOutputMode(args);
3338
+ try {
3339
+ output(await createClientFromArgs(args).collections(), args);
3340
+ } catch (error) {
3341
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3342
+ process.exit(1);
3343
+ }
3344
+ }
3345
+ });
3346
+ const getCommand = defineCommand({
3347
+ meta: {
3348
+ name: "get",
3349
+ description: "Get collection with fields"
3350
+ },
3351
+ args: {
3352
+ collection: {
3353
+ type: "positional",
3354
+ description: "Collection slug",
3355
+ required: true
3356
+ },
3357
+ ...connectionArgs
3358
+ },
3359
+ async run({ args }) {
3360
+ configureOutputMode(args);
3361
+ try {
3362
+ output(await createClientFromArgs(args).collection(args.collection), args);
3363
+ } catch (error) {
3364
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3365
+ process.exit(1);
3366
+ }
3367
+ }
3368
+ });
3369
+ const createCommand = defineCommand({
3370
+ meta: {
3371
+ name: "create",
3372
+ description: "Create a collection"
3373
+ },
3374
+ args: {
3375
+ collection: {
3376
+ type: "positional",
3377
+ description: "Collection slug",
3378
+ required: true
3379
+ },
3380
+ label: {
3381
+ type: "string",
3382
+ description: "Collection label",
3383
+ required: true
3384
+ },
3385
+ "label-singular": {
3386
+ type: "string",
3387
+ description: "Singular label (defaults to label)"
3388
+ },
3389
+ description: {
3390
+ type: "string",
3391
+ description: "Collection description"
3392
+ },
3393
+ ...connectionArgs
3394
+ },
3395
+ async run({ args }) {
3396
+ configureOutputMode(args);
3397
+ try {
3398
+ const data = await createClientFromArgs(args).createCollection({
3399
+ slug: args.collection,
3400
+ label: args.label,
3401
+ labelSingular: args["label-singular"] || args.label,
3402
+ description: args.description
3403
+ });
3404
+ consola$1.success(`Created collection "${args.collection}"`);
3405
+ output(data, args);
3406
+ } catch (error) {
3407
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3408
+ process.exit(1);
3409
+ }
3410
+ }
3411
+ });
3412
+ const deleteCommand = defineCommand({
3413
+ meta: {
3414
+ name: "delete",
3415
+ description: "Delete a collection"
3416
+ },
3417
+ args: {
3418
+ collection: {
3419
+ type: "positional",
3420
+ description: "Collection slug",
3421
+ required: true
3422
+ },
3423
+ force: {
3424
+ type: "boolean",
3425
+ description: "Skip confirmation"
3426
+ },
3427
+ ...connectionArgs
3428
+ },
3429
+ async run({ args }) {
3430
+ configureOutputMode(args);
3431
+ try {
3432
+ if (!args.force) {
3433
+ if (!await consola$1.prompt(`Delete collection "${args.collection}"?`, { type: "confirm" })) {
3434
+ consola$1.info("Cancelled");
3435
+ return;
3436
+ }
3437
+ }
3438
+ await createClientFromArgs(args).deleteCollection(args.collection);
3439
+ consola$1.success(`Deleted collection "${args.collection}"`);
3440
+ } catch (error) {
3441
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3442
+ process.exit(1);
3443
+ }
3444
+ }
3445
+ });
3446
+ const addFieldCommand = defineCommand({
3447
+ meta: {
3448
+ name: "add-field",
3449
+ description: "Add a field to a collection"
3450
+ },
3451
+ args: {
3452
+ collection: {
3453
+ type: "positional",
3454
+ description: "Collection slug",
3455
+ required: true
3456
+ },
3457
+ field: {
3458
+ type: "positional",
3459
+ description: "Field slug",
3460
+ required: true
3461
+ },
3462
+ type: {
3463
+ type: "string",
3464
+ description: "Field type (string, text, number, integer, boolean, datetime, image, reference, portableText, json)",
3465
+ required: true
3466
+ },
3467
+ label: {
3468
+ type: "string",
3469
+ description: "Field label"
3470
+ },
3471
+ required: {
3472
+ type: "boolean",
3473
+ description: "Whether the field is required"
3474
+ },
3475
+ ...connectionArgs
3476
+ },
3477
+ async run({ args }) {
3478
+ configureOutputMode(args);
3479
+ try {
3480
+ const data = await createClientFromArgs(args).createField(args.collection, {
3481
+ slug: args.field,
3482
+ type: args.type,
3483
+ label: args.label || args.field,
3484
+ required: args.required
3485
+ });
3486
+ consola$1.success(`Added field "${args.field}" to "${args.collection}"`);
3487
+ output(data, args);
3488
+ } catch (error) {
3489
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3490
+ process.exit(1);
3491
+ }
3492
+ }
3493
+ });
3494
+ const removeFieldCommand = defineCommand({
3495
+ meta: {
3496
+ name: "remove-field",
3497
+ description: "Remove a field from a collection"
3498
+ },
3499
+ args: {
3500
+ collection: {
3501
+ type: "positional",
3502
+ description: "Collection slug",
3503
+ required: true
3504
+ },
3505
+ field: {
3506
+ type: "positional",
3507
+ description: "Field slug",
3508
+ required: true
3509
+ },
3510
+ ...connectionArgs
3511
+ },
3512
+ async run({ args }) {
3513
+ configureOutputMode(args);
3514
+ try {
3515
+ await createClientFromArgs(args).deleteField(args.collection, args.field);
3516
+ consola$1.success(`Removed field "${args.field}" from "${args.collection}"`);
3517
+ } catch (error) {
3518
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3519
+ process.exit(1);
3520
+ }
3521
+ }
3522
+ });
3523
+ const schemaCommand = defineCommand({
3524
+ meta: {
3525
+ name: "schema",
3526
+ description: "Manage collections and fields"
3527
+ },
3528
+ subCommands: {
3529
+ list: listCommand$1,
3530
+ get: getCommand,
3531
+ create: createCommand,
3532
+ delete: deleteCommand,
3533
+ "add-field": addFieldCommand,
3534
+ "remove-field": removeFieldCommand
3535
+ }
3536
+ });
3537
+
3538
+ //#endregion
3539
+ //#region src/cli/commands/search-cmd.ts
3540
+ /**
3541
+ * dineway search
3542
+ *
3543
+ * Full-text search across content
3544
+ */
3545
+ const searchCommand = defineCommand({
3546
+ meta: {
3547
+ name: "search",
3548
+ description: "Full-text search across content"
3549
+ },
3550
+ args: {
3551
+ query: {
3552
+ type: "positional",
3553
+ description: "Search query",
3554
+ required: true
3555
+ },
3556
+ collection: {
3557
+ type: "string",
3558
+ alias: "c",
3559
+ description: "Filter by collection"
3560
+ },
3561
+ locale: {
3562
+ type: "string",
3563
+ description: "Filter by locale"
3564
+ },
3565
+ limit: {
3566
+ type: "string",
3567
+ alias: "l",
3568
+ description: "Maximum results to return"
3569
+ },
3570
+ ...connectionArgs
3571
+ },
3572
+ async run({ args }) {
3573
+ configureOutputMode(args);
3574
+ try {
3575
+ output(await createClientFromArgs(args).search(args.query, {
3576
+ collection: args.collection,
3577
+ locale: args.locale,
3578
+ limit: args.limit ? parseInt(args.limit, 10) : void 0
3579
+ }), args);
3580
+ } catch (error) {
3581
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3582
+ process.exit(1);
3583
+ }
3584
+ }
3585
+ });
3586
+
3587
+ //#endregion
3588
+ //#region src/cli/commands/seed.ts
3589
+ /**
3590
+ * dineway seed
3591
+ *
3592
+ * Apply a seed file to the database
3593
+ */
3594
+ async function fileExists(path) {
3595
+ try {
3596
+ await access(path);
3597
+ return true;
3598
+ } catch {
3599
+ return false;
3600
+ }
3601
+ }
3602
+ async function readPackageJson(cwd) {
3603
+ const pkgPath = resolve(cwd, "package.json");
3604
+ try {
3605
+ const content = await readFile(pkgPath, "utf-8");
3606
+ return JSON.parse(content);
3607
+ } catch {
3608
+ return null;
3609
+ }
3610
+ }
3611
+ /**
3612
+ * Resolve seed file path from:
3613
+ * 1. Positional argument (if provided)
3614
+ * 2. .dineway/seed.json (convention)
3615
+ * 3. package.json dineway.seed (config)
3616
+ */
3617
+ async function resolveSeedPath(cwd, positional) {
3618
+ if (positional) {
3619
+ const resolved = resolve(cwd, positional);
3620
+ if (await fileExists(resolved)) return resolved;
3621
+ consola.error(`Seed file not found: ${positional}`);
3622
+ return null;
3623
+ }
3624
+ const conventionPath = resolve(cwd, ".dineway", "seed.json");
3625
+ if (await fileExists(conventionPath)) return conventionPath;
3626
+ const pkg = await readPackageJson(cwd);
3627
+ if (pkg?.dineway?.seed) {
3628
+ const pkgSeedPath = resolve(cwd, pkg.dineway.seed);
3629
+ if (await fileExists(pkgSeedPath)) return pkgSeedPath;
3630
+ consola.warn(`Seed file from package.json not found: ${pkg.dineway.seed}`);
3631
+ }
3632
+ return null;
3633
+ }
3634
+ const seedCommand = defineCommand({
3635
+ meta: {
3636
+ name: "seed",
3637
+ description: "Apply a seed file to the database"
3638
+ },
3639
+ args: {
3640
+ path: {
3641
+ type: "positional",
3642
+ description: "Path to seed file (default: .dineway/seed.json)",
3643
+ required: false
3644
+ },
3645
+ database: {
3646
+ type: "string",
3647
+ alias: "d",
3648
+ description: "Database path or URL",
3649
+ default: "./data.db"
3650
+ },
3651
+ cwd: {
3652
+ type: "string",
3653
+ description: "Working directory",
3654
+ default: process.cwd()
3655
+ },
3656
+ validate: {
3657
+ type: "boolean",
3658
+ description: "Validate only, don't apply",
3659
+ default: false
3660
+ },
3661
+ "no-content": {
3662
+ type: "boolean",
3663
+ description: "Skip sample content",
3664
+ default: false
3665
+ },
3666
+ "on-conflict": {
3667
+ type: "string",
3668
+ description: "Conflict handling: skip, update, error",
3669
+ default: "skip"
3670
+ },
3671
+ "uploads-dir": {
3672
+ type: "string",
3673
+ description: "Directory for media uploads",
3674
+ default: "./uploads"
3675
+ },
3676
+ "media-base-url": {
3677
+ type: "string",
3678
+ description: "Base URL for media files",
3679
+ default: "/_dineway/api/media/file"
3680
+ }
3681
+ },
3682
+ async run({ args }) {
3683
+ const cwd = resolve(args.cwd);
3684
+ consola.start("Loading seed file...");
3685
+ const seedPath = await resolveSeedPath(cwd, args.path);
3686
+ if (!seedPath) {
3687
+ consola.error("No seed file found");
3688
+ consola.info("Provide a path, create .dineway/seed.json, or set dineway.seed in package.json");
3689
+ process.exit(1);
3690
+ }
3691
+ consola.info(`Seed file: ${seedPath}`);
3692
+ let seed;
3693
+ try {
3694
+ const content = await readFile(seedPath, "utf-8");
3695
+ seed = JSON.parse(content);
3696
+ } catch (error) {
3697
+ consola.error("Failed to parse seed file:", error);
3698
+ process.exit(1);
3699
+ }
3700
+ consola.start("Validating seed file...");
3701
+ const validation = validateSeed(seed);
3702
+ if (validation.warnings.length > 0) for (const warning of validation.warnings) consola.warn(warning);
3703
+ if (!validation.valid) {
3704
+ consola.error("Seed validation failed:");
3705
+ for (const error of validation.errors) consola.error(` - ${error}`);
3706
+ process.exit(1);
3707
+ }
3708
+ consola.success("Seed file is valid");
3709
+ if (args.validate) {
3710
+ consola.success("Validation complete");
3711
+ return;
3712
+ }
3713
+ const database = resolveCliDatabaseTarget(cwd, args.database);
3714
+ consola.info(`Database: ${database.display}`);
3715
+ const db = createDatabase({ url: database.url });
3716
+ consola.start("Running migrations...");
3717
+ try {
3718
+ const { applied } = await runMigrations(db);
3719
+ if (applied.length > 0) consola.success(`Applied ${applied.length} migrations`);
3720
+ else consola.info("Database up to date");
3721
+ } catch (error) {
3722
+ consola.error("Migration failed:", error);
3723
+ await db.destroy();
3724
+ process.exit(1);
3725
+ }
3726
+ const uploadsDir = resolve(cwd, args["uploads-dir"]);
3727
+ await mkdir(uploadsDir, { recursive: true });
3728
+ const storage = new LocalStorage({
3729
+ directory: uploadsDir,
3730
+ baseUrl: args["media-base-url"]
3731
+ });
3732
+ const onConflictRaw = args["on-conflict"];
3733
+ if (onConflictRaw !== "skip" && onConflictRaw !== "update" && onConflictRaw !== "error") {
3734
+ consola.error(`Invalid --on-conflict value: ${onConflictRaw}`);
3735
+ consola.info("Use: skip, update, or error");
3736
+ await db.destroy();
3737
+ process.exit(1);
3738
+ }
3739
+ const options = {
3740
+ includeContent: !args["no-content"],
3741
+ onConflict: onConflictRaw,
3742
+ storage
3743
+ };
3744
+ consola.start("Applying seed...");
3745
+ try {
3746
+ const result = await applySeed(db, seed, options);
3747
+ consola.success("Seed applied successfully!");
3748
+ consola.log("");
3749
+ if (result.settings.applied > 0) consola.info(`Settings: ${result.settings.applied} applied`);
3750
+ if (result.collections.created > 0 || result.collections.skipped > 0 || result.collections.updated > 0) consola.info(`Collections: ${result.collections.created} created, ${result.collections.skipped} skipped, ${result.collections.updated} updated`);
3751
+ if (result.fields.created > 0 || result.fields.skipped > 0 || result.fields.updated > 0) consola.info(`Fields: ${result.fields.created} created, ${result.fields.skipped} skipped, ${result.fields.updated} updated`);
3752
+ if (result.taxonomies.created > 0 || result.taxonomies.terms > 0) consola.info(`Taxonomies: ${result.taxonomies.created} created, ${result.taxonomies.terms} terms`);
3753
+ if (result.bylines.created > 0 || result.bylines.skipped > 0 || result.bylines.updated > 0) consola.info(`Bylines: ${result.bylines.created} created, ${result.bylines.skipped} skipped, ${result.bylines.updated} updated`);
3754
+ if (result.menus.created > 0 || result.menus.items > 0) consola.info(`Menus: ${result.menus.created} created, ${result.menus.items} items`);
3755
+ if (result.widgetAreas.created > 0 || result.widgetAreas.widgets > 0) consola.info(`Widget Areas: ${result.widgetAreas.created} created, ${result.widgetAreas.widgets} widgets`);
3756
+ if (result.content.created > 0 || result.content.skipped > 0 || result.content.updated > 0) consola.info(`Content: ${result.content.created} created, ${result.content.skipped} skipped, ${result.content.updated} updated`);
3757
+ if (result.media.created > 0 || result.media.skipped > 0) consola.info(`Media: ${result.media.created} created, ${result.media.skipped} skipped`);
3758
+ } catch (error) {
3759
+ consola.error("Seed failed:", error instanceof Error ? error.message : error);
3760
+ await db.destroy();
3761
+ process.exit(1);
3762
+ }
3763
+ await db.destroy();
3764
+ consola.success("Done!");
3765
+ }
3766
+ });
3767
+
3768
+ //#endregion
3769
+ //#region src/cli/commands/taxonomy.ts
3770
+ /**
3771
+ * dineway taxonomy
3772
+ *
3773
+ * Manage taxonomies and terms via the Dineway REST API.
3774
+ */
3775
+ /** Pattern to replace whitespace with hyphens for slug generation */
3776
+ const WHITESPACE_PATTERN = /\s+/g;
3777
+ const listCommand = defineCommand({
3778
+ meta: {
3779
+ name: "list",
3780
+ description: "List all taxonomies"
3781
+ },
3782
+ args: { ...connectionArgs },
3783
+ async run({ args }) {
3784
+ configureOutputMode(args);
3785
+ try {
3786
+ output(await createClientFromArgs(args).taxonomies(), args);
3787
+ } catch (error) {
3788
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3789
+ process.exit(1);
3790
+ }
3791
+ }
3792
+ });
3793
+ const termsCommand = defineCommand({
3794
+ meta: {
3795
+ name: "terms",
3796
+ description: "List terms in a taxonomy"
3797
+ },
3798
+ args: {
3799
+ name: {
3800
+ type: "positional",
3801
+ description: "Taxonomy name",
3802
+ required: true
3803
+ },
3804
+ limit: {
3805
+ type: "string",
3806
+ alias: "l",
3807
+ description: "Maximum terms to return"
3808
+ },
3809
+ cursor: {
3810
+ type: "string",
3811
+ description: "Pagination cursor"
3812
+ },
3813
+ ...connectionArgs
3814
+ },
3815
+ async run({ args }) {
3816
+ configureOutputMode(args);
3817
+ try {
3818
+ output(await createClientFromArgs(args).terms(args.name, {
3819
+ limit: args.limit ? parseInt(args.limit, 10) : void 0,
3820
+ cursor: args.cursor
3821
+ }), args);
3822
+ } catch (error) {
3823
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3824
+ process.exit(1);
3825
+ }
3826
+ }
3827
+ });
3828
+ const addTermCommand = defineCommand({
3829
+ meta: {
3830
+ name: "add-term",
3831
+ description: "Create a term in a taxonomy"
3832
+ },
3833
+ args: {
3834
+ taxonomy: {
3835
+ type: "positional",
3836
+ description: "Taxonomy name",
3837
+ required: true
3838
+ },
3839
+ name: {
3840
+ type: "string",
3841
+ description: "Term label",
3842
+ required: true
3843
+ },
3844
+ slug: {
3845
+ type: "string",
3846
+ description: "Term slug (defaults to slugified name)"
3847
+ },
3848
+ parent: {
3849
+ type: "string",
3850
+ description: "Parent term ID"
3851
+ },
3852
+ ...connectionArgs
3853
+ },
3854
+ async run({ args }) {
3855
+ configureOutputMode(args);
3856
+ try {
3857
+ const client = createClientFromArgs(args);
3858
+ const label = args.name;
3859
+ const slug = args.slug || label.toLowerCase().replace(WHITESPACE_PATTERN, "-");
3860
+ const term = await client.createTerm(args.taxonomy, {
3861
+ slug,
3862
+ label,
3863
+ parentId: args.parent
3864
+ });
3865
+ consola$1.success(`Created term "${label}" in ${args.taxonomy}`);
3866
+ output(term, args);
3867
+ } catch (error) {
3868
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3869
+ process.exit(1);
3870
+ }
3871
+ }
3872
+ });
3873
+ const taxonomyCommand = defineCommand({
3874
+ meta: {
3875
+ name: "taxonomy",
3876
+ description: "Manage taxonomies and terms"
3877
+ },
3878
+ subCommands: {
3879
+ list: listCommand,
3880
+ terms: termsCommand,
3881
+ "add-term": addTermCommand
3882
+ }
3883
+ });
3884
+
3885
+ //#endregion
3886
+ //#region src/cli/commands/types.ts
3887
+ /**
3888
+ * dineway types
3889
+ *
3890
+ * Fetch schema from a Dineway instance and generate TypeScript types
3891
+ */
3892
+ const typesCommand = defineCommand({
3893
+ meta: {
3894
+ name: "types",
3895
+ description: "Generate TypeScript types from schema"
3896
+ },
3897
+ args: {
3898
+ ...connectionArgs,
3899
+ output: {
3900
+ type: "string",
3901
+ alias: "o",
3902
+ description: "Output path for generated types",
3903
+ default: ".dineway/types.ts"
3904
+ },
3905
+ cwd: {
3906
+ type: "string",
3907
+ description: "Working directory",
3908
+ default: process.cwd()
3909
+ }
3910
+ },
3911
+ async run({ args }) {
3912
+ const cwd = resolve(args.cwd);
3913
+ consola.start("Fetching schema...");
3914
+ try {
3915
+ const client = createClientFromArgs(args);
3916
+ const schema = await client.schemaExport();
3917
+ consola.success(`Found ${schema.collections.length} collections`);
3918
+ const types = await client.schemaTypes();
3919
+ const outputPath = resolve(cwd, args.output);
3920
+ await mkdir(dirname(outputPath), { recursive: true });
3921
+ await writeFile(outputPath, types, "utf-8");
3922
+ consola.success(`Generated ${args.output}`);
3923
+ await writeFile(resolve(dirname(outputPath), "schema.json"), JSON.stringify(schema, null, 2), "utf-8");
3924
+ consola.info(`Schema version: ${schema.version}`);
3925
+ consola.box({
3926
+ title: "Types generated",
3927
+ message: `${schema.collections.length} collections\n\nTypes: ${args.output}\nSchema: .dineway/schema.json`
3928
+ });
3929
+ } catch (error) {
3930
+ consola.error("Failed to fetch schema:", error instanceof Error ? error.message : error);
3931
+ process.exit(1);
3932
+ }
3933
+ }
3934
+ });
3935
+
3936
+ //#endregion
3937
+ //#region src/cli/index.ts
3938
+ /**
3939
+ * Dineway CLI
3940
+ *
3941
+ * Built with citty + clack (same stack as Nuxt CLI)
3942
+ *
3943
+ * Commands:
3944
+ * - init: Bootstrap database from template config, or interactive setup
3945
+ * - types: Generate TypeScript types from schema
3946
+ * - dev: Run dev server with a local or remote database
3947
+ * - seed: Apply a seed file to the database
3948
+ * - export-seed: Export database schema and content as a seed file
3949
+ * - auth: Authentication utilities (secret generation)
3950
+ * - login/logout/whoami: Session management
3951
+ * - content: Create, read, update, delete content
3952
+ * - schema: Manage collections and fields
3953
+ * - media: Upload and manage media
3954
+ * - search: Full-text search
3955
+ * - taxonomy: Manage taxonomies and terms
3956
+ * - menu: Manage navigation menus
3957
+ * - plugin: Plugin management (init, bundle, validate, publish, login, logout)
3958
+ */
3959
+ runMain(defineCommand({
3960
+ meta: {
3961
+ name: "dineway",
3962
+ version: "0.0.0",
3963
+ description: "CLI for Dineway Agentic Web builder"
3964
+ },
3965
+ subCommands: {
3966
+ init: initCommand,
3967
+ types: typesCommand,
3968
+ dev: devCommand,
3969
+ doctor: doctorCommand,
3970
+ seed: seedCommand,
3971
+ "export-seed": exportSeedCommand,
3972
+ auth: authCommand,
3973
+ login: loginCommand,
3974
+ logout: logoutCommand,
3975
+ whoami: whoamiCommand,
3976
+ content: contentCommand,
3977
+ schema: schemaCommand,
3978
+ media: mediaCommand,
3979
+ search: searchCommand,
3980
+ taxonomy: taxonomyCommand,
3981
+ menu: menuCommand,
3982
+ plugin: pluginCommand
3983
+ }
3984
+ }));
3985
+
3986
+ //#endregion
3987
+ export { };