fix(skills/understand): canonicalize isCli paths so symlinked SKILL_DIR runs main()

import.meta.url resolves through symlinks but pathToFileURL(process.argv[1])
preserves them, so extract-structure.mjs silently exited 0 without writing
output when invoked via the plugin's symlinked install path — the documented
Claude Code / Copilot CLI layout. Compare both sides via realpathSync and add
a post-write existence assertion plus caller-side guidance in the agent.

Closes #162
This commit is contained in:
Lum1104
2026-05-21 19:36:14 +08:00
parent 58411ac1f7
commit 8a78c94fc6
2 changed files with 24 additions and 4 deletions
@@ -64,6 +64,8 @@ node <SKILL_DIR>/extract-structure.mjs \
If the script exits non-zero, read stderr and report the error. Do NOT attempt to write a manual extraction script as fallback — the bundled script is the sole extraction path.
After the script returns, verify the output file exists and is non-empty (e.g. `test -s $PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-<batchIndex>.json`). Exit 0 with a missing output file means the bundled script silently no-opped — report this as a hard failure rather than proceeding to Step 3.
### Step 3 — Read the extraction results
Read `$PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-<batchIndex>.json`. The output format is:
@@ -19,7 +19,7 @@
import { createRequire } from 'node:module';
import { dirname, resolve, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { readFileSync, writeFileSync } from 'node:fs';
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
// skills/understand/ -> plugin root is two dirs up
@@ -133,6 +133,10 @@ async function main() {
};
writeFileSync(outputPath, JSON.stringify(output, null, 2), 'utf-8');
if (!existsSync(outputPath)) {
throw new Error(`output file missing after write: ${outputPath}`);
}
}
// ---------------------------------------------------------------------------
@@ -302,11 +306,25 @@ export function buildResult(file, totalLines, nonEmptyLines, analysis, callGraph
// ---------------------------------------------------------------------------
// Run only when executed directly as a CLI; importing the module (e.g. from
// tests) must not trigger main().
//
// Canonicalize both sides through realpathSync. Node ESM resolves
// import.meta.url through symlinks but pathToFileURL(process.argv[1]) preserves
// them, so a raw equality check silently no-ops when the script is invoked via
// a symlinked plugin install path (the default in Claude Code / Copilot CLI
// caches). See GitHub issue #162.
// ---------------------------------------------------------------------------
const isCli =
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
function isCliEntry() {
if (!process.argv[1]) return false;
try {
const modulePath = realpathSync(fileURLToPath(import.meta.url));
const argvPath = realpathSync(process.argv[1]);
return modulePath === argvPath;
} catch {
return false;
}
}
if (isCli) {
if (isCliEntry()) {
try {
await main();
} catch (err) {