@keystrokehq/cli 0.1.15 → 0.1.17

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 (46) hide show
  1. package/dist/{dist-BnIugffH.mjs → dist-BRA_tOTT.mjs} +47 -5
  2. package/dist/{dist-BnIugffH.mjs.map → dist-BRA_tOTT.mjs.map} +1 -1
  3. package/dist/dist-BZBvPUyu.mjs +3 -0
  4. package/dist/{dist-PBiyADK0.mjs → dist-C6KqfgGN.mjs} +3 -3
  5. package/dist/{dist-PBiyADK0.mjs.map → dist-C6KqfgGN.mjs.map} +1 -1
  6. package/dist/{dist-B4pkCJsM.mjs → dist-DeRE4uJW.mjs} +2 -2
  7. package/dist/dist-DeRE4uJW.mjs.map +1 -0
  8. package/dist/{dist-BW3AMCud.mjs → dist-E9nHDRnf.mjs} +3 -3
  9. package/dist/{dist-BW3AMCud.mjs.map → dist-E9nHDRnf.mjs.map} +1 -1
  10. package/dist/index.mjs +27 -15
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{maybe-auto-update-BIarxWf3.mjs → maybe-auto-update-douMZFWJ.mjs} +2 -2
  13. package/dist/{maybe-auto-update-BIarxWf3.mjs.map → maybe-auto-update-douMZFWJ.mjs.map} +1 -1
  14. package/dist/skills-bundle/_AGENTS.mcp.md +10 -3
  15. package/dist/skills-bundle/_AGENTS.md +61 -70
  16. package/dist/skills-bundle/skills/keystroke-actions/SKILL.md +60 -12
  17. package/dist/skills-bundle/skills/keystroke-actions/references/catalog-and-imports.md +32 -3
  18. package/dist/skills-bundle/skills/keystroke-agents/SKILL.md +50 -8
  19. package/dist/skills-bundle/skills/keystroke-agents/references/models.md +11 -13
  20. package/dist/skills-bundle/skills/keystroke-agents/references/tools-mcp-codemode.md +45 -3
  21. package/dist/skills-bundle/skills/keystroke-agents/references/workflows-and-testing.md +1 -1
  22. package/dist/skills-bundle/skills/keystroke-apps/SKILL.md +26 -13
  23. package/dist/skills-bundle/skills/keystroke-apps/references/cli-and-catalog.md +47 -16
  24. package/dist/skills-bundle/skills/keystroke-channels/SKILL.md +66 -0
  25. package/dist/skills-bundle/skills/keystroke-channels/references/slack-setup.md +41 -0
  26. package/dist/skills-bundle/skills/keystroke-cli/SKILL.md +41 -93
  27. package/dist/skills-bundle/skills/keystroke-deploy/SKILL.md +10 -9
  28. package/dist/skills-bundle/skills/keystroke-deploy/references/build-and-full-deploy.md +3 -1
  29. package/dist/skills-bundle/skills/keystroke-deploy/references/filtered-deploy.md +3 -2
  30. package/dist/skills-bundle/skills/keystroke-deploy/references/wip-ignore.md +5 -2
  31. package/dist/skills-bundle/skills/keystroke-files/SKILL.md +12 -4
  32. package/dist/skills-bundle/skills/keystroke-skills/SKILL.md +7 -2
  33. package/dist/skills-bundle/skills/keystroke-triggers/SKILL.md +30 -17
  34. package/dist/skills-bundle/skills/keystroke-workflows/SKILL.md +27 -12
  35. package/dist/skills-bundle/skills/keystroke-workflows/references/authoring.md +116 -4
  36. package/dist/skills-bundle/skills/keystroke-workflows/references/testing.md +17 -9
  37. package/dist/templates/hello-world/README.md +19 -8
  38. package/dist/templates/hello-world/src/workflows/greeting.test.ts +1 -1
  39. package/dist/{version-DScIhncv.mjs → version-CJd1mEoq.mjs} +2 -2
  40. package/dist/{version-DScIhncv.mjs.map → version-CJd1mEoq.mjs.map} +1 -1
  41. package/package.json +2 -2
  42. package/dist/dist-B4pkCJsM.mjs.map +0 -1
  43. package/dist/dist-c4WWC9_F.mjs +0 -3
  44. package/dist/skills-bundle/skills/keystroke-cli/references/api-targets.md +0 -87
  45. package/dist/skills-bundle/skills/keystroke-gateways/SKILL.md +0 -43
  46. package/dist/skills-bundle/skills/keystroke-gateways/references/slack-setup.md +0 -27
@@ -10,7 +10,11 @@ async run(input) {
10
10
  const summary = await support.prompt({
11
11
  message: `Summarize signup research:\n${brief}`,
12
12
  });
13
- return postMessage.run({ channel: "#pipeline", text: signupBriefMessage({ ...input, brief }) });
13
+ const sent = await slackSendMessage.run({
14
+ channel: "#pipeline",
15
+ markdown_text: signupBriefMessage({ ...input, brief }),
16
+ });
17
+ return { brief, ...sent };
14
18
  }
15
19
  ```
16
20
 
@@ -18,7 +22,7 @@ Actions are leaf units: an action never calls another action — compose them in
18
22
 
19
23
  ## Agents as workflow steps
20
24
 
21
- Import the agent and call `.prompt()` directly in the workflow `run`. Each call is a durable step keyed as `step:<agentKey>#<occurrence>` (same scheme as actions). No wrapper action required when the step is just "prompt this agent."
25
+ Import the agent and call `.prompt()` directly in the workflow `run`. Each call is a durable step keyed as `step:<agent-slug>#<occurrence>` (same scheme as actions). No wrapper action required when the step is just "prompt this agent."
22
26
 
23
27
  ```ts
24
28
  const result = await support.prompt({
@@ -28,6 +32,110 @@ const result = await support.prompt({
28
32
 
29
33
  Use a wrapper action only when the agent call should also be an agent tool, or when the step bundles non-prompt logic.
30
34
 
35
+ For an explicit step id, actions chain `.stepId("x")`; agent prompts take it as an option: `agent.prompt(input, { stepId: "x" })`.
36
+
37
+ ## LLM steps (`promptLlm`)
38
+
39
+ For a one-shot LLM call without a full agent, use `promptLlm` (from `@keystrokehq/keystroke/workflow`). It's a first-class durable step keyed `step:promptLlm#<occurrence>`. The signature is `promptLlm(prompt, opts)` — the prompt string comes **first**, options second:
40
+
41
+ ```ts
42
+ import { promptLlm } from "@keystrokehq/keystroke/workflow";
43
+
44
+ // Returns a string when no outputSchema is given:
45
+ const summary = await promptLlm(`Summarize:\n${brief}`, {
46
+ model: "anthropic/claude-sonnet-4-6",
47
+ });
48
+ ```
49
+
50
+ Pass `outputSchema` (Zod) to get a parsed, schema-validated object back instead of a string — the return type is inferred from the schema:
51
+
52
+ ```ts
53
+ import { z } from "zod";
54
+
55
+ const Category = z.object({ category: z.enum(["bug", "feature", "question"]) });
56
+
57
+ const { category } = await promptLlm(`Classify this ticket:\n${ticket}`, {
58
+ model: "anthropic/claude-sonnet-4-6",
59
+ outputSchema: Category,
60
+ });
61
+ ```
62
+
63
+ Other options: `system`, `thinkingLevel`, `temperature`, `maxTokens`, `stepId`.
64
+
65
+ ## Durability & retries
66
+
67
+ As each step completes, its result is written to a run event log. If a later step fails and the run is retried, Keystroke replays the log: completed steps return their recorded result instead of running again, and execution resumes at the first unfinished step. Design for this:
68
+
69
+ - **Put side effects inside steps.** Work in the `run` body that isn't an action / agent / `promptLlm` call (a raw `fetch`, a DB write, `Date.now()`, `Math.random()`) runs again on every replay. Move it into a step so it's recorded once.
70
+ - **Steps should be idempotent.** A step can be retried after a transient failure, so an action should tolerate being called twice with the same input (use idempotency keys for external writes where it matters).
71
+ - **Keep control flow deterministic.** Branch on the run's input and recorded step results — not on wall-clock time or randomness — so replays take the same path. Each step is recorded under `step:<slug>#<occurrence>`; pin `.stepId("x")` when an action's order can shift so replays still line up. Ids must be unique within a run.
72
+
73
+ You don't manage retries yourself. The runtime records `step_completed`, `step_retrying`, and `step_failed` events and surfaces them in `keystroke workflow runs get <slug> <run-id> --include steps,trace`.
74
+
75
+ ## Parallel steps
76
+
77
+ Independent steps run concurrently with `Promise.all` — each `.run()` / `.prompt()` / `promptLlm` inside it is still its own durable step, recorded and replayed individually:
78
+
79
+ ```ts
80
+ const [enriched, scored] = await Promise.all([
81
+ enrichLead.run({ leadId }),
82
+ scoreLead.run({ leadId }),
83
+ ]);
84
+ ```
85
+
86
+ Correlation ids are assigned per slug in array order (`step:enrich-lead#0`, `step:score-lead#0`). If the branches can change order between attempts (conditionally included, dynamic arrays), pin a stable `.stepId()` on each so replays line up:
87
+
88
+ ```ts
89
+ const results = await Promise.all(
90
+ leads.map((lead) => enrichLead.run({ leadId: lead.id }).stepId(`enrich:${lead.id}`)),
91
+ );
92
+ ```
93
+
94
+ There's no built-in concurrency limit or fan-out helper — `Promise.all` runs everything at once. For bounded concurrency, batch the array yourself (e.g. chunk and `await` each chunk).
95
+
96
+ ## Handling failures
97
+
98
+ Retries are **queue-level**: a failed run is re-enqueued (default 3 attempts, exponential backoff) and replays from the event log, so completed steps don't re-run. Each failed attempt emits `step_retrying`; the final failure emits `step_failed`. You don't write retry loops — make steps idempotent so a retried step is safe.
99
+
100
+ There's no built-in saga, compensation engine, or dead-letter queue. When a later step fails and you need to undo earlier work, orchestrate it yourself with `try/catch` in `run` — catching the error lets the run complete normally (so attach a `compensated` flag to the output rather than rethrowing if you've handled it):
101
+
102
+ ```ts
103
+ async run(input) {
104
+ await reserveBooking.run({ id: input.bookingId });
105
+ try {
106
+ await chargeCustomer.run({ id: input.bookingId });
107
+ return { ok: true };
108
+ } catch (error) {
109
+ await releaseBooking.run({ id: input.bookingId }); // best-effort cleanup
110
+ return { ok: false, compensated: true };
111
+ }
112
+ }
113
+ ```
114
+
115
+ If you let the error propagate instead, the run fails and the queue retries it — fine for transient failures, but it replays from the log, so any cleanup must itself be a recorded, idempotent step.
116
+
117
+ ## Durable run context (`ctx`)
118
+
119
+ `run(input, ctx)` receives a second argument exposing `runId`, `trigger` (`api` | `cron` | `webhook` | `poll` | `retry`), and the durable wait primitives `ctx.sleep(...)` and `ctx.hook(...)`. Both **suspend** the run (the replay engine resumes it later) so long waits and external callbacks don't hold a process open.
120
+
121
+ ```ts
122
+ // Durable delay: number (ms), duration string, or a Date
123
+ await ctx.sleep("1h");
124
+
125
+ // Durable hook: suspend until an external system POSTs to the resume URL
126
+ async run(input, ctx) {
127
+ const approval = ctx.hook<{ approved: boolean }>();
128
+ await slackSendMessage.run({
129
+ channel: "#approvals",
130
+ markdown_text: `Approve deploy? ${approval.resumeUrl}`,
131
+ });
132
+ const { approved } = await approval; // suspends here until resumed
133
+ return { approved };
134
+ }
135
+ ```
136
+
137
+ `ctx.hook(options?)` takes an optional `token` (reuse a stable resume token) and `schema` (Zod validation of the resume payload). The returned handle exposes `token` and `resumeUrl` before you await it — surface that URL so the approver/callback can resume the run.
138
+
31
139
  ## Agents inside actions
