deepcell-cli 0.6.1__tar.gz → 0.6.2__tar.gz
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.
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/PKG-INFO +6 -4
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/README.md +5 -3
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/pyproject.toml +1 -1
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/__init__.py +1 -1
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/capabilities.py +21 -5
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/capability-contract.json +18 -18
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/help_cmd.py +23 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/sync.py +6 -62
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/version.py +73 -2
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/main.py +25 -4
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/mcp_server.py +7 -5
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/stages.py +4 -1
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/surface.py +17 -6
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/upgrade_check.py +1 -1
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/PKG-INFO +6 -4
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_mcp_server.py +31 -2
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_stages.py +25 -6
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_upgrade_check.py +10 -10
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/setup.cfg +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/__main__.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/_findings.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/client.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/__init__.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_batch_input.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_datatypes.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_negative_args.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_swapped_args.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_version_display.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_write_opts.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/account.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/auth.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/changes.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/deck.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/defs.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/describe.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/doc.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/doctor.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/download.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/edit.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/example.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/export.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/export_docx.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/export_pdf.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/export_pptx.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/files.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/grep.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/guide.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/impact.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/import_cmd.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/ingest.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/merge.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/query.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/reasoning.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/ref.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/replace.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/rules.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/share.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/upgrade.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/variant.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/viewer.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/workspace.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/config.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/context.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/errors.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/logging_setup.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/oauth_provider.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/output.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/revision.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/sync_state.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/xml_replace.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/SOURCES.txt +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/dependency_links.txt +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/entry_points.txt +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/requires.txt +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/top_level.txt +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_anon.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_claim_strength.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_client.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_client_surface_header.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_config.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_conflict_reason.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_deck_help_contract.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_deck_op_shape.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_docs_catalog.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_doctor.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_download.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_findings_provenance.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_guide_eval_a4_a7.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_guide_flag_drift.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_help_flag_drift.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_impact_commands.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_ingest_command.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_logging_setup.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_mcp_auth.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_op_reachability.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_openapi_route_parity.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_output_export_notes.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_output_keeps_the_answer.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_reasoning_commands.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_reasoning_diff_command.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_ref_flag_drift.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_round6_signal_regressions.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_search_reach.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_shed_retry.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_surface_examples.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_surface_walker.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_version_parity.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_write_message_rule.py +0 -0
- {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_xml_replace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepcell-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Build and revise .deepcell documents from a terminal — the reasoning, the calculations, the document and the deck, with the links between them
|
|
5
5
|
Author-email: DeepCell <hello@deepcell.net>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -166,8 +166,8 @@ deepcell defs apply model.deepcell --ops ops.json # many ops, atomically
|
|
|
166
166
|
| Document surface | `doc` — outline, blocks, body, links, lint |
|
|
167
167
|
| Deck surface | `deck` — slides, ordering, binding, branding |
|
|
168
168
|
| Change review | `changes list / diff / revert`, `impact` |
|
|
169
|
-
| Version control | `log`, `diff`, `restore` |
|
|
170
|
-
| Sync | `clone`, `status`, `pull`, `push`, `
|
|
169
|
+
| Version control | `log`, `diff`, `restore`, `commit` |
|
|
170
|
+
| Sync (people, not agents) | `clone`, `status`, `pull`, `push`, `merge` |
|
|
171
171
|
| Import / export | `import`, `ingest`, `to-excel`, `to-docx`, `to-pptx`, `to-pdf` |
|
|
172
172
|
| Reasoning | `reasoning`, `reasoning-diff`, `claim`, `assumption` |
|
|
173
173
|
| Delivery | `viewer`, `share create / list / revoke` |
|
|
@@ -218,7 +218,9 @@ deepcell push -m "Updated assumptions" # upload
|
|
|
218
218
|
deepcell pull # fetch cloud changes
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell
|
|
221
|
+
A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell help clone`.
|
|
222
|
+
|
|
223
|
+
Sync moves *bytes*, not the commands that read them: `query`, `describe --lint`, `cat` and every other read address the project on the server, so inside a clone they answer from the cloud's HEAD and not from the file you just edited. `push` first, then read back. Sync is a human workflow for this reason and is unavailable over MCP.
|
|
222
224
|
|
|
223
225
|
## Import & export
|
|
224
226
|
|
|
@@ -134,8 +134,8 @@ deepcell defs apply model.deepcell --ops ops.json # many ops, atomically
|
|
|
134
134
|
| Document surface | `doc` — outline, blocks, body, links, lint |
|
|
135
135
|
| Deck surface | `deck` — slides, ordering, binding, branding |
|
|
136
136
|
| Change review | `changes list / diff / revert`, `impact` |
|
|
137
|
-
| Version control | `log`, `diff`, `restore` |
|
|
138
|
-
| Sync | `clone`, `status`, `pull`, `push`, `
|
|
137
|
+
| Version control | `log`, `diff`, `restore`, `commit` |
|
|
138
|
+
| Sync (people, not agents) | `clone`, `status`, `pull`, `push`, `merge` |
|
|
139
139
|
| Import / export | `import`, `ingest`, `to-excel`, `to-docx`, `to-pptx`, `to-pdf` |
|
|
140
140
|
| Reasoning | `reasoning`, `reasoning-diff`, `claim`, `assumption` |
|
|
141
141
|
| Delivery | `viewer`, `share create / list / revoke` |
|
|
@@ -186,7 +186,9 @@ deepcell push -m "Updated assumptions" # upload
|
|
|
186
186
|
deepcell pull # fetch cloud changes
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell
|
|
189
|
+
A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell help clone`.
|
|
190
|
+
|
|
191
|
+
Sync moves *bytes*, not the commands that read them: `query`, `describe --lint`, `cat` and every other read address the project on the server, so inside a clone they answer from the cloud's HEAD and not from the file you just edited. `push` first, then read back. Sync is a human workflow for this reason and is unavailable over MCP.
|
|
190
192
|
|
|
191
193
|
## Import & export
|
|
192
194
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deepcell-cli"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.2"
|
|
4
4
|
description = "Build and revise .deepcell documents from a terminal — the reasoning, the calculations, the document and the deck, with the links between them"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -33,6 +33,7 @@ MCP_ALLOWED_COMMANDS = frozenset(
|
|
|
33
33
|
"cell-meta",
|
|
34
34
|
"changes",
|
|
35
35
|
"claim",
|
|
36
|
+
"commit",
|
|
36
37
|
"deck",
|
|
37
38
|
"defs",
|
|
38
39
|
"describe",
|
|
@@ -61,13 +62,23 @@ MCP_ALLOWED_COMMANDS = frozenset(
|
|
|
61
62
|
"rm",
|
|
62
63
|
"rules",
|
|
63
64
|
"share",
|
|
64
|
-
"variant",
|
|
65
65
|
"viewer",
|
|
66
66
|
"whoami",
|
|
67
|
+
# `workspace` is the pre-rename spelling of `project` — a hidden alias
|
|
68
|
+
# of the same Group. Hidden is not unreachable: `_parse_command`
|
|
69
|
+
# dispatches through Click, which runs a hidden command like any other,
|
|
70
|
+
# so the alias needs a policy of its own or it is reachable with none
|
|
71
|
+
# declared. It carries `project`'s policy because it IS `project`.
|
|
72
|
+
"workspace",
|
|
67
73
|
"write",
|
|
68
74
|
}
|
|
69
75
|
)
|
|
70
76
|
|
|
77
|
+
#: Allowed spellings that are aliases of another allowed command. They need an
|
|
78
|
+
#: MCP policy (they run) but must NOT reach the tool catalog: documenting a
|
|
79
|
+
#: second spelling of `project` would put back the row the alias is hidden from.
|
|
80
|
+
MCP_ALIAS_COMMANDS = frozenset({"workspace"})
|
|
81
|
+
|
|
71
82
|
# Reason codes are stable data rendered into help, guide, MCP errors, and the
|
|
72
83
|
# for-agent page. The detail is for maintainers and structured clients; user
|
|
73
84
|
# prose remains translated at its own surface.
|
|
@@ -80,10 +91,6 @@ MCP_BLOCKED_COMMANDS: dict[str, dict[str, str]] = {
|
|
|
80
91
|
"reason": "local_checkout",
|
|
81
92
|
"detail": "creates and updates a local Git working copy",
|
|
82
93
|
},
|
|
83
|
-
"commit": {
|
|
84
|
-
"reason": "local_checkout",
|
|
85
|
-
"detail": "reads local sync state and commits a working copy",
|
|
86
|
-
},
|
|
87
94
|
"login": {
|
|
88
95
|
"reason": "interactive_auth",
|
|
89
96
|
"detail": "opens or polls an interactive browser authentication flow",
|
|
@@ -132,6 +139,15 @@ MCP_BLOCKED_COMMANDS: dict[str, dict[str, str]] = {
|
|
|
132
139
|
"reason": "shared_host_install",
|
|
133
140
|
"detail": "would inspect or replace the CLI installed on the MCP host",
|
|
134
141
|
},
|
|
142
|
+
"variant": {
|
|
143
|
+
"reason": "local_checkout",
|
|
144
|
+
"detail": (
|
|
145
|
+
"branches the whole file set, and only a working copy can use one: "
|
|
146
|
+
"`checkout` resets a local tree, and no write command takes a "
|
|
147
|
+
"`--variant`, so a caller with no clone cannot put anything on a "
|
|
148
|
+
"branch for `diff` or `merge` to act on"
|
|
149
|
+
),
|
|
150
|
+
},
|
|
135
151
|
"verify-email": {
|
|
136
152
|
"reason": "interactive_auth",
|
|
137
153
|
"detail": "prompts for a verification token sent out of band",
|
|
@@ -427,9 +427,9 @@
|
|
|
427
427
|
"reason": "not_dispatched"
|
|
428
428
|
},
|
|
429
429
|
"mcp": {
|
|
430
|
-
"available":
|
|
431
|
-
"detail":
|
|
432
|
-
"reason":
|
|
430
|
+
"available": true,
|
|
431
|
+
"detail": null,
|
|
432
|
+
"reason": null
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
},
|
|
@@ -5817,9 +5817,9 @@
|
|
|
5817
5817
|
"reason": "not_dispatched"
|
|
5818
5818
|
},
|
|
5819
5819
|
"mcp": {
|
|
5820
|
-
"available":
|
|
5821
|
-
"detail":
|
|
5822
|
-
"reason":
|
|
5820
|
+
"available": false,
|
|
5821
|
+
"detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
|
|
5822
|
+
"reason": "local_checkout"
|
|
5823
5823
|
}
|
|
5824
5824
|
}
|
|
5825
5825
|
},
|
|
@@ -5848,9 +5848,9 @@
|
|
|
5848
5848
|
"reason": "not_dispatched"
|
|
5849
5849
|
},
|
|
5850
5850
|
"mcp": {
|
|
5851
|
-
"available":
|
|
5852
|
-
"detail":
|
|
5853
|
-
"reason":
|
|
5851
|
+
"available": false,
|
|
5852
|
+
"detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
|
|
5853
|
+
"reason": "local_checkout"
|
|
5854
5854
|
}
|
|
5855
5855
|
}
|
|
5856
5856
|
},
|
|
@@ -5879,9 +5879,9 @@
|
|
|
5879
5879
|
"reason": "not_dispatched"
|
|
5880
5880
|
},
|
|
5881
5881
|
"mcp": {
|
|
5882
|
-
"available":
|
|
5883
|
-
"detail":
|
|
5884
|
-
"reason":
|
|
5882
|
+
"available": false,
|
|
5883
|
+
"detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
|
|
5884
|
+
"reason": "local_checkout"
|
|
5885
5885
|
}
|
|
5886
5886
|
}
|
|
5887
5887
|
},
|
|
@@ -5910,9 +5910,9 @@
|
|
|
5910
5910
|
"reason": "not_dispatched"
|
|
5911
5911
|
},
|
|
5912
5912
|
"mcp": {
|
|
5913
|
-
"available":
|
|
5914
|
-
"detail":
|
|
5915
|
-
"reason":
|
|
5913
|
+
"available": false,
|
|
5914
|
+
"detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
|
|
5915
|
+
"reason": "local_checkout"
|
|
5916
5916
|
}
|
|
5917
5917
|
}
|
|
5918
5918
|
},
|
|
@@ -5941,9 +5941,9 @@
|
|
|
5941
5941
|
"reason": "not_dispatched"
|
|
5942
5942
|
},
|
|
5943
5943
|
"mcp": {
|
|
5944
|
-
"available":
|
|
5945
|
-
"detail":
|
|
5946
|
-
"reason":
|
|
5944
|
+
"available": false,
|
|
5945
|
+
"detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
|
|
5946
|
+
"reason": "local_checkout"
|
|
5947
5947
|
}
|
|
5948
5948
|
}
|
|
5949
5949
|
},
|
|
@@ -257,6 +257,19 @@ def help_cmd(
|
|
|
257
257
|
if command:
|
|
258
258
|
path = " ".join(command)
|
|
259
259
|
entry = commands.get(path)
|
|
260
|
+
if entry is None:
|
|
261
|
+
# A HIDDEN command is absent from the listing on purpose but still
|
|
262
|
+
# runs, so asking for it by name must answer. `clone` is the case:
|
|
263
|
+
# `cli/README.md` points a person at `deepcell help clone`, and the
|
|
264
|
+
# listing it is hidden from is not the same question as the flags it
|
|
265
|
+
# takes. Only reached on an explicit name — the index, the stage
|
|
266
|
+
# filters and every generated surface still see the visible set.
|
|
267
|
+
hidden_entry = build_commands(include_hidden=True).get(path)
|
|
268
|
+
if hidden_entry is not None:
|
|
269
|
+
entry = {
|
|
270
|
+
**hidden_entry,
|
|
271
|
+
"capability": get_command_capability(path),
|
|
272
|
+
}
|
|
260
273
|
if entry is None:
|
|
261
274
|
# `build_commands()` keys leaf commands only, so a group path misses
|
|
262
275
|
# the table. Asking what is under `defs` is a well-formed question
|
|
@@ -267,6 +280,16 @@ def help_cmd(
|
|
|
267
280
|
children = sorted(
|
|
268
281
|
name for name in commands if name.startswith(f"{path} ")
|
|
269
282
|
)
|
|
283
|
+
if not children:
|
|
284
|
+
# A hidden GROUP is keyed nowhere — `build_commands` keys leaves
|
|
285
|
+
# — so `deepcell help variant` has to find its children the same
|
|
286
|
+
# way, in the hidden-inclusive tree.
|
|
287
|
+
all_commands = build_commands(include_hidden=True)
|
|
288
|
+
children = sorted(
|
|
289
|
+
name for name in all_commands if name.startswith(f"{path} ")
|
|
290
|
+
)
|
|
291
|
+
if children:
|
|
292
|
+
commands = {**all_commands, **commands}
|
|
270
293
|
if children:
|
|
271
294
|
if ctx.fmt == "json":
|
|
272
295
|
output({"group": path, "commands": children}, ctx.fmt)
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
"""Sync commands: clone, pull, push, status
|
|
1
|
+
"""Sync commands: clone, pull, push, status.
|
|
2
2
|
|
|
3
|
-
Git-like sync between a local folder and a cloud DeepCell workspace.
|
|
3
|
+
Git-like sync between a local folder and a cloud DeepCell workspace. Every
|
|
4
|
+
command here genuinely reads or writes a local working copy — that is the
|
|
5
|
+
membership rule, and `commit` failed it: it posts to
|
|
6
|
+
`/workspaces/{slug}/versions/commit` and touches no local file, so it lives in
|
|
7
|
+
`version.py` with the rest of the server-side history commands.
|
|
4
8
|
"""
|
|
5
9
|
|
|
6
10
|
from __future__ import annotations
|
|
@@ -742,63 +746,3 @@ def push(ctx: Ctx, message: str) -> None:
|
|
|
742
746
|
"push again"
|
|
743
747
|
)
|
|
744
748
|
raise click.exceptions.Exit(1)
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
# -- commit (server-side pending) ---------------------------------------------
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
@click.command()
|
|
751
|
-
@click.option("-m", "--message", default=None, help="Commit message.")
|
|
752
|
-
@pass_ctx
|
|
753
|
-
def commit(ctx: Ctx, message: str | None) -> None:
|
|
754
|
-
"""Commit changes staged on the server by something other than you.
|
|
755
|
-
|
|
756
|
-
You almost certainly do not need this. `defs`, `edit`, `write` and
|
|
757
|
-
`reasoning` each commit their own change with their own `-m` — there is no
|
|
758
|
-
staging area they queue into. This commits what a *different* actor left
|
|
759
|
-
pending on the server, such as files created by an agent session.
|
|
760
|
-
|
|
761
|
-
Nothing staged is a no-op, not an error.
|
|
762
|
-
"""
|
|
763
|
-
root, state = _require_sync_root(ctx)
|
|
764
|
-
slug = state.workspace_slug
|
|
765
|
-
|
|
766
|
-
# `-m` is no longer `required=True`, so this reaches the server even when
|
|
767
|
-
# the caller gave no message. The server checks "is anything staged"
|
|
768
|
-
# first and answers the no-op; a commit that will actually write still
|
|
769
|
-
# 422s without a message, and that is re-raised below as a usage error.
|
|
770
|
-
#
|
|
771
|
-
# It used to be rejected by Click during parsing, so a worker whose defs
|
|
772
|
-
# ops had each already committed and who reasonably tried to finalize got
|
|
773
|
-
# "Missing option '-m' / '--message'" — an argument-syntax error for a
|
|
774
|
-
# command with nothing to do. That is what the 2026-08-02 CLI eval hit.
|
|
775
|
-
data = ctx.client.post(
|
|
776
|
-
f"/workspaces/{slug}/versions/commit",
|
|
777
|
-
json={"message": message or ""},
|
|
778
|
-
)
|
|
779
|
-
|
|
780
|
-
# A commit with nothing staged is a benign no-op (the endpoint answers
|
|
781
|
-
# 200 with committed=false, sha=null) — report it instead of slicing a
|
|
782
|
-
# null sha.
|
|
783
|
-
if isinstance(data, dict) and data.get("committed") is False:
|
|
784
|
-
echo_info(
|
|
785
|
-
"Nothing to commit — no pending changes. `defs`, `edit`, `write` "
|
|
786
|
-
"and `reasoning` each commit their own change, so there is "
|
|
787
|
-
"normally nothing here to finalize."
|
|
788
|
-
)
|
|
789
|
-
return
|
|
790
|
-
|
|
791
|
-
sha = (data.get("sha") or "")[:8] if isinstance(data, dict) else ""
|
|
792
|
-
# Staged content is committed either way (the server persists invalid
|
|
793
|
-
# .deepcell on purpose), so report the errors rather than letting a clean
|
|
794
|
-
# "Committed" line stand in for them — parity with write/push.
|
|
795
|
-
invalid = echo_validation_map(
|
|
796
|
-
data.get("validation") if isinstance(data, dict) else None
|
|
797
|
-
)
|
|
798
|
-
if invalid:
|
|
799
|
-
echo_warning(
|
|
800
|
-
f"Committed ({sha}), but {len(invalid)} file(s) have validation "
|
|
801
|
-
f"error(s): {', '.join(invalid)} — fix the errors above"
|
|
802
|
-
)
|
|
803
|
-
raise click.exceptions.Exit(1)
|
|
804
|
-
echo_success(f"Committed ({sha}): {message}")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Version control commands: log, diff, restore."""
|
|
1
|
+
"""Version control commands: log, diff, restore, commit."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,7 +7,8 @@ import click
|
|
|
7
7
|
from deepcell_cli.errors import APIError
|
|
8
8
|
from deepcell_cli.context import Ctx, pass_ctx
|
|
9
9
|
from deepcell_cli.output import (
|
|
10
|
-
echo_error, echo_info, echo_success,
|
|
10
|
+
echo_error, echo_info, echo_success, echo_validation_map, echo_warning,
|
|
11
|
+
output, output_mutation, print_plain,
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
|
|
@@ -443,3 +444,73 @@ def restore(ctx: Ctx, revision: str, filename: str | None, yes: bool) -> None:
|
|
|
443
444
|
"Recover them from a newer revision (`deepcell log`, then "
|
|
444
445
|
"`deepcell download <file> --revision <sha>`)."
|
|
445
446
|
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# -- commit (finalize what another actor staged server-side) -------------------
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@click.command()
|
|
453
|
+
@click.option("-m", "--message", default=None, help="Commit message.")
|
|
454
|
+
@pass_ctx
|
|
455
|
+
def commit(ctx: Ctx, message: str | None) -> None:
|
|
456
|
+
"""Commit changes staged on the server by something other than you.
|
|
457
|
+
|
|
458
|
+
You almost certainly do not need this. `defs`, `edit`, `write` and
|
|
459
|
+
`reasoning` each commit their own change with their own `-m` — there is no
|
|
460
|
+
staging area they queue into. This commits what a *different* actor left
|
|
461
|
+
pending on the server, such as files created by an agent session.
|
|
462
|
+
|
|
463
|
+
Nothing staged is a no-op, not an error.
|
|
464
|
+
"""
|
|
465
|
+
# Addressed by workspace, like everything else in this module. It used to
|
|
466
|
+
# live in `sync.py` and call `_require_sync_root`, which reads
|
|
467
|
+
# `.deepcell/sync.json` — for nothing but the slug, because the work is
|
|
468
|
+
# entirely server-side: `POST /workspaces/{slug}/versions/commit` stages
|
|
469
|
+
# nothing locally and reads no local file.
|
|
470
|
+
#
|
|
471
|
+
# That invented clone requirement cost the command the callers it exists
|
|
472
|
+
# for. It raised "Not inside a synced workspace" for anyone standing
|
|
473
|
+
# anywhere else, and it was blocked over MCP as `local_checkout` — a reason
|
|
474
|
+
# untrue of it. An agent, which is precisely who leaves changes staged,
|
|
475
|
+
# could not finalize them from any transport.
|
|
476
|
+
slug = ctx.require_workspace()
|
|
477
|
+
|
|
478
|
+
# `-m` is no longer `required=True`, so this reaches the server even when
|
|
479
|
+
# the caller gave no message. The server checks "is anything staged"
|
|
480
|
+
# first and answers the no-op; a commit that will actually write still
|
|
481
|
+
# 422s without a message, and that is re-raised below as a usage error.
|
|
482
|
+
#
|
|
483
|
+
# It used to be rejected by Click during parsing, so a worker whose defs
|
|
484
|
+
# ops had each already committed and who reasonably tried to finalize got
|
|
485
|
+
# "Missing option '-m' / '--message'" — an argument-syntax error for a
|
|
486
|
+
# command with nothing to do. That is what the 2026-08-02 CLI eval hit.
|
|
487
|
+
data = ctx.client.post(
|
|
488
|
+
f"/workspaces/{slug}/versions/commit",
|
|
489
|
+
json={"message": message or ""},
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# A commit with nothing staged is a benign no-op (the endpoint answers
|
|
493
|
+
# 200 with committed=false, sha=null) — report it instead of slicing a
|
|
494
|
+
# null sha.
|
|
495
|
+
if isinstance(data, dict) and data.get("committed") is False:
|
|
496
|
+
echo_info(
|
|
497
|
+
"Nothing to commit — no pending changes. `defs`, `edit`, `write` "
|
|
498
|
+
"and `reasoning` each commit their own change, so there is "
|
|
499
|
+
"normally nothing here to finalize."
|
|
500
|
+
)
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
sha = (data.get("sha") or "")[:8] if isinstance(data, dict) else ""
|
|
504
|
+
# Staged content is committed either way (the server persists invalid
|
|
505
|
+
# .deepcell on purpose), so report the errors rather than letting a clean
|
|
506
|
+
# "Committed" line stand in for them — parity with write/push.
|
|
507
|
+
invalid = echo_validation_map(
|
|
508
|
+
data.get("validation") if isinstance(data, dict) else None
|
|
509
|
+
)
|
|
510
|
+
if invalid:
|
|
511
|
+
echo_warning(
|
|
512
|
+
f"Committed ({sha}), but {len(invalid)} file(s) have validation "
|
|
513
|
+
f"error(s): {', '.join(invalid)} — fix the errors above"
|
|
514
|
+
)
|
|
515
|
+
raise click.exceptions.Exit(1)
|
|
516
|
+
echo_success(f"Committed ({sha}): {message}")
|
|
@@ -442,11 +442,11 @@ from deepcell_cli.commands.impact import impact # noqa: E402
|
|
|
442
442
|
from deepcell_cli.commands.export_docx import to_docx # noqa: E402
|
|
443
443
|
from deepcell_cli.commands.export_pdf import to_pdf # noqa: E402
|
|
444
444
|
from deepcell_cli.commands.export_pptx import to_pptx # noqa: E402
|
|
445
|
-
from deepcell_cli.commands.version import log, diff, restore # noqa: E402
|
|
445
|
+
from deepcell_cli.commands.version import log, diff, restore, commit # noqa: E402
|
|
446
446
|
from deepcell_cli.commands.changes import changes # noqa: E402
|
|
447
447
|
from deepcell_cli.commands.variant import variant # noqa: E402
|
|
448
448
|
from deepcell_cli.commands.grep import grep # noqa: E402
|
|
449
|
-
from deepcell_cli.commands.sync import clone, pull, push, sync_status
|
|
449
|
+
from deepcell_cli.commands.sync import clone, pull, push, sync_status # noqa: E402
|
|
450
450
|
from deepcell_cli.commands.merge import merge # noqa: E402
|
|
451
451
|
from deepcell_cli.commands.import_cmd import import_cmd # noqa: E402
|
|
452
452
|
from deepcell_cli.commands.ingest import ingest # noqa: E402
|
|
@@ -499,13 +499,34 @@ cli.add_command(log)
|
|
|
499
499
|
cli.add_command(diff)
|
|
500
500
|
cli.add_command(restore)
|
|
501
501
|
cli.add_command(changes)
|
|
502
|
-
cli.add_command(variant)
|
|
503
502
|
cli.add_command(grep)
|
|
503
|
+
cli.add_command(commit)
|
|
504
|
+
|
|
505
|
+
# Local sync and variants: registered, working, and HIDDEN. Both are a person's
|
|
506
|
+
# workflow at a terminal with a clone, and neither is discoverable any more —
|
|
507
|
+
# `surface.py`, `main`'s own help and `docs/cli-commands.md`'s coverage test all
|
|
508
|
+
# skip hidden commands, so they produce no help row and no entry in any
|
|
509
|
+
# generated surface. Muscle memory and existing scripts keep working, and
|
|
510
|
+
# `deepcell help <name>` still answers for each.
|
|
511
|
+
#
|
|
512
|
+
# Sync moves BYTES while every read addresses the project on the server, so a
|
|
513
|
+
# clone is a working copy the read path cannot see. Variants branch the whole
|
|
514
|
+
# file set, and no write command takes a variant, so a branch is only reachable
|
|
515
|
+
# by checking it out onto disk. Both are therefore clone-shaped all the way
|
|
516
|
+
# down, which is precisely what the agent surfaces do not have.
|
|
517
|
+
#
|
|
518
|
+
# The cost is deliberate and was weighed: the `variants` and `merge` domains
|
|
519
|
+
# leave `docs/backend-cli-semantic-contracts.yaml`, because a hidden command is
|
|
520
|
+
# absent from the capability contract the domain resolves through. Those two
|
|
521
|
+
# backend domains now have no taught CLI command.
|
|
522
|
+
for _person_only in (variant, clone, pull, push, sync_status, merge):
|
|
523
|
+
_person_only.hidden = True
|
|
524
|
+
|
|
525
|
+
cli.add_command(variant)
|
|
504
526
|
cli.add_command(clone)
|
|
505
527
|
cli.add_command(pull)
|
|
506
528
|
cli.add_command(push)
|
|
507
529
|
cli.add_command(sync_status, "status")
|
|
508
|
-
cli.add_command(commit)
|
|
509
530
|
cli.add_command(merge)
|
|
510
531
|
cli.add_command(import_cmd, "import")
|
|
511
532
|
cli.add_command(ingest)
|
|
@@ -24,6 +24,7 @@ from click.testing import CliRunner
|
|
|
24
24
|
from mcp.server.fastmcp import FastMCP
|
|
25
25
|
|
|
26
26
|
from deepcell_cli.capabilities import (
|
|
27
|
+
MCP_ALIAS_COMMANDS,
|
|
27
28
|
MCP_ALLOWED_COMMANDS,
|
|
28
29
|
MCP_BLOCKED_COMMANDS as MCP_BLOCK_POLICY,
|
|
29
30
|
blocked_mcp_commands,
|
|
@@ -48,7 +49,7 @@ AUTO_CONFIRM: dict[str, list[str]] = {
|
|
|
48
49
|
# Top-level commands documented in TOOL_DESCRIPTION. Must stay equal to the
|
|
49
50
|
# registered Click commands minus BLOCKED_COMMANDS — tests/test_mcp_server.py
|
|
50
51
|
# cross-checks both directions so the catalog can't silently drift.
|
|
51
|
-
DOCUMENTED_COMMANDS = frozenset(MCP_ALLOWED_COMMANDS)
|
|
52
|
+
DOCUMENTED_COMMANDS = frozenset(MCP_ALLOWED_COMMANDS) - MCP_ALIAS_COMMANDS
|
|
52
53
|
|
|
53
54
|
TOOL_DESCRIPTION = """\
|
|
54
55
|
Run a DeepCell CLI command. Structured results are JSON; `cat` prints raw XML
|
|
@@ -136,9 +137,6 @@ Available commands:
|
|
|
136
137
|
replace <file> <old> <new>
|
|
137
138
|
String-replace in XML (last resort — prefer defs;
|
|
138
139
|
`edit --replace` is a deprecated alias)
|
|
139
|
-
variant list List variants
|
|
140
|
-
variant create <name> Create a variant
|
|
141
|
-
(Note: variant checkout/diff/merge need a LOCAL clone — blocked here)
|
|
142
140
|
rm <file> Delete a file (auto-confirmed)
|
|
143
141
|
log [--file F] [-n N] Show version history
|
|
144
142
|
diff <rev_a> [rev_b] Diff between revisions
|
|
@@ -147,6 +145,9 @@ Available commands:
|
|
|
147
145
|
changes diff <base> [head] What one change did, semantically
|
|
148
146
|
changes revert <base> <head>
|
|
149
147
|
Undo one change as a new commit (auto-confirmed)
|
|
148
|
+
commit [-m MSG] Finalize changes another actor left staged
|
|
149
|
+
(Rare: every writer commits its own change.
|
|
150
|
+
Nothing staged is a no-op, not an error.)
|
|
150
151
|
|
|
151
152
|
VERIFY — change one thing, see everything it affects
|
|
152
153
|
relationships <file> Show relationship graph
|
|
@@ -182,7 +183,8 @@ Available commands:
|
|
|
182
183
|
|
|
183
184
|
SYNC — local <-> cloud
|
|
184
185
|
download <file> -o <path> Download file to local path
|
|
185
|
-
(
|
|
186
|
+
(Sync proper is blocked here — it needs a clone.
|
|
187
|
+
Note `commit` is NOT sync: see REVISE.)
|
|
186
188
|
|
|
187
189
|
LEARN — the five reference surfaces
|
|
188
190
|
help [command] Every command, flag, exit code and example.
|
|
@@ -99,6 +99,10 @@ COMMAND_TIER: dict[str, str] = {
|
|
|
99
99
|
"log": "revise",
|
|
100
100
|
"diff": "revise",
|
|
101
101
|
"restore": "revise",
|
|
102
|
+
# Filed under SYNC until it was read closely: it posts to
|
|
103
|
+
# `/workspaces/{slug}/versions/commit` and touches no local working copy,
|
|
104
|
+
# so it belongs with the other history commands, not with `clone`/`push`.
|
|
105
|
+
"commit": "revise",
|
|
102
106
|
"rm": "revise",
|
|
103
107
|
# VERIFY — the connectedness story: change one thing, see what it affects.
|
|
104
108
|
"relationships": "verify",
|
|
@@ -132,7 +136,6 @@ COMMAND_TIER: dict[str, str] = {
|
|
|
132
136
|
"status": "sync",
|
|
133
137
|
"pull": "sync",
|
|
134
138
|
"push": "sync",
|
|
135
|
-
"commit": "sync",
|
|
136
139
|
"merge": "sync",
|
|
137
140
|
"download": "sync",
|
|
138
141
|
# LEARN — the five reference surfaces.
|
|
@@ -502,7 +502,12 @@ def _describe_command(path: str, command: click.Command) -> dict[str, Any]:
|
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
|
|
505
|
-
def _walk(
|
|
505
|
+
def _walk(
|
|
506
|
+
command: click.Command,
|
|
507
|
+
path: str,
|
|
508
|
+
out: dict[str, dict[str, Any]],
|
|
509
|
+
include_hidden: bool = False,
|
|
510
|
+
) -> None:
|
|
506
511
|
if isinstance(command, click.Group):
|
|
507
512
|
# An `invoke_without_command=True` group is a command *as well as* a
|
|
508
513
|
# group: `deepcell example --pack finance` runs, and the `--pack` is
|
|
@@ -515,13 +520,13 @@ def _walk(command: click.Command, path: str, out: dict[str, dict[str, Any]]) ->
|
|
|
515
520
|
ctx = click.Context(command)
|
|
516
521
|
for name in command.list_commands(ctx):
|
|
517
522
|
sub = command.get_command(ctx, name)
|
|
518
|
-
if sub is not None and not sub.hidden:
|
|
519
|
-
_walk(sub, f"{path} {name}".strip(), out)
|
|
523
|
+
if sub is not None and (include_hidden or not sub.hidden):
|
|
524
|
+
_walk(sub, f"{path} {name}".strip(), out, include_hidden)
|
|
520
525
|
return
|
|
521
526
|
out[path] = _describe_command(path, command)
|
|
522
527
|
|
|
523
528
|
|
|
524
|
-
def build_commands() -> dict[str, dict[str, Any]]:
|
|
529
|
+
def build_commands(include_hidden: bool = False) -> dict[str, dict[str, Any]]:
|
|
525
530
|
"""Every invocable command, keyed by its full invocation path.
|
|
526
531
|
|
|
527
532
|
Groups are walked rather than listed: ``defs`` is not a command, ``defs
|
|
@@ -534,6 +539,12 @@ def build_commands() -> dict[str, dict[str, Any]]:
|
|
|
534
539
|
its own. ``test_surface_group_options`` fails on that shape rather than
|
|
535
540
|
letting the option vanish the way ``example --pack`` did — the fix is to
|
|
536
541
|
move the option onto the leaves, or make the group invocable.
|
|
542
|
+
|
|
543
|
+
``include_hidden`` is for lookup by name only, never for codegen: a hidden
|
|
544
|
+
command is deliberately absent from every generated surface, but it still
|
|
545
|
+
runs, so ``deepcell help <name>`` must still answer for it. Hidden from the
|
|
546
|
+
index, answerable when asked for — the alternative is a command a person
|
|
547
|
+
can invoke and cannot read the flags of.
|
|
537
548
|
"""
|
|
538
549
|
from deepcell_cli.main import cli
|
|
539
550
|
|
|
@@ -541,8 +552,8 @@ def build_commands() -> dict[str, dict[str, Any]]:
|
|
|
541
552
|
ctx = click.Context(cli)
|
|
542
553
|
for name in cli.list_commands(ctx):
|
|
543
554
|
sub = cli.get_command(ctx, name)
|
|
544
|
-
if sub is not None and not sub.hidden:
|
|
545
|
-
_walk(sub, name, out)
|
|
555
|
+
if sub is not None and (include_hidden or not sub.hidden):
|
|
556
|
+
_walk(sub, name, out, include_hidden)
|
|
546
557
|
return dict(sorted(out.items()))
|
|
547
558
|
|
|
548
559
|
|
|
@@ -95,7 +95,7 @@ class Index(NamedTuple):
|
|
|
95
95
|
# first-answer-wins would then quietly stop noticing releases whenever the
|
|
96
96
|
# earlier index lagged the later one. Nothing would fail; the check would just
|
|
97
97
|
# say nothing forever, which is the failure mode this whole module is built to
|
|
98
|
-
# avoid.
|
|
98
|
+
# avoid.
|
|
99
99
|
DEFAULT_INDEXES = (
|
|
100
100
|
Index(
|
|
101
101
|
f"https://pypi.org/pypi/{PACKAGE_NAME}/json",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepcell-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Build and revise .deepcell documents from a terminal — the reasoning, the calculations, the document and the deck, with the links between them
|
|
5
5
|
Author-email: DeepCell <hello@deepcell.net>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -166,8 +166,8 @@ deepcell defs apply model.deepcell --ops ops.json # many ops, atomically
|
|
|
166
166
|
| Document surface | `doc` — outline, blocks, body, links, lint |
|
|
167
167
|
| Deck surface | `deck` — slides, ordering, binding, branding |
|
|
168
168
|
| Change review | `changes list / diff / revert`, `impact` |
|
|
169
|
-
| Version control | `log`, `diff`, `restore` |
|
|
170
|
-
| Sync | `clone`, `status`, `pull`, `push`, `
|
|
169
|
+
| Version control | `log`, `diff`, `restore`, `commit` |
|
|
170
|
+
| Sync (people, not agents) | `clone`, `status`, `pull`, `push`, `merge` |
|
|
171
171
|
| Import / export | `import`, `ingest`, `to-excel`, `to-docx`, `to-pptx`, `to-pdf` |
|
|
172
172
|
| Reasoning | `reasoning`, `reasoning-diff`, `claim`, `assumption` |
|
|
173
173
|
| Delivery | `viewer`, `share create / list / revoke` |
|
|
@@ -218,7 +218,9 @@ deepcell push -m "Updated assumptions" # upload
|
|
|
218
218
|
deepcell pull # fetch cloud changes
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell
|
|
221
|
+
A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell help clone`.
|
|
222
|
+
|
|
223
|
+
Sync moves *bytes*, not the commands that read them: `query`, `describe --lint`, `cat` and every other read address the project on the server, so inside a clone they answer from the cloud's HEAD and not from the file you just edited. `push` first, then read back. Sync is a human workflow for this reason and is unavailable over MCP.
|
|
222
224
|
|
|
223
225
|
## Import & export
|
|
224
226
|
|
|
@@ -63,11 +63,40 @@ class TestParseCommand:
|
|
|
63
63
|
with pytest.raises(ValueError, match="verify-email.*blocked"):
|
|
64
64
|
_parse_command("verify-email")
|
|
65
65
|
|
|
66
|
-
@pytest.mark.parametrize("cmd", ["clone", "status", "pull", "push", "
|
|
66
|
+
@pytest.mark.parametrize("cmd", ["clone", "status", "pull", "push", "merge"])
|
|
67
67
|
def test_blocked_sync_commands(self, cmd):
|
|
68
68
|
with pytest.raises(ValueError, match=f"{cmd}.*blocked"):
|
|
69
69
|
_parse_command(cmd)
|
|
70
70
|
|
|
71
|
+
def test_variant_is_blocked(self):
|
|
72
|
+
"""A branch you cannot write to is not a capability, it is a trap.
|
|
73
|
+
|
|
74
|
+
`variant checkout` resets a local working tree the MCP caller does not
|
|
75
|
+
have, and `variant diff` / `variant merge` resolve their workspace out
|
|
76
|
+
of `.deepcell/sync.json` — so the family refused itself here anyway,
|
|
77
|
+
with "Not inside a synced workspace" instead of a capability answer.
|
|
78
|
+
The deeper reason it stays blocked rather than being made to work: no
|
|
79
|
+
write command takes a variant, so an agent cannot put anything ON a
|
|
80
|
+
branch it creates, leaving `diff` and `merge` nothing to act on.
|
|
81
|
+
Scenarios are the in-place answer (`guide revise/scenarios`).
|
|
82
|
+
"""
|
|
83
|
+
with pytest.raises(ValueError, match="variant.*blocked"):
|
|
84
|
+
_parse_command("variant create bear-case")
|
|
85
|
+
with pytest.raises(ValueError, match="variant.*blocked"):
|
|
86
|
+
_parse_command("variant list")
|
|
87
|
+
assert "variant" not in DOCUMENTED_COMMANDS
|
|
88
|
+
|
|
89
|
+
def test_commit_is_not_a_sync_command(self):
|
|
90
|
+
"""`commit` sat in this list for its module, not its behaviour.
|
|
91
|
+
|
|
92
|
+
It posts to `/workspaces/{slug}/versions/commit` and reads no local
|
|
93
|
+
file, so `local_checkout` was never true of it. Blocking it kept the
|
|
94
|
+
one caller who needs it — an agent finalizing what it left staged —
|
|
95
|
+
from reaching it on any transport.
|
|
96
|
+
"""
|
|
97
|
+
assert "commit" not in BLOCKED_COMMANDS
|
|
98
|
+
assert "commit" in _parse_command("commit -m 'finalize'")
|
|
99
|
+
|
|
71
100
|
def test_auto_confirm_rm(self):
|
|
72
101
|
result = _parse_command("rm model.deepcell")
|
|
73
102
|
assert "-y" in result
|
|
@@ -275,7 +304,7 @@ class TestDeepcellToolOutput:
|
|
|
275
304
|
# Blocked commands are refused with a clear, non-silent error (not run).
|
|
276
305
|
for cmd in (
|
|
277
306
|
"login", "logout", "register", "verify-email", "to-excel",
|
|
278
|
-
"clone", "status", "pull", "push", "
|
|
307
|
+
"clone", "status", "pull", "push", "merge",
|
|
279
308
|
):
|
|
280
309
|
out = deepcell(cmd)
|
|
281
310
|
# The failure MARKER must lead: `guide exit-codes` tells agents to
|
|
@@ -113,12 +113,26 @@ def _lead_commands(bullet: str) -> list[str]:
|
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
def _top_level() -> list[str]:
|
|
116
|
+
"""Every registered top-level command, HIDDEN ONES INCLUDED.
|
|
117
|
+
|
|
118
|
+
Both questions below are about existence, not about the help index.
|
|
119
|
+
`clone`, `pull`, `push`, `status`, `merge` and `variant` are `hidden=True`
|
|
120
|
+
— retired from discovery, still registered and still runnable — so reading
|
|
121
|
+
them as "no longer exist" would delete their stage entries and, the next
|
|
122
|
+
time one is un-hidden, land it in a bucket `assert_fully_staged` exists to
|
|
123
|
+
forbid. A hidden command keeps its stage for the same reason it keeps its
|
|
124
|
+
MCP policy: it still runs.
|
|
125
|
+
|
|
126
|
+
An ALIAS is still excluded, and that is a different case: `workspace` is
|
|
127
|
+
the pre-rename spelling of `project`, the same Group under a second name.
|
|
128
|
+
It has no stage and no catalog row of its own because it is not a second
|
|
129
|
+
command — staging it would put a duplicate of every `project` row back into
|
|
130
|
+
the help index the alias exists to stay out of.
|
|
131
|
+
"""
|
|
132
|
+
from deepcell_cli.capabilities import MCP_ALIAS_COMMANDS
|
|
133
|
+
|
|
116
134
|
ctx = click.Context(cli)
|
|
117
|
-
return [
|
|
118
|
-
name
|
|
119
|
-
for name in cli.list_commands(ctx)
|
|
120
|
-
if not (cli.get_command(ctx, name) or click.Command(name)).hidden
|
|
121
|
-
]
|
|
135
|
+
return [n for n in cli.list_commands(ctx) if n not in MCP_ALIAS_COMMANDS]
|
|
122
136
|
|
|
123
137
|
|
|
124
138
|
def test_every_command_has_a_stage():
|
|
@@ -360,7 +374,12 @@ def test_the_docs_lead_list_reader_actually_reads_commands():
|
|
|
360
374
|
for b in bullets
|
|
361
375
|
for n in _lead_commands(b)
|
|
362
376
|
}
|
|
363
|
-
|
|
377
|
+
# One sentinel per section, so a reader that went blind in any single one
|
|
378
|
+
# is caught. `download` heads SYNC's only bullet since `clone` and the rest
|
|
379
|
+
# of the sync family became hidden commands, which are named in the
|
|
380
|
+
# preamble's prose rather than in a stage-headed list — and prose is
|
|
381
|
+
# exactly what this reader is built NOT to read.
|
|
382
|
+
assert {"query", "defs", "deck", "changes", "download"} <= found, sorted(found)
|
|
364
383
|
|
|
365
384
|
|
|
366
385
|
def test_the_lead_list_reader_stops_where_the_grouping_claim_stops():
|
|
@@ -25,12 +25,12 @@ from deepcell_cli.main import cli
|
|
|
25
25
|
|
|
26
26
|
PYPI = upgrade_check.DEFAULT_INDEXES[0].json_url
|
|
27
27
|
|
|
28
|
-
# Two stand-ins for the multi-index tests below. `DEFAULT_INDEXES` names
|
|
29
|
-
# index
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
28
|
+
# Two stand-ins for the multi-index tests below. `DEFAULT_INDEXES` names a
|
|
29
|
+
# single index, production PyPI, but `fetch_latest` still asks every index and
|
|
30
|
+
# takes the highest — the tuple is the seam a mirror or a pre-release channel
|
|
31
|
+
# gets added at. Patching the module global is what keeps that loop covered
|
|
32
|
+
# without inventing a second real index; it is a plain attribute, so
|
|
33
|
+
# monkeypatch restores it cleanly.
|
|
34
34
|
MIRROR_A = "https://mirror-a.test/pypi/deepcell-cli/json"
|
|
35
35
|
MIRROR_B = "https://mirror-b.test/pypi/deepcell-cli/json"
|
|
36
36
|
_INSTALL_A = "pip install --upgrade --index-url https://mirror-a.test/simple/ deepcell-cli"
|
|
@@ -184,10 +184,10 @@ def _index_payload(version: str) -> dict:
|
|
|
184
184
|
class TestFetchLatest:
|
|
185
185
|
def test_the_default_index_is_production_pypi(self) -> None:
|
|
186
186
|
# The package ships to production PyPI, which carries its dependencies
|
|
187
|
-
# too
|
|
188
|
-
# --extra-index-url.
|
|
189
|
-
#
|
|
190
|
-
#
|
|
187
|
+
# too, so the notice's install line takes no --index-url and no
|
|
188
|
+
# --extra-index-url. A notice that pins an index sends the reader
|
|
189
|
+
# somewhere this project does not publish, and every later release is
|
|
190
|
+
# then invisible to them.
|
|
191
191
|
assert len(upgrade_check.DEFAULT_INDEXES) == 1
|
|
192
192
|
(index,) = upgrade_check.DEFAULT_INDEXES
|
|
193
193
|
assert index.json_url == "https://pypi.org/pypi/deepcell-cli/json"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|