cline-kit 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/CHANGELOG.md +113 -0
- package/LICENSE +21 -0
- package/NOTICE +60 -0
- package/README.md +188 -0
- package/README.zh-CN.md +168 -0
- package/SECURITY.md +53 -0
- package/ckit.cmd +12 -0
- package/dictionaries/ja.json +622 -0
- package/dictionaries/ko.json +622 -0
- package/dictionaries/vi.json +622 -0
- package/dictionaries/zh-CN.json +623 -0
- package/dictionaries/zh-TW.json +623 -0
- package/package.json +66 -0
- package/src/audit.js +127 -0
- package/src/cdp.js +85 -0
- package/src/cli.js +236 -0
- package/src/config.js +108 -0
- package/src/detect.js +118 -0
- package/src/dict.js +171 -0
- package/src/doctor.js +121 -0
- package/src/engine.js +148 -0
- package/src/features/index.js +83 -0
- package/src/features/sidebar-groups.js +376 -0
- package/src/features/sidebar-groups.logic.js +145 -0
- package/src/injector.js +96 -0
- package/src/install-win.js +155 -0
- package/src/launcher.js +123 -0
- package/src/payload.js +80 -0
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cline-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Desktop enhancement kit for the Cline desktop app (Windows): keeps every registered project visible in the sidebar, and adds optional UI locale packs (zh-CN, zh-TW, ja, ko, vi). Runtime overlay - no binary patching.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cline",
|
|
7
|
+
"desktop-app",
|
|
8
|
+
"sidebar",
|
|
9
|
+
"workspace",
|
|
10
|
+
"project-groups",
|
|
11
|
+
"i18n",
|
|
12
|
+
"l10n",
|
|
13
|
+
"locale",
|
|
14
|
+
"zh-CN",
|
|
15
|
+
"zh-TW",
|
|
16
|
+
"ja",
|
|
17
|
+
"ko",
|
|
18
|
+
"vi",
|
|
19
|
+
"webview2",
|
|
20
|
+
"overlay"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "chentaoxing",
|
|
24
|
+
"notices": "See NOTICE: unofficial project; locale dictionary keys are Cline desktop UI strings (upstream cline/cline is Apache-2.0); translated values are original.",
|
|
25
|
+
"bin": {
|
|
26
|
+
"cline-kit": "src/cli.js",
|
|
27
|
+
"ckit": "src/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"main": "src/cli.js",
|
|
30
|
+
"files": [
|
|
31
|
+
"src/",
|
|
32
|
+
"ckit.cmd",
|
|
33
|
+
"dictionaries/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"README.zh-CN.md",
|
|
36
|
+
"SECURITY.md",
|
|
37
|
+
"NOTICE",
|
|
38
|
+
"CHANGELOG.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.10.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "node scripts/selftest.js",
|
|
46
|
+
"start": "node src/cli.js start",
|
|
47
|
+
"status": "node src/cli.js status",
|
|
48
|
+
"install-ui": "node src/cli.js install",
|
|
49
|
+
"uninstall-ui": "node src/cli.js uninstall",
|
|
50
|
+
"audit": "node src/cli.js audit",
|
|
51
|
+
"dict:update": "node src/cli.js update --force"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/chentaoxing/Cline-kit.git"
|
|
57
|
+
},
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/chentaoxing/Cline-kit/issues"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"registry": "https://registry.npmjs.org/",
|
|
63
|
+
"access": "public",
|
|
64
|
+
"provenance": true
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/audit.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Walk the Cline UI over CDP and list strings still left in English.
|
|
3
|
+
// Useful after a Cline update, to find dictionary gaps. Output: <configDir>/audit-report.json
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const cfg = require("./config");
|
|
7
|
+
const cdp = require("./cdp");
|
|
8
|
+
|
|
9
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
|
+
|
|
11
|
+
const COLLECT = `(() => {
|
|
12
|
+
const en = (s) => /[A-Za-z]{3}/.test(s) && !/[\\u4e00-\\u9fa5]/.test(s);
|
|
13
|
+
// A node counts as visible only if it is inside the viewport AND is the top-most element
|
|
14
|
+
// at its own centre point. This skips panels React keeps mounted but has hidden.
|
|
15
|
+
const onScreen = (el) => {
|
|
16
|
+
const r = el.getBoundingClientRect();
|
|
17
|
+
if (r.width < 1 || r.height < 1) return false;
|
|
18
|
+
if (r.bottom < 0 || r.top > innerHeight || r.right < 0 || r.left > innerWidth) return false;
|
|
19
|
+
const cs = getComputedStyle(el);
|
|
20
|
+
if (cs.visibility === 'hidden' || cs.display === 'none' || Number(cs.opacity) === 0) return false;
|
|
21
|
+
const x = Math.min(r.left + r.width / 2, innerWidth - 1);
|
|
22
|
+
const y = Math.min(r.top + r.height / 2, innerHeight - 1);
|
|
23
|
+
const hit = document.elementFromPoint(x, y);
|
|
24
|
+
if (!hit) return false;
|
|
25
|
+
return hit === el || el.contains(hit) || hit.contains(el);
|
|
26
|
+
};
|
|
27
|
+
const out = { text: [], attr: [] };
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
30
|
+
let n;
|
|
31
|
+
while ((n = w.nextNode())) {
|
|
32
|
+
const t = (n.nodeValue || '').replace(/\\s+/g, ' ').trim();
|
|
33
|
+
if (!t || !en(t) || t.length > 220) continue;
|
|
34
|
+
const el = n.parentElement; if (!el) continue;
|
|
35
|
+
if (!onScreen(el)) continue;
|
|
36
|
+
if (!seen.has('T' + t)) { seen.add('T' + t); out.text.push(t); }
|
|
37
|
+
}
|
|
38
|
+
document.querySelectorAll('*').forEach((e) => {
|
|
39
|
+
for (const a of ['placeholder', 'aria-label', 'title']) {
|
|
40
|
+
const v = e.getAttribute(a);
|
|
41
|
+
if (!v || !en(v) || v.length > 220) continue;
|
|
42
|
+
if (!onScreen(e)) continue;
|
|
43
|
+
if (!seen.has('A' + a + v)) { seen.add('A' + a + v); out.attr.push(a + '::' + v); }
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return JSON.stringify(out);
|
|
47
|
+
})()`;
|
|
48
|
+
|
|
49
|
+
function click(cands) {
|
|
50
|
+
return `(() => {
|
|
51
|
+
const want = ${JSON.stringify(cands)};
|
|
52
|
+
const els = [...document.querySelectorAll('button,[role=button],[role=tab],[role=combobox],a,summary,[role=menuitem]')];
|
|
53
|
+
for (const target of want) {
|
|
54
|
+
const hit = els.find((e) => {
|
|
55
|
+
const t = (e.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
56
|
+
const al = e.getAttribute('aria-label') || '';
|
|
57
|
+
return t === target || al === target || t.startsWith(target + ' ') || al.startsWith(target);
|
|
58
|
+
});
|
|
59
|
+
if (hit) { hit.click(); return 'clicked:' + target; }
|
|
60
|
+
}
|
|
61
|
+
return 'MISS:' + want.join('|');
|
|
62
|
+
})()`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const SCREENS = [
|
|
66
|
+
["settings.general", ["Settings", "设置"], null],
|
|
67
|
+
["settings.apiProviders", null, ["General", "通用"]],
|
|
68
|
+
["settings.voice", null, ["API Providers", "API 服务商"]],
|
|
69
|
+
["settings.import", null, ["Voice", "语音"]],
|
|
70
|
+
["settings.remote", null, ["Import", "导入"]],
|
|
71
|
+
["settings.account", null, ["Remote", "远程"]],
|
|
72
|
+
["home", null, ["Account", "账户"]],
|
|
73
|
+
["picker.workspace", ["Session", "会话"], null],
|
|
74
|
+
["page.schedule", null, ["Schedule", "计划任务"]],
|
|
75
|
+
["page.customize", null, ["Customize", "扩展定制"]],
|
|
76
|
+
["home.final", null, ["Session", "会话"]]
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
async function run(port) {
|
|
80
|
+
const results = {};
|
|
81
|
+
const pages = await cdp.pageTargets(port);
|
|
82
|
+
if (!pages.length) throw new Error("no Cline page target on port " + port);
|
|
83
|
+
const ws = await cdp.open(pages[0].webSocketDebuggerUrl);
|
|
84
|
+
const api = cdp.client(ws);
|
|
85
|
+
await api.rpc("Runtime.enable");
|
|
86
|
+
const ev = async (expr) => {
|
|
87
|
+
const r = await api.rpc("Runtime.evaluate", { expression: expr, returnByValue: true });
|
|
88
|
+
return r && r.result ? r.result.value : null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const [name, nav, sub] of SCREENS) {
|
|
92
|
+
if (nav) await ev(click(nav));
|
|
93
|
+
if (sub) await ev(click(sub));
|
|
94
|
+
await sleep(800);
|
|
95
|
+
const raw = await ev(COLLECT);
|
|
96
|
+
results[name] = raw ? JSON.parse(raw) : { text: [], attr: [] };
|
|
97
|
+
}
|
|
98
|
+
api.close();
|
|
99
|
+
|
|
100
|
+
cfg.ensureDirs();
|
|
101
|
+
const file = path.join(cfg.configDir(), "audit-report.json");
|
|
102
|
+
fs.writeFileSync(file, JSON.stringify(results, null, 1), "utf8");
|
|
103
|
+
|
|
104
|
+
// subtract what the dictionary already covers, so the output is only real gaps
|
|
105
|
+
const dict = require("./dict").load(cfg.read());
|
|
106
|
+
const covered = new Set(Object.keys(dict.entries));
|
|
107
|
+
const ruleRes = (dict.rules || []).map((r) => new RegExp(r.pattern));
|
|
108
|
+
const uniq = new Map();
|
|
109
|
+
for (const [screen, v] of Object.entries(results)) {
|
|
110
|
+
for (const item of [...(v.attr || []), ...(v.text || [])]) {
|
|
111
|
+
const bare = item.replace(/^(title|aria-label|placeholder)::/, "");
|
|
112
|
+
if (covered.has(bare) || ruleRes.some((re) => re.test(bare))) continue;
|
|
113
|
+
if (!uniq.has(bare)) uniq.set(bare, screen);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { file, total: uniq.size, items: [...uniq.entries()] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { run };
|
|
120
|
+
|
|
121
|
+
if (require.main === module) {
|
|
122
|
+
const port = Number(process.argv[2] || cfg.read().port);
|
|
123
|
+
run(port).then((r) => {
|
|
124
|
+
console.log("untranslated strings: " + r.total + " (report: " + r.file + ")");
|
|
125
|
+
r.items.forEach(([s, where]) => console.log(" " + s + " [" + where + "]"));
|
|
126
|
+
}).catch((e) => { console.error("audit failed: " + e.message); process.exit(1); });
|
|
127
|
+
}
|
package/src/cdp.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Minimal Chrome DevTools Protocol client (Node >= 20.10: global fetch + WebSocket).
|
|
3
|
+
const http = require("http");
|
|
4
|
+
|
|
5
|
+
function listTargets(port) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const req = http.get({ host: "127.0.0.1", port, path: "/json/list", timeout: 3000 }, (res) => {
|
|
8
|
+
let body = "";
|
|
9
|
+
res.on("data", (c) => { body += c; });
|
|
10
|
+
res.on("end", () => {
|
|
11
|
+
if (res.statusCode !== 200) return reject(new Error("CDP HTTP " + res.statusCode));
|
|
12
|
+
try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
req.on("error", reject);
|
|
16
|
+
req.on("timeout", () => { req.destroy(new Error("CDP timeout")); });
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pageTargets(port) {
|
|
21
|
+
return listTargets(port).then((list) =>
|
|
22
|
+
list.filter((t) => t.type === "page" && t.webSocketDebuggerUrl));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function open(url) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const ws = new WebSocket(url);
|
|
28
|
+
const t = setTimeout(() => { try { ws.close(); } catch (e) { } reject(new Error("ws timeout")); }, 5000);
|
|
29
|
+
ws.onopen = () => { clearTimeout(t); resolve(ws); };
|
|
30
|
+
ws.onerror = () => { clearTimeout(t); reject(new Error("ws error")); };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function client(ws, timeoutMs) {
|
|
35
|
+
let id = 0;
|
|
36
|
+
const pending = new Map();
|
|
37
|
+
const limit = timeoutMs || 10000;
|
|
38
|
+
ws.onmessage = (ev) => {
|
|
39
|
+
let m;
|
|
40
|
+
try { m = JSON.parse(ev.data); } catch (e) { return; }
|
|
41
|
+
if (m.id && pending.has(m.id)) {
|
|
42
|
+
const p = pending.get(m.id);
|
|
43
|
+
pending.delete(m.id);
|
|
44
|
+
clearTimeout(p.timer);
|
|
45
|
+
m.error ? p.reject(new Error(m.error.message || JSON.stringify(m.error))) : p.resolve(m.result);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
rpc(method, params) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const i = ++id;
|
|
52
|
+
// A wedged or navigating renderer queues commands forever; fail loudly instead of hanging
|
|
53
|
+
// the caller, so `ckit doctor` reports FAIL rather than sitting there silently.
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
if (pending.delete(i)) reject(new Error("CDP command timed out after " + limit + "ms: " + method +
|
|
56
|
+
" (is Cline mid-navigation or frozen?)"));
|
|
57
|
+
}, limit);
|
|
58
|
+
pending.set(i, { resolve, reject, timer });
|
|
59
|
+
ws.send(JSON.stringify({ id: i, method, params: params || {} }));
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
close() { try { ws.close(); } catch (e) { } }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// run fn(api) against every page target; returns per-target results
|
|
67
|
+
async function eachPage(port, fn) {
|
|
68
|
+
const pages = await pageTargets(port);
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const p of pages) {
|
|
71
|
+
let api;
|
|
72
|
+
try {
|
|
73
|
+
const ws = await open(p.webSocketDebuggerUrl);
|
|
74
|
+
api = client(ws);
|
|
75
|
+
out.push({ target: p, result: await fn(api, p) });
|
|
76
|
+
} catch (e) {
|
|
77
|
+
out.push({ target: p, error: e.message });
|
|
78
|
+
} finally {
|
|
79
|
+
if (api) api.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { listTargets, pageTargets, open, client, eachPage };
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Cline-kit - desktop enhancement kit for the Cline desktop app.
|
|
4
|
+
// Flagship feature: keep every registered project visible in the sidebar.
|
|
5
|
+
// Optional: UI locale packs (zh-CN today; zh-TW / ja / ko / vi follow the same data format).
|
|
6
|
+
const cfg = require("./config");
|
|
7
|
+
|
|
8
|
+
function guard() {
|
|
9
|
+
if (typeof WebSocket !== "function") {
|
|
10
|
+
console.error("Cline-kit needs Node.js 20.10+ (global WebSocket). Current: " + process.version);
|
|
11
|
+
console.error("Install a newer Node from https://nodejs.org and retry.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const HELP = `Cline-kit (ckit) - enhancement kit for the Cline desktop app (Windows)
|
|
17
|
+
|
|
18
|
+
Usage: ckit <command> [options]
|
|
19
|
+
|
|
20
|
+
start Launch Cline with the overlay loaded (--restart quits a running instance first)
|
|
21
|
+
stop Stop the background injector (Cline itself is untouched)
|
|
22
|
+
status Show the Cline path, debug port, injector and loaded payload version
|
|
23
|
+
doctor Check the live window: does each feature exist in the DOM right now?
|
|
24
|
+
attach Inject once into a Cline that is already running with a debug port
|
|
25
|
+
install Point the Start Menu / Desktop Cline shortcuts at the enhanced launcher
|
|
26
|
+
uninstall Restore the original shortcuts
|
|
27
|
+
features List feature plugins and whether each is on
|
|
28
|
+
feature Toggle a plugin: ckit feature enable|disable <id>
|
|
29
|
+
update Fetch the latest locale dictionary from GitHub (skipped when offline)
|
|
30
|
+
audit Walk the UI and list strings still without a translation
|
|
31
|
+
dict Dictionary stats and the local override file path
|
|
32
|
+
config View or set: --cline-path=... --port=... --auto-update=on|off
|
|
33
|
+
--dictionary=<locale> --hide=<path> / --unhide=<path>
|
|
34
|
+
|
|
35
|
+
Main feature
|
|
36
|
+
sidebar-groups Keeps every registered project visible in the sidebar's "Projects" group.
|
|
37
|
+
Cline natively lists only folders that already have sessions, so empty
|
|
38
|
+
projects disappear entirely.
|
|
39
|
+
|
|
40
|
+
Optional feature
|
|
41
|
+
locale packs dictionaries/<locale>.json - whole-string replacement only, so provider names,
|
|
42
|
+
model names and code cannot be mangled. Bundled: zh-CN (reference, 476 entries),
|
|
43
|
+
zh-TW, ja, ko, vi. Choose one: ckit config --dictionary=ja (ckit update then
|
|
44
|
+
follows that locale). Only zh-CN is proofread against the running app.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
ckit start
|
|
48
|
+
ckit install
|
|
49
|
+
ckit features
|
|
50
|
+
ckit feature disable sidebar-groups
|
|
51
|
+
ckit config --cline-path "D:\\Programs\\Cline\\cline-app.exe"
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
function parseFlags(argv) {
|
|
55
|
+
const flags = {};
|
|
56
|
+
const rest = [];
|
|
57
|
+
for (const a of argv) {
|
|
58
|
+
const m = a.match(/^--([^=]+)(?:=(.*))?$/);
|
|
59
|
+
if (m) flags[m[1]] = m[2] === undefined ? true : m[2];
|
|
60
|
+
else rest.push(a);
|
|
61
|
+
}
|
|
62
|
+
return { flags, rest };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
guard();
|
|
67
|
+
const argv = process.argv.slice(2);
|
|
68
|
+
const cmd = argv[0] || "help";
|
|
69
|
+
const { flags } = parseFlags(argv.slice(1));
|
|
70
|
+
const conf = cfg.read();
|
|
71
|
+
|
|
72
|
+
if (cmd === "start") {
|
|
73
|
+
const out = await require("./launcher").start({ restart: !!flags.restart });
|
|
74
|
+
if (out.needsRestart) {
|
|
75
|
+
console.log("Cline is already running without a debug port, so the overlay cannot attach.");
|
|
76
|
+
console.log("Run `ckit start --restart`, or quit Cline first and run ckit start again.");
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
console.log(out.debugPortAlive
|
|
81
|
+
? `Cline is up (port ${out.port}, path from ${out.source}) and the overlay is loaded.`
|
|
82
|
+
: `Warning: Cline started but port ${out.port} is not answering; the overlay may not be loaded.`);
|
|
83
|
+
if (!out.debugPortAlive) process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (cmd === "stop") {
|
|
88
|
+
const r = require("./launcher").stop();
|
|
89
|
+
console.log(r.stopped ? "Injector stopped (pid " + r.pids + ")" : "No injector was running");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (cmd === "status") {
|
|
94
|
+
const launcher = require("./launcher");
|
|
95
|
+
const payload = require("./payload");
|
|
96
|
+
const detect = require("./detect");
|
|
97
|
+
const found = detect.detect(conf);
|
|
98
|
+
const alive = conf.port ? await launcher.isPortAlive(conf.port) : false;
|
|
99
|
+
console.log(JSON.stringify({
|
|
100
|
+
clinePath: found.path, clinePathSource: found.source,
|
|
101
|
+
debugPort: conf.port || null, debugPortAlive: alive,
|
|
102
|
+
injector: launcher.injectorRunning() || null,
|
|
103
|
+
dictionary: payload.info(conf),
|
|
104
|
+
shortcut: conf.shortcutPath || null,
|
|
105
|
+
autoUpdate: conf.autoUpdate, updateUrl: conf.updateUrl
|
|
106
|
+
}, null, 2));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (cmd === "doctor") {
|
|
111
|
+
const doctor = require("./doctor");
|
|
112
|
+
const r = await doctor.run();
|
|
113
|
+
console.log(doctor.format(r));
|
|
114
|
+
process.exitCode = r.ok ? 0 : 1;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (cmd === "attach") {
|
|
119
|
+
const port = Number(flags.port) || conf.port;
|
|
120
|
+
if (!port) { console.error("No debug port to attach to. Pass --port=NNNNN"); process.exit(1); }
|
|
121
|
+
const src = require("./payload").build(conf);
|
|
122
|
+
const r = await require("./cdp").eachPage(port, async (api) => {
|
|
123
|
+
const res = await api.rpc("Runtime.evaluate", { expression: src });
|
|
124
|
+
if (res && res.exceptionDetails) throw new Error(res.exceptionDetails.text || "evaluate failed");
|
|
125
|
+
return "ok";
|
|
126
|
+
});
|
|
127
|
+
console.log("Injected once into " + r.length + " page(s): " +
|
|
128
|
+
r.map((x) => x.error ? "FAIL " + x.error : x.result).join(", "));
|
|
129
|
+
console.log("The resident injector is not running, so a reload (Ctrl+R) drops the overlay.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (cmd === "install") {
|
|
134
|
+
const r = await require("./install-win").install(conf);
|
|
135
|
+
console.log("Shortcuts now launch Cline with the overlay:");
|
|
136
|
+
r.shortcuts.forEach((s) => console.log(" " + s));
|
|
137
|
+
console.log("Launcher: " + r.launcher);
|
|
138
|
+
console.log("Open Cline from those shortcuts from now on; `ckit uninstall` restores them.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (cmd === "uninstall") {
|
|
143
|
+
require("./launcher").stop();
|
|
144
|
+
const r = await require("./install-win").uninstall(cfg.read());
|
|
145
|
+
console.log(r.restored.length ? "Restored:\n " + r.restored.join("\n ") : "No shortcuts needed restoring");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (cmd === "update") {
|
|
150
|
+
const r = await require("./dict").update(cfg.read(), { force: !!flags.force });
|
|
151
|
+
if (r.error) { console.error("Update failed (keeping the local dictionary): " + r.error); process.exitCode = 1; return; }
|
|
152
|
+
console.log(r.updated ? `Dictionary updated: v${r.from} -> v${r.to}` : "Already up to date" + (r.reason ? " (" + r.reason + ")" : ""));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (cmd === "audit") {
|
|
157
|
+
if (!conf.port) { console.error("Cline was not started through cline-kit; run ckit start first"); process.exit(1); }
|
|
158
|
+
const r = await require("./audit").run(conf.port);
|
|
159
|
+
console.log(`${r.total} untranslated strings, details: ${r.file}`);
|
|
160
|
+
r.items.slice(0, Number(flags.limit) || 80).forEach(([s, where]) => console.log(" " + s + " [" + where + "]"));
|
|
161
|
+
if (r.total > (Number(flags.limit) || 80)) console.log(" ...");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (cmd === "features" || cmd === "feature") {
|
|
166
|
+
const features = require("./features");
|
|
167
|
+
const conf2 = cfg.read();
|
|
168
|
+
const sub = flags._ || null;
|
|
169
|
+
if (cmd === "features") {
|
|
170
|
+
const enabled = features.enabledSet(conf2);
|
|
171
|
+
for (const f of features.list()) {
|
|
172
|
+
console.log(`${enabled[f.id] ? "on " : "off"} ${f.id} (v${f.version}) ${f.title}`);
|
|
173
|
+
}
|
|
174
|
+
console.log("\nToggle with: ckit feature enable <id> | ckit feature disable <id>");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const [action, id] = argv.slice(1).filter((a) => !a.startsWith("--"));
|
|
178
|
+
const known = features.list().map((f) => f.id);
|
|
179
|
+
if (!known.includes(id)) {
|
|
180
|
+
console.error(`Unknown feature: ${id || "(none)"}; available: ${known.join(", ")}`);
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (action !== "enable" && action !== "disable") {
|
|
185
|
+
console.error("Usage: ckit feature <enable|disable> <id>");
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
conf2.features = Object.assign({}, conf2.features || {}, { [id]: action === "enable" });
|
|
190
|
+
cfg.write(conf2);
|
|
191
|
+
console.log(`${id} -> ${action === "enable" ? "enabled" : "disabled"} (the injector picks it up within ~4s)`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (cmd === "dict") {
|
|
196
|
+
console.log(JSON.stringify(require("./payload").info(conf), null, 2));
|
|
197
|
+
console.log("Local override file (highest priority): " + cfg.localDictFile(conf.dictionary));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (cmd === "config") {
|
|
202
|
+
let changed = false;
|
|
203
|
+
if (flags["cline-path"]) { conf.clinePath = flags["cline-path"]; changed = true; }
|
|
204
|
+
if (flags.port) { conf.port = Number(flags.port); changed = true; }
|
|
205
|
+
if (flags["auto-update"]) { conf.autoUpdate = String(flags["auto-update"]) !== "off"; changed = true; }
|
|
206
|
+
if (flags["update-url"]) { conf.updateUrl = flags["update-url"]; changed = true; }
|
|
207
|
+
if (flags["storage-key"] !== undefined) { conf.storageKey = String(flags["storage-key"]); changed = true; }
|
|
208
|
+
if (flags.dictionary) {
|
|
209
|
+
const want = flags.dictionary;
|
|
210
|
+
const have = require("./dict").available();
|
|
211
|
+
if (!have.includes(want)) {
|
|
212
|
+
console.error(`Unknown locale "${want}". Bundled dictionaries: ${have.join(", ")}`);
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
conf.dictionary = want; changed = true;
|
|
217
|
+
}
|
|
218
|
+
if (flags["hide"]) {
|
|
219
|
+
const kept = (conf.featureHide || []).slice();
|
|
220
|
+
if (kept.indexOf(flags["hide"]) < 0) kept.push(flags["hide"]);
|
|
221
|
+
conf.featureHide = kept;
|
|
222
|
+
changed = true;
|
|
223
|
+
}
|
|
224
|
+
if (flags["unhide"]) {
|
|
225
|
+
conf.featureHide = (conf.featureHide || []).filter((p) => p !== flags["unhide"]);
|
|
226
|
+
changed = true;
|
|
227
|
+
}
|
|
228
|
+
if (changed) { cfg.write(conf); console.log("Wrote " + cfg.configFile()); }
|
|
229
|
+
console.log(JSON.stringify(cfg.read(), null, 2));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(HELP);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
main().catch((e) => { console.error("Cline-kit: " + e.message); process.exit(1); });
|
package/src/config.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Config + paths. Everything lives in %APPDATA%\cline-kit (or ~/.config/cline-kit).
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const DIR_NAME = "cline-kit";
|
|
8
|
+
const LEGACY_DIR_NAME = "cline-zh"; // pre-rename layout, migrated once on first run
|
|
9
|
+
|
|
10
|
+
function baseDir() {
|
|
11
|
+
const appdata = process.env.APPDATA;
|
|
12
|
+
if (appdata) return appdata;
|
|
13
|
+
if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support");
|
|
14
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function configDir() { return path.join(baseDir(), DIR_NAME); }
|
|
18
|
+
function legacyDir() { return path.join(baseDir(), LEGACY_DIR_NAME); }
|
|
19
|
+
|
|
20
|
+
let migrated;
|
|
21
|
+
function migrateLegacy() {
|
|
22
|
+
if (migrated !== undefined) return migrated;
|
|
23
|
+
migrated = false;
|
|
24
|
+
const dst = configDir();
|
|
25
|
+
const src = legacyDir();
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(src) && !fs.existsSync(dst)) {
|
|
28
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
29
|
+
for (const f of ["config.json"]) {
|
|
30
|
+
if (fs.existsSync(path.join(src, f))) fs.copyFileSync(path.join(src, f), path.join(dst, f));
|
|
31
|
+
}
|
|
32
|
+
for (const d of ["cache", "logs"]) {
|
|
33
|
+
copyDir(path.join(src, d), path.join(dst, d));
|
|
34
|
+
}
|
|
35
|
+
migrated = true;
|
|
36
|
+
}
|
|
37
|
+
} catch (e) { /* a failed migration just means a fresh config; never crash over it */ }
|
|
38
|
+
return migrated;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function copyDir(from, to) {
|
|
42
|
+
if (!fs.existsSync(from)) return;
|
|
43
|
+
fs.mkdirSync(to, { recursive: true });
|
|
44
|
+
for (const name of fs.readdirSync(from)) {
|
|
45
|
+
const a = path.join(from, name);
|
|
46
|
+
const b = path.join(to, name);
|
|
47
|
+
if (fs.statSync(a).isDirectory()) copyDir(a, b);
|
|
48
|
+
else fs.copyFileSync(a, b);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function configFile() { return path.join(configDir(), "config.json"); }
|
|
53
|
+
function cacheDir() { return path.join(configDir(), "cache"); }
|
|
54
|
+
function logDir() { return path.join(configDir(), "logs"); }
|
|
55
|
+
|
|
56
|
+
const DEFAULTS = {
|
|
57
|
+
// Cline executable; null = auto-detect on each run
|
|
58
|
+
clinePath: null,
|
|
59
|
+
// 0 = pick a random free port at launch (avoids clashing with other debug servers)
|
|
60
|
+
port: 0,
|
|
61
|
+
// dictionary updates
|
|
62
|
+
autoUpdate: true,
|
|
63
|
+
updateUrl: "https://raw.githubusercontent.com/chentaoxing/Cline-kit/main/dictionaries/zh-CN.json",
|
|
64
|
+
updateIntervalMs: 24 * 60 * 60 * 1000,
|
|
65
|
+
dictionary: "zh-CN",
|
|
66
|
+
// feature id -> boolean; absent keys fall back to each feature's defaultOn
|
|
67
|
+
features: {},
|
|
68
|
+
// extra paths sidebar-groups must not list as projects (containers, scratch folders)
|
|
69
|
+
featureHide: [],
|
|
70
|
+
// override Cline's workspace-registry localStorage key; empty = detect cline.code.workspace-selection.vN
|
|
71
|
+
storageKey: "",
|
|
72
|
+
// installer state
|
|
73
|
+
shortcutPath: null,
|
|
74
|
+
autostart: false,
|
|
75
|
+
lastUpdateCheck: 0,
|
|
76
|
+
remoteVersion: null
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function read() {
|
|
80
|
+
migrateLegacy();
|
|
81
|
+
let data = {};
|
|
82
|
+
try { data = JSON.parse(fs.readFileSync(configFile(), "utf8")); } catch (e) { data = {}; }
|
|
83
|
+
return Object.assign({}, DEFAULTS, data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function write(cfg) {
|
|
87
|
+
migrateLegacy();
|
|
88
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
89
|
+
const tmp = configFile() + ".tmp";
|
|
90
|
+
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2), "utf8");
|
|
91
|
+
fs.renameSync(tmp, configFile());
|
|
92
|
+
return cfg;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureDirs() {
|
|
96
|
+
migrateLegacy();
|
|
97
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
98
|
+
fs.mkdirSync(cacheDir(), { recursive: true });
|
|
99
|
+
fs.mkdirSync(logDir(), { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function localDictFile(name) { return path.join(configDir(), name + ".local.json"); }
|
|
103
|
+
function cachedDictFile(name) { return path.join(cacheDir(), name + ".json"); }
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
configDir, configFile, cacheDir, logDir, read, write, ensureDirs, DEFAULTS,
|
|
107
|
+
localDictFile, cachedDictFile, migrateLegacy, legacyDir, DIR_NAME
|
|
108
|
+
};
|