@eide/foir-cli 0.4.1 → 0.4.2

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
@@ -464,6 +464,9 @@ import { SegmentsService as SegmentsService2 } from "@eide/foir-proto-ts/segment
464
464
  import { ExperimentsService as ExperimentsService2 } from "@eide/foir-proto-ts/experiments/v1/experiments_pb";
465
465
  import { SettingsService as SettingsService2 } from "@eide/foir-proto-ts/settings/v1/settings_pb";
466
466
  import { StorageService as StorageService2 } from "@eide/foir-proto-ts/storage/v1/storage_pb";
467
+ import { OperationsService as OperationsService2 } from "@eide/foir-proto-ts/operations/v1/operations_pb";
468
+ import { HooksService as HooksService2 } from "@eide/foir-proto-ts/hooks/v1/hooks_pb";
469
+ import { SchedulesService as SchedulesService2 } from "@eide/foir-proto-ts/schedules/v1/schedules_pb";
467
470
 
468
471
  // src/lib/rpc/identity.ts
469
472
  import { create } from "@bufbuild/protobuf";
@@ -937,13 +940,18 @@ function createIdentityMethods(client) {
937
940
  },
938
941
  // ── API Keys ──────────────────────────────────────────
939
942
  async createApiKey(params) {
940
- const resp = await client.createApiKey(
941
- create(CreateApiKeyRequestSchema, {
942
- name: params.name,
943
- keyType: params.keyType,
944
- rateLimitPerHour: params.rateLimitPerHour
945
- })
946
- );
943
+ const req = create(CreateApiKeyRequestSchema, {
944
+ name: params.name,
945
+ keyType: params.keyType,
946
+ rateLimitPerHour: params.rateLimitPerHour
947
+ });
948
+ if (params.allowedModels?.length) {
949
+ req.allowedModels = params.allowedModels;
950
+ }
951
+ if (params.allowedFileTypes?.length) {
952
+ req.allowedFileTypes = params.allowedFileTypes;
953
+ }
954
+ const resp = await client.createApiKey(req);
947
955
  return { apiKey: resp.apiKey ?? null };
948
956
  },
949
957
  async getApiKey(id) {
@@ -2693,6 +2701,264 @@ function createStorageMethods(client) {
2693
2701
  };
2694
2702
  }
2695
2703
 
