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.
- package/README.md +7 -5
- package/dist/index.js +220 -141
- 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,69 @@
|
|
|
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/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 =
|
|
35
|
-
var CONFIG_FILE =
|
|
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/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
184
|
+
import chalk3 from "chalk";
|
|
138
185
|
function logout() {
|
|
139
186
|
const cfg = readConfig();
|
|
140
187
|
if (!cfg?.access_token) {
|
|
141
|
-
console.log(
|
|
188
|
+
console.log(chalk3.yellow("Not logged in."));
|
|
142
189
|
return;
|
|
143
190
|
}
|
|
144
191
|
clearConfig();
|
|
145
|
-
console.log(
|
|
192
|
+
console.log(chalk3.green("Logged out."));
|
|
146
193
|
}
|
|
147
194
|
|
|
148
195
|
// src/commands/whoami.ts
|
|
149
|
-
import
|
|
196
|
+
import chalk4 from "chalk";
|
|
150
197
|
function whoami() {
|
|
151
198
|
const cfg = requireAuth();
|
|
152
|
-
console.log(
|
|
153
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
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 ?
|
|
177
|
-
const name = isCurrent ?
|
|
178
|
-
console.log(`${marker}${name} ${
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
271
|
+
spinner.succeed(chalk6.green(`Now using project ${chalk6.bold(data.name)}`));
|
|
208
272
|
} catch (err) {
|
|
209
|
-
spinner.fail(
|
|
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
|
|
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 +
|
|
311
|
+
console.log(prefix + connector + chalk7.bold.blue("\u{1F4C1} " + node.title));
|
|
248
312
|
} else {
|
|
249
|
-
console.log(prefix + connector +
|
|
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(
|
|
327
|
+
console.log(chalk7.bold(`
|
|
264
328
|
${project.name}
|
|
265
329
|
`));
|
|
266
330
|
if (!data?.length) {
|
|
267
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
429
|
+
spinner.succeed(chalk9.green(`Created: ${data.full_path}`));
|
|
366
430
|
} catch {
|
|
367
|
-
spinner.fail(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
484
|
+
spinner.succeed(chalk10.green(`Created: ${fullPath}`));
|
|
421
485
|
}
|
|
422
486
|
} catch {
|
|
423
|
-
spinner.fail(
|
|
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
|
|
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(
|
|
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 =
|
|
513
|
+
const dir = dirname2(outFile);
|
|
450
514
|
if (dir !== ".") mkdirSync2(dir, { recursive: true });
|
|
451
515
|
writeFileSync2(outFile, content, "utf-8");
|
|
452
|
-
spinner.succeed(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
572
|
+
deleteSpinner.succeed(chalk12.green(`Deleted: ${fullPath}`));
|
|
495
573
|
} catch {
|
|
496
|
-
spinner.fail(
|
|
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
|
|
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) +
|
|
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(
|
|
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(
|
|
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(
|
|
640
|
+
console.log(chalk13.bold.cyan(`
|
|
563
641
|
\u{1F4C4} ${m.docPath}`));
|
|
564
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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);
|