@prajwolkc/stk 0.1.1 → 0.2.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/README.md CHANGED
@@ -4,10 +4,13 @@ One CLI to deploy, monitor, and debug your entire stack.
4
4
 
5
5
  Stop opening 5 dashboards. `stk` checks your services, watches your deploys, syncs your env vars, tails your logs, and manages your issues — all from one command.
6
6
 
7
+ <!-- TODO: Add demo GIF here -->
8
+ <!-- ![stk demo](./docs/demo.gif) -->
9
+
7
10
  ## Install
8
11
 
9
12
  ```bash
10
- npm install -g stk-cli
13
+ npm install -g @prajwolkc/stk
11
14
  ```
12
15
 
13
16
  ## Quick Start
@@ -16,6 +19,7 @@ npm install -g stk-cli
16
19
  cd my-project
17
20
  stk init # auto-detect your services
18
21
  stk init --template saas # or use a starter template
22
+ stk doctor # diagnose any misconfig
19
23
  stk health # check everything
20
24
  stk status # one-line summary of your whole stack
21
25
  ```
@@ -27,10 +31,12 @@ stk status # one-line summary of your whole stack
27
31
  | `stk init` | Initialize config (auto-detect or `--template saas\|api\|fullstack\|static\|fly\|aws`) |
28
32
  | `stk status` | One-line summary: git, services, deploys, issues |
29
33
  | `stk health` | Health check all configured services |
30
- | `stk deploy` | Git push + watch Railway/Vercel/Fly deploys |
34
+ | `stk doctor` | Diagnose misconfig, missing env vars, and suggest fixes |
35
+ | `stk deploy` | Git push + watch deploy providers |
31
36
  | `stk env pull` | Pull env vars from Vercel + Railway into `.env.pulled` |
32
37
  | `stk env diff` | Show what's in your local `.env` |
33
- | `stk logs` | Tail Railway deployment logs |
38
+ | `stk logs` | Tail logs from Railway, Vercel, Fly, or Render |
39
+ | `stk logs -p vercel` | Logs from a specific provider |
34
40
  | `stk todo ls` | List open GitHub issues |
35
41
  | `stk todo add "title"` | Create a GitHub issue |
36
42
  | `stk todo close 42` | Close an issue |
@@ -40,6 +46,7 @@ stk status # one-line summary of your whole stack
40
46
  **Deploy providers:** Railway, Vercel, Fly.io, Render, AWS
41
47
  **Databases:** PostgreSQL, MongoDB, Redis, Supabase
42
48
  **Storage & billing:** Cloudflare R2, Stripe
49
+ **Custom:** Add your own via plugins
43
50
 
44
51
  ## Configuration
45
52
 
@@ -82,15 +89,57 @@ stk init --list-templates
82
89
  | `fly` | Fly.io + PostgreSQL + Redis |
83
90
  | `aws` | AWS + PostgreSQL + Redis |
84
91
 
85
- ## Environment Variables
92
+ ## Doctor
93
+
94
+ `stk doctor` scans your config and environment to catch issues before they bite:
95
+
96
+ ```
97
+ $ stk doctor
98
+
99
+ my-saas — Doctor
100
+ ─────────────────────────────────────────
101
+ ✓ railway Configured correctly
102
+ ✗ vercel Missing required: VERCEL_TOKEN
103
+ See https://vercel.com/account/tokens
104
+ ! database Missing optional: RAILWAY_PROJECT_ID
105
+ Some features need these for full functionality
106
+ ✓ stripe Configured correctly
107
+ ```
108
+
109
+ ## Plugins
110
+
111
+ Add custom services without forking. Create `.stk/plugins/my-service.mjs`:
112
+
113
+ ```js
114
+ export default {
115
+ name: "my-plugin",
116
+ services: {
117
+ myservice: {
118
+ name: "My Service",
119
+ envVars: ["MY_SERVICE_TOKEN"],
120
+ healthCheck: async () => {
121
+ const token = process.env.MY_SERVICE_TOKEN;
122
+ if (!token) {
123
+ return { name: "My Service", status: "skipped", detail: "MY_SERVICE_TOKEN not set" };
124
+ }
125
+ // Your check logic here
126
+ return { name: "My Service", status: "healthy", detail: "connected" };
127
+ }
128
+ }
129
+ }
130
+ };
131
+ ```
86
132
 
87
- Set the tokens for your services:
133
+ Plugins are automatically loaded by `stk health` and `stk health --all`.
134
+
135
+ ## Environment Variables
88
136
 
89
137
  ```bash
90
138
  # Deploy providers
91
139
  RAILWAY_API_TOKEN=
92
140
  VERCEL_TOKEN=
93
141
  FLY_API_TOKEN=