2704
+ // src/lib/rpc/operations.ts
2705
+ import { create as create9 } from "@bufbuild/protobuf";
2706
+ import {
2707
+ ListOperationsRequestSchema as ListOperationsRequestSchema2,
2708
+ GetOperationRequestSchema,
2709
+ CreateOperationRequestSchema,
2710
+ UpdateOperationRequestSchema,
2711
+ DeleteOperationRequestSchema
2712
+ } from "@eide/foir-proto-ts/operations/v1/operations_pb";
2713
+ function createOperationsMethods(client) {
2714
+ return {
2715
+ // ── CRUD ──────────────────────────────────────────────────
2716
+ async listOperations(params = {}) {
2717
+ return client.listOperations(
2718
+ create9(ListOperationsRequestSchema2, {
2719
+ configId: params.configId,
2720
+ category: params.category,
2721
+ isActive: params.isActive,
2722
+ search: params.search,
2723
+ limit: params.limit ?? 50,
2724
+ offset: params.offset ?? 0
2725
+ })
2726
+ );
2727
+ },
2728
+ async getOperation(params) {
2729
+ const resp = await client.getOperation(
2730
+ create9(GetOperationRequestSchema, {
2731
+ id: params.id ?? "",
2732
+ key: params.key
2733
+ })
2734
+ );
2735
+ return resp.operation ?? null;
2736
+ },
2737
+ async createOperation(params) {
2738
+ const resp = await client.createOperation(
2739
+ create9(CreateOperationRequestSchema, {
2740
+ key: params.key,
2741
+ name: params.name,
2742
+ endpoint: params.endpoint,
2743
+ description: params.description,
2744
+ icon: params.icon,
2745
+ category: params.category,
2746
+ endpointAuth: params.endpointAuth,
2747
+ timeoutMs: params.timeoutMs,
2748
+ inputSchema: params.inputSchema,
2749
+ outputSchema: params.outputSchema,
2750
+ configId: params.configId
2751
+ })
2752
+ );
2753
+ return resp.operation ?? null;
2754
+ },
2755
+ async updateOperation(params) {
2756
+ const resp = await client.updateOperation(
2757
+ create9(UpdateOperationRequestSchema, {
2758
+ id: params.id,
2759
+ name: params.name,
2760
+ description: params.description,
2761
+ endpoint: params.endpoint,
2762
+ endpointAuth: params.endpointAuth,
2763
+ timeoutMs: params.timeoutMs,
2764
+ inputSchema: params.inputSchema,
2765
+ outputSchema: params.outputSchema,
2766
+ isActive: params.isActive
2767
+ })
2768
+ );
2769
+ return resp.operation ?? null;
2770
+ },
2771
+ async deleteOperation(id) {
2772
+ const resp = await client.deleteOperation(
2773
+ create9(DeleteOperationRequestSchema, { id })
2774
+ );
2775
+ return resp.success;
2776
+ }
2777
+ };
2778
+ }
2779
+
2780
+ // src/lib/rpc/hooks.ts
2781
+ import { create as create10 } from "@bufbuild/protobuf";
2782
+ import {
2783
+ ListHooksRequestSchema,
2784
+ GetHookRequestSchema,
2785
+ GetHookByKeyRequestSchema,
2786
+ CreateHookRequestSchema,
2787
+ UpdateHookRequestSchema,
2788
+ DeleteHookRequestSchema,
2789
+ ListHookDeliveriesRequestSchema,
2790
+ RetryHookDeliveryRequestSchema,
2791
+ TestHookRequestSchema
2792
+ } from "@eide/foir-proto-ts/hooks/v1/hooks_pb";
2793
+ function createHooksMethods(client) {
2794
+ return {
2795
+ // ── Queries ──────────────────────────────────────────────
2796
+ async listHooks(params = {}) {
2797
+ return client.listHooks(
2798
+ create10(ListHooksRequestSchema, {
2799
+ event: params.event,
2800
+ isActive: params.isActive,
2801
+ configId: params.configId,
2802
+ limit: params.limit ?? 50,
2803
+ offset: params.offset ?? 0
2804
+ })
2805
+ );
2806
+ },
2807
+ async getHook(id) {
2808
+ const resp = await client.getHook(create10(GetHookRequestSchema, { id }));
2809
+ return resp.hook ?? null;
2810
+ },
2811
+ // ── Mutations ────────────────────────────────────────────
2812
+ async createHook(params) {
2813
+ const resp = await client.createHook(
2814
+ create10(CreateHookRequestSchema, {
2815
+ key: params.key,
2816
+ name: params.name,
2817
+ event: params.event,
2818
+ targetType: params.targetType,
2819
+ description: params.description,
2820
+ operationKey: params.operationKey,
2821
+ notificationConfig: params.notificationConfig ?? void 0,
2822
+ filter: params.filter ?? void 0,
2823
+ configId: params.configId
2824
+ })
2825
+ );
2826
+ return resp.hook ?? null;
2827
+ },
2828
+ async updateHook(params) {
2829
+ const resp = await client.updateHook(
2830
+ create10(UpdateHookRequestSchema, {
2831
+ id: params.id,
2832
+ name: params.name,
2833
+ description: params.description,
2834
+ operationKey: params.operationKey,
2835
+ notificationConfig: params.notificationConfig ?? void 0,
2836
+ filter: params.filter ?? void 0,
2837
+ isActive: params.isActive
2838
+ })
2839
+ );
2840
+ return resp.hook ?? null;
2841
+ },
2842
+ async deleteHook(id) {
2843
+ const resp = await client.deleteHook(
2844
+ create10(DeleteHookRequestSchema, { id })
2845
+ );
2846
+ return resp.success;
2847
+ },
2848
+ // ── Get by Key ──────────────────────────────────────────
2849
+ async getHookByKey(key) {
2850
+ const resp = await client.getHookByKey(
2851
+ create10(GetHookByKeyRequestSchema, { key })
2852
+ );
2853
+ return resp.hook ?? null;
2854
+ },
2855
+ // ── Deliveries ──────────────────────────────────────────
2856
+ async listHookDeliveries(params) {
2857
+ return client.listHookDeliveries(
2858
+ create10(ListHookDeliveriesRequestSchema, {
2859
+ hookId: params.hookId,
2860
+ status: params.status,
2861
+ limit: params.limit ?? 50,
2862
+ offset: params.offset ?? 0
2863
+ })
2864
+ );
2865
+ },
2866
+ async retryHookDelivery(deliveryId) {
2867
+ const resp = await client.retryHookDelivery(
2868
+ create10(RetryHookDeliveryRequestSchema, { deliveryId })
2869
+ );
2870
+ return resp.success;
2871
+ },
2872
+ // ── Testing ─────────────────────────────────────────────
2873
+ async testHook(params) {
2874
+ const resp = await client.testHook(
2875
+ create10(TestHookRequestSchema, {
2876
+ hookId: params.hookId,
2877
+ testPayload: params.testPayload
2878
+ })
2879
+ );
2880
+ return {
2881
+ success: resp.success,
2882
+ error: resp.error ?? void 0
2883
+ };
2884
+ }
2885
+ };
2886
+ }
2887
+
2888
+ // src/lib/rpc/cron-schedules.ts
2889
+ import { create as create11 } from "@bufbuild/protobuf";
2890
+ import {
2891
+ ListCronSchedulesRequestSchema,
2892
+ GetCronScheduleRequestSchema,
2893
+ GetCronScheduleByKeyRequestSchema,
2894
+ CreateCronScheduleRequestSchema,
2895
+ UpdateCronScheduleRequestSchema,
2896
+ DeleteCronScheduleRequestSchema
2897
+ } from "@eide/foir-proto-ts/schedules/v1/schedules_pb";
2898
+ function createCronSchedulesMethods(client) {
2899
+ return {
2900
+ // ── Queries ──────────────────────────────────────────────
2901
+ async listCronSchedules(params = {}) {
2902
+ return client.listCronSchedules(
2903
+ create11(ListCronSchedulesRequestSchema, {
2904
+ configId: params.configId,
2905
+ isActive: params.isActive,
2906
+ limit: params.limit ?? 50,
2907
+ offset: params.offset ?? 0
2908
+ })
2909
+ );
2910
+ },
2911
+ async getCronSchedule(id) {
2912
+ const resp = await client.getCronSchedule(
2913
+ create11(GetCronScheduleRequestSchema, { id })
2914
+ );
2915
+ return resp.schedule ?? null;
2916
+ },
2917
+ async getCronScheduleByKey(key) {
2918
+ const resp = await client.getCronScheduleByKey(
2919
+ create11(GetCronScheduleByKeyRequestSchema, { key })
2920
+ );
2921
+ return resp.schedule ?? null;
2922
+ },
2923
+ // ── Mutations ────────────────────────────────────────────
2924
+ async createCronSchedule(params) {
2925
+ const resp = await client.createCronSchedule(
2926
+ create11(CreateCronScheduleRequestSchema, {
2927
+ key: params.key,
2928
+ name: params.name,
2929
+ cron: params.cron,
2930
+ timezone: params.timezone,
2931
+ operationKey: params.operationKey,
2932
+ configId: params.configId,
2933
+ targetType: params.targetType,
2934
+ targetConfig: params.targetConfig
2935
+ })
2936
+ );
2937
+ return resp.schedule ?? null;
2938
+ },
2939
+ async updateCronSchedule(params) {
2940
+ const resp = await client.updateCronSchedule(
2941
+ create11(UpdateCronScheduleRequestSchema, {
2942
+ id: params.id,
2943
+ name: params.name,
2944
+ description: params.description,
2945
+ cron: params.cron,
2946
+ timezone: params.timezone,
2947
+ operationKey: params.operationKey,
2948
+ isActive: params.isActive
2949
+ })
2950
+ );
2951
+ return resp.schedule ?? null;
2952
+ },
2953
+ async deleteCronSchedule(id) {
2954
+ const resp = await client.deleteCronSchedule(
2955
+ create11(DeleteCronScheduleRequestSchema, { id })
2956
+ );
2957
+ return resp.success;
2958
+ }
2959
+ };
2960
+ }
2961
+
2696
2962
  // src/lib/client.ts
