Skip to content

Commit acf01c8

Browse files
committed
feat: insta secrets set/unset (user-managed secrets)
Add `insta secrets set <name> [value] [--branch]` and `secrets unset <name> [--branch]`, project-wide by default; value comes from the argument or stdin (keeps secrets out of shell history). Both are handleApproval-aware (202). Extend `policy set`'s help text with the secrets.write action. Also fix a latent commander bug that silently dropped `--branch`/`--org` when passed to a subcommand whose parent group (secrets, billing) declares the same flag name for its own default action — enable positional options so a flag written after the subcommand name binds to the subcommand, not the group.
1 parent 5681a64 commit acf01c8

2 files changed

Lines changed: 42 additions & 2 deletions

File tree

src/commands/secrets.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { writeFile } from 'node:fs/promises'
22
import { ApiClient, requireProject } from '../api.js'
3-
import { info, printJson, serializeEnv, handleApproval } from '../util.js'
3+
import { info, printJson, serializeEnv, handleApproval, die } from '../util.js'
44

55
function q(branch?: string): string {
66
return branch ? `?branch=${encodeURIComponent(branch)}` : ''
@@ -29,3 +29,31 @@ export async function secretsList(opts: { branch?: string }): Promise<void> {
2929
if (handleApproval(res)) return
3030
for (const name of Object.keys(res.body.secrets)) info(name)
3131
}
32+
33+
async function readStdin(): Promise<string> {
34+
let data = ''
35+
for await (const chunk of process.stdin) data += chunk
36+
return data.trim()
37+
}
38+
39+
// Set a user secret. Project-wide by default; --branch scopes it to one branch. Value comes from
40+
// the argument, or stdin when omitted (keeps secret values out of shell history).
41+
export async function secretsSet(name: string, value: string | undefined, opts: { branch?: string }): Promise<void> {
42+
const api = await ApiClient.load()
43+
const p = await requireProject()
44+
const v = value ?? (await readStdin())
45+
if (!v) die('value is required (pass as an argument or on stdin)')
46+
const payload: Record<string, string> = opts.branch ? { value: v, branch: opts.branch } : { value: v }
47+
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}`, payload)
48+
if (handleApproval(res)) return
49+
info(`set ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`)
50+
}
51+
52+
export async function secretsUnset(name: string, opts: { branch?: string }): Promise<void> {
53+
const api = await ApiClient.load()
54+
const p = await requireProject()
55+
const qs = opts.branch ? `?branch=${encodeURIComponent(opts.branch)}` : ''
56+
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}${qs}`)
57+
if (handleApproval(res)) return
58+
info(`unset ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`)
59+
}

src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ const guard = (fn: (...a: any[]) => Promise<unknown>) => (...a: any[]): Promise<
2727
fn(...a).then(() => undefined).catch(onError)
2828

2929
const program = new Command()
30+
// Positional options: some command groups (e.g. `secrets`, `billing`) declare a flag (like
31+
// --branch or --org) both on the group itself (for its own default action) and on a subcommand
32+
// of that group. Without this, commander lets the group's own option greedily match the flag
33+
// no matter where it appears, so e.g. `secrets set NAME val --branch b` silently drops --branch
34+
// into the (unused) group-level options instead of the subcommand's. Positional parsing makes a
35+
// group's own options only match before the subcommand name, so occurrences after it are matched
36+
// against the subcommand's own (identically-named) option instead.
37+
program.enablePositionalOptions()
3038
// Version resolution: INSTA_CLI_VERSION (baked into the standalone binary via bun build --define) →
3139
// the installed package.json (npm/node — ../package.json sits beside dist/) → 0.0.0.
3240
function resolveVersion(): string {
@@ -83,6 +91,10 @@ const sec = program.command('secrets').description('Fetch the credential bundle
8391
.option('--branch <branch>').option('-o, --output <file>', 'output file (default .env)').option('--print', 'print instead of writing').option('--json')
8492
.action(guard((o) => secretsCmd.secrets(o)))
8593
sec.command('list').description('List secret names only').option('--branch <branch>').action(guard((o) => secretsCmd.secretsList(o)))
94+
sec.command('set <name> [value]').description('Set a user secret (project-wide; value from stdin if omitted)')
95+
.option('--branch <branch>', 'scope to one branch').action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o)))
96+
sec.command('unset <name>').description('Remove a user secret')
97+
.option('--branch <branch>', 'scope to one branch').action(guard((n, o) => secretsCmd.secretsUnset(n, o)))
8698

8799
// ---- deploy ----
88100
program.command('deploy [dir]').description('Deploy a source directory (built remotely on Fly) or a prebuilt --image to a branch compute group')
@@ -140,6 +152,6 @@ ob.command('sync').description('Upload findings into the project timeline').acti
140152
// ---- policy ----
141153
const pol = program.command('policy').description('Governance policy')
142154
pol.command('get').option('--json').action(guard((o) => govern.policyGet(o)))
143-
pol.command('set <action> <decision>').description('action: secrets.read|deploy|project.delete|branch.delete|service.add|service.remove|service.scale|service.upgrade; decision: allow|deny|approve').action(guard((a, d) => govern.policySet(a, d)))
155+
pol.command('set <action> <decision>').description('action: secrets.read|secrets.write|deploy|project.delete|branch.delete|service.add|service.remove|service.scale|service.upgrade; decision: allow|deny|approve').action(guard((a, d) => govern.policySet(a, d)))
144156

145157
program.parseAsync(process.argv)

0 commit comments

Comments
 (0)