@axiomatic-labs/claudeflow 2.49.18 → 2.49.19
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/lib/install.js +263 -3
- package/package.json +1 -1
package/lib/install.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
|
-
const { execSync } = require('child_process');
|
|
5
|
+
const { execFileSync, execSync } = require('child_process');
|
|
6
6
|
const { requireAuth } = require('./auth.js');
|
|
7
7
|
const { getLatestRelease, downloadReleaseAsset } = require('./download.js');
|
|
8
8
|
const { writeLocalVersion, readLocalVersion } = require('./version.js');
|
|
@@ -27,12 +27,233 @@ const PER_PROJECT_CONFIGS = ['brand.json', 'risk-signals.json', 'test-context.js
|
|
|
27
27
|
// Paths appended to the project's .gitignore (add-if-absent). These hold TEST credentials / Playwright
|
|
28
28
|
// storage state — they must NEVER be committed. test-context.json is per-project runtime state; .auth/
|
|
29
29
|
// holds storageState files the orchestrator seeds for auth-gated functional verification.
|
|
30
|
-
const GITIGNORE_LINES = ['.claudeflow/test-context.json', '.claudeflow/.auth/', '.claudeflow/telemetry/', '.claudeflow/certification/'];
|
|
30
|
+
const GITIGNORE_LINES = ['.claudeflow/test-context.json', '.claudeflow/.auth/', '.claudeflow/telemetry/', '.claudeflow/certification/', '.claudeflow/tools/'];
|
|
31
31
|
const SHARED_CONFIGS = ['playbook-map.json', 'agent-map.json']; // always refreshed (shared system)
|
|
32
32
|
const CODEX_DOCS = ['claudeflow-codex-migration.md', 'codexflow-codex-native-workflow.md', 'codexflow-local-actions.md'];
|
|
33
33
|
|
|
34
34
|
function rmrf(p) { fs.rmSync(p, { recursive: true, force: true }); }
|
|
35
35
|
|
|
36
|
+
function findExecutable(name) {
|
|
37
|
+
const envPath = process.env.PATH || '';
|
|
38
|
+
const exts = process.platform === 'win32'
|
|
39
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
40
|
+
: [''];
|
|
41
|
+
for (const dir of envPath.split(path.delimiter)) {
|
|
42
|
+
if (!dir) continue;
|
|
43
|
+
for (const ext of exts) {
|
|
44
|
+
const p = path.join(dir, process.platform === 'win32' ? name + ext : name);
|
|
45
|
+
try {
|
|
46
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
47
|
+
return p;
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function optionalSerenaCommand(options = {}) {
|
|
55
|
+
if (options.configureSerena !== true && options.configureTools !== true) return null;
|
|
56
|
+
if (options.noSerena === true || process.env.CLAUDEFLOW_NO_SERENA === '1') return null;
|
|
57
|
+
if (options.serenaCommand) return String(options.serenaCommand);
|
|
58
|
+
if (process.env.CLAUDEFLOW_SERENA_COMMAND) return process.env.CLAUDEFLOW_SERENA_COMMAND;
|
|
59
|
+
return findExecutable('serena');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function serenaMcpServer(command, context) {
|
|
63
|
+
return {
|
|
64
|
+
command,
|
|
65
|
+
args: ['start-mcp-server', '--project-from-cwd', `--context=${context}`, '--open-web-dashboard', 'false'],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function tomlString(value) {
|
|
70
|
+
return JSON.stringify(String(value));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function serenaCodexToml(command) {
|
|
74
|
+
return [
|
|
75
|
+
'[mcp_servers.serena]',
|
|
76
|
+
`command = ${tomlString(command)}`,
|
|
77
|
+
'args = ["start-mcp-server", "--project-from-cwd", "--context=codex", "--open-web-dashboard", "false"]',
|
|
78
|
+
'startup_timeout_sec = 30',
|
|
79
|
+
'tool_timeout_sec = 120',
|
|
80
|
+
'',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function exeName(name) {
|
|
85
|
+
return process.platform === 'win32' ? `${name}.cmd` : name;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function managedToolsHome(options = {}) {
|
|
89
|
+
return options.toolsHome || path.join(options.homeDir || os.homedir(), '.claudeflow', 'tools');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function projectToolsBin(cwd) {
|
|
93
|
+
return path.join(cwd, '.claudeflow', 'tools', 'bin');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function runQuiet(command, args, options = {}) {
|
|
97
|
+
execFileSync(command, args, {
|
|
98
|
+
cwd: options.cwd || process.cwd(),
|
|
99
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
100
|
+
stdio: 'ignore',
|
|
101
|
+
timeout: options.timeout || 120000,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function executableOk(command, args = ['--help']) {
|
|
106
|
+
if (!command) return false;
|
|
107
|
+
try {
|
|
108
|
+
runQuiet(command, args, { timeout: 20000 });
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writeToolWrapper(cwd, name, target) {
|
|
116
|
+
const bin = projectToolsBin(cwd);
|
|
117
|
+
fs.mkdirSync(bin, { recursive: true });
|
|
118
|
+
if (process.platform === 'win32') {
|
|
119
|
+
const wrapper = path.join(bin, `${name}.cmd`);
|
|
120
|
+
fs.writeFileSync(wrapper, `@echo off\r\n"${target}" %*\r\n`);
|
|
121
|
+
return wrapper;
|
|
122
|
+
}
|
|
123
|
+
const wrapper = path.join(bin, name);
|
|
124
|
+
fs.writeFileSync(wrapper, `#!/usr/bin/env sh\nexec "${target}" "$@"\n`);
|
|
125
|
+
fs.chmodSync(wrapper, 0o755);
|
|
126
|
+
return wrapper;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function uvToolBinDir(uvCommand) {
|
|
130
|
+
try {
|
|
131
|
+
return String(execFileSync(uvCommand, ['tool', 'dir', '--bin'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 20000 })).trim();
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function pythonVersionOk(command) {
|
|
138
|
+
if (!command) return false;
|
|
139
|
+
try {
|
|
140
|
+
const out = String(execFileSync(command, ['-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'], {
|
|
141
|
+
encoding: 'utf8',
|
|
142
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
143
|
+
timeout: 20000,
|
|
144
|
+
})).trim();
|
|
145
|
+
const [maj, min] = out.split('.').map(n => parseInt(n, 10) || 0);
|
|
146
|
+
return maj > 3 || (maj === 3 && min >= 11);
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findPython311(options = {}) {
|
|
153
|
+
const candidates = [
|
|
154
|
+
options.pythonCommand,
|
|
155
|
+
process.env.CLAUDEFLOW_PYTHON_COMMAND,
|
|
156
|
+
findExecutable('python3.13'),
|
|
157
|
+
findExecutable('python3.12'),
|
|
158
|
+
findExecutable('python3.11'),
|
|
159
|
+
findExecutable('python3'),
|
|
160
|
+
].filter(Boolean);
|
|
161
|
+
return candidates.find(pythonVersionOk) || null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function venvBinPath(venv, name) {
|
|
165
|
+
return process.platform === 'win32'
|
|
166
|
+
? path.join(venv, 'Scripts', `${name}.exe`)
|
|
167
|
+
: path.join(venv, 'bin', name);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function installSerenaWithPip(options = {}) {
|
|
171
|
+
const python = findPython311(options);
|
|
172
|
+
if (!python) return null;
|
|
173
|
+
const venv = path.join(managedToolsHome(options), 'serena-venv');
|
|
174
|
+
try {
|
|
175
|
+
if (!fs.existsSync(venvBinPath(venv, 'python'))) {
|
|
176
|
+
runQuiet(python, ['-m', 'venv', venv], { timeout: 180000 });
|
|
177
|
+
}
|
|
178
|
+
const venvPython = venvBinPath(venv, 'python');
|
|
179
|
+
runQuiet(venvPython, ['-m', 'pip', 'install', '-U', 'serena-agent'], { timeout: 300000 });
|
|
180
|
+
const command = venvBinPath(venv, 'serena');
|
|
181
|
+
return executableOk(command, ['--help']) ? command : null;
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ensureSerenaTool(cwd, options = {}) {
|
|
188
|
+
if (options.noSerena === true || process.env.CLAUDEFLOW_NO_SERENA === '1') {
|
|
189
|
+
return { status: 'skipped', reason: 'CLAUDEFLOW_NO_SERENA=1' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let command = options.serenaCommand || process.env.CLAUDEFLOW_SERENA_COMMAND || findExecutable('serena');
|
|
193
|
+
if (!executableOk(command, ['--help'])) {
|
|
194
|
+
const uv = options.uvCommand || findExecutable('uv');
|
|
195
|
+
if (uv) {
|
|
196
|
+
try {
|
|
197
|
+
runQuiet(uv, ['tool', 'install', '-p', '3.13', 'serena-agent'], { timeout: 300000 });
|
|
198
|
+
} catch {}
|
|
199
|
+
const binDir = uvToolBinDir(uv);
|
|
200
|
+
const installed = binDir ? path.join(binDir, exeName('serena')) : null;
|
|
201
|
+
command = executableOk(installed, ['--help']) ? installed : findExecutable('serena');
|
|
202
|
+
}
|
|
203
|
+
if (!executableOk(command, ['--help'])) command = installSerenaWithPip(options);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!executableOk(command, ['--help'])) {
|
|
207
|
+
return { status: 'failed', reason: 'serena install failed; need uv or Python 3.11+ with pip' };
|
|
208
|
+
}
|
|
209
|
+
const wrapper = writeToolWrapper(cwd, 'serena', command);
|
|
210
|
+
return { status: 'ready', command: wrapper, target: command };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function ensureAstGrepTool(cwd, options = {}) {
|
|
214
|
+
if (options.noAstGrep === true || process.env.CLAUDEFLOW_NO_AST_GREP === '1') {
|
|
215
|
+
return { status: 'skipped', reason: 'CLAUDEFLOW_NO_AST_GREP=1' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const home = managedToolsHome(options);
|
|
219
|
+
const prefix = path.join(home, 'ast-grep');
|
|
220
|
+
const managed = path.join(prefix, 'node_modules', '.bin', exeName('ast-grep'));
|
|
221
|
+
let command = options.astGrepCommand || process.env.CLAUDEFLOW_AST_GREP_COMMAND || null;
|
|
222
|
+
if (!executableOk(command, ['--version'])) {
|
|
223
|
+
command = executableOk(managed, ['--version'])
|
|
224
|
+
? managed
|
|
225
|
+
: (findExecutable('ast-grep') || findExecutable('sg'));
|
|
226
|
+
}
|
|
227
|
+
if (!executableOk(command, ['--version'])) {
|
|
228
|
+
const npm = options.npmCommand || findExecutable('npm');
|
|
229
|
+
if (!npm) return { status: 'failed', reason: 'npm not found; ast-grep managed install requires npm' };
|
|
230
|
+
try {
|
|
231
|
+
fs.mkdirSync(prefix, { recursive: true });
|
|
232
|
+
runQuiet(npm, ['install', '--prefix', prefix, '@ast-grep/cli@latest'], { timeout: 300000 });
|
|
233
|
+
} catch {
|
|
234
|
+
return { status: 'failed', reason: 'npm install @ast-grep/cli failed' };
|
|
235
|
+
}
|
|
236
|
+
command = managed;
|
|
237
|
+
}
|
|
238
|
+
if (!executableOk(command, ['--version'])) {
|
|
239
|
+
return { status: 'failed', reason: 'ast-grep command did not validate after install' };
|
|
240
|
+
}
|
|
241
|
+
const wrapper = writeToolWrapper(cwd, 'ast-grep', command);
|
|
242
|
+
// `sg` is convenient on macOS, but Linux reserves `sg`; still provide the wrapper because it is project-local.
|
|
243
|
+
const sgWrapper = writeToolWrapper(cwd, 'sg', command);
|
|
244
|
+
return { status: 'ready', command: wrapper, sgCommand: sgWrapper, target: command };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function ensureManagedTools(cwd, options = {}) {
|
|
248
|
+
if (options.configureTools !== true || process.env.CLAUDEFLOW_NO_TOOLS === '1') {
|
|
249
|
+
return { serena: { status: 'skipped', reason: 'tool bootstrap disabled' }, astGrep: { status: 'skipped', reason: 'tool bootstrap disabled' } };
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
serena: ensureSerenaTool(cwd, options),
|
|
253
|
+
astGrep: ensureAstGrepTool(cwd, options),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
36
257
|
function copyDir(src, dst) {
|
|
37
258
|
fs.mkdirSync(dst, { recursive: true });
|
|
38
259
|
for (const e of fs.readdirSync(src, { withFileTypes: true })) {
|
|
@@ -393,6 +614,9 @@ function installFromPayload(src, cwd, options = {}) {
|
|
|
393
614
|
codexPlugin: 0,
|
|
394
615
|
docs: 0,
|
|
395
616
|
mcp: 0,
|
|
617
|
+
serenaMcp: 0,
|
|
618
|
+
serenaCodexConfig: false,
|
|
619
|
+
tooling: {},
|
|
396
620
|
gitignore: 0,
|
|
397
621
|
claudeMd: false,
|
|
398
622
|
agentsMd: false,
|
|
@@ -428,6 +652,17 @@ function installFromPayload(src, cwd, options = {}) {
|
|
|
428
652
|
}
|
|
429
653
|
});
|
|
430
654
|
|
|
655
|
+
// 2b) Managed tool bootstrap — installs/validates semantic-code accelerators once per machine and writes
|
|
656
|
+
// small project-local wrappers under .claudeflow/tools/bin. Deps stay outside the repo; the wrappers give
|
|
657
|
+
// agents stable commands even when their shell PATH differs from the user's interactive shell.
|
|
658
|
+
step('managed-tools', () => {
|
|
659
|
+
out.tooling = ensureManagedTools(cwd, options);
|
|
660
|
+
for (const name of ['serena', 'astGrep']) {
|
|
661
|
+
const tool = out.tooling[name];
|
|
662
|
+
if (tool && tool.status === 'failed') out.failed.push(`tools:${name}:${tool.reason}`);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
431
666
|
// 3) Scripts (hooks + scans + servers) — always refreshed. Per-file isolation so ONE bad file (a socket, a
|
|
432
667
|
// permission denial) can't abort the whole copy — let alone the later settings/CLAUDE.md steps.
|
|
433
668
|
step('scripts', () => {
|
|
@@ -532,6 +767,14 @@ function installFromPayload(src, cwd, options = {}) {
|
|
|
532
767
|
}
|
|
533
768
|
});
|
|
534
769
|
|
|
770
|
+
// 7b) Optional semantic-code MCP. Serena is useful for fresh implementers, but it must not become a hidden
|
|
771
|
+
// dependency. The real install auto-registers it only when a `serena` executable already exists (or the caller
|
|
772
|
+
// passes an explicit command); tests/direct installFromPayload stay deterministic unless configureSerena=true.
|
|
773
|
+
step('serena-mcp', () => {
|
|
774
|
+
const serena = (out.tooling.serena && out.tooling.serena.command) || optionalSerenaCommand(options);
|
|
775
|
+
if (serena) out.serenaMcp = mergeMcp(cwd, { serena: serenaMcpServer(serena, 'claude-code') });
|
|
776
|
+
});
|
|
777
|
+
|
|
535
778
|
// 8) AGENTS.md — upsert the Codex guidance block between markers, preserving project guidance.
|
|
536
779
|
step('agents-md', () => {
|
|
537
780
|
out.agentsMd = syncAgentsMd(cwd, path.join(src, 'assets', 'AGENTS.snippet.md'));
|
|
@@ -546,6 +789,10 @@ function installFromPayload(src, cwd, options = {}) {
|
|
|
546
789
|
const srcConfig = path.join(src, '.codex', 'config.toml');
|
|
547
790
|
if (fs.existsSync(srcConfig)) out.codexConfig = mergeCodexConfig(cwd, fs.readFileSync(srcConfig, 'utf8'));
|
|
548
791
|
});
|
|
792
|
+
step('serena-codex-config', () => {
|
|
793
|
+
const serena = (out.tooling.serena && out.tooling.serena.command) || optionalSerenaCommand(options);
|
|
794
|
+
if (serena) out.serenaCodexConfig = mergeCodexConfig(cwd, serenaCodexToml(serena));
|
|
795
|
+
});
|
|
549
796
|
|
|
550
797
|
// 10) Codex marketplace + local plugin — managed codexflow entry only; user plugins are preserved.
|
|
551
798
|
step('codex-marketplace', () => {
|
|
@@ -633,7 +880,11 @@ async function run() {
|
|
|
633
880
|
execSync(`unzip -qq -o "${tmpZip}" -d "${tmp}"`, { stdio: 'ignore' });
|
|
634
881
|
|
|
635
882
|
ui.step('Installing framework...');
|
|
636
|
-
const summary = installFromPayload(tmp, cwd, {
|
|
883
|
+
const summary = installFromPayload(tmp, cwd, {
|
|
884
|
+
syncGlobalClaude: process.env.CLAUDEFLOW_NO_GLOBAL_SYNC !== '1',
|
|
885
|
+
configureTools: true,
|
|
886
|
+
configureSerena: true,
|
|
887
|
+
});
|
|
637
888
|
writeLocalVersion(`v${version}`);
|
|
638
889
|
|
|
639
890
|
try { rmrf(tmp); fs.rmSync(tmpZip, { force: true }); } catch {}
|
|
@@ -649,6 +900,9 @@ async function run() {
|
|
|
649
900
|
+ (summary.codexAgents ? ` · ${summary.codexAgents} Codex agents` : '')
|
|
650
901
|
+ (summary.codexHooks ? ' · Codex hooks merged' : '')
|
|
651
902
|
+ (summary.codexConfig ? ' · Codex config synced' : '')
|
|
903
|
+
+ (summary.tooling && summary.tooling.serena && summary.tooling.serena.status === 'ready' ? ' · Serena ready' : '')
|
|
904
|
+
+ (summary.tooling && summary.tooling.astGrep && summary.tooling.astGrep.status === 'ready' ? ' · ast-grep ready' : '')
|
|
905
|
+
+ (summary.serenaMcp || summary.serenaCodexConfig ? ' · Serena MCP configured' : '')
|
|
652
906
|
+ (summary.codexMarketplace ? ' · Codex plugin registered' : '')
|
|
653
907
|
+ (summary.mcp ? ` · ${summary.mcp} MCP server${summary.mcp > 1 ? 's' : ''} added to .mcp.json` : '')
|
|
654
908
|
+ (summary.claudeMd ? ' · CLAUDE.md guidance synced' : '')
|
|
@@ -670,3 +924,9 @@ module.exports.mergeGitignore = mergeGitignore;
|
|
|
670
924
|
module.exports.mergeCodexHooks = mergeCodexHooks;
|
|
671
925
|
module.exports.mergeCodexConfig = mergeCodexConfig;
|
|
672
926
|
module.exports.mergeCodexMarketplace = mergeCodexMarketplace;
|
|
927
|
+
module.exports.optionalSerenaCommand = optionalSerenaCommand;
|
|
928
|
+
module.exports.serenaMcpServer = serenaMcpServer;
|
|
929
|
+
module.exports.serenaCodexToml = serenaCodexToml;
|
|
930
|
+
module.exports.ensureManagedTools = ensureManagedTools;
|
|
931
|
+
module.exports.ensureSerenaTool = ensureSerenaTool;
|
|
932
|
+
module.exports.ensureAstGrepTool = ensureAstGrepTool;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.49.
|
|
3
|
+
"version": "2.49.19",
|
|
4
4
|
"description": "Claudeflow — AI-powered development toolkit for Claude Code and Codex. Skills, agents, hooks, and quality gates that ship production apps.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claudeflow": "./bin/cli.js"
|