@6reduk/workspace-pipeline 0.5.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.
Files changed (36) hide show
  1. package/README.md +19 -1
  2. package/docs/desired-state.md +243 -0
  3. package/docs/lifecycle-cli.md +57 -6
  4. package/docs/migrations/unity.md +1 -1
  5. package/docs/rebind.md +1 -1
  6. package/docs/repositories.md +2 -2
  7. package/package.json +1 -1
  8. package/schemas/pipeline-v2.schema.json +43 -0
  9. package/src/cli.js +18 -2
  10. package/src/commands/desired-lifecycle.js +93 -0
  11. package/src/commands/desired-recover.js +28 -0
  12. package/src/commands/desired-remove.js +48 -0
  13. package/src/commands/desired-setup.js +47 -0
  14. package/src/commands/dispatch.js +28 -16
  15. package/src/commands/init.js +1 -1
  16. package/src/commands/interactive-update.js +59 -0
  17. package/src/commands/migration.js +2 -3
  18. package/src/commands/output.js +12 -1
  19. package/src/contracts/desired-state.js +56 -0
  20. package/src/desired-state/apply-files.js +172 -0
  21. package/src/desired-state/binding.js +23 -0
  22. package/src/desired-state/doctor.js +106 -0
  23. package/src/desired-state/global-settings.js +74 -0
  24. package/src/desired-state/inventory.js +102 -0
  25. package/src/desired-state/legacy-state.js +37 -0
  26. package/src/desired-state/local-settings.js +40 -0
  27. package/src/desired-state/lock-recovery.js +78 -0
  28. package/src/desired-state/records.js +75 -0
  29. package/src/desired-state/removal.js +32 -0
  30. package/src/desired-state/reset.js +27 -0
  31. package/src/desired-state/retirement.js +37 -0
  32. package/src/desired-state/settings.js +125 -0
  33. package/src/desired-state/source.js +41 -0
  34. package/src/operations/lock.js +3 -2
  35. package/src/source/desired-package.js +33 -0
  36. package/src/source/git.js +7 -3
