primitive-admin 1.0.50 → 1.0.51

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 (59) hide show
  1. package/README.md +27 -8
  2. package/dist/src/commands/admins.js +25 -27
  3. package/dist/src/commands/admins.js.map +1 -1
  4. package/dist/src/commands/apps.js +8 -0
  5. package/dist/src/commands/apps.js.map +1 -1
  6. package/dist/src/commands/blob-buckets.js +30 -26
  7. package/dist/src/commands/blob-buckets.js.map +1 -1
  8. package/dist/src/commands/catalog.js +17 -18
  9. package/dist/src/commands/catalog.js.map +1 -1
  10. package/dist/src/commands/collection-type-configs.js +9 -9
  11. package/dist/src/commands/collection-type-configs.js.map +1 -1
  12. package/dist/src/commands/collections.js +33 -36
  13. package/dist/src/commands/collections.js.map +1 -1
  14. package/dist/src/commands/database-types.js +17 -18
  15. package/dist/src/commands/database-types.js.map +1 -1
  16. package/dist/src/commands/databases.js +41 -45
  17. package/dist/src/commands/databases.js.map +1 -1
  18. package/dist/src/commands/documents.js +17 -18
  19. package/dist/src/commands/documents.js.map +1 -1
  20. package/dist/src/commands/email-templates.js +9 -9
  21. package/dist/src/commands/email-templates.js.map +1 -1
  22. package/dist/src/commands/group-type-configs.js +9 -9
  23. package/dist/src/commands/group-type-configs.js.map +1 -1
  24. package/dist/src/commands/groups.js +17 -18
  25. package/dist/src/commands/groups.js.map +1 -1
  26. package/dist/src/commands/integrations.js +17 -18
  27. package/dist/src/commands/integrations.js.map +1 -1
  28. package/dist/src/commands/llm.js +4 -2
  29. package/dist/src/commands/llm.js.map +1 -1
  30. package/dist/src/commands/prompts.js +33 -36
  31. package/dist/src/commands/prompts.js.map +1 -1
  32. package/dist/src/commands/rule-sets.js +9 -9
  33. package/dist/src/commands/rule-sets.js.map +1 -1
  34. package/dist/src/commands/sync.d.ts +0 -14
  35. package/dist/src/commands/sync.js +143 -91
  36. package/dist/src/commands/sync.js.map +1 -1
  37. package/dist/src/commands/tokens.js +9 -9
  38. package/dist/src/commands/tokens.js.map +1 -1
  39. package/dist/src/commands/users.js +41 -45
  40. package/dist/src/commands/users.js.map +1 -1
  41. package/dist/src/commands/waitlist.js +9 -9
  42. package/dist/src/commands/waitlist.js.map +1 -1
  43. package/dist/src/commands/webhooks.js +9 -9
  44. package/dist/src/commands/webhooks.js.map +1 -1
  45. package/dist/src/commands/workflows.js +62 -36
  46. package/dist/src/commands/workflows.js.map +1 -1
  47. package/dist/src/lib/api-client.d.ts +9 -24
  48. package/dist/src/lib/api-client.js +36 -33
  49. package/dist/src/lib/api-client.js.map +1 -1
  50. package/dist/src/lib/confirm-prompt.d.ts +25 -8
  51. package/dist/src/lib/confirm-prompt.js +44 -19
  52. package/dist/src/lib/confirm-prompt.js.map +1 -1
  53. package/dist/src/lib/generated-allowlist.d.ts +28 -0
  54. package/dist/src/lib/generated-allowlist.js +181 -0
  55. package/dist/src/lib/generated-allowlist.js.map +1 -0
  56. package/dist/src/lib/workflow-toml-validator.d.ts +26 -17
  57. package/dist/src/lib/workflow-toml-validator.js +52 -141
  58. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  59. package/package.json +4 -1