142
+ FLY_APP_NAME= # needed for stk logs -p fly
94
143
  RENDER_API_KEY=
95
144
  AWS_ACCESS_KEY_ID= / AWS_SECRET_ACCESS_KEY=
96
145
 
@@ -109,6 +158,64 @@ GITHUB_TOKEN=
109
158
  GITHUB_REPO=owner/repo # or auto-detected from git remote
110
159
  ```
111
160
 
161
+ ## Claude Code / MCP Integration
162
+
163
+ `stk` ships with a built-in MCP server so Claude Code can use your infrastructure as native tools.
164
+
165
+ ### Setup
166
+
167
+ 1. Install stk globally:
168
+
169
+ ```bash
170
+ npm install -g @prajwolkc/stk
171
+ ```
172
+
173
+ 2. Add to your project's `.mcp.json` (create it in your project root):
174
+
175
+ ```json
176
+ {
177
+ "mcpServers": {
178
+ "stk": {
179
+ "command": "stk-mcp",
180
+ "args": []
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ 3. Restart Claude Code. Approve the stk MCP server when prompted.
187
+
188
+ ### What Claude can do
189
+
190
+ | Tool | Description |
191
+ |------|-------------|
192
+ | `stk_health` | Check if all services are up before writing code |
193
+ | `stk_status` | Full overview: git, services, deploys, issues |
194
+ | `stk_doctor` | Diagnose misconfig and missing env vars |
195
+ | `stk_logs` | Read production logs to understand bugs |
196
+ | `stk_todo_list` | See what needs to be worked on |
197
+ | `stk_todo_add` | Create GitHub issues |
198
+ | `stk_deploy` | Push code and trigger deploys |
199
+ | `stk_config` | Read the project's stack config |
200
+
201
+ ### Example prompts
202
+
203
+ - *"Check if all my services are healthy"*
204
+ - *"What errors are in my production logs?"*
205
+ - *"What should I work on next?"*
206
+ - *"Deploy this and verify it worked"*
207
+
208
+ ## Development
209
+
210
+ ```bash
211
+ git clone https://github.com/Harden43/stk.git
212
+ cd stk
213
+ npm install
214
+ npm run dev -- health --all # run in dev mode
215
+ npm test # run tests
216
+ npm run build # compile TypeScript
217
+ ```
218
+
112
219
  ## License
113
220
 
114
221
  MIT
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const doctorCommand: Command;
@@ -0,0 +1,220 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { existsSync } from "fs";
4
+ import { loadConfig, enabledServices, CONFIG_FILE } from "../lib/config.js";
5
+ const ENV_REQUIREMENTS = {
6
+ railway: {
7
+ required: ["RAILWAY_API_TOKEN"],
8
+ optional: ["RAILWAY_PROJECT_ID", "RAILWAY_ENVIRONMENT_ID", "RAILWAY_SERVICE_ID"],
9
+ docs: "https://docs.railway.com/guides/public-api",
10
+ },
11
+ vercel: {
12
+ required: ["VERCEL_TOKEN"],
13
+ optional: ["VERCEL_PROJECT_ID"],
14
+ docs: "https://vercel.com/account/tokens",
15
+ },
16
+ fly: {
17
+ required: ["FLY_API_TOKEN"],
18
+ optional: ["FLY_APP_NAME"],
19
+ docs: "https://fly.io/docs/flyctl/tokens/",
20
+ },
21
+ render: {
22
+ required: ["RENDER_API_KEY"],
23
+ optional: [],
24
+ docs: "https://render.com/docs/api",
25
+ },
26
+ aws: {
27
+ required: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
28
+ optional: ["AWS_REGION"],
29
+ },
30
+ database: {
31
+ required: ["DATABASE_URL"],
32
+ optional: [],
33
+ },
34
+ mongodb: {
35
+ required: ["MONGODB_URL"],
36
+ optional: [],
37
+ },
38
+ redis: {
39
+ required: ["REDIS_URL"],
40
+ optional: [],
41
+ },
42
+ supabase: {
43
+ required: ["SUPABASE_URL"],
44
+ optional: ["SUPABASE_SERVICE_KEY", "SUPABASE_ANON_KEY"],
45
+ docs: "https://supabase.com/docs/guides/api",
46
+ },
47
+ r2: {
48
+ required: ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"],
49
+ optional: [],
50
+ docs: "https://developers.cloudflare.com/r2/api/",
51
+ },
52
+ stripe: {
53
+ required: ["STRIPE_SECRET_KEY"],
54
+ optional: [],
55
+ docs: "https://dashboard.stripe.com/apikeys",
56
+ },
57
+ };
58
+ export const doctorCommand = new Command("doctor")
59
+ .description("Diagnose configuration issues and suggest fixes")
60
+ .action(async () => {
61
+ const issues = [];
62
+ // 1. Check config file
63
+ const hasConfig = existsSync(CONFIG_FILE);
64
+ if (!hasConfig) {
65
+ issues.push({
66
+ level: "warn",
67
+ service: "config",
68
+ message: `No ${CONFIG_FILE} found — using auto-detection`,
69
+ fix: `Run ${chalk.white("stk init")} to create one`,
70
+ });
71
+ }
72
+ const config = loadConfig();
73
+ const enabled = enabledServices(config);
74
+ // 2. Check each enabled service for env vars
75
+ for (const svc of enabled) {
76
+ const reqs = ENV_REQUIREMENTS[svc];
77
+ if (!reqs)
78
+ continue;
79
+ const missingRequired = reqs.required.filter((v) => !process.env[v]);
80
+ const missingOptional = reqs.optional.filter((v) => !process.env[v]);
81
+ if (missingRequired.length > 0) {
82
+ issues.push({
83
+ level: "error",
84
+ service: svc,
85
+ message: `Missing required: ${missingRequired.join(", ")}`,
86
+ fix: reqs.docs ? `See ${reqs.docs}` : `Set ${missingRequired[0]} in your .env`,
87
+ });
88
+ }
89
+ if (missingOptional.length > 0) {
90
+ issues.push({
91
+ level: "warn",
92
+ service: svc,
93
+ message: `Missing optional: ${missingOptional.join(", ")}`,
94
+ fix: `Some features (logs, env sync, deploy) need these for full functionality`,
95
+ });
96
+ }
97
+ if (missingRequired.length === 0) {
98
+ issues.push({
99
+ level: "info",
100
+ service: svc,
101
+ message: "Configured correctly",
102
+ });
103
+ }
104
+ }
105
+ // 3. Check for partial config (env vars set but service not enabled)
106
+ for (const [svc, reqs] of Object.entries(ENV_REQUIREMENTS)) {
107
+ if (enabled.includes(svc))
108
+ continue;
109
+ const hasAny = reqs.required.some((v) => process.env[v]);
110
+ if (hasAny) {
111
+ issues.push({
112
+ level: "warn",
113
+ service: svc,
114
+ message: `Env vars found but service not enabled in config`,
115
+ fix: `Add "${svc}": true to services in ${CONFIG_FILE}`,
116
+ });
117
+ }
118
+ }
119
+ // 4. Check for common misconfigurations
120
+ if (process.env.DATABASE_URL) {
121
+ try {
122
+ new URL(process.env.DATABASE_URL);
123
+ }
124
+ catch {
125
+ issues.push({
126
+ level: "error",
127
+ service: "database",
128
+ message: "DATABASE_URL is not a valid URL",
129
+ fix: "Format: postgresql://user:pass@host:5432/dbname",
130
+ });
131
+ }
132
+ }
133
+ if (process.env.REDIS_URL) {
134
+ try {
135
+ new URL(process.env.REDIS_URL);
136
+ }
137
+ catch {
138
+ issues.push({
139
+ level: "error",
140
+ service: "redis",
141
+ message: "REDIS_URL is not a valid URL",
142
+ fix: "Format: redis://default:pass@host:6379",
143
+ });
144
+ }
145
+ }
146
+ if (process.env.STRIPE_SECRET_KEY && !process.env.STRIPE_SECRET_KEY.startsWith("sk_")) {
147
+ issues.push({
148
+ level: "error",
149
+ service: "stripe",
150
+ message: "STRIPE_SECRET_KEY should start with sk_test_ or sk_live_",
151
+ });
152
+ }
153
+ // 5. Check deploy config
154
+ if (hasConfig && config.deploy?.providers) {
155
+ for (const p of config.deploy.providers) {
156
+ if (!enabled.includes(p)) {
157
+ issues.push({
158
+ level: "error",
159
+ service: "deploy",
160
+ message: `Deploy provider "${p}" is not enabled in services`,
161
+ fix: `Add "${p}": true to services, or remove from deploy.providers`,
162
+ });
163
+ }
164
+ }
165
+ }
166
+ // 6. Check GitHub config for todo
167
+ if (!process.env.GITHUB_TOKEN && !config.github?.repo) {
168
+ issues.push({
169
+ level: "info",
170
+ service: "github",
171
+ message: "GITHUB_TOKEN not set — stk todo will have limited access",
172
+ fix: "Set GITHUB_TOKEN for creating/closing issues",
173
+ });
174
+ }
175
+ // Print results
176
+ const ICONS = {
177
+ error: chalk.red("✗"),
178
+ warn: chalk.yellow("!"),
179
+ info: chalk.green("✓"),
180
+ };
181
+ const COLORS = {
182
+ error: chalk.red,
183
+ warn: chalk.yellow,
184
+ info: chalk.green,
185
+ };
186
+ console.log();
187
+ console.log(chalk.bold(` ${config.name} — Doctor`));
188
+ console.log(chalk.dim(" ─────────────────────────────────────────"));
189
+ const grouped = new Map();
190
+ for (const issue of issues) {
191
+ const list = grouped.get(issue.service) ?? [];
192
+ list.push(issue);
193
+ grouped.set(issue.service, list);
194
+ }
195
+ for (const [service, serviceIssues] of grouped) {
196
+ for (const issue of serviceIssues) {
197
+ const icon = ICONS[issue.level];
198
+ const svc = COLORS[issue.level](service.padEnd(12));
199
+ console.log(` ${icon} ${svc} ${issue.message}`);
200
+ if (issue.fix) {
201
+ console.log(` ${" ".repeat(12)} ${chalk.dim(issue.fix)}`);
202
+ }
203
+ }
204
+ }
205
+ const errors = issues.filter((i) => i.level === "error");
206
+ const warns = issues.filter((i) => i.level === "warn");
207
+ const ok = issues.filter((i) => i.level === "info");
208
+ console.log();
209
+ if (errors.length > 0) {
210
+ console.log(chalk.red(` ${errors.length} error${errors.length > 1 ? "s" : ""} found`));
211
+ process.exitCode = 1;
212
+ }
213
+ else if (warns.length > 0) {
214
+ console.log(chalk.yellow(` ${warns.length} warning${warns.length > 1 ? "s" : ""}, ${ok.length} ok`));
215
+ }
216
+ else {
217
+ console.log(chalk.green(` All ${ok.length} checks passed`));
218
+ }
219
+ console.log();
220
+ });
@@ -2,7 +2,8 @@ import { Command } from "commander";
2
2
  import chalk from "chalk";
3
3
  import ora from "ora";
4
4
  import { loadConfig, enabledServices } from "../lib/config.js";
5
- import { getChecker, allCheckerNames } from "../services/registry.js";
5
+ import { getChecker, allCheckerNames, loadPluginCheckers } from "../services/registry.js";
6
+ import { jsonOutput } from "../lib/output.js";
6
7
  const STATUS_ICON = {
7
8
  healthy: chalk.green("✓"),
8
9
  degraded: chalk.yellow("~"),
@@ -19,20 +20,25 @@ export const healthCommand = new Command("health")
19
20
  .description("Check the health of all connected services")
20
21
  .option("-v, --verbose", "Show latency and extra detail")
21
22
  .option("-a, --all", "Check all known services, not just configured ones")
23
+ .option("-j, --json", "Output as JSON")
22
24
  .action(async (opts) => {
23
25
  const config = loadConfig();
24
- const spinner = ora("Checking services...").start();
26
+ await loadPluginCheckers();
25
27
  const serviceList = opts.all
26
28
  ? allCheckerNames()
27
29
  : enabledServices(config);
28
30
  if (serviceList.length === 0) {
29
- spinner.stop();
31
+ if (opts.json) {
32
+ jsonOutput({ services: [], summary: "no services configured" });
33
+ return;
34
+ }
30
35
  console.log();
31
36
  console.log(chalk.yellow(" No services configured."));
32
37
  console.log(chalk.dim(` Run ${chalk.white("stk init")} to set up your project, or ${chalk.white("stk health --all")} to check everything.`));
33
38
  console.log();
34
39
  return;
35
40
  }
41
+ const spinner = opts.json ? null : ora("Checking services...").start();
36
42
  const checks = serviceList.map((name) => {
37
43
  const checker = getChecker(name);
38
44
  if (!checker) {
@@ -45,7 +51,26 @@ export const healthCommand = new Command("health")
45
51
  return checker();
46
52
  });
47
53
  const results = await Promise.all(checks);
48
- spinner.stop();
54
+ spinner?.stop();
55
+ // JSON output
56
+ if (opts.json) {
57
+ const down = results.filter((r) => r.status === "down");
58
+ jsonOutput({
59
+ project: config.name,
60
+ services: results,
61
+ summary: {
62
+ healthy: results.filter((r) => r.status === "healthy").length,
63
+ down: down.length,
64
+ skipped: results.filter((r) => r.status === "skipped").length,
65
+ total: results.length,
66
+ },
67
+ ok: down.length === 0,
68
+ });
69
+ if (down.length > 0)
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ // Human output
49
74
  console.log();
50
75
  console.log(chalk.bold(` ${config.name} — Service Health`));
51
76
  console.log(chalk.dim(" ─────────────────────────────────────────"));