primitive-admin 1.1.0-alpha.43 → 1.1.0-alpha.45
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 +2 -2
- package/dist/bin/primitive.js +0 -0
- package/dist/src/commands/blob-buckets.js +20 -7
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/catalog.js +3 -3
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/databases.js +3 -3
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/guides.d.ts +86 -0
- package/dist/src/commands/guides.js +222 -41
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/integrations.js +2 -2
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/prompts.js +2 -2
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/sync.d.ts +23 -3
- package/dist/src/commands/sync.js +370 -121
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/workflows.js +191 -26
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +1 -0
- package/dist/src/lib/api-client.js +4 -0
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/block-layout.js +5 -5
- package/dist/src/lib/block-layout.js.map +1 -1
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +61 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +127 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
- package/dist/src/lib/config-toml.d.ts +10 -0
- package/dist/src/lib/config-toml.js +42 -0
- package/dist/src/lib/config-toml.js.map +1 -0
- package/dist/src/lib/constants.d.ts +9 -0
- package/dist/src/lib/constants.js +9 -0
- package/dist/src/lib/constants.js.map +1 -1
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +35 -7
- package/dist/src/lib/db-codegen/dbGenerator.js +384 -125
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/db-codegen/dbNaming.d.ts +29 -0
- package/dist/src/lib/db-codegen/dbNaming.js +58 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +114 -5
- package/dist/src/lib/db-codegen/dbTemplates.js +163 -16
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +7 -3
- package/dist/src/lib/db-codegen/dbTsTypes.js +23 -1
- package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +14 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/init-config.js +2 -2
- package/dist/src/lib/init-config.js.map +1 -1
- package/dist/src/lib/toml-database-config.d.ts +10 -39
- package/dist/src/lib/toml-database-config.js +16 -82
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +1 -1
- package/dist/src/lib/toml-metadata-config.js +3 -3
- package/dist/src/lib/toml-metadata-config.js.map +1 -1
- package/dist/src/lib/toml-native-form.d.ts +46 -0
- package/dist/src/lib/toml-native-form.js +78 -0
- package/dist/src/lib/toml-native-form.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +96 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +112 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +70 -0
- package/dist/src/lib/workflow-codegen/generator.js +186 -0
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
- package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
- package/dist/src/lib/workflow-codegen/naming.js +81 -0
- package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +55 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js +237 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
- package/dist/src/lib/workflow-fragments.js +3 -3
- package/dist/src/lib/workflow-fragments.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +1 -1
- package/dist/src/lib/workflow-toml-validator.js +1 -1
- package/dist/src/validators.d.ts +4 -3
- package/dist/src/validators.js +6 -5
- package/dist/src/validators.js.map +1 -1
- package/package.json +5 -3
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, rmdirSync, statSync } from "fs";
|
|
2
2
|
import { join, basename } from "path";
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
|
-
import * as TOML from "@iarna/toml";
|
|
5
4
|
import { lookup as mimeLookup } from "mime-types";
|
|
6
5
|
import { ApiClient, ApiError, ConflictError, SchemaRequiredError, OperationRefError, SchemaBreaksOpsError, SchemaHasUncheckableOpsError, TomlParseError, OpsExistError, } from "../lib/api-client.js";
|
|
7
6
|
import { buildDatabaseTypeTomlData, detectExistingOperationForms, normalizeOperationFromToml, normalizeSubscriptionFromToml, SubscriptionAccessKeyConflictError, } from "../lib/toml-database-config.js";
|
|
7
|
+
import { canEmitNative } from "../lib/toml-native-form.js";
|
|
8
8
|
import { parseMetadataCategoryToml, validateMetadataCategoryConfig, serializeMetadataCategoryConfig, parseDeclaredAccessManifestToml, validateDeclaredAccessManifest, serializeDeclaredAccessManifest, } from "../lib/toml-metadata-config.js";
|
|
9
9
|
import { validateOperations, formatIssue, } from "../lib/toml-params-validator.js";
|
|
10
10
|
import { getServerUrl, resolveAppId } from "../lib/config.js";
|
|
@@ -12,6 +12,7 @@ import { resolveSyncDir, resolveSnapshotsRoot, isAutoResolvedSyncDir, checkLegac
|
|
|
12
12
|
import { createSnapshot, listSnapshots, resolveSnapshot, restoreSnapshot, pruneSnapshots, } from "../lib/snapshots.js";
|
|
13
13
|
import { validateWorkflowToml, formatWorkflowTomlErrors, } from "../lib/workflow-toml-validator.js";
|
|
14
14
|
import { expandWorkflowTomlData } from "../lib/workflow-fragments.js";
|
|
15
|
+
import { parseConfigToml, stringifyConfigToml } from "../lib/config-toml.js";
|
|
15
16
|
import { applyWorkflowBody } from "../lib/workflow-apply.js";
|
|
16
17
|
import { buildWorkflowPayloadFromToml } from "../lib/workflow-payload.js";
|
|
17
18
|
import { detectLayout, migrateLegacyToV2 } from "../lib/block-layout.js";
|
|
@@ -190,6 +191,25 @@ export function normalizeWorkflowTomlDefaults(parsed) {
|
|
|
190
191
|
workflow[field] = defaultValue;
|
|
191
192
|
}
|
|
192
193
|
}
|
|
194
|
+
// Schema-form canonicalization (issue #1446): a legacy file carries
|
|
195
|
+
// `inputSchema` / `outputSchema` as a JSON string, while the native form (and
|
|
196
|
+
// the remote serialization) carries a parsed object. Parse the string form to
|
|
197
|
+
// its object here so the two forms hash equal — `sync diff` stays
|
|
198
|
+
// form-independent and an unchanged schema never shows a false `modified`
|
|
199
|
+
// during the transition. This runs only on the diff-hash path
|
|
200
|
+
// (`hashWorkflowTomlForDiff`); the stored-state hash path is untouched, so
|
|
201
|
+
// there is no forced one-time re-push after upgrade.
|
|
202
|
+
for (const field of ["inputSchema", "outputSchema"]) {
|
|
203
|
+
if (typeof workflow[field] === "string") {
|
|
204
|
+
try {
|
|
205
|
+
workflow[field] = JSON.parse(workflow[field]);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Not valid JSON — leave the raw string so a genuinely malformed value
|
|
209
|
+
// still compares by its literal content.
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
193
213
|
return { ...parsed, workflow };
|
|
194
214
|
}
|
|
195
215
|
/**
|
|
@@ -208,12 +228,14 @@ export function hashWorkflowTomlForDiff(parsed) {
|
|
|
208
228
|
* `getWorkflow` + active `getWorkflowConfig`), mirroring exactly what a fresh
|
|
209
229
|
* `sync pull` would write to disk. Serializes via `serializeWorkflow`, parses
|
|
210
230
|
* the resulting TOML (remote serialized TOML never carries `include`s, so a
|
|
211
|
-
* plain `
|
|
231
|
+
* plain `parseConfigToml` is sufficient — no fragment path needed), then hashes
|
|
212
232
|
* through `hashWorkflowTomlForDiff` so it lines up with the local-file hash.
|
|
213
233
|
*/
|
|
214
234
|
export function hashRemoteWorkflowForDiff(workflow, draft, configs) {
|
|
215
235
|
const serialized = serializeWorkflow(workflow, draft, configs || []);
|
|
216
|
-
|
|
236
|
+
// serializeWorkflow emits via smol-toml (issue #1450) and may contain a
|
|
237
|
+
// heterogeneous array, so re-parse through the TOML 1.0-compliant facade.
|
|
238
|
+
const parsed = parseConfigToml(serialized);
|
|
217
239
|
return hashWorkflowTomlForDiff(parsed);
|
|
218
240
|
}
|
|
219
241
|
// Boolean [auth] keys in app.toml that map 1:1 to app-settings fields.
|
|
@@ -238,7 +260,7 @@ const RECOGNIZED_AUTH_KEYS = new Set([
|
|
|
238
260
|
*
|
|
239
261
|
* Apple/OTP fields round-trip alongside the legacy google/passkey/magic-link
|
|
240
262
|
* keys. Two asymmetries are handled deliberately:
|
|
241
|
-
* - `appleSignInEnabled` comes back as `boolean | null`;
|
|
263
|
+
* - `appleSignInEnabled` comes back as `boolean | null`; smol-toml throws on
|
|
242
264
|
* null, so an unset value (null) is omitted rather than written.
|
|
243
265
|
* - `appleAudiences` is omitted when empty. An empty array and an omitted key
|
|
244
266
|
* both mean "no audiences" (the server normalizes [] -> null), so omitting
|
|
@@ -317,7 +339,7 @@ function serializeAppSettings(settings) {
|
|
|
317
339
|
maxAge: settings.corsMaxAge,
|
|
318
340
|
};
|
|
319
341
|
}
|
|
320
|
-
return
|
|
342
|
+
return stringifyConfigToml(data);
|
|
321
343
|
}
|
|
322
344
|
function serializeIntegration(integration) {
|
|
323
345
|
const data = {
|
|
@@ -330,9 +352,9 @@ function serializeIntegration(integration) {
|
|
|
330
352
|
},
|
|
331
353
|
requestConfig: integration.requestConfig || {},
|
|
332
354
|
};
|
|
333
|
-
return
|
|
355
|
+
return stringifyConfigToml(data);
|
|
334
356
|
}
|
|
335
|
-
function serializeWebhook(webhook) {
|
|
357
|
+
export function serializeWebhook(webhook) {
|
|
336
358
|
const data = {
|
|
337
359
|
webhook: {
|
|
338
360
|
key: webhook.webhookKey,
|
|
@@ -353,7 +375,9 @@ function serializeWebhook(webhook) {
|
|
|
353
375
|
// the template shape here too so a masked or raw value can never be written
|
|
354
376
|
// to TOML.
|
|
355
377
|
if (typeof webhook.signingSecret === "string" &&
|
|
356
|
-
|
|
378
|
+
// Whitespace-tolerant (#1468) to match the server's shared resolver, so a
|
|
379
|
+
// hand-authored spaced reference `{{ secrets.KEY }}` round-trips pull→push.
|
|
380
|
+
/\{\{\s*secrets\.[A-Z][A-Z0-9_]{0,63}\s*\}\}/.test(webhook.signingSecret)) {
|
|
357
381
|
data.webhook.signingSecret = webhook.signingSecret;
|
|
358
382
|
}
|
|
359
383
|
// Parse JSON fields if they're strings
|
|
@@ -384,7 +408,7 @@ function serializeWebhook(webhook) {
|
|
|
384
408
|
if (data.webhook[k] === undefined)
|
|
385
409
|
delete data.webhook[k];
|
|
386
410
|
});
|
|
387
|
-
return
|
|
411
|
+
return stringifyConfigToml(data);
|
|
388
412
|
}
|
|
389
413
|
function serializeCronTrigger(trigger) {
|
|
390
414
|
const data = {
|
|
@@ -420,7 +444,7 @@ function serializeCronTrigger(trigger) {
|
|
|
420
444
|
if (data.cronTrigger[k] === undefined)
|
|
421
445
|
delete data.cronTrigger[k];
|
|
422
446
|
});
|
|
423
|
-
return
|
|
447
|
+
return stringifyConfigToml(data);
|
|
424
448
|
}
|
|
425
449
|
function serializeBlobBucket(bucket) {
|
|
426
450
|
const data = {
|
|
@@ -446,7 +470,32 @@ function serializeBlobBucket(bucket) {
|
|
|
446
470
|
if (data.bucket[k] === undefined)
|
|
447
471
|
delete data.bucket[k];
|
|
448
472
|
});
|
|
449
|
-
return
|
|
473
|
+
return stringifyConfigToml(data);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Build the PATCH payload for a blob-bucket update from local TOML.
|
|
477
|
+
*
|
|
478
|
+
* EXACT extraction of the inline builder shared by the existing-update and
|
|
479
|
+
* 409-adopt branches — do NOT "clean up" the truthiness checks. The server
|
|
480
|
+
* treats `preset` and `accessPolicy` as mutually exclusive, only clears
|
|
481
|
+
* `ruleSetId` when a preset/accessPolicy is also present, and nulls
|
|
482
|
+
* `description` only when the key is explicitly present with a falsy value.
|
|
483
|
+
* `bucketKey` and `ttlTier` are immutable and never sent. Both call sites
|
|
484
|
+
* must go through this helper so their field sets never drift.
|
|
485
|
+
*/
|
|
486
|
+
function buildBlobBucketUpdatePayload(bucket) {
|
|
487
|
+
const updatePayload = {};
|
|
488
|
+
if (bucket.preset)
|
|
489
|
+
updatePayload.preset = bucket.preset;
|
|
490
|
+
else if (bucket.accessPolicy)
|
|
491
|
+
updatePayload.accessPolicy = bucket.accessPolicy;
|
|
492
|
+
if (bucket.ruleSetId)
|
|
493
|
+
updatePayload.ruleSetId = bucket.ruleSetId;
|
|
494
|
+
if (bucket.name)
|
|
495
|
+
updatePayload.name = bucket.name;
|
|
496
|
+
if (bucket.description !== undefined)
|
|
497
|
+
updatePayload.description = bucket.description || null;
|
|
498
|
+
return updatePayload;
|
|
450
499
|
}
|
|
451
500
|
function serializePrompt(prompt) {
|
|
452
501
|
const data = {
|
|
@@ -469,9 +518,102 @@ function serializePrompt(prompt) {
|
|
|
469
518
|
userPromptTemplate: config.userPromptTemplate,
|
|
470
519
|
})),
|
|
471
520
|
};
|
|
472
|
-
return
|
|
521
|
+
return stringifyConfigToml(data);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Emit a workflow schema field (`inputSchema` / `outputSchema`) for the TOML
|
|
525
|
+
* `[workflow]` table.
|
|
526
|
+
*
|
|
527
|
+
* Issue #1446: pull used to `JSON.stringify` both schema fields into a single
|
|
528
|
+
* escaped-JSON line — unreadable and undiffable. We now emit the parsed JSON
|
|
529
|
+
* Schema as a native `[workflow.inputSchema]` / `[workflow.outputSchema]`
|
|
530
|
+
* table whenever it round-trips faithfully (`canEmitNative === null`), falling
|
|
531
|
+
* back to the JSON string only for the values TOML silently drops (`null` /
|
|
532
|
+
* `undefined` anywhere in the schema). The empty-object schema `{}` is a real,
|
|
533
|
+
* distinct value ("accepts anything") and emits as an empty table.
|
|
534
|
+
*
|
|
535
|
+
* Returns `undefined` for an absent schema (the key is then omitted). The
|
|
536
|
+
* sponsor-approved decision is to always move to native aggressively — no
|
|
537
|
+
* form preservation — so this does not consult the existing file's form.
|
|
538
|
+
*/
|
|
539
|
+
function serializeWorkflowSchemaField(value, fieldName, workflowKey, log) {
|
|
540
|
+
if (value === undefined || value === null)
|
|
541
|
+
return undefined;
|
|
542
|
+
// Schemas arrive from the server GET as parsed objects; defensively accept a
|
|
543
|
+
// JSON string too (e.g. an inline-authored file re-serialized here).
|
|
544
|
+
let obj = value;
|
|
545
|
+
if (typeof value === "string") {
|
|
546
|
+
try {
|
|
547
|
+
obj = JSON.parse(value);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
// Not valid JSON — keep the original string as the emitted value.
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (obj && typeof obj === "object") {
|
|
555
|
+
const reason = canEmitNative(obj);
|
|
556
|
+
if (reason) {
|
|
557
|
+
log(`Workflow '${workflowKey}': cannot emit \`${fieldName}\` as native TOML (${reason}); using JSON string.`);
|
|
558
|
+
return JSON.stringify(obj);
|
|
559
|
+
}
|
|
560
|
+
return obj;
|
|
561
|
+
}
|
|
562
|
+
// A non-object schema (unexpected) — preserve it as a JSON string.
|
|
563
|
+
return JSON.stringify(obj);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Rewrite a workflow TOML file's string-form `inputSchema` / `outputSchema` to
|
|
567
|
+
* native object form for `sync migrate-toml` (issue #1446).
|
|
568
|
+
*
|
|
569
|
+
* Purely local, no server fetch. Parses the raw source WITHOUT expanding
|
|
570
|
+
* fragment `include`s (so those directives survive the rewrite), swaps a
|
|
571
|
+
* JSON-string schema for its parsed object when it round-trips faithfully
|
|
572
|
+
* (`canEmitNative === null`), and re-serializes via `stringifyConfigToml`.
|
|
573
|
+
*
|
|
574
|
+
* Only rewrites when a field is actually converted: a file whose schemas are
|
|
575
|
+
* already native, absent, or un-representable (a `null` default) is returned
|
|
576
|
+
* with `changed: false` and its original bytes untouched — so migrate is
|
|
577
|
+
* idempotent and never reports a spurious migration or reformats a file it
|
|
578
|
+
* didn't need to touch.
|
|
579
|
+
*/
|
|
580
|
+
export function rewriteWorkflowSchemasToNative(rawToml, workflowKey = "", log = () => { }) {
|
|
581
|
+
const parsed = parseConfigToml(rawToml);
|
|
582
|
+
if (!parsed || typeof parsed !== "object" || !parsed.workflow) {
|
|
583
|
+
return { content: rawToml, changed: false };
|
|
584
|
+
}
|
|
585
|
+
const workflow = parsed.workflow;
|
|
586
|
+
let converted = false;
|
|
587
|
+
for (const field of ["inputSchema", "outputSchema"]) {
|
|
588
|
+
const value = workflow[field];
|
|
589
|
+
// Only a JSON-string schema is a migration candidate; an object is already
|
|
590
|
+
// native and an absent key needs nothing.
|
|
591
|
+
if (typeof value !== "string")
|
|
592
|
+
continue;
|
|
593
|
+
let obj;
|
|
594
|
+
try {
|
|
595
|
+
obj = JSON.parse(value);
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
// Not JSON — leave the literal string in place.
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (!obj || typeof obj !== "object")
|
|
602
|
+
continue;
|
|
603
|
+
const reason = canEmitNative(obj);
|
|
604
|
+
if (reason) {
|
|
605
|
+
log(`Workflow '${workflowKey}': cannot migrate \`${field}\` to native TOML (${reason}); leaving as JSON string.`);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
workflow[field] = obj;
|
|
609
|
+
converted = true;
|
|
610
|
+
}
|
|
611
|
+
if (!converted)
|
|
612
|
+
return { content: rawToml, changed: false };
|
|
613
|
+
return { content: stringifyConfigToml(parsed), changed: true };
|
|
473
614
|
}
|
|
474
|
-
export function serializeWorkflow(workflow, draft, configs) {
|
|
615
|
+
export function serializeWorkflow(workflow, draft, configs, logger) {
|
|
616
|
+
const log = logger ?? (() => { });
|
|
475
617
|
// Find the active config or use the first one
|
|
476
618
|
const activeConfigId = workflow.activeConfigId;
|
|
477
619
|
const activeConfig = configs?.find((c) => c.configId === activeConfigId) || configs?.[0];
|
|
@@ -516,9 +658,11 @@ export function serializeWorkflow(workflow, draft, configs) {
|
|
|
516
658
|
// they're safe to coerce here.
|
|
517
659
|
requiresClientApply: workflow.requiresClientApply !== false,
|
|
518
660
|
syncCallable: workflow.syncCallable === true,
|
|
519
|
-
// Schemas are at workflow level
|
|
520
|
-
|
|
521
|
-
|
|
661
|
+
// Schemas are at workflow level. Emit native `[workflow.inputSchema]` /
|
|
662
|
+
// `[workflow.outputSchema]` tables when representable, JSON string only
|
|
663
|
+
// as a data-loss fallback (issue #1446).
|
|
664
|
+
inputSchema: serializeWorkflowSchemaField(workflow.inputSchema, "inputSchema", workflow.workflowKey, log),
|
|
665
|
+
outputSchema: serializeWorkflowSchemaField(workflow.outputSchema, "outputSchema", workflow.workflowKey, log),
|
|
522
666
|
},
|
|
523
667
|
// Use active config steps, fall back to draft
|
|
524
668
|
steps,
|
|
@@ -534,7 +678,7 @@ export function serializeWorkflow(workflow, draft, configs) {
|
|
|
534
678
|
// a workflow's `md`/`secrets` declarations round-trip through pull → push.
|
|
535
679
|
...serializeDeclaredAccessManifest(workflow.metadataManifest),
|
|
536
680
|
};
|
|
537
|
-
return
|
|
681
|
+
return stringifyConfigToml(data);
|
|
538
682
|
}
|
|
539
683
|
// Database config serialization helpers
|
|
540
684
|
//
|
|
@@ -543,15 +687,17 @@ export function serializeWorkflow(workflow, draft, configs) {
|
|
|
543
687
|
// rows over JSON-strings stuffed into a single field. The form is sticky
|
|
544
688
|
// per file — if the existing file uses the legacy JSON-string form, we
|
|
545
689
|
// preserve it so we don't generate surprise diffs. New files default to
|
|
546
|
-
// native.
|
|
547
|
-
//
|
|
690
|
+
// native. The round-trip serializes with `smol-toml` (TOML 1.0), so
|
|
691
|
+
// heterogeneous arrays emit natively (issue #1450); only shapes that TOML
|
|
692
|
+
// genuinely can't represent without data loss (null / undefined / an array
|
|
693
|
+
// containing null) fall back to a JSON string per field with a log message.
|
|
548
694
|
//
|
|
549
695
|
// Issue #666: when the type has a stored `schema` (raw TOML of `[models.*]`
|
|
550
696
|
// blocks), `buildDatabaseTypeTomlData` parses + merges it into the same TOML
|
|
551
697
|
// object so the result round-trips as `[models.<Name>.fields.<field>]`.
|
|
552
698
|
function serializeDatabaseType(typeConfig, operations, ruleSetIdToName, options = {}) {
|
|
553
699
|
const data = buildDatabaseTypeTomlData(typeConfig, operations, ruleSetIdToName, options);
|
|
554
|
-
return
|
|
700
|
+
return stringifyConfigToml(data);
|
|
555
701
|
}
|
|
556
702
|
function serializeEmailTemplate(template) {
|
|
557
703
|
// The API returns { emailType, hasOverride, override: { subject, htmlBody, textBody }, default: { ... } }
|
|
@@ -569,7 +715,7 @@ function serializeEmailTemplate(template) {
|
|
|
569
715
|
if (override.textBody) {
|
|
570
716
|
data.template.textBody = override.textBody;
|
|
571
717
|
}
|
|
572
|
-
return
|
|
718
|
+
return stringifyConfigToml(data);
|
|
573
719
|
}
|
|
574
720
|
function serializeRuleSet(ruleSet) {
|
|
575
721
|
const data = {
|
|
@@ -586,7 +732,7 @@ function serializeRuleSet(ruleSet) {
|
|
|
586
732
|
if (Object.keys(rules).length > 0) {
|
|
587
733
|
data.rules = rules;
|
|
588
734
|
}
|
|
589
|
-
return
|
|
735
|
+
return stringifyConfigToml(data);
|
|
590
736
|
}
|
|
591
737
|
export function serializeGroupTypeConfig(config, ruleSetIdToName) {
|
|
592
738
|
const ruleSetName = config.ruleSetId ? (ruleSetIdToName.get(config.ruleSetId) || "") : "";
|
|
@@ -601,7 +747,7 @@ export function serializeGroupTypeConfig(config, ruleSetIdToName) {
|
|
|
601
747
|
// round-trips, exactly like the collection/database/workflow type configs.
|
|
602
748
|
...serializeDeclaredAccessManifest(config.metadataManifest),
|
|
603
749
|
};
|
|
604
|
-
return
|
|
750
|
+
return stringifyConfigToml(data);
|
|
605
751
|
}
|
|
606
752
|
function serializeCollectionTypeConfig(config, ruleSetIdToName) {
|
|
607
753
|
const ruleSetName = config.ruleSetId ? (ruleSetIdToName.get(config.ruleSetId) || "") : "";
|
|
@@ -614,7 +760,7 @@ function serializeCollectionTypeConfig(config, ruleSetIdToName) {
|
|
|
614
760
|
// `[metadata]` + `secrets` fragments so a pull→push→pull cycle round-trips.
|
|
615
761
|
...serializeDeclaredAccessManifest(config.metadataManifest),
|
|
616
762
|
};
|
|
617
|
-
return
|
|
763
|
+
return stringifyConfigToml(data);
|
|
618
764
|
}
|
|
619
765
|
export function parseDatabaseTypeToml(tomlData) {
|
|
620
766
|
const typeSection = tomlData.type || {};
|
|
@@ -680,7 +826,7 @@ export function parseDatabaseTypeToml(tomlData) {
|
|
|
680
826
|
let schemaSerialized = null;
|
|
681
827
|
if (tomlData.models && typeof tomlData.models === "object") {
|
|
682
828
|
try {
|
|
683
|
-
schemaSerialized =
|
|
829
|
+
schemaSerialized = stringifyConfigToml({ models: tomlData.models });
|
|
684
830
|
}
|
|
685
831
|
catch {
|
|
686
832
|
// Fall through — server-side parser will fail with a clear
|
|
@@ -769,7 +915,7 @@ export function parseCollectionTypeConfigToml(tomlData) {
|
|
|
769
915
|
// the original object untouched.
|
|
770
916
|
function parseTomlFile(filePath) {
|
|
771
917
|
const content = readFileSync(filePath, "utf-8");
|
|
772
|
-
const parsed =
|
|
918
|
+
const parsed = parseConfigToml(content);
|
|
773
919
|
return expandWorkflowTomlData(parsed, filePath);
|
|
774
920
|
}
|
|
775
921
|
/**
|
|
@@ -861,7 +1007,7 @@ export function serializeTestCase(testCase, lookupMaps) {
|
|
|
861
1007
|
expectedJsonSubset: testCase.expectedJsonSubset || "{}",
|
|
862
1008
|
},
|
|
863
1009
|
};
|
|
864
|
-
return
|
|
1010
|
+
return stringifyConfigToml(data);
|
|
865
1011
|
}
|
|
866
1012
|
export function parseTestCaseToml(tomlData) {
|
|
867
1013
|
const test = tomlData.test || {};
|
|
@@ -1587,7 +1733,7 @@ Directory Structure:
|
|
|
1587
1733
|
for (const { workflow, draft, configs } of workflows) {
|
|
1588
1734
|
const filename = `${workflow.workflowKey}.toml`;
|
|
1589
1735
|
const filePath = join(configDir, "workflows", filename);
|
|
1590
|
-
writeFileSync(filePath, serializeWorkflow(workflow, draft, configs || []));
|
|
1736
|
+
writeFileSync(filePath, serializeWorkflow(workflow, draft, configs || [], (msg) => info(` ${msg}`)));
|
|
1591
1737
|
// Hash the expanded (post-fragment-splice) form so subsequent pushes
|
|
1592
1738
|
// can detect fragment-only edits. See `computeExpandedContentHash`.
|
|
1593
1739
|
// Pulled workflows never carry `include`, so the expander is a
|
|
@@ -2562,30 +2708,27 @@ Directory Structure:
|
|
|
2562
2708
|
if (existingId) {
|
|
2563
2709
|
// #1020 (D8): idempotently apply preset/ruleSet/name/description
|
|
2564
2710
|
// changes via PATCH. bucketKey/ttlTier are immutable and not sent.
|
|
2565
|
-
const updatePayload =
|
|
2566
|
-
if (bucket.preset)
|
|
2567
|
-
updatePayload.preset = bucket.preset;
|
|
2568
|
-
else if (bucket.accessPolicy)
|
|
2569
|
-
updatePayload.accessPolicy = bucket.accessPolicy;
|
|
2570
|
-
if (bucket.ruleSetId)
|
|
2571
|
-
updatePayload.ruleSetId = bucket.ruleSetId;
|
|
2572
|
-
if (bucket.name)
|
|
2573
|
-
updatePayload.name = bucket.name;
|
|
2574
|
-
if (bucket.description !== undefined)
|
|
2575
|
-
updatePayload.description = bucket.description || null;
|
|
2711
|
+
const updatePayload = buildBlobBucketUpdatePayload(bucket);
|
|
2576
2712
|
changes.push({ type: "blob-bucket", action: "update", key });
|
|
2577
2713
|
if (!options.dryRun) {
|
|
2578
2714
|
try {
|
|
2579
2715
|
await client.updateBlobBucket(resolvedAppId, existingId, updatePayload);
|
|
2580
2716
|
info(` Updated blob bucket: ${key}`);
|
|
2717
|
+
// #1459: stamp the content hash ONLY after a successful
|
|
2718
|
+
// update, and only when this is not a dry run. A failed
|
|
2719
|
+
// update (or a dry run) must leave the file pending rather
|
|
2720
|
+
// than mark it in-sync and suppress it on the next push.
|
|
2721
|
+
if (syncState?.entities?.blobBuckets?.[key]) {
|
|
2722
|
+
syncState.entities.blobBuckets[key].contentHash =
|
|
2723
|
+
computeFileHash(filePath);
|
|
2724
|
+
}
|
|
2581
2725
|
}
|
|
2582
2726
|
catch (err) {
|
|
2727
|
+
// Keep the warn-and-continue contract (sponsor decision):
|
|
2728
|
+
// do not abort the push, just don't stamp the hash.
|
|
2583
2729
|
warn(` Failed to update blob bucket ${key}: ${String(err?.message || err)}`);
|
|
2584
2730
|
}
|
|
2585
2731
|
}
|
|
2586
|
-
if (syncState?.entities?.blobBuckets?.[key]) {
|
|
2587
|
-
syncState.entities.blobBuckets[key].contentHash = computeFileHash(filePath);
|
|
2588
|
-
}
|
|
2589
2732
|
}
|
|
2590
2733
|
else {
|
|
2591
2734
|
const payload = {
|
|
@@ -2620,23 +2763,86 @@ Directory Structure:
|
|
|
2620
2763
|
catch (err) {
|
|
2621
2764
|
const msg = String(err?.message || err);
|
|
2622
2765
|
if (msg.includes("already exists") || err.statusCode === 409) {
|
|
2623
|
-
|
|
2624
|
-
//
|
|
2766
|
+
// #1459: adopt-by-key on 409. A blob bucket can already
|
|
2767
|
+
// exist on the server (an out-of-band create, or a prior
|
|
2768
|
+
// push that aborted before recording it in sync state). The
|
|
2769
|
+
// old handler stamped the LOCAL file's contentHash WITHOUT
|
|
2770
|
+
// applying the local config, so a divergent bucket was
|
|
2771
|
+
// silently marked in-sync and never reconciled. Mirror the
|
|
2772
|
+
// cron/workflow adopt paths: look up the bucket by key,
|
|
2773
|
+
// re-issue the local payload as an UPDATE, and stamp the
|
|
2774
|
+
// hash ONLY after the update succeeds.
|
|
2775
|
+
info(` Blob bucket already exists on server, adopting by key: ${key}`);
|
|
2776
|
+
let existing;
|
|
2625
2777
|
try {
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2778
|
+
existing = await client.getBlobBucket(resolvedAppId, key);
|
|
2779
|
+
}
|
|
2780
|
+
catch (lookupErr) {
|
|
2781
|
+
// A lookup failure after a 409 means we cannot adopt —
|
|
2782
|
+
// surface it instead of swallowing it (the old silent
|
|
2783
|
+
// `catch {}` left no sync-state entry, looping
|
|
2784
|
+
// create→409 on every push).
|
|
2785
|
+
throw wrapEntityError(new Error(`blob bucket "${key}" already exists but could not be adopted (lookup failed: ${String(lookupErr?.message || lookupErr)})`), "create", "blob bucket", key);
|
|
2786
|
+
}
|
|
2787
|
+
if (!existing?.bucketId) {
|
|
2788
|
+
// 409 but no matching bucket found — surface the original
|
|
2789
|
+
// create error rather than silently proceeding.
|
|
2790
|
+
throw wrapEntityError(err, "create", "blob bucket", key);
|
|
2791
|
+
}
|
|
2792
|
+
// Verify the resolved bucket is actually the one we asked
|
|
2793
|
+
// for. `getBlobBucket` resolves the identifier by primary id
|
|
2794
|
+
// (appId, bucketId) BEFORE bucketKey, and a bucketKey may be
|
|
2795
|
+
// a ULID-shaped string (the key regex permits it). If the
|
|
2796
|
+
// local key collides with an unrelated bucket's bucketId,
|
|
2797
|
+
// the lookup returns that unrelated bucket — patching and
|
|
2798
|
+
// stamping it would overwrite the wrong resource. Require an
|
|
2799
|
+
// exact key match, matching the cron/workflow adopt paths'
|
|
2800
|
+
// exact-key equality (they list + filter by key for the same
|
|
2801
|
+
// reason). Surface the mismatch via wrapEntityError; do NOT
|
|
2802
|
+
// patch or stamp.
|
|
2803
|
+
if (existing.bucketKey !== key) {
|
|
2804
|
+
throw wrapEntityError(new Error(`blob bucket "${key}" already exists, but the server resolved a different bucket (bucketKey "${existing.bucketKey}", bucketId "${existing.bucketId}"); refusing to adopt. This happens when the local key collides with an unrelated bucket's id — rename the local bucket key so it does not match an existing bucket id.`), "create", "blob bucket", key);
|
|
2805
|
+
}
|
|
2806
|
+
// ttlTier is immutable and cannot be reconciled by an
|
|
2807
|
+
// UPDATE. A local TOML whose retention tier diverges from
|
|
2808
|
+
// the server must fail loudly (sponsor decision) rather
|
|
2809
|
+
// than be silently blessed while the cosmetic fields
|
|
2810
|
+
// converge.
|
|
2811
|
+
if (bucket.ttlTier &&
|
|
2812
|
+
existing.ttlTier &&
|
|
2813
|
+
bucket.ttlTier !== existing.ttlTier) {
|
|
2814
|
+
throw wrapEntityError(new Error(`blob bucket "${key}" ttlTier mismatch: local TOML has "${bucket.ttlTier}" but the server bucket has "${existing.ttlTier}". ttlTier is immutable; align the TOML with the server (or recreate the bucket) before pushing.`), "update", "blob bucket", key);
|
|
2815
|
+
}
|
|
2816
|
+
const updatePayload = buildBlobBucketUpdatePayload(bucket);
|
|
2817
|
+
let updated;
|
|
2818
|
+
if (Object.keys(updatePayload).length > 0) {
|
|
2819
|
+
// Surface an update failure via wrapEntityError (no
|
|
2820
|
+
// stamp) — matches the cron adopt path.
|
|
2821
|
+
try {
|
|
2822
|
+
updated = await client.updateBlobBucket(resolvedAppId, existing.bucketId, updatePayload);
|
|
2823
|
+
}
|
|
2824
|
+
catch (updateErr) {
|
|
2825
|
+
throw wrapEntityError(updateErr, "update", "blob bucket", key);
|
|
2636
2826
|
}
|
|
2827
|
+
info(` Adopted + updated blob bucket: ${key}`);
|
|
2637
2828
|
}
|
|
2638
|
-
|
|
2639
|
-
//
|
|
2829
|
+
else {
|
|
2830
|
+
// No mutable fields in the local TOML — the server
|
|
2831
|
+
// rejects an empty update (400). Adopt the existing
|
|
2832
|
+
// bucket without a PATCH.
|
|
2833
|
+
info(` Adopted blob bucket (no updatable fields): ${key}`);
|
|
2834
|
+
}
|
|
2835
|
+
if (syncState) {
|
|
2836
|
+
if (!syncState.entities.blobBuckets) {
|
|
2837
|
+
syncState.entities.blobBuckets = {};
|
|
2838
|
+
}
|
|
2839
|
+
syncState.entities.blobBuckets[key] = {
|
|
2840
|
+
id: existing.bucketId,
|
|
2841
|
+
modifiedAt: updated?.modifiedAt ||
|
|
2842
|
+
existing.modifiedAt ||
|
|
2843
|
+
new Date().toISOString(),
|
|
2844
|
+
contentHash: computeFileHash(filePath),
|
|
2845
|
+
};
|
|
2640
2846
|
}
|
|
2641
2847
|
}
|
|
2642
2848
|
else {
|
|
@@ -5097,11 +5303,15 @@ Directory Structure:
|
|
|
5097
5303
|
// Migrate-toml (issue #752): bulk-rewrite database-type TOML files from
|
|
5098
5304
|
// the legacy JSON-string form for `definition`/`params` to the native
|
|
5099
5305
|
// nested-table form. Idempotent: ops already in native form are left as
|
|
5100
|
-
// is;
|
|
5101
|
-
//
|
|
5306
|
+
// is; the round-trip serializes with smol-toml (TOML 1.0, issue #1450), so
|
|
5307
|
+
// heterogeneous arrays now converge to native form too — only shapes that
|
|
5308
|
+
// TOML can't represent without data loss (null / undefined / an array
|
|
5309
|
+
// containing null) fall back to JSON-string per field with a log message.
|
|
5102
5310
|
sync
|
|
5103
5311
|
.command("migrate-toml")
|
|
5104
|
-
.description("Rewrite database-type TOML files to native
|
|
5312
|
+
.description("Rewrite database-type and workflow TOML files to native form " +
|
|
5313
|
+
"([operations.definition] / [[operations.params]] for database types; " +
|
|
5314
|
+
"[workflow.inputSchema] / [workflow.outputSchema] for workflows)")
|
|
5105
5315
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
5106
5316
|
.option("--app <app-id>", "App ID")
|
|
5107
5317
|
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
@@ -5120,83 +5330,122 @@ Directory Structure:
|
|
|
5120
5330
|
process.exit(1);
|
|
5121
5331
|
}
|
|
5122
5332
|
const dbTypesDir = join(configDir, "database-types");
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
const
|
|
5128
|
-
|
|
5129
|
-
|
|
5333
|
+
const workflowsDir = join(configDir, "workflows");
|
|
5334
|
+
const dbFiles = existsSync(dbTypesDir)
|
|
5335
|
+
? readdirSync(dbTypesDir).filter((f) => f.endsWith(".toml"))
|
|
5336
|
+
: [];
|
|
5337
|
+
const workflowFiles = existsSync(workflowsDir)
|
|
5338
|
+
? readdirSync(workflowsDir).filter((f) => f.endsWith(".toml"))
|
|
5339
|
+
: [];
|
|
5340
|
+
// Restructured early-return (issue #1446): don't bail just because
|
|
5341
|
+
// `database-types/` is absent — a workflow-only project still has
|
|
5342
|
+
// `inputSchema`/`outputSchema` to migrate.
|
|
5343
|
+
if (dbFiles.length === 0 && workflowFiles.length === 0) {
|
|
5344
|
+
info("No database-type or workflow TOML files found; nothing to migrate.");
|
|
5130
5345
|
return;
|
|
5131
5346
|
}
|
|
5132
|
-
// Hydrate ruleSetIdToName from local sync state so files that
|
|
5133
|
-
// reference a rule set via the legacy `ruleSetId = "01..."` form
|
|
5134
|
-
// round-trip with the correct `ruleSetName`. Without this, any user
|
|
5135
|
-
// who runs migrate-toml against a file with a rule-set assignment
|
|
5136
|
-
// would silently lose that reference (review feedback r3246633010).
|
|
5137
|
-
//
|
|
5138
|
-
// Files that use the modern key-based `ruleSetName = "..."` form
|
|
5139
|
-
// don't need the map at all — `parseDatabaseTypeToml` stores the
|
|
5140
|
-
// value in `typeConfig._ruleSetName` and the serializer now prefers
|
|
5141
|
-
// that. The map is only load-bearing for the legacy ID-based form.
|
|
5142
|
-
const ruleSetIdToName = new Map();
|
|
5143
|
-
const migrateSyncState = loadSyncState(configDir);
|
|
5144
|
-
if (migrateSyncState?.entities?.ruleSets) {
|
|
5145
|
-
for (const [fileKey, entry] of Object.entries(migrateSyncState.entities.ruleSets)) {
|
|
5146
|
-
if (entry && typeof entry === "object" && "id" in entry && entry.id) {
|
|
5147
|
-
// fileKey is the sanitized rule-set name (see sync pull at
|
|
5148
|
-
// sync.ts:1385). It's the same shape the server returned and
|
|
5149
|
-
// matches what a TOML file's ruleSetName field would carry.
|
|
5150
|
-
ruleSetIdToName.set(entry.id, fileKey);
|
|
5151
|
-
}
|
|
5152
|
-
}
|
|
5153
|
-
}
|
|
5154
5347
|
let migratedCount = 0;
|
|
5155
5348
|
let unchangedCount = 0;
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
//
|
|
5161
|
-
//
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5349
|
+
// ---- database-types/ pass ----
|
|
5350
|
+
if (dbFiles.length > 0) {
|
|
5351
|
+
// Hydrate ruleSetIdToName from local sync state so files that
|
|
5352
|
+
// reference a rule set via the legacy `ruleSetId = "01..."` form
|
|
5353
|
+
// round-trip with the correct `ruleSetName`. Without this, any user
|
|
5354
|
+
// who runs migrate-toml against a file with a rule-set assignment
|
|
5355
|
+
// would silently lose that reference (review feedback r3246633010).
|
|
5356
|
+
//
|
|
5357
|
+
// Files that use the modern key-based `ruleSetName = "..."` form
|
|
5358
|
+
// don't need the map at all — `parseDatabaseTypeToml` stores the
|
|
5359
|
+
// value in `typeConfig._ruleSetName` and the serializer now prefers
|
|
5360
|
+
// that. The map is only load-bearing for the legacy ID-based form.
|
|
5361
|
+
const ruleSetIdToName = new Map();
|
|
5362
|
+
const migrateSyncState = loadSyncState(configDir);
|
|
5363
|
+
if (migrateSyncState?.entities?.ruleSets) {
|
|
5364
|
+
for (const [fileKey, entry] of Object.entries(migrateSyncState.entities.ruleSets)) {
|
|
5365
|
+
if (entry && typeof entry === "object" && "id" in entry && entry.id) {
|
|
5366
|
+
// fileKey is the sanitized rule-set name (see sync pull at
|
|
5367
|
+
// sync.ts:1385). It's the same shape the server returned and
|
|
5368
|
+
// matches what a TOML file's ruleSetName field would carry.
|
|
5369
|
+
ruleSetIdToName.set(entry.id, fileKey);
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5168
5372
|
}
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5373
|
+
for (const file of dbFiles) {
|
|
5374
|
+
const filePath = join(dbTypesDir, file);
|
|
5375
|
+
const rawBefore = readFileSync(filePath, "utf-8");
|
|
5376
|
+
const tomlData = parseTomlFile(filePath);
|
|
5377
|
+
// `accessRule`/`access` conflicts surface as a clear error here too,
|
|
5378
|
+
// rather than silently dropping the field during a migrate rewrite.
|
|
5379
|
+
let typeConfig;
|
|
5380
|
+
let operations;
|
|
5381
|
+
let subscriptions;
|
|
5382
|
+
try {
|
|
5383
|
+
({ typeConfig, operations, subscriptions } =
|
|
5384
|
+
parseDatabaseTypeToml(tomlData));
|
|
5385
|
+
}
|
|
5386
|
+
catch (err) {
|
|
5387
|
+
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
5388
|
+
error(` ${file}: ${err.message}`);
|
|
5389
|
+
error(`Aborting migrate-toml: subscription \`access\`/\`accessRule\` conflict in ${file}.`);
|
|
5390
|
+
process.exit(1);
|
|
5391
|
+
}
|
|
5392
|
+
throw err;
|
|
5393
|
+
}
|
|
5394
|
+
// Force-native: ignore existing hints so every TOMLable field
|
|
5395
|
+
// ends up in nested-table form. Subscriptions are preserved on the
|
|
5396
|
+
// rewrite (issue #803) so migrate-toml never drops them.
|
|
5397
|
+
const rewritten = serializeDatabaseType(typeConfig, operations, ruleSetIdToName, {
|
|
5398
|
+
defaultForm: "native",
|
|
5399
|
+
logger: (msg) => info(` ${msg}`),
|
|
5400
|
+
subscriptions,
|
|
5401
|
+
});
|
|
5402
|
+
if (rewritten === rawBefore) {
|
|
5403
|
+
unchangedCount++;
|
|
5404
|
+
continue;
|
|
5174
5405
|
}
|
|
5175
|
-
|
|
5406
|
+
if (options.dryRun) {
|
|
5407
|
+
info(`Would migrate database-types/${file}`);
|
|
5408
|
+
}
|
|
5409
|
+
else {
|
|
5410
|
+
writeFileSync(filePath, rewritten);
|
|
5411
|
+
info(`Migrated database-types/${file}`);
|
|
5412
|
+
// Update content hash in sync state if we have an entry for it.
|
|
5413
|
+
const syncState = loadSyncState(configDir);
|
|
5414
|
+
const dbType = typeConfig.databaseType || basename(file, ".toml");
|
|
5415
|
+
if (syncState?.entities?.databaseTypes?.[dbType]) {
|
|
5416
|
+
syncState.entities.databaseTypes[dbType].contentHash = computeFileHash(filePath);
|
|
5417
|
+
saveSyncState(configDir, syncState);
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
migratedCount++;
|
|
5176
5421
|
}
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5422
|
+
}
|
|
5423
|
+
// ---- workflows/ pass (issue #1446) ----
|
|
5424
|
+
// Convert string-form `inputSchema`/`outputSchema` to native tables.
|
|
5425
|
+
// Purely local, mirroring the database-types pass. The stored workflow
|
|
5426
|
+
// hash uses `computeExpandedContentHash` (parse then hash), NOT
|
|
5427
|
+
// `computeFileHash`, matching the workflow convention on the pull/push
|
|
5428
|
+
// paths — otherwise a push after migrate would spuriously re-push.
|
|
5429
|
+
for (const file of workflowFiles) {
|
|
5430
|
+
const filePath = join(workflowsDir, file);
|
|
5431
|
+
const rawBefore = readFileSync(filePath, "utf-8");
|
|
5432
|
+
const workflowKey = basename(file, ".toml");
|
|
5433
|
+
const { content: rewritten, changed } = rewriteWorkflowSchemasToNative(rawBefore, workflowKey, (msg) => info(` ${msg}`));
|
|
5434
|
+
if (!changed) {
|
|
5186
5435
|
unchangedCount++;
|
|
5187
5436
|
continue;
|
|
5188
5437
|
}
|
|
5189
5438
|
if (options.dryRun) {
|
|
5190
|
-
info(`Would migrate
|
|
5439
|
+
info(`Would migrate workflows/${file}`);
|
|
5191
5440
|
}
|
|
5192
5441
|
else {
|
|
5193
5442
|
writeFileSync(filePath, rewritten);
|
|
5194
|
-
info(`Migrated
|
|
5195
|
-
// Update content hash
|
|
5443
|
+
info(`Migrated workflows/${file}`);
|
|
5444
|
+
// Update the stored content hash so a subsequent push sees a no-op.
|
|
5196
5445
|
const syncState = loadSyncState(configDir);
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5446
|
+
if (syncState?.entities?.workflows?.[workflowKey]) {
|
|
5447
|
+
syncState.entities.workflows[workflowKey].contentHash =
|
|
5448
|
+
computeExpandedContentHash(parseTomlFile(filePath));
|
|
5200
5449
|
saveSyncState(configDir, syncState);
|
|
5201
5450
|
}
|
|
5202
5451
|
}
|