@@ -12,6 +12,7 @@ import { createSnapshot, listSnapshots, resolveSnapshot, restoreSnapshot, pruneS
12
12
  import { validateWorkflowToml, formatWorkflowTomlErrors, } from "../lib/workflow-toml-validator.js";
13
13
  import { expandWorkflowTomlData } from "../lib/workflow-fragments.js";
14
14
  import { success, error, info, warn, keyValue, json, divider, } from "../lib/output.js";
15
+ import { confirmPrompt } from "../lib/confirm-prompt.js";
15
16
  import chalk from "chalk";
16
17
  function ensureDir(dirPath) {
17
18
  if (!existsSync(dirPath)) {
@@ -260,7 +261,13 @@ function serializeBlobBucket(bucket) {
260
261
  name: bucket.name,
261
262
  description: bucket.description || undefined,
262
263
  ttlTier: bucket.ttlTier,
263
- accessPolicy: bucket.accessPolicy,
264
+ // #1020: `preset` is the honest key; a custom bucket carries ruleSetId
265
+ // instead. Omit preset for custom buckets (preset === "custom").
266
+ preset: bucket.preset && bucket.preset !== "custom"
267
+ ? bucket.preset
268
+ : bucket.ruleSetId
269
+ ? undefined
270
+ : bucket.preset,
264
271
  },
265
272
  };
266
273
  if (bucket.ruleSetId) {
@@ -313,6 +320,9 @@ function serializeWorkflow(workflow, draft, configs) {
313
320
  status: workflow.status,
314
321
  activeConfigName: activeConfigName,
315
322
  accessRule: workflow.accessRule || undefined,
323
+ // #1081 — workflow principal mode. Emit only when set so a fresh
324
+ // pull → push round-trips it without writing a noisy `runAs = ""`.
325
+ runAs: workflow.runAs || undefined,
316
326
  perUserMaxRunning: workflow.perUserMaxRunning,
317
327
  perUserMaxQueued: workflow.perUserMaxQueued,
318
328
  dequeueOrder: workflow.dequeueOrder,
@@ -734,56 +744,6 @@ export async function pullScripts(client, appId, configDir, logger = () => { })
734
744
  }
735
745
  return { scriptEntities, count: items.length };
736
746
  }
737
- /**
738
- * Issue #973 — format the "stale referencing workflows" warning for a script
739
- * push. Pure (no I/O) so it can be unit-tested directly.
740
- *
741
- * `staleWorkflows` is the server's report of active workflows whose frozen
742
- * snapshot still pins a DIFFERENT (old) body hash for `scriptName` — i.e. the
743
- * workflows that will keep running the previous script body until republished.
744
- * Returns `null` when nothing is stale (no warning to print). When `dryRun` is
745
- * true the wording reflects that nothing was mutated yet.
746
- */
747
- export function formatStaleWorkflowsWarning(scriptName, staleWorkflows, dryRun = false) {
748
- if (!staleWorkflows || staleWorkflows.length === 0)
749
- return null;
750
- const labels = staleWorkflows.map((w) => w.workflowKey || w.name || "(unknown workflow)");
751
- const count = labels.length;
752
- const noun = count === 1 ? "workflow" : "workflows";
753
- const verb = dryRun ? "would still run" : "still run";
754
- return (`Script "${scriptName}" ${dryRun ? "would be updated" : "updated"}, but ` +
755
- `${count} ${noun} ${verb} the previous body until re-pushed: ` +
756
- `${labels.join(", ")}. ` +
757
- `Re-push (or republish) ${count === 1 ? "it" : "them"} to pick up the change ` +
758
- `(the run path reads each workflow's frozen snapshot, not the live script).`);
759
- }
760
- /**
761
- * Issue #973 — query the server for active workflows left stale by a script
762
- * push and print a warning naming them. Best-effort: a missing route (older
763
- * server) or any transport error is swallowed so it never fails the sync.
764
- *
765
- * `contentHash` is the body hash being pushed (the server's authoritative hash
766
- * after a real push, or the locally-computed would-be hash for `--dry-run`).
767
- */
768
- async function warnStaleWorkflowsForScript(client, appId, scriptId, contentHash, dryRun) {
769
- let report;
770
- try {
771
- report = await client.getStaleWorkflowsForScript(appId, scriptId, contentHash);
772
- }
773
- catch {
774
- // Older server without the route, or a transient failure — staleness
775
- // info is advisory, so we stay silent rather than break the push.
776
- return;
777
- }
778
- const message = formatStaleWorkflowsWarning(report.scriptName, report.staleWorkflows, dryRun);
779
- if (message) {
780
- warn(` ${message}`);
781
- }
782
- }
783
- /** SHA-256 hex of a string body, matching the server's script `contentHash`. */
784
- function sha256HexString(body) {
785
- return createHash("sha256").update(body, "utf-8").digest("hex");
786
- }
787
747
  async function pullTestCasesForBlock(client, appId, blockType, blockId, blockKey, configDir, testCaseEntities, lookupMaps) {
788
748
  let testCases;
789
749
  try {
@@ -2273,8 +2233,29 @@ Directory Structure:
2273
2233
  continue;
2274
2234
  }
2275
2235
  if (existingId) {
2276
- // Blob buckets don't have an update API - skip if already exists
2277
- info(` Blob bucket already exists, skipping: ${key}`);
2236
+ // #1020 (D8): idempotently apply preset/ruleSet/name/description
2237
+ // changes via PATCH. bucketKey/ttlTier are immutable and not sent.
2238
+ const updatePayload = {};
2239
+ if (bucket.preset)
2240
+ updatePayload.preset = bucket.preset;
2241
+ else if (bucket.accessPolicy)
2242
+ updatePayload.accessPolicy = bucket.accessPolicy;
2243
+ if (bucket.ruleSetId)
2244
+ updatePayload.ruleSetId = bucket.ruleSetId;
2245
+ if (bucket.name)
2246
+ updatePayload.name = bucket.name;
2247
+ if (bucket.description !== undefined)
2248
+ updatePayload.description = bucket.description || null;
2249
+ changes.push({ type: "blob-bucket", action: "update", key });
2250
+ if (!options.dryRun) {
2251
+ try {
2252
+ await client.updateBlobBucket(resolvedAppId, existingId, updatePayload);
2253
+ info(` Updated blob bucket: ${key}`);
2254
+ }
2255
+ catch (err) {
2256
+ warn(` Failed to update blob bucket ${key}: ${String(err?.message || err)}`);
2257
+ }
2258
+ }
2278
2259
  if (syncState?.entities?.blobBuckets?.[key]) {
2279
2260
  syncState.entities.blobBuckets[key].contentHash = computeFileHash(filePath);
2280
2261
  }
@@ -2284,8 +2265,11 @@ Directory Structure:
2284
2265
  bucketKey: key,
2285
2266
  name: bucket.name || key,
2286
2267
  ttlTier: bucket.ttlTier,
2287
- accessPolicy: bucket.accessPolicy,
2288
2268
  };
2269
+ if (bucket.preset)
2270
+ payload.preset = bucket.preset;
2271
+ else if (bucket.accessPolicy)
2272
+ payload.accessPolicy = bucket.accessPolicy;
2289
2273
  if (bucket.description)
2290
2274
  payload.description = bucket.description;
2291
2275
  if (bucket.ruleSetId)
@@ -2578,7 +2562,10 @@ Directory Structure:
2578
2562
  changes.push({ type: "script", action: "update", key: name });
2579
2563
  if (!options.dryRun) {
2580
2564
  try {
2581
- const updated = await client.updateScript(resolvedAppId, existingId, { body });
2565
+ // #1000 push a new ScriptConfig + activate it. Referencing
2566
+ // workflows pick up the new body on their next run (no
2567
+ // fan-out, no re-push needed).
2568
+ await client.pushScriptBody(resolvedAppId, existingId, body);
2582
2569
  info(` Updated script: ${name}`);
2583
2570
  if (syncState) {
2584
2571
  if (!syncState.entities.scripts) {
@@ -2586,24 +2573,16 @@ Directory Structure:
2586
2573
  }
2587
2574
  syncState.entities.scripts[name] = {
2588
2575
  id: existingId,
2589
- modifiedAt: updated?.modifiedAt ||
2590
- syncState.entities.scripts[name]?.modifiedAt ||
2576
+ modifiedAt: syncState.entities.scripts[name]?.modifiedAt ||
2591
2577
  new Date().toISOString(),
2592
2578
  contentHash: computeFileHash(filePath),
2593
2579
  };
2594
2580
  }
2595
- // Issue #973 — warn if active workflows still pin the OLD
2596
- // body. Use the server's authoritative post-push hash.
2597
- await warnStaleWorkflowsForScript(client, resolvedAppId, existingId, updated?.contentHash || sha256HexString(body), false);
2598
2581
  }
2599
2582
  catch (err) {
2600
2583
  throw wrapEntityError(err, "update", "script", name);
2601
2584
  }
2602
2585
  }
2603
- else {
2604
- // Dry-run: surface the would-be stale set without mutating.
2605
- await warnStaleWorkflowsForScript(client, resolvedAppId, existingId, sha256HexString(body), true);
2606
- }
2607
2586
  }
2608
2587
  else {
2609
2588
  // No id in local state — could be brand new OR could be
@@ -2616,7 +2595,7 @@ Directory Structure:
2616
2595
  changes.push({ type: "script", action: "update", key: name });
2617
2596
  if (!options.dryRun) {
2618
2597
  try {
2619
- const updated = await client.updateScript(resolvedAppId, match.scriptId, { body });
2598
+ await client.pushScriptBody(resolvedAppId, match.scriptId, body);
2620
2599
  info(` Updated script: ${name} (adopted from server)`);
2621
2600
  if (syncState) {
2622
2601
  if (!syncState.entities.scripts) {
@@ -2624,21 +2603,15 @@ Directory Structure:
2624
2603
  }
2625
2604
  syncState.entities.scripts[name] = {
2626
2605
  id: match.scriptId,
2627
- modifiedAt: updated?.modifiedAt || new Date().toISOString(),
2606
+ modifiedAt: new Date().toISOString(),
2628
2607
  contentHash: computeFileHash(filePath),
2629
2608
  };
2630
2609
  }
2631
- // Issue #973 — same staleness warning on the adopt path.
2632
- await warnStaleWorkflowsForScript(client, resolvedAppId, match.scriptId, updated?.contentHash || sha256HexString(body), false);
2633
2610
  }
2634
2611
  catch (err) {
2635
2612
  throw wrapEntityError(err, "update", "script", name);
2636
2613
  }
2637
2614
  }
2638
- else {
2639
- // Dry-run on the adopt path.
2640
- await warnStaleWorkflowsForScript(client, resolvedAppId, match.scriptId, sha256HexString(body), true);
2641
- }
2642
2615
  }
2643
2616
  else {
2644
2617
  changes.push({ type: "script", action: "create", key: name });
@@ -2738,6 +2711,10 @@ Directory Structure:
2738
2711
  description: workflow.description,
2739
2712
  status: workflow.status,
2740
2713
  accessRule: workflow.accessRule !== undefined ? (workflow.accessRule || null) : undefined,
2714
+ // #1081 — workflow principal mode. Absent key → undefined
2715
+ // (server leaves it untouched); explicit value/empty is
2716
+ // forwarded so it round-trips.
2717
+ runAs: workflow.runAs !== undefined ? (workflow.runAs || null) : undefined,
2741
2718
  perUserMaxRunning: workflow.perUserMaxRunning,
2742
2719
  perUserMaxQueued: workflow.perUserMaxQueued,
2743
2720
  dequeueOrder: workflow.dequeueOrder,
@@ -2832,6 +2809,8 @@ Directory Structure:
2832
2809
  inputSchema: workflow.inputSchema ? safeJsonParse(workflow.inputSchema) : undefined,
2833
2810
  outputSchema: workflow.outputSchema ? safeJsonParse(workflow.outputSchema) : undefined,
2834
2811
  accessRule: workflow.accessRule || undefined,
2812
+ // #1081 — workflow principal mode (absent → server default).
2813
+ runAs: workflow.runAs || undefined,
2835
2814
  perUserMaxRunning: workflow.perUserMaxRunning,
2836
2815
  perUserMaxQueued: workflow.perUserMaxQueued,
2837
2816
  dequeueOrder: workflow.dequeueOrder,
@@ -2946,6 +2925,15 @@ Directory Structure:
2946
2925
  `\nAborting push: ${validation.errors.length} unresolved $params reference(s) in ${file}.`);
2947
2926
  }
2948
2927
  const existingEntry = syncState?.entities?.databaseTypes?.[dbType];
2928
+ // Issue #915 (defect b): track whether the apply path rejected any
2929
+ // op for THIS file via the schema gate (OperationRefError /
2930
+ // SchemaRequiredError). On a non-transactional fresh-type push the
2931
+ // db-type lands but the bad op is blocked and the loop continues; if
2932
+ // we then wrote the file-level contentHash, the next push's
2933
+ // `shouldPushFile` would short-circuit and the missing op would be
2934
+ // invisible drift. When this flag is set we skip the contentHash
2935
+ // write so the drift stays visible and a corrected re-push converges.
2936
+ let fileHadGateRejectedOp = false;
2949
2937
  // Skip if file hasn't changed since last sync
2950
2938
  if (!options.force && existingEntry && !shouldPushFile(filePath, existingEntry.contentHash)) {
2951
2939
  skippedCount++;
@@ -3032,8 +3020,7 @@ Directory Structure:
3032
3020
  }
3033
3021
  // 2. Op-edit gate (op create/update dry-run) for every op in the
3034
3022
  // TOML. The server runs `runSchemaGate` and short-circuits
3035
- // before persisting. Skipped for a fresh type (no schema on the
3036
- // server yet → gate is a no-op anyway).
3023
+ // before persisting.
3037
3024
  //
3038
3025
  // The op gate is run against the schema THIS push is landing
3039
3026
  // (`schemaOverride`), not the stale stored schema — the real
@@ -3043,11 +3030,36 @@ Directory Structure:
3043
3030
  // a schema landed earlier in the same push"). When the push
3044
3031
  // isn't changing the schema, leave the override unset so the
3045
3032
  // server uses the stored schema.
3046
- if (existingEntry) {
3047
- const validateUpdateData = computeTypeUpdateData();
3048
- const proposedSchema = "schema" in validateUpdateData
3049
- ? validateUpdateData.schema
3050
- : undefined;
3033
+ //
3034
+ // Issue #915 (defect a): this gate now runs for FRESH types too
3035
+ // (previously guarded by `if (existingEntry)`). For a brand-new
3036
+ // db-type the server has no stored config, so we gate the op
3037
+ // against the PROPOSED local schema via `schemaOverride`. The
3038
+ // server's op-create dry-run accepts a missing type when a
3039
+ // `schemaOverride` is present (see
3040
+ // database-type-operations-controller.ts). A fresh schemaless
3041
+ // type (#666 bootstrap) sends an empty/absent override, and
3042
+ // `runSchemaGate` is a no-op without a schema — so that path
3043
+ // still succeeds.
3044
+ {
3045
+ const localHasSchema = typeof typeConfig.schema === "string" &&
3046
+ typeConfig.schema.trim().length > 0;
3047
+ let proposedSchema;
3048
+ if (existingEntry) {
3049
+ const validateUpdateData = computeTypeUpdateData();
3050
+ proposedSchema =
3051
+ "schema" in validateUpdateData
3052
+ ? validateUpdateData.schema
3053
+ : undefined;
3054
+ }
3055
+ else {
3056
+ // Fresh type: the proposed schema is whatever the local TOML
3057
+ // declares. A schemaless fresh type (no `[models.*]`) sends an
3058
+ // empty-string override so the server takes the no-schema =
3059
+ // no-op gate path rather than falling back to a (nonexistent)
3060
+ // stored config — keeping the #666 bootstrap convergent.
3061
+ proposedSchema = localHasSchema ? typeConfig.schema : "";
3062
+ }
3051
3063
  for (const op of operations) {
3052
3064
  const existingOp = existingOpsForType[op.name];
3053
3065
  opGateContext = {
@@ -3071,7 +3083,20 @@ Directory Structure:
3071
3083
  access: op.access,
3072
3084
  definition: op.definition,
3073
3085
  params: op.params,
3074
- }, { dryRun: true, schemaOverride: proposedSchema });
3086
+ }, {
3087
+ dryRun: true,
3088
+ schemaOverride: proposedSchema,
3089
+ // Issue #915 (defect a, follow-up): for a fresh type the
3090
+ // server has no stored config to read `defaultAccess`
3091
+ // from, so an op that omits `access` to inherit the
3092
+ // TOML-declared type-level `defaultAccess` would be
3093
+ // falsely rejected. Thread the proposed value so the
3094
+ // fresh-type dry-run gates the same way the real push
3095
+ // (which lands the type first) will.
3096
+ defaultAccess: typeof typeConfig.defaultAccess === "string"
3097
+ ? typeConfig.defaultAccess
3098
+ : undefined,
3099
+ });
3075
3100
  }
