specdown-cli 0.1.2 → 0.1.3

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 +220 -141
  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,69 @@
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/commands/login.ts
57
+ import http from "http";
58
+ import { exec } from "child_process";
59
+ import crypto from "crypto";
60
+ import chalk2 from "chalk";
61
+ import ora from "ora";
62
+
30
63
  // src/lib/config.ts
31
64
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
32
65
  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");
66
+ import { join as join2 } from "path";
67
+ var CONFIG_DIR = join2(homedir(), ".specdown");
68
+ var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
36
69
  function readConfig() {
37
70
  if (!existsSync(CONFIG_FILE)) return null;
38
71
  try {
@@ -68,97 +101,128 @@ function requireProject(cfg) {
68
101
  };
69
102
  }
70
103
 
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
- });
104
+ // src/commands/login.ts
105
+ var APP_URL = "https://specdown.app";
106
+ var TIMEOUT_MS = 5 * 60 * 1e3;
107
+ function openBrowser(url) {
108
+ const platform = process.platform;
109
+ const cmd = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
110
+ exec(cmd);
81
111
  }
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);
112
+ async function login() {
113
+ console.log(chalk2.bold("\nSpecDown Login\n"));
114
+ const port = 7400 + Math.floor(Math.random() * 100);
115
+ const state = crypto.randomBytes(16).toString("hex");
116
+ const authUrl = `${APP_URL}/cli/auth?port=${port}&state=${state}`;
117
+ return new Promise((resolve, reject) => {
118
+ const server = http.createServer((req, res) => {
119
+ res.setHeader("Access-Control-Allow-Origin", APP_URL);
120
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
121
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
122
+ if (req.method === "OPTIONS") {
123
+ res.writeHead(204);
124
+ res.end();
125
+ return;
126
+ }
127
+ if (req.method !== "POST") {
128
+ res.writeHead(405);
129
+ res.end();
130
+ return;
103
131
  }
132
+ let body = "";
133
+ req.on("data", (chunk) => {
134
+ body += chunk;
135
+ });
136
+ req.on("end", () => {
137
+ try {
138
+ const payload = JSON.parse(body);
139
+ if (payload.state !== state) {
140
+ res.writeHead(403);
141
+ res.end(JSON.stringify({ error: "Invalid state" }));
142
+ return;
143
+ }
144
+ writeConfig({
145
+ access_token: payload.access_token,
146
+ refresh_token: payload.refresh_token,
147
+ user_email: payload.email,
148
+ user_id: payload.user_id
149
+ });
150
+ res.writeHead(200);
151
+ res.end(JSON.stringify({ ok: true }));
152
+ spinner.succeed(chalk2.green(`Logged in as ${chalk2.bold(payload.email)}`));
153
+ server.close();
154
+ resolve();
155
+ } catch {
156
+ res.writeHead(400);
157
+ res.end(JSON.stringify({ error: "Bad request" }));
158
+ }
159
+ });
104
160
  });
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
161
+ const spinner = ora();
162
+ server.listen(port, "127.0.0.1", () => {
163
+ console.log(chalk2.cyan("Opening browser to complete login\u2026"));
164
+ console.log(chalk2.dim(` ${authUrl}
165
+ `));
166
+ console.log(chalk2.dim("If browser did not open, visit the URL above manually.\n"));
167
+ openBrowser(authUrl);
168
+ spinner.start("Waiting for authentication\u2026");
127
169
  });
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
- }
170
+ const timer = setTimeout(() => {
171
+ spinner.fail(chalk2.red("Login timed out (5 minutes). Run `specdown login` again."));
172
+ server.close();
173
+ reject(new Error("timeout"));
174
+ }, TIMEOUT_MS);
175
+ server.on("close", () => clearTimeout(timer));
176
+ server.on("error", (err) => {
177
+ spinner.fail(chalk2.red("Could not start local server: " + err.message));
178
+ reject(err);
179
+ });
180
+ });
134
181
  }
135
182
 
