@nick848/ft 0.1.0

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.js ADDED
@@ -0,0 +1,968 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/commands/init.ts
7
+ import chalk2 from "chalk";
8
+ import inquirer from "inquirer";
9
+ import { execa as execa2 } from "execa";
10
+
11
+ // src/core/detector.ts
12
+ import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync } from "fs";
13
+ import path2 from "path";
14
+ import { execa } from "execa";
15
+
16
+ // src/core/paths.ts
17
+ import { fileURLToPath } from "url";
18
+ import path from "path";
19
+ import { existsSync, readFileSync } from "fs";
20
+ function getPackageRoot() {
21
+ let dir = path.dirname(fileURLToPath(import.meta.url));
22
+ while (dir !== path.dirname(dir)) {
23
+ const pkgPath = path.join(dir, "package.json");
24
+ if (existsSync(pkgPath)) {
25
+ try {
26
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
27
+ if (pkg.name === "@nick848/ft") {
28
+ return dir;
29
+ }
30
+ } catch {
31
+ }
32
+ }
33
+ dir = path.dirname(dir);
34
+ }
35
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
36
+ }
37
+ function getTemplatesDir() {
38
+ return path.join(getPackageRoot(), "templates");
39
+ }
40
+ function getRulesTemplatesDir() {
41
+ return path.join(getPackageRoot(), "rules-templates");
42
+ }
43
+ function getLocalesDir() {
44
+ return path.join(getPackageRoot(), "locales");
45
+ }
46
+
47
+ // src/core/detector.ts
48
+ var KARPATHY_PATHS = [
49
+ ".cursor/rules/karpathy-guidelines.mdc",
50
+ ".codex/rules/karpathy-guidelines.md",
51
+ ".opencode/rules/karpathy-guidelines.md"
52
+ ];
53
+ var PLATFORM_DIRS = {
54
+ cursor: ".cursor",
55
+ codex: ".codex",
56
+ opencode: ".opencode"
57
+ };
58
+ async function detectComet() {
59
+ try {
60
+ const { stdout } = await execa("comet", ["--version"]);
61
+ return stdout.trim();
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function detectKarpathyRules(projectRoot = process.cwd()) {
67
+ if (KARPATHY_PATHS.some((p) => existsSync2(path2.join(projectRoot, p)))) {
68
+ return true;
69
+ }
70
+ const templatesDir = getRulesTemplatesDir();
71
+ return existsSync2(path2.join(templatesDir, "cursor.mdc"));
72
+ }
73
+ async function detectGitNexus() {
74
+ try {
75
+ const { stdout } = await execa("gitnexus", ["--version"]);
76
+ return stdout.trim();
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ function detectPlatformsInProject(projectRoot = process.cwd()) {
82
+ const found = [];
83
+ for (const [id, dir] of Object.entries(PLATFORM_DIRS)) {
84
+ if (existsSync2(path2.join(projectRoot, dir))) {
85
+ found.push(id);
86
+ }
87
+ }
88
+ return found;
89
+ }
90
+ function detectPackageManager(projectRoot = process.cwd()) {
91
+ if (existsSync2(path2.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
92
+ if (existsSync2(path2.join(projectRoot, "yarn.lock"))) return "yarn";
93
+ if (existsSync2(path2.join(projectRoot, "bun.lockb")) || existsSync2(path2.join(projectRoot, "bun.lock"))) {
94
+ return "bun";
95
+ }
96
+ if (existsSync2(path2.join(projectRoot, "package-lock.json"))) return "npm";
97
+ return "npm";
98
+ }
99
+ function detectProjectMeta(projectRoot = process.cwd()) {
100
+ const pkgPath = path2.join(projectRoot, "package.json");
101
+ let name = path2.basename(projectRoot);
102
+ let scripts = "[NEEDS LLM INPUT]";
103
+ let workspaces = "[NEEDS LLM INPUT]";
104
+ let framework = "unknown";
105
+ let language = "JavaScript";
106
+ let monorepo = "\u5426";
107
+ if (existsSync2(pkgPath)) {
108
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
109
+ name = pkg.name ?? name;
110
+ if (pkg.scripts) {
111
+ scripts = Object.entries(pkg.scripts).map(([k, v]) => `- \`${k}\`: ${v}`).join("\n");
112
+ }
113
+ if (pkg.workspaces) {
114
+ monorepo = "\u662F";
115
+ workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces.join(", ") : JSON.stringify(pkg.workspaces);
116
+ }
117
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
118
+ if (deps["next"]) framework = "Next.js";
119
+ else if (deps["nuxt"]) framework = "Nuxt";
120
+ else if (deps["vue"]) framework = "Vue";
121
+ else if (deps["react"]) framework = "React";
122
+ else if (deps["@angular/core"]) framework = "Angular";
123
+ else if (deps["svelte"]) framework = "Svelte";
124
+ if (deps["typescript"] || existsSync2(path2.join(projectRoot, "tsconfig.json"))) {
125
+ language = "TypeScript";
126
+ }
127
+ }
128
+ return {
129
+ name,
130
+ packageManager: detectPackageManager(projectRoot),
131
+ framework,
132
+ language,
133
+ monorepo,
134
+ workspaces,
135
+ scripts
136
+ };
137
+ }
138
+ function isStdoutTTY() {
139
+ return Boolean(process.stdout.isTTY);
140
+ }
141
+ function agentsMdHasPendingPlaceholders(projectRoot = process.cwd()) {
142
+ const agentsPath = path2.join(projectRoot, "AGENTS.md");
143
+ if (!existsSync2(agentsPath)) return true;
144
+ const content = readFileSync2(agentsPath, "utf-8");
145
+ return content.includes("[NEEDS LLM INPUT]");
146
+ }
147
+ function gitNexusGraphExists(projectRoot = process.cwd()) {
148
+ const graphDir = path2.join(projectRoot, ".gitnexus");
149
+ return existsSync2(graphDir);
150
+ }
151
+ function countGitNexusIndexFiles(projectRoot = process.cwd()) {
152
+ const graphDir = path2.join(projectRoot, ".gitnexus");
153
+ if (!existsSync2(graphDir)) return 0;
154
+ try {
155
+ return readdirSync(graphDir).length;
156
+ } catch {
157
+ return 0;
158
+ }
159
+ }
160
+
161
+ // src/core/config.ts
162
+ import { cosmiconfig } from "cosmiconfig";
163
+ import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync as existsSync3 } from "fs";
164
+ import path3 from "path";
165
+ import YAML from "yaml";
166
+ var MODULE_NAME = "ft";
167
+ var DEFAULT_CONFIG = {
168
+ language: "zh-CN",
169
+ rtk: {
170
+ enabled: true,
171
+ auto_trigger_lines: 500,
172
+ exclude_patterns: ["--json", "--format json", "--xml"]
173
+ },
174
+ tools: ["cursor", "codex", "opencode"],
175
+ gitnexus: {
176
+ graph_path: ".gitnexus/",
177
+ auto_prompt_in_agents: true
178
+ }
179
+ };
180
+ function mergeConfig(partial) {
181
+ return {
182
+ ...DEFAULT_CONFIG,
183
+ ...partial,
184
+ rtk: { ...DEFAULT_CONFIG.rtk, ...partial?.rtk },
185
+ gitnexus: { ...DEFAULT_CONFIG.gitnexus, ...partial?.gitnexus },
186
+ tools: partial?.tools ?? DEFAULT_CONFIG.tools
187
+ };
188
+ }
189
+ async function loadConfig(projectRoot = process.cwd()) {
190
+ const explorer = cosmiconfig(MODULE_NAME, {
191
+ searchPlaces: [
192
+ ".ft/config.yaml",
193
+ ".ft/config.yml",
194
+ ".ft/config.json",
195
+ "ft.config.yaml",
196
+ "ft.config.json"
197
+ ]
198
+ });
199
+ const result = await explorer.search(projectRoot);
200
+ if (!result?.config) {
201
+ return { ...DEFAULT_CONFIG };
202
+ }
203
+ return mergeConfig(result.config);
204
+ }
205
+ function resolveLanguage(cliLang, configLang) {
206
+ if (cliLang === "en" || cliLang === "zh-CN") {
207
+ return cliLang;
208
+ }
209
+ if (configLang) {
210
+ return configLang;
211
+ }
212
+ return "zh-CN";
213
+ }
214
+ function writeDefaultConfig(projectRoot, config) {
215
+ const ftDir = path3.join(projectRoot, ".ft");
216
+ if (!existsSync3(ftDir)) {
217
+ mkdirSync(ftDir, { recursive: true });
218
+ }
219
+ const configPath = path3.join(ftDir, "config.yaml");
220
+ writeFileSync(configPath, YAML.stringify(config), "utf-8");
221
+ }
222
+
223
+ // src/core/agents.ts
224
+ import Handlebars from "handlebars";
225
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync, existsSync as existsSync4 } from "fs";
226
+ import path4 from "path";
227
+
228
+ // src/core/template.ts
229
+ function getGitNexusCondition(lang) {
230
+ if (lang === "en") {
231
+ return "If GitNexus is installed in this project, AI **must prioritize** GitNexus MCP tools (e.g. `gitnexus_get_symbol`) for code scanning and dependency analysis. If not installed, ignore this rule.";
232
+ }
233
+ return "\u5982\u679C\u5F53\u524D\u5DE5\u7A0B\u5DF2\u5B89\u88C5 GitNexus\uFF0CAI \u5728\u8FDB\u884C\u4EE3\u7801\u626B\u63CF\u548C\u5206\u6790\u65F6**\u5FC5\u987B\u4F18\u5148\u4F7F\u7528** GitNexus MCP \u5DE5\u5177\uFF08\u5982 `gitnexus_get_symbol`\uFF09\u83B7\u53D6\u7B26\u53F7\u7EA7\u4F9D\u8D56\u5173\u7CFB\u3002\u82E5\u672A\u5B89\u88C5\uFF0C\u8BF7\u5FFD\u7565\u6B64\u6761\u3002";
234
+ }
235
+ function buildAgentsTemplateData(meta, lang) {
236
+ return {
237
+ projectName: meta.name,
238
+ packageManager: meta.packageManager,
239
+ framework: meta.framework,
240
+ language: meta.language,
241
+ monorepo: meta.monorepo,
242
+ workspaces: meta.workspaces,
243
+ scripts: meta.scripts,
244
+ gitnexus_condition: getGitNexusCondition(lang)
245
+ };
246
+ }
247
+
248
+ // src/core/agents.ts
249
+ function renderAgentsMd(meta, lang) {
250
+ const templateName = lang === "en" ? "AGENTS.md.en.hbs" : "AGENTS.md.zh.hbs";
251
+ const templatePath = path4.join(getTemplatesDir(), templateName);
252
+ const source = readFileSync4(templatePath, "utf-8");
253
+ const template = Handlebars.compile(source);
254
+ const data = buildAgentsTemplateData(meta, lang);
255
+ return template(data);
256
+ }
257
+ function generateAgentsMd(meta, lang, projectRoot = process.cwd()) {
258
+ const agentsPath = path4.join(projectRoot, "AGENTS.md");
259
+ const backupPath = path4.join(projectRoot, "AGENTS-BAK.md");
260
+ if (existsSync4(agentsPath)) {
261
+ renameSync(agentsPath, backupPath);
262
+ }
263
+ const content = renderAgentsMd(meta, lang);
264
+ writeFileSync2(agentsPath, content, "utf-8");
265
+ }
266
+ function updateGraphTimestampInAgents(projectRoot = process.cwd(), timestamp) {
267
+ const agentsPath = path4.join(projectRoot, "AGENTS.md");
268
+ if (!existsSync4(agentsPath)) return;
269
+ const ts = timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
270
+ let content = readFileSync4(agentsPath, "utf-8");
271
+ const marker = "\u56FE\u8C31\u6700\u540E\u5237\u65B0\u65F6\u95F4";
272
+ const enMarker = "Graph last refreshed";
273
+ const isEnglish = content.includes("## AI Working Guide");
274
+ const line = isEnglish ? `- ${enMarker}: ${ts}` : `- ${marker}: ${ts}`;
275
+ const regex = /- (图谱最后刷新时间|Graph last refreshed): .+/;
276
+ if (regex.test(content)) {
277
+ content = content.replace(regex, line);
278
+ } else if (content.includes("## AI \u5DE5\u4F5C\u6307\u5357") || isEnglish) {
279
+ const sectionRegex = /(## AI (工作指南|Working Guide)\n)/;
280
+ content = content.replace(sectionRegex, `$1${line}
281
+ `);
282
+ } else {
283
+ content += `
284
+ ## AI \u5DE5\u4F5C\u6307\u5357
285
+ ${line}
286
+ `;
287
+ }
288
+ writeFileSync2(agentsPath, content, "utf-8");
289
+ }
290
+
291
+ // src/core/i18n.ts
292
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
293
+ import path5 from "path";
294
+ var cache = {};
295
+ function loadLocale(lang) {
296
+ if (cache[lang]) return cache[lang];
297
+ const filePath = path5.join(getLocalesDir(), `${lang}.json`);
298
+ if (!existsSync5(filePath)) {
299
+ return {};
300
+ }
301
+ cache[lang] = JSON.parse(readFileSync5(filePath, "utf-8"));
302
+ return cache[lang];
303
+ }
304
+ function t(key, lang = "zh-CN") {
305
+ const locale = loadLocale(lang);
306
+ return locale[key] ?? key;
307
+ }
308
+ function tf(key, lang, vars) {
309
+ let text = t(key, lang);
310
+ if (!vars) return text;
311
+ for (const [k, v] of Object.entries(vars)) {
312
+ text = text.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
313
+ }
314
+ return text;
315
+ }
316
+
317
+ // src/adapters/index.ts
318
+ import { homedir } from "os";
319
+ import path6 from "path";
320
+ import {
321
+ readFileSync as readFileSync6,
322
+ writeFileSync as writeFileSync3,
323
+ mkdirSync as mkdirSync2,
324
+ existsSync as existsSync6,
325
+ copyFileSync
326
+ } from "fs";
327
+ import deepmerge from "deepmerge";
328
+ var GITNEXUS_SNIPPET = {
329
+ gitnexus: {
330
+ command: "gitnexus",
331
+ args: ["mcp"],
332
+ disabled: false
333
+ }
334
+ };
335
+ function readJsonFile(filePath) {
336
+ if (!existsSync6(filePath)) {
337
+ return { mcpServers: {} };
338
+ }
339
+ const raw = readFileSync6(filePath, "utf-8");
340
+ try {
341
+ return JSON.parse(raw);
342
+ } catch {
343
+ return { mcpServers: {} };
344
+ }
345
+ }
346
+ function writeJsonFile(filePath, data) {
347
+ const dir = path6.dirname(filePath);
348
+ if (!existsSync6(dir)) {
349
+ mkdirSync2(dir, { recursive: true });
350
+ }
351
+ writeFileSync3(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
352
+ }
353
+ function ensureMcpServersShape(config) {
354
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
355
+ config.mcpServers = {};
356
+ }
357
+ return config;
358
+ }
359
+ function createAdapter(id, rulesTargetPath, rulesTemplateName, mcpConfigPath) {
360
+ return {
361
+ id,
362
+ rulesTargetPath,
363
+ rulesTemplateName,
364
+ mcpConfigPath,
365
+ getGitNexusMcpEntry() {
366
+ return { ...GITNEXUS_SNIPPET };
367
+ },
368
+ mergeGitNexusMcp(config) {
369
+ const base = ensureMcpServersShape({ ...config });
370
+ const servers = base.mcpServers;
371
+ if (servers.gitnexus) {
372
+ return base;
373
+ }
374
+ return deepmerge(base, { mcpServers: GITNEXUS_SNIPPET });
375
+ },
376
+ hasGitNexusMcp(config) {
377
+ const servers = config.mcpServers;
378
+ return Boolean(servers?.gitnexus);
379
+ },
380
+ injectRules(projectRoot) {
381
+ const target = path6.join(projectRoot, rulesTargetPath);
382
+ const source = path6.join(getRulesTemplatesDir(), rulesTemplateName);
383
+ const dir = path6.dirname(target);
384
+ if (!existsSync6(dir)) {
385
+ mkdirSync2(dir, { recursive: true });
386
+ }
387
+ copyFileSync(source, target);
388
+ },
389
+ async configureMcp() {
390
+ const configPath = mcpConfigPath;
391
+ const snippet = JSON.stringify(GITNEXUS_SNIPPET, null, 2);
392
+ try {
393
+ const existing = readJsonFile(configPath);
394
+ if (this.hasGitNexusMcp(existing)) {
395
+ return { success: true, path: configPath, snippet };
396
+ }
397
+ const merged = this.mergeGitNexusMcp(existing);
398
+ writeJsonFile(configPath, merged);
399
+ return { success: true, path: configPath, snippet };
400
+ } catch {
401
+ return { success: false, path: configPath, snippet };
402
+ }
403
+ }
404
+ };
405
+ }
406
+ var cursorAdapter = createAdapter(
407
+ "cursor",
408
+ ".cursor/rules/karpathy-guidelines.mdc",
409
+ "cursor.mdc",
410
+ path6.join(homedir(), ".cursor", "mcp.json")
411
+ );
412
+ var codexAdapter = createAdapter(
413
+ "codex",
414
+ ".codex/rules/karpathy-guidelines.md",
415
+ "codex.md",
416
+ path6.join(homedir(), ".codex", "mcp.json")
417
+ );
418
+ var opencodeAdapter = createAdapter(
419
+ "opencode",
420
+ ".opencode/rules/karpathy-guidelines.md",
421
+ "opencode.md",
422
+ path6.join(homedir(), ".config", "opencode", "mcp.json")
423
+ );
424
+ var adapters = {
425
+ cursor: cursorAdapter,
426
+ codex: codexAdapter,
427
+ opencode: opencodeAdapter
428
+ };
429
+ function getAdaptersForPlatforms(platforms) {
430
+ return platforms.map((p) => adapters[p]);
431
+ }
432
+
433
+ // src/adapters/mcp-setup.ts
434
+ import chalk from "chalk";
435
+ import readline from "readline";
436
+ async function configureMcpForAdapters(adapterList, lang) {
437
+ for (const adapter of adapterList) {
438
+ const result = await adapter.configureMcp();
439
+ if (result.success) {
440
+ console.log(chalk.green(tf("mcp.writeSuccess", lang, { path: result.path })));
441
+ } else {
442
+ await promptManualMcpConfig(adapter, lang, result.path, result.snippet);
443
+ }
444
+ }
445
+ }
446
+ async function promptManualMcpConfig(_adapter, lang, configPath, snippet) {
447
+ const border = "\u2550".repeat(60);
448
+ console.log(chalk.yellow(`
449
+ ${border}`));
450
+ console.log(chalk.yellow.bold(tf("mcp.manualTitle", lang, { path: configPath })));
451
+ console.log(chalk.cyan(snippet));
452
+ console.log(chalk.yellow(`${border}
453
+ `));
454
+ console.log(chalk.gray(tf("mcp.pressKey", lang)));
455
+ await new Promise((resolve) => {
456
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
457
+ process.stdin.setRawMode?.(true);
458
+ process.stdin.resume();
459
+ process.stdin.once("data", () => {
460
+ rl.close();
461
+ process.stdin.setRawMode?.(false);
462
+ resolve();
463
+ });
464
+ });
465
+ }
466
+
467
+ // src/cli/commands/init.ts
468
+ async function selectPlatforms(mode, lang) {
469
+ if (mode === "all") {
470
+ return ["cursor", "codex", "opencode"];
471
+ }
472
+ if (mode === "auto") {
473
+ const detected = detectPlatformsInProject();
474
+ if (detected.length > 0) return detected;
475
+ return ["cursor", "codex", "opencode"];
476
+ }
477
+ const { platforms } = await inquirer.prompt([
478
+ {
479
+ type: "checkbox",
480
+ name: "platforms",
481
+ message: t("init.selectPlatforms", lang),
482
+ choices: [
483
+ { name: "Cursor", value: "cursor" },
484
+ { name: "Codex", value: "codex" },
485
+ { name: "OpenCode", value: "opencode" }
486
+ ],
487
+ validate: (v) => v.length > 0 ? true : t("init.selectAtLeastOne", lang)
488
+ }
489
+ ]);
490
+ return platforms;
491
+ }
492
+ async function selectInjectMode(lang) {
493
+ const { mode } = await inquirer.prompt([
494
+ {
495
+ type: "list",
496
+ name: "mode",
497
+ message: t("init.injectModeQuestion", lang),
498
+ choices: [
499
+ { name: t("init.injectModeManual", lang), value: "manual" },
500
+ { name: t("init.injectModeAuto", lang), value: "auto" },
501
+ { name: t("init.injectModeAll", lang), value: "all" }
502
+ ]
503
+ }
504
+ ]);
505
+ return mode;
506
+ }
507
+ async function runInit(options) {
508
+ const projectRoot = process.cwd();
509
+ const existingConfig = await loadConfig(projectRoot);
510
+ const language = resolveLanguage(options.lang, existingConfig.language);
511
+ const cometVersion = await detectComet();
512
+ if (!cometVersion) {
513
+ console.error(chalk2.red(t("error.cometMissing", language)));
514
+ process.exit(1);
515
+ }
516
+ if (!detectKarpathyRules(projectRoot)) {
517
+ console.error(chalk2.red(t("error.karpathyMissing", language)));
518
+ process.exit(1);
519
+ }
520
+ const gitnexusVersion = await detectGitNexus();
521
+ const gitnexusInstalled = Boolean(gitnexusVersion);
522
+ if (!gitnexusInstalled) {
523
+ console.log(chalk2.gray(t("hint.gitnexusOptional", language)));
524
+ }
525
+ const injectMode = await selectInjectMode(language);
526
+ const selectedPlatforms = await selectPlatforms(injectMode, language);
527
+ const adapterList = getAdaptersForPlatforms(selectedPlatforms);
528
+ for (const adapter of adapterList) {
529
+ adapter.injectRules(projectRoot);
530
+ console.log(chalk2.green(tf("init.rulesInjected", language, { path: adapter.rulesTargetPath })));
531
+ }
532
+ if (gitnexusInstalled) {
533
+ await configureMcpForAdapters(adapterList, language);
534
+ }
535
+ const meta = detectProjectMeta(projectRoot);
536
+ generateAgentsMd(meta, language, projectRoot);
537
+ console.log(chalk2.green(t("init.agentsGenerated", language)));
538
+ const config = mergeConfig({
539
+ ...existingConfig,
540
+ language,
541
+ tools: selectedPlatforms
542
+ });
543
+ writeDefaultConfig(projectRoot, config);
544
+ console.log(chalk2.blue(t("init.runningComet", language)));
545
+ await execa2("comet", ["init"], { stdio: "inherit", cwd: projectRoot });
546
+ console.log(chalk2.green.bold(t("init.success", language)));
547
+ console.log(chalk2.cyan(t("init.nextStep", language)));
548
+ }
549
+
550
+ // src/cli/commands/update.ts
551
+ import { execa as execa3 } from "execa";
552
+ async function runUpdate() {
553
+ await execa3("npm", ["update", "-g", "@nick848/ft"], { stdio: "inherit" });
554
+ }
555
+
556
+ // src/cli/commands/version.ts
557
+ import { readFileSync as readFileSync7 } from "fs";
558
+ import path7 from "path";
559
+ function getFtVersion() {
560
+ try {
561
+ const pkgPath = path7.join(getPackageRoot(), "package.json");
562
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
563
+ return pkg.version ?? "unknown";
564
+ } catch {
565
+ return "unknown";
566
+ }
567
+ }
568
+ async function runVersion() {
569
+ const ftVersion = getFtVersion();
570
+ const cometVersion = await detectComet();
571
+ const gitnexusVersion = await detectGitNexus();
572
+ console.log(`FT: ${ftVersion}`);
573
+ console.log(`Comet: ${cometVersion ?? "not installed"}`);
574
+ console.log(`GitNexus: ${gitnexusVersion ?? "not installed"}`);
575
+ }
576
+
577
+ // src/cli/commands/help.ts
578
+ function printHelp() {
579
+ console.log(`
580
+ FT (Frontend Toolkit) \u2014 CLI orchestration for Comet, Karpathy rules, and GitNexus
581
+
582
+ Usage:
583
+ ft init [--lang en|zh-CN] Full project initialization pipeline
584
+ ft update Update @nick848/ft globally
585
+ ft version Show FT, Comet, and GitNexus versions
586
+ ft help Show this help
587
+ ft slash <command> [args] Run IDE slash command (internal)
588
+
589
+ Slash commands (use via IDE, maps to ft slash):
590
+ fill-context Output prompt recipe to fill AGENTS.md
591
+ open Comet open (passthrough)
592
+ design Comet design (passthrough)
593
+ build Comet build (passthrough)
594
+ verify Comet verify (passthrough)
595
+ archive Comet archive (passthrough)
596
+ hotfix Comet hotfix (passthrough)
597
+ tweak Comet tweak (passthrough)
598
+ graph-setup GitNexus install guide + MCP retry
599
+ graph-init gitnexus analyze
600
+ graph-refresh gitnexus analyze --force
601
+ graph-handoff Graph summary + prompt + AGENTS timestamp
602
+ graph-status Check GitNexus installation and graph state
603
+ `);
604
+ }
605
+
606
+ // src/cli/commands/slash.ts
607
+ import chalk7 from "chalk";
608
+
609
+ // src/slash/fill-context.ts
610
+ import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
611
+ import path8 from "path";
612
+ var PLACEHOLDER = "[NEEDS LLM INPUT]";
613
+ var SECTION_HINTS = {
614
+ "\u7ED3\u6784": { zh: "\u8BF7\u904D\u5386 src/ \u751F\u6210\u6811\u5F62\u76EE\u5F55\u7ED3\u6784", en: "Traverse src/ and produce a tree directory structure" },
615
+ "Structure": { zh: "\u8BF7\u904D\u5386 src/ \u751F\u6210\u6811\u5F62\u76EE\u5F55\u7ED3\u6784", en: "Traverse src/ and produce a tree directory structure" },
616
+ "\u89C4\u8303": { zh: "\u8BF7\u6839\u636E .eslintrc / prettier \u7B49\u914D\u7F6E\u6587\u4EF6\u603B\u7ED3\u4EE3\u7801\u89C4\u8303", en: "Summarize code conventions from eslint/prettier configs" },
617
+ "Conventions": { zh: "\u8BF7\u6839\u636E .eslintrc / prettier \u7B49\u914D\u7F6E\u6587\u4EF6\u603B\u7ED3\u4EE3\u7801\u89C4\u8303", en: "Summarize code conventions from eslint/prettier configs" },
618
+ "\u8DEF\u7531": { zh: "\u8BF7\u6839\u636E\u8DEF\u7531\u914D\u7F6E\u6587\u4EF6\u5217\u51FA\u4E3B\u8981\u8DEF\u7531", en: "List main routes from routing config" },
619
+ "Routing": { zh: "\u8BF7\u6839\u636E\u8DEF\u7531\u914D\u7F6E\u6587\u4EF6\u5217\u51FA\u4E3B\u8981\u8DEF\u7531", en: "List main routes from routing config" }
620
+ };
621
+ function extractSections(content) {
622
+ const lines = content.split("\n");
623
+ const results = [];
624
+ let currentSection = "";
625
+ for (let i = 0; i < lines.length; i++) {
626
+ const line = lines[i];
627
+ if (line.startsWith("## ")) {
628
+ currentSection = line.replace(/^##\s+/, "").trim();
629
+ }
630
+ if (line.includes(PLACEHOLDER)) {
631
+ results.push({ section: currentSection, line: i + 1 });
632
+ }
633
+ }
634
+ return results;
635
+ }
636
+ function findSectionInBackup(section, backup) {
637
+ if (!section) return "";
638
+ const regex = new RegExp(`## ${section}[\\s\\n]+([\\s\\S]*?)(?=\\n## |$)`, "m");
639
+ const match = backup.match(regex);
640
+ return match?.[1]?.trim() ?? "";
641
+ }
642
+ async function runFillContext(config) {
643
+ const projectRoot = process.cwd();
644
+ const agentsPath = path8.join(projectRoot, "AGENTS.md");
645
+ const lang = config.language;
646
+ if (!existsSync7(agentsPath)) {
647
+ console.log(t("fillContext.noAgents", lang));
648
+ return;
649
+ }
650
+ const agentsContent = readFileSync8(agentsPath, "utf-8");
651
+ const placeholders = extractSections(agentsContent);
652
+ const backupPath = path8.join(projectRoot, "AGENTS-BAK.md");
653
+ const backupContent = existsSync7(backupPath) ? readFileSync8(backupPath, "utf-8") : "";
654
+ const langLabel = lang === "en" ? "English" : "\u7B80\u4F53\u4E2D\u6587";
655
+ console.log(`# ${t("fillContext.title", lang)}
656
+ `);
657
+ console.log(t("fillContext.goal", lang));
658
+ console.log(`
659
+ **${t("fillContext.langConstraint", lang)}**: ${langLabel}
660
+ `);
661
+ if (placeholders.length === 0) {
662
+ console.log(t("fillContext.noPlaceholders", lang));
663
+ return;
664
+ }
665
+ console.log(`## ${t("fillContext.taskList", lang)}
666
+ `);
667
+ for (const { section, line } of placeholders) {
668
+ const hint = SECTION_HINTS[section];
669
+ const guide = lang === "en" ? hint?.en ?? "Replace placeholder with accurate project-specific content" : hint?.zh ?? "\u5C06\u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u51C6\u786E\u7684\u9879\u76EE\u76F8\u5173\u5185\u5BB9";
670
+ console.log(`### ${section || "General"} (line ${line})`);
671
+ console.log(`- **${t("fillContext.placeholder", lang)}**: ${PLACEHOLDER}`);
672
+ console.log(`- **${t("fillContext.guide", lang)}**: ${guide}`);
673
+ if (backupContent && section) {
674
+ const ref = findSectionInBackup(section, backupContent);
675
+ if (ref) {
676
+ console.log(`- **${t("fillContext.reference", lang)}**:
677
+ `);
678
+ console.log(ref);
679
+ }
680
+ }
681
+ console.log("");
682
+ }
683
+ if (backupContent) {
684
+ console.log(`## ${t("fillContext.backupNote", lang)}
685
+ `);
686
+ console.log(t("fillContext.backupMerge", lang));
687
+ }
688
+ }
689
+
690
+ // src/core/rtk-bridge.ts
691
+ import { execa as execa4 } from "execa";
692
+
693
+ // src/core/rtk.ts
694
+ var HEAD_LINES = 200;
695
+ var TAIL_LINES = 100;
696
+ function shouldBypassRtk(commandArgs, config) {
697
+ const joined = commandArgs.join(" ");
698
+ return config.rtk.exclude_patterns.some((pattern) => joined.includes(pattern));
699
+ }
700
+ function truncateMiddle(text, headLines = HEAD_LINES, tailLines = TAIL_LINES) {
701
+ const lines = text.split("\n");
702
+ const total = lines.length;
703
+ const threshold = headLines + tailLines;
704
+ if (total <= threshold) {
705
+ return text;
706
+ }
707
+ const omitted = total - headLines - tailLines;
708
+ const head = lines.slice(0, headLines);
709
+ const tail = lines.slice(-tailLines);
710
+ const marker = `...... (\u5DF2\u7701\u7565 ${omitted} \u884C) ......`;
711
+ return [...head, marker, ...tail].join("\n");
712
+ }
713
+ function compressOutputIfNeeded(text, config, rtkDisabled) {
714
+ if (rtkDisabled) return text;
715
+ const lines = text.split("\n");
716
+ const shouldCompress = config.rtk.enabled || lines.length > config.rtk.auto_trigger_lines;
717
+ if (!shouldCompress) return text;
718
+ return truncateMiddle(text);
719
+ }
720
+
721
+ // src/core/rtk-bridge.ts
722
+ async function runWithRtk(command, args, config, options = {}) {
723
+ const bypass = shouldBypassRtk(args, config);
724
+ const ttyPassthrough = options.inheritStdio ?? false;
725
+ const rtkOff = options.rtkDisabled || bypass || ttyPassthrough;
726
+ if (ttyPassthrough) {
727
+ const subprocess = execa4(command, args, {
728
+ cwd: options.cwd,
729
+ stdio: "inherit",
730
+ reject: false
731
+ });
732
+ const result2 = await subprocess;
733
+ return {
734
+ stdout: "",
735
+ stderr: "",
736
+ exitCode: result2.exitCode ?? (result2.failed ? 1 : 0)
737
+ };
738
+ }
739
+ const result = await execa4(command, args, {
740
+ cwd: options.cwd,
741
+ reject: false,
742
+ all: true
743
+ });
744
+ const combined = result.all ?? `${result.stdout}
745
+ ${result.stderr}`;
746
+ const processed = compressOutputIfNeeded(combined, config, rtkOff);
747
+ if (processed !== combined) {
748
+ process.stdout.write(processed);
749
+ if (!processed.endsWith("\n")) process.stdout.write("\n");
750
+ } else {
751
+ if (result.stdout) process.stdout.write(result.stdout);
752
+ if (result.stderr) process.stderr.write(result.stderr);
753
+ }
754
+ return {
755
+ stdout: result.stdout,
756
+ stderr: result.stderr,
757
+ exitCode: result.exitCode ?? 1
758
+ };
759
+ }
760
+ async function runCometPassthrough(subcommand, extraArgs, config, options = {}) {
761
+ const args = [subcommand, ...extraArgs];
762
+ const isTTY = isStdoutTTY() && isStdinTTY();
763
+ if (isTTY) {
764
+ const result = await runWithRtk("comet", args, config, {
765
+ ...options,
766
+ inheritStdio: true,
767
+ rtkDisabled: true
768
+ });
769
+ return result.exitCode;
770
+ }
771
+ const withNonInteractive = ["--non-interactive", ...args];
772
+ try {
773
+ const result = await runWithRtk("comet", withNonInteractive, config, options);
774
+ if (result.exitCode === 0) return 0;
775
+ const retry = await runWithRtk("comet", args, config, options);
776
+ if (retry.exitCode !== 0) {
777
+ throw new Error("NON_INTERACTIVE_FAILED");
778
+ }
779
+ return 0;
780
+ } catch (err) {
781
+ if (err.message?.includes("ENOENT")) {
782
+ throw new Error("COMET_NOT_FOUND");
783
+ }
784
+ throw err;
785
+ }
786
+ }
787
+ function isStdinTTY() {
788
+ return Boolean(process.stdin.isTTY);
789
+ }
790
+
791
+ // src/slash/comet-passthrough.ts
792
+ import chalk3 from "chalk";
793
+ var COMET_MAP = {
794
+ open: "open",
795
+ design: "design",
796
+ build: "build",
797
+ verify: "verify",
798
+ archive: "archive",
799
+ hotfix: "hotfix",
800
+ tweak: "tweak"
801
+ };
802
+ async function runCometSlash(subcommand, args, config) {
803
+ const mapped = COMET_MAP[subcommand];
804
+ if (!mapped) return 1;
805
+ try {
806
+ return await runCometPassthrough(mapped, args, config);
807
+ } catch (err) {
808
+ const message = err.message;
809
+ if (message === "NON_INTERACTIVE_FAILED") {
810
+ console.error(chalk3.red(t("error.cometNonInteractive", config.language)));
811
+ console.error(chalk3.yellow(t("error.cometUseTerminal", config.language)));
812
+ return 1;
813
+ }
814
+ console.error(chalk3.red(String(err)));
815
+ return 1;
816
+ }
817
+ }
818
+
819
+ // src/slash/graph-setup.ts
820
+ import chalk4 from "chalk";
821
+ async function runGraphSetup(config) {
822
+ const lang = config.language;
823
+ const installed = await detectGitNexus();
824
+ if (!installed) {
825
+ console.log(chalk4.yellow(t("graph.setupGuide", lang)));
826
+ console.log(chalk4.cyan("npm install -g gitnexus"));
827
+ } else {
828
+ console.log(chalk4.green(t("graph.alreadyInstalled", lang)));
829
+ }
830
+ const adapterList = getAdaptersForPlatforms(config.tools);
831
+ await configureMcpForAdapters(adapterList, lang);
832
+ }
833
+
834
+ // src/slash/graph-init.ts
835
+ async function runGraphInit(config) {
836
+ const result = await runWithRtk("gitnexus", ["analyze"], config);
837
+ process.exit(result.exitCode);
838
+ }
839
+
840
+ // src/slash/graph-refresh.ts
841
+ async function runGraphRefresh(config) {
842
+ const result = await runWithRtk("gitnexus", ["analyze", "--force"], config);
843
+ process.exit(result.exitCode);
844
+ }
845
+
846
+ // src/slash/graph-handoff.ts
847
+ import chalk5 from "chalk";
848
+ async function runGraphHandoff(config) {
849
+ const lang = config.language;
850
+ console.log(chalk5.bold(`## A. ${t("graph.handoffSummary", lang)}
851
+ `));
852
+ const summary = await runWithRtk("gitnexus", ["query", "--summary"], config);
853
+ if (summary.exitCode !== 0) {
854
+ console.log(chalk5.yellow(t("graph.summaryFailed", lang)));
855
+ }
856
+ console.log(chalk5.bold(`
857
+ ## B. ${t("graph.handoffPrompt", lang)}
858
+ `));
859
+ console.log(t("graph.handoffPromptBody", lang));
860
+ console.log(chalk5.bold(`
861
+ ## C. ${t("graph.handoffTimestamp", lang)}
862
+ `));
863
+ updateGraphTimestampInAgents();
864
+ console.log(chalk5.green(t("graph.timestampUpdated", lang)));
865
+ }
866
+
867
+ // src/slash/graph-status.ts
868
+ import chalk6 from "chalk";
869
+ async function runGraphStatus(config) {
870
+ const lang = config.language;
871
+ const version = await detectGitNexus();
872
+ const graphExists = gitNexusGraphExists();
873
+ const indexCount = countGitNexusIndexFiles();
874
+ console.log(chalk6.bold(t("graph.statusTitle", lang)));
875
+ console.log(`${t("graph.statusInstalled", lang)}: ${version ? chalk6.green(version) : chalk6.red("no")}`);
876
+ console.log(`${t("graph.statusGraph", lang)}: ${graphExists ? chalk6.green("yes") : chalk6.yellow("no")}`);
877
+ console.log(`${t("graph.statusIndexFiles", lang)}: ${indexCount}`);
878
+ }
879
+
880
+ // src/cli/commands/slash.ts
881
+ var COMET_COMMANDS = [
882
+ "open",
883
+ "design",
884
+ "build",
885
+ "verify",
886
+ "archive",
887
+ "hotfix",
888
+ "tweak"
889
+ ];
890
+ var GRAPH_COMMANDS = [
891
+ "graph-setup",
892
+ "graph-init",
893
+ "graph-refresh",
894
+ "graph-handoff",
895
+ "graph-status"
896
+ ];
897
+ var ALL_COMMANDS = [
898
+ "fill-context",
899
+ ...COMET_COMMANDS,
900
+ ...GRAPH_COMMANDS
901
+ ];
902
+ function gateCometCommands(projectRoot, lang) {
903
+ if (agentsMdHasPendingPlaceholders(projectRoot)) {
904
+ console.error(chalk7.yellow(t("gate.fillContextFirst", lang)));
905
+ process.exit(1);
906
+ }
907
+ }
908
+ async function runSlash(command, args) {
909
+ if (!ALL_COMMANDS.includes(command)) {
910
+ console.error(chalk7.red(`Unknown slash command: ${command}`));
911
+ process.exit(1);
912
+ }
913
+ const projectRoot = process.cwd();
914
+ const config = await loadConfig(projectRoot);
915
+ const lang = config.language;
916
+ const slashCmd = command;
917
+ if (COMET_COMMANDS.includes(slashCmd)) {
918
+ gateCometCommands(projectRoot, lang);
919
+ const exitCode = await runCometSlash(slashCmd, args, config);
920
+ process.exit(exitCode);
921
+ }
922
+ switch (slashCmd) {
923
+ case "fill-context":
924
+ await runFillContext(config);
925
+ break;
926
+ case "graph-setup":
927
+ await runGraphSetup(config);
928
+ break;
929
+ case "graph-init":
930
+ await runGraphInit(config);
931
+ break;
932
+ case "graph-refresh":
933
+ await runGraphRefresh(config);
934
+ break;
935
+ case "graph-handoff":
936
+ await runGraphHandoff(config);
937
+ break;
938
+ case "graph-status":
939
+ await runGraphStatus(config);
940
+ break;
941
+ default:
942
+ process.exit(1);
943
+ }
944
+ }
945
+
946
+ // src/cli/index.ts
947
+ var program = new Command();
948
+ program.name("ft").description("Frontend Toolkit \u2014 orchestration layer for Comet and GitNexus").version("0.1.0");
949
+ program.command("init").description("Full project initialization pipeline").option("--lang <lang>", "Language: zh-CN or en").action(async (options) => {
950
+ await runInit({ lang: options.lang });
951
+ });
952
+ program.command("update").description("Update @nick848/ft globally").action(async () => {
953
+ await runUpdate();
954
+ });
955
+ program.command("version").description("Show FT, Comet, and GitNexus versions").action(async () => {
956
+ await runVersion();
957
+ });
958
+ program.command("help").description("Show help").action(() => {
959
+ printHelp();
960
+ });
961
+ program.command("slash").description("Run IDE slash command").argument("<command>", "Slash subcommand name").argument("[args...]", "Additional arguments").action(async (command, args) => {
962
+ await runSlash(command, args);
963
+ });
964
+ program.parseAsync(process.argv).catch((err) => {
965
+ console.error(err);
966
+ process.exit(1);
967
+ });
968
+ //# sourceMappingURL=index.js.map