@redue/cli 0.1.0-alpha.21 → 0.1.0-alpha.23
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 +67 -0
- package/README.md +117 -200
- package/bin/redue.mjs +15 -1
- package/docs/user/agents.md +12 -4
- package/docs/user/direct-typescript.md +3 -1
- package/docs/user/getting-started.md +20 -1
- package/docs/user/node-script.md +23 -4
- package/docs/user/supported-projects.md +5 -1
- package/docs/user/tests-direct.md +8 -1
- package/package.json +1 -1
- package/src/agent-integration.mjs +89 -16
- package/src/claude-hook.mjs +23 -6
- package/src/cli.mjs +12 -6
- package/src/node-script.mjs +24 -0
- package/src/pnpm-inputs.mjs +4 -2
- package/src/setup.mjs +178 -0
- package/src/test-direct.mjs +39 -7
- package/src/test-recorder.cjs +31 -2
- package/src/typescript-list.mjs +8 -0
- package/src/yarn-workspace-inputs.mjs +3 -0
package/src/test-recorder.cjs
CHANGED
|
@@ -15,7 +15,7 @@ if(configFile)install();
|
|
|
15
15
|
function install(){
|
|
16
16
|
const original={openSync:fs.openSync,writeSync:fs.writeSync,readFileSync:fs.readFileSync,
|
|
17
17
|
statSync:fs.statSync,readdirSync:fs.readdirSync,real:fs.realpathSync.native};
|
|
18
|
-
const {threadId}=require('node:worker_threads');
|
|
18
|
+
const {threadId,isInternalThread}=require('node:worker_threads');
|
|
19
19
|
let config,fd;
|
|
20
20
|
try{config=JSON.parse(original.readFileSync(configFile,'utf8'));
|
|
21
21
|
fd=original.openSync(path.join(config.dir,`${process.pid}-${threadId}.jsonl`),'a',0o600);}
|
|
@@ -25,7 +25,10 @@ function install(){
|
|
|
25
25
|
const failed=reason=>emit(['U',reason]);
|
|
26
26
|
// The main script tells the guard which program a process runs (a package
|
|
27
27
|
// manager may start helpers of its own).
|
|
28
|
-
|
|
28
|
+
// Node's own threads (the ESM hooks thread) load it too; the guard does not
|
|
29
|
+
// count them as threads the run started.
|
|
30
|
+
emit(['H',{pid:process.pid,ppid:process.ppid,threadId,main:typeof process.argv[1]==='string'?process.argv[1]:null,
|
|
31
|
+
...(isInternalThread===true?{internal:true}:{})}]);
|
|
29
32
|
// Every node_modules observed as installed: the root's and each workspace package's.
|
|
30
33
|
const sep=path.sep,root=config.root,installed=Array.isArray(config.installed)?config.installed:[path.join(root,'node_modules')],
|
|
31
34
|
tmp=config.tmp;
|
|
@@ -395,6 +398,32 @@ function install(){
|
|
|
395
398
|
catch{failed('recorder failure');}}
|
|
396
399
|
};
|
|
397
400
|
}
|
|
401
|
+
// process.execve replaces this process with another program, under the
|
|
402
|
+
// same PID: the row comes first, since a successful call does not return
|
|
403
|
+
// (a failed one aborts the process). Node refuses it in a worker and for
|
|
404
|
+
// bad arguments by throwing; the row is then withdrawn. The kernel does not
|
|
405
|
+
// search PATH, so the file is taken from the working directory. A Node
|
|
406
|
+
// program that replaces this one loads the recorder again and appends a
|
|
407
|
+
// header to the same record.
|
|
408
|
+
if(typeof process.execve==='function'){const execve=process.execve;
|
|
409
|
+
process.execve=function(file,args,env,...rest){
|
|
410
|
+
let row=false;
|
|
411
|
+
if(threadId===0)try{const options={env:env??process.env},full=path.resolve(String(file)),where_=where(options);
|
|
412
|
+
emit(['X',full,argsOf(Array.isArray(args)?args.slice(1):[]),...where_,scriptOf(full,where_[0],null)]);row=true;}
|
|
413
|
+
catch{failed('recorder failure');}
|
|
414
|
+
try{return execve.call(this,file,args,env,...rest);}
|
|
415
|
+
catch(error){if(row)emit(['Y']);throw error;}};}
|
|
416
|
+
// Bindings that read files, use the network or start programs out of the
|
|
417
|
+
// recorder's sight. Only bindings without such access are allowed.
|
|
418
|
+
const harmlessBindings=new Set(['async_wrap','buffer','config','constants','contextify','crypto','http_parser','icu',
|
|
419
|
+
'js_stream','natives','os','signal_wrap','stream_wrap','tty_wrap','url','util','uv','v8','zlib']);
|
|
420
|
+
if(typeof process.binding==='function'){const binding=process.binding;
|
|
421
|
+
process.binding=function(name,...rest){
|
|
422
|
+
const result=binding.call(this,name,...rest);
|
|
423
|
+
if(!harmlessBindings.has(name))failed(`process.binding('${String(name)}') reads files, uses the network or starts programs out of REDUE's sight`);
|
|
424
|
+
return result;};}
|
|
425
|
+
// Node's internal modules include child_process's unhooked internals.
|
|
426
|
+
if(process.execArgv.some(arg=>/^--expose[-_]internals(=|$)/.test(arg)))failed('a Node process exposed its internal modules (--expose-internals)');
|
|
398
427
|
const workers=require('node:worker_threads'),Worker=workers.Worker;
|
|
399
428
|
workers.Worker=class extends Worker{
|
|
400
429
|
constructor(...args){super(...args);try{emit(['T',this.threadId]);}catch{failed('recorder failure');}}
|
package/src/typescript-list.mjs
CHANGED
|
@@ -63,6 +63,14 @@ export function queriesMatch(queries) {
|
|
|
63
63
|
return queries.every(([kind,file,answer,options])=>
|
|
64
64
|
JSON.stringify(queryAnswer(kind,file,options))===JSON.stringify(answer));
|
|
65
65
|
}
|
|
66
|
+
// Why a recipe built on TypeScript's compiler API cannot use the TypeScript
|
|
67
|
+
// installed at `tsRoot`, or null: TypeScript 7 is a native compiler and
|
|
68
|
+
// ships no such API.
|
|
69
|
+
export function compilerApiIssue(tsRoot) {
|
|
70
|
+
let version=null;try{version=JSON.parse(fs.readFileSync(path.join(tsRoot,'package.json'),'utf8')).version;}catch{}
|
|
71
|
+
return Number(String(version).split('.')[0])>=7?
|
|
72
|
+
`TypeScript ${version} is a native compiler without the compiler API this recipe reads; REDUE does not support TypeScript 7 yet`:null;
|
|
73
|
+
}
|
|
66
74
|
export function compilerFiles(tsRoot,cwd,config='tsconfig.json') {
|
|
67
75
|
const require=createRequire(import.meta.url),ts=require(tsRoot);
|
|
68
76
|
if(typeof ts.createProgram!=='function'||typeof ts.createCompilerHost!=='function')return null;
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {execFileSync} from 'node:child_process';
|
|
4
4
|
import {createRequire} from 'node:module';
|
|
5
|
+
import {compilerApiIssue} from './typescript-list.mjs';
|
|
5
6
|
import micromatch from './glob.mjs';
|
|
6
7
|
import {withinPath as within,realObservedPath} from './path-identity.mjs';
|
|
7
8
|
|
|
@@ -58,6 +59,8 @@ export function yarnWorkspacePackages(root){
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function parsedTypeScript(root,dir){
|
|
62
|
+
const apiIssue=compilerApiIssue(path.join(root,'node_modules','typescript'));
|
|
63
|
+
if(apiIssue)throw Error(apiIssue);
|
|
61
64
|
const require=createRequire(path.join(root,'package.json'));
|
|
62
65
|
const ts=require(path.join(root,'node_modules','typescript'));
|
|
63
66
|
const config=path.join(root,dir,'tsconfig.json'),reads=new Set();
|