136
183
  // src/commands/logout.ts
137
- import chalk2 from "chalk";
184
+ import chalk3 from "chalk";
138
185
  function logout() {
139
186
  const cfg = readConfig();
140
187
  if (!cfg?.access_token) {
141
- console.log(chalk2.yellow("Not logged in."));
188
+ console.log(chalk3.yellow("Not logged in."));
142
189
  return;
143
190
  }
144
191
  clearConfig();
145
- console.log(chalk2.green("Logged out."));
192
+ console.log(chalk3.green("Logged out."));
146
193
  }
147
194
 
148
195
  // src/commands/whoami.ts
149
- import chalk3 from "chalk";
196
+ import chalk4 from "chalk";
150
197
  function whoami() {
151
198
  const cfg = requireAuth();
152
- console.log(chalk3.bold(cfg.user_email));
153
- console.log(chalk3.gray("ID: ") + cfg.user_id);
199
+ console.log(chalk4.bold(cfg.user_email));
200
+ console.log(chalk4.gray("ID: ") + cfg.user_id);
154
201
  if (cfg.current_project_name) {
155
- console.log(chalk3.gray("Project: ") + cfg.current_project_name + chalk3.gray(` (${cfg.current_project_slug})`));
202
+ console.log(chalk4.gray("Project: ") + cfg.current_project_name + chalk4.gray(` (${cfg.current_project_slug})`));
156
203
  }
157
204
  }
158
205
 
159
206
  // src/commands/projects.ts
160
- import chalk4 from "chalk";
207
+ import chalk5 from "chalk";
161
208
  import ora2 from "ora";