3076
3101
  }
3077
3102
  // Every op gate passed; clear the context so a later (non-op)
@@ -3563,6 +3588,10 @@ Directory Structure:
3563
3588
  key: `${dbType}/${op.name}`,
3564
3589
  message: err.message,
3565
3590
  });
3591
+ // Issue #915 (defect b): a gate-rejected op means this
3592
+ // file's state did NOT fully land — don't poison the
3593
+ // content hash (see the contentHash write below).
3594
+ fileHadGateRejectedOp = true;
3566
3595
  }
3567
3596
  else if (err instanceof OperationRefError) {
3568
3597
  schemaErrors.opRefs.push({
@@ -3571,6 +3600,7 @@ Directory Structure:
3571
3600
  refs: err.refs,
3572
3601
  message: err.message,
3573
3602
  });
3603
+ fileHadGateRejectedOp = true;
3574
3604
  }
3575
3605
  else {
3576
3606
  throw wrapEntityError(err, "update", "operation", `${dbType}/${op.name}`);
@@ -3608,6 +3638,10 @@ Directory Structure:
3608
3638
  key: `${dbType}/${op.name}`,
3609
3639
  message: err.message,
3610
3640
  });
3641
+ // Issue #915 (defect b): a gate-rejected op means this
3642
+ // file's state did NOT fully land — don't poison the
3643
+ // content hash (see the contentHash write below).
3644
+ fileHadGateRejectedOp = true;
3611
3645
  }
