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/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, path14) {
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: path14,
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, `${path14}[]`);
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: path14,
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, path14) {
1836
+ function diffObjectSchema(expected, actual, path16) {
1754
1837
  if (actual.type !== "object") {
1755
1838
  return [
1756
1839
  {
1757
- path: path14,
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 = path14 === "$" ? `$.${key}` : `${path14}.${key}`;
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 = path14 === "$" ? `$.${key}` : `${path14}.${key}`;
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/variables/${plug.name}`);
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 fetch(`${baseUrl}/variables/${plugName}`);
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 fetch(`${baseUrl}/variables/${plugName}`, {
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 fetch(`${baseUrl}/variables/${plugName}`, {
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 fetch(`${baseUrl}/debug`);
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/debug/${plugName}`);
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 fetch(`${baseUrl}/debug/${plugName}`);
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 fetch(`${baseUrl}/debug/${plugName}`, {
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 fetch(`${baseUrl}/debug/${plugName}`);
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/plugs`);
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 fetch(`${baseUrl}/wires/${plug.name}`);
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 fetch(`${baseUrl}/wires/${plugName}`);
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 fetch(`${baseUrl}/wires/${plugName}`, {
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 fetch(`${baseUrl}/wires/${plugName}`);
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 fetch(`${baseUrl}/wires/${plugName}`, {
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 fetch(url, init);
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
- const res = await fetch(`${baseUrl}/flows`);
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();