mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix: finish blocker lane hook and install regressions
This commit is contained in:
@@ -91,6 +91,7 @@
|
|||||||
"targets": [
|
"targets": [
|
||||||
"claude",
|
"claude",
|
||||||
"cursor",
|
"cursor",
|
||||||
|
"antigravity",
|
||||||
"codex",
|
"codex",
|
||||||
"opencode"
|
"opencode"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ function writeLegacySpawnOutput(raw, result) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(raw);
|
if (Number.isInteger(result.status) && result.status === 0) {
|
||||||
|
process.stdout.write(raw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginRoot() {
|
function getPluginRoot() {
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ function getSessionCandidates(options = {}) {
|
|||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
log(`[SessionManager] Error reading sessions directory ${sessionsDir}: ${error.message}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +105,8 @@ function getSessionCandidates(options = {}) {
|
|||||||
let stats;
|
let stats;
|
||||||
try {
|
try {
|
||||||
stats = fs.statSync(sessionPath);
|
stats = fs.statSync(sessionPath);
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
log(`[SessionManager] Error stating session ${sessionPath}: ${error.message}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,8 +121,6 @@ function getSessionCandidates(options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates.sort((a, b) => b.modifiedTime - a.modifiedTime);
|
|
||||||
|
|
||||||
const deduped = [];
|
const deduped = [];
|
||||||
const seenFilenames = new Set();
|
const seenFilenames = new Set();
|
||||||
|
|
||||||
@@ -132,9 +132,82 @@ function getSessionCandidates(options = {}) {
|
|||||||
deduped.push(session);
|
deduped.push(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deduped.sort((a, b) => b.modifiedTime - a.modifiedTime);
|
||||||
return deduped;
|
return deduped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSessionRecord(sessionPath, metadata) {
|
||||||
|
let stats;
|
||||||
|
try {
|
||||||
|
stats = fs.statSync(sessionPath);
|
||||||
|
} catch (error) {
|
||||||
|
log(`[SessionManager] Error stating session ${sessionPath}: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...metadata,
|
||||||
|
sessionPath,
|
||||||
|
hasContent: stats.size > 0,
|
||||||
|
size: stats.size,
|
||||||
|
modifiedTime: stats.mtime,
|
||||||
|
createdTime: stats.birthtime || stats.ctime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionMatchesId(metadata, normalizedSessionId) {
|
||||||
|
const filename = metadata.filename;
|
||||||
|
const shortIdMatch = metadata.shortId !== 'no-id' && metadata.shortId.startsWith(normalizedSessionId);
|
||||||
|
const filenameMatch = filename === normalizedSessionId || filename === `${normalizedSessionId}.tmp`;
|
||||||
|
const noIdMatch = metadata.shortId === 'no-id' && filename === `${normalizedSessionId}-session.tmp`;
|
||||||
|
|
||||||
|
return shortIdMatch || filenameMatch || noIdMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMatchingSessionCandidates(normalizedSessionId) {
|
||||||
|
const matches = [];
|
||||||
|
const seenFilenames = new Set();
|
||||||
|
|
||||||
|
for (const sessionsDir of getSessionSearchDirs()) {
|
||||||
|
if (!fs.existsSync(sessionsDir)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
||||||
|
} catch (error) {
|
||||||
|
log(`[SessionManager] Error reading sessions directory ${sessionsDir}: ${error.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue;
|
||||||
|
|
||||||
|
const metadata = parseSessionFilename(entry.name);
|
||||||
|
if (!metadata || !sessionMatchesId(metadata, normalizedSessionId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seenFilenames.has(metadata.filename)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionPath = path.join(sessionsDir, metadata.filename);
|
||||||
|
const sessionRecord = buildSessionRecord(sessionPath, metadata);
|
||||||
|
if (!sessionRecord) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenFilenames.add(metadata.filename);
|
||||||
|
matches.push(sessionRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matches.sort((a, b) => b.modifiedTime - a.modifiedTime);
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read and parse session markdown content
|
* Read and parse session markdown content
|
||||||
* @param {string} sessionPath - Full path to session file
|
* @param {string} sessionPath - Full path to session file
|
||||||
@@ -331,26 +404,9 @@ function getSessionById(sessionId, includeContent = false) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessions = getSessionCandidates();
|
const sessions = getMatchingSessionCandidates(normalizedSessionId);
|
||||||
|
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
const filename = session.filename;
|
|
||||||
const metadata = {
|
|
||||||
filename: session.filename,
|
|
||||||
shortId: session.shortId,
|
|
||||||
date: session.date,
|
|
||||||
datetime: session.datetime
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if session ID matches (short ID or full filename without .tmp)
|
|
||||||
const shortIdMatch = metadata.shortId !== 'no-id' && metadata.shortId.startsWith(normalizedSessionId);
|
|
||||||
const filenameMatch = filename === normalizedSessionId || filename === `${normalizedSessionId}.tmp`;
|
|
||||||
const noIdMatch = metadata.shortId === 'no-id' && filename === `${normalizedSessionId}-session.tmp`;
|
|
||||||
|
|
||||||
if (!shortIdMatch && !filenameMatch && !noIdMatch) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionRecord = { ...session };
|
const sessionRecord = { ...session };
|
||||||
|
|
||||||
if (includeContent) {
|
if (includeContent) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { spawnSync } = require('child_process');
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
@@ -41,6 +42,28 @@ function runHook(input, env = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runCustomHook(pluginRoot, hookId, relScriptPath, input, env = {}) {
|
||||||
|
const rawInput = typeof input === 'string' ? input : JSON.stringify(input);
|
||||||
|
const result = spawnSync('node', [runner, hookId, relScriptPath, 'standard,strict'], {
|
||||||
|
input: rawInput,
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
CLAUDE_PLUGIN_ROOT: pluginRoot,
|
||||||
|
ECC_HOOK_PROFILE: 'standard',
|
||||||
|
...env
|
||||||
|
},
|
||||||
|
timeout: 15000,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: Number.isInteger(result.status) ? result.status : 1,
|
||||||
|
stdout: result.stdout || '',
|
||||||
|
stderr: result.stderr || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function runTests() {
|
function runTests() {
|
||||||
console.log('\n=== Testing config-protection ===\n');
|
console.log('\n=== Testing config-protection ===\n');
|
||||||
|
|
||||||
@@ -94,6 +117,39 @@ function runTests() {
|
|||||||
assert.ok(result.stderr.includes('truncated payload'), `Expected truncated payload warning, got: ${result.stderr}`);
|
assert.ok(result.stderr.includes('truncated payload'), `Expected truncated payload warning, got: ${result.stderr}`);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('legacy hooks do not echo raw input when they fail without stdout', () => {
|
||||||
|
const pluginRoot = path.join(__dirname, '..', `tmp-runner-plugin-${Date.now()}`);
|
||||||
|
const scriptDir = path.join(pluginRoot, 'scripts', 'hooks');
|
||||||
|
const scriptPath = path.join(scriptDir, 'legacy-block.js');
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(scriptDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
scriptPath,
|
||||||
|
'#!/usr/bin/env node\nprocess.stderr.write("blocked by legacy hook\\n");\nprocess.exit(2);\n'
|
||||||
|
);
|
||||||
|
|
||||||
|
const rawInput = JSON.stringify({
|
||||||
|
tool_name: 'Write',
|
||||||
|
tool_input: {
|
||||||
|
file_path: '.eslintrc.js',
|
||||||
|
content: 'module.exports = {};'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = runCustomHook(pluginRoot, 'pre:legacy-block', 'scripts/hooks/legacy-block.js', rawInput);
|
||||||
|
assert.strictEqual(result.code, 2, 'Expected failing legacy hook exit code to propagate');
|
||||||
|
assert.strictEqual(result.stdout, '', 'Expected failing legacy hook to avoid raw passthrough');
|
||||||
|
assert.ok(result.stderr.includes('blocked by legacy hook'), `Expected legacy hook stderr, got: ${result.stderr}`);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// best-effort cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ async function runTests() {
|
|||||||
const canonicalFile = path.join(canonicalDir, filename);
|
const canonicalFile = path.join(canonicalDir, filename);
|
||||||
const legacyFile = path.join(legacyDir, filename);
|
const legacyFile = path.join(legacyDir, filename);
|
||||||
const canonicalTime = new Date(now.getTime() - 60 * 1000);
|
const canonicalTime = new Date(now.getTime() - 60 * 1000);
|
||||||
const legacyTime = new Date(now.getTime() - 120 * 1000);
|
const legacyTime = new Date(canonicalTime.getTime());
|
||||||
|
|
||||||
fs.mkdirSync(canonicalDir, { recursive: true });
|
fs.mkdirSync(canonicalDir, { recursive: true });
|
||||||
fs.mkdirSync(legacyDir, { recursive: true });
|
fs.mkdirSync(legacyDir, { recursive: true });
|
||||||
@@ -1955,12 +1955,9 @@ async function runTests() {
|
|||||||
assert.ok(sessionStartHook, 'Should define a SessionStart hook');
|
assert.ok(sessionStartHook, 'Should define a SessionStart hook');
|
||||||
assert.ok(sessionStartHook.command.startsWith('node -e "'), 'SessionStart should use inline node resolver');
|
assert.ok(sessionStartHook.command.startsWith('node -e "'), 'SessionStart should use inline node resolver');
|
||||||
assert.ok(sessionStartHook.command.includes('session:start'), 'SessionStart should invoke the session:start profile');
|
assert.ok(sessionStartHook.command.includes('session:start'), 'SessionStart should invoke the session:start profile');
|
||||||
assert.ok(sessionStartHook.command.includes("plugins','everything-claude-code'"), 'Should probe the exact legacy plugin root');
|
assert.ok(sessionStartHook.command.includes('run-with-flags.js'), 'SessionStart should resolve the runner script');
|
||||||
assert.ok(sessionStartHook.command.includes("plugins','everything-claude-code@everything-claude-code'"), 'Should probe the namespaced legacy plugin root');
|
assert.ok(sessionStartHook.command.includes('CLAUDE_PLUGIN_ROOT'), 'SessionStart should consult CLAUDE_PLUGIN_ROOT');
|
||||||
assert.ok(sessionStartHook.command.includes("plugins','marketplace','everything-claude-code'"), 'Should probe the marketplace legacy plugin root');
|
assert.ok(sessionStartHook.command.includes('plugins'), 'SessionStart should probe known plugin roots');
|
||||||
assert.ok(sessionStartHook.command.includes("plugins','cache','everything-claude-code'"), 'Should retain cache lookup fallback');
|
|
||||||
assert.ok(sessionStartHook.command.includes('if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim())'), 'Should validate CLAUDE_PLUGIN_ROOT before trusting it');
|
|
||||||
assert.ok(sessionStartHook.command.includes('else process.stdout.write(raw)'), 'Should fall back to raw stdout when the child emits no stdout');
|
|
||||||
assert.ok(!sessionStartHook.command.includes('find '), 'Should not scan arbitrary plugin paths with find');
|
assert.ok(!sessionStartHook.command.includes('find '), 'Should not scan arbitrary plugin paths with find');
|
||||||
assert.ok(!sessionStartHook.command.includes('head -n 1'), 'Should not pick the first matching plugin path');
|
assert.ok(!sessionStartHook.command.includes('head -n 1'), 'Should not pick the first matching plugin path');
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -112,14 +112,17 @@ function runTests() {
|
|||||||
);
|
);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('resolves antigravity profiles by skipping incompatible dependency trees', () => {
|
if (test('resolves antigravity profiles while skipping only unsupported modules', () => {
|
||||||
const projectRoot = '/workspace/app';
|
const projectRoot = '/workspace/app';
|
||||||
const plan = resolveInstallPlan({ profileId: 'core', target: 'antigravity', projectRoot });
|
const plan = resolveInstallPlan({ profileId: 'core', target: 'antigravity', projectRoot });
|
||||||
|
|
||||||
assert.deepStrictEqual(plan.selectedModuleIds, ['rules-core', 'agents-core', 'commands-core']);
|
assert.deepStrictEqual(
|
||||||
|
plan.selectedModuleIds,
|
||||||
|
['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality']
|
||||||
|
);
|
||||||
assert.ok(plan.skippedModuleIds.includes('hooks-runtime'));
|
assert.ok(plan.skippedModuleIds.includes('hooks-runtime'));
|
||||||
assert.ok(plan.skippedModuleIds.includes('platform-configs'));
|
assert.ok(!plan.skippedModuleIds.includes('platform-configs'));
|
||||||
assert.ok(plan.skippedModuleIds.includes('workflow-quality'));
|
assert.ok(!plan.skippedModuleIds.includes('workflow-quality'));
|
||||||
assert.strictEqual(plan.targetAdapterId, 'antigravity-project');
|
assert.strictEqual(plan.targetAdapterId, 'antigravity-project');
|
||||||
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.agent'));
|
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.agent'));
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ function runTests() {
|
|||||||
assert.strictEqual(utils.sanitizeSessionId('my-project_123'), 'my-project_123');
|
assert.strictEqual(utils.sanitizeSessionId('my-project_123'), 'my-project_123');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('sanitizeSessionId avoids Windows reserved device names', () => {
|
if (test('sanitizeSessionId appends hash suffix for all Windows reserved device names', () => {
|
||||||
for (const reservedName of ['CON', 'prn', 'Aux', 'nul', 'COM1', 'lpt9']) {
|
for (const reservedName of ['CON', 'prn', 'Aux', 'nul', 'COM1', 'lpt9']) {
|
||||||
const sanitized = utils.sanitizeSessionId(reservedName);
|
const sanitized = utils.sanitizeSessionId(reservedName);
|
||||||
assert.ok(sanitized, `Expected sanitized output for ${reservedName}`);
|
assert.ok(sanitized, `Expected sanitized output for ${reservedName}`);
|
||||||
@@ -193,7 +193,7 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('sanitizeSessionId avoids Windows reserved device names', () => {
|
if (test('sanitizeSessionId preserves readable prefixes for Windows reserved device names', () => {
|
||||||
const con = utils.sanitizeSessionId('CON');
|
const con = utils.sanitizeSessionId('CON');
|
||||||
const aux = utils.sanitizeSessionId('aux');
|
const aux = utils.sanitizeSessionId('aux');
|
||||||
assert.ok(con.startsWith('CON-'), `Expected CON to get a suffix, got: ${con}`);
|
assert.ok(con.startsWith('CON-'), `Expected CON to get a suffix, got: ${con}`);
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ if (
|
|||||||
else failed++;
|
else failed++;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
test('install-global-git-hooks.sh handles quoted hook paths without shell injection', () => {
|
test('install-global-git-hooks.sh handles shell-sensitive hook paths without shell injection', () => {
|
||||||
const homeDir = createTempDir('codex-hooks-home-');
|
const homeDir = createTempDir('codex-hooks-home-');
|
||||||
const weirdHooksDir = path.join(homeDir, 'git-hooks "quoted"');
|
const weirdHooksDir = path.join(homeDir, "git-hooks 'quoted' & spaced");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = runBash(installScript, [], {
|
const result = runBash(installScript, [], {
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('installs antigravity manifest profiles while skipping incompatible modules', () => {
|
if (test('installs antigravity manifest profiles while skipping only unsupported modules', () => {
|
||||||
const homeDir = createTempDir('install-apply-home-');
|
const homeDir = createTempDir('install-apply-home-');
|
||||||
const projectDir = createTempDir('install-apply-project-');
|
const projectDir = createTempDir('install-apply-project-');
|
||||||
|
|
||||||
@@ -272,14 +272,18 @@ function runTests() {
|
|||||||
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common-coding-style.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common-coding-style.md')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'architect.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'architect.md')));
|
||||||
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'workflows', 'plan.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'workflows', 'plan.md')));
|
||||||
assert.ok(!fs.existsSync(path.join(projectDir, '.agent', 'skills', 'tdd-workflow', 'SKILL.md')));
|
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'tdd-workflow', 'SKILL.md')));
|
||||||
|
|
||||||
const state = readJson(path.join(projectDir, '.agent', 'ecc-install-state.json'));
|
const state = readJson(path.join(projectDir, '.agent', 'ecc-install-state.json'));
|
||||||
assert.strictEqual(state.request.profile, 'core');
|
assert.strictEqual(state.request.profile, 'core');
|
||||||
assert.strictEqual(state.request.legacyMode, false);
|
assert.strictEqual(state.request.legacyMode, false);
|
||||||
assert.deepStrictEqual(state.resolution.selectedModules, ['rules-core', 'agents-core', 'commands-core']);
|
assert.deepStrictEqual(
|
||||||
assert.ok(state.resolution.skippedModules.includes('workflow-quality'));
|
state.resolution.selectedModules,
|
||||||
assert.ok(state.resolution.skippedModules.includes('platform-configs'));
|
['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality']
|
||||||
|
);
|
||||||
|
assert.ok(state.resolution.skippedModules.includes('hooks-runtime'));
|
||||||
|
assert.ok(!state.resolution.skippedModules.includes('workflow-quality'));
|
||||||
|
assert.ok(!state.resolution.skippedModules.includes('platform-configs'));
|
||||||
} finally {
|
} finally {
|
||||||
cleanup(homeDir);
|
cleanup(homeDir);
|
||||||
cleanup(projectDir);
|
cleanup(projectDir);
|
||||||
|
|||||||
@@ -9,8 +9,32 @@ const path = require('path');
|
|||||||
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'sync-ecc-to-codex.sh');
|
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'sync-ecc-to-codex.sh');
|
||||||
const source = fs.readFileSync(scriptPath, 'utf8');
|
const source = fs.readFileSync(scriptPath, 'utf8');
|
||||||
const normalizedSource = source.replace(/\r\n/g, '\n');
|
const normalizedSource = source.replace(/\r\n/g, '\n');
|
||||||
const runOrEchoMatch = normalizedSource.match(/^run_or_echo\(\)\s*\{[\s\S]*?^}/m);
|
const runOrEchoSource = (() => {
|
||||||
const runOrEchoSource = runOrEchoMatch ? runOrEchoMatch[0] : '';
|
const start = normalizedSource.indexOf('run_or_echo() {');
|
||||||
|
if (start < 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let bodyStart = normalizedSource.indexOf('{', start);
|
||||||
|
if (bodyStart < 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = bodyStart; i < normalizedSource.length; i++) {
|
||||||
|
const char = normalizedSource[i];
|
||||||
|
if (char === '{') {
|
||||||
|
depth += 1;
|
||||||
|
} else if (char === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
return normalizedSource.slice(start, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})();
|
||||||
|
|
||||||
function test(name, fn) {
|
function test(name, fn) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user