@eide/foir-cli 0.9.0 → 0.10.1

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
@@ -468,7 +468,7 @@ import { OperationsService as OperationsService2 } from "@eide/foir-proto-ts/ope
468
468
  import { HooksService as HooksService2 } from "@eide/foir-proto-ts/hooks/v1/hooks_pb";
469
469
  import { NotificationsService as NotificationsService2 } from "@eide/foir-proto-ts/notifications/v1/notifications_pb";
470
470
  import { SchedulesService as SchedulesService2 } from "@eide/foir-proto-ts/schedules/v1/schedules_pb";
471
- import { IntegrationsService as IntegrationsService2 } from "@eide/foir-proto-ts/integrations/v1/integrations_pb";
471
+ import { AppsService as AppsService2 } from "@eide/foir-proto-ts/apps/v1/apps_service_pb";
472
472
 
473
473
  // src/lib/rpc/identity.ts
474
474
  import { create } from "@bufbuild/protobuf";
@@ -1145,125 +1145,111 @@ function createIdentityMethods(client) {
1145
1145
  };
1146
1146
  }
1147
1147
 
1148
- // src/lib/rpc/integrations.ts
1148
+ // src/lib/rpc/apps.ts
1149
1149
  import { create as create2 } from "@bufbuild/protobuf";