2697
2963
  import { GraphQLClient } from "graphql-request";
2698
2964
  async function createPlatformClient(options) {
@@ -2739,7 +3005,14 @@ async function createPlatformClient(options) {
2739
3005
  createRpcClient(ExperimentsService2, transport)
2740
3006
  ),
2741
3007
  settings: createSettingsMethods(createRpcClient(SettingsService2, transport)),
2742
- storage: createStorageMethods(createRpcClient(StorageService2, transport))
3008
+ storage: createStorageMethods(createRpcClient(StorageService2, transport)),
3009
+ operations: createOperationsMethods(
3010
+ createRpcClient(OperationsService2, transport)
3011
+ ),
3012
+ hooks: createHooksMethods(createRpcClient(HooksService2, transport)),
3013
+ cronSchedules: createCronSchedulesMethods(
3014
+ createRpcClient(SchedulesService2, transport)
3015
+ )
2743
3016
  };
2744
3017
  }
2745
3018
  function createPlatformClientWithHeaders(apiUrl, headers) {
@@ -2764,7 +3037,14 @@ function createPlatformClientWithHeaders(apiUrl, headers) {
2764
3037
  createRpcClient(ExperimentsService2, transport)
2765
3038
  ),
2766
3039
  settings: createSettingsMethods(createRpcClient(SettingsService2, transport)),
2767
- storage: createStorageMethods(createRpcClient(StorageService2, transport))
3040
+ storage: createStorageMethods(createRpcClient(StorageService2, transport)),
3041
+ operations: createOperationsMethods(
3042
+ createRpcClient(OperationsService2, transport)
3043
+ ),
3044
+ hooks: createHooksMethods(createRpcClient(HooksService2, transport)),
3045
+ cronSchedules: createCronSchedulesMethods(
3046
+ createRpcClient(SchedulesService2, transport)
3047
+ )
2768
3048
  };
