@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 +486 -398
- package/dist/lib/config-helpers.d.ts +37 -56
- package/dist/lib/config-helpers.js +0 -10
- package/package.json +2 -2
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 {
|
|
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/
|
|
1148
|
+
// src/lib/rpc/apps.ts
|
|
1149
1149
|
import { create as create2 } from "@bufbuild/protobuf";
|
|
1150
1150
|
import {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
|
1159
|
-
const resp = await client.
|
|
1160
|
-
create2(
|
|
1164
|
+
async listApps(tenantId, projectId) {
|
|
1165
|
+
const resp = await client.listApps(
|
|
1166
|
+
create2(ListAppsRequestSchema, { tenantId, projectId })
|
|
1161
1167
|
);
|
|
1162
|
-
return resp.
|
|
1168
|
+
return resp.apps;
|
|
1163
1169
|
},
|
|
1164
|
-
async
|
|
1165
|
-
const resp = await client.
|
|
1166
|
-
create2(
|
|
1170
|
+
async getApp(tenantId, projectId, name) {
|
|
1171
|
+
const resp = await client.getApp(
|
|
1172
|
+
create2(GetAppRequestSchema, { tenantId, projectId, name })
|
|
1167
1173
|
);
|
|
1168
|
-
return resp.
|
|
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
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
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
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
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
|
|
5379
|
-
const
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
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
|
|
5397
|
-
for (const
|
|
5398
|
-
if (!
|
|
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
|
-
|
|
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
|
|
5415
|
-
|
|
5416
|
-
|
|
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
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
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
|
-
|
|
5490
|
-
|
|
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
|
|
5379
|
+
function toSourceMappings(input) {
|
|
5493
5380
|
const out = {};
|
|
5494
|
-
for (const [k, v] of Object.entries(
|
|
5495
|
-
|
|
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
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
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
|
|
5555
|
-
const
|
|
5556
|
-
|
|
5557
|
-
|
|
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
|
|
5664
|
-
|
|
5665
|
-
|
|
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
|
|
5791
|
-
let extensions;
|
|
5616
|
+
let apps;
|
|
5792
5617
|
if (resolved) {
|
|
5793
5618
|
const projectId = resolved.project.id;
|
|
5794
|
-
const
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
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.
|
|
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
|
-
...
|
|
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 (
|
|
5869
|
-
parts.push(`${Object.keys(
|
|
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 (
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
258
|
-
title: string;
|
|
259
|
-
hints?: Record<string, unknown>;
|
|
237
|
+
field: string;
|
|
260
238
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
282
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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",
|