@keystrokehq/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS-blurb.md +123 -0
- package/LICENSE +42 -0
- package/README.md +177 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/keystroke.mjs +107 -0
- package/dist/_manifest-JSRE3H8k.mjs +385 -0
- package/dist/agent-bundle-package-DWV6B_5q-BtV7Xycc.mjs +2344 -0
- package/dist/agent-manifest-CDnbkR2f.mjs +245 -0
- package/dist/agents-CZJGxVqV.mjs +228 -0
- package/dist/api-keys-D2lgguuY.mjs +40 -0
- package/dist/auth-DN2VusyU.mjs +59 -0
- package/dist/auth.handler-CT1BQUvu.mjs +340 -0
- package/dist/browser-qwFrUH82.mjs +24 -0
- package/dist/build-agents-BmM_AsSd-BGi9wtzt.mjs +514 -0
- package/dist/build-metadata-BWS7uhd_-DR8gJjTX.mjs +1422 -0
- package/dist/build-progress-DgYKb4hB.mjs +183 -0
- package/dist/build-tasks-CdihpudT-D5r5HUHe.mjs +91 -0
- package/dist/build-workflows-CfxBnIWh-CdYPv8w2.mjs +370 -0
- package/dist/build.handler-4799CjWH.mjs +36 -0
- package/dist/chunk-CH6r78ws.mjs +37 -0
- package/dist/clear-cache.handler-B9tqSoSM.mjs +11 -0
- package/dist/clear.handler-BTIXXPTJ.mjs +42 -0
- package/dist/clear.handler-BydlX-zE.mjs +11 -0
- package/dist/commander-DfTVqQ-3.mjs +133 -0
- package/dist/concurrency-gXn9Rw8x-DNl2YtrS.mjs +20 -0
- package/dist/connect-BUXkeH0F.mjs +43 -0
- package/dist/connect.handler-CYel9cy6.mjs +430 -0
- package/dist/constants-CPpPdSNg.mjs +8 -0
- package/dist/context-T7HZuB97.mjs +138 -0
- package/dist/credential-env-map-CI8yWHVy.mjs +28 -0
- package/dist/credential-schema-mismatch-BKo5PjcQ.mjs +76 -0
- package/dist/credentials-CvmjU0lK.mjs +171 -0
- package/dist/credentials-OfVHOtG3.mjs +151216 -0
- package/dist/current-deployment-workflow-poHt27i3.mjs +94 -0
- package/dist/current.handler-B8zKzfPp.mjs +21 -0
- package/dist/delete.handler-bAu1iXVQ.mjs +17 -0
- package/dist/deploy-7Jjls436.mjs +26 -0
- package/dist/deploy-BOPIpRWm.mjs +74 -0
- package/dist/deploy-progress-BmGUNFKg.mjs +70 -0
- package/dist/deploy.handler-BAzgiNhd.mjs +370 -0
- package/dist/detect-env-access-CwkOYeYM-D_BCZqV6.mjs +209 -0
- package/dist/diff-utils-NEfcjqxt.mjs +185 -0
- package/dist/diff.handler-Du7SY8K4.mjs +47 -0
- package/dist/dist-BkJUoBiG.mjs +1116 -0
- package/dist/dist-CUK7yBM0.mjs +308 -0
- package/dist/env-91KwMKov.mjs +140 -0
- package/dist/env.handler-BAzBuMzQ.mjs +277 -0
- package/dist/error-boundary-VL-JLfIa.mjs +34 -0
- package/dist/file-metadata-D1vm-XY2.mjs +191 -0
- package/dist/get-intrinsic-zLxwtrLK.mjs +658 -0
- package/dist/import-module-CV84H5fZ-B_CBCmb4.mjs +1747 -0
- package/dist/init-DpMCotSK.mjs +45 -0
- package/dist/init.handler-CPRnif52.mjs +585 -0
- package/dist/inspect.handler-DT_cD036.mjs +146 -0
- package/dist/integration-catalog-Bt-L3GjF.mjs +104 -0
- package/dist/integrations-DlatPK4W.mjs +79 -0
- package/dist/keystroke.d.mts +3 -0
- package/dist/keystroke.mjs +707 -0
- package/dist/layout-CbMtQ2tm.mjs +67 -0
- package/dist/list-enrichment-y-cwizLr.mjs +189 -0
- package/dist/list.handler-BTWvCyjA.mjs +52 -0
- package/dist/list.handler-CWF_Dj15.mjs +24 -0
- package/dist/list.handler-CZ6G2x_G.mjs +75 -0
- package/dist/list.handler-DWaQkJaR.mjs +51 -0
- package/dist/list.handler-DqbFcBW7.mjs +180 -0
- package/dist/list.handler-lq3ZGAn4.mjs +104 -0
- package/dist/logs-BEg9L5l8.mjs +28 -0
- package/dist/logs.handler-6hoMBzqw.mjs +35 -0
- package/dist/logs.handler-BD_dXiL1.mjs +231 -0
- package/dist/metadata-layout-GUYIUo0i-_aG2zjue.mjs +5877 -0
- package/dist/normalize-path-CojS-CgQ-DLCOvnD1.mjs +20 -0
- package/dist/options-CeaTcFxP.mjs +43 -0
- package/dist/org-xLzBtt2_.mjs +41 -0
- package/dist/output-DM4b7KgY.mjs +72 -0
- package/dist/oxc-B3KI3rf_-n9d1hKNq.mjs +119 -0
- package/dist/paused.handler-BMFm9Cff.mjs +94 -0
- package/dist/project-config-D1qsQlO7.mjs +107 -0
- package/dist/projects-CHkRE9rS.mjs +1574 -0
- package/dist/projects-Cjb7sovS.mjs +30 -0
- package/dist/read-credential-keys-77a91T8M-KA0Iw0Z1.mjs +9 -0
- package/dist/register.handler-BPCdor1_.mjs +86 -0
- package/dist/requirements.handler-DPXdSks3.mjs +201 -0
- package/dist/resolve-project-DDJ29sCF.mjs +35 -0
- package/dist/rolldown-runtime-twds-ZHy-BWWzu8VG.mjs +15 -0
- package/dist/run-polling-CAgFRdK3.mjs +20 -0
- package/dist/runs-D9hNLb9A.mjs +259 -0
- package/dist/schedule-BXx3uXwr.mjs +1142 -0
- package/dist/schema-17qMfNyI.mjs +18 -0
- package/dist/schema-display-CgmeKigW.mjs +130 -0
- package/dist/schemas-CDib1RhE.mjs +125 -0
- package/dist/skills-sync.handler-DIy8GR16.mjs +34 -0
- package/dist/skills.command-CrjI2dN9.mjs +35 -0
- package/dist/skills.handler-Bz8bJKql.mjs +9 -0
- package/dist/source-analysis-Cj-ADyu--BJQcFPCG.mjs +144 -0
- package/dist/spinner-progress-DMVwgqO9.mjs +173 -0
- package/dist/src-C0X6u_Mw.mjs +1340 -0
- package/dist/src-eHwu-Gfw.mjs +369 -0
- package/dist/status.handler-BO4nwvWn.mjs +101 -0
- package/dist/switch.handler-D_9213Vf.mjs +51 -0
- package/dist/sync-BL_Mo5st.mjs +39 -0
- package/dist/sync-keystroke-agent-skills-Kx_H7UTd.mjs +70 -0
- package/dist/sync.handler-BUFPdzWz.mjs +82 -0
- package/dist/task-B2sZMaZu.mjs +8 -0
- package/dist/task-target-build-CBeCKbu2.mjs +432 -0
- package/dist/task-target-deploy-C5X-USeR.mjs +4 -0
- package/dist/task-target-deploy-CA6elFpF-BEr4gkol.mjs +271 -0
- package/dist/task-target-deploy-runner.d.mts +3 -0
- package/dist/task-target-deploy-runner.mjs +202 -0
- package/dist/test-BHTgR3UA.mjs +698 -0
- package/dist/test.handler-BcPQ8b74.mjs +13 -0
- package/dist/trigger-artifacts-DQPbQNqC-B4yeeFBY.mjs +239 -0
- package/dist/trigger-manifest-CY7brZeg.mjs +30 -0
- package/dist/try-deploy.handler-DqybNhXx.mjs +490 -0
- package/dist/upload-CkU--iDC.mjs +207 -0
- package/dist/upload.handler-DCtiznQp.mjs +441 -0
- package/dist/utils-CywxCDM7.mjs +14 -0
- package/dist/validate.handler-DOcTaJL0.mjs +280 -0
- package/dist/workflow-build-DBQaBfnn.mjs +1819 -0
- package/dist/workflow-bundler-BPiqVscj-X1PFFAuP.mjs +167 -0
- package/dist/workflows-g9z87AJJ.mjs +799 -0
- package/dist/writer-BG8poUm3-BbXlU2kI.mjs +426 -0
- package/package.json +87 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { t as createTypedCommand } from "./commander-DfTVqQ-3.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/commands/init/init.command.ts
|
|
6
|
+
const InitOptionsSchema = z.object({
|
|
7
|
+
path: z.string().optional().describe("Directory to initialize (default: current directory)"),
|
|
8
|
+
name: z.string().optional().describe("Project name (skips prompt; default: directory basename)"),
|
|
9
|
+
description: z.string().optional().describe("Project description for the API (skips prompt when creating a new project)"),
|
|
10
|
+
scaffold: z.boolean().default(true).describe("Scaffold project files (package.json, vitest.config.ts, etc.)"),
|
|
11
|
+
install: z.boolean().default(true).describe("Run package manager install after scaffolding (when scaffold is enabled)")
|
|
12
|
+
});
|
|
13
|
+
const INIT_OPTIONS_CONFIG = {
|
|
14
|
+
path: {
|
|
15
|
+
flag: "--path <dir>",
|
|
16
|
+
description: "Directory to initialize (default: current directory)"
|
|
17
|
+
},
|
|
18
|
+
name: {
|
|
19
|
+
flag: "--name <name>",
|
|
20
|
+
description: "Project name (non-interactive; default: directory basename)"
|
|
21
|
+
},
|
|
22
|
+
description: {
|
|
23
|
+
flag: "--description <text>",
|
|
24
|
+
description: "Project description when creating the remote project (non-interactive)"
|
|
25
|
+
},
|
|
26
|
+
scaffold: {
|
|
27
|
+
flag: "--no-scaffold",
|
|
28
|
+
description: "Skip scaffolding project files (package.json, vitest.config.ts, etc.)"
|
|
29
|
+
},
|
|
30
|
+
install: {
|
|
31
|
+
flag: "--no-install",
|
|
32
|
+
description: "Skip running package manager install after scaffolding"
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function createInitCommand() {
|
|
36
|
+
return createTypedCommand({
|
|
37
|
+
name: "init",
|
|
38
|
+
description: "Initialize a Keystroke project (creates keystroke.config.ts)",
|
|
39
|
+
schema: InitOptionsSchema,
|
|
40
|
+
optionsConfig: INIT_OPTIONS_CONFIG,
|
|
41
|
+
loadHandler: async () => (await import("./init.handler-CPRnif52.mjs")).handleInit
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { createInitCommand };
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { C as CliExitError, t as ui } from "./keystroke.mjs";
|
|
4
|
+
import { d as trackProject } from "./dist-CUK7yBM0.mjs";
|
|
5
|
+
import { a as writeProjectConfig, i as readProjectConfig, r as getProjectConfigPath } from "./project-config-D1qsQlO7.mjs";
|
|
6
|
+
import { i as requireClient } from "./context-T7HZuB97.mjs";
|
|
7
|
+
import { t as syncKeystrokeAgentSkills } from "./sync-keystroke-agent-skills-Kx_H7UTd.mjs";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import { accessSync, readFileSync } from "node:fs";
|
|
10
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import * as path$1 from "node:path";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { cancel, isCancel, text } from "@clack/prompts";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
//#region src/lib/keystroke-local-registry-npmrc.ts
|
|
17
|
+
/**
|
|
18
|
+
* Default local registry for consumer projects (Verdaccio default port).
|
|
19
|
+
* Override with `KEYSTROKE_LOCAL_NPM_REGISTRY` when publishing or documenting for authors.
|
|
20
|
+
*/
|
|
21
|
+
function getKeystrokeLocalNpmRegistryUrl() {
|
|
22
|
+
const raw = process.env.KEYSTROKE_LOCAL_NPM_REGISTRY?.trim();
|
|
23
|
+
if (!raw) return "http://localhost:4873/";
|
|
24
|
+
return `${raw.replace(/\/+$/, "")}/`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `.npmrc` line so installs resolve `@keystroke/*` from the local registry (Verdaccio).
|
|
28
|
+
*/
|
|
29
|
+
function createKeystrokeLocalRegistryNpmrcContent() {
|
|
30
|
+
return `@keystroke:registry=${getKeystrokeLocalNpmRegistryUrl()}\n`;
|
|
31
|
+
}
|
|
32
|
+
/** When the CLI cannot resolve `@keystroke/workflow-core` on disk (e.g. unusual installs). */
|
|
33
|
+
const FALLBACK_WORKFLOW_CORE_VERSION_RANGE = "^0.0.1";
|
|
34
|
+
/**
|
|
35
|
+
* Semver range for scaffold `package.json` — uses the version of `@keystroke/workflow-core`
|
|
36
|
+
* installed next to this CLI (same as the Keystroke toolchain the author runs).
|
|
37
|
+
*/
|
|
38
|
+
function workflowCoreScaffoldVersionRange() {
|
|
39
|
+
try {
|
|
40
|
+
const raw = readFileSync(createRequire(import.meta.url).resolve("@keystroke/workflow-core/package.json"), "utf-8");
|
|
41
|
+
const version = JSON.parse(raw).version?.trim();
|
|
42
|
+
if (version) return `^${version}`;
|
|
43
|
+
} catch {}
|
|
44
|
+
return FALLBACK_WORKFLOW_CORE_VERSION_RANGE;
|
|
45
|
+
}
|
|
46
|
+
/** When the CLI cannot resolve `@keystroke/skills` on disk. */
|
|
47
|
+
const FALLBACK_SKILLS_VERSION_RANGE = "^0.0.1";
|
|
48
|
+
/**
|
|
49
|
+
* Semver range for scaffold `package.json` devDependency `@keystroke/skills` (published to local Verdaccio).
|
|
50
|
+
*/
|
|
51
|
+
function keystrokeSkillsScaffoldVersionRange() {
|
|
52
|
+
try {
|
|
53
|
+
const raw = readFileSync(createRequire(import.meta.url).resolve("@keystroke/skills/package.json"), "utf-8");
|
|
54
|
+
const version = JSON.parse(raw).version?.trim();
|
|
55
|
+
if (version) return `^${version}`;
|
|
56
|
+
} catch {}
|
|
57
|
+
return FALLBACK_SKILLS_VERSION_RANGE;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/commands/init/agents-md.ts
|
|
61
|
+
const AGENTS_FILENAME = "AGENTS.md";
|
|
62
|
+
const AGENTS_BLURB_PACKAGE_PATH = "AGENTS-blurb.md";
|
|
63
|
+
function normalizeLineEndings(value) {
|
|
64
|
+
return value.replace(/\r\n/g, "\n");
|
|
65
|
+
}
|
|
66
|
+
function normalizeBlurb(value) {
|
|
67
|
+
return `${normalizeLineEndings(value).trim()}\n`;
|
|
68
|
+
}
|
|
69
|
+
function resolveMonorepoSkillsBlurbPath() {
|
|
70
|
+
return fileURLToPath(new URL("../../../../../packages/skills/AGENTS-blurb.md", import.meta.url));
|
|
71
|
+
}
|
|
72
|
+
function resolveCliPackageSkillsBlurbPath() {
|
|
73
|
+
const relativePath = import.meta.url.includes("/dist/") ? "../AGENTS-blurb.md" : "../../../AGENTS-blurb.md";
|
|
74
|
+
return fileURLToPath(new URL(relativePath, import.meta.url));
|
|
75
|
+
}
|
|
76
|
+
function resolveBundledKeystrokeAgentsBlurbPath() {
|
|
77
|
+
const require = createRequire(import.meta.url);
|
|
78
|
+
try {
|
|
79
|
+
const skillsPackageJsonPath = require.resolve("@keystroke/skills/package.json");
|
|
80
|
+
return path$1.join(path$1.dirname(skillsPackageJsonPath), AGENTS_BLURB_PACKAGE_PATH);
|
|
81
|
+
} catch {}
|
|
82
|
+
try {
|
|
83
|
+
return resolveCliPackageSkillsBlurbPath();
|
|
84
|
+
} catch {}
|
|
85
|
+
return resolveMonorepoSkillsBlurbPath();
|
|
86
|
+
}
|
|
87
|
+
async function loadKeystrokeAgentsBlurb() {
|
|
88
|
+
try {
|
|
89
|
+
return normalizeBlurb(await readFile(resolveBundledKeystrokeAgentsBlurbPath(), "utf-8"));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new CliExitError("Could not load the bundled Keystroke AGENTS.md blurb. Reinstall the CLI or republish @keystrokehq/cli with AGENTS-blurb.md.", { cause: error });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function ensureAgentsMarkdown(targetDir, blurb) {
|
|
95
|
+
const agentsFilePath = path$1.join(targetDir, AGENTS_FILENAME);
|
|
96
|
+
const normalizedBlurb = normalizeBlurb(blurb);
|
|
97
|
+
try {
|
|
98
|
+
const normalizedExisting = normalizeLineEndings(await readFile(agentsFilePath, "utf-8"));
|
|
99
|
+
if (normalizedExisting.includes(normalizedBlurb.trim())) return "skipped";
|
|
100
|
+
await writeFile(agentsFilePath, normalizedExisting.trim().length > 0 ? `${normalizedExisting.trimEnd()}\n\n${normalizedBlurb}` : normalizedBlurb, "utf-8");
|
|
101
|
+
return "updated";
|
|
102
|
+
} catch {
|
|
103
|
+
await writeFile(agentsFilePath, normalizedBlurb, "utf-8");
|
|
104
|
+
return "created";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/commands/init/templates/biome-config.ts
|
|
109
|
+
/**
|
|
110
|
+
* Template for generating biome.json for a new Keystroke project.
|
|
111
|
+
* Enables noProcessEnv and loads GritQL rules from @keystroke/workflow-core.
|
|
112
|
+
*/
|
|
113
|
+
function createBiomeConfigContent() {
|
|
114
|
+
return JSON.stringify({
|
|
115
|
+
$schema: "https://biomejs.dev/schemas/2.4.13/schema.json",
|
|
116
|
+
linter: {
|
|
117
|
+
enabled: true,
|
|
118
|
+
rules: { style: { noProcessEnv: "error" } }
|
|
119
|
+
},
|
|
120
|
+
plugins: ["./node_modules/@keystroke/workflow-core/biome/no-env-access.grit"]
|
|
121
|
+
}, null, 2);
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/commands/init/templates/env-example.ts
|
|
125
|
+
/**
|
|
126
|
+
* Template for generating a .env.example for a new Keystroke project.
|
|
127
|
+
*/
|
|
128
|
+
function createEnvExampleContent() {
|
|
129
|
+
return `# Keystroke credential environment variables (fallback when server credentials unavailable)
|
|
130
|
+
# Naming convention: KEYSTROKE_<KEY>
|
|
131
|
+
#
|
|
132
|
+
# The Vitest plugin will first try to fetch credentials from the Keystroke server
|
|
133
|
+
# (using your CLI auth). If that fails, it falls back to these env vars.
|
|
134
|
+
#
|
|
135
|
+
# Example for Slack credentialSet:
|
|
136
|
+
# KEYSTROKE_SLACK_BOT_TOKEN=xoxb-...
|
|
137
|
+
#
|
|
138
|
+
# Example for Anthropic credentialSet:
|
|
139
|
+
# KEYSTROKE_ANTHROPIC_API_KEY=sk-ant-...
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/commands/init/templates/gitignore.ts
|
|
144
|
+
/**
|
|
145
|
+
* Template for generating a .gitignore for a new Keystroke project.
|
|
146
|
+
*/
|
|
147
|
+
function createGitignoreContent() {
|
|
148
|
+
return `node_modules/
|
|
149
|
+
dist/
|
|
150
|
+
.env
|
|
151
|
+
.env.local
|
|
152
|
+
.env.test.local
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/commands/init/templates/hello-workflow.ts
|
|
157
|
+
/**
|
|
158
|
+
* Templates for generating an example workflow with steps for a new Keystroke project.
|
|
159
|
+
*
|
|
160
|
+
* Generates:
|
|
161
|
+
* workflows/hello/greet.step.ts — Greet step
|
|
162
|
+
* workflows/hello/shout.step.ts — Shout step
|
|
163
|
+
* workflows/hello/hello.workflow.ts — Hello workflow composing both steps
|
|
164
|
+
* workflows/hello/hello.test.ts — Smoke test for the sample workflow
|
|
165
|
+
*/
|
|
166
|
+
const HELLO_WORKFLOW_SCAFFOLD_FILES = {
|
|
167
|
+
greetStep: "workflows/hello/greet.step.ts",
|
|
168
|
+
shoutStep: "workflows/hello/shout.step.ts",
|
|
169
|
+
workflow: "workflows/hello/hello.workflow.ts",
|
|
170
|
+
test: "workflows/hello/hello.test.ts"
|
|
171
|
+
};
|
|
172
|
+
function createGreetStepContent() {
|
|
173
|
+
return `import { Step } from '@keystroke/workflow-core';
|
|
174
|
+
import { z } from 'zod';
|
|
175
|
+
|
|
176
|
+
export const greet = new Step({
|
|
177
|
+
id: 'greet',
|
|
178
|
+
name: 'Greet',
|
|
179
|
+
description: 'Creates a greeting message',
|
|
180
|
+
input: z.object({ name: z.string() }),
|
|
181
|
+
output: z.object({ message: z.string() }),
|
|
182
|
+
run: async (input) => {
|
|
183
|
+
return { message: \`Hello, \${input.name}!\` };
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
function createShoutStepContent() {
|
|
189
|
+
return `import { Step } from '@keystroke/workflow-core';
|
|
190
|
+
import { z } from 'zod';
|
|
191
|
+
|
|
192
|
+
export const shout = new Step({
|
|
193
|
+
id: 'shout',
|
|
194
|
+
name: 'Shout',
|
|
195
|
+
description: 'Converts text to uppercase',
|
|
196
|
+
input: z.object({ text: z.string() }),
|
|
197
|
+
output: z.object({ text: z.string() }),
|
|
198
|
+
run: async (input) => {
|
|
199
|
+
return { text: input.text.toUpperCase() };
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
function createHelloWorkflowContent() {
|
|
205
|
+
return `import { Workflow } from '@keystroke/workflow-core';
|
|
206
|
+
import { z } from 'zod';
|
|
207
|
+
import { greet } from './greet.step';
|
|
208
|
+
import { shout } from './shout.step';
|
|
209
|
+
|
|
210
|
+
export const hello = new Workflow({
|
|
211
|
+
id: 'hello',
|
|
212
|
+
name: 'Hello',
|
|
213
|
+
description: 'A simple hello workflow that greets and shouts',
|
|
214
|
+
input: z.object({ name: z.string() }),
|
|
215
|
+
output: z.object({ text: z.string() }),
|
|
216
|
+
run: async (input) => {
|
|
217
|
+
const greeting = await greet.run(input);
|
|
218
|
+
const result = await shout.run({ text: greeting.message });
|
|
219
|
+
return result;
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
function createHelloWorkflowTestContent() {
|
|
225
|
+
return `import { describe, expect, it } from 'vitest';
|
|
226
|
+
import { hello } from './hello.workflow';
|
|
227
|
+
|
|
228
|
+
describe('hello workflow', () => {
|
|
229
|
+
it('exports expected metadata', () => {
|
|
230
|
+
expect(hello.id).toBe('hello');
|
|
231
|
+
expect(hello.name).toBe('Hello');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/commands/init/templates/package-json.ts
|
|
238
|
+
/**
|
|
239
|
+
* Template for generating a package.json for a new Keystroke project.
|
|
240
|
+
*
|
|
241
|
+
* `@keystroke/workflow-core` uses a semver range; installs resolve via `.npmrc` → local Verdaccio
|
|
242
|
+
* (see `pnpm publish:local` from the Keystroke monorepo).
|
|
243
|
+
*/
|
|
244
|
+
function createPackageJsonContent(projectName, options) {
|
|
245
|
+
return JSON.stringify({
|
|
246
|
+
name: projectName,
|
|
247
|
+
private: true,
|
|
248
|
+
type: "module",
|
|
249
|
+
scripts: {
|
|
250
|
+
test: "vitest run",
|
|
251
|
+
typecheck: "tsc --noEmit",
|
|
252
|
+
lint: "biome check ."
|
|
253
|
+
},
|
|
254
|
+
dependencies: {
|
|
255
|
+
"@keystroke/workflow-core": options.workflowCoreVersionRange,
|
|
256
|
+
zod: "^4.3.6"
|
|
257
|
+
},
|
|
258
|
+
devDependencies: {
|
|
259
|
+
"@keystroke/skills": options.skillsVersionRange,
|
|
260
|
+
"@biomejs/biome": "2.4.13",
|
|
261
|
+
vitest: "^4.0.18",
|
|
262
|
+
typescript: "^5.9.3"
|
|
263
|
+
}
|
|
264
|
+
}, null, 2);
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/commands/init/templates/tsconfig.ts
|
|
268
|
+
/**
|
|
269
|
+
* Template for generating a tsconfig.json for a new Keystroke project.
|
|
270
|
+
*/
|
|
271
|
+
function createTsconfigContent() {
|
|
272
|
+
return JSON.stringify({
|
|
273
|
+
compilerOptions: {
|
|
274
|
+
target: "ES2022",
|
|
275
|
+
lib: ["ES2022"],
|
|
276
|
+
module: "ESNext",
|
|
277
|
+
moduleResolution: "bundler",
|
|
278
|
+
strict: true,
|
|
279
|
+
esModuleInterop: true,
|
|
280
|
+
skipLibCheck: true,
|
|
281
|
+
forceConsistentCasingInFileNames: true,
|
|
282
|
+
resolveJsonModule: true,
|
|
283
|
+
isolatedModules: true,
|
|
284
|
+
noUncheckedIndexedAccess: true,
|
|
285
|
+
noEmit: true
|
|
286
|
+
},
|
|
287
|
+
include: ["**/*.ts"],
|
|
288
|
+
exclude: ["node_modules", "dist"]
|
|
289
|
+
}, null, 2);
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/commands/init/templates/vitest-config.ts
|
|
293
|
+
/**
|
|
294
|
+
* Template for generating a vitest.config.ts for a new Keystroke project.
|
|
295
|
+
*/
|
|
296
|
+
function createVitestConfigContent() {
|
|
297
|
+
return `import { defineConfig } from 'vitest/config';
|
|
298
|
+
import { keystrokeTestPlugin } from '@keystroke/workflow-core/vitest';
|
|
299
|
+
|
|
300
|
+
export default defineConfig({
|
|
301
|
+
plugins: [keystrokeTestPlugin()],
|
|
302
|
+
test: {
|
|
303
|
+
include: ['**/*.test.ts'],
|
|
304
|
+
passWithNoTests: true,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/commands/init/templates/vscode-extensions.ts
|
|
311
|
+
/**
|
|
312
|
+
* Template for generating .vscode/extensions.json recommending the Biome extension.
|
|
313
|
+
*/
|
|
314
|
+
function createVscodeExtensionsContent() {
|
|
315
|
+
return JSON.stringify({ recommendations: ["biomejs.biome"] }, null, 2);
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/commands/init/init.handler.ts
|
|
319
|
+
function hasCliDescriptionOption(options) {
|
|
320
|
+
return options.description !== void 0;
|
|
321
|
+
}
|
|
322
|
+
async function promptProjectName(options, defaultName) {
|
|
323
|
+
const cliName = options.name;
|
|
324
|
+
if (cliName !== void 0 && cliName.trim().length > 0) return cliName.trim();
|
|
325
|
+
if (!process.stdin.isTTY) return defaultName.trim() || "project";
|
|
326
|
+
const answer = await text({
|
|
327
|
+
message: "Project name",
|
|
328
|
+
initialValue: defaultName.trim() || "project",
|
|
329
|
+
validate: (value) => {
|
|
330
|
+
if (!value?.trim()) return "Name is required";
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
if (isCancel(answer)) {
|
|
334
|
+
cancel("Init cancelled.");
|
|
335
|
+
throw new CliExitError("Init cancelled.", {
|
|
336
|
+
exitCode: 0,
|
|
337
|
+
reported: true
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return answer.trim();
|
|
341
|
+
}
|
|
342
|
+
async function promptProjectDescription(options) {
|
|
343
|
+
if (hasCliDescriptionOption(options)) {
|
|
344
|
+
const t = options.description?.trim();
|
|
345
|
+
return t && t.length > 0 ? t : void 0;
|
|
346
|
+
}
|
|
347
|
+
if (!process.stdin.isTTY) return;
|
|
348
|
+
const answer = await text({
|
|
349
|
+
message: "Project description (optional)",
|
|
350
|
+
placeholder: "Shown in the Keystroke dashboard"
|
|
351
|
+
});
|
|
352
|
+
if (isCancel(answer)) {
|
|
353
|
+
cancel("Init cancelled.");
|
|
354
|
+
throw new CliExitError("Init cancelled.", {
|
|
355
|
+
exitCode: 0,
|
|
356
|
+
reported: true
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const t = typeof answer === "string" ? answer.trim() : "";
|
|
360
|
+
return t.length > 0 ? t : void 0;
|
|
361
|
+
}
|
|
362
|
+
async function ensureScaffoldPackageJson(targetDir, projectName, ranges) {
|
|
363
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
364
|
+
try {
|
|
365
|
+
const raw = await readFile(pkgPath, "utf-8");
|
|
366
|
+
const pkg = JSON.parse(raw);
|
|
367
|
+
pkg.dependencies = pkg.dependencies ?? {};
|
|
368
|
+
pkg.dependencies["@keystroke/workflow-core"] = ranges.workflowCore;
|
|
369
|
+
pkg.dependencies.zod = pkg.dependencies.zod ?? "^4.3.6";
|
|
370
|
+
pkg.devDependencies = pkg.devDependencies ?? {};
|
|
371
|
+
pkg.devDependencies["@keystroke/skills"] = ranges.skills;
|
|
372
|
+
pkg.devDependencies.vitest = pkg.devDependencies.vitest ?? "^4.0.18";
|
|
373
|
+
pkg.devDependencies.typescript = pkg.devDependencies.typescript ?? "^5.9.3";
|
|
374
|
+
if (!pkg.name) pkg.name = projectName;
|
|
375
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
|
|
376
|
+
return "updated";
|
|
377
|
+
} catch {
|
|
378
|
+
await writeFile(pkgPath, `${createPackageJsonContent(projectName, {
|
|
379
|
+
workflowCoreVersionRange: ranges.workflowCore,
|
|
380
|
+
skillsVersionRange: ranges.skills
|
|
381
|
+
})}\n`, "utf-8");
|
|
382
|
+
return "created";
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/** Ensure `.npmrc` sends `@keystroke/*` to the local Verdaccio registry. */
|
|
386
|
+
async function ensureKeystrokeLocalRegistryNpmrc(targetDir) {
|
|
387
|
+
const npmrcPath = path.join(targetDir, ".npmrc");
|
|
388
|
+
const line = createKeystrokeLocalRegistryNpmrcContent().trimEnd();
|
|
389
|
+
try {
|
|
390
|
+
const existing = await readFile(npmrcPath, "utf-8");
|
|
391
|
+
if (existing.split(/\r?\n/).some((l) => l.trim().startsWith("@keystroke:registry="))) return "skipped";
|
|
392
|
+
await writeFile(npmrcPath, existing.trimEnd().length > 0 ? `${existing.trimEnd()}\n${line}\n` : `${line}\n`, "utf-8");
|
|
393
|
+
return "updated";
|
|
394
|
+
} catch {
|
|
395
|
+
await writeFile(npmrcPath, createKeystrokeLocalRegistryNpmrcContent(), "utf-8");
|
|
396
|
+
return "created";
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function handleInit(options, ctx) {
|
|
400
|
+
const targetDir = path.resolve(options.path ?? process.cwd());
|
|
401
|
+
const client = requireClient(ctx);
|
|
402
|
+
const configPath = getProjectConfigPath(targetDir);
|
|
403
|
+
const configFileName = path.basename(configPath);
|
|
404
|
+
let alreadyExists = false;
|
|
405
|
+
try {
|
|
406
|
+
await access(configPath);
|
|
407
|
+
alreadyExists = true;
|
|
408
|
+
} catch {}
|
|
409
|
+
const existing = await readProjectConfig(targetDir);
|
|
410
|
+
if (alreadyExists && !existing) throw new CliExitError(`${configFileName} exists but is invalid or uses an unsupported legacy shape. Migrate it before running \`keystroke init\`.`);
|
|
411
|
+
const needsRemoteProject = !existing?.organizationId || !existing?.projectId;
|
|
412
|
+
const projectName = await promptProjectName(options, existing?.name?.trim() || path.basename(targetDir));
|
|
413
|
+
let projectDescription;
|
|
414
|
+
if (needsRemoteProject) projectDescription = await promptProjectDescription(options);
|
|
415
|
+
const existingOrganizationId = existing?.organizationId;
|
|
416
|
+
const existingProjectId = existing?.projectId;
|
|
417
|
+
let organizationId = existingOrganizationId;
|
|
418
|
+
let projectId = existingProjectId;
|
|
419
|
+
if (!organizationId || !projectId) {
|
|
420
|
+
const auth = await client.public.auth.validate();
|
|
421
|
+
const res = await client.projects.create({
|
|
422
|
+
name: projectName,
|
|
423
|
+
description: projectDescription ?? null,
|
|
424
|
+
isDefault: false,
|
|
425
|
+
skipRepoCreation: true
|
|
426
|
+
});
|
|
427
|
+
organizationId = auth.organizationId;
|
|
428
|
+
projectId = res.project.id;
|
|
429
|
+
}
|
|
430
|
+
if (alreadyExists) {
|
|
431
|
+
ui.warn(`${configFileName} already exists in ${targetDir}`);
|
|
432
|
+
ui.hint("Updating name and tracking.");
|
|
433
|
+
} else {
|
|
434
|
+
await mkdir(targetDir, { recursive: true });
|
|
435
|
+
ui.success(`Created ${configFileName} in ${targetDir}`);
|
|
436
|
+
}
|
|
437
|
+
const descriptionForConfig = needsRemoteProject && projectDescription !== void 0 && projectDescription.length > 0 ? projectDescription : existing?.description;
|
|
438
|
+
await writeProjectConfig(targetDir, {
|
|
439
|
+
name: projectName,
|
|
440
|
+
organizationId,
|
|
441
|
+
projectId,
|
|
442
|
+
...descriptionForConfig && descriptionForConfig.length > 0 ? { description: descriptionForConfig } : {}
|
|
443
|
+
});
|
|
444
|
+
ui.hint(`Project "${projectName}" is now tracked.`);
|
|
445
|
+
ui.hint(`Project ID: ${projectId}`);
|
|
446
|
+
await trackProject(targetDir, { name: projectName });
|
|
447
|
+
const agentsAction = await ensureAgentsMarkdown(targetDir, await loadKeystrokeAgentsBlurb());
|
|
448
|
+
if (agentsAction === "created") ui.success("Created AGENTS.md with Keystroke project guidance.");
|
|
449
|
+
else if (agentsAction === "updated") ui.success("Updated AGENTS.md with Keystroke project guidance.");
|
|
450
|
+
if (options.scaffold) await scaffoldProject(targetDir, projectName, workflowCoreScaffoldVersionRange(), keystrokeSkillsScaffoldVersionRange(), options.install);
|
|
451
|
+
}
|
|
452
|
+
/** Write a file only if it does not already exist. Returns true if written. */
|
|
453
|
+
async function writeIfMissing(filePath, content) {
|
|
454
|
+
try {
|
|
455
|
+
await access(filePath);
|
|
456
|
+
return false;
|
|
457
|
+
} catch {
|
|
458
|
+
await writeFile(filePath, content, "utf-8");
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Detect package manager from lockfiles. New projects (no lockfile) default to **pnpm** so `keystroke init`
|
|
464
|
+
* matches the Keystroke monorepo toolchain.
|
|
465
|
+
*/
|
|
466
|
+
function detectPackageManager(targetDir) {
|
|
467
|
+
try {
|
|
468
|
+
accessSync(path.join(targetDir, "pnpm-lock.yaml"));
|
|
469
|
+
return "pnpm";
|
|
470
|
+
} catch {}
|
|
471
|
+
try {
|
|
472
|
+
accessSync(path.join(targetDir, "yarn.lock"));
|
|
473
|
+
return "yarn";
|
|
474
|
+
} catch {}
|
|
475
|
+
try {
|
|
476
|
+
accessSync(path.join(targetDir, "package-lock.json"));
|
|
477
|
+
return "npm";
|
|
478
|
+
} catch {}
|
|
479
|
+
return "pnpm";
|
|
480
|
+
}
|
|
481
|
+
/** Run install in targetDir. Returns true if succeeded. */
|
|
482
|
+
function runInstall(targetDir) {
|
|
483
|
+
const pm = detectPackageManager(targetDir);
|
|
484
|
+
const [cmd, ...args] = pm === "pnpm" ? ["pnpm", "install"] : pm === "yarn" ? ["yarn", "install"] : ["npm", "install"];
|
|
485
|
+
return new Promise((resolve) => {
|
|
486
|
+
spawn(cmd, args, {
|
|
487
|
+
cwd: targetDir,
|
|
488
|
+
stdio: "inherit",
|
|
489
|
+
shell: true
|
|
490
|
+
}).on("close", (code) => resolve(code === 0));
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
async function trySyncAgentSkillsAfterScaffold(targetDir) {
|
|
494
|
+
const syncResult = await syncKeystrokeAgentSkills(targetDir);
|
|
495
|
+
if (syncResult.ok) ui.success(`Synced ${syncResult.copied.length} skill(s) to .cursor/skills and .claude/skills: ${syncResult.copied.join(", ")}`);
|
|
496
|
+
else if (syncResult.reason === "not_installed") ui.hint("After install completes, run `keystroke skills sync` to copy @keystroke/skills for Cursor / Claude Code.");
|
|
497
|
+
else ui.warn(`@keystroke/skills is installed at ${syncResult.packageRoot} but no skill directories with SKILL.md were found.`);
|
|
498
|
+
}
|
|
499
|
+
async function scaffoldProject(targetDir, projectName, workflowCoreVersionRange, skillsVersionRange, runInstallAfterScaffold) {
|
|
500
|
+
const pkgAction = await ensureScaffoldPackageJson(targetDir, projectName, {
|
|
501
|
+
workflowCore: workflowCoreVersionRange,
|
|
502
|
+
skills: skillsVersionRange
|
|
503
|
+
});
|
|
504
|
+
ui.success(pkgAction === "created" ? `Created package.json (@keystroke/workflow-core ${workflowCoreVersionRange}, @keystroke/skills ${skillsVersionRange})` : `Updated package.json (Keystroke deps → workflow-core ${workflowCoreVersionRange}, skills ${skillsVersionRange})`);
|
|
505
|
+
const registryUrl = getKeystrokeLocalNpmRegistryUrl();
|
|
506
|
+
const npmrcAction = await ensureKeystrokeLocalRegistryNpmrc(targetDir);
|
|
507
|
+
if (npmrcAction === "created") ui.success(`Created .npmrc (@keystroke → local registry ${registryUrl.trim()})`);
|
|
508
|
+
else if (npmrcAction === "updated") ui.success("Updated .npmrc with @keystroke registry line");
|
|
509
|
+
ui.hint("Before install: run Verdaccio (e.g. pnpm dlx verdaccio), then from the Keystroke repo: pnpm publish:local");
|
|
510
|
+
const files = [
|
|
511
|
+
{
|
|
512
|
+
rel: "vitest.config.ts",
|
|
513
|
+
content: createVitestConfigContent()
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
rel: "tsconfig.json",
|
|
517
|
+
content: createTsconfigContent()
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
rel: "biome.json",
|
|
521
|
+
content: createBiomeConfigContent()
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
rel: ".vscode/extensions.json",
|
|
525
|
+
content: createVscodeExtensionsContent()
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
rel: ".env.example",
|
|
529
|
+
content: createEnvExampleContent()
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
rel: ".gitignore",
|
|
533
|
+
content: createGitignoreContent()
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
rel: HELLO_WORKFLOW_SCAFFOLD_FILES.greetStep,
|
|
537
|
+
content: createGreetStepContent()
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
rel: HELLO_WORKFLOW_SCAFFOLD_FILES.shoutStep,
|
|
541
|
+
content: createShoutStepContent()
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
rel: HELLO_WORKFLOW_SCAFFOLD_FILES.workflow,
|
|
545
|
+
content: createHelloWorkflowContent()
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
rel: HELLO_WORKFLOW_SCAFFOLD_FILES.test,
|
|
549
|
+
content: createHelloWorkflowTestContent()
|
|
550
|
+
}
|
|
551
|
+
];
|
|
552
|
+
const created = [];
|
|
553
|
+
for (const { rel, content } of files) {
|
|
554
|
+
const filePath = path.join(targetDir, rel);
|
|
555
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
556
|
+
if (await writeIfMissing(filePath, content)) created.push(rel);
|
|
557
|
+
}
|
|
558
|
+
if (created.length > 0) ui.success(`Scaffolded ${created.length} file(s): ${created.join(", ")}`);
|
|
559
|
+
else ui.hint("Scaffold files already exist — skipped (package.json was still updated).");
|
|
560
|
+
if (runInstallAfterScaffold) {
|
|
561
|
+
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
562
|
+
try {
|
|
563
|
+
await access(pkgJsonPath);
|
|
564
|
+
} catch {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
ui.hint("Running install...");
|
|
568
|
+
const ok = await runInstall(targetDir);
|
|
569
|
+
const pm = detectPackageManager(targetDir);
|
|
570
|
+
const testCmd = pm === "pnpm" ? "pnpm test" : pm === "yarn" ? "yarn test" : "npm test";
|
|
571
|
+
if (ok) {
|
|
572
|
+
ui.success("Dependencies installed.");
|
|
573
|
+
await trySyncAgentSkillsAfterScaffold(targetDir);
|
|
574
|
+
ui.hint(`Run \`${testCmd}\` to run tests.`);
|
|
575
|
+
} else {
|
|
576
|
+
ui.warn("Install failed. Run your package manager install manually.");
|
|
577
|
+
await trySyncAgentSkillsAfterScaffold(targetDir);
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
ui.hint("Run `pnpm install`, then `pnpm test` to run tests (or use your lockfile’s package manager).");
|
|
581
|
+
await trySyncAgentSkillsAfterScaffold(targetDir);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
//#endregion
|
|
585
|
+
export { handleInit };
|