specdown-cli 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/README.md +7 -5
  2. package/dist/index.js +228 -142
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,13 +14,15 @@ Read, write, push, pull, and search spec documents. Works great in CI/CD pipelin
14
14
  ## Install
15
15
 
16
16
  ```bash
17
- # Run without installing (recommended):
18
- npx specdown-cli --help
19
-
20
- # Or install globally:
21
17
  npm install -g specdown-cli
22
18
  ```
23
19
 
20
+ Or run once without installing:
21
+
22
+ ```bash
23
+ npx specdown-cli --help
24
+ ```
25
+
24
26
  **Requirements:** Node.js ≥ 18
25
27
 
26
28
  ---
@@ -48,7 +50,7 @@ specdown read /README.md
48
50
  ### Authentication
49
51
 
50
52
  ```bash
51
- specdown login # Sign in with email + password
53
+ specdown login # Sign in via browser (Google OAuth)
52
54
  specdown logout # Sign out and clear credentials
53
55
  specdown whoami # Show current user and active project
54
56
  ```
package/dist/index.js CHANGED
@@ -3,36 +3,74 @@
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
5
 
6
- // src/commands/login.ts
6
+ // src/lib/update-check.ts
7
7
  import chalk from "chalk";
8
- import ora from "ora";
9
-
10
- // src/lib/api.ts
11
- import { createClient } from "@supabase/supabase-js";
12
- var SUPABASE_URL = "https://zjvjdalqgrxdhefqqifd.supabase.co";
13
- var SUPABASE_ANON_KEY = "sb_publishable_y-O0ly4bNH0G4KJii3m25g_WXujFZDo";
14
- function getClient(cfg) {
15
- const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
16
- auth: { persistSession: false }
17
- });
18
- client.auth.setSession({
19
- access_token: cfg.access_token,
20
- refresh_token: cfg.refresh_token
21
- });
22
- return client;
8
+ import { createRequire } from "module";
9
+ import { fileURLToPath } from "url";
10
+ import { dirname, join } from "path";
11
+ var PKG_NAME = "specdown-cli";
12
+ var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
13
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
14
+ function getCurrentVersion() {
15
+ try {
16
+ const require2 = createRequire(import.meta.url);
17
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json");
18
+ const pkg = require2(pkgPath);
19
+ return pkg.version;
20
+ } catch {
21
+ return "0.0.0";
22
+ }
23
23
  }
