@keystrokehq/cli 0.0.1

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 (122) hide show
  1. package/AGENTS-blurb.md +123 -0
  2. package/LICENSE +42 -0
  3. package/README.md +177 -0
  4. package/THIRD_PARTY_NOTICES.md +16 -0
  5. package/bin/keystroke.mjs +107 -0
  6. package/dist/_manifest-JSRE3H8k.mjs +385 -0
  7. package/dist/agent-bundle-package-DWV6B_5q-BtV7Xycc.mjs +2344 -0
  8. package/dist/agent-manifest-CDnbkR2f.mjs +245 -0
  9. package/dist/agents-CZJGxVqV.mjs +228 -0
  10. package/dist/api-keys-D2lgguuY.mjs +40 -0
  11. package/dist/auth-DN2VusyU.mjs +59 -0
  12. package/dist/auth.handler-CT1BQUvu.mjs +340 -0
  13. package/dist/browser-qwFrUH82.mjs +24 -0
  14. package/dist/build-agents-BmM_AsSd-BGi9wtzt.mjs +514 -0
  15. package/dist/build-metadata-BWS7uhd_-DR8gJjTX.mjs +1422 -0
  16. package/dist/build-progress-DgYKb4hB.mjs +183 -0
  17. package/dist/build-tasks-CdihpudT-D5r5HUHe.mjs +91 -0
  18. package/dist/build-workflows-CfxBnIWh-CdYPv8w2.mjs +370 -0
  19. package/dist/build.handler-4799CjWH.mjs +36 -0
  20. package/dist/chunk-CH6r78ws.mjs +37 -0
  21. package/dist/clear-cache.handler-B9tqSoSM.mjs +11 -0
  22. package/dist/clear.handler-BTIXXPTJ.mjs +42 -0
  23. package/dist/clear.handler-BydlX-zE.mjs +11 -0
  24. package/dist/commander-DfTVqQ-3.mjs +133 -0
  25. package/dist/concurrency-gXn9Rw8x-DNl2YtrS.mjs +20 -0
  26. package/dist/connect-BUXkeH0F.mjs +43 -0
  27. package/dist/connect.handler-CYel9cy6.mjs +430 -0
  28. package/dist/constants-CPpPdSNg.mjs +8 -0
  29. package/dist/context-T7HZuB97.mjs +138 -0
  30. package/dist/credential-env-map-CI8yWHVy.mjs +28 -0
  31. package/dist/credential-schema-mismatch-BKo5PjcQ.mjs +76 -0
  32. package/dist/credentials-CvmjU0lK.mjs +171 -0
  33. package/dist/credentials-OfVHOtG3.mjs +151216 -0
  34. package/dist/current-deployment-workflow-poHt27i3.mjs +94 -0
  35. package/dist/current.handler-B8zKzfPp.mjs +21 -0
  36. package/dist/delete.handler-bAu1iXVQ.mjs +17 -0
  37. package/dist/deploy-7Jjls436.mjs +26 -0
  38. package/dist/deploy-BOPIpRWm.mjs +74 -0
  39. package/dist/deploy-progress-BmGUNFKg.mjs +70 -0
  40. package/dist/deploy.handler-BAzgiNhd.mjs +370 -0
  41. package/dist/detect-env-access-CwkOYeYM-D_BCZqV6.mjs +209 -0
  42. package/dist/diff-utils-NEfcjqxt.mjs +185 -0
  43. package/dist/diff.handler-Du7SY8K4.mjs +47 -0
  44. package/dist/dist-BkJUoBiG.mjs +1116 -0
  45. package/dist/dist-CUK7yBM0.mjs +308 -0
  46. package/dist/env-91KwMKov.mjs +140 -0
  47. package/dist/env.handler-BAzBuMzQ.mjs +277 -0
  48. package/dist/error-boundary-VL-JLfIa.mjs +34 -0
  49. package/dist/file-metadata-D1vm-XY2.mjs +191 -0
  50. package/dist/get-intrinsic-zLxwtrLK.mjs +658 -0
  51. package/dist/import-module-CV84H5fZ-B_CBCmb4.mjs +1747 -0
  52. package/dist/init-DpMCotSK.mjs +45 -0
  53. package/dist/init.handler-CPRnif52.mjs +585 -0
  54. package/dist/inspect.handler-DT_cD036.mjs +146 -0
  55. package/dist/integration-catalog-Bt-L3GjF.mjs +104 -0
  56. package/dist/integrations-DlatPK4W.mjs +79 -0
  57. package/dist/keystroke.d.mts +3 -0
  58. package/dist/keystroke.mjs +707 -0
  59. package/dist/layout-CbMtQ2tm.mjs +67 -0
  60. package/dist/list-enrichment-y-cwizLr.mjs +189 -0
  61. package/dist/list.handler-BTWvCyjA.mjs +52 -0
  62. package/dist/list.handler-CWF_Dj15.mjs +24 -0
  63. package/dist/list.handler-CZ6G2x_G.mjs +75 -0
  64. package/dist/list.handler-DWaQkJaR.mjs +51 -0
  65. package/dist/list.handler-DqbFcBW7.mjs +180 -0
  66. package/dist/list.handler-lq3ZGAn4.mjs +104 -0
  67. package/dist/logs-BEg9L5l8.mjs +28 -0
  68. package/dist/logs.handler-6hoMBzqw.mjs +35 -0
  69. package/dist/logs.handler-BD_dXiL1.mjs +231 -0
  70. package/dist/metadata-layout-GUYIUo0i-_aG2zjue.mjs +5877 -0
  71. package/dist/normalize-path-CojS-CgQ-DLCOvnD1.mjs +20 -0
  72. package/dist/options-CeaTcFxP.mjs +43 -0
  73. package/dist/org-xLzBtt2_.mjs +41 -0
  74. package/dist/output-DM4b7KgY.mjs +72 -0
  75. package/dist/oxc-B3KI3rf_-n9d1hKNq.mjs +119 -0
  76. package/dist/paused.handler-BMFm9Cff.mjs +94 -0
  77. package/dist/project-config-D1qsQlO7.mjs +107 -0
  78. package/dist/projects-CHkRE9rS.mjs +1574 -0
  79. package/dist/projects-Cjb7sovS.mjs +30 -0
  80. package/dist/read-credential-keys-77a91T8M-KA0Iw0Z1.mjs +9 -0
  81. package/dist/register.handler-BPCdor1_.mjs +86 -0
  82. package/dist/requirements.handler-DPXdSks3.mjs +201 -0
  83. package/dist/resolve-project-DDJ29sCF.mjs +35 -0
  84. package/dist/rolldown-runtime-twds-ZHy-BWWzu8VG.mjs +15 -0
  85. package/dist/run-polling-CAgFRdK3.mjs +20 -0
  86. package/dist/runs-D9hNLb9A.mjs +259 -0
  87. package/dist/schedule-BXx3uXwr.mjs +1142 -0
  88. package/dist/schema-17qMfNyI.mjs +18 -0
  89. package/dist/schema-display-CgmeKigW.mjs +130 -0
  90. package/dist/schemas-CDib1RhE.mjs +125 -0
  91. package/dist/skills-sync.handler-DIy8GR16.mjs +34 -0
  92. package/dist/skills.command-CrjI2dN9.mjs +35 -0
  93. package/dist/skills.handler-Bz8bJKql.mjs +9 -0
  94. package/dist/source-analysis-Cj-ADyu--BJQcFPCG.mjs +144 -0
  95. package/dist/spinner-progress-DMVwgqO9.mjs +173 -0
  96. package/dist/src-C0X6u_Mw.mjs +1340 -0
  97. package/dist/src-eHwu-Gfw.mjs +369 -0
  98. package/dist/status.handler-BO4nwvWn.mjs +101 -0
  99. package/dist/switch.handler-D_9213Vf.mjs +51 -0
  100. package/dist/sync-BL_Mo5st.mjs +39 -0
  101. package/dist/sync-keystroke-agent-skills-Kx_H7UTd.mjs +70 -0
  102. package/dist/sync.handler-BUFPdzWz.mjs +82 -0
  103. package/dist/task-B2sZMaZu.mjs +8 -0
  104. package/dist/task-target-build-CBeCKbu2.mjs +432 -0
  105. package/dist/task-target-deploy-C5X-USeR.mjs +4 -0
  106. package/dist/task-target-deploy-CA6elFpF-BEr4gkol.mjs +271 -0
  107. package/dist/task-target-deploy-runner.d.mts +3 -0
  108. package/dist/task-target-deploy-runner.mjs +202 -0
  109. package/dist/test-BHTgR3UA.mjs +698 -0
  110. package/dist/test.handler-BcPQ8b74.mjs +13 -0
  111. package/dist/trigger-artifacts-DQPbQNqC-B4yeeFBY.mjs +239 -0
  112. package/dist/trigger-manifest-CY7brZeg.mjs +30 -0
  113. package/dist/try-deploy.handler-DqybNhXx.mjs +490 -0
  114. package/dist/upload-CkU--iDC.mjs +207 -0
  115. package/dist/upload.handler-DCtiznQp.mjs +441 -0
  116. package/dist/utils-CywxCDM7.mjs +14 -0
  117. package/dist/validate.handler-DOcTaJL0.mjs +280 -0
  118. package/dist/workflow-build-DBQaBfnn.mjs +1819 -0
  119. package/dist/workflow-bundler-BPiqVscj-X1PFFAuP.mjs +167 -0
  120. package/dist/workflows-g9z87AJJ.mjs +799 -0
  121. package/dist/writer-BG8poUm3-BbXlU2kI.mjs +426 -0
  122. package/package.json +87 -0
