@laivc/laicode 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -7
- package/bin/laicode.js +179 -7
- package/lib/adapters.js +297 -10
- package/package.json +1 -1
- package/scripts/smoke-test.js +116 -5
package/README.md
CHANGED
|
@@ -16,15 +16,21 @@ Network and model calls use TTY-aware loading animations. JSON output and non-in
|
|
|
16
16
|
```bash
|
|
17
17
|
laicode # interactive console
|
|
18
18
|
laicode --fancy # arrow-key console where supported
|
|
19
|
+
laicode tools # AI tool scan, install, configure, uninstall, reinstall
|
|
20
|
+
laicode tools install --tool codex
|
|
21
|
+
laicode tools install --tool opencode
|
|
22
|
+
laicode tools reinstall --tool claude
|
|
23
|
+
laicode tools configure --tool codex --apply
|
|
24
|
+
laicode init --tool codex # preview a one-click Codex profile
|
|
25
|
+
laicode init --tool codex --apply
|
|
26
|
+
laicode init --tool opencode
|
|
27
|
+
laicode init --tool vscode # guide VS Code extension routes
|
|
28
|
+
laicode rollback # undo the latest Laicode config write
|
|
19
29
|
laicode brand # brand mark and palette preview
|
|
20
30
|
laicode status # live cockpit snapshot
|
|
21
31
|
laicode status --refresh # bypass the short dashboard cache
|
|
22
32
|
laicode commands # command catalog
|
|
23
33
|
laicode completion bash # print shell completion script
|
|
24
|
-
laicode tools # inspect local AI tools
|
|
25
|
-
laicode init --tool codex # preview a one-click Codex profile
|
|
26
|
-
laicode init --tool codex --apply
|
|
27
|
-
laicode rollback # undo the latest Laicode config write
|
|
28
34
|
laicode login
|
|
29
35
|
laicode models --online
|
|
30
36
|
laicode chat --model gpt-5.5 --message "Say hello"
|
|
@@ -37,7 +43,7 @@ laicode config
|
|
|
37
43
|
laicode logout
|
|
38
44
|
```
|
|
39
45
|
|
|
40
|
-
Local state is stored in `~/.laicode` by default. LaiCode
|
|
46
|
+
Local state is stored in `~/.laicode` by default. LaiCode previews every AI tool change before applying it, backs up Laicode-managed writes, and can roll them back.
|
|
41
47
|
When logged in, the interactive cockpit shows live account status, online model count, balance, request count, gateway URL, and npm release status. Cockpit snapshots use a short local cache so returning to the menu stays fast.
|
|
42
48
|
|
|
43
49
|
## First Run
|
|
@@ -49,6 +55,9 @@ laicode models --online
|
|
|
49
55
|
laicode doctor
|
|
50
56
|
laicode tools
|
|
51
57
|
laicode init --tool codex
|
|
58
|
+
laicode init --tool opencode
|
|
59
|
+
laicode init --tool vscode
|
|
60
|
+
laicode tools install --tool codex
|
|
52
61
|
```
|
|
53
62
|
|
|
54
63
|
`status` shows the next recommended actions when the local state is empty.
|
|
@@ -69,15 +78,27 @@ Phase 1 focuses on checking local AI tools and safely connecting them to Lai.vc.
|
|
|
69
78
|
|
|
70
79
|
```bash
|
|
71
80
|
laicode tools
|
|
81
|
+
laicode tools install --tool codex
|
|
82
|
+
laicode tools install --tool opencode
|
|
72
83
|
laicode init --tool codex
|
|
73
84
|
laicode init --tool codex --apply
|
|
74
85
|
codex --profile laicode
|
|
86
|
+
laicode init --tool opencode
|
|
87
|
+
laicode init --tool opencode --apply
|
|
88
|
+
opencode
|
|
89
|
+
laicode tools reinstall --tool codex
|
|
75
90
|
laicode rollback
|
|
76
91
|
```
|
|
77
92
|
|
|
78
|
-
|
|
93
|
+
The interactive menu puts "AI 工具一键配置" first. Inside it you can scan all tools, pick one tool, preview/apply configuration, install, uninstall, reinstall, and rollback from a nested menu instead of typing separate commands.
|
|
94
|
+
|
|
95
|
+
`tools install|uninstall|reinstall` preview shell commands by default. `--apply` executes the command. `init` previews the files it would write by default. `--apply` creates a backup under `~/.laicode/backups` before writing, and `rollback` restores the latest Laicode-managed write.
|
|
96
|
+
|
|
97
|
+
The high-priority scan order follows common AI developer tool usage: Claude Code, Codex CLI, VS Code, Cursor, OpenCode, Continue, and Cline.
|
|
98
|
+
|
|
99
|
+
Codex creates `~/.codex/laicode.config.toml` instead of modifying the main Codex config. OpenCode can create or merge `~/.config/opencode/opencode.json` with a Lai.vc OpenAI-compatible provider. Continue can create a fresh `~/.continue/config.yaml` when no existing Continue config is present.
|
|
79
100
|
|
|
80
|
-
|
|
101
|
+
VS Code is treated as an editor host, not a single API client: LaiCode guides users into Codex, Continue, or Cline routes instead of writing editor secure storage directly. Claude Code is detected and installable, but Lai.vc connection is blocked until the gateway exposes Anthropic Messages compatibility. Cursor and Cline are currently inspected with guided setup to avoid unsafe writes into app-managed secure storage.
|
|
81
102
|
|
|
82
103
|
The Codex profile uses Codex's command-backed auth to call `laicode credential api-key`, so the Lai.vc key stays in `~/.laicode` instead of being copied into Codex config.
|
|
83
104
|
|
package/bin/laicode.js
CHANGED
|
@@ -8,13 +8,15 @@ const readline = require("readline")
|
|
|
8
8
|
const { spawn } = require("child_process")
|
|
9
9
|
const {
|
|
10
10
|
applyInitPlan,
|
|
11
|
+
applyToolActionPlan,
|
|
11
12
|
buildInitPlan,
|
|
13
|
+
buildToolActionPlan,
|
|
12
14
|
detectTools,
|
|
13
15
|
rollbackLatest,
|
|
14
16
|
safePlan,
|
|
15
17
|
} = require("../lib/adapters")
|
|
16
18
|
|
|
17
|
-
const VERSION = "0.2.
|
|
19
|
+
const VERSION = "0.2.1"
|
|
18
20
|
const DEFAULT_SITE_BASE = "https://lai.vc"
|
|
19
21
|
const DEFAULT_API_BASE = "https://api.lai.vc/v1"
|
|
20
22
|
const STATE_DIR = process.env.LAICODE_HOME || path.join(os.homedir(), ".laicode")
|
|
@@ -22,14 +24,14 @@ const CONFIG_FILE = path.join(STATE_DIR, "config.json")
|
|
|
22
24
|
const DASHBOARD_CACHE_TTL_MS = 60_000
|
|
23
25
|
|
|
24
26
|
const COMMAND_SPECS = [
|
|
27
|
+
{ name: "tools", usage: "laicode tools [scan|install|uninstall|reinstall|configure] [--tool codex] [--apply] [--json]", summary: "AI 工具体检、安装、配置和维护", flags: ["scan", "install", "uninstall", "reinstall", "configure", "--tool", "--apply", "--json"] },
|
|
28
|
+
{ name: "init", usage: "laicode init [--tool codex] [--apply] [--json]", summary: "预览或应用一键接入计划", flags: ["--tool", "--model", "--apply", "--json"] },
|
|
29
|
+
{ name: "rollback", usage: "laicode rollback [--json]", summary: "回滚最近一次接入写入", flags: ["--json"] },
|
|
25
30
|
{ name: "menu", usage: "laicode menu", summary: "打开交互控制台", flags: ["--fancy"] },
|
|
26
31
|
{ name: "brand", usage: "laicode brand", summary: "预览终端字标和品牌色板", flags: ["--color", "--plain"] },
|
|
27
32
|
{ name: "status", usage: "laicode status [--refresh] [--json]", summary: "查看实时驾驶舱状态", flags: ["--refresh", "--json"] },
|
|
28
33
|
{ name: "commands", usage: "laicode commands [--json]", summary: "查看命令目录和 flag 清单", flags: ["--json"] },
|
|
29
34
|
{ name: "completion", usage: "laicode completion [bash|zsh|fish|powershell]", summary: "输出 shell 补全脚本", flags: ["bash", "zsh", "fish", "powershell"] },
|
|
30
|
-
{ name: "tools", usage: "laicode tools [--json]", summary: "检查本机 AI 工具接入状态", flags: ["--json"] },
|
|
31
|
-
{ name: "init", usage: "laicode init [--tool codex] [--apply] [--json]", summary: "预览或应用一键接入计划", flags: ["--tool", "--model", "--apply", "--json"] },
|
|
32
|
-
{ name: "rollback", usage: "laicode rollback [--json]", summary: "回滚最近一次接入写入", flags: ["--json"] },
|
|
33
35
|
{ name: "login", usage: "laicode login [--tier std] [--no-open]", summary: "通过浏览器设备授权登录", flags: ["--tier", "--no-open", "--no-key"] },
|
|
34
36
|
{ name: "doctor", usage: "laicode doctor [--model gpt-5.5] [--json]", summary: "运行本机、会话、网关诊断", flags: ["--model", "--json"] },
|
|
35
37
|
{ name: "models", usage: "laicode models [--json] [--online]", summary: "查看模型雷达和价格档位", flags: ["--json", "--online", "--category", "--vendor"] },
|
|
@@ -587,10 +589,22 @@ function supportLabel(value) {
|
|
|
587
589
|
if (value === "auto") return ui.ok("可自动接入")
|
|
588
590
|
if (value === "auto-new") return ui.ok("可创建配置")
|
|
589
591
|
if (value === "manual-merge") return ui.warn("需合并")
|
|
592
|
+
if (value === "blocked") return ui.warn("待网关")
|
|
590
593
|
return ui.dim("指引")
|
|
591
594
|
}
|
|
592
595
|
|
|
593
596
|
async function cmdTools(args = {}) {
|
|
597
|
+
const action = args._[1] || "scan"
|
|
598
|
+
if (action === "configure") {
|
|
599
|
+
return await cmdInit({ ...args, _: ["init"], tool: args.tool || args._[2] || "codex" })
|
|
600
|
+
}
|
|
601
|
+
if (["install", "uninstall", "reinstall"].includes(action)) {
|
|
602
|
+
return await cmdToolAction(action, args)
|
|
603
|
+
}
|
|
604
|
+
if (action !== "scan" && action !== "list") {
|
|
605
|
+
die("Usage: laicode tools [scan|install|uninstall|reinstall|configure] [--tool codex]")
|
|
606
|
+
}
|
|
607
|
+
|
|
594
608
|
const tools = await withSpinner("扫描本机 AI 工具", () => Promise.resolve(detectTools()), {
|
|
595
609
|
enabled: !args.json,
|
|
596
610
|
delayMs: 80,
|
|
@@ -621,6 +635,67 @@ async function cmdTools(args = {}) {
|
|
|
621
635
|
)
|
|
622
636
|
}
|
|
623
637
|
|
|
638
|
+
function printToolActionPlan(plan) {
|
|
639
|
+
printHero("AI 工具维护")
|
|
640
|
+
printPanel("动作", [
|
|
641
|
+
`工具 ${plan.tool}`,
|
|
642
|
+
`动作 ${plan.action}`,
|
|
643
|
+
`模式 ${plan.mode}`,
|
|
644
|
+
`状态 ${plan.ok ? "可执行" : "不可执行"}`,
|
|
645
|
+
`说明 ${plan.summary || plan.reason || "-"}`,
|
|
646
|
+
])
|
|
647
|
+
|
|
648
|
+
if (plan.commands?.length) {
|
|
649
|
+
console.log("")
|
|
650
|
+
printPanel("将执行的命令", plan.commands.map((command) => command.display))
|
|
651
|
+
}
|
|
652
|
+
if (plan.warnings?.length) {
|
|
653
|
+
console.log("")
|
|
654
|
+
printPanel("注意", plan.warnings)
|
|
655
|
+
}
|
|
656
|
+
if (plan.postSteps?.length) {
|
|
657
|
+
console.log("")
|
|
658
|
+
printPanel("后续", plan.postSteps)
|
|
659
|
+
}
|
|
660
|
+
if (!plan.ok && plan.reason) console.log(ui.warn(plan.reason))
|
|
661
|
+
else console.log(ui.dim(`执行命令: laicode tools ${plan.action} --tool ${plan.tool} --apply`))
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function cmdToolAction(action, args = {}) {
|
|
665
|
+
const tool = args.tool || args._[2] || "codex"
|
|
666
|
+
const apply = Boolean(args.apply)
|
|
667
|
+
const plan = buildToolActionPlan(tool, action, { env: process.env })
|
|
668
|
+
|
|
669
|
+
if (args.json && !apply) {
|
|
670
|
+
console.log(JSON.stringify({ apply, plan }, null, 2))
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!args.json) printToolActionPlan(plan)
|
|
675
|
+
if (!apply) return
|
|
676
|
+
if (!plan.ok) die(plan.reason || "当前动作不可执行。")
|
|
677
|
+
|
|
678
|
+
const result = await withSpinner(
|
|
679
|
+
`执行 ${plan.tool} ${plan.action}`,
|
|
680
|
+
() => applyToolActionPlan(plan, { stdio: args.json ? "pipe" : "inherit" }),
|
|
681
|
+
{
|
|
682
|
+
enabled: !args.json,
|
|
683
|
+
delayMs: 80,
|
|
684
|
+
}
|
|
685
|
+
)
|
|
686
|
+
if (args.json) {
|
|
687
|
+
console.log(JSON.stringify({ apply, plan, result }, null, 2))
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
console.log("")
|
|
691
|
+
printPanel("已完成", [
|
|
692
|
+
`工具 ${result.tool}`,
|
|
693
|
+
`动作 ${result.action}`,
|
|
694
|
+
`命令数 ${result.commands.length}`,
|
|
695
|
+
...(result.postSteps || []),
|
|
696
|
+
])
|
|
697
|
+
}
|
|
698
|
+
|
|
624
699
|
function printInitPlan(plan, safe) {
|
|
625
700
|
printHero(plan.ok ? "接入计划" : "接入指引")
|
|
626
701
|
printPanel("目标", [
|
|
@@ -2252,6 +2327,103 @@ async function menuInit() {
|
|
|
2252
2327
|
await pause()
|
|
2253
2328
|
}
|
|
2254
2329
|
|
|
2330
|
+
async function chooseAiTool(title = "选择 AI 工具") {
|
|
2331
|
+
const tools = detectTools()
|
|
2332
|
+
const items = tools.map((tool) => ({
|
|
2333
|
+
label: tool.name,
|
|
2334
|
+
description: `${tool.detected ? "已发现" : "未检测"} · ${tool.status}`,
|
|
2335
|
+
value: tool.id,
|
|
2336
|
+
}))
|
|
2337
|
+
items.push({ label: "返回", description: "", value: null })
|
|
2338
|
+
return selectMenu(title, items, { cancelValue: null })
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
async function maybeApplyToolCommand(action, tool) {
|
|
2342
|
+
await cmdTools({ _: ["tools", action], tool })
|
|
2343
|
+
const answer = await question(`执行 ${tool} ${action}?[y/N]: `, "n")
|
|
2344
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
2345
|
+
await cmdTools({ _: ["tools", action], tool, apply: true })
|
|
2346
|
+
}
|
|
2347
|
+
await pause()
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
async function maybeApplyToolConfig(tool) {
|
|
2351
|
+
await cmdInit({ _: ["init"], tool })
|
|
2352
|
+
const answer = await question(`应用 ${tool} 接入配置?[y/N]: `, "n")
|
|
2353
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
2354
|
+
await cmdInit({ _: ["init"], tool, apply: true })
|
|
2355
|
+
}
|
|
2356
|
+
await pause()
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
async function menuToolDetail(tool) {
|
|
2360
|
+
for (;;) {
|
|
2361
|
+
const action = await selectMenu(
|
|
2362
|
+
`管理 ${tool}`,
|
|
2363
|
+
[
|
|
2364
|
+
{ label: "预览接入配置", description: "只展示 diff/计划,不写入", value: "preview" },
|
|
2365
|
+
{ label: "应用接入配置", description: "备份后写入,可 rollback", value: "apply-config" },
|
|
2366
|
+
{ label: "安装工具", description: "预览安装命令,可确认执行", value: "install" },
|
|
2367
|
+
{ label: "卸载工具", description: "仅管理 Laicode 可控的 npm 全局安装", value: "uninstall" },
|
|
2368
|
+
{ label: "重装工具", description: "卸载后重新安装", value: "reinstall" },
|
|
2369
|
+
{ label: "查看工具矩阵", description: "回到总览表", value: "matrix" },
|
|
2370
|
+
{ label: "返回", description: "", value: null },
|
|
2371
|
+
],
|
|
2372
|
+
{ cancelValue: null }
|
|
2373
|
+
)
|
|
2374
|
+
if (!action) return
|
|
2375
|
+
if (action === "preview") {
|
|
2376
|
+
await cmdInit({ _: ["init"], tool })
|
|
2377
|
+
await pause()
|
|
2378
|
+
} else if (action === "apply-config") {
|
|
2379
|
+
await maybeApplyToolConfig(tool)
|
|
2380
|
+
} else if (action === "install" || action === "uninstall" || action === "reinstall") {
|
|
2381
|
+
await maybeApplyToolCommand(action, tool)
|
|
2382
|
+
} else if (action === "matrix") {
|
|
2383
|
+
await cmdTools({ _: ["tools"] })
|
|
2384
|
+
await pause()
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
async function menuToolAction(action) {
|
|
2390
|
+
const tool = await chooseAiTool(action === "configure" ? "选择要配置的工具" : "选择要维护的工具")
|
|
2391
|
+
if (!tool) return
|
|
2392
|
+
if (action === "configure") await maybeApplyToolConfig(tool)
|
|
2393
|
+
else await maybeApplyToolCommand(action, tool)
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
async function menuToolHub() {
|
|
2397
|
+
for (;;) {
|
|
2398
|
+
const action = await selectMenu(
|
|
2399
|
+
"AI 工具一键配置",
|
|
2400
|
+
[
|
|
2401
|
+
{ label: "体检全部工具", description: "扫描 Claude、Codex、VS Code、Cursor、OpenCode", value: "scan" },
|
|
2402
|
+
{ label: "选择工具管理", description: "进入单个工具的完整操作菜单", value: "manage" },
|
|
2403
|
+
{ label: "一键配置工具", description: "预览并应用 Lai.vc 接入配置", value: "configure" },
|
|
2404
|
+
{ label: "安装工具", description: "生成安装命令,可确认执行", value: "install" },
|
|
2405
|
+
{ label: "卸载工具", description: "卸载 Laicode 可控的安装", value: "uninstall" },
|
|
2406
|
+
{ label: "重装工具", description: "卸载并重新安装", value: "reinstall" },
|
|
2407
|
+
{ label: "回滚最近接入", description: "撤销最近一次 Laicode 写入", value: "rollback" },
|
|
2408
|
+
{ label: "返回主菜单", description: "", value: null },
|
|
2409
|
+
],
|
|
2410
|
+
{ cancelValue: null }
|
|
2411
|
+
)
|
|
2412
|
+
if (!action) return
|
|
2413
|
+
if (action === "scan") {
|
|
2414
|
+
await cmdTools({ _: ["tools"] })
|
|
2415
|
+
await pause()
|
|
2416
|
+
} else if (action === "manage") {
|
|
2417
|
+
const tool = await chooseAiTool()
|
|
2418
|
+
if (tool) await menuToolDetail(tool)
|
|
2419
|
+
} else if (action === "configure" || action === "install" || action === "uninstall" || action === "reinstall") {
|
|
2420
|
+
await menuToolAction(action)
|
|
2421
|
+
} else if (action === "rollback") {
|
|
2422
|
+
await menuRollback()
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2255
2427
|
async function menuChat() {
|
|
2256
2428
|
let cfg = await readConfig()
|
|
2257
2429
|
if (!cfg.accessToken && !cfg.apiKey) {
|
|
@@ -2374,9 +2546,8 @@ async function cmdMenu(args = {}) {
|
|
|
2374
2546
|
const action = await selectMenu(
|
|
2375
2547
|
"选择任务",
|
|
2376
2548
|
[
|
|
2549
|
+
{ label: "AI 工具一键配置", description: "体检、安装、配置、重装、卸载、回滚", value: "tool-hub" },
|
|
2377
2550
|
{ label: "开始对话", description: "流式测试单个模型", value: "chat" },
|
|
2378
|
-
{ label: "AI 工具体检", description: "检查 Codex、Cursor、Continue、Cline", value: "tools" },
|
|
2379
|
-
{ label: "一键接入", description: "预览 diff、备份、写入、可回滚", value: "init" },
|
|
2380
2551
|
{ label: "模型雷达", description: "在线模型、价格、多源、延迟", value: "models" },
|
|
2381
2552
|
{ label: "网关压测", description: "首 token、总耗时、成功率", value: "bench" },
|
|
2382
2553
|
{ label: "系统诊断", description: "会话、密钥、目录、真实调用", value: "doctor" },
|
|
@@ -2392,7 +2563,8 @@ async function cmdMenu(args = {}) {
|
|
|
2392
2563
|
)
|
|
2393
2564
|
|
|
2394
2565
|
if (!action || action === "exit") return
|
|
2395
|
-
if (action === "
|
|
2566
|
+
if (action === "tool-hub") await menuToolHub()
|
|
2567
|
+
else if (action === "login") await menuLogin()
|
|
2396
2568
|
else if (action === "tools") await menuTools()
|
|
2397
2569
|
else if (action === "init") await menuInit()
|
|
2398
2570
|
else if (action === "models") await menuModels()
|
package/lib/adapters.js
CHANGED
|
@@ -8,6 +8,33 @@ const path = require("path")
|
|
|
8
8
|
const DEFAULT_API_BASE = "https://api.lai.vc/v1"
|
|
9
9
|
const DEFAULT_MODEL = "gpt-5.5"
|
|
10
10
|
|
|
11
|
+
const TOOL_PACKAGES = {
|
|
12
|
+
claude: {
|
|
13
|
+
packageName: "@anthropic-ai/claude-code",
|
|
14
|
+
binary: "claude",
|
|
15
|
+
name: "Claude Code",
|
|
16
|
+
installNotes: ["官方支持 native installer、Homebrew、WinGet、npm;Laicode 默认使用 npm 全局安装。"],
|
|
17
|
+
},
|
|
18
|
+
codex: {
|
|
19
|
+
packageName: "@openai/codex",
|
|
20
|
+
binary: "codex",
|
|
21
|
+
name: "Codex CLI",
|
|
22
|
+
installNotes: ["官方支持 standalone、npm、Homebrew;Laicode 默认使用 npm 全局安装,便于卸载和重装。"],
|
|
23
|
+
},
|
|
24
|
+
opencode: {
|
|
25
|
+
packageName: "opencode-ai",
|
|
26
|
+
binary: "opencode",
|
|
27
|
+
name: "OpenCode",
|
|
28
|
+
installNotes: ["OpenCode 官方支持 install script、npm、Bun、pnpm、Yarn、Homebrew、Chocolatey、Scoop;Laicode 默认使用 npm 全局安装。"],
|
|
29
|
+
},
|
|
30
|
+
continue: {
|
|
31
|
+
packageName: "@continuedev/cli",
|
|
32
|
+
binary: "cn",
|
|
33
|
+
name: "Continue CLI",
|
|
34
|
+
installNotes: ["Continue CLI 安装后提供 `cn` 命令;IDE 扩展仍需要在对应编辑器内安装。"],
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
11
38
|
function homeDir(env = process.env) {
|
|
12
39
|
return env.HOME || env.USERPROFILE || os.homedir()
|
|
13
40
|
}
|
|
@@ -51,6 +78,14 @@ function continueHome(env = process.env) {
|
|
|
51
78
|
return path.join(homeDir(env), ".continue")
|
|
52
79
|
}
|
|
53
80
|
|
|
81
|
+
function claudeHome(env = process.env) {
|
|
82
|
+
return path.join(homeDir(env), ".claude")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function opencodeHome(env = process.env) {
|
|
86
|
+
return path.join(env.XDG_CONFIG_HOME || path.join(homeDir(env), ".config"), "opencode")
|
|
87
|
+
}
|
|
88
|
+
|
|
54
89
|
function platformConfigDir(appName, env = process.env) {
|
|
55
90
|
if (process.platform === "darwin") return path.join(homeDir(env), "Library", "Application Support", appName)
|
|
56
91
|
if (process.platform === "win32") return path.join(env.APPDATA || path.join(homeDir(env), "AppData", "Roaming"), appName)
|
|
@@ -61,6 +96,8 @@ function detectTools(opts = {}) {
|
|
|
61
96
|
const env = opts.env || process.env
|
|
62
97
|
const codexDir = codexHome(env)
|
|
63
98
|
const continueDir = continueHome(env)
|
|
99
|
+
const claudeDir = claudeHome(env)
|
|
100
|
+
const opencodeDir = opencodeHome(env)
|
|
64
101
|
const cursorDir = platformConfigDir("Cursor", env)
|
|
65
102
|
const codeDir = platformConfigDir("Code", env)
|
|
66
103
|
const vscodeExtensions = path.join(homeDir(env), ".vscode", "extensions")
|
|
@@ -74,6 +111,15 @@ function detectTools(opts = {}) {
|
|
|
74
111
|
fs.readdirSync(vscodeExtensions).some((name) => /(^|\.)(cline|claude-dev)/i.test(name)))
|
|
75
112
|
|
|
76
113
|
return [
|
|
114
|
+
{
|
|
115
|
+
id: "claude",
|
|
116
|
+
name: "Claude Code",
|
|
117
|
+
detected: commandExists("claude", env) || commandExists("ant", env) || exists(claudeDir) || exists(path.join(homeDir(env), ".claude.json")),
|
|
118
|
+
support: "blocked",
|
|
119
|
+
status: commandExists("claude", env) ? "已安装,接入待 Anthropic 网关" : "可安装,接入待 Anthropic 网关",
|
|
120
|
+
configPath: path.join(claudeDir, "settings.json"),
|
|
121
|
+
next: "laicode init --tool claude",
|
|
122
|
+
},
|
|
77
123
|
{
|
|
78
124
|
id: "codex",
|
|
79
125
|
name: "Codex CLI",
|
|
@@ -83,6 +129,33 @@ function detectTools(opts = {}) {
|
|
|
83
129
|
configPath: path.join(codexDir, "laicode.config.toml"),
|
|
84
130
|
next: "laicode init --tool codex --apply",
|
|
85
131
|
},
|
|
132
|
+
{
|
|
133
|
+
id: "vscode",
|
|
134
|
+
name: "VS Code",
|
|
135
|
+
detected: commandExists("code", env) || exists(codeDir),
|
|
136
|
+
support: "guide",
|
|
137
|
+
status: commandExists("code", env) || exists(codeDir) ? "编辑器已发现,请选择具体 AI 扩展接入" : "未检测到",
|
|
138
|
+
configPath: path.join(codeDir, "User"),
|
|
139
|
+
next: "laicode init --tool vscode",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "cursor",
|
|
143
|
+
name: "Cursor",
|
|
144
|
+
detected: exists(cursorDir) || commandExists("cursor", env),
|
|
145
|
+
support: "guide",
|
|
146
|
+
status: exists(cursorDir) || commandExists("cursor", env) ? "密钥在安全存储,暂不脚本写入" : "未检测到",
|
|
147
|
+
configPath: path.join(cursorDir, "User"),
|
|
148
|
+
next: "laicode init --tool cursor",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "opencode",
|
|
152
|
+
name: "OpenCode",
|
|
153
|
+
detected: commandExists("opencode", env) || exists(opencodeDir),
|
|
154
|
+
support: "auto",
|
|
155
|
+
status: exists(path.join(opencodeDir, "opencode.json")) ? "可合并 Lai.vc provider" : "可创建 Lai.vc provider",
|
|
156
|
+
configPath: path.join(opencodeDir, "opencode.json"),
|
|
157
|
+
next: "laicode init --tool opencode --apply",
|
|
158
|
+
},
|
|
86
159
|
{
|
|
87
160
|
id: "continue",
|
|
88
161
|
name: "Continue",
|
|
@@ -101,18 +174,18 @@ function detectTools(opts = {}) {
|
|
|
101
174
|
configPath: path.join(codeDir, "User"),
|
|
102
175
|
next: "laicode init --tool cline",
|
|
103
176
|
},
|
|
104
|
-
{
|
|
105
|
-
id: "cursor",
|
|
106
|
-
name: "Cursor",
|
|
107
|
-
detected: exists(cursorDir) || commandExists("cursor", env),
|
|
108
|
-
support: "guide",
|
|
109
|
-
status: exists(cursorDir) || commandExists("cursor", env) ? "密钥在安全存储,暂不脚本写入" : "未检测到",
|
|
110
|
-
configPath: path.join(cursorDir, "User"),
|
|
111
|
-
next: "laicode init --tool cursor",
|
|
112
|
-
},
|
|
113
177
|
]
|
|
114
178
|
}
|
|
115
179
|
|
|
180
|
+
function normalizeToolId(tool) {
|
|
181
|
+
const value = String(tool || "codex").toLowerCase()
|
|
182
|
+
if (value === "claude-code" || value === "claudecli") return "claude"
|
|
183
|
+
if (value === "continue-cli") return "continue"
|
|
184
|
+
if (value === "open-code" || value === "opencode-ai") return "opencode"
|
|
185
|
+
if (value === "code" || value === "visual-studio-code") return "vscode"
|
|
186
|
+
return value
|
|
187
|
+
}
|
|
188
|
+
|
|
116
189
|
function tomlString(value) {
|
|
117
190
|
return JSON.stringify(String(value))
|
|
118
191
|
}
|
|
@@ -121,6 +194,10 @@ function yamlString(value) {
|
|
|
121
194
|
return JSON.stringify(String(value))
|
|
122
195
|
}
|
|
123
196
|
|
|
197
|
+
function plainObject(value) {
|
|
198
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {}
|
|
199
|
+
}
|
|
200
|
+
|
|
124
201
|
function codexProfileContent(ctx) {
|
|
125
202
|
const model = ctx.model || DEFAULT_MODEL
|
|
126
203
|
const apiBaseUrl = ctx.apiBaseUrl || DEFAULT_API_BASE
|
|
@@ -161,6 +238,43 @@ function continueConfigContent(ctx) {
|
|
|
161
238
|
].join("\n")
|
|
162
239
|
}
|
|
163
240
|
|
|
241
|
+
function opencodeConfigContent(ctx, existing = {}) {
|
|
242
|
+
const model = ctx.model || DEFAULT_MODEL
|
|
243
|
+
const apiBaseUrl = ctx.apiBaseUrl || DEFAULT_API_BASE
|
|
244
|
+
const apiKey = ctx.apiKey || "sk-..."
|
|
245
|
+
const existingProviders = plainObject(existing.provider)
|
|
246
|
+
const existingLaivc = plainObject(existingProviders.laivc)
|
|
247
|
+
const existingOptions = plainObject(existingLaivc.options)
|
|
248
|
+
const existingModels = plainObject(existingLaivc.models)
|
|
249
|
+
const existingModel = plainObject(existingModels[model])
|
|
250
|
+
const next = {
|
|
251
|
+
"$schema": "https://opencode.ai/config.json",
|
|
252
|
+
...existing,
|
|
253
|
+
provider: {
|
|
254
|
+
...existingProviders,
|
|
255
|
+
laivc: {
|
|
256
|
+
...existingLaivc,
|
|
257
|
+
npm: "@ai-sdk/openai-compatible",
|
|
258
|
+
name: "Lai.vc",
|
|
259
|
+
options: {
|
|
260
|
+
...existingOptions,
|
|
261
|
+
baseURL: apiBaseUrl,
|
|
262
|
+
apiKey,
|
|
263
|
+
},
|
|
264
|
+
models: {
|
|
265
|
+
...existingModels,
|
|
266
|
+
[model]: {
|
|
267
|
+
...existingModel,
|
|
268
|
+
name: model,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
model: `laivc/${model}`,
|
|
274
|
+
}
|
|
275
|
+
return JSON.stringify(next, null, 2) + "\n"
|
|
276
|
+
}
|
|
277
|
+
|
|
164
278
|
function manualPlan(tool, ctx, reason, instructions) {
|
|
165
279
|
return {
|
|
166
280
|
tool,
|
|
@@ -176,7 +290,7 @@ function manualPlan(tool, ctx, reason, instructions) {
|
|
|
176
290
|
|
|
177
291
|
function buildInitPlan(tool, ctx = {}) {
|
|
178
292
|
const env = ctx.env || process.env
|
|
179
|
-
const normalized =
|
|
293
|
+
const normalized = normalizeToolId(tool)
|
|
180
294
|
const apiBaseUrl = ctx.apiBaseUrl || DEFAULT_API_BASE
|
|
181
295
|
const model = ctx.model || DEFAULT_MODEL
|
|
182
296
|
|
|
@@ -232,6 +346,58 @@ function buildInitPlan(tool, ctx = {}) {
|
|
|
232
346
|
}
|
|
233
347
|
}
|
|
234
348
|
|
|
349
|
+
if (normalized === "opencode") {
|
|
350
|
+
const target = path.join(opencodeHome(env), "opencode.json")
|
|
351
|
+
let existing = {}
|
|
352
|
+
if (exists(target)) {
|
|
353
|
+
try {
|
|
354
|
+
existing = JSON.parse(fs.readFileSync(target, "utf8"))
|
|
355
|
+
} catch {
|
|
356
|
+
return manualPlan("opencode", { ...ctx, apiBaseUrl, model }, "检测到 OpenCode 配置,但不是有效 JSON,当前版本不自动覆盖。", [
|
|
357
|
+
`打开 ${target}。`,
|
|
358
|
+
"按 OpenCode provider 文档新增 `laivc` provider。",
|
|
359
|
+
`baseURL 填 ${apiBaseUrl},npm adapter 使用 @ai-sdk/openai-compatible。`,
|
|
360
|
+
])
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
tool: "opencode",
|
|
365
|
+
ok: true,
|
|
366
|
+
mode: exists(target) ? "json-merge" : "new-config",
|
|
367
|
+
summary: "创建或合并 OpenCode Lai.vc provider。",
|
|
368
|
+
apiBaseUrl,
|
|
369
|
+
model,
|
|
370
|
+
runCommand: "opencode",
|
|
371
|
+
operations: [
|
|
372
|
+
{
|
|
373
|
+
action: "write",
|
|
374
|
+
path: target,
|
|
375
|
+
mode: 0o600,
|
|
376
|
+
description: "OpenCode Lai.vc provider",
|
|
377
|
+
content: opencodeConfigContent({ ...ctx, apiBaseUrl, model }, existing),
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (normalized === "claude") {
|
|
384
|
+
return manualPlan("claude", { ...ctx, apiBaseUrl, model }, "Claude Code 使用 Anthropic Messages API;当前 Lai.vc 只提供 OpenAI compatible /v1,不能直接接入。", [
|
|
385
|
+
"等待 Lai.vc 主站提供 Anthropic `/v1/messages` 兼容端点或官方代理决策。",
|
|
386
|
+
"届时可配置 `ANTHROPIC_BASE_URL` 指向 Lai.vc 的 Anthropic-compatible 网关。",
|
|
387
|
+
"认证可使用 `ANTHROPIC_AUTH_TOKEN` 或 `ANTHROPIC_API_KEY`,具体以主站契约为准。",
|
|
388
|
+
"当前请使用 Codex、Continue、Cursor 或 Cline 的 OpenAI compatible 接入路径测试 Lai.vc。",
|
|
389
|
+
])
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (normalized === "vscode") {
|
|
393
|
+
return manualPlan("vscode", { ...ctx, apiBaseUrl, model }, "VS Code 是编辑器宿主,本身没有统一的 OpenAI-compatible 网关配置;请选择具体扩展接入。", [
|
|
394
|
+
"优先使用 Codex CLI/扩展时,运行 `laicode init --tool codex` 创建独立 Codex profile。",
|
|
395
|
+
"使用 Continue 扩展时,运行 `laicode init --tool continue` 预览 Continue 配置。",
|
|
396
|
+
"使用 Cline 扩展时,在 Cline 设置中选择 OpenAI Compatible,并填入 Lai.vc base URL 和 API Key。",
|
|
397
|
+
"Laicode 后续会按扩展稳定配置契约逐个自动化,不直接改 VS Code 安全存储。",
|
|
398
|
+
])
|
|
399
|
+
}
|
|
400
|
+
|
|
235
401
|
if (normalized === "cline") {
|
|
236
402
|
return manualPlan("cline", { ...ctx, apiBaseUrl, model }, "Cline 设置目前由 VS Code 扩展 UI 管理,先提供可靠指引。", [
|
|
237
403
|
"打开 Cline 设置。",
|
|
@@ -254,6 +420,125 @@ function buildInitPlan(tool, ctx = {}) {
|
|
|
254
420
|
return manualPlan(normalized, { ...ctx, apiBaseUrl, model }, `暂不支持工具: ${normalized}`, ["运行 `laicode tools` 查看当前支持矩阵。"])
|
|
255
421
|
}
|
|
256
422
|
|
|
423
|
+
function commandPlan(command, args) {
|
|
424
|
+
return {
|
|
425
|
+
command,
|
|
426
|
+
args,
|
|
427
|
+
display: [command, ...args].join(" "),
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function buildToolActionPlan(tool, action, ctx = {}) {
|
|
432
|
+
const normalized = normalizeToolId(tool)
|
|
433
|
+
const normalizedAction = String(action || "scan").toLowerCase()
|
|
434
|
+
const meta = TOOL_PACKAGES[normalized]
|
|
435
|
+
const env = ctx.env || process.env
|
|
436
|
+
|
|
437
|
+
if (normalizedAction === "configure") {
|
|
438
|
+
return buildInitPlan(normalized, ctx)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (!["install", "uninstall", "reinstall"].includes(normalizedAction)) {
|
|
442
|
+
return manualPlan(normalized, ctx, `暂不支持动作: ${normalizedAction}`, [
|
|
443
|
+
"支持动作: install、uninstall、reinstall、configure。",
|
|
444
|
+
"运行 `laicode tools` 查看工具状态。",
|
|
445
|
+
])
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!meta) {
|
|
449
|
+
return manualPlan(normalized, ctx, "当前工具没有安全的自动安装/卸载策略。", [
|
|
450
|
+
"请按工具官方文档安装。",
|
|
451
|
+
"安装后运行 `laicode tools` 重新检测。",
|
|
452
|
+
])
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"
|
|
456
|
+
const hasNpm = commandExists(npmCommand, env) || commandExists("npm", env)
|
|
457
|
+
const commands = []
|
|
458
|
+
if (normalizedAction === "install") commands.push(commandPlan(npmCommand, ["install", "-g", meta.packageName]))
|
|
459
|
+
if (normalizedAction === "uninstall") commands.push(commandPlan(npmCommand, ["uninstall", "-g", meta.packageName]))
|
|
460
|
+
if (normalizedAction === "reinstall") {
|
|
461
|
+
commands.push(commandPlan(npmCommand, ["uninstall", "-g", meta.packageName]))
|
|
462
|
+
commands.push(commandPlan(npmCommand, ["install", "-g", meta.packageName]))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const postSteps = []
|
|
466
|
+
if (normalized === "codex") {
|
|
467
|
+
postSteps.push("运行 `laicode init --tool codex` 预览接入 profile。")
|
|
468
|
+
postSteps.push("应用后使用 `codex --profile laicode` 测试 Lai.vc。")
|
|
469
|
+
} else if (normalized === "continue") {
|
|
470
|
+
postSteps.push("运行 `laicode init --tool continue` 预览 Continue 配置。")
|
|
471
|
+
postSteps.push("IDE 扩展仍需在 Cursor/VS Code 扩展市场安装。")
|
|
472
|
+
} else if (normalized === "claude") {
|
|
473
|
+
postSteps.push("当前只能安装 Claude Code;接入 Lai.vc 需等待 Anthropic Messages 兼容端点。")
|
|
474
|
+
} else if (normalized === "opencode") {
|
|
475
|
+
postSteps.push("运行 `laicode init --tool opencode` 预览 OpenCode provider。")
|
|
476
|
+
postSteps.push("应用后使用 `opencode` 测试 Lai.vc。")
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
tool: normalized,
|
|
481
|
+
action: normalizedAction,
|
|
482
|
+
ok: hasNpm,
|
|
483
|
+
mode: "command",
|
|
484
|
+
summary: `${meta.name} ${normalizedAction} 计划`,
|
|
485
|
+
reason: hasNpm ? "" : "未找到 npm,无法自动执行安装/卸载命令。",
|
|
486
|
+
commands,
|
|
487
|
+
warnings: [
|
|
488
|
+
"默认只预览命令;传 `--apply` 才会执行。",
|
|
489
|
+
"卸载/重装只覆盖 npm 全局安装的包;通过 Homebrew、WinGet 或官方安装器安装的版本需用对应工具管理。",
|
|
490
|
+
...meta.installNotes,
|
|
491
|
+
],
|
|
492
|
+
postSteps,
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function runCommand(command, args, opts = {}) {
|
|
497
|
+
const { spawnSync } = require("child_process")
|
|
498
|
+
const result = spawnSync(command, args, {
|
|
499
|
+
encoding: "utf8",
|
|
500
|
+
stdio: opts.stdio || "pipe",
|
|
501
|
+
env: opts.env || process.env,
|
|
502
|
+
})
|
|
503
|
+
return {
|
|
504
|
+
command,
|
|
505
|
+
args,
|
|
506
|
+
status: result.status,
|
|
507
|
+
ok: result.status === 0,
|
|
508
|
+
stdout: result.stdout || "",
|
|
509
|
+
stderr: result.stderr || "",
|
|
510
|
+
error: result.error ? result.error.message : null,
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function applyToolActionPlan(plan, opts = {}) {
|
|
515
|
+
if (!plan.ok || !plan.commands?.length) {
|
|
516
|
+
throw new Error(plan.reason || "No commands to apply")
|
|
517
|
+
}
|
|
518
|
+
const results = []
|
|
519
|
+
for (const command of plan.commands) {
|
|
520
|
+
const result = runCommand(command.command, command.args, {
|
|
521
|
+
stdio: opts.stdio || "inherit",
|
|
522
|
+
env: opts.env || process.env,
|
|
523
|
+
})
|
|
524
|
+
results.push(result)
|
|
525
|
+
if (!result.ok) {
|
|
526
|
+
const err = new Error(`Command failed: ${command.display}`)
|
|
527
|
+
err.result = result
|
|
528
|
+
err.results = results
|
|
529
|
+
throw err
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
tool: plan.tool,
|
|
534
|
+
action: plan.action,
|
|
535
|
+
appliedAt: new Date().toISOString(),
|
|
536
|
+
commands: plan.commands,
|
|
537
|
+
results,
|
|
538
|
+
postSteps: plan.postSteps || [],
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
257
542
|
function redactText(value, secrets = []) {
|
|
258
543
|
let out = String(value || "")
|
|
259
544
|
for (const secret of secrets.filter(Boolean)) {
|
|
@@ -372,7 +657,9 @@ module.exports = {
|
|
|
372
657
|
DEFAULT_API_BASE,
|
|
373
658
|
DEFAULT_MODEL,
|
|
374
659
|
applyInitPlan,
|
|
660
|
+
applyToolActionPlan,
|
|
375
661
|
buildInitPlan,
|
|
662
|
+
buildToolActionPlan,
|
|
376
663
|
detectTools,
|
|
377
664
|
rollbackLatest,
|
|
378
665
|
safePlan,
|
package/package.json
CHANGED
package/scripts/smoke-test.js
CHANGED
|
@@ -31,7 +31,14 @@ fs.writeFileSync(
|
|
|
31
31
|
{ mode: 0o600 }
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
const env = {
|
|
34
|
+
const env = {
|
|
35
|
+
...process.env,
|
|
36
|
+
CODEX_HOME: codexHome,
|
|
37
|
+
HOME: home,
|
|
38
|
+
LAICODE_HOME: home,
|
|
39
|
+
LAICODE_PLAIN: "1",
|
|
40
|
+
XDG_CONFIG_HOME: path.join(home, ".config"),
|
|
41
|
+
}
|
|
35
42
|
|
|
36
43
|
function run(args) {
|
|
37
44
|
const result = spawnSync(process.execPath, args, { encoding: "utf8", env })
|
|
@@ -76,13 +83,13 @@ const help = run(["bin/laicode.js", "--help"])
|
|
|
76
83
|
assertIncludes(help, "laicode status [--refresh] [--json]", "help lists status")
|
|
77
84
|
assertIncludes(help, "laicode commands [--json]", "help lists commands")
|
|
78
85
|
assertIncludes(help, "laicode completion [bash|zsh|fish|powershell]", "help lists completion")
|
|
79
|
-
assertIncludes(help, "laicode tools [
|
|
86
|
+
assertIncludes(help, "laicode tools [scan|install|uninstall|reinstall|configure]", "help lists tools actions")
|
|
80
87
|
assertIncludes(help, "laicode init [--tool codex] [--apply] [--json]", "help lists init")
|
|
81
88
|
assertIncludes(help, "laicode doctor [--model gpt-5.5] [--json]", "help lists doctor json")
|
|
82
89
|
assertIncludes(help, "laicode bench [--model gpt-5.5] [--count 3] [--json]", "help lists bench json")
|
|
83
90
|
assertIncludes(help, "LAICODE_NO_SPINNER", "help lists spinner toggle")
|
|
84
|
-
assertIncludes(run(["bin/laicode.js", "--version"]), "0.2.
|
|
85
|
-
assertIncludes(run(["bin/laicode.js", "-v"]), "0.2.
|
|
91
|
+
assertIncludes(run(["bin/laicode.js", "--version"]), "0.2.1", "long version")
|
|
92
|
+
assertIncludes(run(["bin/laicode.js", "-v"]), "0.2.1", "short version")
|
|
86
93
|
assertIncludes(run(["bin/laicode.js", "--plain", "brand"]), "Lai.vc 终端字标", "plain brand command")
|
|
87
94
|
assertIncludes(run(["bin/laicode.js", "--color", "brand"]), "品牌色板", "color brand command")
|
|
88
95
|
assertIncludes(run(["bin/laicode.js", "status"]), "快照 缓存", "status uses local dashboard cache")
|
|
@@ -95,15 +102,119 @@ if (!Array.isArray(status.nextActions) || !status.nextActions.find((action) => a
|
|
|
95
102
|
console.error("Smoke assertion failed: status json includes first-run next action")
|
|
96
103
|
process.exit(1)
|
|
97
104
|
}
|
|
98
|
-
|
|
105
|
+
const commandsJson = run(["bin/laicode.js", "commands", "--json"])
|
|
106
|
+
assertIncludes(commandsJson, '"name": "commands"', "commands json")
|
|
107
|
+
const commandCatalog = parseJson(commandsJson, "commands json parses")
|
|
108
|
+
if (commandCatalog.commands[0]?.name !== "tools") {
|
|
109
|
+
console.error("Smoke assertion failed: tools command is first in command catalog")
|
|
110
|
+
process.exit(1)
|
|
111
|
+
}
|
|
99
112
|
assertIncludes(run(["bin/laicode.js", "completion", "bash"]), "complete -F _laicode_completion laicode", "bash completion")
|
|
100
113
|
assertIncludes(run(["bin/laicode.js", "completion", "fish"]), "__fish_seen_subcommand_from bench", "fish completion")
|
|
101
114
|
const toolsJson = run(["bin/laicode.js", "tools", "--json"])
|
|
102
115
|
const tools = parseJson(toolsJson, "tools json parses")
|
|
116
|
+
const toolOrder = tools.tools.map((tool) => tool.id)
|
|
117
|
+
if (toolOrder.slice(0, 5).join(",") !== "claude,codex,vscode,cursor,opencode") {
|
|
118
|
+
console.error("Smoke assertion failed: high-priority tool order")
|
|
119
|
+
console.error(toolOrder.join(","))
|
|
120
|
+
process.exit(1)
|
|
121
|
+
}
|
|
103
122
|
if (!tools.tools.find((tool) => tool.id === "codex")) {
|
|
104
123
|
console.error("Smoke assertion failed: tools json includes codex")
|
|
105
124
|
process.exit(1)
|
|
106
125
|
}
|
|
126
|
+
const vscodeTool = tools.tools.find((tool) => tool.id === "vscode")
|
|
127
|
+
if (!vscodeTool || vscodeTool.support !== "guide") {
|
|
128
|
+
console.error("Smoke assertion failed: tools json includes guided vscode support")
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
const opencodeTool = tools.tools.find((tool) => tool.id === "opencode")
|
|
132
|
+
if (!opencodeTool || opencodeTool.support !== "auto") {
|
|
133
|
+
console.error("Smoke assertion failed: tools json includes automatic opencode support")
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
|
136
|
+
const claudeTool = tools.tools.find((tool) => tool.id === "claude")
|
|
137
|
+
if (!claudeTool || claudeTool.support !== "blocked") {
|
|
138
|
+
console.error("Smoke assertion failed: tools json includes blocked claude support")
|
|
139
|
+
process.exit(1)
|
|
140
|
+
}
|
|
141
|
+
const claudeInitJson = run(["bin/laicode.js", "init", "--tool", "claude", "--json"])
|
|
142
|
+
const claudeInit = parseJson(claudeInitJson, "claude init json parses")
|
|
143
|
+
if (claudeInit.plan.ok !== false || !String(claudeInit.plan.reason || "").includes("Anthropic Messages")) {
|
|
144
|
+
console.error("Smoke assertion failed: claude init explains Anthropic compatibility gap")
|
|
145
|
+
process.exit(1)
|
|
146
|
+
}
|
|
147
|
+
const vscodeInitJson = run(["bin/laicode.js", "init", "--tool", "vscode", "--json"])
|
|
148
|
+
const vscodeInit = parseJson(vscodeInitJson, "vscode init json parses")
|
|
149
|
+
if (vscodeInit.plan.ok !== false || !String(vscodeInit.plan.reason || "").includes("VS Code 是编辑器宿主")) {
|
|
150
|
+
console.error("Smoke assertion failed: vscode init explains editor-host boundary")
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
const opencodeInitJson = run(["bin/laicode.js", "init", "--tool", "opencode", "--json"])
|
|
154
|
+
assertExcludes(opencodeInitJson, "sk-smoke-test-secret-value", "opencode init preview redacts api key")
|
|
155
|
+
const opencodeInit = parseJson(opencodeInitJson, "opencode init json parses")
|
|
156
|
+
if (
|
|
157
|
+
opencodeInit.plan.ok !== true ||
|
|
158
|
+
opencodeInit.plan.tool !== "opencode" ||
|
|
159
|
+
!opencodeInit.plan.operations.find((op) => String(op.path || "").endsWith("opencode.json"))
|
|
160
|
+
) {
|
|
161
|
+
console.error("Smoke assertion failed: opencode init creates provider plan")
|
|
162
|
+
process.exit(1)
|
|
163
|
+
}
|
|
164
|
+
const opencodePath = path.join(home, ".config", "opencode", "opencode.json")
|
|
165
|
+
fs.mkdirSync(path.dirname(opencodePath), { recursive: true, mode: 0o700 })
|
|
166
|
+
fs.writeFileSync(
|
|
167
|
+
opencodePath,
|
|
168
|
+
JSON.stringify(
|
|
169
|
+
{
|
|
170
|
+
provider: {
|
|
171
|
+
other: { name: "Other Provider" },
|
|
172
|
+
laivc: {
|
|
173
|
+
options: { customOption: "keep-me" },
|
|
174
|
+
models: { "old-model": { name: "old-model" } },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
null,
|
|
179
|
+
2
|
|
180
|
+
) + "\n",
|
|
181
|
+
{ mode: 0o600 }
|
|
182
|
+
)
|
|
183
|
+
const opencodeMergeJson = run(["bin/laicode.js", "init", "--tool", "opencode", "--json"])
|
|
184
|
+
assertExcludes(opencodeMergeJson, "sk-smoke-test-secret-value", "opencode merge preview redacts api key")
|
|
185
|
+
assertIncludes(opencodeMergeJson, "old-model", "opencode merge preserves existing models")
|
|
186
|
+
assertIncludes(opencodeMergeJson, "customOption", "opencode merge preserves existing options")
|
|
187
|
+
const opencodeMerge = parseJson(opencodeMergeJson, "opencode merge json parses")
|
|
188
|
+
if (opencodeMerge.plan.mode !== "json-merge") {
|
|
189
|
+
console.error("Smoke assertion failed: opencode merge uses json-merge mode")
|
|
190
|
+
process.exit(1)
|
|
191
|
+
}
|
|
192
|
+
const opencodeMergePreview = parseJson(opencodeMerge.plan.operations[0].preview, "opencode merge preview parses")
|
|
193
|
+
if (
|
|
194
|
+
opencodeMergePreview.provider.laivc.options.customOption !== "keep-me" ||
|
|
195
|
+
!opencodeMergePreview.provider.laivc.models["old-model"]
|
|
196
|
+
) {
|
|
197
|
+
console.error("Smoke assertion failed: opencode merge keeps existing laivc provider details")
|
|
198
|
+
process.exit(1)
|
|
199
|
+
}
|
|
200
|
+
const installCodexJson = run(["bin/laicode.js", "tools", "install", "--tool", "codex", "--json"])
|
|
201
|
+
const installCodex = parseJson(installCodexJson, "codex install plan json parses")
|
|
202
|
+
if (installCodex.plan.action !== "install" || !installCodex.plan.commands.find((cmd) => cmd.display.includes("@openai/codex"))) {
|
|
203
|
+
console.error("Smoke assertion failed: codex install plan includes npm package")
|
|
204
|
+
process.exit(1)
|
|
205
|
+
}
|
|
206
|
+
const installOpencodeJson = run(["bin/laicode.js", "tools", "install", "--tool", "opencode", "--json"])
|
|
207
|
+
const installOpencode = parseJson(installOpencodeJson, "opencode install plan json parses")
|
|
208
|
+
if (installOpencode.plan.action !== "install" || !installOpencode.plan.commands.find((cmd) => cmd.display.includes("opencode-ai"))) {
|
|
209
|
+
console.error("Smoke assertion failed: opencode install plan includes npm package")
|
|
210
|
+
process.exit(1)
|
|
211
|
+
}
|
|
212
|
+
const reinstallClaudeJson = run(["bin/laicode.js", "tools", "reinstall", "--tool", "claude", "--json"])
|
|
213
|
+
const reinstallClaude = parseJson(reinstallClaudeJson, "claude reinstall plan json parses")
|
|
214
|
+
if (reinstallClaude.plan.action !== "reinstall" || reinstallClaude.plan.commands.length !== 2) {
|
|
215
|
+
console.error("Smoke assertion failed: claude reinstall plan includes uninstall and install")
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
107
218
|
const initPreviewJson = run(["bin/laicode.js", "init", "--tool", "codex", "--json"])
|
|
108
219
|
assertExcludes(initPreviewJson, "sk-smoke-test-secret-value", "init preview redacts api key")
|
|
109
220
|
const initPreview = parseJson(initPreviewJson, "init preview json parses")
|