@@ -33,6 +33,9 @@ import {
3333import type { EnvDiff } from "../env/types.ts" ;
3434import { reloadStacks } from "../compose/reload.ts" ;
3535import type { ReloadResult } from "../compose/reload.ts" ;
36+ import { planOperation } from "../compose/plan.ts" ;
37+ import type { PlanResult } from "../compose/plan.ts" ;
38+ import { CompletionsCommand } from "@cliffy/command/completions" ;
3639import {
3740 checkTooling ,
3841 cleanDecryptedEnvFiles ,
@@ -62,10 +65,24 @@ export async function main(args: string[]): Promise<number> {
6265 }
6366}
6467
68+ /**
69+ * Best-effort stack-name completion provider.
70+ * Returns stack names discovered from the repository.
71+ * Never throws — returns an empty array if config or discovery fails.
72+ */
73+ async function completeStackNames ( ) : Promise < string [ ] > {
74+ try {
75+ const config = await resolveConfig ( { profile : undefined , cwd : Deno . cwd ( ) } ) ;
76+ const repoRoot = config . base . repoRoot ?? Deno . cwd ( ) ;
77+ const discovery = await discoverComposeFiles ( { repoRoot } ) ;
78+ return Object . keys ( discovery . stacks ) ;
79+ } catch {
80+ return [ ] ;
81+ }
82+ }
83+
6584/**
6685 * Build the stackctl CLI command tree.
67- * Commands are registered here in their skeleton form;
68- * full implementations are added in subsequent issues.
6986 */
7087export function buildCli ( ) : Command {
7188 const cli = new Command ( )
@@ -1470,31 +1487,98 @@ export function buildCli(): Command {
14701487 cli . command ( "plan" , "Produce a deterministic plan of what an operation would do." )
14711488 . arguments ( "<operation:string>" )
14721489 . option ( "--profile <name:string>" , "Use a specific profile." )
1473- . option ( "--stacks <names:string>" , "Comma-separated list of stack names." )
1490+ . option ( "--stacks <names:string>" , "Comma-separated list of stack names." , {
1491+ complete : completeStackNames ,
1492+ } as any )
14741493 . option ( "--override <files:string>" , "Comma-separated list of override files." )
14751494 . option ( "--json" , "Output machine-readable JSON." )
1476- . action ( ( ) => {
1477- console . error ( "plan: not yet implemented (issue #15)" ) ;
1478- exitCode = 1 ;
1495+ . description (
1496+ "Shows a structured summary of what the specified operation would do without executing it.\n\n" +
1497+ "Supported operations:\n" +
1498+ " up - Preview stack deployment\n" +
1499+ " down - Preview stack removal\n" +
1500+ " sync - Preview full generate+render+deploy pipeline\n" +
1501+ " generate - Preview stack generation only\n" +
1502+ " render - Preview rendering only\n" +
1503+ " reload - Preview config-first reload\n" +
1504+ " env - Preview env file scaffolding\n" +
1505+ " secrets - Preview secrets workflow\n" +
1506+ " all - Preview everything" ,
1507+ )
1508+ . example (
1509+ "Preview what would happen during a sync" ,
1510+ "stackctl plan sync" ,
1511+ )
1512+ . example (
1513+ "Preview with a specific profile" ,
1514+ "stackctl plan up --profile staging" ,
1515+ )
1516+ . example (
1517+ "Preview specific stacks only" ,
1518+ "stackctl plan generate --stacks api,web" ,
1519+ )
1520+ . example (
1521+ "Machine-readable JSON output" ,
1522+ "stackctl plan all --json" ,
1523+ )
1524+ . action ( ( opts : Record < string , unknown > , operation : string ) => {
1525+ const profile = opts . profile as string | undefined ;
1526+ const stacks = opts . stacks
1527+ ? ( opts . stacks as string ) . split ( "," ) . map ( ( s : string ) => s . trim ( ) )
1528+ : undefined ;
1529+ const overrides = opts . override
1530+ ? ( opts . override as string ) . split ( "," ) . map ( ( s : string ) => s . trim ( ) )
1531+ : undefined ;
1532+
1533+ planOperation ( {
1534+ operation,
1535+ profile,
1536+ stacks,
1537+ overrides,
1538+ } )
1539+ . then ( ( plan : PlanResult ) => {
1540+ if ( opts . json ) {
1541+ console . log ( JSON . stringify ( plan . json , null , 2 ) ) ;
1542+ return ;
1543+ }
1544+
1545+ // Human-readable output
1546+ console . log ( `Plan: ${ plan . operation } ` ) ;
1547+ console . log ( "=" . repeat ( 40 ) ) ;
1548+
1549+ for ( const section of plan . sections ) {
1550+ console . log ( `\n${ section . title } ` ) ;
1551+ console . log ( "-" . repeat ( section . title . length ) ) ;
1552+ for ( const item of section . items ) {
1553+ console . log ( item ) ;
1554+ }
1555+ }
1556+
1557+ if ( plan . warnings . length > 0 ) {
1558+ console . log ( "\nWarnings:" ) ;
1559+ for ( const w of plan . warnings ) {
1560+ console . log ( ` ! ${ w } ` ) ;
1561+ }
1562+ }
1563+
1564+ if ( plan . errors . length > 0 ) {
1565+ console . log ( "\nErrors:" ) ;
1566+ for ( const e of plan . errors ) {
1567+ console . log ( ` ✗ ${ e } ` ) ;
1568+ }
1569+ Deno . exit ( ExitCode . DriftOrValidation ) ;
1570+ }
1571+ } )
1572+ . catch ( ( err : unknown ) => {
1573+ console . error (
1574+ `error: ${ err instanceof Error ? err . message : String ( err ) } ` ,
1575+ ) ;
1576+ Deno . exit ( ExitCode . UnexpectedError ) ;
1577+ } ) ;
14791578 } ) ;
14801579
14811580 // --- completions (issue #10) ---
1482- const completionsCmd = cli . command ( "completions" , "Generate shell completion scripts." ) ;
1483- completionsCmd . command ( "bash" , "Generate bash completion script." )
1484- . action ( ( ) => {
1485- console . error ( "completions bash: not yet implemented (issue #10)" ) ;
1486- exitCode = 1 ;
1487- } ) ;
1488- completionsCmd . command ( "zsh" , "Generate zsh completion script." )
1489- . action ( ( ) => {
1490- console . error ( "completions zsh: not yet implemented (issue #10)" ) ;
1491- exitCode = 1 ;
1492- } ) ;
1493- completionsCmd . command ( "fish" , "Generate fish completion script." )
1494- . action ( ( ) => {
1495- console . error ( "completions fish: not yet implemented (issue #10)" ) ;
1496- exitCode = 1 ;
1497- } ) ;
1581+ cli . command ( "completions" , new CompletionsCommand ( ) ) ;
14981582
14991583 return cli as unknown as Command ;
15001584}
0 commit comments