24
- function anonClient() {
25
- return createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
26
- auth: { persistSession: false }
24
+ function compareVersions(current, latest) {
25
+ const parse = (v) => v.split(".").map(Number);
26
+ const [cMaj, cMin, cPat] = parse(current);
27
+ const [lMaj, lMin, lPat] = parse(latest);
28
+ if (lMaj !== cMaj) return lMaj > cMaj;
29
+ if (lMin !== cMin) return lMin > cMin;
30
+ return lPat > cPat;
31
+ }
32
+ function checkForUpdate() {
33
+ if (process.env.npm_lifecycle_event === "npx" || process.env.CI) return;
34
+ const current = getCurrentVersion();
35
+ setImmediate(async () => {
36
+ try {
37
+ const controller = new AbortController();
38
+ const timeout = setTimeout(() => controller.abort(), 3e3);
39
+ const res = await fetch(REGISTRY_URL, { signal: controller.signal });
40
+ clearTimeout(timeout);
41
+ if (!res.ok) return;
42
+ const data = await res.json();
43
+ const latest = data.version;
44
+ if (compareVersions(current, latest)) {
45
+ setTimeout(() => {
46
+ console.error(
47
+ "\n" + chalk.yellow("\u250C\u2500 Update available ") + chalk.dim(`${current}`) + chalk.yellow(" \u2192 ") + chalk.green(latest) + chalk.yellow(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500") + "\n" + chalk.yellow("\u2502 ") + chalk.bold("npm install -g specdown-cli") + "\n" + chalk.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500") + "\n"
48
+ );
49
+ }, 50);
50
+ }
51
+ } catch {
52
+ }
27
53
  });
28
54
  }
29
55
 
56
+ // src/index.ts
57
+ import { createRequire as createRequire2 } from "module";
58
+ import { fileURLToPath as fileURLToPath2 } from "url";
59
+ import { dirname as dirname3, join as join3 } from "path";
60
+
61
+ // src/commands/login.ts
62
+ import http from "http";
63
+ import { exec } from "child_process";
64
+ import crypto from "crypto";
65
+ import chalk2 from "chalk";
66
+ import ora from "ora";
67
+
30
68
  // src/lib/config.ts
31
69
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
32
70
  import { homedir } from "os";
33
- import { join } from "path";
34
- var CONFIG_DIR = join(homedir(), ".specdown");
35
- var CONFIG_FILE = join(CONFIG_DIR, "config.json");
71
+ import { join as join2 } from "path";
72
+ var CONFIG_DIR = join2(homedir(), ".specdown");
73
+ var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
36
74
  function readConfig() {
37
75
  if (!existsSync(CONFIG_FILE)) return null;
38
76
  try {
@@ -68,97 +106,128 @@ function requireProject(cfg) {
68
106
  };
69
107
  }
70
108
 
71
- // src/lib/prompt.ts
72
- import { createInterface } from "readline";
73
- function ask(question) {
74
- const rl = createInterface({ input: process.stdin, output: process.stdout });
75
- return new Promise((resolve) => {
76
- rl.question(question, (answer) => {
77
- rl.close();
78
- resolve(answer.trim());
79
- });
80
- });
109
+ // src/commands/login.ts
110
+ var APP_URL = "https://specdown.app";
111
+ var TIMEOUT_MS = 5 * 60 * 1e3;
112
+ function openBrowser(url) {
113
+ const platform = process.platform;
114
+ const cmd = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
115
+ exec(cmd);
81
116
  }
82
- function askPassword(question) {
83
- return new Promise((resolve) => {
84
- process.stdout.write(question);
85
- const stdin = process.stdin;
86
- const chunks = [];
87
- stdin.setRawMode(true);
88
- stdin.resume();
89
- stdin.on("data", function handler(chunk) {
90
- const char = chunk.toString();
91
- if (char === "\r" || char === "\n") {
92
- stdin.setRawMode(false);
93
- stdin.pause();
94
- stdin.removeListener("data", handler);
95
- process.stdout.write("\n");
96
- resolve(Buffer.concat(chunks).toString());
97
- } else if (char === "") {
98
- process.exit();
99
- } else if (char === "\x7F") {
100
- if (chunks.length) chunks.pop();
101
- } else {
102
- chunks.push(chunk);
117
+ async function login() {
118
+ console.log(chalk2.bold("\nSpecDown Login\n"));
119
+ const port = 7400 + Math.floor(Math.random() * 100);
120
+ const state = crypto.randomBytes(16).toString("hex");
121
+ const authUrl = `${APP_URL}/cli/auth?port=${port}&state=${state}`;
122
+ return new Promise((resolve, reject) => {
123
+ const server = http.createServer((req, res) => {
124
+ res.setHeader("Access-Control-Allow-Origin", APP_URL);
125
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
126
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
127
+ if (req.method === "OPTIONS") {
128
+ res.writeHead(204);
129
+ res.end();
130
+ return;
103
131
  }
132
+ if (req.method !== "POST") {
133
+ res.writeHead(405);
134
+ res.end();
135
+ return;
136
+ }
137
+ let body = "";
138
+ req.on("data", (chunk) => {
139
+ body += chunk;
140
+ });
141
+ req.on("end", () => {
142
+ try {
143
+ const payload = JSON.parse(body);
144
+ if (payload.state !== state) {
145
+ res.writeHead(403);
146
+ res.end(JSON.stringify({ error: "Invalid state" }));
147
+ return;
148
+ }
149
+ writeConfig({
150
+ access_token: payload.access_token,
151
+ refresh_token: payload.refresh_token,
152
+ user_email: payload.email,
153
+ user_id: payload.user_id
154
+ });
155
+ res.writeHead(200);
156
+ res.end(JSON.stringify({ ok: true }));
157
+ spinner.succeed(chalk2.green(`Logged in as ${chalk2.bold(payload.email)}`));
158
+ server.close();
159
+ resolve();
160
+ } catch {
161
+ res.writeHead(400);
162
+ res.end(JSON.stringify({ error: "Bad request" }));
163
+ }
164
+ });
104
165
  });
105
- });
106
- }
107
-
108
- // src/commands/login.ts
109
- async function login() {
110
- console.log(chalk.bold("\nSpecDown Login\n"));
111
- const email = await ask("Email: ");
112
- const password = await askPassword("Password: ");
113
- const spinner = ora("Signing in\u2026").start();
114
- try {
115
- const supabase = anonClient();
116
- const { data, error } = await supabase.auth.signInWithPassword({ email, password });
117
- if (error || !data.session) {
118
- spinner.fail(chalk.red("Login failed: " + (error?.message ?? "No session")));
119
- process.exit(1);
120
- }
121
- const { session, user } = data;
122
- writeConfig({
123
- access_token: session.access_token,
124
- refresh_token: session.refresh_token,
125
- user_email: user.email ?? email,
126
- user_id: user.id
166
+ const spinner = ora();
167
+ server.listen(port, "127.0.0.1", () => {
168
+ console.log(chalk2.cyan("Opening browser to complete login\u2026"));
169
+ console.log(chalk2.dim(` ${authUrl}
170
+ `));
171
+ console.log(chalk2.dim("If browser did not open, visit the URL above manually.\n"));
172
+ openBrowser(authUrl);
173
+ spinner.start("Waiting for authentication\u2026");
127
174
  });
128
- spinner.succeed(chalk.green(`Logged in as ${chalk.bold(user.email)}`));
129
- } catch (err) {
130
- spinner.fail(chalk.red("Login failed"));
131
- console.error(err);
132
- process.exit(1);
133
- }
175
+ const timer = setTimeout(() => {
176
+ spinner.fail(chalk2.red("Login timed out (5 minutes). Run `specdown login` again."));
177
+ server.close();
178
+ reject(new Error("timeout"));
179
+ }, TIMEOUT_MS);
180
+ server.on("close", () => clearTimeout(timer));
181
+ server.on("error", (err) => {
182
+ spinner.fail(chalk2.red("Could not start local server: " + err.message));
183
+ reject(err);
184
+ });
185
+ });
134
186
  }
135
187
 
136
188
  // src/commands/logout.ts
137
- import chalk2 from "chalk";
189
+ import chalk3 from "chalk";
138
190
  function logout() {
139
191
  const cfg = readConfig();
140
192
  if (!cfg?.access_token) {
141
- console.log(chalk2.yellow("Not logged in."));
193
+ console.log(chalk3.yellow("Not logged in."));
142
194
  return;
143
195
  }
144
196
  clearConfig();
145
- console.log(chalk2.green("Logged out."));
197
+ console.log(chalk3.green("Logged out."));
146
198
  }
147
199
 
148
200
  // src/commands/whoami.ts
149
- import chalk3 from "chalk";
201
+ import chalk4 from "chalk";
150
202
  function whoami() {
151
203
  const cfg = requireAuth();
152
- console.log(chalk3.bold(cfg.user_email));
153
- console.log(chalk3.gray("ID: ") + cfg.user_id);
204
+ console.log(chalk4.bold(cfg.user_email));
205
+ console.log(chalk4.gray("ID: ") + cfg.user_id);
154
206
  if (cfg.current_project_name) {
155
- console.log(chalk3.gray("Project: ") + cfg.current_project_name + chalk3.gray(` (${cfg.current_project_slug})`));
207
+ console.log(chalk4.gray("Project: ") + cfg.current_project_name + chalk4.gray(` (${cfg.current_project_slug})`));
156
208
  }
157
209
  }
158
210
 
159
211
  // src/commands/projects.ts
160
- import chalk4 from "chalk";
212
+ import chalk5 from "chalk";
161
213
  import ora2 from "ora";
214
+
215
+ // src/lib/api.ts
216
+ import { createClient } from "@supabase/supabase-js";
217
+ var SUPABASE_URL = "https://zjvjdalqgrxdhefqqifd.supabase.co";
218
+ var SUPABASE_ANON_KEY = "sb_publishable_y-O0ly4bNH0G4KJii3m25g_WXujFZDo";
219
+ function getClient(cfg) {
220
+ const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
221
+ auth: { persistSession: false }
222
+ });
223
+ client.auth.setSession({
224
+ access_token: cfg.access_token,
225
+ refresh_token: cfg.refresh_token
226
+ });
227
+ return client;
228
+ }
229
+
230
+ // src/commands/projects.ts
162
231
  async function listProjects() {
163
232
  const cfg = requireAuth();
164
233
  const supabase = getClient(cfg);
@@ -168,24 +237,24 @@ async function listProjects() {
168
237
  if (error) throw error;
169
238
  spinner.stop();
170
239
  if (!data?.length) {
171
- console.log(chalk4.gray("No projects found."));
240
+ console.log(chalk5.gray("No projects found."));
172
241
  return;
173
242
  }
174
243
  for (const p of data) {
175
244
  const isCurrent = p.slug === cfg.current_project_slug;
176
- const marker = isCurrent ? chalk4.green("* ") : " ";
177
- const name = isCurrent ? chalk4.bold.green(p.name) : chalk4.bold(p.name);
178
- console.log(`${marker}${name} ${chalk4.gray(p.slug)}`);
245
+ const marker = isCurrent ? chalk5.green("* ") : " ";
246
+ const name = isCurrent ? chalk5.bold.green(p.name) : chalk5.bold(p.name);
247
+ console.log(`${marker}${name} ${chalk5.gray(p.slug)}`);
179
248
  }
180
249
  } catch (err) {
181
- spinner.fail(chalk4.red("Failed to fetch projects"));
250
+ spinner.fail(chalk5.red("Failed to fetch projects"));
182
251
  console.error(err);
183
252
  process.exit(1);
184
253
  }
185
254
  }
186
255
 
187
256
  // src/commands/use.ts
188
- import chalk5 from "chalk";
257
+ import chalk6 from "chalk";
189
258
  import ora3 from "ora";
190
259
  async function useProject(slug) {
191
260
  const cfg = requireAuth();
@@ -194,7 +263,7 @@ async function useProject(slug) {
194
263
  try {
195
264
  const { data, error } = await supabase.from("projects").select("id, name, slug").eq("slug", slug).single();
196
265
  if (error || !data) {
197
- spinner.fail(chalk5.red(`Project "${slug}" not found.`));
266
+ spinner.fail(chalk6.red(`Project "${slug}" not found.`));
198
267
  process.exit(1);
199
268
  }
200
269
  const current = readConfig() ?? cfg;
@@ -204,16 +273,16 @@ async function useProject(slug) {
204
273
  current_project_slug: data.slug,
205
274
  current_project_name: data.name
206
275
  });
207
- spinner.succeed(chalk5.green(`Now using project ${chalk5.bold(data.name)}`));
276
+ spinner.succeed(chalk6.green(`Now using project ${chalk6.bold(data.name)}`));
208
277
  } catch (err) {
209
- spinner.fail(chalk5.red("Failed to switch project"));
278
+ spinner.fail(chalk6.red("Failed to switch project"));
210
279
  console.error(err);
211
280
  process.exit(1);
212
281
  }
213
282
  }
214
283
 
215
284
  // src/commands/ls.ts
216
- import chalk6 from "chalk";
285
+ import chalk7 from "chalk";
217
286
  import ora4 from "ora";
218
287
  function buildTree(docs) {
219
288
  const map = /* @__PURE__ */ new Map();
@@ -244,9 +313,9 @@ function printTree(nodes, prefix = "") {
244
313
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
245
314
  const childPrefix = prefix + (isLast ? " " : "\u2502 ");
246
315
  if (node.is_folder) {
247
- console.log(prefix + connector + chalk6.bold.blue("\u{1F4C1} " + node.title));
316
+ console.log(prefix + connector + chalk7.bold.blue("\u{1F4C1} " + node.title));
248
317
  } else {
249
- console.log(prefix + connector + chalk6.white("\u{1F4C4} " + node.title) + chalk6.gray(" " + node.path));
318
+ console.log(prefix + connector + chalk7.white("\u{1F4C4} " + node.title) + chalk7.gray(" " + node.path));
250
319
  }
251
320
  if (node.children.length) printTree(node.children, childPrefix);
252
321
  }
@@ -260,25 +329,25 @@ async function ls() {
260
329
  const { data, error } = await supabase.from("documents").select("id, title, slug, path, full_path, is_folder, parent_id, sort_order, updated_at").eq("project_id", project.id).is("deleted_at", null).order("is_folder", { ascending: false }).order("sort_order").order("title");
261
330
  if (error) throw error;
262
331
  spinner.stop();
263
- console.log(chalk6.bold(`
332
+ console.log(chalk7.bold(`
264
333
  ${project.name}
265
334
  `));
266
335
  if (!data?.length) {
267
- console.log(chalk6.gray(" (empty)"));
336
+ console.log(chalk7.gray(" (empty)"));
268
337
  return;
269
338
  }
270
339
  const tree = buildTree(data);
271
340
  printTree(tree);
272
341
  console.log();
273
342
  } catch (err) {
274
- spinner.fail(chalk6.red("Failed to list documents"));
343
+ spinner.fail(chalk7.red("Failed to list documents"));
275
344
  console.error(err);
276
345
  process.exit(1);
277
346
  }
278
347
  }
279
348
 
280
349
  // src/commands/read.ts
281
- import chalk7 from "chalk";
350
+ import chalk8 from "chalk";
282
351
  import ora5 from "ora";
283
352
  function normalizePath(p) {
284
353
  return p.startsWith("/") ? p : `/${p}`;
@@ -291,7 +360,7 @@ async function readDoc(pathArg, opts) {
291
360
  try {
292
361
  const { data, error } = await supabase.from("documents").select("title, full_path, content, updated_at").eq("project_id", project.id).eq("is_folder", false).is("deleted_at", null).eq("full_path", normalizePath(pathArg)).single();
293
362
  if (error || !data) {
294
- spinner.fail(chalk7.red(`Document not found: ${pathArg}`));
363
+ spinner.fail(chalk8.red(`Document not found: ${pathArg}`));
295
364
  process.exit(1);
296
365
  }
297
366
  spinner.stop();
@@ -300,29 +369,29 @@ async function readDoc(pathArg, opts) {
300
369
  const fromLine = opts.from ? Math.max(1, parseInt(opts.from, 10)) : 1;
301
370
  const toLine = opts.to ? Math.min(totalLines, parseInt(opts.to, 10)) : totalLines;
302
371
  if (fromLine > toLine) {
303
- console.error(chalk7.red(`Invalid range: --from ${fromLine} is after --to ${toLine}`));
372
+ console.error(chalk8.red(`Invalid range: --from ${fromLine} is after --to ${toLine}`));
304
373
  process.exit(1);
305
374
  }
306
375
  const selectedLines = rawLines.slice(fromLine - 1, toLine);
307
376
  const rangeLabel = fromLine === 1 && toLine === totalLines ? `${totalLines} lines` : `lines ${fromLine}\u2013${toLine} of ${totalLines}`;
308
- console.log(chalk7.gray(`# ${data.full_path} (${rangeLabel}, updated ${new Date(data.updated_at).toLocaleString()})
377
+ console.log(chalk8.gray(`# ${data.full_path} (${rangeLabel}, updated ${new Date(data.updated_at).toLocaleString()})
309
378
  `));
310
379
  if (opts.lineNumbers) {
311
380
  const padWidth = String(toLine).length;
312
381
  selectedLines.forEach((line, i) => {
313
- console.log(chalk7.gray(String(fromLine + i).padStart(padWidth) + " ") + line);
382
+ console.log(chalk8.gray(String(fromLine + i).padStart(padWidth) + " ") + line);
314
383
  });
315
384
  } else {
316
385
  console.log(selectedLines.join("\n"));
317
386
  }
318
387
  } catch {
319
- spinner.fail(chalk7.red("Failed to read document"));
388
+ spinner.fail(chalk8.red("Failed to read document"));
320
389
  process.exit(1);
321
390
  }
322
391
  }
323
392
 
324
393
  // src/commands/new.ts
325
- import chalk8 from "chalk";
394
+ import chalk9 from "chalk";
326
395
  import ora6 from "ora";
327
396
  function normalizePath2(p) {
328
397
  return p.startsWith("/") ? p : `/${p}`;
@@ -341,7 +410,7 @@ async function newDoc(title, opts) {
341
410
  const parentPath = normalizePath2(opts.parent);
342
411
  const { data: parentDoc } = await supabase.from("documents").select("id, full_path").eq("project_id", project.id).eq("is_folder", true).eq("full_path", parentPath).single();
343
412
  if (!parentDoc) {
344
- spinner.fail(chalk8.red(`Parent folder not found: ${opts.parent}`));
413
+ spinner.fail(chalk9.red(`Parent folder not found: ${opts.parent}`));
345
414
  process.exit(1);
346
415
  }
347
416
  parentId = parentDoc.id;
@@ -362,16 +431,16 @@ async function newDoc(title, opts) {
362
431
  `
363
432
  }).select("id, full_path").single();
364
433
  if (error) throw error;
365
- spinner.succeed(chalk8.green(`Created: ${data.full_path}`));
434
+ spinner.succeed(chalk9.green(`Created: ${data.full_path}`));
366
435
  } catch {
367
- spinner.fail(chalk8.red("Failed to create document"));
436
+ spinner.fail(chalk9.red("Failed to create document"));
368
437
  process.exit(1);
369
438
  }
370
439
  }
371
440
 
372
441
  // src/commands/push.ts
373
442
  import { readFileSync as readFileSync2, existsSync as existsSync2, statSync } from "fs";
374
- import chalk9 from "chalk";
443
+ import chalk10 from "chalk";
375
444
  import ora7 from "ora";
376
445
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
377
446
  function normalizePath3(p) {
@@ -379,12 +448,12 @@ function normalizePath3(p) {
379
448
  }
380
449
  async function push(filePath, docPath) {
381
450
  if (!existsSync2(filePath)) {
382
- console.error(chalk9.red(`File not found: ${filePath}`));
451
+ console.error(chalk10.red(`File not found: ${filePath}`));
383
452
  process.exit(1);
384
453
  }
385
454
  const stat = statSync(filePath);
386
455
  if (stat.size > MAX_FILE_BYTES) {
387
- console.error(chalk9.red(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Max 10 MB.`));
456
+ console.error(chalk10.red(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Max 10 MB.`));
388
457
  process.exit(1);
389
458
  }
390
459
  const content = readFileSync2(filePath, "utf-8");
@@ -398,7 +467,7 @@ async function push(filePath, docPath) {
398
467
  if (existing) {
399
468
  const { error } = await supabase.from("documents").update({ content, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id);
400
469
  if (error) throw error;
401
- spinner.succeed(chalk9.green(`Updated: ${fullPath}`));
470
+ spinner.succeed(chalk10.green(`Updated: ${fullPath}`));
402
471
  } else {
403
472
  const parts = fullPath.split("/");
404
473
  const filename = parts.pop() ?? "doc.md";
@@ -417,18 +486,18 @@ async function push(filePath, docPath) {
417
486
  content
418
487
  });
419
488
  if (error) throw error;
420
- spinner.succeed(chalk9.green(`Created: ${fullPath}`));
489
+ spinner.succeed(chalk10.green(`Created: ${fullPath}`));
421
490
  }
422
491
  } catch {
423
- spinner.fail(chalk9.red("Push failed"));
492
+ spinner.fail(chalk10.red("Push failed"));
424
493
  process.exit(1);
425
494
  }
426
495
  }
427
496
 
428
497
  // src/commands/pull.ts
429
498
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
430
- import { dirname } from "path";
431
- import chalk10 from "chalk";
499
+ import { dirname as dirname2 } from "path";
500
+ import chalk11 from "chalk";
432
501
  import ora8 from "ora";
433
502
  function normalizePath4(p) {
434
503
  return p.startsWith("/") ? p : `/${p}`;
@@ -441,28 +510,42 @@ async function pull(docPath, outFile) {
441
510
  try {
442
511
  const { data, error } = await supabase.from("documents").select("title, full_path, content").eq("project_id", project.id).eq("is_folder", false).is("deleted_at", null).eq("full_path", normalizePath4(docPath)).single();
443
512
  if (error || !data) {
444
- spinner.fail(chalk10.red(`Document not found: ${docPath}`));
513
+ spinner.fail(chalk11.red(`Document not found: ${docPath}`));
445
514
  process.exit(1);
446
515
  }
447
516
  const content = data.content ?? "";
448
517
  if (outFile) {
449
- const dir = dirname(outFile);
518
+ const dir = dirname2(outFile);
450
519
  if (dir !== ".") mkdirSync2(dir, { recursive: true });
451
520
  writeFileSync2(outFile, content, "utf-8");
452
- spinner.succeed(chalk10.green(`Saved to ${outFile}`));
521
+ spinner.succeed(chalk11.green(`Saved to ${outFile}`));
453
522
  } else {
454
523
  spinner.stop();
455
524
  process.stdout.write(content);
456
525
  }
457
526
  } catch {
458
- spinner.fail(chalk10.red("Pull failed"));
527
+ spinner.fail(chalk11.red("Pull failed"));
459
528
  process.exit(1);
460
529
  }
461
530
  }
462
531
 
463
532
  // src/commands/rm.ts
464
- import chalk11 from "chalk";
533
+ import chalk12 from "chalk";
465
534
  import ora9 from "ora";
535
+
536
+ // src/lib/prompt.ts
537
+ import { createInterface } from "readline";
538
+ function ask(question) {
539
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
540
+ return new Promise((resolve) => {
541
+ rl.question(question, (answer) => {
542
+ rl.close();
543
+ resolve(answer.trim());
544
+ });
545
+ });
546
+ }
547
+
548
+ // src/commands/rm.ts
466
549
  function normalizePath5(p) {
467
550
  return p.startsWith("/") ? p : `/${p}`;
468
551
  }
@@ -475,13 +558,13 @@ async function rm(docPath, opts) {
475
558
  try {
476
559
  const { data, error } = await supabase.from("documents").select("id, title, is_folder").eq("project_id", project.id).is("deleted_at", null).eq("full_path", fullPath).single();
477
560
  if (error || !data) {
478
- spinner.fail(chalk11.red(`Document not found: ${fullPath}`));
561
+ spinner.fail(chalk12.red(`Document not found: ${fullPath}`));
479
562
  process.exit(1);
480
563
  }
481
564
  spinner.stop();
482
565
  if (!opts.force) {
483
566
  const answer = await ask(
484
- chalk11.yellow(`Delete "${data.title}"${data.is_folder ? " (folder + all children)" : ""}? [y/N] `)
567
+ chalk12.yellow(`Delete "${data.title}"${data.is_folder ? " (folder + all children)" : ""}? [y/N] `)
485
568
  );
486
569
  if (answer.toLowerCase() !== "y") {
487
570
  console.log("Aborted.");
@@ -491,15 +574,15 @@ async function rm(docPath, opts) {
491
574
  const deleteSpinner = ora9("Deleting\u2026").start();
492
575
  const { error: delErr } = await supabase.from("documents").update({ deleted_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", data.id);
493
576
  if (delErr) throw delErr;
494
- deleteSpinner.succeed(chalk11.green(`Deleted: ${fullPath}`));
577
+ deleteSpinner.succeed(chalk12.green(`Deleted: ${fullPath}`));
495
578
  } catch {
496
- spinner.fail(chalk11.red("Delete failed"));
579
+ spinner.fail(chalk12.red("Delete failed"));
497
580
  process.exit(1);
498
581
  }
499
582
  }
500
583
 
501
584
  // src/commands/search.ts
502
- import chalk12 from "chalk";
585
+ import chalk13 from "chalk";
503
586
  import ora10 from "ora";
504
587
  function searchInContent(content, query, contextLines) {
505
588
  const lines = content.split("\n");
@@ -520,10 +603,10 @@ function searchInContent(content, query, contextLines) {
520
603
  function highlight(line, query) {
521
604
  const idx = line.toLowerCase().indexOf(query.toLowerCase());
522
605
  if (idx === -1) return line;
523
- return line.slice(0, idx) + chalk12.bgYellow.black(line.slice(idx, idx + query.length)) + line.slice(idx + query.length);
606
+ return line.slice(0, idx) + chalk13.bgYellow.black(line.slice(idx, idx + query.length)) + line.slice(idx + query.length);
524
607
  }
525
608
  function printContextLine(lineNo, line) {
526
- console.log(chalk12.gray(String(lineNo).padStart(4) + " \u2502 ") + chalk12.dim(line));
609
+ console.log(chalk13.gray(String(lineNo).padStart(4) + " \u2502 ") + chalk13.dim(line));
527
610
  }
528
611
  async function search(query, opts) {
529
612
  const cfg = requireAuth();
@@ -551,7 +634,7 @@ async function search(query, opts) {
551
634
  }
552
635
  }
553
636
  if (!allMatches.length) {
554
- console.log(chalk12.yellow(`No results for "${query}" in ${project.name}`));
637
+ console.log(chalk13.yellow(`No results for "${query}" in ${project.name}`));
555
638
  return;
556
639
  }
557
640
  let currentFile = "";
@@ -559,29 +642,32 @@ async function search(query, opts) {
559
642
  if (m.docPath !== currentFile) {
560
643
  if (currentFile) console.log();
561
644
  currentFile = m.docPath;
562
- console.log(chalk12.bold.cyan(`
645
+ console.log(chalk13.bold.cyan(`
563
646
  \u{1F4C4} ${m.docPath}`));
564
- console.log(chalk12.gray("\u2500".repeat(50)));
647
+ console.log(chalk13.gray("\u2500".repeat(50)));
565
648
  }
566
649
  m.before.forEach((l, i) => printContextLine(m.lineNo - m.before.length + i, l));
567
- console.log(chalk12.green(String(m.lineNo).padStart(4) + " \u2502 ") + highlight(m.line, query));
650
+ console.log(chalk13.green(String(m.lineNo).padStart(4) + " \u2502 ") + highlight(m.line, query));
568
651
  m.after.forEach((l, i) => printContextLine(m.lineNo + 1 + i, l));
569
- if (m.after.length || m.before.length) console.log(chalk12.gray(" \xB7"));
652
+ if (m.after.length || m.before.length) console.log(chalk13.gray(" \xB7"));
570
653
  }
571
654
  const fileCount = new Set(allMatches.map((m) => m.docPath)).size;
572
655
  console.log();
573
656
  console.log(
574
- chalk12.bold(`${allMatches.length} match${allMatches.length === 1 ? "" : "es"}`) + chalk12.gray(` across ${fileCount} file${fileCount === 1 ? "" : "s"} in `) + chalk12.bold(project.name)
657
+ chalk13.bold(`${allMatches.length} match${allMatches.length === 1 ? "" : "es"}`) + chalk13.gray(` across ${fileCount} file${fileCount === 1 ? "" : "s"} in `) + chalk13.bold(project.name)
575
658
  );
576
659
  } catch {
577
- spinner.fail(chalk12.red("Search failed"));
660
+ spinner.fail(chalk13.red("Search failed"));
578
661
  process.exit(1);
579
662
  }
580
663
  }
581
664
 
582
665
  // src/index.ts
666
+ var _require = createRequire2(import.meta.url);
667
+ var _pkg = _require(join3(dirname3(fileURLToPath2(import.meta.url)), "../package.json"));
668
+ checkForUpdate();
583
669
  var program = new Command();
584
- program.name("specdown").description("Manage SpecDown docs from your terminal").version("0.1.0");
670
+ program.name("specdown").description("Manage SpecDown docs from your terminal").version(_pkg.version);
585
671
  program.command("login").description("Log in to SpecDown").action(login);
586
672
  program.command("logout").description("Log out").action(logout);
587
673
  program.command("whoami").description("Show current user and active project").action(whoami);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specdown-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI for SpecDown — manage spec docs from your terminal",
5
5
  "type": "module",
6
6
  "engines": {