@@ -0,0 +1,698 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { C as CliExitError, D as throwReportedCliExit, h as toErrorMessage, t as ui } from "./keystroke.mjs";
4
+ import { l as resolveAuthOptions } from "./dist-CUK7yBM0.mjs";
5
+ import { t as assertWorkflowProjectRoot } from "./project-config-D1qsQlO7.mjs";
6
+ import { i as writeJson, n as JsonOptionSchema, t as JSON_OPTION_CONFIG } from "./output-DM4b7KgY.mjs";
7
+ import { t as createTypedCommand } from "./commander-DfTVqQ-3.mjs";
8
+ import { a as readManifestsFromOutDir } from "./dist-BkJUoBiG.mjs";
9
+ import { t as requireWorkflowsDir } from "./resolve-project-DDJ29sCF.mjs";
10
+ import { _ as getOfficialIntegrationMetadata, a as clearHostedActionDispatcher, c as registerHostedActionExecutionPolicy, d as clearOperationContext, f as registerOperationContext, g as registerRuntime, h as clearRuntime, l as clearOperationCredentialResolver, o as clearHostedActionExecutionPolicy, s as registerHostedActionDispatcher, u as registerOperationCredentialResolver } from "./src-C0X6u_Mw.mjs";
11
+ import { a as runWorkflowBuild, n as renderBuildFailure } from "./workflow-build-DBQaBfnn.mjs";
12
+ import { n as getProcessEnv } from "./env-91KwMKov.mjs";
13
+ import { i as requireClient, t as assertProjectConfigMatchesAuthenticatedOrg } from "./context-T7HZuB97.mjs";
14
+ import { t as lookupCurrentDeploymentWorkflow } from "./current-deployment-workflow-poHt27i3.mjs";
15
+ import { n as WorkflowsRunOptionsSchema, t as RUN_OPTIONS_CONFIG } from "./options-CeaTcFxP.mjs";
16
+ import { a as validateInputOrExit, i as resolveInput, r as pollForCompletion, t as handleWorkflowsTryDeploy } from "./try-deploy.handler-DqybNhXx.mjs";
17
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
18
+ import { tmpdir } from "node:os";
19
+ import path from "node:path";
20
+ import { z } from "zod";
21
+ import { pathToFileURL } from "node:url";
22
+ //#region ../../packages/workflow-core/src/test-runtime/credentials/env-resolver.ts
23
+ /**
24
+ * Computes the env var name for a credential key.
25
+ * Uses the credential set's namespace as the prefix if present, otherwise the raw id.
26
+ * e.g., namespace='keystroke', key='SLACK_BOT_TOKEN' → 'KEYSTROKE_SLACK_BOT_TOKEN'
27
+ * id='my-api', key='API_KEY' → 'MY_API_API_KEY'
28
+ */
29
+ function normalizeEnvVarName(credentialSet, key) {
30
+ return `${(credentialSet.namespace ?? credentialSet.id).replace(/[-]/g, "_").toUpperCase()}_${key}`;
31
+ }
32
+ function getCredentialSetKeys(credentialSet) {
33
+ return Object.keys(credentialSet.auth.shape);
34
+ }
35
+ function resolveCredentialsFromEnv(credentialSet, options = {}) {
36
+ const credentials = {};
37
+ const envVarNames = [];
38
+ for (const key of getCredentialSetKeys(credentialSet)) {
39
+ const envVarName = normalizeEnvVarName(credentialSet, key);
40
+ const value = process.env[envVarName];
41
+ if (value !== void 0) {
42
+ credentials[key] = value;
43
+ envVarNames.push(envVarName);
44
+ continue;
45
+ }
46
+ if (!options.lenient) throw new Error(`Missing credential "${envVarName}"`);
47
+ }
48
+ return {
49
+ credentials,
50
+ envVarNames
51
+ };
52
+ }
53
+ //#endregion
54
+ //#region ../../packages/workflow-core/src/test-runtime/credentials/cache.ts
55
+ function createCredentialCache() {
56
+ const cache = /* @__PURE__ */ new Map();
57
+ return { async getOrCreate(key, createValue) {
58
+ const existing = cache.get(key);
59
+ if (existing) return existing;
60
+ const pendingValue = createValue().catch((error) => {
61
+ cache.delete(key);
62
+ throw error;
63
+ });
64
+ cache.set(key, pendingValue);
65
+ return pendingValue;
66
+ } };
67
+ }
68
+ //#endregion
69
+ //#region ../../packages/workflow-core/src/test-runtime/credentials/server-resolver.ts
70
+ async function createServerCredentialResolver(options, dependencies = {
71
+ fetch: globalThis.fetch,
72
+ resolveAuthOptions
73
+ }) {
74
+ try {
75
+ const resolvedAuth = await dependencies.resolveAuthOptions({
76
+ apiKey: options.apiKey,
77
+ baseUrl: options.baseUrl,
78
+ credentialsPath: options.credentialsPath
79
+ });
80
+ if (!resolvedAuth.apiKey || !resolvedAuth.baseUrl) return;
81
+ const cache = createCredentialCache();
82
+ return { async resolveCredentialSet(credentialSet, keys) {
83
+ const resolvedCredentialSetId = credentialSet.resolvedCredentialSetId;
84
+ const cacheKey = `${resolvedCredentialSetId}:${[...keys].sort().join(",")}`;
85
+ return cache.getOrCreate(cacheKey, async () => {
86
+ const response = await dependencies.fetch(new URL("api/v1/credentials/resolve", resolvedAuth.baseUrl).toString(), {
87
+ method: "POST",
88
+ headers: {
89
+ Authorization: `Bearer ${resolvedAuth.apiKey}`,
90
+ "Content-Type": "application/json"
91
+ },
92
+ body: JSON.stringify({
93
+ credentialSetId: resolvedCredentialSetId,
94
+ keys: [...keys]
95
+ })
96
+ });
97
+ if (!response.ok) {
98
+ const message = formatCredentialResolutionErrorMessage({
99
+ credentialSet,
100
+ envVarNames: keys.map((key) => normalizeEnvVarName(credentialSet, key)),
101
+ errorBody: await readCredentialResolutionErrorBody(response),
102
+ status: response.status
103
+ });
104
+ if (response.status === 404) throw new Error(message);
105
+ throw new Error(message);
106
+ }
107
+ return (await response.json()).credentials;
108
+ });
109
+ } };
110
+ } catch {
111
+ return;
112
+ }
113
+ }
114
+ async function readCredentialResolutionErrorBody(response) {
115
+ try {
116
+ return await response.json();
117
+ } catch {
118
+ return;
119
+ }
120
+ }
121
+ function formatCredentialResolutionErrorMessage(params) {
122
+ const label = params.errorBody?.remediation?.displayName ?? humanizeCredentialSetLabel(params.credentialSet);
123
+ const primaryGuidance = params.errorBody?.remediation?.summary ?? params.errorBody?.message ?? params.errorBody?.detail ?? `Set up ${label} in Keystroke and retry.`;
124
+ const envHint = params.envVarNames.length > 0 ? ` Or export ${params.envVarNames.join(", ")} in the current shell and retry.` : "";
125
+ if (params.status === 404) return `Keystroke test credential resolution could not resolve ${label}. ${primaryGuidance}${envHint}`;
126
+ return `Keystroke test credential resolution failed for ${label}. ${primaryGuidance}${envHint}`;
127
+ }
128
+ function humanizeCredentialSetLabel(credentialSet) {
129
+ return credentialSet.id.split(/[-_]/u).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
130
+ }
131
+ //#endregion
132
+ //#region ../../packages/workflow-core/src/test-runtime/credentials/create-test-credential-resolver.ts
133
+ function pickMissingCredentialSets(credentialSets, existingCredentials) {
134
+ return credentialSets.filter((credentialSet) => existingCredentials?.[credentialSet.id] === void 0);
135
+ }
136
+ function createHostedIntegrationCredentialError(integrationId, sourceLabel, guidance, reason) {
137
+ const message = `Hosted integration "${integrationId}" cannot resolve credentials locally in ${sourceLabel}. Official hosted integrations must execute through the Keystroke dev server. ${guidance}`;
138
+ return reason instanceof Error ? new Error(message, { cause: reason }) : new Error(message);
139
+ }
140
+ async function createTestCredentialResolver(options, runtimeOptions = {}) {
141
+ if (options.mode === "off") return;
142
+ const sourceLabel = runtimeOptions.sourceLabel ?? "Keystroke test runtime";
143
+ const createCredentialGuidance = (integrationId) => (runtimeOptions.credentialGuidance ?? "Run `keystroke auth`, configure server-backed credentials (for example `keystroke connect ${integrationId}` when applicable), then retry.").replaceAll("${integrationId}", integrationId);
144
+ const serverResolver = options.mode === "env-only" ? void 0 : await createServerCredentialResolver(options);
145
+ const warnedCredentialSets = /* @__PURE__ */ new Set();
146
+ return { async resolveOperationCredentials(step, partialContext) {
147
+ const hostedIntegrationMetadata = getOfficialIntegrationMetadata(step);
148
+ const missingCredentialSets = pickMissingCredentialSets(step.credentialSets, partialContext.credentials);
149
+ if (missingCredentialSets.length === 0) return;
150
+ const resolvedCredentials = {};
151
+ for (const credentialSet of missingCredentialSets) {
152
+ const overrideCredentials = options.overrides?.[credentialSet.id];
153
+ const requiredKeys = getCredentialSetKeys(credentialSet);
154
+ if (overrideCredentials) {
155
+ resolvedCredentials[credentialSet.id] = overrideCredentials;
156
+ continue;
157
+ }
158
+ if (hostedIntegrationMetadata?.hosted) {
159
+ if (!serverResolver) throw createHostedIntegrationCredentialError(hostedIntegrationMetadata.integrationId, sourceLabel, createCredentialGuidance(hostedIntegrationMetadata.integrationId));
160
+ try {
161
+ resolvedCredentials[credentialSet.id] = await serverResolver.resolveCredentialSet(credentialSet, requiredKeys);
162
+ } catch (error) {
163
+ throw createHostedIntegrationCredentialError(hostedIntegrationMetadata.integrationId, sourceLabel, createCredentialGuidance(hostedIntegrationMetadata.integrationId), error);
164
+ }
165
+ continue;
166
+ }
167
+ const envResult = resolveCredentialsFromEnv(credentialSet, { lenient: true });
168
+ if (Object.keys(envResult.credentials).length > 0 && !warnedCredentialSets.has(credentialSet.id)) {
169
+ warnedCredentialSets.add(credentialSet.id);
170
+ console.warn(`[keystroke:test] Using local env vars for "${credentialSet.id}": ${envResult.envVarNames.join(", ")}`);
171
+ }
172
+ if (requiredKeys.every((key) => envResult.credentials[key] !== void 0)) {
173
+ resolvedCredentials[credentialSet.id] = envResult.credentials;
174
+ continue;
175
+ }
176
+ if (!serverResolver) {
177
+ const strictEnvResult = resolveCredentialsFromEnv(credentialSet);
178
+ resolvedCredentials[credentialSet.id] = strictEnvResult.credentials;
179
+ continue;
180
+ }
181
+ const serverCredentials = await serverResolver.resolveCredentialSet(credentialSet, requiredKeys);
182
+ resolvedCredentials[credentialSet.id] = {
183
+ ...serverCredentials,
184
+ ...envResult.credentials
185
+ };
186
+ }
187
+ return Object.keys(resolvedCredentials).length > 0 ? resolvedCredentials : void 0;
188
+ } };
189
+ }
190
+ //#endregion
191
+ //#region ../../packages/workflow-core/src/test-runtime/credentials/options.ts
192
+ const DEFAULT_TEST_CREDENTIAL_MODE = "auto";
193
+ //#endregion
194
+ //#region ../../packages/workflow-core/src/shared/create-mock-hook.ts
195
+ /**
196
+ * Creates a Hook that resolves immediately when awaited.
197
+ *
198
+ * Use when testing workflows that call ctx.createHook() (e.g., human-in-the-loop
199
+ * approval steps). Without a mock, the workflow would block waiting for external
200
+ * approval. This hook resolves immediately so tests complete without delay.
201
+ *
202
+ * @example
203
+ * const runtime = createTestWorkflowContext({
204
+ * createHook: () => createMockHook(),
205
+ * });
206
+ * await myWorkflow.run(input, runtime);
207
+ */
208
+ function createMockHook() {
209
+ const resolved = Promise.resolve(void 0);
210
+ return {
211
+ then: (onfulfilled, onrejected) => resolved.then(onfulfilled, onrejected),
212
+ token: Promise.resolve("mock-token"),
213
+ resumeUrl: Promise.resolve("http://mock/resume"),
214
+ cancelUrl: Promise.resolve("http://mock/cancel"),
215
+ approvalPageUrl: Promise.resolve("http://mock/approve")
216
+ };
217
+ }
218
+ //#endregion
219
+ //#region ../../packages/workflow-core/src/test-runtime/helpers/create-test-workflow-context.ts
220
+ function createTestWorkflowContext(options = {}) {
221
+ return {
222
+ createHook: options.createHook ?? ((_name) => createMockHook()),
223
+ stepContext: options.stepContext,
224
+ wait: options.wait ?? (async () => {}),
225
+ hasCredentialSet: options.hasCredentialSet ?? (() => false),
226
+ workflowGlobals: options.workflowGlobals,
227
+ workflowId: options.workflowId ?? "test-workflow-run"
228
+ };
229
+ }
230
+ //#endregion
231
+ //#region ../../packages/workflow-core/src/test-runtime/hosted-actions/create-test-hosted-action-dispatcher.ts
232
+ function createHostedDispatchError(integrationId, sourceLabel, detail, cause) {
233
+ const message = `Hosted integration "${integrationId}" failed in ${sourceLabel}. ${detail}`;
234
+ return cause instanceof Error ? new Error(message, { cause }) : new Error(message);
235
+ }
236
+ async function createTestHostedActionDispatcher(options, dependencies = {
237
+ fetch: globalThis.fetch,
238
+ resolveAuthOptions
239
+ }) {
240
+ if (options.mode !== "auto") return;
241
+ const source = options.source ?? "test-runtime";
242
+ const sourceLabel = options.sourceLabel ?? "Keystroke test runtime";
243
+ let resolvedAuth;
244
+ let authResolutionError;
245
+ try {
246
+ resolvedAuth = await dependencies.resolveAuthOptions({
247
+ apiKey: options.apiKey,
248
+ baseUrl: options.baseUrl,
249
+ credentialsPath: options.credentialsPath
250
+ });
251
+ } catch (error) {
252
+ authResolutionError = error;
253
+ }
254
+ return { async dispatch(operation, integrationId, input) {
255
+ const actionId = `${integrationId}.${operation.id}`;
256
+ const publicCredentialSetId = (operation.credentialSets ?? [])[0]?.id ?? integrationId;
257
+ if (authResolutionError) throw createHostedDispatchError(integrationId, sourceLabel, "Run `keystroke auth` and ensure the Keystroke dev server URL is configured before retrying.", authResolutionError);
258
+ if (!resolvedAuth?.apiKey) throw createHostedDispatchError(integrationId, sourceLabel, "Run `keystroke auth` so the test runtime can authenticate hosted action dispatch.");
259
+ if (!resolvedAuth.baseUrl) throw createHostedDispatchError(integrationId, sourceLabel, "Configure the Keystroke dev server URL and retry.");
260
+ const { apiKey, baseUrl } = resolvedAuth;
261
+ const organizationId = resolvedAuth.activeOrg?.organizationId ?? "local";
262
+ try {
263
+ const response = await dependencies.fetch(new URL("/api/v1/hosted-actions/dispatch", baseUrl).toString(), {
264
+ method: "POST",
265
+ headers: {
266
+ Authorization: `Bearer ${apiKey}`,
267
+ "Content-Type": "application/json"
268
+ },
269
+ body: JSON.stringify({
270
+ version: 1,
271
+ actionId,
272
+ source,
273
+ organizationId,
274
+ connection: { publicCredentialSetId },
275
+ correlation: { testRunId: crypto.randomUUID() },
276
+ input
277
+ })
278
+ });
279
+ if (response.status === 404) throw createHostedDispatchError(integrationId, sourceLabel, "The connected Keystroke server does not support hosted action dispatch for this integration.");
280
+ if (response.status === 401 || response.status === 403) throw createHostedDispatchError(integrationId, sourceLabel, "Authentication failed. Run `keystroke auth`, confirm the active organization, and retry.");
281
+ if (response.status === 422) {
282
+ const text = await response.text().catch(() => "");
283
+ throw createHostedDispatchError(integrationId, sourceLabel, "The Keystroke dev server is missing hosted/platform credentials for this integration. Configure the server-side credentials and retry.", new Error(text));
284
+ }
285
+ if (!response.ok) {
286
+ const text = await response.text().catch(() => "");
287
+ throw createHostedDispatchError(integrationId, sourceLabel, `Dispatch returned status ${response.status}. ${text}`.trim());
288
+ }
289
+ const body = await response.json();
290
+ if (!body.ok) throw createHostedDispatchError(integrationId, sourceLabel, `Hosted action "${actionId}" failed: [${body.error?.code}] ${body.error?.message}`);
291
+ return {
292
+ handled: true,
293
+ result: body.result
294
+ };
295
+ } catch (error) {
296
+ if (error instanceof TypeError && "cause" in error) throw createHostedDispatchError(integrationId, sourceLabel, "The Keystroke dev server is unreachable. Start the server and retry.", error);
297
+ throw error;
298
+ }
299
+ } };
300
+ }
301
+ //#endregion
302
+ //#region ../../packages/workflow-core/src/test-runtime/runtime/create-keystroke-test-runtime.ts
303
+ async function createKeystrokeTestRuntime(options = {}) {
304
+ clearHostedActionDispatcher();
305
+ clearHostedActionExecutionPolicy();
306
+ clearOperationCredentialResolver();
307
+ clearOperationContext();
308
+ clearRuntime();
309
+ const credentials = {
310
+ mode: DEFAULT_TEST_CREDENTIAL_MODE,
311
+ ...options.credentials
312
+ };
313
+ const sourceLabel = options.sourceLabel ?? "Keystroke test runtime";
314
+ const credentialResolver = await createTestCredentialResolver(credentials, { sourceLabel });
315
+ const hostedActionDispatcher = await createTestHostedActionDispatcher({
316
+ ...credentials,
317
+ source: options.source,
318
+ sourceLabel
319
+ });
320
+ registerRuntime({ getRuntime: () => ({
321
+ context: createTestWorkflowContext(options),
322
+ stepContext: options.stepContext
323
+ }) });
324
+ registerOperationContext({ getOperationContext: () => ({
325
+ attempt: options.stepContext?.attempt,
326
+ credentials: options.stepContext?.credentials ?? {},
327
+ maxAttempts: options.stepContext?.maxAttempts,
328
+ stepId: options.stepContext?.stepId ?? "test-step-run",
329
+ workflowGlobals: options.workflowGlobals
330
+ }) });
331
+ if (credentials.mode !== "off") registerHostedActionExecutionPolicy("strict");
332
+ if (credentialResolver) registerOperationCredentialResolver(credentialResolver);
333
+ if (hostedActionDispatcher) registerHostedActionDispatcher(hostedActionDispatcher);
334
+ return { dispose() {
335
+ clearHostedActionDispatcher();
336
+ clearHostedActionExecutionPolicy();
337
+ clearOperationCredentialResolver();
338
+ clearOperationContext();
339
+ clearRuntime();
340
+ } };
341
+ }
342
+ //#endregion
343
+ //#region ../../packages/workflow-core/src/test-runtime/tools/create-test-tool-context.ts
344
+ /**
345
+ * Builds a minimal `OperationRunContext` for tool/step unit tests.
346
+ *
347
+ * Propagates every `Partial<OperationRunContext>` field the caller provides
348
+ * (credentials, workflowGlobals, stepId, attempt, maxAttempts, toolCallId,
349
+ * traceparent, tracestate). This keeps tests authoritative when they want
350
+ * to assert on retry counts, telemetry headers, or tool-call ids.
351
+ */
352
+ function createTestToolContext(options = {}) {
353
+ return {
354
+ ...options,
355
+ credentials: options.credentials ?? {},
356
+ workflowGlobals: options.workflowGlobals
357
+ };
358
+ }
359
+ //#endregion
360
+ //#region ../../packages/workflow-core/src/test-runtime/tools/run-tool.ts
361
+ async function runTool(tool, input, options) {
362
+ const ctx = createTestToolContext(options);
363
+ return tool.run(input, ctx);
364
+ }
365
+ //#endregion
366
+ //#region src/commands/test/tool.handler.ts
367
+ const SECRET_FIELD_PATTERN = /(api[_-]?key|access[_-]?key|private[_-]?key|password|secret|token|credential)/i;
368
+ const testToolDependencies = {
369
+ readManifestsFromOutDir,
370
+ runWorkflowBuild,
371
+ createKeystrokeTestRuntime,
372
+ runTool
373
+ };
374
+ async function handleTestTool(options, ctx) {
375
+ const client = requireClient(ctx);
376
+ const workflowsDir = await requireWorkflowsDir(options.path);
377
+ const projectConfig = await assertWorkflowProjectRoot(workflowsDir);
378
+ await assertProjectConfigMatchesAuthenticatedOrg(client, projectConfig);
379
+ const input = await resolveInput({
380
+ ...options,
381
+ workflow: options.toolName
382
+ });
383
+ const resolved = await resolveBuiltTool(workflowsDir, options.toolName, options.agent);
384
+ if (resolved.tool.sourceKind === "workflow") {
385
+ await executeWorkflowTool({
386
+ options,
387
+ ctx,
388
+ input,
389
+ resolved,
390
+ projectId: projectConfig.projectId
391
+ });
392
+ return;
393
+ }
394
+ if (resolved.tool.sourceKind === "operation") {
395
+ await executeOperationTool({
396
+ options,
397
+ ctx,
398
+ input,
399
+ resolved
400
+ });
401
+ return;
402
+ }
403
+ throwReportedCliExit(`Tool "${options.toolName}" has unsupported sourceKind "${resolved.tool.sourceKind ?? "unknown"}".`);
404
+ }
405
+ async function executeWorkflowTool(params) {
406
+ const { options, ctx, input, resolved, projectId } = params;
407
+ const client = requireClient(ctx);
408
+ if (!resolved.tool.authoredWorkflowId) throwReportedCliExit(`Workflow tool "${options.toolName}" is missing authoredWorkflowId.`);
409
+ const inputSchema = (await resolveWorkflowManifest(resolved.workflowsDir, resolved.tool.authoredWorkflowId)).workflowSchemas.input;
410
+ if (inputSchema && typeof inputSchema === "object" && !Array.isArray(inputSchema)) validateInputOrExit(options.toolName, input, inputSchema);
411
+ const deploymentLookup = await lookupCurrentDeploymentWorkflow(client, {
412
+ projectId,
413
+ authoredWorkflowId: resolved.tool.authoredWorkflowId
414
+ });
415
+ if (deploymentLookup.status !== "found") throwReportedCliExit(`Workflow tool "${options.toolName}" is not available in the current deployment. Run \`keystroke deploy\` first.`);
416
+ if (resolved.tool.deploymentId && deploymentLookup.workflow.deploymentId !== resolved.tool.deploymentId) throwReportedCliExit(`Workflow tool "${options.toolName}" is pinned to deployment ${resolved.tool.deploymentId}, but the current deployment is ${deploymentLookup.workflow.deploymentId}. Redeploy the agent before testing.`);
417
+ ui.header(`Testing workflow tool "${options.toolName}"...`);
418
+ const { runId } = await client.workflows.execute({
419
+ projectId,
420
+ authoredWorkflowId: resolved.tool.authoredWorkflowId,
421
+ args: [input]
422
+ });
423
+ const snapshot = await pollForCompletion(client, runId, options.timeout, options.verbose);
424
+ if (ctx.jsonMode) {
425
+ writeJson({
426
+ toolName: options.toolName,
427
+ sourceKind: "workflow",
428
+ runId,
429
+ run: snapshot.run,
430
+ logs: snapshot.logs
431
+ });
432
+ return;
433
+ }
434
+ ui.hint(`Run ID: ${runId}`);
435
+ if (snapshot.run.status === "completed") {
436
+ ui.success(`Tool "${options.toolName}" completed.`);
437
+ if (snapshot.run.output !== void 0 && snapshot.run.output !== null) {
438
+ ui.br();
439
+ ui.header("Output:");
440
+ ui.text(JSON.stringify(snapshot.run.output, null, 2));
441
+ }
442
+ return;
443
+ }
444
+ if (snapshot.run.status === "failed") {
445
+ if (snapshot.run.error) ui.error(toErrorMessage(snapshot.run.error));
446
+ throw new CliExitError(`Tool "${options.toolName}" failed.`);
447
+ }
448
+ ui.warn(`Tool "${options.toolName}" still ${snapshot.run.status} after ${options.timeout}s.`);
449
+ throw new CliExitError(`Tool "${options.toolName}" did not complete before timeout.`);
450
+ }
451
+ async function executeOperationTool(params) {
452
+ const { options, ctx, input, resolved } = params;
453
+ const operation = await loadOperationTool(resolved.artifact, resolved.tool);
454
+ const runtime = await testToolDependencies.createKeystrokeTestRuntime({
455
+ credentials: {
456
+ mode: "auto",
457
+ apiKey: ctx.apiKey,
458
+ baseUrl: ctx.baseUrl,
459
+ credentialsPath: ctx.credentialsPath
460
+ },
461
+ source: "cli",
462
+ sourceLabel: "CLI"
463
+ });
464
+ try {
465
+ ui.header(`Testing operation tool "${options.toolName}"...`);
466
+ const redacted = redactToolOutput(await testToolDependencies.runTool(operation, input), resolved.artifact.manifest);
467
+ if (ctx.jsonMode) {
468
+ writeJson({
469
+ toolName: options.toolName,
470
+ sourceKind: "operation",
471
+ output: redacted.value
472
+ });
473
+ return;
474
+ }
475
+ ui.success(`Tool "${options.toolName}" completed.`);
476
+ if (redacted.changed) ui.warn("Output contained credential-looking values and was redacted.");
477
+ ui.br();
478
+ ui.header("Output:");
479
+ ui.text(JSON.stringify(redacted.value, null, 2));
480
+ } finally {
481
+ runtime.dispose();
482
+ }
483
+ }
484
+ async function resolveBuiltTool(workflowsDir, toolName, agentRef) {
485
+ let build;
486
+ try {
487
+ build = await testToolDependencies.runWorkflowBuild({
488
+ workflowsDir,
489
+ verbose: false,
490
+ force: false
491
+ });
492
+ } catch (error) {
493
+ renderBuildFailure(error);
494
+ throwReportedCliExit(`Build failed while resolving tool "${toolName}".`, { cause: error });
495
+ }
496
+ const matches = findToolMatches(build.result.agentArtifacts, toolName, agentRef, workflowsDir);
497
+ if (matches.length === 0) throwReportedCliExit(`Tool "${toolName}" not found in built agent manifests.`);
498
+ if (matches.length > 1) {
499
+ ui.error(`Tool "${toolName}" exists in multiple agents.`);
500
+ for (const match of matches) ui.hint(`- ${match.artifact.manifest.authoredAgentId} (${match.artifact.manifest.agentName})`);
501
+ ui.hint("Pass --agent <agentId> to select one.");
502
+ throwReportedCliExit(`Tool "${toolName}" exists in multiple agents.`);
503
+ }
504
+ const match = matches[0];
505
+ if (!match) throwReportedCliExit(`Tool "${toolName}" not found in built agent manifests.`);
506
+ return match;
507
+ }
508
+ function findToolMatches(artifacts, toolName, agentRef, workflowsDir) {
509
+ const matches = [];
510
+ for (const artifact of artifacts) {
511
+ if (agentRef && !manifestMatchesRef(artifact.manifest, agentRef)) continue;
512
+ const tool = (artifact.manifest.tools ?? []).find((candidate) => candidate.toolName === toolName);
513
+ if (tool) matches.push({
514
+ artifact,
515
+ tool,
516
+ workflowsDir: workflowsDir ?? ""
517
+ });
518
+ }
519
+ return matches;
520
+ }
521
+ async function resolveWorkflowManifest(workflowsDir, authoredWorkflowId) {
522
+ const match = (await testToolDependencies.readManifestsFromOutDir(workflowsDir, authoredWorkflowId))[0];
523
+ if (match) return match.manifest;
524
+ throwReportedCliExit(`Workflow "${authoredWorkflowId}" for tool was not found locally.`);
525
+ }
526
+ async function loadOperationTool(artifact, tool) {
527
+ const operation = ((await loadAgentFromBundle(artifact)).tools ?? []).find((candidate) => {
528
+ if (!candidate || typeof candidate !== "object") return false;
529
+ const candidateId = candidate.id;
530
+ return candidateId === tool.authoredOperationId || candidateId === tool.toolName;
531
+ });
532
+ if (!operation || typeof operation.run !== "function") throwReportedCliExit(`Operation tool "${tool.toolName}" was not found in the built agent bundle.`);
533
+ return operation;
534
+ }
535
+ async function loadAgentFromBundle(artifact) {
536
+ const tempDir = await mkdtemp(path.join(tmpdir(), "keystroke-agent-tool-test-"));
537
+ const modulePath = path.join(tempDir, "agent-bundle.mjs");
538
+ try {
539
+ await writeFile(modulePath, artifact.bundle.code, "utf-8");
540
+ const mod = await import(`${pathToFileURL(modulePath).href}?t=${Date.now()}`);
541
+ return mod[artifact.agent.localExportName] ?? mod.default;
542
+ } finally {
543
+ await rm(tempDir, {
544
+ recursive: true,
545
+ force: true
546
+ });
547
+ }
548
+ }
549
+ function manifestMatchesRef(manifest, agentRef) {
550
+ return [
551
+ manifest.authoredAgentId,
552
+ manifest.agentId,
553
+ manifest.agentName,
554
+ manifest.displayName,
555
+ manifest.exportName
556
+ ].some((value) => value === agentRef);
557
+ }
558
+ function redactToolOutput(output, manifest) {
559
+ return redactUnknown(output, collectKnownCredentialValues(manifest));
560
+ }
561
+ function collectKnownCredentialValues(manifest) {
562
+ const env = getProcessEnv();
563
+ const values = /* @__PURE__ */ new Set();
564
+ for (const credentialSet of manifest.credentialSets ?? []) for (const key of credentialSet.credentialKeys) for (const envName of [key, `KEYSTROKE_${key}`]) {
565
+ const value = env[envName];
566
+ if (typeof value === "string" && value.length >= 3) values.add(value);
567
+ }
568
+ return values;
569
+ }
570
+ function redactUnknown(value, knownSecretValues) {
571
+ if (typeof value === "string") return redactString(value, knownSecretValues);
572
+ if (Array.isArray(value)) {
573
+ let changed = false;
574
+ return {
575
+ value: value.map((entry) => {
576
+ const result = redactUnknown(entry, knownSecretValues);
577
+ changed ||= result.changed;
578
+ return result.value;
579
+ }),
580
+ changed
581
+ };
582
+ }
583
+ if (isPlainRecord(value)) {
584
+ let changed = false;
585
+ const redacted = {};
586
+ for (const [key, entry] of Object.entries(value)) {
587
+ if (SECRET_FIELD_PATTERN.test(key) && typeof entry === "string") {
588
+ redacted[key] = "[REDACTED]";
589
+ changed = true;
590
+ continue;
591
+ }
592
+ const result = redactUnknown(entry, knownSecretValues);
593
+ redacted[key] = result.value;
594
+ changed ||= result.changed;
595
+ }
596
+ return {
597
+ value: redacted,
598
+ changed
599
+ };
600
+ }
601
+ return {
602
+ value,
603
+ changed: false
604
+ };
605
+ }
606
+ function isPlainRecord(value) {
607
+ if (!value || typeof value !== "object") return false;
608
+ const prototype = Object.getPrototypeOf(value);
609
+ return prototype === Object.prototype || prototype === null;
610
+ }
611
+ function redactString(value, knownSecretValues) {
612
+ let redacted = value;
613
+ for (const secret of knownSecretValues) redacted = redacted.split(secret).join("[REDACTED]");
614
+ return {
615
+ value: redacted,
616
+ changed: redacted !== value
617
+ };
618
+ }
619
+ //#endregion
620
+ //#region src/commands/test/test.command.ts
621
+ const TestOptionsSchema = JsonOptionSchema;
622
+ const TestToolOptionsSchema = JsonOptionSchema.extend({
623
+ toolName: z.string().min(1),
624
+ agent: z.string().min(1).optional(),
625
+ input: z.string().optional(),
626
+ inputFile: z.string().optional(),
627
+ path: z.string().optional(),
628
+ timeout: z.coerce.number().int().min(1).default(120),
629
+ verbose: z.boolean().default(false)
630
+ });
631
+ const TEST_OPTIONS_CONFIG = { ...JSON_OPTION_CONFIG };
632
+ const TEST_TOOL_OPTIONS_CONFIG = {
633
+ ...JSON_OPTION_CONFIG,
634
+ agent: {
635
+ flag: "--agent <agentId>",
636
+ description: "Authored agent id, agent id, or agent name containing the tool"
637
+ },
638
+ input: {
639
+ flag: "--input <json>",
640
+ description: "Tool input as inline JSON string"
641
+ },
642
+ inputFile: {
643
+ flag: "--input-file <path>",
644
+ description: "Path to a JSON file containing tool input"
645
+ },
646
+ path: {
647
+ flag: "--path <dir>",
648
+ description: "Path to project root (directory containing keystroke.config.ts); auto-discovered from CWD if omitted"
649
+ },
650
+ timeout: {
651
+ flag: "--timeout <seconds>",
652
+ description: "Max seconds to wait for workflow completion (default: 120)"
653
+ },
654
+ verbose: {
655
+ flag: "--verbose",
656
+ description: "Show detailed execution logs while polling workflow tools"
657
+ }
658
+ };
659
+ function handleTestHelp() {
660
+ ui.hint("Use `keystroke test tool <toolName> --input='{...}'` or `keystroke test workflow <workflow>`.");
661
+ }
662
+ function createTestCommand() {
663
+ const cmd = createTypedCommand({
664
+ name: "test",
665
+ description: "Test workflows and agent-callable tools",
666
+ schema: TestOptionsSchema,
667
+ optionsConfig: TEST_OPTIONS_CONFIG,
668
+ handler: handleTestHelp,
669
+ subcommands: [createTypedCommand({
670
+ name: "tool",
671
+ description: "Test an agent-callable tool from a built agent manifest",
672
+ schema: TestToolOptionsSchema,
673
+ optionsConfig: TEST_TOOL_OPTIONS_CONFIG,
674
+ argument: {
675
+ name: "toolName",
676
+ description: "Agent tool name from the built manifest",
677
+ key: "toolName"
678
+ },
679
+ handler: handleTestTool
680
+ }), createTypedCommand({
681
+ name: "workflow",
682
+ description: "Build, upload, and run a workflow on the server",
683
+ schema: WorkflowsRunOptionsSchema,
684
+ optionsConfig: RUN_OPTIONS_CONFIG,
685
+ argument: {
686
+ name: "workflow",
687
+ description: "Authored workflow id (preferred) or workflow name. Multiple workflows with the same name run sequentially.",
688
+ key: "workflow"
689
+ },
690
+ handler: handleWorkflowsTryDeploy
691
+ })]
692
+ });
693
+ cmd.enablePositionalOptions();
694
+ cmd.passThroughOptions();
695
+ return cmd;
696
+ }
697
+ //#endregion
698
+ export { createTestCommand };