primitive-admin 1.1.0-alpha.75 → 1.1.0-alpha.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/bin/primitive.js +98 -10
  2. package/dist/bin/primitive.js.map +1 -1
  3. package/dist/src/commands/functions.js +53 -18
  4. package/dist/src/commands/functions.js.map +1 -1
  5. package/dist/src/commands/prompts.js +54 -39
  6. package/dist/src/commands/prompts.js.map +1 -1
  7. package/dist/src/commands/sync.d.ts +39 -0
  8. package/dist/src/commands/sync.js +163 -90
  9. package/dist/src/commands/sync.js.map +1 -1
  10. package/dist/src/commands/workflows.js +61 -30
  11. package/dist/src/commands/workflows.js.map +1 -1
  12. package/dist/src/lib/api-client.d.ts +16 -1
  13. package/dist/src/lib/api-client.js +16 -3
  14. package/dist/src/lib/api-client.js.map +1 -1
  15. package/dist/src/lib/block-selector.d.ts +58 -0
  16. package/dist/src/lib/block-selector.js +92 -0
  17. package/dist/src/lib/block-selector.js.map +1 -0
  18. package/dist/src/lib/config-object-descriptor.js +1 -3
  19. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  20. package/dist/src/lib/function-collect.d.ts +29 -7
  21. package/dist/src/lib/function-collect.js +135 -11
  22. package/dist/src/lib/function-collect.js.map +1 -1
  23. package/dist/src/lib/function-grants-preflight.d.ts +34 -88
  24. package/dist/src/lib/function-grants-preflight.js +32 -145
  25. package/dist/src/lib/function-grants-preflight.js.map +1 -1
  26. package/dist/src/lib/function-sync.d.ts +3 -14
  27. package/dist/src/lib/function-sync.js +10 -49
  28. package/dist/src/lib/function-sync.js.map +1 -1
  29. package/dist/src/lib/function-triggers.d.ts +6 -0
  30. package/dist/src/lib/function-triggers.js +63 -4
  31. package/dist/src/lib/function-triggers.js.map +1 -1
  32. package/dist/src/lib/generated-config-surfaces.d.ts +188 -236
  33. package/dist/src/lib/generated-config-surfaces.js +355 -453
  34. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  35. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  36. package/dist/src/lib/generated-sdk-types.js +1 -1
  37. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  38. package/dist/src/lib/toml-database-config.js +19 -2
  39. package/dist/src/lib/toml-database-config.js.map +1 -1
  40. package/package.json +1 -1
@@ -35,11 +35,11 @@ import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, toml
35
35
  import { CONFIG_SURFACES, isEmptyForEmit, } from "../lib/generated-config-surfaces.js";
36
36
  // #3179 — the function's two halves: the header payloads and the envelope
37
37
  // `config push` sends, and the bytes `config pull` writes back.
