prdforge-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ import { Command } from "commander";
2
+ import { config } from "../utils/config.js";
3
+ import { success, error, info, header, table } from "../utils/output.js";
4
+
5
+ const CONFIGURABLE_KEYS = ["apiUrl", "defaultFormat", "defaultProject", "outputDir"];
6
+
7
+ export function configCommand() {
8
+ const cmd = new Command("config").description("Manage CLI configuration");
9
+
10
+ cmd
11
+ .command("list")
12
+ .description("Show all current configuration values")
13
+ .action(() => {
14
+ header("Configuration");
15
+ table(
16
+ ["Key", "Value"],
17
+ CONFIGURABLE_KEYS.map((k) => [k, String(config.get(k) || "")])
18
+ );
19
+ info(`Config file: ${config.path}`);
20
+ });
21
+
22
+ cmd
23
+ .command("set <key> <value>")
24
+ .description("Set a configuration value")
25
+ .action((key, value) => {
26
+ if (!CONFIGURABLE_KEYS.includes(key)) {
27
+ error(`Unknown key: ${key}`);
28
+ info(`Valid keys: ${CONFIGURABLE_KEYS.join(", ")}`);
29
+ process.exit(1);
30
+ }
31
+ config.set(key, value);
32
+ success(`${key} = ${value}`);
33
+ });
34
+
35
+ cmd
36
+ .command("get <key>")
37
+ .description("Get a configuration value")
38
+ .action((key) => {
39
+ console.log(config.get(key) ?? "");
40
+ });
41
+
42
+ cmd
43
+ .command("reset")
44
+ .description("Reset all configuration to defaults")
45
+ .action(async () => {
46
+ const { default: inquirer } = await import("inquirer");
47
+ const { confirm } = await inquirer.prompt([
48
+ { type: "confirm", name: "confirm", message: "Reset all config? (API key will be kept)", default: false },
49
+ ]);
50
+ if (!confirm) return;
51
+ const key = config.get("apiKey");
52
+ config.clear();
53
+ if (key) config.set("apiKey", key);
54
+ success("Configuration reset to defaults.");
55
+ });
56
+
57
+ return cmd;
58
+ }
@@ -0,0 +1,21 @@
1
+ import { Command } from "commander";
2
+ import { user, isAuthError } from "../api/client.js";
3
+ import { requireApiKey } from "../utils/config.js";
4
+ import { header, dim, error } from "../utils/output.js";
5
+
6
+ export function creditsCommand() {
7
+ return new Command("credits")
8
+ .description("Show monthly credit usage and subscription tier")
9
+ .action(async () => {
10
+ requireApiKey();
11
+ try {
12
+ const data = await user.validate();
13
+ header("Credits");
14
+ dim(` Credits used this month: ${data.credits_used_this_month}`);
15
+ dim(` Subscription tier: ${data.subscription}`);
16
+ } catch (err) {
17
+ error(err.message);
18
+ process.exit(isAuthError(err) ? 2 : 1);
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,88 @@
1
+ import { Command } from "commander";
2
+ import React from "react";
3
+ import { render } from "ink";
4
+ import { Dashboard } from "../ui/Dashboard.js";
5
+ import { requireApiKey } from "../utils/config.js";
6
+
7
+ export function dashboardCommand() {
8
+ return new Command("dashboard")
9
+ .alias("dash")
10
+ .description("Open interactive project overview (keyboard navigable)")
11
+ .action(async () => {
12
+ requireApiKey();
13
+
14
+ let createArgs = null;
15
+
16
+ const { waitUntilExit } = render(
17
+ React.createElement(Dashboard, {
18
+ onCreateNew: (args) => { createArgs = args; },
19
+ })
20
+ );
21
+
22
+ await waitUntilExit();
23
+
24
+ // If user pressed Enter in compose mode, launch the prd create flow
25
+ if (createArgs) {
26
+ const { prdCommand } = await import("./prd.js");
27
+ const { projects, prd, isAuthError } = await import("../api/client.js");
28
+ const { success, error, info, dim } = await import("../utils/output.js");
29
+ const { default: ora } = await import("ora");
30
+ const { render: inkRender } = await import("ink");
31
+ const { PrdCreation, buildStages, setStageStatus, markAllDone } = await import("../ui/PrdCreation.js");
32
+ const { theme } = await import("../ui/theme.js");
33
+
34
+ // Prompt for project name
35
+ const { default: inquirer } = await import("inquirer");
36
+ const { projectName } = await inquirer.prompt([
37
+ {
38
+ type: "input",
39
+ name: "projectName",
40
+ message: "Project name:",
41
+ validate: (v) => v.trim().length > 0 || "Required",
42
+ },
43
+ ]);
44
+
45
+ const { prompt, modelId } = createArgs;
46
+
47
+ // Animated stage progress
48
+ let stages = buildStages(theme.stages);
49
+ stages = setStageStatus(stages, 0, "active");
50
+
51
+ const inkInstance = inkRender(
52
+ React.createElement(PrdCreation, { projectName: projectName.trim(), stages })
53
+ );
54
+
55
+ const STAGE_INTERVAL_MS = 8000;
56
+ let currentStage = 0;
57
+ const timer = setInterval(() => {
58
+ if (currentStage < theme.stages.length - 1) {
59
+ stages = setStageStatus(stages, currentStage, "done");
60
+ currentStage += 1;
61
+ stages = setStageStatus(stages, currentStage, "active");
62
+ inkInstance.rerender(
63
+ React.createElement(PrdCreation, { projectName: projectName.trim(), stages })
64
+ );
65
+ }
66
+ }, STAGE_INTERVAL_MS);
67
+
68
+ try {
69
+ const project = await projects.create(projectName.trim(), prompt);
70
+ const result = await prd.generate(project.id, prompt, modelId ?? undefined);
71
+ clearInterval(timer);
72
+ stages = markAllDone(stages);
73
+ inkInstance.rerender(
74
+ React.createElement(PrdCreation, { projectName: projectName.trim(), stages })
75
+ );
76
+ inkInstance.unmount();
77
+ success(`Project "${projectName.trim()}" created`);
78
+ dim(` ID: ${project.id}`);
79
+ info(`View in browser: https://prdforge.netlify.app/workspace/${project.id}`);
80
+ } catch (err) {
81
+ clearInterval(timer);
82
+ inkInstance.unmount();
83
+ error(err.message);
84
+ process.exit(isAuthError(err) ? 2 : 1);
85
+ }
86
+ }
87
+ });
88
+ }
@@ -0,0 +1,67 @@
1
+ import { Command } from "commander";
2
+ import { writeFileSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { exports_, isAuthError } from "../api/client.js";
5
+ import { config } from "../utils/config.js";
6
+ import { success, error, info, dim } from "../utils/output.js";
7
+
8
+ /** API supports markdown and json only. See SYNC.md for adding new formats. */
9
+ const FORMATS = ["markdown", "json"];
10
+
11
+ export function exportCommand() {
12
+ const cmd = new Command("export").description("Export a PRD in various formats");
13
+
14
+ cmd
15
+ .argument("<projectId>", "Project ID to export")
16
+ .option("-f, --format <format>", `Export format: ${FORMATS.join(", ")}`, "markdown")
17
+ .option("-o, --output <path>", "Output file path (default: ./prd-exports/<id>.<ext>)")
18
+ .option("--stdout", "Print to stdout instead of saving to file")
19
+ .action(async (projectId, opts) => {
20
+ const { default: ora } = await import("ora");
21
+
22
+ if (!FORMATS.includes(opts.format)) {
23
+ error(`Unknown format: ${opts.format}. Valid options: ${FORMATS.join(", ")}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const spinner = ora(`Exporting as ${opts.format}…`).start();
28
+ try {
29
+ const result = await exports_.export(projectId, opts.format);
30
+ spinner.stop();
31
+
32
+ const content =
33
+ opts.format === "json"
34
+ ? JSON.stringify(result.data ?? result, null, 2)
35
+ : result.content ?? result.data ?? String(result);
36
+
37
+ if (opts.stdout) {
38
+ console.log(content);
39
+ return;
40
+ }
41
+
42
+ const ext = opts.format === "markdown" ? "md" : opts.format;
43
+ const outDir = config.get("outputDir");
44
+ mkdirSync(outDir, { recursive: true });
45
+ const filePath = opts.output || join(outDir, `${projectId.slice(0, 8)}.${ext}`);
46
+ writeFileSync(filePath, content, "utf8");
47
+
48
+ success(`Exported to: ${filePath}`);
49
+ dim(` Format: ${opts.format}`);
50
+ info("Feed this file directly to Claude Code, Cursor, or any AI coding tool.");
51
+ } catch (err) {
52
+ spinner.fail("Export failed");
53
+ error(err.message);
54
+ process.exit(isAuthError(err) ? 2 : 1);
55
+ }
56
+ });
57
+
58
+ cmd.addHelpText(
59
+ "after",
60
+ `
61
+ Examples:
62
+ prdforge export <projectId> -f markdown -o PRD.md
63
+ prdforge export <projectId> --format json --stdout
64
+ `
65
+ );
66
+ return cmd;
67
+ }
@@ -0,0 +1,117 @@
1
+ import { Command } from "commander";
2
+ import { writeFileSync, mkdirSync, existsSync, renameSync } from "fs";
3
+ import { join } from "path";
4
+ import { projects, prd, exports_, isAuthError } from "../api/client.js";
5
+ import { success, error, info, dim } from "../utils/output.js";
6
+
7
+ function timestamp() {
8
+ const d = new Date();
9
+ return [
10
+ d.getFullYear(),
11
+ String(d.getMonth() + 1).padStart(2, "0"),
12
+ String(d.getDate()).padStart(2, "0"),
13
+ String(d.getHours()).padStart(2, "0"),
14
+ String(d.getMinutes()).padStart(2, "0"),
15
+ String(d.getSeconds()).padStart(2, "0"),
16
+ ].join("-");
17
+ }
18
+
19
+ export function generateCommand() {
20
+ const cmd = new Command("generate")
21
+ .description("Generate a PRD and save it to your app folder (with optional backup of existing PRD.md)")
22
+ .option("-n, --name <name>", "Project name (for new PRD)")
23
+ .option("-p, --prompt <prompt>", "Product idea or update prompt (or set PRDFORGE_PROMPT)")
24
+ .option("--project <id>", "Existing project ID (for update or refresh)")
25
+ .option("--refresh", "Regenerate all stale stages, then save")
26
+ .option("-d, --dir <path>", "App folder to write PRD.md (default: current directory)", process.cwd())
27
+ .option("-o, --output <path>", "Output filename (default: PRD.md)", "PRD.md")
28
+ .option("--no-backup", "Do not backup existing PRD.md before overwriting")
29
+ .option("-m, --model <modelId>", "AI model ID to use")
30
+ .option("--json", "Output raw JSON instead of writing file")
31
+ .addHelpText(
32
+ "after",
33
+ `
34
+ Examples:
35
+ prdforge generate -n "My App" -p "A todo app with due dates and tags"
36
+ prdforge generate --project <id> -p "Add OAuth section"
37
+ prdforge generate --project <id> --refresh
38
+ prdforge generate -n "API" -p "REST API for payments" -d ./my-app -o PRD.md
39
+ `
40
+ );
41
+
42
+ cmd.action(async (opts) => {
43
+ const { default: ora } = await import("ora");
44
+ const prompt = opts.prompt ?? process.env.PRDFORGE_PROMPT ?? "";
45
+
46
+ const isRefresh = !!opts.project && !!opts.refresh;
47
+ const isUpdate = !!opts.project && !opts.refresh && prompt !== "";
48
+ const isNew = !!opts.name && prompt !== "";
49
+
50
+ if (!isNew && !isUpdate && !isRefresh) {
51
+ if (opts.name) {
52
+ error("Provide -p/--prompt or set PRDFORGE_PROMPT to generate a new PRD.");
53
+ } else {
54
+ error("Use -n/--name with -p/--prompt for a new PRD, or --project <id> with -p/--prompt or --refresh.");
55
+ }
56
+ process.exit(1);
57
+ }
58
+
59
+ let projectId;
60
+ let content;
61
+
62
+ try {
63
+ if (isNew) {
64
+ const spinner = ora("Creating project…").start();
65
+ const project = await projects.create(opts.name, prompt);
66
+ projectId = project.id;
67
+ spinner.text = "Generating PRD (this may take ~30–60s)…";
68
+ await prd.generate(projectId, prompt, opts.model);
69
+ spinner.succeed("PRD generated!");
70
+ } else if (isUpdate) {
71
+ projectId = opts.project;
72
+ const spinner = ora("Analysing and merging update…").start();
73
+ await prd.update(projectId, prompt, opts.model);
74
+ spinner.succeed("PRD updated!");
75
+ } else {
76
+ projectId = opts.project;
77
+ const spinner = ora("Regenerating stale stages…").start();
78
+ await prd.regenerateStale(projectId, opts.model);
79
+ spinner.succeed("Stale stages refreshed!");
80
+ }
81
+
82
+ const spinner = ora("Exporting markdown…").start();
83
+ const result = await exports_.export(projectId, "markdown");
84
+ spinner.stop();
85
+ content =
86
+ result.content ?? result.data ?? (typeof result === "string" ? result : JSON.stringify(result, null, 2));
87
+ } catch (err) {
88
+ if (opts.json && err.message) {
89
+ console.error(JSON.stringify({ error: err.message, httpStatus: err.httpStatus }));
90
+ } else {
91
+ error(err.message);
92
+ }
93
+ process.exit(isAuthError(err) ? 2 : 1);
94
+ }
95
+
96
+ if (opts.json) {
97
+ console.log(JSON.stringify({ project_id: projectId, content }, null, 2));
98
+ return;
99
+ }
100
+
101
+ const dir = opts.dir || process.cwd();
102
+ const outputPath = join(dir, opts.output || "PRD.md");
103
+ mkdirSync(dir, { recursive: true });
104
+
105
+ if (existsSync(outputPath) && opts.backup !== false) {
106
+ const backupPath = `${outputPath}.backup.${timestamp()}`;
107
+ renameSync(outputPath, backupPath);
108
+ info(`Backed up existing file to ${backupPath}`);
109
+ }
110
+
111
+ writeFileSync(outputPath, content, "utf8");
112
+ success(`PRD saved to ${outputPath}`);
113
+ dim(" Use this file with Cursor, Claude Code, or any AI coding tool.");
114
+ });
115
+
116
+ return cmd;
117
+ }
@@ -0,0 +1,100 @@
1
+ import { Command } from "commander";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { projects, prd, exports_ } from "../api/client.js";
5
+ import { requireApiKey } from "../utils/config.js";
6
+
7
+ export function mcpCommand() {
8
+ const cmd = new Command("mcp").description("MCP (Model Context Protocol) server for PRDForge");
9
+
10
+ cmd
11
+ .command("serve")
12
+ .description("Run the MCP server (stdio). Use with Cursor or other MCP clients.")
13
+ .action(async () => {
14
+ // Ensure API key is available before starting (fail fast)
15
+ requireApiKey();
16
+
17
+ const mcpServer = new McpServer({
18
+ name: "prdforge",
19
+ version: "0.1.0",
20
+ });
21
+
22
+ mcpServer.registerTool(
23
+ "prdforge_list_projects",
24
+ {
25
+ description: "List all PRDForge projects for the authenticated user",
26
+ inputSchema: {},
27
+ },
28
+ async () => {
29
+ try {
30
+ const data = await projects.list();
31
+ const text =
32
+ Array.isArray(data) && data.length > 0
33
+ ? JSON.stringify(data.map((p) => ({ id: p.id, name: p.name, updated_at: p.updated_at })), null, 2)
34
+ : "No projects yet.";
35
+ return { content: [{ type: "text", text }] };
36
+ } catch (err) {
37
+ return { content: [{ type: "text", text: err.message }], isError: true };
38
+ }
39
+ }
40
+ );
41
+
42
+ mcpServer.registerTool(
43
+ "prdforge_get_prd",
44
+ {
45
+ description: "Export a PRD project as markdown by project ID",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ project_id: { type: "string", description: "PRDForge project ID (UUID)" },
50
+ },
51
+ required: ["project_id"],
52
+ },
53
+ },
54
+ async ({ project_id }) => {
55
+ try {
56
+ const result = await exports_.export(project_id, "markdown");
57
+ const content =
58
+ result.content ?? result.data ?? (typeof result === "string" ? result : JSON.stringify(result, null, 2));
59
+ return { content: [{ type: "text", text: content }] };
60
+ } catch (err) {
61
+ return { content: [{ type: "text", text: err.message }], isError: true };
62
+ }
63
+ }
64
+ );
65
+
66
+ mcpServer.registerTool(
67
+ "prdforge_get_sections",
68
+ {
69
+ description: "Get PRD sections for a project (optionally filter by section_type)",
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {
73
+ project_id: { type: "string", description: "PRDForge project ID (UUID)" },
74
+ section_type: { type: "string", description: "Optional section type filter (e.g. feature_list)" },
75
+ },
76
+ required: ["project_id"],
77
+ },
78
+ },
79
+ async ({ project_id, section_type }) => {
80
+ try {
81
+ const sections = await prd.getSections(project_id);
82
+ const filtered = section_type
83
+ ? sections.filter((s) => s.section_type === section_type)
84
+ : sections;
85
+ const text = JSON.stringify(filtered, null, 2);
86
+ return { content: [{ type: "text", text }] };
87
+ } catch (err) {
88
+ return { content: [{ type: "text", text: err.message }], isError: true };
89
+ }
90
+ }
91
+ );
92
+
93
+ const transport = new StdioServerTransport();
94
+ await mcpServer.connect(transport);
95
+ // Log to stderr so stdout stays clean for JSON-RPC
96
+ console.error("PRDForge MCP server running on stdio. Press Ctrl+C to stop.");
97
+ });
98
+
99
+ return cmd;
100
+ }
@@ -0,0 +1,221 @@
1
+ import { Command } from "commander";
2
+ import React from "react";
3
+ import { render } from "ink";
4
+ import { prd, projects, isAuthError } from "../api/client.js";
5
+ import { success, error, info, dim, header, table, printPRDSection } from "../utils/output.js";
6
+ import { PrdCreation, buildStages, setStageStatus, markAllDone } from "../ui/PrdCreation.js";
7
+ import { theme } from "../ui/theme.js";
8
+
9
+ export function prdCommand() {
10
+ const cmd = new Command("prd").description("Generate, view, and update PRDs");
11
+
12
+ // prdforge prd list
13
+ cmd
14
+ .command("list")
15
+ .description("List all your PRD projects")
16
+ .option("--json", "Output raw JSON")
17
+ .action(async (opts) => {
18
+ const { default: ora } = await import("ora");
19
+ const spinner = ora("Fetching projects…").start();
20
+ try {
21
+ const data = await projects.list();
22
+ spinner.stop();
23
+ if (opts.json) {
24
+ console.log(JSON.stringify(data, null, 2));
25
+ return;
26
+ }
27
+ header("Your PRD Projects");
28
+ if (!data?.length) {
29
+ info("No projects yet. Run 'prdforge prd create' to start.");
30
+ return;
31
+ }
32
+ table(
33
+ ["ID", "Name", "Stages", "Updated"],
34
+ data.map((p) => [
35
+ p.id.slice(0, 8) + "…",
36
+ p.name,
37
+ `${p.completed_stages || 0}/8`,
38
+ new Date(p.updated_at).toLocaleDateString(),
39
+ ])
40
+ );
41
+ } catch (err) {
42
+ spinner.fail("Failed");
43
+ error(err.message);
44
+ process.exit(isAuthError(err) ? 2 : 1);
45
+ }
46
+ });
47
+
48
+ // prdforge prd get <id>
49
+ cmd
50
+ .command("get <projectId>")
51
+ .description("Show all PRD sections for a project")
52
+ .option("--section <type>", "Show a specific section type")
53
+ .option("--json", "Output raw JSON")
54
+ .action(async (projectId, opts) => {
55
+ const { default: ora } = await import("ora");
56
+ const spinner = ora("Loading PRD…").start();
57
+ try {
58
+ const sections = await prd.getSections(projectId);
59
+ spinner.stop();
60
+ if (opts.json) {
61
+ console.log(JSON.stringify(sections, null, 2));
62
+ return;
63
+ }
64
+ const filtered = opts.section
65
+ ? sections.filter((s) => s.section_type === opts.section)
66
+ : sections;
67
+ header(`PRD Sections (${filtered.length})`);
68
+ filtered.forEach(printPRDSection);
69
+ } catch (err) {
70
+ spinner.fail("Failed");
71
+ error(err.message);
72
+ process.exit(isAuthError(err) ? 2 : 1);
73
+ }
74
+ });
75
+
76
+ // prdforge prd create
77
+ cmd
78
+ .command("create")
79
+ .description("Create a new PRD project from a prompt")
80
+ .requiredOption("-n, --name <name>", "Project name")
81
+ .option("-p, --prompt <prompt>", "Product idea or description")
82
+ .option("-m, --model <modelId>", "AI model ID to use")
83
+ .option("--json", "Output raw JSON")
84
+ .action(async (opts) => {
85
+ let prompt = opts.prompt;
86
+
87
+ if (!prompt) {
88
+ const { default: inquirer } = await import("inquirer");
89
+ const ans = await inquirer.prompt([
90
+ {
91
+ type: "editor",
92
+ name: "prompt",
93
+ message: "Describe your product idea (opens your $EDITOR):",
94
+ },
95
+ ]);
96
+ prompt = ans.prompt;
97
+ }
98
+
99
+ // If JSON output requested, use plain spinner (no TUI)
100
+ if (opts.json) {
101
+ const { default: ora } = await import("ora");
102
+ const spinner = ora("Creating project…").start();
103
+ try {
104
+ const project = await projects.create(opts.name, prompt);
105
+ spinner.text = "Generating PRD…";
106
+ const result = await prd.generate(project.id, prompt, opts.model);
107
+ spinner.stop();
108
+ console.log(JSON.stringify({ project, result }, null, 2));
109
+ } catch (err) {
110
+ spinner.fail("Failed");
111
+ error(err.message);
112
+ process.exit(isAuthError(err) ? 2 : 1);
113
+ }
114
+ return;
115
+ }
116
+
117
+ // Animated Ink stage progress
118
+ let stages = buildStages(theme.stages);
119
+ stages = setStageStatus(stages, 0, "active");
120
+
121
+ const inkInstance = render(
122
+ React.createElement(PrdCreation, { projectName: opts.name, stages })
123
+ );
124
+
125
+ // Advance stage indicators on a timer while the API call runs (~8s per stage)
126
+ const STAGE_INTERVAL_MS = 8000;
127
+ let currentStage = 0;
128
+ const timer = setInterval(() => {
129
+ if (currentStage < theme.stages.length - 1) {
130
+ stages = setStageStatus(stages, currentStage, "done");
131
+ currentStage += 1;
132
+ stages = setStageStatus(stages, currentStage, "active");
133
+ inkInstance.rerender(
134
+ React.createElement(PrdCreation, { projectName: opts.name, stages })
135
+ );
136
+ }
137
+ }, STAGE_INTERVAL_MS);
138
+
139
+ try {
140
+ const project = await projects.create(opts.name, prompt);
141
+ const result = await prd.generate(project.id, prompt, opts.model);
142
+ clearInterval(timer);
143
+ stages = markAllDone(stages);
144
+ inkInstance.rerender(
145
+ React.createElement(PrdCreation, { projectName: opts.name, stages })
146
+ );
147
+ inkInstance.unmount();
148
+ success(`Project "${opts.name}" created`);
149
+ dim(` ID: ${project.id}`);
150
+ info(`View in browser: https://prdforge.netlify.app/workspace/${project.id}`);
151
+ } catch (err) {
152
+ clearInterval(timer);
153
+ inkInstance.unmount();
154
+ error(err.message);
155
+ process.exit(isAuthError(err) ? 2 : 1);
156
+ }
157
+ });
158
+
159
+ // prdforge prd update <id>
160
+ cmd
161
+ .command("update <projectId>")
162
+ .description("Intelligently merge an update into an existing PRD")
163
+ .option("-p, --prompt <prompt>", "What to update or add")
164
+ .option("-m, --model <modelId>", "AI model ID to use")
165
+ .option("--json", "Output raw JSON")
166
+ .action(async (projectId, opts) => {
167
+ const { default: ora } = await import("ora");
168
+ let prompt = opts.prompt;
169
+
170
+ if (!prompt) {
171
+ const { default: inquirer } = await import("inquirer");
172
+ const ans = await inquirer.prompt([
173
+ {
174
+ type: "editor",
175
+ name: "prompt",
176
+ message: "Describe the update (opens your $EDITOR):",
177
+ },
178
+ ]);
179
+ prompt = ans.prompt;
180
+ }
181
+
182
+ const spinner = ora("Analysing and merging update…").start();
183
+ try {
184
+ const result = await prd.update(projectId, prompt, opts.model);
185
+ spinner.succeed("PRD updated!");
186
+ if (opts.json) {
187
+ console.log(JSON.stringify(result, null, 2));
188
+ } else {
189
+ success(`Smart merge complete`);
190
+ if (result.summary) dim(` Summary: ${result.summary}`);
191
+ if (result.sectionsUpdated != null) dim(` Sections updated: ${result.sectionsUpdated}`);
192
+ if (result.cascadeTriggered) info(" Downstream stages (modules, tasks, etc.) were regenerated.");
193
+ }
194
+ } catch (err) {
195
+ spinner.fail("Failed");
196
+ error(err.message);
197
+ process.exit(isAuthError(err) ? 2 : 1);
198
+ }
199
+ });
200
+
201
+ // prdforge prd refresh <id>
202
+ cmd
203
+ .command("refresh <projectId>")
204
+ .description("Regenerate all stale stages for a project")
205
+ .option("-m, --model <modelId>", "AI model ID to use")
206
+ .action(async (projectId, opts) => {
207
+ const { default: ora } = await import("ora");
208
+ const spinner = ora("Regenerating stale stages…").start();
209
+ try {
210
+ const result = await prd.regenerateStale(projectId, opts.model);
211
+ spinner.succeed("All stale stages refreshed!");
212
+ if (result?.summary) dim(` ${result.summary}`);
213
+ } catch (err) {
214
+ spinner.fail("Failed");
215
+ error(err.message);
216
+ process.exit(isAuthError(err) ? 2 : 1);
217
+ }
218
+ });
219
+
220
+ return cmd;
221
+ }