@redue/cli 0.1.0-alpha.20 → 0.1.0-alpha.21
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 +24 -0
- package/bin/redue.mjs +2 -1
- package/docs/user/node-script.md +7 -6
- package/docs/user/supported-projects.md +7 -2
- package/package.json +1 -1
- package/src/node-onboarding.mjs +16 -6
- package/src/node-script.mjs +11 -9
- package/src/workspace-manifests.mjs +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.21 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
`init` finds more typecheck scripts and no longer stops in large monorepos.
|
|
6
|
+
In the census of 108 public TypeScript repos, the general recipe qualifies 26
|
|
7
|
+
typecheck scripts (was 23) and 20 lint scripts.
|
|
8
|
+
|
|
9
|
+
- `init` prefers typecheck scripts named `tsc`, `types` or `lint:types`, then
|
|
10
|
+
takes one script named under a typecheck name (`typecheck:test`,
|
|
11
|
+
`tsc:build`, ...), watch scripts aside. Two such scripts are reported as
|
|
12
|
+
ambiguous unless a command alone decides. Scripts named `build` or `dev`
|
|
13
|
+
that run tsc are not taken: they write output.
|
|
14
|
+
- A workspace of more than 50 packages no longer stops `init`. It reads up to
|
|
15
|
+
1000 packages, proposes the root's checks, and past 50 packages skips
|
|
16
|
+
per-package checks and says so (`skipped` in `--json`, a "Skipped:" line
|
|
17
|
+
otherwise). Five census repos got no check at all before.
|
|
18
|
+
- General recipe: pnpm 12 writes its workspace state through a temporary
|
|
19
|
+
`node_modules/.tmpXXXXXX` file. Like the state file, it counts as the run's
|
|
20
|
+
output when the pnpm executable ran. Known limit: the agent eval's seed lint
|
|
21
|
+
run on one pnpm 12 project is still UNVERIFIED for an input event REDUE does
|
|
22
|
+
not yet name.
|
|
23
|
+
|
|
24
|
+
- Contributors: the agent eval records each run's seed and final receipt,
|
|
25
|
+
and the files a session changed when its seed run is not stable.
|
|
26
|
+
|
|
3
27
|
## 0.1.0-alpha.20 — 2026-10-24
|
|
4
28
|
|
|
5
29
|
A fix for agents on macOS.
|
package/bin/redue.mjs
CHANGED
|
@@ -120,7 +120,7 @@ if(command==='init'){
|
|
|
120
120
|
package_manager:discovery.manager.name,version:discovery.manager.version,
|
|
121
121
|
installation_layout:discovery.manager.layout,
|
|
122
122
|
issue:discovery.observationIssue||discovery.manager.issue,workspaces:discovery.workspaces,
|
|
123
|
-
ambiguous:discovery.ambiguous,
|
|
123
|
+
ambiguous:discovery.ambiguous,skipped:discovery.skipped||[],
|
|
124
124
|
checks:discovery.checks.map(({config:check,level,reason})=>({name:check.name,
|
|
125
125
|
script:check.script||null,command:check.command||null,
|
|
126
126
|
workspace:check.workspace||null,level,reason})),config:configFile,
|
|
@@ -139,6 +139,7 @@ if(command==='init'){
|
|
|
139
139
|
}
|
|
140
140
|
for(const row of summary.ambiguous)
|
|
141
141
|
console.log(` ${row.kind}: ambiguous scripts (${row.scripts.join(', ')}); choose one explicitly`);
|
|
142
|
+
for(const row of summary.skipped)console.log(` Skipped: ${row.reason}`);
|
|
142
143
|
if(!summary.checks.length)console.log('No useful existing verification scripts found.');
|
|
143
144
|
console.log(configured?`Existing ${configFile} left unchanged.`:
|
|
144
145
|
dryRun?`Preview only; ${configFile} was not written.`:
|
package/docs/user/node-script.md
CHANGED
|
@@ -67,13 +67,14 @@ UNVERIFIED. At the top of each `node_modules`, only `.cache`, `.vite`,
|
|
|
67
67
|
`.vite-temp`, `.vitest` and `.vitest-cache` are left out, and `*.tsbuildinfo`
|
|
68
68
|
anywhere in it.
|
|
69
69
|
|
|
70
|
-
Two installed files are rewritten by tools during a run
|
|
70
|
+
Two installed files are rewritten by tools during a run. pnpm rewrites
|
|
71
71
|
`node_modules/.pnpm-workspace-state-v1.json` at the first run after an
|
|
72
|
-
install,
|
|
73
|
-
before it reads them.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
install, through a temporary `node_modules/.tmpXXXXXX` file. vue-tsc writes
|
|
73
|
+
`node_modules/.vue-global-types/*.d.ts` before it reads them. These files stay
|
|
74
|
+
inputs. When they are the only files a run changed, and the run wrote them,
|
|
75
|
+
the result is kept with the files as the run left them. A recorded process
|
|
76
|
+
counts as writing a file it wrote; for pnpm's files, the pnpm executable must
|
|
77
|
+
have run. A later change to them makes the result STALE.
|
|
77
78
|
|
|
78
79
|
It does not mean a rerun would give the same result: a test that depends on
|
|
79
80
|
the time or on randomness can pass now and fail later on the same inputs.
|
|
@@ -12,7 +12,10 @@ is known. When npm is not pinned in `package.json`,
|
|
|
12
12
|
leaves the version unknown rather than guessing if that inspection fails.
|
|
13
13
|
|
|
14
14
|
At the repository root, `init` looks for existing typecheck, test, lint, and
|
|
15
|
-
build scripts. It prefers familiar script names
|
|
15
|
+
build scripts. It prefers familiar script names (for typecheck: `typecheck`,
|
|
16
|
+
`type-check`, `check-types`, `check:types`, `types:check`, `tsc`, `types`,
|
|
17
|
+
`lint:types`), then for typecheck one script named under such a name
|
|
18
|
+
(`typecheck:test`, `tsc:build`; not a watch script), then unique recognizable
|
|
16
19
|
`tsc`, Vitest/Jest, ESLint, or Vite commands under other names. Multiple
|
|
17
20
|
matches are reported as ambiguous rather than guessed. It never creates a new
|
|
18
21
|
project script. Without `--recipe`, `init` tries the strict recipes below first,
|
|
@@ -30,7 +33,9 @@ not npm. Full installed compiler identity, config/membership, installed inputs
|
|
|
30
33
|
and relevant Node environment are checked before reuse. See the
|
|
31
34
|
[exact direct boundary](direct-typescript.md). Unsupported plugins, preloads,
|
|
32
35
|
configuration or outside-root inputs remain UNVERIFIED. Root and package-level npm/Yarn workspace scripts are discovered from
|
|
33
|
-
`package.json` workspace declarations.
|
|
36
|
+
`package.json` workspace declarations. In a workspace of more than 50
|
|
37
|
+
packages, `init` proposes the root's checks only and names what it skipped;
|
|
38
|
+
add package checks to the config yourself. Up to 1000 packages are read. Pinned Yarn 4 `node-modules`/classic
|
|
34
39
|
workspaces with a standalone `tsc -p .` script, local TypeScript, a conventional
|
|
35
40
|
`src` include, and Node module resolution can qualify automatically. For one
|
|
36
41
|
workspace, use `redue init --workspace NAME --check typecheck`; it writes only
|
package/package.json
CHANGED
package/src/node-onboarding.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import {commandLineIssue,scriptCheckInputs,scriptPath,scriptProject,scriptQualif
|
|
|
4
4
|
// Init proposes the general recipe for typecheck and lint scripts; test
|
|
5
5
|
// scripts use the direct test recipe (Vitest, Jest).
|
|
6
6
|
const generalKinds=['typecheck','lint'];
|
|
7
|
+
const perPackageLimit=50;
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import {spawnSync} from 'node:child_process';
|
|
@@ -17,7 +18,7 @@ import {partName,tscRuns} from './typescript-invocation.mjs';
|
|
|
17
18
|
const exists=file=>{try{fs.accessSync(file);return true;}catch{return false;}};
|
|
18
19
|
const readJson=file=>JSON.parse(fs.readFileSync(file,'utf8'));
|
|
19
20
|
const scriptNames={
|
|
20
|
-
typecheck:['typecheck','type-check','check:types','types:check','check-types'],
|
|
21
|
+
typecheck:['typecheck','type-check','check:types','types:check','check-types','tsc','types','lint:types'],
|
|
21
22
|
test:['test','test:unit','test:check'],
|
|
22
23
|
lint:['lint','lint:check'],
|
|
23
24
|
build:['build'],
|
|
@@ -160,10 +161,16 @@ function classify(kind,scriptName,script,pkg,managerInfo,root) {
|
|
|
160
161
|
'Vite detected; build environment and generated inputs need review':
|
|
161
162
|
'build script detected; generated and runtime inputs need review'};
|
|
162
163
|
}
|
|
164
|
+
// Typecheck scripts named under a typecheck name (`typecheck:test`,
|
|
165
|
+
// `tsc:build`), watch scripts aside.
|
|
166
|
+
const typecheckFamily=/^(?:typecheck|type-check|check-types|check:types|types:check|tsc):(?!.*watch)/;
|
|
163
167
|
function selectScript(kind,names,scripts) {
|
|
164
168
|
const preferred=names.find(name=>typeof scripts[name]==='string'&&
|
|
165
169
|
scripts[name].trim()&&!/^echo\s+["']?Error: no test specified/.test(scripts[name]));
|
|
166
170
|
if(preferred)return {script:preferred};
|
|
171
|
+
const family=kind==='typecheck'?Object.keys(scripts).filter(name=>typecheckFamily.test(name)&&
|
|
172
|
+
typeof scripts[name]==='string'&&scripts[name].trim()&&!/(?:^|\s)(?:--watch|-w)(?:\s|$)/.test(scripts[name])):[];
|
|
173
|
+
if(family.length===1)return {script:family[0]};
|
|
167
174
|
const candidates=Object.keys(scripts).filter(name=>{
|
|
168
175
|
const value=scripts[name]?.trim();
|
|
169
176
|
if(typeof value!=='string')return false;
|
|
@@ -177,7 +184,7 @@ function selectScript(kind,names,scripts) {
|
|
|
177
184
|
const exact=kind==='typecheck'?candidates.filter(name=>exactTypeScript(scripts[name])):[];
|
|
178
185
|
if(exact.length===1)return {script:exact[0]};
|
|
179
186
|
return candidates.length===1?{script:candidates[0]}:
|
|
180
|
-
candidates.length>1?{ambiguous:candidates}:{};
|
|
187
|
+
candidates.length>1?{ambiguous:candidates}:family.length>1?{ambiguous:family}:{};
|
|
181
188
|
}
|
|
182
189
|
// A pnpm workspace package's TypeScript run, as its check's command, cwd and inputs.
|
|
183
190
|
function pnpmCheck(root,name,dir,manifest,run){
|
|
@@ -233,7 +240,7 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
233
240
|
workspacePatterns.some(value=>typeof value!=='string'||path.isAbsolute(value)||
|
|
234
241
|
value.split('/').includes('..'))))
|
|
235
242
|
throw Error('workspace patterns need a supported project-relative package list');
|
|
236
|
-
const checks=[],ambiguous=[];
|
|
243
|
+
const checks=[],ambiguous=[],skipped=[];
|
|
237
244
|
for(const [kind,names] of Object.entries(scriptNames)) {
|
|
238
245
|
const selected=selectScript(kind,names,scripts),script=selected.script;
|
|
239
246
|
if(selected.ambiguous)ambiguous.push({kind,scripts:selected.ambiguous});
|
|
@@ -304,8 +311,11 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
304
311
|
checks.push({config:check,...classification});
|
|
305
312
|
}
|
|
306
313
|
if(workspaces&&!observationIssue){
|
|
307
|
-
const seen=new Set();
|
|
308
|
-
|
|
314
|
+
const seen=new Set(),members=workspaceManifests(root,workspacePatterns);
|
|
315
|
+
// Past 50 packages, init proposes the root's checks only and says so.
|
|
316
|
+
if(members.length>perPackageLimit)skipped.push({kind:'workspace',packages:members.length,
|
|
317
|
+
reason:`${members.length} workspace packages, more than ${perPackageLimit}: per-package checks were not proposed; add the ones you need to the config`});
|
|
318
|
+
for(const {file,pkg:member} of members.length>perPackageLimit?[]:members){
|
|
309
319
|
const dir=path.posix.dirname(file),name=member.name;
|
|
310
320
|
if(typeof name!=='string'||!name||seen.has(name)){
|
|
311
321
|
ambiguous.push({kind:'workspace',scripts:[dir],reason:'missing or duplicate package name'});
|
|
@@ -367,6 +377,6 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
367
377
|
}
|
|
368
378
|
}
|
|
369
379
|
return {schema:1,root,manager:pm,observationIssue,workspaces,node:process.version,
|
|
370
|
-
checks,ambiguous,
|
|
380
|
+
checks,ambiguous,skipped,
|
|
371
381
|
config:{schema:1,packageManager:pm.name||'unknown',checks:checks.map(row=>row.config)}};
|
|
372
382
|
}
|
package/src/node-script.mjs
CHANGED
|
@@ -42,19 +42,21 @@ export function scriptCheckInputs(dirs=[]){
|
|
|
42
42
|
installedInputs:[...modules.flatMap(at=>[`${at}/**`,`!${at}/.cache/**`,`!${at}/.vite/**`,`!${at}/.vite-temp/**`,
|
|
43
43
|
`!${at}/.vitest/**`,`!${at}/.vitest-cache/**`,`!${at}/**/*.tsbuildinfo`]),...installState]};
|
|
44
44
|
}
|
|
45
|
-
// Installed files a tool rewrites during a run
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
45
|
+
// Installed files a tool rewrites during a run. pnpm writes its workspace
|
|
46
|
+
// state in the root node_modules at the first run after an install, through
|
|
47
|
+
// a temporary .tmpXXXXXX file it renames. vue-tsc writes its global types in
|
|
48
|
+
// a workspace node_modules before it reads them. They stay inputs. When they
|
|
49
|
+
// are a run's only change and the run wrote them, the run is kept with the
|
|
50
|
+
// files as the run left them.
|
|
50
51
|
const pnpmState=rel=>/^node_modules\/\.pnpm-workspace-state(?:-v\d+)?\.json$/.test(rel);
|
|
52
|
+
const pnpmTemp=rel=>/^node_modules\/\.tmp[A-Za-z0-9]{6}$/.test(rel);
|
|
51
53
|
const vueTypes=rel=>{const parts=rel.split('/');
|
|
52
54
|
return /^[^/]+\.d\.ts$/.test(parts.at(-1))&&parts.at(-2)==='.vue-global-types'&&parts.at(-3)==='node_modules'&&
|
|
53
55
|
!parts.slice(0,-3).includes('node_modules')&&!rel.startsWith('external:');};
|
|
54
|
-
export const toolOutputPath=rel=>pnpmState(rel)||vueTypes(rel);
|
|
55
|
-
// The run wrote it: a recorded process did, or
|
|
56
|
-
// writes
|
|
57
|
-
export const toolOutput=(rel,guard)=>toolOutputPath(rel)&&(guard.wroteInstalled(rel)||pnpmState(rel)&&guard.opaque);
|
|
56
|
+
export const toolOutputPath=rel=>pnpmState(rel)||pnpmTemp(rel)||vueTypes(rel);
|
|
57
|
+
// The run wrote it: a recorded process did, or, for pnpm's files, the pnpm
|
|
58
|
+
// executable ran, which writes them natively.
|
|
59
|
+
export const toolOutput=(rel,guard)=>toolOutputPath(rel)&&(guard.wroteInstalled(rel)||(pnpmState(rel)||pnpmTemp(rel))&&guard.opaque);
|
|
58
60
|
// The installed paths the recorder treats as observed by identity.
|
|
59
61
|
export const scriptInstalledPaths=(root,dirs=[])=>[...modulesDirs(dirs),...installState].map(at=>path.join(root,at));
|
|
60
62
|
// Variables a recorded run cannot be seen reading: Node's and libc's (as in
|
|
@@ -5,7 +5,9 @@ import path from 'node:path';
|
|
|
5
5
|
import {spawnSync} from 'node:child_process';
|
|
6
6
|
import {patternMatcher} from './glob.mjs';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Up to 1000 packages: every workspace node_modules is an installed input of
|
|
9
|
+
// a root check, so all of them are listed.
|
|
10
|
+
export function workspaceManifests(root,patterns,limit=1000) {
|
|
9
11
|
const listing=spawnSync('git',['ls-files','--cached','--others','--exclude-standard',
|
|
10
12
|
'-z','--',':(glob)**/package.json'],{cwd:root,encoding:'utf8',timeout:5000,
|
|
11
13
|
maxBuffer:32*1024*1024,stdio:['ignore','pipe','ignore'],
|
|
@@ -17,6 +19,6 @@ export function workspaceManifests(root,patterns) {
|
|
|
17
19
|
const dir=path.posix.dirname(file);
|
|
18
20
|
return dir!=='.'&&!dir.split('/').includes('node_modules')&&included(dir);
|
|
19
21
|
}).sort();
|
|
20
|
-
if(matched.length>
|
|
22
|
+
if(matched.length>limit)throw Error(`more than ${limit} workspace packages found; select checks explicitly`);
|
|
21
23
|
return matched.map(file=>({file,pkg:JSON.parse(fs.readFileSync(path.join(root,file),'utf8'))}));
|
|
22
24
|
}
|