@phnx-labs/agents-cli 1.20.35 → 1.20.36
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/CHANGELOG.md +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/lib/mcp.js
CHANGED
|
@@ -34,6 +34,18 @@ export function parseMcpServerConfig(filePath) {
|
|
|
34
34
|
}
|
|
35
35
|
return validateMcpYamlConfig(parsed);
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Validate an MCP server name. Rejects names that could be misinterpreted as
|
|
39
|
+
* command-line options or that contain characters unsafe for argv/identifier use.
|
|
40
|
+
*/
|
|
41
|
+
export function validateMcpServerName(name) {
|
|
42
|
+
if (name.startsWith('-')) {
|
|
43
|
+
throw new Error(`Invalid MCP server name '${name}': names cannot start with '-'`);
|
|
44
|
+
}
|
|
45
|
+
if (/[\s\0-\x1f\x7f]/.test(name)) {
|
|
46
|
+
throw new Error(`Invalid MCP server name '${name}': names cannot contain whitespace or control characters`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
function validateMcpYamlConfig(parsed) {
|
|
38
50
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
39
51
|
return null;
|
|
@@ -41,6 +53,7 @@ function validateMcpYamlConfig(parsed) {
|
|
|
41
53
|
const config = parsed;
|
|
42
54
|
if (typeof config.name !== 'string' || config.name.length === 0)
|
|
43
55
|
return null;
|
|
56
|
+
validateMcpServerName(config.name);
|
|
44
57
|
if (config.transport !== 'stdio' && config.transport !== 'http')
|
|
45
58
|
return null;
|
|
46
59
|
const result = {
|
|
@@ -205,12 +218,12 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
|
|
|
205
218
|
envArgs.push('--env', `${key}=${value}`);
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
|
-
// claude mcp add --scope user --transport stdio
|
|
221
|
+
// claude mcp add --scope user --transport stdio [--env K=V]... -- <name> <cmd> [args...]
|
|
209
222
|
const args = [
|
|
210
223
|
'mcp', 'add', '--scope', 'user', '--transport', 'stdio',
|
|
211
|
-
server.name,
|
|
212
224
|
...envArgs,
|
|
213
225
|
'--',
|
|
226
|
+
server.name,
|
|
214
227
|
server.config.command,
|
|
215
228
|
...(server.config.args || [])
|
|
216
229
|
];
|
|
@@ -222,8 +235,8 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
|
|
|
222
235
|
});
|
|
223
236
|
}
|
|
224
237
|
else {
|
|
225
|
-
// claude mcp add --scope user --transport http <name> <url>
|
|
226
|
-
execFileSync(binaryPath, ['mcp', 'add', '--scope', 'user', '--transport', 'http', server.name, server.config.url], {
|
|
238
|
+
// claude mcp add --scope user --transport http -- <name> <url>
|
|
239
|
+
execFileSync(binaryPath, ['mcp', 'add', '--scope', 'user', '--transport', 'http', '--', server.name, server.config.url], {
|
|
227
240
|
stdio: 'pipe',
|
|
228
241
|
timeout: 30000,
|
|
229
242
|
env: execEnv,
|
|
@@ -237,10 +250,11 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
|
|
|
237
250
|
*/
|
|
238
251
|
function installMcpViaCodex(binaryPath, server, versionHome) {
|
|
239
252
|
if (server.config.transport === 'stdio') {
|
|
240
|
-
// codex mcp add <name>
|
|
253
|
+
// codex mcp add -- <name> <cmd> [args...]
|
|
241
254
|
const args = [
|
|
242
|
-
'mcp', 'add',
|
|
255
|
+
'mcp', 'add',
|
|
243
256
|
'--',
|
|
257
|
+
server.name,
|
|
244
258
|
server.config.command,
|
|
245
259
|
...(server.config.args || [])
|
|
246
260
|
];
|
|
@@ -272,11 +286,12 @@ export async function registerMcpCommandToTargets(targets, name, commandSpec, sc
|
|
|
272
286
|
}
|
|
273
287
|
function registerMcpCommand(agentId, name, commandSpec, scope, transport, options = {}) {
|
|
274
288
|
try {
|
|
289
|
+
validateMcpServerName(name);
|
|
275
290
|
const bin = options.binary || AGENTS[agentId].cliCommand;
|
|
276
291
|
const commandArgs = [commandSpec.command, ...commandSpec.args];
|
|
277
292
|
const args = agentId === 'claude'
|
|
278
|
-
? ['mcp', 'add', '--transport', transport, '--scope', scope,
|
|
279
|
-
: ['mcp', 'add',
|
|
293
|
+
? ['mcp', 'add', '--transport', transport, '--scope', scope, '--', name, ...commandArgs]
|
|
294
|
+
: ['mcp', 'add', '--', name, ...commandArgs];
|
|
280
295
|
const env = options.home ? { ...process.env, HOME: options.home } : process.env;
|
|
281
296
|
execFileSync(bin, args, { stdio: 'pipe', timeout: 30000, env, shell: needsWindowsShell(bin) });
|
|
282
297
|
return { success: true };
|
|
@@ -508,6 +523,7 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
|
|
|
508
523
|
* Write an MCP server config to ~/.agents/mcp/.
|
|
509
524
|
*/
|
|
510
525
|
export function writeMcpServerConfig(config) {
|
|
526
|
+
validateMcpServerName(config.name);
|
|
511
527
|
const mcpDir = getUserMcpDir();
|
|
512
528
|
fs.mkdirSync(mcpDir, { recursive: true });
|
|
513
529
|
const fileName = `${config.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}.yaml`;
|
package/dist/lib/migrate.d.ts
CHANGED
|
@@ -25,6 +25,25 @@
|
|
|
25
25
|
* LEGACY_SYSTEM_DIR" without duplicating data.
|
|
26
26
|
*/
|
|
27
27
|
export declare function foldLegacySystemRepo(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Migrate a legacy single-repo agents.yaml into ~/.agents/agents.yaml.
|
|
30
|
+
*
|
|
31
|
+
* The post-fold system dir (~/.agents/.system) is a pull-only mirror of the
|
|
32
|
+
* npm-shipped defaults. Its agents.yaml is a TRACKED file that readMeta() reads
|
|
33
|
+
* in place as the defaults base (see SYSTEM_META_FILE in state.ts). Deleting or
|
|
34
|
+
* moving a tracked file out of the mirror dirties its working tree, which
|
|
35
|
+
* permanently wedges `agents setup`, `agents sync`, and background auto-pull
|
|
36
|
+
* ("Working tree has uncommitted changes.") because the mirror sync refuses a
|
|
37
|
+
* dirty tree. So when agents.yaml is tracked there, treat the mirror as strictly
|
|
38
|
+
* read-only and leave the file alone.
|
|
39
|
+
*
|
|
40
|
+
* Only an UNTRACKED agents.yaml is residue from the pre-split single-repo layout
|
|
41
|
+
* (or a legacy ~/.agents-system fold) and is safe to move/drop.
|
|
42
|
+
*
|
|
43
|
+
* Params default to the real on-disk locations; they are injectable so tests can
|
|
44
|
+
* drive a fixture tree without touching the user's ~/.agents.
|
|
45
|
+
*/
|
|
46
|
+
export declare function migrateAgentsYaml(systemDir?: string, userDir?: string): void;
|
|
28
47
|
/**
|
|
29
48
|
* Repair self-referential agent binary symlinks.
|
|
30
49
|
*
|
package/dist/lib/migrate.js
CHANGED
|
@@ -8,6 +8,9 @@ import * as fs from 'fs';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import * as yaml from 'yaml';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
13
|
+
import { machineId } from './machine-id.js';
|
|
11
14
|
import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
|
|
12
15
|
import { createLink } from './platform/index.js';
|
|
13
16
|
const HOME = process.env.HOME ?? os.homedir();
|
|
@@ -79,21 +82,46 @@ export function foldLegacySystemRepo() {
|
|
|
79
82
|
}
|
|
80
83
|
catch { /* best-effort */ }
|
|
81
84
|
}
|
|
85
|
+
/** True when `relPath` is a file tracked by a git repo rooted at `repoDir`. */
|
|
86
|
+
function isTrackedInGitRepo(repoDir, relPath) {
|
|
87
|
+
try {
|
|
88
|
+
execSync(`git ls-files --error-unmatch -- ${relPath}`, { cwd: repoDir, stdio: 'ignore' });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Not a git repo, or the file is untracked — either way, not tracked.
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
82
96
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
97
|
+
* Migrate a legacy single-repo agents.yaml into ~/.agents/agents.yaml.
|
|
98
|
+
*
|
|
99
|
+
* The post-fold system dir (~/.agents/.system) is a pull-only mirror of the
|
|
100
|
+
* npm-shipped defaults. Its agents.yaml is a TRACKED file that readMeta() reads
|
|
101
|
+
* in place as the defaults base (see SYSTEM_META_FILE in state.ts). Deleting or
|
|
102
|
+
* moving a tracked file out of the mirror dirties its working tree, which
|
|
103
|
+
* permanently wedges `agents setup`, `agents sync`, and background auto-pull
|
|
104
|
+
* ("Working tree has uncommitted changes.") because the mirror sync refuses a
|
|
105
|
+
* dirty tree. So when agents.yaml is tracked there, treat the mirror as strictly
|
|
106
|
+
* read-only and leave the file alone.
|
|
107
|
+
*
|
|
108
|
+
* Only an UNTRACKED agents.yaml is residue from the pre-split single-repo layout
|
|
109
|
+
* (or a legacy ~/.agents-system fold) and is safe to move/drop.
|
|
110
|
+
*
|
|
111
|
+
* Params default to the real on-disk locations; they are injectable so tests can
|
|
112
|
+
* drive a fixture tree without touching the user's ~/.agents.
|
|
87
113
|
*/
|
|
88
|
-
function migrateAgentsYaml() {
|
|
89
|
-
const src = path.join(
|
|
90
|
-
const dest = path.join(
|
|
114
|
+
export function migrateAgentsYaml(systemDir = SYSTEM_DIR, userDir = USER_DIR) {
|
|
115
|
+
const src = path.join(systemDir, 'agents.yaml');
|
|
116
|
+
const dest = path.join(userDir, 'agents.yaml');
|
|
91
117
|
if (!fs.existsSync(src))
|
|
92
118
|
return;
|
|
119
|
+
// Tracked in the system mirror → npm-shipped defaults; never mutate. readMeta()
|
|
120
|
+
// already surfaces it in place, so no user-dir copy is needed either.
|
|
121
|
+
if (isTrackedInGitRepo(systemDir, 'agents.yaml'))
|
|
122
|
+
return;
|
|
93
123
|
if (fs.existsSync(dest)) {
|
|
94
|
-
// User copy is authoritative — drop the stale system leftover.
|
|
95
|
-
// repo (npm-shipped) does not track agents.yaml; any copy here is residue
|
|
96
|
-
// from the pre-split layout.
|
|
124
|
+
// User copy is authoritative — drop the untracked stale system leftover.
|
|
97
125
|
try {
|
|
98
126
|
fs.unlinkSync(src);
|
|
99
127
|
}
|
|
@@ -101,7 +129,7 @@ function migrateAgentsYaml() {
|
|
|
101
129
|
return;
|
|
102
130
|
}
|
|
103
131
|
try {
|
|
104
|
-
fs.mkdirSync(
|
|
132
|
+
fs.mkdirSync(userDir, { recursive: true, mode: 0o700 });
|
|
105
133
|
fs.renameSync(src, dest);
|
|
106
134
|
console.error('Migrated agents.yaml to ~/.agents/');
|
|
107
135
|
}
|
|
@@ -656,22 +684,31 @@ function migratePermissionSetsToPresets() {
|
|
|
656
684
|
* switching is owned by `agents use`, not the migrator.
|
|
657
685
|
*/
|
|
658
686
|
function repairAgentConfigSymlinks() {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
catch {
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
const agentsBlock = yaml.match(/^agents:\s*\n((?: [^\n]*\n)+)/m);
|
|
667
|
-
if (!agentsBlock)
|
|
668
|
-
return;
|
|
687
|
+
// Version pins live in the per-device file post-split (~/.agents/devices/<machine>/
|
|
688
|
+
// agents.yaml); central agents.yaml may still carry them mid-transition. Read
|
|
689
|
+
// device first (authoritative), then central, first-seen-agent wins.
|
|
669
690
|
const defaults = [];
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
691
|
+
const collectPins = (file) => {
|
|
692
|
+
let text;
|
|
693
|
+
try {
|
|
694
|
+
text = fs.readFileSync(file, 'utf-8');
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const block = text.match(/^agents:\s*\n((?: [^\n]*\n)+)/m);
|
|
700
|
+
if (!block)
|
|
701
|
+
return;
|
|
702
|
+
for (const line of block[1].split('\n')) {
|
|
703
|
+
const m = line.match(/^\s+([a-z][a-z0-9_-]*):\s*([^\s#]+)/);
|
|
704
|
+
if (m && !defaults.some((d) => d.agent === m[1]))
|
|
705
|
+
defaults.push({ agent: m[1], version: m[2] });
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
collectPins(path.join(USER_DIR, 'devices', machineId(), 'agents.yaml'));
|
|
709
|
+
collectPins(path.join(USER_DIR, 'agents.yaml'));
|
|
710
|
+
if (defaults.length === 0)
|
|
711
|
+
return;
|
|
675
712
|
let repaired = 0;
|
|
676
713
|
for (const { agent, version } of defaults) {
|
|
677
714
|
const configDirName = agent in AGENTS ? agentConfigDirName(agent) : `.${agent}`;
|
|
@@ -1625,6 +1662,73 @@ function migrateVersionResourcesToPatterns() {
|
|
|
1625
1662
|
console.error('Migrated agents.yaml versions: entries to pattern format');
|
|
1626
1663
|
}
|
|
1627
1664
|
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Split the machine-local fields out of the committed central agents.yaml so it
|
|
1667
|
+
* becomes portable and syncs cleanly (no more skip-worktree band-aid):
|
|
1668
|
+
* agents: -> ~/.agents/devices/<machineId>/agents.yaml (committed, per-device)
|
|
1669
|
+
* versions: -> ~/.agents/.history/version-resources.json (gitignored, machine-local)
|
|
1670
|
+
* Then clear any externally-set skip-worktree bit. Idempotent: no-op once central
|
|
1671
|
+
* carries neither field. Operates on raw YAML (never through state.ts).
|
|
1672
|
+
*/
|
|
1673
|
+
function migrateSplitDeviceLocalMeta() {
|
|
1674
|
+
const metaFile = path.join(USER_DIR, 'agents.yaml');
|
|
1675
|
+
if (!fs.existsSync(metaFile))
|
|
1676
|
+
return;
|
|
1677
|
+
let meta;
|
|
1678
|
+
try {
|
|
1679
|
+
meta = yaml.parse(fs.readFileSync(metaFile, 'utf-8')) || {};
|
|
1680
|
+
}
|
|
1681
|
+
catch {
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
const agents = meta.agents;
|
|
1685
|
+
const versions = meta.versions;
|
|
1686
|
+
const hasLocal = (!!agents && Object.keys(agents).length > 0) || (!!versions && Object.keys(versions).length > 0);
|
|
1687
|
+
const HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n\n';
|
|
1688
|
+
// Only rewrite central when it actually carries machine-local fields — a
|
|
1689
|
+
// machine whose agents.yaml is already portable-only is left untouched.
|
|
1690
|
+
if (hasLocal) {
|
|
1691
|
+
// agents: -> per-device file (merge, existing device entries win).
|
|
1692
|
+
if (agents && Object.keys(agents).length > 0) {
|
|
1693
|
+
const devicePath = path.join(USER_DIR, 'devices', machineId(), 'agents.yaml');
|
|
1694
|
+
let existing = {};
|
|
1695
|
+
try {
|
|
1696
|
+
const dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
|
|
1697
|
+
if (dm?.agents)
|
|
1698
|
+
existing = dm.agents;
|
|
1699
|
+
}
|
|
1700
|
+
catch { /* absent */ }
|
|
1701
|
+
fs.mkdirSync(path.dirname(devicePath), { recursive: true });
|
|
1702
|
+
atomicWriteFileSync(devicePath, HEADER + yaml.stringify({ agents: { ...agents, ...existing } }));
|
|
1703
|
+
}
|
|
1704
|
+
// versions: -> machine-local history JSON (merge, existing wins).
|
|
1705
|
+
if (versions && Object.keys(versions).length > 0) {
|
|
1706
|
+
const vrPath = path.join(USER_DIR, '.history', 'version-resources.json');
|
|
1707
|
+
let existing = {};
|
|
1708
|
+
try {
|
|
1709
|
+
existing = JSON.parse(fs.readFileSync(vrPath, 'utf-8')) || {};
|
|
1710
|
+
}
|
|
1711
|
+
catch { /* absent */ }
|
|
1712
|
+
fs.mkdirSync(path.dirname(vrPath), { recursive: true });
|
|
1713
|
+
atomicWriteFileSync(vrPath, JSON.stringify({ ...versions, ...existing }, null, 2) + '\n');
|
|
1714
|
+
}
|
|
1715
|
+
// Strip machine-local fields from central and rewrite (portable only) — after
|
|
1716
|
+
// the device/history writes above, so a crash never loses data.
|
|
1717
|
+
delete meta.agents;
|
|
1718
|
+
delete meta.versions;
|
|
1719
|
+
atomicWriteFileSync(metaFile, HEADER + yaml.stringify(meta));
|
|
1720
|
+
}
|
|
1721
|
+
// Always clear any skip-worktree bit (idempotent, best-effort) so agents.yaml
|
|
1722
|
+
// syncs cleanly on every machine — even one that had nothing to split. Runs
|
|
1723
|
+
// after any rewrite so the tracked content already matches the split shape.
|
|
1724
|
+
try {
|
|
1725
|
+
execSync('git update-index --no-skip-worktree agents.yaml', { cwd: USER_DIR, stdio: 'ignore' });
|
|
1726
|
+
}
|
|
1727
|
+
catch { /* not a git repo / bit not set */ }
|
|
1728
|
+
if (hasLocal) {
|
|
1729
|
+
console.error('Split agents.yaml: agents: -> devices/, versions: -> .history/version-resources.json');
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1628
1732
|
/**
|
|
1629
1733
|
* Rename the legacy `extras-extras/` plugin-marketplace dir to `agents-extras/`
|
|
1630
1734
|
* inside every installed agent version-home, and rewrite cross-references in
|
|
@@ -1805,6 +1909,10 @@ export async function runMigration() {
|
|
|
1805
1909
|
foldUserHooksYamlIntoAgentsYaml();
|
|
1806
1910
|
foldBrowserProfilesIntoAgentsYaml();
|
|
1807
1911
|
migrateVersionResourcesToPatterns();
|
|
1912
|
+
// Split machine-local fields (agents:/versions:) out of the committed central
|
|
1913
|
+
// agents.yaml. After migrateVersionResourcesToPatterns so versions: is already
|
|
1914
|
+
// in pattern form when it moves to the history file.
|
|
1915
|
+
migrateSplitDeviceLocalMeta();
|
|
1808
1916
|
// Bucket moves: collapse runtime state into ~/.agents/.history and ~/.agents/.cache.
|
|
1809
1917
|
migrateRuntimeToHistory();
|
|
1810
1918
|
migrateRuntimeToCache();
|
package/dist/lib/overdue.js
CHANGED
|
@@ -44,6 +44,9 @@ export function detectOverdueJobs(now = new Date()) {
|
|
|
44
44
|
for (const job of listJobs()) {
|
|
45
45
|
if (!job.enabled || job.runOnce)
|
|
46
46
|
continue;
|
|
47
|
+
// Trigger-only jobs (no cron schedule) never have an expected fire time.
|
|
48
|
+
if (!job.schedule)
|
|
49
|
+
continue;
|
|
47
50
|
let expected = null;
|
|
48
51
|
try {
|
|
49
52
|
const cronOptions = { paused: true };
|
package/dist/lib/picker.d.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
/** Configuration for the interactive picker prompt. */
|
|
9
9
|
export interface PickerConfig<T> {
|
|
10
10
|
message: string;
|
|
11
|
+
/** Optional dim hint line rendered directly under the header (above the rows). */
|
|
12
|
+
subtitle?: string;
|
|
11
13
|
items: T[];
|
|
12
14
|
filter: (query: string) => T[];
|
|
13
15
|
labelFor: (item: T, query: string) => string;
|
package/dist/lib/picker.js
CHANGED
|
@@ -174,7 +174,10 @@ export function itemPicker(config) {
|
|
|
174
174
|
const help = previewOpen
|
|
175
175
|
? chalk.gray(`↑↓ navigate · space: close preview · ⏎ ${enter} · esc: cancel`)
|
|
176
176
|
: chalk.gray(`↑↓ navigate${hasPreview ? ' · space: preview' : ''} · ⏎ ${enter} · esc: cancel`);
|
|
177
|
-
const parts = [header
|
|
177
|
+
const parts = [header];
|
|
178
|
+
if (cfg.subtitle)
|
|
179
|
+
parts.push(cfg.subtitle);
|
|
180
|
+
parts.push(page);
|
|
178
181
|
if (results.length === 0) {
|
|
179
182
|
parts.push(chalk.gray(` ${cfg.emptyMessage ?? 'No matches.'}`));
|
|
180
183
|
}
|
|
@@ -17,3 +17,49 @@ export declare function needsWindowsShell(binary: string, platform?: NodeJS.Plat
|
|
|
17
17
|
* shell would actually run, matching `which` semantics on POSIX.
|
|
18
18
|
*/
|
|
19
19
|
export declare function findExecutable(name: string, platform?: NodeJS.Platform): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Absolute path of the POSIX shell for `sh -c <command string>` composition.
|
|
22
|
+
* `/bin/sh` on POSIX; on Windows there is no /bin/sh, so resolve Git's
|
|
23
|
+
* `sh.exe`/`bash.exe` from PATH (present wherever Git for Windows is — dev
|
|
24
|
+
* boxes and the GitHub windows runners alike). Falls back to the bare name so
|
|
25
|
+
* a missing shell surfaces as a clear spawn ENOENT on `sh` rather than a
|
|
26
|
+
* misleading one on `/bin/sh`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function posixShellPath(platform?: NodeJS.Platform): string;
|
|
29
|
+
/**
|
|
30
|
+
* Quote one argument for a Windows `cmd.exe` command line, as built by Node's
|
|
31
|
+
* `spawn(..., { shell: true })` on win32 (the `.cmd` agent shims, `agents secrets
|
|
32
|
+
* exec`, ...). cmd.exe does NO quoting of its own, so an unquoted arg with a space
|
|
33
|
+
* is split into several args, and a cmd metacharacter (`&|<>()^`) would be
|
|
34
|
+
* interpreted by the shell. We wrap any arg with whitespace, a quote, or a
|
|
35
|
+
* metacharacter in double quotes and escape embedded quotes / trailing
|
|
36
|
+
* backslashes per the CommandLineToArgvW rules, so the *child's* argv parse
|
|
37
|
+
* reconstructs the original argument.
|
|
38
|
+
*
|
|
39
|
+
* CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
|
|
40
|
+
* BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
|
|
41
|
+
* "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
|
|
42
|
+
* the callers here run a command whose `%`/`!`-bearing tokens are the caller's
|
|
43
|
+
* own (an agent prompt against the caller's shell, a bundle the caller owns), so
|
|
44
|
+
* caller-controlled `%`/`!` is not a privilege boundary. If that ever changes
|
|
45
|
+
* (composing an untrusted command line), route through a shell that disables
|
|
46
|
+
* expansion rather than relying on this quoter. An empty arg becomes `""`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function quoteWin32ExecArg(arg: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Compose a DEP0190-safe Windows shell command line from a command and its args.
|
|
51
|
+
*
|
|
52
|
+
* Node's `spawn(cmd, args, { shell: true })` on win32 concatenates `cmd` and
|
|
53
|
+
* every element of `args` into a single cmd.exe line WITHOUT escaping — that is
|
|
54
|
+
* both Node's DEP0190 deprecation (a future hard error) and a real injection
|
|
55
|
+
* surface, since user-controlled text (an agent prompt, a secrets-exec command)
|
|
56
|
+
* flows through `args`. Passing the fully-composed line as the SOLE `command`
|
|
57
|
+
* with an EMPTY args array sidesteps both: we quote every token with
|
|
58
|
+
* `quoteWin32ExecArg` so the child's CommandLineToArgvW parse reconstructs the
|
|
59
|
+
* exact original argv, and Node has no args array left to concatenate.
|
|
60
|
+
*
|
|
61
|
+
* Callers spawn the result as `spawn(line, [], { shell: true })`. A simple arg
|
|
62
|
+
* (no whitespace/quote/metachar) is passed through untouched, so the composed
|
|
63
|
+
* line is byte-identical to the old unquoted join for the common case.
|
|
64
|
+
*/
|
|
65
|
+
export declare function composeWin32CommandLine(command: string, args: string[]): string;
|
|
@@ -39,3 +39,77 @@ export function findExecutable(name, platform = process.platform) {
|
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Absolute path of the POSIX shell for `sh -c <command string>` composition.
|
|
44
|
+
* `/bin/sh` on POSIX; on Windows there is no /bin/sh, so resolve Git's
|
|
45
|
+
* `sh.exe`/`bash.exe` from PATH (present wherever Git for Windows is — dev
|
|
46
|
+
* boxes and the GitHub windows runners alike). Falls back to the bare name so
|
|
47
|
+
* a missing shell surfaces as a clear spawn ENOENT on `sh` rather than a
|
|
48
|
+
* misleading one on `/bin/sh`.
|
|
49
|
+
*/
|
|
50
|
+
export function posixShellPath(platform = process.platform) {
|
|
51
|
+
if (platform !== 'win32')
|
|
52
|
+
return '/bin/sh';
|
|
53
|
+
return findExecutable('sh', platform) ?? findExecutable('bash', platform) ?? 'sh';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Quote one argument for a Windows `cmd.exe` command line, as built by Node's
|
|
57
|
+
* `spawn(..., { shell: true })` on win32 (the `.cmd` agent shims, `agents secrets
|
|
58
|
+
* exec`, ...). cmd.exe does NO quoting of its own, so an unquoted arg with a space
|
|
59
|
+
* is split into several args, and a cmd metacharacter (`&|<>()^`) would be
|
|
60
|
+
* interpreted by the shell. We wrap any arg with whitespace, a quote, or a
|
|
61
|
+
* metacharacter in double quotes and escape embedded quotes / trailing
|
|
62
|
+
* backslashes per the CommandLineToArgvW rules, so the *child's* argv parse
|
|
63
|
+
* reconstructs the original argument.
|
|
64
|
+
*
|
|
65
|
+
* CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
|
|
66
|
+
* BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
|
|
67
|
+
* "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
|
|
68
|
+
* the callers here run a command whose `%`/`!`-bearing tokens are the caller's
|
|
69
|
+
* own (an agent prompt against the caller's shell, a bundle the caller owns), so
|
|
70
|
+
* caller-controlled `%`/`!` is not a privilege boundary. If that ever changes
|
|
71
|
+
* (composing an untrusted command line), route through a shell that disables
|
|
72
|
+
* expansion rather than relying on this quoter. An empty arg becomes `""`.
|
|
73
|
+
*/
|
|
74
|
+
export function quoteWin32ExecArg(arg) {
|
|
75
|
+
if (arg.length > 0 && !/[\s"&|<>()^]/.test(arg))
|
|
76
|
+
return arg;
|
|
77
|
+
let result = '"';
|
|
78
|
+
let backslashes = 0;
|
|
79
|
+
for (const ch of arg) {
|
|
80
|
+
if (ch === '\\') {
|
|
81
|
+
backslashes += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === '"') {
|
|
85
|
+
// Double the run of backslashes, then escape this quote.
|
|
86
|
+
result += '\\'.repeat(backslashes * 2 + 1) + '"';
|
|
87
|
+
backslashes = 0;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
result += '\\'.repeat(backslashes) + ch;
|
|
91
|
+
backslashes = 0;
|
|
92
|
+
}
|
|
93
|
+
// Trailing backslashes precede the closing quote → must be doubled.
|
|
94
|
+
result += '\\'.repeat(backslashes * 2) + '"';
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Compose a DEP0190-safe Windows shell command line from a command and its args.
|
|
99
|
+
*
|
|
100
|
+
* Node's `spawn(cmd, args, { shell: true })` on win32 concatenates `cmd` and
|
|
101
|
+
* every element of `args` into a single cmd.exe line WITHOUT escaping — that is
|
|
102
|
+
* both Node's DEP0190 deprecation (a future hard error) and a real injection
|
|
103
|
+
* surface, since user-controlled text (an agent prompt, a secrets-exec command)
|
|
104
|
+
* flows through `args`. Passing the fully-composed line as the SOLE `command`
|
|
105
|
+
* with an EMPTY args array sidesteps both: we quote every token with
|
|
106
|
+
* `quoteWin32ExecArg` so the child's CommandLineToArgvW parse reconstructs the
|
|
107
|
+
* exact original argv, and Node has no args array left to concatenate.
|
|
108
|
+
*
|
|
109
|
+
* Callers spawn the result as `spawn(line, [], { shell: true })`. A simple arg
|
|
110
|
+
* (no whitespace/quote/metachar) is passed through untouched, so the composed
|
|
111
|
+
* line is byte-identical to the old unquoted join for the common case.
|
|
112
|
+
*/
|
|
113
|
+
export function composeWin32CommandLine(command, args) {
|
|
114
|
+
return [command, ...args].map(quoteWin32ExecArg).join(' ');
|
|
115
|
+
}
|
|
@@ -9,6 +9,37 @@
|
|
|
9
9
|
* process counts as success.
|
|
10
10
|
*/
|
|
11
11
|
export declare function killTree(pid: number): void;
|
|
12
|
+
/**
|
|
13
|
+
* Spawn options for a long-lived background child (daemon, detached worker,
|
|
14
|
+
* sidecar server, fire-and-forget job).
|
|
15
|
+
*
|
|
16
|
+
* POSIX: `detached: true` — the child leads its own process group, so it
|
|
17
|
+
* survives the parent and group kills (`kill(-pid)`) still reach it.
|
|
18
|
+
*
|
|
19
|
+
* Windows: the child must not share the launcher's console (a console-close
|
|
20
|
+
* event when the launcher exits would tear it down, #556) and must not flash
|
|
21
|
+
* a window. How to get there depends on the child's stdio:
|
|
22
|
+
*
|
|
23
|
+
* - All stdio non-inherited ('ignore'/'pipe'): `windowsHide: true`, NOT
|
|
24
|
+
* detached. CREATE_NO_WINDOW gives the child its own hidden console that
|
|
25
|
+
* every console-subsystem descendant (powershell, git, a .cmd shim's cmd.exe
|
|
26
|
+
* wrapper) inherits — no window anywhere down the tree. `detached` would
|
|
27
|
+
* defeat it: DETACHED_PROCESS makes CreateProcess ignore CREATE_NO_WINDOW.
|
|
28
|
+
*
|
|
29
|
+
* - Any stdio slot redirected to an fd (log files — `fdStdio: true`): libuv
|
|
30
|
+
* skips CREATE_NO_WINDOW whenever a stdio fd is inherited, so windowsHide
|
|
31
|
+
* cannot engage and a non-detached child would share the launcher's console
|
|
32
|
+
* and die with it. Keep DETACHED_PROCESS: the child runs console-less and
|
|
33
|
+
* windowless; its console-tool spawns stay invisible because the leaf call
|
|
34
|
+
* sites pass their own `windowsHide` with piped stdio.
|
|
35
|
+
*/
|
|
36
|
+
export declare function backgroundSpawnOptions(opts?: {
|
|
37
|
+
fdStdio?: boolean;
|
|
38
|
+
platform?: NodeJS.Platform;
|
|
39
|
+
}): {
|
|
40
|
+
detached: boolean;
|
|
41
|
+
windowsHide: boolean;
|
|
42
|
+
};
|
|
12
43
|
/**
|
|
13
44
|
* Is a process with this PID currently alive?
|
|
14
45
|
*
|
|
@@ -17,7 +17,7 @@ export function killTree(pid) {
|
|
|
17
17
|
return;
|
|
18
18
|
if (process.platform === 'win32') {
|
|
19
19
|
try {
|
|
20
|
-
execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore' });
|
|
20
|
+
execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true });
|
|
21
21
|
}
|
|
22
22
|
catch { /* already gone, or no such pid */ }
|
|
23
23
|
}
|
|
@@ -28,6 +28,39 @@ export function killTree(pid) {
|
|
|
28
28
|
catch { /* already gone */ }
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Spawn options for a long-lived background child (daemon, detached worker,
|
|
33
|
+
* sidecar server, fire-and-forget job).
|
|
34
|
+
*
|
|
35
|
+
* POSIX: `detached: true` — the child leads its own process group, so it
|
|
36
|
+
* survives the parent and group kills (`kill(-pid)`) still reach it.
|
|
37
|
+
*
|
|
38
|
+
* Windows: the child must not share the launcher's console (a console-close
|
|
39
|
+
* event when the launcher exits would tear it down, #556) and must not flash
|
|
40
|
+
* a window. How to get there depends on the child's stdio:
|
|
41
|
+
*
|
|
42
|
+
* - All stdio non-inherited ('ignore'/'pipe'): `windowsHide: true`, NOT
|
|
43
|
+
* detached. CREATE_NO_WINDOW gives the child its own hidden console that
|
|
44
|
+
* every console-subsystem descendant (powershell, git, a .cmd shim's cmd.exe
|
|
45
|
+
* wrapper) inherits — no window anywhere down the tree. `detached` would
|
|
46
|
+
* defeat it: DETACHED_PROCESS makes CreateProcess ignore CREATE_NO_WINDOW.
|
|
47
|
+
*
|
|
48
|
+
* - Any stdio slot redirected to an fd (log files — `fdStdio: true`): libuv
|
|
49
|
+
* skips CREATE_NO_WINDOW whenever a stdio fd is inherited, so windowsHide
|
|
50
|
+
* cannot engage and a non-detached child would share the launcher's console
|
|
51
|
+
* and die with it. Keep DETACHED_PROCESS: the child runs console-less and
|
|
52
|
+
* windowless; its console-tool spawns stay invisible because the leaf call
|
|
53
|
+
* sites pass their own `windowsHide` with piped stdio.
|
|
54
|
+
*/
|
|
55
|
+
export function backgroundSpawnOptions(opts = {}) {
|
|
56
|
+
const platform = opts.platform ?? process.platform;
|
|
57
|
+
if (platform === 'win32') {
|
|
58
|
+
return opts.fdStdio
|
|
59
|
+
? { detached: true, windowsHide: true }
|
|
60
|
+
: { detached: false, windowsHide: true };
|
|
61
|
+
}
|
|
62
|
+
return { detached: true, windowsHide: false };
|
|
63
|
+
}
|
|
31
64
|
/**
|
|
32
65
|
* Is a process with this PID currently alive?
|
|
33
66
|
*
|
|
@@ -107,6 +107,7 @@ function runPowerShell(script, extraEnv) {
|
|
|
107
107
|
encoding: 'utf-8',
|
|
108
108
|
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
|
|
109
109
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
110
|
+
windowsHide: true,
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
113
|
/** Parse the read script's stdout into the original value kind and RAW value. */
|
|
@@ -163,6 +164,7 @@ export function getEffectiveExecutionPolicy() {
|
|
|
163
164
|
const out = execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', 'Get-ExecutionPolicy'], {
|
|
164
165
|
encoding: 'utf-8',
|
|
165
166
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
167
|
+
windowsHide: true,
|
|
166
168
|
}).trim();
|
|
167
169
|
return out || null;
|
|
168
170
|
}
|
package/dist/lib/plugins.js
CHANGED
|
@@ -172,16 +172,26 @@ export function loadPluginManifest(pluginRoot) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
export function validatePluginName(name) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
if (!name || name.length === 0 || name === '.' || name === '..') {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const normalized = path.normalize(name);
|
|
179
|
+
if (normalized === '.' || normalized === '' || normalized === '..') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (/[/\\]/.test(name) || name.includes('\0')) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (path.basename(normalized) !== name) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
179
189
|
}
|
|
180
190
|
export function assertPluginTargetContained(targetRoot, pluginsDir) {
|
|
181
191
|
const resolvedPluginsDir = path.resolve(pluginsDir);
|
|
182
192
|
const resolvedTargetRoot = path.resolve(targetRoot);
|
|
183
|
-
if (resolvedTargetRoot
|
|
184
|
-
|
|
193
|
+
if (resolvedTargetRoot === resolvedPluginsDir
|
|
194
|
+
|| !resolvedTargetRoot.startsWith(`${resolvedPluginsDir}${path.sep}`)) {
|
|
185
195
|
throw new Error(`Plugin install target escapes plugins directory: ${targetRoot}`);
|
|
186
196
|
}
|
|
187
197
|
}
|