@redue/cli 0.1.0-alpha.10 → 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 +53 -0
- package/README.md +1 -1
- package/bin/redue.mjs +35 -19
- package/docs/user/agents.md +47 -1
- package/docs/user/direct-typescript.md +18 -0
- package/docs/user/pilot.md +9 -6
- package/package.json +1 -1
- package/src/agent-integration.mjs +59 -13
- package/src/claude-hook.mjs +115 -0
- package/src/cli.mjs +51 -11
- package/src/daemon.mjs +236 -82
- package/src/decision-validation.mjs +10 -0
- package/src/direct-run-certificate.mjs +161 -16
- package/src/direct-typescript-probe.mjs +6 -40
- package/src/direct-typescript.mjs +48 -15
- package/src/history-worker.mjs +19 -16
- package/src/observation.mjs +32 -12
- package/src/probe-result.mjs +7 -3
- package/src/run-recorder.cjs +78 -0
- package/src/typescript-input-facts.mjs +5 -2
- package/src/typescript-list.mjs +76 -7
package/src/typescript-list.mjs
CHANGED
|
@@ -14,13 +14,27 @@ export function queryAnswer(kind,file,options) {
|
|
|
14
14
|
if(kind==='readFile')return hash(fs.readFileSync(file,'utf8'));
|
|
15
15
|
if(kind==='fileExists')return fs.statSync(file).isFile();
|
|
16
16
|
if(kind==='directoryExists')return fs.statSync(file).isDirectory();
|
|
17
|
-
|
|
17
|
+
// The compiler resolves through realpathSync.native outside Windows.
|
|
18
|
+
if(kind==='realpath')return process.platform==='win32'?fs.realpathSync(file):fs.realpathSync.native(file);
|
|
18
19
|
if(kind==='directories')return fs.readdirSync(file,{withFileTypes:true})
|
|
19
20
|
.filter(entry=>entry.isDirectory()||entry.isSymbolicLink()&&
|
|
20
21
|
fs.statSync(path.join(file,entry.name)).isDirectory()).map(entry=>entry.name).sort();
|
|
21
22
|
// Directory entries include names/types, including newly created candidates.
|
|
22
23
|
if(kind==='entries')return fs.readdirSync(file,{withFileTypes:true})
|
|
23
24
|
.map(entry=>[entry.name,entry.isDirectory()?'d':entry.isSymbolicLink()?'l':'f']).sort();
|
|
25
|
+
// A compiler directory listing: subdirectories and files with a requested
|
|
26
|
+
// extension, classifying links by their targets as matchFiles does.
|
|
27
|
+
if(kind==='listing'){
|
|
28
|
+
const suffixes=options?.extensions?.map(value=>value.toLowerCase());
|
|
29
|
+
return fs.readdirSync(file,{withFileTypes:true}).flatMap(entry=>{
|
|
30
|
+
let type=entry;
|
|
31
|
+
if(entry.isSymbolicLink())try{type=fs.statSync(path.join(file,entry.name));}catch{return [];}
|
|
32
|
+
if(type.isDirectory())return [[entry.name,'d']];
|
|
33
|
+
if(type.isFile()&&(!suffixes||suffixes.some(value=>entry.name.toLowerCase().endsWith(value))))
|
|
34
|
+
return [[entry.name,'f']];
|
|
35
|
+
return [];
|
|
36
|
+
}).sort();
|
|
37
|
+
}
|
|
24
38
|
throw Error('unknown compiler filesystem question');
|
|
25
39
|
}catch(error){if(['ENOENT','ENOTDIR'].includes(error.code))return null;throw error;}
|
|
26
40
|
}
|
|
@@ -33,20 +47,47 @@ export function compilerFiles(tsRoot,cwd) {
|
|
|
33
47
|
const require=createRequire(import.meta.url),ts=require(tsRoot);
|
|
34
48
|
if(typeof ts.createProgram!=='function'||typeof ts.createCompilerHost!=='function')return null;
|
|
35
49
|
const queries=new Map();
|
|
36
|
-
const remember=(kind,file)=>{
|
|
37
|
-
const full=path.resolve(file),answer=queryAnswer(kind,full);
|
|
38
|
-
queries.set(kind+'\0'+full,
|
|
50
|
+
const remember=(kind,file,options)=>{
|
|
51
|
+
const full=path.resolve(file),answer=queryAnswer(kind,full,options);
|
|
52
|
+
queries.set(kind+'\0'+full+(options?JSON.stringify(options):''),
|
|
53
|
+
options?[kind,full,answer,options]:[kind,full,answer]);return answer;
|
|
39
54
|
};
|
|
40
55
|
const readFile=file=>{remember('readFile',file);return ts.sys.readFile(file);};
|
|
41
56
|
const fileExists=file=>{remember('fileExists',file);return ts.sys.fileExists(file);};
|
|
42
57
|
const directoryExists=file=>{remember('directoryExists',file);return ts.sys.directoryExists(file);};
|
|
43
58
|
const getDirectories=file=>{remember('directories',file);return ts.sys.getDirectories(file);};
|
|
44
59
|
const readDirectory=(dir,...args)=>{
|
|
45
|
-
|
|
60
|
+
// Reviewed compilers enumerate each visited directory through fs.readdirSync.
|
|
61
|
+
// Record those listings as ordinary questions so a validator without the
|
|
62
|
+
// compiler API can replay this answer; the listing itself stays authoritative.
|
|
63
|
+
const listed=[],readdirSync=fs.readdirSync;
|
|
64
|
+
fs.readdirSync=function(file,...rest){listed.push(path.resolve(String(file)));
|
|
65
|
+
return readdirSync.call(this,file,...rest);};
|
|
66
|
+
let result;try{result=ts.sys.readDirectory(dir,...args);}finally{fs.readdirSync=readdirSync;}
|
|
46
67
|
// JSON turns undefined array entries into null. TypeScript distinguishes
|
|
47
68
|
// them (an omitted exclude list is not a null list), so retain that fact.
|
|
48
69
|
const full=path.resolve(dir),options={tsRoot,args,argsVersion:1,
|
|
49
70
|
undefinedArguments:args.flatMap((value,i)=>value===undefined?[i]:[])};
|
|
71
|
+
const visited=new Set(listed);
|
|
72
|
+
if(typeof ts.getFileMatcherPatterns==='function'&&
|
|
73
|
+
result.every(file=>visited.has(path.dirname(path.resolve(file)))))try{
|
|
74
|
+
// Base paths and symlinked directories reach matchFiles through realpath
|
|
75
|
+
// deduplication without a listing; a retargeted link changes this answer.
|
|
76
|
+
for(const base of ts.getFileMatcherPatterns(full,args[1],args[2],
|
|
77
|
+
ts.sys.useCaseSensitiveFileNames,process.cwd()).basePaths)remember('realpath',base);
|
|
78
|
+
const extensions=Array.isArray(args[0])?args[0]:null;
|
|
79
|
+
for(const directory of visited){
|
|
80
|
+
// matchFiles also resolves every visited directory to detect cycles.
|
|
81
|
+
remember('realpath',directory);
|
|
82
|
+
remember('listing',directory,{extensions});
|
|
83
|
+
let entries=[];
|
|
84
|
+
try{entries=fs.readdirSync(directory,{withFileTypes:true});}
|
|
85
|
+
catch(error){if(!['ENOENT','ENOTDIR'].includes(error.code))throw error;}
|
|
86
|
+
for(const entry of entries)
|
|
87
|
+
if(entry.isSymbolicLink())remember('realpath',path.join(directory,entry.name));
|
|
88
|
+
}
|
|
89
|
+
options.covered=1;
|
|
90
|
+
}catch{/* uncovered listings are replayed only through the compiler API */}
|
|
50
91
|
queries.set('readDirectory\0'+full+JSON.stringify(args),
|
|
51
92
|
['readDirectory',full,[...result].sort(),options]);
|
|
52
93
|
return result;
|
|
@@ -56,7 +97,7 @@ export function compilerFiles(tsRoot,cwd) {
|
|
|
56
97
|
onUnRecoverableConfigFileDiagnostic(){}});
|
|
57
98
|
if(!parsed||parsed.errors?.length)throw Error('TypeScript configuration unavailable');
|
|
58
99
|
const host=ts.createCompilerHost(parsed.options);
|
|
59
|
-
Object.assign(host,{readFile,fileExists,directoryExists,getDirectories,
|
|
100
|
+
Object.assign(host,{readFile,fileExists,directoryExists,getDirectories,readDirectory,
|
|
60
101
|
realpath(file){remember('realpath',file);return ts.sys.realpath(file);},
|
|
61
102
|
getSourceFile(file,languageVersion,onError){
|
|
62
103
|
const text=readFile(file);return text===undefined?undefined:
|
|
@@ -68,5 +109,33 @@ export function compilerFiles(tsRoot,cwd) {
|
|
|
68
109
|
// Stable answers are necessary even at first capture; a concurrent edit
|
|
69
110
|
// cannot become the basis for a reusable discovery result.
|
|
70
111
|
const facts=[...queries.values()];
|
|
71
|
-
|
|
112
|
+
// A certificate must cover every program file; references parse further
|
|
113
|
+
// configurations outside this host and are not certified.
|
|
114
|
+
const complete=!parsed.projectReferences?.length&&files.every(file=>queries.has('readFile\0'+file));
|
|
115
|
+
return {files,queries:complete&&queriesMatch(facts)?facts:null};
|
|
116
|
+
}
|
|
117
|
+
// Identity of a read file and, for a link, of its target. Writes through any
|
|
118
|
+
// alias change ctime. A file changed within the last 2 s has no identity, so a
|
|
119
|
+
// coarse timestamp cannot hide a second write in the same tick (git's racy rule).
|
|
120
|
+
export function fileIdentity(file,now=Date.now()) {
|
|
121
|
+
try{const link=fs.lstatSync(file,{bigint:true}),target=link.isSymbolicLink()?fs.statSync(file,{bigint:true}):link;
|
|
122
|
+
if(Number(link.ctimeNs/1000000n)>=now-2000||Number(target.ctimeNs/1000000n)>=now-2000)return null;
|
|
123
|
+
const fields=st=>['dev','ino','mode','nlink','size','mtimeNs','ctimeNs'].map(key=>String(st[key]));
|
|
124
|
+
return JSON.stringify([fields(target),target===link?null:[fs.realpathSync(file),...fields(link)]]);}
|
|
125
|
+
catch(error){if(['ENOENT','ENOTDIR'].includes(error.code))return null;throw error;}
|
|
126
|
+
}
|
|
127
|
+
// In-process replay of a certificate. Covered directory listings are checked
|
|
128
|
+
// through their recorded listings instead of loading the compiler API. A read
|
|
129
|
+
// file is re-hashed until a match records its identity, taken before the read,
|
|
130
|
+
// and again whenever that identity changes.
|
|
131
|
+
export function queriesHold(queries,identities=new Map()) {
|
|
132
|
+
if(!queries?.length)return false;
|
|
133
|
+
return queries.every(([kind,file,answer,options])=>{
|
|
134
|
+
if(kind==='readDirectory')return options?.covered===1;
|
|
135
|
+
const identity=kind==='readFile'?fileIdentity(file):null;
|
|
136
|
+
if(identity&&identities.get(file)===identity)return true;
|
|
137
|
+
const held=JSON.stringify(queryAnswer(kind,file,options))===JSON.stringify(answer);
|
|
138
|
+
if(kind==='readFile'){if(held&&identity)identities.set(file,identity);else identities.delete(file);}
|
|
139
|
+
return held;
|
|
140
|
+
});
|
|
72
141
|
}
|