@6reduk/workspace-pipeline 0.6.0 → 0.7.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,18 @@
1
1
  # Workspace Pipeline CLI
2
2
 
3
+ ## Desired-state installation (0.7.0 release candidate)
4
+
5
+ The working tree adds manifest schema 2: ordinary `setup --source ... --adapters ...`
6
+ creates `workspace.json` after confirmation; `update` uses it without a separate
7
+ preview file. Whole-owned adapter contents are replaced, including custom files;
8
+ backups are opt-in with `--backup`. Shared configurations change only declared fields.
9
+ See [setup, update, doctor and source authoring](docs/desired-state.md).
10
+ This is not in the published 0.6.0 package. The sections below describe the legacy
11
+ release and explicit recovery routes; do not mix their backup/preview contracts
12
+ with schema 2.
13
+
14
+ ## Published legacy lifecycle
15
+
3
16
  Everyday update: `workspace-pipeline update --workspace <directory>` shows the
4
17
  changes and asks for confirmation. Use `--yes` for unattended application or
5
18
  `--preview --json` to retain the advanced saved-plan workflow. See
@@ -77,7 +90,7 @@ Startup cleanup is disabled unless the user enables a local policy via
77
90
  that policy; public setup/update uses the same coordinator. Policy commands only
78
91
  write `.pipeline/retention.json`, never harness settings or project documents.
79
92
  Repository init/adopt previews acquire the explicitly selected Git pipeline source;
80
- remote access additionally requires `--network`. No command launches MCP or changes
93
+ remote acquisition follows the selected source without an extra flag. No command launches MCP or changes
81
94
  credentials. Repository setup and provider activation are separate steps.
82
95
  See [repository commands](docs/repositories.md), [recovery](docs/repository-recovery.md)
83
96
  and [manual recovery boundaries](docs/repository-manual-recovery.md).
