@odla-ai/cli 0.12.0 → 0.13.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/README.md +43 -7
- package/REQUIREMENTS.md +11 -3
- package/dist/bin.cjs +318 -60
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-Y4HWUEFF.js → chunk-I7XDJZB6.js} +314 -56
- package/dist/chunk-I7XDJZB6.js.map +1 -0
- package/dist/index.cjs +318 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -13
- package/dist/index.d.ts +66 -13
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/skills/odla/SKILL.md +5 -3
- package/skills/odla/references/build.md +9 -2
- package/skills/odla/references/sdks.md +25 -0
- package/skills/odla-migrate/references/phase-2-db.md +2 -2
- package/dist/chunk-Y4HWUEFF.js.map +0 -1
|
@@ -786,6 +786,77 @@ function isRecord3(value) {
|
|
|
786
786
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
787
787
|
import { dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
788
788
|
import { pathToFileURL } from "url";
|
|
789
|
+
|
|
790
|
+
// src/integration-validation.ts
|
|
791
|
+
function validateIntegrations(cfg, path, defaultServices) {
|
|
792
|
+
if (cfg.integrations === void 0) return;
|
|
793
|
+
if (!Array.isArray(cfg.integrations)) throw new Error(`${path}: integrations must be an array`);
|
|
794
|
+
const ids = /* @__PURE__ */ new Set();
|
|
795
|
+
for (const [index, integration] of cfg.integrations.entries()) {
|
|
796
|
+
const at = `${path}: integrations[${index}]`;
|
|
797
|
+
if (!isRecord4(integration)) throw new Error(`${at} must be an object`);
|
|
798
|
+
if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
|
|
799
|
+
if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
|
|
800
|
+
ids.add(integration.id);
|
|
801
|
+
if (!safeText(integration.title, 200)) throw new Error(`${at}.title is required`);
|
|
802
|
+
if (!safeText(integration.npm, 200)) throw new Error(`${at}.npm is required`);
|
|
803
|
+
if (integration.schema !== void 0 && (!isRecord4(integration.schema) || !isRecord4(integration.schema.entities))) {
|
|
804
|
+
throw new Error(`${at}.schema must contain an entities object`);
|
|
805
|
+
}
|
|
806
|
+
if (integration.rules !== void 0 && !isRecord4(integration.rules)) throw new Error(`${at}.rules must be an object`);
|
|
807
|
+
validateSeeds(integration, at);
|
|
808
|
+
validateProbes(integration, at);
|
|
809
|
+
}
|
|
810
|
+
const needsDb = cfg.integrations.some((integration) => integration.schema || integration.rules || integration.seeds?.length);
|
|
811
|
+
const services = unique(cfg.services?.length ? cfg.services : defaultServices);
|
|
812
|
+
if (needsDb && !services.includes("db")) throw new Error(`${path}: schema/rules/seed integrations require the db service`);
|
|
813
|
+
}
|
|
814
|
+
function validateSeeds(integration, at) {
|
|
815
|
+
if (integration.seeds === void 0) return;
|
|
816
|
+
if (!Array.isArray(integration.seeds)) throw new Error(`${at}.seeds must be an array`);
|
|
817
|
+
const ids = /* @__PURE__ */ new Set();
|
|
818
|
+
for (const [index, seed] of integration.seeds.entries()) {
|
|
819
|
+
const sat = `${at}.seeds[${index}]`;
|
|
820
|
+
if (!isRecord4(seed) || !safeText(seed.id, 200) || !safeText(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
|
|
821
|
+
if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
|
|
822
|
+
ids.add(seed.id);
|
|
823
|
+
if (!isRecord4(seed.key) || !safeText(seed.key.attr, 200) || !safeText(seed.key.value, 2048)) {
|
|
824
|
+
throw new Error(`${sat}.key requires string attr and value`);
|
|
825
|
+
}
|
|
826
|
+
if (!isRecord4(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
|
|
827
|
+
if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
|
|
828
|
+
throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function validateProbes(integration, at) {
|
|
833
|
+
if (integration.probes === void 0) return;
|
|
834
|
+
if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
|
|
835
|
+
for (const [index, probe] of integration.probes.entries()) {
|
|
836
|
+
const pat = `${at}.probes[${index}]`;
|
|
837
|
+
if (!isRecord4(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
|
|
838
|
+
if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
|
|
839
|
+
throw new Error(`${pat}.expectedStatus must be an HTTP status`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function isRecord4(value) {
|
|
844
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
845
|
+
}
|
|
846
|
+
function safeText(value, max) {
|
|
847
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
848
|
+
}
|
|
849
|
+
function safeProbePath(value) {
|
|
850
|
+
return typeof value === "string" && /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/.test(value);
|
|
851
|
+
}
|
|
852
|
+
function validId(value) {
|
|
853
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
854
|
+
}
|
|
855
|
+
function unique(values) {
|
|
856
|
+
return [...new Set(values.filter(Boolean))];
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/config.ts
|
|
789
860
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
790
861
|
var DEFAULT_ENVS = ["dev"];
|
|
791
862
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
@@ -800,8 +871,8 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
800
871
|
validateRawConfig(raw, resolved);
|
|
801
872
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
802
873
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
803
|
-
const envs =
|
|
804
|
-
const services =
|
|
874
|
+
const envs = unique2(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
|
|
875
|
+
const services = unique2(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
805
876
|
validateCalendarConfig(raw, envs, services, resolved);
|
|
806
877
|
const local = {
|
|
807
878
|
tokenFile: resolve2(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
@@ -835,6 +906,8 @@ async function resolveDataExport(cfg, value, names) {
|
|
|
835
906
|
throw new Error(`${value} did not export ${names.join(", ")} or default`);
|
|
836
907
|
}
|
|
837
908
|
function buildPlan(cfg) {
|
|
909
|
+
const integrationSchema = cfg.integrations?.some((integration) => integration.schema) ?? false;
|
|
910
|
+
const integrationRules = cfg.integrations?.some((integration) => integration.rules) ?? false;
|
|
838
911
|
return {
|
|
839
912
|
appId: cfg.app.id,
|
|
840
913
|
appName: cfg.app.name,
|
|
@@ -842,8 +915,9 @@ function buildPlan(cfg) {
|
|
|
842
915
|
dbEndpoint: cfg.dbEndpoint,
|
|
843
916
|
envs: cfg.envs,
|
|
844
917
|
services: cfg.services,
|
|
845
|
-
|
|
846
|
-
|
|
918
|
+
integrations: (cfg.integrations ?? []).map((integration) => integration.id),
|
|
919
|
+
hasSchema: !!cfg.db?.schema || integrationSchema,
|
|
920
|
+
hasRules: !!cfg.db?.rules || integrationRules || (!!cfg.db?.schema || integrationSchema) && cfg.db?.defaultRules !== false,
|
|
847
921
|
aiProvider: cfg.ai?.provider
|
|
848
922
|
};
|
|
849
923
|
}
|
|
@@ -852,7 +926,7 @@ function calendarServiceConfig(cfg, env) {
|
|
|
852
926
|
if (!cfg.envs.includes(env)) throw new Error(`calendar env "${env}" is not declared in config envs`);
|
|
853
927
|
const google = cfg.calendar?.google;
|
|
854
928
|
if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
|
|
855
|
-
const availability =
|
|
929
|
+
const availability = unique2(
|
|
856
930
|
(google.availabilityCalendars?.[env] ?? google.calendars?.[env]).map((id) => id.trim())
|
|
857
931
|
);
|
|
858
932
|
return {
|
|
@@ -888,15 +962,16 @@ function validateRawConfig(raw, path) {
|
|
|
888
962
|
if (!raw || typeof raw !== "object") throw new Error(`${path} must export an object`);
|
|
889
963
|
const cfg = raw;
|
|
890
964
|
if (!cfg.app || typeof cfg.app !== "object") throw new Error(`${path}: missing app object`);
|
|
891
|
-
if (!
|
|
965
|
+
if (!validId2(cfg.app.id)) throw new Error(`${path}: app.id must be lowercase letters, numbers, and hyphens`);
|
|
892
966
|
if (!cfg.app.name || typeof cfg.app.name !== "string") throw new Error(`${path}: app.name is required`);
|
|
893
967
|
if (cfg.envs !== void 0 && (!Array.isArray(cfg.envs) || cfg.envs.some((env) => typeof env !== "string"))) {
|
|
894
968
|
throw new Error(`${path}: envs must be an array of names`);
|
|
895
969
|
}
|
|
896
|
-
if (cfg.envs?.some((env) => !
|
|
970
|
+
if (cfg.envs?.some((env) => !validId2(env))) throw new Error(`${path}: env names must be lowercase letters, numbers, and hyphens`);
|
|
897
971
|
if (cfg.services !== void 0 && (!Array.isArray(cfg.services) || cfg.services.some((service) => typeof service !== "string" || !service.trim()))) {
|
|
898
972
|
throw new Error(`${path}: services must be an array of non-empty names`);
|
|
899
973
|
}
|
|
974
|
+
validateIntegrations(cfg, path, DEFAULT_SERVICES);
|
|
900
975
|
}
|
|
901
976
|
function validateCalendarConfig(cfg, envs, services, path) {
|
|
902
977
|
const enabled = services.includes("calendar");
|
|
@@ -904,9 +979,9 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
904
979
|
if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
|
|
905
980
|
return;
|
|
906
981
|
}
|
|
907
|
-
if (!
|
|
982
|
+
if (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
|
|
908
983
|
assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
|
|
909
|
-
if (!
|
|
984
|
+
if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
|
|
910
985
|
const google = cfg.calendar.google;
|
|
911
986
|
assertOnly(
|
|
912
987
|
google,
|
|
@@ -918,7 +993,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
918
993
|
throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
|
|
919
994
|
}
|
|
920
995
|
const availability = google[availabilityKey];
|
|
921
|
-
if (!
|
|
996
|
+
if (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
|
|
922
997
|
const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
|
|
923
998
|
if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
|
|
924
999
|
for (const env of envs) {
|
|
@@ -929,22 +1004,22 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
929
1004
|
if (ids.length > 10) {
|
|
930
1005
|
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
|
|
931
1006
|
}
|
|
932
|
-
if (ids.some((id) => !
|
|
1007
|
+
if (ids.some((id) => !safeText2(id, 1024))) {
|
|
933
1008
|
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
|
|
934
1009
|
}
|
|
935
1010
|
}
|
|
936
1011
|
if (google.bookingCalendar !== void 0) {
|
|
937
|
-
if (!
|
|
1012
|
+
if (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
|
|
938
1013
|
const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
|
|
939
1014
|
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
|
|
940
1015
|
for (const [env, value] of Object.entries(google.bookingCalendar)) {
|
|
941
|
-
if (!
|
|
1016
|
+
if (!safeText2(value, 1024)) {
|
|
942
1017
|
throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
|
|
943
1018
|
}
|
|
944
1019
|
}
|
|
945
1020
|
}
|
|
946
1021
|
if (google.bookingPageUrl !== void 0) {
|
|
947
|
-
if (!
|
|
1022
|
+
if (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
|
|
948
1023
|
const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
|
|
949
1024
|
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
|
|
950
1025
|
for (const [env, value] of Object.entries(google.bookingPageUrl)) {
|
|
@@ -959,10 +1034,10 @@ function assertOnly(value, allowed, label) {
|
|
|
959
1034
|
const extra = Object.keys(value).find((key) => !allowed.includes(key));
|
|
960
1035
|
if (extra) throw new Error(`${label}.${extra} is not supported`);
|
|
961
1036
|
}
|
|
962
|
-
function
|
|
1037
|
+
function isRecord5(value) {
|
|
963
1038
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
964
1039
|
}
|
|
965
|
-
function
|
|
1040
|
+
function safeText2(value, max) {
|
|
966
1041
|
return typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
967
1042
|
}
|
|
968
1043
|
function safeHttpsUrl(value) {
|
|
@@ -974,7 +1049,7 @@ function safeHttpsUrl(value) {
|
|
|
974
1049
|
return false;
|
|
975
1050
|
}
|
|
976
1051
|
}
|
|
977
|
-
function
|
|
1052
|
+
function validId2(value) {
|
|
978
1053
|
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
979
1054
|
}
|
|
980
1055
|
async function loadConfigModule(path) {
|
|
@@ -987,7 +1062,7 @@ async function loadConfigModule(path) {
|
|
|
987
1062
|
function trimSlash(value) {
|
|
988
1063
|
return value.replace(/\/+$/, "");
|
|
989
1064
|
}
|
|
990
|
-
function
|
|
1065
|
+
function unique2(values) {
|
|
991
1066
|
return [...new Set(values.filter(Boolean))];
|
|
992
1067
|
}
|
|
993
1068
|
|
|
@@ -999,9 +1074,9 @@ var CAPABILITIES = {
|
|
|
999
1074
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
1000
1075
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
1001
1076
|
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
1002
|
-
"
|
|
1077
|
+
"compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
|
|
1003
1078
|
"apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
|
|
1004
|
-
"validate
|
|
1079
|
+
"validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
|
|
1005
1080
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
1006
1081
|
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
1007
1082
|
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
@@ -1009,7 +1084,7 @@ var CAPABILITIES = {
|
|
|
1009
1084
|
agent: [
|
|
1010
1085
|
"install and import the selected odla SDKs",
|
|
1011
1086
|
"wrap the Worker with withObservability and choose useful telemetry",
|
|
1012
|
-
"make application-specific schema, rules, auth, UI, and migration decisions",
|
|
1087
|
+
"install capability packages, mount their runtime routes, and make application-specific schema, rules, auth, UI, and migration decisions",
|
|
1013
1088
|
"wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
|
|
1014
1089
|
],
|
|
1015
1090
|
human: [
|
|
@@ -1681,20 +1756,121 @@ function readPackageJson(rootDir) {
|
|
|
1681
1756
|
}
|
|
1682
1757
|
}
|
|
1683
1758
|
|
|
1759
|
+
// src/integrations.ts
|
|
1760
|
+
import { isDeepStrictEqual } from "util";
|
|
1761
|
+
async function resolveDatabaseConfig(cfg, options = {}) {
|
|
1762
|
+
const integrations = cfg.integrations ?? [];
|
|
1763
|
+
if (!cfg.services.includes("db")) return { schema: void 0, rules: void 0, integrations };
|
|
1764
|
+
const authoredSchema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
1765
|
+
const schema = mergeSchemas(authoredSchema, integrations);
|
|
1766
|
+
if (options.validateSeeds !== false) assertSeedContracts(integrations, schema);
|
|
1767
|
+
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
1768
|
+
const explicitRules = mergeRules(configuredRules, integrations);
|
|
1769
|
+
const rules = configuredRules === void 0 && schema && cfg.db?.defaultRules !== false ? { ...rulesFromSchema(schema), ...explicitRules ?? {} } : explicitRules;
|
|
1770
|
+
return { schema, rules, integrations };
|
|
1771
|
+
}
|
|
1772
|
+
function integrationWarnings(integrations, schema, rules) {
|
|
1773
|
+
const warnings = [];
|
|
1774
|
+
const entities = new Set(serializedEntities(schema));
|
|
1775
|
+
for (const integration of integrations) {
|
|
1776
|
+
const namespaces = Object.keys(integration.schema?.entities ?? {});
|
|
1777
|
+
for (const ns of namespaces) {
|
|
1778
|
+
if (!entities.has(ns)) warnings.push(`integration "${integration.id}" schema namespace "${ns}" is missing`);
|
|
1779
|
+
const rule = rules?.[ns];
|
|
1780
|
+
if (!rule) {
|
|
1781
|
+
warnings.push(`integration "${integration.id}" has no rules for "${ns}"`);
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
for (const action of ["view", "create", "update", "delete"]) {
|
|
1785
|
+
if (rule[action] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action}" is missing`);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
for (const seed of integration.seeds ?? []) {
|
|
1789
|
+
if (!entities.has(seed.ns)) {
|
|
1790
|
+
warnings.push(`integration "${integration.id}" seed "${seed.id}" targets unknown namespace "${seed.ns}"`);
|
|
1791
|
+
} else if (!isUniqueAttr(schema, seed.ns, seed.key.attr)) {
|
|
1792
|
+
warnings.push(`integration "${integration.id}" seed "${seed.id}" key "${seed.ns}.${seed.key.attr}" is not declared unique`);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return warnings;
|
|
1797
|
+
}
|
|
1798
|
+
function mergeSchemas(base, integrations) {
|
|
1799
|
+
const fragments = integrations.filter((integration) => integration.schema);
|
|
1800
|
+
if (fragments.length === 0) return base;
|
|
1801
|
+
const merged = normalizeSchema(base);
|
|
1802
|
+
for (const integration of fragments) {
|
|
1803
|
+
const fragment = normalizeSchema(integration.schema);
|
|
1804
|
+
mergeMap(merged.entities, fragment.entities, `integration "${integration.id}" schema namespace`);
|
|
1805
|
+
mergeMap(merged.links, fragment.links, `integration "${integration.id}" schema link`);
|
|
1806
|
+
}
|
|
1807
|
+
return merged;
|
|
1808
|
+
}
|
|
1809
|
+
function mergeRules(base, integrations) {
|
|
1810
|
+
const fragments = integrations.filter((integration) => integration.rules);
|
|
1811
|
+
if (fragments.length === 0) return base;
|
|
1812
|
+
const merged = { ...base ?? {} };
|
|
1813
|
+
for (const integration of fragments) {
|
|
1814
|
+
mergeMap(merged, integration.rules ?? {}, `integration "${integration.id}" rule namespace`);
|
|
1815
|
+
}
|
|
1816
|
+
return merged;
|
|
1817
|
+
}
|
|
1818
|
+
function assertSeedContracts(integrations, schema) {
|
|
1819
|
+
const entities = new Set(serializedEntities(schema));
|
|
1820
|
+
for (const integration of integrations) {
|
|
1821
|
+
for (const seed of integration.seeds ?? []) {
|
|
1822
|
+
if (!entities.has(seed.ns)) {
|
|
1823
|
+
throw new Error(`integration "${integration.id}" seed "${seed.id}" targets unknown namespace "${seed.ns}"`);
|
|
1824
|
+
}
|
|
1825
|
+
if (!isUniqueAttr(schema, seed.ns, seed.key.attr)) {
|
|
1826
|
+
throw new Error(`integration "${integration.id}" seed "${seed.id}" key "${seed.ns}.${seed.key.attr}" must be declared unique`);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
function isUniqueAttr(schema, ns, attr) {
|
|
1832
|
+
if (!isRecord6(schema) || !isRecord6(schema.entities)) return false;
|
|
1833
|
+
const entity = schema.entities[ns];
|
|
1834
|
+
if (!isRecord6(entity) || !isRecord6(entity.attrs)) return false;
|
|
1835
|
+
const definition = entity.attrs[attr];
|
|
1836
|
+
return isRecord6(definition) && definition.unique === true;
|
|
1837
|
+
}
|
|
1838
|
+
function normalizeSchema(value) {
|
|
1839
|
+
if (value === void 0 || value === null) return { entities: {}, links: {} };
|
|
1840
|
+
if (!isRecord6(value) || !isRecord6(value.entities)) {
|
|
1841
|
+
throw new Error("db schema must be a serialized schema object with an entities map");
|
|
1842
|
+
}
|
|
1843
|
+
if (value.links !== void 0 && !isRecord6(value.links)) throw new Error("db schema links must be an object");
|
|
1844
|
+
return {
|
|
1845
|
+
entities: { ...value.entities },
|
|
1846
|
+
links: { ...value.links ?? {} }
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
function mergeMap(target, fragment, label) {
|
|
1850
|
+
for (const [name, value] of Object.entries(fragment)) {
|
|
1851
|
+
if (Object.hasOwn(target, name) && !isDeepStrictEqual(target[name], value)) {
|
|
1852
|
+
throw new Error(`${label} "${name}" conflicts with an existing definition`);
|
|
1853
|
+
}
|
|
1854
|
+
target[name] = value;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function isRecord6(value) {
|
|
1858
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1684
1861
|
// src/doctor.ts
|
|
1685
1862
|
async function doctor(options) {
|
|
1686
1863
|
const out = options.stdout ?? console;
|
|
1687
1864
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1688
1865
|
const plan = buildPlan(cfg);
|
|
1689
|
-
const
|
|
1690
|
-
const
|
|
1691
|
-
const configuredRules = hasDb ? await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]) : void 0;
|
|
1692
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
1866
|
+
const database = await resolveDatabaseConfig(cfg, { validateSeeds: false });
|
|
1867
|
+
const { schema, rules } = database;
|
|
1693
1868
|
const entities = serializedEntities(schema);
|
|
1694
1869
|
out.log(`config: ${cfg.configPath}`);
|
|
1695
1870
|
out.log(`app: ${plan.appName} (${plan.appId})`);
|
|
1696
1871
|
out.log(`envs: ${plan.envs.join(", ")}`);
|
|
1697
1872
|
out.log(`svc: ${plan.services.join(", ")}`);
|
|
1873
|
+
out.log(`integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
|
|
1698
1874
|
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
1699
1875
|
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
1700
1876
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
@@ -1716,6 +1892,7 @@ async function doctor(options) {
|
|
|
1716
1892
|
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
1717
1893
|
}
|
|
1718
1894
|
}
|
|
1895
|
+
warnings.push(...integrationWarnings(database.integrations, schema, rules));
|
|
1719
1896
|
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
1720
1897
|
if (cfg.auth?.clerk) {
|
|
1721
1898
|
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
@@ -1806,6 +1983,9 @@ function configTemplate(input) {
|
|
|
1806
1983
|
},
|
|
1807
1984
|
envs: ${JSON.stringify(input.envs)},
|
|
1808
1985
|
services: ${JSON.stringify(input.services)},
|
|
1986
|
+
// App capabilities are npm modules, not hosted services. Import their
|
|
1987
|
+
// data-only descriptor and add it here (for example createCrmIntegration()).
|
|
1988
|
+
integrations: [],
|
|
1809
1989
|
db: {
|
|
1810
1990
|
schema: "./src/odla/schema.mjs",
|
|
1811
1991
|
rules: "./src/odla/rules.mjs",
|
|
@@ -1956,9 +2136,61 @@ function assertWranglerConfig(cfg) {
|
|
|
1956
2136
|
|
|
1957
2137
|
// src/provision.ts
|
|
1958
2138
|
import { createAppsClient, tenantIdFor as tenantIdFor2 } from "@odla-ai/apps";
|
|
1959
|
-
import {
|
|
2139
|
+
import { putSecret } from "@odla-ai/ai";
|
|
1960
2140
|
import process8 from "process";
|
|
1961
2141
|
|
|
2142
|
+
// src/integration-provision.ts
|
|
2143
|
+
import { uuidv7 } from "@odla-ai/db";
|
|
2144
|
+
async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, integrations, env, out) {
|
|
2145
|
+
const base = `${endpoint}/app/${encodeURIComponent(tenantId)}`;
|
|
2146
|
+
for (const integration of integrations) {
|
|
2147
|
+
for (const seed of integration.seeds ?? []) {
|
|
2148
|
+
const payload = await postJson(doFetch, `${base}/query`, dbKey, {
|
|
2149
|
+
query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
|
|
2150
|
+
});
|
|
2151
|
+
const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
|
|
2152
|
+
if (!Array.isArray(rows)) {
|
|
2153
|
+
throw new Error(`${env}: integration ${integration.id} seed ${seed.id} query returned an invalid response`);
|
|
2154
|
+
}
|
|
2155
|
+
if (rows.length > 0) {
|
|
2156
|
+
out.log(`${env}: integration ${integration.id} seed ${seed.id} already exists`);
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
await postJson(doFetch, `${base}/transact`, dbKey, {
|
|
2160
|
+
mutationId: `integration:${integration.id}:seed:${seed.id}`,
|
|
2161
|
+
ops: [{
|
|
2162
|
+
t: "update",
|
|
2163
|
+
ns: seed.ns,
|
|
2164
|
+
// A fresh id plus the declared-unique natural key is create-only: a
|
|
2165
|
+
// concurrent creator gets a unique violation instead of being overwritten.
|
|
2166
|
+
id: uuidv7(),
|
|
2167
|
+
attrs: { ...seed.attrs, [seed.key.attr]: seed.key.value }
|
|
2168
|
+
}]
|
|
2169
|
+
});
|
|
2170
|
+
out.log(`${env}: integration ${integration.id} seed ${seed.id} created`);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
async function postJson(doFetch, url, bearer, body) {
|
|
2175
|
+
const res = await doFetch(url, {
|
|
2176
|
+
method: "POST",
|
|
2177
|
+
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
2178
|
+
body: JSON.stringify(body)
|
|
2179
|
+
});
|
|
2180
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await responseText(res)}`);
|
|
2181
|
+
return res.json().catch(() => ({}));
|
|
2182
|
+
}
|
|
2183
|
+
function isRecord7(value) {
|
|
2184
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2185
|
+
}
|
|
2186
|
+
async function responseText(res) {
|
|
2187
|
+
try {
|
|
2188
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
2189
|
+
} catch {
|
|
2190
|
+
return "";
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1962
2194
|
// src/provision-credentials.ts
|
|
1963
2195
|
import { tenantIdFor } from "@odla-ai/apps";
|
|
1964
2196
|
async function provisionEnvCredentials(opts) {
|
|
@@ -2018,14 +2250,14 @@ async function mintDbKey(opts, tenantId) {
|
|
|
2018
2250
|
appId: tenantId
|
|
2019
2251
|
})
|
|
2020
2252
|
});
|
|
2021
|
-
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await
|
|
2253
|
+
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText3(created)}`);
|
|
2022
2254
|
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
2023
2255
|
method: "POST",
|
|
2024
2256
|
headers,
|
|
2025
2257
|
body: "{}"
|
|
2026
2258
|
});
|
|
2027
2259
|
}
|
|
2028
|
-
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await
|
|
2260
|
+
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
|
|
2029
2261
|
const body = await res.json();
|
|
2030
2262
|
if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
|
|
2031
2263
|
return body.key;
|
|
@@ -2041,12 +2273,12 @@ async function issueO11yToken(opts) {
|
|
|
2041
2273
|
`o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
|
|
2042
2274
|
);
|
|
2043
2275
|
}
|
|
2044
|
-
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await
|
|
2276
|
+
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText3(res)}`);
|
|
2045
2277
|
const body = await res.json();
|
|
2046
2278
|
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
2047
2279
|
return body.token;
|
|
2048
2280
|
}
|
|
2049
|
-
async function
|
|
2281
|
+
async function safeText3(res) {
|
|
2050
2282
|
try {
|
|
2051
2283
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
2052
2284
|
} catch {
|
|
@@ -2054,6 +2286,18 @@ async function safeText2(res) {
|
|
|
2054
2286
|
}
|
|
2055
2287
|
}
|
|
2056
2288
|
|
|
2289
|
+
// src/provision-helpers.ts
|
|
2290
|
+
import { DEFAULT_SECRET_NAMES } from "@odla-ai/ai";
|
|
2291
|
+
function serviceRank(service) {
|
|
2292
|
+
if (service === "db") return 0;
|
|
2293
|
+
if (service === "calendar") return 2;
|
|
2294
|
+
return 1;
|
|
2295
|
+
}
|
|
2296
|
+
function defaultSecretName(provider) {
|
|
2297
|
+
const names = DEFAULT_SECRET_NAMES;
|
|
2298
|
+
return names[provider] ?? `${provider}_api_key`;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2057
2301
|
// src/provision.ts
|
|
2058
2302
|
async function provision(options) {
|
|
2059
2303
|
const out = options.stdout ?? console;
|
|
@@ -2072,19 +2316,23 @@ async function provision(options) {
|
|
|
2072
2316
|
out.log(` db: ${plan.dbEndpoint}`);
|
|
2073
2317
|
out.log(` envs: ${plan.envs.join(", ")}`);
|
|
2074
2318
|
out.log(` services: ${plan.services.join(", ")}`);
|
|
2319
|
+
out.log(` integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
|
|
2075
2320
|
const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
|
|
2076
2321
|
if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
|
|
2077
2322
|
throw new Error(
|
|
2078
2323
|
`refusing to provision production env ${productionEnvs.join(", ")} without --yes; run --dry-run first`
|
|
2079
2324
|
);
|
|
2080
2325
|
}
|
|
2081
|
-
const
|
|
2082
|
-
const
|
|
2083
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
2326
|
+
const database = await resolveDatabaseConfig(cfg);
|
|
2327
|
+
const { schema, rules } = database;
|
|
2084
2328
|
if (options.dryRun) {
|
|
2085
2329
|
out.log("dry run: no network calls or file writes");
|
|
2086
2330
|
out.log(` schema: ${schema ? "yes" : "no"}`);
|
|
2087
2331
|
out.log(` rules: ${rules ? Object.keys(rules).length : 0} namespaces`);
|
|
2332
|
+
for (const integration of database.integrations) {
|
|
2333
|
+
const namespaces = Object.keys(integration.schema?.entities ?? {}).length;
|
|
2334
|
+
out.log(` integration.${integration.id}: ${namespaces} namespaces, ${integration.seeds?.length ?? 0} seeds, ${integration.probes?.length ?? 0} smoke probes`);
|
|
2335
|
+
}
|
|
2088
2336
|
out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
2089
2337
|
if (cfg.services.includes("calendar")) {
|
|
2090
2338
|
for (const env of cfg.envs) {
|
|
@@ -2194,13 +2442,16 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
2194
2442
|
}
|
|
2195
2443
|
}
|
|
2196
2444
|
if (schema && dbKey) {
|
|
2197
|
-
await
|
|
2445
|
+
await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
2198
2446
|
out.log(`${env}: schema pushed`);
|
|
2199
2447
|
}
|
|
2200
2448
|
if (rules) {
|
|
2201
|
-
await
|
|
2449
|
+
await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
|
|
2202
2450
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
2203
2451
|
}
|
|
2452
|
+
if (dbKey) {
|
|
2453
|
+
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
2454
|
+
}
|
|
2204
2455
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
2205
2456
|
const key = process8.env[cfg.ai.keyEnv];
|
|
2206
2457
|
if (key) {
|
|
@@ -2241,11 +2492,6 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
2241
2492
|
}
|
|
2242
2493
|
}
|
|
2243
2494
|
}
|
|
2244
|
-
function serviceRank(service) {
|
|
2245
|
-
if (service === "db") return 0;
|
|
2246
|
-
if (service === "calendar") return 2;
|
|
2247
|
-
return 1;
|
|
2248
|
-
}
|
|
2249
2495
|
async function readRegistryApp(cfg, token, doFetch) {
|
|
2250
2496
|
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
|
|
2251
2497
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -2255,13 +2501,13 @@ async function readRegistryApp(cfg, token, doFetch) {
|
|
|
2255
2501
|
const json = await res.json();
|
|
2256
2502
|
return json.app ?? null;
|
|
2257
2503
|
}
|
|
2258
|
-
async function
|
|
2504
|
+
async function postJson2(doFetch, url, bearer, body) {
|
|
2259
2505
|
const res = await doFetch(url, {
|
|
2260
2506
|
method: "POST",
|
|
2261
2507
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
2262
2508
|
body: JSON.stringify(body)
|
|
2263
2509
|
});
|
|
2264
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await
|
|
2510
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
|
|
2265
2511
|
}
|
|
2266
2512
|
function normalizeClerkConfig(value) {
|
|
2267
2513
|
if (!value) return null;
|
|
@@ -2274,11 +2520,7 @@ function normalizeClerkConfig(value) {
|
|
|
2274
2520
|
const publishableKey = envValue(cfg.publishableKey);
|
|
2275
2521
|
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
2276
2522
|
}
|
|
2277
|
-
function
|
|
2278
|
-
const names = DEFAULT_SECRET_NAMES;
|
|
2279
|
-
return names[provider] ?? `${provider}_api_key`;
|
|
2280
|
-
}
|
|
2281
|
-
async function safeText3(res) {
|
|
2523
|
+
async function safeText4(res) {
|
|
2282
2524
|
try {
|
|
2283
2525
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
2284
2526
|
} catch {
|
|
@@ -3130,7 +3372,8 @@ async function smoke(options) {
|
|
|
3130
3372
|
assertCalendarHealthy(status, calendarServiceConfig(cfg, env));
|
|
3131
3373
|
out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
|
|
3132
3374
|
}
|
|
3133
|
-
const
|
|
3375
|
+
const database = await resolveDatabaseConfig(cfg);
|
|
3376
|
+
const expectedSchema = database.schema;
|
|
3134
3377
|
const expectedEntities = serializedEntities(expectedSchema);
|
|
3135
3378
|
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
|
|
3136
3379
|
const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
|
|
@@ -3142,7 +3385,7 @@ async function smoke(options) {
|
|
|
3142
3385
|
out.log(` schema: ${liveEntities.length} entities`);
|
|
3143
3386
|
const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
|
|
3144
3387
|
if (aggregateEntity) {
|
|
3145
|
-
const aggregate = await
|
|
3388
|
+
const aggregate = await postJson3(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
|
|
3146
3389
|
ns: aggregateEntity,
|
|
3147
3390
|
aggregate: { count: true }
|
|
3148
3391
|
});
|
|
@@ -3151,6 +3394,21 @@ async function smoke(options) {
|
|
|
3151
3394
|
} else {
|
|
3152
3395
|
out.log(` aggregate: skipped (schema has no entities)`);
|
|
3153
3396
|
}
|
|
3397
|
+
const probes = database.integrations.flatMap(
|
|
3398
|
+
(integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
|
|
3399
|
+
);
|
|
3400
|
+
if (probes.length) {
|
|
3401
|
+
const link = cfg.links?.[env];
|
|
3402
|
+
if (!link) throw new Error(`integration smoke probes require links.${env} in odla.config.mjs`);
|
|
3403
|
+
for (const { integration, probe } of probes) {
|
|
3404
|
+
const probeUrl = new URL(probe.path, link).toString();
|
|
3405
|
+
const res = await doFetch(probeUrl, { redirect: "manual" });
|
|
3406
|
+
if (res.status !== probe.expectedStatus) {
|
|
3407
|
+
throw new Error(`integration "${integration}" probe ${probe.path} returned ${res.status}; expected ${probe.expectedStatus}`);
|
|
3408
|
+
}
|
|
3409
|
+
out.log(` integration.${integration}: ${probe.path} \u2192 ${res.status}`);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3154
3412
|
out.log("ok");
|
|
3155
3413
|
}
|
|
3156
3414
|
function assertCalendarHealthy(status, expected) {
|
|
@@ -3168,16 +3426,16 @@ async function getJson(doFetch, url, bearer) {
|
|
|
3168
3426
|
const res = await doFetch(url, {
|
|
3169
3427
|
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
3170
3428
|
});
|
|
3171
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
3429
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
3172
3430
|
return res.json();
|
|
3173
3431
|
}
|
|
3174
|
-
async function
|
|
3432
|
+
async function postJson3(doFetch, url, bearer, body) {
|
|
3175
3433
|
const res = await doFetch(url, {
|
|
3176
3434
|
method: "POST",
|
|
3177
3435
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
3178
3436
|
body: JSON.stringify(body)
|
|
3179
3437
|
});
|
|
3180
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
3438
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
3181
3439
|
return res.json();
|
|
3182
3440
|
}
|
|
3183
3441
|
function publicConfigUrl(platformUrl, appId, env) {
|
|
@@ -3185,7 +3443,7 @@ function publicConfigUrl(platformUrl, appId, env) {
|
|
|
3185
3443
|
url.searchParams.set("env", env);
|
|
3186
3444
|
return url.toString();
|
|
3187
3445
|
}
|
|
3188
|
-
async function
|
|
3446
|
+
async function safeText5(res) {
|
|
3189
3447
|
try {
|
|
3190
3448
|
return (await res.text()).slice(0, 500);
|
|
3191
3449
|
} catch {
|
|
@@ -3512,8 +3770,8 @@ Commands:
|
|
|
3512
3770
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
3513
3771
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
3514
3772
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
3515
|
-
provision Register services,
|
|
3516
|
-
smoke Verify
|
|
3773
|
+
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
3774
|
+
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
3517
3775
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
3518
3776
|
copilot, gemini, or agents (repeatable or comma-separated).
|
|
3519
3777
|
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
@@ -4249,4 +4507,4 @@ export {
|
|
|
4249
4507
|
exitCodeFor,
|
|
4250
4508
|
runCli
|
|
4251
4509
|
};
|
|
4252
|
-
//# sourceMappingURL=chunk-
|
|
4510
|
+
//# sourceMappingURL=chunk-I7XDJZB6.js.map
|