@andespindola/crow-skills 0.10.0 → 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,17 +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
- child = sub.add_parser("autoupdate", parents=[common], help="Atualização diária e no login do crow-skills e das skills gerenciadas")
199
+ child.add_argument("--exec-prefix", action="store_true",
200
+ help="Só imprimir o prefixo que roda um job pesado em prioridade baixa neste sistema (taskpolicy/nice/ionice/start /low)")
201
+ child = command_parser(sub, "autoupdate", [common])
100
202
  child.add_argument("action", choices=("on", "off", "status", "run"))
101
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)")
102
204
  child.add_argument("--skills-only", action="store_true", help="run: só atualizar as skills, sem atualizar o crow-skills")
103
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)
104
207
  for name in ("rollback", "recover"):
105
- child = sub.add_parser(name, parents=[common])
208
+ child = command_parser(sub, name, [common])
106
209
  child.add_argument("--transaction", required=name == "rollback")
210
+ p.common = common
211
+ p.commands = sub.choices
107
212
  return p
108
213
 
109
214
 
@@ -299,7 +404,12 @@ def integrations_preview(args, home, store, plan):
299
404
  if getattr(args, "project", None) or getattr(args, "dest", None):
300
405
  return {"changes": [], "skipped": [{"integration": "*", "reason": "Integrações de interface só existem no escopo do usuário (--global)."}],
301
406
  "disabled": True}
302
- 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)
303
413
 
304
414
 
305
415
  def integration_agents(args, home, store, plan=None, selected=None):
@@ -316,7 +426,7 @@ def integration_agents(args, home, store, plan=None, selected=None):
316
426
 
317
427
  def integrations_command(args, home, store, interactive):
318
428
  if args.action == "status":
319
- return {"integrations": integrations.status(store, home)}
429
+ return {"integrations": integrations.status(store, home, probe=False)}
320
430
  if not getattr(args, "global_scope", False):
321
431
  raise CrowError("usage", "integrations " + args.action + " exige --global explícito (único escopo suportado).")
322
432
  wanted = []
@@ -325,12 +435,12 @@ def integrations_command(args, home, store, interactive):
325
435
  if args.action == "install" and not wanted:
326
436
  raise CrowError("guide_requires_skill", "crow-behaviour não está instalada para nenhum agente; instale antes: "
327
437
  "crow-skills install crow-behaviour --agents claude --global --yes")
328
- preview = integrations.sync(store, home, wanted, dry_run=True)
438
+ preview = integrations.sync(store, home, wanted, dry_run=True, trust=trusting(args))
329
439
  if preview["changes"]:
330
440
  authorize(args, interactive, preview)
331
441
  if getattr(args, "dry_run", False):
332
442
  return {**preview, "applied": False}
333
- return {**integrations.sync(store, home, wanted), "applied": True}
443
+ return {**integrations.sync(store, home, wanted, trust=trusting(args)), "applied": True}
334
444
 
335
445
 
336
446
  def progress_command(args, home):
@@ -352,7 +462,7 @@ def mode_command(args, home):
352
462
  wanted = args.action if args.action != "toggle" else ("off" if current["mode"] == "on" else "on")
353
463
  current = hook.write_mode(home, wanted, "cli")
354
464
  try:
355
- 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)
356
466
  if r["status"] == "installed" and r["integration"].endswith(("-hooks", "-statusline", "-command", "tmux-status"))]
357
467
  except CrowError:
358
468
  installed = []
@@ -520,12 +630,13 @@ def autoupdate_command(args, home, store, interactive):
520
630
  if args.action == "status":
521
631
  return autoupdate.status(store, home)
522
632
  if args.action == "run":
523
- 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})
524
635
  if getattr(args, "dry_run", False):
525
- return {"install": autoupdate.detect(), "applied": False,
526
- "would": ["atualizar o crow-skills" if not (args.skills_only or args.no_self) else None,
527
- "update --global das skills gerenciadas, por agente"]}
528
- 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))
529
640
  if args.action == "on":
530
641
  preview = autoupdate.turn_on(store, home, dry_run=True, git_pull=args.git_pull)
