@keystrokehq/cli 0.0.21 → 0.0.23

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 (83) hide show
  1. package/dist/{accept.handler-C6KBLKmW.mjs → accept.handler-tvT9pleH.mjs} +1 -1
  2. package/dist/{admin-D2CQoZAN.mjs → admin-DsAQ0WWj.mjs} +9 -9
  3. package/dist/{agents-Bn0g5o0o.mjs → agents-Ccw0IZCx.mjs} +4 -4
  4. package/dist/{api-J9UL8pqZ.mjs → api-O5tdGdzc.mjs} +29 -3
  5. package/dist/{api-keys-BixCnZJW.mjs → api-keys-tle_m3kk.mjs} +5 -5
  6. package/dist/{auth-yCNMT8sJ.mjs → auth-DLaY5yCZ.mjs} +6 -11
  7. package/dist/{auth.handler-BedGpKh1.mjs → auth.handler-Dq2fXO3S.mjs} +16 -47
  8. package/dist/{build.handler-CyDc8jiZ.mjs → build.handler-ChqSwsT_.mjs} +3 -3
  9. package/dist/{clear-cache.handler-FmJPHdWG.mjs → clear-cache.handler-DpP1VlbR.mjs} +1 -1
  10. package/dist/{clear.handler-Cvb9chs4.mjs → clear.handler-FzohTmpU.mjs} +2 -6
  11. package/dist/{commander-9Kro0Dl3.mjs → commander-BTMzBiLq.mjs} +1 -1
  12. package/dist/{connect-DzVxjeYr.mjs → connect-BUu2ojK7.mjs} +26 -2
  13. package/dist/{connect.handler-DFQdxkWZ.mjs → connect.handler-D7oO_5WS.mjs} +38 -3
  14. package/dist/{context-B2cQ-Nt3.mjs → context-DHOTSgPb.mjs} +5 -10
  15. package/dist/{create.handler-v9B0Z9Yf.mjs → create.handler-BuxP18uj.mjs} +1 -1
  16. package/dist/{credential-env-map-Dvp00a4M.mjs → credential-env-map-CtmzNkwU.mjs} +1 -1
  17. package/dist/{credentials-Dr5lD7Hm.mjs → credentials-CELZ0QHu.mjs} +5 -5
  18. package/dist/{current-deployment-workflow-qMfOrRIu.mjs → current-deployment-workflow-CnzlDCBv.mjs} +2 -2
  19. package/dist/{current.handler-Cm_-JLyZ.mjs → current.handler-BXec-Bhy.mjs} +1 -1
  20. package/dist/{delete.handler-DtP_zUaq.mjs → delete.handler-CpYOMtsv.mjs} +1 -1
  21. package/dist/{deploy-CB6pfCuB.mjs → deploy-Cn3jN7Rl.mjs} +2 -2
  22. package/dist/{deploy.handler-Bg0dpSTj.mjs → deploy.handler-BikVS9ER.mjs} +7 -7
  23. package/dist/{diff.handler-CJPrszL1.mjs → diff.handler-C3EWVBOj.mjs} +3 -3
  24. package/dist/dist-D_KgdxW5.mjs +539 -0
  25. package/dist/{env.handler-6TrLd3fo.mjs → env.handler-BuFdzUoX.mjs} +4 -4
  26. package/dist/{init-DBMtY3eO.mjs → init-6tGGTpYO.mjs} +2 -2
  27. package/dist/{init.handler-BoYbE-6H.mjs → init.handler-Dcg9MOqx.mjs} +2 -2
  28. package/dist/{inspect.handler-Juu2vGbB.mjs → inspect.handler-BN6p2hI_.mjs} +3 -3
  29. package/dist/{integration-catalog-cYlTmOSb.mjs → integration-catalog-Cub_7xCw.mjs} +1 -1
  30. package/dist/{integrations-cwRfplNG.mjs → integrations-BRMzYHz1.mjs} +4 -4
  31. package/dist/{invites-DHyHZOY_.mjs → invites-RO4Dy-m6.mjs} +4 -4
  32. package/dist/{invites.list.handler-C-QpsG2J.mjs → invites.list.handler-B2RoiFCu.mjs} +1 -1
  33. package/dist/{invites.resend.handler-KFKbSPzR.mjs → invites.resend.handler-C4rzRkqX.mjs} +1 -1
  34. package/dist/{invites.revoke.handler-Cuz7jrGC.mjs → invites.revoke.handler-CMf6PpeL.mjs} +1 -1
  35. package/dist/keystroke.mjs +21 -21
  36. package/dist/{list.handler-DRe38pAj.mjs → list.handler-BjKZ9-QO.mjs} +2 -2
  37. package/dist/{list.handler-Cc-V1TNz.mjs → list.handler-DpdVFRYl.mjs} +1 -1
  38. package/dist/{list.handler-Do2tVOnu.mjs → list.handler-DrY5bgm1.mjs} +1 -1
  39. package/dist/{list.handler-CLGQDuo5.mjs → list.handler-cK8Y-daR.mjs} +3 -3
  40. package/dist/{list.handler-BWsl4iYw.mjs → list.handler-fcyAKTQe.mjs} +3 -3
  41. package/dist/{list.handler-6x3GVumu.mjs → list.handler-ijBH6Ow_.mjs} +1 -1
  42. package/dist/{list.handler-RtHhrTZ3.mjs → list.handler-wYGZhl1g.mjs} +1 -1
  43. package/dist/{listen-DZdSevsB.mjs → listen-CEn4PucV.mjs} +2 -2
  44. package/dist/{listen.handler-BccHe1jh.mjs → listen.handler-B7s6mz82.mjs} +1 -1
  45. package/dist/{logs-Bh_PBnu6.mjs → logs-DiH8JXn1.mjs} +1 -1
  46. package/dist/{logs.handler-DItDS1zw.mjs → logs.handler-D0sNlOz4.mjs} +1 -1
  47. package/dist/{members.add.handler-D3nQ_Ln6.mjs → members.add.handler-Dr9SCjrS.mjs} +1 -1
  48. package/dist/{members.invite.handler-rKhg5n_C.mjs → members.invite.handler-8-pTOtw_.mjs} +1 -1
  49. package/dist/{members.list.handler-C9Yh469k.mjs → members.list.handler-CUZGd-3B.mjs} +1 -1
  50. package/dist/{members.remove.handler-Dnrck-E6.mjs → members.remove.handler-BCiLt3pa.mjs} +1 -1
  51. package/dist/{members.update.handler-qo5r6arJ.mjs → members.update.handler-BAbB9ssa.mjs} +1 -1
  52. package/dist/{org-DnES84sS.mjs → org-BD4fj8Yh.mjs} +15 -15
  53. package/dist/{orgs.create.handler-DF4eEL-2.mjs → orgs.create.handler-B_7WjV3s.mjs} +1 -1
  54. package/dist/{orgs.get.handler-BmJnseQa.mjs → orgs.get.handler-BgjeDmfl.mjs} +1 -1
  55. package/dist/{orgs.list.handler-DDVvSbsT.mjs → orgs.list.handler-ZRdb-yu5.mjs} +1 -1
  56. package/dist/{paused.handler-BLUchSMD.mjs → paused.handler-CzQkBKS6.mjs} +1 -1
  57. package/dist/{projects-Cv14bBGy.mjs → projects-CgtfPFGu.mjs} +4 -4
  58. package/dist/{requirements.handler-BKFocUof.mjs → requirements.handler-ZZfHV6f0.mjs} +2 -2
  59. package/dist/{resolve-cli-credentials-DaMDaamj.mjs → resolve-cli-credentials-B4crOe_y.mjs} +5 -5
  60. package/dist/{resolve-project-Cj3MFnU0.mjs → resolve-project-DJJZIOmu.mjs} +1 -1
  61. package/dist/{run.handler-DZuUx0fi.mjs → run.handler-xeUVmlFk.mjs} +4 -4
  62. package/dist/{runs-BOo3j297.mjs → runs-CZRwB58H.mjs} +2 -2
  63. package/dist/{skills.command-B-MhRN3J.mjs → skills.command-DGIIIRX_.mjs} +1 -1
  64. package/dist/{status.handler-CW-EFhy3.mjs → status.handler-Ch_DtyBp.mjs} +4 -11
  65. package/dist/{switch.handler-BFGvj5c6.mjs → switch.handler-B3QBoSSl.mjs} +2 -2
  66. package/dist/{sync-CZ3iUPTA.mjs → sync-BlmgsC2W.mjs} +2 -2
  67. package/dist/{sync.handler-B1L8I9lF.mjs → sync.handler-xVxeG-S0.mjs} +4 -4
  68. package/dist/{task-target-build-CTgl4L42.mjs → task-target-build-CrPLSXnu.mjs} +1 -1
  69. package/dist/task-target-deploy-runner.mjs +4 -4
  70. package/dist/{test-Byq4hG3C.mjs → test-cuU0rf9C.mjs} +2 -2
  71. package/dist/{test.handler-BsrMMj5O.mjs → test.handler-B_C-T_IM.mjs} +4 -4
  72. package/dist/{test.handler-CAsVgOpT.mjs → test.handler-CLqnDqY6.mjs} +1 -1
  73. package/dist/{tool.handler-BHS5Z4J_.mjs → tool.handler-CmpzYYiC.mjs} +6 -6
  74. package/dist/{upgrade-bZVjVXnu.mjs → upgrade-B6Prb1K-.mjs} +1 -1
  75. package/dist/{upload.handler-DXVx2u3A.mjs → upload.handler-81mbKHTY.mjs} +4 -4
  76. package/dist/{users.get.handler-DqD2ELK2.mjs → users.get.handler-DoajzImx.mjs} +1 -1
  77. package/dist/{users.list.handler-DZSPvpGF.mjs → users.list.handler-CRk2J8mi.mjs} +1 -1
  78. package/dist/{users.set-role.handler-73smNUVF.mjs → users.set-role.handler-CHYjbx5M.mjs} +1 -1
  79. package/dist/{validate.handler-CmfcMX0t.mjs → validate.handler-CWG5HyO3.mjs} +3 -3
  80. package/dist/{workflow-build-Bi1Aacc5.mjs → workflow-build-CVG4DSCw.mjs} +1 -1
  81. package/dist/{workflows-C_C13Zr0.mjs → workflows-eztTnue4.mjs} +12 -12
  82. package/package.json +8 -8
  83. package/dist/dist-DuJjDZIf.mjs +0 -1094
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { n as JsonOptionSchema, t as JSON_OPTION_CONFIG } from "./output-BWcVRt-T.mjs";
4
- import { t as createTypedCommand } from "./commander-9Kro0Dl3.mjs";
4
+ import { t as createTypedCommand } from "./commander-BTMzBiLq.mjs";
5
5
  import { i as CredentialScopeValues, r as CredentialScopeSchema } from "./schema-DFJiNWyd.mjs";