2769
3049
  }
2770
3050
  async function getStorageAuth(options) {
@@ -4235,6 +4515,296 @@ Edit the files, then run:
4235
4515
  import chalk6 from "chalk";
4236
4516
  import { existsSync as existsSync4, readFileSync, writeFileSync as writeFileSync2 } from "fs";
4237
4517
  import { resolve as resolve4 } from "path";
4518
+
4519
+ // src/lib/reconciler.ts
4520
+ function zeroCounts() {
4521
+ return { created: 0, updated: 0, deleted: 0 };
4522
+ }
4523
+ async function reconcileConfig(client, configId, manifest) {
4524
+ const summary = {
4525
+ models: zeroCounts(),
4526
+ operations: zeroCounts(),
4527
+ hooks: zeroCounts(),
4528
+ segments: zeroCounts(),
4529
+ cronSchedules: zeroCounts(),
4530
+ authProviders: zeroCounts(),
4531
+ profileSchemaUpdated: false,
4532
+ apiKeys: []
4533
+ };
4534
+ await reconcileModels(client, configId, manifest.models ?? [], summary);
4535
+ await reconcileOperations(client, configId, manifest.operations ?? [], summary);
4536
+ await reconcileHooks(client, configId, manifest.hooks ?? [], summary);
4537
+ await reconcileSegments(client, manifest.segments ?? [], summary);
4538
+ await reconcileCronSchedules(client, configId, manifest.schedules ?? [], summary);
4539
+ await reconcileAuthProviders(client, manifest.authProviders ?? [], summary);
4540
+ await reconcileProfileSchema(client, manifest, summary);
4541
+ await reconcileApiKeys(client, manifest.apiKeys ?? [], summary);
4542
+ return summary;
4543
+ }
4544
+ async function reconcileModels(client, configId, models, summary) {
4545
+ const existing = await client.models.listModels({ limit: 200 });
4546
+ const configOwned = existing.items.filter(
4547
+ (m) => m.configId === configId
4548
+ );
4549
+ const existingByKey = new Map(
4550
+ configOwned.map((m) => [m.key, m])
4551
+ );
4552
+ const manifestKeys = /* @__PURE__ */ new Set();
4553
+ for (const m of models) {
4554
+ if (!m.key || !m.name) continue;
4555
+ manifestKeys.add(m.key);
4556
+ const ex = existingByKey.get(m.key);
4557
+ if (ex) {
4558
+ await client.models.updateModel({
4559
+ id: ex.id,
4560
+ name: m.name,
4561
+ fields: m.fields,
4562
+ config: m.config
4563
+ });
4564
+ summary.models.updated++;
4565
+ } else {
4566
+ await client.models.createModel({
4567
+ key: m.key,
4568
+ name: m.name,
4569
+ fields: m.fields,
4570
+ config: m.config,
4571
+ configId
4572
+ });
4573
+ summary.models.created++;
4574
+ }
4575
+ }
4576
+ for (const [key, ex] of existingByKey) {
4577
+ if (!manifestKeys.has(key)) {
4578
+ await client.models.deleteModel(
4579
+ ex.id
4580
+ );
4581
+ summary.models.deleted++;
4582
+ }
4583
+ }
4584
+ }
4585
+ async function reconcileOperations(client, configId, operations, summary) {
4586
+ const existing = await client.operations.listOperations({ configId, limit: 200 });
4587
+ const existingByKey = new Map(
4588
+ (existing.operations ?? []).map((o) => [o.key, o])
4589
+ );
4590
+ const manifestKeys = /* @__PURE__ */ new Set();
4591
+ for (const op of operations) {
4592
+ if (!op.key || !op.name) continue;
4593
+ manifestKeys.add(op.key);
4594
+ const ex = existingByKey.get(op.key);
4595
+ if (ex) {
4596
+ await client.operations.updateOperation({
4597
+ id: ex.id,
4598
+ name: op.name,
4599
+ description: op.description,
4600
+ endpoint: op.endpoint
4601
+ });
4602
+ summary.operations.updated++;
4603
+ } else {
4604
+ await client.operations.createOperation({
4605
+ key: op.key,
4606
+ name: op.name,
4607
+ endpoint: op.endpoint ?? "",
4608
+ description: op.description,
4609
+ category: op.category,
4610
+ configId
4611
+ });
4612
+ summary.operations.created++;
4613
+ }
4614
+ }
4615
+ for (const [key, ex] of existingByKey) {
4616
+ if (!manifestKeys.has(key)) {
4617
+ await client.operations.deleteOperation(
4618
+ ex.id
4619
+ );
4620
+ summary.operations.deleted++;
4621
+ }
4622
+ }
4623
+ }
4624
+ async function reconcileHooks(client, configId, hooks, summary) {
4625
+ const existing = await client.hooks.listHooks({ configId, limit: 200 });
4626
+ const existingByKey = new Map(
4627
+ (existing.hooks ?? []).map((h) => [h.key, h])
4628
+ );
4629
+ const manifestKeys = /* @__PURE__ */ new Set();
4630
+ for (const hook of hooks) {
4631
+ const key = hook.key || `${hook.event}-${hook.operationKey ?? "default"}`;
4632
+ if (!hook.event) continue;
4633
+ manifestKeys.add(key);
4634
+ const name = hook.name || key;
4635
+ const ex = existingByKey.get(key);
4636
+ if (ex) {
4637
+ await client.hooks.updateHook({
4638
+ id: ex.id,
4639
+ name,
4640
+ operationKey: hook.operationKey,
4641
+ filter: hook.filter
4642
+ });
4643
+ summary.hooks.updated++;
4644
+ } else {
4645
+ await client.hooks.createHook({
4646
+ key,
4647
+ name,
4648
+ event: hook.event,
4649
+ targetType: hook.type ?? "operation",
4650
+ operationKey: hook.operationKey,
4651
+ filter: hook.filter,
4652
+ configId
4653
+ });
4654
+ summary.hooks.created++;
4655
+ }
4656
+ }
4657
+ for (const [key, ex] of existingByKey) {
4658
+ if (!manifestKeys.has(key)) {
4659
+ await client.hooks.deleteHook(
4660
+ ex.id
4661
+ );
4662
+ summary.hooks.deleted++;
4663
+ }
4664
+ }
4665
+ }
4666
+ async function reconcileSegments(client, segments, summary) {
4667
+ const existing = await client.segments.listSegments({ limit: 200 });
4668
+ const existingByKey = new Map(
4669
+ (existing.segments ?? []).map((s) => [s.key, s])
4670
+ );
4671
+ for (const seg of segments) {
4672
+ if (!seg.key || !seg.name) continue;
4673
+ const ex = existingByKey.get(seg.key);
4674
+ if (ex) {
4675
+ await client.segments.updateSegment({
4676
+ id: ex.id,
4677
+ name: seg.name,
4678
+ description: seg.description,
4679
+ rules: seg.rules,
4680
+ evaluationMode: seg.evaluationMode,
4681
+ isActive: seg.isActive
4682
+ });
4683
+ summary.segments.updated++;
4684
+ } else {
4685
+ await client.segments.createSegment({
4686
+ key: seg.key,
4687
+ name: seg.name,
4688
+ description: seg.description,
4689
+ rules: seg.rules,
4690
+ evaluationMode: seg.evaluationMode,
4691
+ isActive: seg.isActive
4692
+ });
4693
+ summary.segments.created++;
4694
+ }
4695
+ }
4696
+ }
4697
+ async function reconcileCronSchedules(client, configId, schedules, summary) {
4698
+ const existing = await client.cronSchedules.listCronSchedules({ configId, limit: 200 });
4699
+ const schedulesList = existing.schedules ?? [];
4700
+ const existingByKey = new Map(
4701
+ schedulesList.map((s) => [s.key, s])
4702
+ );
4703
+ const manifestKeys = /* @__PURE__ */ new Set();
4704
+ for (const sched of schedules) {
4705
+ const key = sched.operationKey;
4706
+ if (!key || !sched.cron) continue;
4707
+ manifestKeys.add(key);
4708
+ const ex = existingByKey.get(key);
4709
+ if (ex) {
4710
+ await client.cronSchedules.updateCronSchedule({
4711
+ id: ex.id,
4712
+ name: key,
4713
+ cron: sched.cron,
4714
+ timezone: sched.timezone,
4715
+ operationKey: sched.operationKey
4716
+ });
4717
+ summary.cronSchedules.updated++;
4718
+ } else {
4719
+ await client.cronSchedules.createCronSchedule({
4720
+ key,
4721
+ name: key,
4722
+ cron: sched.cron,
4723
+ timezone: sched.timezone,
4724
+ operationKey: sched.operationKey,
4725
+ configId
4726
+ });
4727
+ summary.cronSchedules.created++;
4728
+ }
4729
+ }
4730
+ for (const [key, ex] of existingByKey) {
4731
+ if (!manifestKeys.has(key)) {
4732
+ await client.cronSchedules.deleteCronSchedule(
4733
+ ex.id
4734
+ );
4735
+ summary.cronSchedules.deleted++;
4736
+ }
4737
+ }
4738
+ }
4739
+ async function reconcileAuthProviders(client, providers, summary) {
4740
+ const existing = await client.identity.listAuthProviders({ limit: 200 });
4741
+ const existingByKey = new Map(
4742
+ existing.items.map((p) => [p.key, p])
4743
+ );
4744
+ for (const prov of providers) {
4745
+ if (!prov.key || !prov.name || !prov.type) continue;
4746
+ const ex = existingByKey.get(prov.key);
4747
+ if (ex) {
4748
+ await client.identity.updateAuthProvider({
4749
+ id: ex.id,
4750
+ name: prov.name,
4751
+ config: prov.config,
4752
+ enabled: prov.enabled,
4753
+ isDefault: prov.isDefault,
4754
+ priority: prov.priority
4755
+ });
4756
+ summary.authProviders.updated++;
4757
+ } else {
4758
+ await client.identity.createAuthProvider({
4759
+ key: prov.key,
4760
+ name: prov.name,
4761
+ type: prov.type.toUpperCase(),
4762
+ config: prov.config,
4763
+ enabled: prov.enabled ?? true,
4764
+ isDefault: prov.isDefault ?? false,
4765
+ priority: prov.priority ?? 0
4766
+ });
4767
+ summary.authProviders.created++;
4768
+ }
4769
+ }
4770
+ }
4771
+ async function reconcileProfileSchema(client, manifest, summary) {
4772
+ const profileSchema = manifest["customerProfileSchema"];
4773
+ if (!profileSchema) return;
4774
+ await client.settings.updateCustomerProfileSchema({
4775
+ fields: profileSchema.fields,
4776
+ publicFields: profileSchema.publicFields ?? []
4777
+ });
4778
+ summary.profileSchemaUpdated = true;
4779
+ }
4780
+ async function reconcileApiKeys(client, apiKeys, summary) {
4781
+ if (apiKeys.length === 0) return;
4782
+ const existing = await client.identity.listApiKeys({ limit: 200 });
4783
+ const existingByName = new Map(
4784
+ existing.items.map((k) => [k.name, k])
4785
+ );
4786
+ for (const key of apiKeys) {
4787
+ if (!key.name || !key.keyType || !key.envVar) continue;
4788
+ if (existingByName.has(key.name)) continue;
4789
+ const result = await client.identity.createApiKey({
4790
+ name: key.name,
4791
+ keyType: key.keyType === "secret" ? 2 : 1,
4792
+ allowedModels: key.allowedModels,
4793
+ allowedFileTypes: key.allowedFileTypes
4794
+ });
4795
+ const rawKey = result?.apiKey?.rawKey;
4796
+ if (rawKey) {
4797
+ summary.apiKeys.push({
4798
+ name: key.name,
4799
+ keyType: key.keyType,
4800
+ envVar: key.envVar,
4801
+ rawKey
4802
+ });
4803
+ }
4804
+ }
4805
+ }
4806
+
4807
+ // src/commands/push.ts
4238
4808
  var CONFIG_FILE_NAMES = [
4239
4809
  "foir.config.ts",
4240
4810
  "foir.config.js",
@@ -4265,6 +4835,30 @@ function writeEnvVar(envPath, key, value) {
4265
4835
  writeFileSync2(envPath, content, "utf-8");
4266
4836
  return true;
4267
4837
  }
4838
+ function printSummary(summary) {
4839
+ const lines = [];
4840
+ const fmt = (label, c) => {
4841
+ const parts = [];
4842
+ if (c.created) parts.push(`${c.created} created`);
4843
+ if (c.updated) parts.push(`${c.updated} updated`);
4844
+ if (c.deleted) parts.push(`${c.deleted} deleted`);
4845
+ if (parts.length > 0) lines.push(` ${label.padEnd(13)} ${parts.join(", ")}`);
4846
+ };
4847
+ fmt("Models:", summary.models);
4848
+ fmt("Operations:", summary.operations);
4849
+ fmt("Hooks:", summary.hooks);
4850
+ fmt("Segments:", summary.segments);
4851
+ fmt("Schedules:", summary.cronSchedules);
4852
+ fmt("Auth:", summary.authProviders);
4853
+ if (summary.profileSchemaUpdated) {
4854
+ lines.push(" Profile: schema updated");
4855
+ }
4856
+ if (lines.length > 0) {
4857
+ for (const line of lines) {
4858
+ console.log(line);
4859
+ }
4860
+ }
4861
+ }
4268
4862
  function registerPushCommand(program2, globalOpts) {
4269
4863
  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(
4270
4864
  withErrorHandler(
@@ -4286,85 +4880,38 @@ function registerPushCommand(program2, globalOpts) {
4286
4880
  'Config must have at least "key" and "name" fields.'
4287
4881
  );
4288
4882
  }
4289
- if (opts.force) {
4290
- config2.force = true;
4291
- }
4292
4883
  const client = await createPlatformClient(globalOpts());
4293
4884
  console.log(
4294
4885
  chalk6.dim(`Pushing config "${config2.key}" to platform...`)
4295
4886
  );
4296
- const result = await client.configs.applyConfig(
4887
+ const applyResult = await client.configs.applyConfig(
4297
4888
  config2.key,
4298
4889
  config2
4299
4890
  );
4300
- if (!result) {
4891
+ if (!applyResult) {
4301
4892
  throw new Error(
4302
4893
  "Failed to apply config \u2014 no response from server."
4303
4894
  );
4304
4895
  }
4896
+ const configId = applyResult.id;
4897
+ console.log(chalk6.dim("Reconciling resources..."));
4898
+ const summary = await reconcileConfig(client, configId, config2);
4305
4899
  console.log();
4306
4900
  console.log(chalk6.green("\u2713 Config applied successfully"));
4307
- console.log(` Config ID: ${chalk6.cyan(result.id)}`);
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
- }
4901
+ console.log(` Config ID: ${chalk6.cyan(configId)}`);
4902
+ console.log(` Config Key: ${chalk6.cyan(config2.key)}`);
4903
+ console.log();
4904
+ printSummary(summary);
4354
4905
  const envPath = resolve4(opts.env ?? ".env");
4355
- const r = result;
4356
- const provisionedKeys = r.provisionedApiKeys;
4357
- const webhookSecret = r.webhookSecret;
4358
4906
  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
- }
4907
+ for (const pk of summary.apiKeys) {
4908
+ envWrites.push({
4909
+ key: pk.envVar,
4910
+ value: pk.rawKey,
4911
+ label: `${pk.name} (${pk.keyType})`
4912
+ });
4367
4913
  }
4914
+ const webhookSecret = applyResult.webhookSecret;
4368
4915
  if (webhookSecret) {
4369
4916
  envWrites.push({
4370
4917
  key: "FOIR_WEBHOOK_SECRET",
@@ -6471,6 +7018,98 @@ function buildDispatchTable() {
6471
7018
  },
6472
7019
  deleteCustomerAuthProvider: async (v, c) => await c.identity.deleteAuthProvider(str(v.id))
6473
7020
  },
7021
+ // ── Rollouts ────────────────────────────────────────────────
7022
+ rollouts: {
7023
+ listPublishBatches: async (v, c) => wrapList(
7024
+ await c.records.listPublishBatches({ limit: num(v.limit, 50) })
7025
+ ),
7026
+ getPublishBatch: async (v, c) => await c.records.getPublishBatch(str(v.id)),
7027
+ createPublishBatch: async (v, c) => {
7028
+ const input = v.input;
7029
+ if (!input) throw new Error("Input required (--data or --file)");
7030
+ return await c.records.createPublishBatch({
7031
+ name: str(input.name),
7032
+ versionIds: input.versionIds ?? [],
7033
+ scheduledAt: input.scheduledAt ? new Date(String(input.scheduledAt)) : void 0
7034
+ });
7035
+ },
7036
+ updatePublishBatch: async (v, c) => {
7037
+ const input = v.input;
7038
+ if (!input) throw new Error("Input required (--data or --file)");
7039
+ return await c.records.updatePublishBatch({
7040
+ batchId: str(input.id ?? v.id),
7041
+ name: str(input.name),
7042
+ scheduledAt: input.scheduledAt ? new Date(String(input.scheduledAt)) : void 0
7043
+ });
7044
+ },
7045
+ cancelPublishBatch: async (v, c) => await c.records.cancelPublishBatch(str(v.id)),
7046
+ rollbackPublishBatch: async (v, c) => await c.records.rollbackPublishBatch(str(v.id)),
7047
+ retryFailedBatchItems: async (v, c) => await c.records.retryFailedBatchItems(str(v.id)),
7048
+ addItemsToPublishBatch: async (v, c) => {
7049
+ const input = v.input;
7050
+ if (!input) throw new Error("Input required (--data or --file)");
7051
+ return await c.records.addItemsToPublishBatch(
7052
+ str(v.id),
7053
+ input.versionIds ?? []
7054
+ );
7055
+ },
7056
+ removeItemsFromPublishBatch: async (v, c) => {
7057
+ const input = v.input;
7058
+ if (!input) throw new Error("Input required (--data or --file)");
7059
+ return await c.records.removeItemsFromPublishBatch(
7060
+ str(v.id),
7061
+ input.versionIds ?? []
7062
+ );
7063
+ }
7064
+ },
7065
+ // ── Hooks ──────────────────────────────────────────────────
7066
+ hooks: {
7067
+ hooks: async (v, c) => {
7068
+ const resp = await c.hooks.listHooks({ limit: num(v.limit, 50) });
7069
+ return { items: resp.hooks ?? [], total: resp.total ?? 0 };
7070
+ },
7071
+ hookByKey: async (v, c) => await c.hooks.getHookByKey(str(v.key)),
7072
+ createHook: async (v, c) => {
7073
+ const input = v.input;
7074
+ if (!input) throw new Error("Input required (--data or --file)");
7075
+ return await c.hooks.createHook({
7076
+ key: str(input.key),
7077
+ name: str(input.name),
7078
+ event: str(input.event),
7079
+ targetType: str(input.targetType) ?? "operation",
7080
+ description: str(input.description),
7081
+ operationKey: str(input.operationKey),
7082
+ filter: input.filter
7083
+ });
7084
+ },
7085
+ updateHook: async (v, c) => {
7086
+ const input = v.input;
7087
+ if (!input) throw new Error("Input required (--data or --file)");
7088
+ return await c.hooks.updateHook({
7089
+ id: str(input.id ?? v.id),
7090
+ name: str(input.name),
7091
+ operationKey: str(input.operationKey),
7092
+ filter: input.filter,
7093
+ isActive: input.isActive
7094
+ });
7095
+ },
7096
+ deleteHook: async (v, c) => await c.hooks.deleteHook(str(v.id)),
7097
+ hookDeliveries: async (v, c) => {
7098
+ const resp = await c.hooks.listHookDeliveries({
7099
+ hookId: str(v.hookId),
7100
+ limit: num(v.limit, 50)
7101
+ });
7102
+ return { items: resp.deliveries ?? [], total: resp.total ?? 0 };
7103
+ },
7104
+ retryHookDelivery: async (v, c) => await c.hooks.retryHookDelivery(str(v.deliveryId)),
7105
+ testHook: async (v, c) => {
7106
+ const data = v.data;
7107
+ return await c.hooks.testHook({
7108
+ hookId: str(v.hookId),
7109
+ testPayload: data
7110
+ });
7111
+ }
7112
+ },
6474
7113
  // ── Configs ─────────────────────────────────────────────────
6475
7114
  configs: {
6476
7115
  configs: async (v, c) => {
@@ -23,6 +23,7 @@ interface FieldDefinitionInput {
23
23
  required?: boolean;
24
24
  helpText?: string;
25
25
  placeholder?: string;
26
+ defaultValue?: unknown;
26
27
  config?: Record<string, unknown>;
27
28
  itemType?: string;
28
29
  storage?: string;
@@ -105,8 +106,12 @@ interface ApplyConfigApiKeyInput {
105
106
  keyType: 'public' | 'secret';
106
107
  /** Environment variable name to write the key to in .env (e.g. "FOIR_PUBLIC_KEY"). */
107
108
  envVar: string;
108
- /** Optional scopes to restrict the key. */
109
- scopes?: Record<string, unknown>;
109
+ /** Scopes to restrict the key (e.g. ["configs:read", "records:write"]). Use ["*"] for full access. */
110
+ scopes?: string[];
111
+ /** Restrict the key to specific model keys (e.g. ["tilly_note", "tilly_block"]). */
112
+ allowedModels?: string[];
113
+ /** Restrict file uploads to specific MIME types (e.g. ["image/*", "video/*"]). */
114
+ allowedFileTypes?: string[];
110
115
  }
111
116
  interface ApplyConfigInput {
112
117
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eide/foir-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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.3.1",
52
+ "@eide/foir-proto-ts": "^0.3.3",
53
53
  "chalk": "^5.3.0",
54
54
  "commander": "^12.1.0",
55
55
  "dotenv": "^16.4.5",