prdforge-cli 0.1.1 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prdforge-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Official CLI for PRDForge — generate and manage PRDs from your terminal or AI agents",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -40,6 +40,7 @@
40
40
  "commander": "^12.1.0",
41
41
  "conf": "^13.0.0",
42
42
  "dotenv": "^16.4.5",
43
+ "figlet": "^1.11.0",
43
44
  "ink": "^5.2.1",
44
45
  "ink-spinner": "^5.0.0",
45
46
  "ink-text-input": "^6.0.0",
package/src/api/client.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getApiUrl, requireApiKey } from "../utils/config.js";
2
+ import { debugLog } from "../utils/debug.js";
2
3
 
3
4
  /** User-facing messages for HTTP status codes (centralized for CLI and agents). */
4
5
  function messageForStatus(status, bodyMessage) {
@@ -24,16 +25,25 @@ function messageForStatus(status, bodyMessage) {
24
25
  * Make an authenticated request to the PRDForge API (Supabase Edge Functions).
25
26
  * Throws an Error with user-facing message and .httpStatus for exit code handling.
26
27
  */
28
+ export function buildHeaders(apiKey) {
29
+ return {
30
+ "Content-Type": "application/json",
31
+ "Authorization": `Bearer ${apiKey}`,
32
+ "x-prdforge-cli": "1",
33
+ };
34
+ }
35
+
27
36
  async function request(path, { method = "GET", body } = {}) {
28
37
  const apiKey = requireApiKey();
29
38
  const baseUrl = getApiUrl();
30
39
  const url = `${baseUrl}${path}`;
31
40
 
32
- const headers = {
33
- "Content-Type": "application/json",
34
- "Authorization": `Bearer ${apiKey}`,
35
- "x-prdforge-cli": "1",
36
- };
41
+ const headers = buildHeaders(apiKey);
42
+
43
+ debugLog("request", { method, url });
44
+
45
+ const controller = new AbortController();
46
+ const timeoutId = setTimeout(() => controller.abort(), 30_000);
37
47
 
38
48
  let res;
39
49
  try {
@@ -41,13 +51,23 @@ async function request(path, { method = "GET", body } = {}) {
41
51
  method,
42
52
  headers,
43
53
  body: body ? JSON.stringify(body) : undefined,
54
+ signal: controller.signal,
44
55
  });
45
56
  } catch (e) {
46
- const err = new Error("Could not reach PRDForge. Check your connection and API URL.");
57
+ const isTimeout = e.name === "AbortError";
58
+ const err = new Error(
59
+ isTimeout
60
+ ? "Request timed out. Check your connection and try again."
61
+ : "Could not reach PRDForge. Check your connection and API URL."
62
+ );
47
63
  err.httpStatus = 0;
48
64
  throw err;
65
+ } finally {
66
+ clearTimeout(timeoutId);
49
67
  }
50
68
 
69
+ debugLog("response", { status: res.status });
70
+
51
71
  if (!res.ok) {
52
72
  let bodyMessage;
53
73
  try {
@@ -131,10 +151,16 @@ export const exports_ = {
131
151
  // ─── Auth ────────────────────────────────────────────────────────────────────
132
152
 
133
153
  export const auth = {
134
- validate: (apiKey) =>
135
- fetch(`${getApiUrl()}/prdforge-api/auth/validate`, {
154
+ validate: (apiKey) => {
155
+ const controller = new AbortController();
156
+ const timeoutId = setTimeout(() => controller.abort(), 30_000);
157
+ return fetch(`${getApiUrl()}/prdforge-api/auth/validate`, {
136
158
  headers: { Authorization: `Bearer ${apiKey}` },
137
- }).then((r) => r.json()),
159
+ signal: controller.signal,
160
+ })
161
+ .then((r) => r.json())
162
+ .finally(() => clearTimeout(timeoutId));
163
+ },
138
164
  };
139
165
 
140
166
  // ─── User ────────────────────────────────────────────────────────────────────
@@ -0,0 +1,97 @@
1
+ import { buildHeaders } from "./client.js";
2
+ import { getApiUrl, requireApiKey } from "../utils/config.js";
3
+ import { debugLog } from "../utils/debug.js";
4
+
5
+ /**
6
+ * Async generator that streams PRD AI events via SSE.
7
+ * Falls back to a single synthesized "done" event if the server returns JSON.
8
+ *
9
+ * @param {{ action: string, project_id?: string, prompt?: string, model_id?: string }} body
10
+ * @param {AbortSignal} [signal]
11
+ * @yields {{ event: string, data: object }}
12
+ */
13
+ export async function* streamPrdAi(body, signal) {
14
+ const apiKey = requireApiKey();
15
+ const url = `${getApiUrl()}/prdforge-ai`;
16
+ debugLog("stream:request", { url, action: body.action });
17
+
18
+ let res;
19
+ try {
20
+ res = await fetch(url, {
21
+ method: "POST",
22
+ headers: { ...buildHeaders(apiKey), "Accept": "text/event-stream" },
23
+ body: JSON.stringify(body),
24
+ signal,
25
+ });
26
+ } catch (e) {
27
+ const isAbort = e.name === "AbortError";
28
+ if (isAbort) return; // clean cancellation
29
+ const err = new Error("Could not reach PRDForge. Check your connection and API URL.");
30
+ err.httpStatus = 0;
31
+ throw err;
32
+ }
33
+
34
+ // Fallback: server returned JSON (no SSE support yet)
35
+ if (!res.headers.get("content-type")?.includes("text/event-stream")) {
36
+ debugLog("stream:fallback", { status: res.status });
37
+ let json;
38
+ try { json = await res.json(); } catch { json = {}; }
39
+ if (!res.ok) {
40
+ const err = new Error(json.error ?? `Request failed (HTTP ${res.status}).`);
41
+ err.httpStatus = res.status;
42
+ throw err;
43
+ }
44
+ yield { event: "done", data: json };
45
+ return;
46
+ }
47
+
48
+ // SSE stream
49
+ const decoder = new TextDecoder();
50
+ let buffer = "";
51
+
52
+ try {
53
+ for await (const chunk of res.body) {
54
+ buffer += decoder.decode(chunk, { stream: true });
55
+ const blocks = buffer.split("\n\n");
56
+ buffer = blocks.pop(); // keep incomplete trailing block
57
+ for (const block of blocks) {
58
+ const parsed = parseSSEBlock(block);
59
+ if (parsed) {
60
+ debugLog("stream:event", { event: parsed.event });
61
+ yield parsed;
62
+ if (parsed.event === "done" || parsed.event === "error") return;
63
+ }
64
+ }
65
+ }
66
+ } catch (e) {
67
+ if (e.name === "AbortError") return;
68
+ throw e;
69
+ }
70
+
71
+ // Flush any remaining buffer after stream ends
72
+ if (buffer.trim()) {
73
+ const parsed = parseSSEBlock(buffer);
74
+ if (parsed) yield parsed;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Parse a single SSE block (lines separated by \n, blocks separated by \n\n).
80
+ * @param {string} block
81
+ * @returns {{ event: string, data: object | string } | null}
82
+ */
83
+ function parseSSEBlock(block) {
84
+ let event = "message";
85
+ let data = null;
86
+
87
+ for (const line of block.split("\n")) {
88
+ if (line.startsWith("event: ")) {
89
+ event = line.slice(7).trim();
90
+ } else if (line.startsWith("data: ")) {
91
+ const raw = line.slice(6);
92
+ try { data = JSON.parse(raw); } catch { data = raw; }
93
+ }
94
+ }
95
+
96
+ return data !== null ? { event, data } : null;
97
+ }
@@ -120,10 +120,141 @@ export function authCommand() {
120
120
  info("Run 'prdforge prd list' to see your projects.");
121
121
  });
122
122
 
123
+ cmd
124
+ .command("signup")
125
+ .description("Create a new PRDForge account")
126
+ .action(async () => {
127
+ const { default: inquirer } = await import("inquirer");
128
+ const { default: ora } = await import("ora");
129
+
130
+ const { email } = await inquirer.prompt([{
131
+ type: "input",
132
+ name: "email",
133
+ message: "Email address:",
134
+ validate: (v) => v.trim().includes("@") || "Enter a valid email",
135
+ }]);
136
+
137
+ const { firstName } = await inquirer.prompt([{
138
+ type: "input",
139
+ name: "firstName",
140
+ message: "First name (optional, press Enter to skip):",
141
+ }]);
142
+
143
+ const authBase = getAuthBase();
144
+ process.stdout.write("\n");
145
+ const spinner = ora("Creating your account…").start();
146
+
147
+ let sendRes, sendData;
148
+ try {
149
+ sendRes = await fetch(authBase, {
150
+ method: "POST",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify({
153
+ action: "signup",
154
+ email: email.trim().toLowerCase(),
155
+ ...(firstName.trim() ? { first_name: firstName.trim().slice(0, 50) } : {}),
156
+ }),
157
+ });
158
+ sendData = await sendRes.json().catch(() => ({}));
159
+ } catch {
160
+ spinner.fail("Network error — could not reach PRDForge.");
161
+ process.exit(1);
162
+ }
163
+
164
+ const errMsg = (sendData.error ?? "").toLowerCase();
165
+
166
+ if (!sendRes.ok && !errMsg.includes("already") && !errMsg.includes("exists")
167
+ && sendRes.status !== 404 && sendRes.status !== 405) {
168
+ spinner.fail(sendData.error ?? "Signup failed.");
169
+ process.exit(1);
170
+ }
171
+
172
+ if (errMsg.includes("already") || errMsg.includes("exists")) {
173
+ spinner.warn("Account already exists — sending a login code instead.");
174
+ try {
175
+ sendRes = await fetch(authBase, {
176
+ method: "POST",
177
+ headers: { "Content-Type": "application/json" },
178
+ body: JSON.stringify({ action: "request", email: email.trim().toLowerCase() }),
179
+ });
180
+ sendData = await sendRes.json().catch(() => ({}));
181
+ } catch {
182
+ error("Network error.");
183
+ process.exit(1);
184
+ }
185
+ if (!sendRes.ok || !sendData.success) {
186
+ error(sendData.error ?? "Failed to send code.");
187
+ process.exit(1);
188
+ }
189
+ } else if (sendRes.status === 404 || sendRes.status === 405) {
190
+ // No dedicated signup endpoint — fall back to OTP request silently
191
+ try {
192
+ sendRes = await fetch(authBase, {
193
+ method: "POST",
194
+ headers: { "Content-Type": "application/json" },
195
+ body: JSON.stringify({ action: "request", email: email.trim().toLowerCase() }),
196
+ });
197
+ sendData = await sendRes.json().catch(() => ({}));
198
+ } catch {
199
+ spinner.fail("Network error.");
200
+ process.exit(1);
201
+ }
202
+ if (!sendRes.ok || !sendData.success) {
203
+ spinner.fail(sendData.error ?? "Failed to send verification code.");
204
+ process.exit(1);
205
+ }
206
+ }
207
+
208
+ spinner.succeed("Verification code sent — check your email.");
209
+
210
+ const { code } = await inquirer.prompt([{
211
+ type: "input",
212
+ name: "code",
213
+ message: "Enter the 6-digit code:",
214
+ validate: (v) => /^\d{6}$/.test(v.trim()) || "Must be 6 digits",
215
+ }]);
216
+
217
+ const verSpinner = ora("Verifying…").start();
218
+ let verRes, verData;
219
+ try {
220
+ verRes = await fetch(authBase, {
221
+ method: "POST",
222
+ headers: { "Content-Type": "application/json" },
223
+ body: JSON.stringify({ action: "verify", email: email.trim().toLowerCase(), code: code.trim() }),
224
+ });
225
+ verData = await verRes.json().catch(() => ({}));
226
+ } catch {
227
+ verSpinner.fail("Network error.");
228
+ process.exit(1);
229
+ }
230
+
231
+ if (!verRes.ok || !verData.success) {
232
+ verSpinner.fail(verData.error ?? "Invalid or expired code.");
233
+ process.exit(1);
234
+ }
235
+
236
+ config.set("apiKey", verData.api_key);
237
+ setEmail(verData.email ?? email.trim().toLowerCase());
238
+ verSpinner.succeed(`Welcome to PRDForge! Signed in as ${verData.email ?? email}`);
239
+ dim(" Config: " + config.path);
240
+ info("Run 'prdforge prd list' or just 'prdforge' to get started.");
241
+ });
242
+
123
243
  cmd
124
244
  .command("logout")
125
245
  .description("Remove stored API key")
126
- .action(() => {
246
+ .option("-f, --force", "Skip confirmation prompt")
247
+ .action(async (opts) => {
248
+ if (!opts.force) {
249
+ const { default: inquirer } = await import("inquirer");
250
+ const { confirmed } = await inquirer.prompt([{
251
+ type: "confirm",
252
+ name: "confirmed",
253
+ message: "Remove your stored API key?",
254
+ default: false,
255
+ }]);
256
+ if (!confirmed) { info("Logout cancelled."); return; }
257
+ }
127
258
  config.delete("apiKey");
128
259
  success("Logged out — API key removed.");
129
260
  });
@@ -1,7 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import React from "react";
3
3
  import { render } from "ink";
4
- import { Dashboard } from "../ui/Dashboard.js";
5
4
  import { requireApiKey } from "../utils/config.js";
6
5
 
7
6
  export function dashboardCommand() {
@@ -10,79 +9,8 @@ export function dashboardCommand() {
10
9
  .description("Open interactive project overview (keyboard navigable)")
11
10
  .action(async () => {
12
11
  requireApiKey();
13
-
14
- let createArgs = null;
15
-
16
- const { waitUntilExit } = render(
17
- React.createElement(Dashboard, {
18
- onCreateNew: (args) => { createArgs = args; },
19
- })
20
- );
21
-
12
+ const { REPLDashboard } = await import("../ui/REPLDashboard.js");
13
+ const { waitUntilExit } = render(React.createElement(REPLDashboard));
22
14
  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
15
  });
88
16
  }
@@ -0,0 +1,20 @@
1
+ import { useState } from "react";
2
+
3
+ /**
4
+ * Virtual windowing scroll hook.
5
+ * @param {number} totalItems - total number of items in the list
6
+ * @param {number} visibleRows - how many rows can be shown at once
7
+ */
8
+ export function useScroll(totalItems, visibleRows) {
9
+ const [offset, setOffset] = useState(0);
10
+
11
+ const clamp = (o) => Math.min(Math.max(o, 0), Math.max(0, totalItems - visibleRows));
12
+
13
+ return {
14
+ offset,
15
+ scrollDown: () => setOffset((o) => clamp(o + 1)),
16
+ scrollUp: () => setOffset((o) => clamp(o - 1)),
17
+ jumpToBottom: () => setOffset(clamp(totalItems)),
18
+ reset: () => setOffset(0),
19
+ };
20
+ }
@@ -0,0 +1,144 @@
1
+ import { useState, useRef, useCallback, useEffect } from "react";
2
+ import { streamPrdAi } from "../api/stream.js";
3
+ import { theme } from "../ui/theme.js";
4
+
5
+ /**
6
+ * Manages SSE streaming state for PRD generation and updates.
7
+ *
8
+ * Returns:
9
+ * stages — [{label, status, preview}] for StageIndicator rendering
10
+ * messages — [{id, role, text, status}] for ReplPanel history
11
+ * setMessages — direct setter (for adding user messages)
12
+ * isStreaming — bool
13
+ * error — string | null
14
+ * startStream — (body, options?) => void begins stream
15
+ * abort — () => void cancels current stream
16
+ */
17
+ export function useStream() {
18
+ const [stages, setStages] = useState([]);
19
+ const [messages, setMessages] = useState([]);
20
+ const [isStreaming, setIsStreaming] = useState(false);
21
+ const [error, setError] = useState(null);
22
+
23
+ const abortRef = useRef(null);
24
+ const chunkBufRef = useRef({});
25
+ const flushIntervalRef = useRef(null);
26
+
27
+ // Flush accumulated chunk buffers to stage preview state at ~12fps
28
+ useEffect(() => {
29
+ if (!isStreaming) return;
30
+ const id = setInterval(() => {
31
+ const buf = chunkBufRef.current;
32
+ if (Object.keys(buf).length === 0) return;
33
+ setStages((prev) =>
34
+ prev.map((s, i) =>
35
+ buf[i] !== undefined ? { ...s, preview: buf[i].slice(0, 120) } : s
36
+ )
37
+ );
38
+ }, 80);
39
+ flushIntervalRef.current = id;
40
+ return () => clearInterval(id);
41
+ }, [isStreaming]);
42
+
43
+ const startStream = useCallback(async (body, { initialStages } = {}) => {
44
+ const controller = new AbortController();
45
+ abortRef.current = controller;
46
+ chunkBufRef.current = {};
47
+ setError(null);
48
+ setIsStreaming(true);
49
+
50
+ if (initialStages) {
51
+ setStages(initialStages.map((label) => ({ label, status: "pending", preview: undefined })));
52
+ }
53
+
54
+ // Add a streaming placeholder message in REPL history (for smart_update)
55
+ if (body.action === "smart_update") {
56
+ setMessages((prev) => [
57
+ ...prev,
58
+ { id: crypto.randomUUID(), role: "ai", text: "Analysing PRD…", status: "streaming" },
59
+ ]);
60
+ }
61
+
62
+ try {
63
+ for await (const { event, data } of streamPrdAi(body, controller.signal)) {
64
+ if (event === "stage_start") {
65
+ const i = data.stage_index ?? 0;
66
+ setStages((prev) =>
67
+ prev.map((s, idx) =>
68
+ idx < i ? { ...s, status: "done", preview: undefined } :
69
+ idx === i ? { ...s, status: "streaming" } : s
70
+ )
71
+ );
72
+ } else if (event === "stage_chunk") {
73
+ const i = data.stage_index ?? 0;
74
+ chunkBufRef.current[i] = (chunkBufRef.current[i] ?? "") + (data.chunk ?? "");
75
+ } else if (event === "stage_done") {
76
+ const i = data.stage_index ?? 0;
77
+ delete chunkBufRef.current[i];
78
+ setStages((prev) =>
79
+ prev.map((s, idx) =>
80
+ idx === i ? { ...s, status: "done", preview: undefined } : s
81
+ )
82
+ );
83
+ } else if (event === "update_progress") {
84
+ const msg = data.message ?? "Updating…";
85
+ setMessages((prev) => {
86
+ const idx = prev.findLastIndex((m) => m.status === "streaming");
87
+ if (idx === -1) return prev;
88
+ return prev.map((m, i) => i === idx ? { ...m, text: msg } : m);
89
+ });
90
+ } else if (event === "done") {
91
+ const summary = data.summary ? ` — ${data.summary}` : "";
92
+ setStages((prev) =>
93
+ prev.map((s) =>
94
+ s.status === "streaming" || s.status === "pending"
95
+ ? { ...s, status: "done", preview: undefined }
96
+ : s
97
+ )
98
+ );
99
+ if (body.action === "smart_update") {
100
+ setMessages((prev) =>
101
+ prev.map((m) =>
102
+ m.status === "streaming"
103
+ ? { ...m, role: "status", text: `✓ Done${summary}`, status: "done" }
104
+ : m
105
+ )
106
+ );
107
+ }
108
+ break;
109
+ } else if (event === "error") {
110
+ const msg = data.message ?? "Generation failed";
111
+ setError(msg);
112
+ setMessages((prev) =>
113
+ prev.map((m) =>
114
+ m.status === "streaming"
115
+ ? { ...m, text: msg, status: "error" }
116
+ : m
117
+ )
118
+ );
119
+ break;
120
+ }
121
+ }
122
+ } catch (e) {
123
+ if (e.name !== "AbortError") {
124
+ const msg = e.message ?? "Unexpected error";
125
+ setError(msg);
126
+ setMessages((prev) =>
127
+ prev.map((m) =>
128
+ m.status === "streaming" ? { ...m, text: msg, status: "error" } : m
129
+ )
130
+ );
131
+ }
132
+ } finally {
133
+ clearInterval(flushIntervalRef.current);
134
+ setIsStreaming(false);
135
+ abortRef.current = null;
136
+ }
137
+ }, []);
138
+
139
+ const abort = useCallback(() => {
140
+ abortRef.current?.abort();
141
+ }, []);
142
+
143
+ return { stages, setStages, messages, setMessages, isStreaming, error, startStream, abort };
144
+ }
@@ -0,0 +1,24 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useStdout } from "ink";
3
+
4
+ /**
5
+ * Returns live terminal dimensions, updating on window resize.
6
+ * @returns {{ columns: number, rows: number }}
7
+ */
8
+ export function useTerminalSize() {
9
+ const { stdout } = useStdout();
10
+ const [size, setSize] = useState({
11
+ columns: stdout?.columns ?? 80,
12
+ rows: stdout?.rows ?? 24,
13
+ });
14
+
15
+ useEffect(() => {
16
+ if (!stdout) return;
17
+ const handler = () =>
18
+ setSize({ columns: stdout.columns ?? 80, rows: stdout.rows ?? 24 });
19
+ stdout.on("resize", handler);
20
+ return () => stdout.off("resize", handler);
21
+ }, [stdout]);
22
+
23
+ return size;
24
+ }
package/src/index.js CHANGED
@@ -13,7 +13,7 @@ import { creditsCommand } from "./commands/credits.js";
13
13
  import { dashboardCommand } from "./commands/dashboard.js";
14
14
 
15
15
  const LOGO = `
16
- \x1b[1m\x1b[36m PRDForge CLI\x1b[0m \x1b[2mv0.1.0\x1b[0m
16
+ \x1b[1m\x1b[36m PRDForge CLI\x1b[0m \x1b[2mv0.2.0\x1b[0m
17
17
  \x1b[90mThe planning layer for AI-first development\x1b[0m
18
18
  `;
19
19
 
@@ -22,14 +22,17 @@ const program = new Command();
22
22
  program
23
23
  .name("prdforge")
24
24
  .description("PRDForge CLI — generate, manage, and export PRDs from your terminal or AI agents")
25
- .version("0.1.0", "-v, --version")
25
+ .version("0.2.0", "-v, --version")
26
26
  .addHelpText("beforeAll", LOGO)
27
- .hook("preAction", async () => {
27
+ .option("--debug", "Enable verbose debug logging")
28
+ .hook("preAction", async (thisCommand) => {
28
29
  // Load .env from cwd if present
29
30
  try {
30
31
  const { config: dotenv } = await import("dotenv");
31
32
  dotenv();
32
33
  } catch {}
34
+ // Activate debug mode
35
+ if (thisCommand.opts().debug) global.__prdforgeDebug = true;
33
36
  });
34
37
 
35
38
  program.addCommand(authCommand());
@@ -51,14 +54,16 @@ program.on("command:*", (cmds) => {
51
54
  process.exit(1);
52
55
  });
53
56
 
54
- // When run with no arguments: open dashboard if authenticated, else show help
57
+ // When run with no arguments: open dashboard if authenticated, else show welcome screen
55
58
  if (process.argv.length === 2) {
56
59
  const { getApiKey } = await import("./utils/config.js");
57
60
  if (getApiKey()) {
58
61
  const { dashboardCommand } = await import("./commands/dashboard.js");
59
62
  await dashboardCommand().parseAsync([], { from: "user" });
60
63
  } else {
61
- program.help();
64
+ const { printWelcome } = await import("./utils/logo.js");
65
+ printWelcome();
66
+ process.exit(0);
62
67
  }
63
68
  }
64
69