firth 0.0.1 → 0.0.3

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/dist/config.js ADDED
@@ -0,0 +1,66 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ // Production control plane (InsForge compute). Override with `firth login --api-url`
5
+ // or FIRTH_API_URL=… for local dev against http://localhost:8080.
6
+ const DEFAULT_API = 'https://firth-control-plane-0662c2ef-202a-4feb-8267-5501b3b60037.fly.dev';
7
+ const gpath = (home) => join(home, '.firth', 'config.json');
8
+ const lpath = (cwd) => join(cwd, '.firth', 'project.json');
9
+ export function readConfig(home = homedir(), env = process.env) {
10
+ let file = {};
11
+ const p = gpath(home);
12
+ if (existsSync(p))
13
+ file = JSON.parse(readFileSync(p, 'utf8'));
14
+ return { ...file, apiUrl: env.FIRTH_API_URL ?? file.apiUrl ?? DEFAULT_API };
15
+ }
16
+ export function writeConfig(cfg, home = homedir()) {
17
+ mkdirSync(join(home, '.firth'), { recursive: true });
18
+ writeFileSync(gpath(home), JSON.stringify(cfg, null, 2));
19
+ }
20
+ export function readProjectLink(cwd = process.cwd()) {
21
+ const p = lpath(cwd);
22
+ return existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : null;
23
+ }
24
+ // One-time marker so related-skill installation runs once per linked project, not on every command.
25
+ export function markSkillsInstalled(cwd = process.cwd()) {
26
+ const link = readProjectLink(cwd);
27
+ if (!link)
28
+ return;
29
+ link.skillsInstalled = true;
30
+ writeFileSync(lpath(cwd), JSON.stringify(link, null, 2));
31
+ }
32
+ export function writeProjectLink(projectId, cwd = process.cwd()) {
33
+ mkdirSync(join(cwd, '.firth'), { recursive: true });
34
+ writeFileSync(lpath(cwd), JSON.stringify({ projectId }, null, 2));
35
+ }
36
+ export function setCurrentBranch(branch, cwd = process.cwd()) {
37
+ const link = readProjectLink(cwd);
38
+ if (!link)
39
+ throw new Error('not linked');
40
+ if (branch !== null) {
41
+ link.branch = branch;
42
+ }
43
+ else {
44
+ delete link.branch;
45
+ }
46
+ writeFileSync(lpath(cwd), JSON.stringify(link, null, 2));
47
+ }
48
+ export function clearProjectLink(cwd = process.cwd()) {
49
+ const p = lpath(cwd);
50
+ if (existsSync(p))
51
+ unlinkSync(p);
52
+ }
53
+ // Append any missing entries to the project's ./.gitignore (creating it if absent).
54
+ // Idempotent: entries already present are left alone. Returns the entries it added.
55
+ export function ensureGitignore(cwd, entries) {
56
+ const p = join(cwd, '.gitignore');
57
+ const existing = existsSync(p) ? readFileSync(p, 'utf8') : '';
58
+ const have = new Set(existing.split('\n').map((l) => l.trim()));
59
+ const missing = entries.filter((e) => !have.has(e));
60
+ if (missing.length === 0)
61
+ return [];
62
+ const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
63
+ const block = `${prefix}\n# Firth: agent skills installed by \`firth\` / \`npx skills add\` (regenerable, not source)\n${missing.join('\n')}\n`;
64
+ writeFileSync(p, existing + block);
65
+ return missing;
66
+ }
@@ -0,0 +1,47 @@
1
+ import { readProjectLink, markSkillsInstalled, ensureGitignore } from './config.js';
2
+ // Where `npx skills add` / `firth skills pull` drop skills, per detected agent.
3
+ // These are regenerable agent context, not the developer's source — keep them out of git.
4
+ const SKILL_DIRS = ['.claude/skills/', '.agents/skills/', '.github/skills/'];
5
+ // Agent skills installed once per linked project so the developer's agent has
6
+ // Neon / Tigris / Firth context. Run via `npx skills add` (vercel-labs/skills),
7
+ // fully non-interactively: pin the agents (Claude Code → `.claude/skills/`, Codex
8
+ // → `.agents/skills/`; both already gitignored) so there's no agent prompt and the
9
+ // tool doesn't fan out to ~13–72 agent dirs; explicit `-s` skill names (no skill
10
+ // prompt); `-y` (no scope/confirm prompt); `--copy` (real files, not symlinks into
11
+ // a transient package cache).
12
+ const AGENT_FLAGS = ['-a', 'claude-code', '-a', 'codex', '-y', '--copy'];
13
+ const SKILLS = [
14
+ { label: 'neon-postgres', args: ['skills', 'add', 'neondatabase/agent-skills', '-s', 'neon-postgres', ...AGENT_FLAGS] },
15
+ { label: 'tigris', args: ['skills', 'add', 'tigrisdata/skills',
16
+ '-s', 'tigris-object-operations', '-s', 'file-storage', '-s', 'tigris-sdk-guide',
17
+ '-s', 'tigris-security-access-control', '-s', 'tigris-image-optimization',
18
+ '-s', 'tigris-s3-migration', '-s', 'tigris-static-assets', '-s', 'tigris-agent-kit',
19
+ ...AGENT_FLAGS] },
20
+ { label: 'firth', args: ['skills', 'add', 'firthstack/firth', '-s', 'firth', ...AGENT_FLAGS] },
21
+ ];
22
+ // Install the related agent skills once per linked project. No-op unless deps.run is set
23
+ // (production wires the real runner; tests inject a fake; other command tests omit it → no-op,
24
+ // so nothing is spawned). Convenience only — never blocks or fails the host command.
25
+ export async function ensureSkills(deps) {
26
+ const run = deps.run;
27
+ if (!run)
28
+ return;
29
+ try {
30
+ const link = readProjectLink(deps.cwd);
31
+ if (!link || link.skillsInstalled)
32
+ return; // not linked yet, or already done
33
+ deps.print('installing related agent skills (neon, tigris, firth) via `npx skills add` …');
34
+ for (const s of SKILLS) {
35
+ const r = await run('npx', s.args, true); // streamed so the user sees progress
36
+ deps.print(r.ok ? ` ${s.label} ✓` : ` ${s.label} failed — add manually: npx ${s.args.join(' ')}`);
37
+ }
38
+ // keep the installed (regenerable) skill folders out of the developer's git history
39
+ const added = ensureGitignore(deps.cwd, SKILL_DIRS);
40
+ if (added.length)
41
+ deps.print(` .gitignore += ${added.join(', ')}`);
42
+ markSkillsInstalled(deps.cwd); // mark done even on partial failure so we don't re-run every command
43
+ }
44
+ catch {
45
+ /* convenience only — never block the command */
46
+ }
47
+ }
package/dist/fly.js ADDED
@@ -0,0 +1,25 @@
1
+ import { spawn } from 'node:child_process';
2
+ export const defaultRunner = (cmd, args, inherit = false) => new Promise((resolve) => {
3
+ const p = spawn(cmd, args, { stdio: inherit ? 'inherit' : 'ignore' });
4
+ p.on('error', () => resolve({ ok: false })); // ENOENT etc.
5
+ p.on('close', (code) => resolve({ ok: code === 0 }));
6
+ });
7
+ // Ensure flyctl is installed. No-op unless deps.run is set (production wires defaultRunner;
8
+ // tests inject a fake; other command tests omit it → this is a no-op so nothing is spawned).
9
+ export async function ensureFlyctl(deps) {
10
+ const run = deps.run;
11
+ if (!run)
12
+ return;
13
+ try {
14
+ if ((await run('flyctl', ['version'])).ok)
15
+ return; // already installed
16
+ if (!(await run('brew', ['--version'])).ok) { // no Homebrew → just hint
17
+ deps.print('note: flyctl (fly CLI) not found and Homebrew is unavailable — install it: https://fly.io/docs/flyctl/install/');
18
+ return;
19
+ }
20
+ deps.print('flyctl not found — installing with `brew install flyctl` (one-time)…');
21
+ const r = await run('brew', ['install', 'flyctl'], true);
22
+ deps.print(r.ok ? 'flyctl installed ✓' : 'flyctl install failed — install manually: https://fly.io/docs/flyctl/install/');
23
+ }
24
+ catch { /* convenience only — never block the command */ }
25
+ }
package/dist/index.js ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { homedir } from 'node:os';
6
+ import { login, logout } from './commands/auth.js';
7
+ import { projectCreate, projectLink, projectList, projectDelete } from './commands/project.js';
8
+ import { branchCreate, branchList, branchSwitch, branchDelete } from './commands/branch.js';
9
+ import { secrets } from './commands/secrets.js';
10
+ import { skillsPull } from './commands/skills.js';
11
+ import { deploy } from './commands/deploy.js';
12
+ import { events } from './commands/events.js';
13
+ import { observeSync } from './commands/observe.js';
14
+ import { status } from './commands/status.js';
15
+ import { defaultRunner } from './fly.js';
16
+ function readVersion() {
17
+ try {
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version ?? '0.0.0';
20
+ }
21
+ catch {
22
+ return '0.0.0';
23
+ }
24
+ }
25
+ const USAGE = `firth <command>
26
+
27
+ Commands:
28
+ login Sign in (--email, --password; --api-url <url> sets the control-plane host)
29
+ logout Clear stored credentials
30
+ project create <name> Create + link a project
31
+ project link <id> Link this directory to a project
32
+ project list List your projects
33
+ branch create <name> Create a branch (--from <parent>, default main)
34
+ branch list List the linked project's branches
35
+ secrets Fetch the linked project's secrets into .env (--branch <id>)
36
+ skills pull Install the firth skill into ./.claude/skills
37
+ deploy Deploy --image <url> to the project's compute (--from, --port)
38
+ events Show the project's action↔side-effect timeline (--branch, --limit)
39
+ observe sync Upload local observe-hook findings (.firth/audit.jsonl) to the timeline
40
+ status Show login, linked project, and current branch
41
+ project delete Delete the linked project + all resources (--yes)
42
+ branch switch <name> Set the current branch (secrets/events default to it)
43
+ branch delete <name> Delete a branch + its Neon branch (--yes)
44
+ --help Show this help
45
+ --version, -v Print the CLI version`;
46
+ // Command handlers registered by later tasks. Each: (argv, deps) => Promise<number>.
47
+ export const COMMANDS = {};
48
+ COMMANDS['login'] = login;
49
+ COMMANDS['logout'] = logout;
50
+ COMMANDS['project create'] = projectCreate;
51
+ COMMANDS['project link'] = projectLink;
52
+ COMMANDS['project list'] = projectList;
53
+ COMMANDS['branch create'] = branchCreate;
54
+ COMMANDS['branch list'] = branchList;
55
+ COMMANDS['secrets'] = secrets;
56
+ COMMANDS['skills pull'] = skillsPull;
57
+ COMMANDS['deploy'] = deploy;
58
+ COMMANDS['events'] = events;
59
+ COMMANDS['observe sync'] = observeSync;
60
+ COMMANDS['branch switch'] = branchSwitch;
61
+ COMMANDS['branch delete'] = branchDelete;
62
+ COMMANDS['project delete'] = projectDelete;
63
+ COMMANDS['status'] = status;
64
+ export async function route(argv, deps) {
65
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
66
+ deps.print(USAGE);
67
+ return 0;
68
+ }
69
+ if (argv[0] === '--version' || argv[0] === '-v') {
70
+ deps.print(readVersion());
71
+ return 0;
72
+ }
73
+ // Support two-word commands ("project create") and one-word ("login").
74
+ const key2 = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : '';
75
+ const handler = COMMANDS[key2] ?? COMMANDS[argv[0]];
76
+ if (!handler) {
77
+ deps.print(`unknown command: ${argv.join(' ')}\n\n${USAGE}`);
78
+ return 1;
79
+ }
80
+ const rest = COMMANDS[key2] ? argv.slice(2) : argv.slice(1);
81
+ try {
82
+ return await handler(rest, deps);
83
+ }
84
+ catch (e) {
85
+ deps.print(`error: ${e instanceof Error ? e.message : String(e)}`);
86
+ return 1;
87
+ }
88
+ }
89
+ export async function main() {
90
+ const code = await route(process.argv.slice(2), {
91
+ print: (s) => console.log(s), home: homedir(), cwd: process.cwd(), env: process.env,
92
+ run: defaultRunner,
93
+ });
94
+ process.exit(code);
95
+ }
96
+ if (process.env.NODE_ENV !== 'test') {
97
+ void main();
98
+ }
@@ -0,0 +1,58 @@
1
+ # Firth Skills
2
+
3
+ This directory holds the **Skills bundle** for Firth — Markdown files in the [Anthropic Skills format](https://docs.claude.com) that teach AI coding agents how to operate cloud platforms in the context of a Firth project.
4
+
5
+ ## Layout (planned)
6
+
7
+ ```
8
+ skills/
9
+ ├── README.md ← you are here
10
+ ├── stack-overview/
11
+ │ └── SKILL.md ← what stack this project uses, why
12
+ ├── deploy-flow/
13
+ │ └── SKILL.md ← how `firth deploy` actually works
14
+ ├── debug-runbook/
15
+ │ └── SKILL.md ← what to do when a deploy / migration / build fails
16
+ ├── cost-and-scaling/
17
+ │ └── SKILL.md ← free-tier limits, when to scale up, how
18
+ ├── handoff/
19
+ │ └── SKILL.md ← how to produce a context dump for a fresh agent
20
+
21
+ ├── neon/ ← provider-specific Skills
22
+ │ ├── connection-pooling/
23
+ │ │ └── SKILL.md
24
+ │ └── ...
25
+ ├── vercel/
26
+ │ └── ...
27
+ ├── railway/
28
+ │ └── ...
29
+ └── ...
30
+ ```
31
+
32
+ ## Skill format
33
+
34
+ Each Skill is a single `SKILL.md` file with YAML frontmatter:
35
+
36
+ ```markdown
37
+ ---
38
+ name: <skill-name>
39
+ description: <one-line description used by the agent to decide whether to invoke this Skill>
40
+ ---
41
+
42
+ # <Skill title>
43
+
44
+ <Markdown body — instructions, examples, gotchas, references to firth CLI commands>
45
+ ```
46
+
47
+ See the Anthropic Skills documentation for full schema details.
48
+
49
+ ## Authoring guidelines (early draft)
50
+
51
+ - **One Skill, one capability.** Don't write a 2,000-line "everything about Vercel" Skill. Split by task: `vercel-deploy`, `vercel-env-vars`, `vercel-debug-build`.
52
+ - **Reference the Firth CLI.** Skills should tell the agent which `firth` command to invoke, not how to call provider APIs directly. Provider-specific knowledge lives behind the CLI.
53
+ - **Include known failure modes.** A Skill is most valuable when it teaches the agent what can go wrong and how to recover.
54
+ - **Keep examples runnable.** Code blocks should be copy-pasteable; agents will execute them.
55
+
56
+ ## Status
57
+
58
+ Empty for now. The first batch of Skills will land alongside the first golden path (Next.js + Hono + Neon + Vercel + Railway).
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: firth
3
+ description: Use when working in a project managed by Firth — provisioning DB/storage/compute, creating/switching/merging branches, deploying, checking status, or wiring app secrets via the firth CLI.
4
+ ---
5
+
6
+ # Firth
7
+
8
+ Firth provisions and governs a project's cloud resources behind one CLI and one credential seam. The CLI talks **only** to the Firth control plane; you never configure a cloud backend directly.
9
+
10
+ ## Resources Firth provisions
11
+ Every project automatically gets three base resources — build your app directly against them:
12
+ - **Postgres database** (Neon) — your app's relational DB.
13
+ - **S3-compatible storage** (Tigris) — object/blob storage.
14
+ - **Compute** (Fly.io) — where your container runs.
15
+
16
+ `firth secrets` writes the connection credentials for all three into `./.env`.
17
+
18
+ ## Setup
19
+ - `firth login --email <e> --password <p>` — sign in. Add `--api-url <url>` to target a non-local control plane; it persists for later commands.
20
+ - `firth status` — login state, linked project, current branch.
21
+ - `firth --version`.
22
+
23
+ ## Workflow
24
+ 1. **Create or link a project** — you end up linked (`./.firth/project.json`) and on the default branch `main`:
25
+ - `firth project create <name>` — provisions DB + storage + compute.
26
+ - `firth project link <id>` — link an existing project.
27
+ 2. `firth secrets` — write the current branch's credentials into `./.env`. **This is how an agent gets DB/storage access.** (`--branch <id>` targets a specific branch.)
28
+ 3. `firth deploy --image <url>` — deploy a container image to the **current branch's** compute (`--port`; `--from <branch>` targets a specific branch's app instead). See **Deploying your app** for frontend/backend patterns.
29
+ 4. `firth events` — the action ↔ resource-side-effect timeline (`--branch`, `--limit`).
30
+
31
+ ## Database & migrations
32
+ You connect **directly** to the Postgres database (the `DATABASE_URL` from `firth secrets` — there is no ORM/abstraction in front of it). Keep a **`migrations/` directory at the project root** to store and track your database migration files, so schema changes are versioned and reproducible — this is essential for applying the same schema to a branch DB and re-applying it on `main` after a merge.
33
+
34
+ ## Deploying your app (frontend & backend)
35
+ Firth compute is a **container** running on Fly.io — **one container per branch**, exposed over HTTPS at `https://<app>.fly.dev` on the single port you choose. Deploy is **image-based**: build a container image, push it to a registry your runtime can pull, then:
36
+
37
+ ```
38
+ firth deploy --image <registry/image:tag> --port <n> # runs it on the CURRENT branch's compute
39
+ # --from <branch> targets a specific branch; the URL is printed on success
40
+ ```
41
+
42
+ **Secrets are injected for you.** At deploy time Firth decrypts the branch's credentials (`DATABASE_URL`, `AWS_*`, `BUCKET_NAME`, …) and passes them into the container as **environment variables**. Read them from the process environment in production — do **not** bake `./.env` into the image (that file, from `firth secrets`, is for *local* development only).
43
+
44
+ ### Backend
45
+ Containerize your server so it listens on the port you pass to `--port` and reads credentials from the environment:
46
+
47
+ ```dockerfile
48
+ FROM node:20-alpine
49
+ WORKDIR /app
50
+ COPY package*.json ./
51
+ RUN npm ci --omit=dev
52
+ COPY . .
53
+ EXPOSE 8080
54
+ CMD ["node", "server.js"] # reads process.env.DATABASE_URL etc.
55
+ ```
56
+ `firth deploy --image <registry>/api:tag --port 8080`
57
+
58
+ ### Frontend (SPA)
59
+ Build the static assets, then serve them from a tiny static-server container. Rewrite unknown paths to `index.html` so client-side routing works:
60
+
61
+ ```dockerfile
62
+ FROM node:20-alpine AS build
63
+ WORKDIR /app
64
+ COPY package*.json ./
65
+ RUN npm ci
66
+ COPY . .
67
+ RUN npm run build # produces ./dist
68
+
69
+ FROM caddy:alpine
70
+ COPY --from=build /app/dist /srv
71
+ # Caddyfile: `:80 { root * /srv; try_files {path} /index.html; file_server }`
72
+ COPY Caddyfile /etc/caddy/Caddyfile
73
+ EXPOSE 80
74
+ ```
75
+ `firth deploy --image <registry>/web:tag --port 80`
76
+
77
+ ### Full-stack — one branch = one container + one port
78
+ A branch's compute serves **one app on one port**, so ship frontend + backend as a **single image**: have your backend serve the built frontend's static files (framework SSR, or copy the SPA's `dist/` into the server's static directory). One `firth deploy`, one URL, and the frontend can call the backend at the same origin. If you genuinely need separate frontend and backend services, host the static frontend on a dedicated static/CDN host and deploy only the backend container to Firth compute.
79
+
80
+ ## Branching — isolate risky changes
81
+ Before a high-risk change (schema migration, data backfill, risky refactor), do the work on a **branch**, verify it, then merge back to `main`.
82
+
83
+ - `firth branch create <name>` provisions an **isolated environment** for the branch: a new **Neon DB branch** (a full copy of the parent's data, isolated from `main`, with its own `DATABASE_URL`) **and a new dedicated compute** (its own Fly app). **Only the storage bucket is shared** across branches.
84
+ - Each branch has its own compute app, so multiple branches' environments run **in parallel** — working on one branch never disturbs another's. To rebuild the branch's running environment, **redeploy your branch's code** to its app (`firth deploy` targets the current branch's compute).
85
+ - `firth branch switch <name>` then `firth secrets` → `./.env` now has the branch's `DATABASE_URL`. Run your migrations against the branch DB, then `firth deploy` → the branch's isolated compute runs your branch code, an environment to validate the change.
86
+
87
+ ### Merging a branch back to main
88
+ Firth does **not** auto-merge — you do it in your local code repo:
89
+ 1. Merge the branch's frontend + backend **code** into `main`.
90
+ 2. Merge the branch's **migration files** into `main`'s `migrations/`.
91
+ 3. Switch to main: `firth branch switch main` → `firth secrets`.
92
+ 4. Re-run the DB **migrations** against `main`'s database, then **re-deploy** compute: `firth deploy`.
93
+
94
+ ## Delete (destructive — require `--yes`)
95
+ - `firth project delete --yes` — tears down ALL resources (Neon DB, Fly app, Tigris bucket) and unlinks the directory.
96
+ - `firth branch delete <name> --yes` — tears down the branch's Neon branch **and its Fly app**. The default branch can't be deleted.
97
+
98
+ ## Rules for agents
99
+ - Treat `./.env` as the **only** source of resource credentials — run `firth secrets` to populate it; never hardcode credential values or print them.
100
+ - `DATABASE_URL` **and compute** are isolated **per branch**; storage credentials (`AWS_*` / `BUCKET_NAME`) are **shared** across branches.
101
+ - Track all DB schema changes as files under `migrations/` so they can be replayed on a branch DB and on `main` after merge.
102
+ - The CLI auto-installs `flyctl` (via Homebrew) when missing during project/branch commands, so the Fly app is manageable directly if needed.
@@ -0,0 +1,46 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const statePath = (cwd) => join(cwd, '.firth', 'sync-state.json');
4
+ export function readAuditOffset(cwd) {
5
+ const p = statePath(cwd);
6
+ if (!existsSync(p))
7
+ return 0;
8
+ try {
9
+ const s = JSON.parse(readFileSync(p, 'utf8'));
10
+ return typeof s?.audit?.offset === 'number' ? s.audit.offset : 0;
11
+ }
12
+ catch {
13
+ return 0;
14
+ }
15
+ }
16
+ export function writeAuditOffset(cwd, offset, now) {
17
+ mkdirSync(join(cwd, '.firth'), { recursive: true });
18
+ writeFileSync(statePath(cwd), JSON.stringify({ audit: { offset, syncedAt: now } }, null, 2));
19
+ }
20
+ // Complete (non-blank) lines from `offset` to the last newline boundary.
21
+ // `ends[i]` = byte offset just past `lines[i]`; `newOffset` = byte offset past
22
+ // the last complete line (incl. any blank lines). A trailing partial line is
23
+ // excluded. `offset > byteLength(content)` (truncation) restarts from 0.
24
+ export function readNewAuditLines(content, offset) {
25
+ const byteLen = Buffer.byteLength(content, 'utf8');
26
+ const start = offset > byteLen ? 0 : offset;
27
+ const tail = Buffer.from(content, 'utf8').subarray(start).toString('utf8');
28
+ const lastNl = tail.lastIndexOf('\n');
29
+ if (lastNl < 0)
30
+ return { lines: [], ends: [], newOffset: start };
31
+ const block = tail.slice(0, lastNl + 1); // complete lines incl. trailing newline
32
+ const raw = block.split('\n');
33
+ if (raw[raw.length - 1] === '')
34
+ raw.pop(); // drop empty tail after the final '\n'
35
+ const lines = [];
36
+ const ends = [];
37
+ let cursor = start;
38
+ for (const l of raw) {
39
+ cursor += Buffer.byteLength(l, 'utf8') + 1; // + the newline
40
+ if (l.trim()) {
41
+ lines.push(l);
42
+ ends.push(cursor);
43
+ }
44
+ }
45
+ return { lines, ends, newOffset: cursor };
46
+ }
package/package.json CHANGED
@@ -1,45 +1,20 @@
1
1
  {
2
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.",
3
+ "version": "0.0.3",
4
+ "description": "The Firth CLI provision and govern a project's cloud resources (Neon Postgres, Tigris storage, Fly.io compute) behind one credential seam.",
5
5
  "type": "module",
6
- "bin": {
7
- "firth": "dist/cli.js"
8
- },
9
- "files": [
10
- "dist",
11
- "README.md"
12
- ],
6
+ "bin": { "firth": "dist/index.js" },
7
+ "files": ["dist", "README.md"],
8
+ "engines": { "node": ">=20" },
9
+ "license": "MIT",
10
+ "keywords": ["firth", "cli", "ai-agent", "neon", "tigris", "fly", "deploy", "secrets", "branching"],
11
+ "repository": { "type": "git", "url": "git+https://github.com/firthstack/firth.git", "directory": "cli" },
13
12
  "scripts": {
14
- "dev": "tsx src/cli.ts",
15
- "build": "tsup",
16
- "typecheck": "tsc --noEmit",
13
+ "build": "tsc -p tsconfig.json && node -e \"require('node:fs').cpSync('../skills','dist/skills',{recursive:true})\"",
17
14
  "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"
15
+ "dev": "tsx src/index.ts",
16
+ "prepublishOnly": "npm run build"
37
17
  },
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
- }
18
+ "dependencies": { "@insforge/sdk": "^1.4.2" },
19
+ "devDependencies": { "@types/node": "^20.14.0", "tsx": "^4.16.0", "typescript": "^5.5.0", "vitest": "^2.0.0" }
45
20
  }