feat(router-gate): stream A — path-normalization + glob util (§3.1.1)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// tools/path-normalization.mjs
|
||||
/**
|
||||
* Path normalization — router-gate v4 spec §3.1.1.
|
||||
* + glob-matcher util (used by skill-scope-verifier, tdd-real-test-verifier).
|
||||
* Pure-core; I/O (realpath) injected via opts for testability.
|
||||
*/
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const ENV_WHITELIST = ['HOME', 'USERPROFILE', 'APPDATA', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME'];
|
||||
|
||||
export function expandHome(target, homedir) {
|
||||
if (target === '~') return homedir;
|
||||
if (target.startsWith('~/') || target.startsWith('~\\')) {
|
||||
return homedir + target.slice(1);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
export function expandEnvVars(target, env) {
|
||||
let out = target;
|
||||
for (const name of ENV_WHITELIST) {
|
||||
const val = env[name];
|
||||
if (val === undefined) continue;
|
||||
out = out.split(`%${name}%`).join(val);
|
||||
out = out.split(`\${${name}}`).join(val);
|
||||
// bare $VAR — only when followed by non-word boundary
|
||||
out = out.replace(new RegExp(`\\$${name}(?![A-Za-z0-9_])`, 'g'), val);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function caseFold(p, platform) {
|
||||
return platform === 'win32' ? p.toLowerCase() : p;
|
||||
}
|
||||
|
||||
export function globToRegExp(pattern) {
|
||||
let re = '';
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
const c = pattern[i];
|
||||
if (c === '*') {
|
||||
if (pattern[i + 1] === '*') {
|
||||
re += '.*'; // ** — across separators
|
||||
i++;
|
||||
if (pattern[i + 1] === '/') i++; // consume trailing slash of **/
|
||||
} else {
|
||||
re += '[^/]*'; // * — within segment
|
||||
}
|
||||
} else if (c === '?') {
|
||||
re += '[^/]';
|
||||
} else if ('.+^${}()|[]\\'.includes(c)) {
|
||||
re += '\\' + c;
|
||||
} else {
|
||||
re += c;
|
||||
}
|
||||
}
|
||||
return new RegExp(`^${re}$`);
|
||||
}
|
||||
|
||||
export function globMatch(pathStr, pattern) {
|
||||
const norm = (s) => s.split('\\').join('/');
|
||||
return globToRegExp(norm(pattern)).test(norm(pathStr));
|
||||
}
|
||||
|
||||
export function pathNormalize(target, opts = {}) {
|
||||
const {
|
||||
homedir = os.homedir(),
|
||||
env = process.env,
|
||||
platform = process.platform,
|
||||
realpath = fs.realpathSync,
|
||||
resolve = path.resolve,
|
||||
} = opts;
|
||||
let p = expandHome(target, homedir);
|
||||
p = expandEnvVars(p, env);
|
||||
const resolved = resolve(p);
|
||||
let real;
|
||||
try {
|
||||
real = realpath(resolved);
|
||||
} catch {
|
||||
real = resolved; // ENOENT — best-effort resolved path
|
||||
}
|
||||
return caseFold(real, platform);
|
||||
}
|
||||
Reference in New Issue
Block a user