@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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/commands/deploy.d.ts +2 -0
  4. package/dist/commands/deploy.js +152 -0
  5. package/dist/commands/env.d.ts +2 -0
  6. package/dist/commands/env.js +136 -0
  7. package/dist/commands/health.d.ts +2 -0
  8. package/dist/commands/health.js +77 -0
  9. package/dist/commands/init.d.ts +2 -0
  10. package/dist/commands/init.js +111 -0
  11. package/dist/commands/logs.d.ts +2 -0
  12. package/dist/commands/logs.js +151 -0
  13. package/dist/commands/status.d.ts +2 -0
  14. package/dist/commands/status.js +130 -0
  15. package/dist/commands/todo.d.ts +2 -0
  16. package/dist/commands/todo.js +187 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +22 -0
  19. package/dist/lib/config.d.ts +36 -0
  20. package/dist/lib/config.js +102 -0
  21. package/dist/services/aws.d.ts +2 -0
  22. package/dist/services/aws.js +32 -0
  23. package/dist/services/checker.d.ts +10 -0
  24. package/dist/services/checker.js +20 -0
  25. package/dist/services/database.d.ts +2 -0
  26. package/dist/services/database.js +27 -0
  27. package/dist/services/fly.d.ts +2 -0
  28. package/dist/services/fly.js +17 -0
  29. package/dist/services/mongodb.d.ts +2 -0
  30. package/dist/services/mongodb.js +25 -0
  31. package/dist/services/r2.d.ts +2 -0
  32. package/dist/services/r2.js +20 -0
  33. package/dist/services/railway.d.ts +2 -0
  34. package/dist/services/railway.js +26 -0
  35. package/dist/services/redis.d.ts +2 -0
  36. package/dist/services/redis.js +34 -0
  37. package/dist/services/registry.d.ts +4 -0
  38. package/dist/services/registry.js +37 -0
  39. package/dist/services/render.d.ts +2 -0
  40. package/dist/services/render.js +19 -0
  41. package/dist/services/stripe.d.ts +2 -0
  42. package/dist/services/stripe.js +21 -0
  43. package/dist/services/supabase.d.ts +2 -0
  44. package/dist/services/supabase.js +24 -0
  45. package/dist/services/vercel.d.ts +2 -0
  46. package/dist/services/vercel.js +35 -0
  47. package/dist/templates/index.d.ts +8 -0
  48. package/dist/templates/index.js +105 -0
  49. package/package.json +55 -0
