feat(cli): [group] manifest + pic plan/apply for a group node (Phase 5 C2)

Phase 5 C2. A `picloud.toml` can now declare a `[group]` node (instead of
`[app]`): the group's own scripts + `[vars]` (+ secret names). `pic plan` /
`pic apply` reconcile it via the C1 group endpoints — the same plan/apply/
bound-token flow as an app, routed by node kind.

  * `Manifest` is now app-XOR-group: `app`/`group` are both optional with an
    exactly-one check in `parse`, plus a group-node guard that rejects
    `[[routes]]`/`[[triggers]]` (those bind to an app). Accessors `slug()` /
    `is_group()` replace the hardcoded `manifest.app.slug`.
  * `client`: `plan_node`/`apply_node` take a `NodeKind { App | Group }` that
    selects the `apps` vs `groups` API path; the old `plan`/`apply` wrappers
    are gone (callers pass the kind).
  * `pic plan`/`apply` detect the node kind from the manifest and label output
    `group`/`app`; `pic config --effective` cleanly rejects a group manifest
    (effective config is an app's inherited view).

Live-validated: a `[group]` manifest with a script + var → `pic plan` (script
+ var creates) → `pic apply` (group-owned) → idempotent re-plan noop →
`pic scripts ls --group` shows it. Manifest unit test for group parse +
app-only-block rejection; init/pull/plan/apply/overlay journeys all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-25 21:57:58 +02:00
parent 65049a6262
commit da03fda1d6
7 changed files with 183 additions and 51 deletions

View File

@@ -45,8 +45,14 @@ pub async fn run(
// masked report targets the app those commands would act on — not the
// base slug — when an overlay re-points slug/secrets.
let manifest = Manifest::load_with_env(manifest_path, env)?;
if manifest.is_group() {
bail!(
"`pic config --effective` resolves an APP's inherited config; \
a [group] manifest has no single effective view"
);
}
let eff = client.config_effective(&manifest.app.slug).await?;
let eff = client.config_effective(manifest.slug()).await?;
// --- vars: the resolved view, with winning owner + provenance. ---
let mut vars_table = Table::new(["key", "value", "owner", "scope"]);