product-console-companion 0.7.2 → 0.7.3
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/dist/claude-binary.js +121 -0
- package/dist/claude-cli-caller.js +2 -2
- package/dist/cli.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import crossSpawn from 'cross-spawn';
|
|
2
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
5
|
+
const CMD_UNSAFE_ARGUMENT = /[\^<>|&%!]/;
|
|
6
|
+
const FALLBACK_ARGUMENT_ERROR = "claude resolves to a .cmd shim and this argument can't pass through cmd.exe intact; install Claude Code so claude.exe is reachable, or update the companion";
|
|
7
|
+
const defaultFs = { existsSync, readFileSync };
|
|
8
|
+
let cachedCommand = null;
|
|
9
|
+
function envValue(env, name) {
|
|
10
|
+
const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
11
|
+
return key ? env[key] : undefined;
|
|
12
|
+
}
|
|
13
|
+
function windowsPathEntries(env) {
|
|
14
|
+
return (envValue(env, 'PATH') ?? '')
|
|
15
|
+
.split(';')
|
|
16
|
+
.map((entry) => entry.trim().replace(/^"|"$/g, ''))
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
function windowsExtensions(env) {
|
|
20
|
+
const pathExt = envValue(env, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD';
|
|
21
|
+
return pathExt
|
|
22
|
+
.split(';')
|
|
23
|
+
.map((extension) => extension.trim().toLowerCase())
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((extension) => extension.startsWith('.') ? extension : `.${extension}`);
|
|
26
|
+
}
|
|
27
|
+
function existingCandidate(directory, extension, fs) {
|
|
28
|
+
const lowerCaseCandidate = join(directory, `claude${extension.toLowerCase()}`);
|
|
29
|
+
if (fs.existsSync(lowerCaseCandidate))
|
|
30
|
+
return resolve(lowerCaseCandidate);
|
|
31
|
+
const originalCaseCandidate = join(directory, `claude${extension}`);
|
|
32
|
+
return fs.existsSync(originalCaseCandidate) ? resolve(originalCaseCandidate) : null;
|
|
33
|
+
}
|
|
34
|
+
function parsedShimExecutable(shimPath, fs) {
|
|
35
|
+
let contents;
|
|
36
|
+
try {
|
|
37
|
+
contents = fs.readFileSync(shimPath, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const shimDirectory = dirname(shimPath);
|
|
43
|
+
for (const match of contents.matchAll(/"([^"\r\n]*claude\.exe)"/gi)) {
|
|
44
|
+
let target = match[1].replace(/%(?:~dp0|dp0)%?/gi, shimDirectory);
|
|
45
|
+
// Tests inject win32 while running on Linux CI; normalizing both separator forms keeps the
|
|
46
|
+
// resolver testable with a real temporary filesystem and is a no-op for native Windows paths.
|
|
47
|
+
target = target.replace(/[\\/]/g, sep);
|
|
48
|
+
if (!isAbsolute(target))
|
|
49
|
+
target = resolve(shimDirectory, target);
|
|
50
|
+
if (fs.existsSync(target))
|
|
51
|
+
return resolve(target);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function executableForShim(shimPath, fs) {
|
|
56
|
+
const parsed = parsedShimExecutable(shimPath, fs);
|
|
57
|
+
if (parsed)
|
|
58
|
+
return parsed;
|
|
59
|
+
const conventional = join(dirname(shimPath), 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe');
|
|
60
|
+
return fs.existsSync(conventional) ? resolve(conventional) : null;
|
|
61
|
+
}
|
|
62
|
+
function resolveUncached(platform, env, fs) {
|
|
63
|
+
if (platform !== 'win32') {
|
|
64
|
+
return { command: { command: 'claude', viaShell: true }, fallbackReason: null };
|
|
65
|
+
}
|
|
66
|
+
const directories = windowsPathEntries(env);
|
|
67
|
+
const extensions = windowsExtensions(env);
|
|
68
|
+
for (const directory of directories) {
|
|
69
|
+
for (const extension of extensions) {
|
|
70
|
+
if (extension !== '.cmd' && extension !== '.exe')
|
|
71
|
+
continue;
|
|
72
|
+
const candidate = existingCandidate(directory, extension, fs);
|
|
73
|
+
if (!candidate)
|
|
74
|
+
continue;
|
|
75
|
+
if (extension === '.exe') {
|
|
76
|
+
return { command: { command: candidate, viaShell: false }, fallbackReason: null };
|
|
77
|
+
}
|
|
78
|
+
const executable = executableForShim(candidate, fs);
|
|
79
|
+
if (executable) {
|
|
80
|
+
return { command: { command: executable, viaShell: false }, fallbackReason: null };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
command: { command: 'claude', viaShell: true },
|
|
84
|
+
fallbackReason: `Found ${candidate}, but its claude.exe target was not reachable; falling back to the .cmd shim.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
command: { command: 'claude', viaShell: true },
|
|
90
|
+
fallbackReason: 'No claude.exe or claude.cmd was found on PATH; falling back to cross-spawn command lookup.',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Resolve once in production. Injected dependencies bypass the cache so Linux CI can exercise
|
|
94
|
+
* the Windows lookup rules without mutating process globals or depending on Windows filesystem
|
|
95
|
+
* semantics. */
|
|
96
|
+
export function resolveClaudeCommand(options = {}) {
|
|
97
|
+
const injected = options.platform !== undefined || options.env !== undefined || options.fs !== undefined;
|
|
98
|
+
if (!injected && cachedCommand)
|
|
99
|
+
return cachedCommand;
|
|
100
|
+
const result = resolveUncached(options.platform ?? process.platform, options.env ?? process.env, options.fs ?? defaultFs);
|
|
101
|
+
if (!injected) {
|
|
102
|
+
cachedCommand = result.command;
|
|
103
|
+
if (result.fallbackReason)
|
|
104
|
+
console.warn(`[product-console-companion] ${result.fallbackReason}`);
|
|
105
|
+
}
|
|
106
|
+
return result.command;
|
|
107
|
+
}
|
|
108
|
+
export function assertClaudeArgumentsSafeForFallback(args, platform = process.platform) {
|
|
109
|
+
if (platform === 'win32' && args.some((argument) => CMD_UNSAFE_ARGUMENT.test(argument))) {
|
|
110
|
+
throw new Error(FALLBACK_ARGUMENT_ERROR);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Native executables go straight through CreateProcess via Node. cross-spawn remains only for
|
|
114
|
+
* the unresolved command-name path, where it supplies the existing .cmd compatibility behavior. */
|
|
115
|
+
export function spawnClaude(args, options = {}, resolution = resolveClaudeCommand()) {
|
|
116
|
+
if (resolution.viaShell) {
|
|
117
|
+
assertClaudeArgumentsSafeForFallback(args);
|
|
118
|
+
return crossSpawn(resolution.command, args, options);
|
|
119
|
+
}
|
|
120
|
+
return nodeSpawn(resolution.command, args, options);
|
|
121
|
+
}
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* --max-budget-usd's exceeded-budget behavior) was confirmed with real, budgeted `claude` calls
|
|
11
11
|
* during development, not guessed from --help text alone — see the Phase 2 PR description.
|
|
12
12
|
*/
|
|
13
|
-
import spawn from 'cross-spawn';
|
|
14
13
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
15
14
|
import { tmpdir } from 'node:os';
|
|
16
15
|
import { join } from 'node:path';
|
|
17
16
|
import { createInterface } from 'node:readline';
|
|
18
17
|
import { StringDecoder } from 'node:string_decoder';
|
|
19
18
|
import { assertSingleLineArguments } from './cli-arguments.js';
|
|
19
|
+
import { spawnClaude } from './claude-binary.js';
|
|
20
20
|
import { gitErrorMessage } from './sandbox.js';
|
|
21
21
|
import { appendDiffToVolatile, prepareShipmentAnalysis } from './shipment.js';
|
|
22
22
|
import { ModelRefusalError } from './text-model.js';
|
|
@@ -100,7 +100,7 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
100
100
|
let firstUnparseableLineFirst = '';
|
|
101
101
|
let firstUnparseableLineLast = '';
|
|
102
102
|
const typeCounts = new Map();
|
|
103
|
-
const child =
|
|
103
|
+
const child = spawnClaude(args, { cwd: sandboxPath });
|
|
104
104
|
// stdout/stderr are only null when stdio is explicitly configured away from 'pipe' — the
|
|
105
105
|
// default here, so both are always real streams in practice; the ! just satisfies the type.
|
|
106
106
|
const stdoutDecoder = new StringDecoder('utf8');
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import Anthropic from '@anthropic-ai/sdk';
|
|
3
|
-
import spawn from 'cross-spawn';
|
|
4
3
|
import { resolve } from 'node:path';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { addConnection, configPath, enableWorkspacePolling, loadConfig } from './config.js';
|
|
@@ -8,6 +7,7 @@ import { CompanionClient, CompanionHttpError, PairingHttpError, } from './http-c
|
|
|
8
7
|
import { assertIsRepo, checkRepositoryReadiness, currentCommit, GIT_UNAVAILABLE_MESSAGE, REMOTE_TROUBLESHOOTING_HINT, repositoryIdentity, RepositoryIdentityError, repositoryTopLevel, } from './local-scan.js';
|
|
9
8
|
import { runAnalyseShipmentTask, runDesignPromptTask, runGenerateSpecBatchTask, runGenericTask, runProposeNewCapabilitiesTask, runProposeSkeletonTask, runStructuredModelCall, runVerifyModuleTask } from './api-key-caller.js';
|
|
10
9
|
import { runAnalyseShipmentTaskViaCli, runDesignPromptTaskViaCli, runGenerateSpecBatchTaskViaCli, runGenericTaskViaCli, runProposeNewCapabilitiesTaskViaCli, runProposeSkeletonTaskViaCli, runStructuredModelCallViaCli, runVerifyModuleTaskViaCli } from './claude-cli-caller.js';
|
|
10
|
+
import { spawnClaude } from './claude-binary.js';
|
|
11
11
|
import { checkCodexLogin, CODEX_CODE_MODE_MODELS, codexTextModel, defaultCodexHome, runStructuredModelCallViaCodex } from './codex-cli-caller.js';
|
|
12
12
|
import { formatInvestigationReaderDiagnostic, INVESTIGATION_MODEL, runAndReportInvestigation } from './investigation-reader.js';
|
|
13
13
|
import { anthropicTextModel } from './text-model.js';
|
|
@@ -80,7 +80,7 @@ function parseFlags(argv) {
|
|
|
80
80
|
}
|
|
81
81
|
async function checkClaudeCliAvailable() {
|
|
82
82
|
return new Promise((resolvePromise) => {
|
|
83
|
-
const child =
|
|
83
|
+
const child = spawnClaude(['--version']);
|
|
84
84
|
let out = '';
|
|
85
85
|
child.stdout?.on('data', (chunk) => (out += chunk.toString()));
|
|
86
86
|
child.on('error', () => resolvePromise(null)); // e.g. ENOENT — not installed / not on PATH
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-console-companion",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Local-first BYOS companion for the Fynch Product Console — reads code and runs AI calls from your own machine instead of the hosted server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|