38
- import { applyFunctionPull, buildFunctionConfigPush, buildFunctionCreatePayload, buildFunctionUpdatePayload, collectManifestForPreflight, functionDiffEntity, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
38
+ import { applyFunctionPull, buildFunctionConfigPush, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
39
39
  import { FunctionBuildError } from "../lib/function-bundle.js";
40
- // #3182 — the local half of grant validation. The grammar is the vendored
40
+ // #3182 — the local half of capability validation. The grammar is the vendored
41
41
  // module the server also imports; this is where an author's own file is read.
42
- import { configObjectKeysFromTree, databaseTypesFromServer, databaseTypesFromTree, functionGrantErrors, functionNeedsManifest, mergeKnownDatabaseTypes, mergeKnownGrantTargets, } from "../lib/function-grants-preflight.js";
42
+ import { configObjectKeysFromTree, functionGrantErrors, mergeKnownGrantTargets, } from "../lib/function-grants-preflight.js";
43
43
  // #3182 — the typed handle's generated declarations and the compiler wiring
44
44
  // that makes an editor load them.
45
45
  import { functionTypeWiringProblems, staleFunctionTypeArtifacts, writeFunctionTypeArtifacts, } from "../lib/function-db-types.js";
@@ -3209,6 +3209,114 @@ export function diffProjectedRecords(local, remote) {
3209
3209
  }
3210
3210
  return diffs;
3211
3211
  }
3212
+ /**
3213
+ * The paths at which two projected records disagree, named the way the FILE
3214
+ * spells them (#3267) — what a `config diff` Modified row prints.
3215
+ *
3216
+ * `diffProjectedRecords` answers at the granularity of the projection, and
3217
+ * for a database type that is too coarse to act on: the 27 operations arrive
3218
+ * as one `operations` extra, the whole `[models.*]` tree as one `schema`
3219
+ * string. A row that read Modified for a reason the operator could not see —
3220
+ * an empty params set spelled two ways, an operation block the file declares
3221
+ * twice — was undiagnosable from the CLI. So this walks INTO each differing
3222
+ * field: a keyed child collection (`spec.childKeys`) is compared row by row
3223
+ * and named `operations["getInstitution"].params`, with a row only one side
3224
+ * has or that the file repeats named as such; a TOML-string field
3225
+ * (`spec.tomlFields`) is parsed on both sides and named by the path inside it
3226
+ * (`schema (models.Institution.fields.name.required)`); everything else is
3227
+ * walked as a value to its first differing leaf.
3228
+ *
3229
+ * Paths come back in projected-key order, keyed rows in key order, so the
3230
+ * list is stable across runs, and every leaf is its own entry — a schema
3231
+ * that differs in seven model fields is seven paths, so the caller's "+N
3232
+ * more" cap counts them the way it counts operations. A value the walk
3233
+ * cannot descend into (an array of a different length, a scalar against an
3234
+ * object) is named at the level it stopped.
3235
+ */
3236
+ export function describeProjectedDifferences(spec, local, remote) {
3237
+ const paths = [];
3238
+ for (const { field, local: l, server: r } of diffProjectedRecords(local, remote)) {
3239
+ const childKey = spec.childKeys?.[field];
3240
+ if (childKey && Array.isArray(l) && Array.isArray(r)) {
3241
+ paths.push(...keyedChildDifferences(field, childKey, l, r));
3242
+ continue;
3243
+ }
3244
+ if (spec.tomlFields?.includes(field) && typeof l === "string" && typeof r === "string") {
3245
+ let inner;
3246
+ try {
3247
+ inner = valueDifferences("", parseConfigToml(l), parseConfigToml(r));
3248
+ }
3249
+ catch {
3250
+ // A side that does not parse is named as the whole field below.
3251
+ }
3252
+ if (inner) {
3253
+ if (inner.length === 0) {
3254
+ paths.push(field);
3255
+ }
3256
+ else {
3257
+ paths.push(...inner.map((path) => `${field} (${path})`));
3258
+ }
3259
+ continue;
3260
+ }
3261
+ }
3262
+ paths.push(...valueDifferences(field, l, r));
3263
+ }
3264
+ return paths;
3265
+ }
3266
+ /** The leaf paths under `path` at which two values differ (see above). */
3267
+ function valueDifferences(path, a, b) {
3268
+ if (canonicalJsonStringify(a) === canonicalJsonStringify(b))
3269
+ return [];
3270
+ const join = (key) => (path ? `${path}.${key}` : key);
3271
+ if (isPlainObject(a) && isPlainObject(b)) {
3272
+ const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
3273
+ return keys.flatMap((key) => valueDifferences(join(key), a[key], b[key]));
3274
+ }
3275
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
3276
+ return a.flatMap((item, i) => valueDifferences(`${path}[${i}]`, item, b[i]));
3277
+ }
3278
+ return [path || "(document)"];
3279
+ }
3280
+ /**
3281
+ * A keyed child collection's differences, row by row. A key the file repeats
3282
+ * is named as the cause rather than compared: the server keys rows by it, so
3283
+ * only one of the blocks landed and no push can make the two sides agree
3284
+ * while the file says two things under one name.
3285
+ */
3286
+ function keyedChildDifferences(field, key, local, remote) {
3287
+ const label = (k) => `${field}[${JSON.stringify(k)}]`;
3288
+ const localByKey = new Map();
3289
+ for (const row of local) {
3290
+ const k = String(row?.[key] ?? "");
3291
+ localByKey.set(k, [...(localByKey.get(k) ?? []), row]);
3292
+ }
3293
+ const remoteByKey = new Map(remote.map((row) => [String(row?.[key] ?? ""), row]));
3294
+ const keys = [...new Set([...localByKey.keys(), ...remoteByKey.keys()])].sort();
3295
+ const paths = [];
3296
+ for (const k of keys) {
3297
+ const rows = localByKey.get(k);
3298
+ const remoteRow = remoteByKey.get(k);
3299
+ if (rows && rows.length > 1) {
3300
+ paths.push(`${label(k)} (declared ${rows.length} times locally)`);
3301
+ }
3302
+ else if (!rows) {
3303
+ paths.push(`${label(k)} (server only)`);
3304
+ }
3305
+ else if (remoteRow === undefined) {
3306
+ paths.push(`${label(k)} (local only)`);
3307
+ }
3308
+ else {
3309
+ paths.push(...valueDifferences(label(k), rows[0], remoteRow));
3310
+ }
3311
+ }
3312
+ return paths;
3313
+ }
3314
+ function isPlainObject(value) {
3315
+ return (value !== null &&
3316
+ typeof value === "object" &&
3317
+ !Array.isArray(value) &&
3318
+ typeof value.toJSON !== "function");
3319
+ }
3212
3320
  /**
3213
3321
  * The differing fields the definition says an UPDATE does not accept (#2731 B8).
3214
3322
  *
@@ -3415,7 +3523,6 @@ const CONFIG_DIFF_SPECS = {
3415
3523
  capabilities: Array.isArray(entity.capabilities)
3416
3524
  ? [...entity.capabilities].map((c) => String(c)).sort()
3417
3525
  : [],
3418
- unscopedReads: entity.unscopedReads === true,
3419
3526
  }),
3420
3527
  describe: (entity) => `function "${entity?.functionKey}"`,
3421
3528
  },
@@ -3692,6 +3799,8 @@ const CONFIG_DIFF_SPECS = {
3692
3799
  subscriptions: byKey(entity.__subscriptions, "subscriptionKey"),
3693
3800
  _unresolvedRuleSetName: entity._unresolvedRuleSetName,
3694
3801
  }),
3802
+ childKeys: { operations: "name", subscriptions: "subscriptionKey" },
3803
+ tomlFields: ["schema"],
3695
3804
  ruleSetRef: true,
3696
3805
  describe: (entity) => `database type "${entity?.databaseType}"`,
3697
3806
  },
@@ -3786,9 +3895,11 @@ export function compareLocalToRemote(spec, localParsed, remoteRecord, maps = {
3786
3895
  const localHash = computeExpandedContentHash(pair.local);
3787
3896
  if (remoteHash === localHash)
3788
3897
  return { status: "exists" };
3898
+ // #3267 — WHICH field, ahead of the remedy, so a row that differs for a
3899
+ // reason the operator cannot see in the file is diagnosable from the CLI.
3789
3900
  return {
3790
3901
  status: "modified",
3791
- hint: "run `config pull` to rewrite the local TOML to match running state",
3902
+ hint: namedDifferences(describeProjectedDifferences(spec, pair.local, pair.remote)) + "run `config pull` to rewrite the local TOML to match running state",
3792
3903
  };
3793
3904
  }
3794
3905
  catch {
@@ -3797,6 +3908,24 @@ export function compareLocalToRemote(spec, localParsed, remoteRecord, maps = {
3797
3908
  return { status: "exists", hint: "content not compared (comparison failed)" };
3798
3909
  }
3799
3910
  }
3911
+ /**
3912
+ * How many differing paths a Modified row names before saying "+N more"
3913
+ * (#3267). Enough to see the shape of a difference at a glance; the row is
3914
+ * one line of a whole-tree diff, not the place to print 27 operations.
3915
+ */
3916
+ const NAMED_DIFFERENCE_LIMIT = 5;
3917
+ /**
3918
+ * The `differs in …; ` prefix of a Modified row's hint (#3267), or nothing
3919
+ * when the walk could name no path — the hash still differs, so the row is
3920
+ * still Modified, but the hint then says only what it always said.
3921
+ */
3922
+ function namedDifferences(paths) {
3923
+ if (paths.length === 0)
3924
+ return "";
3925
+ const shown = paths.slice(0, NAMED_DIFFERENCE_LIMIT).join(", ");
3926
+ const more = paths.length - NAMED_DIFFERENCE_LIMIT;
3927
+ return `differs in ${shown}${more > 0 ? ` (+${more} more)` : ""}; `;
3928
+ }
3800
3929
  /** Hash a LOCAL config file's definition-projected field set (#2644). */
3801
3930
  export function hashLocalConfigForDiff(spec, parsedToml, maps, extra) {
3802
3931
  return computeExpandedContentHash(projectLocalConfig(spec, parsedToml, maps, extra));
@@ -8310,52 +8439,35 @@ export function registerConfigSyncCommands(sync) {
8310
8439
  preflightValidationErrors.push(` vars.toml: ${err?.message || String(err)}`);
8311
8440
  }
8312
8441
  }
8313
- // #3182 — a function's database grants, checked against the author's
8314
- // own file before anything is applied. The grammar and the
8315
- // unscoped-read rule are purely local; whether a grant names a type and
8316
- // model that will exist is answered against the tree's own
8317
- // `database-type-configs/` (applied BEFORE functions since this child)
8318
- // merged with the types the server already has. If the live listing
8319
- // cannot be fetched and the tree declares no types, the target check is
8320
- // skipped and the server — which is authoritative — has the last word.
8442
+ // #3182 — a function's capabilities, checked against the author's own
8443
+ // file before anything is applied. The grammar is purely local (and
8444
+ // since #3279 it keeps three kinds: `integration:<key>`,
8445
+ // `secret:<NAME>` and the high-blast opt-ins — a retired grant string
8446
+ // is refused here with the retirement); whether an `integration:`
8447
+ // names an integration that will exist is answered against the
8448
+ // tree's own `integrations/` (applied BEFORE functions) merged with
8449
+ // the integrations the server already has. If the live listing cannot
8450
+ // be fetched and the tree declares none, the target check is skipped
8451
+ // and the server — which is authoritative — has the last word.
8321
8452
  const preflightFunctionsDir = join(configDir, "functions");