32
140
 
33
141
  When an action must call an agent (e.g. the action is reused as an agent tool), call `.prompt()` inside the action's `run`. That prompt is not a separate workflow step — the action is.
@@ -36,6 +144,10 @@ When an action must call an agent (e.g. the action is reused as an agent tool),
36
144
 
37
145
  Sync the app into `src/apps/`, author with `app.action()`, then `keystroke connect <slug>` before testing. Catalog integration actions from npm packages work once the app is connected.
38
146
 
39
- ## Keys
147
+ ## Slugs
148
+
149
+ Workflow `slug`, action `slug`, agent `slug`, and trigger `slug` share one namespace — pick unique names (`signup-pipeline`, not `pipeline`). The workflow's `slug` is also its HTTP route segment (`POST /workflows/{slug}`).
150
+
151
+ ## Subscription mode
40
152
 
41
- Workflow `key`, action `key`, agent `key`, and trigger `key` share one namespace pick unique names (`signup-pipeline`, not `pipeline`).
153
+ `defineWorkflow` accepts an optional `subscription: { mode: "system" | "subscribable" }` to control how the workflow can be subscribed to. Omit it for the default behavior.
@@ -2,15 +2,15 @@
2
2
 
3
3
  ## Unit tests (in-process, no server)
4
4
 
5
- Test through `executeWorkflow` from `@keystrokehq/workflow` — it parses input/output and wires up the action runner. **Never call `workflow.run(...)` directly**: that skips Zod validation and has no action context, so any action step throws.
5
+ Test through `executeWorkflow` from `@keystrokehq/keystroke/workflow` — it parses input/output and wires up the action runner. **Never call `workflow.run(...)` directly in a test**: called at top level (outside `executeWorkflow`) it skips Zod validation and has no action context, so any action step throws.
6
6
 
7
- `executeWorkflow` runs the workflow via the durable replay engine and resolves to a `ReplayResult`: `{ status: "completed", output }`, `{ status: "failed", error }`, or `{ status: "suspended", items }` (when the body hits `ctx.sleep`/`ctx.hook`). Assert on `status` and read `output`.
7
+ `executeWorkflow` runs the workflow via the durable replay engine and resolves to a `ReplayResult`: `{ status: "completed", output }`, `{ status: "failed", error }`, `{ status: "suspended", items }` (when the body hits `ctx.sleep`/`ctx.hook`), or `{ status: "canceled" }`. Assert on `status` and read `output`.
8
8
 
9
9
  `keystroke init` scaffolds Vitest and a starter test (e.g. `src/workflows/greeting.test.ts`):
10
10
 
11
11
  ```ts
12
12
  import { describe, expect, it } from "vitest";
13
- import { executeWorkflow } from "@keystrokehq/workflow";
13
+ import { executeWorkflow } from "@keystrokehq/keystroke/workflow";
14
14
  import greeting from "./greeting";
15
15
 
16
16
  describe("greeting workflow", () => {
@@ -27,7 +27,7 @@ For multi-step workflows:
27
27
 
28
28
  ```ts
29
29
  import { describe, expect, it } from "vitest";
30
- import { executeWorkflow } from "@keystrokehq/workflow";
30
+ import { executeWorkflow } from "@keystrokehq/keystroke/workflow";
31
31
  import workflow from "../signup-pipeline";
32
32
 
