@redue/cli 0.1.0-alpha.18 → 0.1.0-alpha.19
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 +43 -0
- package/README.md +8 -4
- package/bin/redue.mjs +10 -3
- package/docs/user/configuration.md +2 -0
- package/docs/user/getting-started.md +7 -7
- package/docs/user/node-script.md +153 -0
- package/docs/user/supported-projects.md +12 -10
- package/docs/user/tests-direct.md +5 -3
- package/package.json +1 -1
- package/src/agent-integration.mjs +14 -6
- package/src/claude-hook.mjs +24 -12
- package/src/cli.mjs +32 -14
- package/src/daemon.mjs +3 -2
- package/src/declared-plan.mjs +6 -3
- package/src/index.mjs +10 -4
- package/src/node-onboarding.mjs +39 -7
- package/src/node-script.mjs +362 -0
- package/src/pnpm-shim-templates.json +180 -0
- package/src/pnpm-shims.mjs +54 -0
- package/src/presentation.mjs +8 -1
- package/src/test-direct.mjs +42 -9
- package/src/test-recorder.cjs +101 -14
package/src/cli.mjs
CHANGED
|
@@ -3,8 +3,9 @@ import {builtInProbe,probeFloorMs,publishedDeadline} from './probe-deadline.mjs'
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import {directQualification,directInterpretation,observedNpmContext} from './direct-typescript.mjs';
|
|
5
5
|
import {directRunCertificateAllowed,directRunCertificate,directRunRecordable,directRunRecording,probeQuestions,validateRunRecord} from './direct-run-certificate.mjs';
|
|
6
|
-
import {runFactChange} from './typescript-list.mjs';
|
|
6
|
+
import {queryAnswer,runFactChange} from './typescript-list.mjs';
|
|
7
7
|
import {changedFiles,coverageReadable,gitIgnored,recordedEnvironment,testEnvironmentIssue,testGuardConfig,testRunGuard,testRunFacts,sharedInstalledFile,testsInterpretation,testsQualification} from './test-direct.mjs';
|
|
8
|
+
import {childProgram,lookup,pnpmConfigFiles,scriptCaches,scriptInstalledPaths,scriptInterpretation,scriptQualification,scriptRunFacts,toolOutput,toolchainDirs} from './node-script.mjs';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import net from 'node:net';
|
|
10
11
|
import os from 'node:os';
|
|
@@ -69,7 +70,8 @@ const readsFile=path.join(state,'environment-reads-v1.json');
|
|
|
69
70
|
function environmentReads() {
|
|
70
71
|
try{const value=JSON.parse(fs.readFileSync(readsFile,'utf8'));return value?.schema===1&&value.checks||{};}catch{return {};}
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
const recordedRecipes=[testsQualification,scriptQualification];
|
|
74
|
+
function contextHashes() {const reads=config.checks.some(check=>recordedRecipes.includes(check.qualification))?environmentReads():{};
|
|
73
75
|
return Object.fromEntries(config.checks.map(check=>[
|
|
74
76
|
check.name,Object.fromEntries([...(check.environment?.variables||[])
|
|
75
77
|
.map(key=>[key,digest(process.env[key]??'<unset>')]),
|
|
@@ -271,20 +273,28 @@ async function execute() {
|
|
|
271
273
|
const recorded=directRunRecordable(selected)&&!unobservedDirectRun;
|
|
272
274
|
const launched=recorded?[command[0],'--require',path.join(here,'run-recorder.cjs'),
|
|
273
275
|
...command.slice(1)]:command;
|
|
274
|
-
// A test run's processes record what the guard checks (test-recorder.cjs)
|
|
275
|
-
|
|
276
|
-
|
|
276
|
+
// A test run's processes record what the guard checks (test-recorder.cjs),
|
|
277
|
+
// and so do a general recipe run's, through the package manager.
|
|
278
|
+
const general=selected.qualification===scriptQualification;
|
|
279
|
+
const testRun=recordedRecipes.includes(selected.qualification)&&['darwin','linux'].includes(process.platform);
|
|
280
|
+
let testRecord=null,launchEnv=process.env,testFacts={},toolchain=[];
|
|
277
281
|
if(testRun){
|
|
278
282
|
// The package directory and installed node_modules, as the plan has them.
|
|
279
|
-
try{testFacts=testRunFacts(root,selected);}
|
|
280
|
-
catch(e){captureIssues.push(
|
|
283
|
+
try{testFacts=general?scriptRunFacts(root,selected):testRunFacts(root,selected);}
|
|
284
|
+
catch(e){captureIssues.push(`${general?'script':'test'} run facts unavailable (${e.code||e.message})`);}
|
|
281
285
|
const environmentIssue=testEnvironmentIssue();
|
|
282
286
|
if(environmentIssue)captureIssues.push(environmentIssue);
|
|
283
287
|
testRecord=path.join(state,`test-record-${runId}`);
|
|
284
288
|
fs.mkdirSync(testRecord,{mode:0o700});
|
|
285
|
-
const guardFile=path.join(testRecord,'guard.json');
|
|
286
|
-
|
|
289
|
+
const guardFile=path.join(testRecord,'guard.json'),guardConfig=testGuardConfig(testRecord,root,process.env,testFacts);
|
|
290
|
+
if(general){toolchain=toolchainDirs();
|
|
291
|
+
// The Node on PATH, which the environment identifies by its file.
|
|
292
|
+
Object.assign(guardConfig,{installed:scriptInstalledPaths(root,testFacts.dirs||[]),toolchain,
|
|
293
|
+
node:lookup('node',process.env.PATH,process.cwd())});}
|
|
294
|
+
fs.writeFileSync(guardFile,JSON.stringify(guardConfig),{mode:0o600});
|
|
287
295
|
launchEnv=recordedEnvironment(guardFile);
|
|
296
|
+
// npm's and pnpm's update checks read state outside the checkout and the network.
|
|
297
|
+
if(general)launchEnv.npm_config_update_notifier='false';
|
|
288
298
|
}
|
|
289
299
|
let runnerPid=null;
|
|
290
300
|
const record=[];
|
|
@@ -357,10 +367,16 @@ async function execute() {
|
|
|
357
367
|
}else if(!unobservedDirectRun&&!recording)probeAfter=probeHash(probePlan);
|
|
358
368
|
let guard=null;
|
|
359
369
|
if(testRecord){
|
|
360
|
-
let coverage=null;try{coverage=coverageReadable(root,testFacts);}catch{}
|
|
370
|
+
let coverage=null;if(!general)try{coverage=coverageReadable(root,testFacts);}catch{}
|
|
371
|
+
const programs=general?{runner:{file:command[0],args:command.slice(1),cwd:plan.cwd,searchPath:process.env.PATH,
|
|
372
|
+
env:{preload:true,bashEnv:Boolean(process.env.BASH_ENV)}},
|
|
373
|
+
judge:(child,main)=>childProgram(child,{toolchain,parentMain:main}),trusted:scriptCaches}:null;
|
|
361
374
|
guard=testRunGuard(testRecord,{root,sourcePatterns:plan.sourcePatterns||plan.patterns,files:snap?.files,
|
|
362
|
-
runner:runnerPid,environment:selected.environment||{},coverage});
|
|
375
|
+
runner:runnerPid,environment:selected.environment||{},coverage,programs});
|
|
363
376
|
runFacts=guard.runFacts;
|
|
377
|
+
// The pnpm executable reads its configuration natively: kept by content.
|
|
378
|
+
if(guard.opaque)for(const file of pnpmConfigFiles())try{runFacts.push(['readFile',file,queryAnswer('readFile',file)]);}
|
|
379
|
+
catch{guard.issues.push(`pnpm configuration ${file} cannot be read`);}
|
|
364
380
|
const shared=sharedInstalledFile(root,testFacts,path.join(state,'pnpm-store-links-v1.json'));
|
|
365
381
|
if(shared)guard.issues.push(`installed file ${shared.file} ${shared.reason}; a change through it is not observed`);
|
|
366
382
|
fs.rmSync(testRecord,{recursive:true,force:true});
|
|
@@ -392,7 +408,8 @@ async function execute() {
|
|
|
392
408
|
const directory=rel=>{try{return fs.lstatSync(path.join(root,rel)).isDirectory();}catch{return false;}};
|
|
393
409
|
const changed=changedFiles(snap.files,end.files),events=(end.inputEventPaths||[]).filter(rel=>!directory(rel));
|
|
394
410
|
const touched=[...new Set([...changed,...events])].sort();
|
|
395
|
-
const
|
|
411
|
+
const output=rel=>guard.isOutput(rel)||general&&toolOutput(rel,guard);
|
|
412
|
+
const ignored=touched.length&&touched.every(output)?gitIgnored(root,touched):new Set();
|
|
396
413
|
if(touched.every(file=>ignored.has(file))){eventsExplained=true;if(changed.length)outputs=changed;}
|
|
397
414
|
}
|
|
398
415
|
// The end certificate, recorded or probed, must cover what this compile received.
|
|
@@ -447,9 +464,10 @@ async function execute() {
|
|
|
447
464
|
}
|
|
448
465
|
// A recorded run's receipt carries its run facts, even none; observers that
|
|
449
466
|
// predate run facts do not reuse version 2.
|
|
450
|
-
|
|
467
|
+
// The recipe that made the receipt, which status and the hook name.
|
|
468
|
+
const receipt={result,invocation,target,stable,fingerprintVersion:recorded||testRun?2:1,recipe:selected.qualification||null,
|
|
451
469
|
...(recorded||testRun?{runFacts}:{}),
|
|
452
|
-
...(testRun?{verificationRecipe:testsInterpretation,guardIssues:guard?.issues||['the run record is unavailable'],
|
|
470
|
+
...(testRun?{verificationRecipe:general?scriptInterpretation:testsInterpretation,guardIssues:guard?.issues||['the run record is unavailable'],
|
|
453
471
|
environmentReads:guard?.environmentReads||{},...(nondeterministic?{nondeterministic}:{})}:{}),
|
|
454
472
|
resolutionLinks:plan.resolutionLinks||[],
|
|
455
473
|
coverage:{qualified:plan.unresolved.length===0,unresolved:plan.unresolved},
|
package/src/daemon.mjs
CHANGED
|
@@ -352,6 +352,7 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
352
352
|
plan_id:row.plan.id,observed_at:lastObservation,
|
|
353
353
|
invocation:r?.invocation||null,target_provenance:r?.target||null,
|
|
354
354
|
...(r?.verificationRecipe?{verification_recipe:r.verificationRecipe}:{}),
|
|
355
|
+
...(r?.recipe?{recipe:r.recipe}:{}),
|
|
355
356
|
coverage_at_run:r?.coverage||null,declared_inputs_match:null,reuse_eligible:false,
|
|
356
357
|
...(r?.runFacts?.length?{run_facts:r.runFacts.length}:{})};
|
|
357
358
|
if(receiptStateError){item.reason=receiptStateError;return item;}
|
|
@@ -794,8 +795,8 @@ function markInputEvent(name,rel,type,replay) {
|
|
|
794
795
|
if(!hasHistory||historyDisabled||replay){
|
|
795
796
|
const serial=(inputEventSerial.get(name)||0)+1;
|
|
796
797
|
inputEventSerial.set(name,serial);
|
|
797
|
-
// Kept only for test checks; trimmed in batches.
|
|
798
|
-
if(index?.checks.get(name)?.plan.qualification
|
|
798
|
+
// Kept only for recorded test and script checks; trimmed in batches.
|
|
799
|
+
if(['npm-tests-direct-v1','node-script-v1'].includes(index?.checks.get(name)?.plan.qualification)){
|
|
799
800
|
const list=inputEventPaths.get(name)||[];list.push([serial,rel]);
|
|
800
801
|
if(list.length>2*eventPathLimit)list.splice(0,list.length-eventPathLimit);inputEventPaths.set(name,list);
|
|
801
802
|
}
|
package/src/declared-plan.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {permittedRoots,resolveDeclaredInstalled} from './installed-inputs.mjs';
|
|
|
7
7
|
import {qualifyTypeScript} from './typescript-qualification.mjs';
|
|
8
8
|
import {directBuildInfo} from './direct-typescript.mjs';
|
|
9
9
|
import {qualifyTests,testWorkspacePatterns,testsQualification} from './test-direct.mjs';
|
|
10
|
+
import {qualifyNodeScript,scriptQualification} from './node-script.mjs';
|
|
10
11
|
import {yarnWorkspaceTypecheckInputs} from './yarn-workspace-inputs.mjs';
|
|
11
12
|
import {pnpmTypecheckInputs} from './pnpm-inputs.mjs';
|
|
12
13
|
import {checkRun} from './typescript-invocation.mjs';
|
|
@@ -82,6 +83,7 @@ export function discoverDeclared(config) {
|
|
|
82
83
|
[...(selected.installedInputs||[]),...(workspace?.installed||[]),...excluded],allowedRoots);
|
|
83
84
|
timing('plan.installed_resolution',installedStarted,{mappings:installed.mappings.length});
|
|
84
85
|
const automatic=selected.qualification===testsQualification?qualifyTests(root,selected):
|
|
86
|
+
selected.qualification===scriptQualification?qualifyNodeScript(root,selected):
|
|
85
87
|
qualifyTypeScript(root,selected,{workspace,issue:workspaceIssue});
|
|
86
88
|
const sourcePatterns=[...new Set([...selected.inputs,...(workspace?.source||[]),
|
|
87
89
|
...(workspace?.generated||[]),...excluded])].sort();
|
|
@@ -92,9 +94,10 @@ export function discoverDeclared(config) {
|
|
|
92
94
|
...(workspaceIssue?[workspaceIssue]:[])];
|
|
93
95
|
for(const item of workspace?.configurationInputs||[])configurationInputs.add(item);
|
|
94
96
|
for(const item of workspace?.workspacePatterns||[])workspacePatterns.add(item);
|
|
95
|
-
// A workspace test check
|
|
96
|
-
// package manifest appearing or pnpm's
|
|
97
|
-
|
|
97
|
+
// A workspace test check, and a general check in a workspace, is qualified
|
|
98
|
+
// against the current package list: a package manifest appearing or pnpm's
|
|
99
|
+
// package list changing re-plans.
|
|
100
|
+
if(selected.qualification===testsQualification&&selected.workspace||selected.qualification===scriptQualification)try{
|
|
98
101
|
for(const item of testWorkspacePatterns(root,JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'))))
|
|
99
102
|
workspacePatterns.add(item);
|
|
100
103
|
if(fs.existsSync(path.join(root,'pnpm-workspace.yaml')))configurationInputs.add('pnpm-workspace.yaml');
|
package/src/index.mjs
CHANGED
|
@@ -55,7 +55,7 @@ function fileHash(root, rel,installed=false,before=fs.lstatSync(path.join(root,r
|
|
|
55
55
|
const target=fs.statSync(resolution.physical,{bigint:true});
|
|
56
56
|
const physical=path.relative(root,resolution.physical).split(path.sep).join('/');
|
|
57
57
|
const directory=target.isDirectory()&&installed&&observedDir?.(physical);
|
|
58
|
-
if(!target.isFile()&&!directory)throw Error(`directory input symlink unsupported: ${rel}`);
|
|
58
|
+
if(!target.isFile()&&!directory)throw Error(`directory input symlink unsupported: ${rel} -> ${physical}`);
|
|
59
59
|
if(directory)value='dirlink:'+JSON.stringify(resolution.links)+':'+physical;
|
|
60
60
|
else{
|
|
61
61
|
if(installed)racy=racyUntil(target);else bytes=Number(target.size);
|
|
@@ -192,10 +192,16 @@ export class InputIndex {
|
|
|
192
192
|
const installed=this.installedPath(rel);
|
|
193
193
|
try{const started=performance.now();
|
|
194
194
|
// A linked directory counts as observed only strictly inside an
|
|
195
|
-
// installed root,
|
|
196
|
-
//
|
|
195
|
+
// installed root, or as a checkout directory outside node_modules (a
|
|
196
|
+
// workspace package the package manager links), and only if every
|
|
197
|
+
// check matching the link also observes the directory's own files
|
|
198
|
+
// (its package.json stands for them).
|
|
199
|
+
// The second form is for recorded checks only: their recorder
|
|
200
|
+
// classifies a read through the link where it leads.
|
|
201
|
+
const recorded=name=>['node-script-v1','npm-tests-direct-v1'].includes(this.checks.get(name).plan.qualification);
|
|
197
202
|
const observedDir=installed?physical=>!this.includeInstalled&&
|
|
198
|
-
[...this.installedRootSet].some(root=>physical.startsWith(root+'/'))
|
|
203
|
+
([...this.installedRootSet].some(root=>physical.startsWith(root+'/'))||
|
|
204
|
+
!physical.split('/').includes('node_modules')&&!this.installedPath(physical+'/package.json')&&matched.every(recorded))&&
|
|
199
205
|
matched.every(name=>this.matches(this.checks.get(name),physical+'/package.json')):null;
|
|
200
206
|
inspection=fileHash(this.root,rel,installed,stat,observedDir);
|
|
201
207
|
fresh=inspection.hash;
|
package/src/node-onboarding.mjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import {directProject,directQualification} from './direct-typescript.mjs';
|
|
2
|
-
import {testProject,testRecipeScript,testCheckInputs,testsQualification} from './test-direct.mjs';
|
|
2
|
+
import {testProject,testRecipeScript,testCheckInputs,testEnvironmentIssue,testsQualification} from './test-direct.mjs';
|
|
3
|
+
import {commandLineIssue,scriptCheckInputs,scriptPath,scriptProject,scriptQualification} from './node-script.mjs';
|
|
4
|
+
// Init proposes the general recipe for typecheck and lint scripts; test
|
|
5
|
+
// scripts use the direct test recipe (Vitest, Jest).
|
|
6
|
+
const generalKinds=['typecheck','lint'];
|
|
3
7
|
import fs from 'node:fs';
|
|
4
8
|
import path from 'node:path';
|
|
5
9
|
import {spawnSync} from 'node:child_process';
|
|
@@ -130,7 +134,8 @@ function sourceInputs(root,kind,lockfiles) {
|
|
|
130
134
|
'!node_modules/**','!.git/**'])];
|
|
131
135
|
const dirs=likelyDirs.filter(name=>exists(path.join(root,name)))
|
|
132
136
|
.map(name=>`${name}/**`);
|
|
133
|
-
|
|
137
|
+
// A workspace package's node_modules under them is installed, not source.
|
|
138
|
+
return [...new Set([...files,...(dirs.length?[...dirs,'!**/node_modules/**']:['package.json'])])];
|
|
134
139
|
}
|
|
135
140
|
function classify(kind,scriptName,script,pkg,managerInfo,root) {
|
|
136
141
|
const value=script.trim();
|
|
@@ -191,8 +196,27 @@ export function workspacePackages(root){
|
|
|
191
196
|
return workspaceManifests(root,patterns).map(({file,pkg:member})=>
|
|
192
197
|
({name:member.name,dir:path.posix.dirname(file),scripts:member.scripts&&typeof member.scripts==='object'?member.scripts:{}}));
|
|
193
198
|
}
|
|
199
|
+
// The general recipe for a root script, or why it does not apply: a
|
|
200
|
+
// supported package manager and platform, and a script (with its pre and post
|
|
201
|
+
// scripts) that, read as text, starts only Node programs.
|
|
202
|
+
function generalCheck(root,kind,script){
|
|
203
|
+
if(!['darwin','linux'].includes(process.platform))throw Error('the general recipe records runs on macOS and Linux only');
|
|
204
|
+
const environmentIssue=testEnvironmentIssue();
|
|
205
|
+
if(environmentIssue)throw Error(environmentIssue);
|
|
206
|
+
let project;
|
|
207
|
+
try{project=scriptProject(root,script);}catch(e){throw Error(e.code?.replace(/^test-/,'')||e.message);}
|
|
208
|
+
for(const name of ['pre'+script,script,'post'+script]){
|
|
209
|
+
if(typeof project.scripts[name]!=='string')continue;
|
|
210
|
+
const issue=commandLineIssue(project.scripts[name],{cwd:root,searchPath:scriptPath(root,process.env.PATH)});
|
|
211
|
+
if(issue)throw Error(name===script?issue:`${name}: ${issue}`);
|
|
212
|
+
}
|
|
213
|
+
return {config:{name:kind,script,kind,qualification:scriptQualification,...scriptCheckInputs(project.dirs)},manager:project.manager};
|
|
214
|
+
}
|
|
194
215
|
export function discoverNodeProject(root,{recipe=null}={}) {
|
|
195
|
-
if(recipe&&!['typescript-direct','tests-direct'].includes(recipe))
|
|
216
|
+
if(recipe&&!['typescript-direct','tests-direct','node-script'].includes(recipe))
|
|
217
|
+
throw Error('unknown recipe; use typescript-direct, tests-direct or node-script');
|
|
218
|
+
// Without --recipe, init tries the strict recipes first, then the general one.
|
|
219
|
+
const auto=recipe===null;
|
|
196
220
|
root=realObservedPath(root);
|
|
197
221
|
const packageFile=path.join(root,'package.json');
|
|
198
222
|
if(!exists(packageFile))throw Error('run init from a repository containing package.json');
|
|
@@ -225,7 +249,7 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
225
249
|
if(workspaces&&classification.qualification==='typescript-noemit-v1')classification={level:'recording',
|
|
226
250
|
reason:'workspace task and installed-input closure need explicit qualification'};
|
|
227
251
|
const check={name:kind,script,kind,inputs:sourceInputs(root,kind,pm.lockfiles||[])};
|
|
228
|
-
if(recipe==='typescript-direct'&&kind==='typecheck'){
|
|
252
|
+
if((recipe==='typescript-direct'||auto&&pm.name==='npm'&&!workspaces)&&kind==='typecheck'){
|
|
229
253
|
try{
|
|
230
254
|
if(pm.name!=='npm'||workspaces||observationIssue||pm.issue)throw Error('direct recipe requires a supported single-package npm checkout');
|
|
231
255
|
let version;
|
|
@@ -236,7 +260,7 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
236
260
|
reason:`explicit local TypeScript ${version} compilation; npm launcher/lifecycle scripts are not executed; package.json stays unchanged`};
|
|
237
261
|
}catch(e){classification={level:'recording',reason:`Direct recipe unavailable (${e.code||e.message}); original npm script retained, without bypassing required behavior`};}
|
|
238
262
|
}
|
|
239
|
-
if(recipe==='tests-direct'&&kind==='test'){
|
|
263
|
+
if((recipe==='tests-direct'||auto)&&kind==='test'){
|
|
240
264
|
try{
|
|
241
265
|
if(!['npm','pnpm','yarn'].includes(pm.name)||workspaces||observationIssue||pm.issue)
|
|
242
266
|
throw Error('test recipe requires a supported single-package npm, pnpm or Yarn checkout');
|
|
@@ -250,7 +274,7 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
250
274
|
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`};
|
|
251
275
|
}catch(e){classification={level:'recording',reason:`Test recipe unavailable (${e.code||e.message}); original npm script retained`};}
|
|
252
276
|
}
|
|
253
|
-
if(pm.name==='pnpm'&&kind==='typecheck'&&!workspaces&&!pm.issue&&(script===undefined||tscRuns(scripts[script]))){
|
|
277
|
+
if(recipe!=='node-script'&&pm.name==='pnpm'&&kind==='typecheck'&&!workspaces&&!pm.issue&&(script===undefined||tscRuns(scripts[script]))){
|
|
254
278
|
try{
|
|
255
279
|
parts=tscChecks('typecheck',script??null,scripts,run=>{
|
|
256
280
|
const contract=pnpmTypecheckInputs(root,'.',run);
|
|
@@ -263,6 +287,14 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
263
287
|
qualification:'pnpm-tsc-v1'};}
|
|
264
288
|
catch(e){classification={level:'recording',reason:`pnpm TypeScript contract needs review: ${e.message}`};}
|
|
265
289
|
}
|
|
290
|
+
if((auto||recipe==='node-script')&&script&&generalKinds.includes(kind)&&classification.level!=='ready'&&!observationIssue){
|
|
291
|
+
try{
|
|
292
|
+
const general=generalCheck(root,kind,script);
|
|
293
|
+
checks.push({config:general.config,level:'ready',qualification:scriptQualification,
|
|
294
|
+
reason:`general recipe: ${general.manager} runs script ${JSON.stringify(script)} with REDUE's recorder in every Node process; the whole checkout is its input`});
|
|
295
|
+
continue;
|
|
296
|
+
}catch(e){classification={...classification,reason:`General recipe unavailable (${e.message}); ${classification.reason}`};}
|
|
297
|
+
}
|
|
266
298
|
if(!script&&classification.level!=='ready')continue;
|
|
267
299
|
if(parts&&classification.level==='ready'){
|
|
268
300
|
for(const part of parts)checks.push({config:{...check,...part,qualification:classification.qualification},...classification});
|
|
@@ -302,7 +334,7 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
302
334
|
['@which:pnpm','--filter',name,'run',selected.script];
|
|
303
335
|
const check={name:`${name}:${kind}`,kind,command,
|
|
304
336
|
inputs:['package.json',...(pm.lockfiles||[]),`${dir}/**`,
|
|
305
|
-
`!${dir}/node_modules/**`]};
|
|
337
|
+
`!${dir}/node_modules/**`,`!${dir}/**/node_modules/**`]};
|
|
306
338
|
let level=pm.issue&&pm.name!=='pnpm'?'unsupported':'recording';
|
|
307
339
|
let reason=pm.issue||'workspace script found; shared configuration and installed inputs need review';
|
|
308
340
|
if(!pm.issue&&pm.name==='yarn'&&pm.layout==='node-modules/classic'&&
|