primitive-admin 1.0.40 → 1.0.42
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/assets/skill/skills/primitive-platform/SKILL.md +1 -9
- package/dist/bin/primitive.js +132 -16
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.js +107 -0
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +354 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- package/dist/src/commands/collections.js +18 -4
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +364 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/email-templates.js +19 -5
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.js +260 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/init.js +90 -2
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/sync.js +428 -7
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/lib/api-client.js +137 -3
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config.js +51 -53
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/credentials-store.js +307 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/env-resolver.js +121 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/paginate.js +42 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/project-config.js +209 -0
- package/dist/src/lib/project-config.js.map +1 -0
- package/dist/src/lib/sync-paths.js +102 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/version-check.js +5 -2
- package/dist/src/lib/version-check.js.map +1 -1
- package/package.json +1 -1
|
@@ -124,6 +124,62 @@ function serializeWebhook(webhook) {
|
|
|
124
124
|
});
|
|
125
125
|
return TOML.stringify(data);
|
|
126
126
|
}
|
|
127
|
+
function serializeCronTrigger(trigger) {
|
|
128
|
+
const data = {
|
|
129
|
+
cronTrigger: {
|
|
130
|
+
key: trigger.triggerKey,
|
|
131
|
+
displayName: trigger.displayName,
|
|
132
|
+
description: trigger.description || undefined,
|
|
133
|
+
cron: trigger.cron,
|
|
134
|
+
timezone: trigger.timezone || "UTC",
|
|
135
|
+
workflowKey: trigger.workflowKey,
|
|
136
|
+
overlapPolicy: trigger.overlapPolicy || "skip",
|
|
137
|
+
state: trigger.state,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
if (trigger.rootInput) {
|
|
141
|
+
try {
|
|
142
|
+
data.rootInput = JSON.parse(trigger.rootInput);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
data.rootInput = { raw: trigger.rootInput };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (trigger.inputMapping) {
|
|
149
|
+
try {
|
|
150
|
+
data.inputMapping = JSON.parse(trigger.inputMapping);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
data.inputMapping = { raw: trigger.inputMapping };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Remove undefined values
|
|
157
|
+
Object.keys(data.cronTrigger).forEach(k => {
|
|
158
|
+
if (data.cronTrigger[k] === undefined)
|
|
159
|
+
delete data.cronTrigger[k];
|
|
160
|
+
});
|
|
161
|
+
return TOML.stringify(data);
|
|
162
|
+
}
|
|
163
|
+
function serializeBlobBucket(bucket) {
|
|
164
|
+
const data = {
|
|
165
|
+
bucket: {
|
|
166
|
+
key: bucket.bucketKey,
|
|
167
|
+
name: bucket.name,
|
|
168
|
+
description: bucket.description || undefined,
|
|
169
|
+
ttlTier: bucket.ttlTier,
|
|
170
|
+
accessPolicy: bucket.accessPolicy,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
if (bucket.ruleSetId) {
|
|
174
|
+
data.bucket.ruleSetId = bucket.ruleSetId;
|
|
175
|
+
}
|
|
176
|
+
// Remove undefined values
|
|
177
|
+
Object.keys(data.bucket).forEach(k => {
|
|
178
|
+
if (data.bucket[k] === undefined)
|
|
179
|
+
delete data.bucket[k];
|
|
180
|
+
});
|
|
181
|
+
return TOML.stringify(data);
|
|
182
|
+
}
|
|
127
183
|
function serializePrompt(prompt) {
|
|
128
184
|
const data = {
|
|
129
185
|
prompt: {
|
|
@@ -256,6 +312,16 @@ function serializeGroupTypeConfig(config, ruleSetIdToName) {
|
|
|
256
312
|
};
|
|
257
313
|
return TOML.stringify(data);
|
|
258
314
|
}
|
|
315
|
+
function serializeCollectionTypeConfig(config, ruleSetIdToName) {
|
|
316
|
+
const ruleSetName = config.ruleSetId ? (ruleSetIdToName.get(config.ruleSetId) || "") : "";
|
|
317
|
+
const data = {
|
|
318
|
+
collectionTypeConfig: {
|
|
319
|
+
collectionType: config.collectionType,
|
|
320
|
+
ruleSetName: ruleSetName,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
return TOML.stringify(data);
|
|
324
|
+
}
|
|
259
325
|
export function parseDatabaseTypeToml(tomlData) {
|
|
260
326
|
const typeSection = tomlData.type || {};
|
|
261
327
|
const typeConfig = {
|
|
@@ -308,6 +374,20 @@ export function parseGroupTypeConfigToml(tomlData) {
|
|
|
308
374
|
}
|
|
309
375
|
return result;
|
|
310
376
|
}
|
|
377
|
+
export function parseCollectionTypeConfigToml(tomlData) {
|
|
378
|
+
const section = tomlData.collectionTypeConfig || {};
|
|
379
|
+
const result = {
|
|
380
|
+
collectionType: section.collectionType,
|
|
381
|
+
};
|
|
382
|
+
// Support both new key-based reference and legacy ID-based reference
|
|
383
|
+
if (section.ruleSetName && section.ruleSetName !== "") {
|
|
384
|
+
result._ruleSetName = section.ruleSetName;
|
|
385
|
+
}
|
|
386
|
+
else if (section.ruleSetId && section.ruleSetId !== "") {
|
|
387
|
+
result.ruleSetId = section.ruleSetId;
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
311
391
|
// Parsing helpers
|
|
312
392
|
function parseTomlFile(filePath) {
|
|
313
393
|
const content = readFileSync(filePath, "utf-8");
|
|
@@ -770,6 +850,8 @@ Directory Structure:
|
|
|
770
850
|
app.toml # App settings
|
|
771
851
|
integrations/*.toml # Integration configs
|
|
772
852
|
webhooks/*.toml # Webhook configs
|
|
853
|
+
cron-triggers/*.toml # Cron trigger configs
|
|
854
|
+
blob-buckets/*.toml # Blob bucket configs
|
|
773
855
|
prompts/*.toml # Prompt configs
|
|
774
856
|
prompts/{key}.tests/*.toml # Prompt test cases
|
|
775
857
|
workflows/*.toml # Workflow definitions
|
|
@@ -777,6 +859,7 @@ Directory Structure:
|
|
|
777
859
|
database-types/*.toml # Database type configs + operations
|
|
778
860
|
rule-sets/*.toml # Access rule sets
|
|
779
861
|
group-type-configs/*.toml # Group type configs
|
|
862
|
+
collection-type-configs/*.toml # Collection type configs
|
|
780
863
|
email-templates/*.toml # Email template overrides
|
|
781
864
|
`);
|
|
782
865
|
// Init
|
|
@@ -792,11 +875,14 @@ Directory Structure:
|
|
|
792
875
|
ensureDir(configDir);
|
|
793
876
|
ensureDir(join(configDir, "integrations"));
|
|
794
877
|
ensureDir(join(configDir, "webhooks"));
|
|
878
|
+
ensureDir(join(configDir, "cron-triggers"));
|
|
879
|
+
ensureDir(join(configDir, "blob-buckets"));
|
|
795
880
|
ensureDir(join(configDir, "prompts"));
|
|
796
881
|
ensureDir(join(configDir, "workflows"));
|
|
797
882
|
ensureDir(join(configDir, "database-types"));
|
|
798
883
|
ensureDir(join(configDir, "rule-sets"));
|
|
799
884
|
ensureDir(join(configDir, "group-type-configs"));
|
|
885
|
+
ensureDir(join(configDir, "collection-type-configs"));
|
|
800
886
|
ensureDir(join(configDir, "email-templates"));
|
|
801
887
|
const state = {
|
|
802
888
|
appId: resolvedAppId,
|
|
@@ -862,10 +948,11 @@ Directory Structure:
|
|
|
862
948
|
return;
|
|
863
949
|
}
|
|
864
950
|
// Fetch database config resources
|
|
865
|
-
const [databaseTypeConfigsResult, ruleSetsResult, groupTypeConfigsResult] = await Promise.all([
|
|
951
|
+
const [databaseTypeConfigsResult, ruleSetsResult, groupTypeConfigsResult, collectionTypeConfigsResult,] = await Promise.all([
|
|
866
952
|
client.listDatabaseTypeConfigs(resolvedAppId).catch(() => []),
|
|
867
953
|
client.listRuleSets(resolvedAppId).catch(() => []),
|
|
868
954
|
client.listGroupTypeConfigs(resolvedAppId).catch(() => []),
|
|
955
|
+
client.listCollectionTypeConfigs(resolvedAppId).catch(() => []),
|
|
869
956
|
]);
|
|
870
957
|
// Fetch operations for each database type
|
|
871
958
|
const databaseTypesWithOps = await Promise.all((Array.isArray(databaseTypeConfigsResult) ? databaseTypeConfigsResult : []).map(async (typeConfig) => {
|
|
@@ -876,11 +963,14 @@ Directory Structure:
|
|
|
876
963
|
ensureDir(configDir);
|
|
877
964
|
ensureDir(join(configDir, "integrations"));
|
|
878
965
|
ensureDir(join(configDir, "webhooks"));
|
|
966
|
+
ensureDir(join(configDir, "cron-triggers"));
|
|
967
|
+
ensureDir(join(configDir, "blob-buckets"));
|
|
879
968
|
ensureDir(join(configDir, "prompts"));
|
|
880
969
|
ensureDir(join(configDir, "workflows"));
|
|
881
970
|
ensureDir(join(configDir, "database-types"));
|
|
882
971
|
ensureDir(join(configDir, "rule-sets"));
|
|
883
972
|
ensureDir(join(configDir, "group-type-configs"));
|
|
973
|
+
ensureDir(join(configDir, "collection-type-configs"));
|
|
884
974
|
ensureDir(join(configDir, "email-templates"));
|
|
885
975
|
// Write app settings
|
|
886
976
|
if (settings) {
|
|
@@ -918,6 +1008,46 @@ Directory Structure:
|
|
|
918
1008
|
};
|
|
919
1009
|
}
|
|
920
1010
|
info(` Pulled ${webhooks.length} webhook(s)`);
|
|
1011
|
+
// Pull cron triggers
|
|
1012
|
+
let cronTriggerItems = [];
|
|
1013
|
+
try {
|
|
1014
|
+
const cronResult = await client.listCronTriggers(resolvedAppId);
|
|
1015
|
+
cronTriggerItems = cronResult.items || [];
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
// Cron triggers may not be available on older servers
|
|
1019
|
+
}
|
|
1020
|
+
const cronTriggersDir = join(configDir, "cron-triggers");
|
|
1021
|
+
mkdirSync(cronTriggersDir, { recursive: true });
|
|
1022
|
+
const cronTriggerEntities = {};
|
|
1023
|
+
for (const trigger of cronTriggerItems) {
|
|
1024
|
+
const filename = `${trigger.triggerKey}.toml`;
|
|
1025
|
+
const filePath = join(cronTriggersDir, filename);
|
|
1026
|
+
writeFileSync(filePath, serializeCronTrigger(trigger));
|
|
1027
|
+
cronTriggerEntities[trigger.triggerKey] = {
|
|
1028
|
+
id: trigger.triggerId,
|
|
1029
|
+
modifiedAt: trigger.modifiedAt || new Date().toISOString(),
|
|
1030
|
+
contentHash: computeFileHash(filePath),
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
info(` Pulled ${cronTriggerItems.length} cron trigger(s)`);
|
|
1034
|
+
// Pull blob buckets
|
|
1035
|
+
const blobBucketsResult = await client.listBlobBuckets(resolvedAppId).catch(() => ({ items: [] }));
|
|
1036
|
+
const blobBucketItems = blobBucketsResult.items || [];
|
|
1037
|
+
const blobBucketsDir = join(configDir, "blob-buckets");
|
|
1038
|
+
mkdirSync(blobBucketsDir, { recursive: true });
|
|
1039
|
+
const blobBucketEntities = {};
|
|
1040
|
+
for (const bucket of blobBucketItems) {
|
|
1041
|
+
const filename = `${bucket.bucketKey}.toml`;
|
|
1042
|
+
const filePath = join(blobBucketsDir, filename);
|
|
1043
|
+
writeFileSync(filePath, serializeBlobBucket(bucket));
|
|
1044
|
+
blobBucketEntities[bucket.bucketKey] = {
|
|
1045
|
+
id: bucket.bucketId,
|
|
1046
|
+
modifiedAt: bucket.modifiedAt || new Date().toISOString(),
|
|
1047
|
+
contentHash: computeFileHash(filePath),
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
info(` Pulled ${blobBucketItems.length} blob bucket(s)`);
|
|
921
1051
|
// Write prompts
|
|
922
1052
|
const promptEntities = {};
|
|
923
1053
|
for (const prompt of prompts) {
|
|
@@ -1059,6 +1189,21 @@ Directory Structure:
|
|
|
1059
1189
|
};
|
|
1060
1190
|
info(` Wrote group-type-configs/${filename}`);
|
|
1061
1191
|
}
|
|
1192
|
+
// Write collection type configs
|
|
1193
|
+
const collectionTypeConfigEntities = {};
|
|
1194
|
+
const collectionTypeConfigs = Array.isArray(collectionTypeConfigsResult)
|
|
1195
|
+
? collectionTypeConfigsResult
|
|
1196
|
+
: [];
|
|
1197
|
+
for (const config of collectionTypeConfigs) {
|
|
1198
|
+
const filename = `${config.collectionType}.toml`;
|
|
1199
|
+
const filePath = join(configDir, "collection-type-configs", filename);
|
|
1200
|
+
writeFileSync(filePath, serializeCollectionTypeConfig(config, ruleSetIdToName));
|
|
1201
|
+
collectionTypeConfigEntities[config.collectionType] = {
|
|
1202
|
+
modifiedAt: config.modifiedAt || new Date().toISOString(),
|
|
1203
|
+
contentHash: computeFileHash(filePath),
|
|
1204
|
+
};
|
|
1205
|
+
info(` Wrote collection-type-configs/${filename}`);
|
|
1206
|
+
}
|
|
1062
1207
|
// Save sync state
|
|
1063
1208
|
const state = {
|
|
1064
1209
|
appId: resolvedAppId,
|
|
@@ -1068,6 +1213,8 @@ Directory Structure:
|
|
|
1068
1213
|
app: settings ? { modifiedAt: new Date().toISOString(), contentHash: computeFileHash(join(configDir, "app.toml")) } : undefined,
|
|
1069
1214
|
integrations: integrationEntities,
|
|
1070
1215
|
webhooks: webhookEntities,
|
|
1216
|
+
cronTriggers: Object.keys(cronTriggerEntities).length > 0 ? cronTriggerEntities : undefined,
|
|
1217
|
+
blobBuckets: Object.keys(blobBucketEntities).length > 0 ? blobBucketEntities : undefined,
|
|
1071
1218
|
prompts: promptEntities,
|
|
1072
1219
|
workflows: workflowEntities,
|
|
1073
1220
|
emailTemplates: Object.keys(emailTemplateEntities).length > 0 ? emailTemplateEntities : undefined,
|
|
@@ -1075,6 +1222,9 @@ Directory Structure:
|
|
|
1075
1222
|
databaseTypes: Object.keys(databaseTypeEntities).length > 0 ? databaseTypeEntities : undefined,
|
|
1076
1223
|
ruleSets: Object.keys(ruleSetEntities).length > 0 ? ruleSetEntities : undefined,
|
|
1077
1224
|
groupTypeConfigs: Object.keys(groupTypeConfigEntities).length > 0 ? groupTypeConfigEntities : undefined,
|
|
1225
|
+
collectionTypeConfigs: Object.keys(collectionTypeConfigEntities).length > 0
|
|
1226
|
+
? collectionTypeConfigEntities
|
|
1227
|
+
: undefined,
|
|
1078
1228
|
},
|
|
1079
1229
|
};
|
|
1080
1230
|
saveSyncState(configDir, state);
|
|
@@ -1082,6 +1232,8 @@ Directory Structure:
|
|
|
1082
1232
|
success(`Pulled configuration to ${configDir}`);
|
|
1083
1233
|
keyValue("Integrations", integrations.length);
|
|
1084
1234
|
keyValue("Webhooks", webhooks.length);
|
|
1235
|
+
keyValue("Cron Triggers", cronTriggerItems.length);
|
|
1236
|
+
keyValue("Blob Buckets", blobBucketItems.length);
|
|
1085
1237
|
keyValue("Prompts", prompts.length);
|
|
1086
1238
|
keyValue("Workflows", workflows.length);
|
|
1087
1239
|
keyValue("Email Templates", emailTemplates.length);
|
|
@@ -1089,6 +1241,7 @@ Directory Structure:
|
|
|
1089
1241
|
keyValue("Database Types", databaseTypesWithOps.length);
|
|
1090
1242
|
keyValue("Rule Sets", ruleSets.length);
|
|
1091
1243
|
keyValue("Group Type Configs", groupTypeConfigs.length);
|
|
1244
|
+
keyValue("Collection Type Configs", collectionTypeConfigs.length);
|
|
1092
1245
|
}
|
|
1093
1246
|
catch (err) {
|
|
1094
1247
|
error(err.message);
|
|
@@ -1425,6 +1578,152 @@ Directory Structure:
|
|
|
1425
1578
|
}
|
|
1426
1579
|
}
|
|
1427
1580
|
}
|
|
1581
|
+
// Process cron triggers
|
|
1582
|
+
const cronTriggersDir = join(configDir, "cron-triggers");
|
|
1583
|
+
if (existsSync(cronTriggersDir)) {
|
|
1584
|
+
const files = readdirSync(cronTriggersDir).filter((f) => f.endsWith(".toml"));
|
|
1585
|
+
for (const file of files) {
|
|
1586
|
+
const filePath = join(cronTriggersDir, file);
|
|
1587
|
+
const tomlData = parseTomlFile(filePath);
|
|
1588
|
+
const cronTrigger = tomlData.cronTrigger || {};
|
|
1589
|
+
const key = cronTrigger.key || basename(file, ".toml");
|
|
1590
|
+
const existingId = syncState?.entities?.cronTriggers?.[key]?.id;
|
|
1591
|
+
if (!options.force && existingId &&
|
|
1592
|
+
!shouldPushFile(filePath, syncState?.entities?.cronTriggers?.[key]?.contentHash)) {
|
|
1593
|
+
skippedCount++;
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const payload = {
|
|
1597
|
+
triggerKey: key,
|
|
1598
|
+
displayName: cronTrigger.displayName || key,
|
|
1599
|
+
description: cronTrigger.description,
|
|
1600
|
+
cron: cronTrigger.cron,
|
|
1601
|
+
timezone: cronTrigger.timezone,
|
|
1602
|
+
workflowKey: cronTrigger.workflowKey,
|
|
1603
|
+
overlapPolicy: cronTrigger.overlapPolicy,
|
|
1604
|
+
state: cronTrigger.state,
|
|
1605
|
+
};
|
|
1606
|
+
// Handle JSON fields
|
|
1607
|
+
if (tomlData.rootInput) {
|
|
1608
|
+
payload.rootInput = JSON.stringify(tomlData.rootInput);
|
|
1609
|
+
}
|
|
1610
|
+
if (tomlData.inputMapping) {
|
|
1611
|
+
payload.inputMapping = JSON.stringify(tomlData.inputMapping);
|
|
1612
|
+
}
|
|
1613
|
+
if (existingId) {
|
|
1614
|
+
changes.push({ type: "cron-trigger", action: "update", key });
|
|
1615
|
+
if (!options.dryRun) {
|
|
1616
|
+
try {
|
|
1617
|
+
const updated = await client.updateCronTrigger(resolvedAppId, existingId, payload);
|
|
1618
|
+
info(` Updated cron trigger: ${key}`);
|
|
1619
|
+
if (syncState?.entities?.cronTriggers?.[key] && updated?.modifiedAt) {
|
|
1620
|
+
syncState.entities.cronTriggers[key].modifiedAt = updated.modifiedAt;
|
|
1621
|
+
syncState.entities.cronTriggers[key].contentHash = computeFileHash(filePath);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
catch (err) {
|
|
1625
|
+
throw err;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
else {
|
|
1630
|
+
changes.push({ type: "cron-trigger", action: "create", key });
|
|
1631
|
+
if (!options.dryRun) {
|
|
1632
|
+
const created = await client.createCronTrigger(resolvedAppId, payload);
|
|
1633
|
+
info(` Created cron trigger: ${key}`);
|
|
1634
|
+
if (syncState && created?.triggerId && created?.modifiedAt) {
|
|
1635
|
+
if (!syncState.entities.cronTriggers) {
|
|
1636
|
+
syncState.entities.cronTriggers = {};
|
|
1637
|
+
}
|
|
1638
|
+
syncState.entities.cronTriggers[key] = {
|
|
1639
|
+
id: created.triggerId,
|
|
1640
|
+
modifiedAt: created.modifiedAt,
|
|
1641
|
+
contentHash: computeFileHash(filePath),
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
// Process blob buckets
|
|
1649
|
+
const blobBucketsPushDir = join(configDir, "blob-buckets");
|
|
1650
|
+
if (existsSync(blobBucketsPushDir)) {
|
|
1651
|
+
const files = readdirSync(blobBucketsPushDir).filter((f) => f.endsWith(".toml"));
|
|
1652
|
+
for (const file of files) {
|
|
1653
|
+
const filePath = join(blobBucketsPushDir, file);
|
|
1654
|
+
const tomlData = parseTomlFile(filePath);
|
|
1655
|
+
const bucket = tomlData.bucket || {};
|
|
1656
|
+
const key = bucket.key || basename(file, ".toml");
|
|
1657
|
+
const existingId = syncState?.entities?.blobBuckets?.[key]?.id;
|
|
1658
|
+
if (!options.force && existingId &&
|
|
1659
|
+
!shouldPushFile(filePath, syncState?.entities?.blobBuckets?.[key]?.contentHash)) {
|
|
1660
|
+
skippedCount++;
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
if (existingId) {
|
|
1664
|
+
// Blob buckets don't have an update API - skip if already exists
|
|
1665
|
+
info(` Blob bucket already exists, skipping: ${key}`);
|
|
1666
|
+
if (syncState?.entities?.blobBuckets?.[key]) {
|
|
1667
|
+
syncState.entities.blobBuckets[key].contentHash = computeFileHash(filePath);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
const payload = {
|
|
1672
|
+
bucketKey: key,
|
|
1673
|
+
name: bucket.name || key,
|
|
1674
|
+
ttlTier: bucket.ttlTier,
|
|
1675
|
+
accessPolicy: bucket.accessPolicy,
|
|
1676
|
+
};
|
|
1677
|
+
if (bucket.description)
|
|
1678
|
+
payload.description = bucket.description;
|
|
1679
|
+
if (bucket.ruleSetId)
|
|
1680
|
+
payload.ruleSetId = bucket.ruleSetId;
|
|
1681
|
+
changes.push({ type: "blob-bucket", action: "create", key });
|
|
1682
|
+
if (!options.dryRun) {
|
|
1683
|
+
try {
|
|
1684
|
+
const created = await client.createBlobBucket(resolvedAppId, payload);
|
|
1685
|
+
info(` Created blob bucket: ${key}`);
|
|
1686
|
+
if (syncState) {
|
|
1687
|
+
if (!syncState.entities.blobBuckets) {
|
|
1688
|
+
syncState.entities.blobBuckets = {};
|
|
1689
|
+
}
|
|
1690
|
+
syncState.entities.blobBuckets[key] = {
|
|
1691
|
+
id: created.bucketId,
|
|
1692
|
+
modifiedAt: created.modifiedAt || new Date().toISOString(),
|
|
1693
|
+
contentHash: computeFileHash(filePath),
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
catch (err) {
|
|
1698
|
+
const msg = String(err?.message || err);
|
|
1699
|
+
if (msg.includes("already exists") || err.statusCode === 409) {
|
|
1700
|
+
info(` Blob bucket already exists on server: ${key}`);
|
|
1701
|
+
// Fetch the existing bucket to get its ID
|
|
1702
|
+
try {
|
|
1703
|
+
const existing = await client.getBlobBucket(resolvedAppId, key);
|
|
1704
|
+
if (syncState && existing?.bucketId) {
|
|
1705
|
+
if (!syncState.entities.blobBuckets) {
|
|
1706
|
+
syncState.entities.blobBuckets = {};
|
|
1707
|
+
}
|
|
1708
|
+
syncState.entities.blobBuckets[key] = {
|
|
1709
|
+
id: existing.bucketId,
|
|
1710
|
+
modifiedAt: existing.modifiedAt || new Date().toISOString(),
|
|
1711
|
+
contentHash: computeFileHash(filePath),
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
catch {
|
|
1716
|
+
// Ignore fetch errors
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
else {
|
|
1720
|
+
throw err;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1428
1727
|
// Process prompts
|
|
1429
1728
|
const promptsDir = join(configDir, "prompts");
|
|
1430
1729
|
if (existsSync(promptsDir)) {
|
|
@@ -1932,12 +2231,6 @@ Directory Structure:
|
|
|
1932
2231
|
}
|
|
1933
2232
|
}
|
|
1934
2233
|
}
|
|
1935
|
-
// Refresh the cached content hash so an unchanged file isn't re-pushed
|
|
1936
|
-
// next run. This matters when only operations changed (the type-level
|
|
1937
|
-
// PATCH was skipped) or when nothing type-level needed updating at all.
|
|
1938
|
-
if (!options.dryRun && syncState?.entities?.databaseTypes?.[dbType]) {
|
|
1939
|
-
syncState.entities.databaseTypes[dbType].contentHash = computeFileHash(filePath);
|
|
1940
|
-
}
|
|
1941
2234
|
}
|
|
1942
2235
|
}
|
|
1943
2236
|
// Process group type configs
|
|
@@ -2013,6 +2306,77 @@ Directory Structure:
|
|
|
2013
2306
|
}
|
|
2014
2307
|
}
|
|
2015
2308
|
}
|
|
2309
|
+
// Process collection type configs
|
|
2310
|
+
const collectionTypeConfigsDir = join(configDir, "collection-type-configs");
|
|
2311
|
+
if (existsSync(collectionTypeConfigsDir)) {
|
|
2312
|
+
const files = readdirSync(collectionTypeConfigsDir).filter((f) => f.endsWith(".toml"));
|
|
2313
|
+
for (const file of files) {
|
|
2314
|
+
const filePath = join(collectionTypeConfigsDir, file);
|
|
2315
|
+
const tomlData = parseTomlFile(filePath);
|
|
2316
|
+
const configData = parseCollectionTypeConfigToml(tomlData);
|
|
2317
|
+
const collectionType = configData.collectionType || basename(file, ".toml");
|
|
2318
|
+
// Resolve ruleSetName → ruleSetId if using key-based reference
|
|
2319
|
+
resolveRuleSetReference(configData, ruleSetNameToId, `collection type ${collectionType}`);
|
|
2320
|
+
const existingEntry = syncState?.entities?.collectionTypeConfigs?.[collectionType];
|
|
2321
|
+
// Skip if file hasn't changed since last sync
|
|
2322
|
+
if (!options.force && existingEntry && !shouldPushFile(filePath, existingEntry.contentHash)) {
|
|
2323
|
+
skippedCount++;
|
|
2324
|
+
continue;
|
|
2325
|
+
}
|
|
2326
|
+
if (existingEntry) {
|
|
2327
|
+
// Update existing collection type config
|
|
2328
|
+
changes.push({ type: "collection-type-config", action: "update", key: collectionType });
|
|
2329
|
+
if (!options.dryRun) {
|
|
2330
|
+
const expectedModifiedAt = options.force
|
|
2331
|
+
? undefined
|
|
2332
|
+
: existingEntry.modifiedAt;
|
|
2333
|
+
try {
|
|
2334
|
+
const updated = await client.updateCollectionTypeConfig(resolvedAppId, collectionType, {
|
|
2335
|
+
ruleSetId: configData.ruleSetId ?? null,
|
|
2336
|
+
}, expectedModifiedAt);
|
|
2337
|
+
info(` Updated collection type config: ${collectionType}`);
|
|
2338
|
+
if (syncState?.entities?.collectionTypeConfigs?.[collectionType] && updated?.modifiedAt) {
|
|
2339
|
+
syncState.entities.collectionTypeConfigs[collectionType].modifiedAt = updated.modifiedAt;
|
|
2340
|
+
syncState.entities.collectionTypeConfigs[collectionType].contentHash = computeFileHash(filePath);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
catch (err) {
|
|
2344
|
+
if (err instanceof ConflictError) {
|
|
2345
|
+
conflicts.push({
|
|
2346
|
+
type: "collection-type-config",
|
|
2347
|
+
key: collectionType,
|
|
2348
|
+
serverModifiedAt: err.serverModifiedAt,
|
|
2349
|
+
localModifiedAt: expectedModifiedAt || "unknown",
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
else {
|
|
2353
|
+
throw err;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
else {
|
|
2359
|
+
// Create new collection type config
|
|
2360
|
+
changes.push({ type: "collection-type-config", action: "create", key: collectionType });
|
|
2361
|
+
if (!options.dryRun) {
|
|
2362
|
+
const created = await client.createCollectionTypeConfig(resolvedAppId, {
|
|
2363
|
+
collectionType,
|
|
2364
|
+
ruleSetId: configData.ruleSetId || undefined,
|
|
2365
|
+
});
|
|
2366
|
+
info(` Created collection type config: ${collectionType}`);
|
|
2367
|
+
if (syncState) {
|
|
2368
|
+
if (!syncState.entities.collectionTypeConfigs) {
|
|
2369
|
+
syncState.entities.collectionTypeConfigs = {};
|
|
2370
|
+
}
|
|
2371
|
+
syncState.entities.collectionTypeConfigs[collectionType] = {
|
|
2372
|
+
modifiedAt: created?.modifiedAt || new Date().toISOString(),
|
|
2373
|
+
contentHash: computeFileHash(filePath),
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2016
2380
|
// Process email templates
|
|
2017
2381
|
const emailTemplatesDir = join(configDir, "email-templates");
|
|
2018
2382
|
if (existsSync(emailTemplatesDir)) {
|
|
@@ -2171,8 +2535,19 @@ Directory Structure:
|
|
|
2171
2535
|
client.listEmailTemplates(resolvedAppId).catch(() => ({ templates: [] })),
|
|
2172
2536
|
]);
|
|
2173
2537
|
const webhookItems = await fetchAll((p) => client.listWebhooks(resolvedAppId, p));
|
|
2538
|
+
let cronTriggerItemsDiff = [];
|
|
2539
|
+
try {
|
|
2540
|
+
const cronResult = await client.listCronTriggers(resolvedAppId);
|
|
2541
|
+
cronTriggerItemsDiff = cronResult.items || [];
|
|
2542
|
+
}
|
|
2543
|
+
catch {
|
|
2544
|
+
// Cron triggers may not be available on older servers
|
|
2545
|
+
}
|
|
2546
|
+
const blobBucketsDiffResult = await client.listBlobBuckets(resolvedAppId).catch(() => ({ items: [] }));
|
|
2174
2547
|
const remoteIntegrations = new Set(integrationItems.map((i) => i.integrationKey));
|
|
2175
2548
|
const remoteWebhooks = new Set(webhookItems.map((w) => w.webhookKey));
|
|
2549
|
+
const remoteCronTriggers = new Set(cronTriggerItemsDiff.map((t) => t.triggerKey));
|
|
2550
|
+
const remoteBlobBuckets = new Set((blobBucketsDiffResult.items || []).map((b) => b.bucketKey));
|
|
2176
2551
|
const remotePrompts = new Set(promptItems.map((p) => p.promptKey));
|
|
2177
2552
|
const remoteWorkflows = new Set(workflowItems.map((w) => w.workflowKey));
|
|
2178
2553
|
const remoteEmailTemplates = new Set((emailTemplatesResult.templates || [])
|
|
@@ -2181,6 +2556,8 @@ Directory Structure:
|
|
|
2181
2556
|
// Get local files
|
|
2182
2557
|
const localIntegrations = new Set();
|
|
2183
2558
|
const localWebhooks = new Set();
|
|
2559
|
+
const localCronTriggers = new Set();
|
|
2560
|
+
const localBlobBuckets = new Set();
|
|
2184
2561
|
const localPrompts = new Set();
|
|
2185
2562
|
const localWorkflows = new Set();
|
|
2186
2563
|
const localEmailTemplates = new Set();
|
|
@@ -2200,6 +2577,22 @@ Directory Structure:
|
|
|
2200
2577
|
localWebhooks.add(key);
|
|
2201
2578
|
}
|
|
2202
2579
|
}
|
|
2580
|
+
const cronTriggersDirPath = join(configDir, "cron-triggers");
|
|
2581
|
+
if (existsSync(cronTriggersDirPath)) {
|
|
2582
|
+
for (const file of readdirSync(cronTriggersDirPath).filter((f) => f.endsWith(".toml"))) {
|
|
2583
|
+
const tomlData = parseTomlFile(join(cronTriggersDirPath, file));
|
|
2584
|
+
const key = tomlData.cronTrigger?.key || basename(file, ".toml");
|
|
2585
|
+
localCronTriggers.add(key);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
const blobBucketsDiffDir = join(configDir, "blob-buckets");
|
|
2589
|
+
if (existsSync(blobBucketsDiffDir)) {
|
|
2590
|
+
for (const file of readdirSync(blobBucketsDiffDir).filter((f) => f.endsWith(".toml"))) {
|
|
2591
|
+
const tomlData = parseTomlFile(join(blobBucketsDiffDir, file));
|
|
2592
|
+
const key = tomlData.bucket?.key || basename(file, ".toml");
|
|
2593
|
+
localBlobBuckets.add(key);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2203
2596
|
const promptsDir = join(configDir, "prompts");
|
|
2204
2597
|
if (existsSync(promptsDir)) {
|
|
2205
2598
|
for (const file of readdirSync(promptsDir).filter((f) => f.endsWith(".toml"))) {
|
|
@@ -2254,6 +2647,34 @@ Directory Structure:
|
|
|
2254
2647
|
differences.push({ type: "webhook", key, status: "remote only" });
|
|
2255
2648
|
}
|
|
2256
2649
|
}
|
|
2650
|
+
// Cron Triggers
|
|
2651
|
+
for (const key of localCronTriggers) {
|
|
2652
|
+
if (!remoteCronTriggers.has(key)) {
|
|
2653
|
+
differences.push({ type: "cron-trigger", key, status: "local only" });
|
|
2654
|
+
}
|
|
2655
|
+
else {
|
|
2656
|
+
differences.push({ type: "cron-trigger", key, status: "exists" });
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
for (const key of remoteCronTriggers) {
|
|
2660
|
+
if (!localCronTriggers.has(key)) {
|
|
2661
|
+
differences.push({ type: "cron-trigger", key, status: "remote only" });
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
// Blob Buckets
|
|
2665
|
+
for (const key of localBlobBuckets) {
|
|
2666
|
+
if (!remoteBlobBuckets.has(key)) {
|
|
2667
|
+
differences.push({ type: "blob-bucket", key, status: "local only" });
|
|
2668
|
+
}
|
|
2669
|
+
else {
|
|
2670
|
+
differences.push({ type: "blob-bucket", key, status: "exists" });
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
for (const key of remoteBlobBuckets) {
|
|
2674
|
+
if (!localBlobBuckets.has(key)) {
|
|
2675
|
+
differences.push({ type: "blob-bucket", key, status: "remote only" });
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2257
2678
|
// Prompts
|
|
2258
2679
|
for (const key of localPrompts) {
|
|
2259
2680
|
if (!remotePrompts.has(key)) {
|