@redue/cli 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +20 -0
- package/README.md +39 -3
- package/bin/redue.mjs +40 -20
- package/docs/README.md +1 -1
- package/docs/user/agents.md +134 -4
- package/docs/user/configuration.md +5 -2
- package/docs/user/direct-typescript.md +96 -0
- package/docs/user/getting-started.md +21 -2
- package/docs/user/npm-launcher-notice.md +42 -0
- package/docs/user/supported-projects.md +9 -8
- package/package.json +1 -1
- package/src/agent-integration.mjs +244 -0
- package/src/agent-policy.md +52 -0
- package/src/cli.mjs +14 -2
- package/src/daemon.mjs +13 -3
- package/src/direct-typescript-probe.mjs +20 -0
- package/src/direct-typescript.mjs +72 -0
- package/src/node-onboarding.mjs +14 -3
- package/src/npm-typecheck-environment.mjs +36 -0
- package/src/plan-worker.mjs +9 -1
- package/src/presentation.mjs +1 -0
- package/src/project-location.mjs +34 -0
- package/src/typescript-contract-probe.mjs +4 -187
- package/src/typescript-input-facts.mjs +65 -0
- package/src/typescript-qualification.mjs +23 -43
package/src/node-onboarding.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {directProject,directQualification} from './direct-typescript.mjs';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {spawnSync} from 'node:child_process';
|
|
@@ -116,7 +117,7 @@ function classify(kind,scriptName,script,pkg,managerInfo,root) {
|
|
|
116
117
|
managerInfo.name==='npm'&&
|
|
117
118
|
exists(path.join(root,'tsconfig.json'))&&
|
|
118
119
|
exists(path.join(root,'node_modules/typescript/package.json')))
|
|
119
|
-
return {level:'
|
|
120
|
+
return {level:'recording',reason:'npm launcher uses unobserved remote/cache/time inputs; execution can be recorded, but cannot be reused',
|
|
120
121
|
qualification:'typescript-noemit-v1'};
|
|
121
122
|
if(kind==='typecheck')return {level:'recording',reason:
|
|
122
123
|
'TypeScript invocation, installation, or configuration is outside the automatic noEmit contract'};
|
|
@@ -159,7 +160,8 @@ function workspaceManifests(root,patterns) {
|
|
|
159
160
|
if(matched.length>50)throw Error('more than 50 workspace packages found; select checks explicitly');
|
|
160
161
|
return matched.map(file=>({file,pkg:readJson(path.join(root,file))}));
|
|
161
162
|
}
|
|
162
|
-
export function discoverNodeProject(root) {
|
|
163
|
+
export function discoverNodeProject(root,{recipe=null}={}) {
|
|
164
|
+
if(recipe&&recipe!=='typescript-direct')throw Error('unknown recipe; use typescript-direct');
|
|
163
165
|
root=realObservedPath(root);
|
|
164
166
|
const packageFile=path.join(root,'package.json');
|
|
165
167
|
if(!exists(packageFile))throw Error('run init from a repository containing package.json');
|
|
@@ -183,9 +185,18 @@ export function discoverNodeProject(root) {
|
|
|
183
185
|
if(!script)continue;
|
|
184
186
|
let classification=observationIssue?{level:'unsupported',reason:observationIssue}:
|
|
185
187
|
classify(kind,script,scripts[script],pkg,pm,root);
|
|
186
|
-
if(workspaces&&classification.
|
|
188
|
+
if(workspaces&&classification.qualification==='typescript-noemit-v1')classification={level:'recording',
|
|
187
189
|
reason:'workspace task and installed-input closure need explicit qualification'};
|
|
188
190
|
const check={name:kind,script,kind,inputs:sourceInputs(root,kind,pm.lockfiles||[])};
|
|
191
|
+
if(recipe==='typescript-direct'&&kind==='typecheck'){
|
|
192
|
+
try{
|
|
193
|
+
if(pm.name!=='npm'||workspaces||observationIssue||pm.issue)throw Error('direct recipe requires a supported single-package npm checkout');
|
|
194
|
+
const compiler=directProject(root,script);
|
|
195
|
+
check.command=['@node','@typescript-compiler:.','--noEmit'];check.cwd='.';
|
|
196
|
+
classification={level:'ready',qualification:directQualification,
|
|
197
|
+
reason:`explicit local TypeScript ${compiler.version} compilation; npm launcher/lifecycle scripts are not executed; package.json stays unchanged`};
|
|
198
|
+
}catch(e){classification={level:'recording',reason:`Direct recipe unavailable (${e.code||e.message}); original npm script retained, without bypassing required behavior`};}
|
|
199
|
+
}
|
|
189
200
|
if(pm.name==='pnpm'&&kind==='typecheck'&&!workspaces&&!pm.issue&&
|
|
190
201
|
/^(?:tsc --noEmit|tsc -p (?:\.|tsconfig\.json)(?: --noEmit)?)$/.test(scripts[script].trim())){
|
|
191
202
|
try{const contract=pnpmTypecheckInputs(root,'.',script);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Admission for the standalone npm 10.9.2 -> tsc --noEmit contract only.
|
|
2
|
+
// Values are never returned: the caller separately fingerprints every original
|
|
3
|
+
// npm_config_* key/value (including case and empty values).
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export const npmEnvironmentInterpretation = 'npm-tsc-environment@2';
|
|
6
|
+
const proxyKeys = new Set(['proxy', 'https_proxy', 'http_proxy', 'noproxy']);
|
|
7
|
+
export function npmTypecheckEnvironmentIssue(env, {npmVersion, platform = process.platform} = {}) {
|
|
8
|
+
if (env.NODE_OPTIONS || env.NODE_PATH || env.BASH_ENV || env.ENV)
|
|
9
|
+
return 'execution-environment-unsupported';
|
|
10
|
+
const seen = new Map();
|
|
11
|
+
for (const [key, value] of Object.entries(env)) {
|
|
12
|
+
if (/^(DYLD_|TSGO_)/i.test(key)) return 'execution-environment-unsupported';
|
|
13
|
+
if (!/^npm_config_/i.test(key)) continue;
|
|
14
|
+
if (/^npm_config_prefix$/i.test(key) && platform === 'win32' && path.win32.isAbsolute(value)) continue;
|
|
15
|
+
const suffix = key.slice('npm_config_'.length).toLowerCase();
|
|
16
|
+
if (!proxyKeys.has(suffix)) return 'execution-environment-unsupported';
|
|
17
|
+
// http_proxy is an unknown, inert pass-through key in this inspected npm
|
|
18
|
+
// version, not an alias for proxy. Other npm versions require source review.
|
|
19
|
+
if (npmVersion !== '10.9.2') return 'npm-proxy-version-unsupported';
|
|
20
|
+
if (seen.has(suffix) && seen.get(suffix) !== value)
|
|
21
|
+
return 'npm-proxy-context-ambiguous';
|
|
22
|
+
seen.set(suffix, value);
|
|
23
|
+
if (typeof value !== 'string' || /[\x00-\x1f\x7f]/.test(value))
|
|
24
|
+
return 'npm-proxy-value-unsupported';
|
|
25
|
+
// npm ignores empty environment config, but passes the original environment
|
|
26
|
+
// to the child. Empty and absent remain distinct caller identities.
|
|
27
|
+
if (value === '' || (suffix === 'proxy' && value === 'false')) continue;
|
|
28
|
+
if (suffix === 'noproxy') continue;
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(value);
|
|
31
|
+
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname)
|
|
32
|
+
return 'npm-proxy-value-unsupported';
|
|
33
|
+
} catch { return 'npm-proxy-value-unsupported'; }
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
package/src/plan-worker.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import {createHash} from 'node:crypto';
|
|
5
5
|
import {discover} from './plan.mjs';
|
|
6
6
|
import {InputIndex} from './index.mjs';
|
|
7
|
+
import {directProject,directQualification} from './direct-typescript.mjs';
|
|
7
8
|
import {queriesMatch} from './typescript-list.mjs';
|
|
8
9
|
import {planGuard,inputKey} from './decision-validation.mjs';
|
|
9
10
|
import {timing} from './decision-profile.mjs';
|
|
@@ -21,7 +22,14 @@ try {
|
|
|
21
22
|
try{
|
|
22
23
|
guard=planGuard(bundle);
|
|
23
24
|
const queryStarted=performance.now();
|
|
24
|
-
|
|
25
|
+
// Cached membership queries load the compiler API; establish its reviewed
|
|
26
|
+
// implementation before that optimization can execute project code.
|
|
27
|
+
const directRoots=new Set(config.checks.filter(check=>check.qualification===directQualification)
|
|
28
|
+
.map(check=>directProject(config.root,check.script).tsRoot));
|
|
29
|
+
if(directRoots.size&&(workerData.reuse.queries||[]).some(([kind,,,options])=>
|
|
30
|
+
kind==='readDirectory'&&!directRoots.has(options?.tsRoot)))throw Error('retained compiler implementation changed');
|
|
31
|
+
const queriesValid=guard===workerData.reuse.guard&&
|
|
32
|
+
(!workerData.reuse.queries?.length||queriesMatch(workerData.reuse.queries));
|
|
25
33
|
timing('worker.compiler_queries',queryStarted,{queries:workerData.reuse.queries?.length||0,matched:queriesValid?1:0});
|
|
26
34
|
if(guard!==workerData.reuse.guard||!queriesValid)reuseReason='configuration_or_resolution_changed';
|
|
27
35
|
else {
|
package/src/presentation.mjs
CHANGED
|
@@ -33,6 +33,7 @@ export function renderExplain(value,name) {
|
|
|
33
33
|
return rows.map(row=>{
|
|
34
34
|
const lines=[`${row.name}: ${checkLabel(row)}${row.result?` / ${row.result}`:''}`];
|
|
35
35
|
if(row.result==='FAIL')lines.push(`Historical failure; applicability: ${row.freshness}.`);
|
|
36
|
+
if(row.verification_recipe==='npm-direct-typescript@1')lines.push('Recorded recipe: direct local TypeScript compiler; npm and lifecycle scripts were not executed.');
|
|
36
37
|
lines.push(explanation(row));
|
|
37
38
|
if(row.changed_inputs?.length)lines.push('Relevant inputs changed:',...row.changed_inputs.map(file=>` ${file}`));
|
|
38
39
|
if(row.freshness==='STALE')lines.push(`Due again when verification is needed: redue run ${JSON.stringify(row.name)}`);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Shared project selection only; evidence and caller-context semantics stay in the kernel.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import {createHash} from 'node:crypto';
|
|
6
|
+
import {realObservedPath} from './path-identity.mjs';
|
|
7
|
+
import {stateIdentity,userStateBase} from './platform-state.mjs';
|
|
8
|
+
|
|
9
|
+
export function discoverConfig(cwd=process.cwd()) {
|
|
10
|
+
let dir=path.resolve(cwd);const found=[];
|
|
11
|
+
for(;;){
|
|
12
|
+
for(const name of ['redue.config.json','vstate.config.json']) {
|
|
13
|
+
const file=path.join(dir,name);
|
|
14
|
+
if(fs.existsSync(file))found.push(file);
|
|
15
|
+
}
|
|
16
|
+
if(fs.existsSync(path.join(dir,'.git'))||path.dirname(dir)===dir)break;
|
|
17
|
+
dir=path.dirname(dir);
|
|
18
|
+
}
|
|
19
|
+
if(found.length>1)throw Error('multiple REDUE configurations apply; select the intended one with --config FILE');
|
|
20
|
+
return found[0]||path.resolve(cwd,'redue.config.json');
|
|
21
|
+
}
|
|
22
|
+
export function canonicalConfig(configFile) {
|
|
23
|
+
return path.join(realObservedPath(path.dirname(configFile)),path.basename(configFile));
|
|
24
|
+
}
|
|
25
|
+
export function projectState(root,configFile) {
|
|
26
|
+
return path.join(userStateBase(process.platform,process.env,os.homedir()),'vstate',
|
|
27
|
+
`vstate-${createHash('sha256').update(stateIdentity(root)+'\0'+stateIdentity(canonicalConfig(configFile))).digest('hex').slice(0,16)}`);
|
|
28
|
+
}
|
|
29
|
+
export function readProject(configFile) {
|
|
30
|
+
const config=JSON.parse(fs.readFileSync(configFile,'utf8'));
|
|
31
|
+
if(config.schema!==1||!Array.isArray(config.checks)||!config.checks.length)
|
|
32
|
+
throw Error('config needs schema 1 and at least one check');
|
|
33
|
+
return {config,root:realObservedPath(path.resolve(path.dirname(configFile),config.root||'.'))};
|
|
34
|
+
}
|
|
@@ -1,188 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
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;}
|
|
2
|
+
// Retired npm-launcher interpretation. Do not return even a context-only digest:
|
|
3
|
+
// old receipts and cached compiler certificates cannot qualify this obligation.
|
|
4
|
+
console.error('VSTATE_REASON:npm-launcher-unobserved-inputs');
|
|
5
|
+
process.exitCode=2;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Extracted from the existing npm TypeScript probe; shared compiler input logic.
|
|
2
|
+
import fs from 'node:fs';import path from 'node:path';import {createHash} from 'node:crypto';
|
|
3
|
+
import {createRequire} from 'node:module';import {spawnSync} from 'node:child_process';
|
|
4
|
+
import {compilerFiles} from './typescript-list.mjs';
|
|
5
|
+
import {withinPath as within,realObservedPath} from './path-identity.mjs';
|
|
6
|
+
import {timing} from './decision-profile.mjs';
|
|
7
|
+
const require=createRequire(import.meta.url),hash=value=>createHash('sha256').update(value).digest('hex');
|
|
8
|
+
const digestFile=file=>hash(fs.readFileSync(file));
|
|
9
|
+
const fail=code=>{throw Object.assign(Error(code),{code});};
|
|
10
|
+
export function typeScriptInputFacts(root,tsRoot,tsc){
|
|
11
|
+
const configReads=new Set(),visited=new Set();
|
|
12
|
+
let ts;try{ts=require(tsRoot);}catch{}
|
|
13
|
+
if(typeof ts?.getParsedCommandLineOfConfigFile==='function'){
|
|
14
|
+
const host={...ts.sys,readFile(file){configReads.add(path.resolve(file));
|
|
15
|
+
return ts.sys.readFile(file);},onUnRecoverableConfigFileDiagnostic(){}};
|
|
16
|
+
const parseConfig=file=>{
|
|
17
|
+
const absolute=path.resolve(file);
|
|
18
|
+
if(!within(absolute,root))fail('typescript-config-outside-repository');
|
|
19
|
+
if(visited.has(absolute))return;visited.add(absolute);
|
|
20
|
+
if(visited.size>64)fail('typescript-config-graph-too-large');
|
|
21
|
+
const parsed=ts.getParsedCommandLineOfConfigFile(absolute,{},host);
|
|
22
|
+
if(!parsed||parsed.errors?.length)fail('typescript-config-unavailable');
|
|
23
|
+
if(parsed.options?.plugins?.length)fail('typescript-plugin-unsupported');
|
|
24
|
+
if(parsed.options?.incremental||parsed.options?.composite)
|
|
25
|
+
fail('typescript-incremental-unsupported');
|
|
26
|
+
for(const reference of parsed.projectReferences||[])
|
|
27
|
+
parseConfig(reference.path.endsWith('.json')?reference.path:
|
|
28
|
+
path.join(reference.path,'tsconfig.json'));
|
|
29
|
+
};
|
|
30
|
+
parseConfig(path.join(root,'tsconfig.json'));
|
|
31
|
+
}else{
|
|
32
|
+
// TypeScript 7's native compiler does not expose the previous config API.
|
|
33
|
+
// Its CLI reports the effective config; restrict this path to one direct
|
|
34
|
+
// config rather than guessing about extends/reference provenance.
|
|
35
|
+
const config=path.join(root,'tsconfig.json'),raw=fs.readFileSync(config,'utf8');
|
|
36
|
+
if(/"(?:extends|references|plugins|incremental|composite)"\s*:/.test(raw))
|
|
37
|
+
fail('typescript-config-graph-unsupported');
|
|
38
|
+
const shown=spawnSync(process.execPath,[tsc,'--showConfig'],{cwd:root,
|
|
39
|
+
env:process.env,encoding:'utf8',timeout:4000,maxBuffer:16*1024*1024,
|
|
40
|
+
stdio:['ignore','pipe','pipe']});
|
|
41
|
+
if(shown.error||shown.signal||shown.status!==0)fail('typescript-config-unavailable');
|
|
42
|
+
const effective=JSON.parse(shown.stdout);
|
|
43
|
+
if(effective.compilerOptions?.plugins?.length||
|
|
44
|
+
effective.compilerOptions?.incremental||effective.compilerOptions?.composite)
|
|
45
|
+
fail('typescript-config-graph-unsupported');
|
|
46
|
+
configReads.add(config);
|
|
47
|
+
}
|
|
48
|
+
for(const file of configReads)if(!within(realObservedPath(file),root))
|
|
49
|
+
fail('typescript-config-outside-repository');
|
|
50
|
+
const listingStarted=performance.now();
|
|
51
|
+
const captured=process.argv.includes('--redue-checkpoint')?compilerFiles(tsRoot,root):null;
|
|
52
|
+
const listed=captured?{status:0,stdout:captured.files.join('\n')}:spawnSync(process.execPath,[tsc,'--noEmit','--listFilesOnly'],
|
|
53
|
+
{cwd:root,env:process.env,encoding:'utf8',timeout:process.platform==='win32'?10000:4000,
|
|
54
|
+
maxBuffer:16*1024*1024,
|
|
55
|
+
stdio:['ignore','pipe','pipe']});
|
|
56
|
+
timing('typescript_contract.list_files',listingStarted,{status:listed.status??null,
|
|
57
|
+
error:listed.error?.code||null});
|
|
58
|
+
if(listed.error||listed.signal||listed.status!==0)fail('typescript-input-list-unavailable');
|
|
59
|
+
const files=listed.stdout.trim().split(/\r?\n/).filter(Boolean)
|
|
60
|
+
.map(file=>path.resolve(file));
|
|
61
|
+
if(!files.length||files.some(file=>!within(realObservedPath(file),root)))
|
|
62
|
+
fail('typescript-input-outside-repository');
|
|
63
|
+
return {configs:[...configReads].sort().map(file=>[hash(file),digestFile(file)]),
|
|
64
|
+
files:[...new Set(files)].sort().map(file=>[hash(file),digestFile(file)]),queries:captured?.queries};
|
|
65
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {directProject,directQualification,directEnvironment} from './direct-typescript.mjs';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {realObservedPath} from './path-identity.mjs';
|
|
@@ -6,6 +7,25 @@ import {realObservedPath} from './path-identity.mjs';
|
|
|
6
7
|
// The read-only runtime probe separately checks the effective TypeScript file
|
|
7
8
|
// list, config graph, npm toolchain and caller context on every sync/run.
|
|
8
9
|
export function qualifyTypeScript(root,selected,discovered={}) {
|
|
10
|
+
if(selected.qualification===directQualification){
|
|
11
|
+
const issues=[];
|
|
12
|
+
try{
|
|
13
|
+
const compiler=directProject(root,selected.script);
|
|
14
|
+
if(selected.command?.length!==3||selected.command[0]!==process.execPath||
|
|
15
|
+
selected.command[1]!==compiler.entry||selected.command[2]!=='--noEmit'||
|
|
16
|
+
selected.cwd&&selected.cwd!=='.')issues.push('direct compiler invocation changed');
|
|
17
|
+
}catch(e){issues.push(e.code||'direct compiler prerequisites unavailable');}
|
|
18
|
+
if(!selected.inputs?.includes('**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json,jsonc}')||
|
|
19
|
+
!selected.inputs?.includes('package.json')||!selected.inputs?.includes('tsconfig.json')||
|
|
20
|
+
!selected.installedInputs?.includes('node_modules/**'))issues.push('direct compiler source/config/installed inputs incomplete');
|
|
21
|
+
if(!selected.environment?.executableIdentity||
|
|
22
|
+
!directEnvironment.variables.every(key=>selected.environment?.variables?.includes(key))||
|
|
23
|
+
!directEnvironment.prefixes.every(key=>selected.environment?.prefixes?.includes(key)))
|
|
24
|
+
issues.push('direct compiler execution context incomplete');
|
|
25
|
+
if(selected.probes?.length!==1||!selected.probes?.some(argv=>path.basename(argv[1]||'')==='direct-typescript-probe.mjs'&&
|
|
26
|
+
argv[0]===process.execPath&&argv[2]===root&&argv[3]===selected.script))issues.push('direct compiler probe missing');
|
|
27
|
+
return {qualified:issues.length===0,issues};
|
|
28
|
+
}
|
|
9
29
|
if(selected.qualification==='pnpm-tsc-v1'){
|
|
10
30
|
const issues=[];
|
|
11
31
|
const workspace=selected.workspace||'.';
|
|
@@ -50,47 +70,7 @@ export function qualifyTypeScript(root,selected,discovered={}) {
|
|
|
50
70
|
return {qualified:issues.length===0,issues};
|
|
51
71
|
}
|
|
52
72
|
if(selected.qualification!=='typescript-noemit-v1')return null;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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};
|
|
73
|
+
// Retired automatic launcher contract: npm's notifier consumes unobserved
|
|
74
|
+
// remote/cache/time state. A successful execution is still an outcome.
|
|
75
|
+
return {qualified:false,issues:['npm launcher applicability is unqualified: update notification uses unobserved remote/cache/time inputs; run the npm script when needed']};
|
|
96
76
|
}
|