@@ -0,0 +1,243 @@
1
+ # Install and update a workspace pipeline
2
+
3
+ Development status: these commands require the unreleased 0.7.0 desired-state build,
4
+ not the published 0.6.0 package. `workspace-pipeline` below means that build's
5
+ installed executable. During development use `node <cli-repository>/src/cli.js`.
6
+ Do not run these examples against a real workspace until the migration is reviewed.
7
+
8
+ ## What is installed
9
+
10
+ A Git repository contains a schema-2 `pipeline.json` (or YAML equivalent). It
11
+ declares which files to deliver and which named configuration fields to set or
12
+ remove. The CLI does not run scripts from the pipeline source.
13
+
14
+ The wrapper is an existing directory containing your project repository or
15
+ repositories. The CLI configures the wrapper, not the game/application code.
16
+ It does not move, clone, initialize or delete project repositories in this route.
17
+ Create the directory first and place your repositories where you intend them to live.
18
+
19
+ ## First installation — no prepared JSON file required
20
+
21
+ Example with a committed local pipeline source:
22
+
23
+ ```powershell
24
+ workspace-pipeline setup --workspace "C:\Work\Game" --source "C:\Sources\my-pipeline" --subdirectory pipelines/unity --adapters codex,claude-grok
25
+ ```
26
+
27
+ Use actual adapter IDs declared by your source. Unity currently supplies `codex`
28
+ and `claude-grok`; the latter delivers Claude files for both harnesses. Kimi is
29
+ deferred. Native plugins are not installed by this command.
30
+
31
+ `--source` also accepts an HTTPS or SSH Git URL. Git authentication remains yours;
32
+ never place tokens or passwords in the URL. A relative local source path is resolved
33
+ from the shell's current directory, then stored relative to the wrapper. A local
34
+ source on another Windows volume is not supported by the current source schema.
35
+ Only committed Git contents are used, not uncommitted edits. `--ref` defaults to
36
+ `HEAD`, and `--subdirectory` to `.`. Fetching the selected remote source is part of
37
+ preparation; no separate network authorization flag is required. Setup, update and reset may fetch the configured remote Git source
38
+ before confirmation of workspace changes.
39
+
40
+ By default the layout is one repository named `game` at `project`, with documentation
41
+ at `project/docs`. The displayed summary shows these choices before confirmation.
42
+ Override them when the actual structure differs:
43
+
44
+ ```powershell
45
+ workspace-pipeline setup --workspace "C:\Work\Game" --source "C:\Sources\my-pipeline" --adapters codex --repository game=game-repo --documentation game=docs
46
+ ```
47
+
48
+ For multiple repositories, repeat `--repository` and explicitly bind documentation:
49
+
50
+ ```powershell
51
+ workspace-pipeline setup --workspace "C:\Work\Platform" --source "C:\Sources\platform-pipeline" --adapters codex --repository api=services/api --repository docs=knowledge --documentation docs=.
52
+ ```
53
+
54
+ Repository and documentation paths are relative and use `/` for separators. IDs
55
+ are lookup keys, not inferred project types. Repository paths must not overlap.
56
+ The CLI saves the resulting declaration as `workspace.json` only after confirmation.
57
+ It is a source/layout configuration, not an execution preview. Existing
58
+ `workspace.json` is not overwritten by `setup --source`; use the installed binding
59
+ with ordinary `setup`/`update`, or explicitly edit the declaration for a supported
60
+ change. Pipeline-ID switching is not implemented in the new engine.
61
+
62
+ ## Update and customization loss
63
+
64
+ ```powershell
65
+ workspace-pipeline doctor --workspace "C:\Work\Game"
66
+ workspace-pipeline update --workspace "C:\Work\Game"
67
+ workspace-pipeline update --workspace "C:\Work\Game" --backup
68
+ ```
69
+
70
+ `update` resolves the Git ref saved in `workspace.json`, shows affected paths and
71
+ configuration changes, and asks for confirmation. Enter or an answer other than
72
+ `y`, `yes`, `д`, `да` cancels. `--yes` authorizes unattended application; noninteractive
73
+ use otherwise requires `--preview`. `--json` changes presentation, not authorization.
74
+
75
+ There is **no backup by default**. A whole-owned directory is replaced with the
76
+ declared contents: extra files inside it are removed and modified delivered files
77
+ are overwritten. Owned single files are replaced individually. Other directories
78
+ are not implicitly owned. Only declared fields of shared JSON/TOML configs change;
79
+ credentials, models and unrelated fields are preserved. Obsolete recorded targets
80
+ are retired on update. Invalid paths, links, malformed settings and unavailable
81
+ sources are errors, not customization conflicts to bypass.
82
+
83
+ `--backup` saves affected old adapter files/local configuration under
84
+ `.pipeline/backups/desired-*`. A requested global backup stays inside the relevant
85
+ user profile. Paths are reported. These backups may contain secrets: keep them local.
86
+ If requested backup fails, replacement does not proceed.
87
+
88
+ Global changes are explicitly displayed with their resolved user path and affect
89
+ other workspaces. Unity's Claude/Grok adapter sets five user-wide Grok Claude
90
+ compatibility flags; it does not change account/model credentials or grant trust.
91
+
92
+ ## Doctor, preview and interrupted work
93
+
94
+ `doctor` lists extra, modified and missing delivered files, unsafe paths and differing
95
+ configuration fields. It displays the recorded Git source, revision and digest.
96
+ For Git-bound installations it also reports a missing, invalid or changed
97
+ `workspace.json` (source, layout and recorded adapter selection). Repository roots
98
+ from the installation and a valid current declaration remain protected. Changing
99
+ the declaration is reported as drift, not silently accepted as an installed state.
100
+ Older records without adapter IDs cannot verify selection equality; records without
101
+ Git binding retain file/config-only inspection.
102
+ It does not fetch updates or certify source trust, native skill discovery, MCP or
103
+ runtime execution. Version labels do not require rewriting project documentation.
104
+
105
+ `setup ... --preview --json` and `update ... --preview --json` inspect without applying
106
+ or creating the descriptor. In schema 2 this is an observation, **not** a saved-plan
107
+ authorization accepted by `--apply`. The old `--apply --preview <file>` route is
108
+ retained for schema-1/legacy operations only; a schema-2 replayable-plan route is
109
+ not implemented. Normal schema-2 use needs neither external file nor `--apply`.
110
+
111
+ Replacement is not atomic across all files and has no automatic rollback. After a
112
+ failure, inspect the reported partial state and `doctor`. The pending record binds
113
+ the same Git revision/digest; retry must use that same source and selection. If a
114
+ branch moved, the new revision is refused as a retry; `reset` can reacquire the
115
+ recorded commit instead (see below). Do not delete pending records
116
+ or locks to force success. For an abandoned lock use the bounded recovery command
117
+ below; old legacy recovery commands do not apply to these records.
118
+ The schema-2 switch command route is not yet implemented;
119
+ legacy commands are not a substitute. These are release limitations, not a claim
120
+ that the entire migration is finished.
121
+
122
+ ## Reset to the installed version
123
+
124
+ ```powershell
125
+ workspace-pipeline reset --workspace "C:\Work\Game"
126
+ workspace-pipeline reset --workspace "C:\Work\Game" --backup
127
+ ```
128
+
129
+ The default is the recorded installed Git commit, **not latest**. Reset reacquires
130
+ that commit from the saved source and checks its recorded digest before changing
131
+ anything. It restores delivered files and named settings and removes extra files
132
+ inside owned directories. It uses the same confirmation and optional backup rules
133
+ as update. `--preview --json` only displays the proposed effect.
134
+
135
+ Use `update` to move to the current source ref. Reset leaves `workspace.json`
136
+ unchanged, so later updates still follow that ref. If the declaration's source,
137
+ adapter selection or layout changed, reset refuses rather than guessing which
138
+ mapping to restore. Missing binding/adapter metadata or unavailable historical Git
139
+ objects likewise prevents reset before replacement. This is not an offline backup
140
+ restore. No project files are reset and no Git checkout/reset is performed.
141
+
142
+ After a handled installation interruption, reset restores the pending installation's
143
+ recorded commit even if its branch moved. It does not cancel an unfinished removal,
144
+ erase pending metadata or release a stale lock. Global named settings can be
145
+ restored just as during update, with the shared effect displayed before consent.
146
+
147
+ ## Recover an abandoned lock after process termination
148
+
149
+ ```powershell
150
+ workspace-pipeline recover-lock --workspace "C:\Work\Game"
151
+ # Only when the reported global Grok lock belongs to this workspace:
152
+ workspace-pipeline recover-lock --workspace "C:\Work\Game" --global
153
+ ```
154
+
155
+ This command does not kill processes, resume installation or modify configuration.
156
+ It verifies the lock's purpose/workspace, checks that its PID is absent on the same
157
+ host, displays the target and asks for confirmation (`--yes` for unattended use).
158
+ `--preview --json` is read-only. A live/reused PID, unknown host, malformed or older
159
+ owner format, missing owner, links, foreign entries or a conflicting repository
160
+ operation prevent recovery. Lock age alone is never sufficient.
161
+
162
+ The exact stale directory is renamed to a temporary sibling, its owner identity
163
+ is rechecked, and its owner file and directory are deleted nonrecursively. No
164
+ lock archive is retained after success. Pending operation records remain intact.
165
+ Run doctor and explicitly retry reset (installation) or remove (removal) afterward.
166
+ No automatic retry is performed.
167
+
168
+ Recoverers are serialized using the existing OS lease (Windows/Linux only).
169
+ After the atomic rename, recovery never touches the original lock path again:
170
+ a new writer may already own it. If deletion fails, the error reports
171
+ `lockReleased: true` and `residualPath` for inspection. A process crash after
172
+ rename can leave a `*-retiring-<uuid>` sibling; it is not an active lock, and
173
+ recovery does not automatically sweep such leftovers or older recovered-lock
174
+ archives. No recursive cleanup or archive-retention command is provided.
175
+
176
+ These are cooperative-process safeguards, not isolation against hostile concurrent
177
+ filesystem mutation. Empty/partially written owners and old token-only global locks
178
+ need separate inspection, not a force-unlock flag. Source/package corruption and
179
+ partially written configuration are not repaired by lock recovery.
180
+
181
+ ## Remove and reinstall
182
+
183
+ ```powershell
184
+ workspace-pipeline remove --workspace "C:\Work\Game"
185
+ workspace-pipeline update --workspace "C:\Work\Game"
186
+ ```
187
+
188
+ `remove` operates offline on the recorded installation, displays its scope and asks
189
+ for confirmation (`--yes` for unattended use). It removes the entire delivery,
190
+ including custom files in owned directories, and its named local configuration
191
+ fields. It does not need the Git source. `--backup` is optional, off by default.
192
+ `--preview --json` observes the removal without applying it.
193
+
194
+ Project repositories, unrelated config fields, `workspace.json`, existing journals
195
+ and backups are preserved. Shared user-wide Grok compatibility is deliberately
196
+ left unchanged because other workspaces may use it. Remove does not uninstall
197
+ native plugins or erase unknown/unrecorded old adapters. It does not support partial
198
+ provider selection. Missing/invalid ownership, unsafe paths and malformed shared
199
+ configuration cause refusal, not an unrestricted directory wipe.
200
+
201
+ Doctor then reports `not-installed`. Ordinary `update` or `setup` reinstalls using
202
+ the retained source declaration. A handled interruption during removal can be
203
+ continued with the same `remove` command; installation/update cannot replace its
204
+ unfinished marker. Conversely, finish an interrupted installation before removing
205
+ it. After a hard kill, recover abandoned locks first using the separate command.
206
+
207
+ ## For pipeline authors
208
+
209
+ ```json
210
+ {
211
+ "schemaVersion": 2,
212
+ "id": "example",
213
+ "version": "1.0.0",
214
+ "files": [{"source": "core", "target": ".example", "kind": "directory"}],
215
+ "adapters": {
216
+ "codex": {
217
+ "providers": ["codex"],
218
+ "files": [{"source": "instructions.md", "target": "AGENTS.md", "kind": "file"}],
219
+ "settings": []
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ Top-level files are shared and installed once. Adapter files/settings are selected
226
+ by adapter ID; a provider cannot occur twice in a selection. Source directories
227
+ must contain committed files; Git does not supply empty directories. Targets cannot
228
+ overlap each other, repositories, CLI metadata, `workspace.json`, or whole shared
229
+ configuration files. Field declarations use `target`, JSON-pointer `pointer`,
230
+ `operation: set|remove` and a value only for `set`.
231
+
232
+ Supported configuration targets are `codex.workspace` (named agents/MCP),
233
+ `claude.workspace` (`enabledMcpjsonServers`), `claude.mcp` (named MCP),
234
+ `grok.workspace` (named MCP), and `grok.user` (five Claude compatibility booleans).
235
+ They map to CLI-defined paths; sources cannot provide arbitrary home filenames.
236
+ Hooks, credentials, model settings and executable installer extensions are outside
237
+ this configuration contract. Review delivered MCP commands before trusting a source.
238
+
239
+ Keep canonical rules in shared files and provider discovery files small. Validate
240
+ relative links in the installed layout, not just in the source repository. Test
241
+ fresh install, update after custom changes, removed upstream targets, optional
242
+ backup, interruption, and doctor using disposable profiles and the packed CLI.
243
+ Package checks are not human approval or proof of model behavior.
@@ -1,5 +1,10 @@
1
1
  # Lifecycle CLI routing — development scope
2
2
 
3
+ This page describes schema-1 legacy operations and their saved-plan/recovery
4
+ contract. For the new schema-2 desired-state setup/update (no mandatory preview,
5
+ no default backup), see [the desired-state guide](desired-state.md). Do not apply
6
+ the legacy conflict/backup/launcher rules below to the new installer.
7
+
3
8
  The generic dispatcher routes `setup`, `update`, `repair`, `remove`, `switch` and `continue` through
4
9
  the native operation APIs and their coordinators. The packaged entrypoint supplies
5
10
  compiled Codex, Claude, Kimi and Grok adapters. S11 review and native Kimi/Grok
@@ -63,8 +68,8 @@ The temporary preview is not a replacement for durable recovery evidence.
63
68
  Once a trusted registry is provided by the CLI assembly:
64
69
 
65
70
  ```text
