@eide/foir-cli 0.1.47 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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,153 @@ 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 } 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 "@foir/connect-clients/services";
463
+ import { createIdentityMethods } from "@foir/connect-clients/identity";
464
+ import { createModelsMethods } from "@foir/connect-clients/models";
465
+ import { createRecordsMethods } from "@foir/connect-clients/records";
466
+ import { createConfigsMethods } from "@foir/connect-clients/configs";
467
+ import { createSegmentsMethods } from "@foir/connect-clients/segments";
468
+ import { createExperimentsMethods } from "@foir/connect-clients/experiments";
469
+ import { createSettingsMethods } from "@foir/connect-clients/settings";
470
+ import { createStorageMethods } from "@foir/connect-clients/storage";
471
+ async function createPlatformClient(options) {
472
+ const apiUrl = getApiUrl(options);
473
+ const headers = {};
474
+ const envApiKey = process.env.FOIR_API_KEY;
475
+ if (envApiKey) {
476
+ headers["x-api-key"] = envApiKey;
477
+ } else {
478
+ const credentials = await getCredentials();
479
+ if (!credentials) {
480
+ throw new Error(
481
+ "Not authenticated. Run `foir login` or set FOIR_API_KEY."
482
+ );
483
+ }
484
+ if (isTokenExpired(credentials)) {
485
+ throw new Error("Session expired. Run `foir login` to re-authenticate.");
486
+ }
487
+ headers["Authorization"] = `Bearer ${credentials.accessToken}`;
516
488
  }
517
- const result = await response.json();
518
- if (result.errors?.length) {
519
- throw new Error(`GraphQL error: ${result.errors[0].message}`);
489
+ const resolved = await resolveProjectContext(options);
490
+ if (resolved) {
491
+ headers["x-tenant-id"] = resolved.project.tenantId;
492
+ headers["x-project-id"] = resolved.project.id;
520
493
  }
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;
494
+ const authInterceptor = (next) => async (req) => {
495
+ for (const [key, value] of Object.entries(headers)) {
496
+ req.header.set(key, value);
497
+ }
498
+ return next(req);
499
+ };
500
+ const transport = createConnectTransport({
501
+ baseUrl: apiUrl.replace(/\/$/, ""),
502
+ httpVersion: "1.1",
503
+ interceptors: [authInterceptor]
504
+ });
505
+ return {
506
+ identity: createIdentityMethods(createClient(IdentityService, transport)),
507
+ models: createModelsMethods(createClient(ModelsService, transport)),
508
+ records: createRecordsMethods(createClient(RecordsService, transport)),
509
+ configs: createConfigsMethods(createClient(ConfigsService, transport)),
510
+ segments: createSegmentsMethods(createClient(SegmentsService, transport)),
511
+ experiments: createExperimentsMethods(
512
+ createClient(ExperimentsService, transport)
513
+ ),
514
+ settings: createSettingsMethods(createClient(SettingsService, transport)),
515
+ storage: createStorageMethods(createClient(StorageService, transport))
516
+ };
557
517
  }
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;
518
+ function createPlatformClientWithHeaders(apiUrl, headers) {
519
+ const authInterceptor = (next) => async (req) => {
520
+ for (const [key, value] of Object.entries(headers)) {
521
+ req.header.set(key, value);
522
+ }
523
+ return next(req);
524
+ };
525
+ const transport = createConnectTransport({
526
+ baseUrl: apiUrl.replace(/\/$/, ""),
527
+ httpVersion: "1.1",
528
+ interceptors: [authInterceptor]
529
+ });
530
+ return {
531
+ identity: createIdentityMethods(createClient(IdentityService, transport)),
532
+ models: createModelsMethods(createClient(ModelsService, transport)),
533
+ records: createRecordsMethods(createClient(RecordsService, transport)),
534
+ configs: createConfigsMethods(createClient(ConfigsService, transport)),
535
+ segments: createSegmentsMethods(createClient(SegmentsService, transport)),
536
+ experiments: createExperimentsMethods(
537
+ createClient(ExperimentsService, transport)
538
+ ),
539
+ settings: createSettingsMethods(createClient(SettingsService, transport)),
540
+ storage: createStorageMethods(createClient(StorageService, transport))
541
+ };
567
542
  }
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 };
543
+ async function getStorageAuth(options) {
544
+ const apiUrl = getApiUrl(options);
545
+ const baseHeaders = {
546
+ "Content-Type": "application/json"
547
+ };
548
+ const envApiKey = process.env.FOIR_API_KEY;
549
+ if (envApiKey) {
550
+ baseHeaders["x-api-key"] = envApiKey;
551
+ } else {
552
+ const credentials = await getCredentials();
553
+ if (!credentials) {
554
+ throw new Error(
555
+ "Not authenticated. Run `foir login` or set FOIR_API_KEY."
556
+ );
557
+ }
558
+ if (isTokenExpired(credentials)) {
559
+ throw new Error("Session expired. Run `foir login` to re-authenticate.");
560
+ }
561
+ baseHeaders["Authorization"] = `Bearer ${credentials.accessToken}`;
583
562
  }
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 };
563
+ const resolved = await resolveProjectContext(options);
564
+ if (resolved) {
565
+ baseHeaders["x-tenant-id"] = resolved.project.tenantId;
566
+ baseHeaders["x-project-id"] = resolved.project.id;
567
+ }
568
+ let cachedToken = null;
569
+ const getToken = async () => {
570
+ if (cachedToken && Date.now() < cachedToken.expiresAt - 3e4) {
571
+ return cachedToken.token;
572
+ }
573
+ const tokenUrl = `${apiUrl.replace(/\/$/, "")}/api/auth/token`;
574
+ const res = await fetch(tokenUrl, {
575
+ method: "POST",
576
+ headers: baseHeaders,
577
+ body: JSON.stringify({ purpose: "storage" })
578
+ });
579
+ if (!res.ok) {
580
+ throw new Error(
581
+ `Failed to get storage token (${res.status}): ${await res.text()}`
582
+ );
583
+ }
584
+ const data = await res.json();
585
+ cachedToken = {
586
+ token: data.token,
587
+ expiresAt: new Date(data.expiresAt).getTime()
588
+ };
589
+ return cachedToken.token;
590
+ };
591
+ return { getToken };
587
592
  }
