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.
- package/LICENSE +9 -0
- package/README.md +89 -0
- package/dist/adapters-BlzWJG82.d.mts +106 -0
- package/dist/apply-CAPvMfoU.mjs +1339 -0
- package/dist/astro/index.d.mts +50 -0
- package/dist/astro/index.mjs +1326 -0
- package/dist/astro/middleware/auth.d.mts +30 -0
- package/dist/astro/middleware/auth.mjs +708 -0
- package/dist/astro/middleware/redirect.d.mts +21 -0
- package/dist/astro/middleware/redirect.mjs +62 -0
- package/dist/astro/middleware/request-context.d.mts +17 -0
- package/dist/astro/middleware/request-context.mjs +1371 -0
- package/dist/astro/middleware/setup.d.mts +19 -0
- package/dist/astro/middleware/setup.mjs +46 -0
- package/dist/astro/middleware.d.mts +12 -0
- package/dist/astro/middleware.mjs +1716 -0
- package/dist/astro/types.d.mts +269 -0
- package/dist/astro/types.mjs +1 -0
- package/dist/base64-F8-DUraK.mjs +58 -0
- package/dist/byline-DeWCMU_i.mjs +234 -0
- package/dist/bylines-DyqBV9EQ.mjs +137 -0
- package/dist/chunk-ClPoSABd.mjs +21 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +3987 -0
- package/dist/client/external-auth-headers.d.mts +38 -0
- package/dist/client/external-auth-headers.mjs +101 -0
- package/dist/client/index.d.mts +397 -0
- package/dist/client/index.mjs +345 -0
- package/dist/config-Cq8H0SfX.mjs +46 -0
- package/dist/connection-C9pxzuag.mjs +52 -0
- package/dist/content-zSgdNmnt.mjs +836 -0
- package/dist/db/index.d.mts +4 -0
- package/dist/db/index.mjs +62 -0
- package/dist/db/libsql.d.mts +10 -0
- package/dist/db/libsql.mjs +21 -0
- package/dist/db/postgres.d.mts +10 -0
- package/dist/db/postgres.mjs +29 -0
- package/dist/db/sqlite.d.mts +10 -0
- package/dist/db/sqlite.mjs +15 -0
- package/dist/default-WYlzADZL.mjs +80 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs +89 -0
- package/dist/error-DrxtnGPg.mjs +26 -0
- package/dist/index-C-jx21qs.d.mts +4771 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +30 -0
- package/dist/load-C6FCD1FU.mjs +27 -0
- package/dist/loader-qKmo0wAY.mjs +446 -0
- package/dist/manifest-schema-CTSEyIJ3.mjs +186 -0
- package/dist/media/index.d.mts +25 -0
- package/dist/media/index.mjs +54 -0
- package/dist/media/local-runtime.d.mts +38 -0
- package/dist/media/local-runtime.mjs +132 -0
- package/dist/media-DMTr80Gv.mjs +199 -0
- package/dist/mode-BlyYtIFO.mjs +22 -0
- package/dist/page/index.d.mts +148 -0
- package/dist/page/index.mjs +419 -0
- package/dist/placeholder-B3knXwNc.mjs +267 -0
- package/dist/placeholder-bOx1xCTY.d.mts +283 -0
- package/dist/plugin-utils.d.mts +57 -0
- package/dist/plugin-utils.mjs +77 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +21 -0
- package/dist/plugins/adapt-sandbox-entry.mjs +112 -0
- package/dist/query-BiaPl_g2.mjs +459 -0
- package/dist/redirect-JPqLAbxa.mjs +328 -0
- package/dist/registry-DSd1GWB8.mjs +851 -0
- package/dist/request-context.d.mts +49 -0
- package/dist/request-context.mjs +42 -0
- package/dist/runner-B5l1JfOj.d.mts +26 -0
- package/dist/runner-BGUGywgG.mjs +1529 -0
- package/dist/runtime.d.mts +25 -0
- package/dist/runtime.mjs +41 -0
- package/dist/search-BNruJHDL.mjs +11054 -0
- package/dist/seed/index.d.mts +3 -0
- package/dist/seed/index.mjs +15 -0
- package/dist/seo/index.d.mts +69 -0
- package/dist/seo/index.mjs +69 -0
- package/dist/storage/local.d.mts +38 -0
- package/dist/storage/local.mjs +165 -0
- package/dist/storage/s3.d.mts +31 -0
- package/dist/storage/s3.mjs +174 -0
- package/dist/tokens-4vgYuXsZ.mjs +170 -0
- package/dist/transport-C5FYnid7.mjs +417 -0
- package/dist/transport-gIL-e43D.d.mts +41 -0
- package/dist/types-BawVha09.mjs +30 -0
- package/dist/types-BgQeVaPj.d.mts +192 -0
- package/dist/types-CLLdsG3g.d.mts +103 -0
- package/dist/types-D38djUXv.d.mts +1196 -0
- package/dist/types-DShnjzb6.mjs +15 -0
- package/dist/types-DkvMXalq.d.mts +425 -0
- package/dist/types-DuNbGKjF.mjs +74 -0
- package/dist/types-ju-_ORz7.d.mts +182 -0
- package/dist/validate-CXnRKfJK.mjs +327 -0
- package/dist/validate-CqRJb_xU.mjs +96 -0
- package/dist/validate-DVKJJ-M_.d.mts +377 -0
- package/locals.d.ts +47 -0
- 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 { };
|