diff --git a/src/commands/secrets.ts b/src/commands/secrets.ts index 9747797..4f95008 100644 --- a/src/commands/secrets.ts +++ b/src/commands/secrets.ts @@ -94,6 +94,58 @@ export async function secretsUnset(name: string, opts: { branch?: string }): Pro info(`unset ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`) } +export async function secretsBind(envName: string, source: string, opts: { branch?: string; to?: string; sourceName?: string; json?: boolean }): Promise { + if (!opts.to) die('--to is required') + const api = await ApiClient.load() + const p = await requireProject() + const branch = opts.branch ?? p.branch + const res = await api.rawRequest('PUT', `/projects/${p.projectId}/secret-bindings/${encodeURIComponent(envName)}`, { + branch, + target: opts.to, + source, + ...(opts.sourceName ? { sourceName: opts.sourceName } : {}), + }) + if (handleApproval(res)) return + if (opts.json) return printJson({ ok: true }) + info(`bound ${envName} on ${opts.to} to ${source}${opts.sourceName ? `.${opts.sourceName}` : ''} (branch ${branch})`) +} + +export async function secretsUnbind(envName: string, opts: { branch?: string; from?: string; json?: boolean }): Promise { + if (!opts.from) die('--from is required') + const api = await ApiClient.load() + const p = await requireProject() + const branch = opts.branch ?? p.branch + const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/secret-bindings/${encodeURIComponent(envName)}?branch=${encodeURIComponent(branch)}&target=${encodeURIComponent(opts.from)}`) + if (handleApproval(res)) return + if (opts.json) return printJson({ ok: true }) + info(`unbound ${envName} from ${opts.from} (branch ${branch})`) +} + +export async function secretsBindings(opts: { branch?: string; target?: string; json?: boolean }): Promise { + if (!opts.target) die('--target is required') + const api = await ApiClient.load() + const p = await requireProject() + const branch = opts.branch ?? p.branch + const res = await api.rawRequest('GET', `/projects/${p.projectId}/secret-bindings?branch=${encodeURIComponent(branch)}&target=${encodeURIComponent(opts.target)}`) + if (handleApproval(res)) return + const bindings = res.body.bindings ?? [] + if (opts.json) return printJson(bindings) + if (!bindings.length) return info(`(no secret bindings for ${opts.target} on ${branch})`) + for (const b of bindings) info(`${b.envName} <- ${b.source.type}/${b.source.name}.${b.sourceName}`) +} + +export async function secretsSources(opts: { branch?: string; json?: boolean }): Promise { + const api = await ApiClient.load() + const p = await requireProject() + const branch = opts.branch ?? p.branch + const res = await api.rawRequest('GET', `/projects/${p.projectId}/secret-sources?branch=${encodeURIComponent(branch)}`) + if (handleApproval(res)) return + const sources = res.body.sources ?? [] + if (opts.json) return printJson(sources) + if (!sources.length) return info(`(no credential sources on ${branch})`) + for (const s of sources) info(`${s.service.type}/${s.service.name}: ${s.secrets.join(', ')}`) +} + /** Gitignore the env file we just wrote (git repos only; idempotent). Returns true if added. */ export function ensureIgnored(cwd: string, name: string): boolean { if (!existsSync(join(cwd, '.git'))) return false diff --git a/src/index.ts b/src/index.ts index e2a918a..e1e196e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -162,6 +162,26 @@ sec.command('set [value]').description('Set a user secret (project-wide; .action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o))) sec.command('unset ').description('Remove a user secret') .option('--branch ', 'scope to one branch').action(guard((n, o) => secretsCmd.secretsUnset(n, o))) +sec.command('bind ').description('Bind a service credential into a compute env var') + .option('--branch ', 'branch (default: current)') + .option('--to ', 'target compute service, e.g. compute/api') + .option('--source-name ', 'source credential name when the source exposes more than one') + .option('--json') + .action(guard((n, source, o) => secretsCmd.secretsBind(n, source, o))) +sec.command('unbind ').description('Remove a service credential binding from a compute env var') + .option('--branch ', 'branch (default: current)') + .option('--from ', 'target compute service, e.g. compute/api') + .option('--json') + .action(guard((n, o) => secretsCmd.secretsUnbind(n, o))) +sec.command('bindings').description('List service credential bindings for a compute service') + .option('--branch ', 'branch (default: current)') + .option('--target ', 'target compute service, e.g. compute/api') + .option('--json') + .action(guard((o) => secretsCmd.secretsBindings(o))) +sec.command('sources').description('List service credential sources available for binding') + .option('--branch ', 'branch (default: current)') + .option('--json') + .action(guard((o) => secretsCmd.secretsSources(o))) sec.command('tree').description('Show secrets as project → branch → service → secrets').option('--json') .action(guard((o) => secretsCmd.secretsTree(o)))