209
+
210
+ // src/lib/api.ts
211
+ import { createClient } from "@supabase/supabase-js";
212
+ var SUPABASE_URL = "https://zjvjdalqgrxdhefqqifd.supabase.co";
213
+ var SUPABASE_ANON_KEY = "sb_publishable_y-O0ly4bNH0G4KJii3m25g_WXujFZDo";
214
+ function getClient(cfg) {
215
+ const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
216
+ auth: { persistSession: false }
217
+ });
218
+ client.auth.setSession({
219
+ access_token: cfg.access_token,
220
+ refresh_token: cfg.refresh_token
221
+ });
222
+ return client;
223
+ }
224
+
225
+ // src/commands/projects.ts
162
226
  async function listProjects() {
163
227
  const cfg = requireAuth();
164
228
  const supabase = getClient(cfg);
@@ -168,24 +232,24 @@ async function listProjects() {
168
232
  if (error) throw error;
169
233
  spinner.stop();
170
234
  if (!data?.length) {
171
- console.log(chalk4.gray("No projects found."));
235
+ console.log(chalk5.gray("No projects found."));
172
236
  return;
173
237
  }
174
238
  for (const p of data) {
175
239
  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)}`);
240
+ const marker = isCurrent ? chalk5.green("* ") : " ";
241
+ const name = isCurrent ? chalk5.bold.green(p.name) : chalk5.bold(p.name);
242
+ console.log(`${marker}${name} ${chalk5.gray(p.slug)}`);
179
243
  }
180
244
  } catch (err) {
181
- spinner.fail(chalk4.red("Failed to fetch projects"));
245
+ spinner.fail(chalk5.red("Failed to fetch projects"));
182
246
  console.error(err);
183
247
  process.exit(1);
184
248
  }
185
249
  }
186
250
 
187
251
  // src/commands/use.ts
188
- import chalk5 from "chalk";
252
+ import chalk6 from "chalk";
189
253
  import ora3 from "ora";
190
254
  async function useProject(slug) {
191
255
  const cfg = requireAuth();
@@ -194,7 +258,7 @@ async function useProject(slug) {
194
258
  try {
195
259
  const { data, error } = await supabase.from("projects").select("id, name, slug").eq("slug", slug).single();
196
260
  if (error || !data) {
197
- spinner.fail(chalk5.red(`Project "${slug}" not found.`));
261
+ spinner.fail(chalk6.red(`Project "${slug}" not found.`));
198
262
  process.exit(1);
199
263
  }
200
264
  const current = readConfig() ?? cfg;
@@ -204,16 +268,16 @@ async function useProject(slug) {
204
268
  current_project_slug: data.slug,
205
269
  current_project_name: data.name
206
270
  });
207
- spinner.succeed(chalk5.green(`Now using project ${chalk5.bold(data.name)}`));
271
+ spinner.succeed(chalk6.green(`Now using project ${chalk6.bold(data.name)}`));
208
272
  } catch (err) {
209
- spinner.fail(chalk5.red("Failed to switch project"));
273
+ spinner.fail(chalk6.red("Failed to switch project"));
210
274
  console.error(err);
211
275
  process.exit(1);
212
276
  }
213
277
  }
214
278
 
215
279
  // src/commands/ls.ts
216
- import chalk6 from "chalk";
280
+ import chalk7 from "chalk";
217
281
  import ora4 from "ora";
218
282
  function buildTree(docs) {
219
283
  const map = /* @__PURE__ */ new Map();
@@ -244,9 +308,9 @@ function printTree(nodes, prefix = "") {
244
308
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
245
309
  const childPrefix = prefix + (isLast ? " " : "\u2502 ");
246
310
  if (node.is_folder) {
247
- console.log(prefix + connector + chalk6.bold.blue("\u{1F4C1} " + node.title));
311
+ console.log(prefix + connector + chalk7.bold.blue("\u{1F4C1} " + node.title));
248
312
  } else {
249
- console.log(prefix + connector + chalk6.white("\u{1F4C4} " + node.title) + chalk6.gray(" " + node.path));
313
+ console.log(prefix + connector + chalk7.white("\u{1F4C4} " + node.title) + chalk7.gray(" " + node.path));
250
314
  }
251
315
  if (node.children.length) printTree(node.children, childPrefix);
252
316
  }
@@ -260,25 +324,25 @@ async function ls() {
260
324
  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
325
  if (error) throw error;
262
326
  spinner.stop();
263
- console.log(chalk6.bold(`
327
+ console.log(chalk7.bold(`
264
328
  ${project.name}
265
329
  `));
266
330
  if (!data?.length) {
267
- console.log(chalk6.gray(" (empty)"));
331
+ console.log(chalk7.gray(" (empty)"));
268
332
  return;
269
333
  }
270
334
  const tree = buildTree(data);
271
335
  printTree(tree);
272
336
  console.log();
273
337
  } catch (err) {
274
- spinner.fail(chalk6.red("Failed to list documents"));
338
+ spinner.fail(chalk7.red("Failed to list documents"));
275
339
  console.error(err);
276
340
  process.exit(1);
277
341
  }
278
342
  }
279
343
 
280
344
  // src/commands/read.ts
281
- import chalk7 from "chalk";
345
+ import chalk8 from "chalk";
282
346
  import ora5 from "ora";
