feat(cli): [project] manifest block, --takeover, groups ls owner column
Makes §7 ownership usable end to end from the CLI. - manifest: a `[project]` block (slug + optional name), independent of the [app]/[group] XOR; threaded onto every apply from the repo. --dir reads it from the tree ROOT's picloud.toml (a [project] elsewhere is ignored with a note). - client: apply_node/apply_tree carry project + takeover; the 409 branch now surfaces the server message verbatim (covers StateMoved AND OwnershipConflict, both self-contained). New groups_list_with_owner captures the owner slug. - pic apply --takeover flag; pic groups ls gains an `owner` column (the owning project's slug, or — when unclaimed). - server: /api/v1/admin/groups now returns each group flattened with its owner slug (via list_with_owner) — the shared Group deserialize ignores the extra field, so pic groups tree / the dashboard are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ use anyhow::{bail, Context, Result};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::cmds::plan::build_bundle;
|
||||
use crate::manifest::{Manifest, MANIFEST_FILE};
|
||||
use crate::manifest::{Manifest, ManifestProject, MANIFEST_FILE};
|
||||
|
||||
/// Find every `picloud.toml` under `root` (recursively), skipping the
|
||||
/// `.picloud/` link-state dir, `.git`, and `scripts/` source dirs. Per-env
|
||||
@@ -46,9 +46,14 @@ fn walk(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Build the wire tree bundle (`{ nodes: [{kind, slug, bundle}] }`) from every
|
||||
/// manifest under `root`. Returns the JSON plus the node count. Rejects a tree
|
||||
/// that names the same (kind, slug) twice.
|
||||
pub fn build_tree(root: &Path, env: Option<&str>) -> Result<(Value, usize)> {
|
||||
/// manifest under `root`. Returns the JSON, the node count, and the owning
|
||||
/// `[project]` (§7) declared in the tree ROOT's `picloud.toml` (if any) — one
|
||||
/// project owns the whole tree; a `[project]` in any other node is ignored with
|
||||
/// a note. Rejects a tree that names the same (kind, slug) twice.
|
||||
pub fn build_tree(
|
||||
root: &Path,
|
||||
env: Option<&str>,
|
||||
) -> Result<(Value, usize, Option<ManifestProject>)> {
|
||||
let paths = find_manifests(root)?;
|
||||
if paths.is_empty() {
|
||||
bail!(
|
||||
@@ -56,11 +61,24 @@ pub fn build_tree(root: &Path, env: Option<&str>) -> Result<(Value, usize)> {
|
||||
root.display()
|
||||
);
|
||||
}
|
||||
let root_manifest = root.join(MANIFEST_FILE);
|
||||
let mut project: Option<ManifestProject> = None;
|
||||
let mut nodes = Vec::with_capacity(paths.len());
|
||||
let mut seen: HashSet<(String, String)> = HashSet::new();
|
||||
for path in &paths {
|
||||
let manifest = Manifest::load_with_env(path, env)
|
||||
.with_context(|| format!("loading {}", path.display()))?;
|
||||
if let Some(p) = &manifest.project {
|
||||
if path.as_path() == root_manifest {
|
||||
project = Some(p.clone());
|
||||
} else {
|
||||
eprintln!(
|
||||
"note: [project] in {} is ignored — declare it in the tree root {}",
|
||||
path.display(),
|
||||
root_manifest.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
|
||||
let bundle = build_bundle(&manifest, base_dir)?;
|
||||
let kind = if manifest.is_group() { "group" } else { "app" };
|
||||
@@ -71,5 +89,5 @@ pub fn build_tree(root: &Path, env: Option<&str>) -> Result<(Value, usize)> {
|
||||
nodes.push(json!({ "kind": kind, "slug": slug, "bundle": bundle }));
|
||||
}
|
||||
let count = nodes.len();
|
||||
Ok((json!({ "nodes": nodes }), count))
|
||||
Ok((json!({ "nodes": nodes }), count, project))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user