6
6
  import { z } from "zod";
7
7
  //#region src/commands/credentials/list/list.command.ts
@@ -46,7 +46,7 @@ function createCredentialsListCommand() {
46
46
  description: "List credential sets on the server",
47
47
  schema: ListOptionsSchema,
48
48
  optionsConfig: LIST_OPTIONS_CONFIG,
49
- loadHandler: async () => (await import("./list.handler-CLGQDuo5.mjs")).handleCredentialsList
49
+ loadHandler: async () => (await import("./list.handler-cK8Y-daR.mjs")).handleCredentialsList
50
50
  });
51
51
  }
52
52
  //#endregion
@@ -65,7 +65,7 @@ function createCredentialsRequirementsCommand() {
65
65
  description: "Show what credentials built workflows need (keys, KEYSTROKE_* env names, workflows) — from dist manifests, no API call",
66
66
  schema: RequirementsOptionsSchema,
67
67
  optionsConfig: REQUIREMENTS_OPTIONS_CONFIG,
68
- loadHandler: async () => (await import("./requirements.handler-BKFocUof.mjs")).handleCredentialsRequirements
68
+ loadHandler: async () => (await import("./requirements.handler-ZZfHV6f0.mjs")).handleCredentialsRequirements
69
69
  });
70
70
  }
