@redue/cli 0.1.0-alpha.2 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +491 -0
- package/README.md +107 -2
- package/bin/redue.mjs +132 -50
- package/docs/README.md +2 -1
- package/docs/user/agents.md +299 -15
- package/docs/user/configuration.md +28 -2
- package/docs/user/direct-typescript.md +209 -0
- package/docs/user/getting-started.md +23 -2
- package/docs/user/json.md +2 -0
- package/docs/user/node-script.md +154 -0
- package/docs/user/npm-launcher-notice.md +42 -0
- package/docs/user/pilot.md +146 -0
- package/docs/user/receipt-selection-notice.md +60 -0
- package/docs/user/supported-projects.md +56 -23
- package/docs/user/tests-direct.md +132 -0
- package/package.json +4 -3
- package/src/agent-integration.mjs +388 -0
- package/src/agent-policy.md +28 -0
- package/src/cached-state.mjs +46 -8
- package/src/claude-hook.mjs +449 -0
- package/src/cli.mjs +263 -40
- package/src/daemon.mjs +433 -94
- package/src/decision-profile.mjs +15 -1
- package/src/decision-validation.mjs +10 -0
- package/src/declared-plan.mjs +29 -6
- package/src/direct-run-certificate.mjs +252 -0
- package/src/direct-typescript-probe.mjs +24 -0
- package/src/direct-typescript.mjs +185 -0
- package/src/glob.mjs +8 -0
- package/src/history-worker.mjs +21 -13
- package/src/index.mjs +72 -19
- package/src/installed-inputs.mjs +7 -1
- package/src/linux-inotify.py +39 -10
- package/src/node-onboarding.mjs +172 -53
- package/src/node-script.mjs +364 -0
- package/src/npm-integrity.mjs +124 -0
- package/src/npm-typecheck-environment.mjs +36 -0
- package/src/npmrc.mjs +16 -0
- package/src/observation.mjs +34 -12
- package/src/path-identity.mjs +8 -0
- package/src/plan-worker.mjs +17 -17
- package/src/pnpm-inputs.mjs +114 -54
- package/src/pnpm-shim-templates.json +180 -0
- package/src/pnpm-shims.mjs +54 -0
- package/src/pnpm-typecheck-probe.mjs +11 -6
- package/src/presentation.mjs +9 -1
- package/src/probe-deadline.mjs +37 -0
- package/src/probe-result.mjs +7 -3
- package/src/project-location.mjs +34 -0
- package/src/run-recorder.cjs +91 -0
- package/src/state-store.mjs +37 -1
- package/src/test-direct.mjs +560 -0
- package/src/test-recorder.cjs +406 -0
- package/src/typescript-contract-probe.mjs +4 -187
- package/src/typescript-input-facts.mjs +92 -0
- package/src/typescript-invocation.mjs +92 -0
- package/src/typescript-list.mjs +108 -13
- package/src/typescript-qualification.mjs +29 -49
- package/src/workspace-manifests.mjs +24 -0
- package/src/yarn-workspace-typecheck-probe.mjs +2 -1
package/bin/redue.mjs
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import {mark,traceBase,traceContext,traceEnvironment,traceId} from '../src/decision-profile.mjs';
|
|
4
|
+
traceBase({...traceContext(),request:traceId(),work:'client'});
|
|
5
|
+
mark('client.loaded');
|
|
6
|
+
import {directCompiler,directQualification,directEnvironment,directInterpretation} from '../src/direct-typescript.mjs';
|
|
7
|
+
import {runArgv,tscRun} from '../src/typescript-invocation.mjs';
|
|
8
|
+
import {testEnvironment,testRunnerAt,testsInterpretation,testsQualification} from '../src/test-direct.mjs';
|
|
9
|
+
import {scriptEnvironment,scriptInterpretation,scriptKinds,scriptQualification} from '../src/node-script.mjs';
|
|
3
10
|
import os from 'node:os';
|
|
4
11
|
import path from 'node:path';
|
|
5
12
|
import {createHash} from 'node:crypto';
|
|
6
13
|
import {spawn,spawnSync} from 'node:child_process';
|
|
7
14
|
import {createRequire} from 'node:module';
|
|
8
|
-
import {fileURLToPath} from 'node:url';
|
|
9
|
-
import {discoverNodeProject} from '../src/node-onboarding.mjs';
|
|
15
|
+
import {fileURLToPath,pathToFileURL} from 'node:url';
|
|
10
16
|
import {processAlive} from '../src/process-liveness.mjs';
|
|
11
17
|
import {samePath,withinPath,realObservedPath} from '../src/path-identity.mjs';
|
|
12
|
-
import {
|
|
18
|
+
import {discoverConfig,projectState,canonicalConfig} from '../src/project-location.mjs';
|
|
13
19
|
import {findExecutable} from '../src/executable-lookup.mjs';
|
|
14
20
|
import {readCachedStatus} from '../src/cached-state.mjs';
|
|
15
21
|
import {assertOwnedStatePlacement} from '../src/owned-state.mjs';
|
|
@@ -22,8 +28,12 @@ const help=`REDUE ${version}
|
|
|
22
28
|
Usage: redue [--config FILE] [--state-dir DIR] COMMAND [OPTIONS]
|
|
23
29
|
|
|
24
30
|
Commands:
|
|
25
|
-
init [--dry-run] [--workspace NAME] [--check KIND]
|
|
31
|
+
init [--dry-run] [--workspace NAME] [--check KIND] [--recipe typescript-direct|tests-direct|node-script]
|
|
26
32
|
Discover existing scripts; optionally select one workspace/check
|
|
33
|
+
agent setup codex|claude [--dry-run|--apply] [--json]
|
|
34
|
+
agent doctor [codex|claude] [--json]
|
|
35
|
+
agent remove codex|claude [--dry-run|--apply] [--json]
|
|
36
|
+
Connect project instructions; explicit setup only
|
|
27
37
|
start Start the local observer
|
|
28
38
|
stop Stop this observer
|
|
29
39
|
status [--json|--short] Read conservative cached status
|
|
@@ -36,13 +46,20 @@ Commands:
|
|
|
36
46
|
No checks run in the background. Direct commands outside redue create no receipts.`;
|
|
37
47
|
|
|
38
48
|
function error(message){console.error(`redue: ${message}`);process.exit(2);}
|
|
49
|
+
// Exits once queued output is written. On macOS, output to a pipe is written
|
|
50
|
+
// asynchronously; exiting with it queued cuts it (at 8 KB through the pipe a
|
|
51
|
+
// Node program, such as an agent's shell tool, reads).
|
|
52
|
+
async function exit(code){await new Promise(resolve=>process.stdout.write('',resolve));process.exit(code);}
|
|
39
53
|
const args=process.argv.slice(2);
|
|
54
|
+
if(args.length===3&&args[0]==='agent'&&args[1]==='hook'&&args[2]==='claude'){
|
|
55
|
+
// Runs before config discovery: the hook must exit 0 on every failure,
|
|
56
|
+
// because Claude Code treats exit 2 as a block.
|
|
57
|
+
const {runClaudeHook}=await import('../src/claude-hook.mjs');
|
|
58
|
+
await runClaudeHook(fileURLToPath(import.meta.url));process.exit(0);
|
|
59
|
+
}
|
|
40
60
|
if(args.length===1&&args[0]==='--version'){console.log(version);process.exit(0);}
|
|
41
|
-
if(args.includes('--help')||args.includes('-h')||!args.length){console.log(help);
|
|
42
|
-
|
|
43
|
-
const legacyConfig=path.resolve('vstate.config.json');
|
|
44
|
-
let configFile=fs.existsSync(preferredConfig)||!fs.existsSync(legacyConfig)?
|
|
45
|
-
preferredConfig:legacyConfig,stateOverride=null;
|
|
61
|
+
if(args.includes('--help')||args.includes('-h')||!args.length){console.log(help);await exit(0);}
|
|
62
|
+
let configFile=null,stateOverride=null;
|
|
46
63
|
for(let i=0;i<args.length;){
|
|
47
64
|
if(args[i]==='--config'||args[i]==='--state-dir'){
|
|
48
65
|
const flag=args[i],value=args[i+1];if(!value)error(`${flag} requires a path`);
|
|
@@ -51,17 +68,28 @@ for(let i=0;i<args.length;){
|
|
|
51
68
|
}else i++;
|
|
52
69
|
}
|
|
53
70
|
const command=args.shift();
|
|
71
|
+
try{configFile??=command==='init'||stateOverride&&['stop','remove-state'].includes(command)?
|
|
72
|
+
path.resolve(fs.existsSync('redue.config.json')||!fs.existsSync('vstate.config.json')?'redue.config.json':'vstate.config.json'):discoverConfig();}
|
|
73
|
+
catch(e){error(e.message);}
|
|
74
|
+
if(fs.existsSync(path.dirname(configFile)))configFile=canonicalConfig(configFile);
|
|
75
|
+
if(command==='agent'){
|
|
76
|
+
// Loaded on demand: onboarding and agent setup are not on the run/status path.
|
|
77
|
+
const {agentCommand}=await import('../src/agent-integration.mjs');
|
|
78
|
+
try{await agentCommand(args,{configFile,stateOverride,entry:fileURLToPath(import.meta.url)});}
|
|
79
|
+
catch(e){error(e.message);}
|
|
80
|
+
await exit(0);
|
|
81
|
+
}
|
|
54
82
|
if(!['init','start','stop','status','detail','explain','run','remove-state'].includes(command))
|
|
55
83
|
error(`unknown command ${command||'<none>'}; use --help`);
|
|
56
84
|
const json=args.includes('--json');
|
|
57
85
|
const sync=args.includes('--sync');
|
|
58
86
|
const dryRun=args.includes('--dry-run');
|
|
59
87
|
const short=args.includes('--short');
|
|
60
|
-
let workspaceSelection=null,checkSelection=null;
|
|
88
|
+
let workspaceSelection=null,checkSelection=null,recipeSelection=null;
|
|
61
89
|
if(command==='init')for(let i=0;i<args.length;){
|
|
62
|
-
if(args[i]==='--workspace'||args[i]==='--check'){
|
|
90
|
+
if(args[i]==='--workspace'||args[i]==='--check'||args[i]==='--recipe'){
|
|
63
91
|
const flag=args[i],value=args[i+1];if(!value||value.startsWith('--'))error(`${flag} requires a name`);
|
|
64
|
-
if(flag==='--workspace')workspaceSelection=value;else checkSelection=value;
|
|
92
|
+
if(flag==='--workspace')workspaceSelection=value;else if(flag==='--recipe')recipeSelection=value;else checkSelection=value;
|
|
65
93
|
args.splice(i,2);
|
|
66
94
|
}else i++;
|
|
67
95
|
}
|
|
@@ -73,7 +101,8 @@ if(short&&(command!=='status'||json||sync))error('--short is a cached status opt
|
|
|
73
101
|
if(dryRun&&command!=='init')error('--dry-run is for init');
|
|
74
102
|
|
|
75
103
|
if(command==='init'){
|
|
76
|
-
|
|
104
|
+
const {discoverNodeProject}=await import('../src/node-onboarding.mjs');
|
|
105
|
+
let discovery;try{discovery=discoverNodeProject(process.cwd(),{recipe:recipeSelection});}
|
|
77
106
|
catch(e){error(e.message);}
|
|
78
107
|
if(workspaceSelection||checkSelection){
|
|
79
108
|
const chosen=discovery.checks.filter(row=>
|
|
@@ -91,7 +120,7 @@ if(command==='init'){
|
|
|
91
120
|
package_manager:discovery.manager.name,version:discovery.manager.version,
|
|
92
121
|
installation_layout:discovery.manager.layout,
|
|
93
122
|
issue:discovery.observationIssue||discovery.manager.issue,workspaces:discovery.workspaces,
|
|
94
|
-
ambiguous:discovery.ambiguous,
|
|
123
|
+
ambiguous:discovery.ambiguous,skipped:discovery.skipped||[],
|
|
95
124
|
checks:discovery.checks.map(({config:check,level,reason})=>({name:check.name,
|
|
96
125
|
script:check.script||null,command:check.command||null,
|
|
97
126
|
workspace:check.workspace||null,level,reason})),config:configFile,
|
|
@@ -104,11 +133,13 @@ if(command==='init'){
|
|
|
104
133
|
console.log(` ${row.name} (${row.script||row.command?.join(' ')}) — `+
|
|
105
134
|
`${{ready:'ready to qualify',recording:'recording-only',unsupported:'unsupported'}[row.level]||row.level}: ${row.reason}`);
|
|
106
135
|
if(row.script&&row.command)console.log(` Recorded invocation: ${row.command.map(value=>
|
|
107
|
-
value==='@node'?'node':value
|
|
136
|
+
value==='@node'?'node':value==='@typescript-compiler:.'?'[reviewed local TypeScript compiler implementation]':
|
|
137
|
+
value.startsWith('@test-runner:')?`[local ${value.slice('@test-runner:'.length)}]`:value.startsWith('@typescript-bin:')?
|
|
108
138
|
`[installed TypeScript in ${value.slice('@typescript-bin:'.length)}]`:value).join(' ')}`);
|
|
109
139
|
}
|
|
110
140
|
for(const row of summary.ambiguous)
|
|
111
141
|
console.log(` ${row.kind}: ambiguous scripts (${row.scripts.join(', ')}); choose one explicitly`);
|
|
142
|
+
for(const row of summary.skipped)console.log(` Skipped: ${row.reason}`);
|
|
112
143
|
if(!summary.checks.length)console.log('No useful existing verification scripts found.');
|
|
113
144
|
console.log(configured?`Existing ${configFile} left unchanged.`:
|
|
114
145
|
dryRun?`Preview only; ${configFile} was not written.`:
|
|
@@ -121,10 +152,11 @@ if(command==='init'){
|
|
|
121
152
|
if(!json){console.log(`Created ${configFile}. No checks were executed.`);
|
|
122
153
|
console.log('Ready to qualify means the supported contract will be validated; it is not verification evidence.');
|
|
123
154
|
console.log(`Next: review the config, then redue start and redue run ${JSON.stringify(discovery.checks[0].config.name)}.`);
|
|
155
|
+
console.log('Connect your agent: redue agent setup codex (or claude). Preview with --dry-run.');
|
|
124
156
|
console.log('Use redue status first. --sync can establish applicability, but may cost more than rerunning a cheap check.');}
|
|
125
157
|
} else if(unsupported&&!json)console.log(`No config written: ${summary.issue||'unsupported discovery boundary'}.`);
|
|
126
158
|
if(json)console.log(JSON.stringify({...summary,proposed_config:discovery.config}));
|
|
127
|
-
|
|
159
|
+
await exit(0);
|
|
128
160
|
}
|
|
129
161
|
function printLifecycle(output,action,machine){
|
|
130
162
|
if(machine){process.stdout.write(output);return;}
|
|
@@ -141,11 +173,12 @@ function printLifecycle(output,action,machine){
|
|
|
141
173
|
if(stateOverride&&['stop','remove-state'].includes(command)){
|
|
142
174
|
const runtime=path.join(stateOverride,'project-runtime-v1.json');
|
|
143
175
|
if(!fs.existsSync(runtime)){console.log('No REDUE state exists at this path.');process.exit(0);}
|
|
176
|
+
mark('client.engine_submit');
|
|
144
177
|
const result=spawnSync(process.execPath,[engine,runtime,command==='stop'?'stop':'uninstall'],
|
|
145
178
|
{cwd:os.tmpdir(),stdio:['inherit','pipe','inherit'],encoding:'utf8',env:process.env});
|
|
146
179
|
if(result.error)error(result.error.message);
|
|
147
180
|
if(result.stdout)printLifecycle(result.stdout,command,json);
|
|
148
|
-
|
|
181
|
+
await exit(result.status??2);
|
|
149
182
|
}
|
|
150
183
|
let publicConfig;
|
|
151
184
|
try{publicConfig=JSON.parse(fs.readFileSync(configFile,'utf8'));}
|
|
@@ -157,10 +190,7 @@ for(const key of Object.keys(publicConfig))
|
|
|
157
190
|
if(!['schema','root','checks','packageManager'].includes(key))error(`unsupported config field ${key}`);
|
|
158
191
|
if(check&&!publicConfig.checks.some(row=>row.name===check))error(`unknown check ${check}; use redue status to list configured checks`);
|
|
159
192
|
const root=realObservedPath(path.resolve(path.dirname(configFile),publicConfig.root||'.'));
|
|
160
|
-
const
|
|
161
|
-
const state=stateOverride||path.join(stateBase,'vstate',
|
|
162
|
-
`vstate-${createHash('sha256').update(stateIdentity(root)+'\0'+
|
|
163
|
-
stateIdentity(configFile)).digest('hex').slice(0,16)}`);
|
|
193
|
+
const state=stateOverride||projectState(root,configFile);
|
|
164
194
|
if(!/^(?:vstate|redue)-/.test(path.basename(state)))
|
|
165
195
|
error('--state-dir must name a dedicated redue-* directory outside the checkout (legacy vstate-* is also supported)');
|
|
166
196
|
const runtimeFile=path.join(state,'project-runtime-v1.json');
|
|
@@ -169,9 +199,18 @@ function which(name){
|
|
|
169
199
|
if(found)return found;
|
|
170
200
|
error(`cannot resolve executable ${name} on PATH`);
|
|
171
201
|
}
|
|
172
|
-
function token(value){
|
|
202
|
+
function token(value,cwd='.'){
|
|
173
203
|
if(value==='@node')return process.execPath;
|
|
174
204
|
if(value==='@project')return root;
|
|
205
|
+
if(value==='@typescript-compiler:.'){
|
|
206
|
+
try{return directCompiler(root,{verify:false}).entry;}catch(e){error(`direct compiler unavailable: ${e.code||e.message}`);}
|
|
207
|
+
}
|
|
208
|
+
if(value.startsWith('@test-runner:')){
|
|
209
|
+
// Qualification reports a missing runner; status and stop still work.
|
|
210
|
+
try{return testRunnerAt(root,cwd,value.slice('@test-runner:'.length)).entry;}
|
|
211
|
+
catch{return path.join(root,cwd,'node_modules',value.slice('@test-runner:'.length),'<unavailable>');}
|
|
212
|
+
}
|
|
213
|
+
if(value==='@vstate/direct-typescript-probe')return path.join(productRoot,'src','direct-typescript-probe.mjs');
|
|
175
214
|
if(value.startsWith('@typescript-bin:')){
|
|
176
215
|
const dir=value.slice('@typescript-bin:'.length);
|
|
177
216
|
if(!dir||path.isAbsolute(dir)||dir.split('/').includes('..'))
|
|
@@ -192,10 +231,10 @@ function token(value){
|
|
|
192
231
|
if(value.startsWith('@which:'))return which(value.slice(7));
|
|
193
232
|
return value;
|
|
194
233
|
}
|
|
195
|
-
function argv(values,label){
|
|
234
|
+
function argv(values,label,cwd){
|
|
196
235
|
if(!Array.isArray(values)||!values.length||values.some(v=>typeof v!=='string'||!v))
|
|
197
236
|
error(`${label} must be nonempty exact argv`);
|
|
198
|
-
return values.map(token);
|
|
237
|
+
return values.map(value=>token(value,cwd));
|
|
199
238
|
}
|
|
200
239
|
const categories=['source','generated','installedDependencies','environment','toolchain','runtime',
|
|
201
240
|
'probeContinuity'];
|
|
@@ -208,8 +247,11 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
208
247
|
for(const category of categories)if(check.coverage?.[category]&&
|
|
209
248
|
typeof check.coverageReview?.[category]!=='string')
|
|
210
249
|
error(`${check.name}: reviewed coverage ${category} needs a rationale`);
|
|
250
|
+
const npmDirect=check.qualification===directQualification;
|
|
211
251
|
const pnpmTypecheck=check.qualification==='pnpm-tsc-v1';
|
|
212
|
-
|
|
252
|
+
const testsDirect=check.qualification===testsQualification;
|
|
253
|
+
const generalScript=check.qualification===scriptQualification;
|
|
254
|
+
if(check.script&&check.command&&!pnpmTypecheck&&!npmDirect&&!testsDirect)
|
|
213
255
|
error(`${check.name}: choose script or command, not both`);
|
|
214
256
|
const manager=publicConfig.packageManager;
|
|
215
257
|
if(check.script&&!['npm','yarn','pnpm'].includes(manager))
|
|
@@ -218,8 +260,20 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
218
260
|
[`@which:${manager}`,'run',check.script]:null;
|
|
219
261
|
const npmAutomatic=check.qualification==='typescript-noemit-v1';
|
|
220
262
|
const yarnWorkspace=check.qualification==='yarn-workspace-tsc-v1';
|
|
221
|
-
if(check.qualification&&!npmAutomatic&&!yarnWorkspace&&!pnpmTypecheck)
|
|
263
|
+
if(check.qualification&&!npmAutomatic&&!npmDirect&&!yarnWorkspace&&!pnpmTypecheck&&!testsDirect&&!generalScript)
|
|
222
264
|
error(`${check.name}: unknown qualification contract`);
|
|
265
|
+
// The general recipe runs the root package script through the package manager.
|
|
266
|
+
if(generalScript&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.command||check.cwd&&check.cwd!=='.'||
|
|
267
|
+
check.workspace||!scriptKinds.includes(check.kind)))
|
|
268
|
+
error(`${check.name}: the general recipe needs a root typecheck, test or lint script, without command or cwd`);
|
|
269
|
+
if(npmDirect&&(manager!=='npm'||check.cwd&&check.cwd!=='.'||!Array.isArray(check.command)||
|
|
270
|
+
check.command[0]!=='@node'||check.command[1]!=='@typescript-compiler:.'||
|
|
271
|
+
!tscRun({},{script:null,args:check.command.slice(2)}).run?.noEmit))
|
|
272
|
+
error(`${check.name}: direct recipe requires its explicit local compiler command`);
|
|
273
|
+
// The script's own arguments follow the runner; qualification compares them with the script.
|
|
274
|
+
if(testsDirect&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.cwd&&check.cwd!=='.'&&!check.workspace||!Array.isArray(check.command)||
|
|
275
|
+
check.command[0]!=='@node'||!/^@test-runner:(?:vitest|jest)$/.test(check.command[1]||'')))
|
|
276
|
+
error(`${check.name}: test recipe requires its explicit local test runner command`);
|
|
223
277
|
if(npmAutomatic&&(manager!=='npm'||check.cwd&&check.cwd!=='.'))
|
|
224
278
|
error(`${check.name}: automatic TypeScript qualification currently needs root npm script`);
|
|
225
279
|
if(yarnWorkspace&&(manager!=='yarn'||!check.workspace||check.cwd&&check.cwd!=='.'||
|
|
@@ -227,25 +281,32 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
227
281
|
check.command[1]!=='workspace'||check.command[2]!==check.workspace||
|
|
228
282
|
check.command[3]!=='run'))
|
|
229
283
|
error(`${check.name}: Yarn workspace qualification needs its discovered workspace command`);
|
|
230
|
-
if(pnpmTypecheck&&(manager!=='pnpm'||!
|
|
284
|
+
if(pnpmTypecheck&&(manager!=='pnpm'||!Array.isArray(check.command)||
|
|
231
285
|
check.command[0]!=='@node'||
|
|
232
286
|
check.command[1]!==`@typescript-bin:${check.cwd||'.'}`||
|
|
233
287
|
check.workspace&&check.cwd==='.'||
|
|
234
|
-
!check.command.slice(2).
|
|
288
|
+
!tscRun({},{script:null,args:check.command.slice(2)}).run))
|
|
235
289
|
error(`${check.name}: pnpm qualification needs its discovered standalone TypeScript command`);
|
|
236
290
|
const reason=check.kind==='test'?'test runtime, transforms and cache inputs need review':
|
|
237
291
|
check.kind==='lint'?'lint plugins, config and resolver inputs need review':
|
|
238
292
|
check.kind==='build'?'build environment and generated inputs need review':
|
|
239
293
|
'check inputs need review';
|
|
240
|
-
|
|
294
|
+
// A chained script has one check per tsc run: each check's run counts them,
|
|
295
|
+
// so a run added to or removed from the script invalidates them all.
|
|
296
|
+
const runCount=(npmDirect||pnpmTypecheck)&&check.script?publicConfig.checks.filter(other=>
|
|
297
|
+
other.qualification===check.qualification&&other.script===check.script&&
|
|
298
|
+
(other.cwd||'.')===(check.cwd||'.')&&(other.workspace||null)===(check.workspace||null)).length:1;
|
|
299
|
+
if(runCount>16)error(`${check.name}: more than 16 checks for script ${check.script}`);
|
|
300
|
+
const run=npmDirect||pnpmTypecheck?runArgv({script:check.script??null,args:check.command.slice(2),count:runCount}):null;
|
|
301
|
+
const autoProbes=npmDirect?[['@node','@vstate/direct-typescript-probe','@project',...run]]:npmAutomatic?[['@node','@vstate/typescript-contract-probe',
|
|
241
302
|
'@project',check.script,'@which:npm']]:yarnWorkspace?
|
|
242
303
|
[['@node','@vstate/yarn-workspace-typecheck-probe','@project',
|
|
243
304
|
check.workspace,check.command[4],'@which:yarn']]:pnpmTypecheck?
|
|
244
305
|
[['@node','@vstate/pnpm-typecheck-probe','@project',
|
|
245
|
-
check.workspace||'.'
|
|
306
|
+
check.workspace||'.',...run]]:[];
|
|
246
307
|
const windowsContext=process.platform==='win32'?
|
|
247
308
|
['ComSpec','COMSPEC','PATHEXT','USERPROFILE']:[];
|
|
248
|
-
const autoEnvironment=npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
|
|
309
|
+
const autoEnvironment=npmDirect?directEnvironment:testsDirect?testEnvironment:generalScript?scriptEnvironment(manager):npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
|
|
249
310
|
'NODE_ENV','BASH_ENV','ENV',...windowsContext],prefixes:['npm_config_','DYLD_','TSGO_'],
|
|
250
311
|
pathExecutables:['node','npm'],executableIdentity:true}:yarnWorkspace?
|
|
251
312
|
{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
|
|
@@ -254,10 +315,11 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
254
315
|
{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
|
|
255
316
|
prefixes:['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'],
|
|
256
317
|
pathExecutables:['node'],executableIdentity:true}:{};
|
|
257
|
-
return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command
|
|
318
|
+
return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`,
|
|
319
|
+
typeof check.cwd==='string'&&check.cwd&&!path.isAbsolute(check.cwd)&&!check.cwd.split('/').includes('..')?check.cwd:'.'),
|
|
258
320
|
cwd:check.cwd,inputs:check.inputs||[],generatedInputs:check.generatedInputs||[],
|
|
259
321
|
installedInputs:check.installedInputs||(
|
|
260
|
-
npmAutomatic?['node_modules/**']:[]),
|
|
322
|
+
npmAutomatic||npmDirect||testsDirect?['node_modules/**']:[]),
|
|
261
323
|
allowedExternalRoots:check.allowedExternalRoots||[],
|
|
262
324
|
probes:[...(check.probes||[]),...autoProbes]
|
|
263
325
|
.map((p,i)=>argv(p,`${check.name}.probes[${i}]`)),
|
|
@@ -266,6 +328,11 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
266
328
|
coverageReasons:check.coverageReasons||Object.fromEntries(
|
|
267
329
|
categories.map(category=>[category,reason])),
|
|
268
330
|
qualification:check.qualification||null,script:check.script||null,
|
|
331
|
+
...(runCount>1?{runCount}:{}),
|
|
332
|
+
...(npmAutomatic?{qualificationInterpretation:'npm-launcher-withheld@1'}:{}),
|
|
333
|
+
...(npmDirect?{qualificationInterpretation:directInterpretation}:{}),
|
|
334
|
+
...(testsDirect?{qualificationInterpretation:testsInterpretation}:{}),
|
|
335
|
+
...(generalScript?{qualificationInterpretation:scriptInterpretation}:{}),
|
|
269
336
|
workspace:check.workspace||null,kind:check.kind||null};
|
|
270
337
|
});
|
|
271
338
|
const runtime={schema:1,provider:'declared-project@1',root,state,checks};
|
|
@@ -277,7 +344,13 @@ if(maintenance&&!fs.existsSync(runtimeFile)){
|
|
|
277
344
|
process.exit(0);
|
|
278
345
|
}
|
|
279
346
|
if(!maintenance)fs.mkdirSync(state,{recursive:true,mode:0o700});
|
|
280
|
-
|
|
347
|
+
// Two commands can meet on fresh state (SessionStart's start and the first
|
|
348
|
+
// status): whichever creates the runtime file first wins, and the other
|
|
349
|
+
// treats it as existing.
|
|
350
|
+
let created=false;
|
|
351
|
+
if(!maintenance&&!fs.existsSync(runtimeFile))
|
|
352
|
+
try{fs.writeFileSync(runtimeFile,serialized,{flag:'wx',mode:0o600});created=true;}catch(e){if(e.code!=='EEXIST')throw e;}
|
|
353
|
+
if(!maintenance&&!created){
|
|
281
354
|
const previous=fs.readFileSync(runtimeFile,'utf8');
|
|
282
355
|
if(previous!==serialized){
|
|
283
356
|
const pidFile=path.join(state,'observer.lock','pid');
|
|
@@ -287,18 +360,24 @@ if(!maintenance&&fs.existsSync(runtimeFile)){
|
|
|
287
360
|
}
|
|
288
361
|
fs.writeFileSync(runtimeFile,serialized,{mode:0o600});
|
|
289
362
|
}
|
|
290
|
-
}
|
|
363
|
+
}
|
|
291
364
|
|
|
292
365
|
if(command==='status'&&!sync){
|
|
293
366
|
const value=readCachedStatus(state,publicConfig.checks);
|
|
294
367
|
console.log(json?JSON.stringify(value):renderStatus(value,{short}));
|
|
295
|
-
|
|
368
|
+
await exit(0);
|
|
296
369
|
}
|
|
297
370
|
|
|
298
371
|
const details=command==='detail'||command==='explain';
|
|
299
372
|
const internal=command==='remove-state'?'uninstall':
|
|
300
373
|
details?'sync':command==='status'&&sync?'sync':command;
|
|
301
|
-
|
|
374
|
+
// On macOS and Linux the engine runs in this process, which saves a Node startup
|
|
375
|
+
// on every run; its own handlers receive SIGINT and SIGTERM. Windows keeps a
|
|
376
|
+
// child engine because cancellation reaches it over IPC.
|
|
377
|
+
if(command==='run'&&process.platform!=='win32'){
|
|
378
|
+
process.argv=[process.argv[0],engine,runtimeFile,internal,check,...(!json?['--human']:[])];
|
|
379
|
+
await import(pathToFileURL(engine).href);
|
|
380
|
+
}else if(command==='run'){
|
|
302
381
|
const child=spawn(process.execPath,[engine,runtimeFile,internal,check,...(!json?['--human']:[])],
|
|
303
382
|
{stdio:['inherit','inherit','inherit','ipc'],env:process.env});
|
|
304
383
|
const forward=signal=>{if(child.exitCode!==null)return;
|
|
@@ -313,17 +392,20 @@ if(command==='run'){
|
|
|
313
392
|
process.off('SIGINT',interrupt);process.off('SIGTERM',terminate);
|
|
314
393
|
if(outcome.error)error(outcome.error.message);
|
|
315
394
|
process.exit(outcome.status??(outcome.signal?128+os.constants.signals[outcome.signal]:1));
|
|
395
|
+
}else{
|
|
396
|
+
mark('client.engine_submit');
|
|
397
|
+
const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
|
|
398
|
+
{stdio:['inherit','pipe','inherit'],
|
|
399
|
+
encoding:'utf8',env:traceEnvironment()});
|
|
400
|
+
mark('client.engine_complete',{exit:result.status,signal:result.signal});
|
|
401
|
+
if(result.error)error(result.error.message);
|
|
402
|
+
if(result.stdout){
|
|
403
|
+
if(['status','detail','explain'].includes(command)){
|
|
404
|
+
let value;try{value=JSON.parse(result.stdout);}catch{error('status response could not be read; applicability is unknown');}
|
|
405
|
+
if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
|
|
406
|
+
if(json)console.log(JSON.stringify(value));
|
|
407
|
+
else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
|
|
408
|
+
}else printLifecycle(result.stdout,command,json);
|
|
409
|
+
}
|
|
410
|
+
await exit(result.status??1);
|
|
316
411
|
}
|
|
317
|
-
const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
|
|
318
|
-
{stdio:['inherit','pipe','inherit'],
|
|
319
|
-
encoding:'utf8',env:process.env});
|
|
320
|
-
if(result.error)error(result.error.message);
|
|
321
|
-
if(result.stdout){
|
|
322
|
-
if(['status','detail','explain'].includes(command)){
|
|
323
|
-
let value;try{value=JSON.parse(result.stdout);}catch{error('status response could not be read; applicability is unknown');}
|
|
324
|
-
if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
|
|
325
|
-
if(json)console.log(JSON.stringify(value));
|
|
326
|
-
else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
|
|
327
|
-
}else printLifecycle(result.stdout,command,json);
|
|
328
|
-
}
|
|
329
|
-
process.exit(result.status??1);
|
package/docs/README.md
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
For users:
|
|
4
4
|
|
|
5
5
|
- [Install, initialize, run, and remove](user/getting-started.md)
|
|
6
|
+
- [Fixed-build opt-in development pilot](user/pilot.md)
|
|
6
7
|
- [Supported projects and conservative boundaries](user/supported-projects.md)
|
|
7
8
|
- [Configuration](user/configuration.md)
|
|
8
|
-
- [
|
|
9
|
+
- [Connect coding agents and apply the decision policy](user/agents.md)
|
|
9
10
|
- [JSON contract](user/json.md)
|
|
10
11
|
- [Shell usage](user/shell.md)
|
|
11
12
|
|