feat(vars): admin CRUD API + pic vars CLI
Completes the vars half of Phase 3 end-to-end:
- vars_repo: VarOwner{Group|App} upsert/delete/list (owner-kind-specific
SQL; ON CONFLICT restates the partial-index predicate).
- vars_api: GET/PUT/DELETE under /apps/{id}/vars and /groups/{id}/vars,
resolve-then-require gated on App/GroupVars{Read,Write}, secrets-style
error mapping + key/env-scope validation.
- pic vars ls/set/rm (--group|--app, --env, --json, --tombstone).
- journey test: a group var is inherited by a descendant app's
vars::get(), and an app-level value overrides it (proximity) — green.
386 manager-core lib tests + the vars journey pass; clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -144,6 +144,14 @@ enum Cmd {
|
||||
cmd: MembersCmd,
|
||||
},
|
||||
|
||||
/// Config vars (Phase 3) — set / list / delete group- or app-owned
|
||||
/// env-scoped vars. Values inherit down the group tree; an app value
|
||||
/// overrides an inherited one (proximity wins).
|
||||
Vars {
|
||||
#[command(subcommand)]
|
||||
cmd: VarsCmd,
|
||||
},
|
||||
|
||||
/// Files inspection — list a collection's blobs, download bytes, or
|
||||
/// delete a file. Read + delete only; writes go through scripts.
|
||||
Files {
|
||||
@@ -1162,6 +1170,53 @@ enum SecretsCmd {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum VarsCmd {
|
||||
/// List the owner's OWN vars (not the resolved/inherited view).
|
||||
Ls {
|
||||
/// Owning group (slug or id). Mutually exclusive with `--app`.
|
||||
#[arg(long)]
|
||||
group: Option<String>,
|
||||
/// Owning app (slug or id). Mutually exclusive with `--group`.
|
||||
#[arg(long)]
|
||||
app: Option<String>,
|
||||
},
|
||||
|
||||
/// Set a var. The value is stored as a JSON string by default; pass
|
||||
/// `--json` to parse it as raw JSON. `--tombstone` writes a deletion
|
||||
/// marker that suppresses an inherited key.
|
||||
Set {
|
||||
key: String,
|
||||
/// Ignored (but accepted) when `--tombstone` is set.
|
||||
#[arg(default_value = "")]
|
||||
value: String,
|
||||
#[arg(long)]
|
||||
group: Option<String>,
|
||||
#[arg(long)]
|
||||
app: Option<String>,
|
||||
/// Environment scope (`*` = env-agnostic, the default).
|
||||
#[arg(long)]
|
||||
env: Option<String>,
|
||||
/// Parse `value` as raw JSON instead of a string literal.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
/// Write a tombstone (suppress an inherited key) instead of a value.
|
||||
#[arg(long)]
|
||||
tombstone: bool,
|
||||
},
|
||||
|
||||
/// Delete a var by key (optionally scoped to one environment).
|
||||
Rm {
|
||||
key: String,
|
||||
#[arg(long)]
|
||||
group: Option<String>,
|
||||
#[arg(long)]
|
||||
app: Option<String>,
|
||||
#[arg(long)]
|
||||
env: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum AdminsCmd {
|
||||
/// List admin accounts.
|
||||
@@ -1761,6 +1816,41 @@ async fn main() -> ExitCode {
|
||||
Cmd::Members {
|
||||
cmd: MembersCmd::Rm { app, user_id },
|
||||
} => cmds::members::rm(&app, &user_id).await,
|
||||
Cmd::Vars {
|
||||
cmd: VarsCmd::Ls { group, app },
|
||||
} => cmds::vars::ls(group.as_deref(), app.as_deref(), mode).await,
|
||||
Cmd::Vars {
|
||||
cmd:
|
||||
VarsCmd::Set {
|
||||
key,
|
||||
value,
|
||||
group,
|
||||
app,
|
||||
env,
|
||||
json,
|
||||
tombstone,
|
||||
},
|
||||
} => {
|
||||
cmds::vars::set(
|
||||
group.as_deref(),
|
||||
app.as_deref(),
|
||||
&key,
|
||||
&value,
|
||||
env.as_deref(),
|
||||
json,
|
||||
tombstone,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Cmd::Vars {
|
||||
cmd:
|
||||
VarsCmd::Rm {
|
||||
key,
|
||||
group,
|
||||
app,
|
||||
env,
|
||||
},
|
||||
} => cmds::vars::rm(group.as_deref(), app.as_deref(), &key, env.as_deref()).await,
|
||||
Cmd::Files {
|
||||
cmd:
|
||||
FilesCmd::Ls {
|
||||
|
||||
Reference in New Issue
Block a user