@redue/cli 0.1.0-alpha.30 → 0.1.0-alpha.31
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 +25 -0
- package/bin/redue.mjs +66 -16
- package/docs/user/getting-started.md +2 -1
- package/docs/user/sandbox.md +14 -0
- package/package.json +1 -1
- package/src/sandbox.mjs +10 -4
- package/src/setup.mjs +22 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.31 — 2026-11-03
|
|
4
|
+
|
|
5
|
+
A check that runs longer than init's trial limit no longer holds up
|
|
6
|
+
`init` for three runs, and can be set up in the sandbox later.
|
|
7
|
+
|
|
8
|
+
- **A run past the trial limit never agrees.** Each trial run at `init`
|
|
9
|
+
stops after 5 minutes. alpha.30 counted a timeout on both sides as
|
|
10
|
+
agreement and ran every trial of a check that timed out. Now, when the
|
|
11
|
+
plain run reaches the limit, `init` runs nothing in the sandbox and
|
|
12
|
+
writes the fallback; when a sandboxed run reaches it, no further trial
|
|
13
|
+
follows. On the 13 census repos with the slowest checks, `init --dry-run`
|
|
14
|
+
took 6224 s in all with alpha.30 and 3670 s now on Linux, 8331 s and
|
|
15
|
+
4776 s on macOS. ADOSMissionControl went from 1138 s to 581 s (Linux),
|
|
16
|
+
agent-afk from 936 s to 323 s, Nomi from 860 s to 528 s (macOS). Checks
|
|
17
|
+
that moved from the sandbox to their fallback: 2 of 210 on Linux
|
|
18
|
+
(ts-liveview typecheck, open-science lint), 3 of 210 on macOS (Nomi and
|
|
19
|
+
ts-liveview typecheck, OneUptime lint).
|
|
20
|
+
- **`redue init --no-time-limit`.** `redue setup` names the checks that
|
|
21
|
+
ran past the limit and prints one command, such as
|
|
22
|
+
`redue init --check typecheck --no-time-limit`. It runs their trials with
|
|
23
|
+
no limit and updates only their rows in the existing config (a row that
|
|
24
|
+
runs the same script), restarts a running observer, and leaves the
|
|
25
|
+
config one `redue remove` still deletes. `init --json` gives such a
|
|
26
|
+
check's kind as `time_limit`.
|
|
27
|
+
|
|
3
28
|
## 0.1.0-alpha.30 — 2026-11-03
|
|
4
29
|
|
|
5
30
|
`init` runs a check fewer times, and tests try the sandbox first.
|
package/bin/redue.mjs
CHANGED
|
@@ -33,7 +33,10 @@ Commands:
|
|
|
33
33
|
init, agent setup claude, start, one recorded run per check
|
|
34
34
|
remove [--yes] Undo setup: integration, observer state, and the config setup created
|
|
35
35
|
init [--dry-run] [--workspace NAME] [--check KIND] [--recipe typescript-direct|tests-direct|node-script|sandbox]
|
|
36
|
-
|
|
36
|
+
[--no-time-limit]
|
|
37
|
+
Discover existing scripts; optionally select one workspace/check.
|
|
38
|
+
--no-time-limit: trial runs have no 5-minute limit; with a config, the checks
|
|
39
|
+
it does not run in the sandbox are tried, and those that qualify are updated
|
|
37
40
|
agent setup codex|claude [--dry-run|--apply] [--json]
|
|
38
41
|
agent doctor [codex|claude] [--json]
|
|
39
42
|
agent remove codex|claude [--dry-run|--apply] [--json]
|
|
@@ -118,11 +121,13 @@ if(command==='init')for(let i=0;i<args.length;){
|
|
|
118
121
|
}
|
|
119
122
|
const check=['run','detail','explain'].includes(command)&&args[0]&&!args[0].startsWith('--')?args.shift():null;
|
|
120
123
|
if(command==='run'&&!check)error('run requires a check name');
|
|
121
|
-
if(args.some(value=>!['--json','--sync','--cached','--dry-run','--short'].includes(value)))error('unknown option; use --help');
|
|
124
|
+
if(args.some(value=>!['--json','--sync','--cached','--dry-run','--short','--no-time-limit'].includes(value)))error('unknown option; use --help');
|
|
122
125
|
if(sync&&!['status','detail','explain'].includes(command))error('--sync is for status or detail');
|
|
123
126
|
if(cachedOnly&&(command!=='status'||args.includes('--sync')))error('--cached is a status option; do not combine it with --sync');
|
|
124
127
|
if(short&&(command!=='status'||json||args.includes('--sync')))error('--short is a cached status option; do not combine it with --sync or --json');
|
|
125
128
|
if(dryRun&&command!=='init')error('--dry-run is for init');
|
|
129
|
+
const noTimeLimit=args.includes('--no-time-limit');
|
|
130
|
+
if(noTimeLimit&&command!=='init')error('--no-time-limit is for init');
|
|
126
131
|
|
|
127
132
|
if(command==='init'){
|
|
128
133
|
const {discoverNodeProject,sourceLinkIssue}=await import('../src/node-onboarding.mjs');
|
|
@@ -164,36 +169,63 @@ if(command==='init'){
|
|
|
164
169
|
// where the plain run passes. Any other difference is tried once with the
|
|
165
170
|
// whole checkout, reusing the plain run. Otherwise init writes what it
|
|
166
171
|
// would have without the sandboxed recipe, with the reason. Each check
|
|
167
|
-
// costs one plain run and one or two sandboxed runs.
|
|
172
|
+
// costs one plain run and one or two sandboxed runs. A run past the trial
|
|
173
|
+
// limit ends the check's trials (time_limit); --no-time-limit sets none.
|
|
168
174
|
const trialIssue=trial=>trial.issue?`the sandbox cannot run here (${trial.issue})`:
|
|
175
|
+
trial.timedOut?`it ran past init's 5-minute trial limit${trial.timedOut==='sandboxed'?' inside the sandbox':''}`+
|
|
176
|
+
` (redue init --check ${trial.kind} --no-time-limit tries it without the limit)`:
|
|
169
177
|
// A failure both ways that shows a refusal cannot be told from a real one.
|
|
170
178
|
trial.same?null:trial.plain!==trial.sandboxed?`exit ${trial.sandboxed} inside the sandbox, ${trial.plain} outside it${trial.line?` (${trial.line})`:''}`:
|
|
171
179
|
trial.line?`it fails both ways (exit ${trial.plain}), with a sign of a refusal inside the sandbox (${trial.line})`:
|
|
172
180
|
`the output differs inside the sandbox: ${JSON.stringify(trial.output.sandboxed)} instead of ${JSON.stringify(trial.output.plain)}`;
|
|
181
|
+
// With --no-time-limit and a config, the checks the config has and does
|
|
182
|
+
// not run in the sandbox are tried, and one it lacks when --check names
|
|
183
|
+
// it. Those that qualify replace their rows (a row that runs the same
|
|
184
|
+
// script) or are added.
|
|
185
|
+
const configured=fs.existsSync(configFile);
|
|
186
|
+
let existing=null,existingText=null;
|
|
187
|
+
if(configured&&noTimeLimit){
|
|
188
|
+
try{existingText=fs.readFileSync(configFile,'utf8');existing=JSON.parse(existingText);}catch(e){error(`cannot read ${configFile}: ${e.message}`);}
|
|
189
|
+
if(!Array.isArray(existing?.checks))error(`cannot read ${configFile}: it has no checks`);
|
|
190
|
+
if((existing.root??'.')!=='.'||!samePath(realObservedPath(path.dirname(configFile)),discovery.root))
|
|
191
|
+
error('init updates only a config at the repository root, without root; run it there');
|
|
192
|
+
}
|
|
193
|
+
const timeout=noTimeLimit?0:undefined,improved=new Map();
|
|
173
194
|
let trials=0;
|
|
174
195
|
for(const [at,row] of [...discovery.checks.entries()]){
|
|
175
196
|
if(row.qualification!==sandboxQualification||row.level!=='ready')continue;
|
|
197
|
+
const old=existing?.checks.find(check=>check.name===row.config.name);
|
|
198
|
+
const sameRun=old&&old.kind===row.config.kind&&(row.config.script?old.script===row.config.script:
|
|
199
|
+
JSON.stringify(old.command)===JSON.stringify(row.config.command));
|
|
200
|
+
const notTried=!existing?null:!old?(checkSelection?null:'the config does not have it; name its kind with --check to add it'):
|
|
201
|
+
isSandboxed(old.qualification)?'the config already runs it in the sandbox':sameRun?null:'the config runs something else under that name';
|
|
202
|
+
if(notTried){discovery.checks[at]=null;discovery.skipped.push({kind:row.config.name,reason:`${row.config.name}: not tried; ${notTried}`});continue;}
|
|
176
203
|
if(!json)console.error(`Running ${row.config.name} inside and outside the sandbox to confirm it...`);
|
|
177
|
-
const docs=await sandboxTrial(discovery.root,row.config,{hide:'docs'});
|
|
204
|
+
const docs={...await sandboxTrial(discovery.root,row.config,{hide:'docs',timeout}),kind:row.config.kind};
|
|
178
205
|
if(!docs.issue)trials++;
|
|
179
206
|
if(!docs.issue&&docs.same){
|
|
180
207
|
row.config={...row.config,qualification:docsQualification};row.qualification=docsQualification;
|
|
181
|
-
row.reason=row.reason.replace('the whole checkout is its input',"the checkout's files git doesn't ignore, except its documentation, are its input")+`; ran both ways at init, exit ${docs.plain}`;
|
|
182
|
-
|
|
183
|
-
|
|
208
|
+
row.reason=row.reason.replace('the whole checkout is its input',"the checkout's files git doesn't ignore, except its documentation, are its input")+`; ran both ways at init, exit ${docs.plain}`;
|
|
209
|
+
improved.set(row.config.name,row.config);continue;}
|
|
210
|
+
if(!docs.issue&&!docs.timedOut&&docs.refusedHidden){
|
|
211
|
+
row.reason+=`; at init it passed, then failed reading a file that was hidden (${docs.refusedHidden}), so the whole checkout is its input, not run that way at init`;
|
|
212
|
+
improved.set(row.config.name,row.config);continue;}
|
|
184
213
|
// Without a list of the ignored files or the documentation, the whole checkout is tried on its own.
|
|
185
|
-
const trial=docs.plainRun?await sandboxTrial(discovery.root,row.config,{plain:docs.plainRun}):
|
|
186
|
-
[hiddenListIssue,hiddenDocsIssue].includes(docs.issue)?await sandboxTrial(discovery.root,row.config):docs;
|
|
214
|
+
const trial=docs.timedOut?docs:docs.plainRun?{...await sandboxTrial(discovery.root,row.config,{plain:docs.plainRun,timeout}),kind:row.config.kind}:
|
|
215
|
+
[hiddenListIssue,hiddenDocsIssue].includes(docs.issue)?{...await sandboxTrial(discovery.root,row.config,{timeout}),kind:row.config.kind}:docs;
|
|
187
216
|
if(trial!==docs&&!trial.issue)trials++;
|
|
188
217
|
const issue=trialIssue(trial);
|
|
189
218
|
if(!issue){row.reason+=`; ran both ways at init, exit ${trial.plain}; the whole checkout is its input: `+
|
|
190
|
-
(docs.issue?docs.issue:`with the files git ignores and the documentation hidden, ${trialIssue(docs)}`);
|
|
191
|
-
|
|
192
|
-
|
|
219
|
+
(docs.issue?docs.issue:`with the files git ignores and the documentation hidden, ${trialIssue(docs)}`);
|
|
220
|
+
improved.set(row.config.name,row.config);continue;}
|
|
221
|
+
// time_limit: the kind that --check takes to try it without the limit.
|
|
222
|
+
const limited=trial.timedOut?{timeLimit:row.config.kind}:{};
|
|
223
|
+
if(row.fallback)discovery.checks[at]={...row.fallback,...limited,reason:`Sandboxed recipe unavailable: ${issue}; ${row.fallback.reason}`};
|
|
224
|
+
else{discovery.checks[at]=null;discovery.skipped.push({kind:row.config.name,...trial.timedOut?{time_limit:row.config.kind}:{},reason:`${row.config.name}: sandboxed recipe unavailable: ${issue}`});}
|
|
193
225
|
}
|
|
194
226
|
discovery.checks=discovery.checks.filter(Boolean).map(({fallback,...row})=>row);
|
|
195
227
|
discovery.config.checks=discovery.checks.map(row=>row.config);
|
|
196
|
-
const
|
|
228
|
+
const unsupported=Boolean(discovery.observationIssue||
|
|
197
229
|
discovery.manager.issue&&!['npm','yarn','pnpm'].includes(discovery.manager.name)||
|
|
198
230
|
discovery.manager.name==='yarn'&&!discovery.manager.version);
|
|
199
231
|
const willWrite=!dryRun&&!configured&&!unsupported&&discovery.checks.length>0;
|
|
@@ -202,9 +234,9 @@ if(command==='init'){
|
|
|
202
234
|
installation_layout:discovery.manager.layout,
|
|
203
235
|
issue:discovery.observationIssue||discovery.manager.issue,workspaces:discovery.workspaces,
|
|
204
236
|
ambiguous:discovery.ambiguous,skipped:discovery.skipped||[],
|
|
205
|
-
checks:discovery.checks.map(({config:check,level,reason})=>({name:check.name,
|
|
237
|
+
checks:discovery.checks.map(({config:check,level,reason,timeLimit})=>({name:check.name,
|
|
206
238
|
script:check.script||null,command:check.command||null,
|
|
207
|
-
workspace:check.workspace||null,level,reason})),config:configFile,
|
|
239
|
+
workspace:check.workspace||null,level,reason,...timeLimit?{time_limit:timeLimit}:{}})),config:configFile,
|
|
208
240
|
written:willWrite,existing:configured,writable:!configured&&!unsupported&&discovery.checks.length>0};
|
|
209
241
|
if(!json) {
|
|
210
242
|
if(summary.package_manager||!discovery.python)console.log(`Node ${summary.node}; ${summary.package_manager||'unknown package manager'}`+
|
|
@@ -223,7 +255,8 @@ if(command==='init'){
|
|
|
223
255
|
console.log(` ${row.kind}: ambiguous scripts (${row.scripts.join(', ')}); choose one explicitly`);
|
|
224
256
|
for(const row of summary.skipped)console.log(` Skipped: ${row.reason}`);
|
|
225
257
|
if(!summary.checks.length)console.log('No useful existing verification scripts found.');
|
|
226
|
-
console.log(
|
|
258
|
+
console.log(existing&&improved.size?(dryRun?`Preview only; ${configFile} was not updated.`:`Updating ${[...improved.keys()].join(', ')} in ${configFile}.`):
|
|
259
|
+
configured?`Existing ${configFile} left unchanged.`:
|
|
227
260
|
dryRun?`Preview only; ${configFile} was not written.`:
|
|
228
261
|
willWrite?`Writing ${configFile}; inspect it before running checks.`:'No configuration will be written.');
|
|
229
262
|
}
|
|
@@ -236,6 +269,23 @@ if(command==='init'){
|
|
|
236
269
|
console.log(`Next: review the config, then redue start and redue run ${JSON.stringify(discovery.checks[0].config.name)}.`);
|
|
237
270
|
console.log('Connect your agent: redue agent setup codex (or claude). Preview with --dry-run.');
|
|
238
271
|
console.log('Then redue status says whether each result applies to the files as they are now.');}
|
|
272
|
+
} else if(existing&&improved.size&&!dryRun){
|
|
273
|
+
if(fs.readFileSync(configFile,'utf8')!==existingText)error(`${configFile} changed while the checks ran; nothing was written. Run init again`);
|
|
274
|
+
// The observer keeps the config it started with: it is restarted around the update.
|
|
275
|
+
const state=stateOverride||projectState(discovery.root,configFile);
|
|
276
|
+
let running=false;try{running=processAlive(Number(fs.readFileSync(path.join(state,'observer.lock','pid'),'utf8')));}catch{}
|
|
277
|
+
const self=action=>spawnSync(process.execPath,[process.argv[1],'--config',configFile,...stateOverride?['--state-dir',stateOverride]:[],action],
|
|
278
|
+
{stdio:['ignore','pipe','pipe'],encoding:'utf8'});
|
|
279
|
+
if(running&&self('stop').status!==0)error('the observer did not stop; nothing was written');
|
|
280
|
+
const names=new Set(existing.checks.map(check=>check.name));
|
|
281
|
+
existing.checks=[...existing.checks.map(check=>improved.get(check.name)??check),...[...improved.values()].filter(check=>!names.has(check.name))];
|
|
282
|
+
const text=JSON.stringify(existing,null,2)+'\n';
|
|
283
|
+
fs.writeFileSync(configFile,text);
|
|
284
|
+
// A config setup created is still one redue remove deletes.
|
|
285
|
+
(await import('../src/setup.mjs')).configRewritten(discovery.root,existingText,text);
|
|
286
|
+
Object.assign(summary,{written:true,updated:[...improved.keys()],observer:!running?'not running':self('start').status===0?'restarted':'not started'});
|
|
287
|
+
if(!json)console.log(`Updated ${[...improved.keys()].join(', ')} in ${configFile}.`+
|
|
288
|
+
{restarted:' Restarted the observer with it.','not started':' The observer did not start again: run redue start.','not running':''}[summary.observer]);
|
|
239
289
|
} else if(unsupported&&!json)console.log(`No config written: ${summary.issue||'unsupported discovery boundary'}.`);
|
|
240
290
|
if(json)console.log(JSON.stringify({...summary,proposed_config:discovery.config}));
|
|
241
291
|
await exit(0);
|
|
@@ -100,7 +100,8 @@ redue explain typecheck
|
|
|
100
100
|
Use a check actually printed by `init`. For a workspace:
|
|
101
101
|
`redue init --workspace @example/package --check typecheck`.
|
|
102
102
|
Initialization previews detected commands and qualification confidence before writing
|
|
103
|
-
`redue.config.json`. An existing config is
|
|
103
|
+
`redue.config.json`. An existing config is not overwritten (`init --no-time-limit`
|
|
104
|
+
updates the rows of checks it [qualifies](sandbox.md#files-git-ignores-and-documentation)). No dependencies are
|
|
104
105
|
installed; a check proposed for the sandboxed recipe runs once plainly and once or
|
|
105
106
|
twice in the sandbox, and no results are recorded. A missing/unsupported boundary is explained. `init` tries
|
|
106
107
|
the strict recipes first and uses the [general recipe](node-script.md) for a root
|
package/docs/user/sandbox.md
CHANGED
|
@@ -87,6 +87,20 @@ reusing the plain run, and writes `sandbox-v1` only if that agrees. So a
|
|
|
87
87
|
check runs two or three times at init, not four. `init` no longer writes
|
|
88
88
|
`sandbox-v2`; a check already set up with it keeps it.
|
|
89
89
|
|
|
90
|
+
Each trial run stops after 5 minutes. A run that stops there never counts as
|
|
91
|
+
agreeing: when the plain run reaches the limit, `init` runs nothing in the
|
|
92
|
+
sandbox and writes the fallback; when a sandboxed run reaches it, no further
|
|
93
|
+
trial follows. `init` marks such a check (`time_limit` in `--json` gives
|
|
94
|
+
its kind), and `redue setup` names them and prints one command:
|
|
95
|
+
`redue init --check typecheck --no-time-limit` (or `redue init
|
|
96
|
+
--no-time-limit` for several kinds). With an existing config, that command
|
|
97
|
+
runs with no limit the checks the config has and does not run in the
|
|
98
|
+
sandbox, and a check it lacks when `--check` names its kind. It replaces
|
|
99
|
+
the row of each that qualifies, if the row runs the same script, or adds
|
|
100
|
+
it, and leaves the rest of the config as it was. It restarts a running
|
|
101
|
+
observer with the new config, and `redue remove` still deletes a config
|
|
102
|
+
`setup` created.
|
|
103
|
+
|
|
90
104
|
With `sandbox-v3`, each run also hides the documentation: the files git
|
|
91
105
|
lists (and does not ignore) named `*.md`, `*.mdx`, `*.rst`, `LICENSE*` or
|
|
92
106
|
`CHANGELOG*` anywhere, and text, image and PDF files (`.txt`, `.adoc`,
|
package/package.json
CHANGED
package/src/sandbox.mjs
CHANGED
|
@@ -595,13 +595,17 @@ function refusedHidden(plain,sandboxed,entries,supervisor){
|
|
|
595
595
|
const others=sandboxed.output.split(/\r?\n/).filter(text=>!hiddenRefusal(text,entries)).join('\n');
|
|
596
596
|
return denialLine(others)?null:line;
|
|
597
597
|
}
|
|
598
|
-
//
|
|
598
|
+
// init's limit on each trial run, in ms; a test can shorten it.
|
|
599
|
+
export const trialLimit=process.env.VSTATE_TEST_FAULTS==='1'&&Number(process.env.VSTATE_TEST_TRIAL_LIMIT)||300000;
|
|
600
|
+
// {same, plain, sandboxed, line, refusedHidden, timedOut} or {issue} when the sandbox cannot run here.
|
|
601
|
+
// A run that reaches `timeout` (0: none) is never the same as another:
|
|
602
|
+
// `timedOut` says which, and when the plain run does, no sandboxed run follows.
|
|
599
603
|
// With `hide` 'docs', the documentation (hiddenDocs) is hidden too.
|
|
600
604
|
// With `hide`, the git-ignored entries hiddenEntries lists are kept from the
|
|
601
605
|
// sandboxed run, and it also returns them, their count, the run times and, on
|
|
602
606
|
// Linux, the launcher's rule count and setup time.
|
|
603
607
|
// `plain`: the plain run of an earlier trial of the same check, reused.
|
|
604
|
-
export async function sandboxTrial(root,check,{timeout=
|
|
608
|
+
export async function sandboxTrial(root,check,{timeout=trialLimit,hide=false,plain:earlier=null}={}){
|
|
605
609
|
const supported=sandboxSupport();
|
|
606
610
|
if(supported.issue)return {issue:supported.issue};
|
|
607
611
|
const python=Boolean(pythonTool(check));
|
|
@@ -621,6 +625,7 @@ export async function sandboxTrial(root,check,{timeout=300000,hide=false,plain:e
|
|
|
621
625
|
const run=async(argv,env)=>{const started=Date.now(),r=await runToEnd(argv,{cwd:root,env,timeout});
|
|
622
626
|
return {status:r.timedOut?'timeout':r.status??r.signal??r.error?.code,output:`${r.stdout}\n${r.stderr}`,ms:Date.now()-started};};
|
|
623
627
|
const plain=earlier??await run(command,{...process.env,...python?{}:{npm_config_update_notifier:'false'}});
|
|
628
|
+
if(plain.status==='timeout')return {same:false,plain:'timeout',sandboxed:null,timedOut:'plain',line:null,refusedHidden:null,plainRun:plain};
|
|
624
629
|
const reads=[root,home,tmp,...sandboxPlaces(selected,process.env,root),...systemPlaces];
|
|
625
630
|
const sandboxed=await run(sandboxArgv(supported.mode,command,{reads,tmp,python:supported.python,hidden,stats,
|
|
626
631
|
...hide==='docs'?{docs:{root,entries:docEntries}}:{},
|
|
@@ -644,8 +649,9 @@ export async function sandboxTrial(root,check,{timeout=300000,hide=false,plain:e
|
|
|
644
649
|
const counts=tests?[plain,sandboxed].map(r=>testCounts(r.output)):null;
|
|
645
650
|
const read=counts?.some(c=>Object.values(c).some(value=>value!==null));
|
|
646
651
|
const differs=tests&&read?(JSON.stringify(counts[0])===JSON.stringify(counts[1])?null:{plain:JSON.stringify(counts[0]),sandboxed:JSON.stringify(counts[1])}):output;
|
|
647
|
-
const
|
|
648
|
-
|
|
652
|
+
const timedOut=sandboxed.status==='timeout'?'sandboxed':null;
|
|
653
|
+
const same=!timedOut&&plain.status===sandboxed.status&&!denied&&!differs&&!supervisor;
|
|
654
|
+
return {same,plain:plain.status,sandboxed:sandboxed.status,output:differs,plainRun:plain,timedOut,
|
|
649
655
|
line:supervisor||denied||(plain.status===sandboxed.status?null:sandboxed.output.trim().split('\n').at(-1)?.slice(0,200)||null),
|
|
650
656
|
refusedHidden:same?null:refusedHidden(plain,sandboxed,entries,supervisor),
|
|
651
657
|
...hide?{hidden:entries.length,entries,docs:docEntries.length,plainMs:plain.ms,sandboxedMs:sandboxed.ms,...launcher?{rules:launcher.rules,setupMs:launcher.ms}:{}}:{}};
|
package/src/setup.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import fs from 'node:fs';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import {createHash} from 'node:crypto';
|
|
14
14
|
import {spawnSync} from 'node:child_process';
|
|
15
|
-
import {testCounts} from './sandbox.mjs';
|
|
15
|
+
import {isSandboxed,testCounts} from './sandbox.mjs';
|
|
16
16
|
import {createInterface} from 'node:readline/promises';
|
|
17
17
|
import {projectState} from './project-location.mjs';
|
|
18
18
|
import {claudeHooksInstalled,removeClaudeHooks,removeIntegration} from './agent-integration.mjs';
|
|
@@ -31,6 +31,18 @@ async function confirm(question,yes,{defaultYes}){
|
|
|
31
31
|
return defaultYes?!/^n/i.test(answer):/^y/i.test(answer);}finally{prompt.close();}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// The setup record of the checkout at `root`, in its shared Git directory.
|
|
35
|
+
function recordOf(root){
|
|
36
|
+
const common=git(root,'rev-parse','--git-common-dir');
|
|
37
|
+
return common.status===0?path.join(path.resolve(root,common.stdout.trim()),`redue-setup-${hash(root).slice(0,16)}.json`):null;
|
|
38
|
+
}
|
|
39
|
+
// When init updates the config setup created, the record follows, so that
|
|
40
|
+
// redue remove still deletes it.
|
|
41
|
+
export function configRewritten(root,before,after){
|
|
42
|
+
const file=recordOf(root),record=file&&JSON.parse(readText(file)??'null');
|
|
43
|
+
if(record?.configCreated&&record.configHash===hash(before))fs.writeFileSync(file,JSON.stringify({...record,configHash:hash(after)},null,2)+'\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
export async function setupCommand(args,{entry,cwd=process.cwd()}){
|
|
35
47
|
const action=args.shift();
|
|
36
48
|
const yes=args.includes('--yes'),rest=args.filter(arg=>arg!=='--yes');
|
|
@@ -50,8 +62,7 @@ export async function setupCommand(args,{entry,cwd=process.cwd()}){
|
|
|
50
62
|
stdio:options.capture?['ignore','pipe','pipe']:'inherit',timeout:options.timeout});
|
|
51
63
|
// One record per checkout, beside the others in the shared Git directory:
|
|
52
64
|
// linked worktrees share .git/info/exclude.
|
|
53
|
-
const
|
|
54
|
-
const recordFile=path.join(common,`redue-setup-${hash(root).slice(0,16)}.json`);
|
|
65
|
+
const recordFile=recordOf(root),common=path.dirname(recordFile);
|
|
55
66
|
// Other checkouts' records, for checkouts that still exist.
|
|
56
67
|
const others=()=>{try{return fs.readdirSync(common).filter(name=>/^redue-setup-[0-9a-f]{16}\.json$/.test(name)&&path.join(common,name)!==recordFile)
|
|
57
68
|
.map(name=>JSON.parse(readText(path.join(common,name))??'null')).filter(other=>other&&fs.existsSync(other.root));}catch{return [];}};
|
|
@@ -78,6 +89,12 @@ async function setup({root,redue,yes,record:previous,recordFile,exclude,save}){
|
|
|
78
89
|
const found=JSON.parse(preview.stdout);
|
|
79
90
|
if(!configExisted&&!found.writable)throw Error(`nothing to set up: ${found.issue||(found.checks.length?found.checks.map(check=>`${check.name}: ${check.reason}`).join('; '):'no typecheck, lint or test script REDUE can use')}`);
|
|
80
91
|
const levels=new Map(found.checks.map(check=>[check.name,check.level]));
|
|
92
|
+
// The checks whose trial ran past init's time limit, unless the config
|
|
93
|
+
// already runs them in the sandbox, and the kinds init's --check takes.
|
|
94
|
+
const sandboxed=new Set(configExisted?JSON.parse(readText(configPath)).checks.filter(check=>isSandboxed(check.qualification)).map(check=>check.name):[]);
|
|
95
|
+
const limited=[...found.checks.map(check=>[check.name,check.time_limit]),...(found.skipped||[]).map(row=>[row.kind,row.time_limit])]
|
|
96
|
+
.filter(([name,kind])=>kind&&!sandboxed.has(name));
|
|
97
|
+
const limitedKinds=[...new Set(limited.map(([,kind])=>kind))];
|
|
81
98
|
const names=configExisted?JSON.parse(readText(configPath)).checks.map(check=>check.name):found.checks.map(check=>check.name);
|
|
82
99
|
const ready=names.filter(name=>levels.get(name)==='ready');
|
|
83
100
|
console.log(`Setting up REDUE for Claude Code in ${root}`);
|
|
@@ -161,6 +178,8 @@ async function setup({root,redue,yes,record:previous,recordFile,exclude,save}){
|
|
|
161
178
|
if(ready.length&&!due.length)console.log(`Already CURRENT: ${ready.join(', ')}.`);
|
|
162
179
|
for(const row of statusOf())console.log(` ${row.name}: ${row.freshness}${row.result?' / '+row.result:''}${row.freshness==='CURRENT'?'':
|
|
163
180
|
levels.get(row.name)!=='ready'?' — recording only':' — '+row.reason}`);
|
|
181
|
+
if(limited.length)console.log(`${limited.map(([name])=>name).join(', ')} ran past init's 5-minute trial limit, so ${limited.length>1?'they are':'it is'} not sandboxed. `+
|
|
182
|
+
`To try ${limited.length>1?'them':'it'} without the limit: redue init${limitedKinds.length>1?'':` --check ${limitedKinds[0]}`} --no-time-limit`);
|
|
164
183
|
console.log('Done. Start a new Claude Code session in this project. Undo with redue remove.');
|
|
165
184
|
return {applied:true};
|
|
166
185
|
}
|