3612
3646
  else if (err instanceof OperationRefError) {
3613
3647
  schemaErrors.opRefs.push({
@@ -3616,6 +3650,7 @@ Directory Structure:
3616
3650
  refs: err.refs,
3617
3651
  message: err.message,
3618
3652
  });
3653
+ fileHadGateRejectedOp = true;
3619
3654
  }
3620
3655
  else {
3621
3656
  throw wrapEntityError(err, "create", "operation", `${dbType}/${op.name}`);
@@ -3747,9 +3782,27 @@ Directory Structure:
3747
3782
  // file. Recompute here so a truly-unchanged subsequent push skips
3748
3783
  // the whole file via `shouldPushFile`. `--force` bypasses that
3749
3784
  // skip, so forced re-pushes are unaffected.
3785
+ //
3786
+ // Issue #915 (defect b): when the gate rejected an op for this
3787
+ // file, do NOT record a matching content hash. The db-type itself
3788
+ // may have been created (the push is non-transactional), but the
3789
+ // blocked op never landed — a matching hash would make
3790
+ // `shouldPushFile` short-circuit the next push and hide the missing
3791
+ // op as "in sync". Clear the hash instead of writing it: an empty
3792
+ // hash forces `shouldPushFile` to re-evaluate the file, so a
3793
+ // subsequent `sync push --dry-run` still surfaces the op as drift
3794
+ // and a corrected re-push converges without manual file edits. We
3795
+ // clear (rather than merely skip) so the fresh-type create path's
3796
+ // earlier optimistic hash write (see `createDatabaseTypeConfig`
3797
+ // above) is also invalidated.
3750
3798
  if (!options.dryRun && syncState?.entities?.databaseTypes?.[dbType]) {
3751
- syncState.entities.databaseTypes[dbType].contentHash =
3752
- computeFileHash(filePath);
3799
+ if (fileHadGateRejectedOp) {
3800
+ syncState.entities.databaseTypes[dbType].contentHash = "";
3801
+ }
3802
+ else {
3803
+ syncState.entities.databaseTypes[dbType].contentHash =
3804
+ computeFileHash(filePath);
3805
+ }
3753
3806
  }
3754
3807
  }
3755
3808
  }
@@ -4572,15 +4625,14 @@ Directory Structure:
4572
4625
  warn(`You have uncommitted git changes under ${configDir}. ` +
4573
4626
  "Reverting will overwrite them.");
4574
4627
  }
4575
- const inquirer = await import("inquirer");
4576
- const { confirm } = await inquirer.default.prompt([
4577
- {
4578
- type: "confirm",
4579
- name: "confirm",
4580
- message: `Restore snapshot ${target.id} into ${configDir}? This overwrites the current sync directory.`,
4581
- default: false,
4582
- },
4583
- ]);
4628
+ let confirm;
4629
+ try {
4630
+ confirm = await confirmPrompt(`Restore snapshot ${target.id} into ${configDir}? This overwrites the current sync directory.`);
4631
+ }
4632
+ catch (err) {
4633
+ error(err.message);
4634
+ process.exit(1);
4635
+ }
4584
4636
  if (!confirm) {
4585
4637
  info("Cancelled.");
4586
4638
  return;