593
+
594
+ // src/commands/select-project.ts
595
+ var CLI_API_KEY_NAME = "Foir CLI";
588
596
  function registerSelectProjectCommand(program2, globalOpts) {
589
597
  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
598
  withErrorHandler(
@@ -598,11 +606,24 @@ function registerSelectProjectCommand(program2, globalOpts) {
598
606
  throw new Error("Not authenticated");
599
607
  }
600
608
  console.log("Fetching your projects...\n");
601
- const sessionContext = await fetchSessionContext(
602
- apiUrl,
603
- credentials.accessToken
609
+ const client = createPlatformClientWithHeaders(apiUrl, {
610
+ Authorization: `Bearer ${credentials.accessToken}`
611
+ });
612
+ const sessionContext = await client.identity.getSessionContext();
613
+ if (!sessionContext) {
614
+ throw new Error("Could not fetch session context");
615
+ }
616
+ const tenants = (sessionContext.availableTenants ?? []).map(
617
+ (t) => ({
618
+ id: t.id,
619
+ name: t.name
620
+ })
604
621
  );
605
- const { availableTenants: tenants, availableProjects: projects } = sessionContext;
622
+ const projects = (sessionContext.availableProjects ?? []).map((p) => ({
623
+ id: p.id,
624
+ name: p.name,
625
+ tenantId: p.tenantId
626
+ }));
606
627
  if (projects.length === 0) {
607
628
  console.log("No projects found. Create one in the platform first.");
608
629
  throw new Error("No projects available");
@@ -651,12 +672,12 @@ function registerSelectProjectCommand(program2, globalOpts) {
651
672
  selectedProject = projects.find((p) => p.id === projectId);
652
673
  }
653
674
  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
- );
675
+ const projectClient = createPlatformClientWithHeaders(apiUrl, {
676
+ Authorization: `Bearer ${credentials.accessToken}`,
677
+ "x-tenant-id": selectedProject.tenantId,
678
+ "x-project-id": selectedProject.id
679
+ });
680
+ const { apiKey, apiKeyId } = await provisionApiKey(projectClient);
660
681
  await writeProjectContext(
661
682
  {
662
683
  id: selectedProject.id,
@@ -681,6 +702,26 @@ function registerSelectProjectCommand(program2, globalOpts) {
681
702
  )
682
703
  );
683
704
  }
705
+ async function provisionApiKey(client) {
706
+ const { items: apiKeys } = await client.identity.listApiKeys({ limit: 100 });
707
+ const existing = apiKeys.find(
708
+ (k) => k.name === CLI_API_KEY_NAME && k.isActive
709
+ );
710
+ if (existing) {
711
+ console.log(" Rotating existing CLI API key...");
712
+ const result2 = await client.identity.rotateApiKey(existing.id);
713
+ if (!result2.apiKey?.rawKey) {
714
+ throw new Error("Failed to rotate API key \u2014 no raw key returned");
715
+ }
716
+ return { apiKey: result2.apiKey.rawKey, apiKeyId: result2.apiKey.id };
717
+ }
718
+ console.log(" Creating CLI API key...");
719
+ const result = await client.identity.createApiKey({ name: CLI_API_KEY_NAME });
720
+ if (!result.apiKey?.rawKey) {
721
+ throw new Error("Failed to create API key \u2014 no raw key returned");
722
+ }
723
+ return { apiKey: result.apiKey.rawKey, apiKeyId: result.apiKey.id };
724
+ }
684
725
 
685
726
  // src/lib/output.ts
686
727
  import chalk2 from "chalk";
@@ -734,11 +775,11 @@ function formatList(items, options, config2) {
734
775
  ${items.length} of ${config2.total} shown`));
735
776
  }
736
777
  }
737
- function pad(str, width) {
738
- if (str.length > width) {
739
- return str.slice(0, width - 1) + "\u2026";
778
+ function pad(str2, width) {
779
+ if (str2.length > width) {
780
+ return str2.slice(0, width - 1) + "\u2026";
740
781
  }
741
- return str.padEnd(width);
782
+ return str2.padEnd(width);
742
783
  }
743
784
  function timeAgo(dateStr) {
744
785
  if (!dateStr) return "\u2014";
@@ -831,99 +872,303 @@ function registerWhoamiCommand(program2, globalOpts) {
831
872
  import { promises as fs2 } from "fs";
832
873
  import { basename } from "path";
833
874
  import chalk3 from "chalk";
875
+ import { createStorageClient } from "@foir/connect-clients/storage";
834
876
 
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.");
877
+ // src/lib/input.ts
878
+ import inquirer2 from "inquirer";
879
+
880
+ // src/lib/config-loader.ts
881
+ import { readFile } from "fs/promises";
882
+ import { pathToFileURL } from "url";
883
+ import { resolve } from "path";
884
+ async function loadConfig(filePath) {
885
+ const absPath = resolve(filePath);
886
+ if (filePath.endsWith(".ts")) {
887
+ const configModule = await import(pathToFileURL(absPath).href);
888
+ return configModule.default;
851
889
  }
852
- if (isTokenExpired(credentials)) {
853
- throw new Error("Session expired. Run `foir login` to re-authenticate.");
890
+ if (filePath.endsWith(".js") || filePath.endsWith(".mjs")) {
891
+ const configModule = await import(pathToFileURL(absPath).href);
892
+ return configModule.default;
854
893
  }
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;
894
+ if (filePath.endsWith(".json")) {
895
+ const content = await readFile(absPath, "utf-8");
896
+ return JSON.parse(content);
860
897
  }
861
- return new GraphQLClient(endpoint, { headers });
898
+ throw new Error(
899
+ `Unsupported file extension for "${filePath}". Supported: .ts, .js, .mjs, .json`
900
+ );
862
901
  }
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.");
902
+
903
+ // src/lib/input.ts
904
+ async function parseInputData(opts) {
905
+ if (opts.data) {
906
+ return JSON.parse(opts.data);
874
907
  }
875
- if (isTokenExpired(credentials)) {
876
- throw new Error("Session expired. Run `foir login` to re-authenticate.");
908
+ if (opts.file) {
909
+ return await loadConfig(opts.file);
877
910
  }
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;
911
+ if (!process.stdin.isTTY) {
912
+ const chunks = [];
913
+ for await (const chunk of process.stdin) {
914
+ chunks.push(chunk);
915
+ }
916
+ const stdinContent = Buffer.concat(chunks).toString("utf-8").trim();
917
+ if (stdinContent) {
918
+ return JSON.parse(stdinContent);
919
+ }
883
920
  }
884
- return { apiUrl, headers };
921
+ throw new Error(
922
+ "No input data provided. Use --data, --file, or pipe via stdin."
923
+ );
924
+ }
925
+ function isUUID(value) {
926
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
927
+ value
928
+ ) || /^c[a-z0-9]{24,}$/.test(value);
929
+ }
930
+ async function confirmAction(message, opts) {
931
+ if (opts?.confirm) return true;
932
+ const { confirmed } = await inquirer2.prompt([
933
+ {
934
+ type: "confirm",
935
+ name: "confirmed",
936
+ message,
937
+ default: false
938
+ }
939
+ ]);
940
+ return confirmed;
885
941
  }
886
942
 
887
943
  // src/commands/media.ts
944
+ var MIME_TYPES = {
945
+ ".jpg": "image/jpeg",
946
+ ".jpeg": "image/jpeg",
947
+ ".png": "image/png",
948
+ ".gif": "image/gif",
949
+ ".webp": "image/webp",
950
+ ".avif": "image/avif",
951
+ ".svg": "image/svg+xml",
952
+ ".mp4": "video/mp4",
953
+ ".webm": "video/webm",
954
+ ".mov": "video/quicktime",
955
+ ".mp3": "audio/mpeg",
956
+ ".wav": "audio/wav",
957
+ ".pdf": "application/pdf",
958
+ ".json": "application/json",
959
+ ".csv": "text/csv",
960
+ ".txt": "text/plain"
961
+ };
962
+ function guessMimeType(filename) {
963
+ const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
964
+ return MIME_TYPES[ext] ?? "application/octet-stream";
965
+ }
966
+ function getStorageUrl() {
967
+ return process.env.FOIR_STORAGE_URL ?? "https://storage.foir.dev";
968
+ }
969
+ function createClient2(getToken) {
970
+ return createStorageClient({
971
+ baseUrl: getStorageUrl(),
972
+ getAuthToken: getToken
973
+ });
974
+ }
888
975
  function registerMediaCommands(program2, globalOpts) {
889
976
  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}`);
977
+ media.command("upload <filepath>").description("Upload a file").option("--folder <folder>", "Target folder").action(
978
+ withErrorHandler(
979
+ globalOpts,
980
+ async (filepath, flags) => {
981
+ const opts = globalOpts();
982
+ const { getToken } = await getStorageAuth(opts);
983
+ const storage = createClient2(getToken);
984
+ const fileBuffer = await fs2.readFile(filepath);
985
+ const filename = basename(filepath);
986
+ const mimeType = guessMimeType(filename);
987
+ const upload = await storage.createFileUpload({
988
+ filename,
989
+ mimeType,
990
+ size: fileBuffer.byteLength,
991
+ folder: flags.folder
992
+ });
993
+ const uploadResp = await fetch(upload.uploadUrl, {
994
+ method: "PUT",
995
+ headers: { "Content-Type": mimeType },
996
+ body: fileBuffer
997
+ });
998
+ if (!uploadResp.ok) {
999
+ throw new Error(
1000
+ `Upload to storage failed (${uploadResp.status}): ${await uploadResp.text()}`
1001
+ );
1002
+ }
1003
+ const file = await storage.confirmFileUpload(upload.uploadId);
1004
+ if (opts.json || opts.jsonl) {
1005
+ formatOutput(file, opts);
1006
+ } else {
1007
+ success(`Uploaded ${filename}`);
1008
+ if (file?.url) {
1009
+ console.log(chalk3.bold(` URL: ${file.url}`));
1010
+ }
1011
+ if (file?.storageKey) {
1012
+ console.log(chalk3.gray(` Key: ${file.storageKey}`));
1013
+ }
1014
+ }
907
1015
  }
908
- const result = await response.json();
1016
+ )
1017
+ );
1018
+ 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(
1019
+ withErrorHandler(
1020
+ globalOpts,
1021
+ async (flags) => {
1022
+ const opts = globalOpts();
1023
+ const { getToken } = await getStorageAuth(opts);
1024
+ const storage = createClient2(getToken);
1025
+ const result = await storage.listFiles({
1026
+ folder: flags.folder,
1027
+ mimeType: flags["mime-type"] ?? flags.mimeType,
1028
+ search: flags.search,
1029
+ includeDeleted: !!flags.includeDeleted,
1030
+ limit: Number(flags.limit) || 50,
1031
+ offset: Number(flags.offset) || 0
1032
+ });
1033
+ formatList(result.items, opts, {
1034
+ columns: [
1035
+ { key: "id", header: "ID", width: 28 },
1036
+ { key: "filename", header: "Filename", width: 24 },
1037
+ { key: "mimeType", header: "Type", width: 16 },
1038
+ {
1039
+ key: "size",
1040
+ header: "Size",
1041
+ width: 10,
1042
+ format: (v) => {
1043
+ const num2 = Number(v);
1044
+ if (num2 < 1024) return `${num2} B`;
1045
+ if (num2 < 1024 * 1024) return `${(num2 / 1024).toFixed(1)} KB`;
1046
+ return `${(num2 / (1024 * 1024)).toFixed(1)} MB`;
1047
+ }
1048
+ },
1049
+ { key: "folder", header: "Folder", width: 16 },
1050
+ {
1051
+ key: "createdAt",
1052
+ header: "Created",
1053
+ width: 12,
1054
+ format: (v) => timeAgo(v)
1055
+ }
1056
+ ],
1057
+ total: result.total
1058
+ });
1059
+ }
1060
+ )
1061
+ );
1062
+ media.command("get <id>").description("Get file details").action(
1063
+ withErrorHandler(globalOpts, async (id) => {
1064
+ const opts = globalOpts();
1065
+ const { getToken } = await getStorageAuth(opts);
1066
+ const storage = createClient2(getToken);
1067
+ const file = await storage.getFile(id);
1068
+ formatOutput(file, opts);
1069
+ })
1070
+ );
1071
+ media.command("usage").description("Get storage usage").action(
1072
+ withErrorHandler(globalOpts, async () => {
1073
+ const opts = globalOpts();
1074
+ const { getToken } = await getStorageAuth(opts);
1075
+ const storage = createClient2(getToken);
1076
+ const usage = await storage.getStorageUsage();
909
1077
  if (opts.json || opts.jsonl) {
910
- formatOutput(result, opts);
1078
+ formatOutput(usage, opts);
911
1079
  } else {
912
- success(`Uploaded ${fileName}`);
913
- if (result.url) {
914
- console.log(chalk3.bold(` URL: ${result.url}`));
1080
+ const mb = (Number(usage?.totalBytes ?? 0) / (1024 * 1024)).toFixed(
1081
+ 1
1082
+ );
1083
+ console.log(chalk3.bold(`Files: ${usage?.totalFiles ?? 0}`));
1084
+ console.log(chalk3.bold(`Storage: ${mb} MB`));
1085
+ }
1086
+ })
1087
+ );
1088
+ 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(
1089
+ withErrorHandler(
1090
+ globalOpts,
1091
+ async (id, flags) => {
1092
+ const opts = globalOpts();
1093
+ const { getToken } = await getStorageAuth(opts);
1094
+ const storage = createClient2(getToken);
1095
+ const file = await storage.updateFile({
1096
+ id,
1097
+ filename: flags.filename,
1098
+ folder: flags.folder,
1099
+ tags: flags.tags?.split(",").map((t) => t.trim())
1100
+ });
1101
+ if (opts.json || opts.jsonl) {
1102
+ formatOutput(file, opts);
1103
+ } else {
1104
+ success("Updated file");
1105
+ }
1106
+ }
1107
+ )
1108
+ );
1109
+ 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(
1110
+ withErrorHandler(
1111
+ globalOpts,
1112
+ async (id, flags) => {
1113
+ const opts = globalOpts();
1114
+ const { getToken } = await getStorageAuth(opts);
1115
+ const storage = createClient2(getToken);
1116
+ const file = await storage.updateFileMetadata({
1117
+ id,
1118
+ altText: flags.altText ?? flags["alt-text"],
1119
+ caption: flags.caption,
1120
+ description: flags.description
1121
+ });
1122
+ if (opts.json || opts.jsonl) {
1123
+ formatOutput(file, opts);
1124
+ } else {
1125
+ success("Updated file metadata");
1126
+ }
1127
+ }
1128
+ )
1129
+ );
1130
+ media.command("delete <id>").description("Delete a file").option("--confirm", "Skip confirmation prompt").option("--permanent", "Permanently delete (cannot be restored)").action(
1131
+ withErrorHandler(
1132
+ globalOpts,
1133
+ async (id, flags) => {
1134
+ const opts = globalOpts();
1135
+ const confirmed = await confirmAction("Delete this file?", {
1136
+ confirm: !!flags.confirm
1137
+ });
1138
+ if (!confirmed) {
1139
+ console.log("Aborted.");
1140
+ return;
915
1141
  }
916
- if (result.storageKey) {
917
- console.log(chalk3.gray(` Key: ${result.storageKey}`));
1142
+ const { getToken } = await getStorageAuth(opts);
1143
+ const storage = createClient2(getToken);
1144
+ if (flags.permanent) {
1145
+ await storage.permanentlyDeleteFile(id);
1146
+ success("Permanently deleted file");
1147
+ } else {
1148
+ await storage.deleteFile(id);
1149
+ success("Deleted file");
918
1150
  }
919
1151
  }
1152
+ )
1153
+ );
1154
+ media.command("restore <id>").description("Restore a deleted file").action(
1155
+ withErrorHandler(globalOpts, async (id) => {
1156
+ const opts = globalOpts();
1157
+ const { getToken } = await getStorageAuth(opts);
1158
+ const storage = createClient2(getToken);
1159
+ const file = await storage.restoreFile(id);
1160
+ if (opts.json || opts.jsonl) {
1161
+ formatOutput(file, opts);
1162
+ } else {
1163
+ success("Restored file");
1164
+ }
920
1165
  })
921
1166
  );
922
1167
  }
923
1168
 
924
1169
  // src/commands/create-config.ts
925
1170
  import chalk4 from "chalk";
926
- import inquirer2 from "inquirer";
1171
+ import inquirer3 from "inquirer";
927
1172
 
928
1173
  // src/scaffold/scaffold.ts
929
1174
  import * as fs4 from "fs";
@@ -1347,7 +1592,7 @@ function getApiTsconfig() {
1347
1592
  return JSON.stringify(config2, null, 2) + "\n";
1348
1593
  }
1349
1594
  function getApiEnvExample(apiUrl) {
1350
- const baseUrl = apiUrl.replace(/\/graphql$/, "");
1595
+ const baseUrl = apiUrl.replace(/\/$/, "");
1351
1596
  return `# Platform API
1352
1597
  PLATFORM_BASE_URL=${baseUrl}
1353
1598
  PLATFORM_API_KEY=sk_your_api_key_here
@@ -1425,11 +1670,7 @@ function isValidConfigType(value) {
1425
1670
  return CONFIG_TYPES.includes(value);
1426
1671
  }
1427
1672
  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(
1673
+ 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
1674
  withErrorHandler(
1434
1675
  globalOpts,
1435
1676
  async (name, cmdOpts) => {
@@ -1439,7 +1680,7 @@ function registerCreateConfigCommand(program2, globalOpts) {
1439
1680
  console.log();
1440
1681
  let configName = name;
1441
1682
  if (!configName) {
1442
- const { inputName } = await inquirer2.prompt([
1683
+ const { inputName } = await inquirer3.prompt([
1443
1684
  {
1444
1685
  type: "input",
1445
1686
  name: "inputName",
@@ -1453,7 +1694,7 @@ function registerCreateConfigCommand(program2, globalOpts) {
1453
1694
  if (cmdOpts?.type && isValidConfigType(cmdOpts.type)) {
1454
1695
  configType = cmdOpts.type;
1455
1696
  } else {
1456
- const { selectedType } = await inquirer2.prompt([
1697
+ const { selectedType } = await inquirer3.prompt([
1457
1698
  {
1458
1699
  type: "list",
1459
1700
  name: "selectedType",
@@ -1464,7 +1705,7 @@ function registerCreateConfigCommand(program2, globalOpts) {
1464
1705
  ]);
1465
1706
  configType = selectedType;
1466
1707
  }
1467
- const apiUrl = cmdOpts?.apiUrl ?? "http://localhost:4000/graphql";
1708
+ const apiUrl = cmdOpts?.apiUrl ?? "http://localhost:4011";
1468
1709
  console.log();
1469
1710
  console.log(
1470
1711
  ` Scaffolding ${chalk4.cyan(`"${configName}"`)} (${configType})...`
@@ -1476,69 +1717,53 @@ function registerCreateConfigCommand(program2, globalOpts) {
1476
1717
  );
1477
1718
  }
1478
1719
 
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
1720
  // src/commands/search.ts
1483
1721
  function registerSearchCommands(program2, globalOpts) {
1484
- program2.command("search <query>").description("Search across all records and media").option(
1722
+ program2.command("search <query>").description("Search across all records").option(
1485
1723
  "--models <keys>",
1486
1724
  "Filter to specific model keys (comma-separated)"
1487
- ).option("--limit <n>", "Max results", "20").option("--no-media", "Exclude media results").action(
1725
+ ).option("--limit <n>", "Max results", "20").action(
1488
1726
  withErrorHandler(
1489
1727
  globalOpts,
1490
1728
  async (query, cmdOpts) => {
1491
1729
  const opts = globalOpts();
1492
- const client = await createClient(opts);
1730
+ const client = await createPlatformClient(opts);
1493
1731
  const modelKeys = typeof cmdOpts.models === "string" ? cmdOpts.models.split(",").map((k) => k.trim()) : void 0;
1494
- const data = await client.request(GlobalSearchDocument, {
1732
+ const result = await client.records.globalSearch({
1495
1733
  query,
1496
1734
  limit: parseInt(String(cmdOpts.limit ?? "20"), 10),
1497
- modelKeys,
1498
- includeMedia: cmdOpts.media !== false
1735
+ modelKeys
1499
1736
  });
1500
1737
  if (opts.json || opts.jsonl) {
1501
- formatOutput(data.globalSearch, opts);
1738
+ formatOutput(result, opts);
1502
1739
  return;
1503
1740
  }
1504
- if (data.globalSearch.records.length > 0) {
1741
+ if (result.items.length > 0) {
1505
1742
  console.log(`
1506
- Records (${data.globalSearch.records.length}):`);
1743
+ Records (${result.items.length}):`);
1507
1744
  formatList(
1508
- data.globalSearch.records,
1745
+ result.items.map((item) => ({
1746
+ id: item.id,
1747
+ modelKey: item.modelKey,
1748
+ naturalKey: item.naturalKey ?? "",
1749
+ score: item.score
1750
+ })),
1509
1751
  opts,
1510
1752
  {
1511
1753
  columns: [
1512
1754
  { key: "id", header: "ID", width: 28 },
1513
1755
  { key: "modelKey", header: "Model", width: 18 },
1514
- { key: "title", header: "Title", width: 28 },
1515
1756
  { key: "naturalKey", header: "Key", width: 20 },
1516
1757
  {
1517
- key: "updatedAt",
1518
- header: "Updated",
1519
- width: 12,
1520
- format: (v) => timeAgo(v)
1758
+ key: "score",
1759
+ header: "Score",
1760
+ width: 8,
1761
+ format: (v) => Number(v).toFixed(2)
1521
1762
  }
1522
1763
  ]
1523
1764
  }
1524
1765
  );
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) {
1766
+ } else {
1542
1767
  console.log("No results found.");
1543
1768
  }
1544
1769
  }
@@ -1549,9 +1774,9 @@ Media (${data.globalSearch.media.length}):`);
1549
1774
  // src/commands/init.ts
1550
1775
  import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
1551
1776
  import { writeFile } from "fs/promises";
1552
- import { resolve as resolve2, join as join4 } from "path";
1777
+ import { resolve as resolve3, join as join4 } from "path";
1553
1778
  import chalk5 from "chalk";
1554
- import inquirer3 from "inquirer";
1779
+ import inquirer4 from "inquirer";
1555
1780
  var FIELD_DEFAULTS = {
1556
1781
  text: "",
1557
1782
  richtext: "",
@@ -1613,9 +1838,9 @@ function generateRecordSeed(model) {
1613
1838
  )) {
1614
1839
  continue;
1615
1840
  }
1616
- if (field.type === "list" && field.items) {
1841
+ if (field.type === "list" && field.itemType) {
1617
1842
  data[field.key] = [
1618
- defaultValueForField({ key: field.key, type: field.items.type })
1843
+ defaultValueForField({ key: field.key, type: field.itemType })
1619
1844
  ];
1620
1845
  } else {
1621
1846
  data[field.key] = defaultValueForField(field);
@@ -1640,7 +1865,7 @@ function registerInitCommands(program2, globalOpts) {
1640
1865
  async (key, opts) => {
1641
1866
  const globalFlags = globalOpts();
1642
1867
  const template = generateModelTemplate(key);
1643
- const outDir = resolve2(opts.output);
1868
+ const outDir = resolve3(opts.output);
1644
1869
  if (!existsSync3(outDir)) {
1645
1870
  mkdirSync2(outDir, { recursive: true });
1646
1871
  }
@@ -1671,10 +1896,19 @@ Edit the file, then run:
1671
1896
  globalOpts,
1672
1897
  async (opts) => {
1673
1898
  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;
1899
+ const client = await createPlatformClient(globalFlags);
1900
+ const result = await client.models.listModels({ limit: 100 });
1901
+ const models = result.items.map((m) => ({
1902
+ key: m.key,
1903
+ name: m.name,
1904
+ fields: (m.fields ?? []).map((f) => ({
1905
+ key: f.key,
1906
+ type: f.type,
1907
+ label: f.label,
1908
+ required: f.required,
1909
+ itemType: f.itemType
1910
+ }))
1911
+ }));
1678
1912
  if (models.length === 0) {
1679
1913
  console.log(
1680
1914
  chalk5.yellow(
@@ -1699,7 +1933,7 @@ Edit the file, then run:
1699
1933
  selectedModels.push(found);
1700
1934
  }
1701
1935
  } else {
1702
- const { selected } = await inquirer3.prompt([
1936
+ const { selected } = await inquirer4.prompt([
1703
1937
  {
1704
1938
  type: "checkbox",
1705
1939
  name: "selected",
@@ -1718,7 +1952,7 @@ Edit the file, then run:
1718
1952
  console.log("No models selected.");
1719
1953
  return;
1720
1954
  }
1721
- const outDir = resolve2(opts.output);
1955
+ const outDir = resolve3(opts.output);
1722
1956
  if (!existsSync3(outDir)) {
1723
1957
  mkdirSync2(outDir, { recursive: true });
1724
1958
  }
@@ -1754,64 +1988,12 @@ Edit the files, then run:
1754
1988
  import chalk6 from "chalk";
1755
1989
  import { existsSync as existsSync4 } from "fs";
1756
1990
  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
1991
  var CONFIG_FILE_NAMES = [
1783
1992
  "foir.config.ts",
1784
1993
  "foir.config.js",
1785
1994
  "foir.config.mjs",
1786
1995
  "foir.config.json"
1787
1996
  ];
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
1997
  function discoverConfigFile() {
1816
1998
  for (const name of CONFIG_FILE_NAMES) {
1817
1999
  const path3 = resolve4(process.cwd(), name);
@@ -1836,69 +2018,32 @@ function registerPushCommand(program2, globalOpts) {
1836
2018
  console.log(chalk6.dim(`Loading ${configPath}...`));
1837
2019
  const config2 = await loadConfig(configPath);
1838
2020
  if (!config2?.key || !config2?.name) {
1839
- throw new Error(
1840
- 'Config must have at least "key" and "name" fields.'
1841
- );
1842
- }
1843
- if (opts.force) {
1844
- config2.force = true;
1845
- }
1846
- const client = await createClient(globalOpts());
1847
- console.log(
1848
- chalk6.dim(`Pushing config "${config2.key}" to platform...`)
1849
- );
1850
- const data = await client.request(
1851
- APPLY_CONFIG_MUTATION,
1852
- { input: config2 }
1853
- );
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."));
1860
- }
1861
- console.log();
1862
- console.log(` Config ID: ${chalk6.cyan(result.configId)}`);
1863
- console.log(` Config Key: ${chalk6.cyan(result.configKey)}`);
1864
- 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)}`
2021
+ throw new Error(
2022
+ 'Config must have at least "key" and "name" fields.'
1894
2023
  );
1895
- console.log();
1896
- console.log(
1897
- chalk6.dim(
1898
- "These credentials are only shown once. Store them securely."
1899
- )
2024
+ }
2025
+ if (opts.force) {
2026
+ config2.force = true;
2027
+ }
2028
+ const client = await createPlatformClient(globalOpts());
2029
+ console.log(
2030
+ chalk6.dim(`Pushing config "${config2.key}" to platform...`)
2031
+ );
2032
+ const result = await client.configs.applyConfig(
2033
+ config2.key,
2034
+ config2
2035
+ );
2036
+ if (!result) {
2037
+ throw new Error(
2038
+ "Failed to apply config \u2014 no response from server."
1900
2039
  );
1901
2040
  }
2041
+ console.log();
2042
+ console.log(chalk6.green("Config applied successfully."));
2043
+ console.log();
2044
+ console.log(` Config ID: ${chalk6.cyan(result.id)}`);
2045
+ console.log(` Config Key: ${chalk6.cyan(result.key)}`);
2046
+ console.log();
1902
2047
  }
1903
2048
  )
