@eide/foir-cli 0.1.37 → 0.1.39

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,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { config } from "dotenv";
5
- import { resolve as resolve6, dirname as dirname6 } from "path";
5
+ import { resolve as resolve7, dirname as dirname6 } from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { createRequire } from "module";
8
8
  import { Command } from "commander";
@@ -127,6 +127,22 @@ function getGraphQLEndpoint(apiUrl) {
127
127
 
128
128
  // src/lib/errors.ts
129
129
  import chalk from "chalk";
130
+ function extractErrorMessage(error) {
131
+ if (!error || typeof error !== "object")
132
+ return String(error ?? "Unknown error");
133
+ const err = error;
134
+ const gqlErrors = err.response?.errors;
135
+ if (gqlErrors && gqlErrors.length > 0) {
136
+ return gqlErrors[0].message;
137
+ }
138
+ if (err.message && err.message !== "undefined") {
139
+ return err.message;
140
+ }
141
+ if (error instanceof Error && error.message) {
142
+ return error.message;
143
+ }
144
+ return "An unknown error occurred. Use --json for details.";
145
+ }
130
146
  function withErrorHandler(optsFn, fn) {
131
147
  return async (...args) => {
132
148
  try {
@@ -145,12 +161,18 @@ function withErrorHandler(optsFn, fn) {
145
161
  error: {
146
162
  message: first.message,
147
163
  code: code ?? "GRAPHQL_ERROR",
148
- ...validationErrors ? { validationErrors } : {}
164
+ ...validationErrors ? { validationErrors } : {},
165
+ ...gqlErrors.length > 1 ? {
166
+ additionalErrors: gqlErrors.slice(1).map((e) => e.message)
167
+ } : {}
149
168
  }
150
169
  })
151
170
  );
152
171
  } else {
153
172
  console.error(chalk.red("Error:"), first.message);
173
+ for (const extra of gqlErrors.slice(1)) {
174
+ console.error(chalk.red(" \u2022"), extra.message);
175
+ }
154
176
  if (validationErrors && Object.keys(validationErrors).length > 0) {
155
177
  console.error("");
156
178
  console.error(chalk.yellow("Field errors:"));
@@ -162,12 +184,12 @@ function withErrorHandler(optsFn, fn) {
162
184
  }
163
185
  if (code === "UNAUTHENTICATED") {
164
186
  console.error(
165
- chalk.gray("Hint: Run `foir login` to authenticate.")
187
+ chalk.gray("\nHint: Run `foir login` to authenticate.")
166
188
  );
167
189
  } else if (code === "FORBIDDEN") {
168
190
  console.error(
169
191
  chalk.gray(
170
- "Hint: You may not have permission. Check your API key scopes."
192
+ "\nHint: You may not have permission. Check your API key scopes."
171
193
  )
172
194
  );
173
195
  }
@@ -190,7 +212,7 @@ function withErrorHandler(optsFn, fn) {
190
212
  }
191
213
  process.exit(1);
192
214
  }
193
- const message = error instanceof Error ? error.message : String(error);
215
+ const message = extractErrorMessage(error);
194
216
  if (opts?.json || opts?.jsonl) {
195
217
  console.error(JSON.stringify({ error: { message } }));
196
218
  } else {
@@ -204,13 +226,13 @@ function withErrorHandler(optsFn, fn) {
204
226
  // src/commands/login.ts
205
227
  async function findAvailablePort(start, end) {
206
228
  for (let port = start; port <= end; port++) {
207
- const available = await new Promise((resolve7) => {
229
+ const available = await new Promise((resolve8) => {
208
230
  const server = http.createServer();
209
231
  server.listen(port, () => {
210
232
  server.close();
211
- resolve7(true);
233
+ resolve8(true);
212
234
  });
213
- server.on("error", () => resolve7(false));
235
+ server.on("error", () => resolve8(false));
214
236
  });
215
237
  if (available) return port;
216
238
  }
@@ -248,7 +270,7 @@ async function loginAction(globalOpts) {
248
270
  const state = crypto.randomBytes(16).toString("hex");
249
271
  const port = await findAvailablePort(9876, 9900);
250
272
  const redirectUri = `http://localhost:${port}/callback`;
251
- const authCode = await new Promise((resolve7, reject) => {
273
+ const authCode = await new Promise((resolve8, reject) => {
252
274
  const server = http.createServer((req, res) => {
253
275
  const url = new URL(req.url, `http://localhost:${port}`);
254
276
  if (url.pathname === "/callback") {
@@ -281,7 +303,7 @@ async function loginAction(globalOpts) {
281
303
  `<html><head><meta http-equiv="refresh" content="2;url=${mainUrl}"></head><body style="font-family:system-ui;text-align:center;padding:50px"><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>`
282
304
  );
283
305
  server.close();
284
- resolve7(code);
306
+ resolve8(code);
285
307
  }
286
308
  });
287
309
  server.listen(port);
@@ -604,8 +626,15 @@ function timeAgo(dateStr) {
604
626
  if (days < 30) return `${days}d ago`;
605
627
  return date.toLocaleDateString();
606
628
  }
607
- function success(message) {
608
- console.log(chalk2.green(`\u2713 ${message}`));
629
+ function success(message, data) {
630
+ let interpolated = message;
631
+ if (data) {
632
+ interpolated = message.replace(
633
+ /\{(\w+)\}/g,
634
+ (_, field) => String(data[field] ?? `{${field}}`)
635
+ );
636
+ }
637
+ console.log(chalk2.green(`\u2713 ${interpolated}`));
609
638
  }
610
639
 
611
640
  // src/commands/whoami.ts
@@ -5617,10 +5646,215 @@ Media (${data.globalSearch.media.length}):`);
5617
5646
  );
5618
5647
  }
5619
5648
 
5649
+ // src/commands/init.ts
5650
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
5651
+ import { writeFile as writeFile2 } from "fs/promises";
5652
+ import { resolve as resolve4, join as join4 } from "path";
5653
+ import chalk6 from "chalk";
5654
+ import inquirer3 from "inquirer";
5655
+ var FIELD_DEFAULTS = {
5656
+ text: "",
5657
+ richtext: "",
5658
+ number: 0,
5659
+ boolean: false,
5660
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
5661
+ select: "",
5662
+ image: null,
5663
+ video: null,
5664
+ file: null,
5665
+ json: {},
5666
+ link: { url: "", label: "" },
5667
+ relationship: null,
5668
+ list: []
5669
+ };
5670
+ function defaultValueForField(field) {
5671
+ return FIELD_DEFAULTS[field.type] ?? null;
5672
+ }
5673
+ function generateModelTemplate(key) {
5674
+ return {
5675
+ key,
5676
+ name: key.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
5677
+ fields: [
5678
+ {
5679
+ key: "title",
5680
+ type: "text",
5681
+ label: "Title",
5682
+ required: true
5683
+ },
5684
+ {
5685
+ key: "slug",
5686
+ type: "text",
5687
+ label: "Slug",
5688
+ required: true
5689
+ },
5690
+ {
5691
+ key: "description",
5692
+ type: "richtext",
5693
+ label: "Description"
5694
+ },
5695
+ {
5696
+ key: "image",
5697
+ type: "image",
5698
+ label: "Featured Image"
5699
+ }
5700
+ ],
5701
+ config: {
5702
+ versioning: true,
5703
+ publishing: true,
5704
+ variants: false
5705
+ }
5706
+ };
5707
+ }
5708
+ function generateRecordSeed(model) {
5709
+ const data = {};
5710
+ for (const field of model.fields) {
5711
+ if (["id", "createdAt", "updatedAt", "createdBy", "updatedBy"].includes(
5712
+ field.key
5713
+ )) {
5714
+ continue;
5715
+ }
5716
+ if (field.type === "list" && field.items) {
5717
+ data[field.key] = [
5718
+ defaultValueForField({ key: field.key, type: field.items.type })
5719
+ ];
5720
+ } else {
5721
+ data[field.key] = defaultValueForField(field);
5722
+ }
5723
+ }
5724
+ return {
5725
+ modelKey: model.key,
5726
+ naturalKey: `example-${model.key}`,
5727
+ data
5728
+ };
5729
+ }
5730
+ function formatAsTypeScript(obj) {
5731
+ const json = JSON.stringify(obj, null, 2);
5732
+ return `export default ${json} as const;
5733
+ `;
5734
+ }
5735
+ function registerInitCommands(program2, globalOpts) {
5736
+ const initGroup = program2.command("init").description("Generate starter config and seed files");
5737
+ initGroup.command("model").description("Generate a starter model definition file").argument("<key>", "Model key (e.g. blog-post)").option("-o, --output <dir>", "Output directory", "models").option("--ts", "Generate TypeScript (.ts) instead of JSON").action(
5738
+ withErrorHandler(
5739
+ globalOpts,
5740
+ async (key, opts) => {
5741
+ const globalFlags = globalOpts();
5742
+ const template = generateModelTemplate(key);
5743
+ const outDir = resolve4(opts.output);
5744
+ if (!existsSync4(outDir)) {
5745
+ mkdirSync2(outDir, { recursive: true });
5746
+ }
5747
+ const ext = opts.ts ? "ts" : "json";
5748
+ const filePath = join4(outDir, `${key}.${ext}`);
5749
+ const content = opts.ts ? formatAsTypeScript(template) : JSON.stringify(template, null, 2) + "\n";
5750
+ await writeFile2(filePath, content, "utf-8");
5751
+ if (!(globalFlags.json || globalFlags.jsonl || globalFlags.quiet)) {
5752
+ success(`Created ${filePath}`);
5753
+ console.log(
5754
+ chalk6.gray(
5755
+ `
5756
+ Edit the file, then run:
5757
+ foir models create -f ${filePath}`
5758
+ )
5759
+ );
5760
+ } else {
5761
+ console.log(JSON.stringify({ path: filePath }));
5762
+ }
5763
+ }
5764
+ )
5765
+ );
5766
+ initGroup.command("records").description("Generate seed files for records (interactive model selector)").option("-o, --output <dir>", "Output directory", "seed").option("--ts", "Generate TypeScript (.ts) instead of JSON").option(
5767
+ "--model <keys...>",
5768
+ "Model keys to generate for (skip interactive selector)"
5769
+ ).action(
5770
+ withErrorHandler(
5771
+ globalOpts,
5772
+ async (opts) => {
5773
+ const globalFlags = globalOpts();
5774
+ const client = await createClient(globalFlags);
5775
+ const query = `query { models(limit: 100) { items { key name fields } total } }`;
5776
+ const result = await client.request(query);
5777
+ const models = result.models.items;
5778
+ if (models.length === 0) {
5779
+ console.log(
5780
+ chalk6.yellow(
5781
+ "No models found. Create models first with `foir models create`."
5782
+ )
5783
+ );
5784
+ return;
5785
+ }
5786
+ let selectedModels;
5787
+ if (opts.model && opts.model.length > 0) {
5788
+ selectedModels = [];
5789
+ for (const key of opts.model) {
5790
+ const found = models.find((m) => m.key === key);
5791
+ if (!found) {
5792
+ console.error(
5793
+ chalk6.red(`Model "${key}" not found.`),
5794
+ "Available:",
5795
+ models.map((m) => m.key).join(", ")
5796
+ );
5797
+ return;
5798
+ }
5799
+ selectedModels.push(found);
5800
+ }
5801
+ } else {
5802
+ const { selected } = await inquirer3.prompt([
5803
+ {
5804
+ type: "checkbox",
5805
+ name: "selected",
5806
+ message: "Select models to generate seed files for:",
5807
+ choices: models.map((m) => ({
5808
+ name: `${m.name} (${m.key})`,
5809
+ value: m.key,
5810
+ short: m.key
5811
+ })),
5812
+ validate: (answer) => answer.length > 0 ? true : "Select at least one model."
5813
+ }
5814
+ ]);
5815
+ selectedModels = selected.map((key) => models.find((m) => m.key === key)).filter(Boolean);
5816
+ }
5817
+ if (selectedModels.length === 0) {
5818
+ console.log("No models selected.");
5819
+ return;
5820
+ }
5821
+ const outDir = resolve4(opts.output);
5822
+ if (!existsSync4(outDir)) {
5823
+ mkdirSync2(outDir, { recursive: true });
5824
+ }
5825
+ const createdFiles = [];
5826
+ for (const model of selectedModels) {
5827
+ const seed = generateRecordSeed(model);
5828
+ const ext = opts.ts ? "ts" : "json";
5829
+ const filePath = join4(outDir, `${model.key}.${ext}`);
5830
+ const content = opts.ts ? formatAsTypeScript(seed) : JSON.stringify(seed, null, 2) + "\n";
5831
+ await writeFile2(filePath, content, "utf-8");
5832
+ createdFiles.push(filePath);
5833
+ if (!(globalFlags.json || globalFlags.jsonl || globalFlags.quiet)) {
5834
+ success(`Created ${filePath}`);
5835
+ }
5836
+ }
5837
+ if (!(globalFlags.json || globalFlags.jsonl || globalFlags.quiet)) {
5838
+ console.log(
5839
+ chalk6.gray(
5840
+ `
5841
+ Edit the files, then run:
5842
+ foir records create --dir ${outDir} --publish`
5843
+ )
5844
+ );
5845
+ } else {
5846
+ console.log(JSON.stringify({ files: createdFiles }));
5847
+ }
5848
+ }
5849
+ )
5850
+ );
5851
+ }
5852
+
5620
5853
  // src/commands/register-commands.ts
5621
- import { readFileSync } from "fs";
5622
- import { resolve as resolve5, dirname as dirname5 } from "path";
5854
+ import { readFileSync, readdirSync } from "fs";
5855
+ import { resolve as resolve6, dirname as dirname5 } from "path";
5623
5856
  import { fileURLToPath } from "url";
5857
+ import chalk7 from "chalk";
5624
5858
 
5625
5859
  // ../command-registry/src/command-map.ts
5626
5860
  var COMMANDS = [
@@ -5655,7 +5889,17 @@ var COMMANDS = [
5655
5889
  operation: "createModel",
5656
5890
  operationType: "mutation",
5657
5891
  acceptsInput: true,
5658
- successMessage: "Created model {key}"
5892
+ successMessage: "Created model {key}",
5893
+ customFlags: [
5894
+ {
5895
+ flag: "--dir <path>",
5896
+ description: "Create models from all files in directory"
5897
+ },
5898
+ {
5899
+ flag: "--upsert",
5900
+ description: "Update if model key already exists"
5901
+ }
5902
+ ]
5659
5903
  },
5660
5904
  {
5661
5905
  group: "models",
@@ -5725,7 +5969,10 @@ var COMMANDS = [
5725
5969
  operation: "createRecord",
5726
5970
  operationType: "mutation",
5727
5971
  acceptsInput: true,
5728
- successMessage: "Created record"
5972
+ successMessage: "Created record",
5973
+ customFlags: [
5974
+ { flag: "--publish", description: "Publish the record after creation" }
5975
+ ]
5729
5976
  },
5730
5977
  {
5731
5978
  group: "records",
@@ -5752,9 +5999,21 @@ var COMMANDS = [
5752
5999
  description: "Publish a record version",
5753
6000
  operation: "publishVersion",
5754
6001
  operationType: "mutation",
5755
- positionalArgs: [{ name: "versionId", graphqlArg: "versionId" }],
6002
+ positionalArgs: [
6003
+ {
6004
+ name: "versionId",
6005
+ graphqlArg: "versionId",
6006
+ description: "Version ID, record ID, or natural key (with --model)"
6007
+ }
6008
+ ],
5756
6009
  scalarResult: true,
5757
- successMessage: "Published version"
6010
+ successMessage: "Published version",
6011
+ customFlags: [
6012
+ {
6013
+ flag: "--model <key>",
6014
+ description: "Model key (use with natural key instead of version ID)"
6015
+ }
6016
+ ]
5758
6017
  },
5759
6018
  {
5760
6019
  group: "records",
@@ -6999,6 +7258,7 @@ var COMMANDS = [
6999
7258
  import {
7000
7259
  buildSchema,
7001
7260
  isObjectType,
7261
+ isInputObjectType,
7002
7262
  isListType,
7003
7263
  isNonNullType,
7004
7264
  isScalarType,
@@ -7156,6 +7416,21 @@ function createSchemaEngine(sdl) {
7156
7416
  const argPart = fieldArgs ? `(${fieldArgs})` : "";
7157
7417
  return `${opType} ${opName}${varPart} { ${entry.operation}${argPart} ${selectionSet} }`;
7158
7418
  }
7419
+ function getInputFields(operationName, operationType, inputArgName) {
7420
+ const field = getField(operationName, operationType);
7421
+ if (!field) return [];
7422
+ const argName = inputArgName ?? "input";
7423
+ const arg = field.args.find((a) => a.name === argName);
7424
+ if (!arg) return [];
7425
+ const namedType = unwrapType(arg.type);
7426
+ if (!isInputObjectType(namedType)) return [];
7427
+ const fields = namedType.getFields();
7428
+ return Object.entries(fields).map(([name, f]) => ({
7429
+ name,
7430
+ type: typeToString(f.type),
7431
+ required: isNonNullType(f.type)
7432
+ }));
7433
+ }
7159
7434
  function getCompletions(partial, commandNames) {
7160
7435
  return commandNames.filter(
7161
7436
  (name) => name.toLowerCase().startsWith(partial.toLowerCase())
@@ -7165,19 +7440,20 @@ function createSchemaEngine(sdl) {
7165
7440
  buildQuery,
7166
7441
  getOperationArgs,
7167
7442
  coerceArgs,
7443
+ getInputFields,
7168
7444
  getCompletions
7169
7445
  };
7170
7446
  }
7171
7447
 
7172
7448
  // src/lib/input.ts
7173
- import inquirer3 from "inquirer";
7449
+ import inquirer4 from "inquirer";
7174
7450
 
7175
7451
  // src/lib/config-loader.ts
7176
7452
  import { readFile } from "fs/promises";
7177
7453
  import { pathToFileURL as pathToFileURL2 } from "url";
7178
- import { resolve as resolve4 } from "path";
7454
+ import { resolve as resolve5 } from "path";
7179
7455
  async function loadConfig(filePath) {
7180
- const absPath = resolve4(filePath);
7456
+ const absPath = resolve5(filePath);
7181
7457
  if (filePath.endsWith(".ts")) {
7182
7458
  const configModule = await import(pathToFileURL2(absPath).href);
7183
7459
  return configModule.default;
@@ -7224,7 +7500,7 @@ function isUUID(value) {
7224
7500
  }
7225
7501
  async function confirmAction(message, opts) {
7226
7502
  if (opts?.confirm) return true;
7227
- const { confirmed } = await inquirer3.prompt([
7503
+ const { confirmed } = await inquirer4.prompt([
7228
7504
  {
7229
7505
  type: "confirm",
7230
7506
  name: "confirmed",
@@ -7239,11 +7515,11 @@ async function confirmAction(message, opts) {
7239
7515
  var __filename = fileURLToPath(import.meta.url);
7240
7516
  var __dirname = dirname5(__filename);
7241
7517
  function loadSchemaSDL() {
7242
- const bundledPath = resolve5(__dirname, "schema.graphql");
7518
+ const bundledPath = resolve6(__dirname, "schema.graphql");
7243
7519
  try {
7244
7520
  return readFileSync(bundledPath, "utf-8");
7245
7521
  } catch {
7246
- const monorepoPath = resolve5(
7522
+ const monorepoPath = resolve6(
7247
7523
  __dirname,
7248
7524
  "../../../graphql-core/schema.graphql"
7249
7525
  );
@@ -7343,6 +7619,26 @@ function registerDynamicCommands(program2, globalOpts) {
7343
7619
  if (entry.acceptsInput) {
7344
7620
  cmd = cmd.option("-d, --data <json>", "Data as JSON");
7345
7621
  cmd = cmd.option("-f, --file <path>", "Read data from file");
7622
+ const inputFields = engine.getInputFields(
7623
+ entry.operation,
7624
+ entry.operationType,
7625
+ entry.inputArgName
7626
+ );
7627
+ if (inputFields.length > 0) {
7628
+ const required = inputFields.filter((f) => f.required);
7629
+ const optional = inputFields.filter((f) => !f.required);
7630
+ let fieldHelp = "\nInput fields:";
7631
+ if (required.length > 0) {
7632
+ fieldHelp += "\n Required: " + required.map((f) => `${f.name} (${f.type})`).join(", ");
7633
+ }
7634
+ if (optional.length > 0) {
7635
+ fieldHelp += "\n Optional: " + optional.map((f) => `${f.name} (${f.type})`).join(", ");
7636
+ }
7637
+ cmd = cmd.addHelpText("after", fieldHelp);
7638
+ }
7639
+ }
7640
+ for (const cf of entry.customFlags ?? []) {
7641
+ cmd = cmd.option(cf.flag, cf.description);
7346
7642
  }
7347
7643
  if (entry.requiresConfirmation) {
7348
7644
  cmd = cmd.option("--confirm", "Skip confirmation prompt");
@@ -7361,9 +7657,15 @@ function registerDynamicCommands(program2, globalOpts) {
7361
7657
  }
7362
7658
  }
7363
7659
  const flags = actionArgs[positionals.length] ?? {};
7660
+ const customFlagNames = new Set(
7661
+ (entry.customFlags ?? []).map(
7662
+ (cf) => cf.flag.replace(/ <.*>$/, "").replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase())
7663
+ )
7664
+ );
7364
7665
  const rawFlags = {};
7365
7666
  for (const [key, val] of Object.entries(flags)) {
7366
7667
  if (key === "data" || key === "file" || key === "confirm") continue;
7668
+ if (customFlagNames.has(key)) continue;
7367
7669
  rawFlags[key] = String(val);
7368
7670
  }
7369
7671
  const coerced = engine.coerceArgs(
@@ -7372,11 +7674,97 @@ function registerDynamicCommands(program2, globalOpts) {
7372
7674
  rawFlags
7373
7675
  );
7374
7676
  Object.assign(variables, coerced);
7677
+ if (flags.dir && entry.acceptsInput) {
7678
+ const dirPath = resolve6(String(flags.dir));
7679
+ const files = readdirSync(dirPath).filter((f) => /\.(json|ts|js|mjs)$/.test(f)).sort();
7680
+ if (files.length === 0) {
7681
+ console.error(
7682
+ chalk7.yellow(`\u26A0 No .json/.ts/.js files found in ${dirPath}`)
7683
+ );
7684
+ return;
7685
+ }
7686
+ let created = 0;
7687
+ let updated = 0;
7688
+ let failed = 0;
7689
+ for (const file of files) {
7690
+ const filePath = resolve6(dirPath, file);
7691
+ const fileData = await parseInputData({ file: filePath });
7692
+ const argName = entry.inputArgName ?? "input";
7693
+ const fileVars = { ...variables, [argName]: fileData };
7694
+ const label = fileData.key ?? fileData.name ?? file;
7695
+ try {
7696
+ const q = engine.buildQuery(entry, fileVars);
7697
+ await client.request(q, fileVars);
7698
+ created++;
7699
+ if (!(opts.json || opts.jsonl || opts.quiet)) {
7700
+ success(`Created ${label}`);
7701
+ }
7702
+ } catch (err) {
7703
+ if (flags.upsert && fileData.key) {
7704
+ try {
7705
+ const updateEntry = COMMANDS.find(
7706
+ (c) => c.group === entry.group && c.name === "update"
7707
+ );
7708
+ if (updateEntry) {
7709
+ const updateVars = {
7710
+ ...variables,
7711
+ [updateEntry.inputArgName ?? "input"]: fileData,
7712
+ ...updateEntry.positionalArgs?.[0] ? {
7713
+ [updateEntry.positionalArgs[0].graphqlArg]: fileData.key
7714
+ } : {}
7715
+ };
7716
+ const uq = engine.buildQuery(updateEntry, updateVars);
7717
+ await client.request(uq, updateVars);
7718
+ updated++;
7719
+ if (!(opts.json || opts.jsonl || opts.quiet)) {
7720
+ success(`Updated ${label}`);
7721
+ }
7722
+ continue;
7723
+ }
7724
+ } catch (updateErr) {
7725
+ failed++;
7726
+ const msg2 = updateErr instanceof Error ? updateErr.message : String(updateErr);
7727
+ console.error(chalk7.red(`\u2717 ${label}:`), msg2);
7728
+ continue;
7729
+ }
7730
+ }
7731
+ failed++;
7732
+ const msg = err instanceof Error ? err.message : String(err);
7733
+ console.error(chalk7.red(`\u2717 ${label}:`), msg);
7734
+ }
7735
+ }
7736
+ if (!(opts.json || opts.jsonl || opts.quiet)) {
7737
+ console.log("");
7738
+ console.log(
7739
+ chalk7.bold(
7740
+ `Done: ${created} created${updated ? `, ${updated} updated` : ""}${failed ? `, ${failed} failed` : ""}`
7741
+ )
7742
+ );
7743
+ }
7744
+ return;
7745
+ }
7375
7746
  if (entry.acceptsInput && (flags.data || flags.file)) {
7376
7747
  const inputData = await parseInputData({
7377
7748
  data: flags.data,
7378
7749
  file: flags.file
7379
7750
  });
7751
+ const inputFields = engine.getInputFields(
7752
+ entry.operation,
7753
+ entry.operationType,
7754
+ entry.inputArgName
7755
+ );
7756
+ const fieldNames = new Set(inputFields.map((f) => f.name));
7757
+ if (fieldNames.has("projectId") && !inputData.projectId || fieldNames.has("tenantId") && !inputData.tenantId) {
7758
+ const project = await getProjectContext();
7759
+ if (project) {
7760
+ if (fieldNames.has("projectId") && !inputData.projectId) {
7761
+ inputData.projectId = project.id;
7762
+ }
7763
+ if (fieldNames.has("tenantId") && !inputData.tenantId) {
7764
+ inputData.tenantId = project.tenantId;
7765
+ }
7766
+ }
7767
+ }
7380
7768
  const argName = entry.inputArgName ?? "input";
7381
7769
  variables[argName] = inputData;
7382
7770
  }
@@ -7414,27 +7802,119 @@ function registerDynamicCommands(program2, globalOpts) {
7414
7802
  return;
7415
7803
  }
7416
7804
  }
7805
+ if (entry.group === "records" && entry.name === "publish" && variables.versionId) {
7806
+ const versionIdValue = String(variables.versionId);
7807
+ if (flags.model) {
7808
+ const lookupQuery = `query RecordByKey($naturalKey: String!) { recordByKey(naturalKey: $naturalKey) { id currentVersion { id } } }`;
7809
+ const lookupResult = await client.request(lookupQuery, {
7810
+ naturalKey: versionIdValue
7811
+ });
7812
+ const record = lookupResult.recordByKey;
7813
+ const currentVersion = record?.currentVersion;
7814
+ if (!currentVersion?.id) {
7815
+ throw new Error(
7816
+ `No current version found for record "${versionIdValue}"`
7817
+ );
7818
+ }
7819
+ variables.versionId = currentVersion.id;
7820
+ } else if (!isUUID(versionIdValue)) {
7821
+ const lookupQuery = `query RecordByKey($naturalKey: String!) { recordByKey(naturalKey: $naturalKey) { id currentVersion { id } } }`;
7822
+ const lookupResult = await client.request(lookupQuery, {
7823
+ naturalKey: versionIdValue
7824
+ });
7825
+ const record = lookupResult.recordByKey;
7826
+ const currentVersion = record?.currentVersion;
7827
+ if (currentVersion?.id) {
7828
+ variables.versionId = currentVersion.id;
7829
+ }
7830
+ } else {
7831
+ try {
7832
+ const lookupQuery = `query Record($id: ID!) { record(id: $id) { id recordType currentVersion { id } } }`;
7833
+ const lookupResult = await client.request(lookupQuery, {
7834
+ id: versionIdValue
7835
+ });
7836
+ const record = lookupResult.record;
7837
+ if (record?.recordType === "record" && record?.currentVersion) {
7838
+ const cv = record.currentVersion;
7839
+ if (cv.id) {
7840
+ variables.versionId = cv.id;
7841
+ }
7842
+ }
7843
+ } catch {
7844
+ }
7845
+ }
7846
+ }
7417
7847
  const queryStr = engine.buildQuery(entry, variables);
7418
- const result = await client.request(queryStr, variables);
7419
- const { data, total } = extractResult(result, entry.operation);
7420
- if (entry.scalarResult) {
7848
+ let result;
7849
+ let usedUpdate = false;
7850
+ try {
7851
+ result = await client.request(queryStr, variables);
7852
+ } catch (createErr) {
7853
+ if (flags.upsert && entry.name === "create") {
7854
+ const updateEntry = COMMANDS.find(
7855
+ (c) => c.group === entry.group && c.name === "update"
7856
+ );
7857
+ const inputArgName = entry.inputArgName ?? "input";
7858
+ const inputData = variables[inputArgName];
7859
+ if (updateEntry && inputData?.key) {
7860
+ const updateVars = {
7861
+ ...variables,
7862
+ [updateEntry.inputArgName ?? "input"]: inputData,
7863
+ ...updateEntry.positionalArgs?.[0] ? {
7864
+ [updateEntry.positionalArgs[0].graphqlArg]: inputData.key
7865
+ } : {}
7866
+ };
7867
+ const uq = engine.buildQuery(updateEntry, updateVars);
7868
+ result = await client.request(uq, updateVars);
7869
+ usedUpdate = true;
7870
+ } else {
7871
+ throw createErr;
7872
+ }
7873
+ } else {
7874
+ throw createErr;
7875
+ }
7876
+ }
7877
+ const activeEntry = usedUpdate ? COMMANDS.find(
7878
+ (c) => c.group === entry.group && c.name === "update"
7879
+ ) ?? entry : entry;
7880
+ const { data, total } = extractResult(result, activeEntry.operation);
7881
+ const responseData = data && typeof data === "object" && !Array.isArray(data) ? data : void 0;
7882
+ const displayEntry = usedUpdate ? activeEntry : entry;
7883
+ if (displayEntry.scalarResult) {
7421
7884
  if (!(opts.json || opts.jsonl || opts.quiet)) {
7422
- if (entry.successMessage) {
7423
- success(entry.successMessage);
7885
+ if (displayEntry.successMessage) {
7886
+ success(displayEntry.successMessage, responseData);
7424
7887
  }
7425
7888
  } else {
7426
7889
  formatOutput(data, opts);
7427
7890
  }
7428
7891
  } else if (Array.isArray(data)) {
7429
- const cliColumns = toCliColumns(entry.columns);
7892
+ const cliColumns = toCliColumns(displayEntry.columns);
7430
7893
  formatList(data, opts, {
7431
7894
  columns: cliColumns ?? autoColumns(data),
7432
7895
  total
7433
7896
  });
7434
7897
  } else {
7435
7898
  formatOutput(data, opts);
7436
- if (entry.successMessage && !(opts.json || opts.jsonl || opts.quiet)) {
7437
- success(entry.successMessage);
7899
+ if (displayEntry.successMessage && !(opts.json || opts.jsonl || opts.quiet)) {
7900
+ success(displayEntry.successMessage, responseData);
7901
+ }
7902
+ }
7903
+ if (flags.publish && entry.group === "records" && entry.name === "create" && responseData) {
7904
+ const version2 = responseData.version;
7905
+ const versionId = version2?.id;
7906
+ if (versionId) {
7907
+ const publishQuery = `mutation PublishVersion($versionId: ID!) { publishVersion(versionId: $versionId) }`;
7908
+ await client.request(publishQuery, { versionId });
7909
+ if (!(opts.json || opts.jsonl || opts.quiet)) {
7910
+ success("Published version {id}", { id: versionId });
7911
+ }
7912
+ } else if (!(opts.json || opts.jsonl || opts.quiet)) {
7913
+ console.error(
7914
+ chalk7.yellow(
7915
+ "\u26A0 Could not auto-publish: no version found in response"
7916
+ )
7917
+ );
7438
7918
  }
7439
7919
  }
7440
7920
  })
@@ -7455,7 +7935,7 @@ function autoColumns(items) {
7455
7935
  // src/cli.ts
7456
7936
  var __filename2 = fileURLToPath2(import.meta.url);
7457
7937
  var __dirname2 = dirname6(__filename2);
7458
- config({ path: resolve6(__dirname2, "../.env.local") });
7938
+ config({ path: resolve7(__dirname2, "../.env.local") });
7459
7939
  var require2 = createRequire(import.meta.url);
7460
7940
  var { version } = require2("../package.json");
7461
7941
  var program = new Command();
@@ -7477,5 +7957,6 @@ registerMediaCommands(program, getGlobalOpts);
7477
7957
  registerSearchCommands(program, getGlobalOpts);
7478
7958
  registerPullCommand(program, getGlobalOpts);
7479
7959
  registerCreateExtensionCommand(program, getGlobalOpts);
7960
+ registerInitCommands(program, getGlobalOpts);
7480
7961
  registerDynamicCommands(program, getGlobalOpts);
7481
7962
  program.parse();
@@ -202,6 +202,7 @@ type Query {
202
202
  billingCustomPackage(id: ID!): BillingCustomPackage
203
203
  billingSubscription: BillingSubscription
204
204
  billingUsageSummary(projectId: ID): BillingUsageSummary!
205
+ billingUsageAlerts: [UsageAlert!]!
205
206
  tenantBillingStatus(status: String, limit: Int, offset: Int): TenantBillingStatusResult!
206
207
  segments(isActive: Boolean, limit: Int, offset: Int): [Segment!]!
207
208
  segment(id: ID!): Segment
@@ -852,8 +853,13 @@ type Mutation {
852
853
  assignBillingCustomPackage(tenantId: ID!, customPackageId: ID!): BillingSubscription!
853
854
  cancelBillingSubscription(immediate: Boolean): BillingSubscription!
854
855
  reactivateBillingSubscription: BillingSubscription!
855
- createPaymentMethodSetupIntent: SetupIntentResult!
856
+
857
+ """
858
+ Create a Stripe Checkout Session for paid plan subscription. Returns URL to redirect to.
859
+ """
860
+ createCheckoutSession(planSlug: String!, successUrl: String!, cancelUrl: String!): CheckoutSessionResult!
856
861
  createBillingPortalSession(returnUrl: String): BillingPortalSession!
862
+ dismissBillingUsageAlert(id: ID!): Boolean!
857
863
  selfServiceSignup(input: SelfServiceSignupInput!): SelfServiceSignupResult!
858
864
  createSignupSession(input: CreateSignupSessionInput!): CreateSignupSessionResult!
859
865
  createSegment(input: CreateSegmentInput!): Segment!
@@ -1829,6 +1835,11 @@ type SessionContext {
1829
1835
 
1830
1836
  """Current user info"""
1831
1837
  user: AdminUser!
1838
+
1839
+ """
1840
+ Effective role for the current tenant+project context (e.g. PLATFORM_ADMIN, TENANT_OWNER, PROJECT_ADMIN, PROJECT_EDITOR, PROJECT_VIEWER)
1841
+ """
1842
+ effectiveRole: String
1832
1843
  }
1833
1844
 
1834
1845
  type Project {
@@ -2782,6 +2793,9 @@ type BillingPlanLimit {
2782
2793
  maxQuantity: BigInt
2783
2794
  overagePriceCents: Int
2784
2795
  overageUnitSize: Int!
2796
+
2797
+ """Whether this limit is unlimited (no max quantity)"""
2798
+ isUnlimited: Boolean!
2785
2799
  }
2786
2800
 
2787
2801
  type BillingCustomPackage {
@@ -2812,6 +2826,9 @@ type BillingPackageLimit {
2812
2826
  includedQuantity: BigInt
2813
2827
  maxQuantity: BigInt
2814
2828
  overagePriceCents: Int
2829
+
2830
+ """Whether this limit is unlimited (no max quantity)"""
2831
+ isUnlimited: Boolean!
2815
2832
  }
2816
2833
 
2817
2834
  type BillingSubscription {
@@ -2856,8 +2873,8 @@ type UsageLimitStatus {
2856
2873
  percentUsed: Float!
2857
2874
  }
2858
2875
 
2859
- type SetupIntentResult {
2860
- clientSecret: String!
2876
+ type CheckoutSessionResult {
2877
+ url: String!
2861
2878
  }
2862
2879
 
2863
2880
  type BillingPortalSession {
@@ -2936,6 +2953,16 @@ type TenantBillingStatusResult {
2936
2953
  total: Int!
2937
2954
  }
2938
2955
 
2956
+ type UsageAlert {
2957
+ id: ID!
2958
+ metric: BillingMetric!
2959
+ threshold: Int!
2960
+ currentUsage: Float!
2961
+ limitValue: Float!
2962
+ percentUsed: Int!
2963
+ createdAt: DateTime!
2964
+ }
2965
+
2939
2966
  input CreateCustomPackageInput {
2940
2967
  tenantId: ID!
2941
2968
  projectId: ID
@@ -3204,6 +3231,15 @@ type ExperimentFunnel {
3204
3231
  steps: [FunnelStep!]!
3205
3232
  }
3206
3233
 
3234
+ """Experiment lifecycle action"""
3235
+ enum ExperimentAction {
3236
+ start
3237
+ pause
3238
+ resume
3239
+ end
3240
+ declareWinner
3241
+ }
3242
+
3207
3243
  """Experiment stored in the dedicated experiments table"""
3208
3244
  type ExperimentRecord {
3209
3245
  id: ID!
@@ -3224,6 +3260,9 @@ type ExperimentRecord {
3224
3260
  funnel: ExperimentFunnel
3225
3261
  targetVariantCatalogId: ID
3226
3262
  isActive: Boolean!
3263
+
3264
+ """Actions available for the current experiment status"""
3265
+ allowedActions: [ExperimentAction!]!
3227
3266
  createdAt: DateTime!
3228
3267
  updatedAt: DateTime!
3229
3268
  }
@@ -4996,6 +5035,18 @@ input UpdateScheduleInput {
4996
5035
  isActive: Boolean
4997
5036
  }
4998
5037
 
5038
+ """Computed capabilities for a model based on its config"""
5039
+ type ModelCapabilities {
5040
+ isVersioned: Boolean!
5041
+ isPublishable: Boolean!
5042
+ hasVariants: Boolean!
5043
+ isInlineOnly: Boolean!
5044
+ recordCapable: Boolean!
5045
+ publicApi: Boolean!
5046
+ customerScoped: Boolean!
5047
+ sharingEnabled: Boolean!
5048
+ }
5049
+
4999
5050
  type Model {
5000
5051
  id: ID!
5001
5052
  key: String!
@@ -5034,6 +5085,9 @@ type Model {
5034
5085
  """Whether this model's fields can be edited"""
5035
5086
  editable: Boolean!
5036
5087
 
5088
+ """Computed capabilities based on config flags"""
5089
+ capabilities: ModelCapabilities!
5090
+
5037
5091
  """Current schema version"""
5038
5092
  currentVersionId: String
5039
5093
  publishedVersionNumber: Int
@@ -5112,6 +5166,19 @@ type Record {
5112
5166
  scheduledPublishAt: DateTime
5113
5167
  scheduledUnpublishAt: DateTime
5114
5168
  customerId: String
5169
+
5170
+ """
5171
+ Whether this record can be published (has unpublished changes on a publishable model)
5172
+ """
5173
+ canPublish: Boolean!
5174
+
5175
+ """
5176
+ Whether this record can be unpublished (has a published version on a publishable model)
5177
+ """
5178
+ canUnpublish: Boolean!
5179
+
5180
+ """Whether the current version differs from the published version"""
5181
+ hasUnpublishedChanges: Boolean!
5115
5182
  createdAt: DateTime!
5116
5183
  updatedAt: DateTime!
5117
5184
  createdBy: String
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eide/foir-cli",
3
- "version": "0.1.37",
4
- "description": "Universal platform CLI for EIDE — scriptable, composable resource management",
3
+ "version": "0.1.39",
4
+ "description": "Universal platform CLI for Foir platform",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"