firth 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.
Files changed (3) hide show
  1. package/README.md +76 -0
  2. package/dist/cli.js +183 -0
  3. package/package.json +45 -0
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # firth-cli
2
+
3
+ > The runtime CLI for [Firth](https://github.com/firthdev/firth) — the cloud platform SDK for AI coding agents.
4
+
5
+ **Status:** Pre-alpha. v0.0.1 ships only `firth init` (project scaffolding).
6
+
7
+ This repo is the **L2 / CLI layer** of the Firth project. The companion repo [`firth`](https://github.com/firthdev/firth) holds the L1 / Knowledge layer (Skills, templates, runbooks, ARCHITECTURE.md).
8
+
9
+ For the project's overall design and rationale, see [`firth/ARCHITECTURE.md`](https://github.com/firthdev/firth/blob/main/ARCHITECTURE.md). This README is just for the CLI itself.
10
+
11
+ ## Local development
12
+
13
+ ```bash
14
+ # from this directory
15
+ npm install
16
+
17
+ # run the CLI in dev (no build step)
18
+ npm run dev -- init
19
+
20
+ # typecheck
21
+ npm run typecheck
22
+
23
+ # tests
24
+ npm test
25
+
26
+ # build a distributable
27
+ npm run build
28
+
29
+ # link into your shell so `firth` works globally during dev
30
+ npm link
31
+ firth init my-test-app
32
+ ```
33
+
34
+ ## Commands (current)
35
+
36
+ ### `firth init [name]`
37
+
38
+ Scaffold a Firth project. Generates `firth.config.ts` and `firth.lock.json`.
39
+
40
+ ```bash
41
+ # interactive
42
+ firth init my-app
43
+
44
+ # in current directory
45
+ firth init .
46
+
47
+ # non-interactive (agent-friendly): use defaults
48
+ firth init my-app --yes
49
+
50
+ # non-interactive with explicit overrides
51
+ firth init my-app --frontend=nextjs --backend=hono --db=neon \
52
+ --frontend-host=vercel --backend-host=railway --yes
53
+ ```
54
+
55
+ Defaults (when `--yes` is passed): Next.js + Hono + Neon Postgres + Vercel + Railway.
56
+
57
+ ## Commands (planned)
58
+
59
+ - `firth deploy` — provision resources and push code across the stack.
60
+ - `firth secrets set/get/list` — sync secrets across providers.
61
+ - `firth logs [--service]` — tail logs.
62
+ - `firth status` — current deployment + resource state.
63
+ - `firth handoff` — generate a context dump for a fresh agent session.
64
+ - `firth db migrate / db reset` — database lifecycle.
65
+
66
+ ## Design notes
67
+
68
+ - **Thin orchestrator, not a wrapper.** Every command shells out to the official provider CLI/API; we never re-implement provider features.
69
+ - **Agent-friendly errors.** Failures emit `ERROR / LIKELY CAUSE / SUGGESTED ACTIONS` so an agent loop can recover.
70
+ - **Local state lives in the project.** `firth.config.ts` (declarative, hand-edited) + `firth.lock.json` (generated, holds resource IDs) — both committed.
71
+
72
+ See [`firth/ARCHITECTURE.md`](https://github.com/firthdev/firth/blob/main/ARCHITECTURE.md) for the full rationale.
73
+
74
+ ## License
75
+
76
+ MIT (planned).
package/dist/cli.js ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { defineCommand as defineCommand2, runMain } from "citty";
5
+
6
+ // src/commands/init.ts
7
+ import { defineCommand } from "citty";
8
+ import * as p from "@clack/prompts";
9
+ import { writeFile, mkdir } from "fs/promises";
10
+ import { existsSync } from "fs";
11
+ import { resolve, basename } from "path";
12
+ var initCommand = defineCommand({
13
+ meta: {
14
+ name: "init",
15
+ description: "Scaffold Firth project files (firth.config.ts + firth.lock.json) in the target directory."
16
+ },
17
+ args: {
18
+ name: {
19
+ type: "positional",
20
+ description: "Project directory (use '.' for current directory)",
21
+ required: false,
22
+ default: "."
23
+ },
24
+ yes: {
25
+ type: "boolean",
26
+ alias: "y",
27
+ description: "Skip all prompts; use defaults (Next.js + Hono + Neon + Vercel + Railway). Safe for non-interactive agent runs.",
28
+ default: false
29
+ },
30
+ frontend: {
31
+ type: "string",
32
+ description: "Frontend framework override (nextjs)"
33
+ },
34
+ backend: {
35
+ type: "string",
36
+ description: "Backend framework override (hono | express | none)"
37
+ },
38
+ db: {
39
+ type: "string",
40
+ description: "Database provider override (neon | none)"
41
+ },
42
+ "frontend-host": {
43
+ type: "string",
44
+ description: "Frontend host override (vercel | none)"
45
+ },
46
+ "backend-host": {
47
+ type: "string",
48
+ description: "Backend host override (railway | none)"
49
+ }
50
+ },
51
+ async run({ args }) {
52
+ const rawName = String(args.name);
53
+ const targetDir = resolve(process.cwd(), rawName);
54
+ const projectName = rawName === "." ? basename(targetDir) : basename(targetDir);
55
+ p.intro("firth init");
56
+ const configPath = resolve(targetDir, "firth.config.ts");
57
+ if (existsSync(configPath)) {
58
+ p.cancel(
59
+ [
60
+ "ERROR: firth.config.ts already exists.",
61
+ `LOCATION: ${configPath}`,
62
+ "SUGGESTED ACTIONS:",
63
+ " 1. Edit firth.config.ts by hand to change the stack.",
64
+ " 2. Or delete firth.config.ts and re-run `firth init`."
65
+ ].join("\n")
66
+ );
67
+ process.exit(1);
68
+ }
69
+ const stack = args.yes ? defaultStack() : await promptStack(args);
70
+ if (!stack) {
71
+ p.cancel("Cancelled.");
72
+ process.exit(0);
73
+ }
74
+ const config = { project: projectName, stack };
75
+ const lock = { version: 1, resources: {} };
76
+ if (!existsSync(targetDir)) {
77
+ await mkdir(targetDir, { recursive: true });
78
+ }
79
+ await writeFile(configPath, renderConfig(config), "utf8");
80
+ await writeFile(
81
+ resolve(targetDir, "firth.lock.json"),
82
+ JSON.stringify(lock, null, 2) + "\n",
83
+ "utf8"
84
+ );
85
+ p.outro(
86
+ [
87
+ `OK: wrote firth.config.ts and firth.lock.json to ${targetDir}`,
88
+ "",
89
+ "NEXT STEPS:",
90
+ " 1. Review firth.config.ts and adjust the stack if needed.",
91
+ " 2. Run `firth deploy` to provision and ship (coming soon)."
92
+ ].join("\n")
93
+ );
94
+ }
95
+ });
96
+ function defaultStack() {
97
+ return {
98
+ frontend: "nextjs",
99
+ backend: "hono",
100
+ db: "neon",
101
+ frontendHost: "vercel",
102
+ backendHost: "railway"
103
+ };
104
+ }
105
+ async function promptStack(args) {
106
+ const frontend = args.frontend ?? await p.select({
107
+ message: "Frontend framework?",
108
+ options: [{ value: "nextjs", label: "Next.js" }],
109
+ initialValue: "nextjs"
110
+ });
111
+ if (p.isCancel(frontend)) return null;
112
+ const backend = args.backend ?? await p.select({
113
+ message: "Backend framework?",
114
+ options: [
115
+ { value: "hono", label: "Hono (recommended)" },
116
+ { value: "express", label: "Express" },
117
+ { value: "none", label: "None (frontend-only)" }
118
+ ],
119
+ initialValue: "hono"
120
+ });
121
+ if (p.isCancel(backend)) return null;
122
+ const db = args.db ?? await p.select({
123
+ message: "Database?",
124
+ options: [
125
+ { value: "neon", label: "Neon Postgres" },
126
+ { value: "none", label: "None" }
127
+ ],
128
+ initialValue: "neon"
129
+ });
130
+ if (p.isCancel(db)) return null;
131
+ const frontendHost = args["frontend-host"] ?? await p.select({
132
+ message: "Frontend hosting?",
133
+ options: [
134
+ { value: "vercel", label: "Vercel" },
135
+ { value: "none", label: "Not yet" }
136
+ ],
137
+ initialValue: "vercel"
138
+ });
139
+ if (p.isCancel(frontendHost)) return null;
140
+ let backendHost;
141
+ if (backend === "none") {
142
+ backendHost = "none";
143
+ } else {
144
+ const result = args["backend-host"] ?? await p.select({
145
+ message: "Backend hosting?",
146
+ options: [
147
+ { value: "railway", label: "Railway" },
148
+ { value: "none", label: "Not yet" }
149
+ ],
150
+ initialValue: "railway"
151
+ });
152
+ if (p.isCancel(result)) return null;
153
+ backendHost = String(result);
154
+ }
155
+ return {
156
+ frontend: String(frontend),
157
+ backend: String(backend),
158
+ db: String(db),
159
+ frontendHost: String(frontendHost),
160
+ backendHost
161
+ };
162
+ }
163
+ function renderConfig(config) {
164
+ return `// firth.config.ts
165
+ // Generated by \`firth init\`. Source-of-truth for this project's stack.
166
+ // Hand-edit, then run \`firth deploy\` to apply changes.
167
+
168
+ export default ${JSON.stringify(config, null, 2)};
169
+ `;
170
+ }
171
+
172
+ // src/cli.ts
173
+ var main = defineCommand2({
174
+ meta: {
175
+ name: "firth",
176
+ version: "0.0.1",
177
+ description: "Cloud platform SDK for AI coding agents."
178
+ },
179
+ subCommands: {
180
+ init: initCommand
181
+ }
182
+ });
183
+ runMain(main);
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "firth",
3
+ "version": "0.0.1",
4
+ "description": "Cloud platform SDK for AI coding agents — scaffold, deploy, and operate cloud stacks alongside your AI coding agent.",
5
+ "type": "module",
6
+ "bin": {
7
+ "firth": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "dev": "tsx src/cli.ts",
15
+ "build": "tsup",
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
19
+ },
20
+ "keywords": [
21
+ "cli",
22
+ "ai-agent",
23
+ "deploy",
24
+ "scaffold",
25
+ "vercel",
26
+ "railway",
27
+ "neon",
28
+ "firth"
29
+ ],
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "dependencies": {
35
+ "@clack/prompts": "^0.7.0",
36
+ "citty": "^0.1.6"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.11.0",
40
+ "tsup": "^8.0.2",
41
+ "tsx": "^4.7.1",
42
+ "typescript": "^5.4.0",
43
+ "vitest": "^1.4.0"
44
+ }
45
+ }