@@ -0,0 +1,10 @@
1
+ export interface CheckResult {
2
+ name: string;
3
+ status: "healthy" | "degraded" | "down" | "skipped";
4
+ latency?: number;
5
+ detail?: string;
6
+ }
7
+ export type ServiceChecker = () => Promise<CheckResult>;
8
+ export declare function runCheck(name: string, fn: () => Promise<{
9
+ detail?: string;
10
+ }>): Promise<CheckResult>;
@@ -0,0 +1,20 @@
1
+ export async function runCheck(name, fn) {
2
+ const start = Date.now();
3
+ try {
4
+ const result = await fn();
5
+ return {
6
+ name,
7
+ status: "healthy",
8
+ latency: Date.now() - start,
9
+ detail: result.detail,
10
+ };
11
+ }
12
+ catch (err) {
13
+ return {
14
+ name,
15
+ status: "down",
16
+ latency: Date.now() - start,
17
+ detail: err.message ?? String(err),
18
+ };
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkDatabase(): Promise<CheckResult>;
@@ -0,0 +1,27 @@
1
+ import { runCheck } from "./checker.js";
2
+ export async function checkDatabase() {
3
+ const url = process.env.DATABASE_URL;
4
+ if (!url) {
5
+ return { name: "PostgreSQL", status: "skipped", detail: "DATABASE_URL not set" };
6
+ }
7
+ return runCheck("PostgreSQL", async () => {
8
+ // Parse host and port from DATABASE_URL to do a basic TCP connect check
9
+ // Full query check would require pg client — keeping deps minimal for now
10
+ const parsed = new URL(url);
11
+ const host = parsed.hostname;
12
+ const port = parseInt(parsed.port || "5432", 10);
13
+ const { createConnection } = await import("net");
14
+ await new Promise((resolve, reject) => {
15
+ const socket = createConnection({ host, port, timeout: 5000 }, () => {
16
+ socket.destroy();
17
+ resolve();
18
+ });
19
+ socket.on("error", reject);
20
+ socket.on("timeout", () => {
21
+ socket.destroy();
22
+ reject(new Error("connection timeout"));
23
+ });
24
+ });
25
+ return { detail: `${host}:${port} reachable` };
26
+ });
27
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkFly(): Promise<CheckResult>;
@@ -0,0 +1,17 @@
1
+ import { runCheck } from "./checker.js";
2
+ export async function checkFly() {
3
+ const token = process.env.FLY_API_TOKEN;
4
+ if (!token) {
5
+ return { name: "Fly.io", status: "skipped", detail: "FLY_API_TOKEN not set" };
6
+ }
7
+ return runCheck("Fly.io", async () => {
8
+ const res = await fetch("https://api.machines.dev/v1/apps", {
9
+ headers: { Authorization: `Bearer ${token}` },
10
+ });
11
+ if (!res.ok)
12
+ throw new Error(`HTTP ${res.status}`);
13
+ const data = (await res.json());
14
+ const apps = data.total_apps ?? data.length ?? 0;
15
+ return { detail: `${apps} app${apps !== 1 ? "s" : ""} found` };
16
+ });
17
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkMongoDB(): Promise<CheckResult>;
@@ -0,0 +1,25 @@
1
+ import { runCheck } from "./checker.js";
2
+ export async function checkMongoDB() {
3
+ const url = process.env.MONGODB_URL ?? process.env.MONGO_URL;
4
+ if (!url) {
5
+ return { name: "MongoDB", status: "skipped", detail: "MONGODB_URL not set" };
6
+ }
7
+ return runCheck("MongoDB", async () => {
8
+ const parsed = new URL(url);
9
+ const host = parsed.hostname;
10
+ const port = parseInt(parsed.port || "27017", 10);
11
+ const { createConnection } = await import("net");
12
+ await new Promise((resolve, reject) => {
13
+ const socket = createConnection({ host, port, timeout: 5000 }, () => {
14
+ socket.destroy();
15
+ resolve();
16
+ });
17
+ socket.on("error", reject);
18
+ socket.on("timeout", () => {
19
+ socket.destroy();
20
+ reject(new Error("connection timeout"));
21
+ });
22
+ });
23
+ return { detail: `${host}:${port} reachable` };
24
+ });
25
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkR2(): Promise<CheckResult>;
@@ -0,0 +1,20 @@
1
+ import { runCheck } from "./checker.js";
2
+ export async function checkR2() {
3
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
4
+ const apiToken = process.env.CLOUDFLARE_API_TOKEN;
5
+ if (!accountId || !apiToken) {
6
+ return {
7
+ name: "R2 Storage",
8
+ status: "skipped",
9
+ detail: "CLOUDFLARE_ACCOUNT_ID or CLOUDFLARE_API_TOKEN not set",
10
+ };
11
+ }
12
+ return runCheck("R2 Storage", async () => {
13
+ const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets`, { headers: { Authorization: `Bearer ${apiToken}` } });
14
+ if (!res.ok)
15
+ throw new Error(`HTTP ${res.status}`);
16
+ const data = await res.json();
17
+ const count = data.result?.length ?? 0;
18
+ return { detail: `${count} bucket${count !== 1 ? "s" : ""} accessible` };
19
+ });
20
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkRailway(): Promise<CheckResult>;
@@ -0,0 +1,26 @@
1
+ import { runCheck } from "./checker.js";
2
+ const RAILWAY_API = "https://backboard.railway.com/graphql/v2";
3
+ export async function checkRailway() {
4
+ const token = process.env.RAILWAY_API_TOKEN;
5
+ if (!token) {
6
+ return { name: "Railway API", status: "skipped", detail: "RAILWAY_API_TOKEN not set" };
7
+ }
8
+ return runCheck("Railway API", async () => {
9
+ const res = await fetch(RAILWAY_API, {
10
+ method: "POST",
11
+ headers: {
12
+ Authorization: `Bearer ${token}`,
13
+ "Content-Type": "application/json",
14
+ },
15
+ body: JSON.stringify({
16
+ query: `{ me { name email } }`,
17
+ }),
18
+ });
19
+ if (!res.ok)
20
+ throw new Error(`HTTP ${res.status}`);
21
+ const data = await res.json();
22
+ if (data.errors)
23
+ throw new Error(data.errors[0].message);
24
+ return { detail: `logged in as ${data.data.me.name}` };
25
+ });
26
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkRedis(): Promise<CheckResult>;
@@ -0,0 +1,34 @@
1
+ import { runCheck } from "./checker.js";
2
+ export async function checkRedis() {
3
+ const url = process.env.REDIS_URL;
4
+ if (!url) {
5
+ return { name: "Redis", status: "skipped", detail: "REDIS_URL not set" };
6
+ }
7
+ return runCheck("Redis", async () => {
8
+ const parsed = new URL(url);
9
+ const host = parsed.hostname;
10
+ const port = parseInt(parsed.port || "6379", 10);
11
+ const { createConnection } = await import("net");
12
+ await new Promise((resolve, reject) => {
13
+ const socket = createConnection({ host, port, timeout: 5000 }, () => {
14
+ // Send PING, expect +PONG
15
+ socket.write("PING\r\n");
16
+ socket.once("data", (data) => {
17
+ socket.destroy();
18
+ if (data.toString().includes("+PONG")) {
19
+ resolve();
20
+ }
21
+ else {
22
+ reject(new Error("unexpected response: " + data.toString().trim()));
23
+ }
24
+ });
25
+ });
26
+ socket.on("error", reject);
27
+ socket.on("timeout", () => {
28
+ socket.destroy();
29
+ reject(new Error("connection timeout"));
30
+ });
31
+ });
32
+ return { detail: `${host}:${port} responding` };
33
+ });
34
+ }
@@ -0,0 +1,4 @@
1
+ import type { CheckResult } from "./checker.js";
2
+ export type HealthChecker = () => Promise<CheckResult>;
3
+ export declare function getChecker(service: string): HealthChecker | null;
4
+ export declare function allCheckerNames(): string[];
@@ -0,0 +1,37 @@
1
+ import { checkRailway } from "./railway.js";
2
+ import { checkDatabase } from "./database.js";
3
+ import { checkRedis } from "./redis.js";
4
+ import { checkVercel } from "./vercel.js";
5
+ import { checkR2 } from "./r2.js";
6
+ import { checkStripe } from "./stripe.js";
7
+ import { checkFly } from "./fly.js";
8
+ import { checkRender } from "./render.js";
9
+ import { checkSupabase } from "./supabase.js";
10
+ import { checkAWS } from "./aws.js";
11
+ import { checkMongoDB } from "./mongodb.js";
12
+ /**
13
+ * Registry mapping service names to their health check functions.
14
+ * New services just need to be added here.
15
+ */
16
+ const registry = {
17
+ // Deploy providers
18
+ railway: checkRailway,
19
+ vercel: checkVercel,
20
+ fly: checkFly,
21
+ render: checkRender,
22
+ aws: checkAWS,
23
+ // Databases
24
+ database: checkDatabase,
25
+ mongodb: checkMongoDB,
26
+ redis: checkRedis,
27
+ supabase: checkSupabase,
28
+ // Storage & billing
29
+ r2: checkR2,
30
+ stripe: checkStripe,
31
+ };
32
+ export function getChecker(service) {
33
+ return registry[service] ?? null;
34
+ }
35
+ export function allCheckerNames() {
36
+ return Object.keys(registry);
37
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkRender(): Promise<CheckResult>;
@@ -0,0 +1,19 @@
1
+ import { runCheck } from "./checker.js";
2
+ export async function checkRender() {
3
+ const token = process.env.RENDER_API_KEY;
4
+ if (!token) {
5
+ return { name: "Render", status: "skipped", detail: "RENDER_API_KEY not set" };
6
+ }
7
+ return runCheck("Render", async () => {
8
+ const res = await fetch("https://api.render.com/v1/services?limit=1", {
9
+ headers: { Authorization: `Bearer ${token}` },
10
+ });
11
+ if (!res.ok)
12
+ throw new Error(`HTTP ${res.status}`);
13
+ const data = (await res.json());
14
+ const svc = data[0]?.service;
15
+ if (!svc)
16
+ return { detail: "no services found" };
17
+ return { detail: `${svc.name} — ${svc.suspended === "not_suspended" ? "running" : svc.suspended}` };
18
+ });
19
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkStripe(): Promise<CheckResult>;
@@ -0,0 +1,21 @@
1
+ import { runCheck } from "./checker.js";
2
+ export async function checkStripe() {
3
+ const key = process.env.STRIPE_SECRET_KEY;
4
+ if (!key) {
5
+ return { name: "Stripe", status: "skipped", detail: "STRIPE_SECRET_KEY not set" };
6
+ }
7
+ return runCheck("Stripe", async () => {
8
+ const res = await fetch("https://api.stripe.com/v1/balance", {
9
+ headers: { Authorization: `Bearer ${key}` },
10
+ });
11
+ if (!res.ok)
12
+ throw new Error(`HTTP ${res.status}`);
13
+ const data = await res.json();
14
+ const mode = key.startsWith("sk_live") ? "live" : "test";
15
+ const available = data.available?.[0];
16
+ const detail = available
17
+ ? `${mode} mode — balance: ${(available.amount / 100).toFixed(2)} ${available.currency.toUpperCase()}`
18
+ : `${mode} mode — API reachable`;
19
+ return { detail };
20
+ });
21
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkSupabase(): Promise<CheckResult>;
@@ -0,0 +1,24 @@
1
+ import { runCheck } from "./checker.js";
2
+ export async function checkSupabase() {
3
+ const url = process.env.SUPABASE_URL;
4
+ const key = process.env.SUPABASE_SERVICE_KEY ?? process.env.SUPABASE_ANON_KEY;
5
+ if (!url || !key) {
6
+ return {
7
+ name: "Supabase",
8
+ status: "skipped",
9
+ detail: "SUPABASE_URL or SUPABASE_SERVICE_KEY not set",
10
+ };
11
+ }
12
+ return runCheck("Supabase", async () => {
13
+ // Hit the REST health endpoint
14
+ const res = await fetch(`${url}/rest/v1/`, {
15
+ headers: {
16
+ apikey: key,
17
+ Authorization: `Bearer ${key}`,
18
+ },
19
+ });
20
+ if (!res.ok)
21
+ throw new Error(`HTTP ${res.status}`);
22
+ return { detail: "API reachable" };
23
+ });
24
+ }
@@ -0,0 +1,2 @@
1
+ import { type CheckResult } from "./checker.js";
2
+ export declare function checkVercel(): Promise<CheckResult>;
@@ -0,0 +1,35 @@
1
+ import { runCheck } from "./checker.js";
2
+ export async function checkVercel() {
3
+ const token = process.env.VERCEL_TOKEN;
4
+ if (!token) {
5
+ return { name: "Vercel", status: "skipped", detail: "VERCEL_TOKEN not set" };
6
+ }
7
+ return runCheck("Vercel", async () => {
8
+ // Get latest deployment
9
+ const res = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
10
+ headers: { Authorization: `Bearer ${token}` },
11
+ });
12
+ if (!res.ok)
13
+ throw new Error(`HTTP ${res.status}`);
14
+ const data = await res.json();
15
+ const dep = data.deployments?.[0];
16
+ if (!dep)
17
+ return { detail: "no deployments found" };
18
+ const ago = timeSince(new Date(dep.created));
19
+ const state = dep.readyState ?? dep.state ?? "unknown";
20
+ return { detail: `last deploy: ${state} (${ago} ago)` };
21
+ });
22
+ }
23
+ function timeSince(date) {
24
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
25
+ if (seconds < 60)
26
+ return `${seconds}s`;
27
+ const minutes = Math.floor(seconds / 60);
28
+ if (minutes < 60)
29
+ return `${minutes}m`;
30
+ const hours = Math.floor(minutes / 60);
31
+ if (hours < 24)
32
+ return `${hours}h`;
33
+ const days = Math.floor(hours / 24);
34
+ return `${days}d`;
35
+ }
@@ -0,0 +1,8 @@
1
+ import type { StkConfig } from "../lib/config.js";
2
+ export interface Template {
3
+ name: string;
4
+ description: string;
5
+ config: StkConfig;
6
+ }
7
+ export declare const templates: Record<string, Template>;
8
+ export declare function listTemplates(): string[];
@@ -0,0 +1,105 @@
1
+ export const templates = {
2
+ saas: {
3
+ name: "SaaS Starter",
4
+ description: "Full SaaS stack — Vercel frontend, Railway backend, PostgreSQL, Redis, Stripe billing, R2 storage",
5
+ config: {
6
+ name: "my-saas",
7
+ services: {
8
+ vercel: true,
9
+ railway: true,
10
+ database: true,
11
+ redis: true,
12
+ stripe: true,
13
+ r2: true,
14
+ },
15
+ deploy: {
16
+ branch: "main",
17
+ providers: ["vercel", "railway"],
18
+ },
19
+ },
20
+ },
21
+ api: {
22
+ name: "API Service",
23
+ description: "Backend API — Railway or Render, PostgreSQL, Redis cache",
24
+ config: {
25
+ name: "my-api",
26
+ services: {
27
+ railway: true,
28
+ database: true,
29
+ redis: true,
30
+ },
31
+ deploy: {
32
+ branch: "main",
33
+ providers: ["railway"],
34
+ },
35
+ },
36
+ },
37
+ fullstack: {
38
+ name: "Fullstack App",
39
+ description: "Vercel + Railway + Supabase with auth and storage built-in",
40
+ config: {
41
+ name: "my-app",
42
+ services: {
43
+ vercel: true,
44
+ railway: true,
45
+ supabase: true,
46
+ stripe: true,
47
+ },
48
+ deploy: {
49
+ branch: "main",
50
+ providers: ["vercel", "railway"],
51
+ },
52
+ },
53
+ },
54
+ static: {
55
+ name: "Static / Jamstack",
56
+ description: "Vercel-only static site with optional Stripe for payments",
57
+ config: {
58
+ name: "my-site",
59
+ services: {
60
+ vercel: true,
61
+ stripe: false,
62
+ },
63
+ deploy: {
64
+ branch: "main",
65
+ providers: ["vercel"],
66
+ },
67
+ },
68
+ },
69
+ fly: {
70
+ name: "Fly.io Stack",
71
+ description: "Fly.io deploy, PostgreSQL, Redis — edge-first architecture",
72
+ config: {
73
+ name: "my-fly-app",
74
+ services: {
75
+ fly: true,
76
+ database: true,
77
+ redis: true,
78
+ },
79
+ deploy: {
80
+ branch: "main",
81
+ providers: ["fly"],
82
+ },
83
+ },
84
+ },
85
+ aws: {
86
+ name: "AWS Stack",
87
+ description: "AWS infrastructure with PostgreSQL and Redis",
88
+ config: {
89
+ name: "my-aws-app",
90
+ services: {
91
+ aws: true,
92
+ database: true,
93
+ redis: true,
94
+ r2: false,
95
+ },
96
+ deploy: {
97
+ branch: "main",
98
+ providers: ["aws"],
99
+ },
100
+ },
101
+ },
102
+ };
103
+ export function listTemplates() {
104
+ return Object.keys(templates);
105
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@prajwolkc/stk",
3
+ "version": "0.1.0",
4
+ "description": "One CLI to deploy, monitor, and debug your entire stack. Health checks, deploy watching, env sync, logs, and GitHub issues — all from one command.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "prajwolkc",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Harden43/stk"
11
+ },
12
+ "keywords": [
13
+ "cli",
14
+ "devops",
15
+ "deploy",
16
+ "monitoring",
17
+ "railway",
18
+ "vercel",
19
+ "fly",
20
+ "render",
21
+ "supabase",
22
+ "aws",
23
+ "stripe",
24
+ "health-check",
25
+ "infrastructure",
26
+ "developer-tools"
27
+ ],
28
+ "bin": {
29
+ "stk": "./dist/index.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc",
41
+ "dev": "tsx src/index.ts",
42
+ "start": "node dist/index.js",
43
+ "prepublishOnly": "npm run build"
44
+ },
45
+ "dependencies": {
46
+ "chalk": "^5.4.1",
47
+ "commander": "^13.1.0",
48
+ "ora": "^8.2.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.13.0",
52
+ "tsx": "^4.19.0",
53
+ "typescript": "^5.7.0"
54
+ }
55
+ }