33
33
  it("runs the pipeline", async () => {
@@ -39,7 +39,7 @@ it("runs the pipeline", async () => {
39
39
 
40
40
  expect(result.status).toBe("completed");
41
41
  if (result.status === "completed") {
42
- expect(result.output).toMatchObject({ channel: "#signups" });
42
+ expect(result.output).toMatchObject({ channel: "#pipeline" });
43
43
  }
44
44
  });
45
45
  ```
@@ -51,7 +51,7 @@ Deterministic actions (pure logic, no network/LLM) need no mocks — run the wor
51
51
  Steps are checkpointed in the durable event log as `step_completed` events keyed by a stable **correlation id**. Before running a step the action runner checks the log for that correlation id; pre-seed a `step_completed` event to skip real work (LLM/agent/HTTP calls) and feed a fixture:
52
52
 
53
53
  ```ts
54
- import { executeWorkflow, MemoryEventLog } from "@keystrokehq/workflow";
54
+ import { executeWorkflow, MemoryEventLog } from "@keystrokehq/keystroke/workflow";
55
55
  import workflow from "../signup-pipeline";
56
56
 
57
57
  it("drafts without calling the agent", async () => {
@@ -83,7 +83,7 @@ it("drafts without calling the agent", async () => {
83
83
  Rules:
84
84
 
85
85
  - Pass a fixed `runId` and the same `MemoryEventLog` to `executeWorkflow`.
86
- - The correlation id is `step:<actionKey>#<occurrence>` — `#0` for the first call to that action, `#1` for the second, and so on — unless the step used `.stepId("x")`, in which case it is `step:x`.
86
+ - The correlation id is `step:<slug>#<occurrence>` — `#0` for the first call to that action/agent, `#1` for the second, and so on — unless an explicit step id was set (`action.stepId("x")` or `agent.prompt(input, { stepId: "x" })`), in which case it is `step:x`.
87
87
  - The action **output** is stored as the event `data` directly (no wrapper). It is re-validated against the action's output schema, so it must be schema-valid.
88
88
 
89
89
  This is the preferred mock: schema-checked, no module-mock hoisting, and it still exercises the real `run` orchestration (branching, loops, output shape) — only the action bodies are stubbed.
@@ -92,11 +92,19 @@ This is the preferred mock: schema-checked, no module-mock hoisting, and it stil
92
92
 
93
93
  When you'd rather stub the underlying dependency (e.g. the agent inside an action) or assert how it was called, use `vi.mock`:
94
94
 
95
+ `agent.prompt()` resolves to a `PromptResponse` — `{ sessionId, messages, error, canceled?, output? }` (`output` is the parsed result when the prompt was called with an `outputSchema`), **not** a bare messages array. Mock that shape:
96
+
95
97
  ```ts
96
98
  import { vi } from "vitest";
97
99
 
98
100
  vi.mock("../agents/support", () => ({
99
- default: { prompt: vi.fn().mockResolvedValue([{ role: "assistant", content: "mocked" }]) },
101
+ default: {
102
+ prompt: vi.fn().mockResolvedValue({
103
+ sessionId: "test-session",
104
+ messages: [{ role: "assistant", content: "mocked" }],
105
+ error: null,
106
+ }),
107
+ },
100
108
  }));
101
109
  ```
102
110
 
@@ -108,7 +116,7 @@ Validation happens at the `executeWorkflow` boundary, so contract tests are free
108
116
  await expect(executeWorkflow(workflow, { email: "x" } as never)).rejects.toThrow();
109
117
  ```
110
118
 
111
- ## Debug runs against a running server (CLI)
119
+ ## Debug runs from the CLI
112
120
 
113
121
  ```bash
114
122
  keystroke workflow run signup-pipeline --input '{"name":"Ada","email":"ada@acme.com","company":"Acme"}'
@@ -4,26 +4,35 @@ Keystroke project — agents, workflows, actions, and triggers under `src/`. See
4
4
 
5
5
  ## Getting started
6
6
 
7
+ Keystroke is deploy-first: build in `src/`, deploy to your platform project, then run and inspect what's deployed with the CLI.
8
+
7
9
  ```bash
8
- pnpm install # @keystrokehq/* from GitHub Packages (see .npmrc)
9
- # .env is created from .env.example on init — set ANTHROPIC_API_KEY and integration keys
10
- keystroke start # API :3002, dashboard :3000
10
+ pnpm install # @keystrokehq/* from GitHub Packages (see .npmrc)
11
+ keystroke auth login # once
12
+ keystroke deploy --project <id> # build + ship src/ (see: keystroke project list)
11
13
  ```
12
14
 
13
- Login at `http://localhost:3000` (`admin@example.com` / `adminadmin` by default).
14
-
15
- Prompt the hello agent:
15
+ <!-- example:start -->
16
+ Prompt the hello agent and run the greeting workflow against your deployed project:
16
17
 
17
18
  ```bash
18
19
  keystroke agent prompt hello --message "Hi"
20
+ keystroke workflow run greeting --input '{"name":"Ada"}'
19
21
  ```
20
22
 
21
- Run the greeting workflow:
23
+ Use `--filter agents/hello` (or `workflows/greeting`) to redeploy a single module fast.
24
+ <!-- example:end -->
25
+
26
+ ## Run locally (optional)
27
+
28
+ To iterate offline without deploying, run a local server. Set `ANTHROPIC_API_KEY` and any integration keys in `.env` (created from `.env.example` on init), then:
22
29
 
23
30
  ```bash
24
- keystroke workflow run greeting --input '{"name":"Ada"}'
31
+ keystroke dev # watch src/, rebuild, restart the API (API :3002, dashboard :3000)
25
32
  ```
26
33
 
34
+ The same `keystroke agent` / `keystroke workflow` commands then target your local server. Login at `http://localhost:3000` (`admin@example.com` / `adminadmin` by default).
35
+
27
36
  ## Linting
28
37
 
29
38
  ```bash
@@ -39,7 +48,9 @@ pnpm test:unit # src/**/*.test.ts
39
48
  pnpm test:int # src/**/*.int.test.ts (needs ANTHROPIC_API_KEY for agent tests)
40
49
  ```
41
50
 
51
+ <!-- example:start -->
42
52
  Example: `src/workflows/greeting.test.ts` uses `executeWorkflow` to test the `greeting` workflow in-process.
53
+ <!-- example:end -->
43
54
 
44
55
  ## Layout
45
56
 
@@ -6,6 +6,6 @@ describe("greeting workflow", () => {
6
6
  it("returns a greeting for a name", async () => {
7
7
  const result = await executeWorkflow(greeting, { name: "Ada" });
8
8
 
9
- expect(result).toEqual({ greeting: "Hello, Ada!" });
9
+ expect(result).toEqual({ status: "completed", output: { greeting: "Hello, Ada!" } });
10
10
  });
11
11
  });
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { st as LOCAL_PLATFORM_ORIGIN, zn as originFromPublicUrl } from "./dist-BnIugffH.mjs";
2
+ import { Vn as originFromPublicUrl, st as LOCAL_PLATFORM_ORIGIN } from "./dist-BRA_tOTT.mjs";
3
3
  import Conf from "conf";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
@@ -166,4 +166,4 @@ function readCliVersion() {
166
166
  //#endregion
167
167
  export { installPlaygroundDependencies as a, createCliConfig as c, getEffectiveApiTarget as d, getPlatformUrl as f, resolvePlatformUrlForWebUrl as g, DEFAULT_WEB_URL as h, installDependencies as i, getCliConfigDir as l, DEFAULT_PLATFORM_URL as m, buildPlaygroundWorkspace as n, resolvePackageManager as o, getWebUrl as p, detectPackageManager as r, resolveCliRoot as s, readCliVersion as t, getConfigDir as u };
168
168
 
169
- //# sourceMappingURL=version-DScIhncv.mjs.map
169
+ //# sourceMappingURL=version-CJd1mEoq.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"version-DScIhncv.mjs","names":[],"sources":["../src/resolve-platform-url.ts","../src/config.ts","../src/project/resolve-cli-root.ts","../src/init/package-manager.ts","../src/version.ts"],"sourcesContent":["import { LOCAL_PLATFORM_ORIGIN, originFromPublicUrl } from \"@keystrokehq/shared\";\n\nexport const DEFAULT_WEB_URL = \"https://keystroke.ai\";\nexport const DEFAULT_PLATFORM_URL = \"https://api.keystroke.ai\";\nexport const LOCAL_WEB_URL = \"http://localhost:3000\";\n\nexport type ResolvePlatformUrlOptions = {\n platformUrl?: string;\n /** Used when webUrl does not match a known deployment. */\n fallback?: string;\n};\n\nfunction webOriginFromUrl(webUrl: string): string {\n return originFromPublicUrl(webUrl, webUrl.replace(/\\/+$/, \"\"));\n}\n\n/** Known web origins with a fixed platform API — undefined for custom/staging URLs. */\nexport function knownPlatformUrlForWebUrl(webUrl: string): string | undefined {\n const webOrigin = webOriginFromUrl(webUrl);\n\n if (webOrigin === LOCAL_WEB_URL || webOrigin === \"http://127.0.0.1:3000\") {\n return LOCAL_PLATFORM_ORIGIN;\n }\n\n if (webOrigin === DEFAULT_WEB_URL) {\n return DEFAULT_PLATFORM_URL;\n }\n\n return undefined;\n}\n\nexport function resolvePlatformUrlForWebUrl(\n webUrl: string,\n options: ResolvePlatformUrlOptions = {},\n): string {\n const explicit = options.platformUrl?.trim();\n if (explicit) {\n return explicit.replace(/\\/+$/, \"\");\n }\n\n const known = knownPlatformUrlForWebUrl(webUrl);\n if (known) {\n return known;\n }\n\n const fallback = options.fallback?.trim() || DEFAULT_PLATFORM_URL;\n return fallback.replace(/\\/+$/, \"\");\n}\n","import Conf from \"conf\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport {\n DEFAULT_PLATFORM_URL,\n DEFAULT_WEB_URL,\n knownPlatformUrlForWebUrl,\n resolvePlatformUrlForWebUrl,\n} from \"./resolve-platform-url\";\n\nexport type ApiTargetMode = \"local\" | \"platform\";\n\nexport type CliConfig = {\n webUrl: string;\n platformUrl: string;\n activeOrganizationId?: string;\n activeProjectId?: string;\n apiTarget?: ApiTargetMode;\n};\n\nexport function getCliConfigDir(cwd = join(homedir(), \".keystroke\")): string {\n return cwd;\n}\n\nexport function getConfigDir(config: Conf<CliConfig>): string {\n return dirname(config.path);\n}\n\nexport function getEffectiveApiTarget(config: Conf<CliConfig>): ApiTargetMode {\n const explicit = config.get(\"apiTarget\");\n if (explicit === \"local\" || explicit === \"platform\") {\n return explicit;\n }\n\n if (process.env.KEYSTROKE_API_KEY?.trim()) {\n return \"platform\";\n }\n\n return \"local\";\n}\n\nfunction syncPlatformUrlWithWebUrl(config: Conf<CliConfig>): void {\n const webUrl = config.get(\"webUrl\");\n const known = knownPlatformUrlForWebUrl(webUrl);\n if (!known) {\n return;\n }\n\n const stored = config.get(\"platformUrl\");\n if (stored !== known) {\n config.set(\"platformUrl\", known);\n }\n}\n\nexport function createCliConfig(cwd = getCliConfigDir()): Conf<CliConfig> {\n const config = new Conf<CliConfig>({\n projectName: \"keystroke\",\n cwd,\n schema: {\n webUrl: {\n type: \"string\",\n default: DEFAULT_WEB_URL,\n },\n platformUrl: {\n type: \"string\",\n default: DEFAULT_PLATFORM_URL,\n },\n activeOrganizationId: {\n type: \"string\",\n },\n activeProjectId: {\n type: \"string\",\n },\n apiTarget: {\n type: \"string\",\n enum: [\"local\", \"platform\"],\n },\n },\n });\n\n syncPlatformUrlWithWebUrl(config);\n return config;\n}\n\nexport function getWebUrl(config: Conf<CliConfig>): string {\n return config.get(\"webUrl\");\n}\n\nexport function getPlatformUrl(config: Conf<CliConfig>): string {\n const apiKey = process.env.KEYSTROKE_API_KEY?.trim();\n const fromEnv = process.env.KEYSTROKE_PLATFORM_URL?.trim();\n if (apiKey && fromEnv) {\n return fromEnv.replace(/\\/+$/, \"\");\n }\n return resolvePlatformUrlForWebUrl(getWebUrl(config), {\n fallback: config.get(\"platformUrl\"),\n });\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nfunction isCliPackage(dir: string): boolean {\n const packageJsonPath = join(dir, \"package.json\");\n if (!existsSync(packageJsonPath)) {\n return false;\n }\n\n const pkg = JSON.parse(readFileSync(packageJsonPath, \"utf8\")) as { name?: string };\n return pkg.name === \"@keystrokehq/cli\";\n}\n\nexport function resolveCliRoot(fromModuleUrl: string): string {\n let dir = dirname(fileURLToPath(fromModuleUrl));\n\n while (dir !== dirname(dir)) {\n if (isCliPackage(dir)) {\n return dir;\n }\n\n dir = dirname(dir);\n }\n\n throw new Error(\"Could not resolve keystroke CLI package root\");\n}\n","import { spawnSync } from \"node:child_process\";\n\nexport type PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n\nconst MANAGERS: PackageManager[] = [\"pnpm\", \"npm\", \"yarn\", \"bun\"];\n\nexport function detectPackageManager(): PackageManager {\n const userAgent = process.env.npm_config_user_agent ?? \"\";\n\n for (const manager of MANAGERS) {\n if (userAgent.startsWith(manager)) {\n return manager;\n }\n }\n\n if (process.env.PNPM_HOME) {\n return \"pnpm\";\n }\n\n return \"npm\";\n}\n\nexport function resolvePackageManager(explicit?: string): PackageManager {\n if (!explicit) {\n return detectPackageManager();\n }\n\n const normalized = explicit.trim().toLowerCase();\n if (!MANAGERS.includes(normalized as PackageManager)) {\n throw new Error(`Unsupported package manager \"${explicit}\". Use npm, pnpm, yarn, or bun.`);\n }\n\n return normalized as PackageManager;\n}\n\n/** Resolve GitHub Packages auth for @keystrokehq/* when NODE_AUTH_TOKEN is unset. */\nexport function resolveGithubPackagesToken(): string | undefined {\n if (process.env.NODE_AUTH_TOKEN) {\n return process.env.NODE_AUTH_TOKEN;\n }\n\n for (const key of [\"GITHUB_TOKEN\", \"GH_TOKEN\"] as const) {\n const value = process.env[key];\n if (value) {\n return value;\n }\n }\n\n const gh = spawnSync(\"gh\", [\"auth\", \"token\"], { encoding: \"utf8\" });\n if (gh.status === 0) {\n const token = gh.stdout.trim();\n if (token) {\n return token;\n }\n }\n\n return undefined;\n}\n\nexport function installDependencies(cwd: string, manager: PackageManager): void {\n const token = resolveGithubPackagesToken();\n const env = token ? { ...process.env, NODE_AUTH_TOKEN: token } : process.env;\n\n const result = spawnSync(manager, [\"install\"], {\n cwd,\n stdio: \"inherit\",\n env,\n });\n\n if (result.status !== 0) {\n throw new Error(`${manager} install failed`);\n }\n}\n\nexport function installPlaygroundDependencies(cwd: string): void {\n const result = spawnSync(\"pnpm\", [\"install\", \"--ignore-workspace\"], {\n cwd,\n stdio: \"inherit\",\n env: process.env,\n });\n\n if (result.status !== 0) {\n throw new Error(\"pnpm install failed\");\n }\n}\n\n/**\n * Build every workspace package so the playground's `link:` deps resolve to\n * compiled `dist/`. Without this, `keystroke dev` fails with\n * `ERR_MODULE_NOT_FOUND` against whichever package was never built. Turbo\n * caches, so this is only slow on the first run.\n */\nexport function buildPlaygroundWorkspace(monorepoRoot: string): void {\n // Build with a clean Node env: the `keystroke-dev` wrapper sets\n // NODE_OPTIONS=--conditions=development, which would make tsdown/turbo resolve\n // their own deps to src/ and fail. Dropping NODE_OPTIONS builds to dist/ normally.\n const { NODE_OPTIONS: _drop, ...buildEnv } = process.env;\n const result = spawnSync(\"pnpm\", [\"run\", \"build\"], {\n cwd: monorepoRoot,\n stdio: \"inherit\",\n env: buildEnv,\n });\n\n if (result.status !== 0) {\n throw new Error(\"workspace build failed (pnpm run build)\");\n }\n}\n","import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { resolveCliRoot } from \"./project/resolve-cli-root\";\n\nexport function readCliVersion(): string {\n const packageJsonPath = join(resolveCliRoot(import.meta.url), \"package.json\");\n const pkg = JSON.parse(readFileSync(packageJsonPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0.0.0\";\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"version-CJd1mEoq.mjs","names":[],"sources":["../src/resolve-platform-url.ts","../src/config.ts","../src/project/resolve-cli-root.ts","../src/init/package-manager.ts","../src/version.ts"],"sourcesContent":["import { LOCAL_PLATFORM_ORIGIN, originFromPublicUrl } from \"@keystrokehq/shared\";\n\nexport const DEFAULT_WEB_URL = \"https://keystroke.ai\";\nexport const DEFAULT_PLATFORM_URL = \"https://api.keystroke.ai\";\nexport const LOCAL_WEB_URL = \"http://localhost:3000\";\n\nexport type ResolvePlatformUrlOptions = {\n platformUrl?: string;\n /** Used when webUrl does not match a known deployment. */\n fallback?: string;\n};\n\nfunction webOriginFromUrl(webUrl: string): string {\n return originFromPublicUrl(webUrl, webUrl.replace(/\\/+$/, \"\"));\n}\n\n/** Known web origins with a fixed platform API — undefined for custom/staging URLs. */\nexport function knownPlatformUrlForWebUrl(webUrl: string): string | undefined {\n const webOrigin = webOriginFromUrl(webUrl);\n\n if (webOrigin === LOCAL_WEB_URL || webOrigin === \"http://127.0.0.1:3000\") {\n return LOCAL_PLATFORM_ORIGIN;\n }\n\n if (webOrigin === DEFAULT_WEB_URL) {\n return DEFAULT_PLATFORM_URL;\n }\n\n return undefined;\n}\n\nexport function resolvePlatformUrlForWebUrl(\n webUrl: string,\n options: ResolvePlatformUrlOptions = {},\n): string {\n const explicit = options.platformUrl?.trim();\n if (explicit) {\n return explicit.replace(/\\/+$/, \"\");\n }\n\n const known = knownPlatformUrlForWebUrl(webUrl);\n if (known) {\n return known;\n }\n\n const fallback = options.fallback?.trim() || DEFAULT_PLATFORM_URL;\n return fallback.replace(/\\/+$/, \"\");\n}\n","import Conf from \"conf\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport {\n DEFAULT_PLATFORM_URL,\n DEFAULT_WEB_URL,\n knownPlatformUrlForWebUrl,\n resolvePlatformUrlForWebUrl,\n} from \"./resolve-platform-url\";\n\nexport type ApiTargetMode = \"local\" | \"platform\";\n\nexport type CliConfig = {\n webUrl: string;\n platformUrl: string;\n activeOrganizationId?: string;\n activeProjectId?: string;\n apiTarget?: ApiTargetMode;\n};\n\nexport function getCliConfigDir(cwd = join(homedir(), \".keystroke\")): string {\n return cwd;\n}\n\nexport function getConfigDir(config: Conf<CliConfig>): string {\n return dirname(config.path);\n}\n\nexport function getEffectiveApiTarget(config: Conf<CliConfig>): ApiTargetMode {\n const explicit = config.get(\"apiTarget\");\n if (explicit === \"local\" || explicit === \"platform\") {\n return explicit;\n }\n\n if (process.env.KEYSTROKE_API_KEY?.trim()) {\n return \"platform\";\n }\n\n return \"local\";\n}\n\nfunction syncPlatformUrlWithWebUrl(config: Conf<CliConfig>): void {\n const webUrl = config.get(\"webUrl\");\n const known = knownPlatformUrlForWebUrl(webUrl);\n if (!known) {\n return;\n }\n\n const stored = config.get(\"platformUrl\");\n if (stored !== known) {\n config.set(\"platformUrl\", known);\n }\n}\n\nexport function createCliConfig(cwd = getCliConfigDir()): Conf<CliConfig> {\n const config = new Conf<CliConfig>({\n projectName: \"keystroke\",\n cwd,\n schema: {\n webUrl: {\n type: \"string\",\n default: DEFAULT_WEB_URL,\n },\n platformUrl: {\n type: \"string\",\n default: DEFAULT_PLATFORM_URL,\n },\n activeOrganizationId: {\n type: \"string\",\n },\n activeProjectId: {\n type: \"string\",\n },\n apiTarget: {\n type: \"string\",\n enum: [\"local\", \"platform\"],\n },\n },\n });\n\n syncPlatformUrlWithWebUrl(config);\n return config;\n}\n\nexport function getWebUrl(config: Conf<CliConfig>): string {\n return config.get(\"webUrl\");\n}\n\nexport function getPlatformUrl(config: Conf<CliConfig>): string {\n const apiKey = process.env.KEYSTROKE_API_KEY?.trim();\n const fromEnv = process.env.KEYSTROKE_PLATFORM_URL?.trim();\n if (apiKey && fromEnv) {\n return fromEnv.replace(/\\/+$/, \"\");\n }\n return resolvePlatformUrlForWebUrl(getWebUrl(config), {\n fallback: config.get(\"platformUrl\"),\n });\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nfunction isCliPackage(dir: string): boolean {\n const packageJsonPath = join(dir, \"package.json\");\n if (!existsSync(packageJsonPath)) {\n return false;\n }\n\n const pkg = JSON.parse(readFileSync(packageJsonPath, \"utf8\")) as { name?: string };\n return pkg.name === \"@keystrokehq/cli\";\n}\n\nexport function resolveCliRoot(fromModuleUrl: string): string {\n let dir = dirname(fileURLToPath(fromModuleUrl));\n\n while (dir !== dirname(dir)) {\n if (isCliPackage(dir)) {\n return dir;\n }\n\n dir = dirname(dir);\n }\n\n throw new Error(\"Could not resolve keystroke CLI package root\");\n}\n","import { spawnSync } from \"node:child_process\";\n\nexport type PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n\nconst MANAGERS: PackageManager[] = [\"pnpm\", \"npm\", \"yarn\", \"bun\"];\n\nexport function detectPackageManager(): PackageManager {\n const userAgent = process.env.npm_config_user_agent ?? \"\";\n\n for (const manager of MANAGERS) {\n if (userAgent.startsWith(manager)) {\n return manager;\n }\n }\n\n if (process.env.PNPM_HOME) {\n return \"pnpm\";\n }\n\n return \"npm\";\n}\n\nexport function resolvePackageManager(explicit?: string): PackageManager {\n if (!explicit) {\n return detectPackageManager();\n }\n\n const normalized = explicit.trim().toLowerCase();\n if (!MANAGERS.includes(normalized as PackageManager)) {\n throw new Error(`Unsupported package manager \"${explicit}\". Use npm, pnpm, yarn, or bun.`);\n }\n\n return normalized as PackageManager;\n}\n\n/** Resolve GitHub Packages auth for @keystrokehq/* when NODE_AUTH_TOKEN is unset. */\nexport function resolveGithubPackagesToken(): string | undefined {\n if (process.env.NODE_AUTH_TOKEN) {\n return process.env.NODE_AUTH_TOKEN;\n }\n\n for (const key of [\"GITHUB_TOKEN\", \"GH_TOKEN\"] as const) {\n const value = process.env[key];\n if (value) {\n return value;\n }\n }\n\n const gh = spawnSync(\"gh\", [\"auth\", \"token\"], { encoding: \"utf8\" });\n if (gh.status === 0) {\n const token = gh.stdout.trim();\n if (token) {\n return token;\n }\n }\n\n return undefined;\n}\n\nexport function installDependencies(cwd: string, manager: PackageManager): void {\n const token = resolveGithubPackagesToken();\n const env = token ? { ...process.env, NODE_AUTH_TOKEN: token } : process.env;\n\n const result = spawnSync(manager, [\"install\"], {\n cwd,\n stdio: \"inherit\",\n env,\n });\n\n if (result.status !== 0) {\n throw new Error(`${manager} install failed`);\n }\n}\n\nexport function installPlaygroundDependencies(cwd: string): void {\n const result = spawnSync(\"pnpm\", [\"install\", \"--ignore-workspace\"], {\n cwd,\n stdio: \"inherit\",\n env: process.env,\n });\n\n if (result.status !== 0) {\n throw new Error(\"pnpm install failed\");\n }\n}\n\n/**\n * Build every workspace package so the playground's `link:` deps resolve to\n * compiled `dist/`. Without this, `keystroke dev` fails with\n * `ERR_MODULE_NOT_FOUND` against whichever package was never built. Turbo\n * caches, so this is only slow on the first run.\n */\nexport function buildPlaygroundWorkspace(monorepoRoot: string): void {\n // Build with a clean Node env: the `keystroke-dev` wrapper sets\n // NODE_OPTIONS=--conditions=development, which would make tsdown/turbo resolve\n // their own deps to src/ and fail. Dropping NODE_OPTIONS builds to dist/ normally.\n const { NODE_OPTIONS: _drop, ...buildEnv } = process.env;\n const result = spawnSync(\"pnpm\", [\"run\", \"build\"], {\n cwd: monorepoRoot,\n stdio: \"inherit\",\n env: buildEnv,\n });\n\n if (result.status !== 0) {\n throw new Error(\"workspace build failed (pnpm run build)\");\n }\n}\n","import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { resolveCliRoot } from \"./project/resolve-cli-root\";\n\nexport function readCliVersion(): string {\n const packageJsonPath = join(resolveCliRoot(import.meta.url), \"package.json\");\n const pkg = JSON.parse(readFileSync(packageJsonPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0.0.0\";\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keystrokehq/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/keystrokehq/keystroke.git",
@@ -40,9 +40,9 @@
40
40
  "tsx": "^4.22.3",
41
41
  "typescript": "^6.0.3",
42
42
  "vitest": "^4.1.7",
43
+ "@keystrokehq/tsdown-config": "0.0.3",
43
44
  "@keystrokehq/oxlint-config": "0.0.4",
44
45
  "@keystrokehq/tsconfig": "0.0.3",
45
- "@keystrokehq/tsdown-config": "0.0.3",
46
46
  "@keystrokehq/vitest-config": "0.0.5"
47
47
  },
48
48
  "scripts": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"dist-B4pkCJsM.mjs","names":[],"sources":["../../../packages/storage/dist/pack-artifact-DVnIKrsg.mjs","../../../packages/storage/dist/index.mjs"],"sourcesContent":["import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { tmpdir } from \"node:os\";\nimport { ROUTE_MANIFEST_REL_PATH, parseStoredRouteManifest } from \"@keystrokehq/shared\";\n//#region src/pack-dir.ts\n/**\n* Pack a directory tree that contains a `dist/` folder into a gzip tarball\n* suitable for project-server extraction.\n*/\nfunction packDistTree(rootContainingDist) {\n\tconst tempDir = mkdtempSync(join(tmpdir(), \"keystroke-artifact-pack-\"));\n\tconst archivePath = join(tempDir, \"artifact.tgz\");\n\ttry {\n\t\tconst result = spawnSync(\"tar\", [\n\t\t\t\"-czf\",\n\t\t\tarchivePath,\n\t\t\t\"--exclude=._*\",\n\t\t\t\"--exclude=.DS_Store\",\n\t\t\t\"-C\",\n\t\t\trootContainingDist,\n\t\t\t\"dist\"\n\t\t], {\n\t\t\tencoding: \"utf8\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tCOPYFILE_DISABLE: \"1\"\n\t\t\t}\n\t\t});\n\t\tif (result.status !== 0) throw new Error(result.stderr?.trim() || \"Failed to pack project artifact\");\n\t\treturn readFileSync(archivePath);\n\t} finally {\n\t\trmSync(tempDir, {\n\t\t\trecursive: true,\n\t\t\tforce: true\n\t\t});\n\t}\n}\n//#endregion\n//#region src/extract-artifact.ts\n/** Extract a packed project artifact tarball into `destDir` (creates `destDir/dist/`). */\nfunction extractProjectArtifact(archive, destDir) {\n\tconst tempDir = mkdtempSync(join(tmpdir(), \"keystroke-artifact-extract-\"));\n\tconst archivePath = join(tempDir, \"artifact.tgz\");\n\ttry {\n\t\twriteFileSync(archivePath, archive);\n\t\tconst result = spawnSync(\"tar\", [\n\t\t\t\"-xzf\",\n\t\t\tarchivePath,\n\t\t\t\"-C\",\n\t\t\tdestDir\n\t\t], {\n\t\t\tencoding: \"utf8\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tCOPYFILE_DISABLE: \"1\"\n\t\t\t}\n\t\t});\n\t\tif (result.status !== 0) throw new Error(result.stderr?.trim() || \"Failed to extract project artifact\");\n\t} finally {\n\t\trmSync(tempDir, {\n\t\t\trecursive: true,\n\t\t\tforce: true\n\t\t});\n\t}\n}\n//#endregion\n//#region src/merge-route-manifest.ts\nfunction moduleFileOf(entry) {\n\treturn \"moduleFile\" in entry && typeof entry.moduleFile === \"string\" ? entry.moduleFile : void 0;\n}\n/** Replace manifest rows for rebuilt modules while keeping untouched routes and metadata. */\nfunction mergeStoredRouteManifest(base, rebuiltEntries) {\n\tconst rebuiltModuleFiles = new Set(rebuiltEntries.map(moduleFileOf).filter((value) => Boolean(value)));\n\tconst keptEntries = base.entries.filter((entry) => {\n\t\tconst moduleFile = moduleFileOf(entry);\n\t\tif (!moduleFile) return true;\n\t\treturn !rebuiltModuleFiles.has(moduleFile);\n\t});\n\tconst filteredRebuilt = rebuiltEntries.filter((entry) => entry.kind !== \"health\");\n\treturn {\n\t\t...base,\n\t\tentries: [...keptEntries, ...filteredRebuilt]\n\t};\n}\n//#endregion\n//#region src/merge-filtered-artifact.ts\nasync function mergeFilteredArtifact(input) {\n\tconst mergeRoot = mkdtempSync(join(tmpdir(), \"keystroke-artifact-merge-\"));\n\ttry {\n\t\textractProjectArtifact(input.baseArchive, mergeRoot);\n\t\tconst manifestPath = join(mergeRoot, ROUTE_MANIFEST_REL_PATH);\n\t\tconst mergedManifest = mergeStoredRouteManifest(parseStoredRouteManifest(JSON.parse(readFileSync(manifestPath, \"utf8\"))), input.filtered.manifestEntries);\n\t\twriteFileSync(manifestPath, `${JSON.stringify(mergedManifest, null, 2)}\\n`);\n\t\tfor (const file of input.filtered.files) {\n\t\t\tconst destination = join(mergeRoot, \"dist\", file.relativePath);\n\t\t\tmkdirSync(dirname(destination), { recursive: true });\n\t\t\twriteFileSync(destination, file.contents);\n\t\t\tif (file.sourceMap) writeFileSync(`${destination}.map`, file.sourceMap);\n\t\t}\n\t\treturn packDistTree(mergeRoot);\n\t} finally {\n\t\trmSync(mergeRoot, {\n\t\t\trecursive: true,\n\t\t\tforce: true\n\t\t});\n\t}\n}\n//#endregion\n//#region src/pack-artifact.ts\n/** Pack `dist/` into a gzip tarball suitable for `/app` extraction in the project server image. */\nfunction packProjectArtifact(projectRoot) {\n\tif (!existsSync(join(projectRoot, \"dist\"))) throw new Error(\"dist/ not found — run keystroke build first\");\n\treturn packDistTree(projectRoot);\n}\n//#endregion\nexport { packDistTree as a, extractProjectArtifact as i, mergeFilteredArtifact as n, mergeStoredRouteManifest as r, packProjectArtifact as t };\n\n//# sourceMappingURL=pack-artifact-DVnIKrsg.mjs.map","import { a as packDistTree, i as extractProjectArtifact, n as mergeFilteredArtifact, r as mergeStoredRouteManifest, t as packProjectArtifact } from \"./pack-artifact-DVnIKrsg.mjs\";\nimport { CreateBucketCommand, DeleteBucketCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutBucketAclCommand, PutBucketPolicyCommand, PutObjectCommand, S3Client } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { tmpdir } from \"node:os\";\nimport { ROUTE_MANIFEST_REL_PATH } from \"@keystrokehq/shared\";\nimport { createHash } from \"node:crypto\";\n//#region src/config.ts\nfunction readEnv$1(env, ...keys) {\n\tfor (const key of keys) {\n\t\tconst value = env[key]?.trim();\n\t\tif (value) return value;\n\t}\n}\nfunction requireEnv(env, keys) {\n\tconst candidates = Array.isArray(keys) ? keys : [keys];\n\tconst value = readEnv$1(env, ...candidates);\n\tif (!value) throw new Error(`${candidates[0]} is required`);\n\treturn value;\n}\n/** Admin S3 credentials for storage plugins (bucket is per-org, not from env). */\nfunction storageAdminConfigFromEnv(env = process.env) {\n\tconst accessKeyId = requireEnv(env, [\"STORAGE_ACCESS_KEY_ID\", \"AWS_ACCESS_KEY_ID\"]);\n\tconst secretAccessKey = requireEnv(env, [\"STORAGE_SECRET_ACCESS_KEY\", \"AWS_SECRET_ACCESS_KEY\"]);\n\tconst endpoint = requireEnv(env, [\n\t\t\"STORAGE_ENDPOINT\",\n\t\t\"AWS_ENDPOINT_URL_S3\",\n\t\t\"AWS_S3_ENDPOINT\"\n\t]);\n\treturn {\n\t\tregion: readEnv$1(env, \"STORAGE_REGION\", \"AWS_REGION\") ?? \"us-east-1\",\n\t\taccessKeyId,\n\t\tsecretAccessKey,\n\t\tendpoint,\n\t\tforcePathStyle: env.STORAGE_FORCE_PATH_STYLE === void 0 ? true : env.STORAGE_FORCE_PATH_STYLE === \"true\"\n\t};\n}\n/** Full client config including bucket — for integration tests and explicit bucket callers. */\nfunction storageConfigFromEnv(env = process.env) {\n\tconst bucket = requireEnv(env, [\"STORAGE_BUCKET\", \"BUCKET_NAME\"]);\n\treturn {\n\t\t...storageAdminConfigFromEnv(env),\n\t\tbucket\n\t};\n}\n/** Endpoint for presigned URLs fetched inside project-server containers (e.g. `http://minio:9000`). */\nfunction containerPresignEndpointFromEnv(hostEndpoint, env = process.env) {\n\tconst override = readEnv$1(env, \"STORAGE_CONTAINER_ENDPOINT\");\n\tif (override) return override;\n\ttry {\n\t\tconst url = new URL(hostEndpoint);\n\t\tif (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") {\n\t\t\tconst port = url.port || (url.protocol === \"https:\" ? \"443\" : \"9000\");\n\t\t\treturn `${url.protocol}//minio:${port}`;\n\t\t}\n\t} catch {}\n\treturn hostEndpoint;\n}\n//#endregion\n//#region src/keys.ts\n/** Object key for a built project artifact tarball. */\nfunction projectArtifactKey(projectId, version) {\n\treturn `projects/${projectId}/artifacts/${version}.tgz`;\n}\n/**\n* Per-artifact source manifest (the deploy's file tree: path -> id + content\n* hash, plus resource refs). One per deploy.\n*/\nfunction projectSourceManifestKey(projectId, artifactId) {\n\treturn `projects/${projectId}/sources/${artifactId}/index.json`;\n}\nconst SHA256_HEX = /^[a-f0-9]{64}$/;\n/** True when `hash` is a lowercase hex sha256 digest (64 chars). */\nfunction isSourceBlobHash(hash) {\n\treturn SHA256_HEX.test(hash);\n}\n/**\n* Content-addressed key for a single source file's contents, deduped across all\n* of a project's deploys. `hash` is a lowercase hex sha256 of the file bytes.\n*/\nfunction projectSourceBlobKey(projectId, hash) {\n\tif (!isSourceBlobHash(hash)) throw new Error(\"Invalid source blob hash\");\n\treturn `projects/${projectId}/sources/blobs/${hash}`;\n}\nconst AVATAR_EXTENSION = /^[a-z0-9]+$/;\n/** Object key for a user's custom profile avatar in the shared keystroke-users bucket. */\nfunction userAvatarStorageKey(userId, extension) {\n\tconst ext = extension.replace(/^\\./, \"\").toLowerCase();\n\tif (!AVATAR_EXTENSION.test(ext)) throw new Error(\"Invalid avatar extension\");\n\treturn `avatars/${userId}/avatar.${ext}`;\n}\n/** True when `key` is the canonical avatar object for `userId`. */\nfunction isUserAvatarStorageKey(key, userId) {\n\tconst prefix = `avatars/${userId}/avatar.`;\n\tif (!key.startsWith(prefix)) return false;\n\tconst ext = key.slice(prefix.length);\n\treturn AVATAR_EXTENSION.test(ext);\n}\n/** Object key for an org logo in the shared keystroke-assets bucket. */\nfunction orgLogoStorageKey(organizationId, variant, extension) {\n\tconst ext = extension.replace(/^\\./, \"\").toLowerCase();\n\tif (!AVATAR_EXTENSION.test(ext)) throw new Error(\"Invalid org logo extension\");\n\treturn `org-logos/${organizationId}/logo-${variant}.${ext}`;\n}\n/** True when `key` is the canonical logo object for `organizationId` and `variant`. */\nfunction isOrgLogoStorageKey(key, organizationId, variant) {\n\tconst prefix = `org-logos/${organizationId}/logo-${variant}.`;\n\tif (!key.startsWith(prefix)) return false;\n\tconst ext = key.slice(prefix.length);\n\treturn AVATAR_EXTENSION.test(ext);\n}\n//#endregion\n//#region src/map-in-batches.ts\n/** Default concurrency for parallel blob downloads and file writes. */\nconst DEFAULT_PARALLEL_BATCH_SIZE = 8;\n/** Run `fn` over `items` in parallel batches of `batchSize`. */\nasync function mapInParallelBatches(items, batchSize, fn) {\n\tif (batchSize < 1) throw new Error(\"batchSize must be at least 1\");\n\tconst results = [];\n\tfor (let index = 0; index < items.length; index += batchSize) {\n\t\tconst batch = items.slice(index, index + batchSize);\n\t\tresults.push(...await Promise.all(batch.map(fn)));\n\t}\n\treturn results;\n}\n//#endregion\n//#region src/load-project-source-files.ts\n/** Matches deploy-time source snapshot limits in @keystrokehq/build walk-project. */\nconst MAX_ACTIVE_SOURCE_DOWNLOAD_BYTES = 8 * 1024 * 1024;\nasync function loadProjectSourceFiles(storage, projectId, manifest, options = {}) {\n\tconst maxTotalBytes = options.maxTotalBytes ?? 8388608;\n\tconst files = [];\n\tlet totalBytes = 0;\n\tfor (let index = 0; index < manifest.files.length; index += 8) {\n\t\tconst batch = manifest.files.slice(index, index + 8);\n\t\tconst batchResults = await Promise.all(batch.map(async (file) => {\n\t\t\tconst bytes = await storage.getObject(projectSourceBlobKey(projectId, file.hash));\n\t\t\tconst contents = new TextDecoder().decode(bytes);\n\t\t\treturn {\n\t\t\t\tpath: file.path,\n\t\t\t\tcontents,\n\t\t\t\tbyteLength: Buffer.byteLength(contents, \"utf8\")\n\t\t\t};\n\t\t}));\n\t\tfor (const result of batchResults) {\n\t\t\ttotalBytes += result.byteLength;\n\t\t\tif (totalBytes > maxTotalBytes) throw new Error(\"Active source snapshot exceeds download size limit\");\n\t\t\tfiles.push({\n\t\t\t\tpath: result.path,\n\t\t\t\tcontents: result.contents\n\t\t\t});\n\t\t}\n\t}\n\treturn files;\n}\n//#endregion\n//#region src/create-storage.ts\nconst DEFAULT_PRESIGN_TTL_SECONDS = 900;\nfunction createStorage(config) {\n\tconst client = new S3Client(toClientConfig(config));\n\treturn {\n\t\tasync putObject(input) {\n\t\t\tawait client.send(new PutObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: input.key,\n\t\t\t\tBody: input.body,\n\t\t\t\tContentType: input.contentType\n\t\t\t}));\n\t\t},\n\t\tasync getObject(key) {\n\t\t\tconst response = await client.send(new GetObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: key\n\t\t\t}));\n\t\t\tif (!response.Body) throw new Error(`Object body missing for key ${key}`);\n\t\t\treturn response.Body.transformToByteArray();\n\t\t},\n\t\tasync headObject(key) {\n\t\t\ttry {\n\t\t\t\treturn { contentLength: (await client.send(new HeadObjectCommand({\n\t\t\t\t\tBucket: config.bucket,\n\t\t\t\t\tKey: key\n\t\t\t\t}))).ContentLength ?? 0 };\n\t\t\t} catch (error) {\n\t\t\t\tif (isNotFoundError(error)) return;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t\tasync presignGet(input) {\n\t\t\treturn getSignedUrl(client, new GetObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: input.key\n\t\t\t}), { expiresIn: input.expiresInSeconds ?? DEFAULT_PRESIGN_TTL_SECONDS });\n\t\t},\n\t\tasync presignPut(input) {\n\t\t\treturn getSignedUrl(client, new PutObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: input.key,\n\t\t\t\tContentType: input.contentType ?? \"application/gzip\"\n\t\t\t}), { expiresIn: input.expiresInSeconds ?? DEFAULT_PRESIGN_TTL_SECONDS });\n\t\t},\n\t\tasync deleteObject(key) {\n\t\t\tawait client.send(new DeleteObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: key\n\t\t\t}));\n\t\t},\n\t\tasync listObjects(prefix) {\n\t\t\tconst keys = [];\n\t\t\tlet continuationToken;\n\t\t\tdo {\n\t\t\t\tconst response = await client.send(new ListObjectsV2Command({\n\t\t\t\t\tBucket: config.bucket,\n\t\t\t\t\tPrefix: prefix,\n\t\t\t\t\tContinuationToken: continuationToken\n\t\t\t\t}));\n\t\t\t\tfor (const object of response.Contents ?? []) if (object.Key) keys.push(object.Key);\n\t\t\t\tcontinuationToken = response.IsTruncated ? response.NextContinuationToken : void 0;\n\t\t\t} while (continuationToken);\n\t\t\treturn keys;\n\t\t}\n\t};\n}\n/** Presign URLs for project-server containers using `STORAGE_CONTAINER_ENDPOINT` when set. */\nfunction createContainerPresignStorage(config, env = process.env) {\n\treturn createStorage({\n\t\t...config,\n\t\tendpoint: containerPresignEndpointFromEnv(config.endpoint, env)\n\t});\n}\nfunction toClientConfig(config) {\n\treturn {\n\t\tregion: config.region,\n\t\tcredentials: {\n\t\t\taccessKeyId: config.accessKeyId,\n\t\t\tsecretAccessKey: config.secretAccessKey\n\t\t},\n\t\tendpoint: config.endpoint,\n\t\tforcePathStyle: config.forcePathStyle\n\t};\n}\nfunction isNotFoundError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst name = \"name\" in error ? String(error.name) : \"\";\n\tconst status = \"$metadata\" in error ? error.$metadata : void 0;\n\treturn name === \"NotFound\" || name === \"NoSuchKey\" || status?.httpStatusCode === 404;\n}\n//#endregion\n//#region src/extract-route-manifest.ts\nvar RouteManifestNotFoundError = class extends Error {\n\tconstructor(message = `Route manifest missing in artifact (${ROUTE_MANIFEST_REL_PATH})`) {\n\t\tsuper(message);\n\t\tthis.name = \"RouteManifestNotFoundError\";\n\t}\n};\n/** Read `.keystroke/route-manifest.json` from a packed project artifact tarball. */\nfunction extractRouteManifestFromArtifact(archive) {\n\tconst tempDir = mkdtempSync(join(tmpdir(), \"keystroke-artifact-\"));\n\tconst archivePath = join(tempDir, \"artifact.tgz\");\n\ttry {\n\t\twriteFileSync(archivePath, archive);\n\t\tif (spawnSync(\"tar\", [\n\t\t\t\"-xzf\",\n\t\t\tarchivePath,\n\t\t\t\"-C\",\n\t\t\ttempDir,\n\t\t\tROUTE_MANIFEST_REL_PATH\n\t\t], { encoding: \"utf8\" }).status !== 0) throw new RouteManifestNotFoundError();\n\t\tconst raw = readFileSync(join(tempDir, ROUTE_MANIFEST_REL_PATH), \"utf8\");\n\t\treturn JSON.parse(raw);\n\t} catch (error) {\n\t\tif (error instanceof RouteManifestNotFoundError) throw error;\n\t\tthrow new RouteManifestNotFoundError(error instanceof Error ? error.message : \"Failed to extract route manifest\");\n\t} finally {\n\t\trmSync(tempDir, {\n\t\t\trecursive: true,\n\t\t\tforce: true\n\t\t});\n\t}\n}\n//#endregion\n//#region src/s3-errors.ts\nfunction isBucketAlreadyExistsError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst name = \"name\" in error ? String(error.name) : \"\";\n\tconst code = \"Code\" in error ? String(error.Code) : \"\";\n\treturn name === \"BucketAlreadyOwnedByYou\" || code === \"BucketAlreadyOwnedByYou\" || name === \"BucketAlreadyExists\" || code === \"BucketAlreadyExists\";\n}\nfunction isStorageNotFoundError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst record = error;\n\tconst name = typeof record.name === \"string\" ? record.name : \"\";\n\tconst code = typeof record.Code === \"string\" ? record.Code : \"\";\n\tconst status = typeof record.$metadata === \"object\" && record.$metadata !== null ? record.$metadata.httpStatusCode : void 0;\n\treturn name === \"NoSuchKey\" || code === \"NoSuchKey\" || code === \"NotFound\" || status === 404;\n}\nfunction isStorageBucketNotFoundError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst record = error;\n\tconst name = typeof record.name === \"string\" ? record.name : \"\";\n\tconst code = typeof record.Code === \"string\" ? record.Code : \"\";\n\tconst status = typeof record.$metadata === \"object\" && record.$metadata !== null ? record.$metadata.httpStatusCode : void 0;\n\treturn name === \"NoSuchBucket\" || code === \"NoSuchBucket\" || name === \"NotFound\" || status === 404;\n}\n//#endregion\n//#region src/asset-storage.ts\n/** Shared bucket for cross-org public assets (avatars, org sidebar logos). */\nconst KEYSTROKE_ASSETS_BUCKET = \"keystroke-assets\";\n/** Env var for the browser-facing origin of public asset objects (no trailing slash). */\nconst KEYSTROKE_ASSETS_PUBLIC_URL_BASE_ENV = \"KEYSTROKE_ASSETS_PUBLIC_URL_BASE\";\nconst KEYSTROKE_ASSETS_AVATAR_PREFIX = \"avatars/\";\nconst KEYSTROKE_ASSETS_ORG_LOGO_PREFIX = \"org-logos/\";\nfunction readEnv(env, key) {\n\treturn env[key]?.trim() || void 0;\n}\n/** Default path-style base when `KEYSTROKE_ASSETS_PUBLIC_URL_BASE` is unset (local MinIO). */\nfunction defaultKeystrokeAssetsPublicUrlBase(env = process.env) {\n\treturn `${storageAdminConfigFromEnv(env).endpoint.replace(/\\/+$/, \"\")}/${KEYSTROKE_ASSETS_BUCKET}`;\n}\n/** Public URL prefix for asset objects — configured base or local path-style default. */\nfunction keystrokeAssetsPublicUrlBase(env = process.env) {\n\tconst configured = readEnv(env, KEYSTROKE_ASSETS_PUBLIC_URL_BASE_ENV);\n\tif (configured) return configured.replace(/\\/+$/, \"\");\n\treturn defaultKeystrokeAssetsPublicUrlBase(env);\n}\nfunction keystrokeAssetsPublicUrlPrefixes(env = process.env) {\n\tconst prefixes = [keystrokeAssetsPublicUrlBase(env)];\n\tif (readEnv(env, \"KEYSTROKE_ASSETS_PUBLIC_URL_BASE\")) prefixes.push(defaultKeystrokeAssetsPublicUrlBase(env));\n\treturn [...new Set(prefixes)];\n}\nfunction createAssetsStorageClient(env = process.env) {\n\treturn createStorage({\n\t\t...storageAdminConfigFromEnv(env),\n\t\tbucket: KEYSTROKE_ASSETS_BUCKET\n\t});\n}\n/** Stable public URL for an object in the keystroke-assets bucket. */\nfunction publicKeystrokeAssetsObjectUrl(key, env = process.env) {\n\treturn `${keystrokeAssetsPublicUrlBase(env)}/${key}`;\n}\n/** Extract the object key from a public keystroke-assets URL when it matches `requiredPrefix`. */\nfunction keystrokeAssetsKeyFromPublicUrl(url, requiredPrefix, env = process.env) {\n\tfor (const prefix of keystrokeAssetsPublicUrlPrefixes(env)) {\n\t\tconst normalizedPrefix = `${prefix.replace(/\\/+$/, \"\")}/`;\n\t\tif (!url.startsWith(normalizedPrefix)) continue;\n\t\tconst key = decodeURIComponent(url.slice(normalizedPrefix.length).split(\"?\")[0] ?? \"\");\n\t\tif (key.startsWith(requiredPrefix)) return key;\n\t}\n\treturn null;\n}\nfunction isKeystrokeAssetsAvatarPublicUrl(url, env = process.env) {\n\treturn keystrokeAssetsKeyFromPublicUrl(url, KEYSTROKE_ASSETS_AVATAR_PREFIX, env) !== null;\n}\nfunction keystrokeAssetsAvatarKeyFromPublicUrl(url, env = process.env) {\n\treturn keystrokeAssetsKeyFromPublicUrl(url, KEYSTROKE_ASSETS_AVATAR_PREFIX, env);\n}\nfunction isKeystrokeAssetsOrgLogoPublicUrl(url, env = process.env) {\n\treturn keystrokeAssetsKeyFromPublicUrl(url, KEYSTROKE_ASSETS_ORG_LOGO_PREFIX, env) !== null;\n}\nfunction keystrokeAssetsOrgLogoKeyFromPublicUrl(url, env = process.env) {\n\treturn keystrokeAssetsKeyFromPublicUrl(url, KEYSTROKE_ASSETS_ORG_LOGO_PREFIX, env);\n}\nasync function ensureKeystrokeAssetsBucket(env = process.env) {\n\tconst adminConfig = storageAdminConfigFromEnv(env);\n\tconst adminClient = new S3Client({\n\t\tregion: adminConfig.region,\n\t\tcredentials: {\n\t\t\taccessKeyId: adminConfig.accessKeyId,\n\t\t\tsecretAccessKey: adminConfig.secretAccessKey\n\t\t},\n\t\tendpoint: adminConfig.endpoint,\n\t\tforcePathStyle: adminConfig.forcePathStyle\n\t});\n\tawait createAssetsBucketIfNeeded(adminClient, KEYSTROKE_ASSETS_BUCKET);\n\tawait applyPublicAssetsBucketAcl(adminClient, KEYSTROKE_ASSETS_BUCKET);\n\tawait applyPublicAssetsBucketPolicy(adminClient, KEYSTROKE_ASSETS_BUCKET);\n}\nasync function createAssetsBucketIfNeeded(adminClient, bucket) {\n\ttry {\n\t\tawait adminClient.send(new CreateBucketCommand({\n\t\t\tBucket: bucket,\n\t\t\tACL: \"public-read\"\n\t\t}));\n\t\treturn;\n\t} catch (error) {\n\t\tif (isBucketAlreadyExistsError(error)) return;\n\t}\n\ttry {\n\t\tawait adminClient.send(new CreateBucketCommand({ Bucket: bucket }));\n\t} catch (error) {\n\t\tif (!isBucketAlreadyExistsError(error)) throw error;\n\t}\n}\nasync function applyPublicAssetsBucketAcl(adminClient, bucket) {\n\ttry {\n\t\tawait adminClient.send(new PutBucketAclCommand({\n\t\t\tBucket: bucket,\n\t\t\tACL: \"public-read\"\n\t\t}));\n\t} catch (error) {\n\t\tif (!isUnsupportedStorageFeatureError(error)) console.warn(\"[storage] PutBucketAcl failed (continuing):\", error);\n\t}\n}\nasync function applyPublicAssetsBucketPolicy(adminClient, bucket) {\n\ttry {\n\t\tawait adminClient.send(new PutBucketPolicyCommand({\n\t\t\tBucket: bucket,\n\t\t\tPolicy: JSON.stringify({\n\t\t\t\tVersion: \"2012-10-17\",\n\t\t\t\tStatement: [{\n\t\t\t\t\tEffect: \"Allow\",\n\t\t\t\t\tPrincipal: \"*\",\n\t\t\t\t\tAction: [\"s3:GetObject\"],\n\t\t\t\t\tResource: [`arn:aws:s3:::${bucket}/${KEYSTROKE_ASSETS_AVATAR_PREFIX}*`, `arn:aws:s3:::${bucket}/${KEYSTROKE_ASSETS_ORG_LOGO_PREFIX}*`]\n\t\t\t\t}]\n\t\t\t})\n\t\t}));\n\t} catch (error) {\n\t\tif (!isUnsupportedStorageFeatureError(error)) console.warn(\"[storage] PutBucketPolicy failed (continuing):\", error);\n\t}\n}\nfunction isUnsupportedStorageFeatureError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst name = \"name\" in error ? String(error.name) : \"\";\n\tconst code = \"Code\" in error ? String(error.Code) : \"\";\n\treturn name === \"NotImplemented\" || code === \"NotImplemented\" || code === \"NotSupported\";\n}\n//#endregion\n//#region src/provision/names.ts\nconst DEFAULT_BUCKET_NAME_PREFIX = \"ks\";\nconst S3_BUCKET_NAME_MAX = 63;\n/** Deterministic bucket segment; keeps short test ids readable, hashes UUID-length ids. */\nfunction bucketIdSegment(id, hashLength = 10) {\n\tif (id.length <= 24 && !/^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(id)) return id;\n\treturn createHash(\"sha256\").update(id).digest(\"hex\").slice(0, hashLength);\n}\nfunction assertBucketName(name) {\n\tif (name.length > S3_BUCKET_NAME_MAX) throw new Error(`bucket name exceeds ${S3_BUCKET_NAME_MAX} chars (${name.length}): ${name}`);\n\treturn name;\n}\nfunction orgStorageBucketName(input, prefix = DEFAULT_BUCKET_NAME_PREFIX) {\n\treturn assertBucketName(`${prefix.trim().replace(/-+$/g, \"\")}-${input.organizationId.toLowerCase()}`);\n}\nfunction orgAgentBucketName(input) {\n\tconst orgBucket = input.orgBucket ?? (input.organizationId ? orgStorageBucketName({ organizationId: input.organizationId }) : void 0);\n\tif (!orgBucket) throw new Error(\"orgAgentBucketName requires organizationId or orgBucket\");\n\treturn assertBucketName(`${orgBucket}-${bucketIdSegment(input.agentId)}`);\n}\n//#endregion\n//#region src/workspace/sync.ts\nconst DEFAULT_UPLOAD_CONCURRENCY = 8;\nfunction walkFiles(root) {\n\tconst files = [];\n\tconst stack = [root];\n\twhile (stack.length > 0) {\n\t\tconst current = stack.pop();\n\t\tfor (const entry of readdirSync(current, { withFileTypes: true })) {\n\t\t\tconst full = join(current, entry.name);\n\t\t\tif (entry.isDirectory()) stack.push(full);\n\t\t\telse if (entry.isFile()) files.push(full);\n\t\t}\n\t}\n\treturn files;\n}\nfunction collectWorkspaceObjects(workspaceRoot) {\n\treturn walkFiles(workspaceRoot).map((file) => ({\n\t\tkey: relative(workspaceRoot, file).split(\"\\\\\").join(\"/\"),\n\t\tbody: readFileSync(file)\n\t}));\n}\n/** Keys present in object storage but absent from the local workspace root. */\nfunction workspaceKeysToDelete(remoteKeys, localKeys) {\n\treturn remoteKeys.filter((key) => !localKeys.has(key));\n}\nfunction s3Client(config) {\n\treturn new S3Client({\n\t\tregion: config.region,\n\t\tendpoint: config.endpoint,\n\t\tforcePathStyle: config.forcePathStyle,\n\t\tcredentials: {\n\t\t\taccessKeyId: config.accessKeyId,\n\t\t\tsecretAccessKey: config.secretAccessKey\n\t\t}\n\t});\n}\n/** Create the bucket named in `config` when missing (idempotent for races). */\nasync function ensureStorageBucket(config) {\n\tconst client = s3Client(config);\n\ttry {\n\t\tawait client.send(new CreateBucketCommand({ Bucket: config.bucket }));\n\t} catch (error) {\n\t\tif (!isBucketAlreadyExistsError(error)) throw error;\n\t}\n}\nasync function uploadWorkspaceObjects(config, objects, concurrency = DEFAULT_UPLOAD_CONCURRENCY) {\n\tif (objects.length === 0) return;\n\tconst client = s3Client(config);\n\tlet index = 0;\n\tasync function worker() {\n\t\twhile (index < objects.length) {\n\t\t\tconst current = objects[index++];\n\t\t\tawait client.send(new PutObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: current.key,\n\t\t\t\tBody: current.body\n\t\t\t}));\n\t\t}\n\t}\n\tconst workers = Array.from({ length: Math.min(concurrency, objects.length) }, () => worker());\n\tawait Promise.all(workers);\n}\nasync function deleteWorkspaceObjects(config, keys, concurrency = DEFAULT_UPLOAD_CONCURRENCY) {\n\tif (keys.length === 0) return;\n\tconst client = s3Client(config);\n\tlet index = 0;\n\tasync function worker() {\n\t\twhile (index < keys.length) {\n\t\t\tconst key = keys[index++];\n\t\t\tawait client.send(new DeleteObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: key\n\t\t\t}));\n\t\t}\n\t}\n\tconst workers = Array.from({ length: Math.min(concurrency, keys.length) }, () => worker());\n\tawait Promise.all(workers);\n}\n/** Mirror the host agent workspace directory to the per-agent bucket (upsert + delete stale keys). */\nasync function syncAgentWorkspaceToBucket(input) {\n\tconst bucket = orgAgentBucketName({\n\t\torgBucket: input.orgBucket,\n\t\tagentId: input.agentId\n\t});\n\tconst config = {\n\t\t...input.admin,\n\t\tbucket\n\t};\n\tawait ensureStorageBucket(config);\n\tconst objects = collectWorkspaceObjects(input.agentRoot);\n\tconst localKeys = new Set(objects.map((object) => object.key));\n\tawait deleteWorkspaceObjects(config, workspaceKeysToDelete(await createStorage(config).listObjects(\"\"), localKeys));\n\tawait uploadWorkspaceObjects(config, objects);\n}\n//#endregion\n//#region src/provision/minio.ts\nfunction minioStoragePlugin(options = {}) {\n\tconst adminConfig = storageAdminConfigFromEnv(options.env ?? process.env);\n\tconst adminClient = new S3Client({\n\t\tregion: adminConfig.region,\n\t\tcredentials: {\n\t\t\taccessKeyId: adminConfig.accessKeyId,\n\t\t\tsecretAccessKey: adminConfig.secretAccessKey\n\t\t},\n\t\tendpoint: adminConfig.endpoint,\n\t\tforcePathStyle: adminConfig.forcePathStyle\n\t});\n\treturn {\n\t\tasync provisionOrganization(input) {\n\t\t\tconst bucket = orgStorageBucketName(input, options.bucketNamePrefix);\n\t\t\ttry {\n\t\t\t\tawait adminClient.send(new CreateBucketCommand({ Bucket: bucket }));\n\t\t\t} catch (error) {\n\t\t\t\tif (!isBucketAlreadyExistsError(error)) throw error;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tbucket,\n\t\t\t\tendpoint: adminConfig.endpoint,\n\t\t\t\taccessKeyId: adminConfig.accessKeyId,\n\t\t\t\tsecretAccessKey: adminConfig.secretAccessKey,\n\t\t\t\tregion: adminConfig.region,\n\t\t\t\tforcePathStyle: adminConfig.forcePathStyle\n\t\t\t};\n\t\t},\n\t\tasync deprovisionOrganization(result) {\n\t\t\tconst client = new S3Client({\n\t\t\t\tregion: result.region ?? adminConfig.region,\n\t\t\t\tcredentials: {\n\t\t\t\t\taccessKeyId: result.accessKeyId,\n\t\t\t\t\tsecretAccessKey: result.secretAccessKey\n\t\t\t\t},\n\t\t\t\tendpoint: result.endpoint,\n\t\t\t\tforcePathStyle: result.forcePathStyle ?? true\n\t\t\t});\n\t\t\tif (((await client.send(new ListObjectsV2Command({\n\t\t\t\tBucket: result.bucket,\n\t\t\t\tMaxKeys: 1\n\t\t\t}))).KeyCount ?? 0) > 0) throw new Error(`Refusing to delete non-empty storage bucket ${result.bucket}`);\n\t\t\tawait client.send(new DeleteBucketCommand({ Bucket: result.bucket }));\n\t\t}\n\t};\n}\n//#endregion\nexport { DEFAULT_PARALLEL_BATCH_SIZE, KEYSTROKE_ASSETS_AVATAR_PREFIX, KEYSTROKE_ASSETS_BUCKET, KEYSTROKE_ASSETS_ORG_LOGO_PREFIX, KEYSTROKE_ASSETS_PUBLIC_URL_BASE_ENV, MAX_ACTIVE_SOURCE_DOWNLOAD_BYTES, RouteManifestNotFoundError, bucketIdSegment, collectWorkspaceObjects, containerPresignEndpointFromEnv, createAssetsStorageClient, createContainerPresignStorage, createStorage, defaultKeystrokeAssetsPublicUrlBase, deleteWorkspaceObjects, ensureKeystrokeAssetsBucket, ensureStorageBucket, extractProjectArtifact, extractRouteManifestFromArtifact, isBucketAlreadyExistsError, isKeystrokeAssetsAvatarPublicUrl, isKeystrokeAssetsOrgLogoPublicUrl, isOrgLogoStorageKey, isSourceBlobHash, isStorageBucketNotFoundError, isStorageNotFoundError, isUserAvatarStorageKey, keystrokeAssetsAvatarKeyFromPublicUrl, keystrokeAssetsKeyFromPublicUrl, keystrokeAssetsOrgLogoKeyFromPublicUrl, keystrokeAssetsPublicUrlBase, loadProjectSourceFiles, mapInParallelBatches, mergeFilteredArtifact, mergeStoredRouteManifest, minioStoragePlugin, orgAgentBucketName, orgLogoStorageKey, orgStorageBucketName, packDistTree, packProjectArtifact, projectArtifactKey, projectSourceBlobKey, projectSourceManifestKey, publicKeystrokeAssetsObjectUrl, storageAdminConfigFromEnv, storageConfigFromEnv, syncAgentWorkspaceToBucket, uploadWorkspaceObjects, userAvatarStorageKey, workspaceKeysToDelete };\n\n//# sourceMappingURL=index.mjs.map"],"mappings":"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"}
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import { m as emitStoredRouteManifestForProject } from "./dist-BW3AMCud.mjs";
3
- export { emitStoredRouteManifestForProject };
@@ -1,87 +0,0 @@
1
- # API target routing
2
-
3
- The CLI resolves a `baseUrl` before runtime commands (`workflow`, `agent`, `trigger`, `app`, `connect`, `health`). Implementation: `apps/cli/src/resolve-api-target.ts`.
4
-
5
- ## Mental model
6
-
7
- - **Local** = your **keystroke server** (`keystroke dev` / `keystroke start`). Use for authoring, unit/integration tests in the repo, and manual runs while iterating.
8
- - **Cloud** = **keystroke platform** control plane routes to a deployed keystroke server (`/api/projects/:projectId/*`). Use for live invocation, listing triggers on deploy, webhook URLs, and auditing production runs.
9
-
10
- Auth is independent of target. `keystroke auth login` stores a bearer token in the OS keychain keyed by `webUrl` hostname and persists matching `webUrl` / `platformUrl` in config. Cloud API calls attach that token; local dev often runs without auth unless `BETTER_AUTH_SECRET` is set.
11
-
12
- ## Config (`~/.keystroke`)
13
-
14
- | Key | Default | Role |
15
- | ----------------- | -------------------------- | -------------------------------- |
16
- | `platformUrl` | `https://api.keystroke.ai` | Keystroke platform control plane |
17
- | `webUrl` | `https://keystroke.ai` | Dashboard + device login |
18
- | `activeProjectId` | unset | Deployed project id (persistent) |
19
- | `apiTarget` | inferred | `local` or `platform` |
20
-
21
- Local commands use `PUBLIC_PLATFORM_URL` from the project `.env` (dev default `http://localhost:3002` when unset). They do not read `platformUrl` when `apiTarget=local`, so cloud login does not redirect local workflow runs to production.
22
-
23
- If `apiTarget` is unset and `activeProjectId` exists, effective target is **platform** (backward compatible). Explicit `apiTarget=local` overrides that without clearing `activeProjectId`.
24
-
25
- `keystroke config show` prints effective `apiTarget` and any active dev session.
26
-
27
- ## Switching without losing cloud state
28
-
29
- ```bash
30
- keystroke deploy --project proj_abc # activeProjectId=proj_abc, apiTarget=platform
31
- keystroke config use local # apiTarget=local; proj_abc unchanged
32
- keystroke workflow run foo --input '{}' # → localhost:3002
33
- keystroke config use cloud # apiTarget=platform; same proj_abc
34
- keystroke trigger list # → platform runtime
35
- ```
36
-
37
- Swap cloud project:
38
-
39
- ```bash
40
- keystroke config use project proj_xyz
41
- ```
42
-
43
- Target another project once:
44
-
45
- ```bash
46
- keystroke --project proj_xyz workflow runs list my-workflow
47
- ```
48
-
49
- ## Dev session auto-local
50
-
51
- `keystroke dev` writes `dev-session.json` beside config (pid, port, serverUrl). While that process is alive, resolved target is **local** even if `apiTarget=platform`. Stopping dev removes the session; routing falls back to config.
52
-
53
- `--project` still wins over the dev session when you need to hit cloud while dev is up.
54
-
55
- ## Global flags
56
-
57
- | Flag | Effect |
58
- | ---------------- | -------------------------------------------------------------------------- |
59
- | `--local` | Force local API (`PUBLIC_PLATFORM_URL` / project `.env`) for this command |
60
- | `--project <id>` | Force platform runtime for that project; does not update `activeProjectId` |
61
-
62
- ## Platform projects
63
-
64
- Platform projects are deploy targets on the control plane. List or create them before the first deploy:
65
-
66
- ```bash
67
- keystroke project list
68
- keystroke project create --name "My app"
69
- keystroke deploy --project <id> # activates runtime; sets activeProjectId
70
- keystroke config use project <id> # swap default cloud target
71
- ```
72
-
73
- New projects start **inactive**. The CLI hints with `keystroke deploy --project <id>` when appropriate.
74
-
75
- ## When agents should choose local
76
-
77
- - User is editing `src/` and wants to test a workflow or agent change
78
- - `keystroke dev` or `keystroke start` is running (or should be started first)
79
- - Debugging with fast iteration; no deploy mentioned
80
-
81
- ## When agents should choose cloud
82
-
83
- - User asks about deployed triggers, webhook URLs, or production runs
84
- - After `keystroke deploy` or when operating an existing platform project
85
- - Listing or invoking resources on the live environment
86
-
87
- If unsure, run `keystroke config show` and prefer **local** when the task is development/testing.
@@ -1,43 +0,0 @@
1
- ---
2
- name: keystroke-gateways
3
- description: Chat with keystroke agents from Slack via @keystrokehq/slack — OAuth, dashboard binding, channel attach. Use when setting up messaging for agents.
4
- metadata:
5
- keystroke-domain: gateways
6
- ---
7
-
8
- # Gateways
9
-
10
- Gateways route **Slack (and similar) messages** to an agent — users chat in-channel instead of using the CLI.
11
-
12
- ## Setup (Slack)
13
-
14
- 1. Set Slack app env vars in `.env` (see apps skill)
15
- 2. `keystroke start` — open dashboard at `:3000`
16
- 3. Complete Slack OAuth install
17
- 4. **Gateways → Slack → Attach** — pick an agent key + channel
18
- 5. Message the bot in Slack
19
-
20
- ## What happens at runtime
21
-
22
- Inbound Slack event → lookup channel attachment → `runPrompt` for the bound agent → reply in thread.
23
-
24
- ## Audit agent behavior from gateway traffic
25
-
26
- ```bash
27
- keystroke agent sessions list <agent-key> --source gateway
28
- keystroke agent sessions get <agent-key> <session-id> --include messages,trace
29
- ```
30
-
31
- ## Troubleshooting
32
-
33
- | Issue | Check |
34
- | ----------- | ------------------------------------------------------------------- |
35
- | Bot silent | `keystroke agent sessions list` for errors; agent key on attachment |
36
- | OAuth fails | Slack redirect URLs match your dashboard origin |
37
- | Wrong agent | Re-bind attachment in dashboard |
38
-
39
- ## Next references
40
-
41
- - [slack-setup.md](references/slack-setup.md) — env vars, attach flow
42
-
43
- Related: [agents](.agents/skills/keystroke-agents/SKILL.md), [apps](.agents/skills/keystroke-apps/SKILL.md).
@@ -1,27 +0,0 @@
1
- # Slack gateway
2
-
3
- ## .env
4
-
5
- Set Slack OAuth provider env vars in `.env` (client id, secret, signing secret) — then connect the Slack app with `keystroke connect slack`.
6
-
7
- ## Install & bind
8
-
9
- 1. `keystroke start`
10
- 2. Dashboard → install Slack app (OAuth) — includes `chat:write.customize` for agent-named replies
11
- 3. **Gateways → Slack → Attach** — agent key + channel (attach URLs include `team` + `channel` query params)
12
- 4. Send a test message in Slack
13
-
14
- ## Audit
15
-
16
- ```bash
17
- keystroke agent sessions list <agent-key> --source gateway
18
- keystroke agent sessions get <agent-key> <session-id> --include messages,trace
19
- ```
20
-
21
- ## Common fixes
22
-
23
- | Issue | Fix |
24
- | ------------- | -------------------------------------------------- |
25
- | 401 on events | Check signing secret in `.env` |
26
- | No reply | Session errors via `keystroke agent sessions list` |
27
- | Wrong agent | Re-attach in dashboard |