@prajwolkc/stk 0.1.0
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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.js +152 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +77 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +111 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +151 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +130 -0
- package/dist/commands/todo.d.ts +2 -0
- package/dist/commands/todo.js +187 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/lib/config.d.ts +36 -0
- package/dist/lib/config.js +102 -0
- package/dist/services/aws.d.ts +2 -0
- package/dist/services/aws.js +32 -0
- package/dist/services/checker.d.ts +10 -0
- package/dist/services/checker.js +20 -0
- package/dist/services/database.d.ts +2 -0
- package/dist/services/database.js +27 -0
- package/dist/services/fly.d.ts +2 -0
- package/dist/services/fly.js +17 -0
- package/dist/services/mongodb.d.ts +2 -0
- package/dist/services/mongodb.js +25 -0
- package/dist/services/r2.d.ts +2 -0
- package/dist/services/r2.js +20 -0
- package/dist/services/railway.d.ts +2 -0
- package/dist/services/railway.js +26 -0
- package/dist/services/redis.d.ts +2 -0
- package/dist/services/redis.js +34 -0
- package/dist/services/registry.d.ts +4 -0
- package/dist/services/registry.js +37 -0
- package/dist/services/render.d.ts +2 -0
- package/dist/services/render.js +19 -0
- package/dist/services/stripe.d.ts +2 -0
- package/dist/services/stripe.js +21 -0
- package/dist/services/supabase.d.ts +2 -0
- package/dist/services/supabase.js +24 -0
- package/dist/services/vercel.d.ts +2 -0
- package/dist/services/vercel.js +35 -0
- package/dist/templates/index.d.ts +8 -0
- package/dist/templates/index.js +105 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 prajwolkc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# stk
|
|
2
|
+
|
|
3
|
+
One CLI to deploy, monitor, and debug your entire stack.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g stk-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd my-project
|
|
17
|
+
stk init # auto-detect your services
|
|
18
|
+
stk init --template saas # or use a starter template
|
|
19
|
+
stk health # check everything
|
|
20
|
+
stk status # one-line summary of your whole stack
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
| Command | Description |
|
|
26
|
+
|---------|-------------|
|
|
27
|
+
| `stk init` | Initialize config (auto-detect or `--template saas\|api\|fullstack\|static\|fly\|aws`) |
|
|
28
|
+
| `stk status` | One-line summary: git, services, deploys, issues |
|
|
29
|
+
| `stk health` | Health check all configured services |
|
|
30
|
+
| `stk deploy` | Git push + watch Railway/Vercel/Fly deploys |
|
|
31
|
+
| `stk env pull` | Pull env vars from Vercel + Railway into `.env.pulled` |
|
|
32
|
+
| `stk env diff` | Show what's in your local `.env` |
|
|
33
|
+
| `stk logs` | Tail Railway deployment logs |
|
|
34
|
+
| `stk todo ls` | List open GitHub issues |
|
|
35
|
+
| `stk todo add "title"` | Create a GitHub issue |
|
|
36
|
+
| `stk todo close 42` | Close an issue |
|
|
37
|
+
|
|
38
|
+
## Supported Services
|
|
39
|
+
|
|
40
|
+
**Deploy providers:** Railway, Vercel, Fly.io, Render, AWS
|
|
41
|
+
**Databases:** PostgreSQL, MongoDB, Redis, Supabase
|
|
42
|
+
**Storage & billing:** Cloudflare R2, Stripe
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
`stk init` creates a `stk.config.json` in your project root:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"name": "my-saas",
|
|
51
|
+
"services": {
|
|
52
|
+
"vercel": true,
|
|
53
|
+
"railway": true,
|
|
54
|
+
"database": true,
|
|
55
|
+
"redis": true,
|
|
56
|
+
"stripe": true,
|
|
57
|
+
"r2": true
|
|
58
|
+
},
|
|
59
|
+
"deploy": {
|
|
60
|
+
"branch": "main",
|
|
61
|
+
"providers": ["vercel", "railway"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Only configured services are checked — no noise from services you don't use.
|
|
67
|
+
|
|
68
|
+
If no config file exists, `stk` auto-detects services from your environment variables.
|
|
69
|
+
|
|
70
|
+
## Templates
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
stk init --list-templates
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Template | Stack |
|
|
77
|
+
|----------|-------|
|
|
78
|
+
| `saas` | Vercel + Railway + PostgreSQL + Redis + Stripe + R2 |
|
|
79
|
+
| `api` | Railway + PostgreSQL + Redis |
|
|
80
|
+
| `fullstack` | Vercel + Railway + Supabase + Stripe |
|
|
81
|
+
| `static` | Vercel only |
|
|
82
|
+
| `fly` | Fly.io + PostgreSQL + Redis |
|
|
83
|
+
| `aws` | AWS + PostgreSQL + Redis |
|
|
84
|
+
|
|
85
|
+
## Environment Variables
|
|
86
|
+
|
|
87
|
+
Set the tokens for your services:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Deploy providers
|
|
91
|
+
RAILWAY_API_TOKEN=
|
|
92
|
+
VERCEL_TOKEN=
|
|
93
|
+
FLY_API_TOKEN=
|
|
94
|
+
RENDER_API_KEY=
|
|
95
|
+
AWS_ACCESS_KEY_ID= / AWS_SECRET_ACCESS_KEY=
|
|
96
|
+
|
|
97
|
+
# Databases
|
|
98
|
+
DATABASE_URL=
|
|
99
|
+
MONGODB_URL=
|
|
100
|
+
REDIS_URL=
|
|
101
|
+
SUPABASE_URL= / SUPABASE_SERVICE_KEY=
|
|
102
|
+
|
|
103
|
+
# Storage & billing
|
|
104
|
+
CLOUDFLARE_ACCOUNT_ID= / CLOUDFLARE_API_TOKEN=
|
|
105
|
+
STRIPE_SECRET_KEY=
|
|
106
|
+
|
|
107
|
+
# GitHub (for stk todo)
|
|
108
|
+
GITHUB_TOKEN=
|
|
109
|
+
GITHUB_REPO=owner/repo # or auto-detected from git remote
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { loadConfig, enabledServices } from "../lib/config.js";
|
|
6
|
+
const DEPLOY_PROVIDERS = {
|
|
7
|
+
vercel: watchVercel,
|
|
8
|
+
railway: watchRailway,
|
|
9
|
+
};
|
|
10
|
+
export const deployCommand = new Command("deploy")
|
|
11
|
+
.description("Push to branch and watch deploy providers")
|
|
12
|
+
.option("-b, --branch <branch>", "branch to push (default: from config or main)")
|
|
13
|
+
.option("--skip-push", "skip git push, just watch deploys")
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
const branch = opts.branch ?? config.deploy?.branch ?? "main";
|
|
17
|
+
// Step 1: Git push
|
|
18
|
+
if (!opts.skipPush) {
|
|
19
|
+
const pushSpinner = ora("Pushing to remote...").start();
|
|
20
|
+
try {
|
|
21
|
+
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
}).trim();
|
|
24
|
+
if (currentBranch !== branch) {
|
|
25
|
+
pushSpinner.warn(`You're on ${chalk.yellow(currentBranch)}, not ${chalk.green(branch)}`);
|
|
26
|
+
console.log(chalk.dim(` Run: git checkout ${branch} && git merge ${currentBranch}`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
execSync(`git push origin ${branch}`, {
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
stdio: "pipe",
|
|
32
|
+
});
|
|
33
|
+
pushSpinner.succeed(`Pushed to ${chalk.green(`origin/${branch}`)}`);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
pushSpinner.fail("Git push failed");
|
|
37
|
+
console.error(chalk.red(` ${err.message}`));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Step 2: Watch deploys — only providers enabled in config
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(chalk.bold(` ${config.name} — Watching deploys...`));
|
|
45
|
+
console.log(chalk.dim(" ─────────────────────────────────────────"));
|
|
46
|
+
const providers = config.deploy?.providers ?? enabledServices(config).filter((s) => s in DEPLOY_PROVIDERS);
|
|
47
|
+
const watchers = [];
|
|
48
|
+
for (const provider of providers) {
|
|
49
|
+
const watcher = DEPLOY_PROVIDERS[provider];
|
|
50
|
+
if (watcher) {
|
|
51
|
+
watchers.push(watcher());
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log(chalk.dim(` ○ ${provider} no deploy watcher available`));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (watchers.length === 0) {
|
|
58
|
+
console.log(chalk.yellow(" No deploy providers configured."));
|
|
59
|
+
console.log(chalk.dim(` Add "deploy.providers" to stk.config.json`));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
await Promise.all(watchers);
|
|
63
|
+
}
|
|
64
|
+
console.log();
|
|
65
|
+
});
|
|
66
|
+
async function watchVercel() {
|
|
67
|
+
const token = process.env.VERCEL_TOKEN;
|
|
68
|
+
const spinner = ora({ text: "Vercel deploying...", indent: 2 }).start();
|
|
69
|
+
for (let i = 0; i < 60; i++) {
|
|
70
|
+
await sleep(5000);
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
|
|
73
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
74
|
+
});
|
|
75
|
+
const data = (await res.json());
|
|
76
|
+
const dep = data.deployments?.[0];
|
|
77
|
+
if (!dep)
|
|
78
|
+
continue;
|
|
79
|
+
const state = dep.readyState ?? dep.state;
|
|
80
|
+
if (state === "READY") {
|
|
81
|
+
spinner.succeed(`Vercel ${chalk.green("ready")} — ${chalk.dim(dep.url)}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
else if (state === "ERROR") {
|
|
85
|
+
spinner.fail(`Vercel deploy ${chalk.red("failed")}`);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
spinner.text = `Vercel: ${state.toLowerCase()}...`;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// retry
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
spinner.warn("Vercel: timed out waiting (5 min)");
|
|
96
|
+
}
|
|
97
|
+
async function watchRailway() {
|
|
98
|
+
const token = process.env.RAILWAY_API_TOKEN;
|
|
99
|
+
const spinner = ora({ text: "Railway deploying...", indent: 2 }).start();
|
|
100
|
+
for (let i = 0; i < 60; i++) {
|
|
101
|
+
await sleep(5000);
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch("https://backboard.railway.com/graphql/v2", {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer ${token}`,
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
query: `{
|
|
111
|
+
me {
|
|
112
|
+
projects(first: 1) {
|
|
113
|
+
edges {
|
|
114
|
+
node {
|
|
115
|
+
deployments(first: 1) {
|
|
116
|
+
edges {
|
|
117
|
+
node { status }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}`,
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
const data = (await res.json());
|
|
128
|
+
const deployment = data.data?.me?.projects?.edges?.[0]?.node?.deployments?.edges?.[0]
|
|
129
|
+
?.node;
|
|
130
|
+
if (!deployment)
|
|
131
|
+
continue;
|
|
132
|
+
const status = deployment.status;
|
|
133
|
+
if (status === "SUCCESS") {
|
|
134
|
+
spinner.succeed(`Railway ${chalk.green("deployed")}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
else if (status === "FAILED" || status === "CRASHED") {
|
|
138
|
+
spinner.fail(`Railway deploy ${chalk.red(status.toLowerCase())}`);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
spinner.text = `Railway: ${status.toLowerCase()}...`;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// retry
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
spinner.warn("Railway: timed out waiting (5 min)");
|
|
149
|
+
}
|
|
150
|
+
function sleep(ms) {
|
|
151
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
152
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { writeFileSync, readFileSync, existsSync } from "fs";
|
|
5
|
+
export const envCommand = new Command("env")
|
|
6
|
+
.description("Sync environment variables across services");
|
|
7
|
+
envCommand
|
|
8
|
+
.command("pull")
|
|
9
|
+
.description("Pull env vars from Vercel and Railway into a local .env file")
|
|
10
|
+
.option("-o, --output <file>", "output file", ".env.pulled")
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
const vars = {};
|
|
13
|
+
const spinner = ora("Pulling env vars...").start();
|
|
14
|
+
// Pull from Vercel
|
|
15
|
+
if (process.env.VERCEL_TOKEN) {
|
|
16
|
+
spinner.text = "Pulling from Vercel...";
|
|
17
|
+
try {
|
|
18
|
+
const projectId = process.env.VERCEL_PROJECT_ID;
|
|
19
|
+
const url = projectId
|
|
20
|
+
? `https://api.vercel.com/v9/projects/${projectId}/env`
|
|
21
|
+
: "https://api.vercel.com/v9/projects";
|
|
22
|
+
if (!projectId) {
|
|
23
|
+
spinner.warn("Vercel: set VERCEL_PROJECT_ID to pull env vars (skipping)");
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok)
|
|
30
|
+
throw new Error(`HTTP ${res.status}`);
|
|
31
|
+
const data = (await res.json());
|
|
32
|
+
const envVars = data.envs ?? [];
|
|
33
|
+
for (const env of envVars) {
|
|
34
|
+
if (env.value) {
|
|
35
|
+
vars[env.key] = { value: env.value, source: "vercel" };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.log(chalk.dim(` Vercel: pulled ${envVars.length} variables`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.log(chalk.yellow(` Vercel: ${err.message}`));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(chalk.dim(" ○ Vercel skipped (VERCEL_TOKEN not set)"));
|
|
47
|
+
}
|
|
48
|
+
// Pull from Railway
|
|
49
|
+
if (process.env.RAILWAY_API_TOKEN) {
|
|
50
|
+
spinner.text = "Pulling from Railway...";
|
|
51
|
+
try {
|
|
52
|
+
const projectId = process.env.RAILWAY_PROJECT_ID;
|
|
53
|
+
const environmentId = process.env.RAILWAY_ENVIRONMENT_ID;
|
|
54
|
+
const serviceId = process.env.RAILWAY_SERVICE_ID;
|
|
55
|
+
if (!projectId || !environmentId || !serviceId) {
|
|
56
|
+
console.log(chalk.dim(" ○ Railway: set RAILWAY_PROJECT_ID, RAILWAY_ENVIRONMENT_ID, RAILWAY_SERVICE_ID to pull env vars"));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const res = await fetch("https://backboard.railway.com/graphql/v2", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${process.env.RAILWAY_API_TOKEN}`,
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
query: `query {
|
|
67
|
+
variables(
|
|
68
|
+
projectId: "${projectId}",
|
|
69
|
+
environmentId: "${environmentId}",
|
|
70
|
+
serviceId: "${serviceId}"
|
|
71
|
+
)
|
|
72
|
+
}`,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok)
|
|
76
|
+
throw new Error(`HTTP ${res.status}`);
|
|
77
|
+
const data = (await res.json());
|
|
78
|
+
const railwayVars = data.data?.variables ?? {};
|
|
79
|
+
for (const [key, value] of Object.entries(railwayVars)) {
|
|
80
|
+
if (!vars[key]) {
|
|
81
|
+
vars[key] = { value: value, source: "railway" };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk.dim(` Railway: pulled ${Object.keys(railwayVars).length} variables`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.log(chalk.yellow(` Railway: ${err.message}`));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(chalk.dim(" ○ Railway skipped (RAILWAY_API_TOKEN not set)"));
|
|
93
|
+
}
|
|
94
|
+
spinner.stop();
|
|
95
|
+
// Write to file
|
|
96
|
+
const lines = Object.entries(vars)
|
|
97
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
98
|
+
.map(([key, { value, source }]) => `# source: ${source}\n${key}=${value}`);
|
|
99
|
+
if (lines.length === 0) {
|
|
100
|
+
console.log(chalk.yellow("\n No env vars pulled."));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
writeFileSync(opts.output, lines.join("\n\n") + "\n");
|
|
104
|
+
console.log(`\n ${chalk.green("✓")} Wrote ${Object.keys(vars).length} variables to ${chalk.bold(opts.output)}`);
|
|
105
|
+
});
|
|
106
|
+
envCommand
|
|
107
|
+
.command("diff")
|
|
108
|
+
.description("Compare local .env with remote services")
|
|
109
|
+
.option("-f, --file <file>", "local env file to compare", ".env")
|
|
110
|
+
.action(async (opts) => {
|
|
111
|
+
if (!existsSync(opts.file)) {
|
|
112
|
+
console.log(chalk.red(` File not found: ${opts.file}`));
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const local = parseEnvFile(readFileSync(opts.file, "utf-8"));
|
|
117
|
+
console.log(`\n Local ${chalk.bold(opts.file)}: ${Object.keys(local).length} variables`);
|
|
118
|
+
console.log(chalk.dim(" ─────────────────────────────────────────"));
|
|
119
|
+
for (const [key] of Object.entries(local).sort(([a], [b]) => a.localeCompare(b))) {
|
|
120
|
+
console.log(` ${chalk.dim("•")} ${key}`);
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
});
|
|
124
|
+
function parseEnvFile(content) {
|
|
125
|
+
const vars = {};
|
|
126
|
+
for (const line of content.split("\n")) {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
129
|
+
continue;
|
|
130
|
+
const eqIdx = trimmed.indexOf("=");
|
|
131
|
+
if (eqIdx === -1)
|
|
132
|
+
continue;
|
|
133
|
+
vars[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
|
|
134
|
+
}
|
|
135
|
+
return vars;
|
|
136
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { loadConfig, enabledServices } from "../lib/config.js";
|
|
5
|
+
import { getChecker, allCheckerNames } from "../services/registry.js";
|
|
6
|
+
const STATUS_ICON = {
|
|
7
|
+
healthy: chalk.green("✓"),
|
|
8
|
+
degraded: chalk.yellow("~"),
|
|
9
|
+
down: chalk.red("✗"),
|
|
10
|
+
skipped: chalk.dim("○"),
|
|
11
|
+
};
|
|
12
|
+
const STATUS_COLOR = {
|
|
13
|
+
healthy: chalk.green,
|
|
14
|
+
degraded: chalk.yellow,
|
|
15
|
+
down: chalk.red,
|
|
16
|
+
skipped: chalk.dim,
|
|
17
|
+
};
|
|
18
|
+
export const healthCommand = new Command("health")
|
|
19
|
+
.description("Check the health of all connected services")
|
|
20
|
+
.option("-v, --verbose", "Show latency and extra detail")
|
|
21
|
+
.option("-a, --all", "Check all known services, not just configured ones")
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
const config = loadConfig();
|
|
24
|
+
const spinner = ora("Checking services...").start();
|
|
25
|
+
const serviceList = opts.all
|
|
26
|
+
? allCheckerNames()
|
|
27
|
+
: enabledServices(config);
|
|
28
|
+
if (serviceList.length === 0) {
|
|
29
|
+
spinner.stop();
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(chalk.yellow(" No services configured."));
|
|
32
|
+
console.log(chalk.dim(` Run ${chalk.white("stk init")} to set up your project, or ${chalk.white("stk health --all")} to check everything.`));
|
|
33
|
+
console.log();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const checks = serviceList.map((name) => {
|
|
37
|
+
const checker = getChecker(name);
|
|
38
|
+
if (!checker) {
|
|
39
|
+
return Promise.resolve({
|
|
40
|
+
name,
|
|
41
|
+
status: "skipped",
|
|
42
|
+
detail: `unknown service "${name}"`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return checker();
|
|
46
|
+
});
|
|
47
|
+
const results = await Promise.all(checks);
|
|
48
|
+
spinner.stop();
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(chalk.bold(` ${config.name} — Service Health`));
|
|
51
|
+
console.log(chalk.dim(" ─────────────────────────────────────────"));
|
|
52
|
+
for (const r of results) {
|
|
53
|
+
const icon = STATUS_ICON[r.status];
|
|
54
|
+
const name = r.name.padEnd(16);
|
|
55
|
+
const latencyStr = opts.verbose && r.latency != null ? chalk.dim(` ${r.latency}ms`) : "";
|
|
56
|
+
const detail = r.detail ? chalk.dim(` ${r.detail}`) : "";
|
|
57
|
+
console.log(` ${icon} ${STATUS_COLOR[r.status](name)}${latencyStr}${detail}`);
|
|
58
|
+
}
|
|
59
|
+
console.log();
|
|
60
|
+
const down = results.filter((r) => r.status === "down");
|
|
61
|
+
const healthy = results.filter((r) => r.status === "healthy");
|
|
62
|
+
const skipped = results.filter((r) => r.status === "skipped");
|
|
63
|
+
if (down.length > 0) {
|
|
64
|
+
console.log(chalk.red(` ${down.length} service${down.length > 1 ? "s" : ""} down`));
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
|
67
|
+
else if (healthy.length === 0) {
|
|
68
|
+
console.log(chalk.yellow(" No services reachable — check your env vars"));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log(chalk.green(` All ${healthy.length} configured service${healthy.length > 1 ? "s" : ""} healthy`));
|
|
72
|
+
}
|
|
73
|
+
if (skipped.length > 0 && opts.verbose) {
|
|
74
|
+
console.log(chalk.dim(` ${skipped.length} skipped (missing env vars)`));
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { writeFileSync, existsSync } from "fs";
|
|
4
|
+
import { basename } from "path";
|
|
5
|
+
import { CONFIG_FILE, KNOWN_SERVICES } from "../lib/config.js";
|
|
6
|
+
import { templates, listTemplates } from "../templates/index.js";
|
|
7
|
+
const DEPLOY_PROVIDERS = ["railway", "vercel", "fly", "render", "aws"];
|
|
8
|
+
export const initCommand = new Command("init")
|
|
9
|
+
.description("Initialize stk config for the current project")
|
|
10
|
+
.option("--force", "overwrite existing config")
|
|
11
|
+
.option("-t, --template <name>", `use a starter template (${listTemplates().join(", ")})`)
|
|
12
|
+
.option("--list-templates", "show available templates")
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
// List templates
|
|
15
|
+
if (opts.listTemplates) {
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(chalk.bold(" Available Templates"));
|
|
18
|
+
console.log(chalk.dim(" ─────────────────────────────────────────"));
|
|
19
|
+
for (const [key, tpl] of Object.entries(templates)) {
|
|
20
|
+
console.log(` ${chalk.green(key.padEnd(12))} ${tpl.name}`);
|
|
21
|
+
console.log(` ${" ".repeat(12)} ${chalk.dim(tpl.description)}`);
|
|
22
|
+
}
|
|
23
|
+
console.log();
|
|
24
|
+
console.log(chalk.dim(` Usage: stk init --template saas`));
|
|
25
|
+
console.log();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (existsSync(CONFIG_FILE) && !opts.force) {
|
|
29
|
+
console.log(chalk.yellow(` ${CONFIG_FILE} already exists. Use --force to overwrite.`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const projectName = basename(process.cwd());
|
|
33
|
+
// Template-based init
|
|
34
|
+
if (opts.template) {
|
|
35
|
+
const tpl = templates[opts.template];
|
|
36
|
+
if (!tpl) {
|
|
37
|
+
console.log(chalk.red(` Unknown template: "${opts.template}"`));
|
|
38
|
+
console.log(chalk.dim(` Available: ${listTemplates().join(", ")}`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const config = {
|
|
42
|
+
...tpl.config,
|
|
43
|
+
name: projectName,
|
|
44
|
+
};
|
|
45
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(` ${chalk.green("✓")} Created ${chalk.bold(CONFIG_FILE)} from ${chalk.cyan(tpl.name)} template`);
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.bold(" Services:"));
|
|
50
|
+
for (const [name, enabled] of Object.entries(config.services)) {
|
|
51
|
+
const icon = enabled ? chalk.green("✓") : chalk.dim("○");
|
|
52
|
+
console.log(` ${icon} ${enabled ? chalk.white(name) : chalk.dim(name)}`);
|
|
53
|
+
}
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(chalk.dim(` Deploy branch: ${config.deploy?.branch ?? "main"}`));
|
|
56
|
+
console.log(chalk.dim(` Deploy providers: ${config.deploy?.providers?.join(", ") || "none"}`));
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(chalk.dim(` Set your env vars, then run ${chalk.white("stk health")} to verify.`));
|
|
59
|
+
console.log();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Auto-detect init
|
|
63
|
+
const detected = {};
|
|
64
|
+
const envChecks = {
|
|
65
|
+
railway: ["RAILWAY_API_TOKEN"],
|
|
66
|
+
vercel: ["VERCEL_TOKEN"],
|
|
67
|
+
fly: ["FLY_API_TOKEN"],
|
|
68
|
+
render: ["RENDER_API_KEY"],
|
|
69
|
+
aws: ["AWS_ACCESS_KEY_ID"],
|
|
70
|
+
database: ["DATABASE_URL"],
|
|
71
|
+
mongodb: ["MONGODB_URL", "MONGO_URL"],
|
|
72
|
+
redis: ["REDIS_URL"],
|
|
73
|
+
supabase: ["SUPABASE_URL"],
|
|
74
|
+
r2: ["CLOUDFLARE_ACCOUNT_ID"],
|
|
75
|
+
stripe: ["STRIPE_SECRET_KEY"],
|
|
76
|
+
};
|
|
77
|
+
for (const [service, vars] of Object.entries(envChecks)) {
|
|
78
|
+
if (vars.some((v) => process.env[v])) {
|
|
79
|
+
detected[service] = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const config = {
|
|
83
|
+
name: projectName,
|
|
84
|
+
services: {
|
|
85
|
+
...Object.fromEntries(KNOWN_SERVICES.map((s) => [s, false])),
|
|
86
|
+
...detected,
|
|
87
|
+
},
|
|
88
|
+
deploy: {
|
|
89
|
+
branch: "main",
|
|
90
|
+
providers: Object.keys(detected).filter((s) => DEPLOY_PROVIDERS.includes(s)),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
94
|
+
console.log();
|
|
95
|
+
console.log(` ${chalk.green("✓")} Created ${chalk.bold(CONFIG_FILE)}`);
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(chalk.bold(" Detected services:"));
|
|
98
|
+
const serviceNames = Object.entries(config.services);
|
|
99
|
+
for (const [name, enabled] of serviceNames) {
|
|
100
|
+
const icon = enabled ? chalk.green("✓") : chalk.dim("○");
|
|
101
|
+
const label = enabled
|
|
102
|
+
? chalk.white(name)
|
|
103
|
+
: chalk.dim(`${name} (not detected — enable in config)`);
|
|
104
|
+
console.log(` ${icon} ${label}`);
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(chalk.dim(` Edit ${CONFIG_FILE} to enable/disable services or add config.`));
|
|
108
|
+
console.log(chalk.dim(` Or try: ${chalk.white("stk init --template saas")} for a pre-configured stack.`));
|
|
109
|
+
console.log(chalk.dim(` Then run ${chalk.white("stk health")} to verify.`));
|
|
110
|
+
console.log();
|
|
111
|
+
});
|