283
347
  function normalizePath(p) {
284
348
  return p.startsWith("/") ? p : `/${p}`;
@@ -291,7 +355,7 @@ async function readDoc(pathArg, opts) {
291
355
  try {
292
356
  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
357
  if (error || !data) {
294
- spinner.fail(chalk7.red(`Document not found: ${pathArg}`));
358
+ spinner.fail(chalk8.red(`Document not found: ${pathArg}`));
295
359
  process.exit(1);
296
360
  }
297
361
  spinner.stop();
@@ -300,29 +364,29 @@ async function readDoc(pathArg, opts) {
300
364
  const fromLine = opts.from ? Math.max(1, parseInt(opts.from, 10)) : 1;
301
365
  const toLine = opts.to ? Math.min(totalLines, parseInt(opts.to, 10)) : totalLines;
302
366
  if (fromLine > toLine) {
303
- console.error(chalk7.red(`Invalid range: --from ${fromLine} is after --to ${toLine}`));
367
+ console.error(chalk8.red(`Invalid range: --from ${fromLine} is after --to ${toLine}`));
304
368
  process.exit(1);
305
369
  }
306
370
  const selectedLines = rawLines.slice(fromLine - 1, toLine);
307
371
  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()})
372
+ console.log(chalk8.gray(`# ${data.full_path} (${rangeLabel}, updated ${new Date(data.updated_at).toLocaleString()})
309
373
  `));
310
374
  if (opts.lineNumbers) {
311
375
  const padWidth = String(toLine).length;
312
376
  selectedLines.forEach((line, i) => {
313
- console.log(chalk7.gray(String(fromLine + i).padStart(padWidth) + " ") + line);
377
+ console.log(chalk8.gray(String(fromLine + i).padStart(padWidth) + " ") + line);
314
378
  });
315
379
  } else {
316
380
  console.log(selectedLines.join("\n"));
317
381
  }
318
382
  } catch {
319
- spinner.fail(chalk7.red("Failed to read document"));
383
+ spinner.fail(chalk8.red("Failed to read document"));
320
384
  process.exit(1);
321
385
  }
322
386
  }
323
387
 
324
388
  // src/commands/new.ts
325
- import chalk8 from "chalk";
389
+ import chalk9 from "chalk";
326
390
  import ora6 from "ora";
