@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 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
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@6reduk/workspace-pipeline",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Workspace-local Git pipeline configuration dispatcher",
5
5
  "type": "module",
6
6
  "bin": {
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
- process.exitCode=await runCli(args,{stdout:outputWriter(write(process.stdout),json),stderr:outputWriter(write(process.stderr),json),registry:providerRegistry,compatibility:createClaudeCompatibility()});
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
+ }
@@ -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
+ }
@@ -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 safe = value => String(value).replace(/[\u0000-\u001f\u007f-\u009f]/g, c => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
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) {