symforge 1.0.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/bin/launcher.js +173 -0
- package/bin/symforge.js +11 -0
- package/package.json +35 -0
- package/scripts/install.js +406 -0
package/bin/launcher.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const childProcess = require("child_process");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
|
|
9
|
+
function createLauncher(overrides = {}) {
|
|
10
|
+
const fsMod = overrides.fs || fs;
|
|
11
|
+
const pathMod = overrides.path || path;
|
|
12
|
+
const osMod = overrides.os || os;
|
|
13
|
+
const processMod = overrides.process || process;
|
|
14
|
+
const consoleMod = overrides.console || console;
|
|
15
|
+
const spawnSyncFn = overrides.spawnSync || childProcess.spawnSync;
|
|
16
|
+
const execFileSyncFn = overrides.execFileSync || childProcess.execFileSync;
|
|
17
|
+
const packageJson = overrides.packageJson || require("../package.json");
|
|
18
|
+
const installScriptPath = overrides.installScriptPath
|
|
19
|
+
|| pathMod.join(__dirname, "..", "scripts", "install.js");
|
|
20
|
+
|
|
21
|
+
function resolveInstallDir() {
|
|
22
|
+
if (overrides.installDir) {
|
|
23
|
+
return overrides.installDir;
|
|
24
|
+
}
|
|
25
|
+
if (processMod.env.SYMFORGE_HOME) {
|
|
26
|
+
return pathMod.join(processMod.env.SYMFORGE_HOME, "bin");
|
|
27
|
+
}
|
|
28
|
+
return pathMod.join(osMod.homedir(), ".symforge", "bin");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ext = processMod.platform === "win32" ? ".exe" : "";
|
|
32
|
+
const installDir = resolveInstallDir();
|
|
33
|
+
const binPath = pathMod.join(installDir, "symforge" + ext);
|
|
34
|
+
const pendingPath = pathMod.join(installDir, "symforge.pending" + ext);
|
|
35
|
+
|
|
36
|
+
function relayInstallerOutput(output) {
|
|
37
|
+
if (!output) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const text = typeof output === "string" ? output : String(output);
|
|
41
|
+
for (const line of text.split(/\r?\n/)) {
|
|
42
|
+
if (line) {
|
|
43
|
+
consoleMod.error(line);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getInstalledVersion() {
|
|
49
|
+
try {
|
|
50
|
+
const output = execFileSyncFn(binPath, ["--version"], {
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
env: processMod.env,
|
|
54
|
+
}).trim();
|
|
55
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
56
|
+
return match ? match[1] : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applyPendingUpdate() {
|
|
63
|
+
if (!fsMod.existsSync(pendingPath)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
fsMod.renameSync(pendingPath, binPath);
|
|
69
|
+
consoleMod.error("symforge: applied pending update.");
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runInstaller() {
|
|
77
|
+
try {
|
|
78
|
+
const stdout = execFileSyncFn(processMod.execPath, [installScriptPath], {
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
81
|
+
env: processMod.env,
|
|
82
|
+
});
|
|
83
|
+
relayInstallerOutput(stdout);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
relayInstallerOutput(error.stdout);
|
|
86
|
+
relayInstallerOutput(error.stderr);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function detectClients() {
|
|
92
|
+
const clients = [];
|
|
93
|
+
const home = osMod.homedir();
|
|
94
|
+
if (fsMod.existsSync(pathMod.join(home, ".claude"))) clients.push("claude");
|
|
95
|
+
if (fsMod.existsSync(pathMod.join(home, ".codex"))) clients.push("codex");
|
|
96
|
+
if (clients.length === 0 || clients.length >= 2) return "all";
|
|
97
|
+
return clients[0];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function runAutoInit() {
|
|
101
|
+
const client = detectClients();
|
|
102
|
+
consoleMod.error(`symforge: auto-configuring for ${client}...`);
|
|
103
|
+
try {
|
|
104
|
+
const output = execFileSyncFn(binPath, ["init", "--client", client], {
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
timeout: 15000,
|
|
107
|
+
env: processMod.env,
|
|
108
|
+
});
|
|
109
|
+
relayInstallerOutput(output);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
consoleMod.error(
|
|
112
|
+
`symforge: auto-init warning: ${error.message}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ensureInstalledBinary() {
|
|
118
|
+
const pendingApplied = applyPendingUpdate();
|
|
119
|
+
|
|
120
|
+
const expectedVersion = packageJson.version;
|
|
121
|
+
const hasBinary = fsMod.existsSync(binPath);
|
|
122
|
+
const installedVersion = hasBinary ? getInstalledVersion() : null;
|
|
123
|
+
|
|
124
|
+
if (installedVersion === expectedVersion) {
|
|
125
|
+
// If a pending update was just applied, run init to ensure config matches
|
|
126
|
+
if (pendingApplied) {
|
|
127
|
+
runAutoInit();
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!hasBinary) {
|
|
133
|
+
consoleMod.error("symforge binary not found. Running install...");
|
|
134
|
+
} else {
|
|
135
|
+
consoleMod.error(
|
|
136
|
+
`symforge binary version ${installedVersion || "unknown"} does not match wrapper version ${expectedVersion}. Running install...`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
runInstaller();
|
|
141
|
+
applyPendingUpdate();
|
|
142
|
+
|
|
143
|
+
if (!fsMod.existsSync(binPath)) {
|
|
144
|
+
throw new Error("symforge binary is still missing after install.");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function main(args) {
|
|
149
|
+
ensureInstalledBinary();
|
|
150
|
+
const result = spawnSyncFn(binPath, args, {
|
|
151
|
+
stdio: "inherit",
|
|
152
|
+
env: processMod.env,
|
|
153
|
+
});
|
|
154
|
+
return result.status ?? 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
applyPendingUpdate,
|
|
159
|
+
detectClients,
|
|
160
|
+
ensureInstalledBinary,
|
|
161
|
+
getInstalledVersion,
|
|
162
|
+
getBinaryPath: () => binPath,
|
|
163
|
+
getPendingPath: () => pendingPath,
|
|
164
|
+
main,
|
|
165
|
+
runAutoInit,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { createLauncher };
|
|
170
|
+
|
|
171
|
+
if (require.main === module) {
|
|
172
|
+
process.exit(createLauncher().main(process.argv.slice(2)));
|
|
173
|
+
}
|
package/bin/symforge.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "symforge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SymForge — in-memory code intelligence for Claude Code, Codex, and Gemini CLI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/special-place-administrator/symforge"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"symforge": "bin/symforge.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test tests/*.test.js",
|
|
15
|
+
"postinstall": "node scripts/install.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"scripts/"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"code-indexing",
|
|
24
|
+
"code-retrieval",
|
|
25
|
+
"ai-coding",
|
|
26
|
+
"tree-sitter",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"codex",
|
|
29
|
+
"gemini-cli",
|
|
30
|
+
"hooks"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const childProcess = require("child_process");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const https = require("https");
|
|
9
|
+
const http = require("http");
|
|
10
|
+
|
|
11
|
+
const REPO = "special-place-administrator/tokenizor_agentic_mcp";
|
|
12
|
+
|
|
13
|
+
function createInstaller(overrides = {}) {
|
|
14
|
+
const fsMod = overrides.fs || fs;
|
|
15
|
+
const pathMod = overrides.path || path;
|
|
16
|
+
const osMod = overrides.os || os;
|
|
17
|
+
const processMod = overrides.process || process;
|
|
18
|
+
const consoleMod = overrides.console || console;
|
|
19
|
+
const execSyncFn = overrides.execSync || childProcess.execSync;
|
|
20
|
+
const execFileSyncFn = overrides.execFileSync || childProcess.execFileSync;
|
|
21
|
+
const sleep = overrides.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
22
|
+
const packageJson = overrides.packageJson || require("../package.json");
|
|
23
|
+
|
|
24
|
+
function resolveInstallDir() {
|
|
25
|
+
if (overrides.installDir) {
|
|
26
|
+
return overrides.installDir;
|
|
27
|
+
}
|
|
28
|
+
if (processMod.env.SYMFORGE_HOME) {
|
|
29
|
+
return pathMod.join(processMod.env.SYMFORGE_HOME, "bin");
|
|
30
|
+
}
|
|
31
|
+
return pathMod.join(osMod.homedir(), ".symforge", "bin");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Binary lives outside node_modules so npm can update the JS wrapper
|
|
35
|
+
// even while the MCP server holds a lock on the running .exe (Windows).
|
|
36
|
+
const installDir = resolveInstallDir();
|
|
37
|
+
|
|
38
|
+
function getPlatformArtifact() {
|
|
39
|
+
const platform = processMod.platform;
|
|
40
|
+
const arch = processMod.arch;
|
|
41
|
+
|
|
42
|
+
if (platform === "win32" && arch === "x64") return "symforge-windows-x64.exe";
|
|
43
|
+
if (platform === "darwin" && arch === "arm64") return "symforge-macos-arm64";
|
|
44
|
+
if (platform === "darwin" && arch === "x64") return "symforge-macos-x64";
|
|
45
|
+
if (platform === "linux" && arch === "x64") return "symforge-linux-x64";
|
|
46
|
+
|
|
47
|
+
consoleMod.error(`Unsupported platform: ${platform}-${arch}`);
|
|
48
|
+
consoleMod.error("Build from source: https://github.com/" + REPO);
|
|
49
|
+
processMod.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getVersion() {
|
|
53
|
+
return packageJson.version;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getBinaryPath() {
|
|
57
|
+
const ext = processMod.platform === "win32" ? ".exe" : "";
|
|
58
|
+
return pathMod.join(installDir, "symforge" + ext);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getPendingPath() {
|
|
62
|
+
const ext = processMod.platform === "win32" ? ".exe" : "";
|
|
63
|
+
return pathMod.join(installDir, "symforge.pending" + ext);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function download(url) {
|
|
67
|
+
if (overrides.download) {
|
|
68
|
+
return overrides.download(url);
|
|
69
|
+
}
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const client = url.startsWith("https") ? https : http;
|
|
72
|
+
client.get(url, { headers: { "User-Agent": "symforge" } }, (res) => {
|
|
73
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
74
|
+
return download(res.headers.location).then(resolve).catch(reject);
|
|
75
|
+
}
|
|
76
|
+
if (res.statusCode !== 200) {
|
|
77
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
78
|
+
}
|
|
79
|
+
const chunks = [];
|
|
80
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
81
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
82
|
+
res.on("error", reject);
|
|
83
|
+
}).on("error", reject);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getInstalledVersion(binPath) {
|
|
88
|
+
try {
|
|
89
|
+
const output = execFileSyncFn(binPath, ["--version"], {
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
timeout: 5000,
|
|
92
|
+
}).trim();
|
|
93
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
94
|
+
return match ? match[1] : null;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isLockedError(error) {
|
|
101
|
+
return error && (error.code === "EPERM" || error.code === "EBUSY");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function removePendingIfPresent(pendingPath) {
|
|
105
|
+
try {
|
|
106
|
+
fsMod.unlinkSync(pendingPath);
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeInstalledBinary(binPath, pendingPath, data) {
|
|
111
|
+
fsMod.writeFileSync(binPath, data);
|
|
112
|
+
fsMod.chmodSync(binPath, 0o755);
|
|
113
|
+
removePendingIfPresent(pendingPath);
|
|
114
|
+
consoleMod.log(`Installed: ${binPath}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stopRunningWindowsProcesses(binPath) {
|
|
118
|
+
if (processMod.platform !== "win32") {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// NOTE: PowerShell -and operators must stay on the same line as their
|
|
123
|
+
// operands — semicolons are statement terminators, not line joiners.
|
|
124
|
+
const script = [
|
|
125
|
+
"$target = [System.IO.Path]::GetFullPath($env:SYMFORGE_TARGET_BIN)",
|
|
126
|
+
"$comparer = [System.StringComparer]::OrdinalIgnoreCase",
|
|
127
|
+
"$procs = Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'symforge.exe' -and $_.ExecutablePath -and $comparer.Equals([System.IO.Path]::GetFullPath($_.ExecutablePath), $target) }",
|
|
128
|
+
"$ids = @($procs | ForEach-Object { [int]$_.ProcessId })",
|
|
129
|
+
"if ($ids.Count -gt 0) { Stop-Process -Id $ids -Force -ErrorAction SilentlyContinue; $ids | ConvertTo-Json -Compress }",
|
|
130
|
+
].join("; ");
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const output = execFileSyncFn(
|
|
134
|
+
"powershell.exe",
|
|
135
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script],
|
|
136
|
+
{
|
|
137
|
+
encoding: "utf8",
|
|
138
|
+
env: { ...processMod.env, SYMFORGE_TARGET_BIN: binPath },
|
|
139
|
+
}
|
|
140
|
+
).trim();
|
|
141
|
+
|
|
142
|
+
if (!output) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const parsed = JSON.parse(output);
|
|
146
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
147
|
+
} catch (error) {
|
|
148
|
+
consoleMod.log(
|
|
149
|
+
`Failed to stop running SymForge processes automatically: ${error.message}`
|
|
150
|
+
);
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Stop symforge *daemon* processes (the background server), but leave
|
|
157
|
+
* the stdio MCP process alive — it may be actively serving Claude Code.
|
|
158
|
+
*
|
|
159
|
+
* Daemons are identifiable because they were launched with the `daemon` arg,
|
|
160
|
+
* visible in the process command line. On Windows we filter via WMI
|
|
161
|
+
* CommandLine; on Unix we use `pkill -f`.
|
|
162
|
+
*
|
|
163
|
+
* Returns an array of killed PIDs (Windows) or [] (Unix, best-effort).
|
|
164
|
+
*/
|
|
165
|
+
function stopDaemonProcesses() {
|
|
166
|
+
if (processMod.platform === "win32") {
|
|
167
|
+
// Match symforge.exe processes whose CommandLine contains " daemon"
|
|
168
|
+
// This avoids killing the MCP stdio process that Claude Code is using.
|
|
169
|
+
// NOTE: PowerShell -and operators must stay on the same line as their
|
|
170
|
+
// operands — semicolons are statement terminators, not line joiners.
|
|
171
|
+
const script = [
|
|
172
|
+
"$procs = Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'symforge.exe' -and $_.CommandLine -and $_.CommandLine -match '\\bdaemon\\b' }",
|
|
173
|
+
"$ids = @($procs | ForEach-Object { [int]$_.ProcessId })",
|
|
174
|
+
"if ($ids.Count -gt 0) { Stop-Process -Id $ids -Force -ErrorAction SilentlyContinue; $ids | ConvertTo-Json -Compress }",
|
|
175
|
+
].join("; ");
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const output = execFileSyncFn(
|
|
179
|
+
"powershell.exe",
|
|
180
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script],
|
|
181
|
+
{ encoding: "utf8", env: processMod.env }
|
|
182
|
+
).trim();
|
|
183
|
+
|
|
184
|
+
if (!output) return [];
|
|
185
|
+
const parsed = JSON.parse(output);
|
|
186
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
187
|
+
} catch (error) {
|
|
188
|
+
consoleMod.log(
|
|
189
|
+
`Note: could not stop daemon processes: ${error.message}`
|
|
190
|
+
);
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Unix: kill only daemon processes (best-effort)
|
|
196
|
+
try {
|
|
197
|
+
execSyncFn("pkill -f 'symforge daemon' 2>/dev/null || true", {
|
|
198
|
+
encoding: "utf8",
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
// Ignore — process may not exist
|
|
202
|
+
}
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function retryInstallAfterStop(binPath, pendingPath, data) {
|
|
207
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
208
|
+
try {
|
|
209
|
+
writeInstalledBinary(binPath, pendingPath, data);
|
|
210
|
+
return true;
|
|
211
|
+
} catch (retryErr) {
|
|
212
|
+
if (!isLockedError(retryErr)) {
|
|
213
|
+
throw retryErr;
|
|
214
|
+
}
|
|
215
|
+
await sleep(250);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function installDownloadedBinary(binPath, pendingPath, data) {
|
|
222
|
+
fsMod.mkdirSync(installDir, { recursive: true });
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
writeInstalledBinary(binPath, pendingPath, data);
|
|
226
|
+
return { status: "installed", stoppedProcessIds: [] };
|
|
227
|
+
} catch (writeErr) {
|
|
228
|
+
if (!isLockedError(writeErr)) {
|
|
229
|
+
throw writeErr;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const stoppedProcessIds = stopRunningWindowsProcesses(binPath);
|
|
233
|
+
if (stoppedProcessIds.length > 0) {
|
|
234
|
+
consoleMod.log(
|
|
235
|
+
`Stopping ${stoppedProcessIds.length} running SymForge process(es) so the update can be applied...`
|
|
236
|
+
);
|
|
237
|
+
const installedAfterStop = await retryInstallAfterStop(binPath, pendingPath, data);
|
|
238
|
+
if (installedAfterStop) {
|
|
239
|
+
return { status: "installed", stoppedProcessIds };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fsMod.writeFileSync(pendingPath, data);
|
|
244
|
+
fsMod.chmodSync(pendingPath, 0o755);
|
|
245
|
+
consoleMod.log(`Binary is locked (MCP server running). Staged update at: ${pendingPath}`);
|
|
246
|
+
consoleMod.log(`Update will apply automatically on next launch.`);
|
|
247
|
+
return { status: "staged", stoppedProcessIds };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Detect which CLI agents are installed and return the appropriate
|
|
253
|
+
* `--client` flag value for `symforge init`.
|
|
254
|
+
*/
|
|
255
|
+
function detectClients() {
|
|
256
|
+
const clients = [];
|
|
257
|
+
|
|
258
|
+
// Claude Code: check for ~/.claude directory
|
|
259
|
+
const claudeDir = pathMod.join(osMod.homedir(), ".claude");
|
|
260
|
+
if (fsMod.existsSync(claudeDir)) {
|
|
261
|
+
clients.push("claude");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Codex: check for ~/.codex directory
|
|
265
|
+
const codexDir = pathMod.join(osMod.homedir(), ".codex");
|
|
266
|
+
if (fsMod.existsSync(codexDir)) {
|
|
267
|
+
clients.push("codex");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Gemini: check for ~/.gemini directory
|
|
271
|
+
const geminiDir = pathMod.join(osMod.homedir(), ".gemini");
|
|
272
|
+
if (fsMod.existsSync(geminiDir)) {
|
|
273
|
+
clients.push("gemini");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If all, none, or more than 1 detected, use "all"
|
|
277
|
+
if (clients.length === 0 || clients.length >= 2) {
|
|
278
|
+
return "all";
|
|
279
|
+
}
|
|
280
|
+
return clients[0];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Run `symforge init` after successful install to configure
|
|
285
|
+
* hooks and MCP server registration for detected CLI agents.
|
|
286
|
+
*/
|
|
287
|
+
function runAutoInit(binPath) {
|
|
288
|
+
const client = detectClients();
|
|
289
|
+
consoleMod.log(`Auto-configuring for detected client(s): ${client}`);
|
|
290
|
+
try {
|
|
291
|
+
const output = execFileSyncFn(binPath, ["init", "--client", client], {
|
|
292
|
+
encoding: "utf8",
|
|
293
|
+
timeout: 15000,
|
|
294
|
+
env: processMod.env,
|
|
295
|
+
});
|
|
296
|
+
if (output) {
|
|
297
|
+
for (const line of output.trim().split(/\r?\n/)) {
|
|
298
|
+
consoleMod.log(line);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
consoleMod.log(
|
|
303
|
+
`Auto-init warning: ${error.message}\nYou can run manually: symforge init --client all`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function main() {
|
|
309
|
+
const binPath = getBinaryPath();
|
|
310
|
+
const pendingPath = getPendingPath();
|
|
311
|
+
const version = getVersion();
|
|
312
|
+
|
|
313
|
+
// Skip only if binary exists AND matches the expected version
|
|
314
|
+
if (fsMod.existsSync(binPath)) {
|
|
315
|
+
const installed = getInstalledVersion(binPath);
|
|
316
|
+
if (installed === version) {
|
|
317
|
+
removePendingIfPresent(pendingPath);
|
|
318
|
+
consoleMod.log(`symforge v${version} already installed at ${binPath}`);
|
|
319
|
+
// Still run init to ensure config is up to date
|
|
320
|
+
runAutoInit(binPath);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
consoleMod.log(
|
|
324
|
+
`symforge v${installed || "unknown"} found, updating to v${version}...`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Stop the background daemon process before install. The daemon holds a
|
|
329
|
+
// file handle on the binary (Windows), but the MCP stdio process is left
|
|
330
|
+
// alive so Claude Code keeps working. If the binary is still locked by
|
|
331
|
+
// the stdio process, the installer will stage to .pending and the
|
|
332
|
+
// launcher will apply it on next start.
|
|
333
|
+
const stoppedPids = stopDaemonProcesses();
|
|
334
|
+
if (stoppedPids.length > 0) {
|
|
335
|
+
consoleMod.log(
|
|
336
|
+
`Stopped ${stoppedPids.length} symforge daemon process(es) for update`
|
|
337
|
+
);
|
|
338
|
+
// Brief pause to let OS release file handles
|
|
339
|
+
await sleep(500);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const artifact = getPlatformArtifact();
|
|
343
|
+
const url = `https://github.com/${REPO}/releases/download/v${version}/${artifact}`;
|
|
344
|
+
|
|
345
|
+
consoleMod.log(
|
|
346
|
+
`Downloading symforge v${version} for ${processMod.platform}-${processMod.arch}...`
|
|
347
|
+
);
|
|
348
|
+
consoleMod.log(` ${url}`);
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const data = await download(url);
|
|
352
|
+
const result = await installDownloadedBinary(binPath, pendingPath, data);
|
|
353
|
+
|
|
354
|
+
// Clean up npm cache to reclaim disk space after download.
|
|
355
|
+
// npm cache grows unbounded over time; verify removes stale entries.
|
|
356
|
+
try {
|
|
357
|
+
execSyncFn("npm cache verify --silent", {
|
|
358
|
+
encoding: "utf8",
|
|
359
|
+
timeout: 30000,
|
|
360
|
+
stdio: "ignore",
|
|
361
|
+
});
|
|
362
|
+
consoleMod.log("npm cache verified (stale entries cleaned)");
|
|
363
|
+
} catch {
|
|
364
|
+
// Non-fatal — skip if npm isn't available or verify fails
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (result.status === "installed") {
|
|
368
|
+
// Binary replaced in place — run init now.
|
|
369
|
+
runAutoInit(binPath);
|
|
370
|
+
} else if (result.status === "staged") {
|
|
371
|
+
// Binary is staged as .pending because the MCP server is still running.
|
|
372
|
+
// Init will run automatically on next launch via the launcher.
|
|
373
|
+
consoleMod.log(
|
|
374
|
+
"Auto-init deferred: will run on next launch when the pending update is applied."
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
consoleMod.error(`Failed to download binary: ${err.message}`);
|
|
379
|
+
consoleMod.error("");
|
|
380
|
+
consoleMod.error("You can build from source instead:");
|
|
381
|
+
consoleMod.error(" git clone https://github.com/" + REPO);
|
|
382
|
+
consoleMod.error(" cd tokenizor_agentic_mcp");
|
|
383
|
+
consoleMod.error(" cargo build --release");
|
|
384
|
+
processMod.exit(1);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
getBinaryPath,
|
|
390
|
+
getPendingPath,
|
|
391
|
+
getInstalledVersion,
|
|
392
|
+
installDownloadedBinary,
|
|
393
|
+
isLockedError,
|
|
394
|
+
main,
|
|
395
|
+
stopRunningWindowsProcesses,
|
|
396
|
+
stopDaemonProcesses,
|
|
397
|
+
detectClients,
|
|
398
|
+
runAutoInit,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = { createInstaller };
|
|
403
|
+
|
|
404
|
+
if (require.main === module) {
|
|
405
|
+
createInstaller().main();
|
|
406
|
+
}
|