327
391
  function normalizePath2(p) {
328
392
  return p.startsWith("/") ? p : `/${p}`;
@@ -341,7 +405,7 @@ async function newDoc(title, opts) {
341
405
  const parentPath = normalizePath2(opts.parent);
342
406
  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
407
  if (!parentDoc) {
344
- spinner.fail(chalk8.red(`Parent folder not found: ${opts.parent}`));
408
+ spinner.fail(chalk9.red(`Parent folder not found: ${opts.parent}`));
345
409
  process.exit(1);
346
410
  }
347
411
  parentId = parentDoc.id;
@@ -362,16 +426,16 @@ async function newDoc(title, opts) {
362
426
  `
363
427
  }).select("id, full_path").single();
364
428
  if (error) throw error;
365
- spinner.succeed(chalk8.green(`Created: ${data.full_path}`));
429
+ spinner.succeed(chalk9.green(`Created: ${data.full_path}`));
366
430
  } catch {
367
- spinner.fail(chalk8.red("Failed to create document"));
431
+ spinner.fail(chalk9.red("Failed to create document"));
368
432
  process.exit(1);
369
433
  }
370
434
  }
371
435
 
372
436
  // src/commands/push.ts
373
437
  import { readFileSync as readFileSync2, existsSync as existsSync2, statSync } from "fs";
374
- import chalk9 from "chalk";
438
+ import chalk10 from "chalk";
375
439
  import ora7 from "ora";
376
440
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
377
441
  function normalizePath3(p) {
@@ -379,12 +443,12 @@ function normalizePath3(p) {
379
443
  }
380
444
  async function push(filePath, docPath) {
381
445
  if (!existsSync2(filePath)) {
382
- console.error(chalk9.red(`File not found: ${filePath}`));
446
+ console.error(chalk10.red(`File not found: ${filePath}`));
383
447
  process.exit(1);
384
448
  }
385
449
  const stat = statSync(filePath);
386
450
  if (stat.size > MAX_FILE_BYTES) {
387
- console.error(chalk9.red(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Max 10 MB.`));
451
+ console.error(chalk10.red(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Max 10 MB.`));
388
452
  process.exit(1);
389
453
  }
390
454
  const content = readFileSync2(filePath, "utf-8");
@@ -398,7 +462,7 @@ async function push(filePath, docPath) {
398
462
  if (existing) {
399
463
  const { error } = await supabase.from("documents").update({ content, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id);
400
464
  if (error) throw error;
401
- spinner.succeed(chalk9.green(`Updated: ${fullPath}`));
465
+ spinner.succeed(chalk10.green(`Updated: ${fullPath}`));
402
466
  } else {
403
467
  const parts = fullPath.split("/");
404
468
  const filename = parts.pop() ?? "doc.md";
@@ -417,18 +481,18 @@ async function push(filePath, docPath) {
417
481
  content
418
482
  });
419
483
  if (error) throw error;
420
- spinner.succeed(chalk9.green(`Created: ${fullPath}`));
484
+ spinner.succeed(chalk10.green(`Created: ${fullPath}`));
421
485
  }
422
486
  } catch {
423
- spinner.fail(chalk9.red("Push failed"));
487
+ spinner.fail(chalk10.red("Push failed"));
424
488
  process.exit(1);
425
489
  }
426
490
  }
427
491
 
428
492
  // src/commands/pull.ts
429
493
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
430
- import { dirname } from "path";
431
- import chalk10 from "chalk";
494
+ import { dirname as dirname2 } from "path";
495
+ import chalk11 from "chalk";
432
496
  import ora8 from "ora";
433
497
  function normalizePath4(p) {
434
498
  return p.startsWith("/") ? p : `/${p}`;
@@ -441,28 +505,42 @@ async function pull(docPath, outFile) {
441
505
  try {
442
506
  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
507
  if (error || !data) {
444
- spinner.fail(chalk10.red(`Document not found: ${docPath}`));
508
+ spinner.fail(chalk11.red(`Document not found: ${docPath}`));
445
509
  process.exit(1);
446
510
  }
447
511
  const content = data.content ?? "";
448
512
  if (outFile) {
449
- const dir = dirname(outFile);
513
+ const dir = dirname2(outFile);
450
514
  if (dir !== ".") mkdirSync2(dir, { recursive: true });
451
515
  writeFileSync2(outFile, content, "utf-8");
452
- spinner.succeed(chalk10.green(`Saved to ${outFile}`));
516
+ spinner.succeed(chalk11.green(`Saved to ${outFile}`));
453
517
  } else {
454
518
  spinner.stop();
455
519
  process.stdout.write(content);
456
520
  }
457
521
  } catch {
458
- spinner.fail(chalk10.red("Pull failed"));
522
+ spinner.fail(chalk11.red("Pull failed"));
459
523
  process.exit(1);
460
524
  }
461
525
  }
462
526
 
463
527
  // src/commands/rm.ts
464
- import chalk11 from "chalk";
528
+ import chalk12 from "chalk";
465
529
  import ora9 from "ora";
530
+
531
+ // src/lib/prompt.ts
532
+ import { createInterface } from "readline";
533
+ function ask(question) {
534
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
535
+ return new Promise((resolve) => {
536
+ rl.question(question, (answer) => {
537
+ rl.close();
538
+ resolve(answer.trim());
539
+ });
540
+ });
541
+ }
542
+
543
+ // src/commands/rm.ts
466
544
  function normalizePath5(p) {
467
545
  return p.startsWith("/") ? p : `/${p}`;
468
546
  }
@@ -475,13 +553,13 @@ async function rm(docPath, opts) {
475
553
  try {
476
554
  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
555
  if (error || !data) {
478
- spinner.fail(chalk11.red(`Document not found: ${fullPath}`));
556
+ spinner.fail(chalk12.red(`Document not found: ${fullPath}`));
479
557
  process.exit(1);
480
558
  }
481
559
  spinner.stop();
482
560
  if (!opts.force) {
483
561
  const answer = await ask(
484
- chalk11.yellow(`Delete "${data.title}"${data.is_folder ? " (folder + all children)" : ""}? [y/N] `)
562
+ chalk12.yellow(`Delete "${data.title}"${data.is_folder ? " (folder + all children)" : ""}? [y/N] `)
485
563
  );
486
564
  if (answer.toLowerCase() !== "y") {
487
565
  console.log("Aborted.");
@@ -491,15 +569,15 @@ async function rm(docPath, opts) {
491
569
  const deleteSpinner = ora9("Deleting\u2026").start();
492
570
  const { error: delErr } = await supabase.from("documents").update({ deleted_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", data.id);
493
571
  if (delErr) throw delErr;
494
- deleteSpinner.succeed(chalk11.green(`Deleted: ${fullPath}`));
572
+ deleteSpinner.succeed(chalk12.green(`Deleted: ${fullPath}`));
495
573
  } catch {
496
- spinner.fail(chalk11.red("Delete failed"));
574
+ spinner.fail(chalk12.red("Delete failed"));
497
575
  process.exit(1);
498
576
  }
499
577
  }
500
578
 
501
579
  // src/commands/search.ts
502
- import chalk12 from "chalk";
580
+ import chalk13 from "chalk";
503
581
  import ora10 from "ora";
504
582
  function searchInContent(content, query, contextLines) {
505
583
  const lines = content.split("\n");
@@ -520,10 +598,10 @@ function searchInContent(content, query, contextLines) {
520
598
  function highlight(line, query) {
521
599
  const idx = line.toLowerCase().indexOf(query.toLowerCase());
522
600
  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);
601
+ return line.slice(0, idx) + chalk13.bgYellow.black(line.slice(idx, idx + query.length)) + line.slice(idx + query.length);
524
602
  }
525
603
  function printContextLine(lineNo, line) {
526
- console.log(chalk12.gray(String(lineNo).padStart(4) + " \u2502 ") + chalk12.dim(line));
604
+ console.log(chalk13.gray(String(lineNo).padStart(4) + " \u2502 ") + chalk13.dim(line));
527
605
  }
528
606
  async function search(query, opts) {
529
607
  const cfg = requireAuth();
@@ -551,7 +629,7 @@ async function search(query, opts) {
551
629
  }
552
630
  }
553
631
  if (!allMatches.length) {
554
- console.log(chalk12.yellow(`No results for "${query}" in ${project.name}`));
632
+ console.log(chalk13.yellow(`No results for "${query}" in ${project.name}`));
555
633
  return;
556
634
  }
557
635
  let currentFile = "";
@@ -559,27 +637,28 @@ async function search(query, opts) {
559
637
  if (m.docPath !== currentFile) {
560
638
  if (currentFile) console.log();
561
639
  currentFile = m.docPath;
562
- console.log(chalk12.bold.cyan(`
640
+ console.log(chalk13.bold.cyan(`
563
641
  \u{1F4C4} ${m.docPath}`));
564
- console.log(chalk12.gray("\u2500".repeat(50)));
642
+ console.log(chalk13.gray("\u2500".repeat(50)));
565
643
  }
566
644
  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));
