@redue/cli 0.0.0-stage → 0.1.0-alpha.11
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/CHANGELOG.md +181 -0
- package/LICENSE +180 -0
- package/README.md +162 -2
- package/bin/redue.mjs +371 -0
- package/bin/vstate.mjs +3 -0
- package/docs/README.md +21 -0
- package/docs/user/agents.md +253 -0
- package/docs/user/configuration.md +60 -0
- package/docs/user/direct-typescript.md +114 -0
- package/docs/user/getting-started.md +148 -0
- package/docs/user/json.md +59 -0
- package/docs/user/npm-launcher-notice.md +42 -0
- package/docs/user/pilot.md +145 -0
- package/docs/user/receipt-selection-notice.md +60 -0
- package/docs/user/shell.md +25 -0
- package/docs/user/supported-projects.md +90 -0
- package/examples/redue.config.json +8 -0
- package/package.json +47 -4
- package/src/agent-integration.mjs +294 -0
- package/src/agent-policy.md +70 -0
- package/src/cached-state.mjs +42 -0
- package/src/claude-hook.mjs +115 -0
- package/src/cli.mjs +499 -0
- package/src/control-endpoint.mjs +11 -0
- package/src/daemon.mjs +1236 -0
- package/src/decision-profile.mjs +32 -0
- package/src/decision-validation.mjs +51 -0
- package/src/declared-plan.mjs +125 -0
- package/src/direct-run-certificate.mjs +185 -0
- package/src/direct-typescript-probe.mjs +22 -0
- package/src/direct-typescript.mjs +109 -0
- package/src/executable-lookup.mjs +18 -0
- package/src/glob.mjs +9 -0
- package/src/history-worker.mjs +26 -0
- package/src/index.mjs +306 -0
- package/src/installed-inputs.mjs +110 -0
- package/src/linux-inotify.py +228 -0
- package/src/linux-observer.mjs +79 -0
- package/src/node-onboarding.mjs +274 -0
- package/src/npm-typecheck-environment.mjs +36 -0
- package/src/npm-typecheck-probe.mjs +120 -0
- package/src/observation.mjs +59 -0
- package/src/owned-state.mjs +32 -0
- package/src/path-identity.mjs +43 -0
- package/src/plan-worker.mjs +140 -0
- package/src/plan.mjs +10 -0
- package/src/platform-observation.mjs +17 -0
- package/src/platform-state.mjs +14 -0
- package/src/pnpm-inputs.mjs +355 -0
- package/src/pnpm-typecheck-probe.mjs +47 -0
- package/src/presentation.mjs +53 -0
- package/src/probe-result.mjs +25 -0
- package/src/process-liveness.mjs +11 -0
- package/src/project-location.mjs +34 -0
- package/src/run-lock.mjs +77 -0
- package/src/run-recorder.cjs +78 -0
- package/src/state-store.mjs +73 -0
- package/src/toolchain-probe.mjs +21 -0
- package/src/typescript-contract-probe.mjs +5 -0
- package/src/typescript-input-facts.mjs +68 -0
- package/src/typescript-list.mjs +141 -0
- package/src/typescript-qualification.mjs +77 -0
- package/src/windows-command.mjs +16 -0
- package/src/windows-process.mjs +15 -0
- package/src/yarn-workspace-inputs.mjs +148 -0
- package/src/yarn-workspace-typecheck-probe.mjs +107 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Opt-in, bounded local diagnostics. Never include paths, command arguments,
|
|
2
|
+
// environment values, source, or probe output in a timing record.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
5
|
+
import {randomUUID} from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
const file=process.env.REDUE_DECISION_PROFILE_FILE;
|
|
8
|
+
export const profiling=process.env.REDUE_DECISION_PROFILE==='1'&&!!file;
|
|
9
|
+
|
|
10
|
+
const traces=new AsyncLocalStorage();
|
|
11
|
+
const safe=value=>typeof value==='string'&&/^[a-zA-Z0-9_-]{1,64}$/.test(value)?value:undefined;
|
|
12
|
+
const clean=value=>Object.fromEntries(['request','generation','work'].map(key=>[key,safe(value?.[key])]).filter(([,v])=>v));
|
|
13
|
+
let base={};
|
|
14
|
+
try{base=clean(JSON.parse(process.env.REDUE_PROFILE_CONTEXT||'{}'));}catch{}
|
|
15
|
+
export const traceContext=()=>profiling?{...base,...traces.getStore()}:{};
|
|
16
|
+
export function traceBase(value){if(profiling)base=clean(value);}
|
|
17
|
+
export const traceId=()=>randomUUID();
|
|
18
|
+
export const traceScope=(value,fn)=>profiling?traces.run({...traceContext(),...clean(value)},fn):fn();
|
|
19
|
+
export const traceEnvironment=()=>profiling?{...process.env,REDUE_PROFILE_CONTEXT:JSON.stringify(traceContext())}:process.env;
|
|
20
|
+
export function mark(phase,fields={}){timing(phase,performance.now(),fields);}
|
|
21
|
+
|
|
22
|
+
export function timing(phase,started,counts={}) {
|
|
23
|
+
if(!profiling)return;
|
|
24
|
+
const memory=process.memoryUsage();
|
|
25
|
+
try{fs.appendFileSync(file,'REDUE_TIMING '+JSON.stringify({
|
|
26
|
+
at:Date.now(),monoNs:process.hrtime.bigint().toString(),pid:process.pid,phase,...traceContext(),
|
|
27
|
+
ms:Math.round((performance.now()-started)*10)/10,
|
|
28
|
+
rssMiB:Math.round(memory.rss/1048576*10)/10,
|
|
29
|
+
...counts
|
|
30
|
+
})+'\n');}
|
|
31
|
+
catch{/* Diagnostics must never change probe or verification behavior. */}
|
|
32
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Deterministic reuse keys for already-resolved plans. These are not receipts
|
|
2
|
+
// or a second content cache: the existing InputIndex still reads current bytes
|
|
3
|
+
// and membership. A changed key falls back to provider discovery.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {execFileSync} from 'node:child_process';
|
|
7
|
+
import micromatch from './glob.mjs';
|
|
8
|
+
import {sha} from './plan.mjs';
|
|
9
|
+
|
|
10
|
+
export function inputKey(row) {
|
|
11
|
+
return sha(JSON.stringify([row.plan.id,row.files.size,row.sum.toString('hex')]));
|
|
12
|
+
}
|
|
13
|
+
export function planGuard(bundle) {
|
|
14
|
+
const root=bundle.root;
|
|
15
|
+
const paths=new Set(['package.json','package-lock.json','.npmrc','.yarnrc.yml',
|
|
16
|
+
'pnpm-workspace.yaml','.pnpmfile.cjs','.pnpmfile.mjs',
|
|
17
|
+
...bundle.workspaceManifests||[],...bundle.configurationInputs||[],
|
|
18
|
+
...Object.values(bundle.plans).flatMap(plan=>plan.resolutionTriggers||[])]);
|
|
19
|
+
const patterns=bundle.workspacePatterns?.length?bundle.workspacePatterns:
|
|
20
|
+
(()=>{const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json')));
|
|
21
|
+
return Array.isArray(pkg.workspaces)?pkg.workspaces:pkg.workspaces?.packages||[];})();
|
|
22
|
+
if(patterns.length){
|
|
23
|
+
const files=execFileSync('git',['ls-files','--cached','--others','--exclude-standard',
|
|
24
|
+
'-z','--',':(glob)**/package.json'],{cwd:root,
|
|
25
|
+
env:{...process.env,GIT_OPTIONAL_LOCKS:'0'},timeout:5000,maxBuffer:32*1024*1024})
|
|
26
|
+
.toString().split('\0').filter(Boolean);
|
|
27
|
+
for(const file of files)if(micromatch.isMatch(path.posix.dirname(file),patterns,{dot:true}))
|
|
28
|
+
paths.add(file);
|
|
29
|
+
}
|
|
30
|
+
const values=[...paths].sort().map(rel=>{
|
|
31
|
+
const file=path.isAbsolute(rel)?rel:path.join(root,rel);
|
|
32
|
+
try{const st=fs.lstatSync(file,{bigint:true});
|
|
33
|
+
if(st.isSymbolicLink())return [rel,'link',fs.readlinkSync(file)];
|
|
34
|
+
if(st.isDirectory())return [rel,'directory',String(st.dev),String(st.ino)];
|
|
35
|
+
if(!st.isFile())throw Error('unobservable plan dependency');
|
|
36
|
+
return [rel,sha(fs.readFileSync(file))];
|
|
37
|
+
}catch(e){if(e.code==='ENOENT')return [rel,'missing'];throw e;}
|
|
38
|
+
});
|
|
39
|
+
const rootStat=fs.statSync(root,{bigint:true});
|
|
40
|
+
return sha(JSON.stringify([String(rootStat.dev),String(rootStat.ino),values]));
|
|
41
|
+
}
|
|
42
|
+
// Identity of the reviewed compiler tree and the probe's Node executable. A full
|
|
43
|
+
// probe verifies their bytes after this is taken, so any later write changes it.
|
|
44
|
+
export function toolchainKey(tsRoot,executable) {
|
|
45
|
+
const values=[];
|
|
46
|
+
const visit=file=>{const st=fs.lstatSync(file,{bigint:true});
|
|
47
|
+
values.push([file,...['dev','ino','mode','nlink','size','mtimeNs','ctimeNs'].map(key=>String(st[key]))]);
|
|
48
|
+
if(st.isDirectory())for(const name of fs.readdirSync(file).sort())visit(path.join(file,name));};
|
|
49
|
+
visit(tsRoot);visit(fs.realpathSync(executable));
|
|
50
|
+
return sha(JSON.stringify(values));
|
|
51
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// A single-project plan. Declarations are explicit;
|
|
2
|
+
// this provider does not infer complete coverage from a successful command.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {createHash} from 'node:crypto';
|
|
6
|
+
import {permittedRoots,resolveDeclaredInstalled} from './installed-inputs.mjs';
|
|
7
|
+
import {qualifyTypeScript} from './typescript-qualification.mjs';
|
|
8
|
+
import {yarnWorkspaceTypecheckInputs} from './yarn-workspace-inputs.mjs';
|
|
9
|
+
import {pnpmTypecheckInputs} from './pnpm-inputs.mjs';
|
|
10
|
+
import {observedRelative,withinPath,realObservedPath} from './path-identity.mjs';
|
|
11
|
+
import {timing} from './decision-profile.mjs';
|
|
12
|
+
|
|
13
|
+
const identity=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
14
|
+
const safePattern=value=>{
|
|
15
|
+
if(typeof value!=='string'||!value)throw Error('input pattern must be nonempty');
|
|
16
|
+
const raw=value.startsWith('!')?value.slice(1):value;
|
|
17
|
+
if(!raw||path.isAbsolute(raw)||raw.split(/[\\/]/).includes('..'))
|
|
18
|
+
throw Error(`unsafe repository-relative input pattern: ${value}`);
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function discoverDeclared(config) {
|
|
23
|
+
const root=realObservedPath(config.root),plans={},configurationInputs=new Set(),
|
|
24
|
+
workspaceManifests=new Set(),workspacePatterns=new Set();
|
|
25
|
+
if(!Array.isArray(config.checks)||!config.checks.length)throw Error('selected checks required');
|
|
26
|
+
for(const selected of config.checks){
|
|
27
|
+
if(typeof selected.name!=='string'||!selected.name||plans[selected.name])
|
|
28
|
+
throw Error('check names must be unique nonempty strings');
|
|
29
|
+
if(!Array.isArray(selected.command)||!selected.command.length||
|
|
30
|
+
!selected.command.every(item=>typeof item==='string'&&item))
|
|
31
|
+
throw Error(`exact command required for ${selected.name}`);
|
|
32
|
+
const cwd=path.resolve(root,selected.cwd||'.');
|
|
33
|
+
if(!withinPath(cwd,root))throw Error('check cwd escapes checkout');
|
|
34
|
+
for(const key of ['inputs','generatedInputs','installedInputs']){
|
|
35
|
+
if(!Array.isArray(selected[key]||[]))throw Error(`${key} must be a path-pattern array`);
|
|
36
|
+
for(const value of selected[key]||[])safePattern(value);
|
|
37
|
+
}
|
|
38
|
+
if(!selected.inputs?.some(value=>!value.startsWith('!')))
|
|
39
|
+
throw Error(`positive source inputs required for ${selected.name}`);
|
|
40
|
+
for(const argv of selected.probes||[])if(!Array.isArray(argv)||!argv.length||
|
|
41
|
+
!argv.every(value=>typeof value==='string'&&value))
|
|
42
|
+
throw Error('state probes must be exact nonempty argv arrays');
|
|
43
|
+
if(!Array.isArray(selected.environment?.variables||[])||
|
|
44
|
+
!(selected.environment?.variables||[]).every(value=>typeof value==='string'&&value))
|
|
45
|
+
throw Error('environment variables must be a string array');
|
|
46
|
+
if(!Array.isArray(selected.environment?.prefixes||[])||
|
|
47
|
+
!(selected.environment?.prefixes||[]).every(value=>
|
|
48
|
+
typeof value==='string'&&/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)))
|
|
49
|
+
throw Error('environment prefixes must be simple nonempty names');
|
|
50
|
+
if(!Array.isArray(selected.environment?.pathExecutables||[])||
|
|
51
|
+
!(selected.environment?.pathExecutables||[]).every(value=>
|
|
52
|
+
typeof value==='string'&&/^[A-Za-z_][A-Za-z0-9_-]*$/.test(value)))
|
|
53
|
+
throw Error('PATH executables must be simple command names');
|
|
54
|
+
const allowedRoots=permittedRoots(root,selected.allowedExternalRoots||[]);
|
|
55
|
+
let workspace=null,workspaceIssue=null;
|
|
56
|
+
const workspaceStarted=performance.now();
|
|
57
|
+
if(selected.qualification==='yarn-workspace-tsc-v1'){
|
|
58
|
+
try{workspace=yarnWorkspaceTypecheckInputs(root,selected.workspace,
|
|
59
|
+
selected.command[4]);}
|
|
60
|
+
catch(e){workspaceIssue=e.message;}
|
|
61
|
+
}
|
|
62
|
+
if(selected.qualification==='pnpm-tsc-v1'){
|
|
63
|
+
try{workspace=pnpmTypecheckInputs(root,selected.workspace||'.',
|
|
64
|
+
selected.script||selected.command.at(-1));}
|
|
65
|
+
catch(e){workspaceIssue=e.message;}
|
|
66
|
+
}
|
|
67
|
+
timing('plan.workspace_inputs',workspaceStarted,{available:workspace?1:0});
|
|
68
|
+
const installedStarted=performance.now();
|
|
69
|
+
const installed=resolveDeclaredInstalled(root,
|
|
70
|
+
[...(selected.installedInputs||[]),...(workspace?.installed||[])],allowedRoots);
|
|
71
|
+
timing('plan.installed_resolution',installedStarted,{mappings:installed.mappings.length});
|
|
72
|
+
const automatic=qualifyTypeScript(root,selected,
|
|
73
|
+
{workspace,issue:workspaceIssue});
|
|
74
|
+
const sourcePatterns=[...new Set([...selected.inputs,...(workspace?.source||[]),
|
|
75
|
+
...(workspace?.generated||[])])].sort();
|
|
76
|
+
const additivePatterns=[...new Set([...(selected.generatedInputs||[]),
|
|
77
|
+
...installed.internalPatterns])].sort();
|
|
78
|
+
const unresolved=[...installed.unresolved,...(automatic?.issues||[]),
|
|
79
|
+
...(workspace?.limitations||[]),
|
|
80
|
+
...(workspaceIssue?[workspaceIssue]:[])];
|
|
81
|
+
for(const item of workspace?.configurationInputs||[])configurationInputs.add(item);
|
|
82
|
+
for(const item of workspace?.workspacePatterns||[])workspacePatterns.add(item);
|
|
83
|
+
for(const item of workspace?.closure||[])workspaceManifests.add(item.manifest);
|
|
84
|
+
if(!automatic)for(const category of ['source','generated','installedDependencies',
|
|
85
|
+
'environment','toolchain','runtime'])if(!selected.coverage?.[category])
|
|
86
|
+
unresolved.push(selected.coverageReasons?.[category]||
|
|
87
|
+
`${category} coverage not reviewed for this command`);
|
|
88
|
+
if((selected.probes||[]).length&&!automatic&&!selected.coverage?.probeContinuity)
|
|
89
|
+
unresolved.push('probe state continuity during execution not established');
|
|
90
|
+
const installedPhysicalRoots=[...new Set(installed.physicalRoots
|
|
91
|
+
.filter(file=>withinPath(file,root)&&observedRelative(root,file)!=='')
|
|
92
|
+
.map(file=>observedRelative(root,file)))].sort();
|
|
93
|
+
const externalPatterns=installed.externalPatterns;
|
|
94
|
+
const plan={name:selected.name,provider:'declared-project@1',inputResolutionVersion:1,
|
|
95
|
+
root,cwd,selectedConfigHash:identity(selected),command:selected.command,
|
|
96
|
+
target:selected.name,taskCommand:null,taskGraph:[],
|
|
97
|
+
patterns:[...new Set([...sourcePatterns,...additivePatterns])].sort(),
|
|
98
|
+
sourcePatterns,additivePatterns,generated:[],
|
|
99
|
+
declaredGeneratedInputs:[...new Set([...(selected.generatedInputs||[]),
|
|
100
|
+
...(workspace?.generated||[])])].sort(),
|
|
101
|
+
declaredInstalledInputs:[...new Set([...(selected.installedInputs||[]),
|
|
102
|
+
...(workspace?.installed||[])])].sort(),
|
|
103
|
+
installation:workspace?.installation||null,
|
|
104
|
+
...(workspace?.synchronizedInstalledRead?{synchronizedInstalledRead:true}:{}),
|
|
105
|
+
installedInstances:workspace?.installedInstances||[],
|
|
106
|
+
resolutionCandidates:[],resolutionFindings:workspace?.absences||[],
|
|
107
|
+
installedPhysicalRoots,externalPatterns,
|
|
108
|
+
externalObservationRoots:Object.keys(externalPatterns).sort(),
|
|
109
|
+
resolutionLinks:installed.links,
|
|
110
|
+
resolutionTriggers:[...new Set([...installed.triggers,
|
|
111
|
+
...(workspace?.linkTriggers||[]),
|
|
112
|
+
...(workspace?.resolutionCandidates||[]).map(row=>row.path)])],
|
|
113
|
+
installedMappings:installed.mappings,effectiveEnvironmentGlobs:{},
|
|
114
|
+
probes:selected.probes||[],environment:selected.environment||{},
|
|
115
|
+
qualification:selected.qualification||null,
|
|
116
|
+
executionProvenance:'declared-command',
|
|
117
|
+
unresolved:[...new Set(unresolved)].sort()};
|
|
118
|
+
plan.resolutionCandidates=workspace?.resolutionCandidates||[];
|
|
119
|
+
plan.id=identity(plan);plans[selected.name]=plan;
|
|
120
|
+
}
|
|
121
|
+
return {schema:1,provider:'declared-project@1',root,plans,discoveredAt:Date.now(),
|
|
122
|
+
workspaceCount:1,workspacePatterns:[...workspacePatterns],
|
|
123
|
+
configurationInputs:[...configurationInputs].sort(),
|
|
124
|
+
workspaceManifests:[...workspaceManifests].sort()};
|
|
125
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// A caller validates discovery, never an execution outcome. No persistent cache.
|
|
2
|
+
// The direct compiler run records the answers it received (run-recorder.cjs);
|
|
3
|
+
// those answers, not a separate probe, show it consumed the certified inputs.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {createHash} from 'node:crypto';
|
|
7
|
+
import {inflateSync} from 'node:zlib';
|
|
8
|
+
import {fileURLToPath} from 'node:url';
|
|
9
|
+
import {timing} from './decision-profile.mjs';
|
|
10
|
+
import {directCompiler,directContextHash,directEnvironmentIssue,directProbeOutput,directProbeValue} from './direct-typescript.mjs';
|
|
11
|
+
import {withinPath} from './path-identity.mjs';
|
|
12
|
+
const hash=value=>createHash('sha256').update(value).digest('hex');
|
|
13
|
+
export function directRunCertificateAllowed(selected) {
|
|
14
|
+
return ['darwin','linux'].includes(process.platform)&&selected.qualification==='npm-typescript-direct-v1'&&
|
|
15
|
+
selected.probes?.length===1&&
|
|
16
|
+
selected.probes[0][1]===fileURLToPath(new URL('direct-typescript-probe.mjs',import.meta.url))&&
|
|
17
|
+
!(process.env.VSTATE_TEST_FAULTS==='1'&&process.env.VSTATE_TEST_FULL_RUN_PROBES==='1');
|
|
18
|
+
}
|
|
19
|
+
// The start snapshot's discovery certificate, checked without a child process.
|
|
20
|
+
// Returns what a recorded run is compared against, or null for full probes.
|
|
21
|
+
export function directRunCertificate(plan,selected,snapshot,root) {
|
|
22
|
+
const started=performance.now(),c=snapshot?.discoveryCertificate;
|
|
23
|
+
const fallback=reason=>{timing('cli.run_certificate',started,{outcome:'fallback',reason});return null;};
|
|
24
|
+
if(!directRunCertificateAllowed(selected))return fallback('unsupported');
|
|
25
|
+
if(!snapshot?.observationHealthy||!c||c.schema!==2||c.planId!==plan.id||
|
|
26
|
+
c.planId!==snapshot.planId||c.cwd!==plan.cwd||
|
|
27
|
+
c.probeDefinition!==hash(JSON.stringify(selected.probes))||
|
|
28
|
+
![c.output,c.context,c.stderrHash].every(s=>typeof s==='string'&&/^[a-f0-9]{64}$/.test(s))||
|
|
29
|
+
typeof c.queryData!=='string'||c.queryData.length>512*1024)
|
|
30
|
+
return fallback('certificate_unavailable');
|
|
31
|
+
// The compile runs in this process's environment; the daemon's is never substituted.
|
|
32
|
+
if(directEnvironmentIssue())return fallback('environment_unsupported');
|
|
33
|
+
try{
|
|
34
|
+
const argv=selected.probes[0];
|
|
35
|
+
const value=directProbeValue(argv,c.output,c.stderrHash);
|
|
36
|
+
if(value!==snapshot.probeHash)return fallback('probe_identity_changed');
|
|
37
|
+
// Bytes were verified by the daemon's full probe; its toolchain identity
|
|
38
|
+
// check at the start and end snapshots covers later changes.
|
|
39
|
+
const compiler=directCompiler(root,{verify:false});
|
|
40
|
+
if(selected.command[1]!==compiler.entry)return fallback('command_unsupported');
|
|
41
|
+
const queries=JSON.parse(inflateSync(Buffer.from(c.queryData,'base64'),{maxOutputLength:8*1024*1024}));
|
|
42
|
+
if(!Array.isArray(queries)||!queries.length)return fallback('certificate_invalid');
|
|
43
|
+
timing('cli.run_certificate',started,{outcome:'available',queries:queries.length});
|
|
44
|
+
return {value,queries,context:c.context,compiler};
|
|
45
|
+
}catch{return fallback('certificate_invalid');}
|
|
46
|
+
}
|
|
47
|
+
// Without a start certificate, the daemon may offer to build one from this run's
|
|
48
|
+
// recording at the end checkpoint. Returns its token, or null for full probes.
|
|
49
|
+
export function directRunRecording(plan,selected,snapshot,root) {
|
|
50
|
+
const token=snapshot?.recordedCertificate;
|
|
51
|
+
if(!directRunCertificateAllowed(selected)||!snapshot.observationHealthy||
|
|
52
|
+
typeof token!=='string'||!/^[0-9a-f-]{36}$/.test(token)||snapshot.planId!==plan.id||directEnvironmentIssue())
|
|
53
|
+
return null;
|
|
54
|
+
try{if(selected.command[1]!==directCompiler(root,{verify:false}).entry)return null;}catch{return null;}
|
|
55
|
+
return token;
|
|
56
|
+
}
|
|
57
|
+
// Answers derived from a recorded raw listing, exactly as queryAnswer computes them.
|
|
58
|
+
function derived(kind,entries,options) {
|
|
59
|
+
if(entries===null)return null;
|
|
60
|
+
if(kind==='entries')return entries.map(([name,type])=>[name,type==='d'?'d':type==='l'?'l':'f']).sort();
|
|
61
|
+
const resolved=entries.flatMap(([name,type,target])=>type!=='l'?[[name,type]]:target?[[name,target]]:[]);
|
|
62
|
+
if(kind==='directories'){
|
|
63
|
+
if(entries.some(([,type,target])=>type==='l'&&!target))return null;
|
|
64
|
+
return resolved.filter(([,type])=>type==='d').map(([name])=>name).sort();
|
|
65
|
+
}
|
|
66
|
+
const suffixes=options?.extensions?.map(value=>value.toLowerCase());
|
|
67
|
+
return resolved.flatMap(([name,type])=>type==='d'?[[name,'d']]:
|
|
68
|
+
type==='f'&&(!suffixes||suffixes.some(value=>name.toLowerCase().endsWith(value)))?[[name,'f']]:[]).sort();
|
|
69
|
+
}
|
|
70
|
+
// What one certificate fact says about a recorded stat answer: true, false, or
|
|
71
|
+
// null when it says nothing. A parent listing that does not name the path only
|
|
72
|
+
// rules some answers out, so it must agree but does not count as cover.
|
|
73
|
+
function statAgrees([kind,file,answer,options],target,result) {
|
|
74
|
+
if(file===target){
|
|
75
|
+
// queryAnswer reports a missing path as null, not false.
|
|
76
|
+
if(answer===null)return ['fileExists','directoryExists','readFile'].includes(kind)?result===null:null;
|
|
77
|
+
if(kind==='fileExists')return result!==null&&(result==='f')===answer;
|
|
78
|
+
if(kind==='directoryExists')return result!==null&&(result==='d')===answer;
|
|
79
|
+
if(kind==='readFile')return result==='f';
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if(file!==path.dirname(target)||!Array.isArray(answer))return null;
|
|
83
|
+
const name=path.basename(target);
|
|
84
|
+
if(kind==='directories')return answer.includes(name)?result==='d':result==='d'?false:undefined;
|
|
85
|
+
if(kind==='entries'){const entry=answer.find(row=>row[0]===name);
|
|
86
|
+
return !entry?result===null:entry[1]==='l'?null:entry[1]===result;}
|
|
87
|
+
if(kind==='listing'){const entry=answer.find(row=>row[0]===name);
|
|
88
|
+
if(entry)return entry[1]===result;
|
|
89
|
+
const suffixes=options?.extensions?.map(value=>value.toLowerCase());
|
|
90
|
+
const listed=!suffixes||suffixes.some(value=>name.toLowerCase().endsWith(value));
|
|
91
|
+
return result==='d'||listed&&result==='f'?false:undefined;}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const withinCompiler=compiler=>file=>withinPath(file,compiler.tsRoot)||
|
|
95
|
+
file!==compiler.entry&&file.toLowerCase()===compiler.entry.toLowerCase();
|
|
96
|
+
// Null when every answer the run received agrees with the certified state,
|
|
97
|
+
// otherwise why not. The compiler is deterministic given its context and its
|
|
98
|
+
// answers, so a run whose answers all agree computed the certified result.
|
|
99
|
+
// Each answer needs at least one certificate fact about it. Paths inside the
|
|
100
|
+
// compiler tree are covered by the daemon's toolchain identity instead. tsc
|
|
101
|
+
// also stats a case-swapped copy of its own path to detect case sensitivity;
|
|
102
|
+
// only that one stat is exempt.
|
|
103
|
+
export function runRecordIssue(record,certificate) {
|
|
104
|
+
if(record?.schema!==1||!Array.isArray(record.facts))return 'record unavailable';
|
|
105
|
+
if(record.unsupported)return `unsupported ${record.unsupported}`;
|
|
106
|
+
if(record.conflict)return `answer changed during the run: ${record.conflict}`;
|
|
107
|
+
const {compiler,queries}=certificate;
|
|
108
|
+
if(record.main!==compiler.entry||!Array.isArray(record.node)||
|
|
109
|
+
directContextHash(compiler,record.node)!==certificate.context)return 'execution context changed';
|
|
110
|
+
const inCompiler=withinCompiler(compiler);
|
|
111
|
+
const facts=new Map();
|
|
112
|
+
for(const query of queries){if(!facts.has(query[1]))facts.set(query[1],[]);facts.get(query[1]).push(query);}
|
|
113
|
+
for(const [kind,file,answer] of record.facts){
|
|
114
|
+
if(inCompiler(file))continue;
|
|
115
|
+
const known=facts.get(file)||[];
|
|
116
|
+
let checks=[];
|
|
117
|
+
if(kind==='read')checks=known.filter(q=>q[0]==='readFile')
|
|
118
|
+
.map(q=>JSON.stringify(q[2])===JSON.stringify(answer));
|
|
119
|
+
else if(kind==='realpath')checks=known.filter(q=>q[0]==='realpath')
|
|
120
|
+
.map(q=>q[2]===answer);
|
|
121
|
+
else if(kind==='readdir')checks=known.filter(q=>['listing','directories','entries'].includes(q[0]))
|
|
122
|
+
.map(q=>JSON.stringify(derived(q[0],answer,q[3]))===JSON.stringify(q[2]));
|
|
123
|
+
else if(kind==='stat')checks=[...known,...(facts.get(path.dirname(file))||[])]
|
|
124
|
+
.map(q=>statAgrees(q,file,answer)).filter(value=>value!==null);
|
|
125
|
+
else return `unknown answer ${kind}`;
|
|
126
|
+
if(!checks.some(value=>value!==undefined))return `${kind} ${file} is not in the discovery certificate`;
|
|
127
|
+
if(checks.includes(false))return `${kind} ${file} differs from the discovery certificate`;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
// Compares the record run-recorder.cjs wrote to fd 3.
|
|
132
|
+
export function validateRunRecord(text,certificate) {
|
|
133
|
+
const started=performance.now();
|
|
134
|
+
let record=null;
|
|
135
|
+
try{record=JSON.parse(text);}catch{}
|
|
136
|
+
let issue;
|
|
137
|
+
try{issue=runRecordIssue(record,certificate);}catch{issue='record unreadable';}
|
|
138
|
+
timing('cli.run_recording',started,{outcome:issue?'rejected':'validated',
|
|
139
|
+
facts:record?.facts?.length||0,...(issue?{reason:issue}:{})});
|
|
140
|
+
return issue;
|
|
141
|
+
}
|
|
142
|
+
// A certificate for the current inputs, built from a recorded run and the
|
|
143
|
+
// certificate of the last full probe, or {issue}. The run must read the same
|
|
144
|
+
// files, with the same configuration, and every answer it received must be
|
|
145
|
+
// covered by the old questions; only file contents may differ. The program
|
|
146
|
+
// layout is then the same, so the output is rebuilt as the probe computes it.
|
|
147
|
+
// Limit: a file whose role changes (a package.json newly imported as a module)
|
|
148
|
+
// keeps its old role, so a later full probe can report the check STALE.
|
|
149
|
+
export function recordedCertificate(text,cache,compiler) {
|
|
150
|
+
let record=null;
|
|
151
|
+
try{record=JSON.parse(text);}catch{}
|
|
152
|
+
if(record?.schema!==1||!Array.isArray(record.facts))return {issue:'record unavailable'};
|
|
153
|
+
if(!cache?.layout||!cache.queries?.length)return {issue:'no previous layout'};
|
|
154
|
+
const inCompiler=withinCompiler(compiler),reads=new Map();
|
|
155
|
+
for(const [kind,file,answer] of record.facts)if(kind==='read'&&!inCompiler(file)){
|
|
156
|
+
if(answer!==null&&typeof answer!=='string')return {issue:`read ${file} failed`};
|
|
157
|
+
reads.set(file,answer);
|
|
158
|
+
}
|
|
159
|
+
const before=new Map(cache.queries.filter(q=>q[0]==='readFile'&&!inCompiler(q[1])).map(q=>[q[1],q[2]]));
|
|
160
|
+
if(reads.size!==before.size||[...before.keys()].some(file=>!reads.has(file)))
|
|
161
|
+
return {issue:'the run read different files'};
|
|
162
|
+
if(cache.layout.configs.some(([file])=>!reads.has(file)||reads.get(file)!==before.get(file)))
|
|
163
|
+
return {issue:'configuration changed'};
|
|
164
|
+
// Lib files are in the compiler tree: their bytes are covered by its identity,
|
|
165
|
+
// but which of them the program loads must not change.
|
|
166
|
+
const libs=list=>[...new Set(list.filter(file=>inCompiler(file)&&file.endsWith('.d.ts')))].sort();
|
|
167
|
+
if(!Array.isArray(record.compilerReads)||JSON.stringify(libs(record.compilerReads))!==
|
|
168
|
+
JSON.stringify(libs(cache.queries.filter(q=>q[0]==='readFile').map(q=>q[1]))))
|
|
169
|
+
return {issue:'the run loaded different lib files'};
|
|
170
|
+
const changed=new Set([...reads].filter(([file,answer])=>before.get(file)!==answer).map(([file])=>file));
|
|
171
|
+
const queries=cache.queries.map(q=>q[0]==='readFile'&&changed.has(q[1])?[q[0],q[1],reads.get(q[1]),...q.slice(3)]:q);
|
|
172
|
+
const issue=runRecordIssue(record,{compiler,queries,context:cache.context});
|
|
173
|
+
if(issue)return {issue};
|
|
174
|
+
const files=[];
|
|
175
|
+
for(const [file,digest] of cache.layout.files){
|
|
176
|
+
if(!changed.has(file)){files.push([file,digest]);continue;}
|
|
177
|
+
let bytes;
|
|
178
|
+
try{bytes=fs.readFileSync(file);}catch{return {issue:`${file} unreadable`};}
|
|
179
|
+
// The same answer the compiler received (queryAnswer's readFile).
|
|
180
|
+
if(hash(JSON.stringify(bytes.toString('utf8')))!==reads.get(file))return {issue:`${file} changed after the run`};
|
|
181
|
+
files.push([file,hash(bytes)]);
|
|
182
|
+
}
|
|
183
|
+
const layout={...cache.layout,files};
|
|
184
|
+
return {queries,layout,output:directProbeOutput(layout),changed};
|
|
185
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {directProject,directInterpretation,directContextHash,directProbeOutput} from './direct-typescript.mjs';
|
|
3
|
+
import {typeScriptInputFacts} from './typescript-input-facts.mjs';
|
|
4
|
+
import {contextOnly,probeResult} from './probe-result.mjs';
|
|
5
|
+
import {realObservedPath} from './path-identity.mjs';
|
|
6
|
+
import {timing} from './decision-profile.mjs';
|
|
7
|
+
try{
|
|
8
|
+
const [rootArg,script]=process.argv.slice(2),root=realObservedPath(rootArg);
|
|
9
|
+
// Verify all installed implementation bytes before loading compiler API/code,
|
|
10
|
+
// including the optimized context-only path. A version label is insufficient.
|
|
11
|
+
const implementationStarted=performance.now();
|
|
12
|
+
const compiler=directProject(root,script);
|
|
13
|
+
timing('direct_probe.implementation',implementationStarted,{version:compiler.version});
|
|
14
|
+
const context={interpretation:directInterpretation,compiler:compiler.digest,
|
|
15
|
+
entry:compiler.entry,node:[realObservedPath(process.execPath),process.version]};
|
|
16
|
+
const contextHash=directContextHash(compiler,context.node);
|
|
17
|
+
if(!contextOnly(contextHash)){
|
|
18
|
+
const {queries,configs,files}=typeScriptInputFacts(root,compiler.tsRoot,compiler.entry);
|
|
19
|
+
const layout={root,script,configs,files,context};
|
|
20
|
+
probeResult(directProbeOutput(layout),contextHash,queries,layout);
|
|
21
|
+
}
|
|
22
|
+
}catch(e){console.error(`VSTATE_REASON:${e.code&&/^[a-z-]+$/.test(e.code)?e.code:'typescript-contract-unavailable'}`);process.exitCode=2;}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Versioned, explicit offline compiler recipe. No package manager is executed.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {createRequire} from 'node:module';
|
|
6
|
+
import {realObservedPath,withinPath} from './path-identity.mjs';
|
|
7
|
+
export const directInterpretation='npm-direct-typescript@1';
|
|
8
|
+
export const directQualification='npm-typescript-direct-v1';
|
|
9
|
+
const reviewed={
|
|
10
|
+
'5.5.2':{entry:'lib/tsc.js',digest:'953b4844b5f6edc745be844b6c367c227b59029b73c4182f508f90cc47e21816'},
|
|
11
|
+
'5.6.3':{entry:'lib/tsc.js',digest:'e674b5dc4dac50f6e91be30094aeea983a1f340231717e62b0b6199ae3049eb2'},
|
|
12
|
+
'5.9.3':{entry:'lib/_tsc.js',digest:'d1b32e75475710fdc85102b1365254d84ceb192d3639d962c53fe1355ce274ca'},
|
|
13
|
+
};
|
|
14
|
+
const fail=code=>{throw Object.assign(Error(code),{code});};
|
|
15
|
+
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
16
|
+
export const directEnvironment={variables:['NODE_ENV','NODE_OPTIONS','NODE_PATH','NODE_COMPILE_CACHE','VSCODE_INSPECTOR_OPTIONS'],
|
|
17
|
+
prefixes:['NODE_','DYLD_','LD_','TSGO_','TSC_','TS_ETW_'],executableIdentity:true};
|
|
18
|
+
// NODE_OPTIONS flags that only size the V8 heap or pick TLS/DNS behavior; the
|
|
19
|
+
// reviewed compilers open no network connections. Anything else, including any
|
|
20
|
+
// code loader (--require, --import, --loader), is unclassified.
|
|
21
|
+
const inertNodeOptions=[/^--max-(?:old|semi)-space-size=\d+$/,/^--use-(?:openssl|bundled|system)-ca$/,
|
|
22
|
+
/^--dns-result-order=(?:ipv4first|ipv6first|verbatim)$/];
|
|
23
|
+
// One variable's effect on the direct compiler: null when it cannot change what
|
|
24
|
+
// tsc reads or reports. Accepted values stay fingerprinted as NODE_ context.
|
|
25
|
+
export function directVariableIssue(key,value){
|
|
26
|
+
const unsupported='direct-execution-environment-unsupported';
|
|
27
|
+
// The 5.5.2 API can require this unreviewed module during probe loading.
|
|
28
|
+
// Reject even empty/case aliases; do not mutate the caller's environment.
|
|
29
|
+
if(/^TS_ETW_MODULE_PATH$/i.test(key)||/^VSCODE_INSPECTOR_OPTIONS$/i.test(key)&&value)return unsupported;
|
|
30
|
+
if(/^NODE_/i.test(key)){
|
|
31
|
+
// Node 22.13.0 predates env-proxy initialization. This injected flag is
|
|
32
|
+
// inert there, but remains strictly fingerprinted as NODE_ context.
|
|
33
|
+
if(key==='NODE_USE_ENV_PROXY'&&value==='1'&&process.version==='v22.13.0'&&
|
|
34
|
+
!process.allowedNodeEnvironmentFlags.has('--use-env-proxy'))return null;
|
|
35
|
+
if(key==='NODE_ENV'&&['','production','test'].includes(value))return null;
|
|
36
|
+
// PEM data for TLS roots, not code.
|
|
37
|
+
if(key==='NODE_EXTRA_CA_CERTS')return null;
|
|
38
|
+
// Only bare CommonJS requires consult it; the reviewed compilers make one,
|
|
39
|
+
// source-map-support, and only under NODE_ENV=development.
|
|
40
|
+
if(key==='NODE_PATH')return null;
|
|
41
|
+
// No quotes or escapes, so Node splits the value exactly as this does.
|
|
42
|
+
if(key==='NODE_OPTIONS'&&/^[A-Za-z0-9=._ -]*$/.test(value)&&
|
|
43
|
+
value.split(' ').filter(Boolean).every(flag=>inertNodeOptions.some(re=>re.test(flag))))return null;
|
|
44
|
+
if(value!=='')return unsupported;
|
|
45
|
+
}
|
|
46
|
+
if(/^(DYLD_|LD_|TSGO_|TSC_)/i.test(key)&&value!=='')return unsupported;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
export function directEnvironmentIssue(env=process.env){
|
|
50
|
+
if(![22,24].includes(Number(process.versions.node.split('.')[0])))return 'direct-node-version-unsupported';
|
|
51
|
+
for(const [key,value] of Object.entries(env)){const issue=directVariableIssue(key,value);if(issue)return issue;}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
export function directCompiler(root,{verify=true}={}){
|
|
55
|
+
root=realObservedPath(root);
|
|
56
|
+
const manifest=createRequire(path.join(root,'package.json')).resolve('typescript/package.json');
|
|
57
|
+
const tsRoot=path.dirname(realObservedPath(manifest));
|
|
58
|
+
if(!withinPath(tsRoot,root)||tsRoot!==realObservedPath(path.join(root,'node_modules/typescript')))
|
|
59
|
+
fail('direct-typescript-layout-unsupported');
|
|
60
|
+
const version=JSON.parse(fs.readFileSync(manifest)).version,spec=reviewed[version];
|
|
61
|
+
if(!spec)fail('direct-typescript-version-unsupported');
|
|
62
|
+
if(verify){
|
|
63
|
+
const files=[];
|
|
64
|
+
const walk=(dir,relative='')=>{
|
|
65
|
+
for(const entry of fs.readdirSync(dir,{withFileTypes:true})){
|
|
66
|
+
const name=relative?relative+'/'+entry.name:entry.name,full=path.join(dir,entry.name);
|
|
67
|
+
if(entry.isDirectory())walk(full,name);
|
|
68
|
+
else if(entry.isFile())files.push([name,full]);
|
|
69
|
+
else fail('direct-typescript-implementation-unreviewed');
|
|
70
|
+
}
|
|
71
|
+
};walk(tsRoot);
|
|
72
|
+
const digest=createHash('sha256');
|
|
73
|
+
for(const [name,file] of files.sort(([a],[b])=>a<b?-1:a>b?1:0))digest.update(name+'\0'+hash(fs.readFileSync(file))+'\0');
|
|
74
|
+
if(digest.digest('hex')!==spec.digest)fail('direct-typescript-implementation-unreviewed');
|
|
75
|
+
}
|
|
76
|
+
return {tsRoot,version,entry:path.join(tsRoot,spec.entry),digest:spec.digest};
|
|
77
|
+
}
|
|
78
|
+
// The direct probe's output: root, script, configuration and program files with
|
|
79
|
+
// their byte digests, and the execution context. A recorded run rebuilds it from
|
|
80
|
+
// the same layout (direct-run-certificate.mjs).
|
|
81
|
+
export function directProbeOutput({root,script,configs,files,context}){
|
|
82
|
+
const named=list=>list.map(([file,digest])=>[hash(file),digest]);
|
|
83
|
+
return hash(JSON.stringify({root,script,configs:named(configs),files:named(files),...context}));
|
|
84
|
+
}
|
|
85
|
+
// The probe value the daemon stores for one direct probe's checkpoint output.
|
|
86
|
+
export function directProbeValue(argv,output,stderrHash){
|
|
87
|
+
return hash(JSON.stringify([[argv,0,hash(output),stderrHash]]));
|
|
88
|
+
}
|
|
89
|
+
// The execution context a direct probe certifies and a recorded run must match.
|
|
90
|
+
export function directContextHash(compiler,node){
|
|
91
|
+
return hash(JSON.stringify({interpretation:directInterpretation,compiler:compiler.digest,
|
|
92
|
+
entry:compiler.entry,node}));
|
|
93
|
+
}
|
|
94
|
+
export function directProject(root,script,{verify=true}={}){
|
|
95
|
+
const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json')));
|
|
96
|
+
if(pkg.workspaces||pkg.packageManager&&!/^npm@/.test(pkg.packageManager)||
|
|
97
|
+
!['package-lock.json','npm-shrinkwrap.json'].some(file=>fs.existsSync(path.join(root,file)))||
|
|
98
|
+
['yarn.lock','pnpm-lock.yaml','bun.lock','bun.lockb'].some(file=>fs.existsSync(path.join(root,file))))
|
|
99
|
+
fail('direct-npm-project-unsupported');
|
|
100
|
+
if(pkg.scripts?.[script]?.trim()!=='tsc --noEmit'||pkg.scripts?.['pre'+script]||pkg.scripts?.['post'+script])
|
|
101
|
+
fail('typecheck-script-changed');
|
|
102
|
+
const issue=directEnvironmentIssue();if(issue)fail(issue);
|
|
103
|
+
return directCompiler(root,{verify});
|
|
104
|
+
}
|
|
105
|
+
// Provenance only: retain a digest, never proxy URLs/credentials or a raw dump.
|
|
106
|
+
// No npm process runs in this recipe; these keys are not compiler applicability.
|
|
107
|
+
export function observedNpmContext(env=process.env){
|
|
108
|
+
return hash(JSON.stringify(Object.entries(env).filter(([key])=>/^npm_/i.test(key)||/^(?:https?_proxy|all_proxy|no_proxy)$/i.test(key)).sort(([a],[b])=>a<b?-1:a>b?1:0)));
|
|
109
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Resolve the same project-local PATH that will be used for execution. Windows
|
|
5
|
+
// command shims have no extension in package scripts but normally end in .cmd.
|
|
6
|
+
export function findExecutable(name,{platform=process.platform,env=process.env,
|
|
7
|
+
cwd=process.cwd(),paths=path,access=fs.accessSync,realpath=fs.realpathSync}={}) {
|
|
8
|
+
if(!name||paths.basename(name)!==name)return null;
|
|
9
|
+
// An extensionless POSIX shell shim can coexist with npm.cmd on Windows.
|
|
10
|
+
// CreateProcess cannot run the shell shim, so prefer native PATHEXT files.
|
|
11
|
+
const suffixes=platform==='win32'&&!paths.extname(name)?
|
|
12
|
+
[...(env.PATHEXT||'.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean),'']:[''];
|
|
13
|
+
for(const folder of (env.PATH||'').split(paths.delimiter))for(const suffix of suffixes) {
|
|
14
|
+
const candidate=paths.join(folder||cwd,name+suffix);
|
|
15
|
+
try{access(candidate,fs.constants.X_OK);return realpath(candidate);}catch{}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
package/src/glob.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import picomatch from 'picomatch';
|
|
2
|
+
|
|
3
|
+
// These are the exact delegates used by micromatch 4.0.8 matcher/isMatch,
|
|
4
|
+
// with the same pinned picomatch 2.3.2. REDUE does not use brace expansion;
|
|
5
|
+
// importing the smaller dependency avoids shipping its unused vulnerable parser.
|
|
6
|
+
export default {
|
|
7
|
+
matcher(pattern,options){return picomatch(pattern,options);},
|
|
8
|
+
isMatch(file,patterns,options){return picomatch(patterns,options)(file);}
|
|
9
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Native history is an optional acceleration. Keep its operations off the
|
|
2
|
+
// control thread so a wedged native call cannot block status or shutdown.
|
|
3
|
+
// Each helper serves one query; the observer starts it ahead of that query.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import {mark} from './decision-profile.mjs';
|
|
6
|
+
mark('history.worker_loaded');
|
|
7
|
+
import watcher from '@parcel/watcher';
|
|
8
|
+
|
|
9
|
+
process.on('disconnect',()=>process.exit(0));
|
|
10
|
+
process.once('message',async({root,previous,next,backend,faultFile})=>{
|
|
11
|
+
try {
|
|
12
|
+
let fault;
|
|
13
|
+
try {fault=faultFile&&fs.readFileSync(faultFile,'utf8').trim();}
|
|
14
|
+
catch(e){if(e.code!=='ENOENT')throw e;}
|
|
15
|
+
if(fault==='hang')Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
|
|
16
|
+
if(fault==='fail')throw Error('injected history failure');
|
|
17
|
+
mark('history.snapshot_start');
|
|
18
|
+
await watcher.writeSnapshot(root,next,{backend});
|
|
19
|
+
mark('history.snapshot_complete');
|
|
20
|
+
const changes=previous?await watcher.getEventsSince(root,previous,{backend}):[];
|
|
21
|
+
mark('history.events_complete',{events:changes.length});
|
|
22
|
+
process.send({ok:true,changes});
|
|
23
|
+
} catch(error) {
|
|
24
|
+
process.send({ok:false,error:error.message,code:error.code||error.name});
|
|
25
|
+
}
|
|
26
|
+
});
|