@eide/foir-cli 0.1.46 → 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");
915
1105
  }
916
- if (result.storageKey) {
917
- console.log(chalk3.gray(` Key: ${result.storageKey}`));
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");
918
1126
  }
919
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;
1141
+ }
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");
1150
+ }
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,224 +1717,53 @@ function registerCreateConfigCommand(program2, globalOpts) {
1476
1717
  );
1477
1718
  }
1478
1719
 
1479
- // src/graphql/generated.ts
1480
- var GlobalSearchDocument = {
1481
- kind: "Document",
1482
- definitions: [
1483
- {
1484
- kind: "OperationDefinition",
1485
- operation: "query",
1486
- name: { kind: "Name", value: "GlobalSearch" },
1487
- variableDefinitions: [
1488
- {
1489
- kind: "VariableDefinition",
1490
- variable: {
1491
- kind: "Variable",
1492
- name: { kind: "Name", value: "query" }
1493
- },
1494
- type: {
1495
- kind: "NonNullType",
1496
- type: {
1497
- kind: "NamedType",
1498
- name: { kind: "Name", value: "String" }
1499
- }
1500
- }
1501
- },
1502
- {
1503
- kind: "VariableDefinition",
1504
- variable: {
1505
- kind: "Variable",
1506
- name: { kind: "Name", value: "limit" }
1507
- },
1508
- type: { kind: "NamedType", name: { kind: "Name", value: "Int" } }
1509
- },
1510
- {
1511
- kind: "VariableDefinition",
1512
- variable: {
1513
- kind: "Variable",
1514
- name: { kind: "Name", value: "modelKeys" }
1515
- },
1516
- type: {
1517
- kind: "ListType",
1518
- type: {
1519
- kind: "NonNullType",
1520
- type: {
1521
- kind: "NamedType",
1522
- name: { kind: "Name", value: "String" }
1523
- }
1524
- }
1525
- }
1526
- },
1527
- {
1528
- kind: "VariableDefinition",
1529
- variable: {
1530
- kind: "Variable",
1531
- name: { kind: "Name", value: "includeMedia" }
1532
- },
1533
- type: { kind: "NamedType", name: { kind: "Name", value: "Boolean" } }
1534
- }
1535
- ],
1536
- selectionSet: {
1537
- kind: "SelectionSet",
1538
- selections: [
1539
- {
1540
- kind: "Field",
1541
- name: { kind: "Name", value: "globalSearch" },
1542
- arguments: [
1543
- {
1544
- kind: "Argument",
1545
- name: { kind: "Name", value: "query" },
1546
- value: {
1547
- kind: "Variable",
1548
- name: { kind: "Name", value: "query" }
1549
- }
1550
- },
1551
- {
1552
- kind: "Argument",
1553
- name: { kind: "Name", value: "limit" },
1554
- value: {
1555
- kind: "Variable",
1556
- name: { kind: "Name", value: "limit" }
1557
- }
1558
- },
1559
- {
1560
- kind: "Argument",
1561
- name: { kind: "Name", value: "modelKeys" },
1562
- value: {
1563
- kind: "Variable",
1564
- name: { kind: "Name", value: "modelKeys" }
1565
- }
1566
- },
1567
- {
1568
- kind: "Argument",
1569
- name: { kind: "Name", value: "includeMedia" },
1570
- value: {
1571
- kind: "Variable",
1572
- name: { kind: "Name", value: "includeMedia" }
1573
- }
1574
- }
1575
- ],
1576
- selectionSet: {
1577
- kind: "SelectionSet",
1578
- selections: [
1579
- {
1580
- kind: "Field",
1581
- name: { kind: "Name", value: "records" },
1582
- selectionSet: {
1583
- kind: "SelectionSet",
1584
- selections: [
1585
- { kind: "Field", name: { kind: "Name", value: "id" } },
1586
- {
1587
- kind: "Field",
1588
- name: { kind: "Name", value: "modelKey" }
1589
- },
1590
- { kind: "Field", name: { kind: "Name", value: "title" } },
1591
- {
1592
- kind: "Field",
1593
- name: { kind: "Name", value: "naturalKey" }
1594
- },
1595
- {
1596
- kind: "Field",
1597
- name: { kind: "Name", value: "subtitle" }
1598
- },
1599
- {
1600
- kind: "Field",
1601
- name: { kind: "Name", value: "updatedAt" }
1602
- }
1603
- ]
1604
- }
1605
- },
1606
- {
1607
- kind: "Field",
1608
- name: { kind: "Name", value: "media" },
1609
- selectionSet: {
1610
- kind: "SelectionSet",
1611
- selections: [
1612
- { kind: "Field", name: { kind: "Name", value: "id" } },
1613
- {
1614
- kind: "Field",
1615
- name: { kind: "Name", value: "fileName" }
1616
- },
1617
- {
1618
- kind: "Field",
1619
- name: { kind: "Name", value: "altText" }
1620
- },
1621
- {
1622
- kind: "Field",
1623
- name: { kind: "Name", value: "fileUrl" }
1624
- }
1625
- ]
1626
- }
1627
- }
1628
- ]
1629
- }
1630
- }
1631
- ]
1632
- }
1633
- }
1634
- ]
1635
- };
1636
-
1637
1720
  // src/commands/search.ts
