@6reduk/workspace-pipeline 0.5.0 → 0.6.0
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/README.md +5 -0
- package/docs/lifecycle-cli.md +46 -0
- package/package.json +1 -1
- package/src/cli.js +15 -2
- package/src/commands/dispatch.js +1 -0
- package/src/commands/interactive-update.js +59 -0
- package/src/commands/output.js +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Workspace Pipeline CLI
|
|
2
2
|
|
|
3
|
+
Everyday update: `workspace-pipeline update --workspace <directory>` shows the
|
|
4
|
+
changes and asks for confirmation. Use `--yes` for unattended application or
|
|
5
|
+
`--preview --json` to retain the advanced saved-plan workflow. See
|
|
6
|
+
[update modes](docs/lifecycle-cli.md#everyday-update-executable).
|
|
7
|
+
|
|
3
8
|
Claude/Grok shared delivery: [one Claude configuration, plain Grok launch,
|
|
4
9
|
global compatibility preview and recovery](docs/claude-grok.md).
|
|
5
10
|
|
package/docs/lifecycle-cli.md
CHANGED
|
@@ -14,6 +14,52 @@ CLI assembly supplies built-in adapters, never package code.
|
|
|
14
14
|
|
|
15
15
|
## Preview and apply contract
|
|
16
16
|
|
|
17
|
+
### Everyday update (executable)
|
|
18
|
+
|
|
19
|
+
`workspace-pipeline update --workspace <absolute-wrapper>` prepares a plan,
|
|
20
|
+
shows every affected path and any user-wide Grok compatibility changes, and
|
|
21
|
+
asks `Apply these changes? [y/N]`. Accepted confirmations are `y`, `yes`, `д`,
|
|
22
|
+
or `да` (case-insensitive, surrounding whitespace ignored). Enter or any other
|
|
23
|
+
answer cancels. Cancellation exits 0 with `status: cancelled, applied: false`;
|
|
24
|
+
exit 0 alone does not mean an update was applied. The CLI
|
|
25
|
+
manages a private temporary preview internally and uses the same approval and
|
|
26
|
+
drift checks as saved-plan apply. No automatic reset or conflict bypass occurs.
|
|
27
|
+
|
|
28
|
+
`--yes` authorizes this update without a prompt (including displayed global
|
|
29
|
+
changes). Non-interactive input without `--yes` refuses before preparation.
|
|
30
|
+
`--preview` explicitly requests preparation only. `--preview --json` saves a
|
|
31
|
+
machine plan; for compatibility, `update --json` alone also remains preview-only.
|
|
32
|
+
`--yes --json` emits the final machine result on stdout and the change summary
|
|
33
|
+
on stderr; `--json` by itself never authorizes writes. The deterministic embedded
|
|
34
|
+
`runCli` API keeps its prior preview/apply behavior; this convenience is in the
|
|
35
|
+
executable entry layer. Other commands keep their existing explicit flow.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
workspace-pipeline update --workspace C:\Work\Game
|
|
39
|
+
workspace-pipeline update --workspace C:\Work\Game --yes
|
|
40
|
+
workspace-pipeline update --workspace C:\Work\Game --preview --json > preview.json
|
|
41
|
+
workspace-pipeline update --workspace C:\Work\Game --apply --preview C:\Private\preview.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Saved plans remain useful for LLM work, migrations and separate approval. Do not
|
|
45
|
+
share them publicly: configuration payloads can contain private values. A failed
|
|
46
|
+
apply can have partial effects: inspect doctor/recovery, do not blindly retry.
|
|
47
|
+
|
|
48
|
+
The internal preview is in the current user's OS temporary directory, inside a
|
|
49
|
+
unique `wpc-approved-update-*` directory. It may contain private configuration.
|
|
50
|
+
On POSIX the preview is created with mode `0600`; Windows/NTFS does not enforce
|
|
51
|
+
that POSIX mode, so confidentiality depends on the inherited Windows ACLs of
|
|
52
|
+
your temporary directory. The CLI does not change those ACLs. Use a private
|
|
53
|
+
user temp directory, not a shared writable temp location.
|
|
54
|
+
|
|
55
|
+
Normal completion (including handled apply failure) attempts to remove only
|
|
56
|
+
that preview and its empty directory. A killed process or failed cleanup can
|
|
57
|
+
leave the directory behind; there is no automatic janitor. After stopping the
|
|
58
|
+
owning update process and inspecting doctor/recovery, you may manually remove
|
|
59
|
+
its obsolete `wpc-approved-update-*` directory. Do not delete another active
|
|
60
|
+
run's directory or `.pipeline/transactions`/backups as part of this cleanup.
|
|
61
|
+
The temporary preview is not a replacement for durable recovery evidence.
|
|
62
|
+
|
|
17
63
|
Once a trusted registry is provided by the CLI assembly:
|
|
18
64
|
|
|
19
65
|
```text
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {runCli} from './commands/dispatch.js';
|
|
3
2
|
import {providerRegistry} from './providers/registry.js';
|
|
4
3
|
import {outputWriter} from './commands/output.js';
|
|
5
4
|
import {createClaudeCompatibility} from './compat/claude.js';
|
|
5
|
+
import {runInteractiveUpdate} from './commands/interactive-update.js';
|
|
6
|
+
import {createInterface} from 'node:readline/promises';
|
|
6
7
|
const write=stream=>text=>new Promise((resolve,reject)=>stream.write(text,error=>error?reject(error):resolve()));
|
|
7
8
|
// Prevent an unhandled pipe error; write callbacks report delivery failures.
|
|
8
9
|
process.stdout.on('error',()=>{});process.stderr.on('error',()=>{});
|
|
9
10
|
const args=process.argv.slice(2),json=args.includes('--json');
|
|
10
|
-
|
|
11
|
+
const options={stdout:outputWriter(write(process.stdout),json),stderr:outputWriter(write(process.stderr),json),registry:providerRegistry,compatibility:createClaudeCompatibility()};
|
|
12
|
+
try{
|
|
13
|
+
process.exitCode=await runInteractiveUpdate(args,options,{
|
|
14
|
+
isTTY:Boolean(process.stdin.isTTY&&process.stderr.isTTY),display:write(process.stderr),
|
|
15
|
+
confirm:async()=>{
|
|
16
|
+
const rl=createInterface({input:process.stdin,output:process.stderr});
|
|
17
|
+
try{return /^(y|yes|д|да)$/i.test((await rl.question('Apply these changes? [y/N] ')).trim());}
|
|
18
|
+
catch{return false;}finally{rl.close();}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}catch{
|
|
22
|
+
await options.stderr(JSON.stringify({error:'cli.interactive-io',next:'Inspect doctor before retry; no automatic retry or reset.'})+'\n');process.exitCode=2;
|
|
23
|
+
}
|
package/src/commands/dispatch.js
CHANGED
|
@@ -31,6 +31,7 @@ import {parseCompat,runCompat} from './compat.js';
|
|
|
31
31
|
|
|
32
32
|
export const help=`Workspace Pipeline CLI — development preview
|
|
33
33
|
Usage: workspace-pipeline doctor --workspace <absolute-directory> [--recovery <relative-record>] [--json]
|
|
34
|
+
workspace-pipeline update --workspace <absolute-directory> [--yes | --preview] [--json]
|
|
34
35
|
workspace-pipeline compat claude [recover-lock] [--apply --preview <absolute-json-file>] [--json]
|
|
35
36
|
workspace-pipeline launch grok --workspace <absolute-directory> --executable <absolute-native-executable> [--inspect] [--execute]
|
|
36
37
|
workspace-pipeline <init|adopt|wrap> --workspace <absolute-directory> --choices <absolute-json-file> [--manifest <absolute-file>] [--network]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {mkdtemp,writeFile,unlink,rmdir} from 'node:fs/promises';
|
|
2
|
+
import {tmpdir} from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {runCli,parseCommand} from './dispatch.js';
|
|
5
|
+
import {safeTerminalText as safe} from './output.js';
|
|
6
|
+
|
|
7
|
+
export function updateSummary(saved){
|
|
8
|
+
const p=saved.kind==='prepared-with-claude-compatibility'?saved.workspace:saved;
|
|
9
|
+
const plan=p.preview.plan;
|
|
10
|
+
const lines=['Workspace Pipeline — update',`Workspace: ${safe(plan.workspace)}`,
|
|
11
|
+
`Pipeline: ${safe(plan.desired.pipelineId)} @ ${safe(plan.desired.version)}`,
|
|
12
|
+
`Providers: ${plan.desired.providers.map(safe).join(', ')}`,`Changes: ${plan.targets.length}`];
|
|
13
|
+
for(const t of plan.targets)lines.push(` ${safe(t.action)} ${safe(t.path)}`);
|
|
14
|
+
const c=saved.compatibility;
|
|
15
|
+
if(c){lines.push(`Grok global compatibility: ${safe(c.status)}`);
|
|
16
|
+
if(c.path)lines.push(` Config: ${safe(c.path)}`);
|
|
17
|
+
for(const k of c.changes??[])lines.push(` compat.claude.${safe(k)} = true`);
|
|
18
|
+
for(const b of c.blockers??[])lines.push(` BLOCKED: ${safe(b)}`);
|
|
19
|
+
if(c.warning)lines.push(safe(c.warning));
|
|
20
|
+
}
|
|
21
|
+
lines.push('No automatic reset. Apply rechecks files and approval before writing.');
|
|
22
|
+
return lines.join('\n')+'\n';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Human-facing entry only. runCli remains the deterministic preview/apply API.
|
|
26
|
+
export async function runInteractiveUpdate(args,options,{isTTY=false,confirm,display,execute=runCli}={}){
|
|
27
|
+
if(args[0]!=='update'||args.includes('--apply'))return execute(args,options);
|
|
28
|
+
const yes=args.includes('--yes'),dry=args.includes('--preview');
|
|
29
|
+
const clean=args.filter(x=>x!=='--yes'&&x!=='--preview');
|
|
30
|
+
if(args.filter(x=>x==='--yes').length>1||args.filter(x=>x==='--preview').length>1||(yes&&dry)){
|
|
31
|
+
await options.stdout(JSON.stringify({error:'cli.arguments'})+'\n');return 2;
|
|
32
|
+
}
|
|
33
|
+
// --json alone retains the existing machine preview contract; it never authorizes writes.
|
|
34
|
+
if(dry||(args.includes('--json')&&!yes))return execute(clean,options);
|
|
35
|
+
try{parseCommand(clean);}catch{return execute(clean,options);}
|
|
36
|
+
if(!yes&&(!isTTY||!confirm)){
|
|
37
|
+
await options.stdout(JSON.stringify({error:'cli.confirmation-required',next:'Use an interactive terminal, --yes to apply, or --preview --json for a saved plan.'})+'\n');return 2;
|
|
38
|
+
}
|
|
39
|
+
let raw='';
|
|
40
|
+
const code=await execute(clean,{...options,stdout:async text=>{raw+=text;}});
|
|
41
|
+
if(code!==0){if(raw)await options.stdout(raw);return code;}
|
|
42
|
+
const saved=JSON.parse(raw);
|
|
43
|
+
await display(updateSummary(saved));
|
|
44
|
+
if(saved.compatibility?.status==='blocked'){
|
|
45
|
+
await options.stdout(JSON.stringify({status:'blocked',error:'grok-compat.blocked'})+'\n');return 1;
|
|
46
|
+
}
|
|
47
|
+
if(!yes&&await confirm()!==true){await options.stdout(JSON.stringify({status:'cancelled',applied:false})+'\n');return 0;}
|
|
48
|
+
// Feed the exact approved bytes through the same public saved-preview verifier.
|
|
49
|
+
const dir=await mkdtemp(path.join(tmpdir(),'wpc-approved-update-'));
|
|
50
|
+
const file=path.join(dir,'preview.json');
|
|
51
|
+
try{
|
|
52
|
+
await writeFile(file,raw,{flag:'wx',mode:0o600});
|
|
53
|
+
const command=parseCommand(clean);
|
|
54
|
+
return await execute(['update','--workspace',command.workspace,'--apply','--preview',file],options);
|
|
55
|
+
}finally{
|
|
56
|
+
// Only our one named temporary file, never a recursive workspace cleanup.
|
|
57
|
+
await unlink(file).catch(()=>{});await rmdir(dir).catch(()=>{});
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/commands/output.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Presentation boundary only: internal command results and saved JSON contracts
|
|
2
2
|
// remain unchanged. Never infer success from the shape of a rendered result.
|
|
3
|
-
const
|
|
3
|
+
export const safeTerminalText = value => String(value).replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, c => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
4
|
+
const safe = safeTerminalText;
|
|
4
5
|
const privateKey = /bytes|base64|content|secret|token|password/i;
|
|
5
6
|
|
|
6
7
|
export function formatResult(value) {
|