@andespindola/crow-skills 0.1.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 +62 -0
- package/bin/npm-crow-skills.cjs +20 -0
- package/catalog.json +398 -0
- package/crow_skills/__init__.py +3 -0
- package/crow_skills/__main__.py +3 -0
- package/crow_skills/agents.py +45 -0
- package/crow_skills/cli.py +241 -0
- package/crow_skills/npm_entry.py +14 -0
- package/crow_skills/source.py +256 -0
- package/crow_skills/storage.py +396 -0
- package/docs/cli-security.md +33 -0
- package/docs/cli.md +138 -0
- package/package.json +35 -0
- package/release.json +607 -0
- package/skills/crow-accessibility/SKILL.md +33 -0
- package/skills/crow-agent-security/SKILL.md +33 -0
- package/skills/crow-api-test/SKILL.md +34 -0
- package/skills/crow-architecture/SKILL.md +33 -0
- package/skills/crow-backend/SKILL.md +33 -0
- package/skills/crow-behaviour/SKILL.md +70 -0
- package/skills/crow-behaviour/agents/openai.yaml +4 -0
- package/skills/crow-behaviour/references/orchestration.md +64 -0
- package/skills/crow-behaviour/references/quality-patterns.md +56 -0
- package/skills/crow-browser/SKILL.md +33 -0
- package/skills/crow-debug/SKILL.md +33 -0
- package/skills/crow-design/SKILL.md +33 -0
- package/skills/crow-discovery/SKILL.md +33 -0
- package/skills/crow-documents/SKILL.md +33 -0
- package/skills/crow-domain/SKILL.md +33 -0
- package/skills/crow-frontend/SKILL.md +33 -0
- package/skills/crow-growth/SKILL.md +33 -0
- package/skills/crow-infra/SKILL.md +38 -0
- package/skills/crow-infra/references/kubernetes.md +3 -0
- package/skills/crow-interface/NOTICE.md +20 -0
- package/skills/crow-interface/SKILL.md +55 -0
- package/skills/crow-interface/agents/impeccable_asset_producer.toml +43 -0
- package/skills/crow-interface/agents/impeccable_documenter.toml +28 -0
- package/skills/crow-interface/agents/impeccable_finish_reviewer.toml +42 -0
- package/skills/crow-interface/agents/impeccable_manual_edit_applier.toml +96 -0
- package/skills/crow-interface/agents/openai.yaml +3 -0
- package/skills/crow-interface/licenses/Apache-2.0.txt +191 -0
- package/skills/crow-interface/licenses/Impeccable-NOTICE.md +11 -0
- package/skills/crow-interface/licenses/modern-screenshot-MIT.txt +9 -0
- package/skills/crow-interface/licenses/platform-design-MIT.txt +21 -0
- package/skills/crow-interface/reference/adapt.md +320 -0
- package/skills/crow-interface/reference/adapt.native.md +60 -0
- package/skills/crow-interface/reference/android.md +48 -0
- package/skills/crow-interface/reference/animate.md +91 -0
- package/skills/crow-interface/reference/audit.md +139 -0
- package/skills/crow-interface/reference/audit.native.md +141 -0
- package/skills/crow-interface/reference/bolder.md +35 -0
- package/skills/crow-interface/reference/clarify.md +96 -0
- package/skills/crow-interface/reference/colorize.md +88 -0
- package/skills/crow-interface/reference/craft-floor.md +52 -0
- package/skills/crow-interface/reference/craft.md +7 -0
- package/skills/crow-interface/reference/critique.md +828 -0
- package/skills/crow-interface/reference/degraded/asset-producer.md +5 -0
- package/skills/crow-interface/reference/degraded/documenter.md +5 -0
- package/skills/crow-interface/reference/degraded/finish-reviewer.md +5 -0
- package/skills/crow-interface/reference/degraded/manual-edit-applier.md +5 -0
- package/skills/crow-interface/reference/delight.md +72 -0
- package/skills/crow-interface/reference/distill.md +113 -0
- package/skills/crow-interface/reference/doctor.md +56 -0
- package/skills/crow-interface/reference/document.md +418 -0
- package/skills/crow-interface/reference/extract.md +70 -0
- package/skills/crow-interface/reference/generate.md +103 -0
- package/skills/crow-interface/reference/harden.md +347 -0
- package/skills/crow-interface/reference/hooks.md +113 -0
- package/skills/crow-interface/reference/init.md +133 -0
- package/skills/crow-interface/reference/ios.md +53 -0
- package/skills/crow-interface/reference/layout.md +90 -0
- package/skills/crow-interface/reference/live-setup.md +106 -0
- package/skills/crow-interface/reference/live.md +329 -0
- package/skills/crow-interface/reference/new-work.md +149 -0
- package/skills/crow-interface/reference/onboard.md +236 -0
- package/skills/crow-interface/reference/operate.md +63 -0
- package/skills/crow-interface/reference/optimize.md +260 -0
- package/skills/crow-interface/reference/overdrive.md +129 -0
- package/skills/crow-interface/reference/polish.md +107 -0
- package/skills/crow-interface/reference/quieter.md +101 -0
- package/skills/crow-interface/reference/roles/asset-producer.md +38 -0
- package/skills/crow-interface/reference/roles/documenter.md +23 -0
- package/skills/crow-interface/reference/roles/finish-reviewer.md +37 -0
- package/skills/crow-interface/reference/roles/manual-edit-applier.md +91 -0
- package/skills/crow-interface/reference/routing.md +26 -0
- package/skills/crow-interface/reference/runtime.md +30 -0
- package/skills/crow-interface/reference/shape.md +61 -0
- package/skills/crow-interface/reference/typeset.md +86 -0
- package/skills/crow-interface/reference/visualize.md +48 -0
- package/skills/crow-interface/reference/workflow.md +53 -0
- package/skills/crow-interface/scripts/VERSION +1 -0
- package/skills/crow-interface/scripts/command-metadata.json +98 -0
- package/skills/crow-interface/scripts/data/font-index-failures.json +121 -0
- package/skills/crow-interface/scripts/data/font-index.json +1 -0
- package/skills/crow-interface/scripts/engine-manifest.json +13 -0
- package/skills/crow-interface/scripts/impeccable +106 -0
- package/skills/crow-interface/scripts/impeccable.cmd +3 -0
- package/skills/crow-interface/scripts/live-browser-dom.js +167 -0
- package/skills/crow-interface/scripts/live-browser-ignores.js +242 -0
- package/skills/crow-interface/scripts/live-browser-session.js +144 -0
- package/skills/crow-interface/scripts/live-browser.js +13510 -0
- package/skills/crow-interface/scripts/modern-screenshot.umd.js +14 -0
- package/skills/crow-media/SKILL.md +33 -0
- package/skills/crow-native-mobile/SKILL.md +33 -0
- package/skills/crow-observe/SKILL.md +33 -0
- package/skills/crow-performance/SKILL.md +38 -0
- package/skills/crow-performance/references/measurement.md +3 -0
- package/skills/crow-platform/SKILL.md +33 -0
- package/skills/crow-postgres/SKILL.md +33 -0
- package/skills/crow-product-leadership/SKILL.md +33 -0
- package/skills/crow-qa/SKILL.md +34 -0
- package/skills/crow-release/SKILL.md +35 -0
- package/skills/crow-reverse/SKILL.md +33 -0
- package/skills/crow-secrets/SKILL.md +33 -0
- package/skills/crow-security-review/SKILL.md +33 -0
- package/skills/crow-security-scan/SKILL.md +33 -0
- package/skills/crow-security-test/SKILL.md +33 -0
- package/skills/crow-seo/SKILL.md +33 -0
- package/skills/crow-skill-author/SKILL.md +34 -0
- package/skills/crow-spec/SKILL.md +33 -0
- package/skills/crow-threat-model/SKILL.md +33 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Human and agent CLI. Noninteractive mutations require explicit --yes."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .agents import AGENTS, destinations, inventory
|
|
9
|
+
from .source import CrowError, Source, valid_name
|
|
10
|
+
from .storage import Store, checked
|
|
11
|
+
|
|
12
|
+
DEFAULT_SOURCE = Path(__file__).resolve().parents[1]
|
|
13
|
+
MUTATIONS = {"install", "update", "uninstall", "rollback", "recover"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Parser(argparse.ArgumentParser):
|
|
17
|
+
def error(self, message):
|
|
18
|
+
raise CrowError("usage", message)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parser():
|
|
22
|
+
common = Parser(add_help=False)
|
|
23
|
+
for flag, kwargs in [
|
|
24
|
+
("--source", {"help": "Clone Git existente (padrão: checkout do CLI)"}),
|
|
25
|
+
("--ref", {"help": "Commit/tag/ref da fonte; padrão HEAD, sem checkout"}),
|
|
26
|
+
("--home", {"help": "Perfil local alternativo, para ambientes isolados"}),
|
|
27
|
+
("--agents", {"help": "Agentes separados por vírgula: " + ",".join(AGENTS)}),
|
|
28
|
+
("--global", {"dest": "global_scope", "action": "store_true", "help": "Escopo do usuário"}),
|
|
29
|
+
("--project", {"help": "Diretório físico do projeto"}),
|
|
30
|
+
("--dest", {"help": "Diretório de skills explícito, para outro host"}),
|
|
31
|
+
("--json", {"action": "store_true", "help": "Envelope JSON; nunca abre prompts"}),
|
|
32
|
+
("--non-interactive", {"action": "store_true"}),
|
|
33
|
+
("--yes", {"action": "store_true", "help": "Aplicar o escopo explícito sem prompt"}),
|
|
34
|
+
("--dry-run", {"action": "store_true", "help": "Preview sem gravar estado ou instalar"}),
|
|
35
|
+
("--fetch", {"action": "store_true", "help": "Buscar origin explicitamente; não cria clone"}),
|
|
36
|
+
]:
|
|
37
|
+
common.add_argument(flag, default=argparse.SUPPRESS, **kwargs)
|
|
38
|
+
p = Parser(prog="crow-skills", parents=[common], description="Instale pacotes crow completos, por agente e revisão, sem executar scripts de skills.")
|
|
39
|
+
p.add_argument("--version", action="store_true")
|
|
40
|
+
sub = p.add_subparsers(dest="command")
|
|
41
|
+
for name in ("list", "agents", "status", "doctor"):
|
|
42
|
+
sub.add_parser(name, parents=[common])
|
|
43
|
+
for name in ("info", "related"):
|
|
44
|
+
child = sub.add_parser(name, parents=[common])
|
|
45
|
+
child.add_argument("name")
|
|
46
|
+
for name in ("install", "update", "uninstall"):
|
|
47
|
+
child = sub.add_parser(name, parents=[common])
|
|
48
|
+
child.add_argument("skills", nargs="*")
|
|
49
|
+
child.add_argument("--all", action="store_true", help="Todos do catálogo (install) ou gerenciados (update/uninstall)")
|
|
50
|
+
if name != "uninstall":
|
|
51
|
+
child.add_argument("--replace", action="store_true", help="Autorizar substituição com backup de pacote não gerenciado")
|
|
52
|
+
child.add_argument("--with-handoffs", action="store_true", help="Incluir apenas handoffs diretos opcionais do catálogo")
|
|
53
|
+
child.add_argument("--overwrite-modified", action="store_true", help="Autorizar backup e substituição/remoção de edições locais")
|
|
54
|
+
for name in ("rollback", "recover"):
|
|
55
|
+
child = sub.add_parser(name, parents=[common])
|
|
56
|
+
child.add_argument("--transaction", required=name == "rollback")
|
|
57
|
+
return p
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def prompt(text):
|
|
61
|
+
print(text, file=sys.stderr, end=" ", flush=True)
|
|
62
|
+
line = sys.stdin.readline()
|
|
63
|
+
if not line:
|
|
64
|
+
raise CrowError("cancelled", "Entrada encerrada; nenhuma instalação autorizada.")
|
|
65
|
+
return line.strip()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def terminal(text):
|
|
69
|
+
"""Do not render terminal escapes or invisible bidi controls from metadata."""
|
|
70
|
+
return "".join(c if c.isprintable() or c in "\n\t" else c.encode("unicode_escape").decode("ascii") for c in str(text))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def scope(args, home, interactive=False, read=False):
|
|
74
|
+
if sum(bool(getattr(args, k, None)) for k in ("global_scope", "project", "dest")) > 1:
|
|
75
|
+
raise CrowError("usage", "Escolha somente um de --global, --project ou --dest.")
|
|
76
|
+
agents = getattr(args, "agents", None)
|
|
77
|
+
project = getattr(args, "project", None)
|
|
78
|
+
custom = getattr(args, "dest", None)
|
|
79
|
+
if not read and not any(getattr(args, k, None) for k in ("global_scope", "project", "dest")):
|
|
80
|
+
if not interactive:
|
|
81
|
+
raise CrowError("usage", "Mutações exigem escopo explícito: --global, --project PATH ou --dest PATH.")
|
|
82
|
+
selected = prompt("Escopo: [p]rojeto atual ou [g]lobal? [p]").lower()
|
|
83
|
+
if selected not in ("", "p", "g"):
|
|
84
|
+
raise CrowError("usage", "Escopo inválido.")
|
|
85
|
+
if selected != "g":
|
|
86
|
+
project = str(Path.cwd())
|
|
87
|
+
if project:
|
|
88
|
+
project = checked(project)
|
|
89
|
+
if not project.is_dir():
|
|
90
|
+
raise CrowError("usage", "O diretório do projeto deve existir.")
|
|
91
|
+
if not agents and not custom:
|
|
92
|
+
if read:
|
|
93
|
+
agents = ",".join(AGENTS)
|
|
94
|
+
elif interactive:
|
|
95
|
+
agents = prompt("Agentes separados por vírgula (" + ",".join(AGENTS) + "):")
|
|
96
|
+
return destinations(agents, home, project, custom)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def authorize(args, interactive, summary):
|
|
100
|
+
if getattr(args, "dry_run", False) or getattr(args, "yes", False):
|
|
101
|
+
return
|
|
102
|
+
if not interactive:
|
|
103
|
+
raise CrowError("confirmation_required", "Use --dry-run para preview ou --yes para aplicar o escopo explícito.")
|
|
104
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
105
|
+
if prompt("Aplicar estas alterações? [s/N]").lower() not in ("s", "sim", "y", "yes"):
|
|
106
|
+
raise CrowError("cancelled", "Operação cancelada; nenhuma instalação alterada.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def relationship_view(source, name, store, targets):
|
|
110
|
+
relations = source.related(name)
|
|
111
|
+
names = set(relations["handoffs"] + relations["used_by"] + relations["behaviour_overlays"] + relations["can_coordinate"])
|
|
112
|
+
return {**relations, "availability": [r for r in store.status(targets, names) if r["name"] in names] if names else [],
|
|
113
|
+
"meaning": "Handoffs e overlays são complementos opcionais, não dependências nem ordem automática de execução."}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run(args):
|
|
117
|
+
if args.version:
|
|
118
|
+
return {"version": __version__}
|
|
119
|
+
if not args.command:
|
|
120
|
+
raise CrowError("usage", "Escolha um comando; use crow-skills --help.")
|
|
121
|
+
home = checked(getattr(args, "home", Path.home()))
|
|
122
|
+
store = Store(home)
|
|
123
|
+
interactive = sys.stdin.isatty() and not getattr(args, "non_interactive", False) and not getattr(args, "json", False)
|
|
124
|
+
if getattr(args, "fetch", False) and (getattr(args, "dry_run", False) or args.command not in ("install", "update", "list", "info", "related")):
|
|
125
|
+
raise CrowError("usage", "--fetch requer um comando com fonte e não pode ser combinado com --dry-run.")
|
|
126
|
+
if args.command == "agents":
|
|
127
|
+
return {"agents": inventory(home), "note": "Detecção por PATH/diretórios não comprova carregamento da skill."}
|
|
128
|
+
if args.command in ("status", "doctor"):
|
|
129
|
+
targets = scope(args, home, read=True)
|
|
130
|
+
rows = store.status(targets)
|
|
131
|
+
pending = [t["id"] for t in store.pending()]
|
|
132
|
+
result = {"installations": rows, "pending_transactions": pending,
|
|
133
|
+
"activation_verified": False, "reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
|
|
134
|
+
if args.command == "doctor":
|
|
135
|
+
result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe") for r in rows), agents=inventory(home))
|
|
136
|
+
return result
|
|
137
|
+
if args.command in ("rollback", "recover"):
|
|
138
|
+
if any(getattr(args, k, None) for k in ("agents", "dest", "project", "global_scope")):
|
|
139
|
+
raise CrowError("usage", "Rollback/recover usam os destinos registrados na transação, sem filtros de agente/escopo.")
|
|
140
|
+
ids = [args.transaction] if args.transaction else [t["id"] for t in store.pending()]
|
|
141
|
+
transactions = [store.journal(txid) for txid in ids]
|
|
142
|
+
expected = ("prepared", "restoring") if args.command == "recover" else ("committed",)
|
|
143
|
+
if any(t["phase"] not in expected for t in transactions):
|
|
144
|
+
raise CrowError("invalid_transaction", "Transação incompatível com a operação.")
|
|
145
|
+
summary = {"transactions": [{"id": t["id"], "targets": list(t["before"])} for t in transactions]}
|
|
146
|
+
if transactions:
|
|
147
|
+
authorize(args, interactive, summary)
|
|
148
|
+
if not getattr(args, "dry_run", False):
|
|
149
|
+
for txid in ids:
|
|
150
|
+
store.rollback(txid, recovery=args.command == "recover")
|
|
151
|
+
return {**summary, "dry_run": getattr(args, "dry_run", False), "applied": not getattr(args, "dry_run", False)}
|
|
152
|
+
source = None
|
|
153
|
+
if args.command != "uninstall":
|
|
154
|
+
source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"), getattr(args, "fetch", False))
|
|
155
|
+
if args.command == "list":
|
|
156
|
+
return {"revision": source.revision, "source": str(source.path), "skills": list(source.catalog.values()),
|
|
157
|
+
"composition": {"overlays": source.overlays}, "handoffs_are_optional": True}
|
|
158
|
+
if args.command in ("info", "related"):
|
|
159
|
+
source.require(args.name)
|
|
160
|
+
targets = scope(args, home, read=True)
|
|
161
|
+
return {"revision": source.revision, "skill": source.catalog[args.name],
|
|
162
|
+
"relationships": relationship_view(source, args.name, store, targets)}
|
|
163
|
+
targets = scope(args, home, interactive)
|
|
164
|
+
names = args.skills
|
|
165
|
+
if names and args.all:
|
|
166
|
+
raise CrowError("usage", "Use nomes ou --all, não ambos.")
|
|
167
|
+
if args.command == "install" and not names and not args.all:
|
|
168
|
+
if not interactive:
|
|
169
|
+
raise CrowError("usage", "Escolha nomes de skills ou --all.")
|
|
170
|
+
for name, item in source.catalog.items():
|
|
171
|
+
print(terminal(name + " — " + item.get("title", name)), file=sys.stderr)
|
|
172
|
+
names = prompt("Skills separadas por vírgula:").split(",")
|
|
173
|
+
names = [n.strip() for n in names if n.strip()]
|
|
174
|
+
if not names:
|
|
175
|
+
raise CrowError("usage", "Nenhuma skill selecionada.")
|
|
176
|
+
if args.command == "uninstall" and not names and not args.all:
|
|
177
|
+
raise CrowError("usage", "Uninstall exige nomes explícitos ou --all.")
|
|
178
|
+
if args.command == "install" and args.all:
|
|
179
|
+
names = list(source.catalog)
|
|
180
|
+
names = list(dict.fromkeys(names))
|
|
181
|
+
if getattr(args, "with_handoffs", False):
|
|
182
|
+
if not names:
|
|
183
|
+
raise CrowError("usage", "--with-handoffs exige nomes explícitos.")
|
|
184
|
+
for name in list(names):
|
|
185
|
+
source.require(name)
|
|
186
|
+
names.extend(n for n in source.catalog[name]["next"] if n not in names)
|
|
187
|
+
plan = store.plan(args.command, names, targets, source, getattr(args, "replace", False), args.overwrite_modified)
|
|
188
|
+
summary = {"changes": plan["changes"], "revision": source.revision if source else None,
|
|
189
|
+
"relationships": {n: source.related(n) for n in names} if source else {},
|
|
190
|
+
"handoffs_are_optional": True, "activation_verified": False}
|
|
191
|
+
if plan["items"]:
|
|
192
|
+
authorize(args, interactive, summary)
|
|
193
|
+
txid = None if getattr(args, "dry_run", False) else store.apply(plan)
|
|
194
|
+
return {**summary, "transaction": txid, "dry_run": getattr(args, "dry_run", False),
|
|
195
|
+
"applied": not getattr(args, "dry_run", False),
|
|
196
|
+
"reload_hint": ("Somente preview; nada instalado." if getattr(args, "dry_run", False) else
|
|
197
|
+
"Operação de arquivos concluída; carregamento/ativação devem ser conferidos no host.")}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def emit(command, data, machine):
|
|
201
|
+
envelope = {"schema_version": 1, "ok": True, "command": command, "data": data}
|
|
202
|
+
if machine:
|
|
203
|
+
print(json.dumps(envelope, ensure_ascii=False, separators=(",", ":")))
|
|
204
|
+
elif command == "list":
|
|
205
|
+
print("Fonte: " + data["revision"])
|
|
206
|
+
for item in data["skills"]:
|
|
207
|
+
print(terminal(f"{item['name']} [{item['role']}] → {item['owns']}\n {item['description']}"))
|
|
208
|
+
else:
|
|
209
|
+
print(terminal(json.dumps(data, ensure_ascii=False, indent=2)))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main(argv=None):
|
|
213
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
214
|
+
machine = "--json" in argv
|
|
215
|
+
command = None
|
|
216
|
+
try:
|
|
217
|
+
args = parser().parse_args(argv)
|
|
218
|
+
command = args.command or ("version" if args.version else None)
|
|
219
|
+
result = run(args)
|
|
220
|
+
emit(command, result, machine)
|
|
221
|
+
return 0
|
|
222
|
+
except CrowError as exc:
|
|
223
|
+
if machine:
|
|
224
|
+
print(json.dumps({"schema_version": 1, "ok": False, "command": command,
|
|
225
|
+
"error": {"code": exc.code, "message": str(exc)}}, ensure_ascii=False))
|
|
226
|
+
else:
|
|
227
|
+
print(f"Erro [{exc.code}]: {exc}", file=sys.stderr)
|
|
228
|
+
return 2 if exc.code == "usage" else 1
|
|
229
|
+
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
230
|
+
# No traceback or subprocess stderr in machine output; credentials stay external.
|
|
231
|
+
if machine:
|
|
232
|
+
print(json.dumps({"schema_version": 1, "ok": False, "command": command,
|
|
233
|
+
"error": {"code": "io_error", "message": "Falha de leitura/gravação ou estado inválido; nenhuma conclusão de sucesso."}}))
|
|
234
|
+
else:
|
|
235
|
+
print("Falha de leitura/gravação ou estado inválido. Confira permissões, origem e transações pendentes.", file=sys.stderr)
|
|
236
|
+
return 1
|
|
237
|
+
except KeyboardInterrupt:
|
|
238
|
+
if machine:
|
|
239
|
+
print(json.dumps({"schema_version": 1, "ok": False, "command": command,
|
|
240
|
+
"error": {"code": "interrupted", "message": "Operação interrompida; confira status/recover."}}))
|
|
241
|
+
return 130
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Isolated Python entry used by the npm launcher, not imported as package code."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
if sys.version_info < (3, 10):
|
|
7
|
+
error = {"schema_version": 1, "ok": False, "command": None,
|
|
8
|
+
"error": {"code": "python_version", "message": "Python 3.10+ é necessário."}}
|
|
9
|
+
print(json.dumps(error) if "--json" in sys.argv else error["error"]["message"])
|
|
10
|
+
raise SystemExit(1)
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
12
|
+
from crow_skills.cli import main
|
|
13
|
+
|
|
14
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Read committed Git objects as data. Never check out or execute package code."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path, PurePosixPath
|
|
6
|
+
import re
|
|
7
|
+
import stat
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
NAME = re.compile(r"crow-[a-z0-9]+(?:-[a-z0-9]+)*\Z")
|
|
11
|
+
SHA = re.compile(r"[a-f0-9]{40,64}\Z")
|
|
12
|
+
MAX_FILE = 8 * 1024 * 1024
|
|
13
|
+
MAX_PACKAGE = 32 * 1024 * 1024
|
|
14
|
+
MAX_FILES = 2000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CrowError(Exception):
|
|
18
|
+
def __init__(self, code, message):
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.code = code
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def valid_name(name):
|
|
24
|
+
if not isinstance(name, str) or not NAME.fullmatch(name) or len(name) >= 64:
|
|
25
|
+
raise CrowError("invalid_name", "Nome de skill inválido; use crow-nome.")
|
|
26
|
+
return name
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def safe_relative(value):
|
|
30
|
+
p = PurePosixPath(value)
|
|
31
|
+
if (not value or p.is_absolute() or str(p) != value or
|
|
32
|
+
any(x in (".", "..") for x in p.parts) or "\\" in value or
|
|
33
|
+
any(ord(c) < 32 or ord(c) == 127 for c in value)):
|
|
34
|
+
raise CrowError("unsafe_path", "Caminho de recurso inválido no pacote.")
|
|
35
|
+
return p
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def file_entry(data, executable=False):
|
|
39
|
+
return {"kind": "file", "sha256": hashlib.sha256(data).hexdigest(),
|
|
40
|
+
"executable": bool(executable)}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def entries_for(files):
|
|
44
|
+
entries = {}
|
|
45
|
+
for name, (data, executable) in files.items():
|
|
46
|
+
entries[name] = file_entry(data, executable)
|
|
47
|
+
for parent in PurePosixPath(name).parents:
|
|
48
|
+
if str(parent) != ".":
|
|
49
|
+
entries[str(parent)] = {"kind": "dir"}
|
|
50
|
+
return dict(sorted(entries.items()))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def digest(entries):
|
|
54
|
+
return hashlib.sha256(json.dumps(entries, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Package:
|
|
59
|
+
name: str
|
|
60
|
+
files: dict
|
|
61
|
+
entries: dict
|
|
62
|
+
|
|
63
|
+
def materialize(self, target):
|
|
64
|
+
target.mkdir(mode=0o700)
|
|
65
|
+
for name, (data, executable) in self.files.items():
|
|
66
|
+
p = target / name
|
|
67
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
p.write_bytes(data)
|
|
69
|
+
p.chmod(0o755 if executable else 0o644)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Source:
|
|
73
|
+
def __init__(self, path, ref="HEAD", fetch=False):
|
|
74
|
+
self.path = Path(path).expanduser().resolve(strict=True)
|
|
75
|
+
self.bundle = None
|
|
76
|
+
if (self.path / "release.json").exists() and not (self.path / ".git").exists():
|
|
77
|
+
if fetch:
|
|
78
|
+
raise CrowError("immutable_release", "Pacote npm é um snapshot; atualize o pacote npm ou selecione --source com clone Git.")
|
|
79
|
+
self._init_bundle()
|
|
80
|
+
if ref not in ("HEAD", self.revision):
|
|
81
|
+
raise CrowError("immutable_release", "A referência não está neste snapshot npm; selecione um clone Git para outras refs.")
|
|
82
|
+
raw = self.bundle_file("catalog.json")
|
|
83
|
+
else:
|
|
84
|
+
if fetch:
|
|
85
|
+
self.git("fetch", "--no-recurse-submodules", "origin", timeout=120)
|
|
86
|
+
self.revision = self.git("rev-parse", "--verify", "--end-of-options", ref + "^{commit}").decode().strip()
|
|
87
|
+
raw = self.git("show", self.revision + ":catalog.json")
|
|
88
|
+
if not SHA.fullmatch(self.revision):
|
|
89
|
+
raise CrowError("invalid_revision", "A referência não resolveu para um commit.")
|
|
90
|
+
try:
|
|
91
|
+
if len(raw) > 1024 * 1024:
|
|
92
|
+
raise ValueError("catalog too large")
|
|
93
|
+
data = json.loads(raw)
|
|
94
|
+
skills = data["skills"]
|
|
95
|
+
if data["version"] != 1 or not isinstance(skills, list) or len(skills) > 1000:
|
|
96
|
+
raise ValueError("invalid schema")
|
|
97
|
+
self.catalog = {}
|
|
98
|
+
owners = set()
|
|
99
|
+
for item in skills:
|
|
100
|
+
name = valid_name(item["name"])
|
|
101
|
+
if (name in self.catalog or not isinstance(item["description"], str) or
|
|
102
|
+
not isinstance(item["owns"], str) or not item["owns"] or item["owns"] in owners or
|
|
103
|
+
item["role"] not in ("coordinator", "specialist", "reviewer") or
|
|
104
|
+
not isinstance(item["next"], list)):
|
|
105
|
+
raise ValueError("invalid entry")
|
|
106
|
+
owners.add(item["owns"])
|
|
107
|
+
self.catalog[name] = item
|
|
108
|
+
for item in skills:
|
|
109
|
+
if len(item["next"]) != len(set(item["next"])) or any(n not in self.catalog or n == item["name"] for n in item["next"]):
|
|
110
|
+
raise ValueError("invalid handoff")
|
|
111
|
+
composition = data.get("composition", {})
|
|
112
|
+
if not isinstance(composition, dict):
|
|
113
|
+
raise ValueError("invalid composition")
|
|
114
|
+
self.overlays = composition.get("overlays", [])
|
|
115
|
+
if not isinstance(self.overlays, list):
|
|
116
|
+
raise ValueError("invalid overlays")
|
|
117
|
+
for overlay in self.overlays:
|
|
118
|
+
if overlay["skill"] not in self.catalog or not isinstance(overlay["roles"], list) or any(r not in ("coordinator", "specialist", "reviewer") for r in overlay["roles"]):
|
|
119
|
+
raise ValueError("invalid overlay")
|
|
120
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
121
|
+
raise CrowError("invalid_catalog", "Catálogo inválido no commit selecionado.") from exc
|
|
122
|
+
|
|
123
|
+
def _init_bundle(self):
|
|
124
|
+
try:
|
|
125
|
+
p = self.path / "release.json"
|
|
126
|
+
if p.is_symlink() or p.stat().st_size > 2 * 1024 * 1024:
|
|
127
|
+
raise ValueError("manifest")
|
|
128
|
+
data = json.loads(p.read_bytes())
|
|
129
|
+
if data["schema_version"] != 1 or not SHA.fullmatch(data["revision"]) or not isinstance(data["files"], dict) or len(data["files"]) > 40000:
|
|
130
|
+
raise ValueError("schema")
|
|
131
|
+
for name, entry in data["files"].items():
|
|
132
|
+
safe_relative(name)
|
|
133
|
+
if (not isinstance(entry, dict) or set(entry) != {"kind", "sha256", "executable"} or entry["kind"] != "file" or
|
|
134
|
+
not isinstance(entry["sha256"], str) or not re.fullmatch(r"[a-f0-9]{64}", entry["sha256"]) or not isinstance(entry["executable"], bool)):
|
|
135
|
+
raise ValueError("entry")
|
|
136
|
+
self.bundle = data["files"]
|
|
137
|
+
self.revision = data["revision"]
|
|
138
|
+
except (OSError, ValueError, TypeError, KeyError) as exc:
|
|
139
|
+
raise CrowError("invalid_release", "Manifesto do snapshot npm inválido.") from exc
|
|
140
|
+
|
|
141
|
+
def bundle_file(self, name):
|
|
142
|
+
safe_relative(name)
|
|
143
|
+
try:
|
|
144
|
+
p = self.path / name
|
|
145
|
+
for item in (p, *p.parents):
|
|
146
|
+
if item == self.path:
|
|
147
|
+
break
|
|
148
|
+
if item.is_symlink():
|
|
149
|
+
raise ValueError("symlink")
|
|
150
|
+
info = p.stat()
|
|
151
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > MAX_FILE:
|
|
152
|
+
raise ValueError("size/type")
|
|
153
|
+
data = p.read_bytes()
|
|
154
|
+
if file_entry(data, bool(info.st_mode & 0o111)) != self.bundle[name]:
|
|
155
|
+
raise ValueError("digest/mode")
|
|
156
|
+
return data
|
|
157
|
+
except (OSError, KeyError, ValueError) as exc:
|
|
158
|
+
raise CrowError("invalid_release", "Recurso ausente, alterado ou inseguro no snapshot npm.") from exc
|
|
159
|
+
|
|
160
|
+
def git(self, *args, timeout=30):
|
|
161
|
+
try:
|
|
162
|
+
result = subprocess.run(["git", "--no-replace-objects", "-c", "core.hooksPath=/dev/null", "-c", "gc.auto=0",
|
|
163
|
+
"-c", "protocol.ext.allow=never", "-C", str(self.path), *args],
|
|
164
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
|
|
165
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
166
|
+
raise CrowError("git_unavailable", "Git indisponível ou operação excedeu o tempo limite.") from exc
|
|
167
|
+
if result.returncode:
|
|
168
|
+
# Git/auth helpers can print credentials: never echo their stderr.
|
|
169
|
+
raise CrowError("git_failed", "Git falhou. Verifique clone, referência e acesso ao origin; stderr omitido para proteger credenciais.")
|
|
170
|
+
return result.stdout
|
|
171
|
+
|
|
172
|
+
def related(self, name):
|
|
173
|
+
self.require(name)
|
|
174
|
+
return {
|
|
175
|
+
"handoffs": list(self.catalog[name]["next"]),
|
|
176
|
+
"used_by": [n for n, v in self.catalog.items() if name in v["next"]],
|
|
177
|
+
"behaviour_overlays": [v["skill"] for v in self.overlays
|
|
178
|
+
if v["skill"] != name and self.catalog[name]["role"] in v["roles"]],
|
|
179
|
+
"can_coordinate": [n for v in self.overlays if v["skill"] == name
|
|
180
|
+
for n, item in self.catalog.items() if n != name and item["role"] in v["roles"]],
|
|
181
|
+
"optional": True,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def require(self, name):
|
|
185
|
+
valid_name(name)
|
|
186
|
+
if name not in self.catalog:
|
|
187
|
+
raise CrowError("unknown_skill", "Skill não encontrada no catálogo selecionado: " + name)
|
|
188
|
+
|
|
189
|
+
def _git_files(self, name):
|
|
190
|
+
prefix = "skills/" + name + "/"
|
|
191
|
+
raw = self.git("ls-tree", "-r", "-z", "-l", self.revision, "--", prefix)
|
|
192
|
+
files = {}
|
|
193
|
+
total = 0
|
|
194
|
+
for record in raw.split(b"\0"):
|
|
195
|
+
if not record:
|
|
196
|
+
continue
|
|
197
|
+
try:
|
|
198
|
+
header, path = record.split(b"\t", 1)
|
|
199
|
+
mode, kind, oid, size = header.split()
|
|
200
|
+
path = path.decode("utf-8")
|
|
201
|
+
if not path.startswith(prefix):
|
|
202
|
+
raise ValueError("prefix")
|
|
203
|
+
rel = path[len(prefix):]
|
|
204
|
+
safe_relative(rel)
|
|
205
|
+
if mode not in (b"100644", b"100755") or kind != b"blob":
|
|
206
|
+
raise CrowError("unsafe_source", "Symlinks, submódulos e arquivos especiais não são instaláveis.")
|
|
207
|
+
size = int(size)
|
|
208
|
+
total += size
|
|
209
|
+
if size > MAX_FILE or total > MAX_PACKAGE or len(files) >= MAX_FILES:
|
|
210
|
+
raise CrowError("package_limit", "Pacote excede limite de tamanho/arquivos.")
|
|
211
|
+
blob = self.git("cat-file", "blob", oid.decode())
|
|
212
|
+
if len(blob) != size:
|
|
213
|
+
raise ValueError("size")
|
|
214
|
+
files[rel] = (blob, mode == b"100755")
|
|
215
|
+
except (ValueError, UnicodeError) as exc:
|
|
216
|
+
raise CrowError("unsafe_source", "Inventário de pacote inválido.") from exc
|
|
217
|
+
return files
|
|
218
|
+
|
|
219
|
+
def package(self, name):
|
|
220
|
+
self.require(name)
|
|
221
|
+
if self.bundle is None:
|
|
222
|
+
files = self._git_files(name)
|
|
223
|
+
else:
|
|
224
|
+
prefix = "skills/" + name + "/"
|
|
225
|
+
files = {}
|
|
226
|
+
total = 0
|
|
227
|
+
for path, entry in self.bundle.items():
|
|
228
|
+
if not path.startswith(prefix):
|
|
229
|
+
continue
|
|
230
|
+
data = self.bundle_file(path)
|
|
231
|
+
total += len(data)
|
|
232
|
+
if total > MAX_PACKAGE or len(files) >= MAX_FILES:
|
|
233
|
+
raise CrowError("package_limit", "Snapshot excede limite de tamanho/arquivos.")
|
|
234
|
+
files[path[len(prefix):]] = (data, entry["executable"])
|
|
235
|
+
if "SKILL.md" not in files:
|
|
236
|
+
raise CrowError("invalid_package", "SKILL.md ausente do pacote.")
|
|
237
|
+
try:
|
|
238
|
+
text = files["SKILL.md"][0].decode("utf-8")
|
|
239
|
+
head, sep, _ = text[4:].partition("\n---\n")
|
|
240
|
+
values = dict(line.split(":", 1) for line in head.splitlines())
|
|
241
|
+
if not text.startswith("---\n") or not sep or values["name"].strip() != name or json.loads(values["description"].strip()) != self.catalog[name]["description"]:
|
|
242
|
+
raise ValueError("frontmatter mismatch")
|
|
243
|
+
# Parse only the repository's documented simple YAML subset, never execute it.
|
|
244
|
+
for rel, (content, _) in files.items():
|
|
245
|
+
if rel.endswith(".md"):
|
|
246
|
+
for link in re.findall(r"!?\[[^\]]*\]\(([^)\s]+)\)", content.decode("utf-8")):
|
|
247
|
+
if "://" in link or link.startswith(("#", "mailto:")):
|
|
248
|
+
continue
|
|
249
|
+
import posixpath
|
|
250
|
+
joined = posixpath.normpath(posixpath.join(posixpath.dirname(rel), link.split("#", 1)[0]))
|
|
251
|
+
safe_relative(joined)
|
|
252
|
+
if joined not in files:
|
|
253
|
+
raise ValueError("missing reference")
|
|
254
|
+
except (ValueError, KeyError, UnicodeError) as exc:
|
|
255
|
+
raise CrowError("invalid_package", "Frontmatter/referências do pacote não correspondem ao catálogo.") from exc
|
|
256
|
+
return Package(name, files, entries_for(files))
|