deepcell-cli 0.6.1__py3-none-any.whl
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/__init__.py +12 -0
- deepcell_cli/__main__.py +5 -0
- deepcell_cli/_findings.py +84 -0
- deepcell_cli/capabilities.py +560 -0
- deepcell_cli/capability-contract.json +15622 -0
- deepcell_cli/client.py +503 -0
- deepcell_cli/commands/__init__.py +1 -0
- deepcell_cli/commands/_batch_input.py +29 -0
- deepcell_cli/commands/_datatypes.py +56 -0
- deepcell_cli/commands/_negative_args.py +133 -0
- deepcell_cli/commands/_swapped_args.py +153 -0
- deepcell_cli/commands/_version_display.py +40 -0
- deepcell_cli/commands/_write_opts.py +139 -0
- deepcell_cli/commands/account.py +123 -0
- deepcell_cli/commands/auth.py +610 -0
- deepcell_cli/commands/changes.py +307 -0
- deepcell_cli/commands/deck.py +594 -0
- deepcell_cli/commands/defs.py +3890 -0
- deepcell_cli/commands/describe.py +902 -0
- deepcell_cli/commands/doc.py +529 -0
- deepcell_cli/commands/doctor.py +257 -0
- deepcell_cli/commands/download.py +36 -0
- deepcell_cli/commands/edit.py +384 -0
- deepcell_cli/commands/example.py +161 -0
- deepcell_cli/commands/export.py +81 -0
- deepcell_cli/commands/export_docx.py +57 -0
- deepcell_cli/commands/export_pdf.py +66 -0
- deepcell_cli/commands/export_pptx.py +45 -0
- deepcell_cli/commands/files.py +386 -0
- deepcell_cli/commands/grep.py +90 -0
- deepcell_cli/commands/guide.py +431 -0
- deepcell_cli/commands/help_cmd.py +348 -0
- deepcell_cli/commands/impact.py +382 -0
- deepcell_cli/commands/import_cmd.py +208 -0
- deepcell_cli/commands/ingest.py +110 -0
- deepcell_cli/commands/merge.py +399 -0
- deepcell_cli/commands/query.py +718 -0
- deepcell_cli/commands/reasoning.py +2981 -0
- deepcell_cli/commands/ref.py +279 -0
- deepcell_cli/commands/replace.py +326 -0
- deepcell_cli/commands/rules.py +206 -0
- deepcell_cli/commands/share.py +186 -0
- deepcell_cli/commands/sync.py +804 -0
- deepcell_cli/commands/upgrade.py +185 -0
- deepcell_cli/commands/variant.py +353 -0
- deepcell_cli/commands/version.py +445 -0
- deepcell_cli/commands/viewer.py +54 -0
- deepcell_cli/commands/workspace.py +101 -0
- deepcell_cli/config.py +352 -0
- deepcell_cli/context.py +187 -0
- deepcell_cli/errors.py +141 -0
- deepcell_cli/logging_setup.py +161 -0
- deepcell_cli/main.py +518 -0
- deepcell_cli/mcp_server.py +906 -0
- deepcell_cli/oauth_provider.py +580 -0
- deepcell_cli/output.py +503 -0
- deepcell_cli/revision.py +164 -0
- deepcell_cli/stages.py +223 -0
- deepcell_cli/surface.py +628 -0
- deepcell_cli/sync_state.py +120 -0
- deepcell_cli/upgrade_check.py +399 -0
- deepcell_cli/xml_replace.py +89 -0
- deepcell_cli-0.6.1.dist-info/METADATA +264 -0
- deepcell_cli-0.6.1.dist-info/RECORD +67 -0
- deepcell_cli-0.6.1.dist-info/WHEEL +5 -0
- deepcell_cli-0.6.1.dist-info/entry_points.txt +3 -0
- deepcell_cli-0.6.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""``deepcell help`` — the whole command tree as one document.
|
|
2
|
+
|
|
3
|
+
``--help`` answers a human's question, one command at a time. An agent
|
|
4
|
+
planning a twelve-step build would pay twelve ``--help`` round trips before it
|
|
5
|
+
could write step one; ``deepcell help -f json`` makes planning a single call,
|
|
6
|
+
and gives the agent something it can cache for the rest of the session.
|
|
7
|
+
|
|
8
|
+
Everything is local — the Click tree and the surface data both live in this
|
|
9
|
+
package — so this works with no network and no workspace, which is what makes
|
|
10
|
+
it usable as the *first* call rather than one that needs setup first.
|
|
11
|
+
|
|
12
|
+
See ``docs/cli-agent-surface.md`` §3.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from functools import lru_cache
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from deepcell_cli.capabilities import (
|
|
23
|
+
get_command_capability,
|
|
24
|
+
load_capability_contract,
|
|
25
|
+
)
|
|
26
|
+
from deepcell_cli.context import Ctx, pass_ctx
|
|
27
|
+
from deepcell_cli.output import output, print_plain
|
|
28
|
+
from deepcell_cli.surface import build_commands, build_globals
|
|
29
|
+
|
|
30
|
+
#: Exit 1 with no command-specific sense. Kept out of the manifest's
|
|
31
|
+
#: ``exit_codes`` map — an explicit "nothing special here" value would look
|
|
32
|
+
#: like a sixth sense of exit 1, and there are five.
|
|
33
|
+
_GENERIC_EXIT_1 = "the command failed"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _render_command(entry: dict) -> str:
|
|
37
|
+
lines = [entry["name"], "=" * len(entry["name"]), ""]
|
|
38
|
+
if entry.get("summary"):
|
|
39
|
+
lines += [entry["summary"], ""]
|
|
40
|
+
|
|
41
|
+
lines.append(f"Usage: {entry['example']}")
|
|
42
|
+
if entry.get("example_source") == "derived":
|
|
43
|
+
# Never let a derived example pass for a tested one.
|
|
44
|
+
lines.append(" (shape derived from the signature, not a tested run)")
|
|
45
|
+
lines.append("")
|
|
46
|
+
|
|
47
|
+
if entry["args"]:
|
|
48
|
+
lines.append("Arguments:")
|
|
49
|
+
for arg in entry["args"]:
|
|
50
|
+
suffix = "..." if arg.get("variadic") else ""
|
|
51
|
+
need = "" if arg["required"] else " (optional)"
|
|
52
|
+
lines.append(f" {arg['name']}{suffix}: {arg['type']}{need}")
|
|
53
|
+
lines.append("")
|
|
54
|
+
|
|
55
|
+
if entry["flags"]:
|
|
56
|
+
lines.append("Flags:")
|
|
57
|
+
for flag in entry["flags"]:
|
|
58
|
+
names = ", ".join(flag["flags"])
|
|
59
|
+
bits = [flag["type"] if not flag["is_flag"] else "flag"]
|
|
60
|
+
if flag.get("choices"):
|
|
61
|
+
bits.append("one of: " + "|".join(map(str, flag["choices"])))
|
|
62
|
+
if "default" in flag:
|
|
63
|
+
bits.append(f"default {flag['default']}")
|
|
64
|
+
if flag["required"]:
|
|
65
|
+
bits.append("required")
|
|
66
|
+
lines.append(f" {names} ({'; '.join(bits)})")
|
|
67
|
+
if flag.get("help"):
|
|
68
|
+
lines.append(f" {flag['help']}")
|
|
69
|
+
lines.append("")
|
|
70
|
+
|
|
71
|
+
lines.append("Exit codes:")
|
|
72
|
+
for code in sorted(entry["exit_codes"]):
|
|
73
|
+
lines.append(f" {code} {entry['exit_codes'][code]}")
|
|
74
|
+
if "1" not in entry["exit_codes"]:
|
|
75
|
+
lines.append(f" 1 {_GENERIC_EXIT_1}")
|
|
76
|
+
lines.append(" 2 malformed input — the command was never attempted")
|
|
77
|
+
|
|
78
|
+
if entry["see_also"]:
|
|
79
|
+
lines += ["", "See also: " + " · ".join(entry["see_also"])]
|
|
80
|
+
lines.append("Read one: deepcell ref <id>")
|
|
81
|
+
|
|
82
|
+
capability = entry.get("capability")
|
|
83
|
+
if capability:
|
|
84
|
+
lines += ["", "Availability:"]
|
|
85
|
+
for transport in ("cli", "mcp", "in_process"):
|
|
86
|
+
policy = capability["transports"][transport]
|
|
87
|
+
label = transport.replace("_", "-")
|
|
88
|
+
if policy["available"]:
|
|
89
|
+
lines.append(f" {label}: available")
|
|
90
|
+
else:
|
|
91
|
+
lines.append(f" {label}: unavailable ({policy['reason']})")
|
|
92
|
+
endpoints = capability.get("endpoints") or []
|
|
93
|
+
if endpoints:
|
|
94
|
+
rendered = ", ".join(
|
|
95
|
+
f"{row['method']} {row['path']}" for row in endpoints
|
|
96
|
+
)
|
|
97
|
+
lines.append(f" Jingwei: {rendered}")
|
|
98
|
+
elif capability.get("endpoint_exception"):
|
|
99
|
+
lines.append(
|
|
100
|
+
" Jingwei: none "
|
|
101
|
+
f"({capability['endpoint_exception']['reason']})"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _render_group(
|
|
108
|
+
path: str, children: list[str], commands: dict, *, titled: bool = True
|
|
109
|
+
) -> str:
|
|
110
|
+
"""A group as an index of its subcommands, one summary line each.
|
|
111
|
+
|
|
112
|
+
``titled=False`` when this is appended to the group's *own* command help —
|
|
113
|
+
the heading is already on the page, and repeating it reads as two answers
|
|
114
|
+
to one question.
|
|
115
|
+
"""
|
|
116
|
+
lines = [path, "=" * len(path), ""] if titled else []
|
|
117
|
+
lines += [f"{len(children)} subcommand(s):", ""]
|
|
118
|
+
width = max(len(c) for c in children)
|
|
119
|
+
lines += [
|
|
120
|
+
f" {name:<{width}} {commands[name].get('summary', '')}".rstrip()
|
|
121
|
+
for name in children
|
|
122
|
+
]
|
|
123
|
+
lines += ["", f"One command: deepcell help {children[0]}"]
|
|
124
|
+
return "\n".join(lines)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@lru_cache(maxsize=128)
|
|
128
|
+
def _needle_pattern(needle: str) -> "re.Pattern[str] | None":
|
|
129
|
+
"""Whole-word, separator-flexible matcher — the CLI half of one rule.
|
|
130
|
+
|
|
131
|
+
The authority is ``src.core.search_text.needle_pattern``; this package
|
|
132
|
+
cannot import the backend, so the rule is restated here the same way the
|
|
133
|
+
two ``guide`` index renderers restate their headings. Keep them in step:
|
|
134
|
+
whole words because `lag` must not match *flag*, flexible separators
|
|
135
|
+
because "order mode" is spelled `--order-mode`, and the word guard applied
|
|
136
|
+
per end only where that end is a word character — `--order-mode` and
|
|
137
|
+
`.deepcell` start on punctuation that is its own boundary.
|
|
138
|
+
"""
|
|
139
|
+
words = [p for p in re.split(r"[\s\-_]+", needle) if p]
|
|
140
|
+
if not words:
|
|
141
|
+
return None
|
|
142
|
+
core = r"[\s\-_]+".join(re.escape(w) for w in words)
|
|
143
|
+
head = r"(?<!\w)" if re.match(r"\w", words[0]) else ""
|
|
144
|
+
tail = r"(?!\w)" if re.search(r"\w\Z", words[-1]) else ""
|
|
145
|
+
return re.compile(head + core + tail, re.IGNORECASE)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _detail_hit(text: str, needle: str) -> bool:
|
|
149
|
+
"""Whether *needle* appears as a whole word in *text*."""
|
|
150
|
+
pattern = _needle_pattern(needle)
|
|
151
|
+
return bool(pattern and text and pattern.search(text))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _detail_text(entry: dict) -> str:
|
|
155
|
+
"""The parts of a command a name-and-summary search cannot see.
|
|
156
|
+
|
|
157
|
+
Flag names and their help, the worked example, and the argument names —
|
|
158
|
+
all already in memory from `build_commands()`, so widening the index here
|
|
159
|
+
costs no IO at all. This is where a caller's actual words live: nothing is
|
|
160
|
+
*summarised* as "sensitivity sweep", but a flag's help says it, and
|
|
161
|
+
`deepcell help --search` answering "No command matches" to a word the CLI
|
|
162
|
+
itself prints is the surface calling its own vocabulary unknown.
|
|
163
|
+
"""
|
|
164
|
+
parts: list[str] = [str(entry.get("example") or "")]
|
|
165
|
+
for flag in entry.get("flags") or ():
|
|
166
|
+
parts.append(str(flag.get("name") or ""))
|
|
167
|
+
parts.append(str(flag.get("help") or ""))
|
|
168
|
+
for arg in entry.get("args") or ():
|
|
169
|
+
parts.append(str(arg.get("name") or ""))
|
|
170
|
+
return "\n".join(p for p in parts if p)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _matching(commands: dict, search: str) -> dict:
|
|
174
|
+
"""Commands matching *search*, name and summary hits first.
|
|
175
|
+
|
|
176
|
+
Ordered, not merely filtered: a detail hit may only ever append below the
|
|
177
|
+
name and summary hits, so widening the index cannot push the command you
|
|
178
|
+
typed the name of below one that merely mentions it in a flag's help. The
|
|
179
|
+
same invariant `ref search` and `guide --search` hold, for the same reason.
|
|
180
|
+
"""
|
|
181
|
+
needle = search.lower()
|
|
182
|
+
named, detailed = {}, {}
|
|
183
|
+
for name, entry in commands.items():
|
|
184
|
+
if needle in name.lower() or needle in (entry.get("summary") or "").lower():
|
|
185
|
+
named[name] = entry
|
|
186
|
+
elif _detail_hit(_detail_text(entry), search):
|
|
187
|
+
detailed[name] = entry
|
|
188
|
+
return {**named, **detailed}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@click.command("help")
|
|
192
|
+
@click.argument("command", nargs=-1)
|
|
193
|
+
@click.option(
|
|
194
|
+
"--stage",
|
|
195
|
+
default=None,
|
|
196
|
+
metavar="NAME",
|
|
197
|
+
help="Only this stage's commands (e.g. generate). Same stages as `deepcell guide`.",
|
|
198
|
+
)
|
|
199
|
+
@click.option(
|
|
200
|
+
"--search",
|
|
201
|
+
default=None,
|
|
202
|
+
metavar="TEXT",
|
|
203
|
+
help="Commands whose name or summary matches TEXT.",
|
|
204
|
+
)
|
|
205
|
+
@pass_ctx
|
|
206
|
+
def help_cmd(
|
|
207
|
+
ctx: Ctx,
|
|
208
|
+
command: tuple[str, ...],
|
|
209
|
+
stage: str | None,
|
|
210
|
+
search: str | None,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Every command, flag, exit code and example as one document.
|
|
213
|
+
|
|
214
|
+
\b
|
|
215
|
+
The whole tree: deepcell help
|
|
216
|
+
One stage: deepcell help --stage generate
|
|
217
|
+
Find one: deepcell help --search sensitivity
|
|
218
|
+
One command: deepcell help defs add-calc
|
|
219
|
+
As data: deepcell help -f json
|
|
220
|
+
|
|
221
|
+
`-f json` is the form to parse: it is the same structure the published
|
|
222
|
+
command reference is rendered from, so what you read here and what the
|
|
223
|
+
docs say cannot disagree.
|
|
224
|
+
|
|
225
|
+
This answers what to TYPE. For what to DO next — the procedure, in the
|
|
226
|
+
order the work happens — run `deepcell guide`. The two use the same six
|
|
227
|
+
stage names, so a stage that reads well in one filters the other.
|
|
228
|
+
"""
|
|
229
|
+
commands = {
|
|
230
|
+
name: {
|
|
231
|
+
**entry,
|
|
232
|
+
"capability": get_command_capability(name),
|
|
233
|
+
}
|
|
234
|
+
for name, entry in build_commands().items()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (stage or search) and command:
|
|
238
|
+
raise click.ClickException(
|
|
239
|
+
"--stage/--search filter the listing; they do not apply when you "
|
|
240
|
+
"have already named a command. Drop the filter, or drop the "
|
|
241
|
+
f"command: deepcell help {' '.join(command)}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if stage:
|
|
245
|
+
from deepcell_cli.stages import ALL_TIERS
|
|
246
|
+
|
|
247
|
+
if stage not in ALL_TIERS:
|
|
248
|
+
raise click.ClickException(
|
|
249
|
+
f"Unknown stage: {stage}. One of: {', '.join(ALL_TIERS)}."
|
|
250
|
+
)
|
|
251
|
+
commands = {n: e for n, e in commands.items() if e.get("stage") == stage}
|
|
252
|
+
if search:
|
|
253
|
+
commands = _matching(commands, search)
|
|
254
|
+
if not commands:
|
|
255
|
+
raise click.ClickException(f"No command matches: {search}")
|
|
256
|
+
|
|
257
|
+
if command:
|
|
258
|
+
path = " ".join(command)
|
|
259
|
+
entry = commands.get(path)
|
|
260
|
+
if entry is None:
|
|
261
|
+
# `build_commands()` keys leaf commands only, so a group path misses
|
|
262
|
+
# the table. Asking what is under `defs` is a well-formed question
|
|
263
|
+
# and answering it with a non-zero exit made an agent treat the group
|
|
264
|
+
# as nonexistent — so render the group's leaves instead. Kept here
|
|
265
|
+
# rather than in `build_commands()`, whose output is a codegen source
|
|
266
|
+
# for the surface manifest.
|
|
267
|
+
children = sorted(
|
|
268
|
+
name for name in commands if name.startswith(f"{path} ")
|
|
269
|
+
)
|
|
270
|
+
if children:
|
|
271
|
+
if ctx.fmt == "json":
|
|
272
|
+
output({"group": path, "commands": children}, ctx.fmt)
|
|
273
|
+
else:
|
|
274
|
+
print_plain(_render_group(path, children, commands))
|
|
275
|
+
return
|
|
276
|
+
near = [name for name in commands if name.startswith(path)]
|
|
277
|
+
hint = f" Did you mean: {', '.join(near[:5])}?" if near else ""
|
|
278
|
+
raise click.ClickException(f"Unknown command: {path}.{hint}")
|
|
279
|
+
# A command that is *also* a group (`example`) needs both halves: what
|
|
280
|
+
# running it bare does, and what lives under it. Rendering only the
|
|
281
|
+
# command would hide the subcommands that were the whole reason to
|
|
282
|
+
# look it up.
|
|
283
|
+
children = sorted(name for name in commands if name.startswith(f"{path} "))
|
|
284
|
+
if ctx.fmt == "json":
|
|
285
|
+
output({**entry, "commands": children} if children else entry, ctx.fmt)
|
|
286
|
+
else:
|
|
287
|
+
rendered = _render_command(entry)
|
|
288
|
+
if children:
|
|
289
|
+
rendered += "\n\n" + _render_group(
|
|
290
|
+
path, children, commands, titled=False
|
|
291
|
+
)
|
|
292
|
+
print_plain(rendered)
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
if ctx.fmt == "json":
|
|
296
|
+
contract = load_capability_contract()
|
|
297
|
+
output(
|
|
298
|
+
{
|
|
299
|
+
"commands": commands,
|
|
300
|
+
"globals": build_globals(),
|
|
301
|
+
"capabilities": {
|
|
302
|
+
"schema_version": contract["schema_version"],
|
|
303
|
+
"operation_count": len(contract["operations"]),
|
|
304
|
+
"reader": "deepcell guide --capabilities",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
ctx.fmt,
|
|
308
|
+
)
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
# Grouped by stage, not alphabetical. 161 leaf commands in one A-Z run is
|
|
312
|
+
# not an index, it is a search problem — the same conclusion the guide
|
|
313
|
+
# restructure reached about 55 flat topics. The stage names are guide's,
|
|
314
|
+
# so a reader who oriented there lands in the right block here.
|
|
315
|
+
from deepcell_cli.stages import ALL_TIERS, TIER_SUMMARY
|
|
316
|
+
|
|
317
|
+
width = max(len(name) for name in commands)
|
|
318
|
+
lines: list[str] = []
|
|
319
|
+
for tier in ALL_TIERS:
|
|
320
|
+
block = [(n, e) for n, e in commands.items() if e.get("stage") == tier]
|
|
321
|
+
if not block:
|
|
322
|
+
continue
|
|
323
|
+
if lines:
|
|
324
|
+
lines.append("")
|
|
325
|
+
lines.append(f"{tier.upper()} — {TIER_SUMMARY[tier]}")
|
|
326
|
+
lines += [f" {name:<{width}} {entry['summary']}" for name, entry in block]
|
|
327
|
+
# A command whose top-level name is not staged cannot silently disappear
|
|
328
|
+
# from the catalog just because the index has no home for it.
|
|
329
|
+
unstaged = [(n, e) for n, e in commands.items() if e.get("stage") not in ALL_TIERS]
|
|
330
|
+
if unstaged:
|
|
331
|
+
lines += ["", "UNSTAGED — no stage declared in deepcell_cli.stages"]
|
|
332
|
+
lines += [f" {name:<{width}} {entry['summary']}" for name, entry in unstaged]
|
|
333
|
+
# Printed once, at the end, because they apply to every line above — and
|
|
334
|
+
# because they are declared on the root group, so they appear in no
|
|
335
|
+
# command's own flag list no matter which one you read.
|
|
336
|
+
lines += ["", "Global options — valid on any command, before or after it:"]
|
|
337
|
+
for option in build_globals():
|
|
338
|
+
names = ", ".join(option["flags"])
|
|
339
|
+
kind = "flag" if option["is_flag"] else option["type"]
|
|
340
|
+
lines.append(f" {names} ({kind})")
|
|
341
|
+
if option.get("help"):
|
|
342
|
+
lines.append(f" {option['help']}")
|
|
343
|
+
lines += [
|
|
344
|
+
"",
|
|
345
|
+
f"{len(commands)} commands. Read one: deepcell help <command>",
|
|
346
|
+
"As data (one call, cacheable): deepcell help -f json",
|
|
347
|
+
]
|
|
348
|
+
print_plain("\n".join(lines))
|