| import { readProjectLink } from '../config.js'; | ||
| import { apiFromDeps } from './project.js'; | ||
| // If a gated control-plane action returned `approval_required`, print the guidance and signal "not done". | ||
| export function reportIfGated(res, deps) { | ||
| if (res && res.status === 'approval_required') { | ||
| deps.print(`⛔ ${res.action} requires approval (id ${res.approvalId}) — have a human run \`firth approve ${res.approvalId}\`, then re-run.`); | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| function linkedProjectId(deps) { | ||
| const link = readProjectLink(deps.cwd); | ||
| if (!link) { | ||
| deps.print('this directory is not linked — run `firth project link <id>`'); | ||
| return null; | ||
| } | ||
| return link.projectId; | ||
| } | ||
| export async function approvals(_argv, deps) { | ||
| const projectId = linkedProjectId(deps); | ||
| if (!projectId) | ||
| return 1; | ||
| const list = await apiFromDeps(deps).listApprovals(projectId, 'pending'); | ||
| if (list.length === 0) { | ||
| deps.print('no pending approvals'); | ||
| return 0; | ||
| } | ||
| for (const a of list) | ||
| deps.print(`${a.id} ${a.action} (requested ${a.requested_at})`); | ||
| return 0; | ||
| } | ||
| export async function approve(argv, deps) { | ||
| const id = argv[0]; | ||
| if (!id) { | ||
| deps.print('usage: firth approve <id>'); | ||
| return 1; | ||
| } | ||
| const projectId = linkedProjectId(deps); | ||
| if (!projectId) | ||
| return 1; | ||
| await apiFromDeps(deps).approve(projectId, id); | ||
| deps.print(`approved ${id}`); | ||
| return 0; | ||
| } | ||
| export async function deny(argv, deps) { | ||
| const id = argv[0]; | ||
| if (!id) { | ||
| deps.print('usage: firth deny <id>'); | ||
| return 1; | ||
| } | ||
| const projectId = linkedProjectId(deps); | ||
| if (!projectId) | ||
| return 1; | ||
| await apiFromDeps(deps).deny(projectId, id); | ||
| deps.print(`denied ${id}`); | ||
| return 0; | ||
| } | ||
| export async function policy(argv, deps) { | ||
| const projectId = linkedProjectId(deps); | ||
| if (!projectId) | ||
| return 1; | ||
| const api = apiFromDeps(deps); | ||
| if (argv[0] === 'set') { | ||
| const [, action, decision] = argv; | ||
| if (!action || !decision) { | ||
| deps.print('usage: firth policy set <action> <allow|deny|approve>'); | ||
| return 1; | ||
| } | ||
| const p = await api.setPolicy(projectId, action, decision); | ||
| for (const [a, d] of Object.entries(p)) | ||
| deps.print(`${a}: ${d}`); | ||
| return 0; | ||
| } | ||
| const p = await api.getPolicy(projectId); | ||
| for (const [a, d] of Object.entries(p)) | ||
| deps.print(`${a}: ${d}`); | ||
| return 0; | ||
| } |
+10
-1
@@ -70,4 +70,13 @@ const realFetcher = async (url, init) => { | ||
| const q = branch ? `?branch=${encodeURIComponent(branch)}` : ''; | ||
| return this.req('GET', `/projects/${projectId}/secrets${q}`).then((r) => r.secrets); | ||
| return this.req('GET', `/projects/${projectId}/secrets${q}`); | ||
| } | ||
| listApprovals(projectId, status) { | ||
| return this.req('GET', `/projects/${projectId}/approvals${status ? `?status=${status}` : ''}`).then((r) => r.approvals); | ||
| } | ||
| approve(projectId, id) { return this.req('POST', `/projects/${projectId}/approvals/${id}/approve`).then((r) => r.approval); } | ||
| deny(projectId, id) { return this.req('POST', `/projects/${projectId}/approvals/${id}/deny`).then((r) => r.approval); } | ||
| getPolicy(projectId) { return this.req('GET', `/projects/${projectId}/policy`).then((r) => r.policy); } | ||
| setPolicy(projectId, action, decision) { | ||
| return this.req('PUT', `/projects/${projectId}/policy/${action}`, { decision }).then((r) => r.policy); | ||
| } | ||
| deploy(projectId, opts) { | ||
@@ -74,0 +83,0 @@ return this.req('POST', `/projects/${projectId}/deploy`, opts); |
@@ -5,2 +5,3 @@ import { parseArgs } from 'node:util'; | ||
| import { formatTeardown } from './util.js'; | ||
| import { reportIfGated } from './govern.js'; | ||
| import { ensureFlyctl } from '../fly.js'; | ||
@@ -96,2 +97,4 @@ import { ensureSkills } from '../ensure-skills.js'; | ||
| const out = await apiFromDeps(deps).deleteBranch(projectId, target.id); | ||
| if (reportIfGated(out, deps)) | ||
| return 1; | ||
| // If the deleted branch is the current one, clear it | ||
@@ -98,0 +101,0 @@ const link = readProjectLink(deps.cwd); |
@@ -8,2 +8,3 @@ import { existsSync } from 'node:fs'; | ||
| import { apiFromDeps } from './project.js'; | ||
| import { reportIfGated } from './govern.js'; | ||
| export async function deploy(argv, deps) { | ||
@@ -34,2 +35,4 @@ const { values, positionals } = parseArgs({ args: argv, options: { | ||
| }); | ||
| if (reportIfGated(out, deps)) | ||
| return 1; | ||
| deps.print(`deployed machine ${out.machineId} → ${out.url}`); | ||
@@ -50,2 +53,4 @@ return 0; | ||
| const out = await api.deploy(link.projectId, { image: imageRef, from, branch, port }); | ||
| if (reportIfGated(out, deps)) | ||
| return 1; | ||
| deps.print(`deployed machine ${out.machineId} → ${out.url}`); | ||
@@ -52,0 +57,0 @@ deps.print(`image: ${imageRef} (built remotely)`); |
@@ -7,2 +7,3 @@ import { parseArgs } from 'node:util'; | ||
| import { ensureSkills } from '../ensure-skills.js'; | ||
| import { reportIfGated } from './govern.js'; | ||
| // Build a FirthApi from stored config; tests can override via deps.makeApi. | ||
@@ -80,2 +81,4 @@ export function apiFromDeps(deps) { | ||
| const out = await apiFromDeps(deps).deleteProject(link.projectId); | ||
| if (reportIfGated(out, deps)) | ||
| return 1; | ||
| clearProjectLink(deps.cwd); | ||
@@ -82,0 +85,0 @@ deps.print(`deleted project ${link.projectId}${formatTeardown(out.teardown ?? {})}; unlinked ./.firth/project.json`); |
@@ -6,2 +6,3 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; | ||
| import { apiFromDeps } from './project.js'; | ||
| import { reportIfGated } from './govern.js'; | ||
| export async function secrets(argv, deps) { | ||
@@ -28,4 +29,8 @@ const { values } = parseArgs({ args: argv, options: { branch: { type: 'string' } }, allowPositionals: false }); | ||
| const project = await api.getSecrets(link.projectId); | ||
| if (reportIfGated(project, deps)) | ||
| return 1; | ||
| const branch = await api.getSecrets(link.projectId, target.id); | ||
| const bundle = { ...project, ...branch }; | ||
| if (reportIfGated(branch, deps)) | ||
| return 1; | ||
| const bundle = { ...(project.secrets ?? {}), ...(branch.secrets ?? {}) }; | ||
| // Merge Firth-managed keys into any existing .env, preserving user-added lines/comments. | ||
@@ -32,0 +37,0 @@ const path = join(deps.cwd, '.env'); |
+9
-0
@@ -15,2 +15,3 @@ #!/usr/bin/env node | ||
| import { status } from './commands/status.js'; | ||
| import { approvals, approve, deny, policy } from './commands/govern.js'; | ||
| import { defaultRunner } from './fly.js'; | ||
@@ -46,2 +47,6 @@ import { defaultBuildRunner } from './flyctl-build.js'; | ||
| branch delete <name> Delete a branch + its Neon branch (--yes) | ||
| approvals List pending approvals | ||
| approve <id> Approve a pending request | ||
| deny <id> Deny a pending request | ||
| policy [set <a> <d>] Show or set the project's govern policy | ||
| --help Show this help | ||
@@ -67,2 +72,6 @@ --version, -v Print the CLI version`; | ||
| COMMANDS['status'] = status; | ||
| COMMANDS['approvals'] = approvals; | ||
| COMMANDS['approve'] = approve; | ||
| COMMANDS['deny'] = deny; | ||
| COMMANDS['policy'] = policy; | ||
| export async function route(argv, deps) { | ||
@@ -69,0 +78,0 @@ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') { |
+1
-1
| { | ||
| "name": "firth", | ||
| "version": "0.0.4", | ||
| "version": "0.0.5", | ||
| "description": "The Firth CLI — provision and govern a project's cloud resources (Neon Postgres, Tigris storage, Fly.io compute) behind one credential seam.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
57895
7.76%22
4.76%984
12.84%