@hmj-ai/cflow 1.1.0 → 1.2.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/DESIGN.md +2 -1
- package/README.md +7 -6
- package/dist/public/assets/index-C1_cd_s8.css +1 -0
- package/dist/public/assets/index-DsBDM4p-.js +15 -0
- package/dist/public/index.html +2 -2
- package/dist/src/db.js +2 -2
- package/dist/src/runtime-manifest.js +88 -20
- package/dist/src/runtime-process.js +152 -88
- package/dist/src/runtime.js +160 -120
- package/dist/src/server.js +44 -107
- package/dist/src/workspace.js +43 -2
- package/package.json +2 -1
- package/dist/public/assets/index-BqTfYp5s.js +0 -15
- package/dist/public/assets/index-D0BpmA_V.css +0 -1
package/dist/public/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="color-scheme" content="light" />
|
|
7
7
|
<link rel="icon" href="data:," />
|
|
8
8
|
<title>CF Platform</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DsBDM4p-.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C1_cd_s8.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/dist/src/db.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import { mkdirSync } from 'node:fs';
|
|
3
|
-
import { dirname } from 'node:path';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
4
|
export class Store {
|
|
5
5
|
db;
|
|
6
|
-
constructor(file = process.
|
|
6
|
+
constructor(file = join(process.cwd(), '.cflow', 'cflow.sqlite')) {
|
|
7
7
|
mkdirSync(dirname(file), { recursive: true });
|
|
8
8
|
this.db = new Database(file);
|
|
9
9
|
this.db.pragma('journal_mode = WAL');
|
|
@@ -2,29 +2,96 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
-
const
|
|
5
|
+
export const builtinAdapterDescriptors = [
|
|
6
6
|
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
manifest: {
|
|
8
|
+
schemaVersion: 1,
|
|
9
|
+
id: 'codex',
|
|
10
|
+
name: 'Codex',
|
|
11
|
+
description: '用 CFlow 随包提供的 Codex 来执行步骤。默认只读;能力声明 workspace 写入后可修改工作区文件。',
|
|
12
|
+
backend: 'acp',
|
|
13
|
+
command: 'codex-acp',
|
|
14
|
+
envAllowlist: ['HOME', 'LANG', 'LC_ALL', 'CODEX_HOME', 'OPENAI_API_KEY'],
|
|
15
|
+
capabilities: ['reasoning', 'code', 'structured-output', 'workspace-read'],
|
|
16
|
+
traits: { tokenAccounting: 'approximate' },
|
|
17
|
+
},
|
|
18
|
+
commandAliases: ['codex-acp'],
|
|
19
|
+
bundled: true,
|
|
20
|
+
modelEnvironment: 'CODEX_CONFIG',
|
|
21
|
+
nativeCommand: {
|
|
22
|
+
adapterEnvironment: 'CODEX_PATH',
|
|
23
|
+
overrideEnvironment: 'CFLOW_CODEX_PATH',
|
|
24
|
+
bundled: 'codex',
|
|
25
|
+
},
|
|
26
|
+
permissionMode: {
|
|
27
|
+
environment: 'INITIAL_AGENT_MODE',
|
|
28
|
+
readOnly: 'read-only',
|
|
29
|
+
workspaceWrite: 'workspace-write',
|
|
30
|
+
},
|
|
31
|
+
staticEnvironment: { NO_BROWSER: '1' },
|
|
16
32
|
},
|
|
17
33
|
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
manifest: {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
id: 'claude-code',
|
|
37
|
+
name: 'Claude Code',
|
|
38
|
+
description: '用 CFlow 随包提供的 Claude Code 来执行步骤。',
|
|
39
|
+
backend: 'acp',
|
|
40
|
+
command: 'claude-agent-acp',
|
|
41
|
+
envAllowlist: ['HOME', 'LANG', 'LC_ALL', 'ANTHROPIC_API_KEY'],
|
|
42
|
+
capabilities: ['reasoning', 'code', 'structured-output', 'workspace-read'],
|
|
43
|
+
},
|
|
44
|
+
commandAliases: ['claude-agent-acp'],
|
|
45
|
+
bundled: true,
|
|
46
|
+
modelEnvironment: 'CLAUDE_MODEL_CONFIG',
|
|
47
|
+
nativeCommand: {
|
|
48
|
+
adapterEnvironment: 'CLAUDE_CODE_EXECUTABLE',
|
|
49
|
+
overrideEnvironment: 'CFLOW_CLAUDE_PATH',
|
|
50
|
+
bundled: 'adapter',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
export const externalAdapterDescriptors = [
|
|
55
|
+
{
|
|
56
|
+
manifest: {
|
|
57
|
+
schemaVersion: 1,
|
|
58
|
+
id: 'amp',
|
|
59
|
+
name: 'Amp',
|
|
60
|
+
description: '通过本机安装的 Amp ACP adapter 执行步骤。',
|
|
61
|
+
backend: 'acp',
|
|
62
|
+
command: 'amp-acp',
|
|
63
|
+
},
|
|
64
|
+
commandAliases: ['amp-acp'],
|
|
65
|
+
bundled: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
manifest: {
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
id: 'pi',
|
|
71
|
+
name: 'Pi',
|
|
72
|
+
description: '通过本机安装的 Pi ACP adapter 执行步骤。',
|
|
73
|
+
backend: 'acp',
|
|
74
|
+
command: 'pi-acp',
|
|
75
|
+
},
|
|
76
|
+
commandAliases: ['pi-acp'],
|
|
77
|
+
bundled: false,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
manifest: {
|
|
81
|
+
schemaVersion: 1,
|
|
82
|
+
id: 'github-copilot',
|
|
83
|
+
name: 'GitHub Copilot',
|
|
84
|
+
description: '通过本机安装的 GitHub Copilot ACP 模式执行步骤。',
|
|
85
|
+
backend: 'acp',
|
|
86
|
+
command: 'copilot',
|
|
87
|
+
args: ['--acp'],
|
|
88
|
+
},
|
|
89
|
+
commandAliases: ['copilot'],
|
|
90
|
+
bundled: false,
|
|
26
91
|
},
|
|
27
92
|
];
|
|
93
|
+
const adapterDescriptors = [...builtinAdapterDescriptors, ...externalAdapterDescriptors];
|
|
94
|
+
export const adapterDescriptorForId = (id) => adapterDescriptors.find((descriptor) => descriptor.manifest.id === id);
|
|
28
95
|
const manifestHash = (manifest) => createHash('sha256').update(JSON.stringify(manifest)).digest('hex').slice(0, 16);
|
|
29
96
|
const stringValue = (value, name, max) => {
|
|
30
97
|
if (typeof value !== 'string' || !value.trim() || value.length > max)
|
|
@@ -156,6 +223,7 @@ const record = (value, source, manifestPath, baseDirectory) => {
|
|
|
156
223
|
const manifest = normalizeManifest(value, baseDirectory);
|
|
157
224
|
return { manifest, source, manifestPath, manifestHash: manifestHash(manifest) };
|
|
158
225
|
};
|
|
226
|
+
export const adapterDescriptorRecord = (descriptor, source) => record(descriptor.manifest, source, `descriptor:${descriptor.manifest.id}`);
|
|
159
227
|
const manifestFiles = (directory) => {
|
|
160
228
|
if (!directory || !existsSync(directory))
|
|
161
229
|
return [];
|
|
@@ -201,8 +269,8 @@ export function loadAgentManifestRecords(options = {}) {
|
|
|
201
269
|
const records = [];
|
|
202
270
|
const warnings = [];
|
|
203
271
|
if (options.includeBuiltins !== false)
|
|
204
|
-
for (const
|
|
205
|
-
records.push(
|
|
272
|
+
for (const descriptor of builtinAdapterDescriptors)
|
|
273
|
+
records.push(adapterDescriptorRecord(descriptor, 'builtin'));
|
|
206
274
|
const addValues = (values, source, manifestPath, baseDirectory) => {
|
|
207
275
|
const manifests = Array.isArray(values) ? values : [values];
|
|
208
276
|
for (const value of manifests) {
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { accessSync, constants, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { delimiter, isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
* Process-level helpers shared by the ACP and CLI runtime adapters: locating an
|
|
6
|
-
* executable without a shell, discovering ACP servers on PATH, and reading a
|
|
7
|
-
* structured answer out of an agent's stdout.
|
|
8
|
-
*/
|
|
5
|
+
import crossSpawn from 'cross-spawn';
|
|
9
6
|
/** Package root, so bundled adapter binaries resolve in dev and after build. */
|
|
10
|
-
const runtimePackageRoot = (() => {
|
|
7
|
+
export const runtimePackageRoot = (() => {
|
|
11
8
|
const currentDirectory = fileURLToPath(new URL('.', import.meta.url));
|
|
12
9
|
return currentDirectory.includes(`${sep}dist${sep}`)
|
|
13
10
|
? resolve(currentDirectory, '..', '..')
|
|
@@ -25,72 +22,183 @@ export const existingAbsoluteDirectory = (value, errorCode) => {
|
|
|
25
22
|
}
|
|
26
23
|
return resolve(value);
|
|
27
24
|
};
|
|
28
|
-
export const existingExecutable = (value) => {
|
|
25
|
+
export const existingExecutable = (value, platform = process.platform) => {
|
|
29
26
|
try {
|
|
30
|
-
if (statSync(value).isFile())
|
|
27
|
+
if (!statSync(value).isFile())
|
|
28
|
+
return undefined;
|
|
29
|
+
if (platform !== 'win32')
|
|
31
30
|
accessSync(value, constants.X_OK);
|
|
32
|
-
|
|
33
|
-
}
|
|
31
|
+
return value;
|
|
34
32
|
}
|
|
35
33
|
catch {
|
|
36
34
|
return undefined;
|
|
37
35
|
}
|
|
38
|
-
return undefined;
|
|
39
36
|
};
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.filter((extension) => extension.startsWith('.'))),
|
|
47
|
-
];
|
|
37
|
+
const windowsExecutableExtensions = (env = process.env) => {
|
|
38
|
+
const configured = (env.PATHEXT ?? '.COM;.EXE;.CMD;.BAT')
|
|
39
|
+
.split(';')
|
|
40
|
+
.map((extension) => extension.trim().toLowerCase())
|
|
41
|
+
.filter((extension) => ['.com', '.exe', '.cmd', '.bat'].includes(extension));
|
|
42
|
+
return [...new Set([...configured, '.com', '.exe', '.cmd', '.bat'])];
|
|
48
43
|
};
|
|
49
|
-
export const
|
|
44
|
+
export const executableExtensions = (platform = process.platform, env = process.env) => (platform === 'win32' ? windowsExecutableExtensions(env) : []);
|
|
45
|
+
export const stripExecutableExtension = (command, platform = process.platform, env = process.env) => {
|
|
50
46
|
const lower = command.toLowerCase();
|
|
51
|
-
const extension = executableExtensions()
|
|
47
|
+
const extension = executableExtensions(platform, env)
|
|
52
48
|
.sort((a, b) => b.length - a.length)
|
|
53
49
|
.find((candidate) => lower.endsWith(candidate));
|
|
54
50
|
return extension ? command.slice(0, -extension.length) : command;
|
|
55
51
|
};
|
|
56
|
-
export const executableCandidates = (command) => {
|
|
52
|
+
export const executableCandidates = (command, platform = process.platform, env = process.env) => {
|
|
53
|
+
if (platform !== 'win32')
|
|
54
|
+
return [command];
|
|
57
55
|
const lower = command.toLowerCase();
|
|
58
|
-
if (
|
|
56
|
+
if (/\.ps1$/i.test(lower))
|
|
57
|
+
return [];
|
|
58
|
+
if (executableExtensions(platform, env).some((extension) => lower.endsWith(extension)))
|
|
59
59
|
return [command];
|
|
60
|
-
return
|
|
60
|
+
return executableExtensions(platform, env).map((extension) => `${command}${extension}`);
|
|
61
61
|
};
|
|
62
|
-
export const normalizedAcpCommand = (command) => {
|
|
63
|
-
const normalized = stripExecutableExtension(command);
|
|
62
|
+
export const normalizedAcpCommand = (command, platform = process.platform, env = process.env) => {
|
|
63
|
+
const normalized = stripExecutableExtension(command, platform, env);
|
|
64
64
|
return isAcpCommandName(normalized) ? normalized : undefined;
|
|
65
65
|
};
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
const uniqueDirectories = (values) => {
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
return values.filter(({ directory }) => {
|
|
69
|
+
const normalized = resolve(directory);
|
|
70
|
+
if (seen.has(normalized))
|
|
71
|
+
return false;
|
|
72
|
+
seen.add(normalized);
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
export const commandSearchDirectories = (options = {}) => {
|
|
77
|
+
const env = options.env ?? process.env;
|
|
78
|
+
const platform = options.platform ?? process.platform;
|
|
79
|
+
const packageRoot = resolve(options.bundledRoot ?? runtimePackageRoot);
|
|
80
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
81
|
+
const pathDelimiter = platform === 'win32' ? ';' : delimiter;
|
|
82
|
+
const values = [
|
|
83
|
+
{ directory: join(packageRoot, 'node_modules', '.bin'), source: 'bundled-bin' },
|
|
84
|
+
{ directory: join(projectRoot, 'node_modules', '.bin'), source: 'project-bin' },
|
|
85
|
+
...(env.PATH ?? '')
|
|
86
|
+
.split(pathDelimiter)
|
|
74
87
|
.filter(Boolean)
|
|
75
|
-
.map((
|
|
88
|
+
.map((directory) => ({ directory, source: 'path' })),
|
|
76
89
|
];
|
|
90
|
+
if (platform === 'win32' && env.APPDATA)
|
|
91
|
+
values.push({ directory: join(env.APPDATA, 'npm'), source: 'windows-appdata' });
|
|
92
|
+
if (platform !== 'win32') {
|
|
93
|
+
const userHome = env.HOME ?? homedir();
|
|
94
|
+
values.push({ directory: join(userHome, '.local', 'bin'), source: 'common-path' }, { directory: join(userHome, '.cargo', 'bin'), source: 'common-path' }, { directory: join(userHome, '.npm-global', 'bin'), source: 'common-path' }, { directory: join(userHome, 'Library', 'pnpm'), source: 'common-path' }, { directory: '/opt/homebrew/bin', source: 'common-path' }, { directory: '/usr/local/bin', source: 'common-path' }, { directory: '/usr/bin', source: 'common-path' });
|
|
95
|
+
}
|
|
96
|
+
return uniqueDirectories(values);
|
|
97
|
+
};
|
|
98
|
+
export const resolveCommand = (command, options = {}) => {
|
|
99
|
+
const env = options.env ?? process.env;
|
|
100
|
+
const platform = options.platform ?? process.platform;
|
|
101
|
+
const launchType = (candidate) => platform === 'win32' && /\.(?:cmd|bat)$/i.test(candidate) ? 'windows-command' : 'native';
|
|
102
|
+
const direct = isAbsolute(command) || command.includes('/') || command.includes('\\');
|
|
103
|
+
if (direct) {
|
|
104
|
+
const target = isAbsolute(command)
|
|
105
|
+
? command
|
|
106
|
+
: resolve(options.projectRoot ?? process.cwd(), command);
|
|
107
|
+
for (const candidate of executableCandidates(target, platform, env)) {
|
|
108
|
+
const executable = existingExecutable(candidate, platform);
|
|
109
|
+
if (executable)
|
|
110
|
+
return {
|
|
111
|
+
requested: command,
|
|
112
|
+
executable,
|
|
113
|
+
launchType: launchType(candidate),
|
|
114
|
+
source: 'explicit',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
for (const { directory, source } of commandSearchDirectories(options)) {
|
|
120
|
+
for (const candidate of executableCandidates(command, platform, env)) {
|
|
121
|
+
const executable = existingExecutable(join(directory, candidate), platform);
|
|
122
|
+
if (executable)
|
|
123
|
+
return { requested: command, executable, launchType: launchType(candidate), source };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
};
|
|
128
|
+
export const resolveCommandAliases = (commands, options = {}) => {
|
|
129
|
+
for (const command of commands) {
|
|
130
|
+
const resolved = resolveCommand(command, options);
|
|
131
|
+
if (resolved)
|
|
132
|
+
return resolved;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
};
|
|
136
|
+
export const describeResolvedCommand = (command) => `${command.launchType} ${command.executable} (${command.source})`;
|
|
137
|
+
export const launchProcess = (spec) => crossSpawn(spec.resolved.executable, spec.args, {
|
|
138
|
+
cwd: spec.cwd,
|
|
139
|
+
env: spec.env,
|
|
140
|
+
stdio: spec.stdio,
|
|
141
|
+
shell: false,
|
|
142
|
+
windowsHide: true,
|
|
143
|
+
detached: process.platform !== 'win32',
|
|
144
|
+
});
|
|
145
|
+
const terminatingProcesses = new WeakSet();
|
|
146
|
+
export const terminateProcess = (child) => {
|
|
147
|
+
if (!child.pid ||
|
|
148
|
+
child.exitCode !== null ||
|
|
149
|
+
child.signalCode !== null ||
|
|
150
|
+
terminatingProcesses.has(child))
|
|
151
|
+
return;
|
|
152
|
+
terminatingProcesses.add(child);
|
|
153
|
+
if (process.platform === 'win32') {
|
|
154
|
+
const killer = crossSpawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
|
|
155
|
+
stdio: 'ignore',
|
|
156
|
+
windowsHide: true,
|
|
157
|
+
});
|
|
158
|
+
const fallback = () => {
|
|
159
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
160
|
+
child.kill();
|
|
161
|
+
};
|
|
162
|
+
killer.once('error', fallback);
|
|
163
|
+
killer.once('exit', (code) => {
|
|
164
|
+
if (code !== 0)
|
|
165
|
+
fallback();
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
child.kill('SIGTERM');
|
|
174
|
+
}
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
if (child.exitCode !== null || child.signalCode !== null || !child.pid)
|
|
177
|
+
return;
|
|
178
|
+
try {
|
|
179
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
child.kill('SIGKILL');
|
|
183
|
+
}
|
|
184
|
+
}, 1_000).unref();
|
|
185
|
+
};
|
|
186
|
+
export const discoverAcpCommands = (options = {}) => {
|
|
187
|
+
const env = options.env ?? process.env;
|
|
188
|
+
const platform = options.platform ?? process.platform;
|
|
77
189
|
const commands = new Set();
|
|
78
|
-
const
|
|
79
|
-
for (const directory of directories) {
|
|
80
|
-
if (seen.has(directory))
|
|
81
|
-
continue;
|
|
82
|
-
seen.add(directory);
|
|
190
|
+
for (const { directory } of commandSearchDirectories(options)) {
|
|
83
191
|
try {
|
|
84
192
|
for (const entry of readdirSync(directory)) {
|
|
85
|
-
const command = normalizedAcpCommand(entry);
|
|
193
|
+
const command = normalizedAcpCommand(entry, platform, env);
|
|
86
194
|
if (!command)
|
|
87
195
|
continue;
|
|
88
|
-
if (
|
|
196
|
+
if (resolveCommand(join(directory, entry), options))
|
|
89
197
|
commands.add(command);
|
|
90
198
|
}
|
|
91
199
|
}
|
|
92
200
|
catch {
|
|
93
|
-
// Missing
|
|
201
|
+
// Missing search directories are normal on developer machines.
|
|
94
202
|
}
|
|
95
203
|
}
|
|
96
204
|
return [...commands];
|
|
@@ -105,50 +213,6 @@ export const runtimeNameFromCommand = (command) => command
|
|
|
105
213
|
.replace(/[-_.]+/g, ' ')
|
|
106
214
|
.replace(/\bacp\b/gi, 'ACP')
|
|
107
215
|
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
108
|
-
export const resolveExecutable = (command, env, options = {}) => {
|
|
109
|
-
if (isAbsolute(command)) {
|
|
110
|
-
for (const candidate of executableCandidates(command)) {
|
|
111
|
-
const executable = existingExecutable(candidate);
|
|
112
|
-
if (executable)
|
|
113
|
-
return executable;
|
|
114
|
-
}
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
if (command.includes(sep)) {
|
|
118
|
-
for (const candidate of executableCandidates(resolve(command))) {
|
|
119
|
-
const executable = existingExecutable(candidate);
|
|
120
|
-
if (executable)
|
|
121
|
-
return executable;
|
|
122
|
-
}
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
const localBins = [
|
|
126
|
-
resolve(runtimePackageRoot, 'node_modules', '.bin'),
|
|
127
|
-
resolve('node_modules', '.bin'),
|
|
128
|
-
];
|
|
129
|
-
if (options.includeProjectBin) {
|
|
130
|
-
for (const directory of localBins) {
|
|
131
|
-
for (const candidate of executableCandidates(command)) {
|
|
132
|
-
const localExecutable = existingExecutable(resolve(directory, candidate));
|
|
133
|
-
if (localExecutable)
|
|
134
|
-
return localExecutable;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
for (const dir of (env.PATH ?? process.env.PATH ?? '').split(delimiter)) {
|
|
139
|
-
if (!dir)
|
|
140
|
-
continue;
|
|
141
|
-
if (!options.includeProjectBin && localBins.includes(resolve(dir)))
|
|
142
|
-
continue;
|
|
143
|
-
for (const candidate of executableCandidates(command)) {
|
|
144
|
-
const executable = existingExecutable(resolve(dir, candidate));
|
|
145
|
-
if (executable)
|
|
146
|
-
return executable;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return undefined;
|
|
150
|
-
};
|
|
151
|
-
export const needsWindowsShell = (executable) => process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(executable);
|
|
152
216
|
export const canChangeWorkspace = (effects) => effects.some((effect) => effect.type === 'file-write' || effect.type === 'command');
|
|
153
217
|
export const parseJsonOutput = (text) => {
|
|
154
218
|
const trimmed = text.trim();
|