@andespindola/crow-skills 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/catalog.json +24 -0
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/autoupdate.py +71 -10
- package/crow_skills/cli.py +375 -61
- package/crow_skills/groups.py +76 -0
- package/crow_skills/guide.py +15 -2
- package/crow_skills/integrations.py +188 -13
- package/crow_skills/machine.py +720 -0
- package/crow_skills/trust.py +437 -0
- package/crow_skills/ui.py +198 -0
- package/docs/cli.md +72 -8
- package/package.json +1 -1
- package/release.json +31 -11
package/crow_skills/cli.py
CHANGED
|
@@ -9,22 +9,99 @@ from . import __version__
|
|
|
9
9
|
from . import autoupdate
|
|
10
10
|
from . import broadcast
|
|
11
11
|
from . import events
|
|
12
|
+
from . import groups as groups_mod
|
|
12
13
|
from . import guide
|
|
13
14
|
from . import hook
|
|
14
15
|
from . import integrations
|
|
15
16
|
from . import progress
|
|
16
17
|
from . import resources
|
|
18
|
+
from . import machine
|
|
19
|
+
from . import ui
|
|
17
20
|
from .agents import AGENTS, destinations, inventory
|
|
18
21
|
from .source import CrowError, Source, valid_name
|
|
19
22
|
from .storage import Store, checked
|
|
20
23
|
|
|
21
24
|
DEFAULT_SOURCE = Path(__file__).resolve().parents[1]
|
|
22
|
-
MUTATIONS = {"install", "update", "uninstall", "rollback", "recover", "guide", "integrations", "mode", "autoupdate"}
|
|
25
|
+
MUTATIONS = {"install", "update", "uninstall", "rollback", "recover", "guide", "integrations", "mode", "autoupdate", "setup", "upgrade"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
TRUST_HELP = "Não habilitar/aprovar os hooks do crow-skills nos CLIs (padrão: habilita)"
|
|
29
|
+
|
|
30
|
+
# Per-command help: what it is for and real examples (crow-skills help <cmd> or <cmd> --help).
|
|
31
|
+
COMMANDS = {
|
|
32
|
+
"setup": ("Detecta os CLIs de agente desta máquina (Claude Code, Codex, Gemini CLI, Antigravity, Devin, Uniflow, tmux) e "
|
|
33
|
+
"configura cada um no escopo do usuário: todas as skills do catálogo, guia, integrações, hooks do modo crow "
|
|
34
|
+
"habilitados, modo crow e autoupdate. Rodar de novo só corrige o que mudou.",
|
|
35
|
+
["crow-skills setup --dry-run", "crow-skills setup --yes", "crow-skills setup --agents claude,codex --yes",
|
|
36
|
+
"crow-skills setup --skills crow-behaviour,crow-debug --no-autoupdate --yes"]),
|
|
37
|
+
"upgrade": ("Atualiza o próprio crow-skills (Homebrew, npm ou checkout git) e, em seguida, skills, guias, integrações e "
|
|
38
|
+
"hooks de todo agente configurado. Skills editadas localmente nunca são sobrescritas.",
|
|
39
|
+
["crow-skills upgrade --dry-run", "crow-skills upgrade --yes", "crow-skills upgrade --skills-only --yes"]),
|
|
40
|
+
"help": ("Ajuda curta, por tarefa; help <comando> detalha um comando e help flags lista as flags globais.",
|
|
41
|
+
["crow-skills help", "crow-skills help setup", "crow-skills help flags"]),
|
|
42
|
+
"list": ("Lista o catálogo de skills da fonte atual.", ["crow-skills list", "crow-skills list --json"]),
|
|
43
|
+
"groups": ("Grupos de skills pré-definidos no catálogo, para install --group e setup --group (todas = o catálogo inteiro; "
|
|
44
|
+
"a crow-behaviour entra com qualquer grupo).", ["crow-skills groups", "crow-skills groups --json"]),
|
|
45
|
+
"agents": ("Agentes detectados (PATH e diretórios); não prova que as skills carregam.", ["crow-skills agents --json"]),
|
|
46
|
+
"status": ("Instalações, guias, integrações, modo e autoupdate, por agente.", ["crow-skills status", "crow-skills status --agents claude"]),
|
|
47
|
+
"doctor": ("Como status, com healthy true/false: skills editadas/ausentes, guia alterado, hooks não aprovados, transação pendente.",
|
|
48
|
+
["crow-skills doctor", "crow-skills doctor --json"]),
|
|
49
|
+
"info": ("Detalhes de uma skill do catálogo.", ["crow-skills info crow-backend"]),
|
|
50
|
+
"related": ("Handoffs, overlays e disponibilidade de complementos de uma skill.", ["crow-skills related crow-backend --agents claude --global"]),
|
|
51
|
+
"install": ("Instala skills escolhidas em agentes e escopo explícitos, com preview, backup e transação.",
|
|
52
|
+
["crow-skills install crow-behaviour --agents claude,codex --global --dry-run",
|
|
53
|
+
"crow-skills install --group essencial --agents claude --global --yes",
|
|
54
|
+
"crow-skills install --group web,qualidade --agents codex --global --yes",
|
|
55
|
+
"crow-skills install --all --agents claude --global --yes",
|
|
56
|
+
"crow-skills install crow-debug --agents gemini --project . --yes"]),
|
|
57
|
+
"update": ("Atualiza skills gerenciadas para a revisão da fonte atual (sem self-upgrade; para tudo, use upgrade).",
|
|
58
|
+
["crow-skills update --all --agents claude --global --yes", "crow-skills update crow-debug --agents codex --global --dry-run"]),
|
|
59
|
+
"uninstall": ("Remove skills gerenciadas, com backup; edições locais exigem --overwrite-modified.",
|
|
60
|
+
["crow-skills uninstall crow-debug --agents claude --global --yes"]),
|
|
61
|
+
"guide": ("Instruções gerenciadas para o agente usar as skills (só --global).",
|
|
62
|
+
["crow-skills guide status", "crow-skills guide install --agents claude --global --yes"]),
|
|
63
|
+
"integrations": ("Status lines, hooks do modo crow, comando /crow e segmento tmux (só --global).",
|
|
64
|
+
["crow-skills integrations status", "crow-skills integrations install --global --yes"]),
|
|
65
|
+
"progress": ("Painel de progresso da execução crow-behaviour (ledger, barras, tokens).",
|
|
66
|
+
["crow-skills progress", "crow-skills progress --watch"]),
|
|
67
|
+
"mode": ("Liga/desliga a crow-behaviour em todas as sessões e CLIs, a partir do próximo prompt.",
|
|
68
|
+
["crow-skills mode", "crow-skills mode off", "crow-skills mode on"]),
|
|
69
|
+
"events": ("Barramento de eventos dos hooks (crow-event/1) para terminais.",
|
|
70
|
+
["crow-skills events", "crow-skills events tail --limit 20", "crow-skills events tail --follow"]),
|
|
71
|
+
"broadcast": ("Sessões vivas dos CLIs, arquivos em edição, conflitos e reservas.",
|
|
72
|
+
["crow-skills broadcast", "crow-skills broadcast claim src/api --ttl 30m", "crow-skills broadcast release --all"]),
|
|
73
|
+
"resources": ("Memória, pressão, agentes em execução e paralelismo recomendado (somente leitura).",
|
|
74
|
+
["crow-skills resources", "crow-skills resources --exec-prefix"]),
|
|
75
|
+
"autoupdate": ("Agenda a atualização diária e no login (o mesmo fluxo de upgrade) ou roda agora.",
|
|
76
|
+
["crow-skills autoupdate on --yes", "crow-skills autoupdate status", "crow-skills autoupdate run --yes"]),
|
|
77
|
+
"rollback": ("Desfaz uma transação concluída (skills e o guia ligado a ela).", ["crow-skills rollback --transaction <id> --yes"]),
|
|
78
|
+
"recover": ("Conclui ou desfaz uma transação interrompida.", ["crow-skills recover --dry-run", "crow-skills recover --yes"]),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Formatter(argparse.RawDescriptionHelpFormatter):
|
|
83
|
+
def add_usage(self, usage, actions, groups, prefix=None):
|
|
84
|
+
return super().add_usage(usage, actions, groups, "uso: " if prefix is None else prefix)
|
|
23
85
|
|
|
24
86
|
|
|
25
87
|
class Parser(argparse.ArgumentParser):
|
|
88
|
+
def __init__(self, *args, **kwargs):
|
|
89
|
+
kwargs.setdefault("formatter_class", Formatter)
|
|
90
|
+
super().__init__(*args, **kwargs)
|
|
91
|
+
self._positionals.title = "argumentos"
|
|
92
|
+
self._optionals.title = "opções"
|
|
93
|
+
for action in self._actions:
|
|
94
|
+
if isinstance(action, argparse._HelpAction):
|
|
95
|
+
action.help = "Mostra esta ajuda e sai"
|
|
96
|
+
|
|
26
97
|
def error(self, message):
|
|
27
|
-
raise CrowError("usage", message)
|
|
98
|
+
raise CrowError("usage", ui.translate_usage(message))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def command_parser(sub, name, parents, aliases=()):
|
|
102
|
+
text, examples = COMMANDS[name]
|
|
103
|
+
return sub.add_parser(name, parents=parents, aliases=list(aliases), help=text, description=text,
|
|
104
|
+
epilog="exemplos:\n" + "\n".join(" $ " + e for e in examples))
|
|
28
105
|
|
|
29
106
|
|
|
30
107
|
def parser():
|
|
@@ -38,24 +115,46 @@ def parser():
|
|
|
38
115
|
("--project", {"help": "Diretório físico do projeto"}),
|
|
39
116
|
("--dest", {"help": "Diretório de skills explícito, para outro host"}),
|
|
40
117
|
("--json", {"action": "store_true", "help": "Envelope JSON; nunca abre prompts"}),
|
|
41
|
-
("--non-interactive", {"action": "store_true"}),
|
|
118
|
+
("--non-interactive", {"action": "store_true", "help": "Nunca perguntar; sem --yes, mutações falham"}),
|
|
42
119
|
("--yes", {"action": "store_true", "help": "Aplicar o escopo explícito sem prompt"}),
|
|
43
120
|
("--dry-run", {"action": "store_true", "help": "Preview sem gravar estado ou instalar"}),
|
|
44
121
|
("--fetch", {"action": "store_true", "help": "Buscar origin explicitamente; não cria clone"}),
|
|
45
122
|
]:
|
|
46
123
|
common.add_argument(flag, default=argparse.SUPPRESS, **kwargs)
|
|
47
|
-
|
|
124
|
+
common._optionals.title = "flags globais (crow-skills help flags)"
|
|
125
|
+
p = Parser(prog="crow-skills", parents=[common], add_help=False,
|
|
126
|
+
description="Instale pacotes crow completos, por agente e revisão, sem executar scripts de skills.")
|
|
127
|
+
p.add_argument("-h", "--help", dest="show_help", action="store_true", default=argparse.SUPPRESS,
|
|
128
|
+
help="Ajuda curta, por tarefa")
|
|
48
129
|
p.add_argument("--version", action="store_true")
|
|
49
|
-
sub = p.add_subparsers(dest="command")
|
|
50
|
-
|
|
51
|
-
|
|
130
|
+
sub = p.add_subparsers(dest="command", title="comandos", metavar="<comando>")
|
|
131
|
+
child = command_parser(sub, "setup", [common], aliases=("init",))
|
|
132
|
+
child.add_argument("--skills", help="Só estas skills (separadas por vírgula); padrão: todo o catálogo")
|
|
133
|
+
child.add_argument("--group", help="Só estes grupos (separados por vírgula; veja crow-skills groups); padrão: todas")
|
|
134
|
+
child.add_argument("--no-autoupdate", action="store_true", help="Não ligar o autoupdate")
|
|
135
|
+
child.add_argument("--replace", action="store_true", help="Autorizar backup e substituição de pacote/guia não gerenciado")
|
|
136
|
+
child.add_argument("--overwrite-modified", action="store_true", help="Autorizar backup e substituição de skills/guias editados")
|
|
137
|
+
child.add_argument("--no-trust", action="store_true", help=TRUST_HELP)
|
|
138
|
+
child.add_argument("--no-probe", action="store_true", help=argparse.SUPPRESS)
|
|
139
|
+
child = command_parser(sub, "upgrade", [common])
|
|
140
|
+
child.add_argument("--skills-only", action="store_true", help="Só skills/guias/hooks, sem atualizar o crow-skills")
|
|
141
|
+
child.add_argument("--no-self", action="store_true", help="Igual a --skills-only")
|
|
142
|
+
child.add_argument("--git-pull", action="store_true", help="Checkout git: git pull --ff-only se estiver limpo em main/master")
|
|
143
|
+
child.add_argument("--no-trust", action="store_true", help=TRUST_HELP)
|
|
144
|
+
child = command_parser(sub, "help", [common])
|
|
145
|
+
child.add_argument("topic", nargs="?", help="Comando, ou flags")
|
|
146
|
+
for name in ("list", "groups", "agents", "status", "doctor"):
|
|
147
|
+
command_parser(sub, name, [common])
|
|
52
148
|
for name in ("info", "related"):
|
|
53
|
-
child = sub
|
|
149
|
+
child = command_parser(sub, name, [common])
|
|
54
150
|
child.add_argument("name")
|
|
55
151
|
for name in ("install", "update", "uninstall"):
|
|
56
|
-
child = sub
|
|
152
|
+
child = command_parser(sub, name, [common])
|
|
57
153
|
child.add_argument("skills", nargs="*")
|
|
58
154
|
child.add_argument("--all", action="store_true", help="Todos do catálogo (install) ou gerenciados (update/uninstall)")
|
|
155
|
+
if name == "install":
|
|
156
|
+
child.add_argument("--group", help="Grupos pré-definidos, separados por vírgula (crow-skills groups); "
|
|
157
|
+
"todas = catálogo inteiro; crow-behaviour sempre entra")
|
|
59
158
|
if name != "uninstall":
|
|
60
159
|
child.add_argument("--replace", action="store_true", help="Autorizar substituição com backup de pacote não gerenciado")
|
|
61
160
|
child.add_argument("--with-handoffs", action="store_true", help="Incluir apenas handoffs diretos opcionais do catálogo")
|
|
@@ -63,24 +162,26 @@ def parser():
|
|
|
63
162
|
child.add_argument("--no-guide", action="store_true", help="Não aplicar/atualizar/remover o guia do agente (--global)")
|
|
64
163
|
child.add_argument("--no-integrations", action="store_true",
|
|
65
164
|
help="Não aplicar/remover as integrações de interface (status line, tmux) em --global")
|
|
66
|
-
|
|
165
|
+
if name != "uninstall":
|
|
166
|
+
child.add_argument("--no-trust", action="store_true", help=TRUST_HELP)
|
|
167
|
+
child = command_parser(sub, "guide", [common])
|
|
67
168
|
child.add_argument("action", choices=("install", "status", "uninstall"))
|
|
68
169
|
child.add_argument("--replace", action="store_true", help="Autorizar backup e substituição de conteúdo não gerenciado no caminho do guia")
|
|
69
170
|
child.add_argument("--overwrite-modified", action="store_true", help="Autorizar backup e substituição/remoção de guia editado")
|
|
70
|
-
child = sub
|
|
171
|
+
child = command_parser(sub, "integrations", [common])
|
|
71
172
|
child.add_argument("action", choices=("install", "status", "uninstall"))
|
|
72
|
-
child
|
|
173
|
+
child.add_argument("--no-trust", action="store_true", help=TRUST_HELP)
|
|
174
|
+
child = command_parser(sub, "progress", [common])
|
|
73
175
|
child.add_argument("--watch", action="store_true", help="Redesenhar até a execução terminar ou Ctrl-C")
|
|
74
176
|
child.add_argument("--run", help="ID da execução; padrão: a ativa mais recente do diretório")
|
|
75
177
|
child.add_argument("--cwd", help="Diretório da execução; padrão: o atual")
|
|
76
|
-
child = sub
|
|
178
|
+
child = command_parser(sub, "mode", [common])
|
|
77
179
|
child.add_argument("action", nargs="?", default="status", choices=("on", "off", "status", "toggle"))
|
|
78
|
-
child = sub
|
|
180
|
+
child = command_parser(sub, "events", [common])
|
|
79
181
|
child.add_argument("action", nargs="?", default="snapshot", choices=("snapshot", "tail"))
|
|
80
182
|
child.add_argument("--limit", type=int, default=50, help="tail: quantos eventos (padrão 50; 0 = todos os retidos)")
|
|
81
183
|
child.add_argument("--follow", action="store_true", help="tail: transmitir eventos novos em JSONL até Ctrl-C")
|
|
82
|
-
child = sub
|
|
83
|
-
help="crow-broadcast: sessões vivas dos CLIs, arquivos em edição, conflitos e reservas")
|
|
184
|
+
child = command_parser(sub, "broadcast", [common])
|
|
84
185
|
child.add_argument("action", nargs="?", default="status", choices=("status", "list", "claim", "release", "watch"))
|
|
85
186
|
child.add_argument("targets", nargs="*", help="claim: caminhos, globs ou área; release: ids das reservas")
|
|
86
187
|
child.add_argument("--ttl", default="30m", help="claim: validade (ex.: 30m, 2h; máximo 24h)")
|
|
@@ -93,19 +194,21 @@ def parser():
|
|
|
93
194
|
child.add_argument("--all", action="store_true", help="release: todas as reservas desta sessão")
|
|
94
195
|
child.add_argument("--interval", type=float, default=2.0, help=argparse.SUPPRESS)
|
|
95
196
|
child.add_argument("--max-frames", type=int, default=None, help=argparse.SUPPRESS)
|
|
96
|
-
child = sub
|
|
97
|
-
help="Memória, pressão, agentes em execução e paralelismo recomendado (somente leitura)")
|
|
197
|
+
child = command_parser(sub, "resources", [common])
|
|
98
198
|
child.add_argument("--no-processes", action="store_true", help="Não varrer os processos dos CLIs de agente (mais rápido)")
|
|
99
199
|
child.add_argument("--exec-prefix", action="store_true",
|
|
100
200
|
help="Só imprimir o prefixo que roda um job pesado em prioridade baixa neste sistema (taskpolicy/nice/ionice/start /low)")
|
|
101
|
-
child = sub
|
|
201
|
+
child = command_parser(sub, "autoupdate", [common])
|
|
102
202
|
child.add_argument("action", choices=("on", "off", "status", "run"))
|
|
103
203
|
child.add_argument("--git-pull", action="store_true", help="on: também atualizar um checkout git limpo em main/master (git pull --ff-only)")
|
|
104
204
|
child.add_argument("--skills-only", action="store_true", help="run: só atualizar as skills, sem atualizar o crow-skills")
|
|
105
205
|
child.add_argument("--no-self", action="store_true", help="run: igual a --skills-only")
|
|
206
|
+
child.add_argument("--no-trust", action="store_true", help=TRUST_HELP)
|
|
106
207
|
for name in ("rollback", "recover"):
|
|
107
|
-
child = sub
|
|
208
|
+
child = command_parser(sub, name, [common])
|
|
108
209
|
child.add_argument("--transaction", required=name == "rollback")
|
|
210
|
+
p.common = common
|
|
211
|
+
p.commands = sub.choices
|
|
109
212
|
return p
|
|
110
213
|
|
|
111
214
|
|
|
@@ -301,7 +404,12 @@ def integrations_preview(args, home, store, plan):
|
|
|
301
404
|
if getattr(args, "project", None) or getattr(args, "dest", None):
|
|
302
405
|
return {"changes": [], "skipped": [{"integration": "*", "reason": "Integrações de interface só existem no escopo do usuário (--global)."}],
|
|
303
406
|
"disabled": True}
|
|
304
|
-
return integrations.sync(store, home, integration_agents(args, home, store, plan), dry_run=True)
|
|
407
|
+
return integrations.sync(store, home, integration_agents(args, home, store, plan), dry_run=True, trust=trusting(args))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def trusting(args):
|
|
411
|
+
"""Hooks are enabled/approved in each CLI unless --no-trust."""
|
|
412
|
+
return not getattr(args, "no_trust", False)
|
|
305
413
|
|
|
306
414
|
|
|
307
415
|
def integration_agents(args, home, store, plan=None, selected=None):
|
|
@@ -318,7 +426,7 @@ def integration_agents(args, home, store, plan=None, selected=None):
|
|
|
318
426
|
|
|
319
427
|
def integrations_command(args, home, store, interactive):
|
|
320
428
|
if args.action == "status":
|
|
321
|
-
return {"integrations": integrations.status(store, home)}
|
|
429
|
+
return {"integrations": integrations.status(store, home, probe=False)}
|
|
322
430
|
if not getattr(args, "global_scope", False):
|
|
323
431
|
raise CrowError("usage", "integrations " + args.action + " exige --global explícito (único escopo suportado).")
|
|
324
432
|
wanted = []
|
|
@@ -327,12 +435,12 @@ def integrations_command(args, home, store, interactive):
|
|
|
327
435
|
if args.action == "install" and not wanted:
|
|
328
436
|
raise CrowError("guide_requires_skill", "crow-behaviour não está instalada para nenhum agente; instale antes: "
|
|
329
437
|
"crow-skills install crow-behaviour --agents claude --global --yes")
|
|
330
|
-
preview = integrations.sync(store, home, wanted, dry_run=True)
|
|
438
|
+
preview = integrations.sync(store, home, wanted, dry_run=True, trust=trusting(args))
|
|
331
439
|
if preview["changes"]:
|
|
332
440
|
authorize(args, interactive, preview)
|
|
333
441
|
if getattr(args, "dry_run", False):
|
|
334
442
|
return {**preview, "applied": False}
|
|
335
|
-
return {**integrations.sync(store, home, wanted), "applied": True}
|
|
443
|
+
return {**integrations.sync(store, home, wanted, trust=trusting(args)), "applied": True}
|
|
336
444
|
|
|
337
445
|
|
|
338
446
|
def progress_command(args, home):
|
|
@@ -354,7 +462,7 @@ def mode_command(args, home):
|
|
|
354
462
|
wanted = args.action if args.action != "toggle" else ("off" if current["mode"] == "on" else "on")
|
|
355
463
|
current = hook.write_mode(home, wanted, "cli")
|
|
356
464
|
try:
|
|
357
|
-
installed = [r["integration"] for r in integrations.status(Store(home), home)
|
|
465
|
+
installed = [r["integration"] for r in integrations.status(Store(home), home, probe=False)
|
|
358
466
|
if r["status"] == "installed" and r["integration"].endswith(("-hooks", "-statusline", "-command", "tmux-status"))]
|
|
359
467
|
except CrowError:
|
|
360
468
|
installed = []
|
|
@@ -522,12 +630,13 @@ def autoupdate_command(args, home, store, interactive):
|
|
|
522
630
|
if args.action == "status":
|
|
523
631
|
return autoupdate.status(store, home)
|
|
524
632
|
if args.action == "run":
|
|
525
|
-
|
|
633
|
+
self_step = not (args.skills_only or args.no_self)
|
|
634
|
+
authorize(args, interactive, {"autoupdate": "run", "self_upgrade": self_step})
|
|
526
635
|
if getattr(args, "dry_run", False):
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return
|
|
636
|
+
preview = autoupdate.preview(store, home, self_step=self_step, trust=trusting(args))
|
|
637
|
+
return dict(preview, summary=machine.upgrade_summary(preview.get("skills", []), True))
|
|
638
|
+
done = autoupdate.run(store, home, self_step=self_step, trust=trusting(args))
|
|
639
|
+
return dict(done, summary=machine.upgrade_summary(done["lastRun"].get("skills", []), False))
|
|
531
640
|
if args.action == "on":
|
|
532
641
|
preview = autoupdate.turn_on(store, home, dry_run=True, git_pull=args.git_pull)
|
|
533
642
|
if any(c["action"] != "unchanged" for c in preview["changes"]):
|
|
@@ -589,26 +698,130 @@ def uniflow_notice(home, store, targets, plan):
|
|
|
589
698
|
native[row["target"]]["skills"].append({"name": row["name"], "status": row["status"]})
|
|
590
699
|
if not native:
|
|
591
700
|
return {"native_duplicates": [], "uninspected": uninspected}
|
|
592
|
-
|
|
593
|
-
|
|
701
|
+
# One command per destination with only the names physically managed there: crossing names and agents
|
|
702
|
+
# would reach a Uniflow link (or an absent name) in another CLI and abort the uninstall.
|
|
703
|
+
# Destinations with the same names share one command (the usual case keeps its single command).
|
|
704
|
+
groups = {}
|
|
705
|
+
for item in native.values():
|
|
706
|
+
managed = tuple(sorted({s["name"] for s in item["skills"] if s["status"] != "unmanaged"}))
|
|
707
|
+
if managed:
|
|
708
|
+
groups.setdefault(managed, []).extend(a for a in item["agents"] if a not in groups.get(managed, []))
|
|
709
|
+
commands = ["crow-skills uninstall " + " ".join(names_) + " --agents " + ",".join(sorted(agents_)) + " --global"
|
|
710
|
+
for names_, agents_ in groups.items()]
|
|
594
711
|
return {"native_duplicates": list(native.values()), "uninspected": uninspected,
|
|
595
712
|
"note": "effect=skipped: o Uniflow não cria o link de um nome que o agente já tem nativo, e essa cópia vale no lugar do "
|
|
596
713
|
"store. effect=both: o Uniflow liga o store em ~/.codex/skills e o Codex também lê ~/.agents/skills, então vê as "
|
|
597
714
|
"duas cópias. Nada foi removido. Remover a cópia nativa de crow-behaviour remove também o guia daquele agente.",
|
|
598
|
-
"
|
|
599
|
-
|
|
715
|
+
"suggested_commands": commands, "suggested_command": " && ".join(commands) if commands else None}
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def pick(source):
|
|
719
|
+
"""Terminal picker without dependencies: a group number or name, todas, or skill names. Default: essencial."""
|
|
720
|
+
available = groups_mod.load(source)
|
|
721
|
+
default = next((g["name"] for g in available if g["name"] == "essencial"), groups_mod.ALL)
|
|
722
|
+
print("Grupos de skills (a crow-behaviour entra com qualquer grupo):", file=sys.stderr)
|
|
723
|
+
for n, group in enumerate(available, 1):
|
|
724
|
+
print(terminal(" " + str(n).rjust(2) + ". " + group["name"].ljust(11) + " " + str(len(group["skills"])).rjust(2) +
|
|
725
|
+
" skills " + group["description"]), file=sys.stderr)
|
|
726
|
+
print(" " + str(len(available) + 1).rjust(2) + ". escolher skills individualmente", file=sys.stderr)
|
|
727
|
+
answer = prompt("Número(s) ou nome(s) separados por vírgula [" + default + "]:").strip()
|
|
728
|
+
tokens = [t.strip() for t in answer.split(",") if t.strip()] or [default]
|
|
729
|
+
if tokens == [str(len(available) + 1)]:
|
|
730
|
+
for name, item in source.catalog.items():
|
|
731
|
+
print(terminal(name + " — " + item.get("title", name)), file=sys.stderr)
|
|
732
|
+
tokens = [t.strip() for t in prompt("Skills separadas por vírgula:").split(",") if t.strip()]
|
|
733
|
+
if not tokens:
|
|
734
|
+
raise CrowError("usage", "Nenhuma skill selecionada.")
|
|
735
|
+
by_number = {str(n): g["name"] for n, g in enumerate(available, 1)}
|
|
736
|
+
chosen_groups = [by_number.get(t, t) for t in tokens if t in by_number or t in {g["name"] for g in available}]
|
|
737
|
+
skills = [t for t in tokens if t not in by_number and t not in {g["name"] for g in available}]
|
|
738
|
+
for name in skills:
|
|
739
|
+
source.require(name)
|
|
740
|
+
names = groups_mod.resolve(source, ",".join(chosen_groups)) if chosen_groups else []
|
|
741
|
+
names = list(dict.fromkeys(names + skills))
|
|
742
|
+
if not names:
|
|
743
|
+
raise CrowError("usage", "Nenhuma skill selecionada.")
|
|
744
|
+
return names, ",".join(chosen_groups) or None
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def unmanaged_projections(store, home, rows):
|
|
748
|
+
"""A Uniflow link to a store entry crow-skills does not manage is not an install (same rule as setup)."""
|
|
749
|
+
if not any(r["status"] == "uniflow_link" for r in rows):
|
|
750
|
+
return rows
|
|
751
|
+
managed = guide.store_managed(home, store)
|
|
752
|
+
disabled = machine.store_disabled(home, store)
|
|
753
|
+
for row in rows:
|
|
754
|
+
if row["status"] == "uniflow_link" and row["name"] in disabled:
|
|
755
|
+
# Managed by crow-skills but disabled by Uniflow: reported as such, never as unmanaged (no --replace).
|
|
756
|
+
row.update(status="disabled", projection="uniflow_link_disabled",
|
|
757
|
+
note="Desabilitada no Uniflow (SKILL.md.disabled no store); reabilite no Uniflow para usar.")
|
|
758
|
+
elif row["status"] == "uniflow_link" and row["name"] not in managed:
|
|
759
|
+
row.update(status="unmanaged", projection="uniflow_link_unmanaged",
|
|
760
|
+
note="Link do Uniflow para um pacote do store que o crow-skills não gerencia; não conta como instalada. "
|
|
761
|
+
"Revise o store e, para gerenciá-lo com backup: crow-skills setup --agents uniflow --replace --yes")
|
|
762
|
+
return rows
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def group_coverage(rows):
|
|
766
|
+
"""Per destination, which catalog groups are fully or partially installed (best effort)."""
|
|
767
|
+
try:
|
|
768
|
+
available = [g for g in groups_mod.load(Source(DEFAULT_SOURCE)) if g["name"] != groups_mod.ALL]
|
|
769
|
+
except (CrowError, OSError):
|
|
770
|
+
return []
|
|
771
|
+
by_target = {}
|
|
772
|
+
for row in rows:
|
|
773
|
+
item = by_target.setdefault(row["target"], {"target": row["target"], "agents": row["agents"], "names": set()})
|
|
774
|
+
if row["name"] and row["status"] in ("installed", "modified", "disabled", "uniflow_link"):
|
|
775
|
+
item["names"].add(row["name"])
|
|
776
|
+
return [{"target": v["target"], "agents": v["agents"],
|
|
777
|
+
"groups": [c for c in groups_mod.coverage(available, v["names"]) if c["state"] != "absent"]}
|
|
778
|
+
for v in by_target.values() if v["names"]]
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def groups_command(args):
|
|
782
|
+
source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"), getattr(args, "fetch", False))
|
|
783
|
+
available = groups_mod.load(source)
|
|
784
|
+
return {"revision": source.revision, "groups": available, "always": groups_mod.BEHAVIOUR,
|
|
785
|
+
"note": "crow-behaviour entra com qualquer grupo; handoffs de outros grupos são sugeridos, não instalados "
|
|
786
|
+
"(use --with-handoffs)."}
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def help_command(args, home, topic=None):
|
|
790
|
+
"""Short, task-oriented help; help <command> for one command; help flags for the global flags."""
|
|
791
|
+
p = parser()
|
|
792
|
+
if topic in (None, "", "help") and getattr(args, "command", None) in (None, "help"):
|
|
793
|
+
line, status = machine.status_line(home)
|
|
794
|
+
return {"topic": None, "sections": [{"title": t, "commands": [{"name": n, "summary": d, "example": e} for n, d, e in items]}
|
|
795
|
+
for t, items in ui.SECTIONS],
|
|
796
|
+
"status": status, "_status_line": line, "text": None}
|
|
797
|
+
if topic == "flags":
|
|
798
|
+
return {"topic": "flags", "text": ui.flags_help(p.common._actions)}
|
|
799
|
+
name = "setup" if topic == "init" else topic
|
|
800
|
+
if name not in p.commands:
|
|
801
|
+
raise CrowError("usage", "Comando desconhecido: " + str(topic) + ".")
|
|
802
|
+
return {"topic": name, "text": p.commands[name].format_help(), "examples": COMMANDS.get(name, ("", []))[1]}
|
|
600
803
|
|
|
601
804
|
|
|
602
805
|
def run(args):
|
|
806
|
+
if getattr(args, "command", None) == "init":
|
|
807
|
+
args.command = "setup"
|
|
808
|
+
if getattr(args, "show_help", False):
|
|
809
|
+
return help_command(args, checked(getattr(args, "home", Path.home())), args.command)
|
|
603
810
|
if args.version:
|
|
604
811
|
return {"version": __version__}
|
|
605
|
-
if not args.command:
|
|
606
|
-
raise CrowError("usage", "Escolha um comando; use crow-skills --help.")
|
|
607
812
|
home = checked(getattr(args, "home", Path.home()))
|
|
813
|
+
if not args.command or args.command == "help":
|
|
814
|
+
return help_command(args, home, getattr(args, "topic", None))
|
|
608
815
|
store = Store(home)
|
|
609
816
|
interactive = sys.stdin.isatty() and not getattr(args, "non_interactive", False) and not getattr(args, "json", False)
|
|
610
|
-
if
|
|
817
|
+
if args.command == "setup":
|
|
818
|
+
return machine.setup(args, home, store, interactive)
|
|
819
|
+
if args.command == "upgrade":
|
|
820
|
+
return machine.upgrade(args, home, store, interactive)
|
|
821
|
+
if getattr(args, "fetch", False) and (getattr(args, "dry_run", False) or args.command not in ("install", "update", "list", "info", "related", "guide", "groups")):
|
|
611
822
|
raise CrowError("usage", "--fetch requer um comando com fonte e não pode ser combinado com --dry-run.")
|
|
823
|
+
if args.command == "groups":
|
|
824
|
+
return groups_command(args)
|
|
612
825
|
if args.command == "agents":
|
|
613
826
|
return {"agents": inventory(home), "note": "Detecção por PATH/diretórios não comprova carregamento da skill."}
|
|
614
827
|
if args.command == "guide":
|
|
@@ -633,25 +846,31 @@ def run(args):
|
|
|
633
846
|
return autoupdate_command(args, home, store, interactive)
|
|
634
847
|
if args.command in ("status", "doctor"):
|
|
635
848
|
targets = scope(args, home, read=True)
|
|
636
|
-
rows = store.status(targets)
|
|
849
|
+
rows = unmanaged_projections(store, home, store.status(targets))
|
|
637
850
|
pending = [t["id"] for t in store.pending()]
|
|
638
851
|
global_view = not (getattr(args, "project", None) or getattr(args, "dest", None))
|
|
639
852
|
names = [n.strip() for n in getattr(args, "agents", "").split(",")] if getattr(args, "agents", None) else list(guide.MECHANISMS)
|
|
640
853
|
guides = guide.status(store, home, names) if global_view else []
|
|
641
854
|
try:
|
|
642
|
-
|
|
855
|
+
# Only doctor asks the CLIs (codex app-server writes under ~/.codex); status stays read-only.
|
|
856
|
+
ui = integrations.status(store, home, probe=args.command == "doctor") if global_view else []
|
|
643
857
|
except CrowError as exc:
|
|
644
858
|
ui = [{"integration": "*", "status": "unsafe", "note": str(exc)}]
|
|
645
859
|
result = {"installations": rows, "guides": guides, "integrations": ui, "pending_transactions": pending,
|
|
646
860
|
"mode": hook.read_mode(home), "activation_verified": False,
|
|
647
861
|
"reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
|
|
862
|
+
result["groups"] = group_coverage(rows)
|
|
648
863
|
try:
|
|
649
864
|
result["autoupdate"] = autoupdate.status(store, home) if global_view else None
|
|
650
865
|
except CrowError as exc:
|
|
651
866
|
result["autoupdate"] = {"enabled": None, "healthy": False, "error": exc.code}
|
|
867
|
+
hooks = [r for r in ui_rows(ui) if r.get("status") == "installed" and isinstance(r.get("enabled"), dict)]
|
|
868
|
+
result["hooks"] = [{"integration": r["integration"], "agents": r.get("agents"), "state": r["enabled"].get("state"),
|
|
869
|
+
"fix": r["enabled"].get("fix")} for r in hooks]
|
|
652
870
|
if args.command == "doctor":
|
|
653
871
|
result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe", "external_link") for r in rows + guides)
|
|
654
|
-
and (result["autoupdate"] or {}).get("healthy", True) is not False
|
|
872
|
+
and (result["autoupdate"] or {}).get("healthy", True) is not False
|
|
873
|
+
and all(h["state"] not in machine.BAD_HOOKS for h in result["hooks"]),
|
|
655
874
|
agents=inventory(home))
|
|
656
875
|
return result
|
|
657
876
|
if args.command in ("rollback", "recover"):
|
|
@@ -698,18 +917,20 @@ def run(args):
|
|
|
698
917
|
return {"revision": source.revision, "skill": source.catalog[args.name],
|
|
699
918
|
"relationships": relationship_view(source, args.name, store, targets)}
|
|
700
919
|
targets = scope(args, home, interactive)
|
|
701
|
-
names = args.skills
|
|
702
|
-
|
|
703
|
-
|
|
920
|
+
names = list(args.skills)
|
|
921
|
+
group = getattr(args, "group", None)
|
|
922
|
+
if (names or group) and args.all:
|
|
923
|
+
raise CrowError("usage", "Use nomes/--group ou --all, não ambos.")
|
|
924
|
+
selection = None
|
|
925
|
+
if group is not None:
|
|
926
|
+
selection = groups_mod.parse(group)
|
|
927
|
+
names = list(dict.fromkeys(names + groups_mod.resolve(source, group)))
|
|
704
928
|
if args.command == "install" and not names and not args.all:
|
|
705
929
|
if not interactive:
|
|
706
|
-
raise CrowError("usage", "Escolha nomes
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
names = [n.strip() for n in names if n.strip()]
|
|
711
|
-
if not names:
|
|
712
|
-
raise CrowError("usage", "Nenhuma skill selecionada.")
|
|
930
|
+
raise CrowError("usage", "Escolha as skills: nomes, --group ou --all (ex.: crow-skills install --group essencial "
|
|
931
|
+
"--agents claude --global --yes; grupos: crow-skills groups).")
|
|
932
|
+
names, selection = pick(source)
|
|
933
|
+
selection = groups_mod.parse(selection) if selection else None
|
|
713
934
|
if args.command == "uninstall" and not names and not args.all:
|
|
714
935
|
raise CrowError("usage", "Uninstall exige nomes explícitos ou --all.")
|
|
715
936
|
if args.command == "install" and args.all:
|
|
@@ -726,6 +947,8 @@ def run(args):
|
|
|
726
947
|
ui = integrations_preview(args, home, store, plan)
|
|
727
948
|
summary = {"changes": plan["changes"], "revision": source.revision if source else None,
|
|
728
949
|
"relationships": {n: source.related(n) for n in names} if source else {},
|
|
950
|
+
**({"groups": selection, "suggested_handoffs": groups_mod.suggested_handoffs(source, names)}
|
|
951
|
+
if selection and not getattr(args, "with_handoffs", False) else {}),
|
|
729
952
|
"guides": guides, "integrations": ui, "handoffs_are_optional": True, "activation_verified": False}
|
|
730
953
|
notice = uniflow_notice(home, store, targets, plan) if args.command != "uninstall" else None
|
|
731
954
|
if notice is not None:
|
|
@@ -738,7 +961,7 @@ def run(args):
|
|
|
738
961
|
if not ui.get("disabled"):
|
|
739
962
|
# Best effort after the skills/guide transactions; a refusal is reported, never undoes them.
|
|
740
963
|
try:
|
|
741
|
-
summary["integrations"] = integrations.sync(store, home, integration_agents(args, home, store))
|
|
964
|
+
summary["integrations"] = integrations.sync(store, home, integration_agents(args, home, store), trust=trusting(args))
|
|
742
965
|
except (CrowError, OSError) as exc:
|
|
743
966
|
summary["integrations"] = {"changes": [], "skipped": [{"integration": "*", "reason": "Integrações não aplicadas (" +
|
|
744
967
|
(str(exc) or "falha de gravação") + "); skills e guia mantidos."}]}
|
|
@@ -748,10 +971,84 @@ def run(args):
|
|
|
748
971
|
"Operação de arquivos concluída; carregamento/ativação devem ser conferidos no host.")}
|
|
749
972
|
|
|
750
973
|
|
|
751
|
-
def
|
|
752
|
-
|
|
753
|
-
|
|
974
|
+
def ui_rows(rows):
|
|
975
|
+
return [r for r in rows if isinstance(r, dict)]
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def public_data(data):
|
|
979
|
+
return {k: v for k, v in data.items() if not k.startswith("_")} if isinstance(data, dict) else data
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def render_status(data, doctor=False, stream=None):
|
|
983
|
+
stream = stream or sys.stdout
|
|
984
|
+
mode = (data.get("mode") or {}).get("mode", "?")
|
|
985
|
+
auto = data.get("autoupdate") or {}
|
|
986
|
+
lines = [ui.paint(hook.indicator(mode, ui.unicode_on(stream)), "bold", stream) + " · autoupdate " +
|
|
987
|
+
("ligado" if auto.get("enabled") else "desligado") + " · crow-skills " + __version__, ""]
|
|
988
|
+
by_target = {}
|
|
989
|
+
for row in data["installations"]:
|
|
990
|
+
item = by_target.setdefault(row["target"], {"agents": row["agents"], "ok": 0, "via": 0, "bad": [], "kept": []})
|
|
991
|
+
if row.get("projection") == "uniflow_link_unmanaged":
|
|
992
|
+
item["kept"].append(row["name"] + " link do Uniflow não gerenciado (crow-skills setup --agents uniflow --replace --yes)")
|
|
993
|
+
if row["status"] == "uniflow_link":
|
|
994
|
+
item["ok"] += 1
|
|
995
|
+
item["via"] += 1 # projected by Uniflow from its store: installed for this CLI
|
|
996
|
+
elif row["status"] in ("installed", "disabled"):
|
|
997
|
+
item["ok"] += 1
|
|
998
|
+
elif row["status"] in ("modified", "missing", "unsafe", "external_link"):
|
|
999
|
+
item["bad"].append((row["name"] or "raiz") + " " + row["status"])
|
|
1000
|
+
rows = [[ui.mark("fail" if v["bad"] else ("warn" if v["kept"] else "ok"), stream), ",".join(v["agents"]),
|
|
1001
|
+
str(v["ok"]) + " skills" + (" (" + str(v["via"]) + " via uniflow)" if v["via"] else ""),
|
|
1002
|
+
"; ".join(v["bad"] + v["kept"])] for v in by_target.values() if v["ok"] or v["bad"] or v["kept"]]
|
|
1003
|
+
if rows:
|
|
1004
|
+
lines += ui.table(rows, ["", "agente", "instaladas", "problemas"], stream)
|
|
1005
|
+
cover = [[",".join(c["agents"]), ", ".join(g["group"] + (" ✓" if g["state"] == "complete" and ui.unicode_on(stream) else
|
|
1006
|
+
" completo" if g["state"] == "complete" else
|
|
1007
|
+
" " + str(g["installed"]) + "/" + str(g["total"]))
|
|
1008
|
+
for g in c["groups"])] for c in data.get("groups") or [] if c["groups"]]
|
|
1009
|
+
if cover:
|
|
1010
|
+
lines += ["", " Grupos (completos ✓ ou parciais n/total):" if ui.unicode_on(stream) else " Grupos:"]
|
|
1011
|
+
lines += ui.table(cover, stream=stream, indent=" ")
|
|
1012
|
+
else:
|
|
1013
|
+
lines.append(" Nenhuma skill instalada; comece com: crow-skills setup --yes")
|
|
1014
|
+
guides = [g for g in data.get("guides", []) if g.get("status") != "absent"]
|
|
1015
|
+
if guides:
|
|
1016
|
+
lines.append("")
|
|
1017
|
+
lines += ui.table([[ui.mark("ok" if g["status"] == "installed" else ("warn" if g["status"] == "outdated" else "fail"), stream),
|
|
1018
|
+
"guia " + g["agent"], ui.word(g["status"]), ui.clean(g.get("note") or "")] for g in guides], stream=stream)
|
|
1019
|
+
hooks = data.get("hooks") or []
|
|
1020
|
+
if hooks:
|
|
1021
|
+
lines.append("")
|
|
1022
|
+
lines += ui.table([[ui.mark("ok" if h["state"] in ("trusted", "not_required") else
|
|
1023
|
+
("fail" if h["state"] in machine.BAD_HOOKS else "warn"), stream),
|
|
1024
|
+
h["integration"], ui.word(h["state"]),
|
|
1025
|
+
ui.clean(h.get("fix") or "")] for h in hooks], stream=stream)
|
|
1026
|
+
if data.get("pending_transactions"):
|
|
1027
|
+
lines += ["", ui.mark("fail", stream) + " Transação pendente: crow-skills recover --yes"]
|
|
1028
|
+
if doctor:
|
|
1029
|
+
lines += ["", (ui.mark("ok", stream) + " healthy") if data.get("healthy") else
|
|
1030
|
+
(ui.mark("fail", stream) + " não saudável; detalhes: crow-skills doctor --json")]
|
|
1031
|
+
return "\n".join(lines)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def render_help(data, stream=None):
|
|
1035
|
+
if data.get("topic") is None:
|
|
1036
|
+
return ui.top_help(__version__, stream, data.get("_status_line"))
|
|
1037
|
+
return data["text"].rstrip("\n")
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def emit(command, data, as_json):
|
|
1041
|
+
envelope = {"schema_version": 1, "ok": True, "command": command, "data": public_data(data)}
|
|
1042
|
+
if as_json:
|
|
754
1043
|
print(json.dumps(envelope, ensure_ascii=False, separators=(",", ":")))
|
|
1044
|
+
elif command == "help":
|
|
1045
|
+
print(render_help(data))
|
|
1046
|
+
elif command == "setup":
|
|
1047
|
+
print(machine.render_setup(data))
|
|
1048
|
+
elif command == "upgrade" or (command == "autoupdate" and isinstance(data, dict) and "summary" in data):
|
|
1049
|
+
print(machine.render_upgrade(data))
|
|
1050
|
+
elif command in ("status", "doctor"):
|
|
1051
|
+
print(render_status(data, doctor=command == "doctor"))
|
|
755
1052
|
elif command == "progress":
|
|
756
1053
|
if data is not None:
|
|
757
1054
|
print(data["text"] or "crow: nenhuma execução registrada neste diretório.")
|
|
@@ -771,6 +1068,13 @@ def emit(command, data, machine):
|
|
|
771
1068
|
elif command == "events":
|
|
772
1069
|
if data is not None:
|
|
773
1070
|
print(data["text"] if "text" in data else "\n".join(json.dumps(e, ensure_ascii=False) for e in data["events"]))
|
|
1071
|
+
elif command == "groups":
|
|
1072
|
+
lines = []
|
|
1073
|
+
for g in data["groups"]:
|
|
1074
|
+
lines.append(ui.paint(g["name"], "cyan") + " (" + str(len(g["skills"])) + ") — " + terminal(g["description"]))
|
|
1075
|
+
lines.append(" " + ", ".join(g["skills"]))
|
|
1076
|
+
lines += ["", "crow-behaviour entra com qualquer grupo. Ex.: crow-skills install --group essencial --agents claude --global --yes"]
|
|
1077
|
+
print("\n".join(lines))
|
|
774
1078
|
elif command == "list":
|
|
775
1079
|
print("Fonte: " + data["revision"])
|
|
776
1080
|
for item in data["skills"]:
|
|
@@ -781,31 +1085,41 @@ def emit(command, data, machine):
|
|
|
781
1085
|
|
|
782
1086
|
def main(argv=None):
|
|
783
1087
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
784
|
-
|
|
1088
|
+
as_json = "--json" in argv
|
|
785
1089
|
command = None
|
|
786
1090
|
try:
|
|
787
1091
|
args = parser().parse_args(argv)
|
|
788
1092
|
command = args.command or ("version" if args.version else None)
|
|
1093
|
+
if command == "init":
|
|
1094
|
+
command = "setup"
|
|
1095
|
+
if command is None or getattr(args, "show_help", False):
|
|
1096
|
+
command = "help"
|
|
789
1097
|
result = run(args)
|
|
790
|
-
emit(command, result,
|
|
1098
|
+
emit(command, result, as_json)
|
|
1099
|
+
if command in ("setup", "upgrade") and isinstance(result, dict) and result.get("healthy") is False:
|
|
1100
|
+
return 1
|
|
791
1101
|
return 0
|
|
1102
|
+
except SystemExit as exc: # <command> --help printed by argparse
|
|
1103
|
+
return exc.code if isinstance(exc.code, int) else 0
|
|
792
1104
|
except CrowError as exc:
|
|
793
|
-
if
|
|
1105
|
+
if as_json:
|
|
794
1106
|
print(json.dumps({"schema_version": 1, "ok": False, "command": command,
|
|
795
1107
|
"error": {"code": exc.code, "message": str(exc)}}, ensure_ascii=False))
|
|
796
1108
|
else:
|
|
797
|
-
print(
|
|
1109
|
+
print(ui.error_text(exc.code, str(exc), command, sys.stderr), file=sys.stderr)
|
|
798
1110
|
return 2 if exc.code == "usage" else 1
|
|
799
1111
|
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
800
1112
|
# No traceback or subprocess stderr in machine output; credentials stay external.
|
|
801
|
-
if
|
|
1113
|
+
if as_json:
|
|
802
1114
|
print(json.dumps({"schema_version": 1, "ok": False, "command": command,
|
|
803
1115
|
"error": {"code": "io_error", "message": "Falha de leitura/gravação ou estado inválido; nenhuma conclusão de sucesso."}}))
|
|
804
1116
|
else:
|
|
805
|
-
print("Falha de leitura/gravação ou estado inválido.
|
|
1117
|
+
print(ui.error_text("io_error", "Falha de leitura/gravação ou estado inválido.", command, sys.stderr) +
|
|
1118
|
+
"\n Para corrigir: confira permissões e transações pendentes (crow-skills doctor; crow-skills recover --yes)",
|
|
1119
|
+
file=sys.stderr)
|
|
806
1120
|
return 1
|
|
807
1121
|
except KeyboardInterrupt:
|
|
808
|
-
if
|
|
1122
|
+
if as_json:
|
|
809
1123
|
print(json.dumps({"schema_version": 1, "ok": False, "command": command,
|
|
810
1124
|
"error": {"code": "interrupted", "message": "Operação interrompida; confira status/recover."}}))
|
|
811
1125
|
return 130
|