primitive-admin 1.2.0-alpha.1 → 1.2.0-alpha.2
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 +3 -2
- package/dist/bin/primitive.js +22 -24
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/agent-sessions.d.ts +20 -0
- package/dist/src/commands/agent-sessions.js +275 -0
- package/dist/src/commands/agent-sessions.js.map +1 -0
- package/dist/src/commands/apps-children.d.ts +54 -0
- package/dist/src/commands/apps-children.js +578 -0
- package/dist/src/commands/apps-children.js.map +1 -0
- package/dist/src/commands/apps.js +88 -9
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth-sessions.d.ts +3 -3
- package/dist/src/commands/auth-sessions.js +117 -23
- package/dist/src/commands/auth-sessions.js.map +1 -1
- package/dist/src/commands/auth.js +248 -15
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/documents.d.ts +29 -0
- package/dist/src/commands/documents.js +676 -191
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/env.d.ts +9 -1
- package/dist/src/commands/env.js +158 -45
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/functions.js +7 -1
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/init.js +2 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +14 -2
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/prompts.js +4 -0
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/scripts.js +1 -1
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/sync-app-settings.d.ts +25 -7
- package/dist/src/commands/sync-app-settings.js +81 -20
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +107 -9
- package/dist/src/commands/sync.js +649 -300
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/users.js +77 -2
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +161 -5
- package/dist/src/lib/api-client.js +210 -49
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +9 -1
- package/dist/src/lib/app-settings-descriptor.js +22 -10
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/auth-flow.d.ts +41 -1
- package/dist/src/lib/auth-flow.js +45 -16
- package/dist/src/lib/auth-flow.js.map +1 -1
- package/dist/src/lib/child-apps-local.d.ts +184 -0
- package/dist/src/lib/child-apps-local.js +282 -0
- package/dist/src/lib/child-apps-local.js.map +1 -0
- package/dist/src/lib/ci-session-request.d.ts +22 -0
- package/dist/src/lib/ci-session-request.js +51 -0
- package/dist/src/lib/ci-session-request.js.map +1 -0
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +4 -1
- package/dist/src/lib/codegen-shared/generatedFiles.js +19 -13
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.js +5 -4
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/config-payload.js +2 -3
- package/dist/src/lib/config-payload.js.map +1 -1
- package/dist/src/lib/config-surface.d.ts +2 -1
- package/dist/src/lib/config-surface.js +7 -28
- package/dist/src/lib/config-surface.js.map +1 -1
- package/dist/src/lib/credentials-store.d.ts +17 -2
- package/dist/src/lib/credentials-store.js +72 -6
- package/dist/src/lib/credentials-store.js.map +1 -1
- package/dist/src/lib/document-export-lookups.d.ts +31 -0
- package/dist/src/lib/document-export-lookups.js +53 -0
- package/dist/src/lib/document-export-lookups.js.map +1 -0
- package/dist/src/lib/document-export-permissions.d.ts +7 -5
- package/dist/src/lib/document-export-permissions.js +7 -5
- package/dist/src/lib/document-export-permissions.js.map +1 -1
- package/dist/src/lib/env-resolver-core.d.ts +69 -1
- package/dist/src/lib/env-resolver-core.js +137 -20
- package/dist/src/lib/env-resolver-core.js.map +1 -1
- package/dist/src/lib/env-resolver.d.ts +14 -2
- package/dist/src/lib/env-resolver.js +32 -10
- package/dist/src/lib/env-resolver.js.map +1 -1
- package/dist/src/lib/function-run.d.ts +4 -1
- package/dist/src/lib/function-run.js +24 -4
- package/dist/src/lib/function-run.js.map +1 -1
- package/dist/src/lib/function-sync.d.ts +5 -2
- package/dist/src/lib/function-sync.js +7 -4
- package/dist/src/lib/function-sync.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +49 -52
- package/dist/src/lib/generated-config-surfaces.js +211 -140
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/dist/src/lib/init-adopt.js +12 -3
- package/dist/src/lib/init-adopt.js.map +1 -1
- package/dist/src/lib/local-state.d.ts +17 -1
- package/dist/src/lib/local-state.js +94 -15
- package/dist/src/lib/local-state.js.map +1 -1
- package/dist/src/lib/local-test-cases.d.ts +5 -2
- package/dist/src/lib/local-test-cases.js +7 -4
- package/dist/src/lib/local-test-cases.js.map +1 -1
- package/dist/src/lib/log-inspection.d.ts +25 -0
- package/dist/src/lib/log-inspection.js +24 -0
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/logout-admin-session.d.ts +2 -1
- package/dist/src/lib/logout-admin-session.js +7 -1
- package/dist/src/lib/logout-admin-session.js.map +1 -1
- package/dist/src/lib/pull-write.d.ts +31 -0
- package/dist/src/lib/pull-write.js +49 -0
- package/dist/src/lib/pull-write.js.map +1 -0
- package/dist/src/lib/root-import-decision.d.ts +59 -0
- package/dist/src/lib/root-import-decision.js +95 -0
- package/dist/src/lib/root-import-decision.js.map +1 -0
- package/dist/src/lib/scope-request.d.ts +69 -0
- package/dist/src/lib/scope-request.js +188 -0
- package/dist/src/lib/scope-request.js.map +1 -0
- package/dist/src/lib/session-id.d.ts +9 -0
- package/dist/src/lib/session-id.js +28 -0
- package/dist/src/lib/session-id.js.map +1 -0
- package/dist/src/lib/snapshots.d.ts +17 -1
- package/dist/src/lib/snapshots.js +50 -2
- package/dist/src/lib/snapshots.js.map +1 -1
- package/dist/src/lib/sync-paths.d.ts +35 -0
- package/dist/src/lib/sync-paths.js +57 -5
- package/dist/src/lib/sync-paths.js.map +1 -1
- package/dist/src/lib/transient-retry.d.ts +34 -0
- package/dist/src/lib/transient-retry.js +57 -0
- package/dist/src/lib/transient-retry.js.map +1 -0
- package/dist/src/types/index.d.ts +21 -4
- package/package.json +4 -3
|
@@ -13,7 +13,8 @@ import { parseMetadataCategoryToml, validateMetadataCategoryConfig, serializeMet
|
|
|
13
13
|
import { validateOperations, formatIssue, } from "../lib/toml-params-validator.js";
|
|
14
14
|
import { isPlatformOwnedWorkflow, isPlatformOwnedWorkflowKey, } from "../lib/platform-owned.js";
|
|
15
15
|
import { getCurrentAppId, getServerUrl } from "../lib/config.js";
|
|
16
|
-
import {
|
|
16
|
+
import { requireCurrentEnvironment } from "../lib/env-resolver.js";
|
|
17
|
+
import { resolveSyncDir, resolveSnapshotsRoot, resolveSyncStateReadPath, resolveSyncStateWritePath, SYNC_STATE_FILENAME, checkLegacyDatabaseTypesDir, } from "../lib/sync-paths.js";
|
|
17
18
|
import { createSnapshot, listSnapshots, resolveSnapshot, restoreSnapshot, pruneSnapshots, } from "../lib/snapshots.js";
|
|
18
19
|
import { validateWorkflowToml, collectWorkflowTomlNotes, formatWorkflowTomlErrors, validateWorkflowExpressionsToml, validateWorkflowExpressionsRawToml, } from "../lib/workflow-toml-validator.js";
|
|
19
20
|
import { expandWorkflowTomlData } from "../lib/workflow-fragments.js";
|
|
@@ -22,6 +23,7 @@ import { canonicalizeIntegrationRequestConfig } from "../lib/integration-request
|
|
|
22
23
|
import { canonicalizeServerTextFields } from "../lib/server-text-normalization.js";
|
|
23
24
|
import { canonicalJsonStringify } from "../lib/canonical-json.js";
|
|
24
25
|
import { reconcileWorkflowIncludes } from "../lib/workflow-include-preserve.js";
|
|
26
|
+
import { newPullWriteTally, writePulledFile, } from "../lib/pull-write.js";
|
|
25
27
|
import { serializeSection, parseTomlToAppSettings, buildAppSettingsPayload, } from "../lib/app-settings-descriptor.js";
|
|
26
28
|
import { collectAppSettingsPushErrors, writePulledAppSettings, diffAppSettings, appSettingsFieldDiffs, appSettingsNameExcluded, appSettingsNameExcludedForState, hashAppSettingsForDiff, } from "./sync-app-settings.js";
|
|
27
29
|
import { applyWorkflowBody, legacyDraftSlotWarning } from "../lib/workflow-apply.js";
|
|
@@ -33,12 +35,12 @@ import { buildWorkflowPayloadFromToml, parseWorkflowExpressionsToml, } from "../
|
|
|
33
35
|
// payload builder so the diff normalizer and the wire cannot disagree.
|
|
34
36
|
import { WORKFLOW_MODEL_DEFAULTS } from "../lib/workflow-defaults.js";
|
|
35
37
|
// #2644 — the one definition per configuration object, vendored from the server.
|
|
36
|
-
import { BLOB_BUCKET_TABLE, COLLECTION_TYPE_CONFIG_TABLE, CRON_TRIGGER_TABLE, DATABASE_TYPE_TABLE, EMAIL_TEMPLATE_TABLE, GROUP_TYPE_CONFIG_TABLE, INTEGRATION_TABLE, METADATA_CATEGORY_CONFIG_TABLE, PROMPT_CONFIG_TABLE, PROMPT_TABLE, RULE_SET_TABLE, SERVER_FUNCTION_TABLE, TEST_CASE_SURFACE, TEST_CASE_TABLE, WEBHOOK_TABLE, WORKFLOW_TABLE, configDeclaredTypeErrors, fieldServerDefault, findMisshapenTables, findUnknownDocumentKeys, findUnknownGroupedTomlKeys, findUnknownServerKeys, findUnknownTomlKeys, misshapenTableMessage,
|
|
38
|
+
import { BLOB_BUCKET_TABLE, COLLECTION_TYPE_CONFIG_TABLE, CRON_TRIGGER_TABLE, DATABASE_TYPE_TABLE, EMAIL_TEMPLATE_TABLE, GROUP_TYPE_CONFIG_TABLE, INTEGRATION_TABLE, METADATA_CATEGORY_CONFIG_TABLE, PROMPT_CONFIG_TABLE, PROMPT_TABLE, RULE_SET_TABLE, SERVER_FUNCTION_TABLE, TEST_CASE_SURFACE, TEST_CASE_TABLE, WEBHOOK_TABLE, WORKFLOW_TABLE, configDeclaredTypeErrors, fieldServerDefault, findMisshapenTables, findUnknownDocumentKeys, findUnknownGroupedTomlKeys, findUnknownServerKeys, findUnknownTomlKeys, misshapenTableMessage, retiredTomlKey, spellsDecimalNumber, unrecognizedPullKeysMessage, unrecognizedPushKeysMessage, unrecognizedPushTablesMessage, warnUnrecognizedServerKeys, } from "../lib/config-surface.js";
|
|
37
39
|
import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, tomlOwned, } from "../lib/config-payload.js";
|
|
38
40
|
import { CONFIG_SURFACES, isEmptyForEmit, tomlGroupsOf, } from "../lib/generated-config-surfaces.js";
|
|
39
41
|
// #3626 — the prompt KIND rule, vendored with the definitions so the CLI's
|
|
40
42
|
// preflight refuses exactly what the admin routes refuse.
|
|
41
|
-
import {
|
|
43
|
+
import { promptKindOf, resolvePromptKind, } from "../lib/generated-config-surfaces.js";
|
|
42
44
|
// #3798 — the `[prompt.agent]` declaration rule, vendored the same way.
|
|
43
45
|
import { agentDeclarationFunctionKeys, agentTestCaseRefusal, resolveAgentDeclaration, } from "../lib/generated-config-surfaces.js";
|
|
44
46
|
// #3799 — the meaning-preserving schema translation, vendored the same way, so
|
|
@@ -98,8 +100,13 @@ function printStaleInstanceWarning(updated) {
|
|
|
98
100
|
warn(` ${w.message}`);
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
/**
|
|
104
|
+
* The current environment's baseline. Every reader goes through
|
|
105
|
+
* `resolveSyncStateReadPath`, so a machine-local environment reads its own
|
|
106
|
+
* machine-local baseline and never the committed tree's (#4117).
|
|
107
|
+
*/
|
|
108
|
+
function loadSyncState() {
|
|
109
|
+
const syncFile = resolveSyncStateReadPath();
|
|
103
110
|
if (!existsSync(syncFile)) {
|
|
104
111
|
return null;
|
|
105
112
|
}
|
|
@@ -110,8 +117,8 @@ function loadSyncState(configDir) {
|
|
|
110
117
|
return null;
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
|
-
function saveSyncState(
|
|
114
|
-
const syncFile =
|
|
120
|
+
function saveSyncState(state) {
|
|
121
|
+
const syncFile = resolveSyncStateWritePath();
|
|
115
122
|
writeFileSync(syncFile, JSON.stringify(state, null, 2));
|
|
116
123
|
}
|
|
117
124
|
/**
|
|
@@ -449,6 +456,30 @@ export function hashRemoteWorkflowForDiff(workflow, draft, configs, logger, opti
|
|
|
449
456
|
const parsed = parseConfigToml(serialized);
|
|
450
457
|
return hashWorkflowTomlForDiff(parsed, undefined, options);
|
|
451
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Whether a workflow file already describes the workflow `remoteToml`
|
|
461
|
+
* serializes (#4160), by `config diff`'s comparison: the normalized hash, with
|
|
462
|
+
* activation compared only when the file names a configuration.
|
|
463
|
+
*
|
|
464
|
+
* Three shapes never match, because pull has work to do on them: a file with
|
|
465
|
+
* `[[include]]` fragments (`reconcileWorkflowIncludes` owns those), one still
|
|
466
|
+
* carrying the retired inline `[[configs]]` list, and any other file push
|
|
467
|
+
* refuses (`pullMustRewrite`).
|
|
468
|
+
*/
|
|
469
|
+
export function sameWorkflowAs(workflowKey, remoteToml) {
|
|
470
|
+
return (localContent) => {
|
|
471
|
+
const local = parseConfigToml(localContent);
|
|
472
|
+
if ("include" in local || "configs" in local)
|
|
473
|
+
return false;
|
|
474
|
+
if (pullMustRewrite("workflow", local))
|
|
475
|
+
return false;
|
|
476
|
+
const compareActivation = Boolean(authoredActiveConfigName(local));
|
|
477
|
+
return (hashWorkflowTomlForDiff(local, workflowKey, { compareActivation }) ===
|
|
478
|
+
hashWorkflowTomlForDiff(parseConfigToml(remoteToml), undefined, {
|
|
479
|
+
compareActivation,
|
|
480
|
+
}));
|
|
481
|
+
};
|
|
482
|
+
}
|
|
452
483
|
/**
|
|
453
484
|
* Build the [auth] block of app.toml from server settings (pull direction).
|
|
454
485
|
*
|
|
@@ -1600,20 +1631,32 @@ export function buildCronTriggerPushPayload(key, tomlData, { mode = "update" } =
|
|
|
1600
1631
|
}
|
|
1601
1632
|
return payload;
|
|
1602
1633
|
}
|
|
1603
|
-
export function serializeBlobBucket(bucket, logger) {
|
|
1634
|
+
export function serializeBlobBucket(bucket, ruleSetIdToName = new Map(), logger) {
|
|
1635
|
+
const file = `blob-buckets/${bucket?.bucketKey}.toml`;
|
|
1604
1636
|
// #2644 — the `[bucket]` key set comes from the vendored definition.
|
|
1605
|
-
warnUnrecognizedServerKeys(bucket, BLOB_BUCKET_TABLE,
|
|
1637
|
+
warnUnrecognizedServerKeys(bucket, BLOB_BUCKET_TABLE, file, logger);
|
|
1638
|
+
// #4116 — the rule set is written by NAME, which every app can resolve. An
|
|
1639
|
+
// id the listing cannot name is reported and left out, never written: an id
|
|
1640
|
+
// in the file is the spelling a copied tree cannot resolve (the #3998 rule).
|
|
1641
|
+
const ruleSetName = bucket?.ruleSetId
|
|
1642
|
+
? ruleSetIdToName.get(bucket.ruleSetId)
|
|
1643
|
+
: undefined;
|
|
1644
|
+
if (bucket?.ruleSetId && !ruleSetName) {
|
|
1645
|
+
logger?.(`${file}: rule set ${bucket.ruleSetId} is not in this app's rule-set listing; ` +
|
|
1646
|
+
"no ruleSetName written (an orphaned or unlisted rule set denies access closed)");
|
|
1647
|
+
}
|
|
1606
1648
|
const data = {
|
|
1607
1649
|
bucket: {
|
|
1608
1650
|
key: bucket.bucketKey,
|
|
1609
1651
|
...projectRecordToToml(BLOB_BUCKET_TABLE, bucket, {
|
|
1610
|
-
// #1020: `preset` is the honest key; a custom bucket carries
|
|
1611
|
-
// instead. Omit preset for custom buckets (preset === "custom").
|
|
1652
|
+
// #1020: `preset` is the honest key; a custom bucket carries its rule
|
|
1653
|
+
// set instead. Omit preset for custom buckets (preset === "custom").
|
|
1612
1654
|
preset: (record) => record.preset && record.preset !== "custom"
|
|
1613
1655
|
? record.preset
|
|
1614
1656
|
: record.ruleSetId
|
|
1615
1657
|
? undefined
|
|
1616
1658
|
: record.preset,
|
|
1659
|
+
ruleSetId: () => ruleSetName,
|
|
1617
1660
|
}),
|
|
1618
1661
|
},
|
|
1619
1662
|
};
|
|
@@ -1624,6 +1667,73 @@ export function serializeBlobBucket(bucket, logger) {
|
|
|
1624
1667
|
});
|
|
1625
1668
|
return stringifyConfigToml(data);
|
|
1626
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* The entity a `blob-buckets/<key>.toml` describes (#4116) — the parse
|
|
1672
|
+
* `config push` builds its bodies from and `config diff` compares.
|
|
1673
|
+
*
|
|
1674
|
+
* Read in CREATE mode, so the immutable `ttlTier` stays nameable. The rule set
|
|
1675
|
+
* is left as `_ruleSetName` for `resolveRuleSetReference`; the deprecated
|
|
1676
|
+
* `ruleSetId` spelling is forwarded as the id it already is. A file carrying
|
|
1677
|
+
* both is refused by the preflight (`blobBucketFileErrors`) before this runs.
|
|
1678
|
+
*
|
|
1679
|
+
* An omitted `name` stays omitted: the update leaves the server's name alone,
|
|
1680
|
+
* and only the create body falls back to the key.
|
|
1681
|
+
*/
|
|
1682
|
+
export function parseBlobBucketToml(doc) {
|
|
1683
|
+
const section = doc?.bucket ?? {};
|
|
1684
|
+
const entity = dropUndefined({
|
|
1685
|
+
bucketKey: section.key ?? "",
|
|
1686
|
+
...buildPayloadFromToml(BLOB_BUCKET_TABLE, section, {
|
|
1687
|
+
mode: "create",
|
|
1688
|
+
values: {
|
|
1689
|
+
name: (value) => value || undefined,
|
|
1690
|
+
description: tomlOwned,
|
|
1691
|
+
},
|
|
1692
|
+
}),
|
|
1693
|
+
});
|
|
1694
|
+
// The builder read the `ruleSetName` value onto the `ruleSetId` field.
|
|
1695
|
+
delete entity.ruleSetId;
|
|
1696
|
+
if (typeof section.ruleSetName === "string" && section.ruleSetName.trim() !== "") {
|
|
1697
|
+
// Trimmed, as `applyRuleSetReference` does for the type configs.
|
|
1698
|
+
entity._ruleSetName = section.ruleSetName.trim();
|
|
1699
|
+
}
|
|
1700
|
+
else if (typeof section.ruleSetId === "string" && section.ruleSetId !== "") {
|
|
1701
|
+
entity.ruleSetId = section.ruleSetId;
|
|
1702
|
+
}
|
|
1703
|
+
return entity;
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* The advisory `config push` prints for a bucket file still naming its rule set
|
|
1707
|
+
* by id (#4116). The key is deprecated, not retired: the file pushes.
|
|
1708
|
+
*/
|
|
1709
|
+
export function blobBucketDeprecatedKeyWarning(filePath, tomlData) {
|
|
1710
|
+
const bucket = tomlData?.bucket;
|
|
1711
|
+
if (!bucket || bucket.ruleSetId === undefined)
|
|
1712
|
+
return null;
|
|
1713
|
+
// Beside `ruleSetName` it is a refusal instead (`blobBucketFileErrors`).
|
|
1714
|
+
if (bucket.ruleSetName !== undefined)
|
|
1715
|
+
return null;
|
|
1716
|
+
return (`${filePath}: bucket.ruleSetId is deprecated — a rule-set id belongs to one app, ` +
|
|
1717
|
+
`so a copied tree cannot resolve it; write ruleSetName = "<name>" ` +
|
|
1718
|
+
"(config pull rewrites the file). Retiring the key is a later change.");
|
|
1719
|
+
}
|
|
1720
|
+
/** A bucket file's own refusals, decided from the file alone (#4116). */
|
|
1721
|
+
export function blobBucketFileErrors(filePath, tomlData) {
|
|
1722
|
+
const bucket = tomlData?.bucket;
|
|
1723
|
+
if (bucket && bucket.ruleSetId !== undefined && bucket.ruleSetName !== undefined) {
|
|
1724
|
+
return [`${filePath}: bucket.ruleSetId is set beside bucket.ruleSetName; keep ruleSetName`];
|
|
1725
|
+
}
|
|
1726
|
+
// The deprecated spelling is a TOML-only key, so the declared-type check
|
|
1727
|
+
// reads only `ruleSetName`; the old key keeps the field's type under its
|
|
1728
|
+
// own name, or a mistyped id would be dropped from the body unreported.
|
|
1729
|
+
const field = BLOB_BUCKET_TABLE.fields.find((f) => f.field === "ruleSetId");
|
|
1730
|
+
if (!field)
|
|
1731
|
+
return [];
|
|
1732
|
+
return configDeclaredTypeErrors(filePath, tomlData, {
|
|
1733
|
+
...BLOB_BUCKET_TABLE,
|
|
1734
|
+
fields: [{ ...field, tomlKey: "ruleSetId" }],
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1627
1737
|
/**
|
|
1628
1738
|
* Build the PATCH payload for a blob-bucket update from local TOML.
|
|
1629
1739
|
*
|
|
@@ -1639,7 +1749,9 @@ export function serializeBlobBucket(bucket, logger) {
|
|
|
1639
1749
|
export function buildBlobBucketUpdatePayload(bucket) {
|
|
1640
1750
|
// The access model's truthiness checks are the SERVER's rule and stay
|
|
1641
1751
|
// verbatim; the definition contributes the field list, not the semantics.
|
|
1642
|
-
|
|
1752
|
+
// #4116 — reads the parsed ENTITY (`parseBlobBucketToml`, its rule set
|
|
1753
|
+
// resolved), whose field names are the wire keys.
|
|
1754
|
+
const updatePayload = projectEntityToPayload(BLOB_BUCKET_TABLE, bucket, {
|
|
1643
1755
|
mode: "update",
|
|
1644
1756
|
values: {
|
|
1645
1757
|
// A preset and a rule set are mutually exclusive per request, and a
|
|
@@ -1737,7 +1849,8 @@ export function promptConfigPayload(tomlConfig, options = {}) {
|
|
|
1737
1849
|
: { description: tomlConfig.description ?? null, ...shared };
|
|
1738
1850
|
}
|
|
1739
1851
|
/**
|
|
1740
|
-
* Build the POST body for a blob-bucket create from
|
|
1852
|
+
* Build the POST body for a blob-bucket create from the parsed entity
|
|
1853
|
+
* (`parseBlobBucketToml`, its rule set resolved).
|
|
1741
1854
|
*
|
|
1742
1855
|
* `ttlTier` is here and absent from the update body because the definition says
|
|
1743
1856
|
* so (`writableOn: ["create"]`) — the retention tier is fixed at creation.
|
|
@@ -1745,7 +1858,7 @@ export function promptConfigPayload(tomlConfig, options = {}) {
|
|
|
1745
1858
|
export function buildBlobBucketCreatePayload(key, bucket) {
|
|
1746
1859
|
const payload = {
|
|
1747
1860
|
bucketKey: key,
|
|
1748
|
-
...
|
|
1861
|
+
...projectEntityToPayload(BLOB_BUCKET_TABLE, bucket, {
|
|
1749
1862
|
mode: "create",
|
|
1750
1863
|
values: {
|
|
1751
1864
|
name: (value) => value || key,
|
|
@@ -2059,15 +2172,10 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
|
|
|
2059
2172
|
* differ from the one the server has always received.
|
|
2060
2173
|
*/
|
|
2061
2174
|
const tomlOwned = (value) => (create ? value : (value ?? null));
|
|
2062
|
-
// #3626
|
|
2063
|
-
// spelling is
|
|
2064
|
-
//
|
|
2065
|
-
|
|
2066
|
-
// A key set BOTH ways is refused in preflight
|
|
2067
|
-
// (`promptDeprecatedChatKeyErrors`); the hoist leaves the grouped value
|
|
2068
|
-
// standing so nothing here has to guess.
|
|
2069
|
-
const entry = hoistDeprecatedChatKeys(tomlConfig).entry;
|
|
2070
|
-
return buildPayloadFromToml(PROMPT_CONFIG_TABLE, entry, {
|
|
2175
|
+
// #3626 — the chat keys are read from `[configs.chat]` only. Their flat
|
|
2176
|
+
// spelling is retired (#3642) and refused in preflight, so a root chat key
|
|
2177
|
+
// never reaches this builder as a value.
|
|
2178
|
+
return buildPayloadFromToml(PROMPT_CONFIG_TABLE, tomlConfig, {
|
|
2071
2179
|
mode,
|
|
2072
2180
|
values: {
|
|
2073
2181
|
provider: (value) => (create ? value || "openrouter" : value),
|
|
@@ -2193,16 +2301,19 @@ export function configUnknownKeyError(filePath, tomlData, table) {
|
|
|
2193
2301
|
// once beside the definitions (`src/config-surface/retired-keys.ts`), not the
|
|
2194
2302
|
// generic "check the spelling, or upgrade the CLI" hint, which is exactly
|
|
2195
2303
|
// backwards for it. The server's create/update handlers read the same table.
|
|
2304
|
+
// #3642 — a spelling retired from TOML only (a flat chat key under
|
|
2305
|
+
// `[[configs]]`) is answered from the same module, before the misplaced-key
|
|
2306
|
+
// line below could send the author to the block without saying why.
|
|
2196
2307
|
const prefix = table.tomlPath.join(".");
|
|
2197
2308
|
const retiredHere = [...unknown]
|
|
2198
|
-
.filter((k) =>
|
|
2309
|
+
.filter((k) => retiredTomlKey(prefix, k))
|
|
2199
2310
|
.sort();
|
|
2200
2311
|
const trulyUnknown = [...unknown]
|
|
2201
|
-
.filter((k) => !
|
|
2312
|
+
.filter((k) => !retiredTomlKey(prefix, k))
|
|
2202
2313
|
.sort();
|
|
2203
2314
|
const messages = [];
|
|
2204
2315
|
for (const key of retiredHere) {
|
|
2205
|
-
messages.push(`${filePath}: ${prefix}.${key} — ${
|
|
2316
|
+
messages.push(`${filePath}: ${prefix}.${key} — ${retiredTomlKey(prefix, key).toml}`);
|
|
2206
2317
|
}
|
|
2207
2318
|
// #3626, SO3626-013 — a key the definition DOES declare, written at the
|
|
2208
2319
|
// table root when it is authored under a group. Before grouping,
|
|
@@ -2300,49 +2411,72 @@ export function unknownKeyPreflightTargets() {
|
|
|
2300
2411
|
*/
|
|
2301
2412
|
function localConfigRowKey(label, tomlData, file) {
|
|
2302
2413
|
const fallback = basename(file, ".toml");
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
return stated("webhook", "key");
|
|
2309
|
-
case "cron-trigger":
|
|
2310
|
-
return stated("cronTrigger", "key");
|
|
2311
|
-
case "blob-bucket":
|
|
2312
|
-
return stated("bucket", "key");
|
|
2313
|
-
case "prompt":
|
|
2314
|
-
return stated("prompt", "key");
|
|
2315
|
-
case "workflow":
|
|
2316
|
-
return stated("workflow", "key");
|
|
2317
|
-
// #3320 — the declared key, like the function preflight collector, the
|
|
2318
|
-
// apply loop and `--only`. A renamed file is one object, so both
|
|
2319
|
-
// collectors' messages land on the one comparison row.
|
|
2320
|
-
case "function":
|
|
2321
|
-
return stated("function", "key");
|
|
2322
|
-
case "email-template":
|
|
2323
|
-
return stated("template", "emailType");
|
|
2324
|
-
case "database-type-config":
|
|
2325
|
-
return stated("type", "databaseType");
|
|
2326
|
-
case "group-type-config":
|
|
2327
|
-
return stated("groupTypeConfig", "groupType");
|
|
2328
|
-
case "collection-type-config":
|
|
2329
|
-
return stated("collectionTypeConfig", "collectionType");
|
|
2330
|
-
case "metadata-category-config": {
|
|
2331
|
-
const section = tomlData?.metadataCategoryConfig || {};
|
|
2332
|
-
if (section.resourceType && section.category) {
|
|
2333
|
-
return `${section.resourceType}#${section.category}`;
|
|
2334
|
-
}
|
|
2335
|
-
// `resourceType.category.toml`, the filename fallback push and diff share.
|
|
2336
|
-
const dot = fallback.indexOf(".");
|
|
2337
|
-
return dot > 0
|
|
2338
|
-
? `${fallback.slice(0, dot)}#${fallback.slice(dot + 1)}`
|
|
2339
|
-
: fallback;
|
|
2414
|
+
const identity = CONFIG_ROW_IDENTITY[label];
|
|
2415
|
+
if (label === "metadata-category-config") {
|
|
2416
|
+
const section = tomlData?.metadataCategoryConfig || {};
|
|
2417
|
+
if (section.resourceType && section.category) {
|
|
2418
|
+
return `${section.resourceType}#${section.category}`;
|
|
2340
2419
|
}
|
|
2341
|
-
//
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2420
|
+
// `resourceType.category.toml`, the filename fallback push and diff share.
|
|
2421
|
+
const dot = fallback.indexOf(".");
|
|
2422
|
+
return dot > 0
|
|
2423
|
+
? `${fallback.slice(0, dot)}#${fallback.slice(dot + 1)}`
|
|
2424
|
+
: fallback;
|
|
2345
2425
|
}
|
|
2426
|
+
// Rule sets are keyed by FILE NAME on both sides — a renamed `ruleSet.name`
|
|
2427
|
+
// still updates the same entity, so the row is the file's.
|
|
2428
|
+
if (!identity)
|
|
2429
|
+
return fallback;
|
|
2430
|
+
const [table, key] = identity[0];
|
|
2431
|
+
return tomlData?.[table]?.[key] || fallback;
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* The `[table].key` fields that name the object a config file describes, per
|
|
2435
|
+
* type — what `localConfigRowKey` reads before falling back to the file name.
|
|
2436
|
+
* A type absent here (rule sets, test cases) is identified by its file name
|
|
2437
|
+
* alone.
|
|
2438
|
+
*/
|
|
2439
|
+
const CONFIG_ROW_IDENTITY = {
|
|
2440
|
+
integration: [["integration", "key"]],
|
|
2441
|
+
webhook: [["webhook", "key"]],
|
|
2442
|
+
"cron-trigger": [["cronTrigger", "key"]],
|
|
2443
|
+
"blob-bucket": [["bucket", "key"]],
|
|
2444
|
+
prompt: [["prompt", "key"]],
|
|
2445
|
+
workflow: [["workflow", "key"]],
|
|
2446
|
+
// #3320 — the declared key, like the function preflight collector, the
|
|
2447
|
+
// apply loop and `--only`. A renamed file is one object, so both
|
|
2448
|
+
// collectors' messages land on the one comparison row.
|
|
2449
|
+
function: [["function", "key"]],
|
|
2450
|
+
"email-template": [["template", "emailType"]],
|
|
2451
|
+
"database-type-config": [["type", "databaseType"]],
|
|
2452
|
+
"group-type-config": [["groupTypeConfig", "groupType"]],
|
|
2453
|
+
"collection-type-config": [["collectionTypeConfig", "collectionType"]],
|
|
2454
|
+
"metadata-category-config": [
|
|
2455
|
+
["metadataCategoryConfig", "resourceType"],
|
|
2456
|
+
["metadataCategoryConfig", "category"],
|
|
2457
|
+
],
|
|
2458
|
+
};
|
|
2459
|
+
/**
|
|
2460
|
+
* The local file as `config diff` compares it against the server object pull
|
|
2461
|
+
* writes to `file` (#4160), or `null` when the file names a different object.
|
|
2462
|
+
*
|
|
2463
|
+
* Diff pairs a file with a server object by the identity the file states, or
|
|
2464
|
+
* its file name when it states none, and injects that identity before
|
|
2465
|
+
* comparing. So a file whose stated key is another object's is not this
|
|
2466
|
+
* object's file whatever its other values say, and a file that leaves its
|
|
2467
|
+
* identity to the file name is compared with the identity filled in.
|
|
2468
|
+
*/
|
|
2469
|
+
function localConfigWithServerIdentity(label, local, server, file) {
|
|
2470
|
+
if (localConfigRowKey(label, local, file) !== localConfigRowKey(label, server, file)) {
|
|
2471
|
+
return null;
|
|
2472
|
+
}
|
|
2473
|
+
const resolved = { ...local };
|
|
2474
|
+
for (const [table, key] of CONFIG_ROW_IDENTITY[label] ?? []) {
|
|
2475
|
+
if (server?.[table]?.[key] === undefined)
|
|
2476
|
+
continue;
|
|
2477
|
+
resolved[table] = { ...(resolved[table] ?? {}), [key]: server[table][key] };
|
|
2478
|
+
}
|
|
2479
|
+
return resolved;
|
|
2346
2480
|
}
|
|
2347
2481
|
/**
|
|
2348
2482
|
* `config push`'s preflight — every check the authored files decide on their
|
|
@@ -2373,7 +2507,7 @@ function localConfigRowKey(label, tomlData, file) {
|
|
|
2373
2507
|
* that being a visible change to this signature.
|
|
2374
2508
|
*/
|
|
2375
2509
|
export async function runConfigPushPreflight(input) {
|
|
2376
|
-
const { configDir, resolvedAppId, pushSelection, syncState, force, isCrossAppPush, client, } = input;
|
|
2510
|
+
const { configDir, resolvedAppId, pushSelection, syncState, force, isCrossAppPush, localEnvironment = false, client, } = input;
|
|
2377
2511
|
/**
|
|
2378
2512
|
* Narrow a type's file list to the `--only` selection — the same one-line
|
|
2379
2513
|
* closure the action holds, rebuilt from the selection it passed in rather
|
|
@@ -2514,6 +2648,24 @@ export async function runConfigPushPreflight(input) {
|
|
|
2514
2648
|
}
|
|
2515
2649
|
}
|
|
2516
2650
|
}
|
|
2651
|
+
// #4116 — the deprecated `ruleSetId` spelling in a bucket file. Advisory
|
|
2652
|
+
// only: the file pushes. Bounded by `--only`, like the notice above.
|
|
2653
|
+
const preflightBucketsDir = join(configDir, "blob-buckets");
|
|
2654
|
+
if (existsSync(preflightBucketsDir)) {
|
|
2655
|
+
for (const file of selectFiles("blob-bucket", preflightBucketsDir, readdirSync(preflightBucketsDir).filter((f) => f.endsWith(".toml")))) {
|
|
2656
|
+
let tomlData;
|
|
2657
|
+
try {
|
|
2658
|
+
tomlData = parseTomlFile(join(preflightBucketsDir, file));
|
|
2659
|
+
}
|
|
2660
|
+
catch {
|
|
2661
|
+
// A file that does not parse is reported by the collector below.
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
const advice = blobBucketDeprecatedKeyWarning(`blob-buckets/${file}`, tomlData);
|
|
2665
|
+
if (advice)
|
|
2666
|
+
warn(` ${advice}`);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2517
2669
|
// Validate app.toml settings up-front (issue #1033). A mistyped value
|
|
2518
2670
|
// is a hard error that must abort BEFORE any mutation — same
|
|
2519
2671
|
// fail-before-mutate contract as the vars preflight below. The apply
|
|
@@ -2530,10 +2682,12 @@ export async function runConfigPushPreflight(input) {
|
|
|
2530
2682
|
// #2880 DSO-001 — the same exclusion the apply block computes, so
|
|
2531
2683
|
// the preflight validates the body that will actually be sent.
|
|
2532
2684
|
excludeName: appSettingsNameExcluded({
|
|
2685
|
+
localEnvironment,
|
|
2533
2686
|
isCrossAppPush: !!isCrossAppPush,
|
|
2534
2687
|
recordedExcludedName: syncState?.entities?.app?.excludedName,
|
|
2535
2688
|
fileName: parsedApp?.app?.name,
|
|
2536
2689
|
}),
|
|
2690
|
+
nameOptional: localEnvironment,
|
|
2537
2691
|
});
|
|
2538
2692
|
for (const e of appErrors) {
|
|
2539
2693
|
preflightValidationErrors.push(` app.toml: ${e}`);
|
|
@@ -2671,11 +2825,9 @@ export async function runConfigPushPreflight(input) {
|
|
|
2671
2825
|
preflightValidationErrors.push(` ${message}`);
|
|
2672
2826
|
}
|
|
2673
2827
|
}
|
|
2674
|
-
// #
|
|
2675
|
-
//
|
|
2676
|
-
//
|
|
2677
|
-
// single pull-then-push clears the notice. Printed even under `--dry-run`,
|
|
2678
|
-
// which never reaches the apply — the notice is about the FILE.
|
|
2828
|
+
// #3798 — the prompt files this push selects, read once more for the check
|
|
2829
|
+
// that needs app state: an agent's functions must exist. Bounded by `--only`,
|
|
2830
|
+
// and run before anything is applied.
|
|
2679
2831
|
const preflightPromptsDir = join(configDir, "prompts");
|
|
2680
2832
|
// #3798 — the functions an agent's references are checked against, read
|
|
2681
2833
|
// once and only when a selected prompt is an agent that names one.
|
|
@@ -2691,9 +2843,6 @@ export async function runConfigPushPreflight(input) {
|
|
|
2691
2843
|
// A file that does not parse is reported by the collector above.
|
|
2692
2844
|
continue;
|
|
2693
2845
|
}
|
|
2694
|
-
const notice = promptFlatChatKeyWarning(filePath, tomlData);
|
|
2695
|
-
if (notice)
|
|
2696
|
-
warn(` ${notice}`);
|
|
2697
2846
|
// #3798 — an agent's tool, turn-context and history functions must
|
|
2698
2847
|
// exist, and a tool's must declare both schemas, before anything is
|
|
2699
2848
|
// applied. Bounded by `--only` like the rest of this loop.
|
|
@@ -2766,22 +2915,16 @@ export async function runConfigPushPreflight(input) {
|
|
|
2766
2915
|
/**
|
|
2767
2916
|
* The blocks one `[[configs]]` entry carries that its prompt's kind does not
|
|
2768
2917
|
* name — #3798. An agent's entry may carry only `[configs.agent]`; a chat or
|
|
2769
|
-
* decisions entry may not carry it.
|
|
2770
|
-
* keys the hoist moved under `chat`, named as the author wrote them.
|
|
2918
|
+
* decisions entry may not carry it.
|
|
2771
2919
|
*
|
|
2772
2920
|
* Only the blocks the other arms of `resolvePromptKind` cannot see are checked
|
|
2773
2921
|
* here: a decisions block on a chat entry, and chat keys on a decisions entry,
|
|
2774
2922
|
* stay the resolver's refusals with their #3626 wording.
|
|
2775
2923
|
*/
|
|
2776
|
-
function promptEntryBlockErrors(kind, entry
|
|
2924
|
+
function promptEntryBlockErrors(kind, entry) {
|
|
2777
2925
|
const has = (block) => entry != null && typeof entry === "object" && entry[block] !== undefined;
|
|
2778
2926
|
const errors = [];
|
|
2779
2927
|
if (kind === "agent") {
|
|
2780
|
-
if (movedFlat.length > 0) {
|
|
2781
|
-
errors.push(`sets ${movedFlat.join(", ")} flat — the deprecated [configs.chat] ` +
|
|
2782
|
-
'spelling — but prompt kind is "agent", so it may carry only a ' +
|
|
2783
|
-
"[configs.agent] block; write them under [configs.agent]");
|
|
2784
|
-
}
|
|
2785
2928
|
for (const block of ["chat", "decisions"]) {
|
|
2786
2929
|
if (!has(block))
|
|
2787
2930
|
continue;
|
|
@@ -2801,10 +2944,9 @@ function promptEntryBlockErrors(kind, entry, movedFlat) {
|
|
|
2801
2944
|
/**
|
|
2802
2945
|
* A prompt file's KIND errors — #3626, decidable from the file alone.
|
|
2803
2946
|
*
|
|
2804
|
-
*
|
|
2805
|
-
*
|
|
2806
|
-
*
|
|
2807
|
-
* decisions declaration. The last two come from `resolvePromptKind`, the same
|
|
2947
|
+
* Two refusals, the same rule seen from different angles: a `[[configs]]`
|
|
2948
|
+
* entry carrying the block of the OTHER kind, and a malformed decisions
|
|
2949
|
+
* declaration. Both come from `resolvePromptKind`, the same
|
|
2808
2950
|
* vendored function the admin routes validate with, so the CLI's refusal and
|
|
2809
2951
|
* the server's cannot disagree — and this one lands in the preflight, before
|
|
2810
2952
|
* the first mutating call.
|
|
@@ -2859,27 +3001,28 @@ export function promptKindFileErrors(filePath, tomlData) {
|
|
|
2859
3001
|
errors.push(`${filePath}: ${refusal.message}`);
|
|
2860
3002
|
}
|
|
2861
3003
|
}
|
|
2862
|
-
for (const [index,
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
3004
|
+
for (const [index, rawEntry] of entries.entries()) {
|
|
3005
|
+
// #3642 — the blocks are read as the file wrote them. A chat key at the
|
|
3006
|
+
// entry's root is the retired flat spelling, which the unknown-key check
|
|
3007
|
+
// refuses on its own; it is not a value any rule here reads.
|
|
3008
|
+
const entry = rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry)
|
|
3009
|
+
? rawEntry
|
|
3010
|
+
: {};
|
|
2868
3011
|
// #3798 — the block rule for three kinds, stated per entry: an entry may
|
|
2869
3012
|
// carry only the block its prompt's kind names. An agent's `[configs.agent]`
|
|
2870
3013
|
// holds keys chat also holds, so a stray block would otherwise be read (or
|
|
2871
3014
|
// ignored) in silence by the payload builder.
|
|
2872
|
-
const blockErrors = promptEntryBlockErrors(fileKind, entry
|
|
3015
|
+
const blockErrors = promptEntryBlockErrors(fileKind, entry).map((message) => `${filePath}: configs[${index}] ${message}`);
|
|
2873
3016
|
if (blockErrors.length > 0) {
|
|
2874
3017
|
errors.push(...blockErrors);
|
|
2875
3018
|
continue;
|
|
2876
3019
|
}
|
|
2877
3020
|
if (fileKind === "agent") {
|
|
2878
|
-
const agent =
|
|
3021
|
+
const agent = entry.agent ?? {};
|
|
2879
3022
|
const resolution = resolvePromptKind({
|
|
2880
3023
|
kind: "agent",
|
|
2881
|
-
provider:
|
|
2882
|
-
model:
|
|
3024
|
+
provider: entry.provider,
|
|
3025
|
+
model: entry.model,
|
|
2883
3026
|
config: {
|
|
2884
3027
|
systemPrompt: agent.systemPrompt,
|
|
2885
3028
|
temperature: agent.temperature,
|
|
@@ -2897,8 +3040,8 @@ export function promptKindFileErrors(filePath, tomlData) {
|
|
|
2897
3040
|
}
|
|
2898
3041
|
continue;
|
|
2899
3042
|
}
|
|
2900
|
-
const chat =
|
|
2901
|
-
const decisions =
|
|
3043
|
+
const chat = entry.chat ?? {};
|
|
3044
|
+
const decisions = entry.decisions ?? {};
|
|
2902
3045
|
// #3626 — the FIRST entry of a chat prompt carries the create seed's
|
|
2903
3046
|
// placeholder. `buildPromptCreatePayload` sends `{{ input }}` when the
|
|
2904
3047
|
// file names no template, which is how a prompt authored without one has
|
|
@@ -2918,8 +3061,8 @@ export function promptKindFileErrors(filePath, tomlData) {
|
|
|
2918
3061
|
: chat.userPromptTemplate;
|
|
2919
3062
|
const resolution = resolvePromptKind({
|
|
2920
3063
|
kind: tomlData?.prompt?.kind,
|
|
2921
|
-
provider:
|
|
2922
|
-
model:
|
|
3064
|
+
provider: entry.provider,
|
|
3065
|
+
model: entry.model,
|
|
2923
3066
|
config: {
|
|
2924
3067
|
systemPrompt: chat.systemPrompt,
|
|
2925
3068
|
userPromptTemplate,
|
|
@@ -2944,12 +3087,12 @@ export function promptKindFileErrors(filePath, tomlData) {
|
|
|
2944
3087
|
// against what the server holds.
|
|
2945
3088
|
if (resolution.kind === "chat") {
|
|
2946
3089
|
const strict = resolvePromptStrictOutput({
|
|
2947
|
-
provider:
|
|
3090
|
+
provider: entry.provider,
|
|
2948
3091
|
strictOutput: chat.strictOutput,
|
|
2949
3092
|
outputFormat: chat.outputFormat,
|
|
2950
3093
|
configOutputSchema: chat.outputSchema,
|
|
2951
3094
|
promptOutputSchema: tomlData?.prompt?.outputSchema,
|
|
2952
|
-
status:
|
|
3095
|
+
status: entry.status ?? PROMPT_CONFIG_DEFAULT_STATUS,
|
|
2953
3096
|
});
|
|
2954
3097
|
if (strict.ok === false) {
|
|
2955
3098
|
errors.push(`${filePath}: configs[${index}] — ${strict.error} (${strict.code})`);
|
|
@@ -3165,26 +3308,64 @@ export function promptKindImmutableOutcome(storedKind, localDoc) {
|
|
|
3165
3308
|
};
|
|
3166
3309
|
}
|
|
3167
3310
|
/**
|
|
3168
|
-
* The
|
|
3169
|
-
*
|
|
3311
|
+
* The file-decidable refusals `config push` gives one config file — the
|
|
3312
|
+
* per-file body of `collectConfigFileValidationErrors`, shared with `config
|
|
3313
|
+
* pull` (#4160), which never keeps a local file push would refuse.
|
|
3170
3314
|
*
|
|
3171
|
-
*
|
|
3172
|
-
*
|
|
3315
|
+
* `tables` are the tables whose unrecognized keys are checked here (none for a
|
|
3316
|
+
* workflow, whose own validator owns that message); declared types run over
|
|
3317
|
+
* every table of the surface.
|
|
3173
3318
|
*/
|
|
3174
|
-
export function
|
|
3175
|
-
const
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3319
|
+
export function configFileValidationMessages(filePath, tomlData, surface, tables) {
|
|
3320
|
+
const messages = [];
|
|
3321
|
+
// The document's own shape first: an unrecognized top-level table makes
|
|
3322
|
+
// every per-table check below read an absent table, so its silence would
|
|
3323
|
+
// be misleading.
|
|
3324
|
+
messages.push(...configDocumentShapeErrors(filePath, tomlData, surface));
|
|
3325
|
+
for (const table of tables) {
|
|
3326
|
+
const unknownKeyError = configUnknownKeyError(filePath, tomlData, table);
|
|
3327
|
+
if (unknownKeyError)
|
|
3328
|
+
messages.push(unknownKeyError);
|
|
3180
3329
|
}
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3330
|
+
// Declared types run over EVERY table of the surface, workflows included
|
|
3331
|
+
// — `tables` above is pruned for workflows because `validateWorkflowToml`
|
|
3332
|
+
// owns their unrecognized-key message, which is a different check.
|
|
3333
|
+
for (const table of surface.tables) {
|
|
3334
|
+
messages.push(...configDeclaredTypeErrors(filePath, tomlData, table));
|
|
3335
|
+
}
|
|
3336
|
+
// #3626 — the prompt kind's own file-decidable refusals, run by the same
|
|
3337
|
+
// collector so `config push` and `config diff` cannot disagree about
|
|
3338
|
+
// which prompt files are pushable.
|
|
3339
|
+
if (surface.label === "prompt") {
|
|
3340
|
+
messages.push(...promptKindFileErrors(filePath, tomlData));
|
|
3341
|
+
}
|
|
3342
|
+
// #4116 — a bucket naming its rule set both ways.
|
|
3343
|
+
if (surface.label === "blob-bucket") {
|
|
3344
|
+
messages.push(...blobBucketFileErrors(filePath, tomlData));
|
|
3345
|
+
}
|
|
3346
|
+
return messages;
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Whether `config pull` must rewrite a local file whatever it says (#4160):
|
|
3350
|
+
* `config push` would refuse it, or it spells something in a deprecated form
|
|
3351
|
+
* pull exists to migrate (a bucket naming its rule set by id). Keeping such a
|
|
3352
|
+
* file because its VALUES match would leave the tree unpushable, or the
|
|
3353
|
+
* deprecation unresolved, after a pull that reported success.
|
|
3354
|
+
*/
|
|
3355
|
+
export function pullMustRewrite(label, tomlData) {
|
|
3356
|
+
// A test-case sidecar is not one of the preflight's per-directory targets;
|
|
3357
|
+
// push refuses it through its own collector.
|
|
3358
|
+
if (label === "test-case") {
|
|
3359
|
+
return testCaseSidecarPreflightErrors("", tomlData).length > 0;
|
|
3360
|
+
}
|
|
3361
|
+
const target = unknownKeyPreflightTargets().find((t) => t.surface.label === label);
|
|
3362
|
+
if (target) {
|
|
3363
|
+
const tables = label === "workflow" ? [WORKFLOW_TABLE] : target.tables;
|
|
3364
|
+
if (configFileValidationMessages("", tomlData, target.surface, tables).length > 0) {
|
|
3365
|
+
return true;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
return (label === "blob-bucket" && blobBucketDeprecatedKeyWarning("", tomlData) !== null);
|
|
3188
3369
|
}
|
|
3189
3370
|
export function collectConfigFileValidationErrors(configDir) {
|
|
3190
3371
|
const found = [];
|
|
@@ -3205,28 +3386,7 @@ export function collectConfigFileValidationErrors(configDir) {
|
|
|
3205
3386
|
catch {
|
|
3206
3387
|
continue;
|
|
3207
3388
|
}
|
|
3208
|
-
const messages =
|
|
3209
|
-
// The document's own shape first: an unrecognized top-level table makes
|
|
3210
|
-
// every per-table check below read an absent table, so its silence would
|
|
3211
|
-
// be misleading.
|
|
3212
|
-
messages.push(...configDocumentShapeErrors(filePath, tomlData, surface));
|
|
3213
|
-
for (const table of tables) {
|
|
3214
|
-
const unknownKeyError = configUnknownKeyError(filePath, tomlData, table);
|
|
3215
|
-
if (unknownKeyError)
|
|
3216
|
-
messages.push(unknownKeyError);
|
|
3217
|
-
}
|
|
3218
|
-
// Declared types run over EVERY table of the surface, workflows included
|
|
3219
|
-
// — `tables` above is pruned for workflows because `validateWorkflowToml`
|
|
3220
|
-
// owns their unrecognized-key message, which is a different check.
|
|
3221
|
-
for (const table of surface.tables) {
|
|
3222
|
-
messages.push(...configDeclaredTypeErrors(filePath, tomlData, table));
|
|
3223
|
-
}
|
|
3224
|
-
// #3626 — the prompt kind's own file-decidable refusals, run by the same
|
|
3225
|
-
// collector so `config push` and `config diff` cannot disagree about
|
|
3226
|
-
// which prompt files are pushable.
|
|
3227
|
-
if (surface.label === "prompt") {
|
|
3228
|
-
messages.push(...promptKindFileErrors(filePath, tomlData));
|
|
3229
|
-
}
|
|
3389
|
+
const messages = configFileValidationMessages(filePath, tomlData, surface, tables);
|
|
3230
3390
|
if (messages.length === 0)
|
|
3231
3391
|
continue;
|
|
3232
3392
|
found.push({
|
|
@@ -4810,20 +4970,14 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4810
4970
|
"blob-bucket": {
|
|
4811
4971
|
label: "blob-bucket",
|
|
4812
4972
|
table: BLOB_BUCKET_TABLE,
|
|
4813
|
-
parse: (doc) =>
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
description: tomlOwned,
|
|
4822
|
-
},
|
|
4823
|
-
}),
|
|
4824
|
-
};
|
|
4825
|
-
},
|
|
4826
|
-
serialize: (record) => serializeBlobBucket(record),
|
|
4973
|
+
parse: (doc) => parseBlobBucketToml(doc),
|
|
4974
|
+
// #4116 — the rule set is compared by the thing referred to: the record's
|
|
4975
|
+
// id is rendered as its name, and both sides resolve the name back.
|
|
4976
|
+
serialize: (record, maps) => serializeBlobBucket(record, maps.ruleSetIdToName),
|
|
4977
|
+
extras: (entity) => ({
|
|
4978
|
+
_unresolvedRuleSetName: entity._unresolvedRuleSetName,
|
|
4979
|
+
}),
|
|
4980
|
+
ruleSetRef: true,
|
|
4827
4981
|
// `name` is trimmed on the way in and a blank description is stored as
|
|
4828
4982
|
// none (DSO-003).
|
|
4829
4983
|
canonicalize: (entity) => canonicalizeServerTextFields(entity, {
|
|
@@ -4847,7 +5001,12 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4847
5001
|
keys.push("name");
|
|
4848
5002
|
if (!bucket.preset)
|
|
4849
5003
|
keys.push("preset");
|
|
4850
|
-
|
|
5004
|
+
// Either spelling manages the rule set (#4116) — read as the parse reads
|
|
5005
|
+
// it, so a blank name is no reference rather than a difference the
|
|
5006
|
+
// update cannot carry.
|
|
5007
|
+
const named = typeof bucket.ruleSetName === "string" && bucket.ruleSetName.trim() !== "";
|
|
5008
|
+
const legacy = typeof bucket.ruleSetId === "string" && bucket.ruleSetId !== "";
|
|
5009
|
+
if (!named && !legacy)
|
|
4851
5010
|
keys.push("ruleSetId");
|
|
4852
5011
|
return keys;
|
|
4853
5012
|
},
|
|
@@ -4964,6 +5123,15 @@ export function projectRemoteConfig(spec, record, maps, extra) {
|
|
|
4964
5123
|
export function projectConfigPair(spec, localParsed, remoteRecord, maps, extra, localExtra) {
|
|
4965
5124
|
const local = projectLocalConfig(spec, localParsed, maps, localExtra);
|
|
4966
5125
|
const remote = projectRemoteConfig(spec, remoteRecord, maps, extra);
|
|
5126
|
+
reconcileUnmanagedFields(spec, localParsed, local, remote);
|
|
5127
|
+
return { local, remote };
|
|
5128
|
+
}
|
|
5129
|
+
/**
|
|
5130
|
+
* Take the server's value for every field the local file leaves unmanaged
|
|
5131
|
+
* (#2880), on `local` in place — the reconciliation `projectConfigPair` makes,
|
|
5132
|
+
* shared with `config pull`'s keep-or-write comparison (#4160).
|
|
5133
|
+
*/
|
|
5134
|
+
function reconcileUnmanagedFields(spec, localParsed, local, remote) {
|
|
4967
5135
|
for (const key of spec.unmanaged?.(localParsed) ?? []) {
|
|
4968
5136
|
// Key-for-key, so an unmanaged field the server does not hold either is
|
|
4969
5137
|
// absent on both sides rather than present-and-undefined on one.
|
|
@@ -4972,7 +5140,20 @@ export function projectConfigPair(spec, localParsed, remoteRecord, maps, extra,
|
|
|
4972
5140
|
else
|
|
4973
5141
|
delete local[key];
|
|
4974
5142
|
}
|
|
4975
|
-
|
|
5143
|
+
}
|
|
5144
|
+
/**
|
|
5145
|
+
* One `config diff` row's verdict once both sides exist (#4116, DSO-4116-003).
|
|
5146
|
+
*
|
|
5147
|
+
* A type whose files name a rule set cannot be compared without the app's
|
|
5148
|
+
* rule-set listing: an unresolvable reference would read as a difference that
|
|
5149
|
+
* is not there. So when the listing failed the row says so rather than
|
|
5150
|
+
* comparing; otherwise `compare` decides.
|
|
5151
|
+
*/
|
|
5152
|
+
export function configDiffRowVerdict(parsed, remote, compare, contentComparable) {
|
|
5153
|
+
if (!contentComparable) {
|
|
5154
|
+
return { status: "exists", hint: "content not compared (rule sets unavailable)" };
|
|
5155
|
+
}
|
|
5156
|
+
return compare(parsed, remote);
|
|
4976
5157
|
}
|
|
4977
5158
|
/**
|
|
4978
5159
|
* One `config diff` row's content verdict for a registered type (#2880).
|
|
@@ -5486,6 +5667,59 @@ export function configSemanticHash(label, filePath, maps) {
|
|
|
5486
5667
|
return undefined;
|
|
5487
5668
|
}
|
|
5488
5669
|
}
|
|
5670
|
+
/**
|
|
5671
|
+
* Whether a local config file says what the file `config pull` would write
|
|
5672
|
+
* says (#4160) — the definition-projected field set `config diff` compares, so
|
|
5673
|
+
* a file diff calls Synced keeps its comments, key order and explicit defaults
|
|
5674
|
+
* through a pull.
|
|
5675
|
+
*
|
|
5676
|
+
* Both sides go through the same projection with the same context, so
|
|
5677
|
+
* `extra` is the context a reference is read through (a test case's lookups),
|
|
5678
|
+
* never server-only rows. As in diff, the file is paired by the identity it
|
|
5679
|
+
* states (or `filePath`'s name), and the fields it leaves unmanaged take the
|
|
5680
|
+
* server's values — so a kept file's baseline is the server's projection
|
|
5681
|
+
* (`pulledSemanticHash`), never the file's own.
|
|
5682
|
+
*/
|
|
5683
|
+
export function sameConfigAs(label, serverContent, filePath, maps, extra) {
|
|
5684
|
+
const spec = configDiffSpec(label);
|
|
5685
|
+
return (localContent) => {
|
|
5686
|
+
const parsed = parseConfigToml(localContent);
|
|
5687
|
+
if (pullMustRewrite(label, parsed))
|
|
5688
|
+
return false;
|
|
5689
|
+
const server = parseConfigToml(serverContent);
|
|
5690
|
+
const local = localConfigWithServerIdentity(label, parsed, server, filePath);
|
|
5691
|
+
if (local === null)
|
|
5692
|
+
return false;
|
|
5693
|
+
const localProjected = projectLocalConfig(spec, local, maps, extra);
|
|
5694
|
+
const serverProjected = projectLocalConfig(spec, server, maps, extra);
|
|
5695
|
+
reconcileUnmanagedFields(spec, local, localProjected, serverProjected);
|
|
5696
|
+
return (computeExpandedContentHash(localProjected) ===
|
|
5697
|
+
computeExpandedContentHash(serverProjected));
|
|
5698
|
+
};
|
|
5699
|
+
}
|
|
5700
|
+
/**
|
|
5701
|
+
* The semantic baseline `config pull` stamps for a config file (#4160): the
|
|
5702
|
+
* projection of what the server serializes, whether the file was written or
|
|
5703
|
+
* kept. A kept file can leave unmanaged fields or its identity to the server,
|
|
5704
|
+
* so hashing it would record a baseline the server never had.
|
|
5705
|
+
*/
|
|
5706
|
+
export function pulledSemanticHash(label, serverContent, maps) {
|
|
5707
|
+
try {
|
|
5708
|
+
return hashLocalConfigForDiff(configDiffSpec(label), parseConfigToml(serverContent), maps);
|
|
5709
|
+
}
|
|
5710
|
+
catch {
|
|
5711
|
+
return undefined;
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
/**
|
|
5715
|
+
* Whether a local TOML file parses to the same document as `serverContent`
|
|
5716
|
+
* (#4160): the comparison for `vars.toml`, which has no field definition
|
|
5717
|
+
* behind it, so only comments, key order and spelling are free.
|
|
5718
|
+
*/
|
|
5719
|
+
export function sameTomlAs(serverContent) {
|
|
5720
|
+
return (localContent) => canonicalJsonStringify(parseConfigToml(localContent)) ===
|
|
5721
|
+
canonicalJsonStringify(parseConfigToml(serverContent));
|
|
5722
|
+
}
|
|
5489
5723
|
/**
|
|
5490
5724
|
* The sync state `config push` writes into (#2731 B6, closing #2374).
|
|
5491
5725
|
*
|
|
@@ -5891,7 +6125,9 @@ export async function pullScripts(client, appId, configDir, logger = () => { },
|
|
|
5891
6125
|
* `config pull --only` (issue #2645): which transform names this pull may
|
|
5892
6126
|
* write. Defaults to every one the server lists.
|
|
5893
6127
|
*/
|
|
5894
|
-
selects = () => true
|
|
6128
|
+
selects = () => true,
|
|
6129
|
+
/** Counts each transform as written or left unchanged (#4160). */
|
|
6130
|
+
writes) {
|
|
5895
6131
|
const transformsDir = join(configDir, "transforms");
|
|
5896
6132
|
ensureDir(transformsDir);
|
|
5897
6133
|
const scriptEntities = {};
|
|
@@ -5944,7 +6180,9 @@ selects = () => true) {
|
|
|
5944
6180
|
}
|
|
5945
6181
|
const filename = `${name}.rhai`;
|
|
5946
6182
|
const filePath = join(transformsDir, filename);
|
|
5947
|
-
|
|
6183
|
+
// A transform's comparator is its bytes, so only identical bytes keep the
|
|
6184
|
+
// file (#4160).
|
|
6185
|
+
const fileWritten = writePulledFile(filePath, activeConfig.body, writes);
|
|
5948
6186
|
scriptEntities[name] = {
|
|
5949
6187
|
id: script.scriptId,
|
|
5950
6188
|
modifiedAt: script.modifiedAt || new Date().toISOString(),
|
|
@@ -5957,7 +6195,8 @@ selects = () => true) {
|
|
|
5957
6195
|
semanticHash: computeScriptBodyHash(activeConfig.body),
|
|
5958
6196
|
};
|
|
5959
6197
|
written += 1;
|
|
5960
|
-
|
|
6198
|
+
if (fileWritten)
|
|
6199
|
+
logger(` Wrote transforms/${filename}`);
|
|
5961
6200
|
}
|
|
5962
6201
|
const serverKeys = items.map((s) => s?.name).filter(Boolean);
|
|
5963
6202
|
const presence = detailsOk
|
|
@@ -6033,7 +6272,9 @@ export function pushedFunctionBaseline(input) {
|
|
|
6033
6272
|
* normalizes outside the tree, and the refusals are reported rather than
|
|
6034
6273
|
* swallowed.
|
|
6035
6274
|
*/
|
|
6036
|
-
export async function pullFunctions(client, appId, configDir, logger = () => { }, selects = () => true
|
|
6275
|
+
export async function pullFunctions(client, appId, configDir, logger = () => { }, selects = () => true,
|
|
6276
|
+
/** Counts each function file as written or left unchanged (#4160). */
|
|
6277
|
+
writes) {
|
|
6037
6278
|
const functionsDir = join(configDir, "functions");
|
|
6038
6279
|
ensureDir(functionsDir);
|
|
6039
6280
|
const functionEntities = {};
|
|
@@ -6076,12 +6317,17 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
6076
6317
|
// The logger carries #1446's one warning: a schema holding a `null`
|
|
6077
6318
|
// anywhere cannot be a native table, so it is written as a JSON string
|
|
6078
6319
|
// and the operator is told which key that happened to.
|
|
6079
|
-
|
|
6320
|
+
const content = serializeFunction(record, undefined, logger);
|
|
6321
|
+
const fileWritten = writePulledFile(filePath, content, writes, sameConfigAs("function", content, filePath, {
|
|
6322
|
+
ruleSetIdToName: new Map(),
|
|
6323
|
+
ruleSetNameToId: new Map(),
|
|
6324
|
+
}));
|
|
6080
6325
|
entity.contentHash = computeFileHash(filePath);
|
|
6081
6326
|
entity.semanticHash = hashRemoteConfigForDiff(configDiffSpec("function"), record, { ruleSetIdToName: new Map(), ruleSetNameToId: new Map() });
|
|
6082
6327
|
functionEntities[key] = entity;
|
|
6083
6328
|
written += 1;
|
|
6084
|
-
|
|
6329
|
+
if (fileWritten)
|
|
6330
|
+
logger(` Wrote functions/${key}.toml (no version pushed yet)`);
|
|
6085
6331
|
continue;
|
|
6086
6332
|
}
|
|
6087
6333
|
let active;
|
|
@@ -6137,7 +6383,7 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
6137
6383
|
// this is what says so rather than editing a developer's file in silence.
|
|
6138
6384
|
for (const note of plan.notes)
|
|
6139
6385
|
logger(` ${note}`);
|
|
6140
|
-
const applied = applyFunctionPull(plan);
|
|
6386
|
+
const applied = applyFunctionPull(plan, writes);
|
|
6141
6387
|
for (const refusal of applied.refusals)
|
|
6142
6388
|
logger(` ${refusal}`);
|
|
6143
6389
|
entity.contentHash = computeFileHash(filePath);
|
|
@@ -6175,7 +6421,7 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
6175
6421
|
});
|
|
6176
6422
|
for (const warning of schemaPlan.warnings)
|
|
6177
6423
|
logger(` ${warning}`);
|
|
6178
|
-
const schemaApplied = applyFunctionPull(schemaPlan);
|
|
6424
|
+
const schemaApplied = applyFunctionPull(schemaPlan, writes);
|
|
6179
6425
|
for (const refusal of schemaApplied.refusals)
|
|
6180
6426
|
logger(` ${refusal}`);
|
|
6181
6427
|
const presence = detailsOk
|
|
@@ -6347,7 +6593,7 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6347
6593
|
pulledSlugs.add(slug);
|
|
6348
6594
|
// Write test case TOML
|
|
6349
6595
|
const tomlPath = join(testsDir, `${slug}.toml`);
|
|
6350
|
-
|
|
6596
|
+
writePulledFile(tomlPath, toml, params.writes, sameConfigAs("test-case", toml, tomlPath, { ruleSetIdToName: new Map(), ruleSetNameToId: new Map() }, { lookupMaps }));
|
|
6351
6597
|
// Attachments: the sidecar directory is the authored set, so pull makes it
|
|
6352
6598
|
// match the server — downloading what is there, recording each file's
|
|
6353
6599
|
// content hash (so push can see a byte change under an unchanged name), and
|
|
@@ -6371,7 +6617,8 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6371
6617
|
}
|
|
6372
6618
|
try {
|
|
6373
6619
|
const downloaded = await client.downloadTestCaseAttachment(appId, blockType, blockId, tc.testCaseId, att.filename);
|
|
6374
|
-
|
|
6620
|
+
// An attachment's comparator is its bytes (#4160).
|
|
6621
|
+
writePulledFile(destination, downloaded.data, params.writes);
|
|
6375
6622
|
(attachments ??= {})[att.filename] = computeFileHash(destination);
|
|
6376
6623
|
}
|
|
6377
6624
|
catch (err) {
|
|
@@ -6557,6 +6804,7 @@ export async function pullTestCasesForBlocks(params) {
|
|
|
6557
6804
|
testCaseEntities: params.testCaseEntities,
|
|
6558
6805
|
priorTestCaseEntities: params.priorTestCaseEntities,
|
|
6559
6806
|
lookupMaps: params.lookupMaps,
|
|
6807
|
+
writes: params.writes,
|
|
6560
6808
|
});
|
|
6561
6809
|
if (!outcome.ok) {
|
|
6562
6810
|
skippedBlocks.push(block.key);
|
|
@@ -7881,7 +8129,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
7881
8129
|
lastSyncedAt: new Date().toISOString(),
|
|
7882
8130
|
entities: {},
|
|
7883
8131
|
};
|
|
7884
|
-
saveSyncState(
|
|
8132
|
+
saveSyncState(state);
|
|
7885
8133
|
success(`Initialized config directory: ${configDir}`);
|
|
7886
8134
|
info("Run 'primitive config pull' to download current configuration.");
|
|
7887
8135
|
});
|
|
@@ -7900,6 +8148,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
7900
8148
|
.action(async (options) => {
|
|
7901
8149
|
const resolvedAppId = getCurrentAppId();
|
|
7902
8150
|
const configDir = resolveSyncDir();
|
|
8151
|
+
const pullsLocalEnvironment = requireCurrentEnvironment().local;
|
|
7903
8152
|
if (!options.json) {
|
|
7904
8153
|
info(`Using per-environment config directory: ${configDir}`);
|
|
7905
8154
|
}
|
|
@@ -8106,7 +8355,11 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8106
8355
|
// sync dir (nothing to back up). Prune to 28 days after success.
|
|
8107
8356
|
try {
|
|
8108
8357
|
const snapshotsRoot = resolveSnapshotsRoot({ appId: resolvedAppId });
|
|
8109
|
-
|
|
8358
|
+
// A machine-local environment's baseline is its own file, not the
|
|
8359
|
+
// shared tree's (#4117): that is the one the snapshot carries.
|
|
8360
|
+
const snapshot = createSnapshot(configDir, snapshotsRoot, {
|
|
8361
|
+
baselinePath: pullsLocalEnvironment ? resolveSyncStateReadPath() : undefined,
|
|
8362
|
+
});
|
|
8110
8363
|
if (snapshot) {
|
|
8111
8364
|
info(` Snapshot: ${snapshot.path}`);
|
|
8112
8365
|
pruneSnapshots(snapshotsRoot, 28);
|
|
@@ -8135,11 +8388,15 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8135
8388
|
ensureDir(join(configDir, "collection-type-configs"));
|
|
8136
8389
|
ensureDir(join(configDir, "metadata-category-configs"));
|
|
8137
8390
|
ensureDir(join(configDir, "email-templates"));
|
|
8391
|
+
// #4160 — every pulled file goes through `writePulledFile`, which
|
|
8392
|
+
// leaves a file alone when it already says what the server says, and
|
|
8393
|
+
// counts both outcomes for the summary below.
|
|
8394
|
+
const pullWrites = newPullWriteTally();
|
|
8138
8395
|
// The prior sync state is the record of which local files a previous
|
|
8139
8396
|
// pull wrote — the discriminator the prune pass keys off (#1659). Read
|
|
8140
8397
|
// it now: `saveSyncState` at the end of the pull overwrites it, and the
|
|
8141
8398
|
// files themselves are about to be rewritten.
|
|
8142
|
-
const priorState = loadSyncState(
|
|
8399
|
+
const priorState = loadSyncState();
|
|
8143
8400
|
const priorEntities = priorState?.entities ?? {};
|
|
8144
8401
|
// Write app settings. Under `--only`, `app` has to be named — a pull
|
|
8145
8402
|
// scoped to one workflow must not overwrite app.toml, which is what
|
|
@@ -8166,8 +8423,16 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8166
8423
|
// Every writable setting round-trips as of #2891 — the server
|
|
8167
8424
|
// withholds nothing — so the pulled file is complete and pushable as
|
|
8168
8425
|
// written, with no accompanying note about what it could not carry.
|
|
8169
|
-
|
|
8170
|
-
|
|
8426
|
+
// On a machine-local environment the tree's name is the shared
|
|
8427
|
+
// tree's, and a pull never writes the child's over it (#4117).
|
|
8428
|
+
const written = writePulledAppSettings(configDir, settings, {
|
|
8429
|
+
keepStatedName: pullsLocalEnvironment,
|
|
8430
|
+
tally: pullWrites,
|
|
8431
|
+
});
|
|
8432
|
+
if (written.written)
|
|
8433
|
+
info(" Wrote app.toml");
|
|
8434
|
+
if (written.note)
|
|
8435
|
+
warn(` ${written.note}`);
|
|
8171
8436
|
}
|
|
8172
8437
|
// Write config vars (issue #1423). A single flat vars.toml at the
|
|
8173
8438
|
// sync-dir root (like app.toml), holding the per-environment non-secret
|
|
@@ -8207,17 +8472,21 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8207
8472
|
? Object.fromEntries(Object.entries(parseConfigToml(readFileSync(varsTomlPathPull, "utf-8"))).map(([k, v]) => [k, String(v ?? "")]))
|
|
8208
8473
|
: {};
|
|
8209
8474
|
const scoped = planScopedVarsPull(varsOutcome.vars, pullSelection.varKeys, localVars, priorEntities.vars);
|
|
8210
|
-
|
|
8475
|
+
const varsWritten = writePulledFile(varsTomlPathPull, scoped.content, pullWrites, sameTomlAs(scoped.content));
|
|
8211
8476
|
Object.assign(varEntities, scoped.varEntities);
|
|
8212
|
-
|
|
8477
|
+
if (varsWritten) {
|
|
8478
|
+
info(` Wrote vars.toml (${pullSelection.varKeys.size} selected var(s))`);
|
|
8479
|
+
}
|
|
8213
8480
|
}
|
|
8214
8481
|
else {
|
|
8215
8482
|
const varsPlan = planVarsPull(varsOutcome, varsOutcome.ok ? undefined : priorEntities.vars);
|
|
8216
8483
|
Object.assign(varEntities, varsPlan.varEntities);
|
|
8217
8484
|
if (varsPlan.write && varsPlan.content !== null) {
|
|
8218
|
-
|
|
8485
|
+
const varsWritten = writePulledFile(varsTomlPathPull, varsPlan.content, pullWrites, sameTomlAs(varsPlan.content));
|
|
8219
8486
|
const count = varsOutcome.vars.length;
|
|
8220
|
-
|
|
8487
|
+
if (varsWritten) {
|
|
8488
|
+
info(` Wrote vars.toml (${count} var${count === 1 ? "" : "s"})`);
|
|
8489
|
+
}
|
|
8221
8490
|
}
|
|
8222
8491
|
else {
|
|
8223
8492
|
const err = varsOutcome.error;
|
|
@@ -8237,16 +8506,18 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8237
8506
|
for (const integration of integrations) {
|
|
8238
8507
|
const filename = `${integration.integrationKey}.toml`;
|
|
8239
8508
|
const filePath = join(configDir, "integrations", filename);
|
|
8240
|
-
|
|
8509
|
+
const content = serializeIntegration(integration, (msg) => info(` ${msg}`));
|
|
8510
|
+
const written = writePulledFile(filePath, content, pullWrites, sameConfigAs("integration", content, filePath, integrationPullMaps));
|
|
8241
8511
|
integrationEntities[integration.integrationKey] = {
|
|
8242
8512
|
id: integration.integrationId,
|
|
8243
8513
|
modifiedAt: integration.modifiedAt || new Date().toISOString(),
|
|
8244
8514
|
contentHash: computeFileHash(filePath),
|
|
8245
8515
|
// #2880 — the two sides agree right now, so this is the baseline
|
|
8246
8516
|
// the next push classifies direction against.
|
|
8247
|
-
semanticHash:
|
|
8517
|
+
semanticHash: pulledSemanticHash("integration", content, integrationPullMaps),
|
|
8248
8518
|
};
|
|
8249
|
-
|
|
8519
|
+
if (written)
|
|
8520
|
+
info(` Wrote integrations/${filename}`);
|
|
8250
8521
|
}
|
|
8251
8522
|
// Pull webhooks. #2803 — an ARCHIVED webhook is retired, not
|
|
8252
8523
|
// configuration, so it is not exported: writing a tombstone into the
|
|
@@ -8271,14 +8542,15 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8271
8542
|
for (const webhook of webhooks) {
|
|
8272
8543
|
const filename = `${webhook.webhookKey}.toml`;
|
|
8273
8544
|
const filePath = join(webhooksDir, filename);
|
|
8274
|
-
|
|
8545
|
+
const content = serializeWebhook(webhook, (msg) => info(` ${msg}`));
|
|
8546
|
+
writePulledFile(filePath, content, pullWrites, sameConfigAs("webhook", content, filePath, webhookPullMaps));
|
|
8275
8547
|
webhookEntities[webhook.webhookKey] = {
|
|
8276
8548
|
id: webhook.webhookId,
|
|
8277
8549
|
modifiedAt: webhook.modifiedAt || new Date().toISOString(),
|
|
8278
8550
|
contentHash: computeFileHash(filePath),
|
|
8279
8551
|
// #2880 — the file and the server agree right now, so this is the
|
|
8280
8552
|
// baseline the next push classifies direction against.
|
|
8281
|
-
semanticHash:
|
|
8553
|
+
semanticHash: pulledSemanticHash("webhook", content, webhookPullMaps),
|
|
8282
8554
|
};
|
|
8283
8555
|
// A stored value that is not a reference is never written to TOML, so
|
|
8284
8556
|
// pull time is where an operator finds out: a legacy literal from
|
|
@@ -8311,6 +8583,24 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8311
8583
|
}
|
|
8312
8584
|
}
|
|
8313
8585
|
info(` Pulled ${webhooks.length} webhook(s)`);
|
|
8586
|
+
// #2731 B4 — pull is one of the two moments local and server provably
|
|
8587
|
+
// agree, so it stamps the semantic baseline beside the byte hash (push
|
|
8588
|
+
// stamps the other one, from the entity the server returned). The
|
|
8589
|
+
// rule-set maps the shared projection resolves references through are
|
|
8590
|
+
// filled here, before the first type that names a rule set (blob
|
|
8591
|
+
// buckets, #4116), from the FULL listing rather than the `--only`
|
|
8592
|
+
// selection: a scoped pull of a bucket must still name its rule set.
|
|
8593
|
+
const ruleSetIdToName = new Map();
|
|
8594
|
+
const ruleSetNameToIdPull = new Map();
|
|
8595
|
+
const pullDiffMaps = {
|
|
8596
|
+
ruleSetIdToName,
|
|
8597
|
+
ruleSetNameToId: ruleSetNameToIdPull,
|
|
8598
|
+
};
|
|
8599
|
+
for (const ruleSet of Array.isArray(ruleSetsResult) ? ruleSetsResult : []) {
|
|
8600
|
+
ruleSetIdToName.set(ruleSet.ruleSetId, ruleSet.name);
|
|
8601
|
+
if (ruleSet.name)
|
|
8602
|
+
ruleSetNameToIdPull.set(ruleSet.name, ruleSet.ruleSetId);
|
|
8603
|
+
}
|
|
8314
8604
|
// Pull cron triggers. An older server without the route (or any other
|
|
8315
8605
|
// failure) leaves the type unlisted — tolerated as before, but recorded
|
|
8316
8606
|
// so prune skips it rather than reading it as "no cron triggers".
|
|
@@ -8319,8 +8609,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8319
8609
|
const cronTriggersDir = join(configDir, "cron-triggers");
|
|
8320
8610
|
mkdirSync(cronTriggersDir, { recursive: true });
|
|
8321
8611
|
const cronTriggerEntities = {};
|
|
8322
|
-
// No rule-set reference on
|
|
8323
|
-
// filled until the rule-set leg — see the webhook block above.
|
|
8612
|
+
// No rule-set reference on a cron trigger — see the webhook block above.
|
|
8324
8613
|
const keylessPullMaps = {
|
|
8325
8614
|
ruleSetIdToName: new Map(),
|
|
8326
8615
|
ruleSetNameToId: new Map(),
|
|
@@ -8328,13 +8617,14 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8328
8617
|
for (const trigger of cronTriggerItems) {
|
|
8329
8618
|
const filename = `${trigger.triggerKey}.toml`;
|
|
8330
8619
|
const filePath = join(cronTriggersDir, filename);
|
|
8331
|
-
|
|
8620
|
+
const content = serializeCronTrigger(trigger, (msg) => info(` ${msg}`));
|
|
8621
|
+
writePulledFile(filePath, content, pullWrites, sameConfigAs("cron-trigger", content, filePath, keylessPullMaps));
|
|
8332
8622
|
cronTriggerEntities[trigger.triggerKey] = {
|
|
8333
8623
|
id: trigger.triggerId,
|
|
8334
8624
|
modifiedAt: trigger.modifiedAt || new Date().toISOString(),
|
|
8335
8625
|
contentHash: computeFileHash(filePath),
|
|
8336
8626
|
// #2880 — the baseline the next push classifies direction against.
|
|
8337
|
-
semanticHash:
|
|
8627
|
+
semanticHash: pulledSemanticHash("cron-trigger", content, keylessPullMaps),
|
|
8338
8628
|
};
|
|
8339
8629
|
}
|
|
8340
8630
|
info(` Pulled ${cronTriggerItems.length} cron trigger(s)`);
|
|
@@ -8344,31 +8634,50 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8344
8634
|
const blobBucketsDir = join(configDir, "blob-buckets");
|
|
8345
8635
|
mkdirSync(blobBucketsDir, { recursive: true });
|
|
8346
8636
|
const blobBucketEntities = {};
|
|
8637
|
+
// #4116 — a bound bucket's file names its rule set, which only the
|
|
8638
|
+
// rule-set listing can supply. When that listing failed, the bucket's
|
|
8639
|
+
// file and sync-state entry are kept as they are rather than rewritten
|
|
8640
|
+
// without the name.
|
|
8641
|
+
const unwrittenBlobBucketKeys = [];
|
|
8347
8642
|
for (const bucket of blobBucketItems) {
|
|
8643
|
+
if (!ruleSetsOutcome.ok && bucket.ruleSetId) {
|
|
8644
|
+
unwrittenBlobBucketKeys.push(bucket.bucketKey);
|
|
8645
|
+
continue;
|
|
8646
|
+
}
|
|
8348
8647
|
const filename = `${bucket.bucketKey}.toml`;
|
|
8349
8648
|
const filePath = join(blobBucketsDir, filename);
|
|
8350
|
-
|
|
8649
|
+
const content = serializeBlobBucket(bucket, ruleSetIdToName, (msg) => info(` ${msg}`));
|
|
8650
|
+
writePulledFile(filePath, content, pullWrites, sameConfigAs("blob-bucket", content, filePath, pullDiffMaps));
|
|
8351
8651
|
blobBucketEntities[bucket.bucketKey] = {
|
|
8352
8652
|
id: bucket.bucketId,
|
|
8353
8653
|
modifiedAt: bucket.modifiedAt || new Date().toISOString(),
|
|
8354
8654
|
contentHash: computeFileHash(filePath),
|
|
8355
|
-
// #2880 —
|
|
8356
|
-
semanticHash:
|
|
8655
|
+
// #2880 — the baseline the next push classifies direction against.
|
|
8656
|
+
semanticHash: pulledSemanticHash("blob-bucket", content, pullDiffMaps),
|
|
8357
8657
|
};
|
|
8358
8658
|
}
|
|
8359
|
-
info(` Pulled ${blobBucketItems.length} blob bucket(s)`);
|
|
8659
|
+
info(` Pulled ${blobBucketItems.length - unwrittenBlobBucketKeys.length} blob bucket(s)`);
|
|
8660
|
+
if (unwrittenBlobBucketKeys.length > 0) {
|
|
8661
|
+
warn(`${unwrittenBlobBucketKeys.length} blob bucket(s) not written — the rule-set ` +
|
|
8662
|
+
`listing failed, so their ruleSetName is unknown; existing files kept ` +
|
|
8663
|
+
`(${unwrittenBlobBucketKeys.join(", ")})`);
|
|
8664
|
+
}
|
|
8360
8665
|
// Write prompts
|
|
8361
8666
|
const promptEntities = {};
|
|
8362
8667
|
for (const prompt of prompts) {
|
|
8363
8668
|
const filename = `${prompt.promptKey}.toml`;
|
|
8364
8669
|
const filePath = join(configDir, "prompts", filename);
|
|
8365
|
-
|
|
8670
|
+
const content = serializePrompt(prompt, (msg) => info(` ${msg}`));
|
|
8671
|
+
const written = writePulledFile(filePath, content, pullWrites,
|
|
8672
|
+
// Prompts carry no rule-set reference.
|
|
8673
|
+
sameConfigAs("prompt", content, filePath, keylessPullMaps));
|
|
8366
8674
|
promptEntities[prompt.promptKey] = {
|
|
8367
8675
|
id: prompt.promptId,
|
|
8368
8676
|
modifiedAt: prompt.modifiedAt || new Date().toISOString(),
|
|
8369
8677
|
contentHash: computeFileHash(filePath),
|
|
8370
8678
|
};
|
|
8371
|
-
|
|
8679
|
+
if (written)
|
|
8680
|
+
info(` Wrote prompts/${filename}`);
|
|
8372
8681
|
}
|
|
8373
8682
|
// Write workflows
|
|
8374
8683
|
const workflowEntities = {};
|
|
@@ -8394,13 +8703,21 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8394
8703
|
if (reconciled.message) {
|
|
8395
8704
|
info(` ${reconciled.message}`);
|
|
8396
8705
|
}
|
|
8706
|
+
let workflowWritten = false;
|
|
8397
8707
|
if (reconciled.action === "write-merged") {
|
|
8398
8708
|
writeFileSync(filePath, reconciled.content);
|
|
8709
|
+
pullWrites.written += 1;
|
|
8710
|
+
workflowWritten = true;
|
|
8399
8711
|
}
|
|
8400
8712
|
else if (reconciled.action === "write-expanded") {
|
|
8401
|
-
|
|
8713
|
+
// #4160 — a file with no fragments that already describes the
|
|
8714
|
+
// running workflow keeps its bytes.
|
|
8715
|
+
workflowWritten = writePulledFile(filePath, remoteToml, pullWrites, sameWorkflowAs(workflow.workflowKey, remoteToml));
|
|
8716
|
+
}
|
|
8717
|
+
else {
|
|
8718
|
+
pullWrites.unchanged += 1;
|
|
8402
8719
|
}
|
|
8403
|
-
if (
|
|
8720
|
+
if (workflowWritten) {
|
|
8404
8721
|
info(` Wrote workflows/${filename}`);
|
|
8405
8722
|
}
|
|
8406
8723
|
// #2645 — the workflow's OTHER named configs, one file each. The
|
|
@@ -8412,8 +8729,11 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8412
8729
|
const sidecarDir = workflowConfigsDir(configDir, workflow.workflowKey);
|
|
8413
8730
|
mkdirSync(sidecarDir, { recursive: true });
|
|
8414
8731
|
for (const sidecar of sidecars) {
|
|
8415
|
-
|
|
8416
|
-
|
|
8732
|
+
const sidecarWritten = writePulledFile(join(sidecarDir, `${sidecar.name}.toml`), sidecar.content, pullWrites, (local) => hashWorkflowConfigSidecar(parseConfigToml(local)) ===
|
|
8733
|
+
hashWorkflowConfigSidecar(parseConfigToml(sidecar.content)));
|
|
8734
|
+
if (sidecarWritten) {
|
|
8735
|
+
info(` Wrote workflows/${workflow.workflowKey}.configs/${sidecar.name}.toml`);
|
|
8736
|
+
}
|
|
8417
8737
|
}
|
|
8418
8738
|
}
|
|
8419
8739
|
// A sidecar for a config the server no longer has is a file that
|
|
@@ -8480,7 +8800,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8480
8800
|
return false;
|
|
8481
8801
|
matchedSelectors.add(`transform/${name}`);
|
|
8482
8802
|
return true;
|
|
8483
|
-
});
|
|
8803
|
+
}, pullWrites);
|
|
8484
8804
|
if (pulledScriptsCount > 0) {
|
|
8485
8805
|
info(` Pulled ${pulledScriptsCount} transform(s)`);
|
|
8486
8806
|
}
|
|
@@ -8494,7 +8814,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8494
8814
|
return false;
|
|
8495
8815
|
matchedSelectors.add(`function/${key}`);
|
|
8496
8816
|
return true;
|
|
8497
|
-
});
|
|
8817
|
+
}, pullWrites);
|
|
8498
8818
|
if (pulledFunctionsCount > 0) {
|
|
8499
8819
|
info(` Pulled ${pulledFunctionsCount} function(s)`);
|
|
8500
8820
|
}
|
|
@@ -8530,24 +8850,13 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8530
8850
|
prompts: promptDetails,
|
|
8531
8851
|
configs: blockConfigs,
|
|
8532
8852
|
});
|
|
8533
|
-
// #2731 B4 — pull is one of the two moments local and server provably
|
|
8534
|
-
// agree, so it stamps the semantic baseline beside the byte hash (push
|
|
8535
|
-
// stamps the other one, from the entity the server returned). The
|
|
8536
|
-
// rule-set maps the shared projection resolves references through are
|
|
8537
|
-
// declared here and filled below, before the first type that needs
|
|
8538
|
-
// them is written.
|
|
8539
|
-
const ruleSetIdToName = new Map();
|
|
8540
|
-
const ruleSetNameToIdPull = new Map();
|
|
8541
|
-
const pullDiffMaps = {
|
|
8542
|
-
ruleSetIdToName,
|
|
8543
|
-
ruleSetNameToId: ruleSetNameToIdPull,
|
|
8544
|
-
};
|
|
8545
8853
|
// Write email templates (use detailed emailTemplates, not summaries)
|
|
8546
8854
|
const emailTemplateEntities = {};
|
|
8547
8855
|
for (const template of emailTemplates) {
|
|
8548
8856
|
const filename = `${template.emailType}.toml`;
|
|
8549
8857
|
const templatePath = join(configDir, "email-templates", filename);
|
|
8550
|
-
|
|
8858
|
+
const content = serializeEmailTemplate(template, (msg) => info(` ${msg}`));
|
|
8859
|
+
const written = writePulledFile(templatePath, content, pullWrites, sameConfigAs("email-template", content, templatePath, pullDiffMaps));
|
|
8551
8860
|
emailTemplateEntities[template.emailType] = {
|
|
8552
8861
|
id: template.emailType,
|
|
8553
8862
|
modifiedAt: template.override?.modifiedAt || new Date().toISOString(),
|
|
@@ -8555,9 +8864,10 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8555
8864
|
// live-fetch fallback gates on, and an entry without one fails
|
|
8556
8865
|
// closed rather than being blind-upserted.
|
|
8557
8866
|
contentHash: computeFileHash(templatePath),
|
|
8558
|
-
semanticHash:
|
|
8867
|
+
semanticHash: pulledSemanticHash("email-template", content, pullDiffMaps),
|
|
8559
8868
|
};
|
|
8560
|
-
|
|
8869
|
+
if (written)
|
|
8870
|
+
info(` Wrote email-templates/${filename}`);
|
|
8561
8871
|
}
|
|
8562
8872
|
// Email-template files for removed overrides used to be cleaned up here
|
|
8563
8873
|
// by a hand-coded loop — the only type that pruned at all (#1659). It is
|
|
@@ -8609,6 +8919,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8609
8919
|
priorTestCaseEntities,
|
|
8610
8920
|
lookupMaps: testCaseLookupMaps,
|
|
8611
8921
|
logger: (message) => info(message),
|
|
8922
|
+
writes: pullWrites,
|
|
8612
8923
|
});
|
|
8613
8924
|
totalTestCases += count;
|
|
8614
8925
|
unwrittenTestCases += unwritten.length;
|
|
@@ -8620,13 +8931,10 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8620
8931
|
if (unwrittenTestCases > 0) {
|
|
8621
8932
|
warn(`${unwrittenTestCases} test case(s) not written — references could not be resolved; see above`);
|
|
8622
8933
|
}
|
|
8623
|
-
//
|
|
8934
|
+
// The rule-set files this pull writes. The reference maps above are
|
|
8935
|
+
// filled from the full listing instead, so a scoped pull of a type that
|
|
8936
|
+
// names a rule set still names it (#4116, DSO-4116-001).
|
|
8624
8937
|
const ruleSets = selectPull("rule-set", Array.isArray(ruleSetsResult) ? ruleSetsResult : [], (rs) => ruleSetFileKey(rs));
|
|
8625
|
-
for (const ruleSet of ruleSets) {
|
|
8626
|
-
ruleSetIdToName.set(ruleSet.ruleSetId, ruleSet.name);
|
|
8627
|
-
if (ruleSet.name)
|
|
8628
|
-
ruleSetNameToIdPull.set(ruleSet.name, ruleSet.ruleSetId);
|
|
8629
|
-
}
|
|
8630
8938
|
// Write database types
|
|
8631
8939
|
const databaseTypeEntities = {};
|
|
8632
8940
|
for (const { typeConfig, operations, subscriptions } of databaseTypesWithOps) {
|
|
@@ -8638,14 +8946,15 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8638
8946
|
const hints = existsSync(filePath)
|
|
8639
8947
|
? detectExistingOperationForms(readFileSync(filePath, "utf-8"))
|
|
8640
8948
|
: undefined;
|
|
8641
|
-
|
|
8949
|
+
const content = serializeDatabaseType(typeConfig, operations, ruleSetIdToName, {
|
|
8642
8950
|
hints,
|
|
8643
8951
|
defaultForm: "native",
|
|
8644
8952
|
logger: (msg) => info(` ${msg}`),
|
|
8645
8953
|
// Issue #803: emit `[[subscriptions]]` blocks so a pull → push
|
|
8646
8954
|
// cycle round-trips subscriptions without a spurious diff.
|
|
8647
8955
|
subscriptions,
|
|
8648
|
-
})
|
|
8956
|
+
});
|
|
8957
|
+
const written = writePulledFile(filePath, content, pullWrites, sameConfigAs("database-type-config", content, filePath, pullDiffMaps));
|
|
8649
8958
|
const opsEntities = {};
|
|
8650
8959
|
for (const op of operations) {
|
|
8651
8960
|
opsEntities[op.name] = {
|
|
@@ -8669,11 +8978,12 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8669
8978
|
operations: Object.keys(opsEntities).length > 0 ? opsEntities : undefined,
|
|
8670
8979
|
subscriptions: Object.keys(subsEntities).length > 0 ? subsEntities : undefined,
|
|
8671
8980
|
contentHash: computeFileHash(filePath),
|
|
8672
|
-
semanticHash:
|
|
8981
|
+
semanticHash: pulledSemanticHash("database-type-config", content, pullDiffMaps),
|
|
8673
8982
|
hasSchema: typeof typeConfig.schema === "string" &&
|
|
8674
8983
|
typeConfig.schema.trim().length > 0,
|
|
8675
8984
|
};
|
|
8676
|
-
|
|
8985
|
+
if (written)
|
|
8986
|
+
info(` Wrote database-type-configs/${filename}`);
|
|
8677
8987
|
}
|
|
8678
8988
|
// #3182 — the typed-handle artifacts are REGENERATED, never pulled:
|
|
8679
8989
|
// they are not in the envelope, and a fresh clone must end up with the
|
|
@@ -8696,14 +9006,16 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8696
9006
|
const fileKey = ruleSetFileKey(ruleSet);
|
|
8697
9007
|
const filename = `${fileKey}.toml`;
|
|
8698
9008
|
const filePath = join(configDir, "rule-sets", filename);
|
|
8699
|
-
|
|
9009
|
+
const content = serializeRuleSet(ruleSet, (msg) => info(` ${msg}`));
|
|
9010
|
+
const written = writePulledFile(filePath, content, pullWrites, sameConfigAs("rule-set", content, filePath, pullDiffMaps));
|
|
8700
9011
|
ruleSetEntities[fileKey] = {
|
|
8701
9012
|
id: ruleSet.ruleSetId,
|
|
8702
9013
|
modifiedAt: ruleSet.modifiedAt || new Date().toISOString(),
|
|
8703
9014
|
contentHash: computeFileHash(filePath),
|
|
8704
|
-
semanticHash:
|
|
9015
|
+
semanticHash: pulledSemanticHash("rule-set", content, pullDiffMaps),
|
|
8705
9016
|
};
|
|
8706
|
-
|
|
9017
|
+
if (written)
|
|
9018
|
+
info(` Wrote rule-sets/${filename}`);
|
|
8707
9019
|
}
|
|
8708
9020
|
// Write group type configs
|
|
8709
9021
|
const groupTypeConfigEntities = {};
|
|
@@ -8711,13 +9023,15 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8711
9023
|
for (const config of groupTypeConfigs) {
|
|
8712
9024
|
const filename = `${config.groupType}.toml`;
|
|
8713
9025
|
const filePath = join(configDir, "group-type-configs", filename);
|
|
8714
|
-
|
|
9026
|
+
const content = serializeGroupTypeConfig(config, ruleSetIdToName, (msg) => info(` ${msg}`));
|
|
9027
|
+
const written = writePulledFile(filePath, content, pullWrites, sameConfigAs("group-type-config", content, filePath, pullDiffMaps));
|
|
8715
9028
|
groupTypeConfigEntities[config.groupType] = {
|
|
8716
9029
|
modifiedAt: config.modifiedAt || new Date().toISOString(),
|
|
8717
9030
|
contentHash: computeFileHash(filePath),
|
|
8718
|
-
semanticHash:
|
|
9031
|
+
semanticHash: pulledSemanticHash("group-type-config", content, pullDiffMaps),
|
|
8719
9032
|
};
|
|
8720
|
-
|
|
9033
|
+
if (written)
|
|
9034
|
+
info(` Wrote group-type-configs/${filename}`);
|
|
8721
9035
|
}
|
|
8722
9036
|
// Write collection type configs
|
|
8723
9037
|
const collectionTypeConfigEntities = {};
|
|
@@ -8725,13 +9039,15 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8725
9039
|
for (const config of collectionTypeConfigs) {
|
|
8726
9040
|
const filename = `${config.collectionType}.toml`;
|
|
8727
9041
|
const filePath = join(configDir, "collection-type-configs", filename);
|
|
8728
|
-
|
|
9042
|
+
const content = serializeCollectionTypeConfig(config, ruleSetIdToName, (msg) => info(` ${msg}`));
|
|
9043
|
+
const written = writePulledFile(filePath, content, pullWrites, sameConfigAs("collection-type-config", content, filePath, pullDiffMaps));
|
|
8729
9044
|
collectionTypeConfigEntities[config.collectionType] = {
|
|
8730
9045
|
modifiedAt: config.modifiedAt || new Date().toISOString(),
|
|
8731
9046
|
contentHash: computeFileHash(filePath),
|
|
8732
|
-
semanticHash:
|
|
9047
|
+
semanticHash: pulledSemanticHash("collection-type-config", content, pullDiffMaps),
|
|
8733
9048
|
};
|
|
8734
|
-
|
|
9049
|
+
if (written)
|
|
9050
|
+
info(` Wrote collection-type-configs/${filename}`);
|
|
8735
9051
|
}
|
|
8736
9052
|
// Write metadata category configs (issue #1304, P-B). One file per
|
|
8737
9053
|
// `{resourceType, category}`; the entity key is the `resourceType#category`
|
|
@@ -8741,13 +9057,15 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8741
9057
|
for (const config of metadataCategoryConfigs) {
|
|
8742
9058
|
const filename = `${config.resourceType}.${config.category}.toml`;
|
|
8743
9059
|
const filePath = join(configDir, "metadata-category-configs", filename);
|
|
8744
|
-
|
|
9060
|
+
const content = serializeMetadataCategoryConfig(config, (msg) => info(` ${msg}`));
|
|
9061
|
+
const written = writePulledFile(filePath, content, pullWrites, sameConfigAs("metadata-category-config", content, filePath, pullDiffMaps));
|
|
8745
9062
|
metadataCategoryConfigEntities[`${config.resourceType}#${config.category}`] = {
|
|
8746
9063
|
modifiedAt: config.modifiedAt || new Date().toISOString(),
|
|
8747
9064
|
contentHash: computeFileHash(filePath),
|
|
8748
|
-
semanticHash:
|
|
9065
|
+
semanticHash: pulledSemanticHash("metadata-category-config", content, pullDiffMaps),
|
|
8749
9066
|
};
|
|
8750
|
-
|
|
9067
|
+
if (written)
|
|
9068
|
+
info(` Wrote metadata-category-configs/${filename}`);
|
|
8751
9069
|
}
|
|
8752
9070
|
// ── Prune reconciliation (issue #1659) ───────────────────────────────
|
|
8753
9071
|
//
|
|
@@ -8897,6 +9215,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8897
9215
|
preserveUnwrittenEntries(emailTemplateEntities, priorEntities.emailTemplates, emailTemplatesOutcome.ok
|
|
8898
9216
|
? emailTemplateOverrides.map((t) => t.emailType)
|
|
8899
9217
|
: []);
|
|
9218
|
+
preserveUnwrittenEntries(blobBucketEntities, priorEntities.blobBuckets, unwrittenBlobBucketKeys);
|
|
8900
9219
|
const finalCronTriggerEntities = keepPrior(cronTriggersOutcome.ok, cronTriggerEntities, "cronTriggers");
|
|
8901
9220
|
const finalBlobBucketEntities = keepPrior(blobBucketsOutcome.ok, blobBucketEntities, "blobBuckets");
|
|
8902
9221
|
const finalScriptEntities = keepPrior(scriptListOk, scriptEntities, "scripts");
|
|
@@ -9094,8 +9413,12 @@ export function registerConfigSyncCommands(sync) {
|
|
|
9094
9413
|
// this is the state both sides agree on: the baseline the
|
|
9095
9414
|
// next push classifies against. A pull also ends any
|
|
9096
9415
|
// cross-app name exclusion, because the file it just wrote
|
|
9097
|
-
// states the target's own name
|
|
9098
|
-
|
|
9416
|
+
// states the target's own name — except on a
|
|
9417
|
+
// machine-local environment, whose file states the shared
|
|
9418
|
+
// tree's name and whose baseline never includes one.
|
|
9419
|
+
semanticHash: hashAppSettingsForDiff(settings, {
|
|
9420
|
+
excludeName: pullsLocalEnvironment,
|
|
9421
|
+
}),
|
|
9099
9422
|
}
|
|
9100
9423
|
: pullSelection
|
|
9101
9424
|
? priorEntities.app
|
|
@@ -9122,7 +9445,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
9122
9445
|
: undefined,
|
|
9123
9446
|
},
|
|
9124
9447
|
};
|
|
9125
|
-
saveSyncState(
|
|
9448
|
+
saveSyncState(state);
|
|
9126
9449
|
divider();
|
|
9127
9450
|
success(`Pulled configuration to ${configDir}`);
|
|
9128
9451
|
keyValue("Config Vars", Object.keys(varEntities).length);
|
|
@@ -9145,6 +9468,8 @@ export function registerConfigSyncCommands(sync) {
|
|
|
9145
9468
|
keyValue("Collection Type Configs", collectionTypeConfigs.length);
|
|
9146
9469
|
keyValue("Metadata Category Configs", metadataCategoryConfigs.length);
|
|
9147
9470
|
keyValue("Pruned", prunedCount);
|
|
9471
|
+
keyValue("Files written", pullWrites.written);
|
|
9472
|
+
keyValue("Files unchanged", pullWrites.unchanged);
|
|
9148
9473
|
// A selector the server had nothing for wrote nothing. Say so and fail:
|
|
9149
9474
|
// otherwise a typo pulls an empty set and reports success, and the
|
|
9150
9475
|
// operator reads "Pulled configuration" as "my object is now in the
|
|
@@ -9252,7 +9577,11 @@ What a push guarantees (two stages):
|
|
|
9252
9577
|
// state write holds on the first push and the end-of-run save persists
|
|
9253
9578
|
// the ids. Without it a successful create-push recorded nothing and the
|
|
9254
9579
|
// next push re-entered every create path.
|
|
9255
|
-
let syncState = ensurePushSyncState(loadSyncState(
|
|
9580
|
+
let syncState = ensurePushSyncState(loadSyncState(), resolvedAppId, getServerUrl());
|
|
9581
|
+
// A machine-local environment (#4117) shares a committed environment's
|
|
9582
|
+
// tree, so `[app].name` in it is that environment's name: never sent,
|
|
9583
|
+
// never compared, never hashed into this environment's baseline.
|
|
9584
|
+
const localEnvironment = requireCurrentEnvironment().local;
|
|
9256
9585
|
// Detect cross-app push: if pushing to a different app, clear entity IDs
|
|
9257
9586
|
// so everything goes through the create path
|
|
9258
9587
|
const isCrossAppPush = syncState && syncState.appId !== resolvedAppId;
|
|
@@ -9688,6 +10017,7 @@ What a push guarantees (two stages):
|
|
|
9688
10017
|
syncState,
|
|
9689
10018
|
force: !!options.force,
|
|
9690
10019
|
isCrossAppPush: !!isCrossAppPush,
|
|
10020
|
+
localEnvironment,
|
|
9691
10021
|
client,
|
|
9692
10022
|
});
|
|
9693
10023
|
// Re-derived, not returned: both are one-line functions of inputs the
|
|
@@ -9864,13 +10194,21 @@ What a push guarantees (two stages):
|
|
|
9864
10194
|
// `appNameEdited` — the file has moved away from the recorded name.
|
|
9865
10195
|
// That is a local rename BY CONSTRUCTION, not by comparison: no
|
|
9866
10196
|
// baseline hashed without `name` can ever attribute it.
|
|
10197
|
+
//
|
|
10198
|
+
// On a machine-local environment (#4117) all three are settled
|
|
10199
|
+
// before they start: the name is the shared tree's, so it is out of
|
|
10200
|
+
// the body and the comparison, and no edit to it is this app's.
|
|
9867
10201
|
const payloadExcludesName = appSettingsNameExcluded({
|
|
10202
|
+
localEnvironment,
|
|
9868
10203
|
isCrossAppPush: !!isCrossAppPush,
|
|
9869
10204
|
recordedExcludedName,
|
|
9870
10205
|
fileName: appFileName,
|
|
9871
10206
|
});
|
|
9872
|
-
const compareExcludesName =
|
|
9873
|
-
|
|
10207
|
+
const compareExcludesName = localEnvironment ||
|
|
10208
|
+
!!isCrossAppPush ||
|
|
10209
|
+
recordedExcludedName !== undefined;
|
|
10210
|
+
const appNameEdited = !localEnvironment &&
|
|
10211
|
+
!isCrossAppPush &&
|
|
9874
10212
|
recordedExcludedName !== undefined &&
|
|
9875
10213
|
!payloadExcludesName;
|
|
9876
10214
|
// The whole-truth body. Errors (an unrecognized or retired key, a
|
|
@@ -9878,6 +10216,7 @@ What a push guarantees (two stages):
|
|
|
9878
10216
|
// the preflight pass above, so only warnings remain here.
|
|
9879
10217
|
const { settings: appPayload, warnings: appWarnings } = buildAppSettingsPayload(tomlData, {
|
|
9880
10218
|
excludeName: payloadExcludesName,
|
|
10219
|
+
nameOptional: localEnvironment,
|
|
9881
10220
|
});
|
|
9882
10221
|
for (const w of appWarnings) {
|
|
9883
10222
|
warn(` ${w}`);
|
|
@@ -9900,8 +10239,12 @@ What a push guarantees (two stages):
|
|
|
9900
10239
|
appEntry?.modifiedAt ||
|
|
9901
10240
|
new Date().toISOString(),
|
|
9902
10241
|
contentHash: appFileHash,
|
|
10242
|
+
// Every stamp on a machine-local environment is hashed without
|
|
10243
|
+
// the name, or the next comparison — which never sees it —
|
|
10244
|
+
// could not match the baseline (#4117).
|
|
9903
10245
|
semanticHash: hashAppSettingsForDiff(baselineSettings, {
|
|
9904
|
-
excludeName:
|
|
10246
|
+
excludeName: localEnvironment ||
|
|
10247
|
+
options.recordExclusion !== undefined ||
|
|
9905
10248
|
(options.keepExclusion && compareExcludesName),
|
|
9906
10249
|
}),
|
|
9907
10250
|
...(options.recordExclusion !== undefined
|
|
@@ -11109,16 +11452,25 @@ What a push guarantees (two stages):
|
|
|
11109
11452
|
const liveBuckets = new Map();
|
|
11110
11453
|
let bucketsLiveOk = true;
|
|
11111
11454
|
if (files.length > 0) {
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11455
|
+
// #4116 — a bound bucket's rule set is compared by name, so the
|
|
11456
|
+
// comparison needs the rule-set listing; without it the leg
|
|
11457
|
+
// degrades as the type configs do.
|
|
11458
|
+
bucketsLiveOk = (await loadLiveRuleSets()).ok;
|
|
11459
|
+
if (!bucketsLiveOk) {
|
|
11460
|
+
warnLiveUnavailable("blob-buckets", "rule sets unavailable");
|
|
11118
11461
|
}
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11462
|
+
else {
|
|
11463
|
+
try {
|
|
11464
|
+
const listed = await client.listBlobBuckets(resolvedAppId);
|
|
11465
|
+
for (const b of (listed?.items ?? [])) {
|
|
11466
|
+
if (b?.bucketKey)
|
|
11467
|
+
liveBuckets.set(b.bucketKey, b);
|
|
11468
|
+
}
|
|
11469
|
+
}
|
|
11470
|
+
catch (err) {
|
|
11471
|
+
bucketsLiveOk = false;
|
|
11472
|
+
warnLiveUnavailable("blob-buckets", err);
|
|
11473
|
+
}
|
|
11122
11474
|
}
|
|
11123
11475
|
}
|
|
11124
11476
|
const refetchLiveBucket = async (bucketId) => {
|
|
@@ -11130,7 +11482,13 @@ What a push guarantees (two stages):
|
|
|
11130
11482
|
const tomlData = parseTomlFile(filePath);
|
|
11131
11483
|
const key = tomlData.bucket?.key || basename(file, ".toml");
|
|
11132
11484
|
withDerivedIdentity(tomlData, "bucket", { key });
|
|
11133
|
-
|
|
11485
|
+
// #4116 — `ruleSetName` resolves to THIS app's rule set before any
|
|
11486
|
+
// request; a name that resolves nowhere fails here, so a bucket is
|
|
11487
|
+
// never created or updated without its rule set.
|
|
11488
|
+
const bucket = parseBlobBucketToml(tomlData);
|
|
11489
|
+
resolveRuleSetReference(bucket, ruleSetNameToId, `blob bucket "${key}"`, {
|
|
11490
|
+
hint: ruleSetSelectionHint,
|
|
11491
|
+
});
|
|
11134
11492
|
const existingEntry = syncState?.entities?.blobBuckets?.[key];
|
|
11135
11493
|
const liveBucket = liveBuckets.get(key);
|
|
11136
11494
|
let gate;
|
|
@@ -11601,7 +11959,7 @@ What a push guarantees (two stages):
|
|
|
11601
11959
|
const serverConfig = liveServerConfig(tomlConfig.name);
|
|
11602
11960
|
if (!serverConfig || serverConfig.strictOutput !== true)
|
|
11603
11961
|
continue;
|
|
11604
|
-
const chat =
|
|
11962
|
+
const chat = tomlConfig.chat ?? {};
|
|
11605
11963
|
const retired = String(tomlConfig?.status ?? "").trim().toLowerCase() ===
|
|
11606
11964
|
"archived";
|
|
11607
11965
|
// An entry that stays strict AND live is refused in preflight
|
|
@@ -15835,7 +16193,7 @@ What a push guarantees (two stages):
|
|
|
15835
16193
|
// Update sync state for non-conflicting changes
|
|
15836
16194
|
if (syncState) {
|
|
15837
16195
|
syncState.lastSyncedAt = new Date().toISOString();
|
|
15838
|
-
saveSyncState(
|
|
16196
|
+
saveSyncState(syncState);
|
|
15839
16197
|
}
|
|
15840
16198
|
const successCount = countAppliedChanges({
|
|
15841
16199
|
changes,
|
|
@@ -15866,7 +16224,7 @@ What a push guarantees (two stages):
|
|
|
15866
16224
|
// Update sync state after push
|
|
15867
16225
|
if (syncState) {
|
|
15868
16226
|
syncState.lastSyncedAt = new Date().toISOString();
|
|
15869
|
-
saveSyncState(
|
|
16227
|
+
saveSyncState(syncState);
|
|
15870
16228
|
}
|
|
15871
16229
|
reportDrift();
|
|
15872
16230
|
success(formatPushSummary({
|
|
@@ -15891,7 +16249,7 @@ What a push guarantees (two stages):
|
|
|
15891
16249
|
if (syncState && recordedProgress && !options.dryRun) {
|
|
15892
16250
|
try {
|
|
15893
16251
|
syncState.lastSyncedAt = new Date().toISOString();
|
|
15894
|
-
saveSyncState(
|
|
16252
|
+
saveSyncState(syncState);
|
|
15895
16253
|
}
|
|
15896
16254
|
catch {
|
|
15897
16255
|
// Don't mask the original error
|
|
@@ -15924,7 +16282,7 @@ What a push guarantees (two stages):
|
|
|
15924
16282
|
error(`Config directory not found: ${configDir}`);
|
|
15925
16283
|
process.exit(1);
|
|
15926
16284
|
}
|
|
15927
|
-
const syncState = loadSyncState(
|
|
16285
|
+
const syncState = loadSyncState();
|
|
15928
16286
|
// `--only` scopes the comparison the same way it scopes push and pull.
|
|
15929
16287
|
const diffSelection = readSelection(options);
|
|
15930
16288
|
// `--only app` on its own IS the retired `settings diff`: it reports the
|
|
@@ -16010,7 +16368,6 @@ What a push guarantees (two stages):
|
|
|
16010
16368
|
const remoteIntegrations = new Set(integrationItems.map((i) => i.integrationKey));
|
|
16011
16369
|
const remoteWebhooks = new Set(webhookItems.map((w) => w.webhookKey));
|
|
16012
16370
|
const remoteCronTriggers = new Set(cronTriggerItemsDiff.map((t) => t.triggerKey));
|
|
16013
|
-
const remoteBlobBuckets = new Set((blobBucketsDiffResult.items || []).map((b) => b.bucketKey));
|
|
16014
16371
|
const remotePrompts = new Set(promptItems.map((p) => p.promptKey));
|
|
16015
16372
|
// #1660 — exclude platform-owned workflows (reserved `__internal.*`)
|
|
16016
16373
|
// from the remote set so `diff` never reports them (they are not app
|
|
@@ -16047,7 +16404,6 @@ What a push guarantees (two stages):
|
|
|
16047
16404
|
const localIntegrations = new Set();
|
|
16048
16405
|
const localWebhooks = new Set();
|
|
16049
16406
|
const localCronTriggers = new Set();
|
|
16050
|
-
const localBlobBuckets = new Set();
|
|
16051
16407
|
const localPrompts = new Set();
|
|
16052
16408
|
const localWorkflows = new Set();
|
|
16053
16409
|
const localEmailTemplates = new Set();
|
|
@@ -16123,7 +16479,6 @@ What a push guarantees (two stages):
|
|
|
16123
16479
|
for (const file of readdirSync(blobBucketsDiffDir).filter((f) => f.endsWith(".toml"))) {
|
|
16124
16480
|
const tomlData = parseTomlFile(join(blobBucketsDiffDir, file));
|
|
16125
16481
|
const key = tomlData.bucket?.key || basename(file, ".toml");
|
|
16126
|
-
localBlobBuckets.add(key);
|
|
16127
16482
|
// #2880 — as above.
|
|
16128
16483
|
if (!tomlData.bucket)
|
|
16129
16484
|
tomlData.bucket = {};
|
|
@@ -16476,23 +16831,6 @@ What a push guarantees (two stages):
|
|
|
16476
16831
|
differences.push({ type: "cron-trigger", key, status: remoteOnlyStatus("cronTriggers", key) });
|
|
16477
16832
|
}
|
|
16478
16833
|
}
|
|
16479
|
-
// Blob Buckets — #2880: the same, and the list row is the record here
|
|
16480
|
-
// too.
|
|
16481
|
-
const blobBucketSpec = configDiffSpec("blob-bucket");
|
|
16482
|
-
const remoteBlobBucketRecords = new Map((blobBucketsDiffResult.items || []).map((b) => [b.bucketKey, b]));
|
|
16483
|
-
for (const key of localBlobBuckets) {
|
|
16484
|
-
if (!remoteBlobBuckets.has(key)) {
|
|
16485
|
-
differences.push({ type: "blob-bucket", key, status: localOnlyStatus("blobBuckets", key) });
|
|
16486
|
-
continue;
|
|
16487
|
-
}
|
|
16488
|
-
const { status, hint } = compareLocalToRemote(blobBucketSpec, localBlobBucketsParsed.get(key), remoteBlobBucketRecords.get(key));
|
|
16489
|
-
differences.push({ type: "blob-bucket", key, status, hint });
|
|
16490
|
-
}
|
|
16491
|
-
for (const key of remoteBlobBuckets) {
|
|
16492
|
-
if (!localBlobBuckets.has(key)) {
|
|
16493
|
-
differences.push({ type: "blob-bucket", key, status: remoteOnlyStatus("blobBuckets", key) });
|
|
16494
|
-
}
|
|
16495
|
-
}
|
|
16496
16834
|
// Prompts — #2748: content-aware comparison through the shared registry
|
|
16497
16835
|
// spec, for the reason webhooks got one in #2757. Existence-only
|
|
16498
16836
|
// reporting meant a prompt's `accessRule` (and every other `[prompt]`
|
|
@@ -16918,20 +17256,12 @@ What a push guarantees (two stages):
|
|
|
16918
17256
|
differences.push({ type: typeLabel, key, status: localOnlyStatus(slot, key) });
|
|
16919
17257
|
continue;
|
|
16920
17258
|
}
|
|
16921
|
-
|
|
16922
|
-
|
|
16923
|
-
|
|
16924
|
-
|
|
16925
|
-
|
|
16926
|
-
|
|
16927
|
-
hint = "content not compared (rule sets unavailable)";
|
|
16928
|
-
}
|
|
16929
|
-
else {
|
|
16930
|
-
// The shared verdict names the differing field (#3267) and
|
|
16931
|
-
// degrades a malformed local file to "not compared" rather than
|
|
16932
|
-
// aborting the whole diff.
|
|
16933
|
-
({ status, hint } = compare(parsed, remoteEntities.get(key)));
|
|
16934
|
-
}
|
|
17259
|
+
// Rule-set list failed, so a `ruleSetName` reference can't be
|
|
17260
|
+
// resolved on either side — decline the content comparison rather
|
|
17261
|
+
// than risk a false `Modified`. Otherwise the shared verdict names
|
|
17262
|
+
// the differing field (#3267) and degrades a malformed local file
|
|
17263
|
+
// to "not compared" rather than aborting the whole diff.
|
|
17264
|
+
const { status, hint } = configDiffRowVerdict(parsed, remoteEntities.get(key), compare, contentComparable);
|
|
16935
17265
|
differences.push({ type: typeLabel, key, status, hint });
|
|
16936
17266
|
}
|
|
16937
17267
|
for (const key of remoteEntities.keys()) {
|
|
@@ -16955,6 +17285,9 @@ What a push guarantees (two stages):
|
|
|
16955
17285
|
compareByDefinition("group-type-config", "groupTypeConfigs", localGroupTypeConfigsParsed, remoteGroupTypeConfigs, ruleSetsOk);
|
|
16956
17286
|
compareByDefinition("collection-type-config", "collectionTypeConfigs", localCollectionTypeConfigsParsed, remoteCollectionTypeConfigs, ruleSetsOk);
|
|
16957
17287
|
compareByDefinition("metadata-category-config", "metadataCategoryConfigs", localMetadataCategoryParsed, remoteMetadataCategoryConfigs, true);
|
|
17288
|
+
// Blob buckets — #2880: the list row is the record. #4116: a bucket
|
|
17289
|
+
// names its rule set, so its rows take the type configs' gate.
|
|
17290
|
+
compareByDefinition("blob-bucket", "blobBuckets", localBlobBucketsParsed, new Map((blobBucketsDiffResult.items || []).map((b) => [b.bucketKey, b])), blobBucketsOutcomeDiff.ok && ruleSetsOk);
|
|
16958
17291
|
// database-type-configs: the list carries only the type header, so operations
|
|
16959
17292
|
// and subscriptions are fetched per type (mirroring pull) before the
|
|
16960
17293
|
// content comparison. A per-type fetch failure degrades that one row to
|
|
@@ -17085,12 +17418,16 @@ What a push guarantees (two stages):
|
|
|
17085
17418
|
appRow(VALIDATION_ERROR, `could not parse app.toml: ${err?.message || String(err)}`);
|
|
17086
17419
|
}
|
|
17087
17420
|
if (appTomlData) {
|
|
17088
|
-
const
|
|
17421
|
+
const diffsLocalEnvironment = requireCurrentEnvironment().local;
|
|
17422
|
+
const excludeName = appSettingsNameExcludedForState(syncState, resolvedAppId, appTomlData, diffsLocalEnvironment);
|
|
17089
17423
|
// The SAME validator push's preflight runs (criterion 1): a
|
|
17090
17424
|
// declared-type mismatch, an unrecognized or retired key, and a
|
|
17091
17425
|
// missing required key are reported here with push's own message
|
|
17092
17426
|
// rather than compared past and called Synced.
|
|
17093
|
-
const { settings: localAppSettings, errors: appErrors } = buildAppSettingsPayload(appTomlData, {
|
|
17427
|
+
const { settings: localAppSettings, errors: appErrors } = buildAppSettingsPayload(appTomlData, {
|
|
17428
|
+
excludeName,
|
|
17429
|
+
nameOptional: diffsLocalEnvironment,
|
|
17430
|
+
});
|
|
17094
17431
|
if (appErrors.length > 0) {
|
|
17095
17432
|
appRow(VALIDATION_ERROR, appErrors.join("; "));
|
|
17096
17433
|
}
|
|
@@ -17495,11 +17832,11 @@ What a push guarantees (two stages):
|
|
|
17495
17832
|
writeFileSync(filePath, rewritten);
|
|
17496
17833
|
info(`Migrated database-type-configs/${file}`);
|
|
17497
17834
|
// Update content hash in sync state if we have an entry for it.
|
|
17498
|
-
const syncState = loadSyncState(
|
|
17835
|
+
const syncState = loadSyncState();
|
|
17499
17836
|
const dbType = typeConfig.databaseType || basename(file, ".toml");
|
|
17500
17837
|
if (syncState?.entities?.databaseTypes?.[dbType]) {
|
|
17501
17838
|
syncState.entities.databaseTypes[dbType].contentHash = computeFileHash(filePath);
|
|
17502
|
-
saveSyncState(
|
|
17839
|
+
saveSyncState(syncState);
|
|
17503
17840
|
}
|
|
17504
17841
|
}
|
|
17505
17842
|
migratedCount++;
|
|
@@ -17527,11 +17864,11 @@ What a push guarantees (two stages):
|
|
|
17527
17864
|
writeFileSync(filePath, rewritten);
|
|
17528
17865
|
info(`Migrated workflows/${file}`);
|
|
17529
17866
|
// Update the stored content hash so a subsequent push sees a no-op.
|
|
17530
|
-
const syncState = loadSyncState(
|
|
17867
|
+
const syncState = loadSyncState();
|
|
17531
17868
|
if (syncState?.entities?.workflows?.[workflowKey]) {
|
|
17532
17869
|
syncState.entities.workflows[workflowKey].contentHash =
|
|
17533
17870
|
computeWorkflowContentHash(configDir, workflowKey, parseTomlFile(filePath));
|
|
17534
|
-
saveSyncState(
|
|
17871
|
+
saveSyncState(syncState);
|
|
17535
17872
|
}
|
|
17536
17873
|
}
|
|
17537
17874
|
migratedCount++;
|
|
@@ -17669,14 +18006,26 @@ What a push guarantees (two stages):
|
|
|
17669
18006
|
return;
|
|
17670
18007
|
}
|
|
17671
18008
|
}
|
|
18009
|
+
// A machine-local environment's baseline lives outside the shared tree
|
|
18010
|
+
// (#4117): the snapshot's baseline goes back there, and the tree's own
|
|
18011
|
+
// baseline — the committed environment's — is left exactly as it is.
|
|
18012
|
+
const baselinePath = requireCurrentEnvironment().local
|
|
18013
|
+
? resolveSyncStateWritePath()
|
|
18014
|
+
: undefined;
|
|
17672
18015
|
try {
|
|
17673
|
-
restoreSnapshot(target.path, configDir);
|
|
18016
|
+
restoreSnapshot(target.path, configDir, { baselinePath });
|
|
17674
18017
|
}
|
|
17675
18018
|
catch (err) {
|
|
17676
18019
|
error(`Restore failed: ${err?.message ?? err}`);
|
|
17677
18020
|
process.exit(1);
|
|
17678
18021
|
}
|
|
17679
|
-
|
|
18022
|
+
if (baselinePath) {
|
|
18023
|
+
success(`Restored snapshot ${target.id} into ${configDir} and this environment's baseline into ${baselinePath}.`);
|
|
18024
|
+
info(`The committed baseline in ${configDir} belongs to the shared tree's environment and was left as it was.`);
|
|
18025
|
+
}
|
|
18026
|
+
else {
|
|
18027
|
+
success(`Restored snapshot ${target.id} into ${configDir} (including ${SYNC_STATE_FILENAME}).`);
|
|
18028
|
+
}
|
|
17680
18029
|
info("Run 'primitive config diff' to inspect the restored state versus the server.");
|
|
17681
18030
|
});
|
|
17682
18031
|
}
|