66
- workspace-pipeline setup --workspace <absolute-wrapper> [--manifest <absolute-file>] [--network]
67
- workspace-pipeline update --workspace <absolute-wrapper> [--manifest <absolute-file>] [--network]
71
+ workspace-pipeline setup --workspace <absolute-wrapper> [--manifest <absolute-file>]
72
+ workspace-pipeline update --workspace <absolute-wrapper> [--manifest <absolute-file>]
68
73
  workspace-pipeline setup --workspace <absolute-wrapper> --apply --preview <absolute-json-file>
69
74
  workspace-pipeline update --workspace <absolute-wrapper> --apply --preview <absolute-json-file>
70
75
  ```
@@ -72,7 +77,7 @@ workspace-pipeline update --workspace <absolute-wrapper> --apply --preview <abso
72
77
  Preview displays a human-readable summary by default. Add `--json` to return
73
78
  the complete prepared JSON on stdout for saving and subsequent apply. It may acquire a Git
74
79
  source into temporary storage outside the wrapper, but does not write provider
75
- configuration. Network acquisition requires explicit `--network`. When manifest
80
+ configuration. Remote Git acquisition follows the selected source. When manifest
76
81
  is omitted, setup uses the standard workspace manifest; update uses the recorded
77
82
  manifest origin. Relocation does not silently rebind it. Use the explicit
78
83
  [manifest rebind workflow](rebind.md) to change that origin.
@@ -80,7 +85,7 @@ manifest origin. Relocation does not silently rebind it. Use the explicit
80
85
  Save prepared JSON privately, inspect its operations and then explicitly invoke
81
86
  `--apply --preview`. It may contain configuration secrets and absolute local
82
87
  paths; do not commit it or send it to shared logs. Apply does not accept
83
- `--manifest` or `--network`, acquire a fresh source, or substitute a new preview.
88
+ `--manifest`, acquire a fresh source, or substitute a new preview.
84
89
  Missing/stale staged data is an error, not permission to download replacements.
85
90
 
86
91
  Apply binds the verb and exact wrapper before lock acquisition, then invokes
@@ -147,7 +152,7 @@ Removal preserves other installed providers and shared files while needed; full
147
152
  removal restores taken-over content and removes only owned scope. Missing/corrupt
148
153
  snapshots, backups, unresolved history or conflicting user edits fail closed.
149
154
  Neither route reads the original manifest or downloads from Git. `--manifest`
150
- and `--network` are rejected. `--providers` and `--bundles` are remove-preview-only;
155
+ is rejected. `--providers` and `--bundles` are remove-preview-only;
151
156
  omitting both previews all installed providers and bundles. Use actual installed
152
157
  bundle IDs; selecting a bundle member via `--providers` fails with
153
158
  `remove.bundle-required`. The saved subject fixes the actual selection, so apply
@@ -168,7 +173,7 @@ Interrupted-operation continuation has a separate exact approval boundary below.
168
173
  With the same trusted built-in registry restriction:
169
174
 
170
175
  ```text