71
71
  //#endregion
@@ -126,7 +126,7 @@ function createCredentialsUploadCommand() {
126
126
  description: "Upload credentials from env vars to the server. Default: reads requirements from built workflow manifests. Use --integration for an official integration or --credential-set + --keys for a custom explicit upload.",
127
127
  schema: UploadOptionsSchema,
128
128
  optionsConfig: UPLOAD_OPTIONS_CONFIG,
129
- loadHandler: async () => (await import("./upload.handler-DXVx2u3A.mjs")).handleCredentialsUpload
129
+ loadHandler: async () => (await import("./upload.handler-81mbKHTY.mjs")).handleCredentialsUpload
130
130
  });
131
131
  }
132
132
  //#endregion
@@ -157,7 +157,7 @@ function createCredentialsCommand() {
157
157
  }
158
158
  },
159
159
  handler: async (opts, ctx) => {
160
- const { handleCredentialsList } = await import("./list.handler-CLGQDuo5.mjs");
160
+ const { handleCredentialsList } = await import("./list.handler-cK8Y-daR.mjs");
161
161
  await handleCredentialsList({
162
162
  ...opts,
163
163
  scope: void 0,
@@ -3,9 +3,9 @@
3
3
  import { A as WorkflowResolutionError, a as ui, g as getHttpStatus, h as getApiErrorCode } from "./keystroke.mjs";
4
4
  import { t as assertWorkflowProjectRoot } from "./project-config-DudGRFPO.mjs";
5
5
  import { a as readManifestsFromOutDir } from "./dist-Br4m3sFZ.mjs";
6
- import { t as requireWorkflowsDir } from "./resolve-project-Cj3MFnU0.mjs";
6
+ import { t as requireWorkflowsDir } from "./resolve-project-DJJZIOmu.mjs";
7
7
  import { t as createSpinnerProgress } from "./spinner-progress-BtEIJRX4.mjs";
8
- import { a as runWorkflowBuild, n as renderBuildFailure } from "./workflow-build-Bi1Aacc5.mjs";
8
+ import { a as runWorkflowBuild, n as renderBuildFailure } from "./workflow-build-CVG4DSCw.mjs";
9
9
  //#region src/commands/workflows/_shared/current-deployment-workflow.ts
10
10
  /**
11
11
  * Lightweight resolution: gets projectId from keystroke.config.ts and authoredWorkflowId
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { a as ui } from "./keystroke.mjs";
4
- import { i as requireClient } from "./context-B2cQ-Nt3.mjs";
4
+ import { i as requireClient } from "./context-DHOTSgPb.mjs";
5
5
  //#region src/commands/org/current.handler.ts
6
6
  async function handleOrgCurrent(_options, ctx) {
7
7
  if (!ctx.organizationId) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { a as ui, j as throwReportedCliExit, y as toErrorMessage } from "./keystroke.mjs";
4
- import { i as requireClient } from "./context-B2cQ-Nt3.mjs";
4
+ import { i as requireClient } from "./context-DHOTSgPb.mjs";
5
5
  //#region src/commands/api-keys/delete.handler.ts
6
6
  async function handleApiKeysDelete(options, ctx) {
7
7
  const client = requireClient(ctx);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { t as createTypedCommand } from "./commander-9Kro0Dl3.mjs";
3
+ import { t as createTypedCommand } from "./commander-BTMzBiLq.mjs";
4
4
  import { z } from "zod";
5
5
  //#region src/commands/deploy/deploy.command.ts
6
6
  /**
@@ -64,7 +64,7 @@ function createDeployCommand() {
64
64
  schema: DeployOptionsSchema,
65
65
  optionsConfig: DEPLOY_OPTIONS_CONFIG,
66
66
  contextMode: "auth",
67
- loadHandler: async () => (await import("./deploy.handler-Bg0dpSTj.mjs")).handleDeploy
67
+ loadHandler: async () => (await import("./deploy.handler-BikVS9ER.mjs")).handleDeploy
68
68
  });
69
69
  cmd.enablePositionalOptions();
70
70
  cmd.passThroughOptions();
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { D as CliExitError, P as logger, a as ui, l as isLocalMode, n as style, t as ANSI } from "./keystroke.mjs";
4
- import { i as projects } from "./dist-DuJjDZIf.mjs";
4
+ import { i as projects } from "./dist-D_KgdxW5.mjs";
5
5
  import { t as assertWorkflowProjectRoot } from "./project-config-DudGRFPO.mjs";
6
- import { r as requireAuthOptions, t as assertProjectConfigMatchesAuthenticatedOrg } from "./context-B2cQ-Nt3.mjs";
6
+ import { r as requireAuthOptions, t as assertProjectConfigMatchesAuthenticatedOrg } from "./context-DHOTSgPb.mjs";
7
7
  import { i as readAgentManifestsFromOutDir, o as readWorkflowsFromDisk } from "./dist-Br4m3sFZ.mjs";
8
- import { t as requireWorkflowsDir } from "./resolve-project-Cj3MFnU0.mjs";
9
- import { n as renderBuildFailure, r as renderBuildHeader } from "./workflow-build-Bi1Aacc5.mjs";
8
+ import { t as requireWorkflowsDir } from "./resolve-project-DJJZIOmu.mjs";
9
+ import { n as renderBuildFailure, r as renderBuildHeader } from "./workflow-build-CVG4DSCw.mjs";
10
10
  import { t as createBuildProgress } from "./build-progress-nYa14iBP.mjs";
11
11
  import { t as createDeployProgress } from "./deploy-progress-Dlp9aBDW.mjs";
12
12
  import { t as withErrorBoundary } from "./error-boundary-DVZipk-A.mjs";
13
- import { t as lookupCurrentDeploymentWorkflow } from "./current-deployment-workflow-qMfOrRIu.mjs";
13
+ import { t as lookupCurrentDeploymentWorkflow } from "./current-deployment-workflow-CnzlDCBv.mjs";
14
14
  import { t as computeWorkflowDiff } from "./diff-utils-CXKNQUXO.mjs";
15
15
  import path from "node:path";
16
16
  import { access } from "node:fs/promises";
@@ -198,7 +198,7 @@ async function handleDeploy(options, ctx) {
198
198
  let outDir;
199
199
  let artifactFilter;
200
200
  try {
201
- const { runWorkflowBuild } = await import("./workflow-build-Bi1Aacc5.mjs").then((n) => n.o);
201
+ const { runWorkflowBuild } = await import("./workflow-build-CVG4DSCw.mjs").then((n) => n.o);
202
202
  const buildOutcome = await runWorkflowBuild({
203
203
  workflowsDir,
204
204
  verbose: options.verbose,
@@ -271,7 +271,7 @@ function emitNewWebhookCredentials(result) {
271
271
  }
272
272
  async function handleTaskTargetDeploy(options) {
273
273
  renderBuildHeader(void 0);
274
- const buildResult = await (deployHandlerDependencies.buildTaskTargets ?? (await import("./task-target-build-CTgl4L42.mjs")).buildTaskTargets)({
274
+ const buildResult = await (deployHandlerDependencies.buildTaskTargets ?? (await import("./task-target-build-CrPLSXnu.mjs")).buildTaskTargets)({
275
275
  projectRoot: options.workflowsDir,
276
276
  targetFiles: options.targetFiles,
277
277
  disableSourcemaps: options.options.disableSourcemaps
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { D as CliExitError, a as ui, j as throwReportedCliExit, y as toErrorMessage } from "./keystroke.mjs";
4
- import { i as projects } from "./dist-DuJjDZIf.mjs";
4
+ import { i as projects } from "./dist-D_KgdxW5.mjs";
5
5
  import { i as writeJson } from "./output-BWcVRt-T.mjs";
6
- import { i as requireClient, t as assertProjectConfigMatchesAuthenticatedOrg } from "./context-B2cQ-Nt3.mjs";
7
- import { n as resolveLocalWorkflowManifest, t as lookupCurrentDeploymentWorkflow } from "./current-deployment-workflow-qMfOrRIu.mjs";
6
+ import { i as requireClient, t as assertProjectConfigMatchesAuthenticatedOrg } from "./context-DHOTSgPb.mjs";
7
+ import { n as resolveLocalWorkflowManifest, t as lookupCurrentDeploymentWorkflow } from "./current-deployment-workflow-CnzlDCBv.mjs";
8
8
  import { n as renderDiff, t as computeWorkflowDiff } from "./diff-utils-CXKNQUXO.mjs";
9
9
  //#region src/commands/workflows/diff/diff.handler.ts
10
10
  async function handleWorkflowsDiff(options, ctx) {
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { n as getKeystrokeBaseDir, t as KEYSTROKE_DIR } from "./paths-JzzFkXQA-CEipIeVl.mjs";
4
+ import * as os from "node:os";
5
+ import * as path$1 from "node:path";
6
+ import * as fs from "node:fs/promises";
7
+ import { z } from "zod";
8
+ //#region ../../packages/local-memory/dist/index.mjs
9
+ /**
10
+ * Writes `data` to `filePath` atomically.
11
+ *
12
+ * The contents are first written to a sibling temp file in the same directory,
13
+ * fsynced, chmodded, and then renamed onto the target path. On POSIX (and on
14
+ * Windows when the source and destination are on the same volume), `fs.rename`
15
+ * is atomic — readers see either the previous contents or the new contents,
16
+ * never a partial write.
17
+ *
18
+ * The temp filename embeds the process PID and a high-resolution timestamp
19
+ * (`${path}.tmp.${pid}.${ts}`) so concurrent writers do not collide.
20
+ *
21
+ * The parent directory is created with `recursive: true` if it does not already
22
+ * exist, so callers do not need to mkdir beforehand.
23
+ *
24
+ * If anything fails after the temp file is created (write, fsync, rename), the
25
+ * temp file is best-effort unlinked so we don't leak files.
26
+ */
27
+ async function atomicWriteFile(filePath, data, options = {}) {
28
+ const mode = options.mode ?? 420;
29
+ const dir = path$1.dirname(filePath);
30
+ await fs.mkdir(dir, { recursive: true });
31
+ const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 10)}`;
32
+ let handleClosed = false;
33
+ const handle = await fs.open(tmpPath, "w", mode);
34
+ try {
35
+ await handle.writeFile(data, "utf-8");
36
+ await handle.sync();
37
+ await handle.close();
38
+ handleClosed = true;
39
+ try {
40
+ await fs.chmod(tmpPath, mode);
41
+ } catch {}
42
+ await fs.rename(tmpPath, filePath);
43
+ } catch (error) {
44
+ if (!handleClosed) try {
45
+ await handle.close();
46
+ } catch {}
47
+ try {
48
+ await fs.unlink(tmpPath);
49
+ } catch {}
50
+ throw error;
51
+ }
52
+ }
53
+ /**
54
+ * Removes `filePath`. Returns `true` if the file existed and was removed,
55
+ * `false` if it did not exist (`ENOENT`). Other errors propagate.
56
+ */
57
+ async function unlinkFileIfExists(filePath) {
58
+ try {
59
+ await fs.unlink(filePath);
60
+ return true;
61
+ } catch (error) {
62
+ if (error.code === "ENOENT") return false;
63
+ throw error;
64
+ }
65
+ }
66
+ /**
67
+ * Reads a UTF-8 file and parses it as JSON.
68
+ *
69
+ * - Returns `null` when the file does not exist (`ENOENT`).
70
+ * - Throws on any other read error.
71
+ * - Throws (with the underlying `SyntaxError`) when the contents are not valid JSON.
72
+ *
73
+ * The return type is `unknown` — callers must validate the shape before using the
74
+ * value (typically via a Zod schema).
75
+ */
76
+ async function readJsonFile(filePath) {
77
+ let raw;
78
+ try {
79
+ raw = await fs.readFile(filePath, "utf-8");
80
+ } catch (error) {
81
+ if (error.code === "ENOENT") return null;
82
+ throw error;
83
+ }
84
+ return JSON.parse(raw);
85
+ }
86
+ /**
87
+ * Migrates `raw` to the current schema shape.
88
+ *
89
+ * Strategy:
90
+ * 1. Try the current schema first. If it matches, return immediately.
91
+ * 2. Otherwise find the migration whose `fromSchema` matches `raw`, run its
92
+ * `up`, and re-enter the loop with the result.
93
+ * 3. Continue until the current schema validates the value, or no migration
94
+ * matches — in which case throw.
95
+ *
96
+ * This handles single-hop and chained migrations (V1 → V2 → V3 → ...) without
97
+ * requiring the caller to order migrations carefully.
98
+ *
99
+ * Throws if the value matches no known schema. Callers typically wrap with a
100
+ * descriptive error referencing the file path.
101
+ */
102
+ function migrateRead(raw, def) {
103
+ const direct = def.schema.safeParse(raw);
104
+ if (direct.success) return {
105
+ value: direct.data,
106
+ migrated: false
107
+ };
108
+ const migrations = def.migrations ?? [];
109
+ if (migrations.length === 0) throw new Error("Stored value does not match the current schema and no migrations are configured.");
110
+ let current = raw;
111
+ const visited = /* @__PURE__ */ new Set();
112
+ for (let hop = 0; hop <= migrations.length; hop++) {
113
+ const match = def.schema.safeParse(current);
114
+ if (match.success) return {
115
+ value: match.data,
116
+ migrated: hop > 0
117
+ };
118
+ const next = migrations.find((m) => !visited.has(m) && m.fromSchema.safeParse(current).success);
119
+ if (!next) throw new Error("Stored value does not match the current schema or any known migration source.");
120
+ visited.add(next);
121
+ const validated = next.fromSchema.parse(current);
122
+ current = next.up(validated);
123
+ }
124
+ throw new Error(`Migration chain did not converge after ${migrations.length} hops. Possible cycle in migrations.`);
125
+ }
126
+ const FORBIDDEN_NAME_PATTERNS = [
127
+ {
128
+ test: (n) => n.length === 0,
129
+ message: "must not be empty"
130
+ },
131
+ {
132
+ test: (n) => n.includes("\\"),
133
+ message: "must not contain backslashes"
134
+ },
135
+ {
136
+ test: (n) => n.split("/").some((seg) => seg === ".."),
137
+ message: "must not contain \"..\" segments"
138
+ },
139
+ {
140
+ test: (n) => n.startsWith("/"),
141
+ message: "must be relative (cannot start with \"/\")"
142
+ }
143
+ ];
144
+ function validateStoreName(name) {
145
+ for (const { test, message } of FORBIDDEN_NAME_PATTERNS) if (test(name)) throw new Error(`Invalid Store name ${JSON.stringify(name)}: ${message}.`);
146
+ }
147
+ /**
148
+ * Typed, schema-validated, atomically-written abstraction over a single JSON
149
+ * file under `~/.keystroke/`.
150
+ *
151
+ * `Store` is an internal primitive of `@keystroke/local-memory`. It is not
152
+ * exported from the package's public API — domain controllers (`Credentials`,
153
+ * `Projects`, etc.) construct stores internally and expose domain-shaped methods
154
+ * to consumers.
155
+ */
156
+ var Store = class {
157
+ /** Absolute path to the file this store manages. */
158
+ filePath;
159
+ options;
160
+ constructor(options) {
161
+ validateStoreName(options.name);
162
+ this.options = options;
163
+ const homeDir = options.homeDir ?? os.homedir();
164
+ this.filePath = path$1.join(getKeystrokeBaseDir(homeDir), ...options.name.split("/"));
165
+ }
166
+ /**
167
+ * Returns the parsed contents of the file, or `null` when the file does not
168
+ * exist. If the on-disk shape does not match the current schema, registered
169
+ * migrations are applied and the upgraded value is persisted back.
170
+ *
171
+ * Throws when the file exists but matches no known schema, or when the file
172
+ * is unreadable for reasons other than ENOENT.
173
+ */
174
+ async read() {
175
+ const raw = await readJsonFile(this.filePath);
176
+ if (raw === null) return null;
177
+ let result;
178
+ try {
179
+ result = migrateRead(raw, {
180
+ schema: this.options.schema,
181
+ migrations: this.options.migrations
182
+ });
183
+ } catch (error) {
184
+ const message = error instanceof Error ? error.message : String(error);
185
+ throw new Error(`Invalid file at ${this.filePath}: ${message}`);
186
+ }
187
+ if (result.migrated) await this.write(result.value);
188
+ return result.value;
189
+ }
190
+ /**
191
+ * Replaces the file with `value`. Validates against the schema first (throws
192
+ * synchronously on invalid input, before any I/O). The write is atomic:
193
+ * write-temp + rename, never visible mid-write.
194
+ */
195
+ async write(value) {
196
+ const validated = this.options.schema.parse(value);
197
+ const json = `${JSON.stringify(validated, null, 2)}\n`;
198
+ await atomicWriteFile(this.filePath, json, { mode: this.options.fileMode });
199
+ }
200
+ /**
201
+ * Read-modify-write. Reads the current value (or `defaults` if the file does
202
+ * not exist), passes it to `fn`, validates the result, and writes it back
203
+ * atomically. Returns the new value.
204
+ *
205
+ * Throws if the file does not exist and no `defaults` were configured.
206
+ */
207
+ async update(fn) {
208
+ const current = await this.read();
209
+ let base;
210
+ if (current !== null) base = current;
211
+ else if (this.options.defaults !== void 0) base = this.options.defaults;
212
+ else throw new Error(`Cannot update ${this.filePath}: file does not exist and no defaults are configured.`);
213
+ const next = fn(base);
214
+ await this.write(next);
215
+ return next;
216
+ }
217
+ /**
218
+ * Returns the value at `key`, or `undefined` if the file does not exist.
219
+ * Each call re-reads the file (no caching).
220
+ */
221
+ async get(key) {
222
+ const value = await this.read();
223
+ if (value === null) return void 0;
224
+ return value[key];
225
+ }
226
+ /**
227
+ * Sets the value at `key`. Other keys are preserved. Uses `defaults` if the
228
+ * file does not exist (throws if missing and no defaults).
229
+ */
230
+ async set(key, value) {
231
+ await this.update((current) => ({
232
+ ...current,
233
+ [key]: value
234
+ }));
235
+ }
236
+ /**
237
+ * Returns true if the file exists and the value at `key` is not `undefined`.
238
+ */
239
+ async has(key) {
240
+ return await this.get(key) !== void 0;
241
+ }
242
+ /**
243
+ * Removes the file. Returns `true` if the file existed, `false` if it did
244
+ * not.
245
+ */
246
+ async delete() {
247
+ return unlinkFileIfExists(this.filePath);
248
+ }
249
+ };
250
+ const credentialUserSchema = z.object({
251
+ id: z.string().min(1),
252
+ email: z.email(),
253
+ name: z.string().optional()
254
+ });
255
+ const storedOrgEntrySchema = z.object({
256
+ organizationId: z.uuid(),
257
+ organizationName: z.string().min(1),
258
+ apiKeyId: z.uuid().optional(),
259
+ createdAt: z.string().min(1)
260
+ }).strict().extend({ apiKey: z.string().min(1) });
261
+ const credentialsMetadataSchema = z.object({
262
+ version: z.literal(3),
263
+ serverUrl: z.url(),
264
+ webUrl: z.url(),
265
+ user: credentialUserSchema.optional(),
266
+ activeOrgId: z.uuid().optional(),
267
+ orgs: z.array(storedOrgEntrySchema)
268
+ }).strict();
269
+ const CREDENTIALS_FILE = "credentials.json";
270
+ const METADATA_VERSION = 3;
271
+ /**
272
+ * Domain controller for Keystroke credentials.
273
+ *
274
+ * Backed by one `~/.keystroke/credentials.json` file with mode `0600`.
275
+ */
276
+ var Credentials = class {
277
+ credentials;
278
+ constructor(options = {}) {
279
+ this.credentials = new Store({
280
+ name: CREDENTIALS_FILE,
281
+ version: METADATA_VERSION,
282
+ schema: credentialsMetadataSchema,
283
+ fileMode: 384,
284
+ homeDir: options.homeDir
285
+ });
286
+ }
287
+ get credentialsFilePath() {
288
+ return this.credentials.filePath;
289
+ }
290
+ async getActiveOrg() {
291
+ const meta = await this.credentials.read();
292
+ if (!meta?.activeOrgId) return null;
293
+ const org = meta.orgs.find((o) => o.organizationId === meta.activeOrgId);
294
+ if (!org) return null;
295
+ return {
296
+ org: toOrgEntry(org),
297
+ apiKey: org.apiKey
298
+ };
299
+ }
300
+ async listOrgs() {
301
+ return (await this.credentials.read())?.orgs.map(toOrgEntry) ?? [];
302
+ }
303
+ async getApiKey(orgId) {
304
+ return (await this.credentials.read())?.orgs.find((org) => org.organizationId === orgId)?.apiKey ?? null;
305
+ }
306
+ async getServerUrls() {
307
+ const meta = await this.credentials.read();
308
+ return meta ? {
309
+ serverUrl: meta.serverUrl,
310
+ webUrl: meta.webUrl
311
+ } : null;
312
+ }
313
+ async getUser() {
314
+ return (await this.credentials.read())?.user;
315
+ }
316
+ async getActiveOrgId() {
317
+ return (await this.credentials.read())?.activeOrgId;
318
+ }
319
+ async hasStoredCredentials() {
320
+ const meta = await this.credentials.read();
321
+ return meta !== null && meta.orgs.length > 0;
322
+ }
323
+ async getStorageInfo() {
324
+ await this.credentials.read();
325
+ return { credentialsFilePath: this.credentials.filePath };
326
+ }
327
+ async upsertOrg(input) {
328
+ const existing = await this.credentials.read();
329
+ const orgWithApiKey = {
330
+ ...input.org,
331
+ apiKey: input.apiKey
332
+ };
333
+ const newMeta = existing ? {
334
+ ...existing,
335
+ serverUrl: input.serverUrl,
336
+ webUrl: input.webUrl,
337
+ user: input.user ?? existing.user,
338
+ activeOrgId: input.org.organizationId,
339
+ orgs: [...existing.orgs.filter((o) => o.organizationId !== input.org.organizationId), orgWithApiKey]
340
+ } : {
341
+ version: METADATA_VERSION,
342
+ serverUrl: input.serverUrl,
343
+ webUrl: input.webUrl,
344
+ user: input.user,
345
+ activeOrgId: input.org.organizationId,
346
+ orgs: [orgWithApiKey]
347
+ };
348
+ await this.credentials.write(newMeta);
349
+ }
350
+ async setActiveOrg(orgId) {
351
+ const meta = await this.credentials.read();
352
+ if (!meta) throw new Error("No stored credentials found. Run `keystroke auth` first.");
353
+ if (!meta.orgs.some((o) => o.organizationId === orgId)) throw new Error(`No stored API key for organization ${orgId}. Run \`keystroke auth\` to add credentials for this org.`);
354
+ await this.credentials.update((m) => ({
355
+ ...m,
356
+ activeOrgId: orgId
357
+ }));
358
+ }
359
+ async removeOrg(orgId) {
360
+ const meta = await this.credentials.read();
361
+ if (!meta) return null;
362
+ const removed = meta.orgs.find((o) => o.organizationId === orgId);
363
+ if (!removed) return null;
364
+ const remaining = meta.orgs.filter((o) => o.organizationId !== orgId);
365
+ if (remaining.length === 0) {
366
+ await this.credentials.delete();
367
+ return toOrgEntry(removed);
368
+ }
369
+ const newActiveId = meta.activeOrgId === orgId ? remaining[0]?.organizationId : meta.activeOrgId;
370
+ await this.credentials.write({
371
+ ...meta,
372
+ orgs: remaining,
373
+ activeOrgId: newActiveId
374
+ });
375
+ return toOrgEntry(removed);
376
+ }
377
+ async clear() {
378
+ await this.credentials.delete();
379
+ }
380
+ };
381
+ /**
382
+ * Production singleton — the primary public API for credential storage. Tests
383
+ * should construct `new Credentials({ homeDir })` instead.
384
+ */
385
+ const credentials = new Credentials();
386
+ function toOrgEntry({ apiKey: _apiKey, ...org }) {
387
+ return org;
388
+ }
389
+ const projectEntrySchema = z.object({
390
+ lastAccessed: z.string().min(1),
391
+ name: z.string().min(1).optional()
392
+ });
393
+ const projectsSchema = z.object({
394
+ version: z.literal(1),
395
+ projects: z.record(z.string(), projectEntrySchema),
396
+ lastProject: z.string().optional()
397
+ });
398
+ const PROJECTS_FILE = "projects.json";
399
+ const PROJECTS_VERSION = 1;
400
+ const PROJECTS_DEFAULTS = {
401
+ version: PROJECTS_VERSION,
402
+ projects: {}
403
+ };
404
+ /**
405
+ * Domain controller for tracked Keystroke projects.
406
+ *
407
+ * Backed by a single file under `~/.keystroke/projects.json`.
408
+ *
409
+ * Production code uses the `projects` singleton exported below. Tests
410
+ * construct a fresh instance with `homeDir` pointing at a tempdir.
411
+ */
412
+ var Projects = class {
413
+ store;
414
+ constructor(options = {}) {
415
+ this.store = new Store({
416
+ name: PROJECTS_FILE,
417
+ version: PROJECTS_VERSION,
418
+ schema: projectsSchema,
419
+ defaults: PROJECTS_DEFAULTS,
420
+ homeDir: options.homeDir
421
+ });
422
+ }
423
+ /** Absolute path to the projects file. */
424
+ get filePath() {
425
+ return this.store.filePath;
426
+ }
427
+ /**
428
+ * Upsert a project entry. Fire-and-forget safe — catches all errors
429
+ * internally. Never throws. Call without `await` if you don't need to wait.
430
+ *
431
+ * Tracking is best-effort telemetry; we never want a failed write to bubble
432
+ * up to a CLI command and degrade UX.
433
+ */
434
+ async track(projectPath, options) {
435
+ const absolutePath = path$1.resolve(projectPath);
436
+ try {
437
+ await this.store.update((data) => {
438
+ const previous = data.projects[absolutePath];
439
+ const entry = {
440
+ lastAccessed: (/* @__PURE__ */ new Date()).toISOString(),
441
+ ...options?.name ? { name: options.name } : previous?.name ? { name: previous.name } : {}
442
+ };
443
+ return {
444
+ ...data,
445
+ projects: {
446
+ ...data.projects,
447
+ [absolutePath]: entry
448
+ },
449
+ lastProject: absolutePath
450
+ };
451
+ });
452
+ } catch {}
453
+ }
454
+ /**
455
+ * Removes a project from tracking. Returns `true` if the project existed,
456
+ * `false` if it was not tracked. Errors propagate (this is invoked by an
457
+ * explicit user action; the caller should know if it failed).
458
+ */
459
+ async untrack(projectPath) {
460
+ const absolutePath = path$1.resolve(projectPath);
461
+ const existing = await this.store.read();
462
+ if (!existing || !(absolutePath in existing.projects)) return false;
463
+ const { [absolutePath]: _removed, ...remaining } = existing.projects;
464
+ const newLastProject = existing.lastProject === absolutePath ? void 0 : existing.lastProject;
465
+ await this.store.write({
466
+ ...existing,
467
+ projects: remaining,
468
+ lastProject: newLastProject
469
+ });
470
+ return true;
471
+ }
472
+ /**
473
+ * Returns all tracked projects as an array of `{ path, lastAccessed, name? }`.
474
+ * Empty array when nothing is tracked or the file is missing.
475
+ *
476
+ * Note: order is not guaranteed (matches `Object.entries` over a Record).
477
+ * Callers that need a sort order should sort explicitly.
478
+ *
479
+ * Best-effort: corrupt files are treated as "no projects" rather than
480
+ * throwing, since project tracking is telemetry data.
481
+ */
482
+ async list() {
483
+ const data = await this.readSafe();
484
+ if (!data) return [];
485
+ return Object.entries(data.projects).map(([projectPath, entry]) => ({
486
+ path: projectPath,
487
+ ...entry
488
+ }));
489
+ }
490
+ /**
491
+ * Returns the most-recently-tracked project path, or `undefined` when none
492
+ * has been recorded.
493
+ */
494
+ async getLast() {
495
+ return (await this.readSafe())?.lastProject;
496
+ }
497
+ /**
498
+ * Removes the projects file. Returns `true` if the file existed, `false`
499
+ * if it did not.
500
+ */
501
+ async clear() {
502
+ return this.store.delete();
503
+ }
504
+ /**
505
+ * Reads the underlying store but treats schema/JSON errors as "no projects"
506
+ * rather than propagating. This preserves the previous best-effort contract
507
+ * for the projects file.
508
+ */
509
+ async readSafe() {
510
+ try {
511
+ return await this.store.read();
512
+ } catch (error) {
513
+ if (error instanceof SyntaxError) return null;
514
+ if (error instanceof Error && error.message.startsWith("Invalid file at")) return null;
515
+ throw error;
516
+ }
517
+ }
518
+ };
519
+ /**
520
+ * Production singleton — the primary public API for project tracking. Tests
521
+ * should construct `new Projects({ homeDir })` instead.
522
+ */
523
+ const projects = new Projects();
524
+ /**
525
+ * Returns the Keystroke temp directory path.
526
+ *
527
+ * - `getKeystrokeTmpDir({ projectRoot })` → `projectRoot/.keystroke/tmp` — for build
528
+ * artifacts that require module resolution from the project (workflow-builder).
529
+ * - `getKeystrokeTmpDir()` → `~/.keystroke/tmp` — for general temporary storage.
530
+ *
531
+ * Callers must create the directory (e.g. `mkdir(path, { recursive: true })`)
532
+ * before use.
533
+ */
534
+ function getKeystrokeTmpDir(options) {
535
+ const baseDir = options?.projectRoot ? path$1.resolve(options.projectRoot) : os.homedir();
536
+ return path$1.join(baseDir, KEYSTROKE_DIR, "tmp");
537
+ }
538
+ //#endregion
539
+ export { projects as i, credentials as n, getKeystrokeTmpDir as r, Credentials as t };