feat(cli): nested-manifest discovery + pic plan/apply --dir tree mode (Phase 5 C4)
Phase 5 C4. `pic plan --dir <root>` / `pic apply --dir <root>` discover every
`picloud.toml` under a directory (groups + apps), assemble the wire tree
bundle, and drive the C3 `/tree/{plan,apply}` endpoints — a whole project
subtree reconciled and reviewed as one unit.
* New `discover` module: recursive walk (skips `.picloud`/`.git`/`scripts`/
`target`), one node per manifest, app/group inferred from the manifest;
rejects a duplicate (kind, slug). On-disk nesting is organizational — the
server resolves ancestry from its own group tree.
* `client`: `plan_tree`/`apply_tree` + `TreePlanDto`/`NodePlanDto`. `pic plan
--dir` renders a per-node diff table and records ONE tree bound-token under
a `<tree>` linkstate marker; `pic apply --dir` replays it (force/`--yes`/
prune apply tree-wide). `--dir` conflicts with `--file`.
Live-validated: a nested tree (root group dir + nested app dir, app route bound
to the group's inherited script) → `pic plan --dir .` shows both nodes' changes
→ `pic apply --dir .` applies all in one tx → re-plan all-noop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
mod client;
|
||||
mod cmds;
|
||||
mod config;
|
||||
mod discover;
|
||||
mod linkstate;
|
||||
mod manifest;
|
||||
mod output;
|
||||
@@ -202,6 +203,11 @@ struct ApplyArgs {
|
||||
/// Path to the manifest.
|
||||
#[arg(long, default_value = "picloud.toml")]
|
||||
file: PathBuf,
|
||||
/// Phase 5: apply a whole project TREE — every `picloud.toml` under this
|
||||
/// directory (groups + apps) reconciled in one atomic transaction.
|
||||
/// Mutually exclusive with `--file`.
|
||||
#[arg(long, conflicts_with = "file")]
|
||||
dir: Option<PathBuf>,
|
||||
/// Delete live scripts/routes/triggers absent from the manifest.
|
||||
/// Secrets are never pruned.
|
||||
#[arg(long)]
|
||||
@@ -225,6 +231,10 @@ struct PlanArgs {
|
||||
/// Path to the manifest.
|
||||
#[arg(long, default_value = "picloud.toml")]
|
||||
file: PathBuf,
|
||||
/// Phase 5: plan a whole project TREE — every `picloud.toml` under this
|
||||
/// directory (groups + apps). Mutually exclusive with `--file`.
|
||||
#[arg(long, conflicts_with = "file")]
|
||||
dir: Option<PathBuf>,
|
||||
/// Merge the `picloud.<env>.toml` overlay (per-env slug + secrets) on
|
||||
/// top of the base manifest before diffing.
|
||||
#[arg(long)]
|
||||
@@ -1329,18 +1339,34 @@ async fn main() -> ExitCode {
|
||||
}
|
||||
Cmd::Logout => cmds::logout::run().await,
|
||||
Cmd::Whoami => cmds::whoami::run(mode).await,
|
||||
Cmd::Apply(args) => {
|
||||
cmds::apply::run(
|
||||
&args.file,
|
||||
args.env.as_deref(),
|
||||
args.prune,
|
||||
args.yes,
|
||||
args.force,
|
||||
mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Cmd::Plan(args) => cmds::plan::run(&args.file, args.env.as_deref(), mode).await,
|
||||
Cmd::Apply(args) => match &args.dir {
|
||||
Some(dir) => {
|
||||
cmds::apply::run_tree(
|
||||
dir,
|
||||
args.prune,
|
||||
args.yes,
|
||||
args.force,
|
||||
args.env.as_deref(),
|
||||
mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
cmds::apply::run(
|
||||
&args.file,
|
||||
args.env.as_deref(),
|
||||
args.prune,
|
||||
args.yes,
|
||||
args.force,
|
||||
mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
Cmd::Plan(args) => match &args.dir {
|
||||
Some(dir) => cmds::plan::run_tree(dir, args.env.as_deref(), mode).await,
|
||||
None => cmds::plan::run(&args.file, args.env.as_deref(), mode).await,
|
||||
},
|
||||
Cmd::Pull(args) => cmds::pull::run(&args.app, &args.dir, args.force, mode).await,
|
||||
Cmd::Config(args) => {
|
||||
cmds::config::run(
|
||||
|
||||
Reference in New Issue
Block a user