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.
@@ -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 };
@@ -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 };