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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Windows integration: a no-console launcher + repointing the user's Cline shortcut.
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execFileSync } = require("child_process");
|
|
7
|
+
const cfg = require("./config");
|
|
8
|
+
|
|
9
|
+
const VBS = (ps1) => `' cline-kit launcher - starts the Cline desktop app with the enhancement overlay
|
|
10
|
+
Set sh = CreateObject("WScript.Shell")
|
|
11
|
+
sh.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""${ps1}""", 0, False
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const PS1 = (nodeExe, argsString) => `# cline-kit launcher
|
|
15
|
+
$ErrorActionPreference = 'Stop'
|
|
16
|
+
try {
|
|
17
|
+
Start-Process -FilePath ${psQuote(nodeExe)} -ArgumentList ${psQuote(argsString)} -WindowStyle Hidden
|
|
18
|
+
} catch {
|
|
19
|
+
# surface failures instead of silently doing nothing
|
|
20
|
+
$log = Join-Path $env:APPDATA 'cline-kit\\launcher-error.log'
|
|
21
|
+
Set-Content -LiteralPath $log -Value ((Get-Date).ToString('o') + ' ' + $_.Exception.Message)
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
function psQuote(s) { return "'" + String(s).replace(/'/g, "''") + "'"; }
|
|
26
|
+
// PowerShell does not treat backslash as an escape character, so JSON.stringify would leave
|
|
27
|
+
// doubled backslashes in paths and every comparison downstream would fail. Build a real
|
|
28
|
+
// PowerShell array of single-quoted literals instead.
|
|
29
|
+
function psArray(items) { return "@(" + items.map(psQuote).join(",") + ")"; }
|
|
30
|
+
|
|
31
|
+
function lnkSearchDirs() {
|
|
32
|
+
const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
33
|
+
const progdata = process.env.ProgramData || "C:\\ProgramData";
|
|
34
|
+
return [
|
|
35
|
+
path.join(appdata, "Microsoft", "Windows", "Start Menu", "Programs"),
|
|
36
|
+
path.join(progdata, "Microsoft", "Windows", "Start Menu", "Programs"),
|
|
37
|
+
path.join(os.homedir(), "Desktop"),
|
|
38
|
+
path.join(process.env.Public || "C:\\Users\\Public", "Desktop")
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findShortcuts(exePath) {
|
|
43
|
+
if (process.platform !== "win32") return [];
|
|
44
|
+
const dirs = lnkSearchDirs().filter((d) => fs.existsSync(d));
|
|
45
|
+
const script = `$exe = ${psQuote(exePath)}
|
|
46
|
+
$dirs = ${psArray(dirs)}
|
|
47
|
+
$sh = New-Object -ComObject WScript.Shell
|
|
48
|
+
foreach ($d in $dirs) {
|
|
49
|
+
if (-not (Test-Path -LiteralPath $d)) { continue }
|
|
50
|
+
Get-ChildItem -LiteralPath $d -Filter *.lnk -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
|
|
51
|
+
try {
|
|
52
|
+
$t = $sh.CreateShortcut($_.FullName).TargetPath
|
|
53
|
+
if ($t -and ($t -ieq $exe)) { Write-Output $_.FullName }
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
}`;
|
|
57
|
+
try {
|
|
58
|
+
const out = execFileSync("powershell.exe", ["-NoProfile", "-Command", script], { encoding: "utf8", timeout: 60000 });
|
|
59
|
+
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
60
|
+
} catch (e) { return []; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function setShortcut(lnkPath, target, args, icon, workingDir, description) {
|
|
64
|
+
const script = `$sh = New-Object -ComObject WScript.Shell
|
|
65
|
+
$o = $sh.CreateShortcut(${psQuote(lnkPath)})
|
|
66
|
+
$o.TargetPath = ${psQuote(target)}
|
|
67
|
+
$o.Arguments = ${psQuote(args || "")}
|
|
68
|
+
$o.IconLocation = ${psQuote(icon || target + ",0")}
|
|
69
|
+
$o.WorkingDirectory = ${psQuote(workingDir || path.dirname(target))}
|
|
70
|
+
$o.Description = ${psQuote(description || "")}
|
|
71
|
+
$o.Save()
|
|
72
|
+
Write-Output "ok"`;
|
|
73
|
+
execFileSync("powershell.exe", ["-NoProfile", "-Command", script], { encoding: "utf8", timeout: 30000 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readShortcut(lnkPath) {
|
|
77
|
+
const script = `$sh = New-Object -ComObject WScript.Shell
|
|
78
|
+
$o = $sh.CreateShortcut(${psQuote(lnkPath)})
|
|
79
|
+
Write-Output ($o.TargetPath + [char]9 + $o.Arguments)`;
|
|
80
|
+
const out = execFileSync("powershell.exe", ["-NoProfile", "-Command", script], { encoding: "utf8", timeout: 30000 });
|
|
81
|
+
const line = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0] || "";
|
|
82
|
+
const parts = line.split("\t");
|
|
83
|
+
return { target: parts[0] || "", args: parts[1] || "" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeLauncherFiles(cfgObj) {
|
|
87
|
+
cfg.ensureDirs();
|
|
88
|
+
const cli = path.join(__dirname, "cli.js");
|
|
89
|
+
const ps1 = path.join(cfg.configDir(), "launch-cline-kit.ps1");
|
|
90
|
+
const vbs = path.join(cfg.configDir(), "launch-cline-kit.vbs");
|
|
91
|
+
// Windows PowerShell 5.1 reads .ps1 as ANSI unless a UTF-8 BOM is present, and wscript reads
|
|
92
|
+
// .vbs as ANSI unless it is UTF-16LE with a BOM. Without these, any non-ASCII character in the
|
|
93
|
+
// install path (a Chinese username, for example) silently breaks the launcher.
|
|
94
|
+
fs.writeFileSync(ps1, "" + PS1(process.execPath, '"' + cli + '" start'), "utf8");
|
|
95
|
+
fs.writeFileSync(vbs, "" + VBS(ps1), "utf16le");
|
|
96
|
+
return { ps1, vbs };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function install(cfgObj, opts) {
|
|
100
|
+
opts = opts || {};
|
|
101
|
+
if (process.platform !== "win32") throw new Error("shortcut install is Windows-only for now");
|
|
102
|
+
const detect = require("./detect");
|
|
103
|
+
const found = detect.detect(cfgObj);
|
|
104
|
+
if (!found.path) throw new Error("cline-app.exe not found; set it with `ckit config --cline-path ...`");
|
|
105
|
+
const { vbs } = writeLauncherFiles(cfgObj);
|
|
106
|
+
|
|
107
|
+
// Re-running `install` must not lose the backup of the original shortcut targets.
|
|
108
|
+
const known = (cfgObj.originalShortcuts || []).filter((s) => s && s.lnk && fs.existsSync(s.lnk));
|
|
109
|
+
let toPoint;
|
|
110
|
+
if (known.length) {
|
|
111
|
+
toPoint = known.map((s) => s.lnk);
|
|
112
|
+
} else {
|
|
113
|
+
const existing = findShortcuts(found.path);
|
|
114
|
+
if (existing.length) {
|
|
115
|
+
cfgObj.originalShortcuts = existing.map((p) => Object.assign({ lnk: p }, readShortcut(p)));
|
|
116
|
+
toPoint = existing;
|
|
117
|
+
} else {
|
|
118
|
+
const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
119
|
+
const lnk = path.join(appdata, "Microsoft", "Windows", "Start Menu", "Programs", "Cline (中文).lnk");
|
|
120
|
+
cfgObj.originalShortcuts = [];
|
|
121
|
+
toPoint = [lnk];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const touched = [];
|
|
126
|
+
for (const lnk of toPoint) {
|
|
127
|
+
setShortcut(lnk, "C:\\Windows\\System32\\wscript.exe", '"' + vbs + '"', found.path + ",0",
|
|
128
|
+
path.dirname(found.path), "Cline (Cline-kit enhanced)");
|
|
129
|
+
touched.push(lnk);
|
|
130
|
+
}
|
|
131
|
+
cfgObj.shortcutPath = touched[0];
|
|
132
|
+
cfgObj.clineExeForShortcut = found.path;
|
|
133
|
+
cfg.write(cfgObj);
|
|
134
|
+
return { exe: found.path, shortcuts: touched, launcher: vbs };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function uninstall(cfgObj) {
|
|
138
|
+
const restore = [];
|
|
139
|
+
for (const s of (cfgObj.originalShortcuts || [])) {
|
|
140
|
+
try {
|
|
141
|
+
setShortcut(s.lnk, s.target, s.args, s.target + ",0", path.dirname(s.target), "");
|
|
142
|
+
restore.push(s.lnk);
|
|
143
|
+
} catch (e) { /* ignore individual failures */ }
|
|
144
|
+
}
|
|
145
|
+
// remove a shortcut we created ourselves (no original to restore)
|
|
146
|
+
if (!restore.length && cfgObj.shortcutPath && fs.existsSync(cfgObj.shortcutPath)) {
|
|
147
|
+
try { fs.unlinkSync(cfgObj.shortcutPath); restore.push("removed " + cfgObj.shortcutPath); } catch (e) { }
|
|
148
|
+
}
|
|
149
|
+
cfgObj.shortcutPath = null;
|
|
150
|
+
cfgObj.originalShortcuts = [];
|
|
151
|
+
cfg.write(cfgObj);
|
|
152
|
+
return { restored: restore };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { install, uninstall, findShortcuts, writeLauncherFiles };
|
package/src/launcher.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Start Cline with a private debug port and keep the overlay attached.
|
|
3
|
+
const { spawn, execFileSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const net = require("net");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const cfg = require("./config");
|
|
8
|
+
const cdp = require("./cdp");
|
|
9
|
+
const detect = require("./detect");
|
|
10
|
+
|
|
11
|
+
function freePort() {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const srv = net.createServer();
|
|
14
|
+
srv.on("error", reject);
|
|
15
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
16
|
+
const p = srv.address().port;
|
|
17
|
+
srv.close(() => resolve(p));
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isPortAlive(port) {
|
|
23
|
+
return cdp.pageTargets(port).then((t) => t.length > 0).catch(() => false);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function injectorRunning() {
|
|
27
|
+
if (process.platform !== "win32") return false;
|
|
28
|
+
try {
|
|
29
|
+
const script = "$m='injector.js';" +
|
|
30
|
+
"$ps=Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" | " +
|
|
31
|
+
"Where-Object { $_.CommandLine -match $m }; $ps.ProcessId -join ','";
|
|
32
|
+
const out = execFileSync("powershell.exe", ["-NoProfile", "-Command", script], { encoding: "utf8", timeout: 15000 });
|
|
33
|
+
return out.trim() || "";
|
|
34
|
+
} catch (e) { return ""; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function spawnHidden(file, args, opts) {
|
|
38
|
+
const child = spawn(file, args, Object.assign({
|
|
39
|
+
detached: true,
|
|
40
|
+
stdio: "ignore",
|
|
41
|
+
windowsHide: true
|
|
42
|
+
}, opts || {}));
|
|
43
|
+
child.unref();
|
|
44
|
+
return child;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clineRunning() {
|
|
48
|
+
if (process.platform !== "win32") return false;
|
|
49
|
+
try {
|
|
50
|
+
const out = execFileSync("tasklist.exe", ["/FI", "IMAGENAME eq cline-app.exe", "/NH"], { encoding: "utf8" });
|
|
51
|
+
return /cline-app\.exe/i.test(out);
|
|
52
|
+
} catch (e) { return false; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function killCline() {
|
|
56
|
+
if (process.platform !== "win32") return false;
|
|
57
|
+
try {
|
|
58
|
+
execFileSync("taskkill.exe", ["/IM", "cline-app.exe", "/F"], { encoding: "utf8" });
|
|
59
|
+
return true;
|
|
60
|
+
} catch (e) { return false; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function start(opts) {
|
|
64
|
+
opts = opts || {};
|
|
65
|
+
const conf = cfg.read();
|
|
66
|
+
const found = detect.detect(conf);
|
|
67
|
+
if (!found.path) {
|
|
68
|
+
throw new Error("cline-app.exe not found. Set it with: cline-kit config --cline-path \"C:\\path\\cline-app.exe\"");
|
|
69
|
+
}
|
|
70
|
+
let port = conf.port;
|
|
71
|
+
let alive = port ? await isPortAlive(port) : false;
|
|
72
|
+
|
|
73
|
+
if (!alive && clineRunning() && !opts.restart) {
|
|
74
|
+
// Cline is already up without a debug port; a second launch would just focus the old window.
|
|
75
|
+
return { exe: found.path, source: found.source, port: null, debugPortAlive: false, needsRestart: true };
|
|
76
|
+
}
|
|
77
|
+
if (opts.restart) {
|
|
78
|
+
killCline();
|
|
79
|
+
await new Promise((r) => setTimeout(r, 2500));
|
|
80
|
+
}
|
|
81
|
+
if (!alive) {
|
|
82
|
+
port = await freePort();
|
|
83
|
+
conf.port = port;
|
|
84
|
+
conf.clinePath = found.path;
|
|
85
|
+
cfg.ensureDirs();
|
|
86
|
+
cfg.write(conf);
|
|
87
|
+
const env = Object.assign({}, process.env, {
|
|
88
|
+
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: "--remote-debugging-port=" + port
|
|
89
|
+
});
|
|
90
|
+
spawnHidden(found.path, [], { env, cwd: path.dirname(found.path) });
|
|
91
|
+
for (let i = 0; i < 40 && !(alive = await isPortAlive(port)); i++) {
|
|
92
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// cwd must NOT be src/: a process whose working directory is inside the folder locks it,
|
|
96
|
+
// which blocks renaming the checkout or replacing files on update.
|
|
97
|
+
const repoRoot = path.join(__dirname, "..");
|
|
98
|
+
const injectorPath = path.join(__dirname, "injector.js");
|
|
99
|
+
let stale = false;
|
|
100
|
+
try {
|
|
101
|
+
const running = injectorRunning();
|
|
102
|
+
if (running) {
|
|
103
|
+
const current = require("./payload").compose(cfg.read()).version;
|
|
104
|
+
stale = require("./injector").readActiveVersion() !== current;
|
|
105
|
+
}
|
|
106
|
+
} catch (e) { stale = false; }
|
|
107
|
+
if (stale) stop();
|
|
108
|
+
if (!injectorRunning() || stale) {
|
|
109
|
+
spawnHidden(process.execPath, [injectorPath], { cwd: repoRoot });
|
|
110
|
+
}
|
|
111
|
+
return { exe: found.path, source: found.source, port, debugPortAlive: alive, restartedInjector: stale };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function stop() {
|
|
115
|
+
const ids = injectorRunning();
|
|
116
|
+
if (!ids) return { stopped: false };
|
|
117
|
+
for (const pid of ids.split(",").filter(Boolean)) {
|
|
118
|
+
try { execFileSync("taskkill.exe", ["/PID", pid.trim(), "/F"], { encoding: "utf8" }); } catch (e) { }
|
|
119
|
+
}
|
|
120
|
+
return { stopped: true, pids: ids };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { start, stop, freePort, isPortAlive, injectorRunning, clineRunning, killCline };
|
package/src/payload.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Build the JS source injected into the Cline webview: dictionary + engine + enabled features.
|
|
3
|
+
// The composite version lets the injector notice feature toggles, not just dictionary bumps.
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const dict = require("./dict");
|
|
8
|
+
const features = require("./features");
|
|
9
|
+
|
|
10
|
+
const ENGINE = fs.readFileSync(path.join(__dirname, "engine.js"), "utf8");
|
|
11
|
+
|
|
12
|
+
function json(x) {
|
|
13
|
+
return JSON.stringify(x).replace(/</g, "\\u003c");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hash12(s) {
|
|
17
|
+
return crypto.createHash("sha1").update(s).digest("hex").slice(0, 12);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compose(cfgObj) {
|
|
21
|
+
const d = dict.load(cfgObj);
|
|
22
|
+
// the engine guards on its own build hash so code edits hot-swap even when the dictionary
|
|
23
|
+
// version is unchanged
|
|
24
|
+
d.engineBuild = hash12(ENGINE);
|
|
25
|
+
const { picked } = features.resolve(cfgObj, d);
|
|
26
|
+
const parts = ["const DICT = " + json(d) + ";", ENGINE];
|
|
27
|
+
|
|
28
|
+
for (const f of picked) {
|
|
29
|
+
// per-feature build hash: the feature's own guard compares against it, so editing the script
|
|
30
|
+
// hot-swaps it without anyone remembering to bump a VER constant
|
|
31
|
+
const cfgObjForFeature = Object.assign({}, f.config, { __build: hash12(f.source) });
|
|
32
|
+
parts.push(
|
|
33
|
+
"window.__clineKitFeature = window.__clineKitFeature || {};",
|
|
34
|
+
"window.__clineKitFeature[" + json(f.id) + "] = " + json(cfgObjForFeature) + ";",
|
|
35
|
+
"try {\n" + f.source + "\n} catch (e) { try { console.error('[cline-kit] feature " + f.id + " failed:', e && e.message); } catch (_) { } }"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Version = dictionary version + a hash of the composed source. Keying on the hash means editing
|
|
40
|
+
// engine or feature code triggers a re-inject on its own; a hand-maintained per-feature VER can be
|
|
41
|
+
// forgotten, and the symptom (stale overlay, no error) is nasty to debug.
|
|
42
|
+
const body = parts.join("\n");
|
|
43
|
+
const hash = crypto.createHash("sha1").update(body).digest("hex").slice(0, 9);
|
|
44
|
+
const version = "d" + d.version + "+" + hash;
|
|
45
|
+
|
|
46
|
+
// Everything is wrapped in one function scope. A top-level `const DICT` would throw
|
|
47
|
+
// "Identifier 'DICT' has already been declared" on the *second* Runtime.evaluate in the same
|
|
48
|
+
// document, which would silently kill every hot update after the first one.
|
|
49
|
+
return {
|
|
50
|
+
source: "(function(){\n" + body + "\n})();",
|
|
51
|
+
version,
|
|
52
|
+
dict: d,
|
|
53
|
+
picked
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function build(cfgObj) {
|
|
58
|
+
return compose(cfgObj).source;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function info(cfgObj) {
|
|
62
|
+
const { version, dict: d, picked } = compose(cfgObj);
|
|
63
|
+
const enabled = features.enabledSet(cfgObj);
|
|
64
|
+
return {
|
|
65
|
+
language: d.language,
|
|
66
|
+
version,
|
|
67
|
+
dictionaryVersion: d.version,
|
|
68
|
+
clineVersion: d.clineVersion || null,
|
|
69
|
+
updated: d.updated || null,
|
|
70
|
+
entries: Object.keys(d.entries || {}).length,
|
|
71
|
+
rules: (d.rules || []).length,
|
|
72
|
+
prefixes: (d.prefixes || []).length,
|
|
73
|
+
sources: d.sourceVersions,
|
|
74
|
+
features: features.list().map((f) => ({
|
|
75
|
+
id: f.id, title: f.title, version: f.version, enabled: !!enabled[f.id]
|
|
76
|
+
}))
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { build, info, compose };
|