8322
8453
  if (existsSync(preflightFunctionsDir)) {
8323
8454
  const functionFiles = selectFiles("function", preflightFunctionsDir, readdirSync(preflightFunctionsDir).filter((f) => f.endsWith(".toml")));
8324
8455
  if (functionFiles.length > 0) {
8325
- const treeTypes = databaseTypesFromTree(configDir);
8326
- let knownTypes = null;
8327
- try {
8328
- const live = await client.listDatabaseTypeConfigs(resolvedAppId);
8329
- knownTypes = mergeKnownDatabaseTypes(databaseTypesFromServer(Array.isArray(live) ? live : []), treeTypes);
8330
- }
8331
- catch {
8332
- knownTypes = treeTypes.size > 0 ? treeTypes : null;
8333
- }
8334
- // #3183 — the same question for the two grant families that name
8335
- // config-tree objects. `secret:` and `var:` are absent on purpose
8336
- // (D3183-002): their values are provisioned per environment, out
8337
- // of band, so a tree can never answer for them.
8338
8456
  const treeTargets = configObjectKeysFromTree(configDir);
8339
8457
  let knownTargets = null;
8340
8458
  try {
8341
- const [integrations, prompts] = await Promise.all([
8342
- client.listIntegrations(resolvedAppId, { status: "active" }),
8343
- client.listPrompts(resolvedAppId, { status: "active" }),
8344
- ]);
8459
+ const integrations = await client.listIntegrations(resolvedAppId, {
8460
+ status: "active",
8461
+ });
8345
8462
  knownTargets = mergeKnownGrantTargets({
8346
8463
  integrations: new Set((integrations?.items ?? [])
8347
8464
  .map((row) => String(row?.integrationKey ?? ""))
8348
8465
  .filter(Boolean)),
8349
- prompts: new Set((prompts?.items ?? [])
8350
- .map((row) => String(row?.promptKey ?? ""))
8351
- .filter(Boolean)),
8352
8466
  }, treeTargets);
8353
8467
  }
8354
8468
  catch {
8355
8469
  knownTargets =
8356
- treeTargets.integrations.size > 0 || treeTargets.prompts.size > 0
8357
- ? treeTargets
8358
- : null;
8470
+ treeTargets.integrations.size > 0 ? treeTargets : null;
8359
8471
  }
8360
8472
  for (const file of functionFiles) {
8361
8473
  const filePath = join(preflightFunctionsDir, file);
@@ -8367,37 +8479,10 @@ export function registerConfigSyncCommands(sync) {
8367
8479
  preflightValidationErrors.push(` ${filePath}: ${err?.message || String(err)}`);
8368
8480
  continue;
8369
8481
  }
8370
- // #3187 — a read grant declared without `unscopedReads = true`
8371
- // is accepted when the tree's registered reads are all bound to
8372
- // `$caller`, and only the MANIFEST says whether they are. So the
8373
- // functions whose decision depends on it are built and collected
8374
- // here, before anything is applied; the result is memoized
8375
- // against the built bundle, so the push below does not run the
8376
- // tree's module scope a second time (CR3187-001).
8377
- let manifest = null;
8378
- if (functionNeedsManifest(tomlData)) {
8379
- try {
8380
- manifest = await collectManifestForPreflight({
8381
- configDir,
8382
- // The same key the push loop derives, so a refusal names
8383
- // the function the way the rest of the run does.
8384
- functionKey: tomlData.function?.key || basename(file, ".toml"),
8385
- tomlData,
8386
- });
8387
- }
8388
- catch (err) {
8389
- // Fail-closed and early: the push would refuse this tree for
8390
- // the same reason, naming the same error.
8391
- preflightValidationErrors.push(` ${filePath}: ${err?.message || String(err)}`);
8392
- continue;
8393
- }
8394
- }
8395
8482
  for (const message of functionGrantErrors({
8396
8483
  filePath,
8397
8484
  tomlData,
8398
- knownTypes,
8399
8485
  knownTargets,
8400
- manifest,
8401
8486
  })) {
8402
8487
  preflightValidationErrors.push(` ${message}`);
8403
8488
  }
@@ -12838,7 +12923,7 @@ export function registerConfigSyncCommands(sync) {
12838
12923
  //
12839
12924
  // A function pushes on TWO axes and they are deliberately independent:
12840
12925
  //
12841
- // - the HEADER (`[function]`'s gate, runAs, schemas, plus the three
12926
+ // - the HEADER (`[function]`'s gate, schemas, plus the three
12842
12927
  // version-carried keys, all compared through the shared gate), and
12843
12928
  // - the CODE — the authored TOML bytes, every source file and the
12844
12929
  // built bundle, sent as one immutable config VERSION.
@@ -15422,7 +15507,7 @@ export function registerConfigSyncCommands(sync) {
15422
15507
  // below). Content comparison is a preview of `config pull` (pull would
15423
15508
  // rewrite the local TOML to match running state), exactly like the
15424
15509
  // workflow/transform blocks.
15425
- const compareConfigType = (typeLabel, slot, localParsed, remoteEntities, hashLocal, hashRemote, contentComparable) => {
15510
+ const compareConfigType = (typeLabel, slot, localParsed, remoteEntities, compare, contentComparable) => {
15426
15511
  for (const [key, parsed] of localParsed) {
15427
15512
  if (!remoteEntities.has(key)) {
15428
15513
  differences.push({ type: typeLabel, key, status: localOnlyStatus(slot, key) });
@@ -15437,16 +15522,10 @@ export function registerConfigSyncCommands(sync) {
15437
15522
  hint = "content not compared (rule sets unavailable)";
15438
15523
  }
15439
15524
  else {
15440
- try {
15441
- if (hashLocal(parsed) !== hashRemote(remoteEntities.get(key))) {
15442
- status = "modified";
15443
- hint = "run `config pull` to rewrite the local TOML to match running state";
15444
- }
15445
- }
15446
- catch {
15447
- // A malformed local file must not abort the whole diff.
15448
- hint = "content not compared (comparison failed)";
15449
- }
15525
+ // The shared verdict names the differing field (#3267) and
15526
+ // degrades a malformed local file to "not compared" rather than
15527
+ // aborting the whole diff.
15528
+ ({ status, hint } = compare(parsed, remoteEntities.get(key)));
15450
15529
  }
15451
15530
  differences.push({ type: typeLabel, key, status, hint });
15452
15531
  }
@@ -15465,7 +15544,7 @@ export function registerConfigSyncCommands(sync) {
15465
15544
  };
15466
15545
  const compareByDefinition = (label, slot, localParsed, remoteEntities, contentComparable) => {
15467
15546
  const spec = configDiffSpec(label);
15468
- compareConfigType(label, slot, localParsed, remoteEntities, (p) => hashLocalConfigForDiff(spec, p, diffMaps), (e) => hashRemoteConfigForDiff(spec, e, diffMaps), contentComparable);
15547
+ compareConfigType(label, slot, localParsed, remoteEntities, (p, e) => compareLocalToRemote(spec, p, e, diffMaps), contentComparable);
15469
15548
  };
15470
15549
  compareByDefinition("rule-set", "ruleSets", localRuleSetsParsed, remoteRuleSets, true);
15471
15550
  compareByDefinition("group-type-config", "groupTypeConfigs", localGroupTypeConfigsParsed, remoteGroupTypeConfigs, ruleSetsOk);
@@ -15497,20 +15576,14 @@ export function registerConfigSyncCommands(sync) {
15497
15576
  hint = "content not compared (fetch failed)";
15498
15577
  }
15499
15578
  else {
15500
- try {
15501
- const operations = Array.isArray(opsOutcome.value) ? opsOutcome.value : [];
15502
- const subscriptions = Array.isArray(subsOutcome.value) ? subsOutcome.value : [];
15503
- const dbSpec = configDiffSpec("database-type-config");
15504
- const remoteHash = hashRemoteConfigForDiff(dbSpec, remote, diffMaps, { operations, subscriptions });
15505
- const localHash = hashLocalConfigForDiff(dbSpec, parsed, diffMaps);
15506
- if (remoteHash !== localHash) {
15507
- status = "modified";
15508
- hint = "run `config pull` to rewrite the local TOML to match running state";
15509
- }
15510
- }
15511
- catch {
15512
- hint = "content not compared (comparison failed)";
15513
- }
15579
+ const operations = Array.isArray(opsOutcome.value) ? opsOutcome.value : [];
15580
+ const subscriptions = Array.isArray(subsOutcome.value) ? subsOutcome.value : [];
15581
+ // The shared verdict (#3267): a Modified row names the
15582
+ // operation, subscription, model field or `[type]` scalar it
15583
+ // differs in, and a comparison that throws degrades this one
15584
+ // row to "not compared". The ops/subscriptions are the remote
15585
+ // side's extra only — the file already carries its own.
15586
+ ({ status, hint } = compareLocalToRemote(configDiffSpec("database-type-config"), parsed, remote, diffMaps, { operations, subscriptions }));
15514
15587
  }
15515
15588
  }
15516
15589
  differences.push({ type: "database-type-config", key, status, hint });