primitive-admin 1.1.0-alpha.78 → 1.1.0-alpha.79
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 +38 -17
- package/assets/skill/skills/primitive-platform/SKILL.md +39 -52
- package/dist/bin/primitive.js +14 -25
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.js +8 -18
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +46 -5
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/auth.js +16 -1
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +1 -9
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/config.js +11 -29
- package/dist/src/commands/config.js.map +1 -1
- package/dist/src/commands/database-type-configs.js +1 -9
- package/dist/src/commands/database-type-configs.js.map +1 -1
- package/dist/src/commands/databases.js +8 -38
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/env.js +27 -32
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/functions.js +197 -23
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/groups.js +1 -9
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/init.js +6 -0
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/rule-sets.js +1 -9
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.js +12 -28
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/sync.d.ts +119 -0
- package/dist/src/commands/sync.js +387 -196
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/workflows.js +2 -14
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +46 -11
- package/dist/src/lib/api-client.js +21 -11
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +24 -57
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +30 -204
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
- package/dist/src/lib/config-json-field.d.ts +28 -0
- package/dist/src/lib/config-json-field.js +56 -0
- package/dist/src/lib/config-json-field.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.js +4 -3
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/config-surface.js +1 -1
- package/dist/src/lib/config-surface.js.map +1 -1
- package/dist/src/lib/config.d.ts +21 -14
- package/dist/src/lib/config.js +25 -44
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/credentials-store.d.ts +31 -8
- package/dist/src/lib/credentials-store.js +52 -13
- package/dist/src/lib/credentials-store.js.map +1 -1
- package/dist/src/lib/env-resolver-core.d.ts +18 -0
- package/dist/src/lib/env-resolver-core.js +26 -3
- package/dist/src/lib/env-resolver-core.js.map +1 -1
- package/dist/src/lib/env-resolver.d.ts +15 -0
- package/dist/src/lib/env-resolver.js +21 -1
- package/dist/src/lib/env-resolver.js.map +1 -1
- package/dist/src/lib/function-collect.d.ts +1 -1
- package/dist/src/lib/function-collect.js +138 -7
- package/dist/src/lib/function-collect.js.map +1 -1
- package/dist/src/lib/function-db-types.d.ts +49 -11
- package/dist/src/lib/function-db-types.js +234 -39
- package/dist/src/lib/function-db-types.js.map +1 -1
- package/dist/src/lib/function-document-types.d.ts +50 -0
- package/dist/src/lib/function-document-types.js +163 -0
- package/dist/src/lib/function-document-types.js.map +1 -0
- package/dist/src/lib/function-log-tail.d.ts +83 -0
- package/dist/src/lib/function-log-tail.js +115 -0
- package/dist/src/lib/function-log-tail.js.map +1 -0
- package/dist/src/lib/function-schema-codegen.d.ts +118 -0
- package/dist/src/lib/function-schema-codegen.js +402 -0
- package/dist/src/lib/function-schema-codegen.js.map +1 -0
- package/dist/src/lib/function-sync.d.ts +85 -2
- package/dist/src/lib/function-sync.js +196 -27
- package/dist/src/lib/function-sync.js.map +1 -1
- package/dist/src/lib/function-triggers.d.ts +9 -3
- package/dist/src/lib/function-triggers.js +12 -9
- package/dist/src/lib/function-triggers.js.map +1 -1
- package/dist/src/lib/function-typecheck.d.ts +86 -0
- package/dist/src/lib/function-typecheck.js +370 -0
- package/dist/src/lib/function-typecheck.js.map +1 -0
- package/dist/src/lib/generated-allowlist.js +4 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +422 -0
- package/dist/src/lib/generated-config-surfaces.js +838 -9
- 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/log-inspection.d.ts +116 -4
- package/dist/src/lib/log-inspection.js +147 -2
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/snapshots.d.ts +6 -7
- package/dist/src/lib/snapshots.js +18 -81
- package/dist/src/lib/snapshots.js.map +1 -1
- package/dist/src/lib/sync-paths.d.ts +29 -53
- package/dist/src/lib/sync-paths.js +46 -97
- package/dist/src/lib/sync-paths.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +8 -3
- package/dist/src/lib/workflow-toml-validator.js +8 -3
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/package.json +3 -3
|
@@ -5,12 +5,14 @@ import { lookup as mimeLookup } from "mime-types";
|
|
|
5
5
|
import { ApiClient, ApiError, ConflictError, SchemaRequiredError, OperationRefError, SchemaBreaksOpsError, SchemaHasUncheckableOpsError, TomlParseError, OpsExistError, } from "../lib/api-client.js";
|
|
6
6
|
import { buildDatabaseTypeTomlData, detectExistingOperationForms, normalizeOperationFromToml, normalizeSubscriptionFromToml, SubscriptionAccessKeyConflictError, } from "../lib/toml-database-config.js";
|
|
7
7
|
import { canEmitNative } from "../lib/toml-native-form.js";
|
|
8
|
+
// #1446's `type: "json"` emission rule, shared with `serializeFunction` since
|
|
9
|
+
// #3317 — see `config-json-field.ts`.
|
|
10
|
+
import { serializeJsonField } from "../lib/config-json-field.js";
|
|
8
11
|
import { parseMetadataCategoryToml, validateMetadataCategoryConfig, serializeMetadataCategoryConfig, parseDeclaredAccessManifestToml, validateDeclaredAccessManifest, serializeDeclaredAccessManifest, } from "../lib/toml-metadata-config.js";
|
|
9
12
|
import { validateOperations, formatIssue, } from "../lib/toml-params-validator.js";
|
|
10
13
|
import { isPlatformOwnedWorkflow, isPlatformOwnedWorkflowKey, } from "../lib/platform-owned.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { resolveSyncDir, resolveSnapshotsRoot, assertSyncStateLayout, syncStatePath, SYNC_STATE_FILENAME, isAutoResolvedSyncDir, checkLegacySyncMigration, checkLegacyDatabaseTypesDir, } from "../lib/sync-paths.js";
|
|
14
|
+
import { getCurrentAppId, getServerUrl } from "../lib/config.js";
|
|
15
|
+
import { resolveSyncDir, resolveSnapshotsRoot, assertSyncStateLayout, syncStatePath, SYNC_STATE_FILENAME, checkLegacyDatabaseTypesDir, } from "../lib/sync-paths.js";
|
|
14
16
|
import { createSnapshot, listSnapshots, resolveSnapshot, restoreSnapshot, pruneSnapshots, } from "../lib/snapshots.js";
|
|
15
17
|
import { validateWorkflowToml, collectWorkflowTomlNotes, formatWorkflowTomlErrors, validateWorkflowExpressionsToml, validateWorkflowExpressionsRawToml, } from "../lib/workflow-toml-validator.js";
|
|
16
18
|
import { expandWorkflowTomlData } from "../lib/workflow-fragments.js";
|
|
@@ -35,7 +37,7 @@ import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, toml
|
|
|
35
37
|
import { CONFIG_SURFACES, isEmptyForEmit, } from "../lib/generated-config-surfaces.js";
|
|
36
38
|
// #3179 — the function's two halves: the header payloads and the envelope
|
|
37
39
|
// `config push` sends, and the bytes `config pull` writes back.
|
|
38
|
-
import { applyFunctionPull, buildFunctionConfigPush, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
40
|
+
import { applyFunctionPull, buildFunctionConfigPush, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlPreflightErrors, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
39
41
|
import { FunctionBuildError } from "../lib/function-bundle.js";
|
|
40
42
|
// #3182 — the local half of capability validation. The grammar is the vendored
|
|
41
43
|
// module the server also imports; this is where an author's own file is read.
|
|
@@ -43,6 +45,12 @@ import { configObjectKeysFromTree, functionGrantErrors, mergeKnownGrantTargets,
|
|
|
43
45
|
// #3182 — the typed handle's generated declarations and the compiler wiring
|
|
44
46
|
// that makes an editor load them.
|
|
45
47
|
import { functionTypeWiringProblems, staleFunctionTypeArtifacts, writeFunctionTypeArtifacts, } from "../lib/function-db-types.js";
|
|
48
|
+
// #3281 — the functions declaration and the per-key invokers ride the same
|
|
49
|
+
// plan; a malformed schema is their one refusal, named by file.
|
|
50
|
+
import { FunctionSchemaCodegenError } from "../lib/function-schema-codegen.js";
|
|
51
|
+
// #3331 — and the compiler is actually asked: a push holds the code it ships
|
|
52
|
+
// to the declarations it just generated.
|
|
53
|
+
import { createFunctionTypechecker, } from "../lib/function-typecheck.js";
|
|
46
54
|
import { detectLayout, migrateLegacyToV2 } from "../lib/block-layout.js";
|
|
47
55
|
import { ABSENT_FROM_EXPORT, LOCAL_ONLY_ABSENT, LOCAL_ONLY_NEW, PUSH_PRUNE_SPECS, REMOTE_ONLY_UNMANAGED, REMOTE_ONLY_WILL_DELETE, SYNC_RESOURCE_TYPES, VALIDATION_ERROR, applyPrune, attachmentWritePath, attemptFetch, classifyLocalOnly, classifyPruneDeleteFailure, classifyRemoteOnly, collectPushPruneCandidates, decidePushPruneCandidate, formatPrunePlan, hasUncommittedChanges, isSafeAttachmentFilename, isSafeSyncFilename, planPrune, preserveUnwrittenEntries, removePrunedSidecar, ruleSetFileKey, } from "../lib/sync-resource-types.js";
|
|
48
56
|
import { normalizeTestCaseKey, testCaseKeyError, } from "../lib/test-case-keys.js";
|
|
@@ -1946,50 +1954,6 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
|
|
|
1946
1954
|
},
|
|
1947
1955
|
});
|
|
1948
1956
|
}
|
|
1949
|
-
/**
|
|
1950
|
-
* Emit a `type: "json"` field (a JSON Schema, a provider-options blob) as TOML.
|
|
1951
|
-
*
|
|
1952
|
-
* Issue #1446: pull used to `JSON.stringify` schema fields into a single
|
|
1953
|
-
* escaped-JSON line — unreadable and undiffable. We now emit the parsed value
|
|
1954
|
-
* as a native table whenever it round-trips faithfully
|
|
1955
|
-
* (`canEmitNative === null`), falling back to the JSON string only for the
|
|
1956
|
-
* values TOML silently drops (`null` / `undefined` anywhere inside). The empty
|
|
1957
|
-
* object `{}` is a real, distinct value (for a schema: "accepts anything") and
|
|
1958
|
-
* emits as an empty table, so it survives a pull → push cycle instead of being
|
|
1959
|
-
* cleared server-side.
|
|
1960
|
-
*
|
|
1961
|
-
* Returns `undefined` for an absent value (the key is then omitted). The
|
|
1962
|
-
* sponsor-approved decision is to always move to native aggressively — no
|
|
1963
|
-
* form preservation — so this does not consult the existing file's form.
|
|
1964
|
-
*
|
|
1965
|
-
* `subject` names the object in the fallback warning, e.g. `Workflow 'greet'`.
|
|
1966
|
-
*/
|
|
1967
|
-
function serializeJsonField(value, fieldName, subject, log) {
|
|
1968
|
-
if (value === undefined || value === null)
|
|
1969
|
-
return undefined;
|
|
1970
|
-
// JSON fields arrive from the server GET parsed; defensively accept a JSON
|
|
1971
|
-
// string too (e.g. an inline-authored file re-serialized here).
|
|
1972
|
-
let obj = value;
|
|
1973
|
-
if (typeof value === "string") {
|
|
1974
|
-
try {
|
|
1975
|
-
obj = JSON.parse(value);
|
|
1976
|
-
}
|
|
1977
|
-
catch {
|
|
1978
|
-
// Not valid JSON — keep the original string as the emitted value.
|
|
1979
|
-
return value;
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
if (obj && typeof obj === "object") {
|
|
1983
|
-
const reason = canEmitNative(obj);
|
|
1984
|
-
if (reason) {
|
|
1985
|
-
log(`${subject}: cannot emit \`${fieldName}\` as native TOML (${reason}); using JSON string.`);
|
|
1986
|
-
return JSON.stringify(obj);
|
|
1987
|
-
}
|
|
1988
|
-
return obj;
|
|
1989
|
-
}
|
|
1990
|
-
// A non-object value (unexpected) — preserve it as a JSON string.
|
|
1991
|
-
return JSON.stringify(obj);
|
|
1992
|
-
}
|
|
1993
1957
|
/** The `[workflow]` case of `serializeJsonField` (#1446, message unchanged). */
|
|
1994
1958
|
function serializeWorkflowSchemaField(value, fieldName, workflowKey, log) {
|
|
1995
1959
|
return serializeJsonField(value, fieldName, `Workflow '${workflowKey}'`, log);
|
|
@@ -2168,6 +2132,11 @@ function localConfigRowKey(label, tomlData, file) {
|
|
|
2168
2132
|
return stated("prompt", "key");
|
|
2169
2133
|
case "workflow":
|
|
2170
2134
|
return stated("workflow", "key");
|
|
2135
|
+
// #3320 — the declared key, like the function preflight collector, the
|
|
2136
|
+
// apply loop and `--only`. A renamed file is one object, so both
|
|
2137
|
+
// collectors' messages land on the one comparison row.
|
|
2138
|
+
case "function":
|
|
2139
|
+
return stated("function", "key");
|
|
2171
2140
|
case "email-template":
|
|
2172
2141
|
return stated("template", "emailType");
|
|
2173
2142
|
case "database-type-config":
|
|
@@ -2251,6 +2220,75 @@ export function collectConfigFileValidationErrors(configDir) {
|
|
|
2251
2220
|
}
|
|
2252
2221
|
return found;
|
|
2253
2222
|
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Every `functions/<key>.toml` whose file-decidable errors `config push` would
|
|
2225
|
+
* refuse (#3320).
|
|
2226
|
+
*
|
|
2227
|
+
* A second collector rather than a branch inside
|
|
2228
|
+
* `collectConfigFileValidationErrors`, and deliberately so: push calls that one
|
|
2229
|
+
* UNBOUNDED over the whole tree, which is right for the checks it runs (a
|
|
2230
|
+
* mistyped key anywhere means the tree is not pushable) and wrong for these — a
|
|
2231
|
+
* `config push --only webhook/<key>` must not be blocked by a broken function
|
|
2232
|
+
* the operator did not select (#2645). The push path therefore runs these
|
|
2233
|
+
* inside its own `selectFiles`-bounded function loop, and `config diff`, which
|
|
2234
|
+
* compares the whole tree by construction, runs them from here.
|
|
2235
|
+
*
|
|
2236
|
+
* A file that does not parse is skipped: the per-type loop that reads it names
|
|
2237
|
+
* the parse error itself, and reporting it twice helps nobody.
|
|
2238
|
+
*/
|
|
2239
|
+
export function collectFunctionTomlValidationErrors(configDir) {
|
|
2240
|
+
const found = [];
|
|
2241
|
+
const functionsDir = join(configDir, "functions");
|
|
2242
|
+
if (!existsSync(functionsDir))
|
|
2243
|
+
return found;
|
|
2244
|
+
for (const file of readdirSync(functionsDir).filter((f) => f.endsWith(".toml"))) {
|
|
2245
|
+
const filePath = join(functionsDir, file);
|
|
2246
|
+
let tomlData;
|
|
2247
|
+
try {
|
|
2248
|
+
tomlData = parseTomlFile(filePath);
|
|
2249
|
+
}
|
|
2250
|
+
catch {
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
// The DECLARED key, like the apply loop and `--only` selection: a file
|
|
2254
|
+
// whose name and `[function].key` differ is one object, not two.
|
|
2255
|
+
const key = tomlData?.function?.key || basename(file, ".toml");
|
|
2256
|
+
const messages = functionTomlPreflightErrors({
|
|
2257
|
+
filePath,
|
|
2258
|
+
functionKey: key,
|
|
2259
|
+
tomlData,
|
|
2260
|
+
});
|
|
2261
|
+
if (messages.length === 0)
|
|
2262
|
+
continue;
|
|
2263
|
+
found.push({ type: "function", key, filePath, messages });
|
|
2264
|
+
}
|
|
2265
|
+
return found;
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* The collectors' rows, one per diff row (#2880 symmetry, #3320).
|
|
2269
|
+
*
|
|
2270
|
+
* Two collectors reach the same function file — the definition-driven one
|
|
2271
|
+
* every per-entity type runs, and the function preflight — and a file can
|
|
2272
|
+
* break both at once (a mistyped `entrty`, which then leaves no `entry`).
|
|
2273
|
+
* Push reports every message from both, so an overlay that let one row's
|
|
2274
|
+
* messages REPLACE the other's would drop a diagnostic push prints, which is
|
|
2275
|
+
* exactly the asymmetry #2880 forbids. Merging by row keeps the two commands
|
|
2276
|
+
* reading identically.
|
|
2277
|
+
*/
|
|
2278
|
+
export function mergeConfigFileValidationErrors(collected) {
|
|
2279
|
+
const byRow = new Map();
|
|
2280
|
+
for (const rejected of collected) {
|
|
2281
|
+
const rowId = `${rejected.type}${rejected.key}`;
|
|
2282
|
+
const merged = byRow.get(rowId);
|
|
2283
|
+
if (!merged) {
|
|
2284
|
+
byRow.set(rowId, { ...rejected, messages: [...rejected.messages] });
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
// Collection order is the message order, the same order push prints.
|
|
2288
|
+
merged.messages.push(...rejected.messages);
|
|
2289
|
+
}
|
|
2290
|
+
return [...byRow.values()];
|
|
2291
|
+
}
|
|
2254
2292
|
/**
|
|
2255
2293
|
* Every `<key>.tests/*.toml` sidecar under a config directory.
|
|
2256
2294
|
*
|
|
@@ -3335,6 +3373,10 @@ export function classifyPushChange(input) {
|
|
|
3335
3373
|
const { localHash, remoteHash, baselineHash, legacy } = input;
|
|
3336
3374
|
if (localHash === remoteHash)
|
|
3337
3375
|
return "unchanged";
|
|
3376
|
+
// Ahead of both the baseline and the legacy inference, which are ways of
|
|
3377
|
+
// GUESSING what this states outright.
|
|
3378
|
+
if (input.serverUnchangedSinceSync === true)
|
|
3379
|
+
return "local-edited";
|
|
3338
3380
|
if (baselineHash) {
|
|
3339
3381
|
if (remoteHash === baselineHash)
|
|
3340
3382
|
return "local-edited";
|
|
@@ -3499,7 +3541,7 @@ const CONFIG_DIFF_SPECS = {
|
|
|
3499
3541
|
describe: (entity) => `email template "${entity?.emailType}"`,
|
|
3500
3542
|
},
|
|
3501
3543
|
// #3179 — server functions. The compared entity spans the header AND the
|
|
3502
|
-
//
|
|
3544
|
+
// keys the immutable version carries (`entry`, `mode`, `limits`, …),
|
|
3503
3545
|
// because an author writes one file and a version is never authored
|
|
3504
3546
|
// separately: comparing only the header would report a changed entry point as
|
|
3505
3547
|
// Synced. The SOURCE bytes are a second axis the table cannot express — they
|
|
@@ -3509,13 +3551,22 @@ const CONFIG_DIFF_SPECS = {
|
|
|
3509
3551
|
table: SERVER_FUNCTION_TABLE,
|
|
3510
3552
|
parse: (doc) => functionDiffEntity(doc),
|
|
3511
3553
|
// `extra` is the ACTIVE config version, which is where the remote side's
|
|
3512
|
-
// entry/
|
|
3554
|
+
// entry/mode/limits live; the header row does not carry them.
|
|
3513
3555
|
serialize: (record, _maps, extra) => serializeFunction(record, extra),
|
|
3514
3556
|
// The version-carried keys are not fields of the header table, so they
|
|
3515
3557
|
// travel as extras — present on both sides or absent on both.
|
|
3516
3558
|
extras: (entity) => ({
|
|
3559
|
+
// #3317 — `inputSchema` / `outputSchema` compared as the values they
|
|
3560
|
+
// encode, the rule prompts have had since #2748. A schema reaches the
|
|
3561
|
+
// two sides in either spelling (a native `[function.inputSchema]` table,
|
|
3562
|
+
// or the JSON string the server stores and the fallback pull writes for
|
|
3563
|
+
// a schema TOML cannot hold), and neither spelling is a difference.
|
|
3564
|
+
...jsonFieldsForDiff(SERVER_FUNCTION_TABLE, entity),
|
|
3517
3565
|
entry: entity.entry ?? null,
|
|
3518
|
-
|
|
3566
|
+
// #3281 — the RESOLVED mode (`functionDiffEntity` reads both spellings),
|
|
3567
|
+
// so a legacy `durable = true` file and a server serializing `mode`
|
|
3568
|
+
// compare equal.
|
|
3569
|
+
mode: entity.mode ?? null,
|
|
3519
3570
|
limits: entity.limits ?? null,
|
|
3520
3571
|
// #3182 — the grants are version-carried too, and a widened grant set is
|
|
3521
3572
|
// exactly the change a reviewer must not read as Synced. Sorted because
|
|
@@ -3987,17 +4038,32 @@ export function decidePushForConfig(input) {
|
|
|
3987
4038
|
fields,
|
|
3988
4039
|
};
|
|
3989
4040
|
}
|
|
4041
|
+
// The recorded server timestamp still being the live one. It is the legacy
|
|
4042
|
+
// path's inference signal, and — where the row's timestamp covers the whole
|
|
4043
|
+
// projection — #3317's outright statement that the server did not move.
|
|
4044
|
+
const liveModifiedAtMatchesStored = entry?.modifiedAt !== undefined &&
|
|
4045
|
+
live.modifiedAt !== undefined &&
|
|
4046
|
+
entry.modifiedAt === live.modifiedAt;
|
|
4047
|
+
// The recorded version pointer still being the live one — or no pointer
|
|
4048
|
+
// recorded, which is no claim. A pointer that moved is a server change by
|
|
4049
|
+
// definition, whatever the two timestamps say (#3317: the baseline's
|
|
4050
|
+
// timestamp comes from a refetch a concurrent activation can precede).
|
|
4051
|
+
const liveActiveConfigMatchesStored = entry?.activeConfigId === undefined ||
|
|
4052
|
+
(live.record?.activeConfigId !== undefined &&
|
|
4053
|
+
live.record?.activeConfigId !== null &&
|
|
4054
|
+
String(entry.activeConfigId) === String(live.record.activeConfigId));
|
|
3990
4055
|
const direction = classifyPushChange({
|
|
3991
4056
|
localHash,
|
|
3992
4057
|
remoteHash,
|
|
3993
4058
|
baselineHash: entry?.semanticHash,
|
|
4059
|
+
serverUnchangedSinceSync: input.serverModifiedAtCoversProjection === true &&
|
|
4060
|
+
liveModifiedAtMatchesStored &&
|
|
4061
|
+
liveActiveConfigMatchesStored,
|
|
3994
4062
|
legacy: {
|
|
3995
4063
|
localBytesMatchStoredContentHash: entry?.contentHash !== undefined &&
|
|
3996
4064
|
localFileHash !== undefined &&
|
|
3997
4065
|
entry.contentHash === localFileHash,
|
|
3998
|
-
liveModifiedAtMatchesStored
|
|
3999
|
-
live.modifiedAt !== undefined &&
|
|
4000
|
-
entry.modifiedAt === live.modifiedAt,
|
|
4066
|
+
liveModifiedAtMatchesStored,
|
|
4001
4067
|
},
|
|
4002
4068
|
});
|
|
4003
4069
|
// #2909 — the adopt, for a type whose push path adopts by key. `entry` is
|
|
@@ -4807,6 +4873,55 @@ selects = () => true) {
|
|
|
4807
4873
|
: { ok: false };
|
|
4808
4874
|
return { scriptEntities, count: written, presence, listOk: true, serverKeys };
|
|
4809
4875
|
}
|
|
4876
|
+
/**
|
|
4877
|
+
* The manifest row `config push` records for a function it just pushed
|
|
4878
|
+
* (#3317), and whether the server had already moved past that push.
|
|
4879
|
+
*
|
|
4880
|
+
* The header is refetched AFTER the version push, so the row's `modifiedAt`
|
|
4881
|
+
* is the live timestamp — but the version pointer and the envelope are the
|
|
4882
|
+
* push's own: the manifest names what THIS run activated, not whatever the
|
|
4883
|
+
* refetch happened to find. A concurrent activation landing between the push
|
|
4884
|
+
* and the refetch is exactly the case those two must not be conflated in:
|
|
4885
|
+
* recording the refetched pointer would make the other operator's version the
|
|
4886
|
+
* baseline the local file is then read as a plain edit of, and the next push
|
|
4887
|
+
* would roll it back without `--force`. Recording the pushed pointer beside
|
|
4888
|
+
* the live timestamp is what `decidePushForConfig` reads as "the pointer
|
|
4889
|
+
* moved" — a refusal, until the operator pulls or forces.
|
|
4890
|
+
*
|
|
4891
|
+
* `movedTo` names the version the refetched header points at when it is not
|
|
4892
|
+
* the pushed one, so the caller can say so at the moment it is known rather
|
|
4893
|
+
* than leaving it for the next push to report.
|
|
4894
|
+
*/
|
|
4895
|
+
export function pushedFunctionBaseline(input) {
|
|
4896
|
+
const { refreshed, pushedVersion } = input;
|
|
4897
|
+
const pushedConfigId = pushedVersion?.configId !== undefined && pushedVersion?.configId !== null
|
|
4898
|
+
? String(pushedVersion.configId)
|
|
4899
|
+
: undefined;
|
|
4900
|
+
const liveConfigId = refreshed?.activeConfigId !== undefined && refreshed?.activeConfigId !== null
|
|
4901
|
+
? String(refreshed.activeConfigId)
|
|
4902
|
+
: undefined;
|
|
4903
|
+
const movedTo = pushedConfigId !== undefined &&
|
|
4904
|
+
liveConfigId !== undefined &&
|
|
4905
|
+
liveConfigId !== pushedConfigId
|
|
4906
|
+
? liveConfigId
|
|
4907
|
+
: null;
|
|
4908
|
+
return {
|
|
4909
|
+
entity: {
|
|
4910
|
+
id: input.functionId,
|
|
4911
|
+
modifiedAt: refreshed?.modifiedAt ||
|
|
4912
|
+
input.existingModifiedAt ||
|
|
4913
|
+
new Date().toISOString(),
|
|
4914
|
+
contentHash: input.contentHash,
|
|
4915
|
+
semanticHash: refreshed
|
|
4916
|
+
? hashRemoteConfigForDiff(input.spec, refreshed, input.maps, pushedVersion ?? undefined)
|
|
4917
|
+
: undefined,
|
|
4918
|
+
envelopeHash: input.envelopeHash,
|
|
4919
|
+
activeConfigId: pushedConfigId,
|
|
4920
|
+
sources: input.sources,
|
|
4921
|
+
},
|
|
4922
|
+
movedTo,
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4810
4925
|
/**
|
|
4811
4926
|
* Pull server-side `ServerFunction` rows into `functions/` (#3179).
|
|
4812
4927
|
*
|
|
@@ -4864,7 +4979,10 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
4864
4979
|
};
|
|
4865
4980
|
if (!record.activeConfigId) {
|
|
4866
4981
|
// No code pushed yet: the header is all there is to write.
|
|
4867
|
-
|
|
4982
|
+
// The logger carries #1446's one warning: a schema holding a `null`
|
|
4983
|
+
// anywhere cannot be a native table, so it is written as a JSON string
|
|
4984
|
+
// and the operator is told which key that happened to.
|
|
4985
|
+
writeFileSync(filePath, serializeFunction(record, undefined, logger));
|
|
4868
4986
|
entity.contentHash = computeFileHash(filePath);
|
|
4869
4987
|
entity.semanticHash = hashRemoteConfigForDiff(configDiffSpec("function"), record, { ruleSetIdToName: new Map(), ruleSetNameToId: new Map() });
|
|
4870
4988
|
functionEntities[key] = entity;
|
|
@@ -6426,25 +6544,15 @@ export function registerConfigSyncCommands(sync) {
|
|
|
6426
6544
|
// Init
|
|
6427
6545
|
sync
|
|
6428
6546
|
.command("init")
|
|
6547
|
+
.allowExcessArguments(false)
|
|
6429
6548
|
.description("Initialize the config directory structure")
|
|
6430
|
-
.
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
.action(async (appId, options) => {
|
|
6434
|
-
// Refused before any tree read/write or request (#3153, F1): the tree
|
|
6435
|
-
// path no longer partitions by app.
|
|
6436
|
-
assertTreeApp(appId || options.app, "config init");
|
|
6437
|
-
const resolvedAppId = resolveAppId(appId, options);
|
|
6438
|
-
const configDir = resolveSyncDir({ userDir: options.dir });
|
|
6549
|
+
.action(async (options) => {
|
|
6550
|
+
const resolvedAppId = getCurrentAppId();
|
|
6551
|
+
const configDir = resolveSyncDir();
|
|
6439
6552
|
// A baseline that was never renamed means an unmigrated tree; refused
|
|
6440
6553
|
// here, before any request, snapshot or directory is made (#3153).
|
|
6441
6554
|
assertSyncStateLayout(configDir);
|
|
6442
|
-
|
|
6443
|
-
info(`Using per-environment config directory: ${configDir}`);
|
|
6444
|
-
const legacyHint = checkLegacySyncMigration(configDir);
|
|
6445
|
-
if (legacyHint)
|
|
6446
|
-
warn(legacyHint);
|
|
6447
|
-
}
|
|
6555
|
+
info(`Using per-environment config directory: ${configDir}`);
|
|
6448
6556
|
ensureDir(configDir);
|
|
6449
6557
|
ensureDir(join(configDir, "integrations"));
|
|
6450
6558
|
ensureDir(join(configDir, "webhooks"));
|
|
@@ -6473,10 +6581,8 @@ export function registerConfigSyncCommands(sync) {
|
|
|
6473
6581
|
// Pull
|
|
6474
6582
|
sync
|
|
6475
6583
|
.command("pull")
|
|
6584
|
+
.allowExcessArguments(false)
|
|
6476
6585
|
.description("Download configuration from server to TOML files")
|
|
6477
|
-
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
6478
|
-
.option("--app <app-id>", "App ID")
|
|
6479
|
-
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
6480
6586
|
.option("--json", "Output as JSON instead of writing files")
|
|
6481
6587
|
.option("--only <selectors>", "Fetch just these objects: <type>/<key>, app, or var/<key> (repeatable, or comma-separated). Everything else in the slot is left as it is.",
|
|
6482
6588
|
// Collect rather than replace, the same way push does: commander keeps
|
|
@@ -6484,20 +6590,14 @@ export function registerConfigSyncCommands(sync) {
|
|
|
6484
6590
|
// narrow the pull to one object and report success.
|
|
6485
6591
|
(value, previous) => [...(previous ?? []), value], [])
|
|
6486
6592
|
.option("--no-prune", "Keep local files for entities that are absent from the sync export (the next push will re-create them)")
|
|
6487
|
-
.action(async (
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
assertTreeApp(appId || options.app, "config pull");
|
|
6491
|
-
const resolvedAppId = resolveAppId(appId, options);
|
|
6492
|
-
const configDir = resolveSyncDir({ userDir: options.dir });
|
|
6593
|
+
.action(async (options) => {
|
|
6594
|
+
const resolvedAppId = getCurrentAppId();
|
|
6595
|
+
const configDir = resolveSyncDir();
|
|
6493
6596
|
// A baseline that was never renamed means an unmigrated tree; refused
|
|
6494
6597
|
// here, before any request, snapshot or directory is made (#3153).
|
|
6495
6598
|
assertSyncStateLayout(configDir);
|
|
6496
|
-
if (
|
|
6599
|
+
if (!options.json) {
|
|
6497
6600
|
info(`Using per-environment config directory: ${configDir}`);
|
|
6498
|
-
const legacyHint = checkLegacySyncMigration(configDir);
|
|
6499
|
-
if (legacyHint)
|
|
6500
|
-
warn(legacyHint);
|
|
6501
6601
|
}
|
|
6502
6602
|
// Issue #2760: pull writes the renamed directory and leaves the old one
|
|
6503
6603
|
// untouched beside it, which reads as a successful pull that lost the
|
|
@@ -6697,10 +6797,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
6697
6797
|
// perform it without a recoverable backup. No-op on a fresh/empty
|
|
6698
6798
|
// sync dir (nothing to back up). Prune to 28 days after success.
|
|
6699
6799
|
try {
|
|
6700
|
-
const snapshotsRoot = resolveSnapshotsRoot({
|
|
6701
|
-
appId: resolvedAppId,
|
|
6702
|
-
userDir: options.dir,
|
|
6703
|
-
});
|
|
6800
|
+
const snapshotsRoot = resolveSnapshotsRoot({ appId: resolvedAppId });
|
|
6704
6801
|
const snapshot = createSnapshot(configDir, snapshotsRoot);
|
|
6705
6802
|
if (snapshot) {
|
|
6706
6803
|
info(` Snapshot: ${snapshot.path}`);
|
|
@@ -6711,7 +6808,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
6711
6808
|
error(`Failed to create a pre-pull snapshot: ${snapErr?.message ?? snapErr}`);
|
|
6712
6809
|
error("Aborting pull before any local file is modified. " +
|
|
6713
6810
|
"Resolve the snapshot error (e.g. disk space / permissions) and retry, " +
|
|
6714
|
-
"
|
|
6811
|
+
"and that the project's .primitive/ is writable.");
|
|
6715
6812
|
process.exit(1);
|
|
6716
6813
|
}
|
|
6717
6814
|
// Ensure directories exist
|
|
@@ -7284,12 +7381,17 @@ export function registerConfigSyncCommands(sync) {
|
|
|
7284
7381
|
// they are not in the envelope, and a fresh clone must end up with the
|
|
7285
7382
|
// same three files a push would have written. Written here, after the
|
|
7286
7383
|
// database types, so the declaration describes what this pull wrote.
|
|
7287
|
-
{
|
|
7384
|
+
try {
|
|
7288
7385
|
const artifacts = writeFunctionTypeArtifacts(configDir);
|
|
7289
7386
|
if (artifacts.length > 0) {
|
|
7290
7387
|
info(` Wrote function types: ${artifacts.join(", ")}`);
|
|
7291
7388
|
}
|
|
7292
7389
|
}
|
|
7390
|
+
catch (err) {
|
|
7391
|
+
if (!(err instanceof FunctionSchemaCodegenError))
|
|
7392
|
+
throw err;
|
|
7393
|
+
warn(` Function types not generated: ${err.message}`);
|
|
7394
|
+
}
|
|
7293
7395
|
// Write rule sets
|
|
7294
7396
|
const ruleSetEntities = {};
|
|
7295
7397
|
for (const ruleSet of ruleSets) {
|
|
@@ -7768,10 +7870,8 @@ export function registerConfigSyncCommands(sync) {
|
|
|
7768
7870
|
// Push
|
|
7769
7871
|
sync
|
|
7770
7872
|
.command("push")
|
|
7873
|
+
.allowExcessArguments(false)
|
|
7771
7874
|
.description("Upload local TOML configuration to server")
|
|
7772
|
-
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
7773
|
-
.option("--app <app-id>", "App ID")
|
|
7774
|
-
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
7775
7875
|
.option("--dry-run", "Show what would be changed without applying")
|
|
7776
7876
|
.option("--force", "Overwrite remote even if modified since last pull")
|
|
7777
7877
|
.option("--only <selectors>", "Apply just these objects: <type>/<key>, app, or var/<key> (repeatable, or comma-separated). Everything else about push is unchanged.",
|
|
@@ -7782,21 +7882,35 @@ export function registerConfigSyncCommands(sync) {
|
|
|
7782
7882
|
.option("--prune", "Delete remote entities whose local TOML was removed since the last sync (issue #1655)")
|
|
7783
7883
|
.option("-y, --yes", "Skip the interactive confirmation before pruning (for CI / automation)")
|
|
7784
7884
|
.option("--accept-warnings", "Commit schema diffs that have operations with dynamic refs (issue #666 SCHEMA_HAS_UNCHECKABLE_OPS escape hatch)")
|
|
7785
|
-
|
|
7786
|
-
//
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7885
|
+
// #3320 — what this command actually guarantees, said where it is run.
|
|
7886
|
+
// Two stages, and only the first is all-or-nothing.
|
|
7887
|
+
.addHelpText("after", `
|
|
7888
|
+
What a push guarantees (two stages):
|
|
7889
|
+
|
|
7890
|
+
Preflight — the local tree only. Unknown keys and declared types, workflow
|
|
7891
|
+
lints, function grant grammar, a function's \`entry\`, the function trigger
|
|
7892
|
+
rules, and a webhook trigger's signing-secret requirement and reference
|
|
7893
|
+
shape. Any error among them aborts the whole run and nothing is applied.
|
|
7894
|
+
Those are the server's own rules, run against your files.
|
|
7895
|
+
|
|
7896
|
+
Apply — incremental and convergent, and there is no rollback. A rejection
|
|
7897
|
+
may stop the remaining work depending on the type; whatever was already
|
|
7898
|
+
applied stays applied; the exit status is non-zero; re-running converges.
|
|
7899
|
+
A refused FUNCTION is recorded and the loop continues, so the rest of the
|
|
7900
|
+
tree still lands.
|
|
7901
|
+
|
|
7902
|
+
Not preflighted, because the file cannot answer it: whether a referenced
|
|
7903
|
+
secret exists, whether a scheme's config is valid under this environment's
|
|
7904
|
+
policy, per-app caps, key collisions. Those surface during apply.
|
|
7905
|
+
`)
|
|
7906
|
+
.option("--no-typecheck", "Ship function code without typechecking it against the declarations this push generates (issue #3331)")
|
|
7907
|
+
.action(async (options) => {
|
|
7908
|
+
const resolvedAppId = getCurrentAppId();
|
|
7909
|
+
const configDir = resolveSyncDir();
|
|
7791
7910
|
// A baseline that was never renamed means an unmigrated tree; refused
|
|
7792
7911
|
// here, before any request, snapshot or directory is made (#3153).
|
|
7793
7912
|
assertSyncStateLayout(configDir);
|
|
7794
|
-
|
|
7795
|
-
info(`Using per-environment config directory: ${configDir}`);
|
|
7796
|
-
const legacyHint = checkLegacySyncMigration(configDir);
|
|
7797
|
-
if (legacyHint)
|
|
7798
|
-
warn(legacyHint);
|
|
7799
|
-
}
|
|
7913
|
+
info(`Using per-environment config directory: ${configDir}`);
|
|
7800
7914
|
// Issue #2760. An un-renamed checkout's `database-types/` is ignored, so
|
|
7801
7915
|
// an ordinary push applies nothing from it — worth a warning. `--prune`
|
|
7802
7916
|
// is the destructive case: it flags every prior-state key whose local
|
|
@@ -8486,6 +8600,22 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8486
8600
|
})) {
|
|
8487
8601
|
preflightValidationErrors.push(` ${message}`);
|
|
8488
8602
|
}
|
|
8603
|
+
// #3320 — and everything else the file decides on its own: the
|
|
8604
|
+
// `entry`, the mode the two spellings resolve to, the trigger
|
|
8605
|
+
// rules, and a webhook trigger's signing-secret requirement and
|
|
8606
|
+
// reference shape. These used to be raised inside the apply loop,
|
|
8607
|
+
// after webhooks, integrations, vars and database types had
|
|
8608
|
+
// already been written — the partial tree this issue is about.
|
|
8609
|
+
// Bounded by the same `selectFiles` above, so `--only` keeps its
|
|
8610
|
+
// posture (#2645).
|
|
8611
|
+
const functionKey = tomlData?.function?.key || basename(file, ".toml");
|
|
8612
|
+
for (const message of functionTomlPreflightErrors({
|
|
8613
|
+
filePath,
|
|
8614
|
+
functionKey,
|
|
8615
|
+
tomlData,
|
|
8616
|
+
})) {
|
|
8617
|
+
preflightValidationErrors.push(` ${filePath}: ${message}`);
|
|
8618
|
+
}
|
|
8489
8619
|
}
|
|
8490
8620
|
}
|
|
8491
8621
|
}
|
|
@@ -12946,18 +13076,44 @@ export function registerConfigSyncCommands(sync) {
|
|
|
12946
13076
|
// TREE rather than from the server: the types describe the schemas
|
|
12947
13077
|
// this push is applying. Regenerated every time and never part of the
|
|
12948
13078
|
// envelope — `config pull` recreates them the same way.
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
12952
|
-
|
|
13079
|
+
// #3281 — a malformed schema in a function file cannot be typed;
|
|
13080
|
+
// the file's own push below refuses it by name, so the generators
|
|
13081
|
+
// warn here and the rest of the tree still lands.
|
|
13082
|
+
try {
|
|
13083
|
+
if (!options.dryRun) {
|
|
13084
|
+
const artifacts = writeFunctionTypeArtifacts(configDir);
|
|
13085
|
+
if (artifacts.length > 0) {
|
|
13086
|
+
info(` Wrote function types: ${artifacts.join(", ")}`);
|
|
13087
|
+
}
|
|
13088
|
+
}
|
|
13089
|
+
// A tsconfig push will not rewrite (comments are the author's) or
|
|
13090
|
+
// cannot read is still a tsconfig no compiler loads the handle
|
|
13091
|
+
// through. Say so rather than report success over it (CR3182-003).
|
|
13092
|
+
for (const problem of functionTypeWiringProblems(configDir)) {
|
|
13093
|
+
warn(` functions/tsconfig.json: ${problem}`);
|
|
12953
13094
|
}
|
|
12954
13095
|
}
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
warn(` functions/tsconfig.json: ${problem}`);
|
|
13096
|
+
catch (err) {
|
|
13097
|
+
if (!(err instanceof FunctionSchemaCodegenError))
|
|
13098
|
+
throw err;
|
|
13099
|
+
warn(` Function types not generated: ${err.message}`);
|
|
12960
13100
|
}
|
|
13101
|
+
// #3331 — the compiler, over the declarations written (or, under
|
|
13102
|
+
// `--dry-run`, planned) just above. Built once for the tree and
|
|
13103
|
+
// consulted per function, so one broken file refuses itself and
|
|
13104
|
+
// nothing else; `--no-typecheck` is the escape hatch, and an
|
|
13105
|
+
// unavailable compiler warns rather than failing the push.
|
|
13106
|
+
let typecheckSetup = null;
|
|
13107
|
+
const functionTypechecker = () => {
|
|
13108
|
+
if (options.typecheck === false)
|
|
13109
|
+
return null;
|
|
13110
|
+
if (typecheckSetup === null) {
|
|
13111
|
+
typecheckSetup = createFunctionTypechecker(configDir);
|
|
13112
|
+
for (const warning of typecheckSetup.warnings)
|
|
13113
|
+
warn(` ${warning}`);
|
|
13114
|
+
}
|
|
13115
|
+
return typecheckSetup.checker;
|
|
13116
|
+
};
|
|
12961
13117
|
const files = selectFiles("function", functionsPushDir, readdirSync(functionsPushDir).filter((f) => f.endsWith(".toml")));
|
|
12962
13118
|
const functionSpec = configDiffSpec("function");
|
|
12963
13119
|
const liveFunctions = new Map();
|
|
@@ -13010,8 +13166,26 @@ export function registerConfigSyncCommands(sync) {
|
|
|
13010
13166
|
});
|
|
13011
13167
|
continue;
|
|
13012
13168
|
}
|
|
13169
|
+
// #3331 — and then the types. esbuild erases them, so a build that
|
|
13170
|
+
// succeeded says nothing about whether the code holds against the
|
|
13171
|
+
// declarations this same push generated: `ctx.user` going nullable
|
|
13172
|
+
// (#3279) bundles cleanly and throws on the request path. Refused
|
|
13173
|
+
// here, against THIS function's own sources, before anything is
|
|
13174
|
+
// sent for it and whether or not this is a dry run.
|
|
13175
|
+
const typechecker = functionTypechecker();
|
|
13176
|
+
if (typechecker) {
|
|
13177
|
+
const failure = typechecker.check(key, push.sources.map((source) => source.path));
|
|
13178
|
+
if (failure) {
|
|
13179
|
+
applyFailures.push({
|
|
13180
|
+
type: "function",
|
|
13181
|
+
key,
|
|
13182
|
+
message: failure.message,
|
|
13183
|
+
});
|
|
13184
|
+
continue;
|
|
13185
|
+
}
|
|
13186
|
+
}
|
|
13013
13187
|
// The active version, needed by BOTH axes: the header comparison
|
|
13014
|
-
// reads `entry`/`
|
|
13188
|
+
// reads `entry`/`mode`/`limits` off it, and the code axis
|
|
13015
13189
|
// compares its `envelopeHash`.
|
|
13016
13190
|
let activeVersion = null;
|
|
13017
13191
|
let versionOk = functionsLiveOk;
|
|
@@ -13042,6 +13216,15 @@ export function registerConfigSyncCommands(sync) {
|
|
|
13042
13216
|
entry: existingEntry,
|
|
13043
13217
|
localFileHash: computeFileHash(filePath),
|
|
13044
13218
|
force: options.force,
|
|
13219
|
+
// #3317 — the header row's `modifiedAt` covers everything
|
|
13220
|
+
// compared here: its own fields, and the `activeConfigId`
|
|
13221
|
+
// pointer whose repointing is the only way the immutable
|
|
13222
|
+
// version behind the extras can change. So an unmoved
|
|
13223
|
+
// timestamp proves the server stood still, and a baseline hash
|
|
13224
|
+
// that disagrees is a stale baseline — the reporter's CONFLICT
|
|
13225
|
+
// over two identical timestamps, which is also what every
|
|
13226
|
+
// manifest written before this issue's schema fix would hit.
|
|
13227
|
+
serverModifiedAtCoversProjection: true,
|
|
13045
13228
|
// The create leg adopts by key on the server's 409 (#1006), so
|
|
13046
13229
|
// an untracked function the server already holds is that same
|
|
13047
13230
|
// adopt reached one step earlier.
|
|
@@ -13176,19 +13359,27 @@ export function registerConfigSyncCommands(sync) {
|
|
|
13176
13359
|
// Non-fatal: the baseline falls back to what the listing said.
|
|
13177
13360
|
// The next push reconciles it.
|
|
13178
13361
|
}
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
|
|
13362
|
+
// #3317 — the pointer recorded is the one THIS push activated,
|
|
13363
|
+
// whatever the refetch found: a concurrent activation in the
|
|
13364
|
+
// window between the two is a server move the next push must
|
|
13365
|
+
// refuse over, not a baseline it silently rolls back.
|
|
13366
|
+
const baseline = pushedFunctionBaseline({
|
|
13367
|
+
functionId: String(functionId),
|
|
13368
|
+
refreshed,
|
|
13369
|
+
existingModifiedAt: existingEntry?.modifiedAt,
|
|
13184
13370
|
contentHash: computeFileHash(filePath),
|
|
13185
|
-
|
|
13186
|
-
? hashRemoteConfigForDiff(functionSpec, refreshed, pushDiffMaps, pushedVersion ?? undefined)
|
|
13187
|
-
: undefined,
|
|
13371
|
+
pushedVersion,
|
|
13188
13372
|
envelopeHash: push.envelopeHash,
|
|
13189
|
-
activeConfigId: pushedVersion?.configId,
|
|
13190
13373
|
sources: push.sources.map((src) => src.path),
|
|
13191
|
-
|
|
13374
|
+
spec: functionSpec,
|
|
13375
|
+
maps: pushDiffMaps,
|
|
13376
|
+
});
|
|
13377
|
+
if (baseline.movedTo) {
|
|
13378
|
+
warn(` Function ${key}: the server's active version moved to ${baseline.movedTo} ` +
|
|
13379
|
+
`while version ${pushedVersion?.configId} was being recorded; ` +
|
|
13380
|
+
"the next push will report the drift (`config pull` adopts it, `--force` overrides it).");
|
|
13381
|
+
}
|
|
13382
|
+
syncState.entities.functions[key] = baseline.entity;
|
|
13192
13383
|
}
|
|
13193
13384
|
}
|
|
13194
13385
|
}
|
|
@@ -14507,27 +14698,17 @@ export function registerConfigSyncCommands(sync) {
|
|
|
14507
14698
|
// Diff
|
|
14508
14699
|
sync
|
|
14509
14700
|
.command("diff")
|
|
14701
|
+
.allowExcessArguments(false)
|
|
14510
14702
|
.description("Show differences between local and remote configuration")
|
|
14511
|
-
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
14512
|
-
.option("--app <app-id>", "App ID")
|
|
14513
|
-
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
14514
14703
|
.option("--only <selectors>", "Compare just these objects: <type>/<key>, app, or var/<key> (repeatable, or comma-separated).", (value, previous) => [...(previous ?? []), value], [])
|
|
14515
|
-
.option("--json", "Output as JSON
|
|
14516
|
-
.action(async (
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
assertTreeApp(appId || options.app, "config diff");
|
|
14520
|
-
const resolvedAppId = resolveAppId(appId, options);
|
|
14521
|
-
const configDir = resolveSyncDir({ userDir: options.dir });
|
|
14704
|
+
.option("--json", "Output as JSON: the app.toml field diff with --only app, the compared rows otherwise")
|
|
14705
|
+
.action(async (options) => {
|
|
14706
|
+
const resolvedAppId = getCurrentAppId();
|
|
14707
|
+
const configDir = resolveSyncDir();
|
|
14522
14708
|
// A baseline that was never renamed means an unmigrated tree; refused
|
|
14523
14709
|
// here, before any request, snapshot or directory is made (#3153).
|
|
14524
14710
|
assertSyncStateLayout(configDir);
|
|
14525
|
-
|
|
14526
|
-
info(`Using per-environment config directory: ${configDir}`);
|
|
14527
|
-
const legacyHint = checkLegacySyncMigration(configDir);
|
|
14528
|
-
if (legacyHint)
|
|
14529
|
-
warn(legacyHint);
|
|
14530
|
-
}
|
|
14711
|
+
info(`Using per-environment config directory: ${configDir}`);
|
|
14531
14712
|
// Issue #2760: diff compares the renamed directory, so a legacy one is
|
|
14532
14713
|
// simply invisible — an empty diff would otherwise read as "in sync".
|
|
14533
14714
|
const legacyDbTypes = checkLegacyDatabaseTypesDir(configDir);
|
|
@@ -15205,7 +15386,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
15205
15386
|
}
|
|
15206
15387
|
let status = "exists";
|
|
15207
15388
|
let hint;
|
|
15208
|
-
// The active version carries `entry`, `
|
|
15389
|
+
// The active version carries `entry`, `mode` and `limits`, and its
|
|
15209
15390
|
// `envelopeHash` is the code axis.
|
|
15210
15391
|
const versionOutcome = await attemptFetch(async () => {
|
|
15211
15392
|
if (!remote.activeConfigId)
|
|
@@ -15724,7 +15905,16 @@ export function registerConfigSyncCommands(sync) {
|
|
|
15724
15905
|
// the same preflight push runs decides these rows, and its message —
|
|
15725
15906
|
// verbatim, so the two commands read identically — replaces whatever
|
|
15726
15907
|
// the comparison had to say about the file.
|
|
15727
|
-
|
|
15908
|
+
// #3320 — the function checks join them. Same posture, same messages:
|
|
15909
|
+
// a function file whose `entry`, mode, triggers or webhook
|
|
15910
|
+
// signing-secret the push preflight refuses is not a row this command
|
|
15911
|
+
// calls Synced, Modified or "local only — new".
|
|
15912
|
+
// Merged first: a file both collectors reject keeps every message push
|
|
15913
|
+
// prints, rather than one collector's hint overwriting the other's.
|
|
15914
|
+
for (const rejected of mergeConfigFileValidationErrors([
|
|
15915
|
+
...collectConfigFileValidationErrors(configDir),
|
|
15916
|
+
...collectFunctionTomlValidationErrors(configDir),
|
|
15917
|
+
])) {
|
|
15728
15918
|
const row = differences.find((d) => d.type === rejected.type && d.key === rejected.key);
|
|
15729
15919
|
const hint = rejected.messages.join("; ");
|
|
15730
15920
|
if (row) {
|
|
@@ -15755,6 +15945,22 @@ export function registerConfigSyncCommands(sync) {
|
|
|
15755
15945
|
? selectionIncludesApp(diffSelection)
|
|
15756
15946
|
: selectionIncludesKey(diffSelection, row.type, row.key));
|
|
15757
15947
|
}
|
|
15948
|
+
// #3320 — `--json` over the whole comparison, not just `--only app`.
|
|
15949
|
+
// The Invalid rows this issue adds have to be readable by the thing
|
|
15950
|
+
// that reads a diff programmatically (an editor, a CI job), and the
|
|
15951
|
+
// flag already existed for the app-settings case; a run that asked for
|
|
15952
|
+
// JSON and got a human table was answering a different question.
|
|
15953
|
+
// Emitted after the `--only` filter so a scoped run reports its scope.
|
|
15954
|
+
if (options.json) {
|
|
15955
|
+
console.log(JSON.stringify({
|
|
15956
|
+
appId: resolvedAppId,
|
|
15957
|
+
configDir,
|
|
15958
|
+
differences,
|
|
15959
|
+
testCases: testCaseDiffs,
|
|
15960
|
+
notCompared,
|
|
15961
|
+
}, null, 2));
|
|
15962
|
+
return;
|
|
15963
|
+
}
|
|
15758
15964
|
divider();
|
|
15759
15965
|
const localOnly = differences.filter((d) => d.status === LOCAL_ONLY_NEW);
|
|
15760
15966
|
const localOnlyAbsent = differences.filter((d) => d.status === LOCAL_ONLY_ABSENT);
|
|
@@ -15887,7 +16093,23 @@ export function registerConfigSyncCommands(sync) {
|
|
|
15887
16093
|
// change, so `config diff` is where their freshness is answered: an
|
|
15888
16094
|
// editor or a CI job asks the same command it already asks "what would
|
|
15889
16095
|
// push do?", and gets these named alongside everything else.
|
|
15890
|
-
|
|
16096
|
+
let staleFunctionTypes = [];
|
|
16097
|
+
let wiringProblems = [];
|
|
16098
|
+
try {
|
|
16099
|
+
staleFunctionTypes = staleFunctionTypeArtifacts(configDir);
|
|
16100
|
+
// Freshness is not the only way the handle stops working: a tsconfig
|
|
16101
|
+
// push will not rewrite can be perfectly current and still load none
|
|
16102
|
+
// of it. `config diff` is where CI asks, so it answers both
|
|
16103
|
+
// (CR3182-003).
|
|
16104
|
+
wiringProblems = functionTypeWiringProblems(configDir);
|
|
16105
|
+
}
|
|
16106
|
+
catch (err) {
|
|
16107
|
+
if (!(err instanceof FunctionSchemaCodegenError))
|
|
16108
|
+
throw err;
|
|
16109
|
+
// #3281 — a function file whose schema cannot be typed is reported
|
|
16110
|
+
// like a wiring problem: the generators cannot answer for the tree.
|
|
16111
|
+
wiringProblems = [err.message];
|
|
16112
|
+
}
|
|
15891
16113
|
if (staleFunctionTypes.length > 0) {
|
|
15892
16114
|
console.log();
|
|
15893
16115
|
info("Function types (regenerated by `config push`):");
|
|
@@ -15895,10 +16117,6 @@ export function registerConfigSyncCommands(sync) {
|
|
|
15895
16117
|
console.log(` ${chalk.yellow("~")} ${relative}`);
|
|
15896
16118
|
}
|
|
15897
16119
|
}
|
|
15898
|
-
// Freshness is not the only way the handle stops working: a tsconfig
|
|
15899
|
-
// push will not rewrite can be perfectly current and still load none of
|
|
15900
|
-
// it. `config diff` is where CI asks, so it answers both (CR3182-003).
|
|
15901
|
-
const wiringProblems = functionTypeWiringProblems(configDir);
|
|
15902
16120
|
if (wiringProblems.length > 0) {
|
|
15903
16121
|
console.log();
|
|
15904
16122
|
info("Function types (compiler wiring `config push` cannot repair):");
|
|
@@ -15943,28 +16161,21 @@ export function registerConfigSyncCommands(sync) {
|
|
|
15943
16161
|
// containing null) fall back to JSON-string per field with a log message.
|
|
15944
16162
|
sync
|
|
15945
16163
|
.command("migrate-toml")
|
|
16164
|
+
.allowExcessArguments(false)
|
|
15946
16165
|
.description("Rewrite database-type and workflow TOML files to native form " +
|
|
15947
16166
|
"([operations.definition] / [[operations.params]] for database types; " +
|
|
15948
16167
|
"[workflow.inputSchema] / [workflow.outputSchema] for workflows)")
|
|
15949
|
-
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
15950
|
-
.option("--app <app-id>", "App ID")
|
|
15951
|
-
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
15952
16168
|
.option("--dry-run", "Show what would change without writing files")
|
|
15953
|
-
.action(async (
|
|
15954
|
-
//
|
|
15955
|
-
//
|
|
15956
|
-
//
|
|
15957
|
-
|
|
15958
|
-
|
|
15959
|
-
assertTreeApp(appId || options.app, "config migrate-toml");
|
|
15960
|
-
const resolvedAppId = resolveAppId(appId, options);
|
|
15961
|
-
const configDir = resolveSyncDir({ userDir: options.dir });
|
|
16169
|
+
.action(async (options) => {
|
|
16170
|
+
// The app ID is not used for any API call — `migrate-toml` is a purely
|
|
16171
|
+
// local rewrite — but the environment that names it is what names the
|
|
16172
|
+
// tree being rewritten.
|
|
16173
|
+
const resolvedAppId = getCurrentAppId();
|
|
16174
|
+
const configDir = resolveSyncDir();
|
|
15962
16175
|
// A baseline that was never renamed means an unmigrated tree; refused
|
|
15963
16176
|
// here, before any request, snapshot or directory is made (#3153).
|
|
15964
16177
|
assertSyncStateLayout(configDir);
|
|
15965
|
-
|
|
15966
|
-
info(`Using per-environment config directory: ${configDir}`);
|
|
15967
|
-
}
|
|
16178
|
+
info(`Using per-environment config directory: ${configDir}`);
|
|
15968
16179
|
if (!existsSync(configDir)) {
|
|
15969
16180
|
error(`Config directory not found: ${configDir}`);
|
|
15970
16181
|
process.exit(1);
|
|
@@ -16114,27 +16325,19 @@ export function registerConfigSyncCommands(sync) {
|
|
|
16114
16325
|
// proven), so re-running push/pull is unaffected.
|
|
16115
16326
|
sync
|
|
16116
16327
|
.command("migrate-v2")
|
|
16328
|
+
.allowExcessArguments(false)
|
|
16117
16329
|
.description("Convert legacy per-type sync files (prompts/, integrations/, workflows/, transforms/) into the v2 blocks/<type>/<key>/block.toml layout")
|
|
16118
|
-
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
16119
|
-
.option("--app <app-id>", "App ID")
|
|
16120
|
-
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
16121
16330
|
.option("--dry-run", "Show what would change without writing files")
|
|
16122
16331
|
.option("--force", "Overwrite existing block.toml files instead of skipping them")
|
|
16123
|
-
.action(async (
|
|
16124
|
-
//
|
|
16125
|
-
//
|
|
16126
|
-
|
|
16127
|
-
|
|
16128
|
-
// path no longer partitions by app.
|
|
16129
|
-
assertTreeApp(appId || options.app, "config migrate-v2");
|
|
16130
|
-
const resolvedAppId = resolveAppId(appId, options);
|
|
16131
|
-
const configDir = resolveSyncDir({ userDir: options.dir });
|
|
16332
|
+
.action(async (options) => {
|
|
16333
|
+
// As with migrate-toml: a purely local rewrite, whose tree is the
|
|
16334
|
+
// resolved environment's.
|
|
16335
|
+
const resolvedAppId = getCurrentAppId();
|
|
16336
|
+
const configDir = resolveSyncDir();
|
|
16132
16337
|
// A baseline that was never renamed means an unmigrated tree; refused
|
|
16133
16338
|
// here, before any request, snapshot or directory is made (#3153).
|
|
16134
16339
|
assertSyncStateLayout(configDir);
|
|
16135
|
-
|
|
16136
|
-
info(`Using per-environment config directory: ${configDir}`);
|
|
16137
|
-
}
|
|
16340
|
+
info(`Using per-environment config directory: ${configDir}`);
|
|
16138
16341
|
if (!existsSync(configDir)) {
|
|
16139
16342
|
error(`Config directory not found: ${configDir}`);
|
|
16140
16343
|
process.exit(1);
|
|
@@ -16173,29 +16376,19 @@ export function registerConfigSyncCommands(sync) {
|
|
|
16173
16376
|
// Revert — restore a pre-pull snapshot (issue #578, Phase 1).
|
|
16174
16377
|
sync
|
|
16175
16378
|
.command("revert")
|
|
16379
|
+
.allowExcessArguments(false)
|
|
16176
16380
|
.description("Restore the config directory from a snapshot taken before a previous pull")
|
|
16177
|
-
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
16178
|
-
.option("--app <app-id>", "App ID")
|
|
16179
|
-
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
16180
16381
|
.option("--snapshot <id>", "Snapshot id (timestamp dirname or a unique >=8-char prefix)")
|
|
16181
16382
|
.option("--list", "List available snapshots without restoring")
|
|
16182
16383
|
.option("-y, --yes", "Skip the confirmation prompt")
|
|
16183
|
-
.action(async (
|
|
16184
|
-
|
|
16185
|
-
|
|
16186
|
-
assertTreeApp(appId || options.app, "config revert");
|
|
16187
|
-
const resolvedAppId = resolveAppId(appId, options);
|
|
16188
|
-
const configDir = resolveSyncDir({ userDir: options.dir });
|
|
16384
|
+
.action(async (options) => {
|
|
16385
|
+
const resolvedAppId = getCurrentAppId();
|
|
16386
|
+
const configDir = resolveSyncDir();
|
|
16189
16387
|
// A baseline that was never renamed means an unmigrated tree; refused
|
|
16190
16388
|
// here, before any request, snapshot or directory is made (#3153).
|
|
16191
16389
|
assertSyncStateLayout(configDir);
|
|
16192
|
-
const snapshotsRoot = resolveSnapshotsRoot({
|
|
16193
|
-
|
|
16194
|
-
userDir: options.dir,
|
|
16195
|
-
});
|
|
16196
|
-
if (isAutoResolvedSyncDir(options.dir)) {
|
|
16197
|
-
info(`Using per-environment config directory: ${configDir}`);
|
|
16198
|
-
}
|
|
16390
|
+
const snapshotsRoot = resolveSnapshotsRoot({ appId: resolvedAppId });
|
|
16391
|
+
info(`Using per-environment config directory: ${configDir}`);
|
|
16199
16392
|
const snapshots = listSnapshots(snapshotsRoot);
|
|
16200
16393
|
// --list, or no snapshots at all: enumerate and stop.
|
|
16201
16394
|
if (options.list || snapshots.length === 0) {
|
|
@@ -16255,9 +16448,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
16255
16448
|
}
|
|
16256
16449
|
}
|
|
16257
16450
|
try {
|
|
16258
|
-
|
|
16259
|
-
// it across the full-tree swap so we don't wipe backup history.
|
|
16260
|
-
restoreSnapshot(target.path, configDir, { preserveDir: snapshotsRoot });
|
|
16451
|
+
restoreSnapshot(target.path, configDir);
|
|
16261
16452
|
}
|
|
16262
16453
|
catch (err) {
|
|
16263
16454
|
error(`Restore failed: ${err?.message ?? err}`);
|