@eide/foir-cli 0.1.47 → 0.3.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
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { config } from "dotenv";
5
- import { resolve as resolve6, dirname as dirname5 } from "path";
6
- import { fileURLToPath as fileURLToPath2 } from "url";
5
+ import { resolve as resolve6, dirname as dirname4 } from "path";
6
+ import { fileURLToPath } from "url";
7
7
  import { createRequire } from "module";
8
8
  import { Command } from "commander";
9
9
 
@@ -237,23 +237,15 @@ var DEFAULT_API_URL = "https://api.foir.dev";
237
237
  function getApiUrl(options) {
238
238
  return process.env.FOIR_API_URL ?? options?.apiUrl ?? DEFAULT_API_URL;
239
239
  }
240
- function getGraphQLEndpoint(apiUrl) {
241
- const base = apiUrl.replace(/\/$/, "").replace(/\/graphql$/, "");
242
- return `${base}/graphql`;
243
- }
244
240
 
245
241
  // src/lib/errors.ts
246
242
  import chalk from "chalk";
243
+ import { ConnectError, Code } from "@connectrpc/connect";
247
244
  function extractErrorMessage(error) {
248
245
  if (!error || typeof error !== "object")
249
246
  return String(error ?? "Unknown error");
250
- const err = error;
251
- const gqlErrors = err.response?.errors;
252
- if (gqlErrors && gqlErrors.length > 0) {
253
- return gqlErrors[0].message;
254
- }
255
- if (err.message && err.message !== "undefined") {
256
- return err.message;
247
+ if (error instanceof ConnectError) {
248
+ return error.message;
257
249
  }
258
250
  if (error instanceof Error && error.message) {
259
251
  return error.message;
@@ -266,44 +258,25 @@ function withErrorHandler(optsFn, fn) {
266
258
  await fn(...args);
267
259
  } catch (error) {
268
260
  const opts = optsFn();
269
- const gqlErr = error;
270
- const gqlErrors = gqlErr?.response?.errors;
271
- if (gqlErrors && gqlErrors.length > 0) {
272
- const first = gqlErrors[0];
273
- const code = first.extensions?.code;
274
- const validationErrors = first.extensions?.validationErrors;
261
+ if (error instanceof ConnectError) {
262
+ const code = error.code;
263
+ const message2 = error.message;
275
264
  if (opts?.json || opts?.jsonl) {
276
265
  console.error(
277
266
  JSON.stringify({
278
267
  error: {
279
- message: first.message,
280
- code: code ?? "GRAPHQL_ERROR",
281
- ...validationErrors ? { validationErrors } : {},
282
- ...gqlErrors.length > 1 ? {
283
- additionalErrors: gqlErrors.slice(1).map((e) => e.message)
284
- } : {}
268
+ message: message2,
269
+ code: Code[code] ?? "UNKNOWN"
285
270
  }
286
271
  })
287
272
  );
288
273
  } else {
289
- console.error(chalk.red("Error:"), first.message);
290
- for (const extra of gqlErrors.slice(1)) {
291
- console.error(chalk.red(" \u2022"), extra.message);
292
- }
293
- if (validationErrors && Object.keys(validationErrors).length > 0) {
294
- console.error("");
295
- console.error(chalk.yellow("Field errors:"));
296
- for (const [field, messages] of Object.entries(validationErrors)) {
297
- for (const msg of messages) {
298
- console.error(chalk.gray(` \u2022 ${field}:`), msg);
299
- }
300
- }
301
- }
302
- if (code === "UNAUTHENTICATED") {
274
+ console.error(chalk.red("Error:"), message2);
275
+ if (code === Code.Unauthenticated) {
303
276
  console.error(
304
277
  chalk.gray("\nHint: Run `foir login` to authenticate.")
305
278
  );
306
- } else if (code === "FORBIDDEN") {
279
+ } else if (code === Code.PermissionDenied) {
307
280
  console.error(
308
281
  chalk.gray(
309
282
  "\nHint: You may not have permission. Check your API key scopes."
@@ -313,22 +286,6 @@ function withErrorHandler(optsFn, fn) {
313
286
  }
314
287
  process.exit(1);
315
288
  }
316
- const status = gqlErr?.response?.status;
317
- if (status === 401 || status === 403) {
318
- if (opts?.json || opts?.jsonl) {
319
- console.error(
320
- JSON.stringify({
321
- error: { message: "Authentication failed", code: "AUTH_ERROR" }
322
- })
323
- );
324
- } else {
325
- console.error(chalk.red("Error:"), "Authentication failed.");
326
- console.error(
327
- chalk.gray("Hint: Run `foir login` or set FOIR_API_KEY.")
328
- );
329
- }
330
- process.exit(1);
331
- }
332
289
  const message = extractErrorMessage(error);
333
290
  if (opts?.json || opts?.jsonl) {
334
291
  console.error(JSON.stringify({ error: { message } }));
@@ -489,102 +446,154 @@ function registerLogoutCommand(program2, globalOpts) {
489
446
 
490
447
  // src/commands/select-project.ts
491
448
  import inquirer from "inquirer";
492
- var CLI_API_KEY_NAME = "Foir CLI";
493
- var CLI_API_KEY_SCOPES = [
494
- "records:read",
495
- "records:write",
496
- "records:delete",
497
- "records:publish",
498
- "files:read",
499
- "files:write",
500
- "configs:read",
501
- "operations:read",
502
- "operations:execute"
503
- ];
504
- async function gqlRequest(apiUrl, accessToken, query, variables, extraHeaders) {
505
- const response = await fetch(getGraphQLEndpoint(apiUrl), {
506
- method: "POST",
507
- headers: {
508
- "Content-Type": "application/json",
509
- Authorization: `Bearer ${accessToken}`,
510
- ...extraHeaders
511
- },
512
- body: JSON.stringify({ query, variables })
513
- });
514
- if (!response.ok) {
515
- throw new Error(`GraphQL request failed: ${response.statusText}`);
449
+
450
+ // src/lib/client.ts
451
+ import { createClient as createRpcClient } from "@connectrpc/connect";
452
+ import { createConnectTransport } from "@connectrpc/connect-node";
453
+ import {
454
+ IdentityService,
455
+ ModelsService,
456
+ RecordsService,
457
+ ConfigsService,
458
+ SegmentsService,
459
+ ExperimentsService,
460
+ SettingsService,
461
+ StorageService
462
+ } from "@eide/foir-connect-clients/services";
463
+ import { createIdentityMethods } from "@eide/foir-connect-clients/identity";
464
+ import { createModelsMethods } from "@eide/foir-connect-clients/models";
465
+ import { createRecordsMethods } from "@eide/foir-connect-clients/records";
466
+ import { createConfigsMethods } from "@eide/foir-connect-clients/configs";
467
+ import { createSegmentsMethods } from "@eide/foir-connect-clients/segments";
468
+ import { createExperimentsMethods } from "@eide/foir-connect-clients/experiments";
469
+ import { createSettingsMethods } from "@eide/foir-connect-clients/settings";
470
+ import { createStorageMethods } from "@eide/foir-connect-clients/storage";
471
+ import { GraphQLClient } from "graphql-request";
472
+ async function createPlatformClient(options) {
473
+ const apiUrl = getApiUrl(options);
474
+ const headers = {};
475
+ const envApiKey = process.env.FOIR_API_KEY;
476
+ if (envApiKey) {
477
+ headers["x-api-key"] = envApiKey;
478
+ } else {
479
+ const credentials = await getCredentials();
480
+ if (!credentials) {
481
+ throw new Error(
482
+ "Not authenticated. Run `foir login` or set FOIR_API_KEY."
483
+ );
484
+ }
485
+ if (isTokenExpired(credentials)) {
486
+ throw new Error("Session expired. Run `foir login` to re-authenticate.");
487
+ }
488
+ headers["Authorization"] = `Bearer ${credentials.accessToken}`;
516
489
  }
517
- const result = await response.json();
518
- if (result.errors?.length) {
519
- throw new Error(`GraphQL error: ${result.errors[0].message}`);
490
+ const resolved = await resolveProjectContext(options);
491
+ if (resolved) {
492
+ headers["x-tenant-id"] = resolved.project.tenantId;
493
+ headers["x-project-id"] = resolved.project.id;
520
494
  }
521
- return result.data;
522
- }
523
- async function fetchSessionContext(apiUrl, accessToken) {
524
- const data = await gqlRequest(
525
- apiUrl,
526
- accessToken,
527
- `query { sessionContext { tenantId projectId availableTenants { id name } availableProjects { id name tenantId } } }`
528
- );
529
- return data.sessionContext;
530
- }
531
- async function fetchApiKeys(apiUrl, accessToken, projectId, tenantId) {
532
- const data = await gqlRequest(
533
- apiUrl,
534
- accessToken,
535
- `query { listApiKeys(includeInactive: false, limit: 100) { items { id name isActive } } }`,
536
- void 0,
537
- { "x-tenant-id": tenantId, "x-project-id": projectId }
538
- );
539
- return data.listApiKeys?.items ?? [];
540
- }
541
- async function createApiKey(apiUrl, accessToken, projectId, tenantId) {
542
- const data = await gqlRequest(
543
- apiUrl,
544
- accessToken,
545
- `mutation($input: CreateApiKeyInput!) { createApiKey(input: $input) { apiKey { id name isActive } plainKey } }`,
546
- {
547
- input: {
548
- name: CLI_API_KEY_NAME,
549
- projectId,
550
- keyType: "SECRET",
551
- scopes: CLI_API_KEY_SCOPES
552
- }
553
- },
554
- { "x-tenant-id": tenantId, "x-project-id": projectId }
555
- );
556
- return data.createApiKey;
495
+ const authInterceptor = (next) => async (req) => {
496
+ for (const [key, value] of Object.entries(headers)) {
497
+ req.header.set(key, value);
498
+ }
499
+ return next(req);
500
+ };
501
+ const transport = createConnectTransport({
502
+ baseUrl: apiUrl.replace(/\/$/, ""),
503
+ httpVersion: "1.1",
504
+ interceptors: [authInterceptor]
505
+ });
506
+ return {
507
+ identity: createIdentityMethods(createRpcClient(IdentityService, transport)),
508
+ models: createModelsMethods(createRpcClient(ModelsService, transport)),
509
+ records: createRecordsMethods(createRpcClient(RecordsService, transport)),
510
+ configs: createConfigsMethods(createRpcClient(ConfigsService, transport)),
511
+ segments: createSegmentsMethods(createRpcClient(SegmentsService, transport)),
512
+ experiments: createExperimentsMethods(
513
+ createRpcClient(ExperimentsService, transport)
514
+ ),
515
+ settings: createSettingsMethods(createRpcClient(SettingsService, transport)),
516
+ storage: createStorageMethods(createRpcClient(StorageService, transport))
517
+ };
557
518
  }
558
- async function rotateApiKey(apiUrl, accessToken, projectId, tenantId, keyId) {
559
- const data = await gqlRequest(
560
- apiUrl,
561
- accessToken,
562
- `mutation($id: ID!) { rotateApiKey(id: $id) { apiKey { id name isActive } plainKey } }`,
563
- { id: keyId },
564
- { "x-tenant-id": tenantId, "x-project-id": projectId }
565
- );
566
- return data.rotateApiKey;
519
+ function createPlatformClientWithHeaders(apiUrl, headers) {
520
+ const authInterceptor = (next) => async (req) => {
521
+ for (const [key, value] of Object.entries(headers)) {
522
+ req.header.set(key, value);
523
+ }
524
+ return next(req);
525
+ };
526
+ const transport = createConnectTransport({
527
+ baseUrl: apiUrl.replace(/\/$/, ""),
528
+ httpVersion: "1.1",
529
+ interceptors: [authInterceptor]
530
+ });
531
+ return {
532
+ identity: createIdentityMethods(createRpcClient(IdentityService, transport)),
533
+ models: createModelsMethods(createRpcClient(ModelsService, transport)),
534
+ records: createRecordsMethods(createRpcClient(RecordsService, transport)),
535
+ configs: createConfigsMethods(createRpcClient(ConfigsService, transport)),
536
+ segments: createSegmentsMethods(createRpcClient(SegmentsService, transport)),
537
+ experiments: createExperimentsMethods(
538
+ createRpcClient(ExperimentsService, transport)
539
+ ),
540
+ settings: createSettingsMethods(createRpcClient(SettingsService, transport)),
541
+ storage: createStorageMethods(createRpcClient(StorageService, transport))
542
+ };
567
543
  }
568
- async function provisionApiKey(apiUrl, accessToken, projectId, tenantId) {
569
- const apiKeys = await fetchApiKeys(apiUrl, accessToken, projectId, tenantId);
570
- const existing = apiKeys.find(
571
- (k) => k.name === CLI_API_KEY_NAME && k.isActive
572
- );
573
- if (existing) {
574
- console.log(" Rotating existing CLI API key...");
575
- const rotated = await rotateApiKey(
576
- apiUrl,
577
- accessToken,
578
- projectId,
579
- tenantId,
580
- existing.id
581
- );
582
- return { apiKey: rotated.plainKey, apiKeyId: rotated.apiKey.id };
544
+ async function getStorageAuth(options) {
545
+ const apiUrl = getApiUrl(options);
546
+ const baseHeaders = {
547
+ "Content-Type": "application/json"
548
+ };
549
+ const envApiKey = process.env.FOIR_API_KEY;
550
+ if (envApiKey) {
551
+ baseHeaders["x-api-key"] = envApiKey;
552
+ } else {
553
+ const credentials = await getCredentials();
554
+ if (!credentials) {
555
+ throw new Error(
556
+ "Not authenticated. Run `foir login` or set FOIR_API_KEY."
557
+ );
558
+ }
559
+ if (isTokenExpired(credentials)) {
560
+ throw new Error("Session expired. Run `foir login` to re-authenticate.");
561
+ }
562
+ baseHeaders["Authorization"] = `Bearer ${credentials.accessToken}`;
583
563
  }
584
- console.log(" Creating CLI API key...");
585
- const created = await createApiKey(apiUrl, accessToken, projectId, tenantId);
586
- return { apiKey: created.plainKey, apiKeyId: created.apiKey.id };
564
+ const resolved = await resolveProjectContext(options);
565
+ if (resolved) {
566
+ baseHeaders["x-tenant-id"] = resolved.project.tenantId;
567
+ baseHeaders["x-project-id"] = resolved.project.id;
568
+ }
569
+ let cachedToken = null;
570
+ const getToken = async () => {
571
+ if (cachedToken && Date.now() < cachedToken.expiresAt - 3e4) {
572
+ return cachedToken.token;
573
+ }
574
+ const tokenUrl = `${apiUrl.replace(/\/$/, "")}/api/auth/token`;
575
+ const res = await fetch(tokenUrl, {
576
+ method: "POST",
577
+ headers: baseHeaders,
578
+ body: JSON.stringify({ purpose: "storage" })
579
+ });
580
+ if (!res.ok) {
581
+ throw new Error(
582
+ `Failed to get storage token (${res.status}): ${await res.text()}`
583
+ );
584
+ }
585
+ const data = await res.json();
586
+ cachedToken = {
587
+ token: data.token,
588
+ expiresAt: new Date(data.expiresAt).getTime()
589
+ };
590
+ return cachedToken.token;
591
+ };
592
+ return { getToken };
587
593
  }
594
+
595
+ // src/commands/select-project.ts
596
+ var CLI_API_KEY_NAME = "Foir CLI";
588
597
  function registerSelectProjectCommand(program2, globalOpts) {
589
598
  program2.command("select-project").description("Choose which project to work with").option("--project-id <id>", "Project ID to select directly").option("--save-as <name>", "Save as a named profile").action(
590
599
  withErrorHandler(
@@ -598,11 +607,24 @@ function registerSelectProjectCommand(program2, globalOpts) {
598
607
  throw new Error("Not authenticated");
599
608
  }
600
609
  console.log("Fetching your projects...\n");
601
- const sessionContext = await fetchSessionContext(
602
- apiUrl,
603
- credentials.accessToken
610
+ const client = createPlatformClientWithHeaders(apiUrl, {
611
+ Authorization: `Bearer ${credentials.accessToken}`
612
+ });
613
+ const sessionContext = await client.identity.getSessionContext();
614
+ if (!sessionContext) {
615
+ throw new Error("Could not fetch session context");
616
+ }
617
+ const tenants = (sessionContext.availableTenants ?? []).map(
618
+ (t) => ({
619
+ id: t.id,
620
+ name: t.name
621
+ })
604
622
  );
605
- const { availableTenants: tenants, availableProjects: projects } = sessionContext;
623
+ const projects = (sessionContext.availableProjects ?? []).map((p) => ({
624
+ id: p.id,
625
+ name: p.name,
626
+ tenantId: p.tenantId
627
+ }));
606
628
  if (projects.length === 0) {
607
629
  console.log("No projects found. Create one in the platform first.");
608
630
  throw new Error("No projects available");
@@ -651,12 +673,12 @@ function registerSelectProjectCommand(program2, globalOpts) {
651
673
  selectedProject = projects.find((p) => p.id === projectId);
652
674
  }
653
675
  console.log("\nProvisioning API key for CLI access...");
654
- const { apiKey, apiKeyId } = await provisionApiKey(
655
- apiUrl,
656
- credentials.accessToken,
657
- selectedProject.id,
658
- selectedProject.tenantId
659
- );
676
+ const projectClient = createPlatformClientWithHeaders(apiUrl, {
677
+ Authorization: `Bearer ${credentials.accessToken}`,
678
+ "x-tenant-id": selectedProject.tenantId,
679
+ "x-project-id": selectedProject.id
680
+ });
681
+ const { apiKey, apiKeyId } = await provisionApiKey(projectClient);
660
682
  await writeProjectContext(
661
683
  {
662
684
  id: selectedProject.id,
@@ -681,6 +703,26 @@ function registerSelectProjectCommand(program2, globalOpts) {
681
703
  )
682
704
  );
683
705
  }
706
+ async function provisionApiKey(client) {
707
+ const { items: apiKeys } = await client.identity.listApiKeys({ limit: 100 });
708
+ const existing = apiKeys.find(
709
+ (k) => k.name === CLI_API_KEY_NAME && k.isActive
710
+ );
711
+ if (existing) {
712
+ console.log(" Rotating existing CLI API key...");
713
+ const result2 = await client.identity.rotateApiKey(existing.id);
714
+ if (!result2.apiKey?.rawKey) {
715
+ throw new Error("Failed to rotate API key \u2014 no raw key returned");
716
+ }
717
+ return { apiKey: result2.apiKey.rawKey, apiKeyId: result2.apiKey.id };
718
+ }
719
+ console.log(" Creating CLI API key...");
720
+ const result = await client.identity.createApiKey({ name: CLI_API_KEY_NAME });
721
+ if (!result.apiKey?.rawKey) {
722
+ throw new Error("Failed to create API key \u2014 no raw key returned");
723
+ }
724
+ return { apiKey: result.apiKey.rawKey, apiKeyId: result.apiKey.id };
725
+ }
684
726
 
685
727
  // src/lib/output.ts
686
728
  import chalk2 from "chalk";
@@ -734,11 +776,11 @@ function formatList(items, options, config2) {
734
776
  ${items.length} of ${config2.total} shown`));
735
777
  }
736
778
  }
737
- function pad(str, width) {
738
- if (str.length > width) {
739
- return str.slice(0, width - 1) + "\u2026";
779
+ function pad(str2, width) {
780
+ if (str2.length > width) {
781
+ return str2.slice(0, width - 1) + "\u2026";
740
782
  }
741
- return str.padEnd(width);
783
+ return str2.padEnd(width);
742
784
  }
743
785
  function timeAgo(dateStr) {
744
786
  if (!dateStr) return "\u2014";
@@ -831,99 +873,303 @@ function registerWhoamiCommand(program2, globalOpts) {
831
873
  import { promises as fs2 } from "fs";
832
874
  import { basename } from "path";
833
875
  import chalk3 from "chalk";
876
+ import { createStorageClient } from "@eide/foir-connect-clients/storage";
834
877
 
835
- // src/lib/client.ts
836
- import { GraphQLClient } from "graphql-request";
837
- async function createClient(options) {
838
- const apiUrl = getApiUrl(options);
839
- const endpoint = getGraphQLEndpoint(apiUrl);
840
- const headers = {
841
- "Content-Type": "application/json"
842
- };
843
- const envApiKey = process.env.FOIR_API_KEY;
844
- if (envApiKey) {
845
- headers["x-api-key"] = envApiKey;
846
- return new GraphQLClient(endpoint, { headers });
847
- }
848
- const credentials = await getCredentials();
849
- if (!credentials) {
850
- throw new Error("Not authenticated. Run `foir login` or set FOIR_API_KEY.");
878
+ // src/lib/input.ts
879
+ import inquirer2 from "inquirer";
880
+
881
+ // src/lib/config-loader.ts
882
+ import { readFile } from "fs/promises";
883
+ import { pathToFileURL } from "url";
884
+ import { resolve } from "path";
885
+ async function loadConfig(filePath) {
886
+ const absPath = resolve(filePath);
887
+ if (filePath.endsWith(".ts")) {
888
+ const configModule = await import(pathToFileURL(absPath).href);
889
+ return configModule.default;
851
890
  }
852
- if (isTokenExpired(credentials)) {
853
- throw new Error("Session expired. Run `foir login` to re-authenticate.");
891
+ if (filePath.endsWith(".js") || filePath.endsWith(".mjs")) {
892
+ const configModule = await import(pathToFileURL(absPath).href);
893
+ return configModule.default;
854
894
  }
855
- headers["Authorization"] = `Bearer ${credentials.accessToken}`;
856
- const resolved = await resolveProjectContext(options);
857
- if (resolved) {
858
- headers["x-tenant-id"] = resolved.project.tenantId;
859
- headers["x-project-id"] = resolved.project.id;
895
+ if (filePath.endsWith(".json")) {
896
+ const content = await readFile(absPath, "utf-8");
897
+ return JSON.parse(content);
860
898
  }
861
- return new GraphQLClient(endpoint, { headers });
899
+ throw new Error(
900
+ `Unsupported file extension for "${filePath}". Supported: .ts, .js, .mjs, .json`
901
+ );
862
902
  }
863
- async function getRestAuth(options) {
864
- const apiUrl = getApiUrl(options);
865
- const headers = {};
866
- const envApiKey = process.env.FOIR_API_KEY;
867
- if (envApiKey) {
868
- headers["x-api-key"] = envApiKey;
869
- return { apiUrl, headers };
870
- }
871
- const credentials = await getCredentials();
872
- if (!credentials) {
873
- throw new Error("Not authenticated. Run `foir login` or set FOIR_API_KEY.");
903
+
904
+ // src/lib/input.ts
905
+ async function parseInputData(opts) {
906
+ if (opts.data) {
907
+ return JSON.parse(opts.data);
874
908
  }
875
- if (isTokenExpired(credentials)) {
876
- throw new Error("Session expired. Run `foir login` to re-authenticate.");
909
+ if (opts.file) {
910
+ return await loadConfig(opts.file);
877
911
  }
878
- headers["Authorization"] = `Bearer ${credentials.accessToken}`;
879
- const resolved = await resolveProjectContext(options);
880
- if (resolved) {
881
- headers["x-tenant-id"] = resolved.project.tenantId;
882
- headers["x-project-id"] = resolved.project.id;
912
+ if (!process.stdin.isTTY) {
913
+ const chunks = [];
914
+ for await (const chunk of process.stdin) {
915
+ chunks.push(chunk);
916
+ }
917
+ const stdinContent = Buffer.concat(chunks).toString("utf-8").trim();
918
+ if (stdinContent) {
919
+ return JSON.parse(stdinContent);
920
+ }
883
921
  }
884
- return { apiUrl, headers };
922
+ throw new Error(
923
+ "No input data provided. Use --data, --file, or pipe via stdin."
924
+ );
925
+ }
926
+ function isUUID(value) {
927
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
928
+ value
929
+ ) || /^c[a-z0-9]{24,}$/.test(value);
930
+ }
931
+ async function confirmAction(message, opts) {
932
+ if (opts?.confirm) return true;
933
+ const { confirmed } = await inquirer2.prompt([
934
+ {
935
+ type: "confirm",
936
+ name: "confirmed",
937
+ message,
938
+ default: false
939
+ }
940
+ ]);
941
+ return confirmed;
885
942
  }
886
943
 
887
944
  // src/commands/media.ts
945
+ var MIME_TYPES = {
946
+ ".jpg": "image/jpeg",
947
+ ".jpeg": "image/jpeg",
948
+ ".png": "image/png",
949
+ ".gif": "image/gif",
950
+ ".webp": "image/webp",
951
+ ".avif": "image/avif",
952
+ ".svg": "image/svg+xml",
953
+ ".mp4": "video/mp4",
954
+ ".webm": "video/webm",
955
+ ".mov": "video/quicktime",
956
+ ".mp3": "audio/mpeg",
957
+ ".wav": "audio/wav",
958
+ ".pdf": "application/pdf",
959
+ ".json": "application/json",
960
+ ".csv": "text/csv",
961
+ ".txt": "text/plain"
962
+ };
963
+ function guessMimeType(filename) {
964
+ const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
965
+ return MIME_TYPES[ext] ?? "application/octet-stream";
966
+ }
967
+ function getStorageUrl() {
968
+ return process.env.FOIR_STORAGE_URL ?? "https://storage.foir.dev";
969
+ }
970
+ function createClient(getToken) {
971
+ return createStorageClient({
972
+ baseUrl: getStorageUrl(),
973
+ getAuthToken: getToken
974
+ });
975
+ }
888
976
  function registerMediaCommands(program2, globalOpts) {
889
977
  const media = program2.command("media").description("Media file operations");
890
- media.command("upload <filepath>").description("Upload a file").action(
891
- withErrorHandler(globalOpts, async (filepath) => {
892
- const opts = globalOpts();
893
- const { apiUrl, headers } = await getRestAuth(opts);
894
- const fileBuffer = await fs2.readFile(filepath);
895
- const fileName = basename(filepath);
896
- const formData = new FormData();
897
- formData.append("file", new Blob([fileBuffer]), fileName);
898
- const uploadUrl = `${apiUrl.replace(/\/$/, "")}/api/files/upload`;
899
- const response = await fetch(uploadUrl, {
900
- method: "POST",
901
- headers,
902
- body: formData
903
- });
904
- if (!response.ok) {
905
- const errorText = await response.text();
906
- throw new Error(`Upload failed (${response.status}): ${errorText}`);
978
+ media.command("upload <filepath>").description("Upload a file").option("--folder <folder>", "Target folder").action(
979
+ withErrorHandler(
980
+ globalOpts,
981
+ async (filepath, flags) => {
982
+ const opts = globalOpts();
983
+ const { getToken } = await getStorageAuth(opts);
984
+ const storage = createClient(getToken);
985
+ const fileBuffer = await fs2.readFile(filepath);
986
+ const filename = basename(filepath);
987
+ const mimeType = guessMimeType(filename);
988
+ const upload = await storage.createFileUpload({
989
+ filename,
990
+ mimeType,
991
+ size: fileBuffer.byteLength,
992
+ folder: flags.folder
993
+ });
994
+ const uploadResp = await fetch(upload.uploadUrl, {
995
+ method: "PUT",
996
+ headers: { "Content-Type": mimeType },
997
+ body: fileBuffer
998
+ });
999
+ if (!uploadResp.ok) {
1000
+ throw new Error(
1001
+ `Upload to storage failed (${uploadResp.status}): ${await uploadResp.text()}`
1002
+ );
1003
+ }
1004
+ const file = await storage.confirmFileUpload(upload.uploadId);
1005
+ if (opts.json || opts.jsonl) {
1006
+ formatOutput(file, opts);
1007
+ } else {
1008
+ success(`Uploaded ${filename}`);
1009
+ if (file?.url) {
1010
+ console.log(chalk3.bold(` URL: ${file.url}`));
1011
+ }
1012
+ if (file?.storageKey) {
1013
+ console.log(chalk3.gray(` Key: ${file.storageKey}`));
1014
+ }
1015
+ }
907
1016
  }
908
- const result = await response.json();
1017
+ )
1018
+ );
1019
+ media.command("list").description("List files").option("--folder <folder>", "Filter by folder").option("--mime-type <type>", "Filter by MIME type").option("--search <query>", "Search files").option("--include-deleted", "Include soft-deleted files").option("--limit <n>", "Max results", "50").option("--offset <n>", "Offset", "0").action(
1020
+ withErrorHandler(
1021
+ globalOpts,
1022
+ async (flags) => {
1023
+ const opts = globalOpts();
1024
+ const { getToken } = await getStorageAuth(opts);
1025
+ const storage = createClient(getToken);
1026
+ const result = await storage.listFiles({
1027
+ folder: flags.folder,
1028
+ mimeType: flags["mime-type"] ?? flags.mimeType,
1029
+ search: flags.search,
1030
+ includeDeleted: !!flags.includeDeleted,
1031
+ limit: Number(flags.limit) || 50,
1032
+ offset: Number(flags.offset) || 0
1033
+ });
1034
+ formatList(result.items, opts, {
1035
+ columns: [
1036
+ { key: "id", header: "ID", width: 28 },
1037
+ { key: "filename", header: "Filename", width: 24 },
1038
+ { key: "mimeType", header: "Type", width: 16 },
1039
+ {
1040
+ key: "size",
1041
+ header: "Size",
1042
+ width: 10,
1043
+ format: (v) => {
1044
+ const num2 = Number(v);
1045
+ if (num2 < 1024) return `${num2} B`;
1046
+ if (num2 < 1024 * 1024) return `${(num2 / 1024).toFixed(1)} KB`;
1047
+ return `${(num2 / (1024 * 1024)).toFixed(1)} MB`;
1048
+ }
1049
+ },
1050
+ { key: "folder", header: "Folder", width: 16 },
1051
+ {
1052
+ key: "createdAt",
1053
+ header: "Created",
1054
+ width: 12,
1055
+ format: (v) => timeAgo(v)
1056
+ }
1057
+ ],
1058
+ total: result.total
1059
+ });
1060
+ }
1061
+ )
1062
+ );
1063
+ media.command("get <id>").description("Get file details").action(
1064
+ withErrorHandler(globalOpts, async (id) => {
1065
+ const opts = globalOpts();
1066
+ const { getToken } = await getStorageAuth(opts);
1067
+ const storage = createClient(getToken);
1068
+ const file = await storage.getFile(id);
1069
+ formatOutput(file, opts);
1070
+ })
1071
+ );
1072
+ media.command("usage").description("Get storage usage").action(
1073
+ withErrorHandler(globalOpts, async () => {
1074
+ const opts = globalOpts();
1075
+ const { getToken } = await getStorageAuth(opts);
1076
+ const storage = createClient(getToken);
1077
+ const usage = await storage.getStorageUsage();
909
1078
  if (opts.json || opts.jsonl) {
910
- formatOutput(result, opts);
1079
+ formatOutput(usage, opts);
911
1080
  } else {
912
- success(`Uploaded ${fileName}`);
913
- if (result.url) {
914
- console.log(chalk3.bold(` URL: ${result.url}`));
1081
+ const mb = (Number(usage?.totalBytes ?? 0) / (1024 * 1024)).toFixed(
1082
+ 1
1083
+ );
1084
+ console.log(chalk3.bold(`Files: ${usage?.totalFiles ?? 0}`));
1085
+ console.log(chalk3.bold(`Storage: ${mb} MB`));
1086
+ }
1087
+ })
1088
+ );
1089
+ media.command("update <id>").description("Update a file").option("--filename <name>", "New filename").option("--folder <folder>", "Move to folder").option("--tags <tags>", "Comma-separated tags").action(
1090
+ withErrorHandler(
1091
+ globalOpts,
1092
+ async (id, flags) => {
1093
+ const opts = globalOpts();
1094
+ const { getToken } = await getStorageAuth(opts);
1095
+ const storage = createClient(getToken);
1096
+ const file = await storage.updateFile({
1097
+ id,
1098
+ filename: flags.filename,
1099
+ folder: flags.folder,
1100
+ tags: flags.tags?.split(",").map((t) => t.trim())
1101
+ });
1102
+ if (opts.json || opts.jsonl) {
1103
+ formatOutput(file, opts);
1104
+ } else {
1105
+ success("Updated file");
1106
+ }
1107
+ }
1108
+ )
1109
+ );
1110
+ media.command("update-metadata <id>").description("Update file metadata (alt text, caption, description)").option("--alt-text <text>", "Alt text").option("--caption <text>", "Caption").option("--description <text>", "Description").action(
1111
+ withErrorHandler(
1112
+ globalOpts,
1113
+ async (id, flags) => {
1114
+ const opts = globalOpts();
1115
+ const { getToken } = await getStorageAuth(opts);
1116
+ const storage = createClient(getToken);
1117
+ const file = await storage.updateFileMetadata({
1118
+ id,
1119
+ altText: flags.altText ?? flags["alt-text"],
1120
+ caption: flags.caption,
1121
+ description: flags.description
1122
+ });
1123
+ if (opts.json || opts.jsonl) {
1124
+ formatOutput(file, opts);
1125
+ } else {
1126
+ success("Updated file metadata");
915
1127
  }
916
- if (result.storageKey) {
917
- console.log(chalk3.gray(` Key: ${result.storageKey}`));
1128
+ }
1129
+ )
1130
+ );
1131
+ media.command("delete <id>").description("Delete a file").option("--confirm", "Skip confirmation prompt").option("--permanent", "Permanently delete (cannot be restored)").action(
1132
+ withErrorHandler(
1133
+ globalOpts,
1134
+ async (id, flags) => {
1135
+ const opts = globalOpts();
1136
+ const confirmed = await confirmAction("Delete this file?", {
1137
+ confirm: !!flags.confirm
1138
+ });
1139
+ if (!confirmed) {
1140
+ console.log("Aborted.");
1141
+ return;
1142
+ }
1143
+ const { getToken } = await getStorageAuth(opts);
1144
+ const storage = createClient(getToken);
1145
+ if (flags.permanent) {
1146
+ await storage.permanentlyDeleteFile(id);
1147
+ success("Permanently deleted file");
1148
+ } else {
1149
+ await storage.deleteFile(id);
1150
+ success("Deleted file");
918
1151
  }
919
1152
  }
1153
+ )
1154
+ );
1155
+ media.command("restore <id>").description("Restore a deleted file").action(
1156
+ withErrorHandler(globalOpts, async (id) => {
1157
+ const opts = globalOpts();
1158
+ const { getToken } = await getStorageAuth(opts);
1159
+ const storage = createClient(getToken);
1160
+ const file = await storage.restoreFile(id);
1161
+ if (opts.json || opts.jsonl) {
1162
+ formatOutput(file, opts);
1163
+ } else {
1164
+ success("Restored file");
1165
+ }
920
1166
  })
921
1167
  );
922
1168
  }
923
1169
 
924
1170
  // src/commands/create-config.ts
925
1171
  import chalk4 from "chalk";
926
- import inquirer2 from "inquirer";
1172
+ import inquirer3 from "inquirer";
927
1173
 
928
1174
  // src/scaffold/scaffold.ts
929
1175
  import * as fs4 from "fs";
@@ -1347,7 +1593,7 @@ function getApiTsconfig() {
1347
1593
  return JSON.stringify(config2, null, 2) + "\n";
1348
1594
  }
1349
1595
  function getApiEnvExample(apiUrl) {
1350
- const baseUrl = apiUrl.replace(/\/graphql$/, "");
1596
+ const baseUrl = apiUrl.replace(/\/$/, "");
1351
1597
  return `# Platform API
1352
1598
  PLATFORM_BASE_URL=${baseUrl}
1353
1599
  PLATFORM_API_KEY=sk_your_api_key_here
@@ -1425,11 +1671,7 @@ function isValidConfigType(value) {
1425
1671
  return CONFIG_TYPES.includes(value);
1426
1672
  }
1427
1673
  function registerCreateConfigCommand(program2, globalOpts) {
1428
- program2.command("create-config [name]").description("Scaffold a new Foir config").option("--type <type>", "Config type: custom-editor, workflow, widget").option(
1429
- "--api-url <url>",
1430
- "Platform API URL",
1431
- "http://localhost:4000/graphql"
1432
- ).action(
1674
+ program2.command("create-config [name]").description("Scaffold a new Foir config").option("--type <type>", "Config type: custom-editor, workflow, widget").option("--api-url <url>", "Platform API URL", "http://localhost:4011").action(
1433
1675
  withErrorHandler(
1434
1676
  globalOpts,
1435
1677
  async (name, cmdOpts) => {
@@ -1439,7 +1681,7 @@ function registerCreateConfigCommand(program2, globalOpts) {
1439
1681
  console.log();
1440
1682
  let configName = name;
1441
1683
  if (!configName) {
1442
- const { inputName } = await inquirer2.prompt([
1684
+ const { inputName } = await inquirer3.prompt([
1443
1685
  {
1444
1686
  type: "input",
1445
1687
  name: "inputName",
@@ -1453,7 +1695,7 @@ function registerCreateConfigCommand(program2, globalOpts) {
1453
1695
  if (cmdOpts?.type && isValidConfigType(cmdOpts.type)) {
1454
1696
  configType = cmdOpts.type;
1455
1697
  } else {
1456
- const { selectedType } = await inquirer2.prompt([
1698
+ const { selectedType } = await inquirer3.prompt([
1457
1699
  {
1458
1700
  type: "list",
1459
1701
  name: "selectedType",
@@ -1464,7 +1706,7 @@ function registerCreateConfigCommand(program2, globalOpts) {
1464
1706
  ]);
1465
1707
  configType = selectedType;
1466
1708
  }
1467
- const apiUrl = cmdOpts?.apiUrl ?? "http://localhost:4000/graphql";
1709
+ const apiUrl = cmdOpts?.apiUrl ?? "http://localhost:4011";
1468
1710
  console.log();
1469
1711
  console.log(
1470
1712
  ` Scaffolding ${chalk4.cyan(`"${configName}"`)} (${configType})...`
@@ -1476,69 +1718,53 @@ function registerCreateConfigCommand(program2, globalOpts) {
1476
1718
  );
1477
1719
  }
1478
1720
 
1479
- // src/graphql/generated.ts
1480
- var GlobalSearchDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "GlobalSearch" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "query" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "modelKeys" } }, "type": { "kind": "ListType", "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "includeMedia" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Boolean" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "globalSearch" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "query" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "query" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "limit" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "modelKeys" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "modelKeys" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "includeMedia" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "includeMedia" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "records" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "modelKey" } }, { "kind": "Field", "name": { "kind": "Name", "value": "title" } }, { "kind": "Field", "name": { "kind": "Name", "value": "naturalKey" } }, { "kind": "Field", "name": { "kind": "Name", "value": "subtitle" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updatedAt" } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "media" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "fileName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "altText" } }, { "kind": "Field", "name": { "kind": "Name", "value": "fileUrl" } }] } }] } }] } }] };
1481
-
1482
1721
  // src/commands/search.ts
1483
1722
  function registerSearchCommands(program2, globalOpts) {
1484
- program2.command("search <query>").description("Search across all records and media").option(
1723
+ program2.command("search <query>").description("Search across all records").option(
1485
1724
  "--models <keys>",
1486
1725
  "Filter to specific model keys (comma-separated)"
1487
- ).option("--limit <n>", "Max results", "20").option("--no-media", "Exclude media results").action(
1726
+ ).option("--limit <n>", "Max results", "20").action(
1488
1727
  withErrorHandler(
1489
1728
  globalOpts,
1490
1729
  async (query, cmdOpts) => {
1491
1730
  const opts = globalOpts();
1492
- const client = await createClient(opts);
1731
+ const client = await createPlatformClient(opts);
1493
1732
  const modelKeys = typeof cmdOpts.models === "string" ? cmdOpts.models.split(",").map((k) => k.trim()) : void 0;
1494
- const data = await client.request(GlobalSearchDocument, {
1733
+ const result = await client.records.globalSearch({
1495
1734
  query,
1496
1735
  limit: parseInt(String(cmdOpts.limit ?? "20"), 10),
1497
- modelKeys,
1498
- includeMedia: cmdOpts.media !== false
1736
+ modelKeys
1499
1737
  });
1500
1738
  if (opts.json || opts.jsonl) {
1501
- formatOutput(data.globalSearch, opts);
1739
+ formatOutput(result, opts);
1502
1740
  return;
1503
1741
  }
1504
- if (data.globalSearch.records.length > 0) {
1742
+ if (result.items.length > 0) {
1505
1743
  console.log(`
1506
- Records (${data.globalSearch.records.length}):`);
1744
+ Records (${result.items.length}):`);
1507
1745
  formatList(
1508
- data.globalSearch.records,
1746
+ result.items.map((item) => ({
1747
+ id: item.id,
1748
+ modelKey: item.modelKey,
1749
+ naturalKey: item.naturalKey ?? "",
1750
+ score: item.score
1751
+ })),
1509
1752
  opts,
1510
1753
  {
1511
1754
  columns: [
1512
1755
  { key: "id", header: "ID", width: 28 },
1513
1756
  { key: "modelKey", header: "Model", width: 18 },
1514
- { key: "title", header: "Title", width: 28 },
1515
1757
  { key: "naturalKey", header: "Key", width: 20 },
1516
1758
  {
1517
- key: "updatedAt",
1518
- header: "Updated",
1519
- width: 12,
1520
- format: (v) => timeAgo(v)
1759
+ key: "score",
1760
+ header: "Score",
1761
+ width: 8,
1762
+ format: (v) => Number(v).toFixed(2)
1521
1763
  }
1522
1764
  ]
1523
1765
  }
1524
1766
  );
1525
- }
1526
- if (data.globalSearch.media.length > 0) {
1527
- console.log(`
1528
- Media (${data.globalSearch.media.length}):`);
1529
- formatList(
1530
- data.globalSearch.media,
1531
- opts,
1532
- {
1533
- columns: [
1534
- { key: "id", header: "ID", width: 28 },
1535
- { key: "fileName", header: "File", width: 30 },
1536
- { key: "altText", header: "Alt", width: 24 }
1537
- ]
1538
- }
1539
- );
1540
- }
1541
- if (data.globalSearch.records.length === 0 && data.globalSearch.media.length === 0) {
1767
+ } else {
1542
1768
  console.log("No results found.");
1543
1769
  }
1544
1770
  }
@@ -1549,9 +1775,9 @@ Media (${data.globalSearch.media.length}):`);
1549
1775
  // src/commands/init.ts
1550
1776
  import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
1551
1777
  import { writeFile } from "fs/promises";
1552
- import { resolve as resolve2, join as join4 } from "path";
1778
+ import { resolve as resolve3, join as join4 } from "path";
1553
1779
  import chalk5 from "chalk";
1554
- import inquirer3 from "inquirer";
1780
+ import inquirer4 from "inquirer";
1555
1781
  var FIELD_DEFAULTS = {
1556
1782
  text: "",
1557
1783
  richtext: "",
@@ -1613,9 +1839,9 @@ function generateRecordSeed(model) {
1613
1839
  )) {
1614
1840
  continue;
1615
1841
  }
1616
- if (field.type === "list" && field.items) {
1842
+ if (field.type === "list" && field.itemType) {
1617
1843
  data[field.key] = [
1618
- defaultValueForField({ key: field.key, type: field.items.type })
1844
+ defaultValueForField({ key: field.key, type: field.itemType })
1619
1845
  ];
1620
1846
  } else {
1621
1847
  data[field.key] = defaultValueForField(field);
@@ -1640,7 +1866,7 @@ function registerInitCommands(program2, globalOpts) {
1640
1866
  async (key, opts) => {
1641
1867
  const globalFlags = globalOpts();
1642
1868
  const template = generateModelTemplate(key);
1643
- const outDir = resolve2(opts.output);
1869
+ const outDir = resolve3(opts.output);
1644
1870
  if (!existsSync3(outDir)) {
1645
1871
  mkdirSync2(outDir, { recursive: true });
1646
1872
  }
@@ -1671,10 +1897,19 @@ Edit the file, then run:
1671
1897
  globalOpts,
1672
1898
  async (opts) => {
1673
1899
  const globalFlags = globalOpts();
1674
- const client = await createClient(globalFlags);
1675
- const query = `query { models(limit: 100) { items { key name fields } total } }`;
1676
- const result = await client.request(query);
1677
- const models = result.models.items;
1900
+ const client = await createPlatformClient(globalFlags);
1901
+ const result = await client.models.listModels({ limit: 100 });
1902
+ const models = result.items.map((m) => ({
1903
+ key: m.key,
1904
+ name: m.name,
1905
+ fields: (m.fields ?? []).map((f) => ({
1906
+ key: f.key,
1907
+ type: f.type,
1908
+ label: f.label,
1909
+ required: f.required,
1910
+ itemType: f.itemType
1911
+ }))
1912
+ }));
1678
1913
  if (models.length === 0) {
1679
1914
  console.log(
1680
1915
  chalk5.yellow(
@@ -1699,7 +1934,7 @@ Edit the file, then run:
1699
1934
  selectedModels.push(found);
1700
1935
  }
1701
1936
  } else {
1702
- const { selected } = await inquirer3.prompt([
1937
+ const { selected } = await inquirer4.prompt([
1703
1938
  {
1704
1939
  type: "checkbox",
1705
1940
  name: "selected",
@@ -1718,7 +1953,7 @@ Edit the file, then run:
1718
1953
  console.log("No models selected.");
1719
1954
  return;
1720
1955
  }
1721
- const outDir = resolve2(opts.output);
1956
+ const outDir = resolve3(opts.output);
1722
1957
  if (!existsSync3(outDir)) {
1723
1958
  mkdirSync2(outDir, { recursive: true });
1724
1959
  }
@@ -1754,64 +1989,12 @@ Edit the files, then run:
1754
1989
  import chalk6 from "chalk";
1755
1990
  import { existsSync as existsSync4 } from "fs";
1756
1991
  import { resolve as resolve4 } from "path";
1757
-
1758
- // src/lib/config-loader.ts
1759
- import { readFile } from "fs/promises";
1760
- import { pathToFileURL } from "url";
1761
- import { resolve as resolve3 } from "path";
1762
- async function loadConfig(filePath) {
1763
- const absPath = resolve3(filePath);
1764
- if (filePath.endsWith(".ts")) {
1765
- const configModule = await import(pathToFileURL(absPath).href);
1766
- return configModule.default;
1767
- }
1768
- if (filePath.endsWith(".js") || filePath.endsWith(".mjs")) {
1769
- const configModule = await import(pathToFileURL(absPath).href);
1770
- return configModule.default;
1771
- }
1772
- if (filePath.endsWith(".json")) {
1773
- const content = await readFile(absPath, "utf-8");
1774
- return JSON.parse(content);
1775
- }
1776
- throw new Error(
1777
- `Unsupported file extension for "${filePath}". Supported: .ts, .js, .mjs, .json`
1778
- );
1779
- }
1780
-
1781
- // src/commands/push.ts
1782
1992
  var CONFIG_FILE_NAMES = [
1783
1993
  "foir.config.ts",
1784
1994
  "foir.config.js",
1785
1995
  "foir.config.mjs",
1786
1996
  "foir.config.json"
1787
1997
  ];
1788
- var APPLY_CONFIG_MUTATION = (
1789
- /* GraphQL */
1790
- `
1791
- mutation ApplyConfig($input: ApplyConfigInput!) {
1792
- applyConfig(input: $input) {
1793
- configId
1794
- configKey
1795
- credentials {
1796
- platformApiKey
1797
- platformEditorKey
1798
- webhookSecret
1799
- }
1800
- modelsCreated
1801
- modelsUpdated
1802
- operationsCreated
1803
- operationsUpdated
1804
- segmentsCreated
1805
- segmentsUpdated
1806
- schedulesCreated
1807
- schedulesUpdated
1808
- hooksCreated
1809
- hooksUpdated
1810
- isUpdate
1811
- }
1812
- }
1813
- `
1814
- );
1815
1998
  function discoverConfigFile() {
1816
1999
  for (const name of CONFIG_FILE_NAMES) {
1817
2000
  const path3 = resolve4(process.cwd(), name);
@@ -1843,62 +2026,25 @@ function registerPushCommand(program2, globalOpts) {
1843
2026
  if (opts.force) {
1844
2027
  config2.force = true;
1845
2028
  }
1846
- const client = await createClient(globalOpts());
2029
+ const client = await createPlatformClient(globalOpts());
1847
2030
  console.log(
1848
2031
  chalk6.dim(`Pushing config "${config2.key}" to platform...`)
1849
2032
  );
1850
- const data = await client.request(
1851
- APPLY_CONFIG_MUTATION,
1852
- { input: config2 }
2033
+ const result = await client.configs.applyConfig(
2034
+ config2.key,
2035
+ config2
1853
2036
  );
1854
- const result = data.applyConfig;
1855
- console.log();
1856
- if (result.isUpdate) {
1857
- console.log(chalk6.green("Config updated successfully."));
1858
- } else {
1859
- console.log(chalk6.green("Config applied successfully."));
2037
+ if (!result) {
2038
+ throw new Error(
2039
+ "Failed to apply config \u2014 no response from server."
2040
+ );
1860
2041
  }
1861
2042
  console.log();
1862
- console.log(` Config ID: ${chalk6.cyan(result.configId)}`);
1863
- console.log(` Config Key: ${chalk6.cyan(result.configKey)}`);
2043
+ console.log(chalk6.green("Config applied successfully."));
2044
+ console.log();
2045
+ console.log(` Config ID: ${chalk6.cyan(result.id)}`);
2046
+ console.log(` Config Key: ${chalk6.cyan(result.key)}`);
1864
2047
  console.log();
1865
- const stats = [
1866
- ["Models", result.modelsCreated, result.modelsUpdated],
1867
- ["Operations", result.operationsCreated, result.operationsUpdated],
1868
- ["Segments", result.segmentsCreated, result.segmentsUpdated],
1869
- ["Schedules", result.schedulesCreated, result.schedulesUpdated],
1870
- ["Hooks", result.hooksCreated, result.hooksUpdated]
1871
- ].filter(([, c, u]) => c > 0 || u > 0);
1872
- if (stats.length > 0) {
1873
- for (const [label, created, updated] of stats) {
1874
- const parts = [];
1875
- if (created > 0)
1876
- parts.push(chalk6.green(`${created} created`));
1877
- if (updated > 0)
1878
- parts.push(chalk6.yellow(`${updated} updated`));
1879
- console.log(` ${label}: ${parts.join(", ")}`);
1880
- }
1881
- console.log();
1882
- }
1883
- if (result.credentials) {
1884
- console.log(chalk6.bold.yellow("Credentials (save these now):"));
1885
- console.log();
1886
- console.log(
1887
- ` PLATFORM_API_KEY: ${chalk6.cyan(result.credentials.platformApiKey)}`
1888
- );
1889
- console.log(
1890
- ` PLATFORM_EDITOR_KEY: ${chalk6.cyan(result.credentials.platformEditorKey)}`
1891
- );
1892
- console.log(
1893
- ` WEBHOOK_SECRET: ${chalk6.cyan(result.credentials.webhookSecret)}`
1894
- );
1895
- console.log();
1896
- console.log(
1897
- chalk6.dim(
1898
- "These credentials are only shown once. Store them securely."
1899
- )
1900
- );
1901
- }
1902
2048
  }
1903
2049
  )
1904
2050
  );
@@ -1906,106 +2052,42 @@ function registerPushCommand(program2, globalOpts) {
1906
2052
 
1907
2053
  // src/commands/remove.ts
1908
2054
  import chalk7 from "chalk";
1909
- import inquirer4 from "inquirer";
1910
- var GET_CONFIG_QUERY = (
1911
- /* GraphQL */
1912
- `
1913
- query GetConfigByKey($key: String!) {
1914
- configByKey(key: $key) {
1915
- id
1916
- key
1917
- name
1918
- configType
1919
- }
1920
- }
1921
- `
1922
- );
1923
- var UNREGISTER_MUTATION = (
1924
- /* GraphQL */
1925
- `
1926
- mutation UnregisterConfig($id: ID!) {
1927
- unregisterConfig(id: $id)
1928
- }
1929
- `
1930
- );
2055
+ import inquirer5 from "inquirer";
1931
2056
  function registerRemoveCommand(program2, globalOpts) {
1932
2057
  program2.command("remove <key>").description("Remove a config and all its provisioned resources").option("--force", "Skip confirmation prompt", false).action(
1933
2058
  withErrorHandler(
1934
2059
  globalOpts,
1935
2060
  async (key, opts) => {
1936
- const client = await createClient(globalOpts());
1937
- const { configByKey: config2 } = await client.request(GET_CONFIG_QUERY, { key });
2061
+ const client = await createPlatformClient(globalOpts());
2062
+ const config2 = await client.configs.getConfigByKey(key);
1938
2063
  if (!config2) {
1939
2064
  throw new Error(`Config not found: ${key}`);
1940
2065
  }
1941
2066
  if (!opts.force) {
1942
- const { confirmed } = await inquirer4.prompt([
2067
+ const { confirmed } = await inquirer5.prompt([
1943
2068
  {
1944
2069
  type: "confirm",
1945
2070
  name: "confirmed",
1946
2071
  message: `Remove config "${config2.name}" (${config2.key})? This will delete all its models, operations, hooks, and schedules.`,
1947
2072
  default: false
1948
- }
1949
- ]);
1950
- if (!confirmed) {
1951
- console.log(chalk7.dim("Cancelled."));
1952
- return;
1953
- }
1954
- }
1955
- await client.request(UNREGISTER_MUTATION, { id: config2.id });
1956
- console.log(
1957
- chalk7.green(`Removed config "${config2.name}" (${config2.key}).`)
1958
- );
1959
- }
1960
- )
1961
- );
1962
- }
1963
-
1964
- // src/commands/profiles.ts
1965
- import chalk8 from "chalk";
1966
-
1967
- // src/lib/input.ts
1968
- import inquirer5 from "inquirer";
1969
- async function parseInputData(opts) {
1970
- if (opts.data) {
1971
- return JSON.parse(opts.data);
1972
- }
1973
- if (opts.file) {
1974
- return await loadConfig(opts.file);
1975
- }
1976
- if (!process.stdin.isTTY) {
1977
- const chunks = [];
1978
- for await (const chunk of process.stdin) {
1979
- chunks.push(chunk);
1980
- }
1981
- const stdinContent = Buffer.concat(chunks).toString("utf-8").trim();
1982
- if (stdinContent) {
1983
- return JSON.parse(stdinContent);
1984
- }
1985
- }
1986
- throw new Error(
1987
- "No input data provided. Use --data, --file, or pipe via stdin."
1988
- );
1989
- }
1990
- function isUUID(value) {
1991
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
1992
- value
1993
- ) || /^c[a-z0-9]{24,}$/.test(value);
1994
- }
1995
- async function confirmAction(message, opts) {
1996
- if (opts?.confirm) return true;
1997
- const { confirmed } = await inquirer5.prompt([
1998
- {
1999
- type: "confirm",
2000
- name: "confirmed",
2001
- message,
2002
- default: false
2003
- }
2004
- ]);
2005
- return confirmed;
2073
+ }
2074
+ ]);
2075
+ if (!confirmed) {
2076
+ console.log(chalk7.dim("Cancelled."));
2077
+ return;
2078
+ }
2079
+ }
2080
+ await client.configs.deleteConfig(config2.id);
2081
+ console.log(
2082
+ chalk7.green(`Removed config "${config2.name}" (${config2.key}).`)
2083
+ );
2084
+ }
2085
+ )
2086
+ );
2006
2087
  }
2007
2088
 
2008
2089
  // src/commands/profiles.ts
2090
+ import chalk8 from "chalk";
2009
2091
  function registerProfilesCommand(program2, globalOpts) {
2010
2092
  const profiles = program2.command("profiles").description("Manage named project profiles");
2011
2093
  profiles.command("list").description("List all saved project profiles").action(
@@ -2173,12 +2255,11 @@ function registerProfilesCommand(program2, globalOpts) {
2173
2255
  }
2174
2256
 
2175
2257
  // src/commands/register-commands.ts
2176
- import { readFileSync, readdirSync } from "fs";
2177
- import { resolve as resolve5, dirname as dirname4 } from "path";
2178
- import { fileURLToPath } from "url";
2258
+ import { readdirSync } from "fs";
2259
+ import { resolve as resolve5 } from "path";
2179
2260
  import chalk9 from "chalk";
2180
2261
 
2181
- // ../command-registry/src/command-map.ts
2262
+ // src/command-registry/command-map.ts
2182
2263
  var COMMANDS = [
2183
2264
  // =========================================================================
2184
2265
  // MODELS
@@ -2353,6 +2434,15 @@ var COMMANDS = [
2353
2434
  scalarResult: true,
2354
2435
  successMessage: "Unpublished record"
2355
2436
  },
2437
+ {
2438
+ group: "records",
2439
+ name: "cancel-schedule",
2440
+ description: "Cancel a scheduled publish or unpublish",
2441
+ operation: "cancelScheduledRecordPublish",
2442
+ operationType: "mutation",
2443
+ positionalArgs: [{ name: "versionId", graphqlArg: "versionId", description: "Version ID" }],
2444
+ successMessage: "Cancelled scheduled publish for version {versionId}"
2445
+ },
2356
2446
  {
2357
2447
  group: "records",
2358
2448
  name: "duplicate",
@@ -2409,6 +2499,104 @@ var COMMANDS = [
2409
2499
  successMessage: "Created variant"
2410
2500
  },
2411
2501
  // =========================================================================
2502
+ // ROLLOUTS
2503
+ // =========================================================================
2504
+ {
2505
+ group: "rollouts",
2506
+ name: "list",
2507
+ description: "List rollouts",
2508
+ operation: "listPublishBatches",
2509
+ operationType: "query",
2510
+ columns: [
2511
+ { key: "id", header: "ID", width: 28 },
2512
+ { key: "name", header: "Name", width: 24 },
2513
+ { key: "status", header: "Status", width: 16 },
2514
+ { key: "itemCount", header: "Items", width: 6 },
2515
+ { key: "completedCount", header: "Done", width: 6 },
2516
+ { key: "scheduledAt", header: "Scheduled", width: 12, format: "timeAgo" },
2517
+ { key: "createdAt", header: "Created", width: 12, format: "timeAgo" }
2518
+ ]
2519
+ },
2520
+ {
2521
+ group: "rollouts",
2522
+ name: "get",
2523
+ description: "Get a rollout",
2524
+ operation: "getPublishBatch",
2525
+ operationType: "query",
2526
+ positionalArgs: [{ name: "id", graphqlArg: "id", description: "Rollout ID" }]
2527
+ },
2528
+ {
2529
+ group: "rollouts",
2530
+ name: "create",
2531
+ description: "Create a rollout",
2532
+ operation: "createPublishBatch",
2533
+ operationType: "mutation",
2534
+ acceptsInput: true,
2535
+ successMessage: "Created rollout {name}"
2536
+ },
2537
+ {
2538
+ group: "rollouts",
2539
+ name: "update",
2540
+ description: "Update a rollout",
2541
+ operation: "updatePublishBatch",
2542
+ operationType: "mutation",
2543
+ positionalArgs: [{ name: "id", graphqlArg: "id", description: "Rollout ID" }],
2544
+ acceptsInput: true,
2545
+ successMessage: "Updated rollout"
2546
+ },
2547
+ {
2548
+ group: "rollouts",
2549
+ name: "cancel",
2550
+ description: "Cancel a rollout",
2551
+ operation: "cancelPublishBatch",
2552
+ operationType: "mutation",
2553
+ positionalArgs: [{ name: "id", graphqlArg: "id", description: "Rollout ID" }],
2554
+ requiresConfirmation: true,
2555
+ scalarResult: true,
2556
+ successMessage: "Cancelled rollout"
2557
+ },
2558
+ {
2559
+ group: "rollouts",
2560
+ name: "rollback",
2561
+ description: "Rollback a completed rollout",
2562
+ operation: "rollbackPublishBatch",
2563
+ operationType: "mutation",
2564
+ positionalArgs: [{ name: "id", graphqlArg: "id", description: "Rollout ID" }],
2565
+ requiresConfirmation: true,
2566
+ scalarResult: true,
2567
+ successMessage: "Rolled back rollout"
2568
+ },
2569
+ {
2570
+ group: "rollouts",
2571
+ name: "retry",
2572
+ description: "Retry failed items in a rollout",
2573
+ operation: "retryFailedBatchItems",
2574
+ operationType: "mutation",
2575
+ positionalArgs: [{ name: "id", graphqlArg: "id", description: "Rollout ID" }],
2576
+ scalarResult: true,
2577
+ successMessage: "Retrying failed items in rollout"
2578
+ },
2579
+ {
2580
+ group: "rollouts",
2581
+ name: "add-items",
2582
+ description: "Add version IDs to a rollout",
2583
+ operation: "addItemsToPublishBatch",
2584
+ operationType: "mutation",
2585
+ positionalArgs: [{ name: "id", graphqlArg: "id", description: "Rollout ID" }],
2586
+ acceptsInput: true,
2587
+ successMessage: "Added items to rollout"
2588
+ },
2589
+ {
2590
+ group: "rollouts",
2591
+ name: "remove-items",
2592
+ description: "Remove version IDs from a rollout",
2593
+ operation: "removeItemsFromPublishBatch",
2594
+ operationType: "mutation",
2595
+ positionalArgs: [{ name: "id", graphqlArg: "id", description: "Rollout ID" }],
2596
+ acceptsInput: true,
2597
+ successMessage: "Removed items from rollout"
2598
+ },
2599
+ // =========================================================================
2412
2600
  // LOCALES
2413
2601
  // =========================================================================
2414
2602
  {
@@ -2953,68 +3141,6 @@ var COMMANDS = [
2953
3141
  successMessage: "Updated customer profile"
2954
3142
  },
2955
3143
  // =========================================================================
2956
- // FILES
2957
- // =========================================================================
2958
- {
2959
- group: "files",
2960
- name: "list",
2961
- description: "List files",
2962
- operation: "files",
2963
- operationType: "query",
2964
- columns: [
2965
- { key: "id", header: "ID", width: 28 },
2966
- { key: "filename", header: "Filename", width: 24 },
2967
- { key: "mimeType", header: "Type", width: 16 },
2968
- { key: "size", header: "Size", width: 10, format: "bytes" },
2969
- { key: "folder", header: "Folder", width: 16 },
2970
- { key: "createdAt", header: "Created", width: 12, format: "timeAgo" }
2971
- ]
2972
- },
2973
- {
2974
- group: "files",
2975
- name: "get",
2976
- description: "Get a file",
2977
- operation: "file",
2978
- operationType: "query",
2979
- positionalArgs: [{ name: "id", graphqlArg: "id" }]
2980
- },
2981
- {
2982
- group: "files",
2983
- name: "usage",
2984
- description: "Get file storage usage",
2985
- operation: "fileStorageUsage",
2986
- operationType: "query"
2987
- },
2988
- {
2989
- group: "files",
2990
- name: "update",
2991
- description: "Update a file",
2992
- operation: "updateFile",
2993
- operationType: "mutation",
2994
- positionalArgs: [{ name: "id", graphqlArg: "id" }],
2995
- successMessage: "Updated file"
2996
- },
2997
- {
2998
- group: "files",
2999
- name: "update-metadata",
3000
- description: "Update file metadata",
3001
- operation: "updateFileMetadata",
3002
- operationType: "mutation",
3003
- positionalArgs: [{ name: "id", graphqlArg: "id" }],
3004
- successMessage: "Updated file metadata"
3005
- },
3006
- {
3007
- group: "files",
3008
- name: "delete",
3009
- description: "Delete a file",
3010
- operation: "deleteFile",
3011
- operationType: "mutation",
3012
- positionalArgs: [{ name: "id", graphqlArg: "id" }],
3013
- requiresConfirmation: true,
3014
- scalarResult: true,
3015
- successMessage: "Deleted file"
3016
- },
3017
- // =========================================================================
3018
3144
  // OPERATIONS
3019
3145
  // =========================================================================
3020
3146
  {
@@ -3179,6 +3305,7 @@ var COMMANDS = [
3179
3305
  description: "List hook deliveries",
3180
3306
  operation: "hookDeliveries",
3181
3307
  operationType: "query",
3308
+ positionalArgs: [{ name: "hookId", graphqlArg: "hookId", description: "Hook ID" }],
3182
3309
  columns: [
3183
3310
  { key: "id", header: "ID", width: 28 },
3184
3311
  { key: "event", header: "Event", width: 16 },
@@ -3582,7 +3709,7 @@ var COMMANDS = [
3582
3709
  }
3583
3710
  ];
3584
3711
 
3585
- // ../command-registry/src/schema-engine.ts
3712
+ // src/command-registry/schema-engine.ts
3586
3713
  import {
3587
3714
  buildSchema,
3588
3715
  isObjectType,
@@ -3592,220 +3719,398 @@ import {
3592
3719
  isScalarType,
3593
3720
  isEnumType
3594
3721
  } from "graphql";
3595
- function unwrapType(type) {
3596
- if (isNonNullType(type)) return unwrapType(type.ofType);
3597
- if (isListType(type)) return unwrapType(type.ofType);
3598
- return type;
3599
- }
3600
- function typeToString(type) {
3601
- if (isNonNullType(type)) return `${typeToString(type.ofType)}!`;
3602
- if (isListType(type)) return `[${typeToString(type.ofType)}]`;
3603
- return type.name;
3604
- }
3605
- function detectListWrapper(type) {
3606
- if (!isObjectType(type)) return null;
3607
- const fields = type.getFields();
3608
- const fieldNames = Object.keys(fields);
3609
- let listField = null;
3610
- let totalField = null;
3611
- let itemType = null;
3612
- for (const name of fieldNames) {
3613
- const fieldType = fields[name].type;
3614
- const unwrapped = isNonNullType(fieldType) ? fieldType.ofType : fieldType;
3615
- if (isListType(unwrapped) || isNonNullType(unwrapped) && isListType(unwrapped)) {
3616
- listField = name;
3617
- itemType = unwrapType(fieldType);
3618
- } else {
3619
- const namedType = unwrapType(fieldType);
3620
- if (namedType.name === "Int" && (name === "total" || name === "totalCount")) {
3621
- totalField = name;
3622
- }
3623
- }
3624
- }
3625
- if (listField && totalField && itemType) {
3626
- return { listField, itemType };
3627
- }
3628
- return null;
3629
- }
3630
- function buildSelectionSet(type, isListView, depth = 0) {
3631
- if (!isObjectType(type)) return "";
3632
- const fields = type.getFields();
3633
- const selections = [];
3634
- for (const [name, field] of Object.entries(fields)) {
3635
- const namedType = unwrapType(field.type);
3636
- if (isScalarType(namedType)) {
3637
- if (isListView && namedType.name === "JSON") continue;
3638
- selections.push(name);
3639
- } else if (isEnumType(namedType)) {
3640
- selections.push(name);
3641
- } else if (isObjectType(namedType) && depth === 0 && !isListView) {
3642
- const subSelection = buildSelectionSet(namedType, false, depth + 1);
3643
- if (subSelection) {
3644
- selections.push(`${name} { ${subSelection} }`);
3645
- }
3646
- }
3647
- }
3648
- return selections.join(" ");
3649
- }
3650
- function createSchemaEngine(sdl) {
3651
- const schema = buildSchema(sdl);
3652
- function getField(operationName, operationType) {
3653
- const rootType = operationType === "query" ? schema.getQueryType() : schema.getMutationType();
3654
- if (!rootType) return null;
3655
- const fields = rootType.getFields();
3656
- return fields[operationName] ?? null;
3657
- }
3658
- function getOperationArgs(operationName, operationType) {
3659
- const field = getField(operationName, operationType);
3660
- if (!field) return [];
3661
- return field.args.map((arg) => {
3662
- const namedType = unwrapType(arg.type);
3663
- const rawType = isNonNullType(arg.type) ? arg.type.ofType : arg.type;
3664
- return {
3665
- name: arg.name,
3666
- type: typeToString(arg.type),
3667
- required: isNonNullType(arg.type),
3668
- isList: isListType(rawType) || isNonNullType(rawType) && isListType(rawType.ofType),
3669
- isInput: namedType.name.endsWith("Input")
3670
- };
3671
- });
3672
- }
3673
- function coerceArgs(operationName, operationType, rawArgs) {
3674
- const field = getField(operationName, operationType);
3675
- if (!field) return rawArgs;
3676
- const coerced = {};
3677
- const argMap = new Map(field.args.map((a) => [a.name, a]));
3678
- for (const [key, value] of Object.entries(rawArgs)) {
3679
- const argDef = argMap.get(key);
3680
- if (!argDef) {
3681
- coerced[key] = value;
3682
- continue;
3722
+
3723
+ // src/commands/register-commands.ts
3724
+ import { RecordType } from "@eide/foir-connect-clients/records";
3725
+ function buildDispatchTable() {
3726
+ return {
3727
+ // ── Models ──────────────────────────────────────────────────
3728
+ models: {
3729
+ models: async (_v, c) => wrapList(
3730
+ await c.models.listModels({
3731
+ limit: num(_v.limit, 50),
3732
+ search: str(_v.search),
3733
+ category: str(_v.category),
3734
+ offset: num(_v.offset, 0)
3735
+ })
3736
+ ),
3737
+ modelByKey: async (v, c) => await c.models.getModelByKey(str(v.key)),
3738
+ model: async (v, c) => await c.models.getModel(str(v.id)),
3739
+ createModel: async (v, c) => {
3740
+ const input = v.input;
3741
+ if (!input) throw new Error("Input required (--data or --file)");
3742
+ return await c.models.createModel({
3743
+ key: str(input.key),
3744
+ name: str(input.name),
3745
+ fields: input.fields ?? [],
3746
+ config: input.config,
3747
+ configId: str(input.configId)
3748
+ });
3749
+ },
3750
+ updateModel: async (v, c) => {
3751
+ const input = v.input;
3752
+ if (!input) throw new Error("Input required (--data or --file)");
3753
+ return await c.models.updateModel({
3754
+ id: str(input.id ?? v.id),
3755
+ name: str(input.name),
3756
+ fields: input.fields,
3757
+ config: input.config,
3758
+ changeDescription: str(input.changeDescription)
3759
+ });
3760
+ },
3761
+ deleteModel: async (v, c) => await c.models.deleteModel(str(v.id)),
3762
+ modelVersions: async (v, c) => wrapList(
3763
+ await c.models.listModelVersions(str(v.modelId), {
3764
+ limit: num(v.limit, 50)
3765
+ })
3766
+ )
3767
+ },
3768
+ // ── Records ─────────────────────────────────────────────────
3769
+ records: {
3770
+ records: async (v, c) => wrapList(
3771
+ await c.records.listRecords({
3772
+ modelKey: str(v.modelKey),
3773
+ limit: num(v.limit, 50),
3774
+ offset: num(v.offset, 0),
3775
+ search: str(v.search)
3776
+ })
3777
+ ),
3778
+ record: async (v, c) => await c.records.getRecord(str(v.id)),
3779
+ recordByKey: async (v, c) => await c.records.getRecordByKey(str(v.modelKey), str(v.naturalKey)),
3780
+ createRecord: async (v, c) => {
3781
+ const input = v.input;
3782
+ if (!input) throw new Error("Input required (--data or --file)");
3783
+ return await c.records.createRecord({
3784
+ modelKey: str(input.modelKey),
3785
+ naturalKey: str(input.naturalKey),
3786
+ data: input.data,
3787
+ customerId: str(input.customerId),
3788
+ changeDescription: str(input.changeDescription)
3789
+ });
3790
+ },
3791
+ updateRecord: async (v, c) => {
3792
+ const input = v.input;
3793
+ if (!input) throw new Error("Input required (--data or --file)");
3794
+ return await c.records.updateRecord({
3795
+ id: str(input.id ?? v.id),
3796
+ data: input.data ?? input,
3797
+ naturalKey: str(input.naturalKey)
3798
+ });
3799
+ },
3800
+ deleteRecord: async (v, c) => await c.records.deleteRecord(str(v.id)),
3801
+ publishVersion: async (v, c) => await c.records.publishVersion(str(v.versionId)),
3802
+ unpublishRecord: async (v, c) => await c.records.unpublishRecord(str(v.id)),
3803
+ duplicateRecord: async (v, c) => {
3804
+ const input = v.input;
3805
+ return await c.records.duplicateRecord(
3806
+ str(input?.id ?? v.id),
3807
+ str(input?.newNaturalKey)
3808
+ );
3809
+ },
3810
+ recordVersions: async (v, c) => wrapList(
3811
+ await c.records.listRecordVersions(str(v.parentId), {
3812
+ limit: num(v.limit, 50)
3813
+ })
3814
+ ),
3815
+ recordVariants: async (v, c) => wrapList(
3816
+ await c.records.listRecordVariants(str(v.recordId), {
3817
+ limit: num(v.limit, 50)
3818
+ })
3819
+ ),
3820
+ createVersion: async (v, c) => {
3821
+ const input = v.input;
3822
+ if (!input) throw new Error("Input required (--data or --file)");
3823
+ return await c.records.createVersion(
3824
+ str(input.parentId),
3825
+ input.data,
3826
+ str(input.changeDescription)
3827
+ );
3828
+ },
3829
+ createVariant: async (v, c) => {
3830
+ const input = v.input;
3831
+ if (!input) throw new Error("Input required (--data or --file)");
3832
+ return await c.records.createVariant(
3833
+ str(input.recordId),
3834
+ str(input.variantKey),
3835
+ input.data
3836
+ );
3683
3837
  }
3684
- const namedType = unwrapType(argDef.type);
3685
- switch (namedType.name) {
3686
- case "Int":
3687
- coerced[key] = parseInt(value, 10);
3688
- break;
3689
- case "Float":
3690
- coerced[key] = parseFloat(value);
3691
- break;
3692
- case "Boolean":
3693
- coerced[key] = value === "true" || value === "1";
3694
- break;
3695
- case "JSON":
3696
- try {
3697
- coerced[key] = JSON.parse(value);
3698
- } catch {
3699
- coerced[key] = value;
3700
- }
3701
- break;
3702
- default:
3703
- coerced[key] = value;
3838
+ },
3839
+ // ── Locales ─────────────────────────────────────────────────
3840
+ locales: {
3841
+ locales: async (v, c) => {
3842
+ const resp = await c.settings.listLocales({ limit: num(v.limit, 50) });
3843
+ return { items: resp.locales ?? [], total: resp.total ?? 0 };
3844
+ },
3845
+ locale: async (v, c) => await c.settings.getLocale(str(v.id)),
3846
+ createLocale: async (v, c) => {
3847
+ const input = v.input;
3848
+ if (!input) throw new Error("Input required");
3849
+ return await c.settings.createLocale({
3850
+ locale: str(input.locale),
3851
+ displayName: str(input.displayName),
3852
+ nativeName: str(input.nativeName),
3853
+ isDefault: input.isDefault,
3854
+ isRtl: input.isRtl,
3855
+ fallbackLocale: str(input.fallbackLocale)
3856
+ });
3857
+ },
3858
+ updateLocale: async (v, c) => {
3859
+ const input = v.input;
3860
+ if (!input) throw new Error("Input required");
3861
+ return await c.settings.updateLocale({
3862
+ id: str(input.id ?? v.id),
3863
+ displayName: str(input.displayName),
3864
+ nativeName: str(input.nativeName),
3865
+ isDefault: input.isDefault,
3866
+ isActive: input.isActive,
3867
+ isRtl: input.isRtl,
3868
+ fallbackLocale: str(input.fallbackLocale)
3869
+ });
3870
+ },
3871
+ deleteLocale: async (v, c) => await c.settings.deleteLocale(str(v.id))
3872
+ },
3873
+ // ── Segments ────────────────────────────────────────────────
3874
+ segments: {
3875
+ segments: async (v, c) => {
3876
+ const resp = await c.segments.listSegments({ limit: num(v.limit, 50) });
3877
+ return { items: resp.segments ?? [], total: resp.total ?? 0 };
3878
+ },
3879
+ segment: async (v, c) => await c.segments.getSegment(str(v.id)),
3880
+ segmentByKey: async (v, c) => await c.segments.getSegmentByKey(str(v.key)),
3881
+ createSegment: async (v, c) => {
3882
+ const input = v.input;
3883
+ if (!input) throw new Error("Input required");
3884
+ return await c.segments.createSegment({
3885
+ key: str(input.key),
3886
+ name: str(input.name),
3887
+ description: str(input.description),
3888
+ rules: input.rules,
3889
+ evaluationMode: str(input.evaluationMode),
3890
+ isActive: input.isActive
3891
+ });
3892
+ },
3893
+ updateSegment: async (v, c) => {
3894
+ const input = v.input;
3895
+ if (!input) throw new Error("Input required");
3896
+ return await c.segments.updateSegment({
3897
+ id: str(input.id ?? v.id),
3898
+ name: str(input.name),
3899
+ description: str(input.description),
3900
+ rules: input.rules,
3901
+ evaluationMode: str(input.evaluationMode),
3902
+ isActive: input.isActive
3903
+ });
3904
+ },
3905
+ deleteSegment: async (v, c) => await c.segments.deleteSegment(str(v.id)),
3906
+ previewSegmentRules: async (v, c) => {
3907
+ const input = v.rules;
3908
+ return await c.segments.previewSegmentRules(input);
3909
+ },
3910
+ testSegmentEvaluation: async (v, c) => await c.segments.testSegmentEvaluation(
3911
+ str(v.segmentId),
3912
+ str(v.customerId)
3913
+ )
3914
+ },
3915
+ // ── Experiments ─────────────────────────────────────────────
3916
+ experiments: {
3917
+ experiments: async (v, c) => {
3918
+ const resp = await c.experiments.listExperiments({
3919
+ limit: num(v.limit, 50)
3920
+ });
3921
+ return { items: resp.experiments ?? [], total: resp.total ?? 0 };
3922
+ },
3923
+ experiment: async (v, c) => await c.experiments.getExperiment(str(v.id)),
3924
+ experimentByKey: async (v, c) => await c.experiments.getExperimentByKey(str(v.key)),
3925
+ createExperiment: async (v, c) => {
3926
+ const input = v.input;
3927
+ if (!input) throw new Error("Input required");
3928
+ return await c.experiments.createExperiment({
3929
+ key: str(input.key),
3930
+ name: str(input.name),
3931
+ description: str(input.description),
3932
+ targeting: input.targeting,
3933
+ controlPercent: input.controlPercent,
3934
+ variants: input.variants
3935
+ });
3936
+ },
3937
+ updateExperiment: async (v, c) => {
3938
+ const input = v.input;
3939
+ if (!input) throw new Error("Input required");
3940
+ return await c.experiments.updateExperiment({
3941
+ id: str(input.id ?? v.id),
3942
+ name: str(input.name),
3943
+ description: str(input.description),
3944
+ targeting: input.targeting,
3945
+ controlPercent: input.controlPercent,
3946
+ variants: input.variants
3947
+ });
3948
+ },
3949
+ deleteExperiment: async (v, c) => await c.experiments.deleteExperiment(str(v.id)),
3950
+ startExperiment: async (v, c) => await c.experiments.startExperiment(str(v.experimentId)),
3951
+ pauseExperiment: async (v, c) => await c.experiments.pauseExperiment(str(v.experimentId)),
3952
+ resumeExperiment: async (v, c) => await c.experiments.resumeExperiment(str(v.experimentId)),
3953
+ endExperiment: async (v, c) => await c.experiments.endExperiment(str(v.experimentId)),
3954
+ experimentStats: async (v, c) => await c.experiments.getExperimentStats(str(v.experimentId))
3955
+ },
3956
+ // ── Settings ────────────────────────────────────────────────
3957
+ settings: {
3958
+ allSettings: async (v, c) => await c.settings.getSettings({
3959
+ category: str(v.category),
3960
+ key: str(v.key)
3961
+ }),
3962
+ setting: async (v, c) => {
3963
+ const settings = await c.settings.getSettings({ key: str(v.key) });
3964
+ return settings[0] ?? null;
3965
+ },
3966
+ setSetting: async (v, c) => {
3967
+ const input = v.input;
3968
+ if (!input) throw new Error("Input required");
3969
+ return await c.settings.updateSetting({
3970
+ key: str(input.key),
3971
+ value: input.value ?? input
3972
+ });
3704
3973
  }
3705
- }
3706
- return coerced;
3707
- }
3708
- function buildQuery(entry, variables) {
3709
- const field = getField(entry.operation, entry.operationType);
3710
- if (!field) {
3711
- throw new Error(
3712
- `Operation "${entry.operation}" not found in schema ${entry.operationType} type`
3713
- );
3714
- }
3715
- const opType = entry.operationType === "query" ? "query" : "mutation";
3716
- const opName = entry.operation.charAt(0).toUpperCase() + entry.operation.slice(1);
3717
- const usedArgs = [];
3718
- for (const argDef of field.args) {
3719
- const isPositional = entry.positionalArgs?.some(
3720
- (p) => p.graphqlArg === argDef.name
3721
- );
3722
- const isInputArg = entry.acceptsInput && argDef.name === (entry.inputArgName ?? "input");
3723
- if (isPositional || isInputArg || variables[argDef.name] !== void 0) {
3724
- usedArgs.push({ argDef, varName: argDef.name });
3974
+ },
3975
+ // ── API Keys ────────────────────────────────────────────────
3976
+ "api-keys": {
3977
+ listApiKeys: async (v, c) => wrapList(await c.identity.listApiKeys({ limit: num(v.limit, 50) })),
3978
+ createApiKey: async (v, c) => {
3979
+ const input = v.input;
3980
+ if (!input) throw new Error("Input required");
3981
+ return await c.identity.createApiKey({
3982
+ name: str(input.name),
3983
+ keyType: input.keyType,
3984
+ rateLimitPerHour: input.rateLimitPerHour
3985
+ });
3986
+ },
3987
+ rotateApiKey: async (v, c) => await c.identity.rotateApiKey(str(v.id)),
3988
+ revokeApiKey: async (v, c) => await c.identity.revokeApiKey(str(v.id))
3989
+ },
3990
+ // ── Auth Providers ──────────────────────────────────────────
3991
+ "auth-providers": {
3992
+ customerAuthProviders: async (v, c) => {
3993
+ const resp = await c.identity.listAuthProviders({
3994
+ limit: num(v.limit, 50)
3995
+ });
3996
+ return { items: resp.items, total: resp.total };
3997
+ },
3998
+ customerAuthProvider: async (v, c) => await c.identity.getAuthProvider(str(v.id)),
3999
+ createCustomerAuthProvider: async (v, c) => {
4000
+ const input = v.input;
4001
+ if (!input) throw new Error("Input required");
4002
+ return await c.identity.createAuthProvider({
4003
+ key: str(input.key),
4004
+ name: str(input.name),
4005
+ type: str(input.type),
4006
+ config: input.config,
4007
+ enabled: input.enabled,
4008
+ isDefault: input.isDefault,
4009
+ priority: input.priority
4010
+ });
4011
+ },
4012
+ updateCustomerAuthProvider: async (v, c) => {
4013
+ const input = v.input;
4014
+ if (!input) throw new Error("Input required");
4015
+ return await c.identity.updateAuthProvider({
4016
+ id: str(input.id ?? v.id),
4017
+ name: str(input.name),
4018
+ config: input.config,
4019
+ enabled: input.enabled,
4020
+ isDefault: input.isDefault,
4021
+ priority: input.priority
4022
+ });
4023
+ },
4024
+ deleteCustomerAuthProvider: async (v, c) => await c.identity.deleteAuthProvider(str(v.id))
4025
+ },
4026
+ // ── Configs ─────────────────────────────────────────────────
4027
+ configs: {
4028
+ configs: async (v, c) => {
4029
+ const resp = await c.configs.listConfigs({ limit: num(v.limit, 50) });
4030
+ return { items: resp.configs ?? [], total: resp.total ?? 0 };
4031
+ },
4032
+ config: async (v, c) => await c.configs.getConfig(str(v.id)),
4033
+ configByKey: async (v, c) => await c.configs.getConfigByKey(str(v.key)),
4034
+ registerConfig: async (v, c) => {
4035
+ const input = v.input;
4036
+ if (!input) throw new Error("Input required");
4037
+ return await c.configs.createConfig({
4038
+ key: str(input.key),
4039
+ configType: str(input.configType),
4040
+ name: str(input.name),
4041
+ description: str(input.description),
4042
+ config: input.config,
4043
+ enabled: input.enabled,
4044
+ direction: str(input.direction)
4045
+ });
4046
+ },
4047
+ applyConfig: async (v, c) => {
4048
+ const input = v.input;
4049
+ if (!input) throw new Error("Input required");
4050
+ return await c.configs.applyConfig(
4051
+ str(input.key ?? input.configKey),
4052
+ input
4053
+ );
4054
+ },
4055
+ unregisterConfig: async (v, c) => await c.configs.deleteConfig(str(v.id)),
4056
+ triggerConfigSync: async (v, _c) => {
4057
+ console.log(
4058
+ chalk9.yellow(
4059
+ `Config sync trigger for ${str(v.configId)} is not yet available via ConnectRPC.`
4060
+ )
4061
+ );
4062
+ return { success: false };
3725
4063
  }
3726
- }
3727
- const varDecls = usedArgs.map((a) => `$${a.varName}: ${typeToString(a.argDef.type)}`).join(", ");
3728
- const fieldArgs = usedArgs.map((a) => `${a.argDef.name}: $${a.varName}`).join(", ");
3729
- const returnType = unwrapType(field.type);
3730
- let selectionSet = "";
3731
- if (isScalarType(returnType) || isEnumType(returnType)) {
3732
- selectionSet = "";
3733
- } else if (isObjectType(returnType)) {
3734
- const wrapper = detectListWrapper(returnType);
3735
- if (wrapper) {
3736
- const itemSelection = buildSelectionSet(wrapper.itemType, true);
3737
- selectionSet = `{ ${wrapper.listField} { ${itemSelection} } total }`;
3738
- } else {
3739
- const selection = buildSelectionSet(returnType, false);
3740
- selectionSet = selection ? `{ ${selection} }` : "";
4064
+ },
4065
+ // ── Customers ───────────────────────────────────────────────
4066
+ customers: {
4067
+ customers: async (v, c) => wrapList(
4068
+ await c.identity.listCustomers({
4069
+ limit: num(v.limit, 50),
4070
+ search: str(v.search)
4071
+ })
4072
+ ),
4073
+ customer: async (v, c) => {
4074
+ const result = await c.identity.getCustomer(str(v.id));
4075
+ return result.customer;
4076
+ },
4077
+ createCustomer: async (v, c) => {
4078
+ const input = v.input;
4079
+ if (!input) throw new Error("Input required");
4080
+ const result = await c.identity.createCustomer({
4081
+ email: str(input.email),
4082
+ password: str(input.password)
4083
+ });
4084
+ return result.customer;
4085
+ },
4086
+ updateCustomer: async (v, c) => {
4087
+ const input = v.input;
4088
+ if (!input) throw new Error("Input required");
4089
+ const result = await c.identity.updateCustomer({
4090
+ id: str(input.id ?? v.id),
4091
+ email: str(input.email),
4092
+ status: input.status
4093
+ });
4094
+ return result.customer;
4095
+ },
4096
+ deleteCustomer: async (v, c) => await c.identity.deleteCustomer(str(v.id)),
4097
+ suspendCustomer: async (v, c) => {
4098
+ const result = await c.identity.suspendCustomer(str(v.id));
4099
+ return result.customer;
3741
4100
  }
3742
4101
  }
3743
- const varPart = varDecls ? `(${varDecls})` : "";
3744
- const argPart = fieldArgs ? `(${fieldArgs})` : "";
3745
- return `${opType} ${opName}${varPart} { ${entry.operation}${argPart} ${selectionSet} }`;
3746
- }
3747
- function getInputFields(operationName, operationType, inputArgName) {
3748
- const field = getField(operationName, operationType);
3749
- if (!field) return [];
3750
- const argName = inputArgName ?? "input";
3751
- const arg = field.args.find((a) => a.name === argName);
3752
- if (!arg) return [];
3753
- const namedType = unwrapType(arg.type);
3754
- if (!isInputObjectType(namedType)) return [];
3755
- const fields = namedType.getFields();
3756
- return Object.entries(fields).map(([name, f]) => ({
3757
- name,
3758
- type: typeToString(f.type),
3759
- required: isNonNullType(f.type)
3760
- }));
3761
- }
3762
- function getCompletions(partial, commandNames) {
3763
- return commandNames.filter(
3764
- (name) => name.toLowerCase().startsWith(partial.toLowerCase())
3765
- );
3766
- }
3767
- return {
3768
- buildQuery,
3769
- getOperationArgs,
3770
- coerceArgs,
3771
- getInputFields,
3772
- getCompletions
3773
4102
  };
3774
4103
  }
3775
-
3776
- // src/commands/register-commands.ts
3777
- var __filename = fileURLToPath(import.meta.url);
3778
- var __dirname = dirname4(__filename);
3779
- function loadSchemaSDL() {
3780
- const bundledPath = resolve5(__dirname, "schema.graphql");
3781
- try {
3782
- return readFileSync(bundledPath, "utf-8");
3783
- } catch {
3784
- const monorepoPath = resolve5(
3785
- __dirname,
3786
- "../../../graphql-core/schema.graphql"
3787
- );
3788
- try {
3789
- return readFileSync(monorepoPath, "utf-8");
3790
- } catch {
3791
- throw new Error(
3792
- "Could not find schema.graphql. Try reinstalling @eide/foir-cli."
3793
- );
3794
- }
3795
- }
4104
+ function str(v) {
4105
+ return v != null ? String(v) : void 0;
3796
4106
  }
3797
- function extractResult(result, operationName) {
3798
- const raw = result[operationName];
3799
- if (!raw || typeof raw !== "object") return { data: raw };
3800
- const obj = raw;
3801
- if ("total" in obj) {
3802
- for (const [key, val] of Object.entries(obj)) {
3803
- if (key !== "total" && key !== "__typename" && Array.isArray(val)) {
3804
- return { data: val, total: obj.total };
3805
- }
3806
- }
3807
- }
3808
- return { data: raw };
4107
+ function num(v, fallback) {
4108
+ if (v == null) return fallback;
4109
+ const n = Number(v);
4110
+ return Number.isNaN(n) ? fallback : n;
4111
+ }
4112
+ function wrapList(result) {
4113
+ return result;
3809
4114
  }
3810
4115
  function toCliColumns(columns) {
3811
4116
  if (!columns) return void 0;
@@ -3824,8 +4129,8 @@ function toCliColumns(columns) {
3824
4129
  break;
3825
4130
  case "truncate":
3826
4131
  cliCol.format = (v) => {
3827
- const str = String(v ?? "");
3828
- return str.length > 40 ? str.slice(0, 39) + "\u2026" : str;
4132
+ const s = String(v ?? "");
4133
+ return s.length > 40 ? s.slice(0, 39) + "\u2026" : s;
3829
4134
  };
3830
4135
  break;
3831
4136
  case "join":
@@ -3833,27 +4138,37 @@ function toCliColumns(columns) {
3833
4138
  break;
3834
4139
  case "bytes":
3835
4140
  cliCol.format = (v) => {
3836
- const num = Number(v);
3837
- if (num < 1024) return `${num} B`;
3838
- if (num < 1024 * 1024) return `${(num / 1024).toFixed(1)} KB`;
3839
- return `${(num / (1024 * 1024)).toFixed(1)} MB`;
4141
+ const n = Number(v);
4142
+ if (n < 1024) return `${n} B`;
4143
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
4144
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
3840
4145
  };
3841
4146
  break;
3842
4147
  }
3843
4148
  return cliCol;
3844
4149
  });
3845
4150
  }
3846
- function registerDynamicCommands(program2, globalOpts) {
3847
- let engine;
3848
- try {
3849
- const sdl = loadSchemaSDL();
3850
- engine = createSchemaEngine(sdl);
3851
- } catch (err) {
3852
- console.error(
3853
- `Warning: Could not load schema for dynamic commands: ${err instanceof Error ? err.message : String(err)}`
4151
+ function autoColumns(items) {
4152
+ if (items.length === 0) return [];
4153
+ const first = items[0];
4154
+ return Object.keys(first).filter((k) => k !== "__typename" && k !== "$typeName" && k !== "$unknown").slice(0, 6).map((key) => ({
4155
+ key,
4156
+ header: key,
4157
+ width: 20
4158
+ }));
4159
+ }
4160
+ var DISPATCH = buildDispatchTable();
4161
+ async function executeCommand(entry, variables, client) {
4162
+ const groupHandlers = DISPATCH[entry.group];
4163
+ const handler = groupHandlers?.[entry.operation];
4164
+ if (!handler) {
4165
+ throw new Error(
4166
+ `Command not yet migrated to ConnectRPC: ${entry.group}.${entry.operation}`
3854
4167
  );
3855
- return;
3856
4168
  }
4169
+ return handler(variables, client);
4170
+ }
4171
+ function registerDynamicCommands(program2, globalOpts) {
3857
4172
  const groups = /* @__PURE__ */ new Map();
3858
4173
  for (const cmd of COMMANDS) {
3859
4174
  if (!groups.has(cmd.group)) groups.set(cmd.group, []);
@@ -3862,42 +4177,15 @@ function registerDynamicCommands(program2, globalOpts) {
3862
4177
  for (const [groupName, entries] of groups) {
3863
4178
  const group = program2.command(groupName).description(`Manage ${groupName}`);
3864
4179
  for (const entry of entries) {
4180
+ if (!DISPATCH[entry.group]?.[entry.operation]) continue;
3865
4181
  let cmd = group.command(entry.name).description(entry.description);
3866
4182
  for (const pos of entry.positionalArgs ?? []) {
3867
4183
  cmd = cmd.argument(`<${pos.name}>`, pos.description ?? pos.name);
3868
4184
  }
3869
- const schemaArgs = engine.getOperationArgs(
3870
- entry.operation,
3871
- entry.operationType
3872
- );
3873
- for (const arg of schemaArgs) {
3874
- if (entry.positionalArgs?.some((p) => p.graphqlArg === arg.name))
3875
- continue;
3876
- if (entry.acceptsInput && arg.name === (entry.inputArgName ?? "input"))
3877
- continue;
3878
- const reqStr = arg.required ? " (required)" : "";
3879
- cmd = cmd.option(`--${arg.name} <value>`, `${arg.name}${reqStr}`);
3880
- }
4185
+ cmd = cmd.option("--limit <n>", "Max results");
3881
4186
  if (entry.acceptsInput) {
3882
4187
  cmd = cmd.option("-d, --data <json>", "Data as JSON");
3883
4188
  cmd = cmd.option("-f, --file <path>", "Read data from file");
3884
- const inputFields = engine.getInputFields(
3885
- entry.operation,
3886
- entry.operationType,
3887
- entry.inputArgName
3888
- );
3889
- if (inputFields.length > 0) {
3890
- const required = inputFields.filter((f) => f.required);
3891
- const optional = inputFields.filter((f) => !f.required);
3892
- let fieldHelp = "\nInput fields:";
3893
- if (required.length > 0) {
3894
- fieldHelp += "\n Required: " + required.map((f) => `${f.name} (${f.type})`).join(", ");
3895
- }
3896
- if (optional.length > 0) {
3897
- fieldHelp += "\n Optional: " + optional.map((f) => `${f.name} (${f.type})`).join(", ");
3898
- }
3899
- cmd = cmd.addHelpText("after", fieldHelp);
3900
- }
3901
4189
  }
3902
4190
  for (const cf of entry.customFlags ?? []) {
3903
4191
  cmd = cmd.option(cf.flag, cf.description);
@@ -3908,7 +4196,7 @@ function registerDynamicCommands(program2, globalOpts) {
3908
4196
  cmd.action(
3909
4197
  withErrorHandler(globalOpts, async (...actionArgs) => {
3910
4198
  const opts = globalOpts();
3911
- const client = await createClient(opts);
4199
+ const client = await createPlatformClient(opts);
3912
4200
  const variables = {};
3913
4201
  const positionals = entry.positionalArgs ?? [];
3914
4202
  for (let i = 0; i < positionals.length; i++) {
@@ -3921,27 +4209,27 @@ function registerDynamicCommands(program2, globalOpts) {
3921
4209
  const flags = actionArgs[positionals.length] ?? {};
3922
4210
  const customFlagNames = new Set(
3923
4211
  (entry.customFlags ?? []).map(
3924
- (cf) => cf.flag.replace(/ <.*>$/, "").replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase())
4212
+ (cf) => cf.flag.replace(/ <.*>$/, "").replace(/^--/, "").replace(/-([a-z])/g, (_, ch) => ch.toUpperCase())
3925
4213
  )
3926
4214
  );
3927
- const rawFlags = {};
3928
4215
  for (const [key, val] of Object.entries(flags)) {
3929
- if (key === "data" || key === "file" || key === "confirm") continue;
3930
- if (customFlagNames.has(key)) continue;
3931
- rawFlags[key] = String(val);
4216
+ if (key === "data" || key === "file" || key === "confirm" || key === "limit")
4217
+ continue;
4218
+ if (customFlagNames.has(key)) {
4219
+ variables[key] = val;
4220
+ continue;
4221
+ }
4222
+ variables[key] = val;
4223
+ }
4224
+ if (flags.limit) {
4225
+ variables.limit = flags.limit;
3932
4226
  }
3933
- const coerced = engine.coerceArgs(
3934
- entry.operation,
3935
- entry.operationType,
3936
- rawFlags
3937
- );
3938
- Object.assign(variables, coerced);
3939
4227
  if (flags.dir && entry.acceptsInput) {
3940
4228
  const dirPath = resolve5(String(flags.dir));
3941
4229
  const files = readdirSync(dirPath).filter((f) => /\.(json|ts|js|mjs)$/.test(f)).sort();
3942
4230
  if (files.length === 0) {
3943
4231
  console.error(
3944
- chalk9.yellow(`\u26A0 No .json/.ts/.js files found in ${dirPath}`)
4232
+ chalk9.yellow(`No .json/.ts/.js files found in ${dirPath}`)
3945
4233
  );
3946
4234
  return;
3947
4235
  }
@@ -3955,8 +4243,7 @@ function registerDynamicCommands(program2, globalOpts) {
3955
4243
  const fileVars = { ...variables, [argName]: fileData };
3956
4244
  const label = fileData.key ?? fileData.name ?? file;
3957
4245
  try {
3958
- const q = engine.buildQuery(entry, fileVars);
3959
- await client.request(q, fileVars);
4246
+ await executeCommand(entry, fileVars, client);
3960
4247
  created++;
3961
4248
  if (!(opts.json || opts.jsonl || opts.quiet)) {
3962
4249
  success(`Created ${label}`);
@@ -3975,8 +4262,7 @@ function registerDynamicCommands(program2, globalOpts) {
3975
4262
  [updateEntry.positionalArgs[0].graphqlArg]: fileData.key
3976
4263
  } : {}
3977
4264
  };
3978
- const uq = engine.buildQuery(updateEntry, updateVars);
3979
- await client.request(uq, updateVars);
4265
+ await executeCommand(updateEntry, updateVars, client);
3980
4266
  updated++;
3981
4267
  if (!(opts.json || opts.jsonl || opts.quiet)) {
3982
4268
  success(`Updated ${label}`);
@@ -4010,19 +4296,13 @@ function registerDynamicCommands(program2, globalOpts) {
4010
4296
  data: flags.data,
4011
4297
  file: flags.file
4012
4298
  });
4013
- const inputFields = engine.getInputFields(
4014
- entry.operation,
4015
- entry.operationType,
4016
- entry.inputArgName
4017
- );
4018
- const fieldNames = new Set(inputFields.map((f) => f.name));
4019
- if (fieldNames.has("projectId") && !inputData.projectId || fieldNames.has("tenantId") && !inputData.tenantId) {
4299
+ if (!inputData.projectId || !inputData.tenantId) {
4020
4300
  const resolved = await resolveProjectContext(opts);
4021
4301
  if (resolved) {
4022
- if (fieldNames.has("projectId") && !inputData.projectId) {
4302
+ if (!inputData.projectId) {
4023
4303
  inputData.projectId = resolved.project.id;
4024
4304
  }
4025
- if (fieldNames.has("tenantId") && !inputData.tenantId) {
4305
+ if (!inputData.tenantId) {
4026
4306
  inputData.tenantId = resolved.project.tenantId;
4027
4307
  }
4028
4308
  }
@@ -4053,12 +4333,7 @@ function registerDynamicCommands(program2, globalOpts) {
4053
4333
  if (flags.modelKey) {
4054
4334
  variables.modelKey = String(flags.modelKey);
4055
4335
  }
4056
- const queryStr2 = engine.buildQuery(altEntry, variables);
4057
- const result2 = await client.request(
4058
- queryStr2,
4059
- variables
4060
- );
4061
- const { data: data2 } = extractResult(result2, altEntry.operation);
4336
+ const data2 = await executeCommand(altEntry, variables, client);
4062
4337
  formatOutput(data2, opts);
4063
4338
  return;
4064
4339
  }
@@ -4075,13 +4350,10 @@ function registerDynamicCommands(program2, globalOpts) {
4075
4350
  if (entry.group === "records" && entry.name === "publish" && variables.versionId) {
4076
4351
  const versionIdValue = String(variables.versionId);
4077
4352
  if (flags.modelKey) {
4078
- const modelKey = String(flags.modelKey);
4079
- const lookupQuery = `query RecordByKey($modelKey: String!, $naturalKey: String!) { recordByKey(modelKey: $modelKey, naturalKey: $naturalKey) { id currentVersionId } }`;
4080
- const lookupResult = await client.request(lookupQuery, {
4081
- modelKey,
4082
- naturalKey: versionIdValue
4083
- });
4084
- const record = lookupResult.recordByKey;
4353
+ const record = await client.records.getRecordByKey(
4354
+ String(flags.modelKey),
4355
+ versionIdValue
4356
+ );
4085
4357
  if (!record?.currentVersionId) {
4086
4358
  throw new Error(
4087
4359
  `No current version found for record "${versionIdValue}"`
@@ -4094,23 +4366,18 @@ function registerDynamicCommands(program2, globalOpts) {
4094
4366
  );
4095
4367
  } else {
4096
4368
  try {
4097
- const lookupQuery = `query Record($id: ID!) { record(id: $id) { id recordType currentVersionId } }`;
4098
- const lookupResult = await client.request(lookupQuery, {
4099
- id: versionIdValue
4100
- });
4101
- const record = lookupResult.record;
4102
- if (record?.recordType === "record" && record?.currentVersionId) {
4369
+ const record = await client.records.getRecord(versionIdValue);
4370
+ if (record?.recordType === RecordType.RECORD && record?.currentVersionId) {
4103
4371
  variables.versionId = record.currentVersionId;
4104
4372
  }
4105
4373
  } catch {
4106
4374
  }
4107
4375
  }
4108
4376
  }
4109
- const queryStr = engine.buildQuery(entry, variables);
4110
- let result;
4377
+ let data;
4111
4378
  let usedUpdate = false;
4112
4379
  try {
4113
- result = await client.request(queryStr, variables);
4380
+ data = await executeCommand(entry, variables, client);
4114
4381
  } catch (createErr) {
4115
4382
  if (flags.upsert && entry.name === "create") {
4116
4383
  const updateEntry = COMMANDS.find(
@@ -4126,8 +4393,7 @@ function registerDynamicCommands(program2, globalOpts) {
4126
4393
  [updateEntry.positionalArgs[0].graphqlArg]: inputData.key
4127
4394
  } : {}
4128
4395
  };
4129
- const uq = engine.buildQuery(updateEntry, updateVars);
4130
- result = await client.request(uq, updateVars);
4396
+ data = await executeCommand(updateEntry, updateVars, client);
4131
4397
  usedUpdate = true;
4132
4398
  } else {
4133
4399
  throw createErr;
@@ -4139,8 +4405,14 @@ function registerDynamicCommands(program2, globalOpts) {
4139
4405
  const activeEntry = usedUpdate ? COMMANDS.find(
4140
4406
  (c) => c.group === entry.group && c.name === "update"
4141
4407
  ) ?? entry : entry;
4142
- const { data, total } = extractResult(result, activeEntry.operation);
4143
- const responseData = data && typeof data === "object" && !Array.isArray(data) ? data : void 0;
4408
+ let displayData = data;
4409
+ let total;
4410
+ if (data && typeof data === "object" && "items" in data) {
4411
+ const listResult = data;
4412
+ displayData = listResult.items;
4413
+ total = listResult.total;
4414
+ }
4415
+ const responseData = displayData && typeof displayData === "object" && !Array.isArray(displayData) ? displayData : void 0;
4144
4416
  const displayEntry = usedUpdate ? activeEntry : entry;
4145
4417
  if (displayEntry.scalarResult) {
4146
4418
  if (!(opts.json || opts.jsonl || opts.quiet)) {
@@ -4148,16 +4420,16 @@ function registerDynamicCommands(program2, globalOpts) {
4148
4420
  success(displayEntry.successMessage, responseData);
4149
4421
  }
4150
4422
  } else {
4151
- formatOutput(data, opts);
4423
+ formatOutput(displayData, opts);
4152
4424
  }
4153
- } else if (Array.isArray(data)) {
4425
+ } else if (Array.isArray(displayData)) {
4154
4426
  const cliColumns = toCliColumns(displayEntry.columns);
4155
- formatList(data, opts, {
4156
- columns: cliColumns ?? autoColumns(data),
4427
+ formatList(displayData, opts, {
4428
+ columns: cliColumns ?? autoColumns(displayData),
4157
4429
  total
4158
4430
  });
4159
4431
  } else {
4160
- formatOutput(data, opts);
4432
+ formatOutput(displayData, opts);
4161
4433
  if (displayEntry.successMessage && !(opts.json || opts.jsonl || opts.quiet)) {
4162
4434
  success(displayEntry.successMessage, responseData);
4163
4435
  }
@@ -4167,15 +4439,14 @@ function registerDynamicCommands(program2, globalOpts) {
4167
4439
  const record = responseData.record;
4168
4440
  const versionId = version2?.id ?? record?.currentVersionId;
4169
4441
  if (versionId) {
4170
- const publishQuery = `mutation PublishVersion($versionId: ID!) { publishVersion(versionId: $versionId) }`;
4171
- await client.request(publishQuery, { versionId });
4442
+ await client.records.publishVersion(versionId);
4172
4443
  if (!(opts.json || opts.jsonl || opts.quiet)) {
4173
4444
  success("Published version {id}", { id: versionId });
4174
4445
  }
4175
4446
  } else if (!(opts.json || opts.jsonl || opts.quiet)) {
4176
4447
  console.error(
4177
4448
  chalk9.yellow(
4178
- "\u26A0 Could not auto-publish: no version found in response"
4449
+ "Could not auto-publish: no version found in response"
4179
4450
  )
4180
4451
  );
4181
4452
  }
@@ -4185,20 +4456,11 @@ function registerDynamicCommands(program2, globalOpts) {
4185
4456
  }
4186
4457
  }
4187
4458
  }
4188
- function autoColumns(items) {
4189
- if (items.length === 0) return [];
4190
- const first = items[0];
4191
- return Object.keys(first).filter((k) => k !== "__typename").slice(0, 6).map((key) => ({
4192
- key,
4193
- header: key,
4194
- width: 20
4195
- }));
4196
- }
4197
4459
 
4198
4460
  // src/cli.ts
4199
- var __filename2 = fileURLToPath2(import.meta.url);
4200
- var __dirname2 = dirname5(__filename2);
4201
- config({ path: resolve6(__dirname2, "../.env.local") });
4461
+ var __filename = fileURLToPath(import.meta.url);
4462
+ var __dirname = dirname4(__filename);
4463
+ config({ path: resolve6(__dirname, "../.env.local") });
4202
4464
  var require2 = createRequire(import.meta.url);
4203
4465
  var { version } = require2("../package.json");
4204
4466
  var program = new Command();