feat(cli): pic init scaffolds a new declarative project

Offline project scaffolding — the on-ramp to pull/plan/apply. Writes a
`picloud.toml` describing a minimal *working* app (a `hello` endpoint
bound to GET /hello) plus commented examples for the other blocks,
`scripts/hello.rhai`, and a `.gitignore` that pre-ignores the project
tool's `.picloud/` link-state directory (the home for the bound-plan
state token, landing next). Refuses to overwrite an existing manifest
without `--force`; derives the app slug from the directory name when not
given (validated against the canonical slug rule).

The active manifest is rendered through the same `to_toml` model the rest
of the tool uses, so a freshly-init'd project is guaranteed valid and
immediately `pic plan`-able. Offline — no server contact, so the journey
tests need no DATABASE_URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-22 21:32:07 +02:00
parent d9b3e9973c
commit b8a4f30219
5 changed files with 369 additions and 0 deletions

View File

@@ -171,6 +171,11 @@ enum Cmd {
/// (+ `scripts/<name>.rhai` sources) for declarative management with
/// `pic plan` / `pic apply`.
Pull(PullArgs),
/// Scaffold a new declarative project: a `picloud.toml` (minimal working
/// app), `scripts/hello.rhai`, and a `.gitignore`. Offline; deploy with
/// `pic plan` then `pic apply`.
Init(InitArgs),
}
#[derive(Args)]
@@ -204,6 +209,22 @@ struct PullArgs {
dir: PathBuf,
}
#[derive(Args)]
struct InitArgs {
/// App slug for the new project. Defaults to a slug derived from the
/// target directory's name.
slug: Option<String>,
/// Directory to scaffold into.
#[arg(long, default_value = ".")]
dir: PathBuf,
/// Display name for the app. Defaults to a title-cased slug.
#[arg(long)]
name: Option<String>,
/// Overwrite an existing `picloud.toml`.
#[arg(long)]
force: bool,
}
#[derive(Subcommand)]
enum KvCmd {
/// List keys in a collection.
@@ -1094,6 +1115,13 @@ async fn main() -> ExitCode {
Cmd::Apply(args) => cmds::apply::run(&args.file, args.prune, args.yes, mode).await,
Cmd::Plan(args) => cmds::plan::run(&args.file, mode).await,
Cmd::Pull(args) => cmds::pull::run(&args.app, &args.dir, mode).await,
Cmd::Init(args) => cmds::init::run(
&args.dir,
args.slug.as_deref(),
args.name.as_deref(),
args.force,
mode,
),
Cmd::Apps { cmd: AppsCmd::Ls } => cmds::apps::ls(mode).await,
Cmd::Apps {
cmd: