@hmj-ai/cflow 1.1.0 → 1.3.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-CV7qQRWx.css +1 -0
- package/dist/public/assets/index-DGu2YJI7.js +16 -0
- package/dist/public/cflow-lockup.svg +35 -0
- package/dist/public/cflow-mark.svg +23 -0
- package/dist/public/index.html +4 -4
- package/dist/src/compiler.js +30 -0
- 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 +114 -115
- package/dist/src/workspace-files.js +82 -0
- 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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 80" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">CFlow</title>
|
|
3
|
+
<desc id="desc">CFlow logo: a continuous workflow path forming the letter C, paired with the CFlow wordmark.</desc>
|
|
4
|
+
<rect width="300" height="80" rx="12" fill="#171A1F"/>
|
|
5
|
+
<g transform="translate(12 8)">
|
|
6
|
+
<rect width="64" height="64" rx="14" fill="#23272E"/>
|
|
7
|
+
<g transform="rotate(-90 32 32)">
|
|
8
|
+
<path
|
|
9
|
+
d="M47 17H31C21.61 17 14 24.61 14 34s7.61 17 17 17h15"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="#F8FAFC"
|
|
12
|
+
stroke-width="4.5"
|
|
13
|
+
stroke-linecap="round"
|
|
14
|
+
/>
|
|
15
|
+
<circle cx="47" cy="17" r="3.4" fill="#F8FAFC"/>
|
|
16
|
+
<path
|
|
17
|
+
d="m40.5 44.5 6.5 6.5-6.5 6.5"
|
|
18
|
+
fill="none"
|
|
19
|
+
stroke="#F8FAFC"
|
|
20
|
+
stroke-width="4.5"
|
|
21
|
+
stroke-linecap="round"
|
|
22
|
+
stroke-linejoin="round"
|
|
23
|
+
/>
|
|
24
|
+
</g>
|
|
25
|
+
</g>
|
|
26
|
+
<text
|
|
27
|
+
x="94"
|
|
28
|
+
y="51"
|
|
29
|
+
fill="#F8FAFC"
|
|
30
|
+
font-family="Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
|
|
31
|
+
font-size="34"
|
|
32
|
+
font-weight="600"
|
|
33
|
+
letter-spacing="0"
|
|
34
|
+
>CFlow</text>
|
|
35
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">CFlow</title>
|
|
3
|
+
<desc id="desc">A continuous white workflow path forming the letter C on a charcoal background.</desc>
|
|
4
|
+
<rect width="64" height="64" rx="14" fill="#171A1F"/>
|
|
5
|
+
<g transform="rotate(-90 32 32)">
|
|
6
|
+
<path
|
|
7
|
+
d="M47 17H31C21.61 17 14 24.61 14 34s7.61 17 17 17h15"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="#F8FAFC"
|
|
10
|
+
stroke-width="4.5"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
/>
|
|
13
|
+
<circle cx="47" cy="17" r="3.4" fill="#F8FAFC"/>
|
|
14
|
+
<path
|
|
15
|
+
d="m40.5 44.5 6.5 6.5-6.5 6.5"
|
|
16
|
+
fill="none"
|
|
17
|
+
stroke="#F8FAFC"
|
|
18
|
+
stroke-width="4.5"
|
|
19
|
+
stroke-linecap="round"
|
|
20
|
+
stroke-linejoin="round"
|
|
21
|
+
/>
|
|
22
|
+
</g>
|
|
23
|
+
</svg>
|
package/dist/public/index.html
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="color-scheme" content="light" />
|
|
7
|
-
<link rel="icon" href="
|
|
8
|
-
<title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/cflow-mark.svg" />
|
|
8
|
+
<title>CFlow</title>
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DGu2YJI7.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CV7qQRWx.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/dist/src/compiler.js
CHANGED
|
@@ -4,6 +4,20 @@ function assert(condition, message) {
|
|
|
4
4
|
if (!condition)
|
|
5
5
|
throw new Error(message);
|
|
6
6
|
}
|
|
7
|
+
export function assertFileReferences(fileReferences) {
|
|
8
|
+
if (!fileReferences)
|
|
9
|
+
return;
|
|
10
|
+
assert(fileReferences.length <= 100, 'CF_FILE_REFERENCES_LIMIT');
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
for (const path of fileReferences) {
|
|
13
|
+
assert(typeof path === 'string' && path.length > 0 && path.length <= 1024, 'CF_FILE_PATH_INVALID');
|
|
14
|
+
assert(!path.startsWith('/') && !path.startsWith('\\') && !/^[A-Za-z]:/.test(path), 'CF_FILE_PATH_INVALID');
|
|
15
|
+
const parts = path.split('/');
|
|
16
|
+
assert(parts.every((part) => part.length > 0 && part !== '.' && part !== '..'), 'CF_FILE_PATH_INVALID');
|
|
17
|
+
assert(!path.includes('\\') && !seen.has(path), 'CF_FILE_PATH_INVALID');
|
|
18
|
+
seen.add(path);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
7
21
|
function checkGraph(nodes, edges, entries) {
|
|
8
22
|
const adj = new Map();
|
|
9
23
|
for (const i of nodes)
|
|
@@ -27,6 +41,7 @@ function checkGraph(nodes, edges, entries) {
|
|
|
27
41
|
assert(seen.size === nodes.length, 'UNREACHABLE_NODE: all nodes must be reachable');
|
|
28
42
|
}
|
|
29
43
|
export function compileCF(draft) {
|
|
44
|
+
assertFileReferences(draft.fileReferences);
|
|
30
45
|
if (draft.effects) {
|
|
31
46
|
const validTypes = new Set(['file-read', 'file-write', 'command']);
|
|
32
47
|
for (const effect of draft.effects) {
|
|
@@ -40,8 +55,23 @@ export function compileCF(draft) {
|
|
|
40
55
|
assertContractSchema(inputContract, 'CF_INPUT');
|
|
41
56
|
assertContractSchema(outputContract, 'CF_OUTPUT');
|
|
42
57
|
assert(draft.does.trim().length > 0, 'CF_DOES_REQUIRED');
|
|
58
|
+
const hasFileAccess = (draft.effects ?? []).some((effect) => effect.type === 'file-read' || effect.type === 'file-write');
|
|
59
|
+
const fileReferenceGuidance = hasFileAccess && draft.fileReferences?.length
|
|
60
|
+
? [
|
|
61
|
+
'Indexed workspace files (priority order):',
|
|
62
|
+
...draft.fileReferences.map((path, index) => `${index + 1}. ${path}`),
|
|
63
|
+
'',
|
|
64
|
+
'File resolution rules:',
|
|
65
|
+
'- Resolve an explicit @{relative/path} against the indexed paths first.',
|
|
66
|
+
'- Resolve a mentioned relative path, filename, or basename against the indexed paths before searching elsewhere.',
|
|
67
|
+
'- When more than one indexed file matches, use the first matching indexed path in the order above.',
|
|
68
|
+
'- If no indexed path matches, search the workspace for the mentioned file.',
|
|
69
|
+
'- Use only files required by the task. These paths are location hints; no file contents are embedded here.',
|
|
70
|
+
].join('\n')
|
|
71
|
+
: undefined;
|
|
43
72
|
const task = [
|
|
44
73
|
draft.does.trim(),
|
|
74
|
+
fileReferenceGuidance,
|
|
45
75
|
draft.input?.trim() ? `Input guidance: ${draft.input.trim()}` : undefined,
|
|
46
76
|
draft.output?.trim() ? `Output guidance: ${draft.output.trim()}` : undefined,
|
|
47
77
|
draft.process?.trim() ? `Process constraints: ${draft.process.trim()}` : undefined,
|
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();
|