@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.
- package/AGENTS-blurb.md +123 -0
- package/LICENSE +42 -0
- package/README.md +177 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/keystroke.mjs +107 -0
- package/dist/_manifest-JSRE3H8k.mjs +385 -0
- package/dist/agent-bundle-package-DWV6B_5q-BtV7Xycc.mjs +2344 -0
- package/dist/agent-manifest-CDnbkR2f.mjs +245 -0
- package/dist/agents-CZJGxVqV.mjs +228 -0
- package/dist/api-keys-D2lgguuY.mjs +40 -0
- package/dist/auth-DN2VusyU.mjs +59 -0
- package/dist/auth.handler-CT1BQUvu.mjs +340 -0
- package/dist/browser-qwFrUH82.mjs +24 -0
- package/dist/build-agents-BmM_AsSd-BGi9wtzt.mjs +514 -0
- package/dist/build-metadata-BWS7uhd_-DR8gJjTX.mjs +1422 -0
- package/dist/build-progress-DgYKb4hB.mjs +183 -0
- package/dist/build-tasks-CdihpudT-D5r5HUHe.mjs +91 -0
- package/dist/build-workflows-CfxBnIWh-CdYPv8w2.mjs +370 -0
- package/dist/build.handler-4799CjWH.mjs +36 -0
- package/dist/chunk-CH6r78ws.mjs +37 -0
- package/dist/clear-cache.handler-B9tqSoSM.mjs +11 -0
- package/dist/clear.handler-BTIXXPTJ.mjs +42 -0
- package/dist/clear.handler-BydlX-zE.mjs +11 -0
- package/dist/commander-DfTVqQ-3.mjs +133 -0
- package/dist/concurrency-gXn9Rw8x-DNl2YtrS.mjs +20 -0
- package/dist/connect-BUXkeH0F.mjs +43 -0
- package/dist/connect.handler-CYel9cy6.mjs +430 -0
- package/dist/constants-CPpPdSNg.mjs +8 -0
- package/dist/context-T7HZuB97.mjs +138 -0
- package/dist/credential-env-map-CI8yWHVy.mjs +28 -0
- package/dist/credential-schema-mismatch-BKo5PjcQ.mjs +76 -0
- package/dist/credentials-CvmjU0lK.mjs +171 -0
- package/dist/credentials-OfVHOtG3.mjs +151216 -0
- package/dist/current-deployment-workflow-poHt27i3.mjs +94 -0
- package/dist/current.handler-B8zKzfPp.mjs +21 -0
- package/dist/delete.handler-bAu1iXVQ.mjs +17 -0
- package/dist/deploy-7Jjls436.mjs +26 -0
- package/dist/deploy-BOPIpRWm.mjs +74 -0
- package/dist/deploy-progress-BmGUNFKg.mjs +70 -0
- package/dist/deploy.handler-BAzgiNhd.mjs +370 -0
- package/dist/detect-env-access-CwkOYeYM-D_BCZqV6.mjs +209 -0
- package/dist/diff-utils-NEfcjqxt.mjs +185 -0
- package/dist/diff.handler-Du7SY8K4.mjs +47 -0
- package/dist/dist-BkJUoBiG.mjs +1116 -0
- package/dist/dist-CUK7yBM0.mjs +308 -0
- package/dist/env-91KwMKov.mjs +140 -0
- package/dist/env.handler-BAzBuMzQ.mjs +277 -0
- package/dist/error-boundary-VL-JLfIa.mjs +34 -0
- package/dist/file-metadata-D1vm-XY2.mjs +191 -0
- package/dist/get-intrinsic-zLxwtrLK.mjs +658 -0
- package/dist/import-module-CV84H5fZ-B_CBCmb4.mjs +1747 -0
- package/dist/init-DpMCotSK.mjs +45 -0
- package/dist/init.handler-CPRnif52.mjs +585 -0
- package/dist/inspect.handler-DT_cD036.mjs +146 -0
- package/dist/integration-catalog-Bt-L3GjF.mjs +104 -0
- package/dist/integrations-DlatPK4W.mjs +79 -0
- package/dist/keystroke.d.mts +3 -0
- package/dist/keystroke.mjs +707 -0
- package/dist/layout-CbMtQ2tm.mjs +67 -0
- package/dist/list-enrichment-y-cwizLr.mjs +189 -0
- package/dist/list.handler-BTWvCyjA.mjs +52 -0
- package/dist/list.handler-CWF_Dj15.mjs +24 -0
- package/dist/list.handler-CZ6G2x_G.mjs +75 -0
- package/dist/list.handler-DWaQkJaR.mjs +51 -0
- package/dist/list.handler-DqbFcBW7.mjs +180 -0
- package/dist/list.handler-lq3ZGAn4.mjs +104 -0
- package/dist/logs-BEg9L5l8.mjs +28 -0
- package/dist/logs.handler-6hoMBzqw.mjs +35 -0
- package/dist/logs.handler-BD_dXiL1.mjs +231 -0
- package/dist/metadata-layout-GUYIUo0i-_aG2zjue.mjs +5877 -0
- package/dist/normalize-path-CojS-CgQ-DLCOvnD1.mjs +20 -0
- package/dist/options-CeaTcFxP.mjs +43 -0
- package/dist/org-xLzBtt2_.mjs +41 -0
- package/dist/output-DM4b7KgY.mjs +72 -0
- package/dist/oxc-B3KI3rf_-n9d1hKNq.mjs +119 -0
- package/dist/paused.handler-BMFm9Cff.mjs +94 -0
- package/dist/project-config-D1qsQlO7.mjs +107 -0
- package/dist/projects-CHkRE9rS.mjs +1574 -0
- package/dist/projects-Cjb7sovS.mjs +30 -0
- package/dist/read-credential-keys-77a91T8M-KA0Iw0Z1.mjs +9 -0
- package/dist/register.handler-BPCdor1_.mjs +86 -0
- package/dist/requirements.handler-DPXdSks3.mjs +201 -0
- package/dist/resolve-project-DDJ29sCF.mjs +35 -0
- package/dist/rolldown-runtime-twds-ZHy-BWWzu8VG.mjs +15 -0
- package/dist/run-polling-CAgFRdK3.mjs +20 -0
- package/dist/runs-D9hNLb9A.mjs +259 -0
- package/dist/schedule-BXx3uXwr.mjs +1142 -0
- package/dist/schema-17qMfNyI.mjs +18 -0
- package/dist/schema-display-CgmeKigW.mjs +130 -0
- package/dist/schemas-CDib1RhE.mjs +125 -0
- package/dist/skills-sync.handler-DIy8GR16.mjs +34 -0
- package/dist/skills.command-CrjI2dN9.mjs +35 -0
- package/dist/skills.handler-Bz8bJKql.mjs +9 -0
- package/dist/source-analysis-Cj-ADyu--BJQcFPCG.mjs +144 -0
- package/dist/spinner-progress-DMVwgqO9.mjs +173 -0
- package/dist/src-C0X6u_Mw.mjs +1340 -0
- package/dist/src-eHwu-Gfw.mjs +369 -0
- package/dist/status.handler-BO4nwvWn.mjs +101 -0
- package/dist/switch.handler-D_9213Vf.mjs +51 -0
- package/dist/sync-BL_Mo5st.mjs +39 -0
- package/dist/sync-keystroke-agent-skills-Kx_H7UTd.mjs +70 -0
- package/dist/sync.handler-BUFPdzWz.mjs +82 -0
- package/dist/task-B2sZMaZu.mjs +8 -0
- package/dist/task-target-build-CBeCKbu2.mjs +432 -0
- package/dist/task-target-deploy-C5X-USeR.mjs +4 -0
- package/dist/task-target-deploy-CA6elFpF-BEr4gkol.mjs +271 -0
- package/dist/task-target-deploy-runner.d.mts +3 -0
- package/dist/task-target-deploy-runner.mjs +202 -0
- package/dist/test-BHTgR3UA.mjs +698 -0
- package/dist/test.handler-BcPQ8b74.mjs +13 -0
- package/dist/trigger-artifacts-DQPbQNqC-B4yeeFBY.mjs +239 -0
- package/dist/trigger-manifest-CY7brZeg.mjs +30 -0
- package/dist/try-deploy.handler-DqybNhXx.mjs +490 -0
- package/dist/upload-CkU--iDC.mjs +207 -0
- package/dist/upload.handler-DCtiznQp.mjs +441 -0
- package/dist/utils-CywxCDM7.mjs +14 -0
- package/dist/validate.handler-DOcTaJL0.mjs +280 -0
- package/dist/workflow-build-DBQaBfnn.mjs +1819 -0
- package/dist/workflow-bundler-BPiqVscj-X1PFFAuP.mjs +167 -0
- package/dist/workflows-g9z87AJJ.mjs +799 -0
- package/dist/writer-BG8poUm3-BbXlU2kI.mjs +426 -0
- 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 };
|