1638
1721
  function registerSearchCommands(program2, globalOpts) {
1639
- program2.command("search <query>").description("Search across all records and media").option(
1722
+ program2.command("search <query>").description("Search across all records").option(
1640
1723
  "--models <keys>",
1641
1724
  "Filter to specific model keys (comma-separated)"
1642
- ).option("--limit <n>", "Max results", "20").option("--no-media", "Exclude media results").action(
1725
+ ).option("--limit <n>", "Max results", "20").action(
1643
1726
  withErrorHandler(
1644
1727
  globalOpts,
1645
1728
  async (query, cmdOpts) => {
1646
1729
  const opts = globalOpts();
1647
- const client = await createClient(opts);
1730
+ const client = await createPlatformClient(opts);
1648
1731
  const modelKeys = typeof cmdOpts.models === "string" ? cmdOpts.models.split(",").map((k) => k.trim()) : void 0;
1649
- const data = await client.request(GlobalSearchDocument, {
1732
+ const result = await client.records.globalSearch({
1650
1733
  query,
1651
1734
  limit: parseInt(String(cmdOpts.limit ?? "20"), 10),
1652
- modelKeys,
1653
- includeMedia: cmdOpts.media !== false
1735
+ modelKeys
1654
1736
  });
1655
1737
  if (opts.json || opts.jsonl) {
1656
- formatOutput(data.globalSearch, opts);
1738
+ formatOutput(result, opts);
1657
1739
  return;
1658
1740
  }
1659
- if (data.globalSearch.records.length > 0) {
1741
+ if (result.items.length > 0) {
1660
1742
  console.log(`
1661
- Records (${data.globalSearch.records.length}):`);
1743
+ Records (${result.items.length}):`);
1662
1744
  formatList(
1663
- 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
+ })),
1664
1751
  opts,
1665
1752
  {
1666
1753
  columns: [
1667
1754
  { key: "id", header: "ID", width: 28 },
1668
1755
  { key: "modelKey", header: "Model", width: 18 },
1669
- { key: "title", header: "Title", width: 28 },
1670
1756
  { key: "naturalKey", header: "Key", width: 20 },
1671
1757
  {
1672
- key: "updatedAt",
1673
- header: "Updated",
1674
- width: 12,
1675
- format: (v) => timeAgo(v)
1758
+ key: "score",
1759
+ header: "Score",
1760
+ width: 8,
1761
+ format: (v) => Number(v).toFixed(2)
1676
1762
  }
1677
1763
  ]
1678
1764
  }
1679
1765
  );
1680
- }
1681
- if (data.globalSearch.media.length > 0) {
1682
- console.log(`
1683
- Media (${data.globalSearch.media.length}):`);
1684
- formatList(
1685
- data.globalSearch.media,
1686
- opts,
1687
- {
1688
- columns: [
1689
- { key: "id", header: "ID", width: 28 },
1690
- { key: "fileName", header: "File", width: 30 },
1691
- { key: "altText", header: "Alt", width: 24 }
1692
- ]
1693
- }
1694
- );
1695
- }
1696
- if (data.globalSearch.records.length === 0 && data.globalSearch.media.length === 0) {
1766
+ } else {
1697
1767
  console.log("No results found.");
1698
1768
  }
1699
1769
  }
@@ -1704,9 +1774,9 @@ Media (${data.globalSearch.media.length}):`);
1704
1774
  // src/commands/init.ts
1705
1775
  import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
1706
1776
  import { writeFile } from "fs/promises";
1707
- import { resolve as resolve2, join as join4 } from "path";
1777
+ import { resolve as resolve3, join as join4 } from "path";
1708
1778
  import chalk5 from "chalk";
1709
- import inquirer3 from "inquirer";
1779
+ import inquirer4 from "inquirer";
1710
1780
  var FIELD_DEFAULTS = {
1711
1781
  text: "",
1712
1782
  richtext: "",
@@ -1768,9 +1838,9 @@ function generateRecordSeed(model) {
1768
1838
  )) {
1769
1839
  continue;
1770
1840
  }
1771
- if (field.type === "list" && field.items) {
1841
+ if (field.type === "list" && field.itemType) {
1772
1842
  data[field.key] = [
1773
- defaultValueForField({ key: field.key, type: field.items.type })
1843
+ defaultValueForField({ key: field.key, type: field.itemType })
1774
1844
  ];
1775
1845
  } else {
1776
1846
  data[field.key] = defaultValueForField(field);
@@ -1795,7 +1865,7 @@ function registerInitCommands(program2, globalOpts) {
1795
1865
  async (key, opts) => {
1796
1866
  const globalFlags = globalOpts();
1797
1867
  const template = generateModelTemplate(key);
1798
- const outDir = resolve2(opts.output);
1868
+ const outDir = resolve3(opts.output);
1799
1869
  if (!existsSync3(outDir)) {
1800
1870
  mkdirSync2(outDir, { recursive: true });
1801
1871
  }
@@ -1826,10 +1896,19 @@ Edit the file, then run:
1826
1896
  globalOpts,
1827
1897
  async (opts) => {
1828
1898
  const globalFlags = globalOpts();
1829
- const client = await createClient(globalFlags);
1830
- const query = `query { models(limit: 100) { items { key name fields } total } }`;
1831
- const result = await client.request(query);
1832
- 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
+ }));
1833
1912
  if (models.length === 0) {
1834
1913
  console.log(
1835
1914
  chalk5.yellow(
@@ -1854,7 +1933,7 @@ Edit the file, then run:
1854
1933
  selectedModels.push(found);
1855
1934
  }
1856
1935
  } else {
1857
- const { selected } = await inquirer3.prompt([
1936
+ const { selected } = await inquirer4.prompt([
1858
1937
  {
1859
1938
  type: "checkbox",
1860
1939
  name: "selected",
@@ -1873,7 +1952,7 @@ Edit the file, then run:
1873
1952
  console.log("No models selected.");
1874
1953
  return;
1875
1954
  }
1876
- const outDir = resolve2(opts.output);
1955
+ const outDir = resolve3(opts.output);
1877
1956
  if (!existsSync3(outDir)) {
1878
1957
  mkdirSync2(outDir, { recursive: true });
1879
1958
  }
@@ -1909,64 +1988,12 @@ Edit the files, then run:
1909
1988
  import chalk6 from "chalk";
1910
1989
  import { existsSync as existsSync4 } from "fs";
1911
1990
  import { resolve as resolve4 } from "path";
1912
-
1913
- // src/lib/config-loader.ts
1914
- import { readFile } from "fs/promises";
1915
- import { pathToFileURL } from "url";
1916
- import { resolve as resolve3 } from "path";
1917
- async function loadConfig(filePath) {
1918
- const absPath = resolve3(filePath);
1919
- if (filePath.endsWith(".ts")) {
1920
- const configModule = await import(pathToFileURL(absPath).href);
1921
- return configModule.default;
1922
- }
1923
- if (filePath.endsWith(".js") || filePath.endsWith(".mjs")) {
1924
- const configModule = await import(pathToFileURL(absPath).href);
1925
- return configModule.default;
1926
- }
1927
- if (filePath.endsWith(".json")) {
1928
- const content = await readFile(absPath, "utf-8");
1929
- return JSON.parse(content);
1930
- }
1931
- throw new Error(
1932
- `Unsupported file extension for "${filePath}". Supported: .ts, .js, .mjs, .json`
1933
- );
1934
- }
1935
-
1936
- // src/commands/push.ts
1937
1991
  var CONFIG_FILE_NAMES = [
1938
1992
  "foir.config.ts",
1939
1993
  "foir.config.js",
1940
1994
  "foir.config.mjs",
1941
1995
  "foir.config.json"
1942
1996
  ];
1943
- var APPLY_CONFIG_MUTATION = (
1944
- /* GraphQL */
1945
- `
1946
- mutation ApplyConfig($input: ApplyConfigInput!) {
1947
- applyConfig(input: $input) {
1948
- configId
1949
- configKey
1950
- credentials {
1951
- platformApiKey
1952
- platformEditorKey
1953
- webhookSecret
1954
- }
1955
- modelsCreated
1956
- modelsUpdated
1957
- operationsCreated
1958
- operationsUpdated
1959
- segmentsCreated
1960
- segmentsUpdated
1961
- schedulesCreated
1962
- schedulesUpdated
1963
- hooksCreated
1964
- hooksUpdated
1965
- isUpdate
1966
- }
1967
- }
1968
- `
1969
- );
1970
1997
  function discoverConfigFile() {
1971
1998
  for (const name of CONFIG_FILE_NAMES) {
1972
1999
  const path3 = resolve4(process.cwd(), name);
@@ -1991,69 +2018,32 @@ function registerPushCommand(program2, globalOpts) {
1991
2018
  console.log(chalk6.dim(`Loading ${configPath}...`));
1992
2019
  const config2 = await loadConfig(configPath);
1993
2020
  if (!config2?.key || !config2?.name) {
1994
- throw new Error(
1995
- 'Config must have at least "key" and "name" fields.'
1996
- );
1997
- }
1998
- if (opts.force) {
1999
- config2.force = true;
2000
- }
2001
- const client = await createClient(globalOpts());
2002
- console.log(
2003
- chalk6.dim(`Pushing config "${config2.key}" to platform...`)
2004
- );
2005
- const data = await client.request(
2006
- APPLY_CONFIG_MUTATION,
2007
- { input: config2 }
2008
- );
2009
- const result = data.applyConfig;
2010
- console.log();
2011
- if (result.isUpdate) {
2012
- console.log(chalk6.green("Config updated successfully."));
2013
- } else {
2014
- console.log(chalk6.green("Config applied successfully."));
2015
- }
2016
- console.log();
2017
- console.log(` Config ID: ${chalk6.cyan(result.configId)}`);
2018
- console.log(` Config Key: ${chalk6.cyan(result.configKey)}`);
2019
- console.log();
2020
- const stats = [
2021
- ["Models", result.modelsCreated, result.modelsUpdated],
2022
- ["Operations", result.operationsCreated, result.operationsUpdated],
2023
- ["Segments", result.segmentsCreated, result.segmentsUpdated],
2024
- ["Schedules", result.schedulesCreated, result.schedulesUpdated],
2025
- ["Hooks", result.hooksCreated, result.hooksUpdated]
2026
- ].filter(([, c, u]) => c > 0 || u > 0);
2027
- if (stats.length > 0) {
2028
- for (const [label, created, updated] of stats) {
2029
- const parts = [];
2030
- if (created > 0)
2031
- parts.push(chalk6.green(`${created} created`));
2032
- if (updated > 0)
2033
- parts.push(chalk6.yellow(`${updated} updated`));
2034
- console.log(` ${label}: ${parts.join(", ")}`);
2035
- }
2036
- console.log();
2037
- }
2038
- if (result.credentials) {
2039
- console.log(chalk6.bold.yellow("Credentials (save these now):"));
2040
- console.log();
2041
- console.log(
2042
- ` PLATFORM_API_KEY: ${chalk6.cyan(result.credentials.platformApiKey)}`
2043
- );
2044
- console.log(
2045
- ` PLATFORM_EDITOR_KEY: ${chalk6.cyan(result.credentials.platformEditorKey)}`
2046
- );
2047
- console.log(
2048
- ` WEBHOOK_SECRET: ${chalk6.cyan(result.credentials.webhookSecret)}`
2021
+ throw new Error(
2022
+ 'Config must have at least "key" and "name" fields.'
2049
2023
  );
2050
- console.log();
2051
- console.log(
2052
- chalk6.dim(
2053
- "These credentials are only shown once. Store them securely."
2054
- )
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."
2055
2039
  );
2056
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();
2057
2047
  }
2058
2048
  )
2059
2049
  );
@@ -2061,40 +2051,19 @@ function registerPushCommand(program2, globalOpts) {
2061
2051
 
2062
2052
  // src/commands/remove.ts
2063
2053
  import chalk7 from "chalk";
2064
- import inquirer4 from "inquirer";
2065
- var GET_CONFIG_QUERY = (
2066
- /* GraphQL */
2067
- `
2068
- query GetConfigByKey($key: String!) {
2069
- configByKey(key: $key) {
2070
- id
2071
- key
2072
- name
2073
- configType
2074
- }
2075
- }
2076
- `
2077
- );
2078
- var UNREGISTER_MUTATION = (
2079
- /* GraphQL */
2080
- `
2081
- mutation UnregisterConfig($id: ID!) {
2082
- unregisterConfig(id: $id)
2083
- }
2084
- `
2085
- );
2054
+ import inquirer5 from "inquirer";
2086
2055
  function registerRemoveCommand(program2, globalOpts) {
2087
2056
  program2.command("remove <key>").description("Remove a config and all its provisioned resources").option("--force", "Skip confirmation prompt", false).action(
2088
2057
  withErrorHandler(
2089
2058
  globalOpts,
2090
2059
  async (key, opts) => {
2091
- const client = await createClient(globalOpts());
2092
- const { configByKey: config2 } = await client.request(GET_CONFIG_QUERY, { key });
2060
+ const client = await createPlatformClient(globalOpts());
2061
+ const config2 = await client.configs.getConfigByKey(key);
2093
2062
  if (!config2) {
2094
2063
  throw new Error(`Config not found: ${key}`);
2095
2064
  }
2096
2065
  if (!opts.force) {
2097
- const { confirmed } = await inquirer4.prompt([
2066
+ const { confirmed } = await inquirer5.prompt([
2098
2067
  {
2099
2068
  type: "confirm",
2100
2069
  name: "confirmed",
@@ -2107,7 +2076,7 @@ function registerRemoveCommand(program2, globalOpts) {
2107
2076
  return;
2108
2077
  }
2109
2078
  }
2110
- await client.request(UNREGISTER_MUTATION, { id: config2.id });
2079
+ await client.configs.deleteConfig(config2.id);
2111
2080
  console.log(
2112
2081
  chalk7.green(`Removed config "${config2.name}" (${config2.key}).`)
2113
2082
  );
@@ -2118,49 +2087,6 @@ function registerRemoveCommand(program2, globalOpts) {
2118
2087
 
2119
2088
  // src/commands/profiles.ts
2120
2089
  import chalk8 from "chalk";
2121
-
2122
- // src/lib/input.ts
2123
- import inquirer5 from "inquirer";
2124
- async function parseInputData(opts) {
2125
- if (opts.data) {
2126
- return JSON.parse(opts.data);
2127
- }
2128
- if (opts.file) {
2129
- return await loadConfig(opts.file);
2130
- }
2131
- if (!process.stdin.isTTY) {
2132
- const chunks = [];
2133
- for await (const chunk of process.stdin) {
2134
- chunks.push(chunk);
2135
- }
2136
- const stdinContent = Buffer.concat(chunks).toString("utf-8").trim();
2137
- if (stdinContent) {
2138
- return JSON.parse(stdinContent);
2139
- }
2140
- }
2141
- throw new Error(
2142
- "No input data provided. Use --data, --file, or pipe via stdin."
2143
- );
2144
- }
2145
- function isUUID(value) {
2146
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
2147
- value
2148
- ) || /^c[a-z0-9]{24,}$/.test(value);
2149
- }
2150
- async function confirmAction(message, opts) {
2151
- if (opts?.confirm) return true;
2152
- const { confirmed } = await inquirer5.prompt([
2153
- {
2154
- type: "confirm",
2155
- name: "confirmed",
2156
- message,
2157
- default: false
2158
- }
2159
- ]);
2160
- return confirmed;
2161
- }
2162
-
2163
- // src/commands/profiles.ts
2164
2090
  function registerProfilesCommand(program2, globalOpts) {
2165
2091
  const profiles = program2.command("profiles").description("Manage named project profiles");
2166
2092
  profiles.command("list").description("List all saved project profiles").action(
@@ -2328,9 +2254,8 @@ function registerProfilesCommand(program2, globalOpts) {
2328
2254
  }
2329
2255
 
2330
2256
  // src/commands/register-commands.ts
2331
- import { readFileSync, readdirSync } from "fs";
2332
- import { resolve as resolve5, dirname as dirname4 } from "path";
2333
- import { fileURLToPath } from "url";
2257
+ import { readdirSync } from "fs";
2258
+ import { resolve as resolve5 } from "path";
2334
2259
  import chalk9 from "chalk";
2335
2260
 
2336
2261
  // ../command-registry/src/command-map.ts
@@ -3108,68 +3033,6 @@ var COMMANDS = [
3108
3033
  successMessage: "Updated customer profile"
3109
3034
  },
3110
3035
  // =========================================================================
3111
- // FILES
3112
- // =========================================================================
3113
- {
3114
- group: "files",
3115
- name: "list",
3116
- description: "List files",
3117
- operation: "files",
3118
- operationType: "query",
3119
- columns: [
3120
- { key: "id", header: "ID", width: 28 },
3121
- { key: "filename", header: "Filename", width: 24 },
3122
- { key: "mimeType", header: "Type", width: 16 },
3123
- { key: "size", header: "Size", width: 10, format: "bytes" },
3124
- { key: "folder", header: "Folder", width: 16 },
3125
- { key: "createdAt", header: "Created", width: 12, format: "timeAgo" }
3126
- ]
3127
- },
3128
- {
3129
- group: "files",
3130
- name: "get",
3131
- description: "Get a file",
3132
- operation: "file",
3133
- operationType: "query",
3134
- positionalArgs: [{ name: "id", graphqlArg: "id" }]
3135
- },
3136
- {
3137
- group: "files",
3138
- name: "usage",
3139
- description: "Get file storage usage",
3140
- operation: "fileStorageUsage",
3141
- operationType: "query"
3142
- },
3143
- {
3144
- group: "files",
3145
- name: "update",
3146
- description: "Update a file",
3147
- operation: "updateFile",
3148
- operationType: "mutation",
3149
- positionalArgs: [{ name: "id", graphqlArg: "id" }],
3150
- successMessage: "Updated file"
3151
- },
3152
- {
3153
- group: "files",
3154
- name: "update-metadata",
3155
- description: "Update file metadata",
3156
- operation: "updateFileMetadata",
3157
- operationType: "mutation",
3158
- positionalArgs: [{ name: "id", graphqlArg: "id" }],
3159
- successMessage: "Updated file metadata"
3160
- },
3161
- {
3162
- group: "files",
3163
- name: "delete",
3164
- description: "Delete a file",
3165
- operation: "deleteFile",
3166
- operationType: "mutation",
3167
- positionalArgs: [{ name: "id", graphqlArg: "id" }],
3168
- requiresConfirmation: true,
3169
- scalarResult: true,
3170
- successMessage: "Deleted file"
3171
- },
3172
- // =========================================================================
3173
3036
  // OPERATIONS
3174
3037
  // =========================================================================
3175
3038
  {
@@ -3737,230 +3600,397 @@ var COMMANDS = [
3737
3600
  }
3738
3601
  ];
3739
3602
 
3740
- // ../command-registry/src/schema-engine.ts
3741
- import {
3742
- buildSchema,
3743
- isObjectType,
3744
- isInputObjectType,
3745
- isListType,
3746
- isNonNullType,
3747
- isScalarType,
3748
- isEnumType
3749
- } from "graphql";
3750
- function unwrapType(type) {
3751
- if (isNonNullType(type)) return unwrapType(type.ofType);
3752
- if (isListType(type)) return unwrapType(type.ofType);
3753
- return type;
3754
- }
3755
- function typeToString(type) {
3756
- if (isNonNullType(type)) return `${typeToString(type.ofType)}!`;
3757
- if (isListType(type)) return `[${typeToString(type.ofType)}]`;
3758
- return type.name;
3759
- }
3760
- function detectListWrapper(type) {
3761
- if (!isObjectType(type)) return null;
3762
- const fields = type.getFields();
3763
- const fieldNames = Object.keys(fields);
3764
- let listField = null;
3765
- let totalField = null;
3766
- let itemType = null;
3767
- for (const name of fieldNames) {
3768
- const fieldType = fields[name].type;
3769
- const unwrapped = isNonNullType(fieldType) ? fieldType.ofType : fieldType;
3770
- if (isListType(unwrapped) || isNonNullType(unwrapped) && isListType(unwrapped)) {
3771
- listField = name;
3772
- itemType = unwrapType(fieldType);
3773
- } else {
3774
- const namedType = unwrapType(fieldType);
3775
- if (namedType.name === "Int" && (name === "total" || name === "totalCount")) {
3776
- totalField = name;
3777
- }
3778
- }
3779
- }
3780
- if (listField && totalField && itemType) {
3781
- return { listField, itemType };
3782
- }
3783
- return null;
3784
- }
3785
- function buildSelectionSet(type, isListView, depth = 0) {
3786
- if (!isObjectType(type)) return "";
3787
- const fields = type.getFields();
3788
- const selections = [];
3789
- for (const [name, field] of Object.entries(fields)) {
3790
- const namedType = unwrapType(field.type);
3791
- if (isScalarType(namedType)) {
3792
- if (isListView && namedType.name === "JSON") continue;
3793
- selections.push(name);
3794
- } else if (isEnumType(namedType)) {
3795
- selections.push(name);
3796
- } else if (isObjectType(namedType) && depth === 0 && !isListView) {
3797
- const subSelection = buildSelectionSet(namedType, false, depth + 1);
3798
- if (subSelection) {
3799
- selections.push(`${name} { ${subSelection} }`);
3800
- }
3801
- }
3802
- }
3803
- return selections.join(" ");
3804
- }
3805
- function createSchemaEngine(sdl) {
3806
- const schema = buildSchema(sdl);
3807
- function getField(operationName, operationType) {
3808
- const rootType = operationType === "query" ? schema.getQueryType() : schema.getMutationType();
3809
- if (!rootType) return null;
3810
- const fields = rootType.getFields();
3811
- return fields[operationName] ?? null;
3812
- }
3813
- function getOperationArgs(operationName, operationType) {
3814
- const field = getField(operationName, operationType);
3815
- if (!field) return [];
3816
- return field.args.map((arg) => {
3817
- const namedType = unwrapType(arg.type);
3818
- const rawType = isNonNullType(arg.type) ? arg.type.ofType : arg.type;
3819
- return {
3820
- name: arg.name,
3821
- type: typeToString(arg.type),
3822
- required: isNonNullType(arg.type),
3823
- isList: isListType(rawType) || isNonNullType(rawType) && isListType(rawType.ofType),
3824
- isInput: namedType.name.endsWith("Input")
3825
- };
3826
- });
3827
- }
3828
- function coerceArgs(operationName, operationType, rawArgs) {
3829
- const field = getField(operationName, operationType);
3830
- if (!field) return rawArgs;
3831
- const coerced = {};
3832
- const argMap = new Map(field.args.map((a) => [a.name, a]));
3833
- for (const [key, value] of Object.entries(rawArgs)) {
3834
- const argDef = argMap.get(key);
3835
- if (!argDef) {
3836
- coerced[key] = value;
3837
- 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
+ );
3838
3717
  }
3839
- const namedType = unwrapType(argDef.type);
3840
- switch (namedType.name) {
3841
- case "Int":
3842
- coerced[key] = parseInt(value, 10);
3843
- break;
3844
- case "Float":
3845
- coerced[key] = parseFloat(value);
3846
- break;
3847
- case "Boolean":
3848
- coerced[key] = value === "true" || value === "1";
3849
- break;
3850
- case "JSON":
3851
- try {
3852
- coerced[key] = JSON.parse(value);
3853
- } catch {
3854
- coerced[key] = value;
3855
- }
3856
- break;
3857
- default:
3858
- 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
+ });
3859
3853
  }
3860
- }
3861
- return coerced;
3862
- }
3863
- function buildQuery(entry, variables) {
3864
- const field = getField(entry.operation, entry.operationType);
3865
- if (!field) {
3866
- throw new Error(
3867
- `Operation "${entry.operation}" not found in schema ${entry.operationType} type`
3868
- );
3869
- }
3870
- const opType = entry.operationType === "query" ? "query" : "mutation";
3871
- const opName = entry.operation.charAt(0).toUpperCase() + entry.operation.slice(1);
3872
- const usedArgs = [];
3873
- for (const argDef of field.args) {
3874
- const isPositional = entry.positionalArgs?.some(
3875
- (p) => p.graphqlArg === argDef.name
3876
- );
3877
- const isInputArg = entry.acceptsInput && argDef.name === (entry.inputArgName ?? "input");
3878
- if (isPositional || isInputArg || variables[argDef.name] !== void 0) {
3879
- 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 };
3880
3943
  }
3881
- }
3882
- const varDecls = usedArgs.map((a) => `$${a.varName}: ${typeToString(a.argDef.type)}`).join(", ");
3883
- const fieldArgs = usedArgs.map((a) => `${a.argDef.name}: $${a.varName}`).join(", ");
3884
- const returnType = unwrapType(field.type);
3885
- let selectionSet = "";
3886
- if (isScalarType(returnType) || isEnumType(returnType)) {
3887
- selectionSet = "";
3888
- } else if (isObjectType(returnType)) {
3889
- const wrapper = detectListWrapper(returnType);
3890
- if (wrapper) {
3891
- const itemSelection = buildSelectionSet(wrapper.itemType, true);
3892
- selectionSet = `{ ${wrapper.listField} { ${itemSelection} } total }`;
3893
- } else {
3894
- const selection = buildSelectionSet(returnType, false);
3895
- 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;
3896
3980
  }
3897
3981
  }
3898
- const varPart = varDecls ? `(${varDecls})` : "";
3899
- const argPart = fieldArgs ? `(${fieldArgs})` : "";
3900
- return `${opType} ${opName}${varPart} { ${entry.operation}${argPart} ${selectionSet} }`;
3901
- }
3902
- function getInputFields(operationName, operationType, inputArgName) {
3903
- const field = getField(operationName, operationType);
3904
- if (!field) return [];
3905
- const argName = inputArgName ?? "input";
3906
- const arg = field.args.find((a) => a.name === argName);
3907
- if (!arg) return [];
3908
- const namedType = unwrapType(arg.type);
3909
- if (!isInputObjectType(namedType)) return [];
3910
- const fields = namedType.getFields();
3911
- return Object.entries(fields).map(([name, f]) => ({
3912
- name,
3913
- type: typeToString(f.type),
3914
- required: isNonNullType(f.type)
3915
- }));
3916
- }
3917
- function getCompletions(partial, commandNames) {
3918
- return commandNames.filter(
3919
- (name) => name.toLowerCase().startsWith(partial.toLowerCase())
3920
- );
3921
- }
3922
- return {
3923
- buildQuery,
3924
- getOperationArgs,
3925
- coerceArgs,
3926
- getInputFields,
3927
- getCompletions
3928
3982
  };
3929
3983
  }
3930
-
3931
- // src/commands/register-commands.ts
3932
- var __filename = fileURLToPath(import.meta.url);
3933
- var __dirname = dirname4(__filename);
3934
- function loadSchemaSDL() {
3935
- const bundledPath = resolve5(__dirname, "schema.graphql");
3936
- try {
3937
- return readFileSync(bundledPath, "utf-8");
3938
- } catch {
3939
- const monorepoPath = resolve5(
3940
- __dirname,
3941
- "../../../graphql-core/schema.graphql"
3942
- );
3943
- try {
3944
- return readFileSync(monorepoPath, "utf-8");
3945
- } catch {
3946
- throw new Error(
3947
- "Could not find schema.graphql. Try reinstalling @eide/foir-cli."
3948
- );
3949
- }
3950
- }
3984
+ function str(v) {
3985
+ return v != null ? String(v) : void 0;
3951
3986
  }
3952
- function extractResult(result, operationName) {
3953
- const raw = result[operationName];
3954
- if (!raw || typeof raw !== "object") return { data: raw };
3955
- const obj = raw;
3956
- if ("total" in obj) {
3957
- for (const [key, val] of Object.entries(obj)) {
3958
- if (key !== "total" && key !== "__typename" && Array.isArray(val)) {
3959
- return { data: val, total: obj.total };
3960
- }
3961
- }
3962
- }
3963
- 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;
3964
3994
  }
3965
3995
  function toCliColumns(columns) {
3966
3996
  if (!columns) return void 0;
@@ -3979,8 +4009,8 @@ function toCliColumns(columns) {
3979
4009
  break;
3980
4010
  case "truncate":
3981
4011
  cliCol.format = (v) => {
3982
- const str = String(v ?? "");
3983
- 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;
3984
4014
  };
3985
4015
  break;
3986
4016
  case "join":
@@ -3988,27 +4018,37 @@ function toCliColumns(columns) {
3988
4018
  break;
3989
4019
  case "bytes":
3990
4020
  cliCol.format = (v) => {
3991
- const num = Number(v);
3992
- if (num < 1024) return `${num} B`;
3993
- if (num < 1024 * 1024) return `${(num / 1024).toFixed(1)} KB`;
3994
- 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`;
3995
4025
  };
3996
4026
  break;
3997
4027
  }
3998
4028
  return cliCol;
3999
4029
  });
4000
4030
  }
4001
- function registerDynamicCommands(program2, globalOpts) {
4002
- let engine;
4003
- try {
4004
- const sdl = loadSchemaSDL();
4005
- engine = createSchemaEngine(sdl);
4006
- } catch (err) {
4007
- console.error(
4008
- `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}`
4009
4047
  );
4010
- return;
4011
4048
  }
4049
+ return handler(variables, client);
4050
+ }
4051
+ function registerDynamicCommands(program2, globalOpts) {
4012
4052
  const groups = /* @__PURE__ */ new Map();
4013
4053
  for (const cmd of COMMANDS) {
4014
4054
  if (!groups.has(cmd.group)) groups.set(cmd.group, []);
@@ -4017,42 +4057,15 @@ function registerDynamicCommands(program2, globalOpts) {
4017
4057
  for (const [groupName, entries] of groups) {
4018
4058
  const group = program2.command(groupName).description(`Manage ${groupName}`);
4019
4059
  for (const entry of entries) {
4060
+ if (!DISPATCH[entry.group]?.[entry.operation]) continue;
4020
4061
  let cmd = group.command(entry.name).description(entry.description);
4021
4062
  for (const pos of entry.positionalArgs ?? []) {
4022
4063
  cmd = cmd.argument(`<${pos.name}>`, pos.description ?? pos.name);
4023
4064
  }
4024
- const schemaArgs = engine.getOperationArgs(
4025
- entry.operation,
4026
- entry.operationType
4027
- );
4028
- for (const arg of schemaArgs) {
4029
- if (entry.positionalArgs?.some((p) => p.graphqlArg === arg.name))
4030
- continue;
4031
- if (entry.acceptsInput && arg.name === (entry.inputArgName ?? "input"))
4032
- continue;
4033
- const reqStr = arg.required ? " (required)" : "";
4034
- cmd = cmd.option(`--${arg.name} <value>`, `${arg.name}${reqStr}`);
4035
- }
4065
+ cmd = cmd.option("--limit <n>", "Max results");
4036
4066
  if (entry.acceptsInput) {
4037
4067
  cmd = cmd.option("-d, --data <json>", "Data as JSON");
4038
4068
  cmd = cmd.option("-f, --file <path>", "Read data from file");
4039
- const inputFields = engine.getInputFields(
4040
- entry.operation,
4041
- entry.operationType,
4042
- entry.inputArgName
4043
- );
4044
- if (inputFields.length > 0) {
4045
- const required = inputFields.filter((f) => f.required);
4046
- const optional = inputFields.filter((f) => !f.required);
4047
- let fieldHelp = "\nInput fields:";
4048
- if (required.length > 0) {
4049
- fieldHelp += "\n Required: " + required.map((f) => `${f.name} (${f.type})`).join(", ");
4050
- }
4051
- if (optional.length > 0) {
4052
- fieldHelp += "\n Optional: " + optional.map((f) => `${f.name} (${f.type})`).join(", ");
4053
- }
4054
- cmd = cmd.addHelpText("after", fieldHelp);
4055
- }
4056
4069
  }
4057
4070
  for (const cf of entry.customFlags ?? []) {
4058
4071
  cmd = cmd.option(cf.flag, cf.description);
@@ -4063,7 +4076,7 @@ function registerDynamicCommands(program2, globalOpts) {
4063
4076
  cmd.action(
4064
4077
  withErrorHandler(globalOpts, async (...actionArgs) => {
4065
4078
  const opts = globalOpts();
4066
- const client = await createClient(opts);
4079
+ const client = await createPlatformClient(opts);
4067
4080
  const variables = {};
4068
4081
  const positionals = entry.positionalArgs ?? [];
4069
4082
  for (let i = 0; i < positionals.length; i++) {
@@ -4076,27 +4089,27 @@ function registerDynamicCommands(program2, globalOpts) {
4076
4089
  const flags = actionArgs[positionals.length] ?? {};
4077
4090
  const customFlagNames = new Set(
4078
4091
  (entry.customFlags ?? []).map(
4079
- (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())
4080
4093
  )
4081
4094
  );
4082
- const rawFlags = {};
4083
4095
  for (const [key, val] of Object.entries(flags)) {
4084
- if (key === "data" || key === "file" || key === "confirm") continue;
4085
- if (customFlagNames.has(key)) continue;
4086
- 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;
4087
4106
  }
4088
- const coerced = engine.coerceArgs(
4089
- entry.operation,
4090
- entry.operationType,
4091
- rawFlags
4092
- );
4093
- Object.assign(variables, coerced);
4094
4107
  if (flags.dir && entry.acceptsInput) {
4095
4108
  const dirPath = resolve5(String(flags.dir));
4096
4109
  const files = readdirSync(dirPath).filter((f) => /\.(json|ts|js|mjs)$/.test(f)).sort();
4097
4110
  if (files.length === 0) {
4098
4111
  console.error(
4099
- chalk9.yellow(`\u26A0 No .json/.ts/.js files found in ${dirPath}`)
4112
+ chalk9.yellow(`No .json/.ts/.js files found in ${dirPath}`)
4100
4113
  );
4101
4114
  return;
4102
4115
  }
@@ -4110,8 +4123,7 @@ function registerDynamicCommands(program2, globalOpts) {
4110
4123
  const fileVars = { ...variables, [argName]: fileData };
4111
4124
  const label = fileData.key ?? fileData.name ?? file;
4112
4125
  try {
4113
- const q = engine.buildQuery(entry, fileVars);
4114
- await client.request(q, fileVars);
4126
+ await executeCommand(entry, fileVars, client);
4115
4127
  created++;
4116
4128
  if (!(opts.json || opts.jsonl || opts.quiet)) {
4117
4129
  success(`Created ${label}`);
@@ -4130,8 +4142,7 @@ function registerDynamicCommands(program2, globalOpts) {
4130
4142
  [updateEntry.positionalArgs[0].graphqlArg]: fileData.key
4131
4143
  } : {}
4132
4144
  };
4133
- const uq = engine.buildQuery(updateEntry, updateVars);
4134
- await client.request(uq, updateVars);
4145
+ await executeCommand(updateEntry, updateVars, client);
4135
4146
  updated++;
4136
4147
  if (!(opts.json || opts.jsonl || opts.quiet)) {
4137
4148
  success(`Updated ${label}`);
@@ -4165,19 +4176,13 @@ function registerDynamicCommands(program2, globalOpts) {
4165
4176
  data: flags.data,
4166
4177
  file: flags.file
4167
4178
  });
4168
- const inputFields = engine.getInputFields(
4169
- entry.operation,
4170
- entry.operationType,
4171
- entry.inputArgName
4172
- );
4173
- const fieldNames = new Set(inputFields.map((f) => f.name));
4174
- if (fieldNames.has("projectId") && !inputData.projectId || fieldNames.has("tenantId") && !inputData.tenantId) {
4179
+ if (!inputData.projectId || !inputData.tenantId) {
4175
4180
  const resolved = await resolveProjectContext(opts);
4176
4181
  if (resolved) {
4177
- if (fieldNames.has("projectId") && !inputData.projectId) {
4182
+ if (!inputData.projectId) {
4178
4183
  inputData.projectId = resolved.project.id;
4179
4184
  }
4180
- if (fieldNames.has("tenantId") && !inputData.tenantId) {
4185
+ if (!inputData.tenantId) {
4181
4186
  inputData.tenantId = resolved.project.tenantId;
4182
4187
  }
4183
4188
  }
@@ -4208,12 +4213,7 @@ function registerDynamicCommands(program2, globalOpts) {
4208
4213
  if (flags.modelKey) {
4209
4214
  variables.modelKey = String(flags.modelKey);
4210
4215
  }
4211
- const queryStr2 = engine.buildQuery(altEntry, variables);
4212
- const result2 = await client.request(
4213
- queryStr2,
4214
- variables
4215
- );
4216
- const { data: data2 } = extractResult(result2, altEntry.operation);
4216
+ const data2 = await executeCommand(altEntry, variables, client);
4217
4217
  formatOutput(data2, opts);
4218
4218
  return;
4219
4219
  }
@@ -4230,13 +4230,10 @@ function registerDynamicCommands(program2, globalOpts) {
4230
4230
  if (entry.group === "records" && entry.name === "publish" && variables.versionId) {
4231
4231
  const versionIdValue = String(variables.versionId);
4232
4232
  if (flags.modelKey) {
4233
- const modelKey = String(flags.modelKey);
4234
- const lookupQuery = `query RecordByKey($modelKey: String!, $naturalKey: String!) { recordByKey(modelKey: $modelKey, naturalKey: $naturalKey) { id currentVersionId } }`;
4235
- const lookupResult = await client.request(lookupQuery, {
4236
- modelKey,
4237
- naturalKey: versionIdValue
4238
- });
4239
- const record = lookupResult.recordByKey;
4233
+ const record = await client.records.getRecordByKey(
4234
+ String(flags.modelKey),
4235
+ versionIdValue
4236
+ );
4240
4237
  if (!record?.currentVersionId) {
4241
4238
  throw new Error(
4242
4239
  `No current version found for record "${versionIdValue}"`
@@ -4249,23 +4246,18 @@ function registerDynamicCommands(program2, globalOpts) {
4249
4246
  );
4250
4247
  } else {
4251
4248
  try {
4252
- const lookupQuery = `query Record($id: ID!) { record(id: $id) { id recordType currentVersionId } }`;
4253
- const lookupResult = await client.request(lookupQuery, {
4254
- id: versionIdValue
4255
- });
4256
- const record = lookupResult.record;
4257
- if (record?.recordType === "record" && record?.currentVersionId) {
4249
+ const record = await client.records.getRecord(versionIdValue);
4250
+ if (record?.recordType === RecordType.RECORD && record?.currentVersionId) {
4258
4251
  variables.versionId = record.currentVersionId;
4259
4252
  }
4260
4253
  } catch {
4261
4254
  }
4262
4255
  }
4263
4256
  }
4264
- const queryStr = engine.buildQuery(entry, variables);
4265
- let result;
4257
+ let data;
4266
4258
  let usedUpdate = false;
4267
4259
  try {
4268
- result = await client.request(queryStr, variables);
4260
+ data = await executeCommand(entry, variables, client);
4269
4261
  } catch (createErr) {
4270
4262
  if (flags.upsert && entry.name === "create") {
4271
4263
  const updateEntry = COMMANDS.find(
@@ -4281,8 +4273,7 @@ function registerDynamicCommands(program2, globalOpts) {
4281
4273
  [updateEntry.positionalArgs[0].graphqlArg]: inputData.key
4282
4274
  } : {}
4283
4275
  };
4284
- const uq = engine.buildQuery(updateEntry, updateVars);
4285
- result = await client.request(uq, updateVars);
4276
+ data = await executeCommand(updateEntry, updateVars, client);
4286
4277
  usedUpdate = true;
4287
4278
  } else {
4288
4279
  throw createErr;
@@ -4294,8 +4285,14 @@ function registerDynamicCommands(program2, globalOpts) {
4294
4285
  const activeEntry = usedUpdate ? COMMANDS.find(
4295
4286
  (c) => c.group === entry.group && c.name === "update"
4296
4287
  ) ?? entry : entry;
4297
- const { data, total } = extractResult(result, activeEntry.operation);
4298
- 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;
4299
4296
  const displayEntry = usedUpdate ? activeEntry : entry;
4300
4297
  if (displayEntry.scalarResult) {
4301
4298
  if (!(opts.json || opts.jsonl || opts.quiet)) {
@@ -4303,16 +4300,16 @@ function registerDynamicCommands(program2, globalOpts) {
4303
4300
  success(displayEntry.successMessage, responseData);
4304
4301
  }
4305
4302
  } else {
4306
- formatOutput(data, opts);
4303
+ formatOutput(displayData, opts);
4307
4304
  }
4308
- } else if (Array.isArray(data)) {
4305
+ } else if (Array.isArray(displayData)) {
4309
4306
  const cliColumns = toCliColumns(displayEntry.columns);
4310
- formatList(data, opts, {
4311
- columns: cliColumns ?? autoColumns(data),
4307
+ formatList(displayData, opts, {
4308
+ columns: cliColumns ?? autoColumns(displayData),
4312
4309
  total
4313
4310
  });
4314
4311
  } else {
4315
- formatOutput(data, opts);
4312
+ formatOutput(displayData, opts);
4316
4313
  if (displayEntry.successMessage && !(opts.json || opts.jsonl || opts.quiet)) {
4317
4314
  success(displayEntry.successMessage, responseData);
4318
4315
  }
@@ -4322,15 +4319,14 @@ function registerDynamicCommands(program2, globalOpts) {
4322
4319
  const record = responseData.record;
4323
4320
  const versionId = version2?.id ?? record?.currentVersionId;
4324
4321
  if (versionId) {
4325
- const publishQuery = `mutation PublishVersion($versionId: ID!) { publishVersion(versionId: $versionId) }`;
4326
- await client.request(publishQuery, { versionId });
4322
+ await client.records.publishVersion(versionId);
4327
4323
  if (!(opts.json || opts.jsonl || opts.quiet)) {
4328
4324
  success("Published version {id}", { id: versionId });
4329
4325
  }
4330
4326
  } else if (!(opts.json || opts.jsonl || opts.quiet)) {
4331
4327
  console.error(
4332
4328
  chalk9.yellow(
4333
- "\u26A0 Could not auto-publish: no version found in response"
4329
+ "Could not auto-publish: no version found in response"
4334
4330
  )
4335
4331
  );
4336
4332
  }
@@ -4340,20 +4336,11 @@ function registerDynamicCommands(program2, globalOpts) {
4340
4336
  }
4341
4337
  }
4342
4338
  }
4343
- function autoColumns(items) {
4344
- if (items.length === 0) return [];
4345
- const first = items[0];
4346
- return Object.keys(first).filter((k) => k !== "__typename").slice(0, 6).map((key) => ({
4347
- key,
4348
- header: key,
4349
- width: 20
4350
- }));
4351
- }
4352
4339
 
4353
4340
  // src/cli.ts
4354
- var __filename2 = fileURLToPath2(import.meta.url);
4355
- var __dirname2 = dirname5(__filename2);
4356
- 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") });
4357
4344
  var require2 = createRequire(import.meta.url);
4358
4345
  var { version } = require2("../package.json");
4359
4346
  var program = new Command();