531
642
  if any(c["action"] != "unchanged" for c in preview["changes"]):
@@ -587,26 +698,130 @@ def uniflow_notice(home, store, targets, plan):
587
698
  native[row["target"]]["skills"].append({"name": row["name"], "status": row["status"]})
588
699
  if not native:
589
700
  return {"native_duplicates": [], "uninspected": uninspected}
590
- agents = sorted({a for n in native.values() for a in n["agents"]})
591
- 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()]
592
711
  return {"native_duplicates": list(native.values()), "uninspected": uninspected,
593
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 "
594
713
  "store. effect=both: o Uniflow liga o store em ~/.codex/skills e o Codex também lê ~/.agents/skills, então vê as "
595
714
  "duas cópias. Nada foi removido. Remover a cópia nativa de crow-behaviour remove também o guia daquele agente.",
596
- "suggested_command": ("crow-skills uninstall " + " ".join(managed) + " --agents " + ",".join(agents) + " --global")
597
- 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]}
598
803
 
599
804
 
600
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)
601
810
  if args.version:
602
811
  return {"version": __version__}
603
- if not args.command:
604
- raise CrowError("usage", "Escolha um comando; use crow-skills --help.")
605
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))
606
815
  store = Store(home)
607
816
  interactive = sys.stdin.isatty() and not getattr(args, "non_interactive", False) and not getattr(args, "json", False)
608
- 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")):
609
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)
610
825
  if args.command == "agents":
611
826
  return {"agents": inventory(home), "note": "Detecção por PATH/diretórios não comprova carregamento da skill."}
612
827
  if args.command == "guide":
@@ -622,6 +837,8 @@ def run(args):
622
837
  if args.command == "broadcast":
623
838
  return broadcast_command(args, home)
624
839
  if args.command == "resources":
840
+ if args.exec_prefix:
841
+ return resources.exec_prefix_data()
625
842
  data = resources.probe(home, processes=not args.no_processes)
626
843
  data["text"] = resources.render(data)
627
844
  return data
@@ -629,25 +846,31 @@ def run(args):
629
846
  return autoupdate_command(args, home, store, interactive)
630
847
  if args.command in ("status", "doctor"):
631
848
  targets = scope(args, home, read=True)
632
- rows = store.status(targets)
849
+ rows = unmanaged_projections(store, home, store.status(targets))
633
850
  pending = [t["id"] for t in store.pending()]
634
851
  global_view = not (getattr(args, "project", None) or getattr(args, "dest", None))
635
852
  names = [n.strip() for n in getattr(args, "agents", "").split(",")] if getattr(args, "agents", None) else list(guide.MECHANISMS)
636
853
  guides = guide.status(store, home, names) if global_view else []
637
854
  try:
638
- 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 []
639
857
  except CrowError as exc:
640
858
  ui = [{"integration": "*", "status": "unsafe", "note": str(exc)}]
641
859
  result = {"installations": rows, "guides": guides, "integrations": ui, "pending_transactions": pending,
642
860
  "mode": hook.read_mode(home), "activation_verified": False,
643
861
  "reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
862
+ result["groups"] = group_coverage(rows)
644
863
  try:
645
864
  result["autoupdate"] = autoupdate.status(store, home) if global_view else None
646
865
  except CrowError as exc:
647
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]
648
870
  if args.command == "doctor":
649
871
  result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe", "external_link") for r in rows + guides)
650
- 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"]),
651
874
  agents=inventory(home))
652
875
  return result
653
876
  if args.command in ("rollback", "recover"):
@@ -694,18 +917,20 @@ def run(args):
694
917
  return {"revision": source.revision, "skill": source.catalog[args.name],
695
918
  "relationships": relationship_view(source, args.name, store, targets)}
696
919
  targets = scope(args, home, interactive)
697
- names = args.skills
698
- if names and args.all:
699
- 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)))
700
928
  if args.command == "install" and not names and not args.all:
701
929
  if not interactive:
