feat(scripts): group-owned script admin API + pic scripts … --group
Phase 4-lite C2. Lets a group own scripts (templates inherited by descendant
apps), with the create/list/manage surface — but not yet the inherited
resolution (binding + runtime), which is C3.
Capabilities: add `GroupScriptsRead` (viewer+ on the group → script:read) and
`GroupScriptsWrite` (editor+ → script:write), mirroring the group-vars tier and
resolved through the same group-ancestor walk.
API:
* New `group_scripts_api`: `POST/GET /groups/{id}/scripts` — create a
group-owned endpoint script and list a group's own (non-inherited) rows.
Phase 4-lite is endpoint-only and self-contained: `kind=module` and any
`import` are rejected (group modules + the lexical resolver are Phase 4b).
Owner resolved first (slug-or-uuid); capability bound to the resolved id.
* The by-id `/scripts/{id}` get/update/delete/logs handlers are now
owner-polymorphic via a `script_cap` helper: app-owned scripts gate on
`App*` exactly as before; group-owned on `GroupScripts*`. This is what
makes `deploy --group` idempotent (update reuses the by-id PUT) and lets a
group script be deleted by id. (C1 had these fail closed for groups.)
Repo: `list_for_group(group_id)` (the group's own rows).
CLI: `pic scripts ls --group <g>`, `pic scripts deploy --group <g>` (create or
update by name; `--app`/`--group` are mutually exclusive, exactly one
required). `pic scripts delete <id>` already works for group scripts via the
owner-polymorphic by-id route.
Live-validated against the dev DB: create → update (v2 via the by-id PUT) →
list → delete, plus module rejection and the polymorphic row shape
(`app_id NULL`, `group_id` set). Group scripts still can't be routed/triggered
or invoked — that lands in C3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -529,15 +529,19 @@ enum DomainsCmd {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ScriptsCmd {
|
||||
/// List scripts. With `--app`, scoped to one app; without, one
|
||||
/// List scripts. With `--app`, scoped to one app; with `--group`, a
|
||||
/// group's own scripts (Phase 4); without either, one
|
||||
/// `GET /admin/scripts` for everything the caller can see.
|
||||
Ls {
|
||||
#[arg(long)]
|
||||
app: Option<String>,
|
||||
/// Phase 4: list a group's own (non-inherited) scripts.
|
||||
#[arg(long, conflicts_with = "app")]
|
||||
group: Option<String>,
|
||||
},
|
||||
|
||||
/// Upload a `.rhai` file. Patches the existing script with the
|
||||
/// matching name in `--app` if one exists, otherwise creates it.
|
||||
/// matching name in `--app`/`--group` if one exists, otherwise creates it.
|
||||
Deploy(DeployArgs),
|
||||
|
||||
/// POST to `/api/v1/execute/{id}`. Body via `--body @path`,
|
||||
@@ -551,8 +555,14 @@ enum ScriptsCmd {
|
||||
#[derive(Args)]
|
||||
struct DeployArgs {
|
||||
file: PathBuf,
|
||||
/// Owning app (slug or id). Exactly one of `--app` / `--group`.
|
||||
#[arg(long)]
|
||||
app: String,
|
||||
app: Option<String>,
|
||||
/// Phase 4: deploy a group-owned script (template inherited by
|
||||
/// descendant apps) instead of an app-owned one. Exactly one of
|
||||
/// `--app` / `--group`.
|
||||
#[arg(long, conflicts_with = "app")]
|
||||
group: Option<String>,
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
#[arg(long)]
|
||||
@@ -1465,15 +1475,16 @@ async fn main() -> ExitCode {
|
||||
},
|
||||
} => cmds::groups::members_rm(&group, &user_id).await,
|
||||
Cmd::Scripts {
|
||||
cmd: ScriptsCmd::Ls { app },
|
||||
} => cmds::scripts::ls(app.as_deref(), mode).await,
|
||||
cmd: ScriptsCmd::Ls { app, group },
|
||||
} => cmds::scripts::ls(app.as_deref(), group.as_deref(), mode).await,
|
||||
Cmd::Scripts {
|
||||
cmd: ScriptsCmd::Deploy(args),
|
||||
} => match args.script_config() {
|
||||
Ok(cfg) => {
|
||||
cmds::scripts::deploy(
|
||||
&args.file,
|
||||
&args.app,
|
||||
args.app.as_deref(),
|
||||
args.group.as_deref(),
|
||||
args.name.as_deref(),
|
||||
args.description.as_deref(),
|
||||
&cfg,
|
||||
@@ -1516,7 +1527,8 @@ async fn main() -> ExitCode {
|
||||
Ok(cfg) => {
|
||||
cmds::scripts::deploy(
|
||||
&args.file,
|
||||
&args.app,
|
||||
args.app.as_deref(),
|
||||
args.group.as_deref(),
|
||||
args.name.as_deref(),
|
||||
args.description.as_deref(),
|
||||
&cfg,
|
||||
|
||||
Reference in New Issue
Block a user