171
- workspace-pipeline switch --workspace <absolute-wrapper> --manifest <absolute-incoming-manifest> [--network]
176
+ workspace-pipeline switch --workspace <absolute-wrapper> --manifest <absolute-incoming-manifest>
172
177
  workspace-pipeline switch --workspace <absolute-wrapper> --apply --preview <absolute-json-file>
173
178
  ```
174
179
 
@@ -7,7 +7,7 @@ synthetic testing; independent review and real deployment approval remain pendin
7
7
 
8
8
  `workspace-pipeline migration unity preview --workspace <absolute-wrapper> --manifest <absolute-manifest>`
9
9
  prepares the complete proposal, staging committed Git source outside the wrapper.
10
- Add `--network` only to permit remote acquisition. Keep the JSON private: it can
10
+ Selecting a remote Git source permits acquisition during preparation. Keep the JSON private: it can
11
11
  contain full config bytes. The command does not disable plugins or install files.
12
12
 
13
13
  `workspace-pipeline migration unity inspect --workspace <absolute-wrapper> --recovery <relative-record> --phase <phase>`
package/docs/rebind.md CHANGED
@@ -27,7 +27,7 @@ and `proposed.manifest`. The first command only reads local state/history and th
27
27
  new manifest. It does not fetch Git, move files, or activate the new origin.
28
28
 
29
29
  `--accept-rebind` explicitly approves the saved proposal for source acquisition.
30
- For a remote source, add `--network` to this second command. Inspect the resulting
30
+ A remote source is fetched during this second command. Inspect the resulting
31
31
  update operations before the third command, which separately approves file writes.
32
32
  The new origin becomes active as part of that normal, locked update, not as a
33
33
  standalone write to `.pipeline/state.json`.
@@ -29,8 +29,8 @@ workspace-pipeline wrap --workspace "C:\Work\GameWorkspace" --manifest "C:\Work\
29
29
 
30
30
  Use `--json` and save the exact JSON stdout as UTF-8 `preview.json` outside the affected repository
31
31
  and wrapper. Read its operations and blockers. Preview may acquire a temporary
32
- Git snapshot, but does not perform repository effects. Remote access is explicit
33
- with `--network`; local Git does not need that flag. Do not put credentials into
32
+ Git snapshot, but does not perform repository effects. A selected remote source
33
+ is fetched during preparation; local Git is read from disk. Do not put credentials into
34
34
  source URLs or retained preview files.
35
35
 
36
36
  ```powershell
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@6reduk/workspace-pipeline",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Workspace-local Git pipeline configuration dispatcher",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "required": ["schemaVersion", "id", "version", "adapters"],
6
+ "properties": {
7
+ "schemaVersion": {"const": 2},
8
+ "id": {"type": "string", "pattern": "^[a-z][a-z0-9-]{0,62}$"},
9
+ "version": {"type": "string", "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"},
10
+ "files": {"$ref": "#/properties/adapters/additionalProperties/properties/files"},
11
+ "adapters": {
12
+ "type": "object", "minProperties": 1,
13
+ "propertyNames": {"pattern": "^[a-z][a-z0-9-]{0,62}$"},
14
+ "additionalProperties": {
15
+ "type": "object", "additionalProperties": false,
16
+ "required": ["providers", "files", "settings"],
17
+ "properties": {
18
+ "providers": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"enum": ["codex", "claude", "grok", "kimi"]}},
19
+ "files": {"type": "array", "maxItems": 10000, "items": {
20
+ "type": "object", "additionalProperties": false,
21
+ "required": ["source", "target", "kind"],
22
+ "properties": {
23
+ "source": {"type": "string", "minLength": 1},
24
+ "target": {"type": "string", "minLength": 1},
25
+ "kind": {"enum": ["file", "directory"]}
26
+ }
27
+ }},
28
+ "settings": {"type": "array", "maxItems": 1000, "items": {
29
+ "type": "object", "additionalProperties": false,
30
+ "required": ["target", "pointer", "operation"],
31
+ "properties": {
32
+ "target": {"enum": ["codex.workspace", "claude.workspace", "claude.mcp", "grok.workspace", "grok.user"]},
33
+ "pointer": {"type": "string", "minLength": 2},
34
+ "operation": {"enum": ["set", "remove"]},
35
+ "value": {}
36
+ },
37
+ "allOf": [{"if": {"properties": {"operation": {"const": "set"}}}, "then": {"required": ["value"]}, "else": {"not": {"required": ["value"]}}}]
38
+ }}
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
package/src/cli.js CHANGED
@@ -3,6 +3,7 @@ import {providerRegistry} from './providers/registry.js';
3
3
  import {outputWriter} from './commands/output.js';
4
4
  import {createClaudeCompatibility} from './compat/claude.js';
5
5
  import {runInteractiveUpdate} from './commands/interactive-update.js';
6
+ import {tryDesiredLifecycle} from './commands/desired-lifecycle.js';
6
7
  import {createInterface} from 'node:readline/promises';
7
8
  const write=stream=>text=>new Promise((resolve,reject)=>stream.write(text,error=>error?reject(error):resolve()));
8
9
  // Prevent an unhandled pipe error; write callbacks report delivery failures.
@@ -10,14 +11,16 @@ process.stdout.on('error',()=>{});process.stderr.on('error',()=>{});
10
11
  const args=process.argv.slice(2),json=args.includes('--json');
11
12
  const options={stdout:outputWriter(write(process.stdout),json),stderr:outputWriter(write(process.stderr),json),registry:providerRegistry,compatibility:createClaudeCompatibility()};
12
13
  try{
13
- process.exitCode=await runInteractiveUpdate(args,options,{
14
+ const interaction={
14
15
  isTTY:Boolean(process.stdin.isTTY&&process.stderr.isTTY),display:write(process.stderr),
15
16
  confirm:async()=>{
16
17
  const rl=createInterface({input:process.stdin,output:process.stderr});
17
18
  try{return /^(y|yes|д|да)$/i.test((await rl.question('Apply these changes? [y/N] ')).trim());}
18
19
  catch{return false;}finally{rl.close();}
19
20
  }
20
- });
21
+ };
22
+ const desired=await tryDesiredLifecycle(args,options,interaction);
23
+ process.exitCode=desired??await runInteractiveUpdate(args,options,interaction);
21
24
  }catch{
22
25
  await options.stderr(JSON.stringify({error:'cli.interactive-io',next:'Inspect doctor before retry; no automatic retry or reset.'})+'\n');process.exitCode=2;
23
26
  }
@@ -0,0 +1,93 @@
1
+ import path from 'node:path';
2
+ import {homedir} from 'node:os';
3
+ import {ContractError,fail,parse} from '../contracts/parse.js';
4
+ import {absoluteRoot} from '../workspace/paths.js';
5
+ import {observeTargets} from '../operations/state.js';
6
+ import {utf8} from '../source/inventory.js';
7
+ import {prepareDesiredWorkspace} from '../desired-state/source.js';
8
+ import {materializeDesiredFiles,inspectDesiredFiles} from '../desired-state/inventory.js';
9
+ import {compileDesiredManifest} from '../contracts/desired-state.js';
10
+ import {readDesiredRecords} from '../desired-state/records.js';
11
+ import {includeRetiredTargets} from '../desired-state/retirement.js';
12
+ import {prepareLocalSettings} from '../desired-state/local-settings.js';
13
+ import {compileDesiredSettings} from '../desired-state/settings.js';
14
+ import {applyDesiredFiles} from '../desired-state/apply-files.js';
15
+ import {safeTerminalText as safe} from './output.js';
16
+ import {desiredArguments,initialDescriptor} from './desired-setup.js';
17
+ import {tryDesiredRemove} from './desired-remove.js';
18
+ import {prepareDesiredReset} from '../desired-state/reset.js';
19
+ import {runDesiredRecovery} from './desired-recover.js';
20
+
21
+ // V2 entrypoint; null preserves the old CLI path for old descriptors. No new
22
+ // source-supplied executable hooks, model changes or external preview required.
23
+ export async function tryDesiredLifecycle(args,options,{isTTY=false,confirm,display,hostOptions={}}={}) {
24
+ if(args[0]==='recover-lock')return runDesiredRecovery(args,options,{isTTY,confirm,display,hostOptions});
25
+ if(args[0]==='remove')return tryDesiredRemove(args,options,{isTTY,confirm,display,hostOptions});
26
+ if(!['setup','update','reset'].includes(args[0]) || args.includes('--apply'))return null;
27
+ const at=args.indexOf('--workspace');if(at<0 || !args[at+1])return null;
28
+ try {
29
+ const workspace=absoluteRoot(args[at+1]);
30
+ const [entry]=await observeTargets(workspace,['workspace.json']);
31
+ const creating=args[0]==='setup'&&args.includes('--source');
32
+ if(!creating&&entry.bytes===null)return null;
33
+ let descriptor=entry.bytes===null?null:parse(utf8(entry.bytes),'json');
34
+ if(!creating&&descriptor.schemaVersion!==2)return null;
35
+ const parsed=desiredArguments(args),{seen}=parsed;
36
+ if(creating) {
37
+ if(entry.bytes!==null)fail('desired.setup-already-configured');
38
+ descriptor=initialDescriptor(workspace,parsed,{cwd:hostOptions.cwd});
39
+ } else if(['--source','--adapters','--ref','--subdirectory','--repository','--documentation'].some(f=>seen.has(f)))fail('cli.arguments');
40
+ const dry=seen.has('--preview'),yes=seen.has('--yes');
41
+ if(dry&&yes)fail('cli.arguments');
42
+ if(!dry && !yes && (!isTTY||!confirm))fail('cli.confirmation-required');
43
+ const records=await readDesiredRecords(workspace,{allowLegacyMigration:true});
44
+ if(records.pending?.value.intent==='remove')fail('desired.finish-remove-before-update');
45
+ // Selecting a remote Git source and requesting setup/update authorizes the
46
+ // source fetch, not changes to the workspace before confirmation.
47
+ const prepare=args[0]==='reset'?prepareDesiredReset:prepareDesiredWorkspace;
48
+ const prepared=await prepare(workspace,JSON.stringify(descriptor),{
49
+ tempRoot:hostOptions.tempRoot,network:descriptor.pipeline?.transport==='remote'});
50
+ const compiled=compileDesiredManifest(prepared.input.manifest,{selected:prepared.input.selected,protectedPaths:prepared.input.protectedPaths});
51
+ const delivered=materializeDesiredFiles(compiled,prepared.input.source);
52
+ const previousRoots=Object.values(records.installed?.value.binding?.layout.repositories??{}).map(r=>r.path);
53
+ const protectedPaths=[...new Set([...prepared.input.protectedPaths,...previousRoots,...(records.legacy?.protectedPaths??[])])];
54
+ compileDesiredManifest(prepared.input.manifest,{selected:prepared.input.selected,protectedPaths});
55
+ const retired=includeRetiredTargets(records.installed?.value??records.legacy?.ownership,compiled,delivered,protectedPaths);
56
+ const files=await inspectDesiredFiles(workspace,retired.desired);
57
+ const local=await prepareLocalSettings(workspace,retired.settings.filter(s=>s.target!=='grok.user'));
58
+ const globalOps=retired.settings.filter(s=>s.target==='grok.user');let global=null;
59
+ if(globalOps.length) {
60
+ const home=absoluteRoot(hostOptions.userHome??homedir());
61
+ const [observed]=await observeTargets(home,['.grok/config.toml']);
62
+ const result=compileDesiredSettings('grok.user',observed.bytes,globalOps);
63
+ global={path:path.join(home,'.grok/config.toml'),changed:result.changed};
64
+ }
65
+ const summary={command:args[0],workspace,pipeline:compiled.pipeline,providers:compiled.providers,
66
+ files,localSettings:local.map(s=>({path:s.path,changed:s.changed})),globalSettings:global,
67
+ backup:seen.has('--backup'),legacyMigration:Boolean(records.legacy),...(prepared.reset?{reset:prepared.reset}:{}),descriptor:{path:'workspace.json',action:creating?'create':'preserve',layout:descriptor.layout},provenance:prepared.provenance,preparation:prepared.preparation};
68
+ if(dry){await options.stdout(JSON.stringify({...summary,status:'preview',applied:false})+'\n');return files.ready?0:1;}
69
+ const lines=[`Workspace Pipeline — ${args[0]}`,`Workspace: ${safe(workspace)}`,
70
+ `Pipeline: ${safe(compiled.pipeline.id)} @ ${safe(compiled.pipeline.version)}`,
71
+ `Backup: ${summary.backup?'enabled':'OFF'}`,
72
+ 'The entire declared adapter scope will be replaced; custom files in it will be removed.'];
73
+ lines.push(`workspace.json: ${creating?'create':'preserve'}`);
74
+ if(prepared.reset)lines.push(`Reset: ${prepared.reset.mode}, Git revision ${safe(prepared.reset.commit)} (not latest).`);
75
+ for(const [id,repo] of Object.entries(descriptor.layout.repositories))lines.push(`Repository ${safe(id)}: ${safe(repo.path)} (not moved or cloned)`);
76
+ lines.push(`Documentation: ${safe(descriptor.layout.documentation.repository)} / ${safe(descriptor.layout.documentation.path)}`);
77
+ for(const category of ['extra','modified','missing','blocked'])for(const item of files[category])lines.push(` ${category}: ${safe(item.path)}`);
78
+ for(const item of summary.localSettings)lines.push(` settings: ${safe(item.path)} (${item.changed?'change':'unchanged'})`);
79
+ if(global)lines.push(`GLOBAL settings: ${safe(global.path)} (${global.changed?'change':'unchanged'}); affects other workspaces.`);
80
+ if(records.legacy)lines.push('Migrate legacy installation state; preserve its record in .pipeline/history and leave existing journals unchanged.');
81
+ if(display)await display(lines.join('\n')+'\n');else await options.stderr(lines.join('\n')+'\n');
82
+ if(!files.ready)fail('desired.unsafe-target');
83
+ if(!yes && await confirm()!==true){await options.stdout(JSON.stringify({status:'cancelled',applied:false})+'\n');return 0;}
84
+ // Apply the already acquired immutable source; do not fetch again after consent.
85
+ const result=await applyDesiredFiles({...prepared.input,backup:summary.backup},{...hostOptions,createDescriptor:creating,migrateLegacy:Boolean(records.legacy),expectedRecords:prepared.expectedRecords});
86
+ await options.stdout(JSON.stringify({...result,...(prepared.reset?{reset:prepared.reset}:{}),provenance:prepared.provenance,preparation:prepared.preparation})+'\n');return 0;
87
+ }catch(error){
88
+ await options.stderr(JSON.stringify({error:error instanceof ContractError?error.code:'cli.desired-io',
89
+ ...(error.code==='desired.setup-already-configured'?{hint:'workspace.json already exists. Run doctor, then update using the saved declaration; do not repeat setup --source. If an installation was interrupted and the source ref moved, use reset. Finish pending removal with remove first.'}:{}),
90
+ ...(error.code==='desired.finish-remove-before-update'?{hint:'Finish the pending removal with remove before setup, update or reset.'}:{}),
91
+ ...(error.desiredState?{application:error.desiredState}:{})})+'\n');return 2;
92
+ }
93
+ }
@@ -0,0 +1,28 @@
1
+ import {ContractError,fail} from '../contracts/parse.js';
2
+ import {desiredArguments} from './desired-setup.js';
3
+ import {inspectDesiredLock,recoverDesiredLock} from '../desired-state/lock-recovery.js';
4
+ import {safeTerminalText as safe} from './output.js';
5
+
6
+ export async function runDesiredRecovery(args,options,{isTTY=false,confirm,display,hostOptions={}}={}) {
7
+ try {
8
+ if(args.filter(a=>a==='--global').length>1)fail('cli.arguments');
9
+ const {seen,values}=desiredArguments(args.filter(a=>a!=='--global'));
10
+ if(!values.has('--workspace')||[...seen].some(f=>!['--workspace','--yes','--preview','--json'].includes(f)))fail('cli.arguments');
11
+ const dry=seen.has('--preview'),yes=seen.has('--yes');
12
+ if(dry&&yes)fail('cli.arguments');
13
+ if(!dry&&!yes&&(!isTTY||!confirm))fail('cli.confirmation-required');
14
+ const workspace=values.get('--workspace'),settings={userHome:hostOptions.userHome,global:args.includes('--global')};
15
+ const observed=await inspectDesiredLock(workspace,settings);
16
+ if(dry){await options.stdout(JSON.stringify({...observed,applied:false})+'\n');return observed.status==='owner-unconfirmed'?1:0;}
17
+ const text=['Workspace Pipeline — recover-lock',`Workspace: ${safe(observed.workspace)}`,
18
+ `Lock: ${safe(observed.path)}`,`Owner status: ${safe(observed.lock?.liveness??'absent')}`,
19
+ 'Only stale lock metadata will be deleted; no process is killed and no installation is resumed.',
20
+ ...(settings.global?['GLOBAL lock: user-wide Grok config lock; configuration remains unchanged.']:[])].join('\n')+'\n';
21
+ if(display)await display(text);else await options.stderr(text);
22
+ if(observed.status==='owner-unconfirmed')fail('desired.recovery-owner-unconfirmed');
23
+ if(!yes&&await confirm()!==true){await options.stdout(JSON.stringify({status:'cancelled',applied:false})+'\n');return 0;}
24
+ await options.stdout(JSON.stringify(await recoverDesiredLock(workspace,observed,settings))+'\n');return 0;
25
+ }catch(error){await options.stderr(JSON.stringify({error:error instanceof ContractError?error.code:'cli.desired-recovery-io',
26
+ ...(error.recovery?{recovery:error.recovery}:{}),
27
+ next:'Inspect lock paths and pending state; do not delete unknown locks or operation records.'})+'\n');return 2;}
28
+ }
@@ -0,0 +1,48 @@
1
+ import {ContractError,fail} from '../contracts/parse.js';
2
+ import {absoluteRoot} from '../workspace/paths.js';
3
+ import {observeTargets} from '../operations/state.js';
4
+ import {readDesiredRecords,installedPath,pendingPath} from '../desired-state/records.js';
5
+ import {prepareDesiredRemoval} from '../desired-state/removal.js';
6
+ import {inspectDesiredFiles} from '../desired-state/inventory.js';
7
+ import {prepareLocalSettings} from '../desired-state/local-settings.js';
8
+ import {applyDesiredFiles} from '../desired-state/apply-files.js';
9
+ import {desiredArguments} from './desired-setup.js';
10
+ import {safeTerminalText as safe} from './output.js';
11
+
12
+ export async function tryDesiredRemove(args,options,{isTTY=false,confirm,display,hostOptions={}}={}) {
13
+ if(args[0]!=='remove')return null;
14
+ const at=args.indexOf('--workspace');if(at<0||!args[at+1])return null;
15
+ try {
16
+ const workspace=absoluteRoot(args[at+1]);
17
+ const markers=await observeTargets(workspace,[installedPath,pendingPath]);
18
+ if(markers.every(m=>m.bytes===null))return null;
19
+ const {seen}=desiredArguments(args);
20
+ if([...seen].some(f=>!['--workspace','--preview','--yes','--json','--backup'].includes(f)))fail('cli.arguments');
21
+ const dry=seen.has('--preview'),yes=seen.has('--yes');
22
+ if(dry&&yes)fail('cli.arguments');
23
+ if(!dry&&!yes&&(!isTTY||!confirm))fail('cli.confirmation-required');
24
+ const records=await readDesiredRecords(workspace),prepared=await prepareDesiredRemoval(workspace,records);
25
+ if(prepared.alreadyRemoved){await options.stdout(JSON.stringify({workspace,status:'removed',applied:false,globalSettings:'preserved'})+'\n');return 0;}
26
+ const files=await inspectDesiredFiles(workspace,prepared.desired);
27
+ const local=await prepareLocalSettings(workspace,prepared.settings);
28
+ const summary={command:'remove',workspace,status:'preview',applied:false,files,
29
+ localSettings:local.map(s=>({path:s.path,changed:s.changed})),backup:seen.has('--backup'),globalSettings:'preserved',
30
+ preserved:['workspace.json','project repositories','.pipeline journals and backups']};
31
+ if(dry){await options.stdout(JSON.stringify(summary)+'\n');return files.ready?0:1;}
32
+ const lines=['Workspace Pipeline — remove',`Workspace: ${safe(workspace)}`,`Backup: ${summary.backup?'enabled':'OFF'}`,
33
+ 'Remove all recorded adapter files, including custom files inside owned directories.',
34
+ 'Preserve repositories, workspace.json, journals/backups and shared global settings.'];
35
+ for(const item of files.extra)lines.push(` remove: ${safe(item.path)}`);
36
+ for(const item of files.blocked)lines.push(` blocked: ${safe(item.path)}`);
37
+ for(const item of summary.localSettings)lines.push(` settings: ${safe(item.path)} (${item.changed?'remove owned fields':'unchanged'})`);
38
+ if(display)await display(lines.join('\n')+'\n');else await options.stderr(lines.join('\n')+'\n');
39
+ if(!files.ready)fail('desired.unsafe-target');
40
+ if(!yes&&await confirm()!==true){await options.stdout(JSON.stringify({status:'cancelled',applied:false})+'\n');return 0;}
41
+ const result=await applyDesiredFiles({workspace,protectedPaths:prepared.protectedPaths,backup:summary.backup},
42
+ {...hostOptions,removeExisting:true,removalExpected:{installed:records.installed?.hash??null,pending:records.pending?.hash??null}});
43
+ await options.stdout(JSON.stringify({...result,workspace,preserved:summary.preserved})+'\n');return 0;
44
+ }catch(error){
45
+ await options.stderr(JSON.stringify({error:error instanceof ContractError?error.code:'cli.desired-remove-io',
46
+ ...(error.desiredState?{application:error.desiredState}:{})})+'\n');return 2;
47
+ }
48
+ }