1150
1150
  import {
1151
- ListIntegrationsRequestSchema,
1152
- ListExtensionsRequestSchema,
1153
- CredentialStrategy,
1154
- ExtensionTarget
1155
- } from "@eide/foir-proto-ts/integrations/v1/integrations_pb";
1156
- function createIntegrationsMethods(client) {
1151
+ ListAppsRequestSchema,
1152
+ GetAppRequestSchema,
1153
+ InstallAppRequestSchema,
1154
+ ConfirmInstallAppRequestSchema,
1155
+ UpdateAppRequestSchema,
1156
+ ConfirmUpdateAppRequestSchema,
1157
+ UninstallAppRequestSchema,
1158
+ SetAppMappingRequestSchema,
1159
+ ValidateManifestRequestSchema,
1160
+ TriggerOperationRequestSchema
1161
+ } from "@eide/foir-proto-ts/apps/v1/apps_service_pb";
1162
+ function createAppsMethods(client) {
1157
1163
  return {
1158
- async listIntegrations(projectId) {
1159
- const resp = await client.listIntegrations(
1160
- create2(ListIntegrationsRequestSchema, { projectId })
1164
+ async listApps(tenantId, projectId) {
1165
+ const resp = await client.listApps(
1166
+ create2(ListAppsRequestSchema, { tenantId, projectId })
1161
1167
  );
1162
- return resp.integrations.map((s) => s.config).filter((c) => c !== void 0);
1168
+ return resp.apps;
1163
1169
  },
1164
- async listExtensions(projectId) {
1165
- const resp = await client.listExtensions(
1166
- create2(ListExtensionsRequestSchema, { projectId })
1170
+ async getApp(tenantId, projectId, name) {
1171
+ const resp = await client.getApp(
1172
+ create2(GetAppRequestSchema, { tenantId, projectId, name })
1167
1173
  );
1168
- return resp.extensions;
1169
- }
1170
- };
1171
- }
1172
- var STRATEGY_FROM_PROTO = {
1173
- [CredentialStrategy.UNSPECIFIED]: "none",
1174
- [CredentialStrategy.OAUTH]: "oauth",
1175
- [CredentialStrategy.API_KEY]: "api_key",
1176
- [CredentialStrategy.SHARED_SECRET]: "shared_secret",
1177
- [CredentialStrategy.SSH_KEY]: "ssh_key",
1178
- [CredentialStrategy.NONE]: "none",
1179
- [CredentialStrategy.MANAGED]: "managed"
1180
- };
1181
- var TARGET_FROM_PROTO = {
1182
- [ExtensionTarget.UNSPECIFIED]: "record",
1183
- [ExtensionTarget.RECORD]: "record",
1184
- [ExtensionTarget.MODEL_LIST]: "model-list"
1185
- };
1186
- var STRATEGY_STRING_MAP = {
1187
- CREDENTIAL_STRATEGY_UNSPECIFIED: "none",
1188
- CREDENTIAL_STRATEGY_OAUTH: "oauth",
1189
- CREDENTIAL_STRATEGY_API_KEY: "api_key",
1190
- CREDENTIAL_STRATEGY_SHARED_SECRET: "shared_secret",
1191
- CREDENTIAL_STRATEGY_SSH_KEY: "ssh_key",
1192
- CREDENTIAL_STRATEGY_NONE: "none",
1193
- CREDENTIAL_STRATEGY_MANAGED: "managed"
1194
- };
1195
- function resolveStrategy(raw) {
1196
- if (typeof raw === "number") {
1197
- return STRATEGY_FROM_PROTO[raw] ?? "none";
1198
- }
1199
- if (typeof raw === "string") {
1200
- return STRATEGY_STRING_MAP[raw] ?? raw.toLowerCase().replace("credential_strategy_", "");
1201
- }
1202
- return "none";
1203
- }
1204
- function integrationConfigToInput(cfg) {
1205
- const sync = {};
1206
- for (const [k, v] of Object.entries(cfg.sync)) {
1207
- sync[k] = syncMappingFromProto(v);
1208
- }
1209
- const out = {
1210
- middleware: { url: cfg.middleware?.url ?? "" },
1211
- credentials: {
1212
- strategy: resolveStrategy(cfg.credentials?.strategy)
1174
+ return resp.app ?? null;
1213
1175
  },
1214
- sync
1215
- };
1216
- out.enabled = cfg.enabled !== false;
1217
- if (cfg.settings && Object.keys(cfg.settings).length > 0) {
1218
- out.settings = cfg.settings;
1219
- }
1220
- if (cfg.metadata && Object.keys(cfg.metadata).length > 0) {
1221
- out.metadata = cfg.metadata;
1222
- }
1223
- return out;
1224
- }
1225
- function syncMappingFromProto(m) {
1226
- const out = {
1227
- model: m.model,
1228
- naturalKey: m.naturalKey,
1229
- fields: { ...m.fields }
1230
- };
1231
- if (m.modelSeed) {
1232
- const seed = { fields: {} };
1233
- for (const [fk, fv] of Object.entries(m.modelSeed.fields)) {
1234
- seed.fields[fk] = {
1235
- type: fv.type,
1236
- ...fv.required ? { required: true } : {},
1237
- ...fv.naturalKey ? { naturalKey: true } : {},
1238
- ...fv.label ? { label: fv.label } : {},
1239
- ...fv.helpText ? { helpText: fv.helpText } : {},
1240
- ...fv.config && Object.keys(fv.config).length > 0 ? { config: fv.config } : {}
1241
- };
1176
+ async installApp(tenantId, projectId, manifestUrl) {
1177
+ return client.installApp(
1178
+ create2(InstallAppRequestSchema, { tenantId, projectId, manifestUrl })
1179
+ );
1180
+ },
1181
+ async confirmInstallApp(params) {
1182
+ const resp = await client.confirmInstallApp(
1183
+ create2(ConfirmInstallAppRequestSchema, {
1184
+ installTicket: params.installTicket,
1185
+ sourceMappings: params.sourceMappings ?? {},
1186
+ sinkMappings: params.sinkMappings ?? {},
1187
+ settings: params.settings,
1188
+ placementFieldChoices: params.placementFieldChoices ?? {}
1189
+ })
1190
+ );
1191
+ return resp.app;
1192
+ },
1193
+ async updateApp(tenantId, projectId, name) {
1194
+ return client.updateApp(
1195
+ create2(UpdateAppRequestSchema, { tenantId, projectId, name })
1196
+ );
1197
+ },
1198
+ async confirmUpdateApp(tenantId, projectId, name, newManifestHash) {
1199
+ const resp = await client.confirmUpdateApp(
1200
+ create2(ConfirmUpdateAppRequestSchema, {
1201
+ tenantId,
1202
+ projectId,
1203
+ name,
1204
+ newManifestHash
1205
+ })
1206
+ );
1207
+ return resp.app;
1208
+ },
1209
+ async uninstallApp(tenantId, projectId, name, force = false) {
1210
+ return client.uninstallApp(
1211
+ create2(UninstallAppRequestSchema, { tenantId, projectId, name, force })
1212
+ );
1213
+ },
1214
+ async setAppMapping(params) {
1215
+ const resp = await client.setAppMapping(
1216
+ create2(SetAppMappingRequestSchema, {
1217
+ tenantId: params.tenantId,
1218
+ projectId: params.projectId,
1219
+ name: params.name,
1220
+ sourceMappings: params.sourceMappings ?? {},
1221
+ sinkMappings: params.sinkMappings ?? {},
1222
+ placementFieldChoices: params.placementFieldChoices ?? {}
1223
+ })
1224
+ );
1225
+ return resp.app;
1226
+ },
1227
+ async validateManifestUrl(manifestUrl) {
1228
+ return client.validateManifest(
1229
+ create2(ValidateManifestRequestSchema, {
1230
+ source: { case: "manifestUrl", value: manifestUrl }
1231
+ })
1232
+ );
1233
+ },
1234
+ async validateManifestJson(manifestJson) {
1235
+ return client.validateManifest(
1236
+ create2(ValidateManifestRequestSchema, {
1237
+ source: { case: "manifestJson", value: manifestJson }
1238
+ })
1239
+ );
1240
+ },
1241
+ async triggerOperation(tenantId, projectId, operationKey, input) {
1242
+ const resp = await client.triggerOperation(
1243
+ create2(TriggerOperationRequestSchema, {
1244
+ tenantId,
1245
+ projectId,
1246
+ operationKey,
1247
+ input
1248
+ })
1249
+ );
1250
+ return resp.executionId;
1242
1251
  }
1243
- out.modelSeed = seed;
1244
- }
1245
- return out;
1246
- }
1247
- function extensionConfigToInput(cfg) {
1248
- const out = {
1249
- url: cfg.url,
1250
- placements: cfg.placements.map((p) => {
1251
- const placement = {
1252
- target: TARGET_FROM_PROTO[p.target] ?? "record",
1253
- model: p.model,
1254
- tab: p.tab,
1255
- title: p.title
1256
- };
1257
- if (p.hints && Object.keys(p.hints).length > 0) {
1258
- placement.hints = p.hints;
1259
- }
1260
- return placement;
1261
- })
1262
1252
  };
1263
- if (cfg.metadata && Object.keys(cfg.metadata).length > 0) {
1264
- out.metadata = cfg.metadata;
1265
- }
1266
- return out;
1267
1253
  }
1268
1254
 
1269
1255
  // src/lib/rpc/models.ts
@@ -3219,9 +3205,7 @@ function createCronSchedulesMethods(client) {
3219
3205
  cron: params.cron,
3220
3206
  timezone: params.timezone,
3221
3207
  operationKey: params.operationKey,
3222
- configId: params.configId,
3223
- targetType: params.targetType,
3224
- targetConfig: params.targetConfig
3208
+ configId: params.configId
3225
3209
  })
3226
3210
  );
3227
3211
  return resp.schedule ?? null;
@@ -3326,9 +3310,7 @@ async function createPlatformClient(options) {
3326
3310
  cronSchedules: createCronSchedulesMethods(
3327
3311
  createRpcClient(SchedulesService2, transport)
3328
3312
  ),
3329
- integrations: createIntegrationsMethods(
3330
- createRpcClient(IntegrationsService2, transport)
3331
- )
3313
+ apps: createAppsMethods(createRpcClient(AppsService2, transport))
3332
3314
  };
3333
3315
  }
3334
3316
  function createPlatformClientWithHeaders(apiUrl, headers) {
@@ -3364,9 +3346,7 @@ function createPlatformClientWithHeaders(apiUrl, headers) {
3364
3346
  cronSchedules: createCronSchedulesMethods(
3365
3347
  createRpcClient(SchedulesService2, transport)
3366
3348
  ),
3367
- integrations: createIntegrationsMethods(
3368
- createRpcClient(IntegrationsService2, transport)
3369
- )
3349
+ apps: createAppsMethods(createRpcClient(AppsService2, transport))
3370
3350
  };
3371
3351
  }
3372
3352
  async function getStorageAuth(options) {
@@ -3648,6 +3628,9 @@ function success(message, data) {
3648
3628
  }
3649
3629
  console.log(chalk2.green(`\u2713 ${interpolated}`));
3650
3630
  }
3631
+ function warn(message) {
3632
+ console.log(chalk2.yellow(`\u26A0 ${message}`));
3633
+ }
3651
3634
 
3652
3635
  // src/commands/whoami.ts
3653
3636
  function registerWhoamiCommand(program2, globalOpts) {
@@ -4890,7 +4873,8 @@ async function reconcileConfig(client, configId, manifest, options = {}) {
4890
4873
  authProviders: zeroCounts(),
4891
4874
  placementsUpdated: false,
4892
4875
  profileSchemaUpdated: false,
4893
- apiKeys: []
4876
+ apiKeys: [],
4877
+ apps: zeroCounts()
4894
4878
  };
4895
4879
  const operationBaseUrl = manifest.operationBaseUrl ?? "";
4896
4880
  await reconcileModels(client, configId, manifest.models ?? [], summary);
@@ -4908,6 +4892,13 @@ async function reconcileConfig(client, configId, manifest, options = {}) {
4908
4892
  summary,
4909
4893
  options.rotateKeys ?? false
4910
4894
  );
4895
+ await reconcileApps(
4896
+ client,
4897
+ options.tenantId,
4898
+ options.projectId,
4899
+ manifest.apps ?? {},
4900
+ summary
4901
+ );
4911
4902
  return summary;
4912
4903
  }
4913
4904
  async function reconcileModels(client, configId, models, summary) {
@@ -5309,274 +5300,110 @@ async function reconcileApiKeys(client, configKey, apiKeys, summary, rotateKeys)
5309
5300
  }
5310
5301
  }
5311
5302
  }
