@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,430 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { D as throwReportedCliExit, h as toErrorMessage, l as AUTH_HINT, m as isNetworkError, t as ui } from "./keystroke.mjs";
4
+ import { i as writeJson } from "./output-DM4b7KgY.mjs";
5
+ import { n as CredentialScopeSchema, t as ConnectionStatusSchema } from "./schema-17qMfNyI.mjs";
6
+ import { t as openBrowser } from "./browser-qwFrUH82.mjs";
7
+ import { t as getIntegrationCatalog } from "./integration-catalog-Bt-L3GjF.mjs";
8
+ import { z } from "zod";
9
+ //#region ../../packages/shared-types/src/connections/api.ts
10
+ /**
11
+ * API request/response types for the integration catalog and connection endpoints.
12
+ *
13
+ * These types are shared between the server route handlers, the Keystroke SDK, and
14
+ * any in-repo consumer (CLI, web). They divide the surface into two clearly-scoped
15
+ * endpoints:
16
+ *
17
+ * GET /api/v1/integrations — the static Keystroke integration catalog (what can
18
+ * be connected). Auth required; not org-scoped; long-cached.
19
+ *
20
+ * GET /api/v1/connections — the caller's configured connections for the current
21
+ * org (what has been connected). Auth + org required; no-cache.
22
+ *
23
+ * Each endpoint validates its query string with the exported `*QuerySchema`, and
24
+ * the SDK types its method signatures against the same schema so that invalid
25
+ * params cannot be sent or accepted.
26
+ */
27
+ const ShopifyShopDomainSchema = z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*\.myshopify\.com$/i, "Shopify store domain must look like \"example.myshopify.com\".");
28
+ const InitiateConnectionRequestSchema = z.object({
29
+ providerAppId: z.string().optional(),
30
+ shopDomain: ShopifyShopDomainSchema.optional(),
31
+ requestedScopes: z.array(z.string()).optional()
32
+ });
33
+ const InitiateConnectionResponseSchema = z.object({
34
+ authUrl: z.string(),
35
+ initiatedAt: z.number()
36
+ });
37
+ const ConnectionFailureReasonSchema = z.enum([
38
+ "missing_state",
39
+ "invalid_state",
40
+ "provider_denied",
41
+ "unknown_integration",
42
+ "provider_app_not_configured",
43
+ "token_exchange_failed",
44
+ "installation_metadata_invalid",
45
+ "validation_failed",
46
+ "persist_failed",
47
+ "unknown_error"
48
+ ]);
49
+ const ConnectionStatusNotStartedSchema = z.object({ status: z.literal("not_started") });
50
+ const ConnectionStatusPendingSchema = z.object({ status: z.literal("pending") });
51
+ const ConnectionStatusConnectedSchema = z.object({ status: z.literal("connected") });
52
+ const ConnectionStatusFailedSchema = z.object({
53
+ status: z.literal("failed"),
54
+ reason: ConnectionFailureReasonSchema,
55
+ /**
56
+ * Optional human-readable detail. Truncated by the server to
57
+ * {@link CONNECTION_FAILURE_DETAIL_MAX_CHARS} characters before being
58
+ * persisted in `credential_sets.last_callback_error_detail` and
59
+ * before being echoed in the callback redirect query.
60
+ */
61
+ message: z.string().max(300).optional(),
62
+ /** ISO-8601 timestamp of when the failure was recorded. */
63
+ failedAt: z.string()
64
+ });
65
+ const ConnectionStatusResponseSchema = z.discriminatedUnion("status", [
66
+ ConnectionStatusNotStartedSchema,
67
+ ConnectionStatusPendingSchema,
68
+ ConnectionStatusConnectedSchema,
69
+ ConnectionStatusFailedSchema
70
+ ]);
71
+ const ConnectionKindSchema = z.enum([
72
+ "oauth",
73
+ "manual",
74
+ "credentials-exchange"
75
+ ]);
76
+ const InternalCredentialSetEntrySchema = z.object({
77
+ resolvedCredentialSetId: z.string(),
78
+ publicId: z.string(),
79
+ displayName: z.string(),
80
+ /**
81
+ * Internal credential-set role. Currently the only first-class role is
82
+ * `'provider-app-definition'`; other values are accepted for forward-compat
83
+ * so newer servers can introduce roles without breaking older SDKs.
84
+ */
85
+ role: z.string()
86
+ });
87
+ const IntegrationCatalogEntryBaseSchema = z.object({
88
+ publicId: z.string(),
89
+ /**
90
+ * Public-facing aliases that resolve to this canonical `publicId`
91
+ * (e.g., `"github"` aliases `"github-account"`). Includes the canonical
92
+ * ID itself — callers can key a lookup table by every entry in this
93
+ * list and get the same catalog record back.
94
+ */
95
+ aliases: z.array(z.string()),
96
+ name: z.string(),
97
+ description: z.string().optional(),
98
+ /**
99
+ * Resolved credential-set metadata for the integration's primary connection.
100
+ * `storedKeys` drives upload flows (what the vault stores), while
101
+ * `authKeys` describes the post-resolve runtime shape.
102
+ */
103
+ credentialSet: z.object({
104
+ resolvedCredentialSetId: z.string(),
105
+ authKeys: z.array(z.string()),
106
+ storedKeys: z.array(z.string()),
107
+ optionalStoredKeys: z.array(z.string()).optional(),
108
+ schemaFingerprint: z.string().optional()
109
+ }),
110
+ /**
111
+ * Internal credential sets associated with the integration (e.g., provider
112
+ * app definitions). Populated only when the caller requests
113
+ * `includeInternal=true`; otherwise empty.
114
+ */
115
+ internalCredentialSets: z.array(InternalCredentialSetEntrySchema)
116
+ });
117
+ const OAuthIntegrationCatalogEntrySchema = IntegrationCatalogEntryBaseSchema.extend({
118
+ connectionKind: z.literal("oauth"),
119
+ tokenType: z.enum(["long-lived", "refreshable"]),
120
+ scopes: z.array(z.string())
121
+ });
122
+ const ManualIntegrationCatalogEntrySchema = IntegrationCatalogEntryBaseSchema.extend({ connectionKind: z.literal("manual") });
123
+ /**
124
+ * Catalog entry for `credentials-exchange`-kind integrations.
125
+ *
126
+ * Carries the JSON-schema projection of the connection's `input` schema so
127
+ * the CLI and web UI can render an input form without re-loading the
128
+ * original Zod schema (which only exists in the integration bundle's
129
+ * runtime module). `credentialSet.storedKeys` reflects the stored-schema
130
+ * keys that get persisted after exchange.
131
+ *
132
+ * @see packages/workflow-core/src/credential-set/connection.ts CredentialsExchangeConnectionConfig
133
+ */
134
+ const CredentialsExchangeIntegrationCatalogEntrySchema = IntegrationCatalogEntryBaseSchema.extend({
135
+ connectionKind: z.literal("credentials-exchange"),
136
+ /** JSON-schema projection of the connection's `input` schema. */
137
+ input: z.record(z.string(), z.unknown()),
138
+ /** Optional free-form copy rendered above the input form. */
139
+ instructions: z.string().optional()
140
+ });
141
+ const IntegrationCatalogEntrySchema = z.discriminatedUnion("connectionKind", [
142
+ OAuthIntegrationCatalogEntrySchema,
143
+ ManualIntegrationCatalogEntrySchema,
144
+ CredentialsExchangeIntegrationCatalogEntrySchema
145
+ ]);
146
+ z.object({
147
+ /** Restrict results to one connection kind. */
148
+ connectionKind: ConnectionKindSchema.optional(),
149
+ /**
150
+ * Return only the integration whose `publicId` (or alias) matches.
151
+ * When no integration matches, the endpoint returns an empty array.
152
+ */
153
+ publicId: z.string().min(1).optional(),
154
+ /**
155
+ * Case-insensitive substring match against `publicId`, `aliases`, and `name`.
156
+ */
157
+ q: z.string().min(1).optional(),
158
+ /**
159
+ * Opt into `internalCredentialSets[]` on each entry. Defaults to `false`
160
+ * so the common catalog call stays lean. CLI commands that need to render
161
+ * credential-set display names set this to `true`.
162
+ */
163
+ includeInternal: z.stringbool().optional().default(false)
164
+ }).strict();
165
+ z.object({ integrations: z.array(IntegrationCatalogEntrySchema) });
166
+ const ConnectionEntrySchema = z.object({
167
+ id: z.string(),
168
+ /**
169
+ * Public-facing integration ID this connection is associated with
170
+ * (post alias resolution). `null` when the underlying credential set is
171
+ * not backed by an official Keystroke integration (e.g., user-defined
172
+ * custom credential sets).
173
+ */
174
+ integrationPublicId: z.string().nullable(),
175
+ /**
176
+ * Resolved credential-set ID, e.g., `"keystroke:slack"` or a custom set's
177
+ * identifier. Stable across alias resolution.
178
+ */
179
+ credentialSetId: z.string(),
180
+ name: z.string(),
181
+ scope: CredentialScopeSchema,
182
+ platformConnected: z.boolean(),
183
+ connectionStatus: ConnectionStatusSchema,
184
+ expiresAt: z.string().nullable(),
185
+ isDefault: z.boolean(),
186
+ createdAt: z.string()
187
+ });
188
+ /**
189
+ * Coerces a `status` query-param value that may arrive as a single string,
190
+ * a comma-separated string, or (in Hono/ky's query handling) an array of
191
+ * strings, into a canonical `ConnectionStatus[]`. Invalid entries cause the
192
+ * surrounding Zod parse to fail with a helpful issue.
193
+ *
194
+ * The field itself is made optional at the object level (see
195
+ * `ListConnectionsQuerySchema`), so passing `undefined` / omitting the key
196
+ * is always valid; this preprocessor only runs when a value is actually
197
+ * present.
198
+ */
199
+ const ConnectionStatusQueryParam = z.preprocess((value) => {
200
+ if (value === void 0 || value === null || value === "") return void 0;
201
+ const trimmed = (Array.isArray(value) ? value : String(value).split(",")).flatMap((entry) => String(entry).split(",")).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
202
+ return trimmed.length === 0 ? void 0 : trimmed;
203
+ }, z.array(ConnectionStatusSchema));
204
+ z.object({
205
+ /** Filter to connections for a specific integration's public ID (alias-resolved). */
206
+ integrationPublicId: z.string().min(1).optional(),
207
+ /**
208
+ * Filter by one or more connection statuses. Accepts a single value, a
209
+ * comma-separated string, or repeated query keys. Omit to return all statuses.
210
+ */
211
+ status: ConnectionStatusQueryParam.optional(),
212
+ scope: CredentialScopeSchema.optional(),
213
+ projectId: z.uuid().optional()
214
+ }).strict();
215
+ z.object({ connections: z.array(ConnectionEntrySchema) });
216
+ //#endregion
217
+ //#region src/commands/connect/connect.handler.ts
218
+ function formatIntegrationLabel(catalog, integrationId) {
219
+ return catalog.lookupByPublicId(integrationId)?.name ?? integrationId;
220
+ }
221
+ function normalizeShopifyShopDomain(shop) {
222
+ const trimmed = shop?.trim().toLowerCase();
223
+ if (!trimmed) return;
224
+ const normalized = trimmed.endsWith(".myshopify.com") ? trimmed : `${trimmed}.myshopify.com`;
225
+ return ShopifyShopDomainSchema.parse(normalized);
226
+ }
227
+ /**
228
+ * Per-reason CLI hint table for `failed` status responses.
229
+ *
230
+ * The hints are operator-facing copy: short, actionable, free of jargon.
231
+ * Anywhere `{label}` appears, the caller substitutes the human-readable
232
+ * integration label (e.g. "Slack"). Values come from
233
+ * `ConnectionFailureReasonValues` in shared-types — keep the two in
234
+ * lock-step (the CLI test asserts every reason has a hint).
235
+ */
236
+ const FAILURE_REASON_HINTS = {
237
+ missing_state: "The OAuth callback was missing its state token. This is almost always transient — try `keystroke connect {label}` again.",
238
+ invalid_state: "The OAuth state token was invalid or expired (10-minute TTL). Run `keystroke connect {label}` again to start a fresh flow.",
239
+ provider_denied: "The provider rejected the authorization request. Approve access at the provider, then run `keystroke connect {label}` again.",
240
+ unknown_integration: "Keystroke does not recognize this integration. Run `keystroke integrations list` to see what is available.",
241
+ provider_app_not_configured: "The Keystroke {label} OAuth app is not configured for this environment. Ask a Keystroke developer to seed the platform credentials and try again.",
242
+ token_exchange_failed: "The provider rejected the token exchange. Re-running `keystroke connect {label}` usually fixes a transient failure; otherwise verify the platform OAuth client credentials.",
243
+ installation_metadata_invalid: "The provider returned an unexpected response shape on success. Re-run `keystroke connect {label}`; if the failure repeats, file a bug with the structured server log.",
244
+ validation_failed: "The {label} credentials passed token exchange but failed Keystroke's post-grant validation (e.g. missing required scope). Approve the listed scopes when re-running `keystroke connect {label}`.",
245
+ persist_failed: "Keystroke failed to persist the {label} connection (database error). Re-run `keystroke connect {label}`; if it keeps failing, check the server logs for the structured error code.",
246
+ unknown_error: "Keystroke encountered an unexpected error finishing the {label} connection. Check the server logs and re-run `keystroke connect {label}`."
247
+ };
248
+ function formatFailureHint(reason, label) {
249
+ return FAILURE_REASON_HINTS[reason].replaceAll("{label}", label);
250
+ }
251
+ function exitWithError(ctx, message, opts) {
252
+ if (ctx.jsonMode) {
253
+ process.stdout.write(`${JSON.stringify({
254
+ error: message,
255
+ ...opts?.code ? { code: opts.code } : {},
256
+ ...opts?.hint ? { hint: opts.hint } : {}
257
+ }, null, 2)}\n`);
258
+ throwReportedCliExit(message);
259
+ }
260
+ ui.error(message);
261
+ if (opts?.hint) ui.hint(opts.hint);
262
+ throwReportedCliExit(message);
263
+ }
264
+ /**
265
+ * Handle the `keystroke connect <integrationId>` command.
266
+ *
267
+ * Initiates a platform OAuth flow:
268
+ * 1. Calls POST /api/v1/connections/:integrationId/initiate to get an auth URL + state token
269
+ * 2. Opens the browser to the auth URL
270
+ * 3. Polls GET /api/v1/connections/:integrationId/status?state=XXX until connected or timeout
271
+ */
272
+ async function handleConnect(options, ctx) {
273
+ const integrationId = options.integrationId?.trim().toLowerCase();
274
+ const { baseUrl, apiKey } = ctx;
275
+ if (!baseUrl || !apiKey) exitWithError(ctx, "Not authenticated.", {
276
+ code: "AUTH_ERROR",
277
+ hint: AUTH_HINT
278
+ });
279
+ if (!integrationId) exitWithError(ctx, "Usage: keystroke connect <integrationId>", {
280
+ code: "USAGE_ERROR",
281
+ hint: "Example: keystroke connect github"
282
+ });
283
+ const catalog = await getIntegrationCatalog(ctx);
284
+ const integrationLabel = catalog.oauthPublicIds.includes(integrationId) ? formatIntegrationLabel(catalog, integrationId) : integrationId;
285
+ const shopDomain = integrationId === "shopify" ? normalizeShopifyShopDomain(options.shop) : void 0;
286
+ if (integrationId === "shopify" && !shopDomain) exitWithError(ctx, "Shopify connections require a store domain.", {
287
+ code: "USAGE_ERROR",
288
+ hint: "Example: keystroke connect shopify --shop example.myshopify.com"
289
+ });
290
+ if (!ctx.jsonMode) {
291
+ ui.text(`Connect ${integrationLabel}`);
292
+ ui.br();
293
+ }
294
+ let authUrl;
295
+ let initiatedAt;
296
+ try {
297
+ const requestBody = InitiateConnectionRequestSchema.parse({ ...shopDomain ? { shopDomain } : {} });
298
+ const hasRequestBody = Object.keys(requestBody).length > 0;
299
+ const response = await fetch(`${baseUrl}/api/v1/connections/${integrationId}/initiate`, {
300
+ method: "POST",
301
+ headers: {
302
+ Authorization: `Bearer ${apiKey}`,
303
+ ...hasRequestBody ? { "Content-Type": "application/json" } : {}
304
+ },
305
+ ...hasRequestBody ? { body: JSON.stringify(requestBody) } : {}
306
+ });
307
+ if (!response.ok) {
308
+ if (response.status === 404) exitWithError(ctx, `Integration "${integrationId}" is not supported for platform OAuth.`, {
309
+ code: "UNSUPPORTED_INTEGRATION",
310
+ hint: `Currently supported official OAuth integrations: ${catalog.oauthPublicIds.join(", ")}. Workspace-authored OAuth connect flows are not available yet.`
311
+ });
312
+ if (response.status === 503) exitWithError(ctx, `Platform credentials not configured for "${integrationId}".`, {
313
+ code: "PLATFORM_NOT_CONFIGURED",
314
+ hint: `Ask a Keystroke developer to configure the ${integrationLabel} account OAuth client for this environment.`
315
+ });
316
+ if (response.status === 401) exitWithError(ctx, "Not authenticated.", {
317
+ code: "AUTH_ERROR",
318
+ hint: AUTH_HINT
319
+ });
320
+ if (response.status === 403) exitWithError(ctx, `You do not have permission to connect ${integrationLabel} in the current organization.`, {
321
+ code: "FORBIDDEN",
322
+ hint: "Verify your active organization and your permissions in that organization."
323
+ });
324
+ if (response.status >= 500) exitWithError(ctx, `The Keystroke server failed while starting the ${integrationLabel} connection.`, {
325
+ code: "SERVER_ERROR",
326
+ hint: `Server returned HTTP ${response.status}. Check the server logs and try again.`
327
+ });
328
+ exitWithError(ctx, `Failed to initiate the ${integrationLabel} connection.`, {
329
+ code: "INITIATE_FAILED",
330
+ hint: `Server returned HTTP ${response.status}.`
331
+ });
332
+ }
333
+ const data = InitiateConnectionResponseSchema.parse(await response.json());
334
+ authUrl = data.authUrl;
335
+ initiatedAt = data.initiatedAt;
336
+ } catch (error) {
337
+ if (error instanceof Error && error.name === "CliExitError") throw error;
338
+ if (isNetworkError(error)) exitWithError(ctx, `Could not reach the Keystroke server to start the ${integrationLabel} connection.`, {
339
+ code: "NETWORK_ERROR",
340
+ hint: `Check that your local services are running and that the CLI is pointed at ${baseUrl}.`
341
+ });
342
+ exitWithError(ctx, `Failed to initiate the ${integrationLabel} connection.`, {
343
+ code: "INITIATE_FAILED",
344
+ hint: toErrorMessage(error)
345
+ });
346
+ }
347
+ if (ctx.jsonMode) {
348
+ writeJson({
349
+ status: "authorization_required",
350
+ integrationId,
351
+ authUrl,
352
+ initiatedAt,
353
+ timeoutSeconds: options.timeout
354
+ });
355
+ return;
356
+ }
357
+ if (!options.noOpen) ui.text("Opening your browser for authorization...");
358
+ ui.br();
359
+ ui.text(options.noOpen ? "Open this URL in your browser:" : "If nothing opens, visit this URL:");
360
+ ui.text(` ${authUrl}`);
361
+ if (!options.noOpen) try {
362
+ await openBrowser(authUrl);
363
+ } catch (error) {
364
+ ui.warn(`Could not open the browser automatically: ${error instanceof Error ? error.message : "unknown error"}`);
365
+ ui.text("Open this URL manually to continue:");
366
+ ui.text(` ${authUrl}`);
367
+ }
368
+ ui.br();
369
+ ui.text(`Waiting for approval in ${integrationLabel}...`);
370
+ ui.text("Return to this terminal after you approve access.");
371
+ ui.text("Press Ctrl+C to cancel.");
372
+ const timeoutMs = options.timeout * 1e3;
373
+ const pollIntervalMs = 2e3;
374
+ const startTime = Date.now();
375
+ let consecutivePollFailures = 0;
376
+ let pollingWarningShown = false;
377
+ while (Date.now() - startTime < timeoutMs) {
378
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
379
+ try {
380
+ const response = await fetch(`${baseUrl}/api/v1/connections/${integrationId}/status?since=${initiatedAt}`, { headers: { Authorization: `Bearer ${apiKey}` } });
381
+ if (!response.ok) {
382
+ consecutivePollFailures += 1;
383
+ if (response.status === 401) exitWithError(ctx, "Your CLI session is no longer authenticated.", {
384
+ code: "AUTH_ERROR",
385
+ hint: AUTH_HINT
386
+ });
387
+ if (response.status === 403) exitWithError(ctx, `You do not have permission to finish the ${integrationLabel} connection in the current organization.`, {
388
+ code: "FORBIDDEN",
389
+ hint: "Verify your active organization and your permissions in that organization."
390
+ });
391
+ if (!pollingWarningShown && consecutivePollFailures >= 3) {
392
+ pollingWarningShown = true;
393
+ ui.warn(`Still waiting for ${integrationLabel} authorization.`);
394
+ ui.hint(`Polling has hit temporary errors (latest HTTP ${response.status}). If you already approved access, wait a moment and try again.`);
395
+ }
396
+ continue;
397
+ }
398
+ consecutivePollFailures = 0;
399
+ const data = ConnectionStatusResponseSchema.parse(await response.json());
400
+ if (data.status === "connected") {
401
+ ui.br();
402
+ ui.success(`Connected ${integrationLabel} successfully.`);
403
+ ui.hint(`Your ${integrationLabel} account is now available to Keystroke workflows.`);
404
+ return;
405
+ }
406
+ if (data.status === "failed") {
407
+ ui.br();
408
+ const headline = data.message ? `Could not finish the ${integrationLabel} connection: ${data.message}` : `Could not finish the ${integrationLabel} connection (${data.reason}).`;
409
+ ui.error(headline);
410
+ ui.hint(formatFailureHint(data.reason, integrationLabel));
411
+ throwReportedCliExit(headline);
412
+ }
413
+ } catch (error) {
414
+ if (error instanceof Error && error.name === "CliExitError") throw error;
415
+ consecutivePollFailures += 1;
416
+ if (!pollingWarningShown && consecutivePollFailures >= 3) {
417
+ pollingWarningShown = true;
418
+ ui.warn(`Still waiting for ${integrationLabel} authorization.`);
419
+ ui.hint(`Polling has hit temporary errors. If you already approved access, wait a moment and try again.`);
420
+ }
421
+ }
422
+ }
423
+ ui.br();
424
+ ui.error(`Timed out waiting for ${integrationLabel} authorization.`);
425
+ ui.text("If authorization is still open in your browser, you can still finish it here:");
426
+ ui.text(` ${authUrl}`);
427
+ throwReportedCliExit(`Timed out waiting for ${integrationLabel} authorization.`);
428
+ }
429
+ //#endregion
430
+ export { handleConnect };
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ //#region ../../packages/shared-types/src/connections/constants.ts
4
+ /** Namespaces reserved for official Keystroke integrations. Builder rejects these in user source files. */
5
+ const RESERVED_NAMESPACES = ["keystroke"];
6
+ RESERVED_NAMESPACES[0];
7
+ //#endregion
8
+ export { RESERVED_NAMESPACES as t };
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { n as __exportAll } from "./chunk-CH6r78ws.mjs";
4
+ import { C as CliExitError, S as AuthenticationError, h as toErrorMessage, i as logger, l as AUTH_HINT, p as isAuthError, t as ui, u as REAUTH_HINT } from "./keystroke.mjs";
5
+ import { l as resolveAuthOptions } from "./dist-CUK7yBM0.mjs";
6
+ import { a as writeJsonError } from "./output-DM4b7KgY.mjs";
7
+ import { t as getEnv } from "./env-91KwMKov.mjs";
8
+ //#region src/lib/context.ts
9
+ var context_exports = /* @__PURE__ */ __exportAll({
10
+ assertProjectConfigMatchesAuthenticatedOrg: () => assertProjectConfigMatchesAuthenticatedOrg,
11
+ requireAuthOptions: () => requireAuthOptions,
12
+ requireClient: () => requireClient,
13
+ resolveAuthContext: () => resolveAuthContext,
14
+ resolveBaseContext: () => resolveBaseContext,
15
+ validateApiKey: () => validateApiKey
16
+ });
17
+ function exitNotAuthenticated(jsonMode) {
18
+ if (jsonMode) writeJsonError(`Not authenticated. ${AUTH_HINT}`, {
19
+ code: "AUTH_ERROR",
20
+ hint: AUTH_HINT
21
+ });
22
+ ui.error(`Not authenticated. ${AUTH_HINT}`);
23
+ throw new AuthenticationError(`Not authenticated. ${AUTH_HINT}`, { reported: true });
24
+ }
25
+ /**
26
+ * Resolves the base CLI context for every command.
27
+ *
28
+ * Org resolution priority (first non-empty value wins):
29
+ * 1. --org flag (ID)
30
+ * 2. KEYSTROKE_ORG_ID env
31
+ * 3. Active org from stored credentials
32
+ *
33
+ * API key: resolved from the matching org entry in stored credentials (or explicit --apiKey flag).
34
+ * Base URL: flags > stored credentials > env.
35
+ */
36
+ async function resolveBaseContext(overrides = {}) {
37
+ const authContext = await resolveAuthContext(overrides);
38
+ let client = null;
39
+ if (authContext.apiKey && authContext.baseUrl) try {
40
+ const { createClient } = await import("./src-eHwu-Gfw.mjs");
41
+ client = createClient({
42
+ apiKey: authContext.apiKey,
43
+ baseUrl: authContext.baseUrl,
44
+ organizationId: authContext.organizationId,
45
+ onRequest: (info) => logger.debug(`-> ${info.method} ${info.url}`, {
46
+ headers: info.headers,
47
+ body: info.body
48
+ }),
49
+ onResponse: (info) => logger.debug(`<- ${info.status} ${info.method} ${info.url} (${info.durationMs}ms)`, {
50
+ headers: info.headers,
51
+ body: info.body
52
+ })
53
+ });
54
+ } catch {}
55
+ return {
56
+ ...authContext,
57
+ client
58
+ };
59
+ }
60
+ async function resolveAuthContext(overrides = {}) {
61
+ const auth = await resolveAuthOptions({
62
+ apiKey: overrides.apiKey,
63
+ baseUrl: overrides.baseUrl
64
+ });
65
+ const env = getEnv();
66
+ let apiKey = auth.apiKey ?? env.KEYSTROKE_API_KEY;
67
+ const baseUrl = auth.baseUrl ?? env.SERVER_URL;
68
+ const orgRaw = overrides.org ?? env.KEYSTROKE_ORG_ID ?? auth.activeOrg?.organizationId;
69
+ const orgSource = overrides.org ? "flag" : env.KEYSTROKE_ORG_ID ? "env" : auth.activeOrg?.organizationId ? "credentials" : void 0;
70
+ let organizationId = orgRaw;
71
+ let matchedStoredOrg = false;
72
+ if (orgRaw && auth.storedCredentials) {
73
+ const matchedOrg = auth.storedCredentials.orgs.find((o) => o.organizationId === orgRaw);
74
+ matchedStoredOrg = matchedOrg !== void 0;
75
+ if (matchedOrg) {
76
+ if (!overrides.apiKey) apiKey = matchedOrg.apiKey;
77
+ organizationId = matchedOrg.organizationId;
78
+ }
79
+ }
80
+ const usingSavedApiKey = !overrides.apiKey && !env.KEYSTROKE_API_KEY && typeof auth.apiKey === "string" && auth.apiKey.length > 0;
81
+ if (typeof orgRaw === "string" && orgRaw.length > 0 && orgSource !== "credentials" && usingSavedApiKey && auth.storedCredentials && !matchedStoredOrg) {
82
+ ui.error(`Organization ID "${orgRaw}" is not available in your saved credentials.`);
83
+ ui.hint("Run `keystroke org switch`, re-authenticate for that organization, or pass --api-key.");
84
+ throw new CliExitError(`Unknown saved organization ID: ${orgRaw}`, { reported: true });
85
+ }
86
+ return {
87
+ apiKey,
88
+ baseUrl,
89
+ jsonMode: false,
90
+ organizationId,
91
+ orgSource,
92
+ storedCredentials: auth.storedCredentials,
93
+ credentialsPath: auth.credentialsPath
94
+ };
95
+ }
96
+ /**
97
+ * Requires that the CLI context has a valid authenticated client.
98
+ * Exits the process with a helpful message if credentials are missing.
99
+ * Outputs a JSON error when `ctx.jsonMode` is true.
100
+ */
101
+ function requireClient(ctx) {
102
+ if (!ctx.client) exitNotAuthenticated(ctx.jsonMode);
103
+ return ctx.client;
104
+ }
105
+ function requireAuthOptions(ctx) {
106
+ if (!ctx.apiKey || !ctx.baseUrl) exitNotAuthenticated(ctx.jsonMode);
107
+ return {
108
+ apiKey: ctx.apiKey,
109
+ baseUrl: ctx.baseUrl,
110
+ ...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
111
+ };
112
+ }
113
+ /**
114
+ * Validates the API key by making a test request to the server.
115
+ * Exits the process with a helpful message if the key is invalid, expired, or unreachable.
116
+ */
117
+ async function validateApiKey(client) {
118
+ try {
119
+ return await client.public.auth.validate();
120
+ } catch (error) {
121
+ const message = toErrorMessage(error);
122
+ if (isAuthError(error)) ui.error(`Invalid or expired API key. ${REAUTH_HINT}`);
123
+ else ui.error(`Failed to verify API key: ${message}`);
124
+ throw new AuthenticationError(`API key validation failed: ${message}`, {
125
+ cause: error,
126
+ reported: true
127
+ });
128
+ }
129
+ }
130
+ async function assertProjectConfigMatchesAuthenticatedOrg(client, projectConfig) {
131
+ const auth = await validateApiKey(client);
132
+ if (auth.organizationId === projectConfig.organizationId) return;
133
+ ui.error(`This checkout is configured for organization ID "${projectConfig.organizationId}", but the current credentials are scoped to organization ID "${auth.organizationId}".`);
134
+ ui.hint("Switch organizations or update your local project config before running this command.");
135
+ throw new CliExitError(`Project organization mismatch: config=${projectConfig.organizationId} auth=${auth.organizationId}`, { reported: true });
136
+ }
137
+ //#endregion
138
+ export { validateApiKey as a, requireClient as i, context_exports as n, requireAuthOptions as r, assertProjectConfigMatchesAuthenticatedOrg as t };
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as fs from "node:fs/promises";
4
+ import * as path$1 from "node:path";
5
+ import { z } from "zod";
6
+ //#region src/lib/credential-env-map.ts
7
+ /**
8
+ * Read optional .keystroke/credential-env-map.json from project root.
9
+ * Maps credentialSetId → { credentialKey: envVarName } to override KEYSTROKE_<KEY> convention.
10
+ */
11
+ const CredentialEnvMapSchema = z.record(z.string(), z.record(z.string(), z.string()));
12
+ const CREDENTIAL_ENV_MAP_FILE = ".keystroke/credential-env-map.json";
13
+ /**
14
+ * Reads .keystroke/credential-env-map.json from project root (workflowsDir).
15
+ * Returns null if file does not exist or is invalid.
16
+ */
17
+ async function readCredentialEnvMap(projectRoot) {
18
+ const filePath = path$1.join(projectRoot, CREDENTIAL_ENV_MAP_FILE);
19
+ try {
20
+ const raw = await fs.readFile(filePath, "utf-8");
21
+ const parsed = CredentialEnvMapSchema.safeParse(JSON.parse(raw));
22
+ return parsed.success ? parsed.data : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ //#endregion
28
+ export { readCredentialEnvMap as t };
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { i as writeJson } from "./output-DM4b7KgY.mjs";
4
+ //#region src/lib/credential-schema-mismatch.ts
5
+ /**
6
+ * Structured detection and rendering for `CredentialSchemaMismatchError`
7
+ * surfaces in the CLI.
8
+ *
9
+ * The error is thrown by the executor's credential resolver and the server
10
+ * upload/preflight paths when a vault row's stored schema fingerprint does
11
+ * not match the current manifest's fingerprint. Because the error crosses
12
+ * worker subprocess and sandbox isolates, detection uses a structural
13
+ * `.name` check rather than `instanceof`.
14
+ *
15
+ * This module centralises both the detection and the human-readable /
16
+ * JSON renderings so every CLI command surfaces the error consistently.
17
+ *
18
+ * @see import('@keystroke/workflow-core/errors').CredentialSchemaMismatchError
19
+ */
20
+ /**
21
+ * JSON output code used for structured CLI output. Keep this literal value
22
+ * stable — agents and scripts match on it.
23
+ */
24
+ const CREDENTIAL_SCHEMA_MISMATCH_CODE = "CREDENTIAL_SCHEMA_MISMATCH";
25
+ /**
26
+ * Structural check — `instanceof` is unreliable across V8 contexts (worker
27
+ * subprocess, sandbox isolates) so we detect by `.name` plus the mandatory
28
+ * structural fields.
29
+ */
30
+ function isCredentialSchemaMismatchErrorLike(error) {
31
+ if (!error || typeof error !== "object") return false;
32
+ const candidate = error;
33
+ return candidate.name === "CredentialSchemaMismatchError" && typeof candidate.manifestId === "string" && typeof candidate.storedFingerprint === "string" && typeof candidate.currentFingerprint === "string" && typeof candidate.remediation === "string";
34
+ }
35
+ function serializeUploadedAt(value) {
36
+ if (value instanceof Date) return value.toISOString();
37
+ if (typeof value === "string" && value.length > 0) {
38
+ const parsed = new Date(value);
39
+ if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
40
+ }
41
+ return null;
42
+ }
43
+ function renderCredentialSchemaMismatchText(ui, error) {
44
+ ui.error(`Credential schema mismatch for "${error.manifestId}".`);
45
+ ui.text("");
46
+ ui.text(` The credentials stored for "${error.manifestId}" were uploaded against a`);
47
+ ui.text(" different schema than the workflow currently expects.");
48
+ ui.text("");
49
+ const uploadedAt = serializeUploadedAt(error.uploadedAt) ?? "unknown";
50
+ ui.text(` Uploaded at: ${uploadedAt}`);
51
+ ui.text(` Uploaded shape: ${error.storedFingerprint}`);
52
+ ui.text(` Current shape: ${error.currentFingerprint}`);
53
+ ui.text("");
54
+ ui.text(" To fix, re-upload credentials:");
55
+ ui.text("");
56
+ ui.text(` ${error.remediation}`);
57
+ }
58
+ /**
59
+ * Emit the mismatch as a structured JSON payload on stdout. Leaves process
60
+ * flow to the caller — paired with `throwReportedCliExit` at the call site
61
+ * so the CLI returns a non-zero exit code after the payload is written.
62
+ */
63
+ function writeCredentialSchemaMismatchJson(error) {
64
+ writeJson({
65
+ ok: false,
66
+ code: CREDENTIAL_SCHEMA_MISMATCH_CODE,
67
+ error: error.message ?? `Credential schema mismatch for "${error.manifestId}"`,
68
+ manifestId: error.manifestId,
69
+ storedFingerprint: error.storedFingerprint,
70
+ currentFingerprint: error.currentFingerprint,
71
+ uploadedAt: serializeUploadedAt(error.uploadedAt),
72
+ remediation: error.remediation
73
+ });
74
+ }
75
+ //#endregion
76
+ export { renderCredentialSchemaMismatchText as n, writeCredentialSchemaMismatchJson as r, isCredentialSchemaMismatchErrorLike as t };