@redue/cli 0.0.0-stage → 0.1.0-alpha.2

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +180 -0
  3. package/README.md +123 -2
  4. package/bin/redue.mjs +329 -0
  5. package/bin/vstate.mjs +3 -0
  6. package/docs/README.md +20 -0
  7. package/docs/user/agents.md +32 -0
  8. package/docs/user/configuration.md +57 -0
  9. package/docs/user/getting-started.md +127 -0
  10. package/docs/user/json.md +59 -0
  11. package/docs/user/shell.md +25 -0
  12. package/docs/user/supported-projects.md +89 -0
  13. package/examples/redue.config.json +8 -0
  14. package/package.json +46 -4
  15. package/src/cached-state.mjs +27 -0
  16. package/src/cli.mjs +390 -0
  17. package/src/control-endpoint.mjs +11 -0
  18. package/src/daemon.mjs +989 -0
  19. package/src/decision-profile.mjs +18 -0
  20. package/src/decision-validation.mjs +41 -0
  21. package/src/declared-plan.mjs +125 -0
  22. package/src/executable-lookup.mjs +18 -0
  23. package/src/glob.mjs +9 -0
  24. package/src/history-worker.mjs +18 -0
  25. package/src/index.mjs +306 -0
  26. package/src/installed-inputs.mjs +110 -0
  27. package/src/linux-inotify.py +228 -0
  28. package/src/linux-observer.mjs +79 -0
  29. package/src/node-onboarding.mjs +263 -0
  30. package/src/npm-typecheck-probe.mjs +120 -0
  31. package/src/observation.mjs +37 -0
  32. package/src/owned-state.mjs +32 -0
  33. package/src/path-identity.mjs +43 -0
  34. package/src/plan-worker.mjs +131 -0
  35. package/src/plan.mjs +10 -0
  36. package/src/platform-observation.mjs +17 -0
  37. package/src/platform-state.mjs +14 -0
  38. package/src/pnpm-inputs.mjs +355 -0
  39. package/src/pnpm-typecheck-probe.mjs +47 -0
  40. package/src/presentation.mjs +52 -0
  41. package/src/probe-result.mjs +21 -0
  42. package/src/process-liveness.mjs +11 -0
  43. package/src/run-lock.mjs +77 -0
  44. package/src/state-store.mjs +37 -0
  45. package/src/toolchain-probe.mjs +21 -0
  46. package/src/typescript-contract-probe.mjs +188 -0
  47. package/src/typescript-list.mjs +68 -0
  48. package/src/typescript-qualification.mjs +96 -0
  49. package/src/windows-command.mjs +16 -0
  50. package/src/windows-process.mjs +15 -0
  51. package/src/yarn-workspace-inputs.mjs +148 -0
  52. package/src/yarn-workspace-typecheck-probe.mjs +107 -0
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ // Read-only applicability probe for the deliberately narrow npm/tsc --noEmit
3
+ // contract. It emits one digest; failures emit only a fixed diagnostic code.
4
+ import {compilerFiles} from './typescript-list.mjs';
5
+ import {contextOnly,probeResult} from './probe-result.mjs';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import {createHash} from 'node:crypto';
10
+ import {createRequire} from 'node:module';
11
+ import {spawnSync} from 'node:child_process';
12
+ import {withinPath as within,realObservedPath} from './path-identity.mjs';
13
+ import {timing} from './decision-profile.mjs';
14
+
15
+ const [rootArg,scriptName,npmArg]=process.argv.slice(2);
16
+ const hash=value=>createHash('sha256').update(value).digest('hex');
17
+ const fail=code=>{throw Object.assign(Error(code),{code});};
18
+ const digestFile=file=>hash(fs.readFileSync(file));
19
+ const require=createRequire(import.meta.url);
20
+ function treeHash(dir){
21
+ const digest=createHash('sha256');let count=0;
22
+ const visit=(folder,rel='')=>{
23
+ for(const entry of fs.readdirSync(folder,{withFileTypes:true}).sort((a,b)=>
24
+ a.name.localeCompare(b.name))){
25
+ const name=rel?rel+'/'+entry.name:entry.name,full=path.join(folder,entry.name);
26
+ if(entry.isDirectory())visit(full,name);
27
+ else if(entry.isFile()){digest.update(name+'\0'+digestFile(full)+'\0');count++;}
28
+ else fail('npm-installation-unobservable');
29
+ }
30
+ };
31
+ visit(dir);return [digest.digest('hex'),count];
32
+ }
33
+ function npmRcKeys(file){
34
+ if(!fs.existsSync(file))return null;
35
+ const keys=[];
36
+ for(const line of fs.readFileSync(file,'utf8').split(/\r?\n/)){
37
+ const trimmed=line.trim();if(!trimmed||/^[#;]/.test(trimmed))continue;
38
+ const at=trimmed.indexOf('=');if(at<0)fail('npm-configuration-unsupported');
39
+ const key=trimmed.slice(0,at).trim();
40
+ if(key!=='registry'&&!/^\/\/[^\s]+\/:_authToken$/.test(key))
41
+ fail('npm-configuration-unsupported');
42
+ keys.push(key);
43
+ }
44
+ return keys.sort();
45
+ }
46
+ try{
47
+ if(!rootArg||!scriptName||!npmArg)fail('probe-arguments');
48
+ const root=realObservedPath(rootArg),npm=realObservedPath(npmArg);
49
+ const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
50
+ if(pkg.scripts?.[scriptName]?.trim()!=='tsc --noEmit'||
51
+ pkg.scripts?.['pre'+scriptName]||pkg.scripts?.['post'+scriptName])
52
+ fail('typecheck-script-changed');
53
+ if(process.env.NODE_OPTIONS||process.env.NODE_PATH||process.env.BASH_ENV||
54
+ process.env.ENV||Object.entries(process.env).some(([key,value])=>{
55
+ if(/^npm_config_prefix$/i.test(key)&&process.platform==='win32'){
56
+ // The Windows runner supplies a global npm prefix independently of
57
+ // the selected local compiler. Its value is part of caller context.
58
+ return !path.isAbsolute(value);
59
+ }
60
+ return /^(npm_config_|DYLD_|TSGO_)/i.test(key);
61
+ }))fail('execution-environment-unsupported');
62
+ let npmRoot;
63
+ if(process.platform==='win32'){
64
+ if(path.basename(npm).toLowerCase()!=='npm.cmd')
65
+ fail('npm-installation-layout-unsupported');
66
+ npmRoot=path.join(path.dirname(npm),'node_modules','npm');
67
+ const body=fs.readFileSync(npm,'utf8');
68
+ if(!body.includes('node_modules\\npm\\bin\\npm-cli.js')||
69
+ !body.includes('node_modules\\npm\\bin\\npm-prefix.js')||
70
+ !body.includes('"%NODE_EXE%" "%NPM_CLI_JS%" %*'))
71
+ fail('npm-installation-layout-unsupported');
72
+ const prefix=spawnSync(process.execPath,[path.join(npmRoot,'bin','npm-prefix.js')],
73
+ {cwd:root,env:process.env,encoding:'utf8',timeout:4000,stdio:['ignore','pipe','ignore']});
74
+ const selectedPrefix=prefix.stdout?.trim();
75
+ if(prefix.error||prefix.status!==0||!selectedPrefix||
76
+ !path.isAbsolute(selectedPrefix))fail('npm-installation-layout-unsupported');
77
+ // npm.cmd selects the prefix's CLI only when that file exists. Otherwise
78
+ // it runs the bundled CLI beside npm.cmd; observe the same branch here.
79
+ const alternate=path.join(selectedPrefix,'node_modules','npm','bin','npm-cli.js');
80
+ if(fs.existsSync(alternate)&&
81
+ realObservedPath(alternate)!==realObservedPath(path.join(npmRoot,'bin','npm-cli.js')))
82
+ fail('npm-installation-layout-unsupported');
83
+ }else npmRoot=path.dirname(path.dirname(npm));
84
+ if(path.basename(npmRoot)!=='npm'||path.basename(path.dirname(npmRoot))!=='node_modules')
85
+ fail('npm-installation-layout-unsupported');
86
+ const home=process.platform==='win32'?os.homedir():process.env.HOME;
87
+ if(!home||!path.isAbsolute(home))fail('home-context-unavailable');
88
+ const npmConfig=[path.join(root,'.npmrc'),path.join(home,'.npmrc')]
89
+ .map(file=>[hash(file),npmRcKeys(file)]);
90
+ const contextFacts={
91
+ npmTree:treeHash(npmRoot),npmConfig,
92
+ node:hash(JSON.stringify([process.version,realObservedPath(process.execPath),
93
+ fs.statSync(process.execPath).size])),
94
+ shell:process.platform==='win32'?hash(JSON.stringify([
95
+ realObservedPath(process.env.ComSpec||process.env.COMSPEC||''),
96
+ digestFile(process.env.ComSpec||process.env.COMSPEC||''),
97
+ process.env.PATHEXT||'',digestFile(npm),
98
+ digestFile(path.join(root,'node_modules','.bin','tsc.cmd'))])):
99
+ digestFile('/bin/sh')};
100
+ const contextHash=hash(JSON.stringify(contextFacts));
101
+ if(contextOnly(contextHash))process.exit(0);
102
+ const tsRoot=path.join(root,'node_modules','typescript');
103
+ let tsc;
104
+ if(process.platform==='win32'){
105
+ const shim=path.join(root,'node_modules','.bin','tsc.cmd');
106
+ const body=fs.readFileSync(shim,'utf8').replace(/\r\n/g,'\n').trim();
107
+ const expected=`@ECHO off
108
+ GOTO start
109
+ :find_dp0
110
+ SET dp0=%~dp0
111
+ EXIT /b
112
+ :start
113
+ SETLOCAL
114
+ CALL :find_dp0
115
+
116
+ IF EXIST "%dp0%\\node.exe" (
117
+ SET "_prog=%dp0%\\node.exe"
118
+ ) ELSE (
119
+ SET "_prog=node"
120
+ SET PATHEXT=%PATHEXT:;.JS;=;%
121
+ )
122
+
123
+ endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\..\\typescript\\bin\\tsc" %*`;
124
+ if(body!==expected)fail('typescript-executable-changed');
125
+ tsc=realObservedPath(path.join(tsRoot,'bin','tsc'));
126
+ }else{
127
+ tsc=realObservedPath(path.join(root,'node_modules','.bin','tsc'));
128
+ if(tsc!==path.join(tsRoot,'bin','tsc'))fail('typescript-executable-changed');
129
+ }
130
+ const configReads=new Set(),visited=new Set();
131
+ let ts;try{ts=require(tsRoot);}catch{}
132
+ if(typeof ts?.getParsedCommandLineOfConfigFile==='function'){
133
+ const host={...ts.sys,readFile(file){configReads.add(path.resolve(file));
134
+ return ts.sys.readFile(file);},onUnRecoverableConfigFileDiagnostic(){}};
135
+ const parseConfig=file=>{
136
+ const absolute=path.resolve(file);
137
+ if(!within(absolute,root))fail('typescript-config-outside-repository');
138
+ if(visited.has(absolute))return;visited.add(absolute);
139
+ if(visited.size>64)fail('typescript-config-graph-too-large');
140
+ const parsed=ts.getParsedCommandLineOfConfigFile(absolute,{},host);
141
+ if(!parsed||parsed.errors?.length)fail('typescript-config-unavailable');
142
+ if(parsed.options?.plugins?.length)fail('typescript-plugin-unsupported');
143
+ if(parsed.options?.incremental||parsed.options?.composite)
144
+ fail('typescript-incremental-unsupported');
145
+ for(const reference of parsed.projectReferences||[])
146
+ parseConfig(reference.path.endsWith('.json')?reference.path:
147
+ path.join(reference.path,'tsconfig.json'));
148
+ };
149
+ parseConfig(path.join(root,'tsconfig.json'));
150
+ }else{
151
+ // TypeScript 7's native compiler does not expose the previous config API.
152
+ // Its CLI reports the effective config; restrict this path to one direct
153
+ // config rather than guessing about extends/reference provenance.
154
+ const config=path.join(root,'tsconfig.json'),raw=fs.readFileSync(config,'utf8');
155
+ if(/"(?:extends|references|plugins|incremental|composite)"\s*:/.test(raw))
156
+ fail('typescript-config-graph-unsupported');
157
+ const shown=spawnSync(process.execPath,[tsc,'--showConfig'],{cwd:root,
158
+ env:process.env,encoding:'utf8',timeout:4000,maxBuffer:16*1024*1024,
159
+ stdio:['ignore','pipe','pipe']});
160
+ if(shown.error||shown.signal||shown.status!==0)fail('typescript-config-unavailable');
161
+ const effective=JSON.parse(shown.stdout);
162
+ if(effective.compilerOptions?.plugins?.length||
163
+ effective.compilerOptions?.incremental||effective.compilerOptions?.composite)
164
+ fail('typescript-config-graph-unsupported');
165
+ configReads.add(config);
166
+ }
167
+ for(const file of configReads)if(!within(realObservedPath(file),root))
168
+ fail('typescript-config-outside-repository');
169
+ const listingStarted=performance.now();
170
+ const captured=process.argv.includes('--redue-checkpoint')?compilerFiles(tsRoot,root):null;
171
+ const listed=captured?{status:0,stdout:captured.files.join('\n')}:spawnSync(process.execPath,[tsc,'--noEmit','--listFilesOnly'],
172
+ {cwd:root,env:process.env,encoding:'utf8',timeout:process.platform==='win32'?10000:4000,
173
+ maxBuffer:16*1024*1024,
174
+ stdio:['ignore','pipe','pipe']});
175
+ timing('typescript_contract.list_files',listingStarted,{status:listed.status??null,
176
+ error:listed.error?.code||null});
177
+ if(listed.error||listed.signal||listed.status!==0)fail('typescript-input-list-unavailable');
178
+ const files=listed.stdout.trim().split(/\r?\n/).filter(Boolean)
179
+ .map(file=>path.resolve(file));
180
+ if(!files.length||files.some(file=>!within(realObservedPath(file),root)))
181
+ fail('typescript-input-outside-repository');
182
+ const facts={schema:1,root:hash(root),script:scriptName,
183
+ configs:[...configReads].sort().map(file=>[hash(file),digestFile(file)]),
184
+ files:[...new Set(files)].sort().map(file=>[hash(file),digestFile(file)]),
185
+ ...contextFacts};
186
+ probeResult(hash(JSON.stringify(facts)),contextHash,captured?.queries);
187
+ }catch(e){console.error(`VSTATE_REASON:${e.code&&/^[a-z-]+$/.test(e.code)?e.code:
188
+ 'typescript-contract-unavailable'}`);process.exitCode=2;}
@@ -0,0 +1,68 @@
1
+ // TypeScript's compiler host exposes the filesystem questions used to resolve
2
+ // a program. Capture those answers, not runtime application accesses. Reuse is
3
+ // allowed only while every answer and the observed input content still matches.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import {createHash} from 'node:crypto';
7
+ import {createRequire} from 'node:module';
8
+ const require=createRequire(import.meta.url);
9
+ const hash=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex');
10
+ export function queryAnswer(kind,file,options) {
11
+ try{
12
+ if(kind==='readDirectory')return require(options.tsRoot).sys.readDirectory(file,...options.args).sort();
13
+ if(kind==='readFile')return hash(fs.readFileSync(file,'utf8'));
14
+ if(kind==='fileExists')return fs.statSync(file).isFile();
15
+ if(kind==='directoryExists')return fs.statSync(file).isDirectory();
16
+ if(kind==='realpath')return fs.realpathSync(file);
17
+ if(kind==='directories')return fs.readdirSync(file,{withFileTypes:true})
18
+ .filter(entry=>entry.isDirectory()||entry.isSymbolicLink()&&
19
+ fs.statSync(path.join(file,entry.name)).isDirectory()).map(entry=>entry.name).sort();
20
+ // Directory entries include names/types, including newly created candidates.
21
+ if(kind==='entries')return fs.readdirSync(file,{withFileTypes:true})
22
+ .map(entry=>[entry.name,entry.isDirectory()?'d':entry.isSymbolicLink()?'l':'f']).sort();
23
+ throw Error('unknown compiler filesystem question');
24
+ }catch(error){if(['ENOENT','ENOTDIR'].includes(error.code))return null;throw error;}
25
+ }
26
+ export function queriesMatch(queries) {
27
+ if(!queries?.length)return false;
28
+ return queries.every(([kind,file,answer,options])=>
29
+ JSON.stringify(queryAnswer(kind,file,options))===JSON.stringify(answer));
30
+ }
31
+ export function compilerFiles(tsRoot,cwd) {
32
+ const require=createRequire(import.meta.url),ts=require(tsRoot);
33
+ if(typeof ts.createProgram!=='function'||typeof ts.createCompilerHost!=='function')return null;
34
+ const queries=new Map();
35
+ const remember=(kind,file)=>{
36
+ const full=path.resolve(file),answer=queryAnswer(kind,full);
37
+ queries.set(kind+'\0'+full,[kind,full,answer]);return answer;
38
+ };
39
+ const readFile=file=>{remember('readFile',file);return ts.sys.readFile(file);};
40
+ const fileExists=file=>{remember('fileExists',file);return ts.sys.fileExists(file);};
41
+ const directoryExists=file=>{remember('directoryExists',file);return ts.sys.directoryExists(file);};
42
+ const getDirectories=file=>{remember('directories',file);return ts.sys.getDirectories(file);};
43
+ const readDirectory=(dir,...args)=>{
44
+ const result=ts.sys.readDirectory(dir,...args);
45
+ const full=path.resolve(dir),options={tsRoot,args};
46
+ queries.set('readDirectory\0'+full+JSON.stringify(args),
47
+ ['readDirectory',full,[...result].sort(),options]);
48
+ return result;
49
+ };
50
+ const parsed=ts.getParsedCommandLineOfConfigFile(path.join(cwd,'tsconfig.json'),{},
51
+ {...ts.sys,readFile,fileExists,directoryExists,getDirectories,readDirectory,
52
+ onUnRecoverableConfigFileDiagnostic(){}});
53
+ if(!parsed||parsed.errors?.length)throw Error('TypeScript configuration unavailable');
54
+ const host=ts.createCompilerHost(parsed.options);
55
+ Object.assign(host,{readFile,fileExists,directoryExists,getDirectories,
56
+ realpath(file){remember('realpath',file);return ts.sys.realpath(file);},
57
+ getSourceFile(file,languageVersion,onError){
58
+ const text=readFile(file);return text===undefined?undefined:
59
+ ts.createSourceFile(file,text,languageVersion);
60
+ }});
61
+ const program=ts.createProgram({rootNames:parsed.fileNames,options:parsed.options,
62
+ projectReferences:parsed.projectReferences,host});
63
+ const files=program.getSourceFiles().map(file=>path.resolve(file.fileName));
64
+ // Stable answers are necessary even at first capture; a concurrent edit
65
+ // cannot become the basis for a reusable discovery result.
66
+ const facts=[...queries.values()];
67
+ return {files,queries:queriesMatch(facts)?facts:null};
68
+ }
@@ -0,0 +1,96 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {realObservedPath} from './path-identity.mjs';
4
+
5
+ // This is a machine-enforced contract, not a claim inferred from a script name.
6
+ // The read-only runtime probe separately checks the effective TypeScript file
7
+ // list, config graph, npm toolchain and caller context on every sync/run.
8
+ export function qualifyTypeScript(root,selected,discovered={}) {
9
+ if(selected.qualification==='pnpm-tsc-v1'){
10
+ const issues=[];
11
+ const workspace=selected.workspace||'.';
12
+ const script=selected.script||selected.command?.at(-1);
13
+ try{
14
+ if(discovered.issue)throw Error(discovered.issue);
15
+ const contract=discovered.workspace;
16
+ if(!contract)throw Error('pnpm TypeScript input discovery unavailable');
17
+ issues.push(...contract.limitations);
18
+ const declared=contract.workspace.pkg.scripts[script].trim().split(/\s+/).slice(1);
19
+ if(path.resolve(root,selected.cwd||'.')!==path.join(root,contract.workspace.dir)||
20
+ selected.command?.[0]!==process.execPath||
21
+ selected.command?.[1]!==contract.tsc||
22
+ JSON.stringify(selected.command.slice(2))!==JSON.stringify(declared))
23
+ issues.push('standalone TypeScript invocation changed');
24
+ }catch(e){issues.push(e.message);}
25
+ if(!selected.probes?.some(argv=>path.basename(argv[1]||'')==='pnpm-typecheck-probe.mjs'))
26
+ issues.push('pnpm TypeScript input probe missing');
27
+ if(!selected.environment?.executableIdentity||
28
+ !['node'].every(name=>selected.environment?.pathExecutables?.includes(name))||
29
+ !['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'].every(name=>
30
+ selected.environment?.prefixes?.includes(name)))
31
+ issues.push('pnpm execution context declaration incomplete');
32
+ return {qualified:issues.length===0,issues};
33
+ }
34
+ if(selected.qualification==='yarn-workspace-tsc-v1'){
35
+ const issues=[];
36
+ try{
37
+ if(!selected.workspace||selected.command?.length!==5||
38
+ selected.command[1]!=='workspace'||selected.command[2]!==selected.workspace||
39
+ selected.command[3]!=='run')issues.push('workspace command changed');
40
+ else if(discovered.issue)throw Error(discovered.issue);
41
+ else if(!discovered.workspace)throw Error('Yarn workspace input discovery unavailable');
42
+ }catch(e){issues.push(e.message);}
43
+ if(!selected.probes?.some(argv=>path.basename(argv[1]||'')==='yarn-workspace-typecheck-probe.mjs'))
44
+ issues.push('workspace TypeScript input probe missing');
45
+ if(!selected.environment?.executableIdentity||
46
+ !['node','yarn'].every(name=>selected.environment?.pathExecutables?.includes(name))||
47
+ !['YARN_','COREPACK_','npm_config_','DYLD_','TSGO_'].every(name=>
48
+ selected.environment?.prefixes?.includes(name)))
49
+ issues.push('Yarn execution context declaration incomplete');
50
+ return {qualified:issues.length===0,issues};
51
+ }
52
+ if(selected.qualification!=='typescript-noemit-v1')return null;
53
+ const issues=[];
54
+ let pkg;
55
+ try{pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));}
56
+ catch{issues.push('package manifest unavailable');}
57
+ if(!selected.script||pkg?.scripts?.[selected.script]?.trim()!=='tsc --noEmit'||
58
+ pkg?.scripts?.['pre'+selected.script]||pkg?.scripts?.['post'+selected.script])
59
+ issues.push('typecheck script is not the supported standalone tsc --noEmit invocation');
60
+ if(selected.command?.length!==3||selected.command[1]!=='run'||
61
+ selected.command[2]!==selected.script)
62
+ issues.push('package-manager invocation differs from the discovered script');
63
+ try{const npm=fs.realpathSync(selected.command[0]);
64
+ const supported=process.platform==='win32'?path.basename(npm).toLowerCase()==='npm.cmd'&&
65
+ fs.existsSync(path.join(path.dirname(npm),'node_modules','npm','bin','npm-cli.js')):
66
+ path.basename(npm)==='npm-cli.js'&&
67
+ path.basename(path.dirname(path.dirname(npm)))==='npm';
68
+ if(!supported)
69
+ issues.push('npm executable layout is not supported');}
70
+ catch{issues.push('npm executable unavailable');}
71
+ if(!fs.existsSync(path.join(root,'tsconfig.json')))
72
+ issues.push('tsconfig.json is missing');
73
+ if(!fs.existsSync(path.join(root,'node_modules','typescript','package.json'))||
74
+ !fs.existsSync(path.join(root,'node_modules','.bin',
75
+ process.platform==='win32'?'tsc.cmd':'tsc')))
76
+ issues.push('local TypeScript installation is missing');
77
+ if(!selected.inputs?.includes('**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json,jsonc}')||
78
+ !selected.inputs?.includes('package.json')||
79
+ !selected.inputs?.includes('tsconfig.json'))
80
+ issues.push('TypeScript source/config file-set declaration is incomplete');
81
+ if(!selected.installedInputs?.includes('node_modules/**'))
82
+ issues.push('installed package contents and membership are not declared');
83
+ if(!selected.environment?.executableIdentity||
84
+ !['node','npm'].every(name=>selected.environment?.pathExecutables?.includes(name))||
85
+ !['NODE_OPTIONS','NODE_PATH','HOME','BASH_ENV','ENV'].every(name=>
86
+ selected.environment?.variables?.includes(name))||
87
+ !['npm_config_','DYLD_','TSGO_'].every(name=>
88
+ selected.environment?.prefixes?.includes(name)))
89
+ issues.push('caller toolchain or execution context declaration is incomplete');
90
+ if(!selected.probes?.some(argv=>path.basename(argv[1]||'')==='typescript-contract-probe.mjs'&&
91
+ argv[2]&&fs.existsSync(argv[2])&&
92
+ realObservedPath(argv[2])===root&&argv[3]===selected.script&&
93
+ argv[4]===selected.command[0]))
94
+ issues.push('TypeScript input and toolchain probe is missing');
95
+ return {qualified:issues.length===0,issues};
96
+ }
@@ -0,0 +1,16 @@
1
+ import path from 'node:path';
2
+
3
+ // Windows cannot CreateProcess a .cmd/.bat file directly. Keep the logical
4
+ // argv in the receipt, and launch only the well-defined, quoted batch subset
5
+ // through cmd.exe. Unrepresentable arguments fail instead of being altered.
6
+ export function windowsLaunch(command,{platform=process.platform,
7
+ comspec=process.env.ComSpec||process.env.COMSPEC||'C:\\Windows\\System32\\cmd.exe'}={}) {
8
+ if(platform!=='win32')return {file:command[0],args:command.slice(1),options:{}};
9
+ if(!/\.(?:cmd|bat)$/i.test(path.win32.extname(command[0])))
10
+ return {file:command[0],args:command.slice(1),options:{}};
11
+ if(command.some(value=>/[\r\n"%!^&|<>]/.test(value)))
12
+ throw Error('Windows batch command contains characters that cannot be passed safely; use an executable or Node script as exact argv');
13
+ const line='"'+command.map(value=>'"'+value+'"').join(' ')+'"';
14
+ return {file:comspec,args:['/d','/s','/c',line],
15
+ options:{windowsVerbatimArguments:true,windowsHide:false},mechanism:'cmd-batch'};
16
+ }
@@ -0,0 +1,15 @@
1
+ import {spawn} from 'node:child_process';
2
+
3
+ // taskkill /T is the native narrow process-tree operation available without
4
+ // elevation. It cannot own deliberately detached descendants.
5
+ export function terminateWindowsTree(pid,{start=spawn,timeoutMs=5000}={}) {
6
+ if(!Number.isSafeInteger(pid)||pid<1)return Promise.reject(Error('invalid check PID'));
7
+ return new Promise((resolve,reject)=>{
8
+ const child=start('taskkill.exe',['/PID',String(pid),'/T','/F'],
9
+ {stdio:'ignore',windowsHide:true});
10
+ const timer=setTimeout(()=>{child.kill();reject(Error('taskkill deadline exceeded'));},timeoutMs);
11
+ child.once('error',error=>{clearTimeout(timer);reject(error);});
12
+ child.once('close',code=>{clearTimeout(timer);code===0?resolve():
13
+ reject(Error(`taskkill exited ${code}`));});
14
+ });
15
+ }
@@ -0,0 +1,148 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {execFileSync} from 'node:child_process';
4
+ import {createRequire} from 'node:module';
5
+ import micromatch from './glob.mjs';
6
+ import {withinPath as within,realObservedPath} from './path-identity.mjs';
7
+
8
+ export function yarnLocalTsc(root){
9
+ const expected=path.join(root,'node_modules','typescript','bin','tsc');
10
+ if(process.platform==='win32'){
11
+ const shim=path.join(root,'node_modules','.bin','tsc.cmd');
12
+ const body=fs.readFileSync(shim,'utf8').replace(/\r\n/g,'\n').trim();
13
+ const standard=`@IF EXIST "%~dp0\\node.exe" (
14
+ "%~dp0\\node.exe" "%~dp0\\..\\typescript\\bin\\tsc" %*
15
+ ) ELSE (
16
+ @SETLOCAL
17
+ @SET PATHEXT=%PATHEXT:;.JS;=;%
18
+ node "%~dp0\\..\\typescript\\bin\\tsc" %*
19
+ )`;
20
+ if(body!==standard)throw Error('local TypeScript executable is not the installed compiler');
21
+ return realObservedPath(expected);
22
+ }
23
+ const tsc=realObservedPath(path.join(root,'node_modules','.bin','tsc'));
24
+ if(tsc!==expected)throw Error('local TypeScript executable is not the installed compiler');
25
+ return tsc;
26
+ }
27
+
28
+ const posix=file=>file.split(path.sep).join('/');
29
+ const read=file=>JSON.parse(fs.readFileSync(file,'utf8'));
30
+ const relative=(root,file)=>posix(path.relative(root,file));
31
+ const safeDirectory=(root,dir)=>{
32
+ const full=path.resolve(root,dir);
33
+ if(!within(full,root))throw Error('workspace path escapes checkout');
34
+ return full;
35
+ };
36
+
37
+ export function yarnWorkspacePackages(root){
38
+ root=realObservedPath(root);
39
+ const pkg=read(path.join(root,'package.json'));
40
+ const patterns=Array.isArray(pkg.workspaces)?pkg.workspaces:pkg.workspaces?.packages;
41
+ if(!Array.isArray(patterns)||!patterns.length||patterns.some(value=>
42
+ typeof value!=='string'||path.isAbsolute(value)||value.split('/').includes('..')))
43
+ throw Error('workspace declarations are unsupported');
44
+ const files=execFileSync('git',['ls-files','--cached','--others','--exclude-standard',
45
+ '-z','--',':(glob)**/package.json'],{cwd:root,env:{...process.env,GIT_OPTIONAL_LOCKS:'0'},
46
+ maxBuffer:32*1024*1024,timeout:5000}).toString().split('\0').filter(Boolean);
47
+ const found=new Map();
48
+ for(const file of files){
49
+ const dir=path.posix.dirname(file);
50
+ if(dir==='.'||dir.split('/').includes('node_modules')||
51
+ !micromatch.isMatch(dir,patterns,{dot:true}))continue;
52
+ const member=read(path.join(root,file));
53
+ if(!member.name||found.has(member.name))throw Error('workspace package name missing or duplicated');
54
+ found.set(member.name,{name:member.name,dir,manifest:file,pkg:member});
55
+ }
56
+ if(found.size>50)throw Error('workspace count exceeds narrow Node onboarding contract');
57
+ return found;
58
+ }
59
+
60
+ function parsedTypeScript(root,dir){
61
+ const require=createRequire(path.join(root,'package.json'));
62
+ const ts=require(path.join(root,'node_modules','typescript'));
63
+ const config=path.join(root,dir,'tsconfig.json'),reads=new Set();
64
+ const host={...ts.sys,readFile(file){reads.add(path.resolve(file));return ts.sys.readFile(file);},
65
+ onUnRecoverableConfigFileDiagnostic(){}};
66
+ const parsed=ts.getParsedCommandLineOfConfigFile(config,{},host);
67
+ if(!parsed||parsed.errors?.length)throw Error('TypeScript configuration unavailable');
68
+ const raw=ts.readConfigFile(config,ts.sys.readFile);
69
+ if(raw.error||!Array.isArray(raw.config?.include)||raw.config.include.length!==1||
70
+ !['src','src/**/*','src/**'].includes(raw.config.include[0]))
71
+ throw Error('workspace TypeScript file membership is outside the supported src include');
72
+ if(parsed.options.plugins?.length||parsed.options.incremental||parsed.options.composite)
73
+ throw Error('TypeScript plugins or incremental/project builds are outside this contract');
74
+ if(parsed.options.paths||parsed.options.baseUrl||parsed.options.rootDirs?.length||
75
+ parsed.options.typeRoots?.length||parsed.options.customConditions?.length||
76
+ parsed.options.moduleSuffixes?.length||
77
+ parsed.options.moduleResolution!==undefined&&
78
+ parsed.options.moduleResolution!==ts.ModuleResolutionKind.Node10)
79
+ throw Error('custom TypeScript module resolution needs explicit input coverage');
80
+ if(parsed.projectReferences?.length)throw Error('TypeScript project references need a separate contract');
81
+ for(const file of reads)if(!within(realObservedPath(file),root))
82
+ throw Error(`TypeScript configuration escapes the checkout: ${path.basename(file)}`);
83
+ return {parsed,configs:[...reads].map(file=>relative(root,file)).sort()};
84
+ }
85
+
86
+ export function yarnWorkspaceTypecheckInputs(root,workspace,script){
87
+ root=realObservedPath(root);
88
+ const rootPkg=read(path.join(root,'package.json'));
89
+ if(!/^yarn@4\./.test(rootPkg.packageManager||''))throw Error('pinned Yarn 4 required');
90
+ const yarnrc=fs.readFileSync(path.join(root,'.yarnrc.yml'),'utf8');
91
+ const linker=yarnrc.match(/^\s*nodeLinker\s*:\s*['"]?([\w-]+)/m)?.[1];
92
+ const mode=yarnrc.match(/^\s*nmMode\s*:\s*['"]?([\w-]+)/m)?.[1]||'classic';
93
+ if(linker!=='node-modules'||mode!=='classic')
94
+ throw Error('Yarn node-modules classic installation required');
95
+ if(/^\s*(?:yarnPath|plugins)\s*:/m.test(yarnrc))
96
+ throw Error('custom Yarn executable or plugins need explicit toolchain coverage');
97
+ const members=yarnWorkspacePackages(root),selected=members.get(workspace);
98
+ if(!selected)throw Error(`workspace ${workspace} is not declared`);
99
+ if(selected.pkg.scripts?.[script]?.trim()!=='tsc -p .'||
100
+ selected.pkg.scripts?.['pre'+script]||selected.pkg.scripts?.['post'+script])
101
+ throw Error('workspace typecheck must be standalone tsc -p .');
102
+ const packageDir=safeDirectory(root,selected.dir);
103
+ if(fs.existsSync(path.join(packageDir,'node_modules','.bin','tsc'))||
104
+ fs.existsSync(path.join(packageDir,'node_modules','.bin','tsc.cmd')))
105
+ throw Error('workspace-local TypeScript executable needs separate toolchain coverage');
106
+ const {parsed,configs}=parsedTypeScript(root,selected.dir);
107
+ const outDir=parsed.options.outDir&&within(path.resolve(parsed.options.outDir),root)?
108
+ relative(root,path.resolve(parsed.options.outDir)):null;
109
+ if(!parsed.options.noEmit&&!outDir)
110
+ throw Error('emitting TypeScript check needs an in-checkout outDir');
111
+ if(outDir&&within(path.join(root,outDir),path.join(packageDir,'src')))
112
+ throw Error('TypeScript output overlaps observed source inputs');
113
+ const closure=new Map(),visit=member=>{
114
+ for(const name of Object.keys({...member.pkg.dependencies,...member.pkg.devDependencies,
115
+ ...member.pkg.peerDependencies,...member.pkg.optionalDependencies})){
116
+ const dep=members.get(name);if(!dep||dep.name===selected.name||closure.has(name))continue;
117
+ closure.set(name,dep);visit(dep);
118
+ }
119
+ };
120
+ visit(selected);
121
+ const source=[...new Set(['package.json','yarn.lock','.yarnrc.yml',
122
+ selected.manifest,...configs,`${selected.dir}/src/**`])];
123
+ const installed=['node_modules/**',`${selected.dir}/node_modules/**`];
124
+ const generated=[];
125
+ const linkTriggers=[];
126
+ for(const member of members.values()){
127
+ const logical=`node_modules/${member.name}`;
128
+ installed.push(`!${logical}`,`!${logical}/**`);
129
+ if(!closure.has(member.name))continue;
130
+ installed.push(`${member.dir}/node_modules/**`);
131
+ const declaration=member.pkg.types||member.pkg.typings;
132
+ if(!declaration||path.isAbsolute(declaration)||declaration.split('/').includes('..'))
133
+ throw Error(`${member.name} has no supported declaration-output location`);
134
+ const output=path.posix.dirname(declaration);
135
+ const outputDir=output==='.'?member.dir:`${member.dir}/${output}`;
136
+ generated.push(`${outputDir}/**`);
137
+ source.push(member.manifest);
138
+ // The logical link is part of resolution identity even though its target
139
+ // contents are indexed at the physical workspace output location.
140
+ linkTriggers.push(logical);
141
+ }
142
+ yarnLocalTsc(root);
143
+ return {workspace:selected,closure:[...closure.values()],source:[...new Set(source)],
144
+ installed,generated:[...new Set(generated)],configs,
145
+ configurationInputs:[...new Set(['package.json','yarn.lock','.yarnrc.yml',
146
+ ...configs,...members.values().map(row=>row.manifest),
147
+ 'node_modules/.yarn-state.yml'])],linkTriggers,outDir};
148
+ }
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ // Read-only applicability probe. Its output is one digest or a fixed reason;
3
+ // source, configuration, environment values, and command output are not logged.
4
+ import {compilerFiles} from './typescript-list.mjs';
5
+ import {contextOnly,probeResult} from './probe-result.mjs';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import {createHash} from 'node:crypto';
10
+ import {spawnSync} from 'node:child_process';
11
+ import {yarnWorkspaceTypecheckInputs,yarnLocalTsc} from './yarn-workspace-inputs.mjs';
12
+ import {withinPath as within,realObservedPath} from './path-identity.mjs';
13
+ import {windowsLaunch} from './windows-command.mjs';
14
+
15
+ const [rootArg,workspace,script,yarnArg]=process.argv.slice(2);
16
+ const hash=value=>createHash('sha256').update(value).digest('hex');
17
+ const digestFile=file=>hash(fs.readFileSync(file));
18
+ const fail=code=>{throw Object.assign(Error(code),{code});};
19
+ function yarnDistribution(yarn,version){
20
+ let loader=yarn;
21
+ if(process.platform==='win32'&&/\.cmd$/i.test(yarn)){
22
+ const body=fs.readFileSync(yarn,'utf8').replace(/\r\n/g,'\n').trim();
23
+ const line=body.split('\n')[2]||'';
24
+ const match=/^ "%~dp0\\node\.exe" "%~dp0\\(.+\\corepack\\dist\\yarn\.js)" %\*$/.exec(line);
25
+ if(!match)fail('yarn-distribution-unobservable');
26
+ const relative=match[1];
27
+ const standard=`@SETLOCAL
28
+ @IF EXIST "%~dp0\\node.exe" (
29
+ "%~dp0\\node.exe" "%~dp0\\${relative}" %*
30
+ ) ELSE (
31
+ @SET PATHEXT=%PATHEXT:;.JS;=;%
32
+ node "%~dp0\\${relative}" %*
33
+ )`;
34
+ if(body!==standard)fail('yarn-distribution-unobservable');
35
+ loader=realObservedPath(path.resolve(path.dirname(yarn),relative));
36
+ }
37
+ if(!/[/\\]corepack[/\\]dist[/\\]yarn\.js$/.test(loader))
38
+ return {distribution:yarn,loader};
39
+ if(process.env.COREPACK_HOME&&!path.isAbsolute(process.env.COREPACK_HOME))
40
+ fail('yarn-distribution-unobservable');
41
+ const candidates=process.env.COREPACK_HOME?
42
+ [path.join(process.env.COREPACK_HOME,'v1/yarn',version,'yarn.js')]:[
43
+ ...(process.platform==='win32'&&path.isAbsolute(process.env.LOCALAPPDATA||'')?
44
+ [path.join(process.env.LOCALAPPDATA,'node/corepack/v1/yarn',version,'yarn.js')]:[]),
45
+ path.join(os.homedir(),'.cache/node/corepack/v1/yarn',version,'yarn.js'),
46
+ path.join(os.homedir(),'Library/Caches/node/corepack/v1/yarn',version,'yarn.js')];
47
+ const found=candidates.find(file=>fs.existsSync(file));
48
+ if(!found)fail('yarn-distribution-unobservable');
49
+ return {distribution:found,loader};
50
+ }
51
+ try{
52
+ if(!rootArg||!workspace||!script||!yarnArg)fail('probe-arguments');
53
+ const root=realObservedPath(rootArg),yarn=realObservedPath(yarnArg);
54
+ const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json')));
55
+ if(process.env.NODE_OPTIONS||process.env.NODE_PATH||process.env.BASH_ENV||
56
+ process.env.ENV||Object.entries(process.env).some(([key,value])=>{
57
+ if(/^npm_config_prefix$/i.test(key)&&process.platform==='win32'){
58
+ return !path.isAbsolute(value);
59
+ }
60
+ if(key==='COREPACK_HOME')return !path.isAbsolute(value);
61
+ return /^(YARN_|COREPACK_|npm_config_|DYLD_|TSGO_)/i.test(key);
62
+ }))
63
+ fail('workspace-execution-environment-unsupported');
64
+ const launch=windowsLaunch([yarn,'--version']);
65
+ const version=spawnSync(launch.file,launch.args,{cwd:root,env:process.env,
66
+ ...launch.options,
67
+ encoding:'utf8',timeout:3000,maxBuffer:1024*1024,stdio:['ignore','pipe','pipe']});
68
+ if(version.error||version.signal||version.status!==0||
69
+ `yarn@${version.stdout.trim()}`!==pkg.packageManager)
70
+ fail('yarn-toolchain-unavailable');
71
+ const distribution=yarnDistribution(yarn,version.stdout.trim());
72
+ const contextFacts={yarnVersion:version.stdout.trim(),yarnShim:digestFile(yarn),
73
+ yarnLoader:digestFile(distribution.loader),
74
+ yarnDistribution:digestFile(distribution.distribution),node:process.version};
75
+ const contextHash=hash(JSON.stringify(contextFacts));
76
+ if(contextOnly(contextHash))process.exit(0);
77
+ const contract=yarnWorkspaceTypecheckInputs(root,workspace,script);
78
+ const cwd=path.join(root,contract.workspace.dir);
79
+ const tsc=yarnLocalTsc(root);
80
+ const captured=process.argv.includes('--redue-checkpoint')?compilerFiles(path.join(root,'node_modules/typescript'),cwd):null;
81
+ const listed=captured?{status:0,stdout:captured.files.join('\n')}:spawnSync(process.execPath,[tsc,'-p','.','--listFilesOnly'],
82
+ {cwd,env:process.env,encoding:'utf8',timeout:4000,maxBuffer:16*1024*1024,
83
+ stdio:['ignore','pipe','pipe']});
84
+ if(listed.error||listed.signal||listed.status!==0)fail('typescript-input-list-unavailable');
85
+ const inputs=listed.stdout.trim().split(/\r?\n/).filter(Boolean)
86
+ .map(file=>realObservedPath(file));
87
+ if(!inputs.length)fail('typescript-input-list-unavailable');
88
+ const selectedRoot=path.join(cwd,'src'),installedRoot=path.join(root,'node_modules');
89
+ const nestedInstalled=[contract.workspace,...contract.closure]
90
+ .map(member=>path.join(root,member.dir,'node_modules'));
91
+ const generated=contract.generated.map(value=>path.join(root,value.slice(0,-3)));
92
+ for(const file of inputs){
93
+ const covered=within(file,installedRoot)||nestedInstalled.some(dir=>within(file,dir))||
94
+ within(file,selectedRoot)||
95
+ generated.some(dir=>within(file,dir));
96
+ if(!covered||!within(file,root))fail('typescript-input-outside-workspace-contract');
97
+ }
98
+ const files=[...new Set(inputs)].sort().map(file=>
99
+ [hash(path.relative(root,file)),digestFile(file)]);
100
+ const configs=contract.configs.map(file=>[hash(file),digestFile(path.join(root,file))]);
101
+ probeResult(hash(JSON.stringify({schema:1,workspace,script,
102
+ files,configs,yarnVersion:version.stdout.trim(),yarnShim:digestFile(yarn),
103
+ yarnLoader:digestFile(distribution.loader),
104
+ yarnDistribution:digestFile(distribution.distribution),
105
+ node:process.version,tsc:digestFile(tsc)})),contextHash,captured?.queries);
106
+ }catch(e){console.error(`VSTATE_REASON:${e.code&&/^[a-z-]+$/.test(e.code)?e.code:
107
+ 'workspace-typecheck-contract-unavailable'}`);process.exitCode=2;}