@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,308 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as fs$1 from "node:fs/promises";
4
+ import * as os from "node:os";
5
+ import * as path$1 from "node:path";
6
+ import { z } from "zod";
7
+ //#region ../../packages/local-memory/dist/index.mjs
8
+ const KEYSTROKE_CREDENTIALS_DIR = ".keystroke";
9
+ const KEYSTROKE_CREDENTIALS_FILE = "credentials.json";
10
+ const credentialUserSchema = z.object({
11
+ id: z.string().min(1),
12
+ email: z.email(),
13
+ name: z.string().optional()
14
+ });
15
+ const storedCredentialsSchemaV1 = z.object({
16
+ version: z.literal(1),
17
+ apiKey: z.string().min(1),
18
+ apiKeyId: z.uuid().optional(),
19
+ serverUrl: z.url(),
20
+ webUrl: z.url(),
21
+ createdAt: z.string().min(1),
22
+ organizationId: z.uuid().optional(),
23
+ organizationName: z.string().optional(),
24
+ user: credentialUserSchema.optional()
25
+ });
26
+ const orgEntrySchema = z.object({
27
+ organizationId: z.uuid(),
28
+ organizationName: z.string().min(1),
29
+ apiKey: z.string().min(1),
30
+ apiKeyId: z.uuid().optional(),
31
+ createdAt: z.string().min(1)
32
+ });
33
+ const storedCredentialsSchema = z.object({
34
+ version: z.literal(2),
35
+ serverUrl: z.url(),
36
+ webUrl: z.url(),
37
+ user: credentialUserSchema.optional(),
38
+ activeOrgId: z.uuid().optional(),
39
+ orgs: z.array(orgEntrySchema)
40
+ });
41
+ function getCredentialsDirPath(homeDir = os.homedir()) {
42
+ return path$1.join(homeDir, KEYSTROKE_CREDENTIALS_DIR);
43
+ }
44
+ function getCredentialsFilePath(homeDir = os.homedir()) {
45
+ return path$1.join(getCredentialsDirPath(homeDir), KEYSTROKE_CREDENTIALS_FILE);
46
+ }
47
+ function migrateV1ToV2(v1) {
48
+ const orgs = [];
49
+ if (v1.organizationId && v1.apiKey) orgs.push({
50
+ organizationId: v1.organizationId,
51
+ organizationName: v1.organizationName ?? "Unknown",
52
+ apiKey: v1.apiKey,
53
+ apiKeyId: v1.apiKeyId,
54
+ createdAt: v1.createdAt
55
+ });
56
+ return {
57
+ version: 2,
58
+ serverUrl: v1.serverUrl,
59
+ webUrl: v1.webUrl,
60
+ user: v1.user,
61
+ activeOrgId: v1.organizationId,
62
+ orgs
63
+ };
64
+ }
65
+ async function readStoredCredentials(filePath = getCredentialsFilePath()) {
66
+ try {
67
+ const raw = await fs$1.readFile(filePath, "utf-8");
68
+ const parsed = JSON.parse(raw);
69
+ const v2 = storedCredentialsSchema.safeParse(parsed);
70
+ if (v2.success) return v2.data;
71
+ const v1 = storedCredentialsSchemaV1.safeParse(parsed);
72
+ if (v1.success) return migrateV1ToV2(v1.data);
73
+ throw new Error(`Invalid credentials file: ${filePath}`);
74
+ } catch (error) {
75
+ if (error.code === "ENOENT") return null;
76
+ throw error;
77
+ }
78
+ }
79
+ async function deleteStoredCredentials(filePath = getCredentialsFilePath()) {
80
+ try {
81
+ await fs$1.unlink(filePath);
82
+ return true;
83
+ } catch (error) {
84
+ if (error.code === "ENOENT") return false;
85
+ throw error;
86
+ }
87
+ }
88
+ async function writeStoredCredentials(credentials, filePath = getCredentialsFilePath()) {
89
+ const parsed = storedCredentialsSchema.parse(credentials);
90
+ await fs$1.mkdir(path$1.dirname(filePath), { recursive: true });
91
+ await fs$1.writeFile(filePath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 384 });
92
+ try {
93
+ await fs$1.chmod(filePath, 384);
94
+ } catch {}
95
+ }
96
+ /**
97
+ * Adds or replaces an org entry in stored credentials. Sets it as the active org.
98
+ * Creates the credentials file if it doesn't exist (requires shared fields).
99
+ */
100
+ async function upsertOrgCredentials(params, filePath = getCredentialsFilePath()) {
101
+ const existing = await readStoredCredentials(filePath);
102
+ if (existing) {
103
+ const orgs = existing.orgs.filter((o) => o.organizationId !== params.orgEntry.organizationId);
104
+ orgs.push(params.orgEntry);
105
+ await writeStoredCredentials({
106
+ ...existing,
107
+ serverUrl: params.serverUrl,
108
+ webUrl: params.webUrl,
109
+ user: params.user ?? existing.user,
110
+ activeOrgId: params.orgEntry.organizationId,
111
+ orgs
112
+ }, filePath);
113
+ } else await writeStoredCredentials({
114
+ version: 2,
115
+ serverUrl: params.serverUrl,
116
+ webUrl: params.webUrl,
117
+ user: params.user,
118
+ activeOrgId: params.orgEntry.organizationId,
119
+ orgs: [params.orgEntry]
120
+ }, filePath);
121
+ }
122
+ /**
123
+ * Sets the active org. Validates the org exists in the stored orgs array.
124
+ * @throws Error if no stored credentials or org not found
125
+ */
126
+ async function setActiveOrg(orgId, filePath = getCredentialsFilePath()) {
127
+ const existing = await readStoredCredentials(filePath);
128
+ if (!existing) throw new Error("No stored credentials found. Run `keystroke auth` first.");
129
+ if (!existing.orgs.find((o) => o.organizationId === orgId)) throw new Error(`No stored API key for organization ${orgId}. Run \`keystroke auth\` to add credentials for this org.`);
130
+ await writeStoredCredentials({
131
+ ...existing,
132
+ activeOrgId: orgId
133
+ }, filePath);
134
+ }
135
+ /**
136
+ * Removes a single org entry from stored credentials.
137
+ * If it was the active org, clears activeOrgId (falls back to first remaining org).
138
+ * Returns the removed entry (for key revocation), or null if not found.
139
+ */
140
+ async function removeOrgCredentials(orgId, filePath = getCredentialsFilePath()) {
141
+ const existing = await readStoredCredentials(filePath);
142
+ if (!existing) return null;
143
+ const removed = existing.orgs.find((o) => o.organizationId === orgId);
144
+ if (!removed) return null;
145
+ const remainingOrgs = existing.orgs.filter((o) => o.organizationId !== orgId);
146
+ if (remainingOrgs.length === 0) {
147
+ await deleteStoredCredentials(filePath);
148
+ return removed;
149
+ }
150
+ const newActiveId = existing.activeOrgId === orgId ? remainingOrgs[0]?.organizationId : existing.activeOrgId;
151
+ await writeStoredCredentials({
152
+ ...existing,
153
+ orgs: remainingOrgs,
154
+ activeOrgId: newActiveId
155
+ }, filePath);
156
+ return removed;
157
+ }
158
+ function toErrorMessage(error) {
159
+ return error instanceof Error ? error.message : String(error);
160
+ }
161
+ function findActiveOrg(creds) {
162
+ if (!creds.activeOrgId) return creds.orgs[0];
163
+ return creds.orgs.find((o) => o.organizationId === creds.activeOrgId) ?? creds.orgs[0];
164
+ }
165
+ /**
166
+ * Resolves API auth options using explicit overrides first, then ~/.keystroke credentials.
167
+ * When a logger is provided, it emits consistent fallback/read diagnostics.
168
+ */
169
+ async function resolveAuthOptions(input) {
170
+ const credentialsPath = input.credentialsPath ?? getCredentialsFilePath();
171
+ let storedCredentials = null;
172
+ try {
173
+ storedCredentials = await readStoredCredentials(credentialsPath);
174
+ } catch (error) {
175
+ input.logger?.warn(`Could not read saved credentials from ${credentialsPath}: ${toErrorMessage(error)}`);
176
+ }
177
+ const activeOrg = storedCredentials ? findActiveOrg(storedCredentials) : void 0;
178
+ const apiKey = input.apiKey ?? activeOrg?.apiKey;
179
+ const baseUrl = input.baseUrl ?? storedCredentials?.serverUrl;
180
+ if (!input.apiKey && activeOrg?.apiKey) input.logger?.info(`Using saved API key from ${credentialsPath}`);
181
+ if (!input.baseUrl && storedCredentials?.serverUrl) input.logger?.info(`Using saved API URL from ${credentialsPath}`);
182
+ return {
183
+ apiKey,
184
+ baseUrl,
185
+ credentialsPath,
186
+ storedCredentials,
187
+ activeOrg
188
+ };
189
+ }
190
+ const KEYSTROKE_LOGS_DIR = "logs";
191
+ const KEYSTROKE_LOG_FILE = "cli.jsonl";
192
+ function getLogFilePath(homeDir = os.homedir()) {
193
+ return path$1.join(homeDir, KEYSTROKE_CREDENTIALS_DIR, KEYSTROKE_LOGS_DIR, KEYSTROKE_LOG_FILE);
194
+ }
195
+ /**
196
+ * Reads the last `count` lines from the log file as raw strings.
197
+ * Returns an empty array if the file does not exist.
198
+ */
199
+ async function readLogFile(count = 100, homeDir = os.homedir()) {
200
+ const filePath = getLogFilePath(homeDir);
201
+ let content;
202
+ try {
203
+ content = await fs$1.readFile(filePath, "utf-8");
204
+ } catch {
205
+ return [];
206
+ }
207
+ const lines = content.split("\n");
208
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
209
+ return lines.slice(-count);
210
+ }
211
+ /**
212
+ * Deletes the log file. Returns true if the file existed and was deleted,
213
+ * false if it did not exist.
214
+ */
215
+ async function clearLogFile(homeDir = os.homedir()) {
216
+ const filePath = getLogFilePath(homeDir);
217
+ try {
218
+ await fs$1.unlink(filePath);
219
+ return true;
220
+ } catch (err) {
221
+ if (err.code === "ENOENT") return false;
222
+ throw err;
223
+ }
224
+ }
225
+ const KEYSTROKE_PROJECTS_FILE = "projects.json";
226
+ const projectEntrySchema = z.object({
227
+ lastAccessed: z.string().min(1),
228
+ name: z.string().min(1).optional()
229
+ });
230
+ const storedProjectsSchema = z.object({
231
+ version: z.literal(1),
232
+ projects: z.record(z.string(), projectEntrySchema),
233
+ lastProject: z.string().optional()
234
+ });
235
+ function getProjectsFilePath(homeDir = os.homedir()) {
236
+ return path$1.join(homeDir, KEYSTROKE_CREDENTIALS_DIR, KEYSTROKE_PROJECTS_FILE);
237
+ }
238
+ async function readStoredProjects(filePath = getProjectsFilePath()) {
239
+ try {
240
+ const raw = await fs$1.readFile(filePath, "utf-8");
241
+ const parsed = JSON.parse(raw);
242
+ const result = storedProjectsSchema.safeParse(parsed);
243
+ if (!result.success) return null;
244
+ return result.data;
245
+ } catch (error) {
246
+ if (error.code === "ENOENT") return null;
247
+ if (error instanceof SyntaxError) return null;
248
+ throw error;
249
+ }
250
+ }
251
+ async function writeStoredProjects(projects, filePath = getProjectsFilePath()) {
252
+ const parsed = storedProjectsSchema.parse(projects);
253
+ await fs$1.mkdir(path$1.dirname(filePath), { recursive: true });
254
+ await fs$1.writeFile(filePath, `${JSON.stringify(parsed, null, 2)}\n`);
255
+ }
256
+ /**
257
+ * Upsert a project entry. Fire-and-forget safe — catches all errors internally.
258
+ * Never throws. Call without `await` if you don't need to wait.
259
+ */
260
+ async function trackProject(projectPath, options) {
261
+ const filePath = options?.filePath ?? getProjectsFilePath();
262
+ const absolutePath = path$1.resolve(projectPath);
263
+ try {
264
+ const data = await readStoredProjects(filePath) ?? {
265
+ version: 1,
266
+ projects: {}
267
+ };
268
+ const previousEntry = data.projects[absolutePath];
269
+ const entry = {
270
+ lastAccessed: (/* @__PURE__ */ new Date()).toISOString(),
271
+ ...options?.name ? { name: options.name } : previousEntry?.name ? { name: previousEntry.name } : {}
272
+ };
273
+ data.projects[absolutePath] = entry;
274
+ data.lastProject = absolutePath;
275
+ await writeStoredProjects(data, filePath);
276
+ } catch {}
277
+ }
278
+ /**
279
+ * Clears the stored projects file (projects.json). Returns true if the file
280
+ * existed and was deleted, false if it did not exist.
281
+ */
282
+ async function clearStoredProjects(filePath = getProjectsFilePath()) {
283
+ try {
284
+ await fs$1.unlink(filePath);
285
+ return true;
286
+ } catch (error) {
287
+ if (error.code === "ENOENT") return false;
288
+ throw error;
289
+ }
290
+ }
291
+ /** Base directory name for all Keystroke local storage (user and project-local). */
292
+ const KEYSTROKE_DIR = KEYSTROKE_CREDENTIALS_DIR;
293
+ /**
294
+ * Returns the Keystroke temp directory path.
295
+ *
296
+ * - `getKeystrokeTmpDir({ projectRoot })` → `projectRoot/.keystroke/tmp` — for build
297
+ * artifacts that require module resolution from the project (workflow-builder).
298
+ * - `getKeystrokeTmpDir()` → `~/.keystroke/tmp` — for general temporary storage.
299
+ *
300
+ * Callers must create the directory (e.g. `mkdir(path, { recursive: true })`)
301
+ * before use.
302
+ */
303
+ function getKeystrokeTmpDir(options) {
304
+ const baseDir = options?.projectRoot ? path$1.resolve(options.projectRoot) : os.homedir();
305
+ return path$1.join(baseDir, KEYSTROKE_DIR, "tmp");
306
+ }
307
+ //#endregion
308
+ export { getKeystrokeTmpDir as a, removeOrgCredentials as c, trackProject as d, upsertOrgCredentials as f, getCredentialsFilePath as i, resolveAuthOptions as l, clearStoredProjects as n, readLogFile as o, deleteStoredCredentials as r, readStoredProjects as s, clearLogFile as t, setActiveOrg as u };
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { z } from "zod";
6
+ import { fileURLToPath } from "node:url";
7
+ import { config } from "dotenv";
8
+ //#region ../../packages/env-utils/dist/index.mjs
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ function findMonorepoRoot() {
12
+ let currentDir = __dirname;
13
+ while (currentDir !== "/") {
14
+ const turboPath = path.join(currentDir, "turbo.json");
15
+ const pnpmWorkspacePath = path.join(currentDir, "pnpm-workspace.yaml");
16
+ if (existsSync(turboPath) && existsSync(pnpmWorkspacePath)) return currentDir;
17
+ currentDir = path.dirname(currentDir);
18
+ }
19
+ throw new Error("Could not find monorepo root (turbo.json + pnpm-workspace.yaml)");
20
+ }
21
+ function ensureRootEnvFile(rootEnvPath) {
22
+ try {
23
+ writeFileSync(rootEnvPath, "# Temporary root env file generated by @keystroke/env-utils\n", { flag: "wx" });
24
+ } catch (error) {
25
+ const fsError = error;
26
+ if (fsError.code === "EEXIST") return;
27
+ throw new Error(`Could not create temporary root env file at ${rootEnvPath}: ${fsError.message ?? "unknown error"}`);
28
+ }
29
+ }
30
+ function resolveEnvPath(options = {}) {
31
+ const fallbackToDefault = options.fallbackToDefault ?? true;
32
+ if (options.absolutePath) {
33
+ if (existsSync(options.absolutePath)) return options.absolutePath;
34
+ if (!fallbackToDefault) throw new Error(`Could not find env file at explicit path: ${options.absolutePath}`);
35
+ }
36
+ const fileName = options.fileName ?? ".env";
37
+ let rootDir = null;
38
+ try {
39
+ rootDir = findMonorepoRoot();
40
+ } catch {}
41
+ if (rootDir) {
42
+ const rootEnvPath = path.join(rootDir, fileName);
43
+ if (existsSync(rootEnvPath)) return rootEnvPath;
44
+ if (fileName === ".env") {
45
+ ensureRootEnvFile(rootEnvPath);
46
+ return rootEnvPath;
47
+ }
48
+ }
49
+ if (!fallbackToDefault) throw new Error(`Could not find env file in monorepo root: ${fileName}`);
50
+ return path.join(process.cwd(), fileName);
51
+ }
52
+ /**
53
+ * Load environment variables from a file in the monorepo root.
54
+ *
55
+ * This function finds the monorepo root by looking for turbo.json and pnpm-workspace.yaml,
56
+ * then loads the requested env file from that location. For the default `.env`, this utility
57
+ * creates a temporary root `.env` file if one does not exist. Other file names can fall back
58
+ * to the current working directory when allowed.
59
+ *
60
+ * All dotenv logs are suppressed to keep output clean.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * import { loadRootEnv } from '@keystroke/env-utils';
65
+ *
66
+ * // Load env vars before accessing process.env
67
+ * loadRootEnv();
68
+ *
69
+ * // Explicit integration env file (use `fallbackToDefault: false` only when a missing file must hard-fail):
70
+ * loadRootEnv({ fileName: '.env.test', fallbackToDefault: true });
71
+ * ```
72
+ */
73
+ function loadRootEnv(options) {
74
+ config({
75
+ path: resolveEnvPath(options),
76
+ quiet: true,
77
+ debug: false
78
+ });
79
+ }
80
+ const EnvironmentSchema = z.enum([
81
+ "development",
82
+ "staging",
83
+ "production"
84
+ ]);
85
+ z.enum([
86
+ "development",
87
+ "production",
88
+ "test"
89
+ ]);
90
+ z.enum([
91
+ "cli",
92
+ "server",
93
+ "worker"
94
+ ]);
95
+ z.enum([
96
+ "development",
97
+ "test",
98
+ "staging",
99
+ "production"
100
+ ]);
101
+ //#endregion
102
+ //#region src/lib/env.ts
103
+ loadRootEnv();
104
+ const cliEnvSchema = z.object({
105
+ WEB_URL: z.url().optional(),
106
+ SERVER_URL: z.url().optional(),
107
+ KEYSTROKE_API_KEY: z.string().min(1).optional(),
108
+ KEYSTROKE_ORG_ID: z.string().min(1).optional(),
109
+ ENVIRONMENT: EnvironmentSchema.optional().default("production")
110
+ });
111
+ let cachedEnv;
112
+ /**
113
+ * Returns the process environment for credential resolution.
114
+ * Used by credentials upload and other features that need KEYSTROKE_* vars.
115
+ */
116
+ function getProcessEnv() {
117
+ return process.env;
118
+ }
119
+ function getEnv() {
120
+ if (cachedEnv) return cachedEnv;
121
+ cachedEnv = cliEnvSchema.parse(process.env);
122
+ return cachedEnv;
123
+ }
124
+ function resolveCliWebUrl(override) {
125
+ if (override) return override;
126
+ const env = getEnv();
127
+ if (!env.WEB_URL) throw new Error("WEB_URL is required (set WEB_URL env or pass --web-url)");
128
+ return env.WEB_URL;
129
+ }
130
+ function isLocalMode() {
131
+ return getEnv().ENVIRONMENT === "development";
132
+ }
133
+ function resolveCliServerUrl(override) {
134
+ if (override) return override;
135
+ const env = getEnv();
136
+ if (!env.SERVER_URL) throw new Error("SERVER_URL is required (set SERVER_URL env or pass --server-url)");
137
+ return env.SERVER_URL;
138
+ }
139
+ //#endregion
140
+ export { resolveCliWebUrl as a, resolveCliServerUrl as i, getProcessEnv as n, isLocalMode as r, getEnv as t };