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.
- package/README.md +7 -5
- package/dist/index.js +228 -142
- 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
|
|
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/
|
|
6
|
+
// src/lib/update-check.ts
|
|
7
7
|
import chalk from "chalk";
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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 =
|
|
35
|
-
var CONFIG_FILE =
|
|
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/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
189
|
+
import chalk3 from "chalk";
|
|
138
190
|
function logout() {
|
|
139
191
|
const cfg = readConfig();
|
|
140
192
|
if (!cfg?.access_token) {
|
|
141
|
-
console.log(
|
|
193
|
+
console.log(chalk3.yellow("Not logged in."));
|
|
142
194
|
return;
|
|
143
195
|
}
|
|
144
196
|
clearConfig();
|
|
145
|
-
console.log(
|
|
197
|
+
console.log(chalk3.green("Logged out."));
|
|
146
198
|
}
|
|
147
199
|
|
|
148
200
|
// src/commands/whoami.ts
|
|
149
|
-
import
|
|
201
|
+
import chalk4 from "chalk";
|
|
150
202
|
function whoami() {
|
|
151
203
|
const cfg = requireAuth();
|
|
152
|
-
console.log(
|
|
153
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
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 ?
|
|
177
|
-
const name = isCurrent ?
|
|
178
|
-
console.log(`${marker}${name} ${
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
276
|
+
spinner.succeed(chalk6.green(`Now using project ${chalk6.bold(data.name)}`));
|
|
208
277
|
} catch (err) {
|
|
209
|
-
spinner.fail(
|
|
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
|
|
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 +
|
|
316
|
+
console.log(prefix + connector + chalk7.bold.blue("\u{1F4C1} " + node.title));
|
|
248
317
|
} else {
|
|
249
|
-
console.log(prefix + connector +
|
|
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(
|
|
332
|
+
console.log(chalk7.bold(`
|
|
264
333
|
${project.name}
|
|
265
334
|
`));
|
|
266
335
|
if (!data?.length) {
|
|
267
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
434
|
+
spinner.succeed(chalk9.green(`Created: ${data.full_path}`));
|
|
366
435
|
} catch {
|
|
367
|
-
spinner.fail(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
489
|
+
spinner.succeed(chalk10.green(`Created: ${fullPath}`));
|
|
421
490
|
}
|
|
422
491
|
} catch {
|
|
423
|
-
spinner.fail(
|
|
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
|
|
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(
|
|
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 =
|
|
518
|
+
const dir = dirname2(outFile);
|
|
450
519
|
if (dir !== ".") mkdirSync2(dir, { recursive: true });
|
|
451
520
|
writeFileSync2(outFile, content, "utf-8");
|
|
452
|
-
spinner.succeed(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
577
|
+
deleteSpinner.succeed(chalk12.green(`Deleted: ${fullPath}`));
|
|
495
578
|
} catch {
|
|
496
|
-
spinner.fail(
|
|
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
|
|
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) +
|
|
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(
|
|
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(
|
|
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(
|
|
645
|
+
console.log(chalk13.bold.cyan(`
|
|
563
646
|
\u{1F4C4} ${m.docPath}`));
|
|
564
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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);
|