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.
Files changed (36) hide show
  1. package/assets/skill/skills/primitive-platform/SKILL.md +1 -9
  2. package/dist/bin/primitive.js +132 -16
  3. package/dist/bin/primitive.js.map +1 -1
  4. package/dist/src/commands/admins.js +107 -0
  5. package/dist/src/commands/admins.js.map +1 -1
  6. package/dist/src/commands/blob-buckets.js +354 -0
  7. package/dist/src/commands/blob-buckets.js.map +1 -0
  8. package/dist/src/commands/collections.js +18 -4
  9. package/dist/src/commands/collections.js.map +1 -1
  10. package/dist/src/commands/cron-triggers.js +364 -0
  11. package/dist/src/commands/cron-triggers.js.map +1 -0
  12. package/dist/src/commands/email-templates.js +19 -5
  13. package/dist/src/commands/email-templates.js.map +1 -1
  14. package/dist/src/commands/env.js +260 -0
  15. package/dist/src/commands/env.js.map +1 -0
  16. package/dist/src/commands/init.js +90 -2
  17. package/dist/src/commands/init.js.map +1 -1
  18. package/dist/src/commands/sync.js +428 -7
  19. package/dist/src/commands/sync.js.map +1 -1
  20. package/dist/src/lib/api-client.js +137 -3
  21. package/dist/src/lib/api-client.js.map +1 -1
  22. package/dist/src/lib/config.js +51 -53
  23. package/dist/src/lib/config.js.map +1 -1
  24. package/dist/src/lib/credentials-store.js +307 -0
  25. package/dist/src/lib/credentials-store.js.map +1 -0
  26. package/dist/src/lib/env-resolver.js +121 -0
  27. package/dist/src/lib/env-resolver.js.map +1 -0
  28. package/dist/src/lib/paginate.js +42 -0
  29. package/dist/src/lib/paginate.js.map +1 -0
  30. package/dist/src/lib/project-config.js +209 -0
  31. package/dist/src/lib/project-config.js.map +1 -0
  32. package/dist/src/lib/sync-paths.js +102 -0
  33. package/dist/src/lib/sync-paths.js.map +1 -0
  34. package/dist/src/lib/version-check.js +5 -2
  35. package/dist/src/lib/version-check.js.map +1 -1
  36. 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)) {