md4ai 0.7.0 → 0.7.2
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/dist/index.bundled.js +1665 -1362
- package/package.json +1 -1
package/dist/index.bundled.js
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
2
11
|
|
|
3
|
-
// dist/
|
|
4
|
-
|
|
12
|
+
// ../packages/shared/dist/types.js
|
|
13
|
+
var init_types = __esm({
|
|
14
|
+
"../packages/shared/dist/types.js"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
}
|
|
17
|
+
});
|
|
5
18
|
|
|
6
19
|
// ../packages/shared/dist/constants.js
|
|
7
|
-
var SUPABASE_URL
|
|
8
|
-
var
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
var SUPABASE_URL, ROOT_FILES, GLOBAL_ROOT_FILES, STALE_THRESHOLD_DAYS, CONFIG_DIR, CREDENTIALS_FILE, STATE_FILE;
|
|
21
|
+
var init_constants = __esm({
|
|
22
|
+
"../packages/shared/dist/constants.js"() {
|
|
23
|
+
"use strict";
|
|
24
|
+
SUPABASE_URL = "https://gkrfwmwlfnixeffhwwju.supabase.co";
|
|
25
|
+
ROOT_FILES = [
|
|
26
|
+
"CLAUDE.md",
|
|
27
|
+
".claude/settings.json",
|
|
28
|
+
".claude/settings.local.json"
|
|
29
|
+
];
|
|
30
|
+
GLOBAL_ROOT_FILES = [
|
|
31
|
+
"~/.claude/CLAUDE.md",
|
|
32
|
+
"~/.claude/settings.json",
|
|
33
|
+
"~/.claude/settings.local.json"
|
|
34
|
+
];
|
|
35
|
+
STALE_THRESHOLD_DAYS = 90;
|
|
36
|
+
CONFIG_DIR = ".md4ai";
|
|
37
|
+
CREDENTIALS_FILE = "credentials.json";
|
|
38
|
+
STATE_FILE = "state.json";
|
|
39
|
+
}
|
|
40
|
+
});
|
|
22
41
|
|
|
23
42
|
// ../packages/shared/dist/supabase.js
|
|
24
43
|
import { createClient } from "@supabase/supabase-js";
|
|
@@ -26,15 +45,28 @@ function createSupabaseClient(anonKey, accessToken) {
|
|
|
26
45
|
const options = accessToken ? { global: { headers: { Authorization: `Bearer ${accessToken}` } } } : {};
|
|
27
46
|
return createClient(SUPABASE_URL, anonKey, options);
|
|
28
47
|
}
|
|
48
|
+
var init_supabase = __esm({
|
|
49
|
+
"../packages/shared/dist/supabase.js"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
init_constants();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ../packages/shared/dist/index.js
|
|
56
|
+
var init_dist = __esm({
|
|
57
|
+
"../packages/shared/dist/index.js"() {
|
|
58
|
+
"use strict";
|
|
59
|
+
init_types();
|
|
60
|
+
init_constants();
|
|
61
|
+
init_supabase();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
29
64
|
|
|
30
65
|
// dist/config.js
|
|
31
66
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
32
67
|
import { join } from "node:path";
|
|
33
68
|
import { homedir } from "node:os";
|
|
34
69
|
import { existsSync } from "node:fs";
|
|
35
|
-
var configPath = join(homedir(), CONFIG_DIR);
|
|
36
|
-
var credentialsPath = join(configPath, CREDENTIALS_FILE);
|
|
37
|
-
var statePath = join(configPath, STATE_FILE);
|
|
38
70
|
async function ensureConfigDir() {
|
|
39
71
|
if (!existsSync(configPath)) {
|
|
40
72
|
await mkdir(configPath, { recursive: true });
|
|
@@ -84,66 +116,16 @@ function getAnonKey() {
|
|
|
84
116
|
}
|
|
85
117
|
return key;
|
|
86
118
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const anonKey = getAnonKey();
|
|
96
|
-
const supabase = createSupabaseClient(anonKey);
|
|
97
|
-
const { data, error } = await supabase.auth.signInWithPassword({
|
|
98
|
-
email,
|
|
99
|
-
password: pwd
|
|
100
|
-
});
|
|
101
|
-
if (error) {
|
|
102
|
-
console.error(chalk.red(`Login failed: ${error.message}`));
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
await saveCredentials({
|
|
106
|
-
accessToken: data.session.access_token,
|
|
107
|
-
refreshToken: data.session.refresh_token,
|
|
108
|
-
expiresAt: Date.now() + data.session.expires_in * 1e3,
|
|
109
|
-
userId: data.user.id,
|
|
110
|
-
email: data.user.email ?? email
|
|
111
|
-
});
|
|
112
|
-
console.log(chalk.green(`
|
|
113
|
-
Logged in as ${data.user.email}`));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// dist/commands/logout.js
|
|
117
|
-
import chalk2 from "chalk";
|
|
118
|
-
async function logoutCommand() {
|
|
119
|
-
await clearCredentials();
|
|
120
|
-
console.log(chalk2.green("Logged out successfully."));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// dist/commands/status.js
|
|
124
|
-
import chalk3 from "chalk";
|
|
125
|
-
async function statusCommand() {
|
|
126
|
-
const creds = await loadCredentials();
|
|
127
|
-
if (!creds?.accessToken) {
|
|
128
|
-
console.log(chalk3.yellow("Not logged in. Run: md4ai login"));
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
console.log(chalk3.blue("MD4AI Status\n"));
|
|
132
|
-
console.log(` User: ${creds.email}`);
|
|
133
|
-
console.log(` Expires: ${new Date(creds.expiresAt).toLocaleString()}`);
|
|
134
|
-
try {
|
|
135
|
-
const anonKey = getAnonKey();
|
|
136
|
-
const supabase = createSupabaseClient(anonKey, creds.accessToken);
|
|
137
|
-
const { count: folderCount } = await supabase.from("claude_folders").select("*", { count: "exact", head: true });
|
|
138
|
-
const { count: deviceCount } = await supabase.from("device_paths").select("*", { count: "exact", head: true });
|
|
139
|
-
console.log(` Folders: ${folderCount ?? 0}`);
|
|
140
|
-
console.log(` Devices: ${deviceCount ?? 0}`);
|
|
141
|
-
const state = await loadState();
|
|
142
|
-
console.log(` Last sync: ${state.lastSyncAt ?? "never"}`);
|
|
143
|
-
} catch {
|
|
144
|
-
console.log(chalk3.yellow(" (Could not fetch remote data)"));
|
|
119
|
+
var configPath, credentialsPath, statePath;
|
|
120
|
+
var init_config = __esm({
|
|
121
|
+
"dist/config.js"() {
|
|
122
|
+
"use strict";
|
|
123
|
+
init_dist();
|
|
124
|
+
configPath = join(homedir(), CONFIG_DIR);
|
|
125
|
+
credentialsPath = join(configPath, CREDENTIALS_FILE);
|
|
126
|
+
statePath = join(configPath, STATE_FILE);
|
|
145
127
|
}
|
|
146
|
-
}
|
|
128
|
+
});
|
|
147
129
|
|
|
148
130
|
// dist/auth.js
|
|
149
131
|
import chalk4 from "chalk";
|
|
@@ -161,180 +143,19 @@ async function getAuthenticatedClient() {
|
|
|
161
143
|
const supabase = createSupabaseClient(anonKey, creds.accessToken);
|
|
162
144
|
return { supabase, userId: creds.userId };
|
|
163
145
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const { supabase, userId } = await getAuthenticatedClient();
|
|
170
|
-
const name = await input2({ message: "Folder name:" });
|
|
171
|
-
const description = await input2({ message: "Description (optional):" });
|
|
172
|
-
const { data: teams } = await supabase.from("team_members").select("team_id, teams(id, name)").eq("user_id", userId);
|
|
173
|
-
const { data: ownedTeams } = await supabase.from("teams").select("id, name").eq("created_by", userId);
|
|
174
|
-
const allTeams = /* @__PURE__ */ new Map();
|
|
175
|
-
for (const t of ownedTeams ?? []) {
|
|
176
|
-
allTeams.set(t.id, t.name);
|
|
177
|
-
}
|
|
178
|
-
for (const t of teams ?? []) {
|
|
179
|
-
const team = t.teams;
|
|
180
|
-
if (team)
|
|
181
|
-
allTeams.set(team.id, team.name);
|
|
182
|
-
}
|
|
183
|
-
let teamId = null;
|
|
184
|
-
if (allTeams.size > 0) {
|
|
185
|
-
const choices = [
|
|
186
|
-
{ name: "Personal (only you)", value: "__personal__" },
|
|
187
|
-
...Array.from(allTeams.entries()).map(([id, teamName]) => ({
|
|
188
|
-
name: teamName,
|
|
189
|
-
value: id
|
|
190
|
-
}))
|
|
191
|
-
];
|
|
192
|
-
const selected = await select({
|
|
193
|
-
message: "Assign to a team?",
|
|
194
|
-
choices
|
|
195
|
-
});
|
|
196
|
-
teamId = selected === "__personal__" ? null : selected;
|
|
197
|
-
}
|
|
198
|
-
const { data, error } = await supabase.from("claude_folders").insert({
|
|
199
|
-
user_id: userId,
|
|
200
|
-
name,
|
|
201
|
-
description: description || null,
|
|
202
|
-
team_id: teamId
|
|
203
|
-
}).select().single();
|
|
204
|
-
if (error) {
|
|
205
|
-
console.error(chalk5.red(`Failed to create folder: ${error.message}`));
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
const teamLabel = teamId ? `(team: ${allTeams.get(teamId)})` : "(personal)";
|
|
209
|
-
console.log(chalk5.green(`
|
|
210
|
-
Folder "${name}" created ${teamLabel} (${data.id})`));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// dist/commands/add-device.js
|
|
214
|
-
import { resolve } from "node:path";
|
|
215
|
-
import { hostname, platform } from "node:os";
|
|
216
|
-
import chalk6 from "chalk";
|
|
217
|
-
import { input as input3, select as select2 } from "@inquirer/prompts";
|
|
218
|
-
function detectOs() {
|
|
219
|
-
const p = platform();
|
|
220
|
-
if (p === "win32")
|
|
221
|
-
return "windows";
|
|
222
|
-
if (p === "darwin")
|
|
223
|
-
return "macos";
|
|
224
|
-
if (p === "linux")
|
|
225
|
-
return "linux";
|
|
226
|
-
return "other";
|
|
227
|
-
}
|
|
228
|
-
function suggestDeviceName() {
|
|
229
|
-
const os = detectOs();
|
|
230
|
-
const host = hostname().split(".")[0];
|
|
231
|
-
const osLabel = os.charAt(0).toUpperCase() + os.slice(1);
|
|
232
|
-
return `${host}-${osLabel}`;
|
|
233
|
-
}
|
|
234
|
-
async function addDeviceCommand() {
|
|
235
|
-
const { supabase, userId } = await getAuthenticatedClient();
|
|
236
|
-
const cwd = resolve(process.cwd());
|
|
237
|
-
console.log(chalk6.blue.bold("\n\u{1F4C2} Where is this Claude project?\n"));
|
|
238
|
-
const localPath = await input3({
|
|
239
|
-
message: "Local project path:",
|
|
240
|
-
default: cwd
|
|
241
|
-
});
|
|
242
|
-
const { data: folders, error: foldersErr } = await supabase.from("claude_folders").select("id, name").order("name");
|
|
243
|
-
if (foldersErr || !folders?.length) {
|
|
244
|
-
console.error(chalk6.red("No folders found. Run: md4ai add-folder"));
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
const folderId = await select2({
|
|
248
|
-
message: "Select folder:",
|
|
249
|
-
choices: folders.map((f) => ({ name: f.name, value: f.id }))
|
|
250
|
-
});
|
|
251
|
-
const suggested = suggestDeviceName();
|
|
252
|
-
const deviceName = await input3({
|
|
253
|
-
message: "Device name:",
|
|
254
|
-
default: suggested
|
|
255
|
-
});
|
|
256
|
-
const osType = await select2({
|
|
257
|
-
message: "OS type:",
|
|
258
|
-
choices: [
|
|
259
|
-
{ name: "Windows", value: "windows" },
|
|
260
|
-
{ name: "macOS", value: "macos" },
|
|
261
|
-
{ name: "Ubuntu", value: "ubuntu" },
|
|
262
|
-
{ name: "Linux", value: "linux" },
|
|
263
|
-
{ name: "Other", value: "other" }
|
|
264
|
-
],
|
|
265
|
-
default: detectOs()
|
|
266
|
-
});
|
|
267
|
-
const description = await input3({ message: "Description (optional):" });
|
|
268
|
-
const { error } = await supabase.from("device_paths").insert({
|
|
269
|
-
user_id: userId,
|
|
270
|
-
folder_id: folderId,
|
|
271
|
-
device_name: deviceName,
|
|
272
|
-
os_type: osType,
|
|
273
|
-
path: localPath,
|
|
274
|
-
description: description || null
|
|
275
|
-
});
|
|
276
|
-
if (error) {
|
|
277
|
-
console.error(chalk6.red(`Failed to add device: ${error.message}`));
|
|
278
|
-
process.exit(1);
|
|
279
|
-
}
|
|
280
|
-
console.log(chalk6.green(`
|
|
281
|
-
Device "${deviceName}" added to folder.`));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// dist/commands/list-devices.js
|
|
285
|
-
import chalk7 from "chalk";
|
|
286
|
-
async function listDevicesCommand() {
|
|
287
|
-
const { supabase } = await getAuthenticatedClient();
|
|
288
|
-
const { data: devices, error } = await supabase.from("device_paths").select(`
|
|
289
|
-
id, device_name, os_type, path, last_synced, description,
|
|
290
|
-
claude_folders!inner ( name )
|
|
291
|
-
`).order("device_name");
|
|
292
|
-
if (error) {
|
|
293
|
-
console.error(chalk7.red(`Failed to list devices: ${error.message}`));
|
|
294
|
-
process.exit(1);
|
|
295
|
-
}
|
|
296
|
-
if (!devices?.length) {
|
|
297
|
-
console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
301
|
-
for (const d of devices) {
|
|
302
|
-
const list = grouped.get(d.device_name) ?? [];
|
|
303
|
-
list.push(d);
|
|
304
|
-
grouped.set(d.device_name, list);
|
|
305
|
-
}
|
|
306
|
-
for (const [deviceName, entries] of grouped) {
|
|
307
|
-
const first = entries[0];
|
|
308
|
-
console.log(chalk7.bold(`
|
|
309
|
-
${deviceName}`) + chalk7.dim(` (${first.os_type})`));
|
|
310
|
-
for (const entry of entries) {
|
|
311
|
-
const folderName = entry.claude_folders?.name ?? "unknown";
|
|
312
|
-
const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
|
|
313
|
-
console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
|
|
314
|
-
console.log(` Last synced: ${synced}`);
|
|
315
|
-
}
|
|
146
|
+
var init_auth = __esm({
|
|
147
|
+
"dist/auth.js"() {
|
|
148
|
+
"use strict";
|
|
149
|
+
init_dist();
|
|
150
|
+
init_config();
|
|
316
151
|
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// dist/commands/map.js
|
|
320
|
-
import { resolve as resolve3 } from "node:path";
|
|
321
|
-
import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
322
|
-
import { existsSync as existsSync7 } from "node:fs";
|
|
323
|
-
import chalk9 from "chalk";
|
|
324
|
-
|
|
325
|
-
// dist/scanner/index.js
|
|
326
|
-
import { readdir as readdir2 } from "node:fs/promises";
|
|
327
|
-
import { join as join8, relative as relative2 } from "node:path";
|
|
328
|
-
import { existsSync as existsSync6 } from "node:fs";
|
|
329
|
-
import { homedir as homedir5 } from "node:os";
|
|
330
|
-
import { createHash } from "node:crypto";
|
|
152
|
+
});
|
|
331
153
|
|
|
332
154
|
// dist/scanner/file-parser.js
|
|
333
155
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
334
156
|
import { existsSync as existsSync2 } from "node:fs";
|
|
335
157
|
import { resolve as resolve2, dirname, join as join2 } from "node:path";
|
|
336
158
|
import { homedir as homedir2 } from "node:os";
|
|
337
|
-
var FILE_EXT_PATTERN = "md|json|ya?ml|ts|js|sh|py|bash";
|
|
338
159
|
async function parseFileReferences(filePath, projectRoot) {
|
|
339
160
|
const refs = [];
|
|
340
161
|
const content = await readFile2(filePath, "utf-8");
|
|
@@ -451,6 +272,13 @@ function classifyFileType(filePath) {
|
|
|
451
272
|
return "plan";
|
|
452
273
|
return "other";
|
|
453
274
|
}
|
|
275
|
+
var FILE_EXT_PATTERN;
|
|
276
|
+
var init_file_parser = __esm({
|
|
277
|
+
"dist/scanner/file-parser.js"() {
|
|
278
|
+
"use strict";
|
|
279
|
+
FILE_EXT_PATTERN = "md|json|ya?ml|ts|js|sh|py|bash";
|
|
280
|
+
}
|
|
281
|
+
});
|
|
454
282
|
|
|
455
283
|
// dist/scanner/git-dates.js
|
|
456
284
|
import { execFileSync } from "node:child_process";
|
|
@@ -479,6 +307,11 @@ function getGitCreationDate(filePath, cwd) {
|
|
|
479
307
|
}
|
|
480
308
|
}
|
|
481
309
|
}
|
|
310
|
+
var init_git_dates = __esm({
|
|
311
|
+
"dist/scanner/git-dates.js"() {
|
|
312
|
+
"use strict";
|
|
313
|
+
}
|
|
314
|
+
});
|
|
482
315
|
|
|
483
316
|
// dist/scanner/graph-builder.js
|
|
484
317
|
import { statSync as statSync2 } from "node:fs";
|
|
@@ -552,6 +385,13 @@ function buildGraph(files, references, projectRoot) {
|
|
|
552
385
|
function sanitiseId(path) {
|
|
553
386
|
return path.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 64);
|
|
554
387
|
}
|
|
388
|
+
var init_graph_builder = __esm({
|
|
389
|
+
"dist/scanner/graph-builder.js"() {
|
|
390
|
+
"use strict";
|
|
391
|
+
init_file_parser();
|
|
392
|
+
init_git_dates();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
555
395
|
|
|
556
396
|
// dist/scanner/orphan-detector.js
|
|
557
397
|
import { statSync as statSync3 } from "node:fs";
|
|
@@ -595,6 +435,12 @@ function detectOrphans(allFiles, references, rootFiles, projectRoot) {
|
|
|
595
435
|
};
|
|
596
436
|
});
|
|
597
437
|
}
|
|
438
|
+
var init_orphan_detector = __esm({
|
|
439
|
+
"dist/scanner/orphan-detector.js"() {
|
|
440
|
+
"use strict";
|
|
441
|
+
init_git_dates();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
598
444
|
|
|
599
445
|
// dist/scanner/skills-parser.js
|
|
600
446
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -680,6 +526,11 @@ async function parseSettingsForPlugins(settingsPath, skills, isMachineWide) {
|
|
|
680
526
|
} catch {
|
|
681
527
|
}
|
|
682
528
|
}
|
|
529
|
+
var init_skills_parser = __esm({
|
|
530
|
+
"dist/scanner/skills-parser.js"() {
|
|
531
|
+
"use strict";
|
|
532
|
+
}
|
|
533
|
+
});
|
|
683
534
|
|
|
684
535
|
// dist/scanner/tooling-detector.js
|
|
685
536
|
import { readFile as readFile4, readdir } from "node:fs/promises";
|
|
@@ -687,13 +538,6 @@ import { existsSync as existsSync4 } from "node:fs";
|
|
|
687
538
|
import { join as join6 } from "node:path";
|
|
688
539
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
689
540
|
import { homedir as homedir4 } from "node:os";
|
|
690
|
-
var CLI_VERSION_COMMANDS = [
|
|
691
|
-
{ name: "node", command: "node", args: ["--version"] },
|
|
692
|
-
{ name: "npm", command: "npm", args: ["--version"] },
|
|
693
|
-
{ name: "pnpm", command: "pnpm", args: ["--version"], conditionFile: "pnpm-lock.yaml" },
|
|
694
|
-
{ name: "supabase-cli", command: "npx", args: ["supabase", "--version"] },
|
|
695
|
-
{ name: "claude-code", command: "claude", args: ["--version"] }
|
|
696
|
-
];
|
|
697
541
|
async function detectToolings(projectRoot) {
|
|
698
542
|
const toolings = [];
|
|
699
543
|
const pkgToolings = await detectFromPackageJson(projectRoot);
|
|
@@ -922,12 +766,24 @@ async function detectFromMcpSettings(projectRoot) {
|
|
|
922
766
|
}
|
|
923
767
|
return toolings;
|
|
924
768
|
}
|
|
769
|
+
var CLI_VERSION_COMMANDS;
|
|
770
|
+
var init_tooling_detector = __esm({
|
|
771
|
+
"dist/scanner/tooling-detector.js"() {
|
|
772
|
+
"use strict";
|
|
773
|
+
CLI_VERSION_COMMANDS = [
|
|
774
|
+
{ name: "node", command: "node", args: ["--version"] },
|
|
775
|
+
{ name: "npm", command: "npm", args: ["--version"] },
|
|
776
|
+
{ name: "pnpm", command: "pnpm", args: ["--version"], conditionFile: "pnpm-lock.yaml" },
|
|
777
|
+
{ name: "supabase-cli", command: "npx", args: ["supabase", "--version"] },
|
|
778
|
+
{ name: "claude-code", command: "claude", args: ["--version"] }
|
|
779
|
+
];
|
|
780
|
+
}
|
|
781
|
+
});
|
|
925
782
|
|
|
926
783
|
// dist/scanner/env-manifest-scanner.js
|
|
927
784
|
import { readFile as readFile5, glob } from "node:fs/promises";
|
|
928
785
|
import { join as join7, relative } from "node:path";
|
|
929
786
|
import { existsSync as existsSync5 } from "node:fs";
|
|
930
|
-
var MANIFEST_PATH = "docs/reference/env-manifest.md";
|
|
931
787
|
async function scanEnvManifest(projectRoot) {
|
|
932
788
|
const manifestFullPath = join7(projectRoot, MANIFEST_PATH);
|
|
933
789
|
if (!existsSync5(manifestFullPath)) {
|
|
@@ -1120,8 +976,20 @@ function computeDrift(variables, apps, localPresence, workflowRefs) {
|
|
|
1120
976
|
workflowBrokenRefs: workflowBrokenRefs.sort()
|
|
1121
977
|
};
|
|
1122
978
|
}
|
|
979
|
+
var MANIFEST_PATH;
|
|
980
|
+
var init_env_manifest_scanner = __esm({
|
|
981
|
+
"dist/scanner/env-manifest-scanner.js"() {
|
|
982
|
+
"use strict";
|
|
983
|
+
MANIFEST_PATH = "docs/reference/env-manifest.md";
|
|
984
|
+
}
|
|
985
|
+
});
|
|
1123
986
|
|
|
1124
987
|
// dist/scanner/index.js
|
|
988
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
989
|
+
import { join as join8, relative as relative2 } from "node:path";
|
|
990
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
991
|
+
import { homedir as homedir5 } from "node:os";
|
|
992
|
+
import { createHash } from "node:crypto";
|
|
1125
993
|
async function scanProject(projectRoot) {
|
|
1126
994
|
const allFiles = await discoverFiles(projectRoot);
|
|
1127
995
|
const rootFiles = identifyRoots(allFiles, projectRoot);
|
|
@@ -1247,121 +1115,22 @@ function detectStaleFiles(allFiles, projectRoot) {
|
|
|
1247
1115
|
}
|
|
1248
1116
|
return stale;
|
|
1249
1117
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1264
|
-
<title>MD4AI \u2014 ${escapeHtml(title)}</title>
|
|
1265
|
-
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
1266
|
-
<style>
|
|
1267
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1268
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
|
1269
|
-
h1 { color: #38bdf8; margin-bottom: 0.5rem; }
|
|
1270
|
-
.subtitle { color: #94a3b8; margin-bottom: 2rem; }
|
|
1271
|
-
.section { background: #1e293b; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
|
|
1272
|
-
.section h2 { color: #38bdf8; margin-bottom: 1rem; font-size: 1.2rem; }
|
|
1273
|
-
.mermaid { background: #0f172a; border-radius: 8px; padding: 1rem; overflow-x: auto; }
|
|
1274
|
-
.badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; margin-left: 0.5rem; }
|
|
1275
|
-
.badge-orphan { background: #ef4444; color: white; }
|
|
1276
|
-
.badge-stale { background: #f59e0b; color: black; }
|
|
1277
|
-
table { width: 100%; border-collapse: collapse; }
|
|
1278
|
-
th, td { padding: 0.5rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
|
|
1279
|
-
th { color: #94a3b8; font-weight: 600; }
|
|
1280
|
-
.check { color: #22c55e; }
|
|
1281
|
-
.meta { color: #94a3b8; }
|
|
1282
|
-
.stale-meta { color: #f59e0b; }
|
|
1283
|
-
.stats { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
1284
|
-
.stat { text-align: center; }
|
|
1285
|
-
.stat-value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
|
|
1286
|
-
.stat-label { color: #94a3b8; font-size: 0.85rem; }
|
|
1287
|
-
.file-list { list-style: none; }
|
|
1288
|
-
.file-list li { padding: 0.3rem 0; color: #cbd5e1; font-family: monospace; font-size: 0.9rem; }
|
|
1289
|
-
#search { width: 100%; padding: 0.75rem 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 1rem; margin-bottom: 1rem; }
|
|
1290
|
-
#search:focus { outline: none; border-color: #38bdf8; }
|
|
1291
|
-
@media print { body { background: white; color: black; } .section { border: 1px solid #ccc; } }
|
|
1292
|
-
</style>
|
|
1293
|
-
</head>
|
|
1294
|
-
<body>
|
|
1295
|
-
<h1>MD4AI \u2014 ${escapeHtml(title)}</h1>
|
|
1296
|
-
<p class="subtitle">Scanned: ${new Date(result.scannedAt).toLocaleString()} | Hash: ${result.dataHash.slice(0, 12)}</p>
|
|
1297
|
-
|
|
1298
|
-
<div class="stats">
|
|
1299
|
-
<div class="stat"><div class="stat-value">${result.graph.nodes.length}</div><div class="stat-label">Files</div></div>
|
|
1300
|
-
<div class="stat"><div class="stat-value">${result.graph.edges.length}</div><div class="stat-label">References</div></div>
|
|
1301
|
-
<div class="stat"><div class="stat-value">${result.orphans.length}</div><div class="stat-label">Orphans</div></div>
|
|
1302
|
-
<div class="stat"><div class="stat-value">${result.staleFiles.length}</div><div class="stat-label">Stale</div></div>
|
|
1303
|
-
<div class="stat"><div class="stat-value">${result.skills.length}</div><div class="stat-label">Skills</div></div>
|
|
1304
|
-
</div>
|
|
1305
|
-
|
|
1306
|
-
<input type="text" id="search" placeholder="Search files..." oninput="filterFiles(this.value)">
|
|
1307
|
-
|
|
1308
|
-
<div class="section">
|
|
1309
|
-
<h2>Dependency Graph</h2>
|
|
1310
|
-
<pre class="mermaid">${escapeHtml(result.graph.mermaid)}</pre>
|
|
1311
|
-
</div>
|
|
1312
|
-
|
|
1313
|
-
${result.orphans.length > 0 ? `
|
|
1314
|
-
<div class="section">
|
|
1315
|
-
<h2>Orphan Files <span class="badge badge-orphan">${result.orphans.length}</span></h2>
|
|
1316
|
-
<p class="meta" style="margin-bottom: 1rem;">Files not reachable from any root configuration file.</p>
|
|
1317
|
-
<ul class="file-list" id="orphan-list">${orphanItems}</ul>
|
|
1318
|
-
</div>
|
|
1319
|
-
` : ""}
|
|
1320
|
-
|
|
1321
|
-
${result.staleFiles.length > 0 ? `
|
|
1322
|
-
<div class="section">
|
|
1323
|
-
<h2>Stale Files <span class="badge badge-stale">${result.staleFiles.length}</span></h2>
|
|
1324
|
-
<p class="meta" style="margin-bottom: 1rem;">Files not modified in over 90 days.</p>
|
|
1325
|
-
<ul class="file-list" id="stale-list">${staleItems}</ul>
|
|
1326
|
-
</div>
|
|
1327
|
-
` : ""}
|
|
1328
|
-
|
|
1329
|
-
<div class="section">
|
|
1330
|
-
<h2>Skills Comparison</h2>
|
|
1331
|
-
<table>
|
|
1332
|
-
<thead><tr><th>Skill/Plugin</th><th>Entire Machine</th><th>Project Specific</th><th>Source</th></tr></thead>
|
|
1333
|
-
<tbody>${skillRows}</tbody>
|
|
1334
|
-
</table>
|
|
1335
|
-
</div>
|
|
1336
|
-
|
|
1337
|
-
<div class="section">
|
|
1338
|
-
<h2>All Files</h2>
|
|
1339
|
-
<ul class="file-list" id="all-files">${fileItems}</ul>
|
|
1340
|
-
</div>
|
|
1341
|
-
|
|
1342
|
-
<script>
|
|
1343
|
-
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
|
|
1344
|
-
|
|
1345
|
-
function filterFiles(query) {
|
|
1346
|
-
var q = query.toLowerCase();
|
|
1347
|
-
document.querySelectorAll('.file-list li').forEach(function(li) {
|
|
1348
|
-
var path = li.getAttribute('data-path') || '';
|
|
1349
|
-
li.style.display = path.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
|
-
</script>
|
|
1353
|
-
|
|
1354
|
-
<script type="application/json" id="scan-data">${dataJson}</script>
|
|
1355
|
-
</body>
|
|
1356
|
-
</html>`;
|
|
1357
|
-
}
|
|
1358
|
-
function escapeHtml(text) {
|
|
1359
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1360
|
-
}
|
|
1118
|
+
var init_scanner = __esm({
|
|
1119
|
+
"dist/scanner/index.js"() {
|
|
1120
|
+
"use strict";
|
|
1121
|
+
init_dist();
|
|
1122
|
+
init_file_parser();
|
|
1123
|
+
init_graph_builder();
|
|
1124
|
+
init_orphan_detector();
|
|
1125
|
+
init_skills_parser();
|
|
1126
|
+
init_git_dates();
|
|
1127
|
+
init_tooling_detector();
|
|
1128
|
+
init_env_manifest_scanner();
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1361
1131
|
|
|
1362
1132
|
// dist/check-update.js
|
|
1363
1133
|
import chalk8 from "chalk";
|
|
1364
|
-
var CURRENT_VERSION = "0.6.2";
|
|
1365
1134
|
async function checkForUpdate() {
|
|
1366
1135
|
try {
|
|
1367
1136
|
const controller = new AbortController();
|
|
@@ -1378,13 +1147,12 @@ async function checkForUpdate() {
|
|
|
1378
1147
|
return;
|
|
1379
1148
|
if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
|
|
1380
1149
|
console.log("");
|
|
1381
|
-
console.log(chalk8.yellow("\u250C\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\
|
|
1382
|
-
console.log(chalk8.yellow("\u2502") + chalk8.bold(" Update available! ") + chalk8.dim(`${CURRENT_VERSION}`) + chalk8.white(" \u2192 ") + chalk8.green.bold(`${latest}`) + "
|
|
1383
|
-
console.log(chalk8.yellow("\u2502") + "
|
|
1384
|
-
console.log(chalk8.yellow("\u2502") + " Run: " + chalk8.cyan("
|
|
1385
|
-
console.log(chalk8.yellow("\u2502") + "
|
|
1386
|
-
console.log(chalk8.yellow("\
|
|
1387
|
-
console.log(chalk8.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\u2500\u2518"));
|
|
1150
|
+
console.log(chalk8.yellow("\u250C\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\u2510"));
|
|
1151
|
+
console.log(chalk8.yellow("\u2502") + chalk8.bold(" Update available! ") + chalk8.dim(`${CURRENT_VERSION}`) + chalk8.white(" \u2192 ") + chalk8.green.bold(`${latest}`) + " " + chalk8.yellow("\u2502"));
|
|
1152
|
+
console.log(chalk8.yellow("\u2502") + " " + chalk8.yellow("\u2502"));
|
|
1153
|
+
console.log(chalk8.yellow("\u2502") + " Run: " + chalk8.cyan("md4ai update") + " " + chalk8.yellow("\u2502"));
|
|
1154
|
+
console.log(chalk8.yellow("\u2502") + " " + chalk8.yellow("\u2502"));
|
|
1155
|
+
console.log(chalk8.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\u2518"));
|
|
1388
1156
|
console.log("");
|
|
1389
1157
|
} else {
|
|
1390
1158
|
console.log(chalk8.green(`md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.`));
|
|
@@ -1403,6 +1171,13 @@ function isNewer(a, b) {
|
|
|
1403
1171
|
}
|
|
1404
1172
|
return false;
|
|
1405
1173
|
}
|
|
1174
|
+
var CURRENT_VERSION;
|
|
1175
|
+
var init_check_update = __esm({
|
|
1176
|
+
"dist/check-update.js"() {
|
|
1177
|
+
"use strict";
|
|
1178
|
+
CURRENT_VERSION = true ? "0.7.2" : "0.0.0-dev";
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1406
1181
|
|
|
1407
1182
|
// dist/commands/push-toolings.js
|
|
1408
1183
|
async function pushToolings(supabase, folderId, toolings) {
|
|
@@ -1422,75 +1197,35 @@ async function pushToolings(supabase, folderId, toolings) {
|
|
|
1422
1197
|
}, { onConflict: "folder_id,tool_name,detection_source" });
|
|
1423
1198
|
}
|
|
1424
1199
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
await checkForUpdate();
|
|
1429
|
-
const projectRoot = resolve3(path ?? process.cwd());
|
|
1430
|
-
if (!existsSync7(projectRoot)) {
|
|
1431
|
-
console.error(chalk9.red(`Path not found: ${projectRoot}`));
|
|
1432
|
-
process.exit(1);
|
|
1433
|
-
}
|
|
1434
|
-
console.log(chalk9.blue(`Scanning: ${projectRoot}
|
|
1435
|
-
`));
|
|
1436
|
-
const result = await scanProject(projectRoot);
|
|
1437
|
-
console.log(` Files found: ${result.graph.nodes.length}`);
|
|
1438
|
-
console.log(` References: ${result.graph.edges.length}`);
|
|
1439
|
-
console.log(` Orphans: ${result.orphans.length}`);
|
|
1440
|
-
console.log(` Stale files: ${result.staleFiles.length}`);
|
|
1441
|
-
console.log(` Skills: ${result.skills.length}`);
|
|
1442
|
-
console.log(` Toolings: ${result.toolings.length}`);
|
|
1443
|
-
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
1444
|
-
console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
|
|
1445
|
-
const outputDir = resolve3(projectRoot, "output");
|
|
1446
|
-
if (!existsSync7(outputDir)) {
|
|
1447
|
-
await mkdir2(outputDir, { recursive: true });
|
|
1200
|
+
var init_push_toolings = __esm({
|
|
1201
|
+
"dist/commands/push-toolings.js"() {
|
|
1202
|
+
"use strict";
|
|
1448
1203
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
const fullPath = resolve3(projectRoot, file.file_path);
|
|
1475
|
-
try {
|
|
1476
|
-
const { unlink } = await import("node:fs/promises");
|
|
1477
|
-
await unlink(fullPath);
|
|
1478
|
-
await supabase.from("folder_files").delete().eq("id", file.id);
|
|
1479
|
-
console.log(chalk9.green(` Deleted: ${file.file_path}`));
|
|
1480
|
-
} catch (err) {
|
|
1481
|
-
console.error(chalk9.red(` Failed to delete ${file.file_path}: ${err}`));
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
|
|
1485
|
-
if (keptIds.length > 0) {
|
|
1486
|
-
for (const id of keptIds) {
|
|
1487
|
-
await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
|
|
1488
|
-
}
|
|
1489
|
-
console.log(chalk9.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
|
|
1490
|
-
}
|
|
1491
|
-
console.log("");
|
|
1492
|
-
}
|
|
1493
|
-
const { error } = await supabase.from("claude_folders").update({
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
// dist/commands/sync.js
|
|
1207
|
+
var sync_exports = {};
|
|
1208
|
+
__export(sync_exports, {
|
|
1209
|
+
syncCommand: () => syncCommand
|
|
1210
|
+
});
|
|
1211
|
+
import chalk12 from "chalk";
|
|
1212
|
+
async function syncCommand(options) {
|
|
1213
|
+
const { supabase } = await getAuthenticatedClient();
|
|
1214
|
+
if (options.all) {
|
|
1215
|
+
const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path");
|
|
1216
|
+
if (error || !devices?.length) {
|
|
1217
|
+
console.error(chalk12.red("No devices found."));
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
for (const device of devices) {
|
|
1221
|
+
console.log(chalk12.blue(`Syncing: ${device.path}`));
|
|
1222
|
+
const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
|
|
1223
|
+
if (proposedAll?.length) {
|
|
1224
|
+
console.log(chalk12.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
const result = await scanProject(device.path);
|
|
1228
|
+
await supabase.from("claude_folders").update({
|
|
1494
1229
|
graph_json: result.graph,
|
|
1495
1230
|
orphans_json: result.orphans,
|
|
1496
1231
|
skills_table_json: result.skills,
|
|
@@ -1498,197 +1233,12 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
1498
1233
|
env_manifest_json: result.envManifest,
|
|
1499
1234
|
last_scanned: result.scannedAt,
|
|
1500
1235
|
data_hash: result.dataHash
|
|
1501
|
-
}).eq("id", folder_id);
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
await saveState({
|
|
1508
|
-
lastFolderId: folder_id,
|
|
1509
|
-
lastDeviceName: device_name,
|
|
1510
|
-
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1511
|
-
});
|
|
1512
|
-
console.log(chalk9.green("Synced to Supabase."));
|
|
1513
|
-
console.log(chalk9.cyan(`
|
|
1514
|
-
https://www.md4ai.com/project/${folder_id}
|
|
1515
|
-
`));
|
|
1516
|
-
const configFiles = await readClaudeConfigFiles(projectRoot);
|
|
1517
|
-
if (configFiles.length > 0) {
|
|
1518
|
-
for (const file of configFiles) {
|
|
1519
|
-
await supabase.from("folder_files").upsert({
|
|
1520
|
-
folder_id,
|
|
1521
|
-
file_path: file.filePath,
|
|
1522
|
-
content: file.content,
|
|
1523
|
-
size_bytes: file.sizeBytes,
|
|
1524
|
-
last_modified: file.lastModified
|
|
1525
|
-
}, { onConflict: "folder_id,file_path" });
|
|
1526
|
-
}
|
|
1527
|
-
console.log(chalk9.green(` Uploaded ${configFiles.length} config file(s).`));
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
} else {
|
|
1531
|
-
console.log(chalk9.yellow("No device path matches this folder. Run: md4ai add-device\n(Local preview still generated.)"));
|
|
1532
|
-
}
|
|
1533
|
-
} catch {
|
|
1534
|
-
console.log(chalk9.yellow("Not logged in \u2014 local preview only."));
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
// dist/commands/simulate.js
|
|
1540
|
-
import { join as join9 } from "node:path";
|
|
1541
|
-
import { existsSync as existsSync8 } from "node:fs";
|
|
1542
|
-
import { homedir as homedir6 } from "node:os";
|
|
1543
|
-
import chalk10 from "chalk";
|
|
1544
|
-
async function simulateCommand(prompt) {
|
|
1545
|
-
const projectRoot = process.cwd();
|
|
1546
|
-
console.log(chalk10.blue(`Simulating prompt: "${prompt}"
|
|
1547
|
-
`));
|
|
1548
|
-
console.log(chalk10.dim("Files Claude would load:\n"));
|
|
1549
|
-
const globalClaude = join9(homedir6(), ".claude", "CLAUDE.md");
|
|
1550
|
-
if (existsSync8(globalClaude)) {
|
|
1551
|
-
console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
|
|
1552
|
-
}
|
|
1553
|
-
for (const rootFile of ROOT_FILES) {
|
|
1554
|
-
const fullPath = join9(projectRoot, rootFile);
|
|
1555
|
-
if (existsSync8(fullPath)) {
|
|
1556
|
-
console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
const settingsFiles = [
|
|
1560
|
-
join9(homedir6(), ".claude", "settings.json"),
|
|
1561
|
-
join9(homedir6(), ".claude", "settings.local.json"),
|
|
1562
|
-
join9(projectRoot, ".claude", "settings.json"),
|
|
1563
|
-
join9(projectRoot, ".claude", "settings.local.json")
|
|
1564
|
-
];
|
|
1565
|
-
for (const sf of settingsFiles) {
|
|
1566
|
-
if (existsSync8(sf)) {
|
|
1567
|
-
const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
|
|
1568
|
-
console.log(chalk10.green(` \u2713 ${display} (settings)`));
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
const words = prompt.split(/\s+/);
|
|
1572
|
-
for (const word of words) {
|
|
1573
|
-
const cleaned = word.replace(/['"]/g, "");
|
|
1574
|
-
const candidatePath = join9(projectRoot, cleaned);
|
|
1575
|
-
if (existsSync8(candidatePath) && cleaned.includes("/")) {
|
|
1576
|
-
console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
console.log(chalk10.dim("\nNote: This is an approximation. Actual file loading depends on Claude Code internals."));
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// dist/commands/print.js
|
|
1583
|
-
import { join as join10 } from "node:path";
|
|
1584
|
-
import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
1585
|
-
import { existsSync as existsSync9 } from "node:fs";
|
|
1586
|
-
import chalk11 from "chalk";
|
|
1587
|
-
async function printCommand(title) {
|
|
1588
|
-
const projectRoot = process.cwd();
|
|
1589
|
-
const scanDataPath = join10(projectRoot, "output", "index.html");
|
|
1590
|
-
if (!existsSync9(scanDataPath)) {
|
|
1591
|
-
console.error(chalk11.red("No scan data found. Run: md4ai scan"));
|
|
1592
|
-
process.exit(1);
|
|
1593
|
-
}
|
|
1594
|
-
const html = await readFile6(scanDataPath, "utf-8");
|
|
1595
|
-
const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
|
|
1596
|
-
if (!match) {
|
|
1597
|
-
console.error(chalk11.red("Could not extract scan data from output/index.html"));
|
|
1598
|
-
process.exit(1);
|
|
1599
|
-
}
|
|
1600
|
-
const result = JSON.parse(match[1]);
|
|
1601
|
-
const printHtml = generatePrintHtml(result, title);
|
|
1602
|
-
const outputPath = join10(projectRoot, "output", `print-${Date.now()}.html`);
|
|
1603
|
-
await writeFile3(outputPath, printHtml, "utf-8");
|
|
1604
|
-
console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
|
|
1605
|
-
}
|
|
1606
|
-
function escapeHtml2(text) {
|
|
1607
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1608
|
-
}
|
|
1609
|
-
function generatePrintHtml(result, title) {
|
|
1610
|
-
const skillRows = result.skills.map((s) => `<tr><td>${escapeHtml2(s.name)}</td><td>${s.machineWide ? '<span class="check">\u2713</span>' : ""}</td><td>${s.projectSpecific ? '<span class="check">\u2713</span>' : ""}</td></tr>`).join("\n");
|
|
1611
|
-
const orphanItems = result.orphans.map((o) => `<li class="orphan">${escapeHtml2(o.path)}</li>`).join("\n");
|
|
1612
|
-
const staleItems = result.staleFiles.map((s) => `<li class="stale">${escapeHtml2(s.path)} (${s.daysSinceModified}d)</li>`).join("\n");
|
|
1613
|
-
return `<!DOCTYPE html>
|
|
1614
|
-
<html lang="en">
|
|
1615
|
-
<head>
|
|
1616
|
-
<meta charset="UTF-8">
|
|
1617
|
-
<title>${escapeHtml2(title)} \u2014 MD4AI Wall Sheet</title>
|
|
1618
|
-
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
1619
|
-
<style>
|
|
1620
|
-
@page { size: A3 landscape; margin: 1cm; }
|
|
1621
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 1rem; }
|
|
1622
|
-
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
1623
|
-
.subtitle { color: #666; margin-bottom: 2rem; }
|
|
1624
|
-
.mermaid { margin: 2rem 0; }
|
|
1625
|
-
.columns { display: flex; gap: 2rem; }
|
|
1626
|
-
.column { flex: 1; }
|
|
1627
|
-
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
1628
|
-
th, td { padding: 0.3rem 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
|
|
1629
|
-
th { background: #f5f5f5; }
|
|
1630
|
-
.orphan { color: #dc2626; }
|
|
1631
|
-
.stale { color: #d97706; }
|
|
1632
|
-
.check { color: #16a34a; font-weight: bold; }
|
|
1633
|
-
</style>
|
|
1634
|
-
</head>
|
|
1635
|
-
<body>
|
|
1636
|
-
<h1>${escapeHtml2(title)}</h1>
|
|
1637
|
-
<p class="subtitle">Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()} | Files: ${result.graph.nodes.length} | Orphans: ${result.orphans.length}</p>
|
|
1638
|
-
|
|
1639
|
-
<pre class="mermaid">${escapeHtml2(result.graph.mermaid)}</pre>
|
|
1640
|
-
|
|
1641
|
-
<div class="columns">
|
|
1642
|
-
<div class="column">
|
|
1643
|
-
<h2>Skills Comparison</h2>
|
|
1644
|
-
<table>
|
|
1645
|
-
<tr><th>Skill</th><th>Machine</th><th>Project</th></tr>
|
|
1646
|
-
${skillRows}
|
|
1647
|
-
</table>
|
|
1648
|
-
</div>
|
|
1649
|
-
<div class="column">
|
|
1650
|
-
${result.orphans.length ? `<h2>Orphans</h2><ul>${orphanItems}</ul>` : ""}
|
|
1651
|
-
${result.staleFiles.length ? `<h2>Stale Files</h2><ul>${staleItems}</ul>` : ""}
|
|
1652
|
-
</div>
|
|
1653
|
-
</div>
|
|
1654
|
-
|
|
1655
|
-
<script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
|
|
1656
|
-
</body>
|
|
1657
|
-
</html>`;
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
// dist/commands/sync.js
|
|
1661
|
-
import chalk12 from "chalk";
|
|
1662
|
-
async function syncCommand(options) {
|
|
1663
|
-
const { supabase } = await getAuthenticatedClient();
|
|
1664
|
-
if (options.all) {
|
|
1665
|
-
const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path");
|
|
1666
|
-
if (error || !devices?.length) {
|
|
1667
|
-
console.error(chalk12.red("No devices found."));
|
|
1668
|
-
process.exit(1);
|
|
1669
|
-
}
|
|
1670
|
-
for (const device of devices) {
|
|
1671
|
-
console.log(chalk12.blue(`Syncing: ${device.path}`));
|
|
1672
|
-
const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
|
|
1673
|
-
if (proposedAll?.length) {
|
|
1674
|
-
console.log(chalk12.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
|
|
1675
|
-
}
|
|
1676
|
-
try {
|
|
1677
|
-
const result = await scanProject(device.path);
|
|
1678
|
-
await supabase.from("claude_folders").update({
|
|
1679
|
-
graph_json: result.graph,
|
|
1680
|
-
orphans_json: result.orphans,
|
|
1681
|
-
skills_table_json: result.skills,
|
|
1682
|
-
stale_files_json: result.staleFiles,
|
|
1683
|
-
env_manifest_json: result.envManifest,
|
|
1684
|
-
last_scanned: result.scannedAt,
|
|
1685
|
-
data_hash: result.dataHash
|
|
1686
|
-
}).eq("id", device.folder_id);
|
|
1687
|
-
await pushToolings(supabase, device.folder_id, result.toolings);
|
|
1688
|
-
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
|
|
1689
|
-
console.log(chalk12.green(` Done: ${device.device_name}`));
|
|
1690
|
-
} catch (err) {
|
|
1691
|
-
console.error(chalk12.red(` Failed: ${device.path}: ${err}`));
|
|
1236
|
+
}).eq("id", device.folder_id);
|
|
1237
|
+
await pushToolings(supabase, device.folder_id, result.toolings);
|
|
1238
|
+
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
|
|
1239
|
+
console.log(chalk12.green(` Done: ${device.device_name}`));
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
console.error(chalk12.red(` Failed: ${device.path}: ${err}`));
|
|
1692
1242
|
}
|
|
1693
1243
|
}
|
|
1694
1244
|
await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -1724,10 +1274,15 @@ async function syncCommand(options) {
|
|
|
1724
1274
|
console.log(chalk12.green("Synced."));
|
|
1725
1275
|
}
|
|
1726
1276
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1277
|
+
var init_sync = __esm({
|
|
1278
|
+
"dist/commands/sync.js"() {
|
|
1279
|
+
"use strict";
|
|
1280
|
+
init_auth();
|
|
1281
|
+
init_config();
|
|
1282
|
+
init_scanner();
|
|
1283
|
+
init_push_toolings();
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1731
1286
|
|
|
1732
1287
|
// dist/device-utils.js
|
|
1733
1288
|
import { hostname as hostname2, platform as platform2 } from "node:os";
|
|
@@ -1761,677 +1316,250 @@ async function resolveDeviceId(supabase, userId) {
|
|
|
1761
1316
|
}
|
|
1762
1317
|
return data.id;
|
|
1763
1318
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
const { supabase, userId } = await getAuthenticatedClient();
|
|
1768
|
-
const cwd = resolve4(process.cwd());
|
|
1769
|
-
const deviceName = detectDeviceName();
|
|
1770
|
-
const osType = detectOs2();
|
|
1771
|
-
const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
|
|
1772
|
-
if (folderErr || !folder) {
|
|
1773
|
-
console.error(chalk13.red("Project not found, or you do not have access."));
|
|
1774
|
-
console.error(chalk13.yellow("Check the project ID in the MD4AI web dashboard."));
|
|
1775
|
-
process.exit(1);
|
|
1319
|
+
var init_device_utils = __esm({
|
|
1320
|
+
"dist/device-utils.js"() {
|
|
1321
|
+
"use strict";
|
|
1776
1322
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
const { error: pathErr } = await supabase.from("device_paths").insert({
|
|
1794
|
-
user_id: userId,
|
|
1795
|
-
folder_id: folder.id,
|
|
1796
|
-
device_name: deviceName,
|
|
1797
|
-
os_type: osType,
|
|
1798
|
-
path: cwd
|
|
1799
|
-
});
|
|
1800
|
-
if (pathErr) {
|
|
1801
|
-
console.error(chalk13.red(`Failed to link: ${pathErr.message}`));
|
|
1802
|
-
process.exit(1);
|
|
1803
|
-
}
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
// dist/mcp/read-configs.js
|
|
1326
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
1327
|
+
import { join as join12 } from "node:path";
|
|
1328
|
+
import { homedir as homedir7 } from "node:os";
|
|
1329
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
1330
|
+
import { readdir as readdir3 } from "node:fs/promises";
|
|
1331
|
+
async function readJsonSafe(path) {
|
|
1332
|
+
try {
|
|
1333
|
+
if (!existsSync11(path))
|
|
1334
|
+
return null;
|
|
1335
|
+
const raw = await readFile8(path, "utf-8");
|
|
1336
|
+
return JSON.parse(raw);
|
|
1337
|
+
} catch {
|
|
1338
|
+
return null;
|
|
1804
1339
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
console.log(` Toolings: ${result.toolings.length}`);
|
|
1813
|
-
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
1814
|
-
const { error: scanErr } = await supabase.from("claude_folders").update({
|
|
1815
|
-
graph_json: result.graph,
|
|
1816
|
-
orphans_json: result.orphans,
|
|
1817
|
-
skills_table_json: result.skills,
|
|
1818
|
-
stale_files_json: result.staleFiles,
|
|
1819
|
-
env_manifest_json: result.envManifest,
|
|
1820
|
-
last_scanned: result.scannedAt,
|
|
1821
|
-
data_hash: result.dataHash
|
|
1822
|
-
}).eq("id", folder.id);
|
|
1823
|
-
if (scanErr) {
|
|
1824
|
-
console.error(chalk13.yellow(`Scan upload warning: ${scanErr.message}`));
|
|
1340
|
+
}
|
|
1341
|
+
function extractPackageName(args) {
|
|
1342
|
+
for (const arg of args) {
|
|
1343
|
+
if (arg.startsWith("-"))
|
|
1344
|
+
continue;
|
|
1345
|
+
if (arg.includes("mcp") || arg.startsWith("@"))
|
|
1346
|
+
return arg;
|
|
1825
1347
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
for (const file of configFiles) {
|
|
1830
|
-
await supabase.from("folder_files").upsert({
|
|
1831
|
-
folder_id: folder.id,
|
|
1832
|
-
file_path: file.filePath,
|
|
1833
|
-
content: file.content,
|
|
1834
|
-
size_bytes: file.sizeBytes,
|
|
1835
|
-
last_modified: file.lastModified
|
|
1836
|
-
}, { onConflict: "folder_id,file_path" });
|
|
1837
|
-
}
|
|
1838
|
-
console.log(chalk13.green(` Uploaded ${configFiles.length} config file(s).`));
|
|
1348
|
+
for (const arg of args) {
|
|
1349
|
+
if (!arg.startsWith("-"))
|
|
1350
|
+
return arg;
|
|
1839
1351
|
}
|
|
1840
|
-
|
|
1841
|
-
await saveState({
|
|
1842
|
-
lastFolderId: folder.id,
|
|
1843
|
-
lastDeviceName: deviceName,
|
|
1844
|
-
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1845
|
-
});
|
|
1846
|
-
const projectUrl = `https://www.md4ai.com/project/${folder.id}`;
|
|
1847
|
-
console.log(chalk13.green("\nDone! Project linked and scanned."));
|
|
1848
|
-
console.log(chalk13.cyan(`
|
|
1849
|
-
${projectUrl}
|
|
1850
|
-
`));
|
|
1851
|
-
console.log(chalk13.grey('Run "md4ai scan" to rescan at any time.'));
|
|
1352
|
+
return null;
|
|
1852
1353
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1354
|
+
function parseServers(data, source) {
|
|
1355
|
+
if (!data?.mcpServers)
|
|
1356
|
+
return [];
|
|
1357
|
+
const entries = [];
|
|
1358
|
+
for (const [name, server] of Object.entries(data.mcpServers)) {
|
|
1359
|
+
const serverType = server.type === "http" ? "http" : "stdio";
|
|
1360
|
+
entries.push({
|
|
1361
|
+
name,
|
|
1362
|
+
source,
|
|
1363
|
+
type: serverType,
|
|
1364
|
+
command: server.command,
|
|
1365
|
+
args: server.args,
|
|
1366
|
+
env: server.env,
|
|
1367
|
+
url: server.url
|
|
1368
|
+
});
|
|
1864
1369
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1370
|
+
return entries;
|
|
1371
|
+
}
|
|
1372
|
+
function parseFlatConfig(data, source) {
|
|
1373
|
+
if (!data)
|
|
1374
|
+
return [];
|
|
1375
|
+
const entries = [];
|
|
1376
|
+
for (const [name, server] of Object.entries(data)) {
|
|
1377
|
+
if (typeof server !== "object" || server === null)
|
|
1378
|
+
continue;
|
|
1379
|
+
if (!server.command && !server.url)
|
|
1380
|
+
continue;
|
|
1381
|
+
const serverType = server.type === "http" ? "http" : "stdio";
|
|
1382
|
+
entries.push({
|
|
1383
|
+
name,
|
|
1384
|
+
source,
|
|
1385
|
+
type: serverType,
|
|
1386
|
+
command: server.command,
|
|
1387
|
+
args: server.args,
|
|
1388
|
+
env: server.env,
|
|
1389
|
+
url: server.url
|
|
1390
|
+
});
|
|
1872
1391
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1392
|
+
return entries;
|
|
1393
|
+
}
|
|
1394
|
+
async function readAllMcpConfigs() {
|
|
1395
|
+
const home = homedir7();
|
|
1396
|
+
const entries = [];
|
|
1397
|
+
const userConfig = await readJsonSafe(join12(home, ".claude.json"));
|
|
1398
|
+
entries.push(...parseServers(userConfig, "global"));
|
|
1399
|
+
const globalMcp = await readJsonSafe(join12(home, ".claude", "mcp.json"));
|
|
1400
|
+
entries.push(...parseServers(globalMcp, "global"));
|
|
1401
|
+
const cwdMcp = await readJsonSafe(join12(process.cwd(), ".mcp.json"));
|
|
1402
|
+
entries.push(...parseServers(cwdMcp, "project"));
|
|
1403
|
+
const pluginsBase = join12(home, ".claude", "plugins", "marketplaces");
|
|
1404
|
+
if (existsSync11(pluginsBase)) {
|
|
1405
|
+
try {
|
|
1406
|
+
const marketplaces = await readdir3(pluginsBase, { withFileTypes: true });
|
|
1407
|
+
for (const mp of marketplaces) {
|
|
1408
|
+
if (!mp.isDirectory())
|
|
1409
|
+
continue;
|
|
1410
|
+
const extDir = join12(pluginsBase, mp.name, "external_plugins");
|
|
1411
|
+
if (!existsSync11(extDir))
|
|
1412
|
+
continue;
|
|
1413
|
+
const plugins = await readdir3(extDir, { withFileTypes: true });
|
|
1414
|
+
for (const plugin of plugins) {
|
|
1415
|
+
if (!plugin.isDirectory())
|
|
1416
|
+
continue;
|
|
1417
|
+
const pluginMcp = await readJsonSafe(join12(extDir, plugin.name, ".mcp.json"));
|
|
1418
|
+
entries.push(...parseServers(pluginMcp, "plugin"));
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
} catch {
|
|
1885
1422
|
}
|
|
1886
1423
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1424
|
+
const cacheBase = join12(home, ".claude", "plugins", "cache");
|
|
1425
|
+
if (existsSync11(cacheBase)) {
|
|
1426
|
+
try {
|
|
1427
|
+
const registries = await readdir3(cacheBase, { withFileTypes: true });
|
|
1428
|
+
for (const reg of registries) {
|
|
1429
|
+
if (!reg.isDirectory())
|
|
1430
|
+
continue;
|
|
1431
|
+
const regDir = join12(cacheBase, reg.name);
|
|
1432
|
+
const plugins = await readdir3(regDir, { withFileTypes: true });
|
|
1433
|
+
for (const plugin of plugins) {
|
|
1434
|
+
if (!plugin.isDirectory())
|
|
1435
|
+
continue;
|
|
1436
|
+
const versionDirs = await readdir3(join12(regDir, plugin.name), { withFileTypes: true });
|
|
1437
|
+
for (const ver of versionDirs) {
|
|
1438
|
+
if (!ver.isDirectory())
|
|
1439
|
+
continue;
|
|
1440
|
+
const mcpPath = join12(regDir, plugin.name, ver.name, ".mcp.json");
|
|
1441
|
+
const flatData = await readJsonSafe(mcpPath);
|
|
1442
|
+
if (flatData) {
|
|
1443
|
+
entries.push(...parseFlatConfig(flatData, "plugin"));
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
} catch {
|
|
1912
1450
|
}
|
|
1913
|
-
await writeFile4(fullPath, file.content, "utf-8");
|
|
1914
|
-
console.log(chalk14.green(` \u2713 ${file.filePath}`));
|
|
1915
1451
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
async function adminUpdateToolCommand(options) {
|
|
1924
|
-
const { supabase } = await getAuthenticatedClient();
|
|
1925
|
-
if (!options.name) {
|
|
1926
|
-
console.error(chalk15.red("--name is required."));
|
|
1927
|
-
process.exit(1);
|
|
1452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1453
|
+
const deduped = [];
|
|
1454
|
+
for (const entry of entries) {
|
|
1455
|
+
if (seen.has(entry.name))
|
|
1456
|
+
continue;
|
|
1457
|
+
seen.add(entry.name);
|
|
1458
|
+
deduped.push(entry);
|
|
1928
1459
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1460
|
+
return deduped;
|
|
1461
|
+
}
|
|
1462
|
+
var init_read_configs = __esm({
|
|
1463
|
+
"dist/mcp/read-configs.js"() {
|
|
1464
|
+
"use strict";
|
|
1932
1465
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
if (error) {
|
|
1952
|
-
console.error(chalk15.red(`Failed to update: ${error.message}`));
|
|
1953
|
-
process.exit(1);
|
|
1954
|
-
}
|
|
1955
|
-
console.log(chalk15.green(`Updated "${existing.display_name}" in the registry.`));
|
|
1956
|
-
} else {
|
|
1957
|
-
if (!options.display || !options.category) {
|
|
1958
|
-
console.error(chalk15.red("New tools require --display and --category."));
|
|
1959
|
-
process.exit(1);
|
|
1960
|
-
}
|
|
1961
|
-
const { error } = await supabase.from("tools_registry").insert({
|
|
1962
|
-
name: options.name,
|
|
1963
|
-
display_name: options.display,
|
|
1964
|
-
category: options.category,
|
|
1965
|
-
latest_stable: options.stable ?? null,
|
|
1966
|
-
latest_beta: options.beta ?? null,
|
|
1967
|
-
source_url: options.source ?? null,
|
|
1968
|
-
install_url: options.install ?? null,
|
|
1969
|
-
notes: options.notes ?? null
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
// dist/mcp/scan-processes.js
|
|
1469
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
1470
|
+
function parsePsOutput(output) {
|
|
1471
|
+
const lines = output.trim().split("\n").slice(1);
|
|
1472
|
+
const entries = [];
|
|
1473
|
+
for (const line of lines) {
|
|
1474
|
+
const trimmed = line.trim();
|
|
1475
|
+
const match = trimmed.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(.+)$/);
|
|
1476
|
+
if (!match)
|
|
1477
|
+
continue;
|
|
1478
|
+
entries.push({
|
|
1479
|
+
pid: parseInt(match[1], 10),
|
|
1480
|
+
tty: match[2],
|
|
1481
|
+
etimeRaw: match[3],
|
|
1482
|
+
rss: parseInt(match[4], 10),
|
|
1483
|
+
args: match[5]
|
|
1970
1484
|
});
|
|
1971
|
-
if (error) {
|
|
1972
|
-
console.error(chalk15.red(`Failed to create: ${error.message}`));
|
|
1973
|
-
process.exit(1);
|
|
1974
|
-
}
|
|
1975
|
-
console.log(chalk15.green(`Added "${options.display}" to the registry.`));
|
|
1976
1485
|
}
|
|
1486
|
+
return entries;
|
|
1977
1487
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
console.error(chalk16.red(`Failed to fetch tools: ${error.message}`));
|
|
1986
|
-
process.exit(1);
|
|
1488
|
+
function parseEtime(etime) {
|
|
1489
|
+
let days = 0;
|
|
1490
|
+
let rest = etime;
|
|
1491
|
+
if (rest.includes("-")) {
|
|
1492
|
+
const [d, r] = rest.split("-");
|
|
1493
|
+
days = parseInt(d, 10);
|
|
1494
|
+
rest = r;
|
|
1987
1495
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
return;
|
|
1496
|
+
const parts = rest.split(":").map(Number);
|
|
1497
|
+
if (parts.length === 3) {
|
|
1498
|
+
return days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
1499
|
+
} else if (parts.length === 2) {
|
|
1500
|
+
return days * 86400 + parts[0] * 60 + parts[1];
|
|
1991
1501
|
}
|
|
1992
|
-
|
|
1993
|
-
const catW = Math.max(10, ...tools.map((t) => t.category.length));
|
|
1994
|
-
const stableW = Math.max(8, ...tools.map((t) => (t.latest_stable ?? "\u2014").length));
|
|
1995
|
-
const betaW = Math.max(8, ...tools.map((t) => (t.latest_beta ?? "\u2014").length));
|
|
1996
|
-
const header = [
|
|
1997
|
-
"Name".padEnd(nameW),
|
|
1998
|
-
"Category".padEnd(catW),
|
|
1999
|
-
"Stable".padEnd(stableW),
|
|
2000
|
-
"Beta".padEnd(betaW),
|
|
2001
|
-
"Last Checked"
|
|
2002
|
-
].join(" ");
|
|
2003
|
-
console.log(chalk16.bold(header));
|
|
2004
|
-
console.log("\u2500".repeat(header.length));
|
|
2005
|
-
for (const tool of tools) {
|
|
2006
|
-
const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
|
|
2007
|
-
const row = [
|
|
2008
|
-
tool.display_name.padEnd(nameW),
|
|
2009
|
-
tool.category.padEnd(catW),
|
|
2010
|
-
(tool.latest_stable ?? "\u2014").padEnd(stableW),
|
|
2011
|
-
(tool.latest_beta ?? "\u2014").padEnd(betaW),
|
|
2012
|
-
lastChecked
|
|
2013
|
-
].join(" ");
|
|
2014
|
-
console.log(row);
|
|
2015
|
-
}
|
|
2016
|
-
console.log(chalk16.grey(`
|
|
2017
|
-
${tools.length} tool(s) in registry.`));
|
|
2018
|
-
}
|
|
2019
|
-
function formatRelative(date) {
|
|
2020
|
-
const diff = Date.now() - date.getTime();
|
|
2021
|
-
const hours = Math.floor(diff / (1e3 * 60 * 60));
|
|
2022
|
-
if (hours < 1)
|
|
2023
|
-
return "just now";
|
|
2024
|
-
if (hours < 24)
|
|
2025
|
-
return `${hours}h ago`;
|
|
2026
|
-
const days = Math.floor(hours / 24);
|
|
2027
|
-
if (days < 7)
|
|
2028
|
-
return `${days}d ago`;
|
|
2029
|
-
return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
|
|
1502
|
+
return days * 86400 + (parts[0] ?? 0);
|
|
2030
1503
|
}
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
"chalk": { type: "npm", package: "chalk" },
|
|
2044
|
-
"commander": { type: "npm", package: "commander" },
|
|
2045
|
-
"inquirer": { type: "npm", package: "inquirer" },
|
|
2046
|
-
"playwright": { type: "npm", package: "playwright" },
|
|
2047
|
-
"resend": { type: "npm", package: "resend" },
|
|
2048
|
-
"@supabase/supabase-js": { type: "npm", package: "@supabase/supabase-js" },
|
|
2049
|
-
"pnpm": { type: "npm", package: "pnpm" },
|
|
2050
|
-
"claude-code": { type: "npm", package: "@anthropic-ai/claude-code" },
|
|
2051
|
-
"turborepo": { type: "npm", package: "turbo" },
|
|
2052
|
-
"npm": { type: "npm", package: "npm" },
|
|
2053
|
-
"node": { type: "github", repo: "nodejs/node" },
|
|
2054
|
-
"supabase-cli": { type: "npm", package: "supabase" }
|
|
2055
|
-
};
|
|
2056
|
-
|
|
2057
|
-
// dist/commands/admin-fetch-versions.js
|
|
2058
|
-
async function adminFetchVersionsCommand() {
|
|
2059
|
-
const { supabase } = await getAuthenticatedClient();
|
|
2060
|
-
const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
|
|
2061
|
-
if (error) {
|
|
2062
|
-
console.error(chalk17.red(`Failed to fetch tools: ${error.message}`));
|
|
2063
|
-
process.exit(1);
|
|
2064
|
-
}
|
|
2065
|
-
if (!tools?.length) {
|
|
2066
|
-
console.log(chalk17.yellow("No tools in the registry."));
|
|
2067
|
-
}
|
|
2068
|
-
const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
|
|
2069
|
-
const registeredNames = new Set((tools ?? []).map((t) => t.name));
|
|
2070
|
-
const unregisteredNames = /* @__PURE__ */ new Set();
|
|
2071
|
-
for (const pt of allProjectToolings ?? []) {
|
|
2072
|
-
if (!registeredNames.has(pt.tool_name) && pt.detection_source === "package.json") {
|
|
2073
|
-
unregisteredNames.add(pt.tool_name);
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
if (unregisteredNames.size > 0) {
|
|
2077
|
-
console.log(chalk17.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
|
|
2078
|
-
`));
|
|
2079
|
-
for (const name of unregisteredNames) {
|
|
2080
|
-
const displayName = name;
|
|
2081
|
-
const { data: inserted, error: insertError } = await supabase.from("tools_registry").upsert({
|
|
2082
|
-
name,
|
|
2083
|
-
display_name: displayName,
|
|
2084
|
-
category: "package",
|
|
2085
|
-
source_url: `https://www.npmjs.com/package/${name}`,
|
|
2086
|
-
install_url: `https://www.npmjs.com/package/${name}`
|
|
2087
|
-
}, { onConflict: "name" }).select().single();
|
|
2088
|
-
if (!insertError && inserted) {
|
|
2089
|
-
tools.push(inserted);
|
|
2090
|
-
await supabase.from("project_toolings").update({ tool_id: inserted.id }).eq("tool_name", name).is("tool_id", null);
|
|
1504
|
+
function findProcessesForConfig(config, processes) {
|
|
1505
|
+
if (config.type === "http")
|
|
1506
|
+
return [];
|
|
1507
|
+
const packageName = config.args ? extractPackageName(config.args) : null;
|
|
1508
|
+
const matches = [];
|
|
1509
|
+
for (const proc of processes) {
|
|
1510
|
+
let matched = false;
|
|
1511
|
+
if (packageName && proc.args.includes(packageName)) {
|
|
1512
|
+
matched = true;
|
|
1513
|
+
} else if (config.command === "node" && config.args?.[0]) {
|
|
1514
|
+
if (proc.args.includes(config.args[0])) {
|
|
1515
|
+
matched = true;
|
|
2091
1516
|
}
|
|
2092
1517
|
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
|
|
2102
|
-
try {
|
|
2103
|
-
const { stable, beta } = await fetchVersions(source);
|
|
2104
|
-
const stableChanged = stable !== tool.latest_stable;
|
|
2105
|
-
const betaChanged = beta !== tool.latest_beta;
|
|
2106
|
-
const updates = {
|
|
2107
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2108
|
-
};
|
|
2109
|
-
if (stableChanged)
|
|
2110
|
-
updates.latest_stable = stable;
|
|
2111
|
-
if (betaChanged)
|
|
2112
|
-
updates.latest_beta = beta;
|
|
2113
|
-
const { error: updateError } = await supabase.from("tools_registry").update(updates).eq("id", tool.id);
|
|
2114
|
-
if (updateError) {
|
|
2115
|
-
return { displayName: tool.display_name, stable, beta, status: "failed" };
|
|
2116
|
-
}
|
|
2117
|
-
return {
|
|
2118
|
-
displayName: tool.display_name,
|
|
2119
|
-
stable,
|
|
2120
|
-
beta,
|
|
2121
|
-
status: stableChanged || betaChanged ? "updated" : "unchanged"
|
|
2122
|
-
};
|
|
2123
|
-
} catch {
|
|
2124
|
-
return { displayName: tool.display_name, stable: null, beta: null, status: "failed" };
|
|
1518
|
+
if (matched) {
|
|
1519
|
+
matches.push({
|
|
1520
|
+
pid: proc.pid,
|
|
1521
|
+
tty: proc.tty === "?" ? "" : proc.tty,
|
|
1522
|
+
uptimeSeconds: parseEtime(proc.etimeRaw),
|
|
1523
|
+
memoryMb: Math.round(proc.rss / 1024),
|
|
1524
|
+
commandLine: proc.args
|
|
1525
|
+
});
|
|
2125
1526
|
}
|
|
2126
|
-
}));
|
|
2127
|
-
const nameW = Math.max(16, ...results.map((r) => r.displayName.length));
|
|
2128
|
-
const stableW = Math.max(8, ...results.map((r) => (r.stable ?? "\u2014").length));
|
|
2129
|
-
const betaW = Math.max(8, ...results.map((r) => (r.beta ?? "\u2014").length));
|
|
2130
|
-
const statusW = 10;
|
|
2131
|
-
const header = [
|
|
2132
|
-
"Tool".padEnd(nameW),
|
|
2133
|
-
"Stable".padEnd(stableW),
|
|
2134
|
-
"Beta".padEnd(betaW),
|
|
2135
|
-
"Status".padEnd(statusW)
|
|
2136
|
-
].join(" ");
|
|
2137
|
-
console.log(chalk17.bold(header));
|
|
2138
|
-
console.log("\u2500".repeat(header.length));
|
|
2139
|
-
for (const result of results) {
|
|
2140
|
-
const statusColour = result.status === "updated" ? chalk17.green : result.status === "unchanged" ? chalk17.grey : result.status === "failed" ? chalk17.red : chalk17.yellow;
|
|
2141
|
-
const row = [
|
|
2142
|
-
result.displayName.padEnd(nameW),
|
|
2143
|
-
(result.stable ?? "\u2014").padEnd(stableW),
|
|
2144
|
-
(result.beta ?? "\u2014").padEnd(betaW),
|
|
2145
|
-
statusColour(result.status.padEnd(statusW))
|
|
2146
|
-
].join(" ");
|
|
2147
|
-
console.log(row);
|
|
2148
1527
|
}
|
|
2149
|
-
|
|
2150
|
-
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
2151
|
-
const failed = results.filter((r) => r.status === "failed").length;
|
|
2152
|
-
const noSource = results.filter((r) => r.status === "no source").length;
|
|
2153
|
-
console.log(chalk17.grey(`
|
|
2154
|
-
${results.length} tool(s): `) + chalk17.green(`${updated} updated`) + ", " + chalk17.grey(`${unchanged} unchanged`) + ", " + chalk17.red(`${failed} failed`) + ", " + chalk17.yellow(`${noSource} no source`));
|
|
1528
|
+
return matches;
|
|
2155
1529
|
}
|
|
2156
|
-
|
|
2157
|
-
const controller = new AbortController();
|
|
2158
|
-
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
1530
|
+
function getProcessTable() {
|
|
2159
1531
|
try {
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
async function fetchNpmVersions(packageName, signal) {
|
|
2170
|
-
const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`;
|
|
2171
|
-
const res = await fetch(url, { signal });
|
|
2172
|
-
if (!res.ok) {
|
|
2173
|
-
throw new Error(`npm registry returned ${res.status}`);
|
|
1532
|
+
const output = execFileSync4("ps", ["-eo", "pid,tty,etime,rss,args"], {
|
|
1533
|
+
encoding: "utf-8",
|
|
1534
|
+
timeout: 5e3
|
|
1535
|
+
});
|
|
1536
|
+
return parsePsOutput(output);
|
|
1537
|
+
} catch {
|
|
1538
|
+
return [];
|
|
2174
1539
|
}
|
|
2175
|
-
const tags = await res.json();
|
|
2176
|
-
const stable = tags.latest ?? null;
|
|
2177
|
-
const beta = tags.next ?? tags.beta ?? tags.rc ?? null;
|
|
2178
|
-
return { stable, beta };
|
|
2179
1540
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
headers: { Accept: "application/vnd.github+json" }
|
|
2185
|
-
});
|
|
2186
|
-
if (!res.ok) {
|
|
2187
|
-
throw new Error(`GitHub API returned ${res.status}`);
|
|
2188
|
-
}
|
|
2189
|
-
const releases = await res.json();
|
|
2190
|
-
let stable = null;
|
|
2191
|
-
let beta = null;
|
|
2192
|
-
for (const release of releases) {
|
|
2193
|
-
if (release.draft)
|
|
2194
|
-
continue;
|
|
2195
|
-
const version = release.tag_name.replace(/^v/, "");
|
|
2196
|
-
if (!release.prerelease && !stable) {
|
|
2197
|
-
stable = version;
|
|
2198
|
-
}
|
|
2199
|
-
if (release.prerelease && !beta) {
|
|
2200
|
-
beta = version;
|
|
2201
|
-
}
|
|
2202
|
-
if (stable && beta)
|
|
2203
|
-
break;
|
|
1541
|
+
var init_scan_processes = __esm({
|
|
1542
|
+
"dist/mcp/scan-processes.js"() {
|
|
1543
|
+
"use strict";
|
|
1544
|
+
init_read_configs();
|
|
2204
1545
|
}
|
|
2205
|
-
|
|
2206
|
-
}
|
|
1546
|
+
});
|
|
2207
1547
|
|
|
2208
1548
|
// dist/commands/mcp-watch.js
|
|
1549
|
+
var mcp_watch_exports = {};
|
|
1550
|
+
__export(mcp_watch_exports, {
|
|
1551
|
+
mcpWatchCommand: () => mcpWatchCommand
|
|
1552
|
+
});
|
|
2209
1553
|
import chalk18 from "chalk";
|
|
2210
1554
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
import { readFile as readFile8 } from "node:fs/promises";
|
|
2214
|
-
import { join as join12 } from "node:path";
|
|
2215
|
-
import { homedir as homedir7 } from "node:os";
|
|
2216
|
-
import { existsSync as existsSync11 } from "node:fs";
|
|
2217
|
-
import { readdir as readdir3 } from "node:fs/promises";
|
|
2218
|
-
async function readJsonSafe(path) {
|
|
1555
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1556
|
+
function detectTty() {
|
|
2219
1557
|
try {
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
return null;
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
function extractPackageName(args) {
|
|
2229
|
-
for (const arg of args) {
|
|
2230
|
-
if (arg.startsWith("-"))
|
|
2231
|
-
continue;
|
|
2232
|
-
if (arg.includes("mcp") || arg.startsWith("@"))
|
|
2233
|
-
return arg;
|
|
2234
|
-
}
|
|
2235
|
-
for (const arg of args) {
|
|
2236
|
-
if (!arg.startsWith("-"))
|
|
2237
|
-
return arg;
|
|
2238
|
-
}
|
|
2239
|
-
return null;
|
|
2240
|
-
}
|
|
2241
|
-
function parseServers(data, source) {
|
|
2242
|
-
if (!data?.mcpServers)
|
|
2243
|
-
return [];
|
|
2244
|
-
const entries = [];
|
|
2245
|
-
for (const [name, server] of Object.entries(data.mcpServers)) {
|
|
2246
|
-
const serverType = server.type === "http" ? "http" : "stdio";
|
|
2247
|
-
entries.push({
|
|
2248
|
-
name,
|
|
2249
|
-
source,
|
|
2250
|
-
type: serverType,
|
|
2251
|
-
command: server.command,
|
|
2252
|
-
args: server.args,
|
|
2253
|
-
env: server.env,
|
|
2254
|
-
url: server.url
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2257
|
-
return entries;
|
|
2258
|
-
}
|
|
2259
|
-
function parseFlatConfig(data, source) {
|
|
2260
|
-
if (!data)
|
|
2261
|
-
return [];
|
|
2262
|
-
const entries = [];
|
|
2263
|
-
for (const [name, server] of Object.entries(data)) {
|
|
2264
|
-
if (typeof server !== "object" || server === null)
|
|
2265
|
-
continue;
|
|
2266
|
-
if (!server.command && !server.url)
|
|
2267
|
-
continue;
|
|
2268
|
-
const serverType = server.type === "http" ? "http" : "stdio";
|
|
2269
|
-
entries.push({
|
|
2270
|
-
name,
|
|
2271
|
-
source,
|
|
2272
|
-
type: serverType,
|
|
2273
|
-
command: server.command,
|
|
2274
|
-
args: server.args,
|
|
2275
|
-
env: server.env,
|
|
2276
|
-
url: server.url
|
|
2277
|
-
});
|
|
2278
|
-
}
|
|
2279
|
-
return entries;
|
|
2280
|
-
}
|
|
2281
|
-
async function readAllMcpConfigs() {
|
|
2282
|
-
const home = homedir7();
|
|
2283
|
-
const entries = [];
|
|
2284
|
-
const userConfig = await readJsonSafe(join12(home, ".claude.json"));
|
|
2285
|
-
entries.push(...parseServers(userConfig, "global"));
|
|
2286
|
-
const globalMcp = await readJsonSafe(join12(home, ".claude", "mcp.json"));
|
|
2287
|
-
entries.push(...parseServers(globalMcp, "global"));
|
|
2288
|
-
const cwdMcp = await readJsonSafe(join12(process.cwd(), ".mcp.json"));
|
|
2289
|
-
entries.push(...parseServers(cwdMcp, "project"));
|
|
2290
|
-
const pluginsBase = join12(home, ".claude", "plugins", "marketplaces");
|
|
2291
|
-
if (existsSync11(pluginsBase)) {
|
|
2292
|
-
try {
|
|
2293
|
-
const marketplaces = await readdir3(pluginsBase, { withFileTypes: true });
|
|
2294
|
-
for (const mp of marketplaces) {
|
|
2295
|
-
if (!mp.isDirectory())
|
|
2296
|
-
continue;
|
|
2297
|
-
const extDir = join12(pluginsBase, mp.name, "external_plugins");
|
|
2298
|
-
if (!existsSync11(extDir))
|
|
2299
|
-
continue;
|
|
2300
|
-
const plugins = await readdir3(extDir, { withFileTypes: true });
|
|
2301
|
-
for (const plugin of plugins) {
|
|
2302
|
-
if (!plugin.isDirectory())
|
|
2303
|
-
continue;
|
|
2304
|
-
const pluginMcp = await readJsonSafe(join12(extDir, plugin.name, ".mcp.json"));
|
|
2305
|
-
entries.push(...parseServers(pluginMcp, "plugin"));
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
} catch {
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
const cacheBase = join12(home, ".claude", "plugins", "cache");
|
|
2312
|
-
if (existsSync11(cacheBase)) {
|
|
2313
|
-
try {
|
|
2314
|
-
const registries = await readdir3(cacheBase, { withFileTypes: true });
|
|
2315
|
-
for (const reg of registries) {
|
|
2316
|
-
if (!reg.isDirectory())
|
|
2317
|
-
continue;
|
|
2318
|
-
const regDir = join12(cacheBase, reg.name);
|
|
2319
|
-
const plugins = await readdir3(regDir, { withFileTypes: true });
|
|
2320
|
-
for (const plugin of plugins) {
|
|
2321
|
-
if (!plugin.isDirectory())
|
|
2322
|
-
continue;
|
|
2323
|
-
const versionDirs = await readdir3(join12(regDir, plugin.name), { withFileTypes: true });
|
|
2324
|
-
for (const ver of versionDirs) {
|
|
2325
|
-
if (!ver.isDirectory())
|
|
2326
|
-
continue;
|
|
2327
|
-
const mcpPath = join12(regDir, plugin.name, ver.name, ".mcp.json");
|
|
2328
|
-
const flatData = await readJsonSafe(mcpPath);
|
|
2329
|
-
if (flatData) {
|
|
2330
|
-
entries.push(...parseFlatConfig(flatData, "plugin"));
|
|
2331
|
-
break;
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
} catch {
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2340
|
-
const deduped = [];
|
|
2341
|
-
for (const entry of entries) {
|
|
2342
|
-
if (seen.has(entry.name))
|
|
2343
|
-
continue;
|
|
2344
|
-
seen.add(entry.name);
|
|
2345
|
-
deduped.push(entry);
|
|
2346
|
-
}
|
|
2347
|
-
return deduped;
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
// dist/mcp/scan-processes.js
|
|
2351
|
-
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
2352
|
-
function parsePsOutput(output) {
|
|
2353
|
-
const lines = output.trim().split("\n").slice(1);
|
|
2354
|
-
const entries = [];
|
|
2355
|
-
for (const line of lines) {
|
|
2356
|
-
const trimmed = line.trim();
|
|
2357
|
-
const match = trimmed.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(.+)$/);
|
|
2358
|
-
if (!match)
|
|
2359
|
-
continue;
|
|
2360
|
-
entries.push({
|
|
2361
|
-
pid: parseInt(match[1], 10),
|
|
2362
|
-
tty: match[2],
|
|
2363
|
-
etimeRaw: match[3],
|
|
2364
|
-
rss: parseInt(match[4], 10),
|
|
2365
|
-
args: match[5]
|
|
2366
|
-
});
|
|
2367
|
-
}
|
|
2368
|
-
return entries;
|
|
2369
|
-
}
|
|
2370
|
-
function parseEtime(etime) {
|
|
2371
|
-
let days = 0;
|
|
2372
|
-
let rest = etime;
|
|
2373
|
-
if (rest.includes("-")) {
|
|
2374
|
-
const [d, r] = rest.split("-");
|
|
2375
|
-
days = parseInt(d, 10);
|
|
2376
|
-
rest = r;
|
|
2377
|
-
}
|
|
2378
|
-
const parts = rest.split(":").map(Number);
|
|
2379
|
-
if (parts.length === 3) {
|
|
2380
|
-
return days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
2381
|
-
} else if (parts.length === 2) {
|
|
2382
|
-
return days * 86400 + parts[0] * 60 + parts[1];
|
|
2383
|
-
}
|
|
2384
|
-
return days * 86400 + (parts[0] ?? 0);
|
|
2385
|
-
}
|
|
2386
|
-
function findProcessesForConfig(config, processes) {
|
|
2387
|
-
if (config.type === "http")
|
|
2388
|
-
return [];
|
|
2389
|
-
const packageName = config.args ? extractPackageName(config.args) : null;
|
|
2390
|
-
const matches = [];
|
|
2391
|
-
for (const proc of processes) {
|
|
2392
|
-
let matched = false;
|
|
2393
|
-
if (packageName && proc.args.includes(packageName)) {
|
|
2394
|
-
matched = true;
|
|
2395
|
-
} else if (config.command === "node" && config.args?.[0]) {
|
|
2396
|
-
if (proc.args.includes(config.args[0])) {
|
|
2397
|
-
matched = true;
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
if (matched) {
|
|
2401
|
-
matches.push({
|
|
2402
|
-
pid: proc.pid,
|
|
2403
|
-
tty: proc.tty === "?" ? "" : proc.tty,
|
|
2404
|
-
uptimeSeconds: parseEtime(proc.etimeRaw),
|
|
2405
|
-
memoryMb: Math.round(proc.rss / 1024),
|
|
2406
|
-
commandLine: proc.args
|
|
2407
|
-
});
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
return matches;
|
|
2411
|
-
}
|
|
2412
|
-
function getProcessTable() {
|
|
2413
|
-
try {
|
|
2414
|
-
const output = execFileSync4("ps", ["-eo", "pid,tty,etime,rss,args"], {
|
|
2415
|
-
encoding: "utf-8",
|
|
2416
|
-
timeout: 5e3
|
|
2417
|
-
});
|
|
2418
|
-
return parsePsOutput(output);
|
|
2419
|
-
} catch {
|
|
2420
|
-
return [];
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
// dist/commands/mcp-watch.js
|
|
2425
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
2426
|
-
var POLL_INTERVAL_MS = 3e4;
|
|
2427
|
-
var ENV_POLL_INTERVAL_MS = 3e5;
|
|
2428
|
-
function detectTty() {
|
|
2429
|
-
try {
|
|
2430
|
-
const result = execFileSync5("ps", ["-o", "tty=", "-p", String(process.pid)], {
|
|
2431
|
-
encoding: "utf-8",
|
|
2432
|
-
timeout: 3e3
|
|
2433
|
-
}).trim();
|
|
2434
|
-
return result && result !== "?" ? result : null;
|
|
1558
|
+
const result = execFileSync5("ps", ["-o", "tty=", "-p", String(process.pid)], {
|
|
1559
|
+
encoding: "utf-8",
|
|
1560
|
+
timeout: 3e3
|
|
1561
|
+
}).trim();
|
|
1562
|
+
return result && result !== "?" ? result : null;
|
|
2435
1563
|
} catch {
|
|
2436
1564
|
return null;
|
|
2437
1565
|
}
|
|
@@ -2550,101 +1678,1157 @@ function printTable(rows, deviceName) {
|
|
|
2550
1678
|
const uptime = formatUptime(s.uptime_seconds ?? 0);
|
|
2551
1679
|
console.log(` ${chalk18.green("\u25CF")} ${s.server_name.padEnd(20)} ${chalk18.dim((s.package_name ?? "").padEnd(30))} ${String(s.memory_mb ?? 0).padStart(4)} MB ${uptime}`);
|
|
2552
1680
|
}
|
|
2553
|
-
console.log("");
|
|
1681
|
+
console.log("");
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (stopped.length > 0 || errored.length > 0) {
|
|
1685
|
+
const notRunning = [...stopped, ...errored];
|
|
1686
|
+
console.log(chalk18.yellow(` Not Running (${notRunning.length})`));
|
|
1687
|
+
for (const s of notRunning) {
|
|
1688
|
+
const icon = s.status === "error" ? chalk18.red("\u2717") : chalk18.yellow("\u25CB");
|
|
1689
|
+
const detail = s.error_detail ? chalk18.dim(` \u2014 ${s.error_detail}`) : "";
|
|
1690
|
+
console.log(` ${icon} ${s.server_name.padEnd(20)} ${chalk18.dim(s.config_source.padEnd(10))}${detail}`);
|
|
1691
|
+
}
|
|
1692
|
+
console.log("");
|
|
1693
|
+
}
|
|
1694
|
+
if (rows.length === 0) {
|
|
1695
|
+
console.log(chalk18.yellow(" No MCP servers configured."));
|
|
1696
|
+
console.log(chalk18.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function formatUptime(seconds) {
|
|
1700
|
+
if (seconds < 60)
|
|
1701
|
+
return `${seconds}s`;
|
|
1702
|
+
if (seconds < 3600)
|
|
1703
|
+
return `${Math.floor(seconds / 60)}m`;
|
|
1704
|
+
const h = Math.floor(seconds / 3600);
|
|
1705
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
1706
|
+
return `${h}h ${m}m`;
|
|
1707
|
+
}
|
|
1708
|
+
async function mcpWatchCommand() {
|
|
1709
|
+
const { supabase, userId } = await getAuthenticatedClient();
|
|
1710
|
+
const deviceId = await resolveDeviceId(supabase, userId);
|
|
1711
|
+
const deviceName = detectDeviceName();
|
|
1712
|
+
const myPid = process.pid;
|
|
1713
|
+
const myTty = detectTty();
|
|
1714
|
+
console.log(chalk18.blue(`Starting MCP monitor for ${deviceName}...`));
|
|
1715
|
+
await supabase.from("mcp_watchers").upsert({
|
|
1716
|
+
device_id: deviceId,
|
|
1717
|
+
pid: myPid,
|
|
1718
|
+
tty: myTty,
|
|
1719
|
+
cli_version: CURRENT_VERSION,
|
|
1720
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1721
|
+
last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
|
|
1722
|
+
}, { onConflict: "device_id,pid" });
|
|
1723
|
+
const staleThreshold = new Date(Date.now() - 12e4).toISOString();
|
|
1724
|
+
await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).lt("last_heartbeat", staleThreshold);
|
|
1725
|
+
async function cycle() {
|
|
1726
|
+
const configs = await readAllMcpConfigs();
|
|
1727
|
+
const rows = buildRows(configs);
|
|
1728
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1729
|
+
await supabase.from("mcp_server_status").delete().eq("device_id", deviceId);
|
|
1730
|
+
if (rows.length > 0) {
|
|
1731
|
+
await supabase.from("mcp_server_status").insert(rows.map((row) => ({
|
|
1732
|
+
device_id: deviceId,
|
|
1733
|
+
...row,
|
|
1734
|
+
checked_at: now
|
|
1735
|
+
})));
|
|
1736
|
+
}
|
|
1737
|
+
await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
|
|
1738
|
+
printTable(rows, deviceName);
|
|
1739
|
+
}
|
|
1740
|
+
await cycle();
|
|
1741
|
+
let lastEnvHash = "";
|
|
1742
|
+
async function envCycle() {
|
|
1743
|
+
const state = await loadState();
|
|
1744
|
+
if (!state.lastFolderId)
|
|
1745
|
+
return;
|
|
1746
|
+
const { data: dp } = await supabase.from("device_paths").select("path").eq("folder_id", state.lastFolderId).eq("device_name", deviceName).maybeSingle();
|
|
1747
|
+
if (!dp?.path)
|
|
1748
|
+
return;
|
|
1749
|
+
const envManifest = await scanEnvManifest(dp.path);
|
|
1750
|
+
if (!envManifest)
|
|
1751
|
+
return;
|
|
1752
|
+
const hash = createHash2("sha256").update(JSON.stringify(envManifest)).digest("hex");
|
|
1753
|
+
if (hash === lastEnvHash)
|
|
1754
|
+
return;
|
|
1755
|
+
lastEnvHash = hash;
|
|
1756
|
+
await supabase.from("claude_folders").update({ env_manifest_json: envManifest }).eq("id", state.lastFolderId);
|
|
1757
|
+
}
|
|
1758
|
+
await envCycle();
|
|
1759
|
+
const envInterval = setInterval(envCycle, ENV_POLL_INTERVAL_MS);
|
|
1760
|
+
const interval = setInterval(cycle, POLL_INTERVAL_MS);
|
|
1761
|
+
const shutdown = async () => {
|
|
1762
|
+
clearInterval(interval);
|
|
1763
|
+
clearInterval(envInterval);
|
|
1764
|
+
await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).eq("pid", myPid);
|
|
1765
|
+
console.log(chalk18.dim("\nMCP monitor stopped."));
|
|
1766
|
+
process.exit(0);
|
|
1767
|
+
};
|
|
1768
|
+
process.on("SIGINT", () => {
|
|
1769
|
+
void shutdown();
|
|
1770
|
+
});
|
|
1771
|
+
process.on("SIGTERM", () => {
|
|
1772
|
+
void shutdown();
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
var POLL_INTERVAL_MS, ENV_POLL_INTERVAL_MS;
|
|
1776
|
+
var init_mcp_watch = __esm({
|
|
1777
|
+
"dist/commands/mcp-watch.js"() {
|
|
1778
|
+
"use strict";
|
|
1779
|
+
init_auth();
|
|
1780
|
+
init_device_utils();
|
|
1781
|
+
init_read_configs();
|
|
1782
|
+
init_scan_processes();
|
|
1783
|
+
init_check_update();
|
|
1784
|
+
init_env_manifest_scanner();
|
|
1785
|
+
init_config();
|
|
1786
|
+
POLL_INTERVAL_MS = 3e4;
|
|
1787
|
+
ENV_POLL_INTERVAL_MS = 3e5;
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// dist/index.js
|
|
1792
|
+
import { Command } from "commander";
|
|
1793
|
+
|
|
1794
|
+
// dist/commands/login.js
|
|
1795
|
+
init_dist();
|
|
1796
|
+
init_config();
|
|
1797
|
+
import chalk from "chalk";
|
|
1798
|
+
import { input, password } from "@inquirer/prompts";
|
|
1799
|
+
async function loginCommand() {
|
|
1800
|
+
console.log(chalk.blue("MD4AI Login\n"));
|
|
1801
|
+
const email = await input({ message: "Email:" });
|
|
1802
|
+
const pwd = await password({ message: "Password:" });
|
|
1803
|
+
const anonKey = getAnonKey();
|
|
1804
|
+
const supabase = createSupabaseClient(anonKey);
|
|
1805
|
+
const { data, error } = await supabase.auth.signInWithPassword({
|
|
1806
|
+
email,
|
|
1807
|
+
password: pwd
|
|
1808
|
+
});
|
|
1809
|
+
if (error) {
|
|
1810
|
+
console.error(chalk.red(`Login failed: ${error.message}`));
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
}
|
|
1813
|
+
await saveCredentials({
|
|
1814
|
+
accessToken: data.session.access_token,
|
|
1815
|
+
refreshToken: data.session.refresh_token,
|
|
1816
|
+
expiresAt: Date.now() + data.session.expires_in * 1e3,
|
|
1817
|
+
userId: data.user.id,
|
|
1818
|
+
email: data.user.email ?? email
|
|
1819
|
+
});
|
|
1820
|
+
console.log(chalk.green(`
|
|
1821
|
+
Logged in as ${data.user.email}`));
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// dist/commands/logout.js
|
|
1825
|
+
init_config();
|
|
1826
|
+
import chalk2 from "chalk";
|
|
1827
|
+
async function logoutCommand() {
|
|
1828
|
+
await clearCredentials();
|
|
1829
|
+
console.log(chalk2.green("Logged out successfully."));
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// dist/commands/status.js
|
|
1833
|
+
init_dist();
|
|
1834
|
+
init_config();
|
|
1835
|
+
import chalk3 from "chalk";
|
|
1836
|
+
async function statusCommand() {
|
|
1837
|
+
const creds = await loadCredentials();
|
|
1838
|
+
if (!creds?.accessToken) {
|
|
1839
|
+
console.log(chalk3.yellow("Not logged in. Run: md4ai login"));
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
console.log(chalk3.blue("MD4AI Status\n"));
|
|
1843
|
+
console.log(` User: ${creds.email}`);
|
|
1844
|
+
console.log(` Expires: ${new Date(creds.expiresAt).toLocaleString()}`);
|
|
1845
|
+
try {
|
|
1846
|
+
const anonKey = getAnonKey();
|
|
1847
|
+
const supabase = createSupabaseClient(anonKey, creds.accessToken);
|
|
1848
|
+
const { count: folderCount } = await supabase.from("claude_folders").select("*", { count: "exact", head: true });
|
|
1849
|
+
const { count: deviceCount } = await supabase.from("device_paths").select("*", { count: "exact", head: true });
|
|
1850
|
+
console.log(` Folders: ${folderCount ?? 0}`);
|
|
1851
|
+
console.log(` Devices: ${deviceCount ?? 0}`);
|
|
1852
|
+
const state = await loadState();
|
|
1853
|
+
console.log(` Last sync: ${state.lastSyncAt ?? "never"}`);
|
|
1854
|
+
} catch {
|
|
1855
|
+
console.log(chalk3.yellow(" (Could not fetch remote data)"));
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// dist/commands/add-folder.js
|
|
1860
|
+
init_auth();
|
|
1861
|
+
import chalk5 from "chalk";
|
|
1862
|
+
import { input as input2, select } from "@inquirer/prompts";
|
|
1863
|
+
async function addFolderCommand() {
|
|
1864
|
+
const { supabase, userId } = await getAuthenticatedClient();
|
|
1865
|
+
const name = await input2({ message: "Folder name:" });
|
|
1866
|
+
const description = await input2({ message: "Description (optional):" });
|
|
1867
|
+
const { data: teams } = await supabase.from("team_members").select("team_id, teams(id, name)").eq("user_id", userId);
|
|
1868
|
+
const { data: ownedTeams } = await supabase.from("teams").select("id, name").eq("created_by", userId);
|
|
1869
|
+
const allTeams = /* @__PURE__ */ new Map();
|
|
1870
|
+
for (const t of ownedTeams ?? []) {
|
|
1871
|
+
allTeams.set(t.id, t.name);
|
|
1872
|
+
}
|
|
1873
|
+
for (const t of teams ?? []) {
|
|
1874
|
+
const team = t.teams;
|
|
1875
|
+
if (team)
|
|
1876
|
+
allTeams.set(team.id, team.name);
|
|
1877
|
+
}
|
|
1878
|
+
let teamId = null;
|
|
1879
|
+
if (allTeams.size > 0) {
|
|
1880
|
+
const choices = [
|
|
1881
|
+
{ name: "Personal (only you)", value: "__personal__" },
|
|
1882
|
+
...Array.from(allTeams.entries()).map(([id, teamName]) => ({
|
|
1883
|
+
name: teamName,
|
|
1884
|
+
value: id
|
|
1885
|
+
}))
|
|
1886
|
+
];
|
|
1887
|
+
const selected = await select({
|
|
1888
|
+
message: "Assign to a team?",
|
|
1889
|
+
choices
|
|
1890
|
+
});
|
|
1891
|
+
teamId = selected === "__personal__" ? null : selected;
|
|
1892
|
+
}
|
|
1893
|
+
const { data, error } = await supabase.from("claude_folders").insert({
|
|
1894
|
+
user_id: userId,
|
|
1895
|
+
name,
|
|
1896
|
+
description: description || null,
|
|
1897
|
+
team_id: teamId
|
|
1898
|
+
}).select().single();
|
|
1899
|
+
if (error) {
|
|
1900
|
+
console.error(chalk5.red(`Failed to create folder: ${error.message}`));
|
|
1901
|
+
process.exit(1);
|
|
1902
|
+
}
|
|
1903
|
+
const teamLabel = teamId ? `(team: ${allTeams.get(teamId)})` : "(personal)";
|
|
1904
|
+
console.log(chalk5.green(`
|
|
1905
|
+
Folder "${name}" created ${teamLabel} (${data.id})`));
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// dist/commands/add-device.js
|
|
1909
|
+
init_auth();
|
|
1910
|
+
import { resolve } from "node:path";
|
|
1911
|
+
import { hostname, platform } from "node:os";
|
|
1912
|
+
import chalk6 from "chalk";
|
|
1913
|
+
import { input as input3, select as select2 } from "@inquirer/prompts";
|
|
1914
|
+
function detectOs() {
|
|
1915
|
+
const p = platform();
|
|
1916
|
+
if (p === "win32")
|
|
1917
|
+
return "windows";
|
|
1918
|
+
if (p === "darwin")
|
|
1919
|
+
return "macos";
|
|
1920
|
+
if (p === "linux")
|
|
1921
|
+
return "linux";
|
|
1922
|
+
return "other";
|
|
1923
|
+
}
|
|
1924
|
+
function suggestDeviceName() {
|
|
1925
|
+
const os = detectOs();
|
|
1926
|
+
const host = hostname().split(".")[0];
|
|
1927
|
+
const osLabel = os.charAt(0).toUpperCase() + os.slice(1);
|
|
1928
|
+
return `${host}-${osLabel}`;
|
|
1929
|
+
}
|
|
1930
|
+
async function addDeviceCommand() {
|
|
1931
|
+
const { supabase, userId } = await getAuthenticatedClient();
|
|
1932
|
+
const cwd = resolve(process.cwd());
|
|
1933
|
+
console.log(chalk6.blue.bold("\n\u{1F4C2} Where is this Claude project?\n"));
|
|
1934
|
+
const localPath = await input3({
|
|
1935
|
+
message: "Local project path:",
|
|
1936
|
+
default: cwd
|
|
1937
|
+
});
|
|
1938
|
+
const { data: folders, error: foldersErr } = await supabase.from("claude_folders").select("id, name").order("name");
|
|
1939
|
+
if (foldersErr || !folders?.length) {
|
|
1940
|
+
console.error(chalk6.red("No folders found. Run: md4ai add-folder"));
|
|
1941
|
+
process.exit(1);
|
|
1942
|
+
}
|
|
1943
|
+
const folderId = await select2({
|
|
1944
|
+
message: "Select folder:",
|
|
1945
|
+
choices: folders.map((f) => ({ name: f.name, value: f.id }))
|
|
1946
|
+
});
|
|
1947
|
+
const suggested = suggestDeviceName();
|
|
1948
|
+
const deviceName = await input3({
|
|
1949
|
+
message: "Device name:",
|
|
1950
|
+
default: suggested
|
|
1951
|
+
});
|
|
1952
|
+
const osType = await select2({
|
|
1953
|
+
message: "OS type:",
|
|
1954
|
+
choices: [
|
|
1955
|
+
{ name: "Windows", value: "windows" },
|
|
1956
|
+
{ name: "macOS", value: "macos" },
|
|
1957
|
+
{ name: "Ubuntu", value: "ubuntu" },
|
|
1958
|
+
{ name: "Linux", value: "linux" },
|
|
1959
|
+
{ name: "Other", value: "other" }
|
|
1960
|
+
],
|
|
1961
|
+
default: detectOs()
|
|
1962
|
+
});
|
|
1963
|
+
const description = await input3({ message: "Description (optional):" });
|
|
1964
|
+
const { error } = await supabase.from("device_paths").insert({
|
|
1965
|
+
user_id: userId,
|
|
1966
|
+
folder_id: folderId,
|
|
1967
|
+
device_name: deviceName,
|
|
1968
|
+
os_type: osType,
|
|
1969
|
+
path: localPath,
|
|
1970
|
+
description: description || null
|
|
1971
|
+
});
|
|
1972
|
+
if (error) {
|
|
1973
|
+
console.error(chalk6.red(`Failed to add device: ${error.message}`));
|
|
1974
|
+
process.exit(1);
|
|
1975
|
+
}
|
|
1976
|
+
console.log(chalk6.green(`
|
|
1977
|
+
Device "${deviceName}" added to folder.`));
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// dist/commands/list-devices.js
|
|
1981
|
+
init_auth();
|
|
1982
|
+
import chalk7 from "chalk";
|
|
1983
|
+
async function listDevicesCommand() {
|
|
1984
|
+
const { supabase } = await getAuthenticatedClient();
|
|
1985
|
+
const { data: devices, error } = await supabase.from("device_paths").select(`
|
|
1986
|
+
id, device_name, os_type, path, last_synced, description,
|
|
1987
|
+
claude_folders!inner ( name )
|
|
1988
|
+
`).order("device_name");
|
|
1989
|
+
if (error) {
|
|
1990
|
+
console.error(chalk7.red(`Failed to list devices: ${error.message}`));
|
|
1991
|
+
process.exit(1);
|
|
1992
|
+
}
|
|
1993
|
+
if (!devices?.length) {
|
|
1994
|
+
console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1998
|
+
for (const d of devices) {
|
|
1999
|
+
const list = grouped.get(d.device_name) ?? [];
|
|
2000
|
+
list.push(d);
|
|
2001
|
+
grouped.set(d.device_name, list);
|
|
2002
|
+
}
|
|
2003
|
+
for (const [deviceName, entries] of grouped) {
|
|
2004
|
+
const first = entries[0];
|
|
2005
|
+
console.log(chalk7.bold(`
|
|
2006
|
+
${deviceName}`) + chalk7.dim(` (${first.os_type})`));
|
|
2007
|
+
for (const entry of entries) {
|
|
2008
|
+
const folderName = entry.claude_folders?.name ?? "unknown";
|
|
2009
|
+
const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
|
|
2010
|
+
console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
|
|
2011
|
+
console.log(` Last synced: ${synced}`);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// dist/commands/map.js
|
|
2017
|
+
init_scanner();
|
|
2018
|
+
init_auth();
|
|
2019
|
+
init_config();
|
|
2020
|
+
import { resolve as resolve3 } from "node:path";
|
|
2021
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
2022
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
2023
|
+
import chalk9 from "chalk";
|
|
2024
|
+
|
|
2025
|
+
// dist/output/html-generator.js
|
|
2026
|
+
function generateOfflineHtml(result, projectRoot) {
|
|
2027
|
+
const title = projectRoot.split("/").pop() ?? "MD4AI";
|
|
2028
|
+
const dataJson = JSON.stringify(result);
|
|
2029
|
+
const orphanItems = result.orphans.map((o) => `<li data-path="${escapeHtml(o.path)}">${escapeHtml(o.path)} <span class="meta">(${o.sizeBytes} bytes)</span></li>`).join("\n");
|
|
2030
|
+
const staleItems = result.staleFiles.map((s) => `<li data-path="${escapeHtml(s.path)}">${escapeHtml(s.path)} <span class="stale-meta">(${s.daysSinceModified} days)</span></li>`).join("\n");
|
|
2031
|
+
const skillRows = result.skills.map((s) => `<tr><td>${escapeHtml(s.name)}</td><td>${s.machineWide ? '<span class="check">\u2713</span>' : ""}</td><td>${s.projectSpecific ? '<span class="check">\u2713</span>' : ""}</td><td class="meta">${escapeHtml(s.source)}</td></tr>`).join("\n");
|
|
2032
|
+
const fileItems = result.graph.nodes.map((n) => `<li data-path="${escapeHtml(n.filePath)}" data-type="${n.type}">${escapeHtml(n.filePath)} <span class="meta">(${n.type})</span></li>`).join("\n");
|
|
2033
|
+
return `<!DOCTYPE html>
|
|
2034
|
+
<html lang="en">
|
|
2035
|
+
<head>
|
|
2036
|
+
<meta charset="UTF-8">
|
|
2037
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2038
|
+
<title>MD4AI \u2014 ${escapeHtml(title)}</title>
|
|
2039
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
2040
|
+
<style>
|
|
2041
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2042
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
|
2043
|
+
h1 { color: #38bdf8; margin-bottom: 0.5rem; }
|
|
2044
|
+
.subtitle { color: #94a3b8; margin-bottom: 2rem; }
|
|
2045
|
+
.section { background: #1e293b; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
|
|
2046
|
+
.section h2 { color: #38bdf8; margin-bottom: 1rem; font-size: 1.2rem; }
|
|
2047
|
+
.mermaid { background: #0f172a; border-radius: 8px; padding: 1rem; overflow-x: auto; }
|
|
2048
|
+
.badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; margin-left: 0.5rem; }
|
|
2049
|
+
.badge-orphan { background: #ef4444; color: white; }
|
|
2050
|
+
.badge-stale { background: #f59e0b; color: black; }
|
|
2051
|
+
table { width: 100%; border-collapse: collapse; }
|
|
2052
|
+
th, td { padding: 0.5rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
|
|
2053
|
+
th { color: #94a3b8; font-weight: 600; }
|
|
2054
|
+
.check { color: #22c55e; }
|
|
2055
|
+
.meta { color: #94a3b8; }
|
|
2056
|
+
.stale-meta { color: #f59e0b; }
|
|
2057
|
+
.stats { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
2058
|
+
.stat { text-align: center; }
|
|
2059
|
+
.stat-value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
|
|
2060
|
+
.stat-label { color: #94a3b8; font-size: 0.85rem; }
|
|
2061
|
+
.file-list { list-style: none; }
|
|
2062
|
+
.file-list li { padding: 0.3rem 0; color: #cbd5e1; font-family: monospace; font-size: 0.9rem; }
|
|
2063
|
+
#search { width: 100%; padding: 0.75rem 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 1rem; margin-bottom: 1rem; }
|
|
2064
|
+
#search:focus { outline: none; border-color: #38bdf8; }
|
|
2065
|
+
@media print { body { background: white; color: black; } .section { border: 1px solid #ccc; } }
|
|
2066
|
+
</style>
|
|
2067
|
+
</head>
|
|
2068
|
+
<body>
|
|
2069
|
+
<h1>MD4AI \u2014 ${escapeHtml(title)}</h1>
|
|
2070
|
+
<p class="subtitle">Scanned: ${new Date(result.scannedAt).toLocaleString()} | Hash: ${result.dataHash.slice(0, 12)}</p>
|
|
2071
|
+
|
|
2072
|
+
<div class="stats">
|
|
2073
|
+
<div class="stat"><div class="stat-value">${result.graph.nodes.length}</div><div class="stat-label">Files</div></div>
|
|
2074
|
+
<div class="stat"><div class="stat-value">${result.graph.edges.length}</div><div class="stat-label">References</div></div>
|
|
2075
|
+
<div class="stat"><div class="stat-value">${result.orphans.length}</div><div class="stat-label">Orphans</div></div>
|
|
2076
|
+
<div class="stat"><div class="stat-value">${result.staleFiles.length}</div><div class="stat-label">Stale</div></div>
|
|
2077
|
+
<div class="stat"><div class="stat-value">${result.skills.length}</div><div class="stat-label">Skills</div></div>
|
|
2078
|
+
</div>
|
|
2079
|
+
|
|
2080
|
+
<input type="text" id="search" placeholder="Search files..." oninput="filterFiles(this.value)">
|
|
2081
|
+
|
|
2082
|
+
<div class="section">
|
|
2083
|
+
<h2>Dependency Graph</h2>
|
|
2084
|
+
<pre class="mermaid">${escapeHtml(result.graph.mermaid)}</pre>
|
|
2085
|
+
</div>
|
|
2086
|
+
|
|
2087
|
+
${result.orphans.length > 0 ? `
|
|
2088
|
+
<div class="section">
|
|
2089
|
+
<h2>Orphan Files <span class="badge badge-orphan">${result.orphans.length}</span></h2>
|
|
2090
|
+
<p class="meta" style="margin-bottom: 1rem;">Files not reachable from any root configuration file.</p>
|
|
2091
|
+
<ul class="file-list" id="orphan-list">${orphanItems}</ul>
|
|
2092
|
+
</div>
|
|
2093
|
+
` : ""}
|
|
2094
|
+
|
|
2095
|
+
${result.staleFiles.length > 0 ? `
|
|
2096
|
+
<div class="section">
|
|
2097
|
+
<h2>Stale Files <span class="badge badge-stale">${result.staleFiles.length}</span></h2>
|
|
2098
|
+
<p class="meta" style="margin-bottom: 1rem;">Files not modified in over 90 days.</p>
|
|
2099
|
+
<ul class="file-list" id="stale-list">${staleItems}</ul>
|
|
2100
|
+
</div>
|
|
2101
|
+
` : ""}
|
|
2102
|
+
|
|
2103
|
+
<div class="section">
|
|
2104
|
+
<h2>Skills Comparison</h2>
|
|
2105
|
+
<table>
|
|
2106
|
+
<thead><tr><th>Skill/Plugin</th><th>Entire Machine</th><th>Project Specific</th><th>Source</th></tr></thead>
|
|
2107
|
+
<tbody>${skillRows}</tbody>
|
|
2108
|
+
</table>
|
|
2109
|
+
</div>
|
|
2110
|
+
|
|
2111
|
+
<div class="section">
|
|
2112
|
+
<h2>All Files</h2>
|
|
2113
|
+
<ul class="file-list" id="all-files">${fileItems}</ul>
|
|
2114
|
+
</div>
|
|
2115
|
+
|
|
2116
|
+
<script>
|
|
2117
|
+
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
|
|
2118
|
+
|
|
2119
|
+
function filterFiles(query) {
|
|
2120
|
+
var q = query.toLowerCase();
|
|
2121
|
+
document.querySelectorAll('.file-list li').forEach(function(li) {
|
|
2122
|
+
var path = li.getAttribute('data-path') || '';
|
|
2123
|
+
li.style.display = path.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
</script>
|
|
2127
|
+
|
|
2128
|
+
<script type="application/json" id="scan-data">${dataJson}</script>
|
|
2129
|
+
</body>
|
|
2130
|
+
</html>`;
|
|
2131
|
+
}
|
|
2132
|
+
function escapeHtml(text) {
|
|
2133
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// dist/commands/map.js
|
|
2137
|
+
init_check_update();
|
|
2138
|
+
init_push_toolings();
|
|
2139
|
+
async function mapCommand(path, options) {
|
|
2140
|
+
await checkForUpdate();
|
|
2141
|
+
const projectRoot = resolve3(path ?? process.cwd());
|
|
2142
|
+
if (!existsSync7(projectRoot)) {
|
|
2143
|
+
console.error(chalk9.red(`Path not found: ${projectRoot}`));
|
|
2144
|
+
process.exit(1);
|
|
2145
|
+
}
|
|
2146
|
+
console.log(chalk9.blue(`Scanning: ${projectRoot}
|
|
2147
|
+
`));
|
|
2148
|
+
const result = await scanProject(projectRoot);
|
|
2149
|
+
console.log(` Files found: ${result.graph.nodes.length}`);
|
|
2150
|
+
console.log(` References: ${result.graph.edges.length}`);
|
|
2151
|
+
console.log(` Orphans: ${result.orphans.length}`);
|
|
2152
|
+
console.log(` Stale files: ${result.staleFiles.length}`);
|
|
2153
|
+
console.log(` Skills: ${result.skills.length}`);
|
|
2154
|
+
console.log(` Toolings: ${result.toolings.length}`);
|
|
2155
|
+
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
2156
|
+
console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
|
|
2157
|
+
const outputDir = resolve3(projectRoot, "output");
|
|
2158
|
+
if (!existsSync7(outputDir)) {
|
|
2159
|
+
await mkdir2(outputDir, { recursive: true });
|
|
2160
|
+
}
|
|
2161
|
+
const htmlPath = resolve3(outputDir, "index.html");
|
|
2162
|
+
const html = generateOfflineHtml(result, projectRoot);
|
|
2163
|
+
await writeFile2(htmlPath, html, "utf-8");
|
|
2164
|
+
console.log(chalk9.green(`
|
|
2165
|
+
Local preview: ${htmlPath}`));
|
|
2166
|
+
if (!options.offline) {
|
|
2167
|
+
try {
|
|
2168
|
+
const { supabase } = await getAuthenticatedClient();
|
|
2169
|
+
const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
|
|
2170
|
+
if (devicePaths?.length) {
|
|
2171
|
+
const { folder_id, device_name } = devicePaths[0];
|
|
2172
|
+
const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
|
|
2173
|
+
if (proposedFiles?.length) {
|
|
2174
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
2175
|
+
console.log(chalk9.yellow(`
|
|
2176
|
+
${proposedFiles.length} file(s) proposed for deletion:
|
|
2177
|
+
`));
|
|
2178
|
+
const toDelete = await checkbox({
|
|
2179
|
+
message: "Select files to delete (space to toggle, enter to confirm)",
|
|
2180
|
+
choices: proposedFiles.map((f) => ({
|
|
2181
|
+
name: `${f.file_path}${f.proposed_at ? ` (proposed ${new Date(f.proposed_at).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })})` : ""}`,
|
|
2182
|
+
value: f
|
|
2183
|
+
}))
|
|
2184
|
+
});
|
|
2185
|
+
for (const file of toDelete) {
|
|
2186
|
+
const fullPath = resolve3(projectRoot, file.file_path);
|
|
2187
|
+
try {
|
|
2188
|
+
const { unlink } = await import("node:fs/promises");
|
|
2189
|
+
await unlink(fullPath);
|
|
2190
|
+
await supabase.from("folder_files").delete().eq("id", file.id);
|
|
2191
|
+
console.log(chalk9.green(` Deleted: ${file.file_path}`));
|
|
2192
|
+
} catch (err) {
|
|
2193
|
+
console.error(chalk9.red(` Failed to delete ${file.file_path}: ${err}`));
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
|
|
2197
|
+
if (keptIds.length > 0) {
|
|
2198
|
+
for (const id of keptIds) {
|
|
2199
|
+
await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
|
|
2200
|
+
}
|
|
2201
|
+
console.log(chalk9.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
|
|
2202
|
+
}
|
|
2203
|
+
console.log("");
|
|
2204
|
+
}
|
|
2205
|
+
const { error } = await supabase.from("claude_folders").update({
|
|
2206
|
+
graph_json: result.graph,
|
|
2207
|
+
orphans_json: result.orphans,
|
|
2208
|
+
skills_table_json: result.skills,
|
|
2209
|
+
stale_files_json: result.staleFiles,
|
|
2210
|
+
env_manifest_json: result.envManifest,
|
|
2211
|
+
last_scanned: result.scannedAt,
|
|
2212
|
+
data_hash: result.dataHash
|
|
2213
|
+
}).eq("id", folder_id);
|
|
2214
|
+
if (error) {
|
|
2215
|
+
console.error(chalk9.yellow(`Sync warning: ${error.message}`));
|
|
2216
|
+
} else {
|
|
2217
|
+
await pushToolings(supabase, folder_id, result.toolings);
|
|
2218
|
+
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
|
|
2219
|
+
await saveState({
|
|
2220
|
+
lastFolderId: folder_id,
|
|
2221
|
+
lastDeviceName: device_name,
|
|
2222
|
+
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2223
|
+
});
|
|
2224
|
+
console.log(chalk9.green("Synced to Supabase."));
|
|
2225
|
+
console.log(chalk9.cyan(`
|
|
2226
|
+
https://www.md4ai.com/project/${folder_id}
|
|
2227
|
+
`));
|
|
2228
|
+
const configFiles = await readClaudeConfigFiles(projectRoot);
|
|
2229
|
+
if (configFiles.length > 0) {
|
|
2230
|
+
for (const file of configFiles) {
|
|
2231
|
+
await supabase.from("folder_files").upsert({
|
|
2232
|
+
folder_id,
|
|
2233
|
+
file_path: file.filePath,
|
|
2234
|
+
content: file.content,
|
|
2235
|
+
size_bytes: file.sizeBytes,
|
|
2236
|
+
last_modified: file.lastModified
|
|
2237
|
+
}, { onConflict: "folder_id,file_path" });
|
|
2238
|
+
}
|
|
2239
|
+
console.log(chalk9.green(` Uploaded ${configFiles.length} config file(s).`));
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
} else {
|
|
2243
|
+
console.log(chalk9.yellow("No device path matches this folder. Run: md4ai add-device\n(Local preview still generated.)"));
|
|
2244
|
+
}
|
|
2245
|
+
} catch {
|
|
2246
|
+
console.log(chalk9.yellow("Not logged in \u2014 local preview only."));
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// dist/commands/simulate.js
|
|
2252
|
+
init_dist();
|
|
2253
|
+
import { join as join9 } from "node:path";
|
|
2254
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
2255
|
+
import { homedir as homedir6 } from "node:os";
|
|
2256
|
+
import chalk10 from "chalk";
|
|
2257
|
+
async function simulateCommand(prompt) {
|
|
2258
|
+
const projectRoot = process.cwd();
|
|
2259
|
+
console.log(chalk10.blue(`Simulating prompt: "${prompt}"
|
|
2260
|
+
`));
|
|
2261
|
+
console.log(chalk10.dim("Files Claude would load:\n"));
|
|
2262
|
+
const globalClaude = join9(homedir6(), ".claude", "CLAUDE.md");
|
|
2263
|
+
if (existsSync8(globalClaude)) {
|
|
2264
|
+
console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
|
|
2265
|
+
}
|
|
2266
|
+
for (const rootFile of ROOT_FILES) {
|
|
2267
|
+
const fullPath = join9(projectRoot, rootFile);
|
|
2268
|
+
if (existsSync8(fullPath)) {
|
|
2269
|
+
console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const settingsFiles = [
|
|
2273
|
+
join9(homedir6(), ".claude", "settings.json"),
|
|
2274
|
+
join9(homedir6(), ".claude", "settings.local.json"),
|
|
2275
|
+
join9(projectRoot, ".claude", "settings.json"),
|
|
2276
|
+
join9(projectRoot, ".claude", "settings.local.json")
|
|
2277
|
+
];
|
|
2278
|
+
for (const sf of settingsFiles) {
|
|
2279
|
+
if (existsSync8(sf)) {
|
|
2280
|
+
const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
|
|
2281
|
+
console.log(chalk10.green(` \u2713 ${display} (settings)`));
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
const words = prompt.split(/\s+/);
|
|
2285
|
+
for (const word of words) {
|
|
2286
|
+
const cleaned = word.replace(/['"]/g, "");
|
|
2287
|
+
const candidatePath = join9(projectRoot, cleaned);
|
|
2288
|
+
if (existsSync8(candidatePath) && cleaned.includes("/")) {
|
|
2289
|
+
console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
console.log(chalk10.dim("\nNote: This is an approximation. Actual file loading depends on Claude Code internals."));
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// dist/commands/print.js
|
|
2296
|
+
import { join as join10 } from "node:path";
|
|
2297
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
2298
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
2299
|
+
import chalk11 from "chalk";
|
|
2300
|
+
async function printCommand(title) {
|
|
2301
|
+
const projectRoot = process.cwd();
|
|
2302
|
+
const scanDataPath = join10(projectRoot, "output", "index.html");
|
|
2303
|
+
if (!existsSync9(scanDataPath)) {
|
|
2304
|
+
console.error(chalk11.red("No scan data found. Run: md4ai scan"));
|
|
2305
|
+
process.exit(1);
|
|
2306
|
+
}
|
|
2307
|
+
const html = await readFile6(scanDataPath, "utf-8");
|
|
2308
|
+
const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
|
|
2309
|
+
if (!match) {
|
|
2310
|
+
console.error(chalk11.red("Could not extract scan data from output/index.html"));
|
|
2311
|
+
process.exit(1);
|
|
2312
|
+
}
|
|
2313
|
+
const result = JSON.parse(match[1]);
|
|
2314
|
+
const printHtml = generatePrintHtml(result, title);
|
|
2315
|
+
const outputPath = join10(projectRoot, "output", `print-${Date.now()}.html`);
|
|
2316
|
+
await writeFile3(outputPath, printHtml, "utf-8");
|
|
2317
|
+
console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
|
|
2318
|
+
}
|
|
2319
|
+
function escapeHtml2(text) {
|
|
2320
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2321
|
+
}
|
|
2322
|
+
function generatePrintHtml(result, title) {
|
|
2323
|
+
const skillRows = result.skills.map((s) => `<tr><td>${escapeHtml2(s.name)}</td><td>${s.machineWide ? '<span class="check">\u2713</span>' : ""}</td><td>${s.projectSpecific ? '<span class="check">\u2713</span>' : ""}</td></tr>`).join("\n");
|
|
2324
|
+
const orphanItems = result.orphans.map((o) => `<li class="orphan">${escapeHtml2(o.path)}</li>`).join("\n");
|
|
2325
|
+
const staleItems = result.staleFiles.map((s) => `<li class="stale">${escapeHtml2(s.path)} (${s.daysSinceModified}d)</li>`).join("\n");
|
|
2326
|
+
return `<!DOCTYPE html>
|
|
2327
|
+
<html lang="en">
|
|
2328
|
+
<head>
|
|
2329
|
+
<meta charset="UTF-8">
|
|
2330
|
+
<title>${escapeHtml2(title)} \u2014 MD4AI Wall Sheet</title>
|
|
2331
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
2332
|
+
<style>
|
|
2333
|
+
@page { size: A3 landscape; margin: 1cm; }
|
|
2334
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 1rem; }
|
|
2335
|
+
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
2336
|
+
.subtitle { color: #666; margin-bottom: 2rem; }
|
|
2337
|
+
.mermaid { margin: 2rem 0; }
|
|
2338
|
+
.columns { display: flex; gap: 2rem; }
|
|
2339
|
+
.column { flex: 1; }
|
|
2340
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
2341
|
+
th, td { padding: 0.3rem 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
|
|
2342
|
+
th { background: #f5f5f5; }
|
|
2343
|
+
.orphan { color: #dc2626; }
|
|
2344
|
+
.stale { color: #d97706; }
|
|
2345
|
+
.check { color: #16a34a; font-weight: bold; }
|
|
2346
|
+
</style>
|
|
2347
|
+
</head>
|
|
2348
|
+
<body>
|
|
2349
|
+
<h1>${escapeHtml2(title)}</h1>
|
|
2350
|
+
<p class="subtitle">Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()} | Files: ${result.graph.nodes.length} | Orphans: ${result.orphans.length}</p>
|
|
2351
|
+
|
|
2352
|
+
<pre class="mermaid">${escapeHtml2(result.graph.mermaid)}</pre>
|
|
2353
|
+
|
|
2354
|
+
<div class="columns">
|
|
2355
|
+
<div class="column">
|
|
2356
|
+
<h2>Skills Comparison</h2>
|
|
2357
|
+
<table>
|
|
2358
|
+
<tr><th>Skill</th><th>Machine</th><th>Project</th></tr>
|
|
2359
|
+
${skillRows}
|
|
2360
|
+
</table>
|
|
2361
|
+
</div>
|
|
2362
|
+
<div class="column">
|
|
2363
|
+
${result.orphans.length ? `<h2>Orphans</h2><ul>${orphanItems}</ul>` : ""}
|
|
2364
|
+
${result.staleFiles.length ? `<h2>Stale Files</h2><ul>${staleItems}</ul>` : ""}
|
|
2365
|
+
</div>
|
|
2366
|
+
</div>
|
|
2367
|
+
|
|
2368
|
+
<script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
|
|
2369
|
+
</body>
|
|
2370
|
+
</html>`;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// dist/index.js
|
|
2374
|
+
init_sync();
|
|
2375
|
+
|
|
2376
|
+
// dist/commands/link.js
|
|
2377
|
+
init_auth();
|
|
2378
|
+
init_config();
|
|
2379
|
+
init_scanner();
|
|
2380
|
+
init_push_toolings();
|
|
2381
|
+
init_device_utils();
|
|
2382
|
+
import { resolve as resolve4 } from "node:path";
|
|
2383
|
+
import chalk13 from "chalk";
|
|
2384
|
+
async function linkCommand(projectId) {
|
|
2385
|
+
const { supabase, userId } = await getAuthenticatedClient();
|
|
2386
|
+
const cwd = resolve4(process.cwd());
|
|
2387
|
+
const deviceName = detectDeviceName();
|
|
2388
|
+
const osType = detectOs2();
|
|
2389
|
+
const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
|
|
2390
|
+
if (folderErr || !folder) {
|
|
2391
|
+
console.error(chalk13.red("Project not found, or you do not have access."));
|
|
2392
|
+
console.error(chalk13.yellow("Check the project ID in the MD4AI web dashboard."));
|
|
2393
|
+
process.exit(1);
|
|
2394
|
+
}
|
|
2395
|
+
console.log(chalk13.blue(`
|
|
2396
|
+
Linking "${folder.name}" to this device...
|
|
2397
|
+
`));
|
|
2398
|
+
console.log(` Project: ${folder.name}`);
|
|
2399
|
+
console.log(` Path: ${cwd}`);
|
|
2400
|
+
console.log(` Device: ${deviceName}`);
|
|
2401
|
+
console.log(` OS: ${osType}`);
|
|
2402
|
+
await supabase.from("devices").upsert({
|
|
2403
|
+
user_id: userId,
|
|
2404
|
+
device_name: deviceName,
|
|
2405
|
+
os_type: osType
|
|
2406
|
+
}, { onConflict: "user_id,device_name" });
|
|
2407
|
+
const { data: existing } = await supabase.from("device_paths").select("id").eq("folder_id", folder.id).eq("device_name", deviceName).maybeSingle();
|
|
2408
|
+
if (existing) {
|
|
2409
|
+
await supabase.from("device_paths").update({ path: cwd, os_type: osType, last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id);
|
|
2410
|
+
} else {
|
|
2411
|
+
const { error: pathErr } = await supabase.from("device_paths").insert({
|
|
2412
|
+
user_id: userId,
|
|
2413
|
+
folder_id: folder.id,
|
|
2414
|
+
device_name: deviceName,
|
|
2415
|
+
os_type: osType,
|
|
2416
|
+
path: cwd
|
|
2417
|
+
});
|
|
2418
|
+
if (pathErr) {
|
|
2419
|
+
console.error(chalk13.red(`Failed to link: ${pathErr.message}`));
|
|
2420
|
+
process.exit(1);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
console.log(chalk13.green("\nLinked successfully."));
|
|
2424
|
+
console.log(chalk13.blue("\nRunning initial scan...\n"));
|
|
2425
|
+
const result = await scanProject(cwd);
|
|
2426
|
+
console.log(` Files: ${result.graph.nodes.length}`);
|
|
2427
|
+
console.log(` References:${result.graph.edges.length}`);
|
|
2428
|
+
console.log(` Orphans: ${result.orphans.length}`);
|
|
2429
|
+
console.log(` Skills: ${result.skills.length}`);
|
|
2430
|
+
console.log(` Toolings: ${result.toolings.length}`);
|
|
2431
|
+
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
2432
|
+
const { error: scanErr } = await supabase.from("claude_folders").update({
|
|
2433
|
+
graph_json: result.graph,
|
|
2434
|
+
orphans_json: result.orphans,
|
|
2435
|
+
skills_table_json: result.skills,
|
|
2436
|
+
stale_files_json: result.staleFiles,
|
|
2437
|
+
env_manifest_json: result.envManifest,
|
|
2438
|
+
last_scanned: result.scannedAt,
|
|
2439
|
+
data_hash: result.dataHash
|
|
2440
|
+
}).eq("id", folder.id);
|
|
2441
|
+
if (scanErr) {
|
|
2442
|
+
console.error(chalk13.yellow(`Scan upload warning: ${scanErr.message}`));
|
|
2443
|
+
}
|
|
2444
|
+
await pushToolings(supabase, folder.id, result.toolings);
|
|
2445
|
+
const configFiles = await readClaudeConfigFiles(cwd);
|
|
2446
|
+
if (configFiles.length > 0) {
|
|
2447
|
+
for (const file of configFiles) {
|
|
2448
|
+
await supabase.from("folder_files").upsert({
|
|
2449
|
+
folder_id: folder.id,
|
|
2450
|
+
file_path: file.filePath,
|
|
2451
|
+
content: file.content,
|
|
2452
|
+
size_bytes: file.sizeBytes,
|
|
2453
|
+
last_modified: file.lastModified
|
|
2454
|
+
}, { onConflict: "folder_id,file_path" });
|
|
2455
|
+
}
|
|
2456
|
+
console.log(chalk13.green(` Uploaded ${configFiles.length} config file(s).`));
|
|
2457
|
+
}
|
|
2458
|
+
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder.id).eq("device_name", deviceName);
|
|
2459
|
+
await saveState({
|
|
2460
|
+
lastFolderId: folder.id,
|
|
2461
|
+
lastDeviceName: deviceName,
|
|
2462
|
+
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2463
|
+
});
|
|
2464
|
+
const projectUrl = `https://www.md4ai.com/project/${folder.id}`;
|
|
2465
|
+
console.log(chalk13.green("\nDone! Project linked and scanned."));
|
|
2466
|
+
console.log(chalk13.cyan(`
|
|
2467
|
+
${projectUrl}
|
|
2468
|
+
`));
|
|
2469
|
+
console.log(chalk13.grey('Run "md4ai scan" to rescan at any time.'));
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// dist/commands/import-bundle.js
|
|
2473
|
+
import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
|
|
2474
|
+
import { join as join11, dirname as dirname2 } from "node:path";
|
|
2475
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
2476
|
+
import chalk14 from "chalk";
|
|
2477
|
+
import { confirm, input as input4 } from "@inquirer/prompts";
|
|
2478
|
+
async function importBundleCommand(zipPath) {
|
|
2479
|
+
if (!existsSync10(zipPath)) {
|
|
2480
|
+
console.error(chalk14.red(`File not found: ${zipPath}`));
|
|
2481
|
+
process.exit(1);
|
|
2482
|
+
}
|
|
2483
|
+
const JSZip = (await import("jszip")).default;
|
|
2484
|
+
const zipData = await readFile7(zipPath);
|
|
2485
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
2486
|
+
const manifestFile = zip.file("manifest.json");
|
|
2487
|
+
if (!manifestFile) {
|
|
2488
|
+
console.error(chalk14.red("Invalid bundle: missing manifest.json"));
|
|
2489
|
+
process.exit(1);
|
|
2490
|
+
}
|
|
2491
|
+
const manifest = JSON.parse(await manifestFile.async("string"));
|
|
2492
|
+
console.log(chalk14.blue(`
|
|
2493
|
+
Bundle: ${manifest.folderName}`));
|
|
2494
|
+
console.log(` Exported by: ${manifest.ownerEmail}`);
|
|
2495
|
+
console.log(` Exported at: ${manifest.exportedAt}`);
|
|
2496
|
+
console.log(` Files: ${manifest.fileCount}`);
|
|
2497
|
+
const files = [];
|
|
2498
|
+
for (const [path, file] of Object.entries(zip.files)) {
|
|
2499
|
+
if (path.startsWith("claude-files/") && !file.dir) {
|
|
2500
|
+
const relativePath = path.replace("claude-files/", "");
|
|
2501
|
+
const content = await file.async("string");
|
|
2502
|
+
files.push({ filePath: relativePath, content });
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
if (files.length === 0) {
|
|
2506
|
+
console.log(chalk14.yellow("No Claude config files found in bundle."));
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
console.log(chalk14.blue(`
|
|
2510
|
+
Files to extract:`));
|
|
2511
|
+
for (const f of files) {
|
|
2512
|
+
console.log(` ${f.filePath}`);
|
|
2513
|
+
}
|
|
2514
|
+
const targetDir = await input4({
|
|
2515
|
+
message: "Extract to directory:",
|
|
2516
|
+
default: process.cwd()
|
|
2517
|
+
});
|
|
2518
|
+
const proceed = await confirm({
|
|
2519
|
+
message: `Extract ${files.length} file(s) to ${targetDir}?`
|
|
2520
|
+
});
|
|
2521
|
+
if (!proceed) {
|
|
2522
|
+
console.log(chalk14.yellow("Cancelled."));
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
for (const file of files) {
|
|
2526
|
+
const fullPath = join11(targetDir, file.filePath);
|
|
2527
|
+
const dir = dirname2(fullPath);
|
|
2528
|
+
if (!existsSync10(dir)) {
|
|
2529
|
+
await mkdir3(dir, { recursive: true });
|
|
2530
|
+
}
|
|
2531
|
+
await writeFile4(fullPath, file.content, "utf-8");
|
|
2532
|
+
console.log(chalk14.green(` \u2713 ${file.filePath}`));
|
|
2533
|
+
}
|
|
2534
|
+
console.log(chalk14.green(`
|
|
2535
|
+
Done! ${files.length} file(s) extracted to ${targetDir}`));
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// dist/commands/admin-update-tool.js
|
|
2539
|
+
init_auth();
|
|
2540
|
+
import chalk15 from "chalk";
|
|
2541
|
+
var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
|
|
2542
|
+
async function adminUpdateToolCommand(options) {
|
|
2543
|
+
const { supabase } = await getAuthenticatedClient();
|
|
2544
|
+
if (!options.name) {
|
|
2545
|
+
console.error(chalk15.red("--name is required."));
|
|
2546
|
+
process.exit(1);
|
|
2547
|
+
}
|
|
2548
|
+
if (options.category && !VALID_CATEGORIES.includes(options.category)) {
|
|
2549
|
+
console.error(chalk15.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
|
|
2550
|
+
process.exit(1);
|
|
2551
|
+
}
|
|
2552
|
+
const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
|
|
2553
|
+
if (existing) {
|
|
2554
|
+
const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2555
|
+
if (options.display)
|
|
2556
|
+
updates.display_name = options.display;
|
|
2557
|
+
if (options.category)
|
|
2558
|
+
updates.category = options.category;
|
|
2559
|
+
if (options.stable)
|
|
2560
|
+
updates.latest_stable = options.stable;
|
|
2561
|
+
if (options.beta)
|
|
2562
|
+
updates.latest_beta = options.beta;
|
|
2563
|
+
if (options.source)
|
|
2564
|
+
updates.source_url = options.source;
|
|
2565
|
+
if (options.install)
|
|
2566
|
+
updates.install_url = options.install;
|
|
2567
|
+
if (options.notes)
|
|
2568
|
+
updates.notes = options.notes;
|
|
2569
|
+
const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
|
|
2570
|
+
if (error) {
|
|
2571
|
+
console.error(chalk15.red(`Failed to update: ${error.message}`));
|
|
2572
|
+
process.exit(1);
|
|
2573
|
+
}
|
|
2574
|
+
console.log(chalk15.green(`Updated "${existing.display_name}" in the registry.`));
|
|
2575
|
+
} else {
|
|
2576
|
+
if (!options.display || !options.category) {
|
|
2577
|
+
console.error(chalk15.red("New tools require --display and --category."));
|
|
2578
|
+
process.exit(1);
|
|
2579
|
+
}
|
|
2580
|
+
const { error } = await supabase.from("tools_registry").insert({
|
|
2581
|
+
name: options.name,
|
|
2582
|
+
display_name: options.display,
|
|
2583
|
+
category: options.category,
|
|
2584
|
+
latest_stable: options.stable ?? null,
|
|
2585
|
+
latest_beta: options.beta ?? null,
|
|
2586
|
+
source_url: options.source ?? null,
|
|
2587
|
+
install_url: options.install ?? null,
|
|
2588
|
+
notes: options.notes ?? null
|
|
2589
|
+
});
|
|
2590
|
+
if (error) {
|
|
2591
|
+
console.error(chalk15.red(`Failed to create: ${error.message}`));
|
|
2592
|
+
process.exit(1);
|
|
2593
|
+
}
|
|
2594
|
+
console.log(chalk15.green(`Added "${options.display}" to the registry.`));
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// dist/commands/admin-list-tools.js
|
|
2599
|
+
init_auth();
|
|
2600
|
+
import chalk16 from "chalk";
|
|
2601
|
+
async function adminListToolsCommand() {
|
|
2602
|
+
const { supabase } = await getAuthenticatedClient();
|
|
2603
|
+
const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
|
|
2604
|
+
if (error) {
|
|
2605
|
+
console.error(chalk16.red(`Failed to fetch tools: ${error.message}`));
|
|
2606
|
+
process.exit(1);
|
|
2607
|
+
}
|
|
2608
|
+
if (!tools?.length) {
|
|
2609
|
+
console.log(chalk16.yellow("No tools in the registry."));
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
|
|
2613
|
+
const catW = Math.max(10, ...tools.map((t) => t.category.length));
|
|
2614
|
+
const stableW = Math.max(8, ...tools.map((t) => (t.latest_stable ?? "\u2014").length));
|
|
2615
|
+
const betaW = Math.max(8, ...tools.map((t) => (t.latest_beta ?? "\u2014").length));
|
|
2616
|
+
const header = [
|
|
2617
|
+
"Name".padEnd(nameW),
|
|
2618
|
+
"Category".padEnd(catW),
|
|
2619
|
+
"Stable".padEnd(stableW),
|
|
2620
|
+
"Beta".padEnd(betaW),
|
|
2621
|
+
"Last Checked"
|
|
2622
|
+
].join(" ");
|
|
2623
|
+
console.log(chalk16.bold(header));
|
|
2624
|
+
console.log("\u2500".repeat(header.length));
|
|
2625
|
+
for (const tool of tools) {
|
|
2626
|
+
const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
|
|
2627
|
+
const row = [
|
|
2628
|
+
tool.display_name.padEnd(nameW),
|
|
2629
|
+
tool.category.padEnd(catW),
|
|
2630
|
+
(tool.latest_stable ?? "\u2014").padEnd(stableW),
|
|
2631
|
+
(tool.latest_beta ?? "\u2014").padEnd(betaW),
|
|
2632
|
+
lastChecked
|
|
2633
|
+
].join(" ");
|
|
2634
|
+
console.log(row);
|
|
2635
|
+
}
|
|
2636
|
+
console.log(chalk16.grey(`
|
|
2637
|
+
${tools.length} tool(s) in registry.`));
|
|
2638
|
+
}
|
|
2639
|
+
function formatRelative(date) {
|
|
2640
|
+
const diff = Date.now() - date.getTime();
|
|
2641
|
+
const hours = Math.floor(diff / (1e3 * 60 * 60));
|
|
2642
|
+
if (hours < 1)
|
|
2643
|
+
return "just now";
|
|
2644
|
+
if (hours < 24)
|
|
2645
|
+
return `${hours}h ago`;
|
|
2646
|
+
const days = Math.floor(hours / 24);
|
|
2647
|
+
if (days < 7)
|
|
2648
|
+
return `${days}d ago`;
|
|
2649
|
+
return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// dist/commands/admin-fetch-versions.js
|
|
2653
|
+
init_auth();
|
|
2654
|
+
import chalk17 from "chalk";
|
|
2655
|
+
|
|
2656
|
+
// dist/commands/version-sources.js
|
|
2657
|
+
var VERSION_SOURCES = {
|
|
2658
|
+
"next": { type: "npm", package: "next" },
|
|
2659
|
+
"react": { type: "npm", package: "react" },
|
|
2660
|
+
"react-dom": { type: "npm", package: "react-dom" },
|
|
2661
|
+
"typescript": { type: "npm", package: "typescript" },
|
|
2662
|
+
"tailwindcss": { type: "npm", package: "tailwindcss" },
|
|
2663
|
+
"esbuild": { type: "npm", package: "esbuild" },
|
|
2664
|
+
"chalk": { type: "npm", package: "chalk" },
|
|
2665
|
+
"commander": { type: "npm", package: "commander" },
|
|
2666
|
+
"inquirer": { type: "npm", package: "inquirer" },
|
|
2667
|
+
"playwright": { type: "npm", package: "playwright" },
|
|
2668
|
+
"resend": { type: "npm", package: "resend" },
|
|
2669
|
+
"@supabase/supabase-js": { type: "npm", package: "@supabase/supabase-js" },
|
|
2670
|
+
"pnpm": { type: "npm", package: "pnpm" },
|
|
2671
|
+
"claude-code": { type: "npm", package: "@anthropic-ai/claude-code" },
|
|
2672
|
+
"turborepo": { type: "npm", package: "turbo" },
|
|
2673
|
+
"npm": { type: "npm", package: "npm" },
|
|
2674
|
+
"node": { type: "github", repo: "nodejs/node" },
|
|
2675
|
+
"supabase-cli": { type: "npm", package: "supabase" }
|
|
2676
|
+
};
|
|
2677
|
+
|
|
2678
|
+
// dist/commands/admin-fetch-versions.js
|
|
2679
|
+
async function adminFetchVersionsCommand() {
|
|
2680
|
+
const { supabase } = await getAuthenticatedClient();
|
|
2681
|
+
const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
|
|
2682
|
+
if (error) {
|
|
2683
|
+
console.error(chalk17.red(`Failed to fetch tools: ${error.message}`));
|
|
2684
|
+
process.exit(1);
|
|
2685
|
+
}
|
|
2686
|
+
if (!tools?.length) {
|
|
2687
|
+
console.log(chalk17.yellow("No tools in the registry."));
|
|
2688
|
+
}
|
|
2689
|
+
const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
|
|
2690
|
+
const registeredNames = new Set((tools ?? []).map((t) => t.name));
|
|
2691
|
+
const unregisteredNames = /* @__PURE__ */ new Set();
|
|
2692
|
+
for (const pt of allProjectToolings ?? []) {
|
|
2693
|
+
if (!registeredNames.has(pt.tool_name) && pt.detection_source === "package.json") {
|
|
2694
|
+
unregisteredNames.add(pt.tool_name);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
if (unregisteredNames.size > 0) {
|
|
2698
|
+
console.log(chalk17.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
|
|
2699
|
+
`));
|
|
2700
|
+
for (const name of unregisteredNames) {
|
|
2701
|
+
const displayName = name;
|
|
2702
|
+
const { data: inserted, error: insertError } = await supabase.from("tools_registry").upsert({
|
|
2703
|
+
name,
|
|
2704
|
+
display_name: displayName,
|
|
2705
|
+
category: "package",
|
|
2706
|
+
source_url: `https://www.npmjs.com/package/${name}`,
|
|
2707
|
+
install_url: `https://www.npmjs.com/package/${name}`
|
|
2708
|
+
}, { onConflict: "name" }).select().single();
|
|
2709
|
+
if (!insertError && inserted) {
|
|
2710
|
+
tools.push(inserted);
|
|
2711
|
+
await supabase.from("project_toolings").update({ tool_id: inserted.id }).eq("tool_name", name).is("tool_id", null);
|
|
2712
|
+
}
|
|
2554
2713
|
}
|
|
2555
2714
|
}
|
|
2556
|
-
if (
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
for (const s of notRunning) {
|
|
2560
|
-
const icon = s.status === "error" ? chalk18.red("\u2717") : chalk18.yellow("\u25CB");
|
|
2561
|
-
const detail = s.error_detail ? chalk18.dim(` \u2014 ${s.error_detail}`) : "";
|
|
2562
|
-
console.log(` ${icon} ${s.server_name.padEnd(20)} ${chalk18.dim(s.config_source.padEnd(10))}${detail}`);
|
|
2563
|
-
}
|
|
2564
|
-
console.log("");
|
|
2715
|
+
if (!tools?.length) {
|
|
2716
|
+
console.log(chalk17.yellow("No tools to fetch."));
|
|
2717
|
+
return;
|
|
2565
2718
|
}
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2719
|
+
console.log(chalk17.bold(`Fetching versions for ${tools.length} tool(s)...
|
|
2720
|
+
`));
|
|
2721
|
+
const results = await Promise.all(tools.map(async (tool) => {
|
|
2722
|
+
const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
|
|
2723
|
+
try {
|
|
2724
|
+
const { stable, beta } = await fetchVersions(source);
|
|
2725
|
+
const stableChanged = stable !== tool.latest_stable;
|
|
2726
|
+
const betaChanged = beta !== tool.latest_beta;
|
|
2727
|
+
const updates = {
|
|
2728
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2729
|
+
};
|
|
2730
|
+
if (stableChanged)
|
|
2731
|
+
updates.latest_stable = stable;
|
|
2732
|
+
if (betaChanged)
|
|
2733
|
+
updates.latest_beta = beta;
|
|
2734
|
+
const { error: updateError } = await supabase.from("tools_registry").update(updates).eq("id", tool.id);
|
|
2735
|
+
if (updateError) {
|
|
2736
|
+
return { displayName: tool.display_name, stable, beta, status: "failed" };
|
|
2737
|
+
}
|
|
2738
|
+
return {
|
|
2739
|
+
displayName: tool.display_name,
|
|
2740
|
+
stable,
|
|
2741
|
+
beta,
|
|
2742
|
+
status: stableChanged || betaChanged ? "updated" : "unchanged"
|
|
2743
|
+
};
|
|
2744
|
+
} catch {
|
|
2745
|
+
return { displayName: tool.display_name, stable: null, beta: null, status: "failed" };
|
|
2746
|
+
}
|
|
2747
|
+
}));
|
|
2748
|
+
const nameW = Math.max(16, ...results.map((r) => r.displayName.length));
|
|
2749
|
+
const stableW = Math.max(8, ...results.map((r) => (r.stable ?? "\u2014").length));
|
|
2750
|
+
const betaW = Math.max(8, ...results.map((r) => (r.beta ?? "\u2014").length));
|
|
2751
|
+
const statusW = 10;
|
|
2752
|
+
const header = [
|
|
2753
|
+
"Tool".padEnd(nameW),
|
|
2754
|
+
"Stable".padEnd(stableW),
|
|
2755
|
+
"Beta".padEnd(betaW),
|
|
2756
|
+
"Status".padEnd(statusW)
|
|
2757
|
+
].join(" ");
|
|
2758
|
+
console.log(chalk17.bold(header));
|
|
2759
|
+
console.log("\u2500".repeat(header.length));
|
|
2760
|
+
for (const result of results) {
|
|
2761
|
+
const statusColour = result.status === "updated" ? chalk17.green : result.status === "unchanged" ? chalk17.grey : result.status === "failed" ? chalk17.red : chalk17.yellow;
|
|
2762
|
+
const row = [
|
|
2763
|
+
result.displayName.padEnd(nameW),
|
|
2764
|
+
(result.stable ?? "\u2014").padEnd(stableW),
|
|
2765
|
+
(result.beta ?? "\u2014").padEnd(betaW),
|
|
2766
|
+
statusColour(result.status.padEnd(statusW))
|
|
2767
|
+
].join(" ");
|
|
2768
|
+
console.log(row);
|
|
2569
2769
|
}
|
|
2770
|
+
const updated = results.filter((r) => r.status === "updated").length;
|
|
2771
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
2772
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
2773
|
+
const noSource = results.filter((r) => r.status === "no source").length;
|
|
2774
|
+
console.log(chalk17.grey(`
|
|
2775
|
+
${results.length} tool(s): `) + chalk17.green(`${updated} updated`) + ", " + chalk17.grey(`${unchanged} unchanged`) + ", " + chalk17.red(`${failed} failed`) + ", " + chalk17.yellow(`${noSource} no source`));
|
|
2570
2776
|
}
|
|
2571
|
-
function
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
}
|
|
2580
|
-
async function mcpWatchCommand() {
|
|
2581
|
-
const { supabase, userId } = await getAuthenticatedClient();
|
|
2582
|
-
const deviceId = await resolveDeviceId(supabase, userId);
|
|
2583
|
-
const deviceName = detectDeviceName();
|
|
2584
|
-
const myPid = process.pid;
|
|
2585
|
-
const myTty = detectTty();
|
|
2586
|
-
console.log(chalk18.blue(`Starting MCP monitor for ${deviceName}...`));
|
|
2587
|
-
await supabase.from("mcp_watchers").upsert({
|
|
2588
|
-
device_id: deviceId,
|
|
2589
|
-
pid: myPid,
|
|
2590
|
-
tty: myTty,
|
|
2591
|
-
cli_version: CURRENT_VERSION,
|
|
2592
|
-
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2593
|
-
last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
|
|
2594
|
-
}, { onConflict: "device_id,pid" });
|
|
2595
|
-
const staleThreshold = new Date(Date.now() - 12e4).toISOString();
|
|
2596
|
-
await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).lt("last_heartbeat", staleThreshold);
|
|
2597
|
-
async function cycle() {
|
|
2598
|
-
const configs = await readAllMcpConfigs();
|
|
2599
|
-
const rows = buildRows(configs);
|
|
2600
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2601
|
-
await supabase.from("mcp_server_status").delete().eq("device_id", deviceId);
|
|
2602
|
-
if (rows.length > 0) {
|
|
2603
|
-
await supabase.from("mcp_server_status").insert(rows.map((row) => ({
|
|
2604
|
-
device_id: deviceId,
|
|
2605
|
-
...row,
|
|
2606
|
-
checked_at: now
|
|
2607
|
-
})));
|
|
2777
|
+
async function fetchVersions(source) {
|
|
2778
|
+
const controller = new AbortController();
|
|
2779
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
2780
|
+
try {
|
|
2781
|
+
if (source.type === "npm") {
|
|
2782
|
+
return await fetchNpmVersions(source.package, controller.signal);
|
|
2783
|
+
} else {
|
|
2784
|
+
return await fetchGitHubVersions(source.repo, controller.signal);
|
|
2608
2785
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2786
|
+
} finally {
|
|
2787
|
+
clearTimeout(timeout);
|
|
2611
2788
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
const { data: dp } = await supabase.from("device_paths").select("path").eq("folder_id", state.lastFolderId).eq("device_name", deviceName).maybeSingle();
|
|
2619
|
-
if (!dp?.path)
|
|
2620
|
-
return;
|
|
2621
|
-
const envManifest = await scanEnvManifest(dp.path);
|
|
2622
|
-
if (!envManifest)
|
|
2623
|
-
return;
|
|
2624
|
-
const hash = createHash2("sha256").update(JSON.stringify(envManifest)).digest("hex");
|
|
2625
|
-
if (hash === lastEnvHash)
|
|
2626
|
-
return;
|
|
2627
|
-
lastEnvHash = hash;
|
|
2628
|
-
await supabase.from("claude_folders").update({ env_manifest_json: envManifest }).eq("id", state.lastFolderId);
|
|
2789
|
+
}
|
|
2790
|
+
async function fetchNpmVersions(packageName, signal) {
|
|
2791
|
+
const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`;
|
|
2792
|
+
const res = await fetch(url, { signal });
|
|
2793
|
+
if (!res.ok) {
|
|
2794
|
+
throw new Error(`npm registry returned ${res.status}`);
|
|
2629
2795
|
}
|
|
2630
|
-
await
|
|
2631
|
-
const
|
|
2632
|
-
const
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
process.on("SIGINT", () => {
|
|
2641
|
-
void shutdown();
|
|
2642
|
-
});
|
|
2643
|
-
process.on("SIGTERM", () => {
|
|
2644
|
-
void shutdown();
|
|
2796
|
+
const tags = await res.json();
|
|
2797
|
+
const stable = tags.latest ?? null;
|
|
2798
|
+
const beta = tags.next ?? tags.beta ?? tags.rc ?? null;
|
|
2799
|
+
return { stable, beta };
|
|
2800
|
+
}
|
|
2801
|
+
async function fetchGitHubVersions(repo, signal) {
|
|
2802
|
+
const url = `https://api.github.com/repos/${repo}/releases?per_page=20`;
|
|
2803
|
+
const res = await fetch(url, {
|
|
2804
|
+
signal,
|
|
2805
|
+
headers: { Accept: "application/vnd.github+json" }
|
|
2645
2806
|
});
|
|
2807
|
+
if (!res.ok) {
|
|
2808
|
+
throw new Error(`GitHub API returned ${res.status}`);
|
|
2809
|
+
}
|
|
2810
|
+
const releases = await res.json();
|
|
2811
|
+
let stable = null;
|
|
2812
|
+
let beta = null;
|
|
2813
|
+
for (const release of releases) {
|
|
2814
|
+
if (release.draft)
|
|
2815
|
+
continue;
|
|
2816
|
+
const version = release.tag_name.replace(/^v/, "");
|
|
2817
|
+
if (!release.prerelease && !stable) {
|
|
2818
|
+
stable = version;
|
|
2819
|
+
}
|
|
2820
|
+
if (release.prerelease && !beta) {
|
|
2821
|
+
beta = version;
|
|
2822
|
+
}
|
|
2823
|
+
if (stable && beta)
|
|
2824
|
+
break;
|
|
2825
|
+
}
|
|
2826
|
+
return { stable, beta };
|
|
2646
2827
|
}
|
|
2647
2828
|
|
|
2829
|
+
// dist/index.js
|
|
2830
|
+
init_mcp_watch();
|
|
2831
|
+
|
|
2648
2832
|
// dist/commands/init-manifest.js
|
|
2649
2833
|
import { resolve as resolve5, join as join13, relative as relative3, dirname as dirname3 } from "node:path";
|
|
2650
2834
|
import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
|
|
@@ -2770,14 +2954,132 @@ Manifest created: ${relative3(projectRoot, manifestPath)}`));
|
|
|
2770
2954
|
console.log(chalk19.dim("Then run `md4ai scan` to verify against your environments."));
|
|
2771
2955
|
}
|
|
2772
2956
|
|
|
2957
|
+
// dist/commands/update.js
|
|
2958
|
+
init_check_update();
|
|
2959
|
+
init_config();
|
|
2960
|
+
import chalk20 from "chalk";
|
|
2961
|
+
import { execFileSync as execFileSync6, spawn } from "node:child_process";
|
|
2962
|
+
async function fetchLatestVersion() {
|
|
2963
|
+
try {
|
|
2964
|
+
const controller = new AbortController();
|
|
2965
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
2966
|
+
const res = await fetch("https://registry.npmjs.org/md4ai/latest", {
|
|
2967
|
+
signal: controller.signal
|
|
2968
|
+
});
|
|
2969
|
+
clearTimeout(timeout);
|
|
2970
|
+
if (!res.ok)
|
|
2971
|
+
return null;
|
|
2972
|
+
const data = await res.json();
|
|
2973
|
+
return data.version ?? null;
|
|
2974
|
+
} catch {
|
|
2975
|
+
return null;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
function isNewer2(a, b) {
|
|
2979
|
+
const pa = a.split(".").map(Number);
|
|
2980
|
+
const pb = b.split(".").map(Number);
|
|
2981
|
+
for (let i = 0; i < 3; i++) {
|
|
2982
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0))
|
|
2983
|
+
return true;
|
|
2984
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0))
|
|
2985
|
+
return false;
|
|
2986
|
+
}
|
|
2987
|
+
return false;
|
|
2988
|
+
}
|
|
2989
|
+
function runNpmInstall() {
|
|
2990
|
+
try {
|
|
2991
|
+
execFileSync6("npm", ["install", "-g", "md4ai"], {
|
|
2992
|
+
stdio: "inherit",
|
|
2993
|
+
timeout: 6e4
|
|
2994
|
+
});
|
|
2995
|
+
return true;
|
|
2996
|
+
} catch {
|
|
2997
|
+
return false;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function spawnPostUpdate() {
|
|
3001
|
+
const child = spawn("md4ai", ["update", "--post-update"], {
|
|
3002
|
+
stdio: "inherit",
|
|
3003
|
+
shell: false
|
|
3004
|
+
});
|
|
3005
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
3006
|
+
}
|
|
3007
|
+
async function postUpdateFlow() {
|
|
3008
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
3009
|
+
console.log(chalk20.green(`
|
|
3010
|
+
md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.
|
|
3011
|
+
`));
|
|
3012
|
+
const state = await loadState();
|
|
3013
|
+
const hasProject = !!state.lastFolderId;
|
|
3014
|
+
if (hasProject) {
|
|
3015
|
+
const wantScan = await confirm2({
|
|
3016
|
+
message: "Rescan your project and refresh the dashboard?",
|
|
3017
|
+
default: true
|
|
3018
|
+
});
|
|
3019
|
+
if (wantScan) {
|
|
3020
|
+
console.log("");
|
|
3021
|
+
const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
3022
|
+
await syncCommand2({ all: false });
|
|
3023
|
+
console.log("");
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
const wantWatch = await confirm2({
|
|
3027
|
+
message: "Start monitoring MCP servers? (runs until you press Ctrl+C)",
|
|
3028
|
+
default: true
|
|
3029
|
+
});
|
|
3030
|
+
if (wantWatch) {
|
|
3031
|
+
console.log("");
|
|
3032
|
+
const { mcpWatchCommand: mcpWatchCommand2 } = await Promise.resolve().then(() => (init_mcp_watch(), mcp_watch_exports));
|
|
3033
|
+
await mcpWatchCommand2();
|
|
3034
|
+
} else {
|
|
3035
|
+
console.log(chalk20.dim("\nYou can start monitoring later with: md4ai mcp-watch\n"));
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
async function updateCommand(options) {
|
|
3039
|
+
if (options.postUpdate) {
|
|
3040
|
+
await postUpdateFlow();
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
console.log(chalk20.blue(`
|
|
3044
|
+
Current version: v${CURRENT_VERSION}`));
|
|
3045
|
+
console.log(chalk20.dim(" Checking for updates...\n"));
|
|
3046
|
+
const latest = await fetchLatestVersion();
|
|
3047
|
+
if (!latest) {
|
|
3048
|
+
console.log(chalk20.yellow(" Could not reach the npm registry. Check your internet connection."));
|
|
3049
|
+
process.exit(1);
|
|
3050
|
+
}
|
|
3051
|
+
if (!isNewer2(latest, CURRENT_VERSION)) {
|
|
3052
|
+
await postUpdateFlow();
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
console.log(chalk20.white(" Update available: ") + chalk20.dim(`v${CURRENT_VERSION}`) + chalk20.white(" \u2192 ") + chalk20.green.bold(`v${latest}
|
|
3056
|
+
`));
|
|
3057
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
3058
|
+
const wantUpdate = await confirm2({
|
|
3059
|
+
message: `Install md4ai v${latest}?`,
|
|
3060
|
+
default: true
|
|
3061
|
+
});
|
|
3062
|
+
if (!wantUpdate) {
|
|
3063
|
+
console.log(chalk20.dim("\nUpdate skipped.\n"));
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
console.log(chalk20.blue("\n Installing...\n"));
|
|
3067
|
+
const ok = runNpmInstall();
|
|
3068
|
+
if (!ok) {
|
|
3069
|
+
console.log(chalk20.red("\n npm install failed. Try running manually:"));
|
|
3070
|
+
console.log(chalk20.cyan(" npm install -g md4ai\n"));
|
|
3071
|
+
process.exit(1);
|
|
3072
|
+
}
|
|
3073
|
+
console.log(chalk20.green("\n Updated successfully.\n"));
|
|
3074
|
+
spawnPostUpdate();
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2773
3077
|
// dist/index.js
|
|
3078
|
+
init_check_update();
|
|
2774
3079
|
var program = new Command();
|
|
2775
3080
|
program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
|
|
2776
3081
|
Update to the latest version:
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
Check for updates:
|
|
2780
|
-
md4ai check-update`);
|
|
3082
|
+
md4ai update`);
|
|
2781
3083
|
program.command("login").description("Log in to MD4AI with email and password").action(loginCommand);
|
|
2782
3084
|
program.command("logout").description("Log out and clear stored credentials").action(logoutCommand);
|
|
2783
3085
|
program.command("status").description("Show login status, device count, folder count, last sync").action(statusCommand);
|
|
@@ -2790,6 +3092,7 @@ program.command("simulate <prompt>").description("Show which files Claude would
|
|
|
2790
3092
|
program.command("print <title>").description("Generate a printable wall-chart HTML from the last scan").action(printCommand);
|
|
2791
3093
|
program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
|
|
2792
3094
|
program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
|
|
3095
|
+
program.command("update").description("Check for updates, install, and optionally rescan and start watching").option("--post-update", "Internal: run post-update flow (called by the update command)").action(updateCommand);
|
|
2793
3096
|
program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
|
|
2794
3097
|
program.command("mcp-watch").description("Monitor MCP server status on this device (runs until Ctrl+C)").action(mcpWatchCommand);
|
|
2795
3098
|
program.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);
|