@eide/foir-cli 0.3.3 → 0.4.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,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { config } from "dotenv";
5
- import { resolve as resolve6, dirname as dirname4 } from "path";
5
+ import { resolve as resolve7, dirname as dirname4 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { createRequire } from "module";
8
8
  import { Command } from "commander";
@@ -300,13 +300,13 @@ function withErrorHandler(optsFn, fn) {
300
300
  // src/commands/login.ts
301
301
  async function findAvailablePort(start, end) {
302
302
  for (let port = start; port <= end; port++) {
303
- const available = await new Promise((resolve7) => {
303
+ const available = await new Promise((resolve8) => {
304
304
  const server = http.createServer();
305
305
  server.listen(port, () => {
306
306
  server.close();
307
- resolve7(true);
307
+ resolve8(true);
308
308
  });
309
- server.on("error", () => resolve7(false));
309
+ server.on("error", () => resolve8(false));
310
310
  });
311
311
  if (available) return port;
312
312
  }
@@ -344,7 +344,7 @@ async function loginAction(globalOpts) {
344
344
  const state = crypto.randomBytes(16).toString("hex");
345
345
  const port = await findAvailablePort(9876, 9900);
346
346
  const redirectUri = `http://localhost:${port}/callback`;
347
- const authCode = await new Promise((resolve7, reject) => {
347
+ const authCode = await new Promise((resolve8, reject) => {
348
348
  let timeoutId;
349
349
  const server = http.createServer((req, res) => {
350
350
  const url = new URL(req.url, `http://localhost:${port}`);
@@ -383,7 +383,7 @@ async function loginAction(globalOpts) {
383
383
  );
384
384
  server.closeAllConnections();
385
385
  server.close();
386
- resolve7(code);
386
+ resolve8(code);
387
387
  }
388
388
  });
389
389
  server.listen(port);
@@ -1860,7 +1860,14 @@ function createConfigsMethods(client) {
1860
1860
  configData
1861
1861
  })
1862
1862
  );
1863
- return resp.config ?? null;
1863
+ const config2 = resp.config ?? null;
1864
+ if (!config2) return null;
1865
+ return {
1866
+ ...config2,
1867
+ summary: resp.summary ?? null,
1868
+ provisionedApiKeys: resp.provisionedApiKeys ?? [],
1869
+ webhookSecret: resp.webhookSecret ?? null
1870
+ };
1864
1871
  },
1865
1872
  async deleteConfig(id) {
1866
1873
  const resp = await client.deleteConfig(
@@ -4226,7 +4233,7 @@ Edit the files, then run:
4226
4233
 
4227
4234
  // src/commands/push.ts
4228
4235
  import chalk6 from "chalk";
4229
- import { existsSync as existsSync4 } from "fs";
4236
+ import { existsSync as existsSync4, readFileSync, writeFileSync as writeFileSync2 } from "fs";
4230
4237
  import { resolve as resolve4 } from "path";
4231
4238
  var CONFIG_FILE_NAMES = [
4232
4239
  "foir.config.ts",
@@ -4241,8 +4248,25 @@ function discoverConfigFile() {
4241
4248
  }
4242
4249
  return null;
4243
4250
  }
4251
+ function writeEnvVar(envPath, key, value) {
4252
+ let content = "";
4253
+ if (existsSync4(envPath)) {
4254
+ content = readFileSync(envPath, "utf-8");
4255
+ const regex = new RegExp(`^${key}=`, "m");
4256
+ if (regex.test(content)) {
4257
+ return false;
4258
+ }
4259
+ }
4260
+ if (content && !content.endsWith("\n")) {
4261
+ content += "\n";
4262
+ }
4263
+ content += `${key}=${value}
4264
+ `;
4265
+ writeFileSync2(envPath, content, "utf-8");
4266
+ return true;
4267
+ }
4244
4268
  function registerPushCommand(program2, globalOpts) {
4245
- program2.command("push").description("Push foir.config.ts to the platform").option("--config <path>", "Path to config file (default: auto-discover)").option("--force", "Force reinstall (delete and recreate)", false).action(
4269
+ program2.command("push").description("Push foir.config.ts to the platform").option("--config <path>", "Path to config file (default: auto-discover)").option("--force", "Force reinstall (delete and recreate)", false).option("--env <path>", "Path to .env file (default: .env)").action(
4246
4270
  withErrorHandler(
4247
4271
  globalOpts,
4248
4272
  async (opts) => {
@@ -4279,18 +4303,203 @@ function registerPushCommand(program2, globalOpts) {
4279
4303
  );
4280
4304
  }
4281
4305
  console.log();
4282
- console.log(chalk6.green("Config applied successfully."));
4283
- console.log();
4306
+ console.log(chalk6.green("\u2713 Config applied successfully"));
4284
4307
  console.log(` Config ID: ${chalk6.cyan(result.id)}`);
4285
4308
  console.log(` Config Key: ${chalk6.cyan(result.key)}`);
4309
+ const summary = result.summary;
4310
+ if (summary) {
4311
+ console.log();
4312
+ const lines = [];
4313
+ if (summary.modelsCreated || summary.modelsUpdated) {
4314
+ lines.push(
4315
+ ` Models: ${summary.modelsCreated ?? 0} created, ${summary.modelsUpdated ?? 0} updated`
4316
+ );
4317
+ }
4318
+ if (summary.operationsCreated || summary.operationsUpdated) {
4319
+ lines.push(
4320
+ ` Operations: ${summary.operationsCreated ?? 0} created, ${summary.operationsUpdated ?? 0} updated`
4321
+ );
4322
+ }
4323
+ if (summary.hooksCreated || summary.hooksUpdated) {
4324
+ lines.push(
4325
+ ` Hooks: ${summary.hooksCreated ?? 0} created, ${summary.hooksUpdated ?? 0} updated`
4326
+ );
4327
+ }
4328
+ if (summary.segmentsCreated || summary.segmentsUpdated) {
4329
+ lines.push(
4330
+ ` Segments: ${summary.segmentsCreated ?? 0} created, ${summary.segmentsUpdated ?? 0} updated`
4331
+ );
4332
+ }
4333
+ if (summary.schedulesCreated || summary.schedulesUpdated) {
4334
+ lines.push(
4335
+ ` Schedules: ${summary.schedulesCreated ?? 0} created, ${summary.schedulesUpdated ?? 0} updated`
4336
+ );
4337
+ }
4338
+ if (summary.authProvidersCreated || summary.authProvidersUpdated) {
4339
+ lines.push(
4340
+ ` Auth: ${summary.authProvidersCreated ?? 0} created, ${summary.authProvidersUpdated ?? 0} updated`
4341
+ );
4342
+ }
4343
+ if (summary.resourcesDeleted) {
4344
+ lines.push(
4345
+ ` Cleaned up: ${summary.resourcesDeleted} orphaned resource(s)`
4346
+ );
4347
+ }
4348
+ if (lines.length > 0) {
4349
+ for (const line of lines) {
4350
+ console.log(line);
4351
+ }
4352
+ }
4353
+ }
4354
+ const envPath = resolve4(opts.env ?? ".env");
4355
+ const r = result;
4356
+ const provisionedKeys = r.provisionedApiKeys;
4357
+ const webhookSecret = r.webhookSecret;
4358
+ const envWrites = [];
4359
+ if (provisionedKeys && provisionedKeys.length > 0) {
4360
+ for (const pk of provisionedKeys) {
4361
+ envWrites.push({
4362
+ key: pk.envVar,
4363
+ value: pk.rawKey,
4364
+ label: `${pk.name} (${pk.keyType})`
4365
+ });
4366
+ }
4367
+ }
4368
+ if (webhookSecret) {
4369
+ envWrites.push({
4370
+ key: "FOIR_WEBHOOK_SECRET",
4371
+ value: webhookSecret,
4372
+ label: "Webhook signing secret"
4373
+ });
4374
+ }
4375
+ if (envWrites.length > 0) {
4376
+ console.log();
4377
+ for (const { key, value, label } of envWrites) {
4378
+ const written = writeEnvVar(envPath, key, value);
4379
+ if (written) {
4380
+ console.log(
4381
+ chalk6.green(`\u2713 ${label}`) + chalk6.dim(` \u2192 ${key} written to ${envPath}`)
4382
+ );
4383
+ } else {
4384
+ console.log(
4385
+ chalk6.dim(` ${label}: ${key} already exists in ${envPath}, skipped`)
4386
+ );
4387
+ }
4388
+ }
4389
+ }
4286
4390
  console.log();
4287
4391
  }
4288
4392
  )
4289
4393
  );
4290
4394
  }
4291
4395
 
4292
- // src/commands/remove.ts
4396
+ // src/commands/pull.ts
4293
4397
  import chalk7 from "chalk";
4398
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
4399
+ import { resolve as resolve5 } from "path";
4400
+ import prettier from "prettier";
4401
+ var DEFAULT_OUTPUT = "foir.config.ts";
4402
+ function registerPullCommand(program2, globalOpts) {
4403
+ program2.command("pull").description("Export platform config to foir.config.ts").option("--key <configKey>", "Config key to export").option("--out <path>", `Output file (default: ${DEFAULT_OUTPUT})`).option("--force", "Overwrite existing file without prompting", false).action(
4404
+ withErrorHandler(
4405
+ globalOpts,
4406
+ async (opts) => {
4407
+ const client = await createPlatformClient(globalOpts());
4408
+ let configKey = opts.key;
4409
+ if (!configKey) {
4410
+ const { configs } = await client.configs.listConfigs({ limit: 50 });
4411
+ if (!configs || configs.length === 0) {
4412
+ throw new Error(
4413
+ "No configs found in this project. Push one first with `foir push`."
4414
+ );
4415
+ }
4416
+ if (configs.length === 1) {
4417
+ configKey = configs[0].key;
4418
+ } else {
4419
+ console.log("Available configs:");
4420
+ for (const c of configs) {
4421
+ console.log(` ${chalk7.cyan(c.key)} \u2014 ${c.name}`);
4422
+ }
4423
+ throw new Error(
4424
+ "Multiple configs found. Use --key <configKey> to specify which one to export."
4425
+ );
4426
+ }
4427
+ }
4428
+ console.log(chalk7.dim(`Fetching config "${configKey}"...`));
4429
+ const config2 = await client.configs.getConfigByKey(configKey);
4430
+ if (!config2) {
4431
+ throw new Error(`Config "${configKey}" not found.`);
4432
+ }
4433
+ const configData = config2.config;
4434
+ if (!configData) {
4435
+ throw new Error(
4436
+ `Config "${configKey}" has no config data. It may have been created via the admin UI without a manifest.`
4437
+ );
4438
+ }
4439
+ const manifest = {
4440
+ key: config2.key,
4441
+ name: config2.name,
4442
+ ...config2.configType && config2.configType !== "custom" ? { configType: config2.configType } : {},
4443
+ ...config2.direction ? { direction: config2.direction } : {},
4444
+ ...config2.description ? { description: config2.description } : {},
4445
+ ...config2.connectionDomain ? { operationBaseUrl: config2.connectionDomain } : {},
4446
+ ...configData
4447
+ };
4448
+ delete manifest.force;
4449
+ const jsonContent = JSON.stringify(manifest, null, 2);
4450
+ const tsContent = `/**
4451
+ * ${manifest.name} \u2014 Foir Config
4452
+ *
4453
+ * Exported from platform via \`foir pull\`.
4454
+ * Push changes back with \`foir push\`.
4455
+ */
4456
+
4457
+ import { defineConfig } from '@eide/foir-cli/configs';
4458
+
4459
+ export default defineConfig(${jsonContent});
4460
+ `;
4461
+ let formatted;
4462
+ try {
4463
+ formatted = await prettier.format(tsContent, {
4464
+ parser: "typescript",
4465
+ singleQuote: true,
4466
+ trailingComma: "all"
4467
+ });
4468
+ } catch {
4469
+ formatted = tsContent;
4470
+ }
4471
+ const outPath = resolve5(opts.out ?? DEFAULT_OUTPUT);
4472
+ if (existsSync5(outPath) && !opts.force) {
4473
+ throw new Error(
4474
+ `${outPath} already exists. Use --force to overwrite.`
4475
+ );
4476
+ }
4477
+ writeFileSync3(outPath, formatted, "utf-8");
4478
+ console.log(chalk7.green(`\u2713 Exported to ${outPath}`));
4479
+ const models = configData.models ?? [];
4480
+ const operations = configData.operations ?? [];
4481
+ const hooks = configData.hooks ?? [];
4482
+ const segments = configData.segments ?? [];
4483
+ const schedules = configData.schedules ?? [];
4484
+ const authProviders = configData.authProviders ?? [];
4485
+ const parts = [];
4486
+ if (models.length > 0) parts.push(`${models.length} model(s)`);
4487
+ if (operations.length > 0) parts.push(`${operations.length} operation(s)`);
4488
+ if (hooks.length > 0) parts.push(`${hooks.length} hook(s)`);
4489
+ if (segments.length > 0) parts.push(`${segments.length} segment(s)`);
4490
+ if (schedules.length > 0) parts.push(`${schedules.length} schedule(s)`);
4491
+ if (authProviders.length > 0) parts.push(`${authProviders.length} auth provider(s)`);
4492
+ if (configData.customerProfileSchema) parts.push("customer profile schema");
4493
+ if (parts.length > 0) {
4494
+ console.log(chalk7.dim(` Contains: ${parts.join(", ")}`));
4495
+ }
4496
+ }
4497
+ )
4498
+ );
4499
+ }
4500
+
4501
+ // src/commands/remove.ts
4502
+ import chalk8 from "chalk";
4294
4503
  import inquirer5 from "inquirer";
4295
4504
  function registerRemoveCommand(program2, globalOpts) {
4296
4505
  program2.command("remove <key>").description("Remove a config and all its provisioned resources").option("--force", "Skip confirmation prompt", false).action(
@@ -4312,13 +4521,13 @@ function registerRemoveCommand(program2, globalOpts) {
4312
4521
  }
4313
4522
  ]);
4314
4523
  if (!confirmed) {
4315
- console.log(chalk7.dim("Cancelled."));
4524
+ console.log(chalk8.dim("Cancelled."));
4316
4525
  return;
4317
4526
  }
4318
4527
  }
4319
4528
  await client.configs.deleteConfig(config2.id);
4320
4529
  console.log(
4321
- chalk7.green(`Removed config "${config2.name}" (${config2.key}).`)
4530
+ chalk8.green(`Removed config "${config2.name}" (${config2.key}).`)
4322
4531
  );
4323
4532
  }
4324
4533
  )
@@ -4326,7 +4535,7 @@ function registerRemoveCommand(program2, globalOpts) {
4326
4535
  }
4327
4536
 
4328
4537
  // src/commands/profiles.ts
4329
- import chalk8 from "chalk";
4538
+ import chalk9 from "chalk";
4330
4539
  function registerProfilesCommand(program2, globalOpts) {
4331
4540
  const profiles = program2.command("profiles").description("Manage named project profiles");
4332
4541
  profiles.command("list").description("List all saved project profiles").action(
@@ -4486,7 +4695,7 @@ function registerProfilesCommand(program2, globalOpts) {
4486
4695
  if (opts.json || opts.jsonl) {
4487
4696
  formatOutput({ deleted: name }, opts);
4488
4697
  } else {
4489
- console.log(chalk8.green(`Deleted profile "${name}".`));
4698
+ console.log(chalk9.green(`Deleted profile "${name}".`));
4490
4699
  }
4491
4700
  }
4492
4701
  )
@@ -4495,8 +4704,8 @@ function registerProfilesCommand(program2, globalOpts) {
4495
4704
 
4496
4705
  // src/commands/register-commands.ts
4497
4706
  import { readdirSync } from "fs";
4498
- import { resolve as resolve5 } from "path";
4499
- import chalk9 from "chalk";
4707
+ import { resolve as resolve6 } from "path";
4708
+ import chalk10 from "chalk";
4500
4709
 
4501
4710
  // src/command-registry/command-map.ts
4502
4711
  var COMMANDS = [
@@ -6294,7 +6503,7 @@ function buildDispatchTable() {
6294
6503
  unregisterConfig: async (v, c) => await c.configs.deleteConfig(str(v.id)),
6295
6504
  triggerConfigSync: async (v, _c) => {
6296
6505
  console.log(
6297
- chalk9.yellow(
6506
+ chalk10.yellow(
6298
6507
  `Config sync trigger for ${str(v.configId)} is not yet available via ConnectRPC.`
6299
6508
  )
6300
6509
  );
@@ -6464,11 +6673,11 @@ function registerDynamicCommands(program2, globalOpts) {
6464
6673
  variables.limit = flags.limit;
6465
6674
  }
6466
6675
  if (flags.dir && entry.acceptsInput) {
6467
- const dirPath = resolve5(String(flags.dir));
6676
+ const dirPath = resolve6(String(flags.dir));
6468
6677
  const files = readdirSync(dirPath).filter((f) => /\.(json|ts|js|mjs)$/.test(f)).sort();
6469
6678
  if (files.length === 0) {
6470
6679
  console.error(
6471
- chalk9.yellow(`No .json/.ts/.js files found in ${dirPath}`)
6680
+ chalk10.yellow(`No .json/.ts/.js files found in ${dirPath}`)
6472
6681
  );
6473
6682
  return;
6474
6683
  }
@@ -6476,7 +6685,7 @@ function registerDynamicCommands(program2, globalOpts) {
6476
6685
  let updated = 0;
6477
6686
  let failed = 0;
6478
6687
  for (const file of files) {
6479
- const filePath = resolve5(dirPath, file);
6688
+ const filePath = resolve6(dirPath, file);
6480
6689
  const fileData = await parseInputData({ file: filePath });
6481
6690
  const argName = entry.inputArgName ?? "input";
6482
6691
  const fileVars = { ...variables, [argName]: fileData };
@@ -6511,19 +6720,19 @@ function registerDynamicCommands(program2, globalOpts) {
6511
6720
  } catch (updateErr) {
6512
6721
  failed++;
6513
6722
  const msg2 = updateErr instanceof Error ? updateErr.message : String(updateErr);
6514
- console.error(chalk9.red(`\u2717 ${label}:`), msg2);
6723
+ console.error(chalk10.red(`\u2717 ${label}:`), msg2);
6515
6724
  continue;
6516
6725
  }
6517
6726
  }
6518
6727
  failed++;
6519
6728
  const msg = err instanceof Error ? err.message : String(err);
6520
- console.error(chalk9.red(`\u2717 ${label}:`), msg);
6729
+ console.error(chalk10.red(`\u2717 ${label}:`), msg);
6521
6730
  }
6522
6731
  }
6523
6732
  if (!(opts.json || opts.jsonl || opts.quiet)) {
6524
6733
  console.log("");
6525
6734
  console.log(
6526
- chalk9.bold(
6735
+ chalk10.bold(
6527
6736
  `Done: ${created} created${updated ? `, ${updated} updated` : ""}${failed ? `, ${failed} failed` : ""}`
6528
6737
  )
6529
6738
  );
@@ -6684,7 +6893,7 @@ function registerDynamicCommands(program2, globalOpts) {
6684
6893
  }
6685
6894
  } else if (!(opts.json || opts.jsonl || opts.quiet)) {
6686
6895
  console.error(
6687
- chalk9.yellow(
6896
+ chalk10.yellow(
6688
6897
  "Could not auto-publish: no version found in response"
6689
6898
  )
6690
6899
  );
@@ -6699,7 +6908,7 @@ function registerDynamicCommands(program2, globalOpts) {
6699
6908
  // src/cli.ts
6700
6909
  var __filename = fileURLToPath(import.meta.url);
6701
6910
  var __dirname = dirname4(__filename);
6702
- config({ path: resolve6(__dirname, "../.env.local") });
6911
+ config({ path: resolve7(__dirname, "../.env.local") });
6703
6912
  var require2 = createRequire(import.meta.url);
6704
6913
  var { version } = require2("../package.json");
6705
6914
  var program = new Command();
@@ -6722,6 +6931,7 @@ registerProfilesCommand(program, getGlobalOpts);
6722
6931
  registerMediaCommands(program, getGlobalOpts);
6723
6932
  registerSearchCommands(program, getGlobalOpts);
6724
6933
  registerPushCommand(program, getGlobalOpts);
6934
+ registerPullCommand(program, getGlobalOpts);
6725
6935
  registerRemoveCommand(program, getGlobalOpts);
6726
6936
  registerCreateConfigCommand(program, getGlobalOpts);
6727
6937
  registerInitCommands(program, getGlobalOpts);
@@ -98,6 +98,16 @@ interface ApplyConfigHookInput {
98
98
  expression?: Record<string, unknown>;
99
99
  hooks?: ApplyConfigHookInput[];
100
100
  }
101
+ interface ApplyConfigApiKeyInput {
102
+ /** Name for this API key (e.g. "Tilly iOS", "Tilly BFF"). */
103
+ name: string;
104
+ /** Key type: 'public' for client apps, 'secret' for BFF/server. */
105
+ keyType: 'public' | 'secret';
106
+ /** Environment variable name to write the key to in .env (e.g. "FOIR_PUBLIC_KEY"). */
107
+ envVar: string;
108
+ /** Optional scopes to restrict the key. */
109
+ scopes?: Record<string, unknown>;
110
+ }
101
111
  interface ApplyConfigInput {
102
112
  key: string;
103
113
  name: string;
@@ -110,6 +120,7 @@ interface ApplyConfigInput {
110
120
  hooks?: ApplyConfigHookInput[];
111
121
  authProviders?: ApplyConfigAuthProviderInput[];
112
122
  placements?: ApplyConfigPlacementInput[];
123
+ apiKeys?: ApplyConfigApiKeyInput[];
113
124
  [key: string]: unknown;
114
125
  }
115
126
  /** Define a complete config manifest. */
@@ -133,4 +144,4 @@ declare function defineHook(hook: ApplyConfigHookInput): ApplyConfigHookInput;
133
144
  /** Define an editor placement (sidebar or main-editor tab). */
134
145
  declare function definePlacement(placement: ApplyConfigPlacementInput): ApplyConfigPlacementInput;
135
146
 
136
- export { type ApplyConfigAuthProviderInput, type ApplyConfigHookInput, type ApplyConfigInput, type ApplyConfigModelInput, type ApplyConfigOperationInput, type ApplyConfigPlacementInput, type ApplyConfigScheduleInput, type ApplyConfigSegmentInput, type FieldDefinitionInput, defineAuthProvider, defineConfig, defineExtension, defineField, defineHook, defineModel, defineOperation, definePlacement, defineSchedule, defineSegment };
147
+ export { type ApplyConfigApiKeyInput, type ApplyConfigAuthProviderInput, type ApplyConfigHookInput, type ApplyConfigInput, type ApplyConfigModelInput, type ApplyConfigOperationInput, type ApplyConfigPlacementInput, type ApplyConfigScheduleInput, type ApplyConfigSegmentInput, type FieldDefinitionInput, defineAuthProvider, defineConfig, defineExtension, defineField, defineHook, defineModel, defineOperation, definePlacement, defineSchedule, defineSegment };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eide/foir-cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Universal platform CLI for Foir platform",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -49,7 +49,7 @@
49
49
  "@bufbuild/protobuf": "^2.0.0",
50
50
  "@connectrpc/connect": "^2.0.0",
51
51
  "@connectrpc/connect-node": "^2.0.0",
52
- "@eide/foir-proto-ts": "^0.1.0",
52
+ "@eide/foir-proto-ts": "^0.3.1",
53
53
  "chalk": "^5.3.0",
54
54
  "commander": "^12.1.0",
55
55
  "dotenv": "^16.4.5",