@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.
@@ -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
- p = Parser(prog="crow-skills", parents=[common], description="Instale pacotes crow completos, por agente e revisão, sem executar scripts de skills.")
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
- for name in ("list", "agents", "status", "doctor"):
51
- sub.add_parser(name, parents=[common])
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.add_parser(name, parents=[common])
149
+ child = command_parser(sub, name, [common])
54
150
  child.add_argument("name")
55
151
  for name in ("install", "update", "uninstall"):
56
- child = sub.add_parser(name, parents=[common])
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
- child = sub.add_parser("guide", parents=[common], help="Instruções gerenciadas para o agente usar as skills instaladas")
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.add_parser("integrations", parents=[common], help="Status line/tmux que mostram a execução crow-behaviour ativa")
171
+ child = command_parser(sub, "integrations", [common])
71
172
  child.add_argument("action", choices=("install", "status", "uninstall"))
72
- child = sub.add_parser("progress", parents=[common], help="Painel de progresso da execução crow-behaviour (ledger, barras, tokens)")
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.add_parser("mode", parents=[common], help="Liga/desliga o modo crow (crow-behaviour) em todas as sessões e CLIs")
178
+ child = command_parser(sub, "mode", [common])
77
179
  child.add_argument("action", nargs="?", default="status", choices=("on", "off", "status", "toggle"))
78
- child = sub.add_parser("events", parents=[common], help="Barramento de eventos dos hooks (crow-event/1) para terminais")
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.add_parser("broadcast", parents=[common],
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.add_parser("resources", parents=[common],
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.add_parser("autoupdate", parents=[common], help="Atualização diária e no login do crow-skills e das skills gerenciadas")
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.add_parser(name, parents=[common])
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
- authorize(args, interactive, {"autoupdate": "run", "self_upgrade": not (args.skills_only or args.no_self)})
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
- return {"install": autoupdate.detect(), "applied": False,
528
- "would": ["atualizar o crow-skills" if not (args.skills_only or args.no_self) else None,
529
- "update --global das skills gerenciadas, por agente"]}
530
- return autoupdate.run(store, home, self_step=not (args.skills_only or args.no_self))
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
- agents = sorted({a for n in native.values() for a in n["agents"]})
593
- managed = sorted({s["name"] for n in native.values() for s in n["skills"] if s["status"] != "unmanaged"})
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
- "suggested_command": ("crow-skills uninstall " + " ".join(managed) + " --agents " + ",".join(agents) + " --global")
599
- if managed else None}
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 getattr(args, "fetch", False) and (getattr(args, "dry_run", False) or args.command not in ("install", "update", "list", "info", "related", "guide")):
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
- ui = integrations.status(store, home) if global_view else []
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
- if names and args.all:
703
- raise CrowError("usage", "Use nomes ou --all, não ambos.")
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 de skills ou --all.")
707
- for name, item in source.catalog.items():
708
- print(terminal(name + " — " + item.get("title", name)), file=sys.stderr)
709
- names = prompt("Skills separadas por vírgula:").split(",")
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 emit(command, data, machine):
752
- envelope = {"schema_version": 1, "ok": True, "command": command, "data": data}
753
- if machine:
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
- machine = "--json" in argv
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, machine)
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 machine:
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(f"Erro [{exc.code}]: {exc}", file=sys.stderr)
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 machine:
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. Confira permissões, origem e transações pendentes.", file=sys.stderr)
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 machine:
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