khotan-data 0.0.1 → 0.1.1
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/AGENTS.md +54 -0
- package/README.md +117 -1
- package/dist/cli.js +2869 -0
- package/dist/factory.cjs +3303 -0
- package/dist/factory.cjs.map +1 -0
- package/dist/factory.d.cts +662 -0
- package/dist/factory.d.ts +662 -0
- package/dist/factory.js +3292 -0
- package/dist/factory.js.map +1 -0
- package/dist/plug-client.cjs +99 -0
- package/dist/plug-client.cjs.map +1 -0
- package/dist/plug-client.d.cts +71 -0
- package/dist/plug-client.d.ts +71 -0
- package/dist/plug-client.js +96 -0
- package/dist/plug-client.js.map +1 -0
- package/dist/templates/agent-skill.md +73 -0
- package/dist/templates/agents.md +41 -0
- package/dist/templates/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.example.ts +36 -0
- package/dist/templates/catch.ts +119 -0
- package/dist/templates/config-page.tsx +20 -0
- package/dist/templates/debug-index-page.tsx +101 -0
- package/dist/templates/debug-page.tsx +48 -0
- package/dist/templates/graph-page.tsx +11 -0
- package/dist/templates/hub.tsx +450 -0
- package/dist/templates/inflow.example.ts +61 -0
- package/dist/templates/inflow.ts +98 -0
- package/dist/templates/khotan-config.ts +49 -0
- package/dist/templates/khotan-route.ts +13 -0
- package/dist/templates/logs-page.tsx +9 -0
- package/dist/templates/logs.tsx +20 -0
- package/dist/templates/mapping-browser.tsx +761 -0
- package/dist/templates/mappings-page.tsx +9 -0
- package/dist/templates/outflow.example.ts +52 -0
- package/dist/templates/outflow.ts +90 -0
- package/dist/templates/pass.example.ts +51 -0
- package/dist/templates/pass.ts +134 -0
- package/dist/templates/plug-debugger.tsx +1185 -0
- package/dist/templates/plug.example.ts +93 -0
- package/dist/templates/plug.ts +806 -0
- package/dist/templates/relay.example.ts +71 -0
- package/dist/templates/relay.ts +104 -0
- package/dist/templates/runs-table.tsx +592 -0
- package/dist/templates/schema.ts +505 -0
- package/dist/templates/skill-dashboard.md +144 -0
- package/dist/templates/skill-plug.md +216 -0
- package/dist/templates/skill-setup.md +161 -0
- package/dist/templates/skill-webhook.md +196 -0
- package/dist/templates/topology-canvas.tsx +1406 -0
- package/dist/templates/var-panel.tsx +276 -0
- package/dist/templates/webhook-events-table.tsx +241 -0
- package/dist/templates/wire-panel.tsx +216 -0
- package/dist/templates/wire.ts +155 -0
- package/package.json +46 -5
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2869 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import fs4 from 'fs';
|
|
4
|
+
import path2 from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import prompts2 from 'prompts';
|
|
8
|
+
|
|
9
|
+
// src/cli/config-template.ts
|
|
10
|
+
function configTemplate(outputDir) {
|
|
11
|
+
return `export default {
|
|
12
|
+
outputDir: "${outputDir}",
|
|
13
|
+
components: [],
|
|
14
|
+
};
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
var PM_INFO = {
|
|
18
|
+
bun: { name: "bun", installCmd: "bun add", devFlag: "-d" },
|
|
19
|
+
pnpm: { name: "pnpm", installCmd: "pnpm add", devFlag: "-D" },
|
|
20
|
+
yarn: { name: "yarn", installCmd: "yarn add", devFlag: "-D" },
|
|
21
|
+
npm: { name: "npm", installCmd: "npm install", devFlag: "--save-dev" }
|
|
22
|
+
};
|
|
23
|
+
var LOCKFILE_PRIORITY = [
|
|
24
|
+
{ file: "bun.lock", pm: "bun" },
|
|
25
|
+
{ file: "pnpm-lock.yaml", pm: "pnpm" },
|
|
26
|
+
{ file: "yarn.lock", pm: "yarn" },
|
|
27
|
+
{ file: "package-lock.json", pm: "npm" }
|
|
28
|
+
];
|
|
29
|
+
function detectPackageManager(cwd) {
|
|
30
|
+
for (const { file, pm } of LOCKFILE_PRIORITY) {
|
|
31
|
+
if (fs4.existsSync(path2.join(cwd, file))) {
|
|
32
|
+
return PM_INFO[pm];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return PM_INFO.npm;
|
|
36
|
+
}
|
|
37
|
+
function checkNpmPackages(cwd, packages) {
|
|
38
|
+
const pkgPath = path2.join(cwd, "package.json");
|
|
39
|
+
if (!fs4.existsSync(pkgPath)) {
|
|
40
|
+
return packages;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const pkgJson = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
44
|
+
const allDeps = {
|
|
45
|
+
...pkgJson.dependencies,
|
|
46
|
+
...pkgJson.devDependencies
|
|
47
|
+
};
|
|
48
|
+
return packages.filter((pkg) => !(pkg in allDeps));
|
|
49
|
+
} catch {
|
|
50
|
+
return packages;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function checkShadcnComponents(cwd, components) {
|
|
54
|
+
const componentsJsonPath = path2.join(cwd, "components.json");
|
|
55
|
+
let uiDir = null;
|
|
56
|
+
if (fs4.existsSync(componentsJsonPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const config = JSON.parse(
|
|
59
|
+
fs4.readFileSync(componentsJsonPath, "utf-8")
|
|
60
|
+
);
|
|
61
|
+
const aliasPath = config.aliases?.components;
|
|
62
|
+
if (aliasPath) {
|
|
63
|
+
const resolved = aliasPath.replace(/^@\//, "src/").replace(/^~\//, "");
|
|
64
|
+
uiDir = path2.join(cwd, resolved, "ui");
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!uiDir) {
|
|
70
|
+
const srcUi = path2.join(cwd, "src", "components", "ui");
|
|
71
|
+
const rootUi = path2.join(cwd, "components", "ui");
|
|
72
|
+
if (fs4.existsSync(srcUi)) {
|
|
73
|
+
uiDir = srcUi;
|
|
74
|
+
} else if (fs4.existsSync(rootUi)) {
|
|
75
|
+
uiDir = rootUi;
|
|
76
|
+
} else {
|
|
77
|
+
return components;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return components.filter((name) => {
|
|
81
|
+
const filePath = path2.join(uiDir, `${name}.tsx`);
|
|
82
|
+
return !fs4.existsSync(filePath);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function installPackages(cwd, packages, opts) {
|
|
86
|
+
if (packages.length === 0) {
|
|
87
|
+
return { success: true };
|
|
88
|
+
}
|
|
89
|
+
const pm = detectPackageManager(cwd);
|
|
90
|
+
const devFlag = opts?.devDependency ? ` ${pm.devFlag}` : "";
|
|
91
|
+
const cmd = `${pm.installCmd} ${packages.join(" ")}${devFlag}`;
|
|
92
|
+
try {
|
|
93
|
+
execSync(cmd, { cwd, stdio: "pipe", encoding: "utf-8" });
|
|
94
|
+
return { success: true };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const e = err;
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
error: e.stderr ?? e.stdout ?? "Install command failed"
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function installShadcnComponents(cwd, components) {
|
|
104
|
+
if (components.length === 0) {
|
|
105
|
+
return { success: true };
|
|
106
|
+
}
|
|
107
|
+
const cmd = `npx shadcn@latest add ${components.join(" ")} --yes`;
|
|
108
|
+
try {
|
|
109
|
+
execSync(cmd, { cwd, stdio: "pipe", encoding: "utf-8" });
|
|
110
|
+
return { success: true };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const e = err;
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: e.stderr ?? e.stdout ?? "shadcn install failed"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
var __dirname$1 = path2.dirname(fileURLToPath(import.meta.url));
|
|
120
|
+
var COMPONENTS = {
|
|
121
|
+
plug: {
|
|
122
|
+
name: "plug",
|
|
123
|
+
description: "Self-contained fetch wrapper with auth, retry, and pagination",
|
|
124
|
+
files: [
|
|
125
|
+
{
|
|
126
|
+
templatePath: path2.resolve(__dirname$1, "templates", "plug.ts"),
|
|
127
|
+
outputFile: "plugs/plug.ts",
|
|
128
|
+
outputBase: "outputDir"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
templatePath: path2.resolve(__dirname$1, "templates", "plug.example.ts"),
|
|
132
|
+
outputFile: "plugs/plug.example.ts",
|
|
133
|
+
outputBase: "outputDir"
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
dependencies: {
|
|
137
|
+
npmPackages: ["zod"]
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
wire: {
|
|
141
|
+
name: "wire",
|
|
142
|
+
description: "Webhook subscription lifecycle management using Plug for HTTP",
|
|
143
|
+
requires: ["plug", "schema"],
|
|
144
|
+
requiresShadcn: true,
|
|
145
|
+
dependencies: {
|
|
146
|
+
npmPackages: ["drizzle-orm"],
|
|
147
|
+
shadcnComponents: ["card", "badge", "button"]
|
|
148
|
+
},
|
|
149
|
+
files: [
|
|
150
|
+
{
|
|
151
|
+
templatePath: path2.resolve(__dirname$1, "templates", "wire.ts"),
|
|
152
|
+
outputFile: "wires/wire.ts",
|
|
153
|
+
outputBase: "outputDir"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
templatePath: path2.resolve(__dirname$1, "templates", "wire-panel.tsx"),
|
|
157
|
+
outputFile: "wire.tsx",
|
|
158
|
+
outputBase: "components"
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
schema: {
|
|
163
|
+
name: "schema",
|
|
164
|
+
description: "Drizzle table definitions for khotan plugs, flows, and runs",
|
|
165
|
+
templatePath: path2.resolve(__dirname$1, "templates", "schema.ts"),
|
|
166
|
+
outputFile: "khotan.ts",
|
|
167
|
+
dependencies: {
|
|
168
|
+
npmPackages: ["drizzle-orm"]
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
cache: {
|
|
172
|
+
name: "cache",
|
|
173
|
+
description: "First-class durable cache definitions for khotan sync workloads",
|
|
174
|
+
requires: ["schema"],
|
|
175
|
+
files: [
|
|
176
|
+
{
|
|
177
|
+
templatePath: path2.resolve(__dirname$1, "templates", "cache.ts"),
|
|
178
|
+
outputFile: "caches/cache.ts",
|
|
179
|
+
outputBase: "outputDir"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
templatePath: path2.resolve(__dirname$1, "templates", "cache.example.ts"),
|
|
183
|
+
outputFile: "caches/cache.example.ts",
|
|
184
|
+
outputBase: "outputDir"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
hub: {
|
|
189
|
+
name: "hub",
|
|
190
|
+
description: "Dashboard UI for managing plugs and flows",
|
|
191
|
+
requiresShadcn: true,
|
|
192
|
+
dependencies: {
|
|
193
|
+
npmPackages: ["drizzle-orm"],
|
|
194
|
+
shadcnComponents: [
|
|
195
|
+
"card",
|
|
196
|
+
"badge",
|
|
197
|
+
"table",
|
|
198
|
+
"switch",
|
|
199
|
+
"button",
|
|
200
|
+
"input",
|
|
201
|
+
"label"
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
files: [
|
|
205
|
+
{
|
|
206
|
+
templatePath: path2.resolve(__dirname$1, "templates", "hub.tsx"),
|
|
207
|
+
outputFile: "hub.tsx",
|
|
208
|
+
outputBase: "components"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
templatePath: path2.resolve(__dirname$1, "templates", "wire-panel.tsx"),
|
|
212
|
+
outputFile: "wire.tsx",
|
|
213
|
+
outputBase: "components"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
templatePath: path2.resolve(__dirname$1, "templates", "var-panel.tsx"),
|
|
217
|
+
outputFile: "var-panel.tsx",
|
|
218
|
+
outputBase: "components"
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
logs: {
|
|
223
|
+
name: "logs",
|
|
224
|
+
description: "Paginated UI tables for runs and webhook events",
|
|
225
|
+
requiresShadcn: true,
|
|
226
|
+
dependencies: {
|
|
227
|
+
shadcnComponents: ["card", "table", "badge", "button"]
|
|
228
|
+
},
|
|
229
|
+
files: [
|
|
230
|
+
{
|
|
231
|
+
templatePath: path2.resolve(__dirname$1, "templates", "logs.tsx"),
|
|
232
|
+
outputFile: "logs.tsx",
|
|
233
|
+
outputBase: "components"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
templatePath: path2.resolve(__dirname$1, "templates", "runs-table.tsx"),
|
|
237
|
+
outputFile: "runs-table.tsx",
|
|
238
|
+
outputBase: "components"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
templatePath: path2.resolve(
|
|
242
|
+
__dirname$1,
|
|
243
|
+
"templates",
|
|
244
|
+
"webhook-events-table.tsx"
|
|
245
|
+
),
|
|
246
|
+
outputFile: "webhook-events-table.tsx",
|
|
247
|
+
outputBase: "components"
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
"mapping-browser": {
|
|
252
|
+
name: "mapping-browser",
|
|
253
|
+
description: "Searchable mappings browser for listing, creating, editing, and deleting resource mappings",
|
|
254
|
+
requiresShadcn: true,
|
|
255
|
+
dependencies: {
|
|
256
|
+
shadcnComponents: ["card", "table", "button", "input", "label"]
|
|
257
|
+
},
|
|
258
|
+
files: [
|
|
259
|
+
{
|
|
260
|
+
templatePath: path2.resolve(
|
|
261
|
+
__dirname$1,
|
|
262
|
+
"templates",
|
|
263
|
+
"mapping-browser.tsx"
|
|
264
|
+
),
|
|
265
|
+
outputFile: "mapping-browser.tsx",
|
|
266
|
+
outputBase: "components"
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
},
|
|
270
|
+
"plug-debugger": {
|
|
271
|
+
name: "plug-debugger",
|
|
272
|
+
description: "Dev-only debug panel for testing plug requests interactively",
|
|
273
|
+
requiresShadcn: true,
|
|
274
|
+
requires: ["plug"],
|
|
275
|
+
dependencies: {
|
|
276
|
+
shadcnComponents: ["card", "badge", "button", "input", "label"]
|
|
277
|
+
},
|
|
278
|
+
files: [
|
|
279
|
+
{
|
|
280
|
+
templatePath: path2.resolve(__dirname$1, "templates", "plug-debugger.tsx"),
|
|
281
|
+
outputFile: "plug-debugger.tsx",
|
|
282
|
+
outputBase: "components"
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
catch: {
|
|
287
|
+
name: "catch",
|
|
288
|
+
description: "Durable webhook event processing via Vercel Workflow",
|
|
289
|
+
requires: ["wire"],
|
|
290
|
+
requiresWorkflowIntegration: true,
|
|
291
|
+
dependencies: {
|
|
292
|
+
npmPackages: ["workflow"]
|
|
293
|
+
},
|
|
294
|
+
files: [
|
|
295
|
+
{
|
|
296
|
+
templatePath: path2.resolve(__dirname$1, "templates", "catch.ts"),
|
|
297
|
+
outputFile: "webhooks/catch.ts",
|
|
298
|
+
outputBase: "outputDir"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
templatePath: path2.resolve(__dirname$1, "templates", "catch.example.ts"),
|
|
302
|
+
outputFile: "webhooks/catch.example.ts",
|
|
303
|
+
outputBase: "outputDir"
|
|
304
|
+
}
|
|
305
|
+
]
|
|
306
|
+
},
|
|
307
|
+
pass: {
|
|
308
|
+
name: "pass",
|
|
309
|
+
description: "Durable webhook event forwarding to another service via Vercel Workflow",
|
|
310
|
+
requires: ["wire", "plug"],
|
|
311
|
+
requiresWorkflowIntegration: true,
|
|
312
|
+
dependencies: {
|
|
313
|
+
npmPackages: ["workflow"]
|
|
314
|
+
},
|
|
315
|
+
files: [
|
|
316
|
+
{
|
|
317
|
+
templatePath: path2.resolve(__dirname$1, "templates", "pass.ts"),
|
|
318
|
+
outputFile: "webhooks/pass.ts",
|
|
319
|
+
outputBase: "outputDir"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
templatePath: path2.resolve(__dirname$1, "templates", "pass.example.ts"),
|
|
323
|
+
outputFile: "webhooks/pass.example.ts",
|
|
324
|
+
outputBase: "outputDir"
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
},
|
|
328
|
+
inflow: {
|
|
329
|
+
name: "inflow",
|
|
330
|
+
description: "Durable flow for pulling data from a plug into your app via Vercel Workflow",
|
|
331
|
+
requires: ["plug", "schema"],
|
|
332
|
+
requiresWorkflowIntegration: true,
|
|
333
|
+
dependencies: {
|
|
334
|
+
npmPackages: ["workflow"]
|
|
335
|
+
},
|
|
336
|
+
files: [
|
|
337
|
+
{
|
|
338
|
+
templatePath: path2.resolve(__dirname$1, "templates", "inflow.ts"),
|
|
339
|
+
outputFile: "flows/inflow.ts",
|
|
340
|
+
outputBase: "outputDir"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
templatePath: path2.resolve(__dirname$1, "templates", "inflow.example.ts"),
|
|
344
|
+
outputFile: "flows/inflow.example.ts",
|
|
345
|
+
outputBase: "outputDir"
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
},
|
|
349
|
+
outflow: {
|
|
350
|
+
name: "outflow",
|
|
351
|
+
description: "Durable flow for pushing app data out through a plug via Vercel Workflow",
|
|
352
|
+
requires: ["plug", "schema"],
|
|
353
|
+
requiresWorkflowIntegration: true,
|
|
354
|
+
dependencies: {
|
|
355
|
+
npmPackages: ["workflow"]
|
|
356
|
+
},
|
|
357
|
+
files: [
|
|
358
|
+
{
|
|
359
|
+
templatePath: path2.resolve(__dirname$1, "templates", "outflow.ts"),
|
|
360
|
+
outputFile: "flows/outflow.ts",
|
|
361
|
+
outputBase: "outputDir"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
templatePath: path2.resolve(
|
|
365
|
+
__dirname$1,
|
|
366
|
+
"templates",
|
|
367
|
+
"outflow.example.ts"
|
|
368
|
+
),
|
|
369
|
+
outputFile: "flows/outflow.example.ts",
|
|
370
|
+
outputBase: "outputDir"
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
},
|
|
374
|
+
relay: {
|
|
375
|
+
name: "relay",
|
|
376
|
+
description: "Durable flow for moving data between plugs via Vercel Workflow",
|
|
377
|
+
requires: ["plug", "schema"],
|
|
378
|
+
requiresWorkflowIntegration: true,
|
|
379
|
+
dependencies: {
|
|
380
|
+
npmPackages: ["workflow"]
|
|
381
|
+
},
|
|
382
|
+
files: [
|
|
383
|
+
{
|
|
384
|
+
templatePath: path2.resolve(__dirname$1, "templates", "relay.ts"),
|
|
385
|
+
outputFile: "flows/relay.ts",
|
|
386
|
+
outputBase: "outputDir"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
templatePath: path2.resolve(__dirname$1, "templates", "relay.example.ts"),
|
|
390
|
+
outputFile: "flows/relay.example.ts",
|
|
391
|
+
outputBase: "outputDir"
|
|
392
|
+
}
|
|
393
|
+
]
|
|
394
|
+
},
|
|
395
|
+
"agent-skill": {
|
|
396
|
+
name: "agent-skill",
|
|
397
|
+
description: "Agent skill that teaches AI agents to use khotan plug for API debugging",
|
|
398
|
+
files: [
|
|
399
|
+
{
|
|
400
|
+
templatePath: path2.resolve(__dirname$1, "templates", "agent-skill.md"),
|
|
401
|
+
outputFile: "khotan-probe",
|
|
402
|
+
outputBase: "agentSkills"
|
|
403
|
+
}
|
|
404
|
+
]
|
|
405
|
+
},
|
|
406
|
+
"skill-setup": {
|
|
407
|
+
name: "skill-setup",
|
|
408
|
+
description: "Agent skill for setting up khotan-data in a Next.js project",
|
|
409
|
+
files: [
|
|
410
|
+
{
|
|
411
|
+
templatePath: path2.resolve(__dirname$1, "templates", "skill-setup.md"),
|
|
412
|
+
outputFile: "khotan-setup",
|
|
413
|
+
outputBase: "agentSkills"
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
},
|
|
417
|
+
"skill-plug": {
|
|
418
|
+
name: "skill-plug",
|
|
419
|
+
description: "Agent skill for creating and configuring khotan Plugs (API clients)",
|
|
420
|
+
files: [
|
|
421
|
+
{
|
|
422
|
+
templatePath: path2.resolve(__dirname$1, "templates", "skill-plug.md"),
|
|
423
|
+
outputFile: "khotan-plug",
|
|
424
|
+
outputBase: "agentSkills"
|
|
425
|
+
}
|
|
426
|
+
]
|
|
427
|
+
},
|
|
428
|
+
"skill-dashboard": {
|
|
429
|
+
name: "skill-dashboard",
|
|
430
|
+
description: "Agent skill for setting up the Hub dashboard and Plug Debugger UI",
|
|
431
|
+
files: [
|
|
432
|
+
{
|
|
433
|
+
templatePath: path2.resolve(
|
|
434
|
+
__dirname$1,
|
|
435
|
+
"templates",
|
|
436
|
+
"skill-dashboard.md"
|
|
437
|
+
),
|
|
438
|
+
outputFile: "khotan-dashboard",
|
|
439
|
+
outputBase: "agentSkills"
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
},
|
|
443
|
+
"skill-webhook": {
|
|
444
|
+
name: "skill-webhook",
|
|
445
|
+
description: "Agent skill for webhook subscriptions with Wires, Catch, and Pass",
|
|
446
|
+
files: [
|
|
447
|
+
{
|
|
448
|
+
templatePath: path2.resolve(__dirname$1, "templates", "skill-webhook.md"),
|
|
449
|
+
outputFile: "khotan-webhook",
|
|
450
|
+
outputBase: "agentSkills"
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
var BLOCKS = {
|
|
456
|
+
"config-page-1": {
|
|
457
|
+
name: "config-page-1",
|
|
458
|
+
description: "Page route at /config that renders the KhotanHub dashboard",
|
|
459
|
+
requires: ["hub"],
|
|
460
|
+
files: [
|
|
461
|
+
{
|
|
462
|
+
templatePath: path2.resolve(__dirname$1, "templates", "config-page.tsx"),
|
|
463
|
+
outputFile: "config/page.tsx",
|
|
464
|
+
outputBase: "appRoot"
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
},
|
|
468
|
+
"debug-page-1": {
|
|
469
|
+
name: "debug-page-1",
|
|
470
|
+
description: "Debug routes at /debug (plug list) and /debug/[plugName] (debugger)",
|
|
471
|
+
requires: ["plug-debugger"],
|
|
472
|
+
files: [
|
|
473
|
+
{
|
|
474
|
+
templatePath: path2.resolve(
|
|
475
|
+
__dirname$1,
|
|
476
|
+
"templates",
|
|
477
|
+
"debug-index-page.tsx"
|
|
478
|
+
),
|
|
479
|
+
outputFile: "debug/page.tsx",
|
|
480
|
+
outputBase: "appRoot"
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
templatePath: path2.resolve(__dirname$1, "templates", "debug-page.tsx"),
|
|
484
|
+
outputFile: "debug/[plugName]/page.tsx",
|
|
485
|
+
outputBase: "appRoot"
|
|
486
|
+
}
|
|
487
|
+
]
|
|
488
|
+
},
|
|
489
|
+
"logs-page-1": {
|
|
490
|
+
name: "logs-page-1",
|
|
491
|
+
description: "Page route at /logs that renders recent runs and webhook events",
|
|
492
|
+
requires: ["logs"],
|
|
493
|
+
files: [
|
|
494
|
+
{
|
|
495
|
+
templatePath: path2.resolve(__dirname$1, "templates", "logs-page.tsx"),
|
|
496
|
+
outputFile: "logs/page.tsx",
|
|
497
|
+
outputBase: "appRoot"
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
},
|
|
501
|
+
"mappings-page-1": {
|
|
502
|
+
name: "mappings-page-1",
|
|
503
|
+
description: "Page route at /mappings that renders the reusable Khotan mappings browser",
|
|
504
|
+
requires: ["mapping-browser"],
|
|
505
|
+
files: [
|
|
506
|
+
{
|
|
507
|
+
templatePath: path2.resolve(
|
|
508
|
+
__dirname$1,
|
|
509
|
+
"templates",
|
|
510
|
+
"mappings-page.tsx"
|
|
511
|
+
),
|
|
512
|
+
outputFile: "mappings/page.tsx",
|
|
513
|
+
outputBase: "appRoot"
|
|
514
|
+
}
|
|
515
|
+
]
|
|
516
|
+
},
|
|
517
|
+
graph: {
|
|
518
|
+
name: "graph",
|
|
519
|
+
description: "Standalone topology graph page at /graph with filtering and run-state overlays",
|
|
520
|
+
requiresShadcn: true,
|
|
521
|
+
dependencies: {
|
|
522
|
+
npmPackages: ["@xyflow/react"],
|
|
523
|
+
shadcnComponents: ["card", "badge"]
|
|
524
|
+
},
|
|
525
|
+
files: [
|
|
526
|
+
{
|
|
527
|
+
templatePath: path2.resolve(
|
|
528
|
+
__dirname$1,
|
|
529
|
+
"templates",
|
|
530
|
+
"topology-canvas.tsx"
|
|
531
|
+
),
|
|
532
|
+
outputFile: "topology-canvas.tsx",
|
|
533
|
+
outputBase: "components"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
templatePath: path2.resolve(__dirname$1, "templates", "graph-page.tsx"),
|
|
537
|
+
outputFile: "graph/page.tsx",
|
|
538
|
+
outputBase: "appRoot"
|
|
539
|
+
}
|
|
540
|
+
]
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
function getComponent(name) {
|
|
544
|
+
return COMPONENTS[name];
|
|
545
|
+
}
|
|
546
|
+
function getEntry(name) {
|
|
547
|
+
const comp = COMPONENTS[name];
|
|
548
|
+
if (comp) return { entry: comp, kind: "component" };
|
|
549
|
+
const block = BLOCKS[name];
|
|
550
|
+
if (block) return { entry: block, kind: "block" };
|
|
551
|
+
return void 0;
|
|
552
|
+
}
|
|
553
|
+
function listComponents() {
|
|
554
|
+
return Object.values(COMPONENTS);
|
|
555
|
+
}
|
|
556
|
+
function listBlocks() {
|
|
557
|
+
return Object.values(BLOCKS);
|
|
558
|
+
}
|
|
559
|
+
function isMultiFile(entry) {
|
|
560
|
+
return Array.isArray(entry.files) && entry.files.length > 0;
|
|
561
|
+
}
|
|
562
|
+
var AGENT_TARGETS = [
|
|
563
|
+
{
|
|
564
|
+
agent: "cursor",
|
|
565
|
+
skillPath: (name) => `.cursor/skills/${name}/SKILL.md`
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
agent: "claude",
|
|
569
|
+
skillPath: (name) => `.claude/skills/${name}/SKILL.md`
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
agent: "codex",
|
|
573
|
+
skillPath: (name) => `.agents/skills/${name}/SKILL.md`
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
agent: "copilot",
|
|
577
|
+
skillPath: (name) => `.github/skills/${name}/SKILL.md`
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
agent: "kiro",
|
|
581
|
+
skillPath: (name) => `.kiro/skills/${name}/SKILL.md`
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
agent: "roo",
|
|
585
|
+
skillPath: (name) => `.roo/rules/${name}/SKILL.md`
|
|
586
|
+
}
|
|
587
|
+
];
|
|
588
|
+
var AGENT_MARKERS = {
|
|
589
|
+
cursor: ".cursor",
|
|
590
|
+
claude: ".claude",
|
|
591
|
+
codex: ".agents",
|
|
592
|
+
copilot: ".github",
|
|
593
|
+
kiro: ".kiro",
|
|
594
|
+
roo: ".roo"
|
|
595
|
+
};
|
|
596
|
+
function detectAgents(cwd) {
|
|
597
|
+
const detected = AGENT_TARGETS.filter((t) => {
|
|
598
|
+
const marker = AGENT_MARKERS[t.agent];
|
|
599
|
+
return marker && fs4.existsSync(path2.join(cwd, marker));
|
|
600
|
+
});
|
|
601
|
+
if (detected.length > 0) return detected;
|
|
602
|
+
return AGENT_TARGETS.filter(
|
|
603
|
+
(t) => t.agent === "cursor" || t.agent === "claude"
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
function installSkills(cwd, skills, agentsTemplatePath) {
|
|
607
|
+
const targets = detectAgents(cwd);
|
|
608
|
+
const result = {
|
|
609
|
+
created: [],
|
|
610
|
+
skipped: [],
|
|
611
|
+
agents: targets.map((t) => t.agent)
|
|
612
|
+
};
|
|
613
|
+
for (const target of targets) {
|
|
614
|
+
for (const skill of skills) {
|
|
615
|
+
const relPath = target.skillPath(skill.name);
|
|
616
|
+
const absPath = path2.resolve(cwd, relPath);
|
|
617
|
+
if (fs4.existsSync(absPath)) {
|
|
618
|
+
result.skipped.push(relPath);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
fs4.mkdirSync(path2.dirname(absPath), { recursive: true });
|
|
622
|
+
const content = fs4.readFileSync(skill.templatePath, "utf-8");
|
|
623
|
+
fs4.writeFileSync(absPath, content, "utf-8");
|
|
624
|
+
result.created.push(relPath);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const agentsPath = path2.resolve(cwd, "AGENTS.md");
|
|
628
|
+
if (!fs4.existsSync(agentsPath)) {
|
|
629
|
+
const content = fs4.readFileSync(agentsTemplatePath, "utf-8");
|
|
630
|
+
const resolved = resolveAgentsMdPaths(content, targets);
|
|
631
|
+
fs4.writeFileSync(agentsPath, resolved, "utf-8");
|
|
632
|
+
result.created.push("AGENTS.md");
|
|
633
|
+
} else {
|
|
634
|
+
result.skipped.push("AGENTS.md");
|
|
635
|
+
}
|
|
636
|
+
return result;
|
|
637
|
+
}
|
|
638
|
+
function resolveAgentsMdPaths(content, targets) {
|
|
639
|
+
if (targets.length === 0) return content;
|
|
640
|
+
const primary = targets[0];
|
|
641
|
+
return content.replace(
|
|
642
|
+
/skills\/(khotan-[\w-]+)\/SKILL\.md/g,
|
|
643
|
+
(_match, name) => primary.skillPath(name)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/cli/commands/init.ts
|
|
648
|
+
var __dirname2 = path2.dirname(fileURLToPath(import.meta.url));
|
|
649
|
+
function resolveOutputDir(projectRoot) {
|
|
650
|
+
if (fs4.existsSync(path2.join(projectRoot, "src", "app"))) {
|
|
651
|
+
return "src/khotan";
|
|
652
|
+
}
|
|
653
|
+
if (fs4.existsSync(path2.join(projectRoot, "app"))) {
|
|
654
|
+
return "khotan";
|
|
655
|
+
}
|
|
656
|
+
return "src/khotan";
|
|
657
|
+
}
|
|
658
|
+
function hasSrcLayout(cwd) {
|
|
659
|
+
return fs4.existsSync(path2.join(cwd, "src", "app"));
|
|
660
|
+
}
|
|
661
|
+
var DRIZZLE_PACKAGES = ["drizzle-orm", "postgres"];
|
|
662
|
+
var DRIZZLE_DEV_PACKAGES = ["drizzle-kit"];
|
|
663
|
+
var SHADCN_COMPONENTS = ["card", "badge", "table", "switch"];
|
|
664
|
+
function scaffoldCoreFiles(cwd, outputDir) {
|
|
665
|
+
const created = [];
|
|
666
|
+
const khotanConfigTemplatePath = path2.resolve(
|
|
667
|
+
__dirname2,
|
|
668
|
+
"templates",
|
|
669
|
+
"khotan-config.ts"
|
|
670
|
+
);
|
|
671
|
+
const routeTemplatePath = path2.resolve(
|
|
672
|
+
__dirname2,
|
|
673
|
+
"templates",
|
|
674
|
+
"khotan-route.ts"
|
|
675
|
+
);
|
|
676
|
+
const khotanTsPath = path2.join(path2.resolve(cwd, outputDir), "khotan.ts");
|
|
677
|
+
if (!fs4.existsSync(khotanTsPath)) {
|
|
678
|
+
fs4.mkdirSync(path2.dirname(khotanTsPath), { recursive: true });
|
|
679
|
+
fs4.copyFileSync(khotanConfigTemplatePath, khotanTsPath);
|
|
680
|
+
created.push(path2.relative(cwd, khotanTsPath));
|
|
681
|
+
} else {
|
|
682
|
+
console.log(
|
|
683
|
+
`\u2713 ${path2.relative(cwd, khotanTsPath)} already exists, skipping`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const routeDir = path2.resolve(
|
|
687
|
+
cwd,
|
|
688
|
+
hasSrcLayout(cwd) ? "src/app/api/khotan/[...all]" : "app/api/khotan/[...all]"
|
|
689
|
+
);
|
|
690
|
+
const routePath = path2.join(routeDir, "route.ts");
|
|
691
|
+
if (!fs4.existsSync(routePath)) {
|
|
692
|
+
fs4.mkdirSync(routeDir, { recursive: true });
|
|
693
|
+
fs4.copyFileSync(routeTemplatePath, routePath);
|
|
694
|
+
created.push(path2.relative(cwd, routePath));
|
|
695
|
+
} else {
|
|
696
|
+
console.log(`\u2713 ${path2.relative(cwd, routePath)} already exists, skipping`);
|
|
697
|
+
}
|
|
698
|
+
return created;
|
|
699
|
+
}
|
|
700
|
+
async function runFullSetup(cwd) {
|
|
701
|
+
const results = [];
|
|
702
|
+
const pm = detectPackageManager(cwd);
|
|
703
|
+
console.log(`Detected package manager: ${pm.name}
|
|
704
|
+
`);
|
|
705
|
+
const missingDrizzle = checkNpmPackages(cwd, DRIZZLE_PACKAGES);
|
|
706
|
+
const missingDrizzleDev = checkNpmPackages(cwd, DRIZZLE_DEV_PACKAGES);
|
|
707
|
+
if (missingDrizzle.length > 0) {
|
|
708
|
+
console.log(`Installing ${missingDrizzle.join(", ")}...`);
|
|
709
|
+
const result = installPackages(cwd, missingDrizzle);
|
|
710
|
+
if (result.success) {
|
|
711
|
+
console.log(`\u2713 Installed ${missingDrizzle.join(", ")}`);
|
|
712
|
+
results.push({ name: "Install drizzle packages", status: "success" });
|
|
713
|
+
} else {
|
|
714
|
+
console.error(`\u2717 Failed: ${result.error ?? "unknown error"}`);
|
|
715
|
+
results.push({
|
|
716
|
+
name: "Install drizzle packages",
|
|
717
|
+
status: "failed",
|
|
718
|
+
error: result.error
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
console.log("\u2713 Drizzle packages already installed, skipping");
|
|
723
|
+
results.push({ name: "Install drizzle packages", status: "skipped" });
|
|
724
|
+
}
|
|
725
|
+
if (missingDrizzleDev.length > 0) {
|
|
726
|
+
console.log(`Installing ${missingDrizzleDev.join(", ")} (dev)...`);
|
|
727
|
+
const result = installPackages(cwd, missingDrizzleDev, {
|
|
728
|
+
devDependency: true
|
|
729
|
+
});
|
|
730
|
+
if (result.success) {
|
|
731
|
+
console.log(
|
|
732
|
+
`\u2713 Installed ${missingDrizzleDev.join(", ")} as dev dependencies`
|
|
733
|
+
);
|
|
734
|
+
results.push({ name: "Install drizzle-kit", status: "success" });
|
|
735
|
+
} else {
|
|
736
|
+
console.error(`\u2717 Failed: ${result.error ?? "unknown error"}`);
|
|
737
|
+
results.push({
|
|
738
|
+
name: "Install drizzle-kit",
|
|
739
|
+
status: "failed",
|
|
740
|
+
error: result.error
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
console.log("\u2713 drizzle-kit already installed, skipping");
|
|
745
|
+
results.push({ name: "Install drizzle-kit", status: "skipped" });
|
|
746
|
+
}
|
|
747
|
+
const componentsJsonPath = path2.join(cwd, "components.json");
|
|
748
|
+
if (!fs4.existsSync(componentsJsonPath)) {
|
|
749
|
+
console.log("\nInitializing shadcn/ui...");
|
|
750
|
+
try {
|
|
751
|
+
execSync("npx shadcn@latest init --defaults --yes", {
|
|
752
|
+
cwd,
|
|
753
|
+
stdio: "pipe",
|
|
754
|
+
encoding: "utf-8"
|
|
755
|
+
});
|
|
756
|
+
console.log("\u2713 Initialized shadcn/ui");
|
|
757
|
+
results.push({ name: "Initialize shadcn", status: "success" });
|
|
758
|
+
} catch (err) {
|
|
759
|
+
const e = err;
|
|
760
|
+
const error = e.stderr ?? e.stdout ?? "shadcn init failed";
|
|
761
|
+
console.error(`\u2717 Failed to initialize shadcn: ${error}`);
|
|
762
|
+
results.push({ name: "Initialize shadcn", status: "failed", error });
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
console.log("\n\u2713 shadcn/ui already configured, skipping init");
|
|
766
|
+
results.push({ name: "Initialize shadcn", status: "skipped" });
|
|
767
|
+
}
|
|
768
|
+
const missingShadcn = checkShadcnComponents(cwd, SHADCN_COMPONENTS);
|
|
769
|
+
if (missingShadcn.length > 0) {
|
|
770
|
+
console.log(
|
|
771
|
+
`
|
|
772
|
+
Installing shadcn components: ${missingShadcn.join(", ")}...`
|
|
773
|
+
);
|
|
774
|
+
const result = installShadcnComponents(cwd, missingShadcn);
|
|
775
|
+
if (result.success) {
|
|
776
|
+
console.log(`\u2713 Installed shadcn components: ${missingShadcn.join(", ")}`);
|
|
777
|
+
results.push({ name: "Install shadcn components", status: "success" });
|
|
778
|
+
} else {
|
|
779
|
+
console.error(`\u2717 Failed: ${result.error ?? "unknown error"}`);
|
|
780
|
+
results.push({
|
|
781
|
+
name: "Install shadcn components",
|
|
782
|
+
status: "failed",
|
|
783
|
+
error: result.error
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
console.log("\n\u2713 All required shadcn components already present, skipping");
|
|
788
|
+
results.push({ name: "Install shadcn components", status: "skipped" });
|
|
789
|
+
}
|
|
790
|
+
const configPath = path2.resolve(cwd, "khotan.config.ts");
|
|
791
|
+
const outputDir = resolveOutputDir(cwd);
|
|
792
|
+
if (!fs4.existsSync(configPath)) {
|
|
793
|
+
fs4.writeFileSync(configPath, configTemplate(outputDir), "utf-8");
|
|
794
|
+
console.log(`
|
|
795
|
+
\u2713 Created khotan.config.ts (outputDir: ${outputDir})`);
|
|
796
|
+
results.push({ name: "Create khotan.config.ts", status: "success" });
|
|
797
|
+
} else {
|
|
798
|
+
console.log("\n\u2713 khotan.config.ts already exists, skipping");
|
|
799
|
+
results.push({ name: "Create khotan.config.ts", status: "skipped" });
|
|
800
|
+
}
|
|
801
|
+
const coreFiles = scaffoldCoreFiles(cwd, outputDir);
|
|
802
|
+
if (coreFiles.length > 0) {
|
|
803
|
+
results.push({
|
|
804
|
+
name: `Scaffold ${coreFiles.join(", ")}`,
|
|
805
|
+
status: "success"
|
|
806
|
+
});
|
|
807
|
+
} else {
|
|
808
|
+
results.push({ name: "Scaffold core files", status: "skipped" });
|
|
809
|
+
}
|
|
810
|
+
results.push(ensureKhotanDataInstalled(cwd));
|
|
811
|
+
return results;
|
|
812
|
+
}
|
|
813
|
+
function ensureKhotanDataInstalled(cwd) {
|
|
814
|
+
const missing = checkNpmPackages(cwd, ["khotan-data"]);
|
|
815
|
+
if (missing.length === 0) {
|
|
816
|
+
console.log("\u2713 khotan-data already installed, skipping");
|
|
817
|
+
return { name: "Install khotan-data", status: "skipped" };
|
|
818
|
+
}
|
|
819
|
+
console.log("Installing khotan-data...");
|
|
820
|
+
const result = installPackages(cwd, ["khotan-data"]);
|
|
821
|
+
if (result.success) {
|
|
822
|
+
console.log("\u2713 Installed khotan-data");
|
|
823
|
+
return { name: "Install khotan-data", status: "success" };
|
|
824
|
+
}
|
|
825
|
+
console.error(
|
|
826
|
+
`\u2717 Failed to install khotan-data: ${result.error ?? "unknown error"}`
|
|
827
|
+
);
|
|
828
|
+
return { name: "Install khotan-data", status: "failed", error: result.error };
|
|
829
|
+
}
|
|
830
|
+
async function runInit(cwd) {
|
|
831
|
+
const configPath = path2.resolve(cwd, "khotan.config.ts");
|
|
832
|
+
const outputDir = resolveOutputDir(cwd);
|
|
833
|
+
if (!fs4.existsSync(configPath)) {
|
|
834
|
+
fs4.writeFileSync(configPath, configTemplate(outputDir), "utf-8");
|
|
835
|
+
console.log(`\u2713 Created khotan.config.ts (outputDir: ${outputDir})`);
|
|
836
|
+
}
|
|
837
|
+
scaffoldCoreFiles(cwd, outputDir);
|
|
838
|
+
ensureKhotanDataInstalled(cwd);
|
|
839
|
+
return fs4.existsSync(configPath);
|
|
840
|
+
}
|
|
841
|
+
var SKILL_COMPONENTS = [
|
|
842
|
+
"skill-setup",
|
|
843
|
+
"skill-plug",
|
|
844
|
+
"skill-dashboard",
|
|
845
|
+
"skill-webhook",
|
|
846
|
+
"agent-skill"
|
|
847
|
+
];
|
|
848
|
+
function scaffoldAgentSkills(cwd) {
|
|
849
|
+
const skills = [];
|
|
850
|
+
for (const name of SKILL_COMPONENTS) {
|
|
851
|
+
const entry = getComponent(name);
|
|
852
|
+
if (!entry || !isMultiFile(entry)) continue;
|
|
853
|
+
for (const file of entry.files) {
|
|
854
|
+
if (file.outputBase === "agentSkills") {
|
|
855
|
+
skills.push({ name: file.outputFile, templatePath: file.templatePath });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (skills.length === 0) return 0;
|
|
860
|
+
const agentsTemplatePath = path2.resolve(__dirname2, "templates", "agents.md");
|
|
861
|
+
const result = installSkills(cwd, skills, agentsTemplatePath);
|
|
862
|
+
if (result.created.length > 0) {
|
|
863
|
+
console.log(` Agents detected: ${result.agents.join(", ")}`);
|
|
864
|
+
for (const f of result.created) {
|
|
865
|
+
console.log(` \u2713 Created ${f}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return result.created.length;
|
|
869
|
+
}
|
|
870
|
+
var initCommand = new Command("init").description("Initialize khotan in your project").option(
|
|
871
|
+
"--full",
|
|
872
|
+
"Full project setup: install drizzle, shadcn, and configure everything"
|
|
873
|
+
).option("-y, --yes", "Auto-accept all prompts").action(async (opts) => {
|
|
874
|
+
const cwd = process.cwd();
|
|
875
|
+
if (opts.full) {
|
|
876
|
+
console.log("Running full khotan setup...\n");
|
|
877
|
+
const results = await runFullSetup(cwd);
|
|
878
|
+
const succeeded = results.filter((r) => r.status === "success");
|
|
879
|
+
const skipped = results.filter((r) => r.status === "skipped");
|
|
880
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
881
|
+
console.log("\n\u2500\u2500 Setup Summary \u2500\u2500");
|
|
882
|
+
for (const r of succeeded) {
|
|
883
|
+
console.log(` \u2713 ${r.name}`);
|
|
884
|
+
}
|
|
885
|
+
for (const r of skipped) {
|
|
886
|
+
console.log(` \u2298 ${r.name} (already done)`);
|
|
887
|
+
}
|
|
888
|
+
for (const r of failed) {
|
|
889
|
+
console.log(` \u2717 ${r.name}: ${r.error ?? "unknown error"}`);
|
|
890
|
+
}
|
|
891
|
+
if (failed.length > 0) {
|
|
892
|
+
console.log(
|
|
893
|
+
`
|
|
894
|
+
${String(failed.length)} step(s) failed. You may need to run them manually.`
|
|
895
|
+
);
|
|
896
|
+
} else {
|
|
897
|
+
console.log("\nAll done! Your project is ready for khotan.");
|
|
898
|
+
}
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const configPath = path2.resolve(cwd, "khotan.config.ts");
|
|
902
|
+
const outputDir = resolveOutputDir(cwd);
|
|
903
|
+
if (fs4.existsSync(configPath)) {
|
|
904
|
+
console.log(`\u2713 khotan.config.ts already exists, skipping`);
|
|
905
|
+
} else {
|
|
906
|
+
fs4.writeFileSync(configPath, configTemplate(outputDir), "utf-8");
|
|
907
|
+
console.log(`\u2713 Created khotan.config.ts (outputDir: ${outputDir})`);
|
|
908
|
+
}
|
|
909
|
+
const coreFiles = scaffoldCoreFiles(cwd, outputDir);
|
|
910
|
+
ensureKhotanDataInstalled(cwd);
|
|
911
|
+
let installSkills2 = opts.yes ?? false;
|
|
912
|
+
if (!installSkills2 && process.stdin.isTTY) {
|
|
913
|
+
const response = await prompts2({
|
|
914
|
+
type: "confirm",
|
|
915
|
+
name: "install",
|
|
916
|
+
message: "Install agent skills for AI-assisted development? (Y/n)",
|
|
917
|
+
initial: true
|
|
918
|
+
});
|
|
919
|
+
installSkills2 = response.install === true;
|
|
920
|
+
}
|
|
921
|
+
if (installSkills2) {
|
|
922
|
+
const count = scaffoldAgentSkills(cwd);
|
|
923
|
+
if (count > 0) {
|
|
924
|
+
console.log(`\u2713 Installed ${String(count)} agent skills`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const allFiles = [
|
|
928
|
+
...fs4.existsSync(configPath) && coreFiles.length === 0 ? [] : ["khotan.config.ts"],
|
|
929
|
+
...coreFiles
|
|
930
|
+
];
|
|
931
|
+
if (allFiles.length > 0 || coreFiles.length > 0) {
|
|
932
|
+
console.log("\nNext steps:");
|
|
933
|
+
console.log(" 1. Update the db import in your khotan config file");
|
|
934
|
+
console.log(" 2. Run `npx khotan add plug` to add the HTTP client");
|
|
935
|
+
console.log(" 3. Run `npx khotan migrate` to create database tables");
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
function readDrizzleConfig(projectRoot) {
|
|
939
|
+
const configPath = path2.join(projectRoot, "drizzle.config.ts");
|
|
940
|
+
if (!fs4.existsSync(configPath)) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
try {
|
|
944
|
+
return { content: fs4.readFileSync(configPath, "utf-8"), configPath };
|
|
945
|
+
} catch {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function parseSchemaValue(content) {
|
|
950
|
+
const schemaMatch = /schema:\s*["'`]([^"'`]+)["'`]/.exec(content);
|
|
951
|
+
return schemaMatch?.[1] ?? null;
|
|
952
|
+
}
|
|
953
|
+
function resolveDrizzleSchemaDir(projectRoot) {
|
|
954
|
+
const config = readDrizzleConfig(projectRoot);
|
|
955
|
+
if (!config) return null;
|
|
956
|
+
const schemaValue = parseSchemaValue(config.content);
|
|
957
|
+
if (!schemaValue) return null;
|
|
958
|
+
const normalized = schemaValue.replace(/^\.\//, "");
|
|
959
|
+
if (normalized.includes("*")) {
|
|
960
|
+
return normalized.replace(/\/\*.*$/, "");
|
|
961
|
+
}
|
|
962
|
+
if (/\.\w+$/.test(normalized)) {
|
|
963
|
+
return path2.dirname(normalized);
|
|
964
|
+
}
|
|
965
|
+
return normalized;
|
|
966
|
+
}
|
|
967
|
+
function detectSingleFileSchema(projectRoot) {
|
|
968
|
+
const config = readDrizzleConfig(projectRoot);
|
|
969
|
+
if (!config) return null;
|
|
970
|
+
const schemaValue = parseSchemaValue(config.content);
|
|
971
|
+
if (!schemaValue) return null;
|
|
972
|
+
const normalized = schemaValue.replace(/^\.\//, "");
|
|
973
|
+
if (normalized.includes("*")) return null;
|
|
974
|
+
if (!/\.\w+$/.test(normalized)) return null;
|
|
975
|
+
const dir = path2.dirname(normalized);
|
|
976
|
+
const prefix = schemaValue.startsWith("./") ? "./" : "";
|
|
977
|
+
return {
|
|
978
|
+
configPath: config.configPath,
|
|
979
|
+
currentValue: schemaValue,
|
|
980
|
+
globValue: `${prefix}${dir}/*`
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function updateDrizzleConfigSchema(configPath, oldValue, newValue) {
|
|
984
|
+
const content = fs4.readFileSync(configPath, "utf-8");
|
|
985
|
+
const updated = content.replace(
|
|
986
|
+
new RegExp(
|
|
987
|
+
`(schema:\\s*)(["'\`])${oldValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\2`
|
|
988
|
+
),
|
|
989
|
+
`$1$2${newValue}$2`
|
|
990
|
+
);
|
|
991
|
+
fs4.writeFileSync(configPath, updated, "utf-8");
|
|
992
|
+
}
|
|
993
|
+
function findNextConfigPath(cwd) {
|
|
994
|
+
const candidates = [
|
|
995
|
+
"next.config.ts",
|
|
996
|
+
"next.config.mts",
|
|
997
|
+
"next.config.js",
|
|
998
|
+
"next.config.mjs"
|
|
999
|
+
];
|
|
1000
|
+
for (const candidate of candidates) {
|
|
1001
|
+
const fullPath = path2.join(cwd, candidate);
|
|
1002
|
+
if (fs4.existsSync(fullPath)) return fullPath;
|
|
1003
|
+
}
|
|
1004
|
+
return path2.join(cwd, "next.config.ts");
|
|
1005
|
+
}
|
|
1006
|
+
function ensureWorkflowImport(source) {
|
|
1007
|
+
if (source.includes(`from "workflow/next"`) || source.includes(`from 'workflow/next'`)) {
|
|
1008
|
+
return source;
|
|
1009
|
+
}
|
|
1010
|
+
const nextImportPattern = /import\s+type\s+\{\s*NextConfig\s*\}\s+from\s+["']next["'];?\n?/;
|
|
1011
|
+
if (nextImportPattern.test(source)) {
|
|
1012
|
+
return source.replace(
|
|
1013
|
+
nextImportPattern,
|
|
1014
|
+
(match) => `${match}import { withWorkflow } from "workflow/next";
|
|
1015
|
+
`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
return `import { withWorkflow } from "workflow/next";
|
|
1019
|
+
${source}`;
|
|
1020
|
+
}
|
|
1021
|
+
function wrapDefaultExport(source) {
|
|
1022
|
+
if (source.includes("export default withWorkflow(")) {
|
|
1023
|
+
return source;
|
|
1024
|
+
}
|
|
1025
|
+
const namedExportPattern = /export\s+default\s+([A-Za-z_$][\w$]*)\s*;?/;
|
|
1026
|
+
if (namedExportPattern.test(source)) {
|
|
1027
|
+
return source.replace(
|
|
1028
|
+
namedExportPattern,
|
|
1029
|
+
"export default withWorkflow($1);"
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
const objectExportPattern = /export\s+default\s+(\{[\s\S]*\})\s*;?/m;
|
|
1033
|
+
if (objectExportPattern.test(source)) {
|
|
1034
|
+
return source.replace(
|
|
1035
|
+
objectExportPattern,
|
|
1036
|
+
"export default withWorkflow($1);"
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
function ensureWorkflowNextConfig(cwd) {
|
|
1042
|
+
const configPath = findNextConfigPath(cwd);
|
|
1043
|
+
if (!fs4.existsSync(configPath)) {
|
|
1044
|
+
fs4.writeFileSync(
|
|
1045
|
+
configPath,
|
|
1046
|
+
[
|
|
1047
|
+
'import type { NextConfig } from "next";',
|
|
1048
|
+
'import { withWorkflow } from "workflow/next";',
|
|
1049
|
+
"",
|
|
1050
|
+
"const nextConfig: NextConfig = {};",
|
|
1051
|
+
"",
|
|
1052
|
+
"export default withWorkflow(nextConfig);",
|
|
1053
|
+
""
|
|
1054
|
+
].join("\n"),
|
|
1055
|
+
"utf-8"
|
|
1056
|
+
);
|
|
1057
|
+
return { status: "created", path: configPath };
|
|
1058
|
+
}
|
|
1059
|
+
const original = fs4.readFileSync(configPath, "utf-8");
|
|
1060
|
+
const withImport = ensureWorkflowImport(original);
|
|
1061
|
+
const wrapped = wrapDefaultExport(withImport);
|
|
1062
|
+
if (!wrapped) {
|
|
1063
|
+
return { status: "unsupported", path: configPath };
|
|
1064
|
+
}
|
|
1065
|
+
if (wrapped === original) {
|
|
1066
|
+
return { status: "skipped", path: configPath };
|
|
1067
|
+
}
|
|
1068
|
+
fs4.writeFileSync(configPath, wrapped, "utf-8");
|
|
1069
|
+
return { status: "updated", path: configPath };
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/cli/commands/add.ts
|
|
1073
|
+
async function loadConfig() {
|
|
1074
|
+
const configPath = path2.resolve(process.cwd(), "khotan.config.ts");
|
|
1075
|
+
if (!fs4.existsSync(configPath)) {
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
const content = fs4.readFileSync(configPath, "utf-8");
|
|
1079
|
+
const outputDirMatch = /outputDir:\s*["']([^"']+)["']/.exec(content);
|
|
1080
|
+
return {
|
|
1081
|
+
outputDir: outputDirMatch?.[1] ?? "src/lib/khotan"
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function hasSrcLayout2(cwd) {
|
|
1085
|
+
return fs4.existsSync(path2.join(cwd, "src", "app"));
|
|
1086
|
+
}
|
|
1087
|
+
function resolveOutputBase(file, cwd, outputDir) {
|
|
1088
|
+
switch (file.outputBase) {
|
|
1089
|
+
case "components":
|
|
1090
|
+
return path2.resolve(
|
|
1091
|
+
cwd,
|
|
1092
|
+
hasSrcLayout2(cwd) ? "src/components/khotan" : "components/khotan"
|
|
1093
|
+
);
|
|
1094
|
+
case "app":
|
|
1095
|
+
return path2.resolve(
|
|
1096
|
+
cwd,
|
|
1097
|
+
hasSrcLayout2(cwd) ? "src/app/api/khotan/[...all]" : "app/api/khotan/[...all]"
|
|
1098
|
+
);
|
|
1099
|
+
case "appRoot":
|
|
1100
|
+
return path2.resolve(cwd, hasSrcLayout2(cwd) ? "src/app" : "app");
|
|
1101
|
+
case "projectRoot":
|
|
1102
|
+
return path2.resolve(cwd);
|
|
1103
|
+
case "outputDir":
|
|
1104
|
+
default:
|
|
1105
|
+
return path2.resolve(cwd, outputDir);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function isScaffolded(entry, cwd, outputDir) {
|
|
1109
|
+
if (isMultiFile(entry)) {
|
|
1110
|
+
return entry.files.some((f) => {
|
|
1111
|
+
const base = resolveOutputBase(f, cwd, outputDir);
|
|
1112
|
+
return fs4.existsSync(path2.join(base, f.outputFile));
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
if (entry.outputFile) {
|
|
1116
|
+
return fs4.existsSync(path2.join(cwd, outputDir, entry.outputFile));
|
|
1117
|
+
}
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
async function scaffoldFile(templatePath, outputPath, force) {
|
|
1121
|
+
if (fs4.existsSync(outputPath) && !force) {
|
|
1122
|
+
const response = await prompts2({
|
|
1123
|
+
type: "confirm",
|
|
1124
|
+
name: "overwrite",
|
|
1125
|
+
message: `${path2.basename(outputPath)} already exists at ${outputPath}. Overwrite?`,
|
|
1126
|
+
initial: false
|
|
1127
|
+
});
|
|
1128
|
+
if (!response.overwrite) {
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
fs4.mkdirSync(path2.dirname(outputPath), { recursive: true });
|
|
1133
|
+
const content = fs4.readFileSync(templatePath, "utf-8");
|
|
1134
|
+
fs4.writeFileSync(outputPath, content, "utf-8");
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
async function checkAndInstallDeps(cwd, deps, autoYes) {
|
|
1138
|
+
const allPackages = [
|
|
1139
|
+
...deps.npmPackages ?? [],
|
|
1140
|
+
...deps.npmDevPackages ?? []
|
|
1141
|
+
];
|
|
1142
|
+
const missingPkgs = allPackages.length > 0 ? checkNpmPackages(cwd, allPackages) : [];
|
|
1143
|
+
const missingShadcn = deps.shadcnComponents ? checkShadcnComponents(cwd, deps.shadcnComponents) : [];
|
|
1144
|
+
if (missingPkgs.length === 0 && missingShadcn.length === 0) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (missingPkgs.length > 0) {
|
|
1148
|
+
console.log(`
|
|
1149
|
+
Missing npm packages: ${missingPkgs.join(", ")}`);
|
|
1150
|
+
}
|
|
1151
|
+
if (missingShadcn.length > 0) {
|
|
1152
|
+
console.log(`Missing shadcn components: ${missingShadcn.join(", ")}`);
|
|
1153
|
+
}
|
|
1154
|
+
let shouldInstall = autoYes;
|
|
1155
|
+
if (!shouldInstall && process.stdin.isTTY) {
|
|
1156
|
+
const response = await prompts2({
|
|
1157
|
+
type: "confirm",
|
|
1158
|
+
name: "install",
|
|
1159
|
+
message: "Install missing dependencies?",
|
|
1160
|
+
initial: true
|
|
1161
|
+
});
|
|
1162
|
+
shouldInstall = response.install === true;
|
|
1163
|
+
}
|
|
1164
|
+
if (!shouldInstall) {
|
|
1165
|
+
console.warn(
|
|
1166
|
+
"\u26A0 Skipping dependency install. The component may not work without them."
|
|
1167
|
+
);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (missingPkgs.length > 0) {
|
|
1171
|
+
const regularPkgs = missingPkgs.filter(
|
|
1172
|
+
(p) => !(deps.npmDevPackages ?? []).includes(p)
|
|
1173
|
+
);
|
|
1174
|
+
const devPkgs = missingPkgs.filter(
|
|
1175
|
+
(p) => (deps.npmDevPackages ?? []).includes(p)
|
|
1176
|
+
);
|
|
1177
|
+
if (regularPkgs.length > 0) {
|
|
1178
|
+
console.log(`Installing ${regularPkgs.join(", ")}...`);
|
|
1179
|
+
const result = installPackages(cwd, regularPkgs);
|
|
1180
|
+
if (result.success) {
|
|
1181
|
+
console.log(`\u2713 Installed ${regularPkgs.join(", ")}`);
|
|
1182
|
+
} else {
|
|
1183
|
+
console.error(
|
|
1184
|
+
`\u2717 Failed to install packages: ${result.error ?? "unknown error"}`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (devPkgs.length > 0) {
|
|
1189
|
+
console.log(`Installing ${devPkgs.join(", ")} (dev)...`);
|
|
1190
|
+
const result = installPackages(cwd, devPkgs, { devDependency: true });
|
|
1191
|
+
if (result.success) {
|
|
1192
|
+
console.log(`\u2713 Installed ${devPkgs.join(", ")} as dev dependencies`);
|
|
1193
|
+
} else {
|
|
1194
|
+
console.error(
|
|
1195
|
+
`\u2717 Failed to install dev packages: ${result.error ?? "unknown error"}`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (missingShadcn.length > 0) {
|
|
1201
|
+
console.log(`Installing shadcn components: ${missingShadcn.join(", ")}...`);
|
|
1202
|
+
const result = installShadcnComponents(cwd, missingShadcn);
|
|
1203
|
+
if (result.success) {
|
|
1204
|
+
console.log(`\u2713 Installed shadcn components: ${missingShadcn.join(", ")}`);
|
|
1205
|
+
} else {
|
|
1206
|
+
console.error(
|
|
1207
|
+
`\u2717 Failed to install shadcn components: ${result.error ?? "unknown error"}`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
var addCommand = new Command("add").description("Add a component or block to your project").argument(
|
|
1213
|
+
"<name>",
|
|
1214
|
+
"Component or block to add (e.g., plug, inflow, outflow, relay, schema, hub, config-page-1)"
|
|
1215
|
+
).option("-f, --force", "Overwrite existing files without prompting").option("-y, --yes", "Auto-accept all install prompts").option("--without-ui", "Skip UI component scaffolding").action(
|
|
1216
|
+
async (componentName, opts) => {
|
|
1217
|
+
let config = await loadConfig();
|
|
1218
|
+
if (!config) {
|
|
1219
|
+
console.log("No khotan.config.ts found. Running init...\n");
|
|
1220
|
+
const initOk = await runInit(process.cwd());
|
|
1221
|
+
if (!initOk) {
|
|
1222
|
+
console.error("\u2717 Init failed. Cannot proceed with add.");
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
config = await loadConfig();
|
|
1226
|
+
if (!config) {
|
|
1227
|
+
console.error("\u2717 Could not read config after init.");
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
console.log("");
|
|
1231
|
+
}
|
|
1232
|
+
const cwd = process.cwd();
|
|
1233
|
+
scaffoldCoreFiles(cwd, config.outputDir);
|
|
1234
|
+
const result = getEntry(componentName);
|
|
1235
|
+
if (!result) {
|
|
1236
|
+
const components = listComponents().map((c) => c.name).join(", ");
|
|
1237
|
+
const blocks = listBlocks().map((b) => b.name).join(", ");
|
|
1238
|
+
console.error(`\u2717 Unknown name "${componentName}".`);
|
|
1239
|
+
console.error(` Components: ${components}`);
|
|
1240
|
+
console.error(` Blocks: ${blocks}`);
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
const { entry: component, kind } = result;
|
|
1244
|
+
if (component.requires && component.requires.length > 0) {
|
|
1245
|
+
for (const reqName of component.requires) {
|
|
1246
|
+
const reqResult = getEntry(reqName);
|
|
1247
|
+
if (!reqResult) continue;
|
|
1248
|
+
const req = reqResult.entry;
|
|
1249
|
+
if (!isScaffolded(req, cwd, config.outputDir)) {
|
|
1250
|
+
let shouldAdd = opts.yes ?? false;
|
|
1251
|
+
if (!shouldAdd && process.stdin.isTTY) {
|
|
1252
|
+
const response = await prompts2({
|
|
1253
|
+
type: "confirm",
|
|
1254
|
+
name: "add",
|
|
1255
|
+
message: `"${componentName}" requires the "${reqName}" component. Add it now?`,
|
|
1256
|
+
initial: true
|
|
1257
|
+
});
|
|
1258
|
+
shouldAdd = response.add === true;
|
|
1259
|
+
} else if (!shouldAdd) {
|
|
1260
|
+
shouldAdd = true;
|
|
1261
|
+
}
|
|
1262
|
+
if (shouldAdd) {
|
|
1263
|
+
console.log(`
|
|
1264
|
+
Adding required component: ${reqName}...`);
|
|
1265
|
+
const { execSync: execSync4 } = await import('child_process');
|
|
1266
|
+
execSync4(
|
|
1267
|
+
`node ${process.argv[1] ?? ""} add ${reqName}${opts.force ? " --force" : ""}${opts.yes ? " --yes" : ""}`,
|
|
1268
|
+
{ cwd, stdio: "inherit" }
|
|
1269
|
+
);
|
|
1270
|
+
console.log("");
|
|
1271
|
+
} else {
|
|
1272
|
+
console.warn(
|
|
1273
|
+
`\u26A0 Skipping "${reqName}". The ${kind} may not work without it.`
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (component.dependencies) {
|
|
1280
|
+
const deps = { ...component.dependencies };
|
|
1281
|
+
if (opts.withoutUi) {
|
|
1282
|
+
delete deps.shadcnComponents;
|
|
1283
|
+
}
|
|
1284
|
+
await checkAndInstallDeps(cwd, deps, opts.yes ?? false);
|
|
1285
|
+
}
|
|
1286
|
+
if (component.requiresShadcn && !opts.withoutUi) {
|
|
1287
|
+
const shadcnConfig = path2.join(cwd, "components.json");
|
|
1288
|
+
if (!fs4.existsSync(shadcnConfig)) {
|
|
1289
|
+
console.warn(
|
|
1290
|
+
"\u26A0 shadcn/ui is required for the Hub component but components.json was not found."
|
|
1291
|
+
);
|
|
1292
|
+
console.warn(
|
|
1293
|
+
" Run `npx shadcn-ui@latest init` first, then re-run this command.\n"
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (isMultiFile(component)) {
|
|
1298
|
+
const hasAgentSkills = component.files.some(
|
|
1299
|
+
(f) => f.outputBase === "agentSkills"
|
|
1300
|
+
);
|
|
1301
|
+
if (hasAgentSkills) {
|
|
1302
|
+
const skills = component.files.filter((f) => f.outputBase === "agentSkills").map((f) => ({ name: f.outputFile, templatePath: f.templatePath }));
|
|
1303
|
+
const agentsTemplatePath = path2.resolve(
|
|
1304
|
+
path2.dirname(component.files[0].templatePath),
|
|
1305
|
+
"agents.md"
|
|
1306
|
+
);
|
|
1307
|
+
const result2 = installSkills(cwd, skills, agentsTemplatePath);
|
|
1308
|
+
if (result2.created.length > 0) {
|
|
1309
|
+
console.log(
|
|
1310
|
+
`
|
|
1311
|
+
\u2713 Installed to ${result2.agents.join(", ")} (${String(result2.created.length)} files):`
|
|
1312
|
+
);
|
|
1313
|
+
for (const f of result2.created) {
|
|
1314
|
+
console.log(` ${f}`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (result2.skipped.length > 0 && result2.created.length === 0) {
|
|
1318
|
+
console.log("\n\u2713 All skill files already exist, skipping");
|
|
1319
|
+
}
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const createdFiles = [];
|
|
1323
|
+
let filesToScaffold = component.files;
|
|
1324
|
+
if (opts.withoutUi) {
|
|
1325
|
+
filesToScaffold = filesToScaffold.filter(
|
|
1326
|
+
(f) => !f.outputFile.endsWith(".tsx")
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
for (const file of filesToScaffold) {
|
|
1330
|
+
if (file.outputBase === "appRoot") {
|
|
1331
|
+
const baseDir = resolveOutputBase(file, cwd, config.outputDir);
|
|
1332
|
+
const outputPath2 = path2.join(baseDir, file.outputFile);
|
|
1333
|
+
const routeDir = path2.dirname(outputPath2);
|
|
1334
|
+
const pageExtensions = [
|
|
1335
|
+
"page.tsx",
|
|
1336
|
+
"page.ts",
|
|
1337
|
+
"page.jsx",
|
|
1338
|
+
"page.js"
|
|
1339
|
+
];
|
|
1340
|
+
const existing = pageExtensions.find(
|
|
1341
|
+
(ext) => fs4.existsSync(path2.join(routeDir, ext))
|
|
1342
|
+
);
|
|
1343
|
+
if (existing && existing !== path2.basename(file.outputFile)) {
|
|
1344
|
+
const relRoute = path2.relative(
|
|
1345
|
+
cwd,
|
|
1346
|
+
path2.join(routeDir, existing)
|
|
1347
|
+
);
|
|
1348
|
+
console.warn(
|
|
1349
|
+
`
|
|
1350
|
+
\u26A0 Route collision: ${relRoute} already exists at this path.`
|
|
1351
|
+
);
|
|
1352
|
+
console.warn(
|
|
1353
|
+
` Adding "${component.name}" will create a conflicting route file.
|
|
1354
|
+
`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
for (const file of filesToScaffold) {
|
|
1360
|
+
const baseDir = resolveOutputBase(file, cwd, config.outputDir);
|
|
1361
|
+
const outputPath2 = path2.join(baseDir, file.outputFile);
|
|
1362
|
+
const created2 = await scaffoldFile(
|
|
1363
|
+
file.templatePath,
|
|
1364
|
+
outputPath2,
|
|
1365
|
+
opts.force ?? false
|
|
1366
|
+
);
|
|
1367
|
+
if (created2) {
|
|
1368
|
+
createdFiles.push(path2.relative(cwd, outputPath2));
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
if (createdFiles.length > 0) {
|
|
1372
|
+
console.log(`
|
|
1373
|
+
\u2713 Created ${String(createdFiles.length)} files:`);
|
|
1374
|
+
for (const f of createdFiles) {
|
|
1375
|
+
console.log(` ${f}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (component.requiresWorkflowIntegration) {
|
|
1379
|
+
const workflowConfig = ensureWorkflowNextConfig(cwd);
|
|
1380
|
+
const relPath = path2.relative(cwd, workflowConfig.path);
|
|
1381
|
+
if (workflowConfig.status === "created") {
|
|
1382
|
+
console.log(`\u2713 Created ${relPath} with Workflow integration`);
|
|
1383
|
+
} else if (workflowConfig.status === "updated") {
|
|
1384
|
+
console.log(`\u2713 Updated ${relPath} with Workflow integration`);
|
|
1385
|
+
} else if (workflowConfig.status === "skipped") {
|
|
1386
|
+
console.log(`\u2713 ${relPath} already has Workflow integration`);
|
|
1387
|
+
} else {
|
|
1388
|
+
console.warn(
|
|
1389
|
+
`\u26A0 Could not automatically update ${relPath} for Workflow integration. Wrap its default export with withWorkflow() from "workflow/next".`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (componentName === "hub") {
|
|
1394
|
+
console.log("\nNext steps:");
|
|
1395
|
+
console.log(
|
|
1396
|
+
" 1. Render <KhotanHub /> on a page, or `npx khotan add config-page-1` for a ready-made /config route"
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (!component.templatePath || !component.outputFile) {
|
|
1402
|
+
console.error(
|
|
1403
|
+
`\u2717 ${kind === "block" ? "Block" : "Component"} "${componentName}" has no template.`
|
|
1404
|
+
);
|
|
1405
|
+
process.exit(1);
|
|
1406
|
+
}
|
|
1407
|
+
let outputDir = path2.resolve(cwd, config.outputDir);
|
|
1408
|
+
let schemaDir = null;
|
|
1409
|
+
if (componentName === "schema") {
|
|
1410
|
+
schemaDir = resolveDrizzleSchemaDir(cwd);
|
|
1411
|
+
if (schemaDir) {
|
|
1412
|
+
outputDir = path2.resolve(cwd, schemaDir);
|
|
1413
|
+
console.log(`\u2713 Detected Drizzle schema directory: ${schemaDir}`);
|
|
1414
|
+
} else if (process.stdin.isTTY) {
|
|
1415
|
+
const response = await prompts2(
|
|
1416
|
+
{
|
|
1417
|
+
type: "text",
|
|
1418
|
+
name: "schemaPath",
|
|
1419
|
+
message: "Where should the schema file be placed?",
|
|
1420
|
+
initial: config.outputDir
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
onCancel: () => {
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
);
|
|
1427
|
+
const schemaPath = response.schemaPath;
|
|
1428
|
+
if (schemaPath) {
|
|
1429
|
+
outputDir = path2.resolve(cwd, schemaPath);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const outputPath = path2.join(outputDir, component.outputFile);
|
|
1434
|
+
const created = await scaffoldFile(
|
|
1435
|
+
component.templatePath,
|
|
1436
|
+
outputPath,
|
|
1437
|
+
opts.force ?? false
|
|
1438
|
+
);
|
|
1439
|
+
if (!created) {
|
|
1440
|
+
console.log("Cancelled.");
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
console.log(`\u2713 Created ${path2.relative(cwd, outputPath)}`);
|
|
1444
|
+
if (component.requiresWorkflowIntegration) {
|
|
1445
|
+
const workflowConfig = ensureWorkflowNextConfig(cwd);
|
|
1446
|
+
const relPath = path2.relative(cwd, workflowConfig.path);
|
|
1447
|
+
if (workflowConfig.status === "created") {
|
|
1448
|
+
console.log(`\u2713 Created ${relPath} with Workflow integration`);
|
|
1449
|
+
} else if (workflowConfig.status === "updated") {
|
|
1450
|
+
console.log(`\u2713 Updated ${relPath} with Workflow integration`);
|
|
1451
|
+
} else if (workflowConfig.status === "skipped") {
|
|
1452
|
+
console.log(`\u2713 ${relPath} already has Workflow integration`);
|
|
1453
|
+
} else {
|
|
1454
|
+
console.warn(
|
|
1455
|
+
`\u26A0 Could not automatically update ${relPath} for Workflow integration. Wrap its default export with withWorkflow() from "workflow/next".`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (componentName === "schema") {
|
|
1460
|
+
const autoYes = opts.yes ?? false;
|
|
1461
|
+
const singleFile = detectSingleFileSchema(cwd);
|
|
1462
|
+
if (singleFile) {
|
|
1463
|
+
const relConfig = path2.relative(cwd, singleFile.configPath);
|
|
1464
|
+
console.log(
|
|
1465
|
+
`
|
|
1466
|
+
\u26A0 ${relConfig} points to a single file ("${singleFile.currentValue}").`
|
|
1467
|
+
);
|
|
1468
|
+
console.log(
|
|
1469
|
+
` Drizzle Kit won't pick up khotan.ts unless the schema is a glob.`
|
|
1470
|
+
);
|
|
1471
|
+
let shouldUpdate = autoYes;
|
|
1472
|
+
if (!shouldUpdate && process.stdin.isTTY) {
|
|
1473
|
+
const response = await prompts2({
|
|
1474
|
+
type: "confirm",
|
|
1475
|
+
name: "update",
|
|
1476
|
+
message: `Update schema to "${singleFile.globValue}" so Drizzle Kit picks up all files?`,
|
|
1477
|
+
initial: true
|
|
1478
|
+
});
|
|
1479
|
+
shouldUpdate = response.update === true;
|
|
1480
|
+
}
|
|
1481
|
+
if (shouldUpdate) {
|
|
1482
|
+
updateDrizzleConfigSchema(
|
|
1483
|
+
singleFile.configPath,
|
|
1484
|
+
singleFile.currentValue,
|
|
1485
|
+
singleFile.globValue
|
|
1486
|
+
);
|
|
1487
|
+
console.log(
|
|
1488
|
+
`\u2713 Updated ${relConfig}: schema \u2192 "${singleFile.globValue}"`
|
|
1489
|
+
);
|
|
1490
|
+
} else {
|
|
1491
|
+
console.log(
|
|
1492
|
+
` Skipped. Update the schema value manually or Drizzle Kit won't see the khotan tables.`
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
const barrelPath = path2.join(outputDir, "index.ts");
|
|
1497
|
+
if (fs4.existsSync(barrelPath)) {
|
|
1498
|
+
const barrelContent = fs4.readFileSync(barrelPath, "utf-8");
|
|
1499
|
+
const relBarrel = path2.relative(cwd, barrelPath);
|
|
1500
|
+
if (barrelContent.includes("./khotan")) {
|
|
1501
|
+
console.log(`\u2713 ${relBarrel} already re-exports khotan`);
|
|
1502
|
+
} else {
|
|
1503
|
+
let shouldUpdate = autoYes;
|
|
1504
|
+
if (!shouldUpdate && process.stdin.isTTY) {
|
|
1505
|
+
const response = await prompts2({
|
|
1506
|
+
type: "confirm",
|
|
1507
|
+
name: "update",
|
|
1508
|
+
message: `Add \`export * from "./khotan"\` to ${relBarrel}?`,
|
|
1509
|
+
initial: true
|
|
1510
|
+
});
|
|
1511
|
+
shouldUpdate = response.update === true;
|
|
1512
|
+
}
|
|
1513
|
+
if (shouldUpdate) {
|
|
1514
|
+
const separator = barrelContent.endsWith("\n") ? "" : "\n";
|
|
1515
|
+
fs4.appendFileSync(
|
|
1516
|
+
barrelPath,
|
|
1517
|
+
`${separator}export * from "./khotan";
|
|
1518
|
+
`
|
|
1519
|
+
);
|
|
1520
|
+
console.log(`\u2713 Updated ${relBarrel} with khotan re-export`);
|
|
1521
|
+
} else {
|
|
1522
|
+
console.log(` Skipped. Add this to ${relBarrel} manually:
|
|
1523
|
+
`);
|
|
1524
|
+
console.log(` export * from "./khotan";`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
} else {
|
|
1528
|
+
const relDir = path2.relative(cwd, outputDir);
|
|
1529
|
+
const importBase = relDir.replace(/^src\//, "@/");
|
|
1530
|
+
console.log(
|
|
1531
|
+
`
|
|
1532
|
+
Add this re-export to your Drizzle schema barrel file:
|
|
1533
|
+
`
|
|
1534
|
+
);
|
|
1535
|
+
console.log(` export * from "${importBase}/khotan";`);
|
|
1536
|
+
}
|
|
1537
|
+
} else {
|
|
1538
|
+
const importBase = config.outputDir.replace(/^src\//, "@/");
|
|
1539
|
+
if (componentName === "wire") {
|
|
1540
|
+
console.log(`
|
|
1541
|
+
Usage:
|
|
1542
|
+
`);
|
|
1543
|
+
console.log(` import { wire } from "${importBase}/wire";`);
|
|
1544
|
+
console.log(` import { myPlug } from "${importBase}/plugs/plug";`);
|
|
1545
|
+
console.log(` import { db } from "@/db";
|
|
1546
|
+
`);
|
|
1547
|
+
console.log(` const myWire = wire({`);
|
|
1548
|
+
console.log(` plug: myPlug,`);
|
|
1549
|
+
console.log(` db,`);
|
|
1550
|
+
console.log(` subscribe: {`);
|
|
1551
|
+
console.log(` path: "/webhooks",`);
|
|
1552
|
+
console.log(
|
|
1553
|
+
` buildBody: (callbackUrl) => ({ url: callbackUrl, events: ["*"] }),`
|
|
1554
|
+
);
|
|
1555
|
+
console.log(` parseId: (res) => (res as { id: string }).id,`);
|
|
1556
|
+
console.log(` },`);
|
|
1557
|
+
console.log(` unsubscribe: {`);
|
|
1558
|
+
console.log(` path: (remoteId) => \`/webhooks/\${remoteId}\`,`);
|
|
1559
|
+
console.log(` },`);
|
|
1560
|
+
console.log(` });`);
|
|
1561
|
+
} else {
|
|
1562
|
+
console.log(`
|
|
1563
|
+
Usage:
|
|
1564
|
+
`);
|
|
1565
|
+
console.log(
|
|
1566
|
+
` import { plug, bearer } from "${importBase}/plugs/${component.name}";`
|
|
1567
|
+
);
|
|
1568
|
+
console.log(`
|
|
1569
|
+
const api = plug({`);
|
|
1570
|
+
console.log(` baseUrl: "https://api.example.com",`);
|
|
1571
|
+
console.log(` auth: bearer(process.env.API_TOKEN!),`);
|
|
1572
|
+
console.log(` });`);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
);
|
|
1577
|
+
function loadOutputDir() {
|
|
1578
|
+
const configPath = path2.resolve(process.cwd(), "khotan.config.ts");
|
|
1579
|
+
if (!fs4.existsSync(configPath)) return "src/lib/khotan";
|
|
1580
|
+
const content = fs4.readFileSync(configPath, "utf-8");
|
|
1581
|
+
const match = /outputDir:\s*["']([^"']+)["']/.exec(content);
|
|
1582
|
+
return match?.[1] ?? "src/lib/khotan";
|
|
1583
|
+
}
|
|
1584
|
+
function runGenerate(cwd) {
|
|
1585
|
+
const schema = getComponent("schema");
|
|
1586
|
+
if (!schema || isMultiFile(schema) || !schema.templatePath || !schema.outputFile) {
|
|
1587
|
+
console.error("\u2717 Could not find schema component in registry.");
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
let outputDir = loadOutputDir();
|
|
1591
|
+
const schemaDir = resolveDrizzleSchemaDir(cwd);
|
|
1592
|
+
if (schemaDir) {
|
|
1593
|
+
outputDir = schemaDir;
|
|
1594
|
+
console.log(`\u2713 Detected Drizzle schema directory: ${schemaDir}`);
|
|
1595
|
+
}
|
|
1596
|
+
const absOutputDir = path2.resolve(cwd, outputDir);
|
|
1597
|
+
const outputPath = path2.join(absOutputDir, schema.outputFile);
|
|
1598
|
+
fs4.mkdirSync(path2.dirname(outputPath), { recursive: true });
|
|
1599
|
+
const content = fs4.readFileSync(schema.templatePath, "utf-8");
|
|
1600
|
+
fs4.writeFileSync(outputPath, content, "utf-8");
|
|
1601
|
+
console.log(`\u2713 Created ${path2.relative(cwd, outputPath)}`);
|
|
1602
|
+
const singleFile = detectSingleFileSchema(cwd);
|
|
1603
|
+
if (singleFile) {
|
|
1604
|
+
const relConfig = path2.relative(cwd, singleFile.configPath);
|
|
1605
|
+
updateDrizzleConfigSchema(
|
|
1606
|
+
singleFile.configPath,
|
|
1607
|
+
singleFile.currentValue,
|
|
1608
|
+
singleFile.globValue
|
|
1609
|
+
);
|
|
1610
|
+
console.log(`\u2713 Updated ${relConfig}: schema \u2192 "${singleFile.globValue}"`);
|
|
1611
|
+
}
|
|
1612
|
+
const barrelPath = path2.join(absOutputDir, "index.ts");
|
|
1613
|
+
if (fs4.existsSync(barrelPath)) {
|
|
1614
|
+
const barrelContent = fs4.readFileSync(barrelPath, "utf-8");
|
|
1615
|
+
const relBarrel = path2.relative(cwd, barrelPath);
|
|
1616
|
+
if (barrelContent.includes("./khotan")) {
|
|
1617
|
+
console.log(`\u2713 ${relBarrel} already re-exports khotan`);
|
|
1618
|
+
} else {
|
|
1619
|
+
const separator = barrelContent.endsWith("\n") ? "" : "\n";
|
|
1620
|
+
fs4.appendFileSync(barrelPath, `${separator}export * from "./khotan";
|
|
1621
|
+
`);
|
|
1622
|
+
console.log(`\u2713 Updated ${relBarrel} with khotan re-export`);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return true;
|
|
1626
|
+
}
|
|
1627
|
+
var generateCommand = new Command("generate").description(
|
|
1628
|
+
"Generate the Khotan schema file and wire it into your Drizzle config"
|
|
1629
|
+
).action(async () => {
|
|
1630
|
+
const cwd = process.cwd();
|
|
1631
|
+
const configPath = path2.resolve(cwd, "khotan.config.ts");
|
|
1632
|
+
if (!fs4.existsSync(configPath)) {
|
|
1633
|
+
console.log("No khotan.config.ts found. Running init...\n");
|
|
1634
|
+
const initOk = await runInit(cwd);
|
|
1635
|
+
if (!initOk) {
|
|
1636
|
+
console.error("\u2717 Init failed. Cannot proceed.");
|
|
1637
|
+
process.exit(1);
|
|
1638
|
+
}
|
|
1639
|
+
console.log("");
|
|
1640
|
+
}
|
|
1641
|
+
const ok = runGenerate(cwd);
|
|
1642
|
+
if (!ok) process.exit(1);
|
|
1643
|
+
});
|
|
1644
|
+
function isSchemaScaffolded(cwd) {
|
|
1645
|
+
const schemaDir = resolveDrizzleSchemaDir(cwd);
|
|
1646
|
+
if (!schemaDir) return false;
|
|
1647
|
+
return fs4.existsSync(path2.join(cwd, schemaDir, "khotan.ts"));
|
|
1648
|
+
}
|
|
1649
|
+
var migrateCommand = new Command("migrate").description(
|
|
1650
|
+
"Generate a migration and apply it (or --push to skip migration files)"
|
|
1651
|
+
).option("--push", "Push schema directly without generating migration files").action(async (opts) => {
|
|
1652
|
+
const cwd = process.cwd();
|
|
1653
|
+
const configPath = path2.resolve(cwd, "khotan.config.ts");
|
|
1654
|
+
if (!fs4.existsSync(configPath)) {
|
|
1655
|
+
console.log("No khotan.config.ts found. Running init...\n");
|
|
1656
|
+
const initOk = await runInit(cwd);
|
|
1657
|
+
if (!initOk) {
|
|
1658
|
+
console.error("\u2717 Init failed. Cannot proceed.");
|
|
1659
|
+
process.exit(1);
|
|
1660
|
+
}
|
|
1661
|
+
console.log("");
|
|
1662
|
+
}
|
|
1663
|
+
if (!isSchemaScaffolded(cwd)) {
|
|
1664
|
+
console.log("Schema not found. Running generate...\n");
|
|
1665
|
+
const ok = runGenerate(cwd);
|
|
1666
|
+
if (!ok) {
|
|
1667
|
+
console.error("\u2717 Generate failed. Cannot proceed.");
|
|
1668
|
+
process.exit(1);
|
|
1669
|
+
}
|
|
1670
|
+
console.log("");
|
|
1671
|
+
} else {
|
|
1672
|
+
console.log("\u2713 Schema already generated");
|
|
1673
|
+
}
|
|
1674
|
+
const pm = detectPackageManager(cwd);
|
|
1675
|
+
const runner = pm.name === "npm" ? "npx" : pm.name;
|
|
1676
|
+
if (opts.push) {
|
|
1677
|
+
console.log("Pushing schema directly to database...\n");
|
|
1678
|
+
try {
|
|
1679
|
+
execSync(`${runner} drizzle-kit push`, {
|
|
1680
|
+
cwd,
|
|
1681
|
+
stdio: "inherit"
|
|
1682
|
+
});
|
|
1683
|
+
console.log("\n\u2713 Schema pushed to database");
|
|
1684
|
+
} catch {
|
|
1685
|
+
console.error("\n\u2717 drizzle-kit push failed.");
|
|
1686
|
+
console.error(
|
|
1687
|
+
" Make sure DATABASE_URL is set and drizzle-kit is installed."
|
|
1688
|
+
);
|
|
1689
|
+
process.exit(1);
|
|
1690
|
+
}
|
|
1691
|
+
} else {
|
|
1692
|
+
console.log("Generating migration...\n");
|
|
1693
|
+
try {
|
|
1694
|
+
execSync(`${runner} drizzle-kit generate`, {
|
|
1695
|
+
cwd,
|
|
1696
|
+
stdio: "inherit"
|
|
1697
|
+
});
|
|
1698
|
+
console.log("\n\u2713 Migration file generated");
|
|
1699
|
+
} catch {
|
|
1700
|
+
console.error("\n\u2717 drizzle-kit generate failed.");
|
|
1701
|
+
process.exit(1);
|
|
1702
|
+
}
|
|
1703
|
+
console.log("\nApplying migration...\n");
|
|
1704
|
+
try {
|
|
1705
|
+
execSync(`${runner} drizzle-kit migrate`, {
|
|
1706
|
+
cwd,
|
|
1707
|
+
stdio: "inherit"
|
|
1708
|
+
});
|
|
1709
|
+
console.log("\n\u2713 Migration applied successfully");
|
|
1710
|
+
} catch {
|
|
1711
|
+
console.error("\n\u2717 drizzle-kit migrate failed.");
|
|
1712
|
+
console.error(
|
|
1713
|
+
" Make sure DATABASE_URL is set and drizzle-kit is installed."
|
|
1714
|
+
);
|
|
1715
|
+
process.exit(1);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
// src/cli/compare.ts
|
|
1721
|
+
function inferShape(value) {
|
|
1722
|
+
if (value === null || value === void 0) {
|
|
1723
|
+
return { type: "null" };
|
|
1724
|
+
}
|
|
1725
|
+
if (Array.isArray(value)) {
|
|
1726
|
+
if (value.length === 0) {
|
|
1727
|
+
return { type: "array", items: null };
|
|
1728
|
+
}
|
|
1729
|
+
const merged = mergeShapes(value.map(inferShape));
|
|
1730
|
+
return { type: "array", items: merged };
|
|
1731
|
+
}
|
|
1732
|
+
switch (typeof value) {
|
|
1733
|
+
case "string":
|
|
1734
|
+
return { type: "string" };
|
|
1735
|
+
case "number":
|
|
1736
|
+
return { type: "number" };
|
|
1737
|
+
case "boolean":
|
|
1738
|
+
return { type: "boolean" };
|
|
1739
|
+
case "object": {
|
|
1740
|
+
const properties = {};
|
|
1741
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1742
|
+
properties[k] = inferShape(v);
|
|
1743
|
+
}
|
|
1744
|
+
return { type: "object", properties };
|
|
1745
|
+
}
|
|
1746
|
+
default:
|
|
1747
|
+
return { type: "string" };
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
function mergeShapes(shapes) {
|
|
1751
|
+
if (shapes.length === 0) return { type: "null" };
|
|
1752
|
+
if (shapes.length === 1) return shapes[0];
|
|
1753
|
+
const objectShapes = shapes.filter(
|
|
1754
|
+
(s) => s.type === "object"
|
|
1755
|
+
);
|
|
1756
|
+
if (objectShapes.length === shapes.length) {
|
|
1757
|
+
const merged = {};
|
|
1758
|
+
for (const shape of objectShapes) {
|
|
1759
|
+
for (const [key, node] of Object.entries(shape.properties)) {
|
|
1760
|
+
if (!merged[key]) {
|
|
1761
|
+
merged[key] = node;
|
|
1762
|
+
} else if (merged[key].type === "object" && node.type === "object") {
|
|
1763
|
+
merged[key] = mergeShapes([merged[key], node]);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return { type: "object", properties: merged };
|
|
1768
|
+
}
|
|
1769
|
+
return shapes[0];
|
|
1770
|
+
}
|
|
1771
|
+
function diffSchemas(expected, actual, basePath = "$") {
|
|
1772
|
+
if ("_type" in expected) {
|
|
1773
|
+
return diffTypedNode(expected, actual, basePath);
|
|
1774
|
+
}
|
|
1775
|
+
return diffObjectSchema(expected, actual, basePath);
|
|
1776
|
+
}
|
|
1777
|
+
function diffTypedNode(expected, actual, path15) {
|
|
1778
|
+
const expectedType = expected["_type"];
|
|
1779
|
+
if (expectedType === "array") {
|
|
1780
|
+
if (actual.type !== "array") {
|
|
1781
|
+
return [
|
|
1782
|
+
{
|
|
1783
|
+
path: path15,
|
|
1784
|
+
issue: "type_mismatch",
|
|
1785
|
+
note: `expected array, got ${actual.type}`
|
|
1786
|
+
}
|
|
1787
|
+
];
|
|
1788
|
+
}
|
|
1789
|
+
const itemSchema = expected["items"];
|
|
1790
|
+
if (!itemSchema || !actual.items) return [];
|
|
1791
|
+
return diffSchemas(itemSchema, actual.items, `${path15}[]`);
|
|
1792
|
+
}
|
|
1793
|
+
const normalizedExpected = normalizeType(expectedType);
|
|
1794
|
+
if (normalizedExpected !== actual.type && actual.type !== "null") {
|
|
1795
|
+
return [
|
|
1796
|
+
{
|
|
1797
|
+
path: path15,
|
|
1798
|
+
issue: "type_mismatch",
|
|
1799
|
+
note: `expected ${expectedType}, got ${actual.type}`
|
|
1800
|
+
}
|
|
1801
|
+
];
|
|
1802
|
+
}
|
|
1803
|
+
return [];
|
|
1804
|
+
}
|
|
1805
|
+
function diffObjectSchema(expected, actual, path15) {
|
|
1806
|
+
if (actual.type !== "object") {
|
|
1807
|
+
return [
|
|
1808
|
+
{
|
|
1809
|
+
path: path15,
|
|
1810
|
+
issue: "type_mismatch",
|
|
1811
|
+
note: `expected object, got ${actual.type}`
|
|
1812
|
+
}
|
|
1813
|
+
];
|
|
1814
|
+
}
|
|
1815
|
+
const mismatches = [];
|
|
1816
|
+
const actualProps = actual.properties;
|
|
1817
|
+
for (const [key, typeDesc] of Object.entries(expected)) {
|
|
1818
|
+
const childPath = path15 === "$" ? `$.${key}` : `${path15}.${key}`;
|
|
1819
|
+
const typeStr = typeof typeDesc === "string" ? typeDesc : null;
|
|
1820
|
+
const isOptional = typeStr?.endsWith("?") ?? false;
|
|
1821
|
+
if (!(key in actualProps)) {
|
|
1822
|
+
if (!isOptional) {
|
|
1823
|
+
mismatches.push({ path: childPath, issue: "missing" });
|
|
1824
|
+
}
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
const actualChild = actualProps[key];
|
|
1828
|
+
if (typeStr !== null) {
|
|
1829
|
+
const baseType = typeStr.replace(/\?$/, "").replace(/ \| null$/, "");
|
|
1830
|
+
if (baseType === "" || baseType === "unknown") ; else if (baseType.endsWith("[]")) {
|
|
1831
|
+
if (actualChild.type !== "array") {
|
|
1832
|
+
mismatches.push({
|
|
1833
|
+
path: childPath,
|
|
1834
|
+
issue: "type_mismatch",
|
|
1835
|
+
note: `expected ${baseType}, got ${actualChild.type}`
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
} else if (baseType === "object" && actualChild.type === "object") ; else {
|
|
1839
|
+
const normalizedBase = normalizeType(baseType);
|
|
1840
|
+
if (normalizedBase !== actualChild.type && actualChild.type !== "null") {
|
|
1841
|
+
mismatches.push({
|
|
1842
|
+
path: childPath,
|
|
1843
|
+
issue: "type_mismatch",
|
|
1844
|
+
note: `expected ${baseType}, got ${actualChild.type}`
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
} else if (typeof typeDesc === "object" && typeDesc !== null) {
|
|
1849
|
+
mismatches.push(
|
|
1850
|
+
...diffSchemas(typeDesc, actualChild, childPath)
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
for (const key of Object.keys(actualProps)) {
|
|
1855
|
+
if (!(key in expected)) {
|
|
1856
|
+
const childPath = path15 === "$" ? `$.${key}` : `${path15}.${key}`;
|
|
1857
|
+
mismatches.push({ path: childPath, issue: "extra" });
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return mismatches;
|
|
1861
|
+
}
|
|
1862
|
+
function normalizeType(typeStr) {
|
|
1863
|
+
const lower = typeStr.toLowerCase();
|
|
1864
|
+
switch (lower) {
|
|
1865
|
+
case "string":
|
|
1866
|
+
case "number":
|
|
1867
|
+
case "boolean":
|
|
1868
|
+
case "null":
|
|
1869
|
+
return lower;
|
|
1870
|
+
case "integer":
|
|
1871
|
+
case "bigint":
|
|
1872
|
+
return "number";
|
|
1873
|
+
default:
|
|
1874
|
+
return lower;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function output(obj) {
|
|
1878
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
1879
|
+
}
|
|
1880
|
+
function fail(error, hint) {
|
|
1881
|
+
output({ ok: false, error, hint });
|
|
1882
|
+
process.exit(1);
|
|
1883
|
+
}
|
|
1884
|
+
function parseEnvFile(filePath) {
|
|
1885
|
+
try {
|
|
1886
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1887
|
+
const values = {};
|
|
1888
|
+
for (const line of content.split("\n")) {
|
|
1889
|
+
const trimmed = line.trim();
|
|
1890
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1891
|
+
const match = /^([A-Z0-9_]+)\s*=\s*["']?(.*?)["']?$/.exec(trimmed);
|
|
1892
|
+
if (match) values[match[1]] = match[2];
|
|
1893
|
+
}
|
|
1894
|
+
return values;
|
|
1895
|
+
} catch {
|
|
1896
|
+
return {};
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
function parsePortFromEnvFile(filePath) {
|
|
1900
|
+
const env = parseEnvFile(filePath);
|
|
1901
|
+
const raw = env["PORT"];
|
|
1902
|
+
if (!raw) return null;
|
|
1903
|
+
const parsed = parseInt(raw, 10);
|
|
1904
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1905
|
+
}
|
|
1906
|
+
function resolvePort(portFlag) {
|
|
1907
|
+
if (portFlag) return parseInt(portFlag, 10);
|
|
1908
|
+
const cwd = process.cwd();
|
|
1909
|
+
return parsePortFromEnvFile(path2.join(cwd, ".env.local")) ?? parsePortFromEnvFile(path2.join(cwd, ".env")) ?? 3e3;
|
|
1910
|
+
}
|
|
1911
|
+
async function checkConnectivity(baseUrl) {
|
|
1912
|
+
let res;
|
|
1913
|
+
try {
|
|
1914
|
+
res = await fetch(`${baseUrl}/plugs`);
|
|
1915
|
+
} catch {
|
|
1916
|
+
fail(
|
|
1917
|
+
"connect_failed",
|
|
1918
|
+
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
if (!res.ok) {
|
|
1922
|
+
fail(
|
|
1923
|
+
"api_unavailable",
|
|
1924
|
+
`Could not reach Khotan API at ${baseUrl}. Check your base path and dev server.`
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
var varsCommand = new Command("vars").description("View and manage plug variables via the running Khotan API").argument("[plugName]", "Name of the plug whose variables you want to manage").argument("[action]", "Action: show | set | clear").option("--port <port>", "Dev server port").option("--base-path <path>", "API base path", "/api/khotan").option("--list", "List all plugs and their variable state").option("--json <json>", "Variable payload for set (JSON object)").action(
|
|
1929
|
+
async (plugName, action, opts) => {
|
|
1930
|
+
const port = resolvePort(opts.port);
|
|
1931
|
+
const baseUrl = `http://localhost:${port}${opts.basePath}`;
|
|
1932
|
+
await checkConnectivity(baseUrl);
|
|
1933
|
+
if (opts.list) {
|
|
1934
|
+
const plugsRes = await fetch(`${baseUrl}/plugs`);
|
|
1935
|
+
const plugs = await plugsRes.json();
|
|
1936
|
+
const variables = await Promise.all(
|
|
1937
|
+
plugs.map(async (plug) => {
|
|
1938
|
+
const res = await fetch(`${baseUrl}/variables/${plug.name}`);
|
|
1939
|
+
if (res.status === 404) {
|
|
1940
|
+
return {
|
|
1941
|
+
plugName: plug.name,
|
|
1942
|
+
configured: false,
|
|
1943
|
+
fields: [],
|
|
1944
|
+
values: {}
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
const data = await res.json();
|
|
1948
|
+
return {
|
|
1949
|
+
plugName: plug.name,
|
|
1950
|
+
configured: data.configured ?? false,
|
|
1951
|
+
fields: data.fields ?? [],
|
|
1952
|
+
values: data.values ?? {}
|
|
1953
|
+
};
|
|
1954
|
+
})
|
|
1955
|
+
);
|
|
1956
|
+
output({ ok: true, variables });
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (!plugName) {
|
|
1960
|
+
fail(
|
|
1961
|
+
"missing_plug",
|
|
1962
|
+
"Usage: khotan plug vars <plugName> [show|set|clear] or khotan plug vars --list"
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
const resolvedAction = action ?? "show";
|
|
1966
|
+
if (resolvedAction === "show") {
|
|
1967
|
+
const res = await fetch(`${baseUrl}/variables/${plugName}`);
|
|
1968
|
+
if (res.status === 404) {
|
|
1969
|
+
fail("plug_not_found", `Plug "${plugName}" not found.`);
|
|
1970
|
+
}
|
|
1971
|
+
const data = await res.json();
|
|
1972
|
+
output({
|
|
1973
|
+
ok: true,
|
|
1974
|
+
plugName,
|
|
1975
|
+
configured: data.configured ?? false,
|
|
1976
|
+
fields: data.fields ?? [],
|
|
1977
|
+
values: data.values ?? {}
|
|
1978
|
+
});
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
if (resolvedAction === "set") {
|
|
1982
|
+
if (!opts.json) {
|
|
1983
|
+
fail(
|
|
1984
|
+
"missing_json",
|
|
1985
|
+
`Provide --json with a JSON object. Example: khotan plug vars myPlug set --json '{"apiKey":"..."}'`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
let payload;
|
|
1989
|
+
try {
|
|
1990
|
+
payload = JSON.parse(opts.json);
|
|
1991
|
+
} catch {
|
|
1992
|
+
fail("invalid_json", "Could not parse --json as JSON.");
|
|
1993
|
+
}
|
|
1994
|
+
const res = await fetch(`${baseUrl}/variables/${plugName}`, {
|
|
1995
|
+
method: "POST",
|
|
1996
|
+
headers: { "Content-Type": "application/json" },
|
|
1997
|
+
body: JSON.stringify(payload)
|
|
1998
|
+
});
|
|
1999
|
+
const data = await res.json().catch(() => ({}));
|
|
2000
|
+
if (!res.ok) {
|
|
2001
|
+
fail(
|
|
2002
|
+
"set_failed",
|
|
2003
|
+
data.error ?? `Failed to set variables for "${plugName}"`
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
output({ ok: true, action: "set", plugName });
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (resolvedAction === "clear") {
|
|
2010
|
+
const res = await fetch(`${baseUrl}/variables/${plugName}`, {
|
|
2011
|
+
method: "DELETE"
|
|
2012
|
+
});
|
|
2013
|
+
if (!res.ok && res.status !== 204) {
|
|
2014
|
+
const data = await res.json().catch(() => ({}));
|
|
2015
|
+
fail(
|
|
2016
|
+
"clear_failed",
|
|
2017
|
+
data.error ?? `Failed to clear variables for "${plugName}"`
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
output({ ok: true, action: "clear", plugName });
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
fail(
|
|
2024
|
+
"invalid_action",
|
|
2025
|
+
`Unknown action "${resolvedAction}". Use show, set, clear, or --list.`
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
);
|
|
2029
|
+
|
|
2030
|
+
// src/cli/commands/probe.ts
|
|
2031
|
+
function output2(obj) {
|
|
2032
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
2033
|
+
}
|
|
2034
|
+
function fail2(error, hint) {
|
|
2035
|
+
output2({ ok: false, error, hint });
|
|
2036
|
+
process.exit(1);
|
|
2037
|
+
}
|
|
2038
|
+
function formatSize(bytes) {
|
|
2039
|
+
if (bytes < 1024) return `${bytes}b`;
|
|
2040
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}kb`;
|
|
2041
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}mb`;
|
|
2042
|
+
}
|
|
2043
|
+
function parsePortFromEnvFile2(filePath) {
|
|
2044
|
+
try {
|
|
2045
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2046
|
+
for (const line of content.split("\n")) {
|
|
2047
|
+
const trimmed = line.trim();
|
|
2048
|
+
if (trimmed.startsWith("#") || !trimmed) continue;
|
|
2049
|
+
const match = /^PORT\s*=\s*["']?(\d+)["']?/.exec(trimmed);
|
|
2050
|
+
if (match) return parseInt(match[1], 10);
|
|
2051
|
+
}
|
|
2052
|
+
} catch {
|
|
2053
|
+
}
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
function resolvePort2(portFlag) {
|
|
2057
|
+
if (portFlag) return parseInt(portFlag, 10);
|
|
2058
|
+
const cwd = process.cwd();
|
|
2059
|
+
const fromLocal = parsePortFromEnvFile2(path2.join(cwd, ".env.local"));
|
|
2060
|
+
if (fromLocal) return fromLocal;
|
|
2061
|
+
const fromEnv = parsePortFromEnvFile2(path2.join(cwd, ".env"));
|
|
2062
|
+
if (fromEnv) return fromEnv;
|
|
2063
|
+
return 3e3;
|
|
2064
|
+
}
|
|
2065
|
+
function tryParseJson(value) {
|
|
2066
|
+
if (typeof value === "string") {
|
|
2067
|
+
try {
|
|
2068
|
+
return JSON.parse(value);
|
|
2069
|
+
} catch {
|
|
2070
|
+
return value;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return value;
|
|
2074
|
+
}
|
|
2075
|
+
async function checkConnectivity2(baseUrl) {
|
|
2076
|
+
let res;
|
|
2077
|
+
try {
|
|
2078
|
+
res = await fetch(`${baseUrl}/debug`);
|
|
2079
|
+
} catch {
|
|
2080
|
+
fail2(
|
|
2081
|
+
"connect_failed",
|
|
2082
|
+
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
if (res.status === 404) {
|
|
2086
|
+
fail2(
|
|
2087
|
+
"debug_disabled",
|
|
2088
|
+
"Debug mode is not enabled. Set KHOTAN_DEBUG=1 in your environment and restart."
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
var plugCommand = new Command("plug").alias("probe").description(
|
|
2093
|
+
"Inspect and test plugs via the running dev server's debug route"
|
|
2094
|
+
).argument("[plugName]", "Name of the plug to probe").argument("[method]", "HTTP method (GET, POST, PUT, DELETE, PATCH)").argument("[path]", "Request path (e.g. /products)").option("--port <port>", "Dev server port").option("--base-path <path>", "API base path", "/api/khotan").option("--list", "List all registered plugs").option("--info", "Show plug metadata and endpoints").option("--endpoint <name>", "Fire request using a named endpoint").option("--compare", "Compare response against declared schema").option("--body <json>", "Request body (JSON string)").option("--params <json>", "Query params (JSON string)").option("--headers <json>", "Extra headers (JSON string)").action(
|
|
2095
|
+
async (plugName, method, reqPath, opts) => {
|
|
2096
|
+
const port = resolvePort2(opts.port);
|
|
2097
|
+
const baseUrl = `http://localhost:${port}${opts.basePath}`;
|
|
2098
|
+
if (opts.list) {
|
|
2099
|
+
await checkConnectivity2(baseUrl);
|
|
2100
|
+
try {
|
|
2101
|
+
const res = await fetch(`${baseUrl}/plugs`);
|
|
2102
|
+
const data = await res.json();
|
|
2103
|
+
const raw = Array.isArray(data) ? data : data.plugs ?? [];
|
|
2104
|
+
const plugs = raw.map((p) => ({
|
|
2105
|
+
name: p.name,
|
|
2106
|
+
baseUrl: p.baseUrl,
|
|
2107
|
+
authType: p.authType,
|
|
2108
|
+
varsConfigured: p.varsConfigured ?? false
|
|
2109
|
+
}));
|
|
2110
|
+
output2({ ok: true, plugs });
|
|
2111
|
+
} catch (e) {
|
|
2112
|
+
fail2("fetch_failed", `Failed to fetch plug list: ${String(e)}`);
|
|
2113
|
+
}
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
if (!plugName) {
|
|
2117
|
+
fail2(
|
|
2118
|
+
"missing_plug",
|
|
2119
|
+
"Usage: khotan plug <plugName> [METHOD] [path] [flags]. Use --list to see available plugs."
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
2122
|
+
await checkConnectivity2(baseUrl);
|
|
2123
|
+
if (opts.info) {
|
|
2124
|
+
try {
|
|
2125
|
+
const res = await fetch(`${baseUrl}/debug/${plugName}`);
|
|
2126
|
+
if (res.status === 404) {
|
|
2127
|
+
fail2(
|
|
2128
|
+
"plug_not_found",
|
|
2129
|
+
`Plug "${plugName}" not found. Use --list to see available plugs.`
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
const data = await res.json();
|
|
2133
|
+
output2({ ok: true, plug: data });
|
|
2134
|
+
} catch (e) {
|
|
2135
|
+
fail2("fetch_failed", `Failed to fetch plug info: ${String(e)}`);
|
|
2136
|
+
}
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
let resolvedMethod = method?.toUpperCase();
|
|
2140
|
+
let resolvedPath = reqPath;
|
|
2141
|
+
let allEndpoints = null;
|
|
2142
|
+
if (opts.endpoint) {
|
|
2143
|
+
try {
|
|
2144
|
+
const res = await fetch(`${baseUrl}/debug/${plugName}`);
|
|
2145
|
+
if (res.status === 404) {
|
|
2146
|
+
fail2(
|
|
2147
|
+
"plug_not_found",
|
|
2148
|
+
`Plug "${plugName}" not found. Use --list to see available plugs.`
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
const data = await res.json();
|
|
2152
|
+
allEndpoints = data.endpoints ?? null;
|
|
2153
|
+
if (!allEndpoints?.[opts.endpoint]) {
|
|
2154
|
+
fail2(
|
|
2155
|
+
"endpoint_not_found",
|
|
2156
|
+
`Endpoint "${opts.endpoint}" not found on plug "${plugName}". Use --info to see available endpoints.`
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
const endpointMeta = allEndpoints[opts.endpoint];
|
|
2160
|
+
resolvedMethod = endpointMeta["method"].toUpperCase();
|
|
2161
|
+
resolvedPath = endpointMeta["path"];
|
|
2162
|
+
} catch (e) {
|
|
2163
|
+
if (e.code === "ECONNREFUSED") {
|
|
2164
|
+
fail2("connect_failed", `Could not connect to dev server.`);
|
|
2165
|
+
}
|
|
2166
|
+
throw e;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (!resolvedMethod || !resolvedPath) {
|
|
2170
|
+
fail2(
|
|
2171
|
+
"missing_args",
|
|
2172
|
+
"Provide METHOD and path, or use --endpoint <name>. Example: khotan plug myPlug GET /items"
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
let body = void 0;
|
|
2176
|
+
if (opts.body) {
|
|
2177
|
+
try {
|
|
2178
|
+
body = JSON.parse(opts.body);
|
|
2179
|
+
} catch {
|
|
2180
|
+
fail2("invalid_json", "Could not parse --body as JSON.");
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
let params;
|
|
2184
|
+
if (opts.params) {
|
|
2185
|
+
try {
|
|
2186
|
+
params = JSON.parse(opts.params);
|
|
2187
|
+
} catch {
|
|
2188
|
+
fail2("invalid_json", "Could not parse --params as JSON.");
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
let extraHeaders;
|
|
2192
|
+
if (opts.headers) {
|
|
2193
|
+
try {
|
|
2194
|
+
extraHeaders = JSON.parse(opts.headers);
|
|
2195
|
+
} catch {
|
|
2196
|
+
fail2("invalid_json", "Could not parse --headers as JSON.");
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
const debugPayload = {
|
|
2200
|
+
method: resolvedMethod,
|
|
2201
|
+
path: resolvedPath
|
|
2202
|
+
};
|
|
2203
|
+
if (body !== void 0) debugPayload["body"] = body;
|
|
2204
|
+
if (params) debugPayload["params"] = params;
|
|
2205
|
+
if (extraHeaders) debugPayload["headers"] = extraHeaders;
|
|
2206
|
+
let debugRes;
|
|
2207
|
+
try {
|
|
2208
|
+
debugRes = await fetch(`${baseUrl}/debug/${plugName}`, {
|
|
2209
|
+
method: "POST",
|
|
2210
|
+
headers: { "Content-Type": "application/json" },
|
|
2211
|
+
body: JSON.stringify(debugPayload)
|
|
2212
|
+
});
|
|
2213
|
+
} catch (e) {
|
|
2214
|
+
fail2("request_failed", `Failed to fire request: ${String(e)}`);
|
|
2215
|
+
}
|
|
2216
|
+
const debugData = await debugRes.json();
|
|
2217
|
+
const rawBody = tryParseJson(debugData["body"]);
|
|
2218
|
+
const responseBodyStr = JSON.stringify(rawBody);
|
|
2219
|
+
const size = formatSize(new TextEncoder().encode(responseBodyStr).length);
|
|
2220
|
+
const debugEndpoint = debugData["endpoint"];
|
|
2221
|
+
const matchedEndpointName = debugEndpoint?.name ?? opts.endpoint ?? null;
|
|
2222
|
+
const responseStatus = debugData["status"] ?? debugRes.status;
|
|
2223
|
+
const isError = responseStatus >= 400;
|
|
2224
|
+
const result = {
|
|
2225
|
+
ok: true,
|
|
2226
|
+
request: {
|
|
2227
|
+
method: resolvedMethod,
|
|
2228
|
+
path: resolvedPath,
|
|
2229
|
+
params: params ?? null,
|
|
2230
|
+
body: body ?? null
|
|
2231
|
+
},
|
|
2232
|
+
response: {
|
|
2233
|
+
status: responseStatus,
|
|
2234
|
+
statusText: debugData["statusText"] ?? debugRes.statusText,
|
|
2235
|
+
timing: debugData["timing"] ?? null,
|
|
2236
|
+
size,
|
|
2237
|
+
headers: debugData["headers"] ?? null,
|
|
2238
|
+
body: rawBody
|
|
2239
|
+
},
|
|
2240
|
+
matchedEndpoint: matchedEndpointName
|
|
2241
|
+
};
|
|
2242
|
+
if (isError && debugData["error"]) {
|
|
2243
|
+
result["response"]["error"] = debugData["error"];
|
|
2244
|
+
}
|
|
2245
|
+
if (opts.compare) {
|
|
2246
|
+
if (!matchedEndpointName) {
|
|
2247
|
+
result["comparison"] = null;
|
|
2248
|
+
result["comparisonNote"] = "No typed endpoint matched this request. Define endpoints on your plug to enable comparison.";
|
|
2249
|
+
} else if (isError) {
|
|
2250
|
+
result["comparison"] = null;
|
|
2251
|
+
result["comparisonNote"] = `Response status ${responseStatus} \u2014 comparison skipped (schemas describe success responses).`;
|
|
2252
|
+
} else {
|
|
2253
|
+
if (!allEndpoints) {
|
|
2254
|
+
try {
|
|
2255
|
+
const metaRes = await fetch(`${baseUrl}/debug/${plugName}`);
|
|
2256
|
+
const metaData = await metaRes.json();
|
|
2257
|
+
allEndpoints = metaData.endpoints ?? null;
|
|
2258
|
+
} catch {
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
const ep = allEndpoints?.[matchedEndpointName];
|
|
2262
|
+
const responses = ep?.["responses"];
|
|
2263
|
+
const responseSchema = responses?.["200"] ?? null;
|
|
2264
|
+
if (!responseSchema) {
|
|
2265
|
+
result["comparison"] = null;
|
|
2266
|
+
result["comparisonNote"] = "No response schema defined for this endpoint";
|
|
2267
|
+
} else {
|
|
2268
|
+
const actualShape = inferShape(rawBody);
|
|
2269
|
+
const mismatches = diffSchemas(responseSchema, actualShape);
|
|
2270
|
+
result["comparison"] = {
|
|
2271
|
+
match: mismatches.length === 0,
|
|
2272
|
+
expected: responseSchema,
|
|
2273
|
+
actual: actualShape,
|
|
2274
|
+
mismatches
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
output2(result);
|
|
2280
|
+
}
|
|
2281
|
+
);
|
|
2282
|
+
plugCommand.addCommand(varsCommand);
|
|
2283
|
+
function output3(obj) {
|
|
2284
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
2285
|
+
}
|
|
2286
|
+
function fail3(error, hint) {
|
|
2287
|
+
output3({ ok: false, error, hint });
|
|
2288
|
+
process.exit(1);
|
|
2289
|
+
}
|
|
2290
|
+
function parseEnvFile2(filePath) {
|
|
2291
|
+
try {
|
|
2292
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2293
|
+
const values = {};
|
|
2294
|
+
for (const line of content.split("\n")) {
|
|
2295
|
+
const trimmed = line.trim();
|
|
2296
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2297
|
+
const match = /^([A-Z0-9_]+)\s*=\s*["']?(.*?)["']?$/.exec(trimmed);
|
|
2298
|
+
if (match) values[match[1]] = match[2];
|
|
2299
|
+
}
|
|
2300
|
+
return values;
|
|
2301
|
+
} catch {
|
|
2302
|
+
return {};
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
function parsePortFromEnvFile3(filePath) {
|
|
2306
|
+
const env = parseEnvFile2(filePath);
|
|
2307
|
+
const raw = env["PORT"];
|
|
2308
|
+
if (!raw) return null;
|
|
2309
|
+
const parsed = parseInt(raw, 10);
|
|
2310
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2311
|
+
}
|
|
2312
|
+
function resolvePort3(portFlag) {
|
|
2313
|
+
if (portFlag) return parseInt(portFlag, 10);
|
|
2314
|
+
const cwd = process.cwd();
|
|
2315
|
+
return parsePortFromEnvFile3(path2.join(cwd, ".env.local")) ?? parsePortFromEnvFile3(path2.join(cwd, ".env")) ?? 3e3;
|
|
2316
|
+
}
|
|
2317
|
+
function resolveWebhookOrigin(originFlag) {
|
|
2318
|
+
if (originFlag) return originFlag.replace(/\/$/, "");
|
|
2319
|
+
const cwd = process.cwd();
|
|
2320
|
+
const env = {
|
|
2321
|
+
...parseEnvFile2(path2.join(cwd, ".env")),
|
|
2322
|
+
...parseEnvFile2(path2.join(cwd, ".env.local"))
|
|
2323
|
+
};
|
|
2324
|
+
const origin = env["KHOTAN_WEBHOOK_URL"] ?? env["NGROK_URL"] ?? env["NEXT_PUBLIC_APP_URL"] ?? `http://localhost:${resolvePort3(void 0)}`;
|
|
2325
|
+
return origin.replace(/\/$/, "");
|
|
2326
|
+
}
|
|
2327
|
+
async function checkConnectivity3(baseUrl) {
|
|
2328
|
+
let res;
|
|
2329
|
+
try {
|
|
2330
|
+
res = await fetch(`${baseUrl}/plugs`);
|
|
2331
|
+
} catch {
|
|
2332
|
+
fail3(
|
|
2333
|
+
"connect_failed",
|
|
2334
|
+
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
if (!res.ok) {
|
|
2338
|
+
fail3(
|
|
2339
|
+
"api_unavailable",
|
|
2340
|
+
`Could not reach Khotan API at ${baseUrl}. Check your base path and dev server.`
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
var wireCommand = new Command("wire").description(
|
|
2345
|
+
"Inspect, connect, and disconnect wires via the running Khotan API"
|
|
2346
|
+
).argument("[plugName]", "Name of the plug whose wire you want to manage").argument("[action]", "Action: info | connect | disconnect").option("--port <port>", "Dev server port").option("--base-path <path>", "API base path", "/api/khotan").option(
|
|
2347
|
+
"--list",
|
|
2348
|
+
"List configured plugs and whether they currently have a wire"
|
|
2349
|
+
).option("--info", "Show current wire state for the plug").option("--callback-url <url>", "Explicit callback URL to register").option("--webhook-origin <url>", "Origin used to build default callback URL").option(
|
|
2350
|
+
"--wire-id <id>",
|
|
2351
|
+
"Explicit wire ID for disconnect (otherwise current wire is used)"
|
|
2352
|
+
).action(
|
|
2353
|
+
async (plugName, action, opts) => {
|
|
2354
|
+
const port = resolvePort3(opts.port);
|
|
2355
|
+
const baseUrl = `http://localhost:${port}${opts.basePath}`;
|
|
2356
|
+
await checkConnectivity3(baseUrl);
|
|
2357
|
+
if (opts.list) {
|
|
2358
|
+
const plugsRes = await fetch(`${baseUrl}/plugs`);
|
|
2359
|
+
const plugs = await plugsRes.json();
|
|
2360
|
+
const wires = await Promise.all(
|
|
2361
|
+
plugs.map(async (plug) => {
|
|
2362
|
+
const res = await fetch(`${baseUrl}/wires/${plug.name}`);
|
|
2363
|
+
const data = await res.json();
|
|
2364
|
+
return {
|
|
2365
|
+
plugName: plug.name,
|
|
2366
|
+
configured: data.configured ?? false,
|
|
2367
|
+
connected: data.wire?.status === "active",
|
|
2368
|
+
wire: data.wire ?? null
|
|
2369
|
+
};
|
|
2370
|
+
})
|
|
2371
|
+
);
|
|
2372
|
+
output3({ ok: true, wires });
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
if (!plugName) {
|
|
2376
|
+
fail3(
|
|
2377
|
+
"missing_plug",
|
|
2378
|
+
"Usage: khotan wire <plugName> [info|connect|disconnect] or khotan wire --list"
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
const resolvedAction = opts.info ? "info" : action ?? "info";
|
|
2382
|
+
if (resolvedAction === "info") {
|
|
2383
|
+
const res = await fetch(`${baseUrl}/wires/${plugName}`);
|
|
2384
|
+
if (res.status === 404) {
|
|
2385
|
+
fail3("plug_not_found", `Plug "${plugName}" not found.`);
|
|
2386
|
+
}
|
|
2387
|
+
const data = await res.json();
|
|
2388
|
+
output3({
|
|
2389
|
+
ok: true,
|
|
2390
|
+
plugName,
|
|
2391
|
+
configured: data.configured ?? false,
|
|
2392
|
+
wire: data.wire ?? null
|
|
2393
|
+
});
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
if (resolvedAction === "connect") {
|
|
2397
|
+
const callbackUrl = opts.callbackUrl ?? `${resolveWebhookOrigin(opts.webhookOrigin)}/api/khotan/webhook/${plugName}`;
|
|
2398
|
+
const res = await fetch(`${baseUrl}/wires/${plugName}`, {
|
|
2399
|
+
method: "POST",
|
|
2400
|
+
headers: { "Content-Type": "application/json" },
|
|
2401
|
+
body: JSON.stringify({ callbackUrl })
|
|
2402
|
+
});
|
|
2403
|
+
const data = await res.json();
|
|
2404
|
+
if (!res.ok) {
|
|
2405
|
+
fail3(
|
|
2406
|
+
"connect_failed",
|
|
2407
|
+
data.error ?? `Failed to connect wire for "${plugName}"`
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
output3({
|
|
2411
|
+
ok: true,
|
|
2412
|
+
action: "connect",
|
|
2413
|
+
plugName,
|
|
2414
|
+
callbackUrl,
|
|
2415
|
+
wire: data.wire ?? null
|
|
2416
|
+
});
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
if (resolvedAction === "disconnect") {
|
|
2420
|
+
let wireId = opts.wireId;
|
|
2421
|
+
if (!wireId) {
|
|
2422
|
+
const currentRes = await fetch(`${baseUrl}/wires/${plugName}`);
|
|
2423
|
+
const current = await currentRes.json();
|
|
2424
|
+
wireId = current.wire?.id;
|
|
2425
|
+
}
|
|
2426
|
+
if (!wireId) {
|
|
2427
|
+
fail3(
|
|
2428
|
+
"missing_wire",
|
|
2429
|
+
`No active wire found for "${plugName}". Use --wire-id to override.`
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
2432
|
+
const res = await fetch(`${baseUrl}/wires/${plugName}`, {
|
|
2433
|
+
method: "DELETE",
|
|
2434
|
+
headers: { "Content-Type": "application/json" },
|
|
2435
|
+
body: JSON.stringify({ wireId })
|
|
2436
|
+
});
|
|
2437
|
+
if (!res.ok && res.status !== 204) {
|
|
2438
|
+
const data = await res.json();
|
|
2439
|
+
fail3(
|
|
2440
|
+
"disconnect_failed",
|
|
2441
|
+
data.error ?? `Failed to disconnect wire "${wireId}"`
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
output3({ ok: true, action: "disconnect", plugName, wireId });
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
fail3(
|
|
2448
|
+
"invalid_action",
|
|
2449
|
+
`Unknown action "${resolvedAction}". Use info, connect, disconnect, or --list.`
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
);
|
|
2453
|
+
function output4(obj) {
|
|
2454
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
2455
|
+
}
|
|
2456
|
+
function fail4(error, hint) {
|
|
2457
|
+
output4({ ok: false, error, hint });
|
|
2458
|
+
process.exit(1);
|
|
2459
|
+
}
|
|
2460
|
+
function parseEnvFile3(filePath) {
|
|
2461
|
+
try {
|
|
2462
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2463
|
+
const values = {};
|
|
2464
|
+
for (const line of content.split("\n")) {
|
|
2465
|
+
const trimmed = line.trim();
|
|
2466
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2467
|
+
const match = /^([A-Z0-9_]+)\s*=\s*["']?(.*?)["']?$/.exec(trimmed);
|
|
2468
|
+
if (match) values[match[1]] = match[2];
|
|
2469
|
+
}
|
|
2470
|
+
return values;
|
|
2471
|
+
} catch {
|
|
2472
|
+
return {};
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
function parsePortFromEnvFile4(filePath) {
|
|
2476
|
+
const raw = parseEnvFile3(filePath)["PORT"];
|
|
2477
|
+
if (!raw) return null;
|
|
2478
|
+
const parsed = parseInt(raw, 10);
|
|
2479
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2480
|
+
}
|
|
2481
|
+
function resolvePort4(portFlag) {
|
|
2482
|
+
if (portFlag) return parseInt(portFlag, 10);
|
|
2483
|
+
const cwd = process.cwd();
|
|
2484
|
+
return parsePortFromEnvFile4(path2.join(cwd, ".env.local")) ?? parsePortFromEnvFile4(path2.join(cwd, ".env")) ?? 3e3;
|
|
2485
|
+
}
|
|
2486
|
+
function resolveBaseUrl(opts) {
|
|
2487
|
+
return `http://localhost:${resolvePort4(opts.port)}${opts.basePath}`;
|
|
2488
|
+
}
|
|
2489
|
+
async function fetchJson(url, init) {
|
|
2490
|
+
const res = await fetch(url, init);
|
|
2491
|
+
const data = await res.json().catch(() => ({}));
|
|
2492
|
+
if (!res.ok) {
|
|
2493
|
+
fail4(
|
|
2494
|
+
"request_failed",
|
|
2495
|
+
data.error ?? `Request to ${url} failed with status ${res.status}`
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
return data;
|
|
2499
|
+
}
|
|
2500
|
+
async function checkConnectivity4(baseUrl) {
|
|
2501
|
+
try {
|
|
2502
|
+
const res = await fetch(`${baseUrl}/flows`);
|
|
2503
|
+
if (!res.ok) {
|
|
2504
|
+
fail4(
|
|
2505
|
+
"api_unavailable",
|
|
2506
|
+
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
} catch {
|
|
2510
|
+
fail4(
|
|
2511
|
+
"connect_failed",
|
|
2512
|
+
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
function parseJsonOption(value, label) {
|
|
2517
|
+
if (!value) return void 0;
|
|
2518
|
+
try {
|
|
2519
|
+
return JSON.parse(value);
|
|
2520
|
+
} catch {
|
|
2521
|
+
fail4("invalid_json", `${label} must be valid JSON.`);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
async function listFlows(baseUrl) {
|
|
2525
|
+
const data = await fetchJson(
|
|
2526
|
+
`${baseUrl}/flows`
|
|
2527
|
+
);
|
|
2528
|
+
return Array.isArray(data) ? data : data.flows ?? [];
|
|
2529
|
+
}
|
|
2530
|
+
function resolveFlow(flows, flowNameOrId, plugName) {
|
|
2531
|
+
const byId = flows.find((flow) => flow.id === flowNameOrId);
|
|
2532
|
+
if (byId) {
|
|
2533
|
+
if (plugName && byId.plugName !== plugName) {
|
|
2534
|
+
fail4(
|
|
2535
|
+
"flow_not_found",
|
|
2536
|
+
`Flow ID "${flowNameOrId}" is not registered on plug "${plugName}".`
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
return byId;
|
|
2540
|
+
}
|
|
2541
|
+
const matches = flows.filter(
|
|
2542
|
+
(flow) => flow.name === flowNameOrId && (!plugName || flow.plugName === plugName)
|
|
2543
|
+
);
|
|
2544
|
+
if (matches.length === 0) {
|
|
2545
|
+
const suffix = plugName ? ` on plug "${plugName}"` : "";
|
|
2546
|
+
fail4("flow_not_found", `Flow "${flowNameOrId}"${suffix} not found.`);
|
|
2547
|
+
}
|
|
2548
|
+
if (matches.length > 1) {
|
|
2549
|
+
const plugs = matches.map((flow) => flow.plugName).filter(Boolean).join(", ");
|
|
2550
|
+
fail4(
|
|
2551
|
+
"ambiguous_flow",
|
|
2552
|
+
`Flow "${flowNameOrId}" is registered on multiple plugs (${plugs}). Pass --plug <plugName>.`
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
return matches[0];
|
|
2556
|
+
}
|
|
2557
|
+
function withApiOptions(command) {
|
|
2558
|
+
return command.option("--port <port>", "Dev server port").option("--base-path <path>", "API base path", "/api/khotan");
|
|
2559
|
+
}
|
|
2560
|
+
var flowsCommand = new Command("flows").alias("flow").description("List, trigger, and inspect flows via the running Khotan API");
|
|
2561
|
+
withApiOptions(
|
|
2562
|
+
flowsCommand.command("list").description("List registered flows").option("--plug <plugName>", "Only show flows for one plug")
|
|
2563
|
+
).action(async (opts) => {
|
|
2564
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
2565
|
+
await checkConnectivity4(baseUrl);
|
|
2566
|
+
const flows = (await listFlows(baseUrl)).filter(
|
|
2567
|
+
(flow) => !opts.plug || flow.plugName === opts.plug
|
|
2568
|
+
);
|
|
2569
|
+
output4({ ok: true, flows });
|
|
2570
|
+
});
|
|
2571
|
+
withApiOptions(
|
|
2572
|
+
flowsCommand.command("info").description("Show one flow by name or ID").argument("<flowNameOrId>", "Flow name or ID").option("--plug <plugName>", "Disambiguate by plug name")
|
|
2573
|
+
).action(async (flowNameOrId, opts) => {
|
|
2574
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
2575
|
+
await checkConnectivity4(baseUrl);
|
|
2576
|
+
const flow = resolveFlow(await listFlows(baseUrl), flowNameOrId, opts.plug);
|
|
2577
|
+
output4({ ok: true, flow });
|
|
2578
|
+
});
|
|
2579
|
+
withApiOptions(
|
|
2580
|
+
flowsCommand.command("trigger").description("Start a tracked flow run").argument("<flowNameOrId>", "Flow name or ID").option("--plug <plugName>", "Disambiguate by plug name").option(
|
|
2581
|
+
"--run-type <type>",
|
|
2582
|
+
"Run type: full, delta, backfill, reconcile, dry-run",
|
|
2583
|
+
"full"
|
|
2584
|
+
).option("--body <json>", "JSON body passed to the flow context")
|
|
2585
|
+
).action(
|
|
2586
|
+
async (flowNameOrId, opts) => {
|
|
2587
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
2588
|
+
await checkConnectivity4(baseUrl);
|
|
2589
|
+
const flow = resolveFlow(await listFlows(baseUrl), flowNameOrId, opts.plug);
|
|
2590
|
+
const requestBody = { runType: opts.runType };
|
|
2591
|
+
const body = parseJsonOption(opts.body, "--body");
|
|
2592
|
+
if (body !== void 0) requestBody["body"] = body;
|
|
2593
|
+
const run = await fetchJson(
|
|
2594
|
+
`${baseUrl}/flows/${encodeURIComponent(flow.id)}/runs`,
|
|
2595
|
+
{
|
|
2596
|
+
method: "POST",
|
|
2597
|
+
headers: { "Content-Type": "application/json" },
|
|
2598
|
+
body: JSON.stringify(requestBody)
|
|
2599
|
+
}
|
|
2600
|
+
);
|
|
2601
|
+
output4({ ok: true, action: "trigger", flow, run });
|
|
2602
|
+
}
|
|
2603
|
+
);
|
|
2604
|
+
withApiOptions(
|
|
2605
|
+
flowsCommand.command("runs").description("List runs for one flow").argument("<flowNameOrId>", "Flow name or ID").option("--plug <plugName>", "Disambiguate by plug name")
|
|
2606
|
+
).action(async (flowNameOrId, opts) => {
|
|
2607
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
2608
|
+
await checkConnectivity4(baseUrl);
|
|
2609
|
+
const flow = resolveFlow(await listFlows(baseUrl), flowNameOrId, opts.plug);
|
|
2610
|
+
const runs = await fetchJson(
|
|
2611
|
+
`${baseUrl}/flows/${encodeURIComponent(flow.id)}/runs`
|
|
2612
|
+
);
|
|
2613
|
+
output4({ ok: true, flow, runs });
|
|
2614
|
+
});
|
|
2615
|
+
withApiOptions(
|
|
2616
|
+
flowsCommand.command("cancel").description("Cancel a running Workflow-backed Khotan run").argument("<runId>", "Khotan run ID")
|
|
2617
|
+
).action(async (runId, opts) => {
|
|
2618
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
2619
|
+
await checkConnectivity4(baseUrl);
|
|
2620
|
+
const run = await fetchJson(
|
|
2621
|
+
`${baseUrl}/runs/${encodeURIComponent(runId)}/cancel`,
|
|
2622
|
+
{ method: "POST" }
|
|
2623
|
+
);
|
|
2624
|
+
output4({ ok: true, action: "cancel", run });
|
|
2625
|
+
});
|
|
2626
|
+
function output5(obj) {
|
|
2627
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
2628
|
+
}
|
|
2629
|
+
function fail5(error, hint) {
|
|
2630
|
+
output5({ ok: false, error, hint });
|
|
2631
|
+
process.exit(1);
|
|
2632
|
+
}
|
|
2633
|
+
function parseEnvFile4(filePath) {
|
|
2634
|
+
try {
|
|
2635
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2636
|
+
const values = {};
|
|
2637
|
+
for (const line of content.split("\n")) {
|
|
2638
|
+
const trimmed = line.trim();
|
|
2639
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2640
|
+
const match = /^([A-Z0-9_]+)\s*=\s*["']?(.*?)["']?$/.exec(trimmed);
|
|
2641
|
+
if (match) values[match[1]] = match[2];
|
|
2642
|
+
}
|
|
2643
|
+
return values;
|
|
2644
|
+
} catch {
|
|
2645
|
+
return {};
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
function parsePortFromEnvFile5(filePath) {
|
|
2649
|
+
const raw = parseEnvFile4(filePath)["PORT"];
|
|
2650
|
+
if (!raw) return null;
|
|
2651
|
+
const parsed = parseInt(raw, 10);
|
|
2652
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2653
|
+
}
|
|
2654
|
+
function resolvePort5(portFlag) {
|
|
2655
|
+
if (portFlag) return parseInt(portFlag, 10);
|
|
2656
|
+
const cwd = process.cwd();
|
|
2657
|
+
return parsePortFromEnvFile5(path2.join(cwd, ".env.local")) ?? parsePortFromEnvFile5(path2.join(cwd, ".env")) ?? 3e3;
|
|
2658
|
+
}
|
|
2659
|
+
function resolveBaseUrl2(opts) {
|
|
2660
|
+
return `http://localhost:${resolvePort5(opts.port)}${opts.basePath}`;
|
|
2661
|
+
}
|
|
2662
|
+
async function fetchJson2(url, init) {
|
|
2663
|
+
const res = await fetch(url, init);
|
|
2664
|
+
const data = await res.json().catch(() => ({}));
|
|
2665
|
+
if (!res.ok) {
|
|
2666
|
+
fail5(
|
|
2667
|
+
"request_failed",
|
|
2668
|
+
data.error ?? `Request to ${url} failed with status ${res.status}`
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
return data;
|
|
2672
|
+
}
|
|
2673
|
+
async function checkConnectivity5(baseUrl) {
|
|
2674
|
+
try {
|
|
2675
|
+
const res = await fetch(`${baseUrl}/flows`);
|
|
2676
|
+
if (!res.ok) {
|
|
2677
|
+
fail5(
|
|
2678
|
+
"api_unavailable",
|
|
2679
|
+
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
} catch {
|
|
2683
|
+
fail5(
|
|
2684
|
+
"connect_failed",
|
|
2685
|
+
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function parseJsonObjectOption(value, label) {
|
|
2690
|
+
if (!value) return void 0;
|
|
2691
|
+
try {
|
|
2692
|
+
const parsed = JSON.parse(value);
|
|
2693
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2694
|
+
fail5("invalid_json", `${label} must be a JSON object.`);
|
|
2695
|
+
}
|
|
2696
|
+
return parsed;
|
|
2697
|
+
} catch {
|
|
2698
|
+
fail5("invalid_json", `${label} must be valid JSON.`);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
function withApiOptions2(command) {
|
|
2702
|
+
return command.option("--port <port>", "Dev server port").option("--base-path <path>", "API base path", "/api/khotan");
|
|
2703
|
+
}
|
|
2704
|
+
async function listResources(baseUrl) {
|
|
2705
|
+
return fetchJson2(`${baseUrl}/resources`);
|
|
2706
|
+
}
|
|
2707
|
+
async function resolveResource(baseUrl, resourceNameOrId) {
|
|
2708
|
+
const resources = await listResources(baseUrl);
|
|
2709
|
+
const match = resources.find(
|
|
2710
|
+
(resource) => resource.id === resourceNameOrId || resource.name === resourceNameOrId
|
|
2711
|
+
);
|
|
2712
|
+
if (!match) {
|
|
2713
|
+
fail5(
|
|
2714
|
+
"resource_not_found",
|
|
2715
|
+
`Resource "${resourceNameOrId}" is not registered in the running Khotan config.`
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
return match;
|
|
2719
|
+
}
|
|
2720
|
+
var mappingsCommand = new Command("mappings").description("List, lookup, and mutate mappings via the running Khotan API");
|
|
2721
|
+
withApiOptions2(
|
|
2722
|
+
mappingsCommand.command("list").description("List mappings for one resource").argument("<resource>", "Resource name or ID").option("--limit <limit>", "Page size", "20").option("--offset <offset>", "Page offset", "0").option("--search <term>", "Search connectValue, refs, and metadata")
|
|
2723
|
+
).action(
|
|
2724
|
+
async (resourceNameOrId, opts) => {
|
|
2725
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2726
|
+
await checkConnectivity5(baseUrl);
|
|
2727
|
+
const resource = await resolveResource(baseUrl, resourceNameOrId);
|
|
2728
|
+
const limit = Math.max(parseInt(opts.limit, 10) || 20, 1);
|
|
2729
|
+
const offset = Math.max(parseInt(opts.offset, 10) || 0, 0);
|
|
2730
|
+
const url = new URL(
|
|
2731
|
+
`${baseUrl}/resources/${encodeURIComponent(resource.id)}/mappings`
|
|
2732
|
+
);
|
|
2733
|
+
url.searchParams.set("limit", String(limit));
|
|
2734
|
+
url.searchParams.set("offset", String(offset));
|
|
2735
|
+
if (opts.search) {
|
|
2736
|
+
url.searchParams.set("search", opts.search);
|
|
2737
|
+
}
|
|
2738
|
+
const data = await fetchJson2(url.toString());
|
|
2739
|
+
output5({
|
|
2740
|
+
ok: true,
|
|
2741
|
+
resource,
|
|
2742
|
+
items: data.items,
|
|
2743
|
+
page: data.page
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
);
|
|
2747
|
+
withApiOptions2(
|
|
2748
|
+
mappingsCommand.command("lookup").description("Lookup a mapping by connect value or plug ref").argument("<resource>", "Resource name or ID").option("--connect-value <value>", "Lookup by canonical connect value").option("--plug <plugName>", "Lookup by plug name").option("--ref <ref>", "Lookup by plug ref")
|
|
2749
|
+
).action(
|
|
2750
|
+
async (resourceNameOrId, opts) => {
|
|
2751
|
+
const usingConnectValue = typeof opts.connectValue === "string";
|
|
2752
|
+
const usingPlugRef = typeof opts.plug === "string" || typeof opts.ref === "string";
|
|
2753
|
+
if (!usingConnectValue && !usingPlugRef) {
|
|
2754
|
+
fail5(
|
|
2755
|
+
"validation_error",
|
|
2756
|
+
"Pass either --connect-value <value> or --plug <plugName> with --ref <ref>."
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
if (usingConnectValue && usingPlugRef) {
|
|
2760
|
+
fail5(
|
|
2761
|
+
"validation_error",
|
|
2762
|
+
"Choose one lookup mode: either --connect-value or --plug with --ref."
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
if (opts.plug && !opts.ref || !opts.plug && opts.ref) {
|
|
2766
|
+
fail5(
|
|
2767
|
+
"validation_error",
|
|
2768
|
+
"Plug-ref lookup requires both --plug <plugName> and --ref <ref>."
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2772
|
+
await checkConnectivity5(baseUrl);
|
|
2773
|
+
const resource = await resolveResource(baseUrl, resourceNameOrId);
|
|
2774
|
+
const payload = usingConnectValue ? {
|
|
2775
|
+
resourceId: resource.id,
|
|
2776
|
+
connectValue: opts.connectValue
|
|
2777
|
+
} : {
|
|
2778
|
+
resourceId: resource.id,
|
|
2779
|
+
plugName: opts.plug,
|
|
2780
|
+
ref: opts.ref
|
|
2781
|
+
};
|
|
2782
|
+
const mapping = await fetchJson2(
|
|
2783
|
+
`${baseUrl}/mappings/lookup`,
|
|
2784
|
+
{
|
|
2785
|
+
method: "POST",
|
|
2786
|
+
headers: { "Content-Type": "application/json" },
|
|
2787
|
+
body: JSON.stringify(payload)
|
|
2788
|
+
}
|
|
2789
|
+
);
|
|
2790
|
+
output5({ ok: true, resource, mapping });
|
|
2791
|
+
}
|
|
2792
|
+
);
|
|
2793
|
+
withApiOptions2(
|
|
2794
|
+
mappingsCommand.command("upsert").description("Create or update one mapping by canonical connect value").argument("<resource>", "Resource name or ID").requiredOption("--connect-value <value>", "Canonical connect value").requiredOption("--refs <json>", "JSON object of refs keyed by plug name").option("--metadata <json>", "Optional JSON object of contextual metadata")
|
|
2795
|
+
).action(
|
|
2796
|
+
async (resourceNameOrId, opts) => {
|
|
2797
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2798
|
+
await checkConnectivity5(baseUrl);
|
|
2799
|
+
const resource = await resolveResource(baseUrl, resourceNameOrId);
|
|
2800
|
+
const refs = parseJsonObjectOption(opts.refs, "--refs");
|
|
2801
|
+
const metadata = parseJsonObjectOption(opts.metadata, "--metadata");
|
|
2802
|
+
const mapping = await fetchJson2(`${baseUrl}/mappings`, {
|
|
2803
|
+
method: "POST",
|
|
2804
|
+
headers: { "Content-Type": "application/json" },
|
|
2805
|
+
body: JSON.stringify({
|
|
2806
|
+
resourceId: resource.id,
|
|
2807
|
+
connectValue: opts.connectValue,
|
|
2808
|
+
refs,
|
|
2809
|
+
metadata
|
|
2810
|
+
})
|
|
2811
|
+
});
|
|
2812
|
+
output5({ ok: true, action: "upsert", resource, mapping });
|
|
2813
|
+
}
|
|
2814
|
+
);
|
|
2815
|
+
withApiOptions2(
|
|
2816
|
+
mappingsCommand.command("update").description("Update one mapping by row ID").argument("<mappingId>", "Mapping row ID").requiredOption("--resource <resource>", "Resource name or ID").requiredOption("--connect-value <value>", "Canonical connect value").requiredOption("--refs <json>", "JSON object of refs keyed by plug name").option("--metadata <json>", "Optional JSON object of contextual metadata")
|
|
2817
|
+
).action(
|
|
2818
|
+
async (mappingId, opts) => {
|
|
2819
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2820
|
+
await checkConnectivity5(baseUrl);
|
|
2821
|
+
const resource = await resolveResource(baseUrl, opts.resource);
|
|
2822
|
+
const refs = parseJsonObjectOption(opts.refs, "--refs");
|
|
2823
|
+
const metadata = parseJsonObjectOption(opts.metadata, "--metadata");
|
|
2824
|
+
const mapping = await fetchJson2(
|
|
2825
|
+
`${baseUrl}/mappings/${encodeURIComponent(mappingId)}`,
|
|
2826
|
+
{
|
|
2827
|
+
method: "PUT",
|
|
2828
|
+
headers: { "Content-Type": "application/json" },
|
|
2829
|
+
body: JSON.stringify({
|
|
2830
|
+
resourceId: resource.id,
|
|
2831
|
+
connectValue: opts.connectValue,
|
|
2832
|
+
refs,
|
|
2833
|
+
metadata
|
|
2834
|
+
})
|
|
2835
|
+
}
|
|
2836
|
+
);
|
|
2837
|
+
output5({ ok: true, action: "update", resource, mapping });
|
|
2838
|
+
}
|
|
2839
|
+
);
|
|
2840
|
+
withApiOptions2(
|
|
2841
|
+
mappingsCommand.command("delete").description("Delete one mapping by row ID").argument("<mappingId>", "Mapping row ID")
|
|
2842
|
+
).action(async (mappingId, opts) => {
|
|
2843
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2844
|
+
await checkConnectivity5(baseUrl);
|
|
2845
|
+
const res = await fetch(`${baseUrl}/mappings/${encodeURIComponent(mappingId)}`, {
|
|
2846
|
+
method: "DELETE"
|
|
2847
|
+
});
|
|
2848
|
+
if (!res.ok) {
|
|
2849
|
+
const data = await res.json().catch(() => ({}));
|
|
2850
|
+
fail5(
|
|
2851
|
+
"request_failed",
|
|
2852
|
+
data.error ?? `Delete request failed for mapping "${mappingId}" with status ${res.status}.`
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
output5({ ok: true, action: "delete", id: mappingId });
|
|
2856
|
+
});
|
|
2857
|
+
|
|
2858
|
+
// src/cli/index.ts
|
|
2859
|
+
var program = new Command();
|
|
2860
|
+
program.name("khotan").description("Scaffold data components into your project").version("0.0.1");
|
|
2861
|
+
program.addCommand(initCommand);
|
|
2862
|
+
program.addCommand(addCommand);
|
|
2863
|
+
program.addCommand(generateCommand);
|
|
2864
|
+
program.addCommand(migrateCommand);
|
|
2865
|
+
program.addCommand(plugCommand);
|
|
2866
|
+
program.addCommand(wireCommand);
|
|
2867
|
+
program.addCommand(flowsCommand);
|
|
2868
|
+
program.addCommand(mappingsCommand);
|
|
2869
|
+
program.parse();
|