5312
-
5313
- // src/lib/validate-integrations.ts
5314
- import { fromJson as fromJson3 } from "@bufbuild/protobuf";
5315
- import { pathToString } from "@bufbuild/protobuf/reflect";
5316
- import { createValidator } from "@bufbuild/protovalidate";
5317
- import {
5318
- IntegrationConfigSchema,
5319
- ExtensionConfigSchema
5320
- } from "@eide/foir-proto-ts/integrations/v1/integrations_pb";
5321
- var IntegrationValidationError = class extends Error {
5322
- errors;
5323
- constructor(errors) {
5324
- super(
5325
- `foir.config.ts failed validation:
5326
- ${errors.map((e) => ` - ${e}`).join("\n")}`
5303
+ async function reconcileApps(client, tenantId, projectId, apps, summary) {
5304
+ const entries = Object.entries(apps);
5305
+ if (entries.length === 0) return;
5306
+ if (!tenantId || !projectId) {
5307
+ console.warn(
5308
+ "Warning: apps declared in foir.config.ts but tenant/project context not supplied; skipping apps reconcile."
5327
5309
  );
5328
- this.name = "IntegrationValidationError";
5329
- this.errors = errors;
5330
- }
5331
- };
5332
- var validator = createValidator();
5333
- function validateIntegrationsAndExtensions(manifest) {
5334
- const errors = [];
5335
- const warnings = [];
5336
- const modelsByKey = /* @__PURE__ */ new Map();
5337
- for (const model of manifest.models ?? []) {
5338
- modelsByKey.set(model.key, model);
5310
+ return;
5339
5311
  }
5340
- if (manifest.integrations) {
5341
- for (const [name, integration] of Object.entries(manifest.integrations)) {
5342
- validateIntegration(name, integration, modelsByKey, errors, warnings);
5312
+ const existingList = await client.apps.listApps(tenantId, projectId);
5313
+ const existingByName = new Map(existingList.map((a) => [a.name, a]));
5314
+ for (const [name, input] of entries) {
5315
+ const existing = existingByName.get(name);
5316
+ if (!existing) {
5317
+ await installApp(client, tenantId, projectId, name, input);
5318
+ summary.apps.created += 1;
5319
+ continue;
5343
5320
  }
5344
- }
5345
- if (manifest.extensions) {
5346
- for (const [name, extension] of Object.entries(manifest.extensions)) {
5347
- validateExtension(name, extension, modelsByKey, errors);
5321
+ if (existing.manifestUrl !== input.source) {
5322
+ console.warn(
5323
+ `Warning: app "${name}" installed from ${existing.manifestUrl} but config declares ${input.source}; skipping. Uninstall explicitly (\`foir apps uninstall ${name}\`) and re-push to change sources.`
5324
+ );
5325
+ continue;
5348
5326
  }
5349
- }
5350
- return { errors, warnings };
5351
- }
5352
- function assertValid(result) {
5353
- if (result.errors.length > 0) {
5354
- throw new IntegrationValidationError(result.errors);
5355
- }
5356
- }
5357
- function validateIntegration(name, integration, modelsByKey, errors, warnings) {
5358
- const prefix = `integration '${name}'`;
5359
- const protoJson = toIntegrationProtoJson(name, integration);
5360
- const structuralErrors = runProtoValidation(
5361
- prefix,
5362
- protoJson,
5363
- (json) => fromJson3(IntegrationConfigSchema, json, { ignoreUnknownFields: true })
5364
- );
5365
- errors.push(...structuralErrors);
5366
- const sync = integration.sync ?? {};
5367
- for (const [sourceType, mapping] of Object.entries(sync)) {
5368
- validateSyncMapping(
5369
- `${prefix} sync.${sourceType}`,
5370
- mapping,
5327
+ await client.apps.setAppMapping({
5328
+ tenantId,
5329
+ projectId,
5371
5330
  name,
5372
- modelsByKey,
5373
- errors,
5374
- warnings
5375
- );
5331
+ sourceMappings: toSourceMappings(input.mappings?.sources ?? {}),
5332
+ sinkMappings: toSinkMappings(input.mappings?.sinks ?? {}),
5333
+ placementFieldChoices: toPlacementFieldChoices(
5334
+ input.mappings?.placementFields ?? {}
5335
+ )
5336
+ });
5337
+ summary.apps.updated += 1;
5376
5338
  }
5377
5339
  }
5378
- function validateSyncMapping(prefix, mapping, integrationName, modelsByKey, errors, warnings) {
5379
- const model = mapping.model ? modelsByKey.get(mapping.model) : void 0;
5380
- const seed = mapping.modelSeed;
5381
- if (!mapping.model) {
5382
- return;
5383
- }
5384
- if (!model && !seed) {
5385
- errors.push(
5386
- `${prefix}: missing model '${mapping.model}' referenced by integration '${integrationName}' \u2014 declare it under models or provide a modelSeed`
5387
- );
5388
- return;
5389
- }
5390
- const fieldKeys = model ? new Set((model.fields ?? []).map((f) => f.key)) : new Set(Object.keys(seed?.fields ?? {}));
5391
- if (mapping.naturalKey && !fieldKeys.has(mapping.naturalKey)) {
5392
- errors.push(
5393
- `${prefix}: naturalKey '${mapping.naturalKey}' does not exist on ${model ? `model '${mapping.model}'` : `modelSeed for '${mapping.model}'`}`
5340
+ async function installApp(client, tenantId, projectId, name, input) {
5341
+ const installResp = await client.apps.installApp(
5342
+ tenantId,
5343
+ projectId,
5344
+ input.source
5345
+ );
5346
+ const previewName = installResp.preview?.manifest?.name ?? name;
5347
+ if (previewName !== name) {
5348
+ throw new Error(
5349
+ `apps.${name}: manifest declares name "${previewName}"; config key must match the manifest name.`
5394
5350
  );
5395
5351
  }
5396
- const fields = mapping.fields ?? {};
5397
- for (const [sourceField, foirField] of Object.entries(fields)) {
5398
- if (!foirField) continue;
5399
- if (!fieldKeys.has(foirField)) {
5400
- errors.push(
5401
- `${prefix}: field mapping '${sourceField} -> ${foirField}' references field '${foirField}' which does not exist on ${model ? `model '${mapping.model}'` : `modelSeed for '${mapping.model}'`}`
5402
- );
5403
- }
5404
- }
5405
- if (seed && model) {
5406
- detectSeedDrift(prefix, seed, model, warnings);
5352
+ const missing = [];
5353
+ for (const needed of installResp.preview?.sourceTypesToMap ?? []) {
5354
+ if (!(input.mappings?.sources ?? {})[needed]) missing.push(`source ${needed}`);
5407
5355
  }
5408
- }
5409
- function detectSeedDrift(prefix, seed, model, warnings) {
5410
- const modelFieldTypes = /* @__PURE__ */ new Map();
5411
- for (const field of model.fields ?? []) {
5412
- modelFieldTypes.set(field.key, field.type);
5356
+ for (const needed of installResp.preview?.sinkContractsToMap ?? []) {
5357
+ if (!(input.mappings?.sinks ?? {})[needed]) missing.push(`sink ${needed}`);
5413
5358
  }
5414
- for (const [key, field] of Object.entries(seed.fields ?? {})) {
5415
- const modelType = modelFieldTypes.get(key);
5416
- if (!modelType) {
5417
- warnings.push(
5418
- `${prefix}: modelSeed.fields.${key} is not present on existing model \u2014 modelSeed is ignored on subsequent pushes`
5419
- );
5420
- } else if (field.type && modelType !== field.type) {
5421
- warnings.push(
5422
- `${prefix}: modelSeed.fields.${key}.type '${field.type}' disagrees with existing model field type '${modelType}' \u2014 modelSeed is ignored on subsequent pushes`
5423
- );
5359
+ for (const needed of installResp.preview?.placementsRequiringFieldChoice ?? []) {
5360
+ if (!(input.mappings?.placementFields ?? {})[needed]) {
5361
+ missing.push(`placement-field ${needed}`);
5424
5362
  }
5425
5363
  }
5426
- }
5427
- function validateExtension(name, extension, modelsByKey, errors) {
5428
- const prefix = `extension '${name}'`;
5429
- const protoJson = toExtensionProtoJson(name, extension);
5430
- const structuralErrors = runProtoValidation(
5431
- prefix,
5432
- protoJson,
5433
- (json) => fromJson3(ExtensionConfigSchema, json, { ignoreUnknownFields: true })
5434
- );
5435
- errors.push(...structuralErrors);
5436
- const placements = extension.placements ?? [];
5437
- placements.forEach((placement, index) => {
5438
- if (placement.model && !modelsByKey.has(placement.model)) {
5439
- errors.push(
5440
- `${prefix} placements[${index}]: missing model '${placement.model}' referenced by extension '${name}'`
5441
- );
5442
- }
5443
- });
5444
- }
5445
- function runProtoValidation(prefix, json, decode) {
5446
- let message;
5447
- try {
5448
- message = decode(json);
5449
- } catch (err) {
5450
- const msg = err instanceof Error ? err.message : String(err);
5451
- return [`${prefix}: cannot decode as proto \u2014 ${msg}`];
5452
- }
5453
- const result = runValidate(message);
5454
- if (result.kind === "valid") return [];
5455
- if (result.kind === "error") {
5456
- return [`${prefix}: validator error \u2014 ${result.error.message}`];
5457
- }
5458
- return result.violations.map((v) => `${prefix}: ${formatViolation(v)}`);
5459
- }
5460
- function runValidate(message) {
5461
- if (!message || typeof message !== "object") {
5462
- return { kind: "error", error: new Error("message is not an object") };
5463
- }
5464
- const anyMsg = message;
5465
- let schema;
5466
- if (anyMsg.$typeName === "integrations.v1.IntegrationConfig") {
5467
- schema = IntegrationConfigSchema;
5468
- } else if (anyMsg.$typeName === "integrations.v1.ExtensionConfig") {
5469
- schema = ExtensionConfigSchema;
5470
- } else {
5471
- return { kind: "error", error: new Error(`unknown message type ${anyMsg.$typeName ?? "?"}`) };
5472
- }
5473
- const result = validator.validate(
5474
- // Both schema/message casts are safe because we picked schema by $typeName.
5475
- schema,
5476
- message
5477
- );
5478
- if (result.kind === "valid") return { kind: "valid" };
5479
- if (result.kind === "error") return { kind: "error", error: result.error };
5480
- return { kind: "invalid", violations: result.violations };
5481
- }
5482
- function formatViolation(v) {
5483
- let path3 = "";
5484
- try {
5485
- path3 = pathToString(v.field);
5486
- } catch {
5487
- path3 = "";
5364
+ if (missing.length > 0) {
5365
+ throw new Error(
5366
+ `apps.${name}: missing mappings in foir.config.ts: ${missing.join(", ")}`
5367
+ );
5488
5368
  }
5489
- const message = v.message ?? "";
5490
- return path3 ? `${path3}: ${message}` : message;
5369
+ await client.apps.confirmInstallApp({
5370
+ installTicket: installResp.installTicket,
5371
+ sourceMappings: toSourceMappings(input.mappings?.sources ?? {}),
5372
+ sinkMappings: toSinkMappings(input.mappings?.sinks ?? {}),
5373
+ settings: input.settings,
5374
+ placementFieldChoices: toPlacementFieldChoices(
5375
+ input.mappings?.placementFields ?? {}
5376
+ )
5377
+ });
5491
5378
  }
5492
- function compact(obj) {
5379
+ function toSourceMappings(input) {
5493
5380
  const out = {};
5494
- for (const [k, v] of Object.entries(obj)) {
5495
- if (v !== void 0) out[k] = v;
5381
+ for (const [k, v] of Object.entries(input)) {
5382
+ out[k] = {
5383
+ toModel: v.toModel,
5384
+ naturalKey: v.naturalKey,
5385
+ fields: v.fields
5386
+ };
5496
5387
  }
5497
5388
  return out;
5498
5389
  }
5499
- function toIntegrationProtoJson(name, integration) {
5500
- return compact({
5501
- name,
5502
- enabled: integration.enabled ?? true,
5503
- middleware: integration.middleware ? { url: integration.middleware.url ?? "" } : void 0,
5504
- credentials: {
5505
- strategy: integration.credentials?.strategy ? credentialStrategyToProto(integration.credentials.strategy) : "CREDENTIAL_STRATEGY_UNSPECIFIED"
5506
- },
5507
- sync: integration.sync ? Object.fromEntries(
5508
- Object.entries(integration.sync).map(([k, v]) => [k, syncMappingToProto(v)])
5509
- ) : {},
5510
- settings: integration.settings,
5511
- metadata: integration.metadata
5512
- });
5513
- }
5514
- function syncMappingToProto(mapping) {
5515
- return compact({
5516
- model: mapping.model ?? "",
5517
- naturalKey: mapping.naturalKey ?? "",
5518
- fields: mapping.fields ?? {},
5519
- modelSeed: mapping.modelSeed ? {
5520
- fields: Object.fromEntries(
5521
- Object.entries(mapping.modelSeed.fields ?? {}).map(([k, v]) => [
5522
- k,
5523
- compact({
5524
- type: v.type ?? "",
5525
- required: v.required ?? false,
5526
- naturalKey: v.naturalKey ?? false,
5527
- label: v.label,
5528
- helpText: v.helpText,
5529
- config: v.config
5530
- })
5531
- ])
5532
- )
5533
- } : void 0
5534
- });
5535
- }
5536
- function credentialStrategyToProto(strategy) {
5537
- switch (strategy) {
5538
- case "oauth":
5539
- return "CREDENTIAL_STRATEGY_OAUTH";
5540
- case "api_key":
5541
- return "CREDENTIAL_STRATEGY_API_KEY";
5542
- case "shared_secret":
5543
- return "CREDENTIAL_STRATEGY_SHARED_SECRET";
5544
- case "ssh_key":
5545
- return "CREDENTIAL_STRATEGY_SSH_KEY";
5546
- case "none":
5547
- return "CREDENTIAL_STRATEGY_NONE";
5548
- case "managed":
5549
- return "CREDENTIAL_STRATEGY_MANAGED";
5550
- default:
5551
- return "CREDENTIAL_STRATEGY_UNSPECIFIED";
5390
+ function toSinkMappings(input) {
5391
+ const out = {};
5392
+ for (const [k, v] of Object.entries(input)) {
5393
+ out[k] = {
5394
+ toModel: v.toModel,
5395
+ naturalKey: v.naturalKey,
5396
+ fields: v.fields
5397
+ };
5552
5398
  }
5399
+ return out;
5553
5400
  }
5554
- function toExtensionProtoJson(name, extension) {
5555
- const placements = (extension.placements ?? []).map(
5556
- (p) => compact({
5557
- target: extensionTargetToProto(p.target),
5558
- model: p.model ?? "",
5559
- tab: p.tab ?? "",
5560
- title: p.title ?? "",
5561
- hints: p.hints
5562
- })
5563
- );
5564
- return compact({
5565
- name,
5566
- url: extension.url ?? "",
5567
- placements,
5568
- metadata: extension.metadata
5569
- });
5570
- }
5571
- function extensionTargetToProto(target) {
5572
- switch (target) {
5573
- case "record":
5574
- return "EXTENSION_TARGET_RECORD";
5575
- case "model-list":
5576
- return "EXTENSION_TARGET_MODEL_LIST";
5577
- default:
5578
- return "EXTENSION_TARGET_UNSPECIFIED";
5401
+ function toPlacementFieldChoices(input) {
5402
+ const out = {};
5403
+ for (const [k, v] of Object.entries(input)) {
5404
+ out[k] = { model: v.model, field: v.field };
5579
5405
  }
5406
+ return out;
5580
5407
  }
5581
5408
 
5582
5409
  // src/commands/push.ts
@@ -5660,12 +5487,9 @@ function registerPushCommand(program2, globalOpts) {
5660
5487
  'Config must have at least "key" and "name" fields.'
5661
5488
  );
5662
5489
  }
5663
- const validation = validateIntegrationsAndExtensions(config2);
5664
- for (const warning of validation.warnings) {
5665
- console.log(chalk6.yellow(`\u26A0 ${warning}`));
5666
- }
5667
- assertValid(validation);
5668
- const client = await createPlatformClient(globalOpts());
5490
+ const gOpts = globalOpts();
5491
+ const client = await createPlatformClient(gOpts);
5492
+ const resolved = await resolveProjectContext(gOpts);
5669
5493
  console.log(
5670
5494
  chalk6.dim(`Pushing config "${config2.key}" to platform...`)
5671
5495
  );
@@ -5681,7 +5505,9 @@ function registerPushCommand(program2, globalOpts) {
5681
5505
  const configId = applyResult.id;
5682
5506
  console.log(chalk6.dim("Reconciling resources..."));
5683
5507
  const summary = await reconcileConfig(client, configId, config2, {
5684
- rotateKeys: opts.rotateKeys ?? false
5508
+ rotateKeys: opts.rotateKeys ?? false,
5509
+ tenantId: resolved?.project.tenantId,
5510
+ projectId: resolved?.project.id
5685
5511
  });
5686
5512
  console.log();
5687
5513
  console.log(chalk6.green("\u2713 Config applied successfully"));
@@ -5787,28 +5613,20 @@ function registerPullCommand(program2, globalOpts) {
5787
5613
  );
5788
5614
  }
5789
5615
  const resolved = await resolveProjectContext(globalOpts());
5790
- let integrations;
5791
- let extensions;
5616
+ let apps;
5792
5617
  if (resolved) {
5793
5618
  const projectId = resolved.project.id;
5794
- const protoIntegrations = await client.integrations.listIntegrations(projectId);
5795
- if (protoIntegrations.length > 0) {
5796
- integrations = {};
5797
- for (const cfg of protoIntegrations) {
5798
- integrations[cfg.name] = integrationConfigToInput(cfg);
5799
- }
5800
- }
5801
- const protoExtensions = await client.integrations.listExtensions(projectId);
5802
- if (protoExtensions.length > 0) {
5803
- extensions = {};
5804
- for (const cfg of protoExtensions) {
5805
- extensions[cfg.name] = extensionConfigToInput(cfg);
5619
+ const tenantId = resolved.project.tenantId;
5620
+ const installedApps = await client.apps.listApps(tenantId, projectId);
5621
+ if (installedApps.length > 0) {
5622
+ apps = {};
5623
+ for (const a of installedApps) {
5624
+ apps[a.name] = appToInput(a);
5806
5625
  }
5807
5626
  }
5808
5627
  }
5809
5628
  const configDataNoInteg = { ...configData };
5810
- delete configDataNoInteg.integrations;
5811
- delete configDataNoInteg.extensions;
5629
+ delete configDataNoInteg.apps;
5812
5630
  const manifest = {
5813
5631
  key: config2.key,
5814
5632
  name: config2.name,
@@ -5817,8 +5635,7 @@ function registerPullCommand(program2, globalOpts) {
5817
5635
  ...config2.description ? { description: config2.description } : {},
5818
5636
  ...config2.connectionDomain ? { operationBaseUrl: config2.connectionDomain } : {},
5819
5637
  ...configDataNoInteg,
5820
- ...integrations ? { integrations } : {},
5821
- ...extensions ? { extensions } : {}
5638
+ ...apps ? { apps } : {}
5822
5639
  };
5823
5640
  delete manifest.force;
5824
5641
  const jsonContent = JSON.stringify(manifest, null, 2);
@@ -5865,11 +5682,8 @@ export default defineConfig(${jsonContent});
5865
5682
  if (schedules.length > 0) parts.push(`${schedules.length} schedule(s)`);
5866
5683
  if (authProviders.length > 0) parts.push(`${authProviders.length} auth provider(s)`);
5867
5684
  if (configData.customerProfileSchema) parts.push("customer profile schema");
5868
- if (integrations && Object.keys(integrations).length > 0) {
5869
- parts.push(`${Object.keys(integrations).length} integration(s)`);
5870
- }
5871
- if (extensions && Object.keys(extensions).length > 0) {
5872
- parts.push(`${Object.keys(extensions).length} extension(s)`);
5685
+ if (apps) {
5686
+ parts.push(`${Object.keys(apps).length} app(s)`);
5873
5687
  }
5874
5688
  if (parts.length > 0) {
5875
5689
  console.log(chalk7.dim(` Contains: ${parts.join(", ")}`));
@@ -5878,6 +5692,25 @@ export default defineConfig(${jsonContent});
5878
5692
  )
5879
5693
  );
5880
5694
  }
5695
+ function appToInput(a) {
5696
+ const mappingsBlob = a.mappings ?? {};
5697
+ const out = { source: a.manifestUrl };
5698
+ const settingsObj = a.settings;
5699
+ if (settingsObj && Object.keys(settingsObj).length > 0) {
5700
+ out.settings = settingsObj;
5701
+ }
5702
+ const sources = mappingsBlob.sources;
5703
+ const sinks = mappingsBlob.sinks;
5704
+ const placementFields = mappingsBlob.placement_fields;
5705
+ if (sources && Object.keys(sources).length > 0 || sinks && Object.keys(sinks).length > 0 || placementFields && Object.keys(placementFields).length > 0) {
5706
+ out.mappings = {
5707
+ ...sources ? { sources } : {},
5708
+ ...sinks ? { sinks } : {},
5709
+ ...placementFields ? { placementFields } : {}
5710
+ };
5711
+ }
5712
+ return out;
5713
+ }
5881
5714
 
5882
5715
  // src/commands/remove.ts
5883
5716
  import chalk8 from "chalk";
@@ -8185,7 +8018,7 @@ function registerNotificationsCommands(program2, globalOpts) {
8185
8018
 
8186
8019
  // src/commands/configs.ts
8187
8020
  function registerConfigsCommands(program2, globalOpts) {
8188
- const configs = program2.command("configs").description("Manage configs (integrations, extensions, webhooks)");
8021
+ const configs = program2.command("configs").description("Manage configs (apps, webhooks)");
8189
8022
  configs.command("list").description("List configs").option("--type <type>", "Filter by config type").option("--enabled", "Only enabled configs").option("--limit <n>", "Max results", "50").action(
8190
8023
  withErrorHandler(globalOpts, async (cmdOpts) => {
8191
8024
  const opts = globalOpts();
@@ -8253,6 +8086,260 @@ function registerConfigsCommands(program2, globalOpts) {
8253
8086
  );
8254
8087
  }
8255
8088
 
8089
+ // src/commands/apps.ts
8090
+ function registerAppsCommands(program2, globalOpts) {
8091
+ const apps = program2.command("apps").description("Install and manage apps");
8092
+ apps.command("list").description("List installed apps").action(
8093
+ withErrorHandler(globalOpts, async () => {
8094
+ const opts = globalOpts();
8095
+ const resolved = await requireProject(opts);
8096
+ const client = await createPlatformClient(opts);
8097
+ const apps2 = await client.apps.listApps(
8098
+ resolved.project.tenantId,
8099
+ resolved.project.id
8100
+ );
8101
+ formatList(apps2, opts, {
8102
+ columns: [
8103
+ { key: "name", header: "Name", width: 24 },
8104
+ {
8105
+ key: "manifest",
8106
+ header: "Version",
8107
+ width: 10,
8108
+ format: (v) => v?.version ?? ""
8109
+ },
8110
+ { key: "manifestUrl", header: "Manifest URL", width: 40 },
8111
+ {
8112
+ key: "installedAt",
8113
+ header: "Installed",
8114
+ width: 12,
8115
+ format: (v) => {
8116
+ const ts = v;
8117
+ if (!ts?.seconds) return "";
8118
+ return new Date(Number(ts.seconds) * 1e3).toLocaleDateString();
8119
+ }
8120
+ }
8121
+ ]
8122
+ });
8123
+ })
8124
+ );
8125
+ apps.command("get <name>").description("Get an installed app by name").action(
8126
+ withErrorHandler(globalOpts, async (name) => {
8127
+ const opts = globalOpts();
8128
+ const resolved = await requireProject(opts);
8129
+ const client = await createPlatformClient(opts);
8130
+ const app = await client.apps.getApp(
8131
+ resolved.project.tenantId,
8132
+ resolved.project.id,
8133
+ name
8134
+ );
8135
+ if (!app) throw new Error(`App "${name}" not installed.`);
8136
+ formatOutput(app, opts);
8137
+ })
8138
+ );
8139
+ apps.command("install <manifestUrl>").description("Install an app from a manifest URL").option(
8140
+ "--source-map <json>",
8141
+ `Inline source-type mappings as JSON, e.g. '{"product":{"toModel":"product","naturalKey":"handle","fields":{"title":"title"}}}'`
8142
+ ).option(
8143
+ "--sink-map <json>",
8144
+ "Inline sink-contract mappings as JSON"
8145
+ ).option(
8146
+ "--field-map <json>",
8147
+ "Inline placement field choices as JSON, keyed by placement key"
8148
+ ).action(
8149
+ withErrorHandler(
8150
+ globalOpts,
8151
+ async (manifestUrl, cmdOpts) => {
8152
+ const opts = globalOpts();
8153
+ const resolved = await requireProject(opts);
8154
+ const client = await createPlatformClient(opts);
8155
+ const installResp = await client.apps.installApp(
8156
+ resolved.project.tenantId,
8157
+ resolved.project.id,
8158
+ manifestUrl
8159
+ );
8160
+ if (!opts.json && !opts.quiet) {
8161
+ const preview = installResp.preview;
8162
+ console.error(
8163
+ `preview: ${preview?.operationsToCreate.length ?? 0} ops, ${preview?.hooksToCreate.length ?? 0} hooks, ${preview?.placementsToCreate.length ?? 0} placements`
8164
+ );
8165
+ const sourcesNeeded = preview?.sourceTypesToMap ?? [];
8166
+ const sinksNeeded = preview?.sinkContractsToMap ?? [];
8167
+ const fieldsNeeded = preview?.placementsRequiringFieldChoice ?? [];
8168
+ if (sourcesNeeded.length) {
8169
+ console.error(
8170
+ `source mappings required for: ${sourcesNeeded.join(", ")}`
8171
+ );
8172
+ }
8173
+ if (sinksNeeded.length) {
8174
+ console.error(
8175
+ `sink mappings required for: ${sinksNeeded.join(", ")}`
8176
+ );
8177
+ }
8178
+ if (fieldsNeeded.length) {
8179
+ console.error(
8180
+ `placement field choices required for: ${fieldsNeeded.join(", ")}`
8181
+ );
8182
+ }
8183
+ }
8184
+ const sourceMappings = parseJsonOpt(cmdOpts.sourceMap, "source-map");
8185
+ const sinkMappings = parseJsonOpt(cmdOpts.sinkMap, "sink-map");
8186
+ const placementFieldChoices = parseJsonOpt(
8187
+ cmdOpts.fieldMap,
8188
+ "field-map"
8189
+ );
8190
+ const app = await client.apps.confirmInstallApp({
8191
+ installTicket: installResp.installTicket,
8192
+ sourceMappings,
8193
+ sinkMappings,
8194
+ placementFieldChoices
8195
+ });
8196
+ if (opts.json) {
8197
+ formatOutput(app, opts);
8198
+ } else {
8199
+ success(`Installed ${app?.name ?? ""}`);
8200
+ }
8201
+ }
8202
+ )
8203
+ );
8204
+ apps.command("update <name>").description("Check for updates and apply if no rejected changes").option("--dry-run", "Show the diff without applying").action(
8205
+ withErrorHandler(
8206
+ globalOpts,
8207
+ async (name, cmdOpts) => {
8208
+ const opts = globalOpts();
8209
+ const resolved = await requireProject(opts);
8210
+ const client = await createPlatformClient(opts);
8211
+ const updateResp = await client.apps.updateApp(
8212
+ resolved.project.tenantId,
8213
+ resolved.project.id,
8214
+ name
8215
+ );
8216
+ if (updateResp.noChanges) {
8217
+ if (!opts.quiet) success("no changes");
8218
+ return;
8219
+ }
8220
+ if (!opts.json && !opts.quiet) {
8221
+ for (const change of updateResp.changes) {
8222
+ const classLabel = classToLabel(change.class);
8223
+ console.error(`[${classLabel}] ${change.path}: ${change.description}`);
8224
+ }
8225
+ }
8226
+ const hasRejected = updateResp.changes.some((c) => c.class === 3);
8227
+ if (hasRejected) {
8228
+ throw new Error(
8229
+ "update rejected: one or more changes require admin resolution; see diff above"
8230
+ );
8231
+ }
8232
+ if (cmdOpts.dryRun) return;
8233
+ const app = await client.apps.confirmUpdateApp(
8234
+ resolved.project.tenantId,
8235
+ resolved.project.id,
8236
+ name,
8237
+ updateResp.newManifestHash
8238
+ );
8239
+ if (opts.json) {
8240
+ formatOutput(app, opts);
8241
+ } else {
8242
+ success(`Updated ${name}`);
8243
+ }
8244
+ }
8245
+ )
8246
+ );
8247
+ apps.command("uninstall <name>").description("Uninstall an app (runs __uninstall if declared)").option("--force", "Skip middleware __uninstall call; remove platform state regardless").action(
8248
+ withErrorHandler(
8249
+ globalOpts,
8250
+ async (name, cmdOpts) => {
8251
+ const opts = globalOpts();
8252
+ const resolved = await requireProject(opts);
8253
+ const client = await createPlatformClient(opts);
8254
+ const resp = await client.apps.uninstallApp(
8255
+ resolved.project.tenantId,
8256
+ resolved.project.id,
8257
+ name,
8258
+ !!cmdOpts.force
8259
+ );
8260
+ if (!opts.quiet) {
8261
+ if (resp.forced) warn(`force-uninstalled ${name} (external resources may be orphaned)`);
8262
+ else success(`uninstalled ${name}`);
8263
+ }
8264
+ }
8265
+ )
8266
+ );
8267
+ apps.command("trigger <appName> <operationKey>").description(
8268
+ "Trigger an operation on an installed app. operationKey is the bare manifest key (the namespaced form is built for you)."
8269
+ ).option("-d, --data <json>", "Input data as JSON").action(
8270
+ withErrorHandler(
8271
+ globalOpts,
8272
+ async (appName, operationKey, cmdOpts) => {
8273
+ const opts = globalOpts();
8274
+ const resolved = await requireProject(opts);
8275
+ const client = await createPlatformClient(opts);
8276
+ const stored = operationKey.includes("/") ? operationKey : `${appName}/${operationKey}`;
8277
+ const input = cmdOpts.data ? JSON.parse(String(cmdOpts.data)) : void 0;
8278
+ const executionId = await client.apps.triggerOperation(
8279
+ resolved.project.tenantId,
8280
+ resolved.project.id,
8281
+ stored,
8282
+ input
8283
+ );
8284
+ if (opts.json) {
8285
+ formatOutput({ executionId }, opts);
8286
+ } else {
8287
+ success(`triggered ${stored}`, { executionId });
8288
+ }
8289
+ }
8290
+ )
8291
+ );
8292
+ apps.command("validate <manifestUrl>").description("Dry-run a manifest URL against the validator; prints any issues").action(
8293
+ withErrorHandler(globalOpts, async (manifestUrl) => {
8294
+ const opts = globalOpts();
8295
+ const client = await createPlatformClient(opts);
8296
+ const resp = await client.apps.validateManifestUrl(manifestUrl);
8297
+ if (opts.json) {
8298
+ formatOutput(resp, opts);
8299
+ return;
8300
+ }
8301
+ if (resp.ok) {
8302
+ success("manifest validates cleanly");
8303
+ return;
8304
+ }
8305
+ for (const iss of resp.issues) {
8306
+ const sev = iss.severity === 1 ? "error" : "warning";
8307
+ console.error(`[${sev}] ${iss.path}: ${iss.message}`);
8308
+ }
8309
+ throw new Error(`manifest has ${resp.issues.length} issue(s)`);
8310
+ })
8311
+ );
8312
+ }
8313
+ async function requireProject(opts) {
8314
+ const resolved = await resolveProjectContext(opts);
8315
+ if (!resolved) {
8316
+ throw new Error(
8317
+ "No project selected. Run `foir select-project` or set FOIR_PROJECT."
8318
+ );
8319
+ }
8320
+ return resolved;
8321
+ }
8322
+ function parseJsonOpt(value, flag) {
8323
+ if (!value) return void 0;
8324
+ try {
8325
+ return JSON.parse(String(value));
8326
+ } catch (err) {
8327
+ throw new Error(`--${flag} is not valid JSON: ${err.message}`);
8328
+ }
8329
+ }
8330
+ function classToLabel(n) {
8331
+ switch (n) {
8332
+ case 1:
8333
+ return "safe";
8334
+ case 2:
8335
+ return "confirm";
8336
+ case 3:
8337
+ return "REJECTED";
8338
+ default:
8339
+ return "?";
8340
+ }
8341
+ }
8342
+
8256
8343
  // src/cli.ts
8257
8344
  var __filename = fileURLToPath(import.meta.url);
8258
8345
  var __dirname = dirname4(__filename);
@@ -8301,4 +8388,5 @@ registerFilesCommands(program, getGlobalOpts);
8301
8388
  registerNotesCommands(program, getGlobalOpts);
8302
8389
  registerNotificationsCommands(program, getGlobalOpts);
8303
8390
  registerConfigsCommands(program, getGlobalOpts);
8391
+ registerAppsCommands(program, getGlobalOpts);
8304
8392
  program.parse();
@@ -211,57 +211,46 @@ interface ApplyConfigApiKeyInput {
211
211
  /** Restrict file uploads to specific MIME types (e.g. ["image/*", "video/*"]). */
212
212
  allowedFileTypes?: string[];
213
213
  }
214
- type CredentialStrategy = 'oauth' | 'api_key' | 'shared_secret' | 'ssh_key' | 'none' | 'managed';
215
- interface IntegrationMiddlewareInput {
216
- /** Full https URL of the middleware handling this integration. */
217
- url: string;
218
- }
219
- interface IntegrationCredentialsInput {
220
- strategy: CredentialStrategy;
221
- }
222
- interface ModelSeedFieldInput {
223
- type: string;
224
- required?: boolean;
225
- naturalKey?: boolean;
226
- label?: string;
227
- helpText?: string;
228
- config?: Record<string, unknown>;
229
- }
230
- interface ModelSeedInput {
231
- fields: Record<string, ModelSeedFieldInput>;
232
- }
233
- interface IntegrationSyncMappingInput {
234
- /** foir model key this source type writes into. */
235
- model: string;
236
- /** foir field holding the upstream natural key. UPSERTs are keyed on this. */
214
+ /**
215
+ * Source-type mapping entry for an app install. Maps a manifest-declared
216
+ * source type onto a project model. See docs/platform/apps.md §Mapping step.
217
+ */
218
+ interface AppSourceMappingInput {
219
+ toModel: string;
237
220
  naturalKey: string;
238
- /** Explicit source field -> foir field mapping. */
239
221
  fields: Record<string, string>;
240
- /** Optional one-time bootstrap schema if the target model is missing. */
241
- modelSeed?: ModelSeedInput;
242
222
  }
243
- interface IntegrationInput {
244
- enabled?: boolean;
245
- middleware: IntegrationMiddlewareInput;
246
- credentials: IntegrationCredentialsInput;
247
- /** Source type -> sync mapping. Optional when field mapping is managed via the admin UI. */
248
- sync?: Record<string, IntegrationSyncMappingInput>;
249
- /** Opaque middleware-specific settings — not validated by the CLI. */
250
- settings?: Record<string, unknown>;
251
- metadata?: Record<string, unknown>;
223
+ /**
224
+ * Sink-contract mapping entry for an app install.
225
+ */
226
+ interface AppSinkMappingInput {
227
+ toModel: string;
228
+ naturalKey: string;
229
+ fields: Record<string, string>;
252
230
  }
253
- type ExtensionTarget = 'record' | 'model-list';
254
- interface ExtensionPlacementInput {
255
- target: ExtensionTarget;
231
+ /**
232
+ * Placement field choice for TARGET_FIELD placements with
233
+ * field_selected_at_install=true. Keyed by placement.key.
234
+ */
235
+ interface AppPlacementFieldChoiceInput {
256
236
  model: string;
257
- tab: string;
258
- title: string;
259
- hints?: Record<string, unknown>;
237
+ field: string;
260
238
  }
261
- interface ExtensionInput {
262
- url: string;
263
- placements: ExtensionPlacementInput[];
264
- metadata?: Record<string, unknown>;
239
+ /**
240
+ * Per-project app installation declared in foir.config.ts under apps.<name>.
241
+ * Platform state is reconciled against this at `foir push` time; the
242
+ * manifest is fetched from `source` each push.
243
+ */
244
+ interface AppInput {
245
+ /** https URL to the manifest JSON. */
246
+ source: string;
247
+ /** Opaque settings forwarded to the app's middleware. */
248
+ settings?: Record<string, unknown>;
249
+ mappings?: {
250
+ sources?: Record<string, AppSourceMappingInput>;
251
+ sinks?: Record<string, AppSinkMappingInput>;
252
+ placementFields?: Record<string, AppPlacementFieldChoiceInput>;
253
+ };
265
254
  }
266
255
  interface ApplyConfigInput {
267
256
  key: string;
@@ -278,16 +267,12 @@ interface ApplyConfigInput {
278
267
  authProviders?: ApplyConfigAuthProviderInput[];
279
268
  placements?: ApplyConfigPlacementInput[];
280
269
  apiKeys?: ApplyConfigApiKeyInput[];
281
- /** Per-project integration declarations, keyed by integration name. */
282
- integrations?: Record<string, IntegrationInput>;
283
- /** Per-project extension declarations, keyed by extension name. */
284
- extensions?: Record<string, ExtensionInput>;
270
+ /** Per-project app declarations, keyed by app name. */
271
+ apps?: Record<string, AppInput>;
285
272
  [key: string]: unknown;
286
273
  }
287
274
  /** Define a complete config manifest. */
288
275
  declare function defineConfig(config: ApplyConfigInput): ApplyConfigInput;
289
- /** @deprecated Use `defineConfig` instead. */
290
- declare const defineExtension: typeof defineConfig;
291
276
  /** Define a model with type-safe field definitions. */
292
277
  declare function defineModel(model: ApplyConfigModelInput): ApplyConfigModelInput;
293
278
  /** Define a field with type safety. */
@@ -306,9 +291,5 @@ declare function defineAuthProvider(provider: ApplyConfigAuthProviderInput): App
306
291
  declare function defineHook(hook: ApplyConfigHookInput): ApplyConfigHookInput;
307
292
  /** Define an editor placement (sidebar or main-editor tab). */
308
293
  declare function definePlacement(placement: ApplyConfigPlacementInput): ApplyConfigPlacementInput;
309
- /** Define an integration declaration. */
310
- declare function defineIntegration(integration: IntegrationInput): IntegrationInput;
311
- /** Define an extension declaration. */
312
- declare function defineExtensionDeclaration(extension: ExtensionInput): ExtensionInput;
313
294
 
314
- export { type ApplyConfigApiKeyInput, type ApplyConfigAuthProviderInput, type ApplyConfigHookInput, type ApplyConfigInput, type ApplyConfigModelInput, type ApplyConfigOperationInput, type ApplyConfigPlacementInput, type ApplyConfigScheduleInput, type ApplyConfigSegmentInput, type CredentialStrategy, type ExpressionPrecondition, type ExtensionInput, type ExtensionPlacementInput, type ExtensionTarget, type FieldDefinitionInput, type IntegrationCredentialsInput, type IntegrationInput, type IntegrationMiddlewareInput, type IntegrationSyncMappingInput, type ModelSeedFieldInput, type ModelSeedInput, type Precondition, type QuotaRule, type SegmentPrecondition, type SelectFieldConfig, type SelectFieldDefinitionInput, defineAuthProvider, defineConfig, defineExtension, defineExtensionDeclaration, defineField, defineHook, defineIntegration, defineModel, defineOperation, definePlacement, defineSchedule, defineSegment, defineSelectField };
295
+ export { type AppInput, type AppPlacementFieldChoiceInput, type AppSinkMappingInput, type AppSourceMappingInput, type ApplyConfigApiKeyInput, type ApplyConfigAuthProviderInput, type ApplyConfigHookInput, type ApplyConfigInput, type ApplyConfigModelInput, type ApplyConfigOperationInput, type ApplyConfigPlacementInput, type ApplyConfigScheduleInput, type ApplyConfigSegmentInput, type ExpressionPrecondition, type FieldDefinitionInput, type Precondition, type QuotaRule, type SegmentPrecondition, type SelectFieldConfig, type SelectFieldDefinitionInput, defineAuthProvider, defineConfig, defineField, defineHook, defineModel, defineOperation, definePlacement, defineSchedule, defineSegment, defineSelectField };
@@ -2,7 +2,6 @@
2
2
  function defineConfig(config) {
3
3
  return config;
4
4
  }
5
- var defineExtension = defineConfig;
6
5
  function defineModel(model) {
7
6
  return model;
8
7
  }
@@ -30,20 +29,11 @@ function defineHook(hook) {
30
29
  function definePlacement(placement) {
31
30
  return placement;
32
31
  }
33
- function defineIntegration(integration) {
34
- return integration;
35
- }
36
- function defineExtensionDeclaration(extension) {
37
- return extension;
38
- }
39
32
  export {
40
33
  defineAuthProvider,
41
34
  defineConfig,
42
- defineExtension,
43
- defineExtensionDeclaration,
44
35
  defineField,
45
36
  defineHook,
46
- defineIntegration,
47
37
  defineModel,
48
38
  defineOperation,
49
39
  definePlacement,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eide/foir-cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Universal platform CLI for Foir platform",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -50,7 +50,7 @@
50
50
  "@bufbuild/protovalidate": "^1.1.1",
51
51
  "@connectrpc/connect": "^2.0.0",
52
52
  "@connectrpc/connect-node": "^2.0.0",
53
- "@eide/foir-proto-ts": "^0.17.0",
53
+ "@eide/foir-proto-ts": "^0.23.0",
54
54
  "chalk": "^5.3.0",
55
55
  "commander": "^12.1.0",
56
56
  "dotenv": "^16.4.5",