1904
2049
  );
@@ -1906,40 +2051,19 @@ function registerPushCommand(program2, globalOpts) {
1906
2051
 
1907
2052
  // src/commands/remove.ts
1908
2053
  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
- );
2054
+ import inquirer5 from "inquirer";
1931
2055
  function registerRemoveCommand(program2, globalOpts) {
1932
2056
  program2.command("remove <key>").description("Remove a config and all its provisioned resources").option("--force", "Skip confirmation prompt", false).action(
1933
2057
  withErrorHandler(
1934
2058
  globalOpts,
1935
2059
  async (key, opts) => {
1936
- const client = await createClient(globalOpts());
1937
- const { configByKey: config2 } = await client.request(GET_CONFIG_QUERY, { key });
2060
+ const client = await createPlatformClient(globalOpts());
2061
+ const config2 = await client.configs.getConfigByKey(key);
1938
2062
  if (!config2) {
1939
2063
  throw new Error(`Config not found: ${key}`);
1940
2064
  }
1941
2065
  if (!opts.force) {
1942
- const { confirmed } = await inquirer4.prompt([
2066
+ const { confirmed } = await inquirer5.prompt([
1943
2067
  {
1944
2068
  type: "confirm",
1945
2069
  name: "confirmed",
@@ -1952,7 +2076,7 @@ function registerRemoveCommand(program2, globalOpts) {
1952
2076
  return;
1953
2077
  }
1954
2078
  }
1955
- await client.request(UNREGISTER_MUTATION, { id: config2.id });
2079
+ await client.configs.deleteConfig(config2.id);
1956
2080
  console.log(
1957
2081
  chalk7.green(`Removed config "${config2.name}" (${config2.key}).`)
1958
2082
  );
@@ -1963,49 +2087,6 @@ function registerRemoveCommand(program2, globalOpts) {
1963
2087
 
1964
2088
  // src/commands/profiles.ts
1965
2089
  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;
2006
- }
2007
-
2008
- // src/commands/profiles.ts
2009
2090
  function registerProfilesCommand(program2, globalOpts) {
2010
2091
  const profiles = program2.command("profiles").description("Manage named project profiles");
2011
2092
  profiles.command("list").description("List all saved project profiles").action(
@@ -2173,9 +2254,8 @@ function registerProfilesCommand(program2, globalOpts) {
2173
2254
  }
2174
2255
 
2175
2256
  // 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";
2257
+ import { readdirSync } from "fs";
2258
+ import { resolve as resolve5 } from "path";
2179
2259
  import chalk9 from "chalk";
2180
2260
 
2181
2261
  // ../command-registry/src/command-map.ts
@@ -2953,68 +3033,6 @@ var COMMANDS = [
2953
3033
  successMessage: "Updated customer profile"
2954
3034
  },
2955
3035
  // =========================================================================
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
3036
  // OPERATIONS
3019
3037
  // =========================================================================
3020
3038
  {
@@ -3582,230 +3600,397 @@ var COMMANDS = [
3582
3600
  }
3583
3601
  ];
3584
3602
 
3585
- // ../command-registry/src/schema-engine.ts
3586
- import {
3587
- buildSchema,
3588
- isObjectType,
3589
- isInputObjectType,
3590
- isListType,
3591
- isNonNullType,
3592
- isScalarType,
3593
- isEnumType
3594
- } 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;
3603
+ // src/commands/register-commands.ts
3604
+ import { RecordType } from "@foir/connect-clients/records";
3605
+ function buildDispatchTable() {
3606
+ return {
3607
+ // ── Models ──────────────────────────────────────────────────
3608
+ models: {
3609
+ models: async (_v, c) => wrapList(
3610
+ await c.models.listModels({
3611
+ limit: num(_v.limit, 50),
3612
+ search: str(_v.search),
3613
+ category: str(_v.category),
3614
+ offset: num(_v.offset, 0)
3615
+ })
3616
+ ),
3617
+ modelByKey: async (v, c) => await c.models.getModelByKey(str(v.key)),
3618
+ model: async (v, c) => await c.models.getModel(str(v.id)),
3619
+ createModel: async (v, c) => {
3620
+ const input = v.input;
3621
+ if (!input) throw new Error("Input required (--data or --file)");
3622
+ return await c.models.createModel({
3623
+ key: str(input.key),
3624
+ name: str(input.name),
3625
+ fields: input.fields ?? [],
3626
+ config: input.config,
3627
+ configId: str(input.configId)
3628
+ });
3629
+ },
3630
+ updateModel: async (v, c) => {
3631
+ const input = v.input;
3632
+ if (!input) throw new Error("Input required (--data or --file)");
3633
+ return await c.models.updateModel({
3634
+ id: str(input.id ?? v.id),
3635
+ name: str(input.name),
3636
+ fields: input.fields,
3637
+ config: input.config,
3638
+ changeDescription: str(input.changeDescription)
3639
+ });
3640
+ },
3641
+ deleteModel: async (v, c) => await c.models.deleteModel(str(v.id)),
3642
+ modelVersions: async (v, c) => wrapList(
3643
+ await c.models.listModelVersions(str(v.modelId), {
3644
+ limit: num(v.limit, 50)
3645
+ })
3646
+ )
3647
+ },
3648
+ // ── Records ─────────────────────────────────────────────────
3649
+ records: {
3650
+ records: async (v, c) => wrapList(
3651
+ await c.records.listRecords({
3652
+ modelKey: str(v.modelKey),
3653
+ limit: num(v.limit, 50),
3654
+ offset: num(v.offset, 0),
3655
+ search: str(v.search)
3656
+ })
3657
+ ),
3658
+ record: async (v, c) => await c.records.getRecord(str(v.id)),
3659
+ recordByKey: async (v, c) => await c.records.getRecordByKey(str(v.modelKey), str(v.naturalKey)),
3660
+ createRecord: async (v, c) => {
3661
+ const input = v.input;
3662
+ if (!input) throw new Error("Input required (--data or --file)");
3663
+ return await c.records.createRecord({
3664
+ modelKey: str(input.modelKey),
3665
+ naturalKey: str(input.naturalKey),
3666
+ data: input.data,
3667
+ customerId: str(input.customerId),
3668
+ changeDescription: str(input.changeDescription)
3669
+ });
3670
+ },
3671
+ updateRecord: async (v, c) => {
3672
+ const input = v.input;
3673
+ if (!input) throw new Error("Input required (--data or --file)");
3674
+ return await c.records.updateRecord({
3675
+ id: str(input.id ?? v.id),
3676
+ data: input.data ?? input,
3677
+ naturalKey: str(input.naturalKey)
3678
+ });
3679
+ },
3680
+ deleteRecord: async (v, c) => await c.records.deleteRecord(str(v.id)),
3681
+ publishVersion: async (v, c) => await c.records.publishVersion(str(v.versionId)),
3682
+ unpublishRecord: async (v, c) => await c.records.unpublishRecord(str(v.id)),
3683
+ duplicateRecord: async (v, c) => {
3684
+ const input = v.input;
3685
+ return await c.records.duplicateRecord(
3686
+ str(input?.id ?? v.id),
3687
+ str(input?.newNaturalKey)
3688
+ );
3689
+ },
3690
+ recordVersions: async (v, c) => wrapList(
3691
+ await c.records.listRecordVersions(str(v.parentId), {
3692
+ limit: num(v.limit, 50)
3693
+ })
3694
+ ),
3695
+ recordVariants: async (v, c) => wrapList(
3696
+ await c.records.listRecordVariants(str(v.recordId), {
3697
+ limit: num(v.limit, 50)
3698
+ })
3699
+ ),
3700
+ createVersion: async (v, c) => {
3701
+ const input = v.input;
3702
+ if (!input) throw new Error("Input required (--data or --file)");
3703
+ return await c.records.createVersion(
3704
+ str(input.parentId),
3705
+ input.data,
3706
+ str(input.changeDescription)
3707
+ );
3708
+ },
3709
+ createVariant: async (v, c) => {
3710
+ const input = v.input;
3711
+ if (!input) throw new Error("Input required (--data or --file)");
3712
+ return await c.records.createVariant(
3713
+ str(input.recordId),
3714
+ str(input.variantKey),
3715
+ input.data
3716
+ );
3683
3717
  }
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;
3718
+ },
3719
+ // ── Locales ─────────────────────────────────────────────────
3720
+ locales: {
3721
+ locales: async (v, c) => {
3722
+ const resp = await c.settings.listLocales({ limit: num(v.limit, 50) });
3723
+ return { items: resp.locales ?? [], total: resp.total ?? 0 };
3724
+ },
3725
+ locale: async (v, c) => await c.settings.getLocale(str(v.id)),
3726
+ createLocale: async (v, c) => {
3727
+ const input = v.input;
3728
+ if (!input) throw new Error("Input required");
3729
+ return await c.settings.createLocale({
3730
+ locale: str(input.locale),
3731
+ displayName: str(input.displayName),
3732
+ nativeName: str(input.nativeName),
3733
+ isDefault: input.isDefault,
3734
+ isRtl: input.isRtl,
3735
+ fallbackLocale: str(input.fallbackLocale)
3736
+ });
3737
+ },
3738
+ updateLocale: async (v, c) => {
3739
+ const input = v.input;
3740
+ if (!input) throw new Error("Input required");
3741
+ return await c.settings.updateLocale({
3742
+ id: str(input.id ?? v.id),
3743
+ displayName: str(input.displayName),
3744
+ nativeName: str(input.nativeName),
3745
+ isDefault: input.isDefault,
3746
+ isActive: input.isActive,
3747
+ isRtl: input.isRtl,
3748
+ fallbackLocale: str(input.fallbackLocale)
3749
+ });
3750
+ },
3751
+ deleteLocale: async (v, c) => await c.settings.deleteLocale(str(v.id))
3752
+ },
3753
+ // ── Segments ────────────────────────────────────────────────
3754
+ segments: {
3755
+ segments: async (v, c) => {
3756
+ const resp = await c.segments.listSegments({ limit: num(v.limit, 50) });
3757
+ return { items: resp.segments ?? [], total: resp.total ?? 0 };
3758
+ },
3759
+ segment: async (v, c) => await c.segments.getSegment(str(v.id)),
3760
+ segmentByKey: async (v, c) => await c.segments.getSegmentByKey(str(v.key)),
3761
+ createSegment: async (v, c) => {
3762
+ const input = v.input;
3763
+ if (!input) throw new Error("Input required");
3764
+ return await c.segments.createSegment({
3765
+ key: str(input.key),
3766
+ name: str(input.name),
3767
+ description: str(input.description),
3768
+ rules: input.rules,
3769
+ evaluationMode: str(input.evaluationMode),
3770
+ isActive: input.isActive
3771
+ });
3772
+ },
3773
+ updateSegment: async (v, c) => {
3774
+ const input = v.input;
3775
+ if (!input) throw new Error("Input required");
3776
+ return await c.segments.updateSegment({
3777
+ id: str(input.id ?? v.id),
3778
+ name: str(input.name),
3779
+ description: str(input.description),
3780
+ rules: input.rules,
3781
+ evaluationMode: str(input.evaluationMode),
3782
+ isActive: input.isActive
3783
+ });
3784
+ },
3785
+ deleteSegment: async (v, c) => await c.segments.deleteSegment(str(v.id)),
3786
+ previewSegmentRules: async (v, c) => {
3787
+ const input = v.rules;
3788
+ return await c.segments.previewSegmentRules(input);
3789
+ },
3790
+ testSegmentEvaluation: async (v, c) => await c.segments.testSegmentEvaluation(
3791
+ str(v.segmentId),
3792
+ str(v.customerId)
3793
+ )
3794
+ },
3795
+ // ── Experiments ─────────────────────────────────────────────
3796
+ experiments: {
3797
+ experiments: async (v, c) => {
3798
+ const resp = await c.experiments.listExperiments({
3799
+ limit: num(v.limit, 50)
3800
+ });
3801
+ return { items: resp.experiments ?? [], total: resp.total ?? 0 };
3802
+ },
3803
+ experiment: async (v, c) => await c.experiments.getExperiment(str(v.id)),
3804
+ experimentByKey: async (v, c) => await c.experiments.getExperimentByKey(str(v.key)),
3805
+ createExperiment: async (v, c) => {
3806
+ const input = v.input;
3807
+ if (!input) throw new Error("Input required");
3808
+ return await c.experiments.createExperiment({
3809
+ key: str(input.key),
3810
+ name: str(input.name),
3811
+ description: str(input.description),
3812
+ targeting: input.targeting,
3813
+ controlPercent: input.controlPercent,
3814
+ variants: input.variants
3815
+ });
3816
+ },
3817
+ updateExperiment: async (v, c) => {
3818
+ const input = v.input;
3819
+ if (!input) throw new Error("Input required");
3820
+ return await c.experiments.updateExperiment({
3821
+ id: str(input.id ?? v.id),
3822
+ name: str(input.name),
3823
+ description: str(input.description),
3824
+ targeting: input.targeting,
3825
+ controlPercent: input.controlPercent,
3826
+ variants: input.variants
3827
+ });
3828
+ },
3829
+ deleteExperiment: async (v, c) => await c.experiments.deleteExperiment(str(v.id)),
3830
+ startExperiment: async (v, c) => await c.experiments.startExperiment(str(v.experimentId)),
3831
+ pauseExperiment: async (v, c) => await c.experiments.pauseExperiment(str(v.experimentId)),
3832
+ resumeExperiment: async (v, c) => await c.experiments.resumeExperiment(str(v.experimentId)),
3833
+ endExperiment: async (v, c) => await c.experiments.endExperiment(str(v.experimentId)),
3834
+ experimentStats: async (v, c) => await c.experiments.getExperimentStats(str(v.experimentId))
3835
+ },
3836
+ // ── Settings ────────────────────────────────────────────────
3837
+ settings: {
3838
+ allSettings: async (v, c) => await c.settings.getSettings({
3839
+ category: str(v.category),
3840
+ key: str(v.key)
3841
+ }),
3842
+ setting: async (v, c) => {
3843
+ const settings = await c.settings.getSettings({ key: str(v.key) });
3844
+ return settings[0] ?? null;
3845
+ },
3846
+ setSetting: async (v, c) => {
3847
+ const input = v.input;
3848
+ if (!input) throw new Error("Input required");
3849
+ return await c.settings.updateSetting({
3850
+ key: str(input.key),
3851
+ value: input.value ?? input
3852
+ });
3704
3853
  }
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 });
3854
+ },
3855
+ // ── API Keys ────────────────────────────────────────────────
3856
+ "api-keys": {
3857
+ listApiKeys: async (v, c) => wrapList(await c.identity.listApiKeys({ limit: num(v.limit, 50) })),
3858
+ createApiKey: async (v, c) => {
3859
+ const input = v.input;
3860
+ if (!input) throw new Error("Input required");
3861
+ return await c.identity.createApiKey({
3862
+ name: str(input.name),
3863
+ keyType: input.keyType,
3864
+ rateLimitPerHour: input.rateLimitPerHour
3865
+ });
3866
+ },
3867
+ rotateApiKey: async (v, c) => await c.identity.rotateApiKey(str(v.id)),
3868
+ revokeApiKey: async (v, c) => await c.identity.revokeApiKey(str(v.id))
3869
+ },
3870
+ // ── Auth Providers ──────────────────────────────────────────
3871
+ "auth-providers": {
3872
+ customerAuthProviders: async (v, c) => {
3873
+ const resp = await c.identity.listAuthProviders({
3874
+ limit: num(v.limit, 50)
3875
+ });
3876
+ return { items: resp.items, total: resp.total };
3877
+ },
3878
+ customerAuthProvider: async (v, c) => await c.identity.getAuthProvider(str(v.id)),
3879
+ createCustomerAuthProvider: async (v, c) => {
3880
+ const input = v.input;
3881
+ if (!input) throw new Error("Input required");
3882
+ return await c.identity.createAuthProvider({
3883
+ key: str(input.key),
3884
+ name: str(input.name),
3885
+ type: str(input.type),
3886
+ config: input.config,
3887
+ enabled: input.enabled,
3888
+ isDefault: input.isDefault,
3889
+ priority: input.priority
3890
+ });
3891
+ },
3892
+ updateCustomerAuthProvider: async (v, c) => {
3893
+ const input = v.input;
3894
+ if (!input) throw new Error("Input required");
3895
+ return await c.identity.updateAuthProvider({
3896
+ id: str(input.id ?? v.id),
3897
+ name: str(input.name),
3898
+ config: input.config,
3899
+ enabled: input.enabled,
3900
+ isDefault: input.isDefault,
3901
+ priority: input.priority
3902
+ });
3903
+ },
3904
+ deleteCustomerAuthProvider: async (v, c) => await c.identity.deleteAuthProvider(str(v.id))
3905
+ },
3906
+ // ── Configs ─────────────────────────────────────────────────
3907
+ configs: {
3908
+ configs: async (v, c) => {
3909
+ const resp = await c.configs.listConfigs({ limit: num(v.limit, 50) });
3910
+ return { items: resp.configs ?? [], total: resp.total ?? 0 };
3911
+ },
3912
+ config: async (v, c) => await c.configs.getConfig(str(v.id)),
3913
+ configByKey: async (v, c) => await c.configs.getConfigByKey(str(v.key)),
3914
+ registerConfig: async (v, c) => {
3915
+ const input = v.input;
3916
+ if (!input) throw new Error("Input required");
3917
+ return await c.configs.createConfig({
3918
+ key: str(input.key),
3919
+ configType: str(input.configType),
3920
+ name: str(input.name),
3921
+ description: str(input.description),
3922
+ config: input.config,
3923
+ enabled: input.enabled,
3924
+ direction: str(input.direction)
3925
+ });
3926
+ },
3927
+ applyConfig: async (v, c) => {
3928
+ const input = v.input;
3929
+ if (!input) throw new Error("Input required");
3930
+ return await c.configs.applyConfig(
3931
+ str(input.key ?? input.configKey),
3932
+ input
3933
+ );
3934
+ },
3935
+ unregisterConfig: async (v, c) => await c.configs.deleteConfig(str(v.id)),
3936
+ triggerConfigSync: async (v, _c) => {
3937
+ console.log(
3938
+ chalk9.yellow(
3939
+ `Config sync trigger for ${str(v.configId)} is not yet available via ConnectRPC.`
3940
+ )
3941
+ );
3942
+ return { success: false };
3725
3943
  }
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} }` : "";
3944
+ },
3945
+ // ── Customers ───────────────────────────────────────────────
3946
+ customers: {
3947
+ customers: async (v, c) => wrapList(
3948
+ await c.identity.listCustomers({
3949
+ limit: num(v.limit, 50),
3950
+ search: str(v.search)
3951
+ })
3952
+ ),
3953
+ customer: async (v, c) => {
3954
+ const result = await c.identity.getCustomer(str(v.id));
3955
+ return result.customer;
3956
+ },
3957
+ createCustomer: async (v, c) => {
3958
+ const input = v.input;
3959
+ if (!input) throw new Error("Input required");
3960
+ const result = await c.identity.createCustomer({
3961
+ email: str(input.email),
3962
+ password: str(input.password)
3963
+ });
3964
+ return result.customer;
3965
+ },
3966
+ updateCustomer: async (v, c) => {
3967
+ const input = v.input;
3968
+ if (!input) throw new Error("Input required");
3969
+ const result = await c.identity.updateCustomer({
3970
+ id: str(input.id ?? v.id),
3971
+ email: str(input.email),
3972
+ status: input.status
3973
+ });
3974
+ return result.customer;
3975
+ },
3976
+ deleteCustomer: async (v, c) => await c.identity.deleteCustomer(str(v.id)),
3977
+ suspendCustomer: async (v, c) => {
3978
+ const result = await c.identity.suspendCustomer(str(v.id));
3979
+ return result.customer;
3741
3980
  }
3742
3981
  }
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
3982
  };
3774
3983
  }
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
- }
3984
+ function str(v) {
3985
+ return v != null ? String(v) : void 0;
3796
3986
  }
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 };
3987
+ function num(v, fallback) {
3988
+ if (v == null) return fallback;
3989
+ const n = Number(v);
3990
+ return Number.isNaN(n) ? fallback : n;
3991
+ }
3992
+ function wrapList(result) {
3993
+ return result;
3809
3994
  }
3810
3995
  function toCliColumns(columns) {
3811
3996
  if (!columns) return void 0;
@@ -3824,8 +4009,8 @@ function toCliColumns(columns) {
3824
4009
  break;
3825
4010
  case "truncate":
3826
4011
  cliCol.format = (v) => {
3827
- const str = String(v ?? "");
3828
- return str.length > 40 ? str.slice(0, 39) + "\u2026" : str;
4012
+ const s = String(v ?? "");
4013
+ return s.length > 40 ? s.slice(0, 39) + "\u2026" : s;
3829
4014
  };
3830
4015
  break;
3831
4016
  case "join":
@@ -3833,27 +4018,37 @@ function toCliColumns(columns) {
3833
4018
  break;
3834
4019
  case "bytes":
3835
4020
  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`;
4021
+ const n = Number(v);
4022
+ if (n < 1024) return `${n} B`;
4023
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
4024
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
3840
4025
  };
3841
4026
  break;
3842
4027
  }
3843
4028
  return cliCol;
3844
4029
  });
3845
4030
  }
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)}`
4031
+ function autoColumns(items) {
4032
+ if (items.length === 0) return [];
4033
+ const first = items[0];
4034
+ return Object.keys(first).filter((k) => k !== "__typename" && k !== "$typeName" && k !== "$unknown").slice(0, 6).map((key) => ({
4035
+ key,
4036
+ header: key,
4037
+ width: 20
4038
+ }));
4039
+ }
4040
+ var DISPATCH = buildDispatchTable();
4041
+ async function executeCommand(entry, variables, client) {
4042
+ const groupHandlers = DISPATCH[entry.group];
4043
+ const handler = groupHandlers?.[entry.operation];
4044
+ if (!handler) {
4045
+ throw new Error(
4046
+ `Command not yet migrated to ConnectRPC: ${entry.group}.${entry.operation}`
3854
4047
  );
3855
- return;
3856
4048
  }
4049
+ return handler(variables, client);
4050
+ }
4051
+ function registerDynamicCommands(program2, globalOpts) {
3857
4052
  const groups = /* @__PURE__ */ new Map();
3858
4053
  for (const cmd of COMMANDS) {
3859
4054
  if (!groups.has(cmd.group)) groups.set(cmd.group, []);
@@ -3862,42 +4057,15 @@ function registerDynamicCommands(program2, globalOpts) {
3862
4057
  for (const [groupName, entries] of groups) {
3863
4058
  const group = program2.command(groupName).description(`Manage ${groupName}`);
3864
4059
  for (const entry of entries) {
4060
+ if (!DISPATCH[entry.group]?.[entry.operation]) continue;
3865
4061
  let cmd = group.command(entry.name).description(entry.description);
3866
4062
  for (const pos of entry.positionalArgs ?? []) {
3867
4063
  cmd = cmd.argument(`<${pos.name}>`, pos.description ?? pos.name);
3868
4064
  }
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
- }
4065
+ cmd = cmd.option("--limit <n>", "Max results");
3881
4066
  if (entry.acceptsInput) {
3882
4067
  cmd = cmd.option("-d, --data <json>", "Data as JSON");
3883
4068
  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
4069
  }
3902
4070
  for (const cf of entry.customFlags ?? []) {
3903
4071
  cmd = cmd.option(cf.flag, cf.description);
@@ -3908,7 +4076,7 @@ function registerDynamicCommands(program2, globalOpts) {
3908
4076
  cmd.action(
3909
4077
  withErrorHandler(globalOpts, async (...actionArgs) => {
3910
4078
  const opts = globalOpts();
3911
- const client = await createClient(opts);
4079
+ const client = await createPlatformClient(opts);
3912
4080
  const variables = {};
3913
4081
  const positionals = entry.positionalArgs ?? [];
3914
4082
  for (let i = 0; i < positionals.length; i++) {
@@ -3921,27 +4089,27 @@ function registerDynamicCommands(program2, globalOpts) {
3921
4089
  const flags = actionArgs[positionals.length] ?? {};
3922
4090
  const customFlagNames = new Set(
3923
4091
  (entry.customFlags ?? []).map(
3924
- (cf) => cf.flag.replace(/ <.*>$/, "").replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase())
4092
+ (cf) => cf.flag.replace(/ <.*>$/, "").replace(/^--/, "").replace(/-([a-z])/g, (_, ch) => ch.toUpperCase())
3925
4093
  )
3926
4094
  );
3927
- const rawFlags = {};
3928
4095
  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);
4096
+ if (key === "data" || key === "file" || key === "confirm" || key === "limit")
4097
+ continue;
4098
+ if (customFlagNames.has(key)) {
4099
+ variables[key] = val;
4100
+ continue;
4101
+ }
4102
+ variables[key] = val;
4103
+ }
4104
+ if (flags.limit) {
4105
+ variables.limit = flags.limit;
3932
4106
  }
3933
- const coerced = engine.coerceArgs(
3934
- entry.operation,
3935
- entry.operationType,
3936
- rawFlags
3937
- );
3938
- Object.assign(variables, coerced);
3939
4107
  if (flags.dir && entry.acceptsInput) {
3940
4108
  const dirPath = resolve5(String(flags.dir));
3941
4109
  const files = readdirSync(dirPath).filter((f) => /\.(json|ts|js|mjs)$/.test(f)).sort();
3942
4110
  if (files.length === 0) {
3943
4111
  console.error(
3944
- chalk9.yellow(`\u26A0 No .json/.ts/.js files found in ${dirPath}`)
4112
+ chalk9.yellow(`No .json/.ts/.js files found in ${dirPath}`)
3945
4113
  );
3946
4114
  return;
3947
4115
  }
@@ -3955,8 +4123,7 @@ function registerDynamicCommands(program2, globalOpts) {
3955
4123
  const fileVars = { ...variables, [argName]: fileData };
3956
4124
  const label = fileData.key ?? fileData.name ?? file;
3957
4125
  try {
3958
- const q = engine.buildQuery(entry, fileVars);
3959
- await client.request(q, fileVars);
4126
+ await executeCommand(entry, fileVars, client);
3960
4127
  created++;
3961
4128
  if (!(opts.json || opts.jsonl || opts.quiet)) {
3962
4129
  success(`Created ${label}`);
@@ -3975,8 +4142,7 @@ function registerDynamicCommands(program2, globalOpts) {
3975
4142
  [updateEntry.positionalArgs[0].graphqlArg]: fileData.key
3976
4143
  } : {}
3977
4144
  };
3978
- const uq = engine.buildQuery(updateEntry, updateVars);
3979
- await client.request(uq, updateVars);
4145
+ await executeCommand(updateEntry, updateVars, client);
3980
4146
  updated++;
3981
4147
  if (!(opts.json || opts.jsonl || opts.quiet)) {
3982
4148
  success(`Updated ${label}`);
@@ -4010,19 +4176,13 @@ function registerDynamicCommands(program2, globalOpts) {
4010
4176
  data: flags.data,
4011
4177
  file: flags.file
4012
4178
  });
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) {
4179
+ if (!inputData.projectId || !inputData.tenantId) {
4020
4180
  const resolved = await resolveProjectContext(opts);
4021
4181
  if (resolved) {
4022
- if (fieldNames.has("projectId") && !inputData.projectId) {
4182
+ if (!inputData.projectId) {
4023
4183
  inputData.projectId = resolved.project.id;
4024
4184
  }
4025
- if (fieldNames.has("tenantId") && !inputData.tenantId) {
4185
+ if (!inputData.tenantId) {
4026
4186
  inputData.tenantId = resolved.project.tenantId;
4027
4187
  }
4028
4188
  }
@@ -4053,12 +4213,7 @@ function registerDynamicCommands(program2, globalOpts) {
4053
4213
  if (flags.modelKey) {
4054
4214
  variables.modelKey = String(flags.modelKey);
4055
4215
  }
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);
4216
+ const data2 = await executeCommand(altEntry, variables, client);
4062
4217
  formatOutput(data2, opts);
4063
4218
  return;
4064
4219
  }
@@ -4075,13 +4230,10 @@ function registerDynamicCommands(program2, globalOpts) {
4075
4230
  if (entry.group === "records" && entry.name === "publish" && variables.versionId) {
4076
4231
  const versionIdValue = String(variables.versionId);
4077
4232
  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;
4233
+ const record = await client.records.getRecordByKey(
4234
+ String(flags.modelKey),
4235
+ versionIdValue
4236
+ );
4085
4237
  if (!record?.currentVersionId) {
4086
4238
  throw new Error(
4087
4239
  `No current version found for record "${versionIdValue}"`
@@ -4094,23 +4246,18 @@ function registerDynamicCommands(program2, globalOpts) {
4094
4246
  );
4095
4247
  } else {
4096
4248
  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) {
4249
+ const record = await client.records.getRecord(versionIdValue);
4250
+ if (record?.recordType === RecordType.RECORD && record?.currentVersionId) {
4103
4251
  variables.versionId = record.currentVersionId;
4104
4252
  }
4105
4253
  } catch {
4106
4254
  }
4107
4255
  }
4108
4256
  }
4109
- const queryStr = engine.buildQuery(entry, variables);
4110
- let result;
4257
+ let data;
4111
4258
  let usedUpdate = false;
4112
4259
  try {
4113
- result = await client.request(queryStr, variables);
4260
+ data = await executeCommand(entry, variables, client);
4114
4261
  } catch (createErr) {
4115
4262
  if (flags.upsert && entry.name === "create") {
4116
4263
  const updateEntry = COMMANDS.find(
@@ -4126,8 +4273,7 @@ function registerDynamicCommands(program2, globalOpts) {
4126
4273
  [updateEntry.positionalArgs[0].graphqlArg]: inputData.key
4127
4274
  } : {}
4128
4275
  };
4129
- const uq = engine.buildQuery(updateEntry, updateVars);
4130
- result = await client.request(uq, updateVars);
4276
+ data = await executeCommand(updateEntry, updateVars, client);
4131
4277
  usedUpdate = true;
4132
4278
  } else {
4133
4279
  throw createErr;
@@ -4139,8 +4285,14 @@ function registerDynamicCommands(program2, globalOpts) {
4139
4285
  const activeEntry = usedUpdate ? COMMANDS.find(
4140
4286
  (c) => c.group === entry.group && c.name === "update"
4141
4287
  ) ?? entry : entry;
4142
- const { data, total } = extractResult(result, activeEntry.operation);
4143
- const responseData = data && typeof data === "object" && !Array.isArray(data) ? data : void 0;
4288
+ let displayData = data;
4289
+ let total;
4290
+ if (data && typeof data === "object" && "items" in data) {
4291
+ const listResult = data;
4292
+ displayData = listResult.items;
4293
+ total = listResult.total;
4294
+ }
4295
+ const responseData = displayData && typeof displayData === "object" && !Array.isArray(displayData) ? displayData : void 0;
4144
4296
  const displayEntry = usedUpdate ? activeEntry : entry;
4145
4297
  if (displayEntry.scalarResult) {
4146
4298
  if (!(opts.json || opts.jsonl || opts.quiet)) {
@@ -4148,16 +4300,16 @@ function registerDynamicCommands(program2, globalOpts) {
4148
4300
  success(displayEntry.successMessage, responseData);
4149
4301
  }
4150
4302
  } else {
4151
- formatOutput(data, opts);
4303
+ formatOutput(displayData, opts);
4152
4304
  }
4153
- } else if (Array.isArray(data)) {
4305
+ } else if (Array.isArray(displayData)) {
4154
4306
  const cliColumns = toCliColumns(displayEntry.columns);
4155
- formatList(data, opts, {
4156
- columns: cliColumns ?? autoColumns(data),
4307
+ formatList(displayData, opts, {
4308
+ columns: cliColumns ?? autoColumns(displayData),
4157
4309
  total
4158
4310
  });
4159
4311
  } else {
4160
- formatOutput(data, opts);
4312
+ formatOutput(displayData, opts);
4161
4313
  if (displayEntry.successMessage && !(opts.json || opts.jsonl || opts.quiet)) {
4162
4314
  success(displayEntry.successMessage, responseData);
4163
4315
  }
@@ -4167,15 +4319,14 @@ function registerDynamicCommands(program2, globalOpts) {
4167
4319
  const record = responseData.record;
4168
4320
  const versionId = version2?.id ?? record?.currentVersionId;
4169
4321
  if (versionId) {
4170
- const publishQuery = `mutation PublishVersion($versionId: ID!) { publishVersion(versionId: $versionId) }`;
4171
- await client.request(publishQuery, { versionId });
4322
+ await client.records.publishVersion(versionId);
4172
4323
  if (!(opts.json || opts.jsonl || opts.quiet)) {
4173
4324
  success("Published version {id}", { id: versionId });
4174
4325
  }
4175
4326
  } else if (!(opts.json || opts.jsonl || opts.quiet)) {
4176
4327
  console.error(
4177
4328
  chalk9.yellow(
4178
- "\u26A0 Could not auto-publish: no version found in response"
4329
+ "Could not auto-publish: no version found in response"
4179
4330
  )
4180
4331
  );
4181
4332
  }
@@ -4185,20 +4336,11 @@ function registerDynamicCommands(program2, globalOpts) {
4185
4336
  }
4186
4337
  }
4187
4338
  }
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
4339
 
4198
4340
  // src/cli.ts
4199
- var __filename2 = fileURLToPath2(import.meta.url);
4200
- var __dirname2 = dirname5(__filename2);
4201
- config({ path: resolve6(__dirname2, "../.env.local") });
4341
+ var __filename = fileURLToPath(import.meta.url);
4342
+ var __dirname = dirname4(__filename);
4343
+ config({ path: resolve6(__dirname, "../.env.local") });
4202
4344
  var require2 = createRequire(import.meta.url);
4203
4345
  var { version } = require2("../package.json");
4204
4346
  var program = new Command();