702
- raise CrowError("usage", "Escolha nomes de skills ou --all.")
703
- for name, item in source.catalog.items():
704
- print(terminal(name + " — " + item.get("title", name)), file=sys.stderr)
705
- names = prompt("Skills separadas por vírgula:").split(",")
706
- names = [n.strip() for n in names if n.strip()]
707
- if not names:
708
- 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
709
934
  if args.command == "uninstall" and not names and not args.all:
710
935
  raise CrowError("usage", "Uninstall exige nomes explícitos ou --all.")
711
936
  if args.command == "install" and args.all:
@@ -722,6 +947,8 @@ def run(args):
722
947
  ui = integrations_preview(args, home, store, plan)
723
948
  summary = {"changes": plan["changes"], "revision": source.revision if source else None,
724
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 {}),
725
952
  "guides": guides, "integrations": ui, "handoffs_are_optional": True, "activation_verified": False}
726
953
  notice = uniflow_notice(home, store, targets, plan) if args.command != "uninstall" else None
727
954
  if notice is not None:
@@ -734,7 +961,7 @@ def run(args):
734
961
  if not ui.get("disabled"):
735
962
  # Best effort after the skills/guide transactions; a refusal is reported, never undoes them.
736
963
  try:
737
- 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))
738
965
  except (CrowError, OSError) as exc:
739
966
  summary["integrations"] = {"changes": [], "skipped": [{"integration": "*", "reason": "Integrações não aplicadas (" +
740
967
  (str(exc) or "falha de gravação") + "); skills e guia mantidos."}]}
@@ -744,10 +971,84 @@ def run(args):
744
971
  "Operação de arquivos concluída; carregamento/ativação devem ser conferidos no host.")}
745
972
 
746
973
 
747
- def emit(command, data, machine):
748
- envelope = {"schema_version": 1, "ok": True, "command": command, "data": data}
749
- 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:
750
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"))
751
1052
  elif command == "progress":
752
1053
  if data is not None:
753
1054
  print(data["text"] or "crow: nenhuma execução registrada neste diretório.")
@@ -767,6 +1068,13 @@ def emit(command, data, machine):
767
1068
  elif command == "events":
768
1069
  if data is not None:
769
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))
770
1078
  elif command == "list":
771
1079
  print("Fonte: " + data["revision"])
772
1080
  for item in data["skills"]:
@@ -777,31 +1085,41 @@ def emit(command, data, machine):
777
1085
 
778
1086
  def main(argv=None):
779
1087
  argv = list(sys.argv[1:] if argv is None else argv)
780
- machine = "--json" in argv
1088
+ as_json = "--json" in argv
781
1089
  command = None
782
1090
  try:
783
1091
  args = parser().parse_args(argv)
784
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"
785
1097
  result = run(args)
786
- 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
787
1101
  return 0
1102
+ except SystemExit as exc: # <command> --help printed by argparse
1103
+ return exc.code if isinstance(exc.code, int) else 0
788
1104
  except CrowError as exc:
789
- if machine:
1105
+ if as_json:
790
1106
  print(json.dumps({"schema_version": 1, "ok": False, "command": command,
791
1107
  "error": {"code": exc.code, "message": str(exc)}}, ensure_ascii=False))
792
1108
  else:
793
- print(f"Erro [{exc.code}]: {exc}", file=sys.stderr)
1109
+ print(ui.error_text(exc.code, str(exc), command, sys.stderr), file=sys.stderr)
794
1110
  return 2 if exc.code == "usage" else 1
795
1111
  except (OSError, ValueError, KeyError, TypeError) as exc:
796
1112
  # No traceback or subprocess stderr in machine output; credentials stay external.
797
- if machine:
1113
+ if as_json:
798
1114
  print(json.dumps({"schema_version": 1, "ok": False, "command": command,
799
1115
  "error": {"code": "io_error", "message": "Falha de leitura/gravação ou estado inválido; nenhuma conclusão de sucesso."}}))
800
1116
  else:
801
- 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)
802
1120
  return 1
803
1121
  except KeyboardInterrupt:
804
- if machine:
1122
+ if as_json:
805
1123
  print(json.dumps({"schema_version": 1, "ok": False, "command": command,
806
1124
  "error": {"code": "interrupted", "message": "Operação interrompida; confira status/recover."}}))
807
1125
  return 130