645
+ console.log(chalk13.green(String(m.lineNo).padStart(4) + " \u2502 ") + highlight(m.line, query));
568
646
  m.after.forEach((l, i) => printContextLine(m.lineNo + 1 + i, l));
569
- if (m.after.length || m.before.length) console.log(chalk12.gray(" \xB7"));
647
+ if (m.after.length || m.before.length) console.log(chalk13.gray(" \xB7"));
570
648
  }
571
649
  const fileCount = new Set(allMatches.map((m) => m.docPath)).size;
572
650
  console.log();
573
651
  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)
652
+ chalk13.bold(`${allMatches.length} match${allMatches.length === 1 ? "" : "es"}`) + chalk13.gray(` across ${fileCount} file${fileCount === 1 ? "" : "s"} in `) + chalk13.bold(project.name)
575
653
  );
576
654
  } catch {
577
- spinner.fail(chalk12.red("Search failed"));
655
+ spinner.fail(chalk13.red("Search failed"));
578
656
  process.exit(1);
579
657
  }
580
658
  }
581
659
 
582
660
  // src/index.ts
661
+ checkForUpdate();
583
662
  var program = new Command();
584
663
  program.name("specdown").description("Manage SpecDown docs from your terminal").version("0.1.0");
585
664
  program.command("login").description("Log in to SpecDown").action(login);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specdown-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI for SpecDown — manage spec docs from your terminal",
5
5
  "type": "module",
6
6
  "engines": {