mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-13 21:33:32 +08:00
fix: resolve PR 371 portability regressions
This commit is contained in:
committed by
Affaan Mustafa
parent
1c5e07ff77
commit
af51fcacb7
@@ -39,7 +39,7 @@ This loads the ECC OpenCode plugin module from npm:
|
|||||||
|
|
||||||
It does **not** auto-register the full ECC command/agent/instruction catalog in your project config. For the full OpenCode setup, either:
|
It does **not** auto-register the full ECC command/agent/instruction catalog in your project config. For the full OpenCode setup, either:
|
||||||
- run OpenCode inside this repository, or
|
- run OpenCode inside this repository, or
|
||||||
- copy the relevant `.opencode/commands/`, `.opencode/prompts/`, `.opencode/instructions/`, and `agent` / `command` config entries into your own project
|
- copy the relevant `.opencode/commands/`, `.opencode/prompts/`, `.opencode/instructions/`, and the `instructions`, `agent`, and `command` config entries into your own project
|
||||||
|
|
||||||
After installation, the `ecc-install` CLI is also available:
|
After installation, the `ecc-install` CLI is also available:
|
||||||
|
|
||||||
|
|||||||
@@ -1055,7 +1055,7 @@ It does **not** automatically add ECC's full command/agent/instruction catalog t
|
|||||||
|
|
||||||
For the full ECC OpenCode setup, either:
|
For the full ECC OpenCode setup, either:
|
||||||
- run OpenCode inside this repository, or
|
- run OpenCode inside this repository, or
|
||||||
- copy the bundled `.opencode/` config assets into your project and wire the `agent` / `command` entries in `opencode.json`
|
- copy the bundled `.opencode/` config assets into your project and wire the `instructions`, `agent`, and `command` entries in `opencode.json`
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,25 @@
|
|||||||
"agents": {
|
"agents": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" }
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agents": { "type": "integer", "minimum": 0 },
|
||||||
|
"commands": { "type": "integer", "minimum": 0 },
|
||||||
|
"skills": { "type": "integer", "minimum": 0 },
|
||||||
|
"configAssets": { "type": "boolean" },
|
||||||
|
"hookEvents": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"customTools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* Fails silently if no formatter is found or installed.
|
* Fails silently if no formatter is found or installed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { execFileSync } = require('child_process');
|
const { execFileSync, spawnSync } = require('child_process');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { getPackageManager } = require('../lib/package-manager');
|
const { getPackageManager } = require('../lib/package-manager');
|
||||||
@@ -50,18 +50,23 @@ process.stdin.on('data', chunk => {
|
|||||||
|
|
||||||
function findProjectRoot(startDir) {
|
function findProjectRoot(startDir) {
|
||||||
let dir = startDir;
|
let dir = startDir;
|
||||||
|
let fallbackDir = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (PROJECT_ROOT_MARKERS.some(marker => fs.existsSync(path.join(dir, marker)))) {
|
if (detectFormatter(dir)) {
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fallbackDir && PROJECT_ROOT_MARKERS.some(marker => fs.existsSync(path.join(dir, marker)))) {
|
||||||
|
fallbackDir = dir;
|
||||||
|
}
|
||||||
|
|
||||||
const parentDir = path.dirname(dir);
|
const parentDir = path.dirname(dir);
|
||||||
if (parentDir === dir) break;
|
if (parentDir === dir) break;
|
||||||
dir = parentDir;
|
dir = parentDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
return startDir;
|
return fallbackDir || startDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectFormatter(projectRoot) {
|
function detectFormatter(projectRoot) {
|
||||||
@@ -114,6 +119,33 @@ function getFormatterCommand(formatter, filePath, projectRoot) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runFormatterCommand(cmd, projectRoot) {
|
||||||
|
if (process.platform === 'win32' && cmd.bin.endsWith('.cmd')) {
|
||||||
|
const result = spawnSync(cmd.bin, cmd.args, {
|
||||||
|
cwd: projectRoot,
|
||||||
|
shell: true,
|
||||||
|
stdio: 'pipe',
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof result.status === 'number' && result.status !== 0) {
|
||||||
|
throw new Error(result.stderr?.toString() || `Formatter exited with status ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
execFileSync(cmd.bin, cmd.args, {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
try {
|
try {
|
||||||
const input = JSON.parse(data);
|
const input = JSON.parse(data);
|
||||||
@@ -126,11 +158,7 @@ process.stdin.on('end', () => {
|
|||||||
const cmd = getFormatterCommand(formatter, filePath, projectRoot);
|
const cmd = getFormatterCommand(formatter, filePath, projectRoot);
|
||||||
|
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
execFileSync(cmd.bin, cmd.args, {
|
runFormatterCommand(cmd, projectRoot);
|
||||||
cwd: projectRoot,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Formatter not installed, file missing, or failed — non-blocking
|
// Formatter not installed, file missing, or failed — non-blocking
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const MAX_STDIN = 1024 * 1024;
|
const MAX_STDIN = 1024 * 1024;
|
||||||
|
const path = require('path');
|
||||||
const { splitShellSegments } = require('../lib/shell-split');
|
const { splitShellSegments } = require('../lib/shell-split');
|
||||||
|
|
||||||
const DEV_COMMAND_WORDS = new Set([
|
const DEV_COMMAND_WORDS = new Set([
|
||||||
@@ -10,13 +11,9 @@ const DEV_COMMAND_WORDS = new Set([
|
|||||||
'yarn',
|
'yarn',
|
||||||
'bun',
|
'bun',
|
||||||
'npx',
|
'npx',
|
||||||
'bash',
|
|
||||||
'sh',
|
|
||||||
'zsh',
|
|
||||||
'fish',
|
|
||||||
'tmux'
|
'tmux'
|
||||||
]);
|
]);
|
||||||
const SKIPPABLE_PREFIX_WORDS = new Set(['env', 'command', 'builtin', 'exec', 'noglob', 'sudo']);
|
const SKIPPABLE_PREFIX_WORDS = new Set(['env', 'command', 'builtin', 'exec', 'noglob', 'sudo', 'nohup']);
|
||||||
const PREFIX_OPTION_VALUE_WORDS = {
|
const PREFIX_OPTION_VALUE_WORDS = {
|
||||||
env: new Set(['-u', '-C', '-S', '--unset', '--chdir', '--split-string']),
|
env: new Set(['-u', '-C', '-S', '--unset', '--chdir', '--split-string']),
|
||||||
sudo: new Set([
|
sudo: new Set([
|
||||||
@@ -97,6 +94,12 @@ function isOptionToken(token) {
|
|||||||
return token.startsWith('-') && token.length > 1;
|
return token.startsWith('-') && token.length > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCommandWord(token) {
|
||||||
|
if (!token) return '';
|
||||||
|
const base = path.basename(token).toLowerCase();
|
||||||
|
return base.replace(/\.(cmd|exe|bat)$/i, '');
|
||||||
|
}
|
||||||
|
|
||||||
function getLeadingCommandWord(segment) {
|
function getLeadingCommandWord(segment) {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let activeWrapper = null;
|
let activeWrapper = null;
|
||||||
@@ -122,8 +125,10 @@ function getLeadingCommandWord(segment) {
|
|||||||
|
|
||||||
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
|
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
|
||||||
|
|
||||||
if (SKIPPABLE_PREFIX_WORDS.has(token)) {
|
const normalizedToken = normalizeCommandWord(token);
|
||||||
activeWrapper = token;
|
|
||||||
|
if (SKIPPABLE_PREFIX_WORDS.has(normalizedToken)) {
|
||||||
|
activeWrapper = normalizedToken;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +139,7 @@ function getLeadingCommandWord(segment) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return normalizedToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ _clv2_resolve_python_cmd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_CLV2_PYTHON_CMD="$(_clv2_resolve_python_cmd 2>/dev/null || true)"
|
_CLV2_PYTHON_CMD="$(_clv2_resolve_python_cmd 2>/dev/null || true)"
|
||||||
|
CLV2_PYTHON_CMD="$_CLV2_PYTHON_CMD"
|
||||||
export CLV2_PYTHON_CMD
|
export CLV2_PYTHON_CMD
|
||||||
|
|
||||||
_clv2_detect_project() {
|
_clv2_detect_project() {
|
||||||
|
|||||||
@@ -101,7 +101,14 @@ function readCommandLog(logFile) {
|
|||||||
return fs.readFileSync(logFile, 'utf8')
|
return fs.readFileSync(logFile, 'utf8')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(line => JSON.parse(line));
|
.map(line => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function withPrependedPath(binDir, env = {}) {
|
function withPrependedPath(binDir, env = {}) {
|
||||||
@@ -873,6 +880,32 @@ async function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('blocks env-wrapped npm run dev outside tmux on non-Windows platforms', async () => {
|
||||||
|
const stdinJson = JSON.stringify({ tool_input: { command: '/usr/bin/env npm run dev' } });
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'pre-bash-dev-server-block.js'), stdinJson);
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
assert.strictEqual(result.code, 0, 'Windows path should pass through');
|
||||||
|
assert.strictEqual(result.stdout, stdinJson, 'Windows path should preserve original input');
|
||||||
|
} else {
|
||||||
|
assert.strictEqual(result.code, 2, 'Unix path should block wrapped dev servers');
|
||||||
|
assert.ok(result.stderr.includes('BLOCKED'), 'Should explain why the command was blocked');
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('blocks nohup-wrapped npm run dev outside tmux on non-Windows platforms', async () => {
|
||||||
|
const stdinJson = JSON.stringify({ tool_input: { command: 'nohup npm run dev >/tmp/dev.log 2>&1 &' } });
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'pre-bash-dev-server-block.js'), stdinJson);
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
assert.strictEqual(result.code, 0, 'Windows path should pass through');
|
||||||
|
assert.strictEqual(result.stdout, stdinJson, 'Windows path should preserve original input');
|
||||||
|
} else {
|
||||||
|
assert.strictEqual(result.code, 2, 'Unix path should block wrapped dev servers');
|
||||||
|
assert.ok(result.stderr.includes('BLOCKED'), 'Should explain why the command was blocked');
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// post-edit-typecheck.js tests
|
// post-edit-typecheck.js tests
|
||||||
console.log('\npost-edit-typecheck.js:');
|
console.log('\npost-edit-typecheck.js:');
|
||||||
|
|
||||||
@@ -1659,7 +1692,14 @@ async function runTests() {
|
|||||||
const formatSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-format.js'), 'utf8');
|
const formatSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-format.js'), 'utf8');
|
||||||
// Strip comments to avoid matching "shell: true" in comment text
|
// Strip comments to avoid matching "shell: true" in comment text
|
||||||
const codeOnly = formatSource.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
const codeOnly = formatSource.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
||||||
assert.ok(!codeOnly.includes('shell:'), 'post-edit-format.js should not pass shell option in code');
|
assert.ok(
|
||||||
|
!/execFileSync\([^)]*shell\s*:/.test(codeOnly),
|
||||||
|
'post-edit-format.js should not pass shell option to execFileSync'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
codeOnly.includes("process.platform === 'win32' && cmd.bin.endsWith('.cmd')"),
|
||||||
|
'Windows shell execution must stay gated to .cmd shims'
|
||||||
|
);
|
||||||
assert.ok(formatSource.includes('npx.cmd'), 'Should use npx.cmd for Windows cross-platform safety');
|
assert.ok(formatSource.includes('npx.cmd'), 'Should use npx.cmd for Windows cross-platform safety');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
@@ -1710,6 +1750,33 @@ async function runTests() {
|
|||||||
assert.ok(detectProjectSource.includes('_clv2_resolve_python_cmd'), 'detect-project.sh should provide shared Python resolution');
|
assert.ok(detectProjectSource.includes('_clv2_resolve_python_cmd'), 'detect-project.sh should provide shared Python resolution');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('detect-project exports the resolved Python command for downstream scripts', async () => {
|
||||||
|
const detectProjectPath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'scripts', 'detect-project.sh');
|
||||||
|
const shellCommand = [
|
||||||
|
`source "${detectProjectPath}" >/dev/null 2>&1`,
|
||||||
|
'printf "%s\\n" "${CLV2_PYTHON_CMD:-}"'
|
||||||
|
].join('; ');
|
||||||
|
|
||||||
|
const shell = process.platform === 'win32' ? 'bash' : 'bash';
|
||||||
|
const proc = spawn(shell, ['-lc', shellCommand], {
|
||||||
|
env: process.env,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
proc.stdout.on('data', data => stdout += data);
|
||||||
|
proc.stderr.on('data', data => stderr += data);
|
||||||
|
|
||||||
|
const code = await new Promise((resolve, reject) => {
|
||||||
|
proc.on('close', resolve);
|
||||||
|
proc.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(code, 0, `detect-project.sh should source cleanly, stderr: ${stderr}`);
|
||||||
|
assert.ok(stdout.trim().length > 0, 'CLV2_PYTHON_CMD should export a resolved interpreter path');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (await asyncTest('matches .tsx extension for type checking', async () => {
|
if (await asyncTest('matches .tsx extension for type checking', async () => {
|
||||||
const testDir = createTestDir();
|
const testDir = createTestDir();
|
||||||
const testFile = path.join(testDir, 'component.tsx');
|
const testFile = path.join(testDir, 'component.tsx');
|
||||||
|
|||||||
Reference in New Issue
Block a user