@redue/cli 0.1.0-alpha.3 → 0.1.0-alpha.30
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 +806 -0
- package/README.md +157 -99
- package/bin/redue.mjs +267 -61
- package/docs/README.md +3 -1
- package/docs/user/agents.md +333 -15
- package/docs/user/configuration.md +28 -2
- package/docs/user/direct-typescript.md +219 -0
- package/docs/user/getting-started.md +49 -7
- package/docs/user/json.md +4 -2
- package/docs/user/node-script.md +177 -0
- package/docs/user/npm-launcher-notice.md +42 -0
- package/docs/user/pilot.md +146 -0
- package/docs/user/sandbox.md +312 -0
- package/docs/user/shell.md +3 -3
- package/docs/user/supported-projects.md +69 -23
- package/docs/user/tests-direct.md +143 -0
- package/package.json +4 -3
- package/src/agent-integration.mjs +467 -0
- package/src/agent-policy.md +28 -0
- package/src/cached-state.mjs +24 -1
- package/src/claude-hook.mjs +616 -0
- package/src/cli.mjs +449 -54
- package/src/daemon.mjs +442 -93
- package/src/decision-profile.mjs +15 -1
- package/src/decision-validation.mjs +12 -1
- package/src/declared-plan.mjs +38 -6
- package/src/direct-run-certificate.mjs +254 -0
- package/src/direct-typescript-probe.mjs +27 -0
- package/src/direct-typescript.mjs +205 -0
- package/src/glob.mjs +8 -0
- package/src/history-worker.mjs +21 -13
- package/src/index.mjs +108 -21
- package/src/installed-inputs.mjs +7 -1
- package/src/installed-observation.mjs +23 -0
- package/src/landlock.py +527 -0
- package/src/linux-inotify.py +39 -10
- package/src/node-onboarding.mjs +203 -54
- package/src/node-script.mjs +406 -0
- package/src/npm-integrity.mjs +124 -0
- package/src/npm-typecheck-environment.mjs +36 -0
- package/src/npmrc.mjs +16 -0
- package/src/observation.mjs +34 -12
- package/src/path-identity.mjs +8 -0
- package/src/plan-worker.mjs +17 -17
- package/src/pnpm-inputs.mjs +122 -57
- package/src/pnpm-shim-templates.json +180 -0
- package/src/pnpm-shims.mjs +54 -0
- package/src/pnpm-typecheck-probe.mjs +11 -6
- package/src/presentation.mjs +13 -4
- package/src/probe-deadline.mjs +37 -0
- package/src/probe-result.mjs +7 -3
- package/src/project-location.mjs +34 -0
- package/src/python-onboarding.mjs +89 -0
- package/src/run-recorder.cjs +91 -0
- package/src/sandbox.mjs +653 -0
- package/src/setup.mjs +205 -0
- package/src/stats.mjs +72 -0
- package/src/test-direct.mjs +608 -0
- package/src/test-recorder.cjs +435 -0
- package/src/typescript-contract-probe.mjs +4 -187
- package/src/typescript-input-facts.mjs +92 -0
- package/src/typescript-invocation.mjs +92 -0
- package/src/typescript-list.mjs +118 -13
- package/src/typescript-qualification.mjs +29 -49
- package/src/workspace-manifests.mjs +24 -0
- package/src/yarn-workspace-inputs.mjs +3 -0
- package/src/yarn-workspace-typecheck-probe.mjs +2 -1
package/src/node-onboarding.mjs
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
import {directProject,directQualification} from './direct-typescript.mjs';
|
|
2
|
+
import {testProject,testRecipeScript,testCheckInputs,testsQualification} from './test-direct.mjs';
|
|
3
|
+
import {generalIssue,scriptCheckInputs,scriptProject,scriptQualification} from './node-script.mjs';
|
|
4
|
+
import {sandboxCheckInputs,sandboxKinds,sandboxQualification} from './sandbox.mjs';
|
|
5
|
+
// Init proposes the general recipe for typecheck and lint scripts; test
|
|
6
|
+
// scripts use the sandboxed recipe first, then the direct test recipe
|
|
7
|
+
// (Vitest, Jest).
|
|
8
|
+
const generalKinds=['typecheck','lint'];
|
|
9
|
+
const perPackageLimit=50;
|
|
1
10
|
import fs from 'node:fs';
|
|
2
11
|
import path from 'node:path';
|
|
3
12
|
import {spawnSync} from 'node:child_process';
|
|
4
|
-
import micromatch from './glob.mjs';
|
|
5
13
|
import {yarnWorkspaceTypecheckInputs} from './yarn-workspace-inputs.mjs';
|
|
6
14
|
import {pnpmInstallInfo,pnpmProjectInfo,pnpmTypecheckInputs} from './pnpm-inputs.mjs';
|
|
7
15
|
import {realObservedPath} from './path-identity.mjs';
|
|
8
16
|
import {findExecutable} from './executable-lookup.mjs';
|
|
17
|
+
import {workspaceManifests} from './workspace-manifests.mjs';
|
|
18
|
+
import {partName,tscRuns} from './typescript-invocation.mjs';
|
|
9
19
|
|
|
10
20
|
const exists=file=>{try{fs.accessSync(file);return true;}catch{return false;}};
|
|
11
21
|
const readJson=file=>JSON.parse(fs.readFileSync(file,'utf8'));
|
|
12
22
|
const scriptNames={
|
|
13
|
-
typecheck:['typecheck','type-check','check:types','types:check'],
|
|
23
|
+
typecheck:['typecheck','type-check','check:types','types:check','check-types','tsc','types','lint:types'],
|
|
14
24
|
test:['test','test:unit','test:check'],
|
|
15
25
|
lint:['lint','lint:check'],
|
|
16
26
|
build:['build'],
|
|
@@ -80,7 +90,29 @@ function manager(root,pkg) {
|
|
|
80
90
|
return {name,version,layout:'node-modules/classic',issue:null,lockfiles:locks};
|
|
81
91
|
}
|
|
82
92
|
const exactTypeScript=script=>script.trim()==='tsc --noEmit';
|
|
83
|
-
|
|
93
|
+
// A script of tsc runs that each pass --noEmit (typescript-invocation.mjs).
|
|
94
|
+
const noEmitRuns=script=>Boolean(tscRuns(script)?.every(run=>run.noEmit));
|
|
95
|
+
// Ready checks for a typecheck, one per tsc run: the script's runs (a chain
|
|
96
|
+
// gives NAME.1 ... NAME.n), or with no script one proposed `tsc --noEmit`.
|
|
97
|
+
// `make(run)` returns a run's command, cwd and inputs, or throws why its
|
|
98
|
+
// contract does not hold.
|
|
99
|
+
function tscChecks(name,script,scripts,make){
|
|
100
|
+
const runs=script===null?[{args:['--noEmit']}]:tscRuns(scripts[script]);
|
|
101
|
+
if(!runs)throw Error('the script is not tsc runs in a supported form');
|
|
102
|
+
return runs.map(({args},i)=>({name:partName(name,i+1,runs.length),
|
|
103
|
+
...(script===null?{}:{script}),...make({script,args,count:runs.length})}));
|
|
104
|
+
}
|
|
105
|
+
// A proposed `tsc --noEmit` only where nothing typechecks already: a script
|
|
106
|
+
// that runs any TypeScript checker in another form (tsc -b, vue-tsc,
|
|
107
|
+
// svelte-check, tsgo, also quoted or wrapped by concurrently or run-p) is the
|
|
108
|
+
// project's own typecheck, which plain tsc would not match.
|
|
109
|
+
const typeChecker=/(?:^|[^\w-])(?:tsc|vue-tsc|tsgo|svelte-check|tsc-watch)(?=$|[^\w-])|\bastro check\b|\bnuxi typecheck\b/;
|
|
110
|
+
const typechecks=scripts=>Object.values(scripts||{}).some(text=>typeof text==='string'&&typeChecker.test(text));
|
|
111
|
+
const proposable=(dir,scripts)=>exists(path.join(dir,'tsconfig.json'))&&!typechecks(scripts);
|
|
112
|
+
// The inputs a TypeScript contract adds to its check's config: no exclusions,
|
|
113
|
+
// which would outlive the tsconfig they came from; the plan adds the current ones.
|
|
114
|
+
const contractInputs=contract=>[...contract.configs,...contract.sourcePatterns.filter(p=>!p.startsWith('!'))];
|
|
115
|
+
export function sourceLinkIssue(root) {
|
|
84
116
|
const stack=likelyDirs.filter(name=>exists(path.join(root,name)));
|
|
85
117
|
let inspected=0;
|
|
86
118
|
while(stack.length){
|
|
@@ -105,7 +137,8 @@ function sourceInputs(root,kind,lockfiles) {
|
|
|
105
137
|
'!node_modules/**','!.git/**'])];
|
|
106
138
|
const dirs=likelyDirs.filter(name=>exists(path.join(root,name)))
|
|
107
139
|
.map(name=>`${name}/**`);
|
|
108
|
-
|
|
140
|
+
// A workspace package's node_modules under them is installed, not source.
|
|
141
|
+
return [...new Set([...files,...(dirs.length?[...dirs,'!**/node_modules/**']:['package.json'])])];
|
|
109
142
|
}
|
|
110
143
|
function classify(kind,scriptName,script,pkg,managerInfo,root) {
|
|
111
144
|
const value=script.trim();
|
|
@@ -116,7 +149,7 @@ function classify(kind,scriptName,script,pkg,managerInfo,root) {
|
|
|
116
149
|
managerInfo.name==='npm'&&
|
|
117
150
|
exists(path.join(root,'tsconfig.json'))&&
|
|
118
151
|
exists(path.join(root,'node_modules/typescript/package.json')))
|
|
119
|
-
return {level:'
|
|
152
|
+
return {level:'recording',reason:'npm launcher uses unobserved remote/cache/time inputs; execution can be recorded, but cannot be reused',
|
|
120
153
|
qualification:'typescript-noemit-v1'};
|
|
121
154
|
if(kind==='typecheck')return {level:'recording',reason:
|
|
122
155
|
'TypeScript invocation, installation, or configuration is outside the automatic noEmit contract'};
|
|
@@ -130,36 +163,63 @@ function classify(kind,scriptName,script,pkg,managerInfo,root) {
|
|
|
130
163
|
'Vite detected; build environment and generated inputs need review':
|
|
131
164
|
'build script detected; generated and runtime inputs need review'};
|
|
132
165
|
}
|
|
166
|
+
// Typecheck scripts named under a typecheck name (`typecheck:test`,
|
|
167
|
+
// `tsc:build`), watch scripts aside.
|
|
168
|
+
const typecheckFamily=/^(?:typecheck|type-check|check-types|check:types|types:check|tsc):(?!.*watch)/;
|
|
133
169
|
function selectScript(kind,names,scripts) {
|
|
134
170
|
const preferred=names.find(name=>typeof scripts[name]==='string'&&
|
|
135
171
|
scripts[name].trim()&&!/^echo\s+["']?Error: no test specified/.test(scripts[name]));
|
|
136
172
|
if(preferred)return {script:preferred};
|
|
173
|
+
const family=kind==='typecheck'?Object.keys(scripts).filter(name=>typecheckFamily.test(name)&&
|
|
174
|
+
typeof scripts[name]==='string'&&scripts[name].trim()&&!/(?:^|\s)(?:--watch|-w)(?:\s|$)/.test(scripts[name])):[];
|
|
175
|
+
if(family.length===1)return {script:family[0]};
|
|
137
176
|
const candidates=Object.keys(scripts).filter(name=>{
|
|
138
177
|
const value=scripts[name]?.trim();
|
|
139
178
|
if(typeof value!=='string')return false;
|
|
140
|
-
if(kind==='typecheck')return
|
|
179
|
+
if(kind==='typecheck')return noEmitRuns(value);
|
|
141
180
|
if(kind==='test')return /^(?:vitest|jest)(?:\s|$)/.test(value);
|
|
142
181
|
if(kind==='lint')return /^eslint(?:\s|$)/.test(value);
|
|
143
182
|
return /^vite\s+build(?:\s|$)/.test(value);
|
|
144
183
|
});
|
|
184
|
+
// A lone plain `tsc --noEmit` wins over other tsc shapes beside it (a
|
|
185
|
+
// project's `typescript` next to `typescript:tests:raw`).
|
|
186
|
+
const exact=kind==='typecheck'?candidates.filter(name=>exactTypeScript(scripts[name])):[];
|
|
187
|
+
if(exact.length===1)return {script:exact[0]};
|
|
145
188
|
return candidates.length===1?{script:candidates[0]}:
|
|
146
|
-
candidates.length>1?{ambiguous:candidates}:{};
|
|
189
|
+
candidates.length>1?{ambiguous:candidates}:family.length>1?{ambiguous:family}:{};
|
|
190
|
+
}
|
|
191
|
+
// A pnpm workspace package's TypeScript run, as its check's command, cwd and inputs.
|
|
192
|
+
function pnpmCheck(root,name,dir,manifest,run){
|
|
193
|
+
const contract=pnpmTypecheckInputs(root,name,run);
|
|
194
|
+
if(contract.limitations.length)throw Error(contract.limitations.join('; '));
|
|
195
|
+
return {cwd:dir,command:['@node',`@typescript-bin:${dir}`,...run.args],
|
|
196
|
+
inputs:[...new Set(['package.json','pnpm-lock.yaml','pnpm-workspace.yaml',manifest,
|
|
197
|
+
...contractInputs(contract)])]};
|
|
198
|
+
}
|
|
199
|
+
// Workspace packages as init finds them, with their scripts; [] without workspaces.
|
|
200
|
+
export function workspacePackages(root){
|
|
201
|
+
root=realObservedPath(root);
|
|
202
|
+
const pkg=readJson(path.join(root,'package.json')),pm=manager(root,pkg);
|
|
203
|
+
const patterns=pm.name==='pnpm'?pm.workspacePatterns:Array.isArray(pkg.workspaces)?pkg.workspaces:pkg.workspaces?.packages;
|
|
204
|
+
if(!Array.isArray(patterns)||!patterns.length)return [];
|
|
205
|
+
return workspaceManifests(root,patterns).map(({file,pkg:member})=>
|
|
206
|
+
({name:member.name,dir:path.posix.dirname(file),scripts:member.scripts&&typeof member.scripts==='object'?member.scripts:{}}));
|
|
147
207
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if(
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
return dir!=='.'&&!dir.split('/').includes('node_modules')&&
|
|
157
|
-
micromatch.isMatch(dir,patterns,{dot:true});
|
|
158
|
-
}).sort();
|
|
159
|
-
if(matched.length>50)throw Error('more than 50 workspace packages found; select checks explicitly');
|
|
160
|
-
return matched.map(file=>({file,pkg:readJson(path.join(root,file))}));
|
|
208
|
+
// The general recipe for a root script, or why it does not apply: a
|
|
209
|
+
// supported package manager and platform, and a script (with its pre and post
|
|
210
|
+
// scripts) that, read as text, starts only Node programs.
|
|
211
|
+
function generalCheck(root,kind,script){
|
|
212
|
+
const issue=generalIssue(root,script);
|
|
213
|
+
if(issue)throw Error(issue);
|
|
214
|
+
const project=scriptProject(root,script);
|
|
215
|
+
return {config:{name:kind,script,kind,qualification:scriptQualification,...scriptCheckInputs(project.dirs)},manager:project.manager};
|
|
161
216
|
}
|
|
162
|
-
export function discoverNodeProject(root) {
|
|
217
|
+
export function discoverNodeProject(root,{recipe=null}={}) {
|
|
218
|
+
if(recipe&&!['typescript-direct','tests-direct','node-script','sandbox'].includes(recipe))
|
|
219
|
+
throw Error('unknown recipe; use typescript-direct, tests-direct, node-script or sandbox');
|
|
220
|
+
// Without --recipe, init tries the strict recipes first, then the general
|
|
221
|
+
// one, then the sandboxed one.
|
|
222
|
+
const auto=recipe===null;
|
|
163
223
|
root=realObservedPath(root);
|
|
164
224
|
const packageFile=path.join(root,'package.json');
|
|
165
225
|
if(!exists(packageFile))throw Error('run init from a repository containing package.json');
|
|
@@ -176,36 +236,116 @@ export function discoverNodeProject(root) {
|
|
|
176
236
|
workspacePatterns.some(value=>typeof value!=='string'||path.isAbsolute(value)||
|
|
177
237
|
value.split('/').includes('..'))))
|
|
178
238
|
throw Error('workspace patterns need a supported project-relative package list');
|
|
179
|
-
const checks=[],ambiguous=[];
|
|
239
|
+
const checks=[],ambiguous=[],skipped=[];
|
|
180
240
|
for(const [kind,names] of Object.entries(scriptNames)) {
|
|
181
241
|
const selected=selectScript(kind,names,scripts),script=selected.script;
|
|
182
242
|
if(selected.ambiguous)ambiguous.push({kind,scripts:selected.ambiguous});
|
|
183
|
-
|
|
243
|
+
// With no typecheck script, a root tsconfig.json gets a proposed
|
|
244
|
+
// `tsc --noEmit` check; package.json stays unchanged. It is kept only
|
|
245
|
+
// when a contract makes it ready.
|
|
246
|
+
const proposed=!script&&!selected.ambiguous&&kind==='typecheck'&&!observationIssue&&
|
|
247
|
+
proposable(root,scripts);
|
|
248
|
+
if(!script&&!proposed)continue;
|
|
184
249
|
let classification=observationIssue?{level:'unsupported',reason:observationIssue}:
|
|
185
|
-
classify(kind,script,scripts[script],pkg,pm,root);
|
|
186
|
-
|
|
250
|
+
script?classify(kind,script,scripts[script],pkg,pm,root):{level:'recording',reason:'no typecheck script'};
|
|
251
|
+
let parts=null;
|
|
252
|
+
if(workspaces&&classification.qualification==='typescript-noemit-v1')classification={level:'recording',
|
|
187
253
|
reason:'workspace task and installed-input closure need explicit qualification'};
|
|
188
254
|
const check={name:kind,script,kind,inputs:sourceInputs(root,kind,pm.lockfiles||[])};
|
|
189
|
-
if(pm.name==='
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
255
|
+
if((recipe==='typescript-direct'||auto&&pm.name==='npm'&&!workspaces)&&kind==='typecheck'){
|
|
256
|
+
try{
|
|
257
|
+
if(pm.name!=='npm'||workspaces||observationIssue||pm.issue)throw Error('direct recipe requires a supported single-package npm checkout');
|
|
258
|
+
let version;
|
|
259
|
+
parts=tscChecks('typecheck',script??null,scripts,run=>{
|
|
260
|
+
version=directProject(root,run).version;
|
|
261
|
+
return {command:['@node','@typescript-compiler:.',...run.args],cwd:'.'};});
|
|
262
|
+
classification={level:'ready',qualification:directQualification,
|
|
263
|
+
reason:`explicit local TypeScript ${version} compilation; npm launcher/lifecycle scripts are not executed; package.json stays unchanged`};
|
|
264
|
+
}catch(e){classification={level:'recording',reason:`Direct recipe unavailable (${e.code||e.message}); original npm script retained, without bypassing required behavior`};}
|
|
265
|
+
}
|
|
266
|
+
if((recipe==='tests-direct'||auto)&&kind==='test'){
|
|
267
|
+
try{
|
|
268
|
+
if(!['npm','pnpm','yarn'].includes(pm.name)||workspaces||observationIssue||pm.issue)
|
|
269
|
+
throw Error('test recipe requires a supported single-package npm, pnpm or Yarn checkout');
|
|
270
|
+
const chosen=testRecipeScript(scripts);
|
|
271
|
+
if(chosen.ambiguous)throw Error(`several scripts run Vitest or Jest directly (${chosen.ambiguous.join(', ')})`);
|
|
272
|
+
if(!chosen.script)throw Error(chosen.issue||'no script runs Vitest or Jest directly');
|
|
273
|
+
const project=testProject(root,chosen.script);
|
|
274
|
+
Object.assign(check,{script:chosen.script,command:['@node',`@test-runner:${project.runner}`,...project.args],cwd:'.',
|
|
275
|
+
...testCheckInputs(project)});
|
|
276
|
+
classification={level:'ready',qualification:testsQualification,
|
|
277
|
+
reason:`explicit local ${project.runner} ${project.version} run of script ${JSON.stringify(chosen.script)}; the whole checkout is its input; npm and lifecycle scripts are not executed`};
|
|
278
|
+
}catch(e){classification={level:'recording',reason:`Test recipe unavailable (${e.code||e.message}); original npm script retained`};}
|
|
279
|
+
}
|
|
280
|
+
if(recipe!=='node-script'&&recipe!=='sandbox'&&pm.name==='pnpm'&&kind==='typecheck'&&!workspaces&&!pm.issue&&(script===undefined||tscRuns(scripts[script]))){
|
|
281
|
+
try{
|
|
282
|
+
parts=tscChecks('typecheck',script??null,scripts,run=>{
|
|
283
|
+
const contract=pnpmTypecheckInputs(root,'.',run);
|
|
284
|
+
if(contract.limitations.length)throw Error(contract.limitations.join('; '));
|
|
285
|
+
return {command:['@node','@typescript-bin:.',...run.args],cwd:'.',
|
|
286
|
+
inputs:[...new Set(['package.json','pnpm-lock.yaml',
|
|
287
|
+
...(exists(path.join(root,'pnpm-workspace.yaml'))?['pnpm-workspace.yaml']:[]),
|
|
288
|
+
...contractInputs(contract)])]};});
|
|
289
|
+
classification={level:'ready',reason:'pnpm TypeScript inputs and installation; checked again before reuse',
|
|
200
290
|
qualification:'pnpm-tsc-v1'};}
|
|
201
291
|
catch(e){classification={level:'recording',reason:`pnpm TypeScript contract needs review: ${e.message}`};}
|
|
202
292
|
}
|
|
293
|
+
// The general recipe's row, when it takes the script.
|
|
294
|
+
let generalIssue=null,generalRow=null;
|
|
295
|
+
if((auto||recipe==='node-script')&&script&&generalKinds.includes(kind)&&classification.level!=='ready'&&!observationIssue){
|
|
296
|
+
try{
|
|
297
|
+
const general=generalCheck(root,kind,script);
|
|
298
|
+
generalRow={config:general.config,level:'ready',qualification:scriptQualification,
|
|
299
|
+
reason:`general recipe: ${general.manager} runs script ${JSON.stringify(script)} with REDUE's recorder in every Node process; the whole checkout is its input`};
|
|
300
|
+
}catch(e){generalIssue=e.message;classification={...classification,reason:`General recipe unavailable (${e.message}); ${classification.reason}`};}
|
|
301
|
+
}
|
|
302
|
+
// After the strict recipes, a root typecheck or lint script gets the
|
|
303
|
+
// sandboxed recipe, which also covers programs the general recipe cannot
|
|
304
|
+
// record. It is ready only once init has run it both ways (bin/redue.mjs);
|
|
305
|
+
// `fallback` is the row init writes when that shows a difference or the
|
|
306
|
+
// sandbox cannot run here: the general recipe's, when it takes the script,
|
|
307
|
+
// or the direct test recipe's.
|
|
308
|
+
// Where the sandbox cannot start at run time, a script the general recipe
|
|
309
|
+
// takes is recorded as it would record it (cli.mjs).
|
|
310
|
+
// A script whose program is not installed gets none, and neither does one
|
|
311
|
+
// that changes files (`--fix`, `--write`): init would run it twice.
|
|
312
|
+
const writes=script&&['pre'+script,script,'post'+script].map(name=>scripts[name]).find(text=>typeof text==='string'&&
|
|
313
|
+
/(?:^|\s)--(?:fix|write|unsafe-fixes)(?=[\s=]|$)/.test(text));
|
|
314
|
+
if(writes)classification={...classification,reason:`Sandboxed recipe unavailable (the script changes files: ${writes.match(/--(?:fix|write|unsafe-fixes)/)[0]}); ${classification.reason}`};
|
|
315
|
+
// A test script gets it before the direct test recipe, which is then its
|
|
316
|
+
// fallback, when both run the same script: in the census the direct
|
|
317
|
+
// recipe's results went STALE when the build changed its output, and the
|
|
318
|
+
// sandboxed ones stayed CURRENT.
|
|
319
|
+
if((auto||recipe==='sandbox')&&script&&sandboxKinds.includes(kind)&&!observationIssue&&
|
|
320
|
+
(classification.level!=='ready'||auto&&kind==='test'&&check.script===script)&&
|
|
321
|
+
!/REDUE cannot find/.test(generalIssue||'')&&!writes){
|
|
322
|
+
try{
|
|
323
|
+
const project=scriptProject(root,script);
|
|
324
|
+
const fallback=generalRow??{config:{...check,...classification.qualification?{qualification:classification.qualification}:{}},...classification};
|
|
325
|
+
checks.push({config:{name:kind,script,kind,qualification:sandboxQualification,...sandboxCheckInputs(project.dirs)},level:'ready',
|
|
326
|
+
qualification:sandboxQualification,fallback,
|
|
327
|
+
reason:`sandboxed recipe: ${project.manager} runs script ${JSON.stringify(script)} inside the OS sandbox${generalIssue?` (the general recipe cannot record it: ${generalIssue})`:
|
|
328
|
+
generalRow?'; where the sandbox cannot run, the general recipe records it':''}; the whole checkout is its input`});
|
|
329
|
+
continue;
|
|
330
|
+
}catch(e){classification={...classification,reason:`Sandboxed recipe unavailable (${e.code?.replace(/^test-/,'')||e.message}); ${classification.reason}`};}
|
|
331
|
+
}
|
|
332
|
+
if(generalRow){checks.push(generalRow);continue;}
|
|
333
|
+
// A proposed `tsc --noEmit` that no recipe takes is left out, with why.
|
|
334
|
+
if(!script&&classification.level!=='ready'){
|
|
335
|
+
skipped.push({kind,reason:`${kind}: no typecheck script, and a proposed tsc --noEmit was not taken (${classification.reason})`});continue;}
|
|
336
|
+
if(parts&&classification.level==='ready'){
|
|
337
|
+
for(const part of parts)checks.push({config:{...check,...part,qualification:classification.qualification},...classification});
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
203
340
|
if(classification.qualification)check.qualification=classification.qualification;
|
|
204
341
|
checks.push({config:check,...classification});
|
|
205
342
|
}
|
|
206
343
|
if(workspaces&&!observationIssue){
|
|
207
|
-
const seen=new Set();
|
|
208
|
-
|
|
344
|
+
const seen=new Set(),members=workspaceManifests(root,workspacePatterns);
|
|
345
|
+
// Past 50 packages, init proposes the root's checks only and says so.
|
|
346
|
+
if(members.length>perPackageLimit)skipped.push({kind:'workspace',packages:members.length,
|
|
347
|
+
reason:`${members.length} workspace packages, more than ${perPackageLimit}: per-package checks were not proposed; add the ones you need to the config`});
|
|
348
|
+
for(const {file,pkg:member} of members.length>perPackageLimit?[]:members){
|
|
209
349
|
const dir=path.posix.dirname(file),name=member.name;
|
|
210
350
|
if(typeof name!=='string'||!name||seen.has(name)){
|
|
211
351
|
ambiguous.push({kind:'workspace',scripts:[dir],reason:'missing or duplicate package name'});
|
|
@@ -217,6 +357,16 @@ export function discoverNodeProject(root) {
|
|
|
217
357
|
const selected=selectScript(kind,names,member.scripts||{});
|
|
218
358
|
if(selected.ambiguous){ambiguous.push({kind:`${name}:${kind}`,
|
|
219
359
|
scripts:selected.ambiguous});continue;}
|
|
360
|
+
// A pnpm package with a tsconfig.json and no typecheck script gets a
|
|
361
|
+
// proposed `tsc --noEmit` check when the pnpm contract holds.
|
|
362
|
+
if(!selected.script&&kind==='typecheck'&&pm.name==='pnpm'&&!pm.issue&&
|
|
363
|
+
proposable(path.join(root,dir),member.scripts)&&!typechecks(scripts)){
|
|
364
|
+
try{for(const part of tscChecks(`${name}:typecheck`,null,{},run=>pnpmCheck(root,name,dir,file,run)))
|
|
365
|
+
checks.push({config:{kind,workspace:name,qualification:'pnpm-tsc-v1',...part},level:'ready',
|
|
366
|
+
reason:'proposed tsc --noEmit for the package\'s tsconfig.json; pnpm workspace TypeScript inputs; checked again before reuse'});}
|
|
367
|
+
catch{}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
220
370
|
if(!selected.script)continue;
|
|
221
371
|
const command=pm.name==='npm'?['@which:npm','run',selected.script,
|
|
222
372
|
'--workspace',name]:pm.name==='yarn'?
|
|
@@ -224,7 +374,7 @@ export function discoverNodeProject(root) {
|
|
|
224
374
|
['@which:pnpm','--filter',name,'run',selected.script];
|
|
225
375
|
const check={name:`${name}:${kind}`,kind,command,
|
|
226
376
|
inputs:['package.json',...(pm.lockfiles||[]),`${dir}/**`,
|
|
227
|
-
`!${dir}/node_modules/**`]};
|
|
377
|
+
`!${dir}/node_modules/**`,`!${dir}/**/node_modules/**`]};
|
|
228
378
|
let level=pm.issue&&pm.name!=='pnpm'?'unsupported':'recording';
|
|
229
379
|
let reason=pm.issue||'workspace script found; shared configuration and installed inputs need review';
|
|
230
380
|
if(!pm.issue&&pm.name==='yarn'&&pm.layout==='node-modules/classic'&&
|
|
@@ -237,27 +387,26 @@ export function discoverNodeProject(root) {
|
|
|
237
387
|
level='ready';reason='pinned Yarn workspace tsc and installed inputs; checked again before reuse';
|
|
238
388
|
}catch(e){reason=`workspace TypeScript contract needs review: ${e.message}`;}
|
|
239
389
|
}
|
|
240
|
-
if(!pm.issue&&pm.name==='pnpm'&&kind==='typecheck'&&
|
|
241
|
-
|
|
242
|
-
(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if(contract.limitations.length)throw Error(contract.limitations.join('; '));
|
|
246
|
-
check.workspace=name;check.qualification='pnpm-tsc-v1';
|
|
247
|
-
check.script=selected.script;
|
|
248
|
-
check.cwd=dir;
|
|
249
|
-
check.command=['@node',`@typescript-bin:${dir}`,
|
|
250
|
-
...member.scripts[selected.script].trim().split(/\s+/).slice(1)];
|
|
251
|
-
check.inputs=['package.json','pnpm-lock.yaml','pnpm-workspace.yaml',file,
|
|
252
|
-
`${dir}/src/**`];
|
|
253
|
-
level='ready';reason='pinned pnpm workspace TypeScript inputs; checked again before reuse';
|
|
390
|
+
if(!pm.issue&&pm.name==='pnpm'&&kind==='typecheck'&&tscRuns((member.scripts||{})[selected.script])){
|
|
391
|
+
try{const parts=tscChecks(`${name}:typecheck`,selected.script,member.scripts,run=>pnpmCheck(root,name,dir,file,run));
|
|
392
|
+
for(const part of parts)checks.push({config:{kind,workspace:name,qualification:'pnpm-tsc-v1',...part},level:'ready',
|
|
393
|
+
reason:'pnpm workspace TypeScript inputs; checked again before reuse'});
|
|
394
|
+
continue;
|
|
254
395
|
}catch(e){reason=`pnpm TypeScript contract needs review: ${e.message}`;}
|
|
255
396
|
}
|
|
397
|
+
if(recipe==='tests-direct'&&kind==='test'&&!pm.issue){
|
|
398
|
+
try{const project=testProject(root,selected.script,dir);
|
|
399
|
+
Object.assign(check,{workspace:name,script:selected.script,cwd:dir,
|
|
400
|
+
command:['@node',`@test-runner:${project.runner}`,...project.args],...testCheckInputs(project),
|
|
401
|
+
qualification:testsQualification});
|
|
402
|
+
level='ready';reason=`explicit local ${project.runner} ${project.version} run of ${name}'s script ${JSON.stringify(selected.script)}; the whole checkout is its input; the package manager and lifecycle scripts are not executed`;
|
|
403
|
+
}catch(e){reason=`Test recipe unavailable (${e.code||e.message}); original script retained`;}
|
|
404
|
+
}
|
|
256
405
|
checks.push({config:check,level,reason});
|
|
257
406
|
}
|
|
258
407
|
}
|
|
259
408
|
}
|
|
260
409
|
return {schema:1,root,manager:pm,observationIssue,workspaces,node:process.version,
|
|
261
|
-
checks,ambiguous,
|
|
410
|
+
checks,ambiguous,skipped,
|
|
262
411
|
config:{schema:1,packageManager:pm.name||'unknown',checks:checks.map(row=>row.config)}};
|
|
263
412
|
}
|