khotan-data 0.1.0 → 0.2.0
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/README.md +89 -6
- package/dist/cli.js +405 -35
- package/dist/factory.cjs +1160 -106
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +262 -38
- package/dist/factory.d.ts +262 -38
- package/dist/factory.js +1158 -108
- package/dist/factory.js.map +1 -1
- package/dist/templates/api-state.tsx +249 -0
- package/dist/templates/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.ts +13 -1
- package/dist/templates/debug-index-page.tsx +56 -36
- package/dist/templates/hub.tsx +9 -23
- package/dist/templates/inflow.ts +5 -6
- package/dist/templates/khotan-config.ts +30 -4
- package/dist/templates/mapping-browser.tsx +773 -0
- package/dist/templates/mappings-page.tsx +9 -0
- package/dist/templates/outflow.ts +5 -5
- package/dist/templates/pass.ts +10 -0
- package/dist/templates/plug-debugger.tsx +15 -7
- package/dist/templates/relay.example.ts +11 -1
- package/dist/templates/relay.ts +16 -7
- package/dist/templates/runs-table.tsx +133 -130
- package/dist/templates/schema.ts +81 -0
- package/dist/templates/skill-plug.md +38 -15
- package/dist/templates/skill-setup.md +80 -3
- package/dist/templates/topology-canvas.tsx +19 -30
- package/dist/templates/var-panel.tsx +33 -10
- package/dist/templates/webhook-events-table.tsx +105 -102
- package/dist/templates/wire-panel.tsx +30 -8
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import path2 from 'path';
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
7
|
import prompts2 from 'prompts';
|
|
8
|
+
import crypto from 'crypto';
|
|
8
9
|
|
|
9
10
|
// src/cli/config-template.ts
|
|
10
11
|
function configTemplate(outputDir) {
|
|
@@ -152,6 +153,11 @@ var COMPONENTS = {
|
|
|
152
153
|
outputFile: "wires/wire.ts",
|
|
153
154
|
outputBase: "outputDir"
|
|
154
155
|
},
|
|
156
|
+
{
|
|
157
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
158
|
+
outputFile: "api-state.tsx",
|
|
159
|
+
outputBase: "components"
|
|
160
|
+
},
|
|
155
161
|
{
|
|
156
162
|
templatePath: path2.resolve(__dirname$1, "templates", "wire-panel.tsx"),
|
|
157
163
|
outputFile: "wire.tsx",
|
|
@@ -168,6 +174,23 @@ var COMPONENTS = {
|
|
|
168
174
|
npmPackages: ["drizzle-orm"]
|
|
169
175
|
}
|
|
170
176
|
},
|
|
177
|
+
cache: {
|
|
178
|
+
name: "cache",
|
|
179
|
+
description: "First-class durable cache definitions for khotan sync workloads",
|
|
180
|
+
requires: ["schema"],
|
|
181
|
+
files: [
|
|
182
|
+
{
|
|
183
|
+
templatePath: path2.resolve(__dirname$1, "templates", "cache.ts"),
|
|
184
|
+
outputFile: "caches/cache.ts",
|
|
185
|
+
outputBase: "outputDir"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
templatePath: path2.resolve(__dirname$1, "templates", "cache.example.ts"),
|
|
189
|
+
outputFile: "caches/cache.example.ts",
|
|
190
|
+
outputBase: "outputDir"
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
},
|
|
171
194
|
hub: {
|
|
172
195
|
name: "hub",
|
|
173
196
|
description: "Dashboard UI for managing plugs and flows",
|
|
@@ -185,6 +208,11 @@ var COMPONENTS = {
|
|
|
185
208
|
]
|
|
186
209
|
},
|
|
187
210
|
files: [
|
|
211
|
+
{
|
|
212
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
213
|
+
outputFile: "api-state.tsx",
|
|
214
|
+
outputBase: "components"
|
|
215
|
+
},
|
|
188
216
|
{
|
|
189
217
|
templatePath: path2.resolve(__dirname$1, "templates", "hub.tsx"),
|
|
190
218
|
outputFile: "hub.tsx",
|
|
@@ -210,6 +238,11 @@ var COMPONENTS = {
|
|
|
210
238
|
shadcnComponents: ["card", "table", "badge", "button"]
|
|
211
239
|
},
|
|
212
240
|
files: [
|
|
241
|
+
{
|
|
242
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
243
|
+
outputFile: "api-state.tsx",
|
|
244
|
+
outputBase: "components"
|
|
245
|
+
},
|
|
213
246
|
{
|
|
214
247
|
templatePath: path2.resolve(__dirname$1, "templates", "logs.tsx"),
|
|
215
248
|
outputFile: "logs.tsx",
|
|
@@ -231,6 +264,30 @@ var COMPONENTS = {
|
|
|
231
264
|
}
|
|
232
265
|
]
|
|
233
266
|
},
|
|
267
|
+
"mapping-browser": {
|
|
268
|
+
name: "mapping-browser",
|
|
269
|
+
description: "Searchable mappings browser for listing, creating, editing, and deleting resource mappings",
|
|
270
|
+
requiresShadcn: true,
|
|
271
|
+
dependencies: {
|
|
272
|
+
shadcnComponents: ["card", "table", "button", "input", "label"]
|
|
273
|
+
},
|
|
274
|
+
files: [
|
|
275
|
+
{
|
|
276
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
277
|
+
outputFile: "api-state.tsx",
|
|
278
|
+
outputBase: "components"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
templatePath: path2.resolve(
|
|
282
|
+
__dirname$1,
|
|
283
|
+
"templates",
|
|
284
|
+
"mapping-browser.tsx"
|
|
285
|
+
),
|
|
286
|
+
outputFile: "mapping-browser.tsx",
|
|
287
|
+
outputBase: "components"
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
},
|
|
234
291
|
"plug-debugger": {
|
|
235
292
|
name: "plug-debugger",
|
|
236
293
|
description: "Dev-only debug panel for testing plug requests interactively",
|
|
@@ -240,6 +297,11 @@ var COMPONENTS = {
|
|
|
240
297
|
shadcnComponents: ["card", "badge", "button", "input", "label"]
|
|
241
298
|
},
|
|
242
299
|
files: [
|
|
300
|
+
{
|
|
301
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
302
|
+
outputFile: "api-state.tsx",
|
|
303
|
+
outputBase: "components"
|
|
304
|
+
},
|
|
243
305
|
{
|
|
244
306
|
templatePath: path2.resolve(__dirname$1, "templates", "plug-debugger.tsx"),
|
|
245
307
|
outputFile: "plug-debugger.tsx",
|
|
@@ -462,6 +524,22 @@ var BLOCKS = {
|
|
|
462
524
|
}
|
|
463
525
|
]
|
|
464
526
|
},
|
|
527
|
+
"mappings-page-1": {
|
|
528
|
+
name: "mappings-page-1",
|
|
529
|
+
description: "Page route at /mappings that renders the reusable Khotan mappings browser",
|
|
530
|
+
requires: ["mapping-browser"],
|
|
531
|
+
files: [
|
|
532
|
+
{
|
|
533
|
+
templatePath: path2.resolve(
|
|
534
|
+
__dirname$1,
|
|
535
|
+
"templates",
|
|
536
|
+
"mappings-page.tsx"
|
|
537
|
+
),
|
|
538
|
+
outputFile: "mappings/page.tsx",
|
|
539
|
+
outputBase: "appRoot"
|
|
540
|
+
}
|
|
541
|
+
]
|
|
542
|
+
},
|
|
465
543
|
graph: {
|
|
466
544
|
name: "graph",
|
|
467
545
|
description: "Standalone topology graph page at /graph with filtering and run-state overlays",
|
|
@@ -471,6 +549,11 @@ var BLOCKS = {
|
|
|
471
549
|
shadcnComponents: ["card", "badge"]
|
|
472
550
|
},
|
|
473
551
|
files: [
|
|
552
|
+
{
|
|
553
|
+
templatePath: path2.resolve(__dirname$1, "templates", "api-state.tsx"),
|
|
554
|
+
outputFile: "api-state.tsx",
|
|
555
|
+
outputBase: "components"
|
|
556
|
+
},
|
|
474
557
|
{
|
|
475
558
|
templatePath: path2.resolve(
|
|
476
559
|
__dirname$1,
|
|
@@ -1722,13 +1805,13 @@ function diffSchemas(expected, actual, basePath = "$") {
|
|
|
1722
1805
|
}
|
|
1723
1806
|
return diffObjectSchema(expected, actual, basePath);
|
|
1724
1807
|
}
|
|
1725
|
-
function diffTypedNode(expected, actual,
|
|
1808
|
+
function diffTypedNode(expected, actual, path16) {
|
|
1726
1809
|
const expectedType = expected["_type"];
|
|
1727
1810
|
if (expectedType === "array") {
|
|
1728
1811
|
if (actual.type !== "array") {
|
|
1729
1812
|
return [
|
|
1730
1813
|
{
|
|
1731
|
-
path:
|
|
1814
|
+
path: path16,
|
|
1732
1815
|
issue: "type_mismatch",
|
|
1733
1816
|
note: `expected array, got ${actual.type}`
|
|
1734
1817
|
}
|
|
@@ -1736,13 +1819,13 @@ function diffTypedNode(expected, actual, path14) {
|
|
|
1736
1819
|
}
|
|
1737
1820
|
const itemSchema = expected["items"];
|
|
1738
1821
|
if (!itemSchema || !actual.items) return [];
|
|
1739
|
-
return diffSchemas(itemSchema, actual.items, `${
|
|
1822
|
+
return diffSchemas(itemSchema, actual.items, `${path16}[]`);
|
|
1740
1823
|
}
|
|
1741
1824
|
const normalizedExpected = normalizeType(expectedType);
|
|
1742
1825
|
if (normalizedExpected !== actual.type && actual.type !== "null") {
|
|
1743
1826
|
return [
|
|
1744
1827
|
{
|
|
1745
|
-
path:
|
|
1828
|
+
path: path16,
|
|
1746
1829
|
issue: "type_mismatch",
|
|
1747
1830
|
note: `expected ${expectedType}, got ${actual.type}`
|
|
1748
1831
|
}
|
|
@@ -1750,11 +1833,11 @@ function diffTypedNode(expected, actual, path14) {
|
|
|
1750
1833
|
}
|
|
1751
1834
|
return [];
|
|
1752
1835
|
}
|
|
1753
|
-
function diffObjectSchema(expected, actual,
|
|
1836
|
+
function diffObjectSchema(expected, actual, path16) {
|
|
1754
1837
|
if (actual.type !== "object") {
|
|
1755
1838
|
return [
|
|
1756
1839
|
{
|
|
1757
|
-
path:
|
|
1840
|
+
path: path16,
|
|
1758
1841
|
issue: "type_mismatch",
|
|
1759
1842
|
note: `expected object, got ${actual.type}`
|
|
1760
1843
|
}
|
|
@@ -1763,7 +1846,7 @@ function diffObjectSchema(expected, actual, path14) {
|
|
|
1763
1846
|
const mismatches = [];
|
|
1764
1847
|
const actualProps = actual.properties;
|
|
1765
1848
|
for (const [key, typeDesc] of Object.entries(expected)) {
|
|
1766
|
-
const childPath =
|
|
1849
|
+
const childPath = path16 === "$" ? `$.${key}` : `${path16}.${key}`;
|
|
1767
1850
|
const typeStr = typeof typeDesc === "string" ? typeDesc : null;
|
|
1768
1851
|
const isOptional = typeStr?.endsWith("?") ?? false;
|
|
1769
1852
|
if (!(key in actualProps)) {
|
|
@@ -1801,7 +1884,7 @@ function diffObjectSchema(expected, actual, path14) {
|
|
|
1801
1884
|
}
|
|
1802
1885
|
for (const key of Object.keys(actualProps)) {
|
|
1803
1886
|
if (!(key in expected)) {
|
|
1804
|
-
const childPath =
|
|
1887
|
+
const childPath = path16 === "$" ? `$.${key}` : `${path16}.${key}`;
|
|
1805
1888
|
mismatches.push({ path: childPath, issue: "extra" });
|
|
1806
1889
|
}
|
|
1807
1890
|
}
|
|
@@ -1822,6 +1905,48 @@ function normalizeType(typeStr) {
|
|
|
1822
1905
|
return lower;
|
|
1823
1906
|
}
|
|
1824
1907
|
}
|
|
1908
|
+
var CLI_TOKEN_SCHEME = "KhotanCLI";
|
|
1909
|
+
function readSecretFromEnvFile(filePath) {
|
|
1910
|
+
try {
|
|
1911
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1912
|
+
for (const line of content.split("\n")) {
|
|
1913
|
+
const trimmed = line.trim();
|
|
1914
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1915
|
+
const match = /^KHOTAN_SECRET\s*=\s*["']?(.*?)["']?\s*$/.exec(trimmed);
|
|
1916
|
+
if (match) return match[1] ?? null;
|
|
1917
|
+
}
|
|
1918
|
+
} catch {
|
|
1919
|
+
}
|
|
1920
|
+
return null;
|
|
1921
|
+
}
|
|
1922
|
+
function resolveKhotanSecret(cwd = process.cwd()) {
|
|
1923
|
+
const fromEnv = process.env["KHOTAN_SECRET"]?.trim();
|
|
1924
|
+
if (fromEnv) return fromEnv;
|
|
1925
|
+
return readSecretFromEnvFile(path2.join(cwd, ".env.local")) ?? readSecretFromEnvFile(path2.join(cwd, ".env")) ?? null;
|
|
1926
|
+
}
|
|
1927
|
+
function cliAuthHeader() {
|
|
1928
|
+
const secret = resolveKhotanSecret();
|
|
1929
|
+
if (!secret) return null;
|
|
1930
|
+
const timestamp = String(Date.now());
|
|
1931
|
+
const sig = crypto.createHmac("sha256", secret).update(`khotan-cli:${timestamp}`).digest("hex");
|
|
1932
|
+
return `${CLI_TOKEN_SCHEME} ${timestamp}.${sig}`;
|
|
1933
|
+
}
|
|
1934
|
+
function cliFetch(url, init) {
|
|
1935
|
+
const auth = cliAuthHeader();
|
|
1936
|
+
if (!auth) {
|
|
1937
|
+
return init === void 0 ? fetch(url) : fetch(url, init);
|
|
1938
|
+
}
|
|
1939
|
+
const headers = {
|
|
1940
|
+
...init?.headers ?? {},
|
|
1941
|
+
Authorization: auth
|
|
1942
|
+
};
|
|
1943
|
+
return fetch(url, { ...init, headers });
|
|
1944
|
+
}
|
|
1945
|
+
function unauthorizedHint() {
|
|
1946
|
+
return resolveKhotanSecret() ? "Unauthorized. The dev server rejected the CLI token \u2014 confirm KHOTAN_SECRET matches the value the server is running with." : "Unauthorized. Set KHOTAN_SECRET in your environment (or .env.local) so the CLI can authenticate against your authorize() hook.";
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// src/cli/commands/plug-vars.ts
|
|
1825
1950
|
function output(obj) {
|
|
1826
1951
|
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
1827
1952
|
}
|
|
@@ -1859,13 +1984,14 @@ function resolvePort(portFlag) {
|
|
|
1859
1984
|
async function checkConnectivity(baseUrl) {
|
|
1860
1985
|
let res;
|
|
1861
1986
|
try {
|
|
1862
|
-
res = await
|
|
1987
|
+
res = await cliFetch(`${baseUrl}/plugs`);
|
|
1863
1988
|
} catch {
|
|
1864
1989
|
fail(
|
|
1865
1990
|
"connect_failed",
|
|
1866
1991
|
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
1867
1992
|
);
|
|
1868
1993
|
}
|
|
1994
|
+
if (res.status === 401) fail("unauthorized", unauthorizedHint());
|
|
1869
1995
|
if (!res.ok) {
|
|
1870
1996
|
fail(
|
|
1871
1997
|
"api_unavailable",
|
|
@@ -1879,11 +2005,11 @@ var varsCommand = new Command("vars").description("View and manage plug variable
|
|
|
1879
2005
|
const baseUrl = `http://localhost:${port}${opts.basePath}`;
|
|
1880
2006
|
await checkConnectivity(baseUrl);
|
|
1881
2007
|
if (opts.list) {
|
|
1882
|
-
const plugsRes = await
|
|
2008
|
+
const plugsRes = await cliFetch(`${baseUrl}/plugs`);
|
|
1883
2009
|
const plugs = await plugsRes.json();
|
|
1884
2010
|
const variables = await Promise.all(
|
|
1885
2011
|
plugs.map(async (plug) => {
|
|
1886
|
-
const res = await
|
|
2012
|
+
const res = await cliFetch(`${baseUrl}/variables/${plug.name}`);
|
|
1887
2013
|
if (res.status === 404) {
|
|
1888
2014
|
return {
|
|
1889
2015
|
plugName: plug.name,
|
|
@@ -1912,7 +2038,7 @@ var varsCommand = new Command("vars").description("View and manage plug variable
|
|
|
1912
2038
|
}
|
|
1913
2039
|
const resolvedAction = action ?? "show";
|
|
1914
2040
|
if (resolvedAction === "show") {
|
|
1915
|
-
const res = await
|
|
2041
|
+
const res = await cliFetch(`${baseUrl}/variables/${plugName}`);
|
|
1916
2042
|
if (res.status === 404) {
|
|
1917
2043
|
fail("plug_not_found", `Plug "${plugName}" not found.`);
|
|
1918
2044
|
}
|
|
@@ -1939,7 +2065,7 @@ var varsCommand = new Command("vars").description("View and manage plug variable
|
|
|
1939
2065
|
} catch {
|
|
1940
2066
|
fail("invalid_json", "Could not parse --json as JSON.");
|
|
1941
2067
|
}
|
|
1942
|
-
const res = await
|
|
2068
|
+
const res = await cliFetch(`${baseUrl}/variables/${plugName}`, {
|
|
1943
2069
|
method: "POST",
|
|
1944
2070
|
headers: { "Content-Type": "application/json" },
|
|
1945
2071
|
body: JSON.stringify(payload)
|
|
@@ -1955,7 +2081,7 @@ var varsCommand = new Command("vars").description("View and manage plug variable
|
|
|
1955
2081
|
return;
|
|
1956
2082
|
}
|
|
1957
2083
|
if (resolvedAction === "clear") {
|
|
1958
|
-
const res = await
|
|
2084
|
+
const res = await cliFetch(`${baseUrl}/variables/${plugName}`, {
|
|
1959
2085
|
method: "DELETE"
|
|
1960
2086
|
});
|
|
1961
2087
|
if (!res.ok && res.status !== 204) {
|
|
@@ -2023,7 +2149,7 @@ function tryParseJson(value) {
|
|
|
2023
2149
|
async function checkConnectivity2(baseUrl) {
|
|
2024
2150
|
let res;
|
|
2025
2151
|
try {
|
|
2026
|
-
res = await
|
|
2152
|
+
res = await cliFetch(`${baseUrl}/debug`);
|
|
2027
2153
|
} catch {
|
|
2028
2154
|
fail2(
|
|
2029
2155
|
"connect_failed",
|
|
@@ -2046,7 +2172,8 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2046
2172
|
if (opts.list) {
|
|
2047
2173
|
await checkConnectivity2(baseUrl);
|
|
2048
2174
|
try {
|
|
2049
|
-
const res = await
|
|
2175
|
+
const res = await cliFetch(`${baseUrl}/plugs`);
|
|
2176
|
+
if (res.status === 401) fail2("unauthorized", unauthorizedHint());
|
|
2050
2177
|
const data = await res.json();
|
|
2051
2178
|
const raw = Array.isArray(data) ? data : data.plugs ?? [];
|
|
2052
2179
|
const plugs = raw.map((p) => ({
|
|
@@ -2070,7 +2197,7 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2070
2197
|
await checkConnectivity2(baseUrl);
|
|
2071
2198
|
if (opts.info) {
|
|
2072
2199
|
try {
|
|
2073
|
-
const res = await
|
|
2200
|
+
const res = await cliFetch(`${baseUrl}/debug/${plugName}`);
|
|
2074
2201
|
if (res.status === 404) {
|
|
2075
2202
|
fail2(
|
|
2076
2203
|
"plug_not_found",
|
|
@@ -2089,7 +2216,7 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2089
2216
|
let allEndpoints = null;
|
|
2090
2217
|
if (opts.endpoint) {
|
|
2091
2218
|
try {
|
|
2092
|
-
const res = await
|
|
2219
|
+
const res = await cliFetch(`${baseUrl}/debug/${plugName}`);
|
|
2093
2220
|
if (res.status === 404) {
|
|
2094
2221
|
fail2(
|
|
2095
2222
|
"plug_not_found",
|
|
@@ -2153,7 +2280,7 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2153
2280
|
if (extraHeaders) debugPayload["headers"] = extraHeaders;
|
|
2154
2281
|
let debugRes;
|
|
2155
2282
|
try {
|
|
2156
|
-
debugRes = await
|
|
2283
|
+
debugRes = await cliFetch(`${baseUrl}/debug/${plugName}`, {
|
|
2157
2284
|
method: "POST",
|
|
2158
2285
|
headers: { "Content-Type": "application/json" },
|
|
2159
2286
|
body: JSON.stringify(debugPayload)
|
|
@@ -2200,7 +2327,7 @@ var plugCommand = new Command("plug").alias("probe").description(
|
|
|
2200
2327
|
} else {
|
|
2201
2328
|
if (!allEndpoints) {
|
|
2202
2329
|
try {
|
|
2203
|
-
const metaRes = await
|
|
2330
|
+
const metaRes = await cliFetch(`${baseUrl}/debug/${plugName}`);
|
|
2204
2331
|
const metaData = await metaRes.json();
|
|
2205
2332
|
allEndpoints = metaData.endpoints ?? null;
|
|
2206
2333
|
} catch {
|
|
@@ -2275,13 +2402,14 @@ function resolveWebhookOrigin(originFlag) {
|
|
|
2275
2402
|
async function checkConnectivity3(baseUrl) {
|
|
2276
2403
|
let res;
|
|
2277
2404
|
try {
|
|
2278
|
-
res = await
|
|
2405
|
+
res = await cliFetch(`${baseUrl}/plugs`);
|
|
2279
2406
|
} catch {
|
|
2280
2407
|
fail3(
|
|
2281
2408
|
"connect_failed",
|
|
2282
2409
|
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2283
2410
|
);
|
|
2284
2411
|
}
|
|
2412
|
+
if (res.status === 401) fail3("unauthorized", unauthorizedHint());
|
|
2285
2413
|
if (!res.ok) {
|
|
2286
2414
|
fail3(
|
|
2287
2415
|
"api_unavailable",
|
|
@@ -2303,11 +2431,11 @@ var wireCommand = new Command("wire").description(
|
|
|
2303
2431
|
const baseUrl = `http://localhost:${port}${opts.basePath}`;
|
|
2304
2432
|
await checkConnectivity3(baseUrl);
|
|
2305
2433
|
if (opts.list) {
|
|
2306
|
-
const plugsRes = await
|
|
2434
|
+
const plugsRes = await cliFetch(`${baseUrl}/plugs`);
|
|
2307
2435
|
const plugs = await plugsRes.json();
|
|
2308
2436
|
const wires = await Promise.all(
|
|
2309
2437
|
plugs.map(async (plug) => {
|
|
2310
|
-
const res = await
|
|
2438
|
+
const res = await cliFetch(`${baseUrl}/wires/${plug.name}`);
|
|
2311
2439
|
const data = await res.json();
|
|
2312
2440
|
return {
|
|
2313
2441
|
plugName: plug.name,
|
|
@@ -2328,7 +2456,7 @@ var wireCommand = new Command("wire").description(
|
|
|
2328
2456
|
}
|
|
2329
2457
|
const resolvedAction = opts.info ? "info" : action ?? "info";
|
|
2330
2458
|
if (resolvedAction === "info") {
|
|
2331
|
-
const res = await
|
|
2459
|
+
const res = await cliFetch(`${baseUrl}/wires/${plugName}`);
|
|
2332
2460
|
if (res.status === 404) {
|
|
2333
2461
|
fail3("plug_not_found", `Plug "${plugName}" not found.`);
|
|
2334
2462
|
}
|
|
@@ -2343,7 +2471,7 @@ var wireCommand = new Command("wire").description(
|
|
|
2343
2471
|
}
|
|
2344
2472
|
if (resolvedAction === "connect") {
|
|
2345
2473
|
const callbackUrl = opts.callbackUrl ?? `${resolveWebhookOrigin(opts.webhookOrigin)}/api/khotan/webhook/${plugName}`;
|
|
2346
|
-
const res = await
|
|
2474
|
+
const res = await cliFetch(`${baseUrl}/wires/${plugName}`, {
|
|
2347
2475
|
method: "POST",
|
|
2348
2476
|
headers: { "Content-Type": "application/json" },
|
|
2349
2477
|
body: JSON.stringify({ callbackUrl })
|
|
@@ -2367,7 +2495,7 @@ var wireCommand = new Command("wire").description(
|
|
|
2367
2495
|
if (resolvedAction === "disconnect") {
|
|
2368
2496
|
let wireId = opts.wireId;
|
|
2369
2497
|
if (!wireId) {
|
|
2370
|
-
const currentRes = await
|
|
2498
|
+
const currentRes = await cliFetch(`${baseUrl}/wires/${plugName}`);
|
|
2371
2499
|
const current = await currentRes.json();
|
|
2372
2500
|
wireId = current.wire?.id;
|
|
2373
2501
|
}
|
|
@@ -2377,7 +2505,7 @@ var wireCommand = new Command("wire").description(
|
|
|
2377
2505
|
`No active wire found for "${plugName}". Use --wire-id to override.`
|
|
2378
2506
|
);
|
|
2379
2507
|
}
|
|
2380
|
-
const res = await
|
|
2508
|
+
const res = await cliFetch(`${baseUrl}/wires/${plugName}`, {
|
|
2381
2509
|
method: "DELETE",
|
|
2382
2510
|
headers: { "Content-Type": "application/json" },
|
|
2383
2511
|
body: JSON.stringify({ wireId })
|
|
@@ -2435,9 +2563,10 @@ function resolveBaseUrl(opts) {
|
|
|
2435
2563
|
return `http://localhost:${resolvePort4(opts.port)}${opts.basePath}`;
|
|
2436
2564
|
}
|
|
2437
2565
|
async function fetchJson(url, init) {
|
|
2438
|
-
const res = await
|
|
2566
|
+
const res = await cliFetch(url, init);
|
|
2439
2567
|
const data = await res.json().catch(() => ({}));
|
|
2440
2568
|
if (!res.ok) {
|
|
2569
|
+
if (res.status === 401) fail4("unauthorized", unauthorizedHint());
|
|
2441
2570
|
fail4(
|
|
2442
2571
|
"request_failed",
|
|
2443
2572
|
data.error ?? `Request to ${url} failed with status ${res.status}`
|
|
@@ -2446,20 +2575,22 @@ async function fetchJson(url, init) {
|
|
|
2446
2575
|
return data;
|
|
2447
2576
|
}
|
|
2448
2577
|
async function checkConnectivity4(baseUrl) {
|
|
2578
|
+
let res;
|
|
2449
2579
|
try {
|
|
2450
|
-
|
|
2451
|
-
if (!res.ok) {
|
|
2452
|
-
fail4(
|
|
2453
|
-
"api_unavailable",
|
|
2454
|
-
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2455
|
-
);
|
|
2456
|
-
}
|
|
2580
|
+
res = await cliFetch(`${baseUrl}/flows`);
|
|
2457
2581
|
} catch {
|
|
2458
2582
|
fail4(
|
|
2459
2583
|
"connect_failed",
|
|
2460
2584
|
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2461
2585
|
);
|
|
2462
2586
|
}
|
|
2587
|
+
if (res.status === 401) fail4("unauthorized", unauthorizedHint());
|
|
2588
|
+
if (!res.ok) {
|
|
2589
|
+
fail4(
|
|
2590
|
+
"api_unavailable",
|
|
2591
|
+
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2463
2594
|
}
|
|
2464
2595
|
function parseJsonOption(value, label) {
|
|
2465
2596
|
if (!value) return void 0;
|
|
@@ -2571,6 +2702,244 @@ withApiOptions(
|
|
|
2571
2702
|
);
|
|
2572
2703
|
output4({ ok: true, action: "cancel", run });
|
|
2573
2704
|
});
|
|
2705
|
+
function output5(obj) {
|
|
2706
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
2707
|
+
}
|
|
2708
|
+
function fail5(error, hint) {
|
|
2709
|
+
output5({ ok: false, error, hint });
|
|
2710
|
+
process.exit(1);
|
|
2711
|
+
}
|
|
2712
|
+
function parseEnvFile4(filePath) {
|
|
2713
|
+
try {
|
|
2714
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2715
|
+
const values = {};
|
|
2716
|
+
for (const line of content.split("\n")) {
|
|
2717
|
+
const trimmed = line.trim();
|
|
2718
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2719
|
+
const match = /^([A-Z0-9_]+)\s*=\s*["']?(.*?)["']?$/.exec(trimmed);
|
|
2720
|
+
if (match) values[match[1]] = match[2];
|
|
2721
|
+
}
|
|
2722
|
+
return values;
|
|
2723
|
+
} catch {
|
|
2724
|
+
return {};
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
function parsePortFromEnvFile5(filePath) {
|
|
2728
|
+
const raw = parseEnvFile4(filePath)["PORT"];
|
|
2729
|
+
if (!raw) return null;
|
|
2730
|
+
const parsed = parseInt(raw, 10);
|
|
2731
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2732
|
+
}
|
|
2733
|
+
function resolvePort5(portFlag) {
|
|
2734
|
+
if (portFlag) return parseInt(portFlag, 10);
|
|
2735
|
+
const cwd = process.cwd();
|
|
2736
|
+
return parsePortFromEnvFile5(path2.join(cwd, ".env.local")) ?? parsePortFromEnvFile5(path2.join(cwd, ".env")) ?? 3e3;
|
|
2737
|
+
}
|
|
2738
|
+
function resolveBaseUrl2(opts) {
|
|
2739
|
+
return `http://localhost:${resolvePort5(opts.port)}${opts.basePath}`;
|
|
2740
|
+
}
|
|
2741
|
+
async function fetchJson2(url, init) {
|
|
2742
|
+
const res = await cliFetch(url, init);
|
|
2743
|
+
const data = await res.json().catch(() => ({}));
|
|
2744
|
+
if (!res.ok) {
|
|
2745
|
+
if (res.status === 401) fail5("unauthorized", unauthorizedHint());
|
|
2746
|
+
fail5(
|
|
2747
|
+
"request_failed",
|
|
2748
|
+
data.error ?? `Request to ${url} failed with status ${res.status}`
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
return data;
|
|
2752
|
+
}
|
|
2753
|
+
async function checkConnectivity5(baseUrl) {
|
|
2754
|
+
let res;
|
|
2755
|
+
try {
|
|
2756
|
+
res = await cliFetch(`${baseUrl}/flows`);
|
|
2757
|
+
} catch {
|
|
2758
|
+
fail5(
|
|
2759
|
+
"connect_failed",
|
|
2760
|
+
`Could not connect to dev server at ${baseUrl.replace("http://", "")}. Is it running?`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
if (res.status === 401) fail5("unauthorized", unauthorizedHint());
|
|
2764
|
+
if (!res.ok) {
|
|
2765
|
+
fail5(
|
|
2766
|
+
"api_unavailable",
|
|
2767
|
+
`Could not reach Khotan flows API at ${baseUrl}. Check your base path and dev server.`
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
function parseJsonObjectOption(value, label) {
|
|
2772
|
+
if (!value) return void 0;
|
|
2773
|
+
try {
|
|
2774
|
+
const parsed = JSON.parse(value);
|
|
2775
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2776
|
+
fail5("invalid_json", `${label} must be a JSON object.`);
|
|
2777
|
+
}
|
|
2778
|
+
return parsed;
|
|
2779
|
+
} catch {
|
|
2780
|
+
fail5("invalid_json", `${label} must be valid JSON.`);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
function withApiOptions2(command) {
|
|
2784
|
+
return command.option("--port <port>", "Dev server port").option("--base-path <path>", "API base path", "/api/khotan");
|
|
2785
|
+
}
|
|
2786
|
+
async function listResources(baseUrl) {
|
|
2787
|
+
return fetchJson2(`${baseUrl}/resources`);
|
|
2788
|
+
}
|
|
2789
|
+
async function resolveResource(baseUrl, resourceNameOrId) {
|
|
2790
|
+
const resources = await listResources(baseUrl);
|
|
2791
|
+
const match = resources.find(
|
|
2792
|
+
(resource) => resource.id === resourceNameOrId || resource.name === resourceNameOrId
|
|
2793
|
+
);
|
|
2794
|
+
if (!match) {
|
|
2795
|
+
fail5(
|
|
2796
|
+
"resource_not_found",
|
|
2797
|
+
`Resource "${resourceNameOrId}" is not registered in the running Khotan config.`
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
return match;
|
|
2801
|
+
}
|
|
2802
|
+
var mappingsCommand = new Command("mappings").description("List, lookup, and mutate mappings via the running Khotan API");
|
|
2803
|
+
withApiOptions2(
|
|
2804
|
+
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")
|
|
2805
|
+
).action(
|
|
2806
|
+
async (resourceNameOrId, opts) => {
|
|
2807
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2808
|
+
await checkConnectivity5(baseUrl);
|
|
2809
|
+
const resource = await resolveResource(baseUrl, resourceNameOrId);
|
|
2810
|
+
const limit = Math.max(parseInt(opts.limit, 10) || 20, 1);
|
|
2811
|
+
const offset = Math.max(parseInt(opts.offset, 10) || 0, 0);
|
|
2812
|
+
const url = new URL(
|
|
2813
|
+
`${baseUrl}/resources/${encodeURIComponent(resource.id)}/mappings`
|
|
2814
|
+
);
|
|
2815
|
+
url.searchParams.set("limit", String(limit));
|
|
2816
|
+
url.searchParams.set("offset", String(offset));
|
|
2817
|
+
if (opts.search) {
|
|
2818
|
+
url.searchParams.set("search", opts.search);
|
|
2819
|
+
}
|
|
2820
|
+
const data = await fetchJson2(url.toString());
|
|
2821
|
+
output5({
|
|
2822
|
+
ok: true,
|
|
2823
|
+
resource,
|
|
2824
|
+
items: data.items,
|
|
2825
|
+
page: data.page
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
);
|
|
2829
|
+
withApiOptions2(
|
|
2830
|
+
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")
|
|
2831
|
+
).action(
|
|
2832
|
+
async (resourceNameOrId, opts) => {
|
|
2833
|
+
const usingConnectValue = typeof opts.connectValue === "string";
|
|
2834
|
+
const usingPlugRef = typeof opts.plug === "string" || typeof opts.ref === "string";
|
|
2835
|
+
if (!usingConnectValue && !usingPlugRef) {
|
|
2836
|
+
fail5(
|
|
2837
|
+
"validation_error",
|
|
2838
|
+
"Pass either --connect-value <value> or --plug <plugName> with --ref <ref>."
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
if (usingConnectValue && usingPlugRef) {
|
|
2842
|
+
fail5(
|
|
2843
|
+
"validation_error",
|
|
2844
|
+
"Choose one lookup mode: either --connect-value or --plug with --ref."
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
if (opts.plug && !opts.ref || !opts.plug && opts.ref) {
|
|
2848
|
+
fail5(
|
|
2849
|
+
"validation_error",
|
|
2850
|
+
"Plug-ref lookup requires both --plug <plugName> and --ref <ref>."
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2854
|
+
await checkConnectivity5(baseUrl);
|
|
2855
|
+
const resource = await resolveResource(baseUrl, resourceNameOrId);
|
|
2856
|
+
const payload = usingConnectValue ? {
|
|
2857
|
+
resourceId: resource.id,
|
|
2858
|
+
connectValue: opts.connectValue
|
|
2859
|
+
} : {
|
|
2860
|
+
resourceId: resource.id,
|
|
2861
|
+
plugName: opts.plug,
|
|
2862
|
+
ref: opts.ref
|
|
2863
|
+
};
|
|
2864
|
+
const mapping = await fetchJson2(
|
|
2865
|
+
`${baseUrl}/mappings/lookup`,
|
|
2866
|
+
{
|
|
2867
|
+
method: "POST",
|
|
2868
|
+
headers: { "Content-Type": "application/json" },
|
|
2869
|
+
body: JSON.stringify(payload)
|
|
2870
|
+
}
|
|
2871
|
+
);
|
|
2872
|
+
output5({ ok: true, resource, mapping });
|
|
2873
|
+
}
|
|
2874
|
+
);
|
|
2875
|
+
withApiOptions2(
|
|
2876
|
+
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")
|
|
2877
|
+
).action(
|
|
2878
|
+
async (resourceNameOrId, opts) => {
|
|
2879
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2880
|
+
await checkConnectivity5(baseUrl);
|
|
2881
|
+
const resource = await resolveResource(baseUrl, resourceNameOrId);
|
|
2882
|
+
const refs = parseJsonObjectOption(opts.refs, "--refs");
|
|
2883
|
+
const metadata = parseJsonObjectOption(opts.metadata, "--metadata");
|
|
2884
|
+
const mapping = await fetchJson2(`${baseUrl}/mappings`, {
|
|
2885
|
+
method: "POST",
|
|
2886
|
+
headers: { "Content-Type": "application/json" },
|
|
2887
|
+
body: JSON.stringify({
|
|
2888
|
+
resourceId: resource.id,
|
|
2889
|
+
connectValue: opts.connectValue,
|
|
2890
|
+
refs,
|
|
2891
|
+
metadata
|
|
2892
|
+
})
|
|
2893
|
+
});
|
|
2894
|
+
output5({ ok: true, action: "upsert", resource, mapping });
|
|
2895
|
+
}
|
|
2896
|
+
);
|
|
2897
|
+
withApiOptions2(
|
|
2898
|
+
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")
|
|
2899
|
+
).action(
|
|
2900
|
+
async (mappingId, opts) => {
|
|
2901
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2902
|
+
await checkConnectivity5(baseUrl);
|
|
2903
|
+
const resource = await resolveResource(baseUrl, opts.resource);
|
|
2904
|
+
const refs = parseJsonObjectOption(opts.refs, "--refs");
|
|
2905
|
+
const metadata = parseJsonObjectOption(opts.metadata, "--metadata");
|
|
2906
|
+
const mapping = await fetchJson2(
|
|
2907
|
+
`${baseUrl}/mappings/${encodeURIComponent(mappingId)}`,
|
|
2908
|
+
{
|
|
2909
|
+
method: "PUT",
|
|
2910
|
+
headers: { "Content-Type": "application/json" },
|
|
2911
|
+
body: JSON.stringify({
|
|
2912
|
+
resourceId: resource.id,
|
|
2913
|
+
connectValue: opts.connectValue,
|
|
2914
|
+
refs,
|
|
2915
|
+
metadata
|
|
2916
|
+
})
|
|
2917
|
+
}
|
|
2918
|
+
);
|
|
2919
|
+
output5({ ok: true, action: "update", resource, mapping });
|
|
2920
|
+
}
|
|
2921
|
+
);
|
|
2922
|
+
withApiOptions2(
|
|
2923
|
+
mappingsCommand.command("delete").description("Delete one mapping by row ID").argument("<mappingId>", "Mapping row ID")
|
|
2924
|
+
).action(async (mappingId, opts) => {
|
|
2925
|
+
const baseUrl = resolveBaseUrl2(opts);
|
|
2926
|
+
await checkConnectivity5(baseUrl);
|
|
2927
|
+
const res = await cliFetch(
|
|
2928
|
+
`${baseUrl}/mappings/${encodeURIComponent(mappingId)}`,
|
|
2929
|
+
{
|
|
2930
|
+
method: "DELETE"
|
|
2931
|
+
}
|
|
2932
|
+
);
|
|
2933
|
+
if (!res.ok) {
|
|
2934
|
+
if (res.status === 401) fail5("unauthorized", unauthorizedHint());
|
|
2935
|
+
const data = await res.json().catch(() => ({}));
|
|
2936
|
+
fail5(
|
|
2937
|
+
"request_failed",
|
|
2938
|
+
data.error ?? `Delete request failed for mapping "${mappingId}" with status ${res.status}.`
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2941
|
+
output5({ ok: true, action: "delete", id: mappingId });
|
|
2942
|
+
});
|
|
2574
2943
|
|
|
2575
2944
|
// src/cli/index.ts
|
|
2576
2945
|
var program = new Command();
|
|
@@ -2582,4 +2951,5 @@ program.addCommand(migrateCommand);
|
|
|
2582
2951
|
program.addCommand(plugCommand);
|
|
2583
2952
|
program.addCommand(wireCommand);
|
|
2584
2953
|
program.addCommand(flowsCommand);
|
|
2954
|
+
program.addCommand(mappingsCommand);
|
|
2585
2955
|
program.parse();
|