package/README.md CHANGED
@@ -1,5 +1,23 @@
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
+
16
+ Everyday update: `workspace-pipeline update --workspace <directory>` shows the
17
+ changes and asks for confirmation. Use `--yes` for unattended application or
18
+ `--preview --json` to retain the advanced saved-plan workflow. See
19
+ [update modes](docs/lifecycle-cli.md#everyday-update-executable).
20
+
3
21
  Claude/Grok shared delivery: [one Claude configuration, plain Grok launch,
4
22
  global compatibility preview and recovery](docs/claude-grok.md).
5
23
 
@@ -72,7 +90,7 @@ Startup cleanup is disabled unless the user enables a local policy via
72
90
  that policy; public setup/update uses the same coordinator. Policy commands only
73
91
  write `.pipeline/retention.json`, never harness settings or project documents.
74
92
  Repository init/adopt previews acquire the explicitly selected Git pipeline source;
75
- 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
76
94
  credentials. Repository setup and provider activation are separate steps.
77
95
  See [repository commands](docs/repositories.md), [recovery](docs/repository-recovery.md)
78
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
@@ -14,11 +19,57 @@ CLI assembly supplies built-in adapters, never package code.
14
19
 
15
20
  ## Preview and apply contract
16
21
 
22
+ ### Everyday update (executable)
23
+
24
+ `workspace-pipeline update --workspace <absolute-wrapper>` prepares a plan,
25
+ shows every affected path and any user-wide Grok compatibility changes, and
26
+ asks `Apply these changes? [y/N]`. Accepted confirmations are `y`, `yes`, `д`,
27
+ or `да` (case-insensitive, surrounding whitespace ignored). Enter or any other
28
+ answer cancels. Cancellation exits 0 with `status: cancelled, applied: false`;
29
+ exit 0 alone does not mean an update was applied. The CLI
30
+ manages a private temporary preview internally and uses the same approval and
31
+ drift checks as saved-plan apply. No automatic reset or conflict bypass occurs.
32
+
33
+ `--yes` authorizes this update without a prompt (including displayed global
34
+ changes). Non-interactive input without `--yes` refuses before preparation.
35
+ `--preview` explicitly requests preparation only. `--preview --json` saves a
36
+ machine plan; for compatibility, `update --json` alone also remains preview-only.
37
+ `--yes --json` emits the final machine result on stdout and the change summary
38
+ on stderr; `--json` by itself never authorizes writes. The deterministic embedded
39
+ `runCli` API keeps its prior preview/apply behavior; this convenience is in the
40
+ executable entry layer. Other commands keep their existing explicit flow.
41
+
42
+ ```
43
+ workspace-pipeline update --workspace C:\Work\Game
44
+ workspace-pipeline update --workspace C:\Work\Game --yes
45
+ workspace-pipeline update --workspace C:\Work\Game --preview --json > preview.json
46
+ workspace-pipeline update --workspace C:\Work\Game --apply --preview C:\Private\preview.json
47
+ ```
48
+
49
+ Saved plans remain useful for LLM work, migrations and separate approval. Do not
50
+ share them publicly: configuration payloads can contain private values. A failed
51
+ apply can have partial effects: inspect doctor/recovery, do not blindly retry.
52
+
53
+ The internal preview is in the current user's OS temporary directory, inside a
54
+ unique `wpc-approved-update-*` directory. It may contain private configuration.
55
+ On POSIX the preview is created with mode `0600`; Windows/NTFS does not enforce
56
+ that POSIX mode, so confidentiality depends on the inherited Windows ACLs of
57
+ your temporary directory. The CLI does not change those ACLs. Use a private
58
+ user temp directory, not a shared writable temp location.
59
+
60
+ Normal completion (including handled apply failure) attempts to remove only
61
+ that preview and its empty directory. A killed process or failed cleanup can
62
+ leave the directory behind; there is no automatic janitor. After stopping the
63
+ owning update process and inspecting doctor/recovery, you may manually remove
64
+ its obsolete `wpc-approved-update-*` directory. Do not delete another active
65
+ run's directory or `.pipeline/transactions`/backups as part of this cleanup.
66
+ The temporary preview is not a replacement for durable recovery evidence.
67
+
17
68
  Once a trusted registry is provided by the CLI assembly:
18
69
 
19
70
  ```text
20
- workspace-pipeline setup --workspace <absolute-wrapper> [--manifest <absolute-file>] [--network]
21
- 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>]
22
73
  workspace-pipeline setup --workspace <absolute-wrapper> --apply --preview <absolute-json-file>
23
74
  workspace-pipeline update --workspace <absolute-wrapper> --apply --preview <absolute-json-file>
24
75
  ```
@@ -26,7 +77,7 @@ workspace-pipeline update --workspace <absolute-wrapper> --apply --preview <abso
26
77
  Preview displays a human-readable summary by default. Add `--json` to return
27
78
  the complete prepared JSON on stdout for saving and subsequent apply. It may acquire a Git
28
79
  source into temporary storage outside the wrapper, but does not write provider
29
- configuration. Network acquisition requires explicit `--network`. When manifest
80
+ configuration. Remote Git acquisition follows the selected source. When manifest
30
81
  is omitted, setup uses the standard workspace manifest; update uses the recorded
31
82
  manifest origin. Relocation does not silently rebind it. Use the explicit
32
83
  [manifest rebind workflow](rebind.md) to change that origin.
@@ -34,7 +85,7 @@ manifest origin. Relocation does not silently rebind it. Use the explicit
34
85
  Save prepared JSON privately, inspect its operations and then explicitly invoke
35
86
  `--apply --preview`. It may contain configuration secrets and absolute local
36
87
  paths; do not commit it or send it to shared logs. Apply does not accept
37
- `--manifest` or `--network`, acquire a fresh source, or substitute a new preview.
88
+ `--manifest`, acquire a fresh source, or substitute a new preview.
38
89
  Missing/stale staged data is an error, not permission to download replacements.
39
90
 
40
91
  Apply binds the verb and exact wrapper before lock acquisition, then invokes
@@ -101,7 +152,7 @@ Removal preserves other installed providers and shared files while needed; full
101
152
  removal restores taken-over content and removes only owned scope. Missing/corrupt
102
153
  snapshots, backups, unresolved history or conflicting user edits fail closed.
103
154
  Neither route reads the original manifest or downloads from Git. `--manifest`
104
- and `--network` are rejected. `--providers` and `--bundles` are remove-preview-only;
155
+ is rejected. `--providers` and `--bundles` are remove-preview-only;
105
156
  omitting both previews all installed providers and bundles. Use actual installed
106
157
  bundle IDs; selecting a bundle member via `--providers` fails with
107
158
  `remove.bundle-required`. The saved subject fixes the actual selection, so apply
@@ -122,7 +173,7 @@ Interrupted-operation continuation has a separate exact approval boundary below.
122
173
  With the same trusted built-in registry restriction:
123
174
 
124
175
  ```text
125
- workspace-pipeline switch --workspace <absolute-wrapper> --manifest <absolute-incoming-manifest> [--network]
176
+ workspace-pipeline switch --workspace <absolute-wrapper> --manifest <absolute-incoming-manifest>
126
177
  workspace-pipeline switch --workspace <absolute-wrapper> --apply --preview <absolute-json-file>
127
178
  ```
128
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.5.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
@@ -1,10 +1,26 @@
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 {tryDesiredLifecycle} from './commands/desired-lifecycle.js';
7
+ import {createInterface} from 'node:readline/promises';
6
8
  const write=stream=>text=>new Promise((resolve,reject)=>stream.write(text,error=>error?reject(error):resolve()));
7
9
  // Prevent an unhandled pipe error; write callbacks report delivery failures.
8
10
  process.stdout.on('error',()=>{});process.stderr.on('error',()=>{});
9
11
  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()});
12
+ const options={stdout:outputWriter(write(process.stdout),json),stderr:outputWriter(write(process.stderr),json),registry:providerRegistry,compatibility:createClaudeCompatibility()};
13
+ try{
14
+ const interaction={
15
+ isTTY:Boolean(process.stdin.isTTY&&process.stderr.isTTY),display:write(process.stderr),
16
+ confirm:async()=>{
17
+ const rl=createInterface({input:process.stdin,output:process.stderr});
18
+ try{return /^(y|yes|д|да)$/i.test((await rl.question('Apply these changes? [y/N] ')).trim());}
19
+ catch{return false;}finally{rl.close();}
20
+ }
21
+ };
22
+ const desired=await tryDesiredLifecycle(args,options,interaction);
23
+ process.exitCode=desired??await runInteractiveUpdate(args,options,interaction);
24
+ }catch{
25
+ await options.stderr(JSON.stringify({error:'cli.interactive-io',next:'Inspect doctor before retry; no automatic retry or reset.'})+'\n');process.exitCode=2;
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
+ }