@jaimevalasek/aioson 1.7.2 → 1.8.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/CHANGELOG.md +35 -0
- package/README.md +153 -10
- package/docs/en/cli-reference.md +56 -1
- package/docs/en/i18n.md +18 -18
- package/docs/en/schemas/index.json +10 -0
- package/docs/en/schemas/parallel-assign.schema.json +9 -0
- package/docs/en/schemas/parallel-doctor.schema.json +36 -0
- package/docs/en/schemas/parallel-guard.schema.json +63 -0
- package/docs/en/schemas/parallel-merge.schema.json +84 -0
- package/docs/en/schemas/parallel-status.schema.json +91 -1
- package/docs/integrations/apps-publish-marketplace.md +94 -0
- package/docs/pt/README.md +9 -0
- package/docs/pt/agentes.md +324 -3
- package/docs/pt/clientes-ai.md +7 -3
- package/docs/pt/comandos-cli.md +160 -13
- package/docs/pt/compress-agents.md +304 -0
- package/docs/pt/design-docs-governance.md +59 -0
- package/docs/pt/feature-archive.md +191 -0
- package/docs/pt/genome-3.0-spec.md +115 -4
- package/docs/pt/genome-distribution.md +232 -0
- package/docs/pt/inicio-rapido.md +1 -0
- package/docs/pt/motor-hardening.md +492 -0
- package/docs/pt/runner-system.md +113 -0
- package/package.json +2 -1
- package/src/agent-manifests.js +66 -0
- package/src/agents.js +27 -7
- package/src/autonomy-policy.js +139 -0
- package/src/brain-query.js +161 -0
- package/src/cli.js +1377 -1099
- package/src/commands/agents.js +102 -7
- package/src/commands/artifact-validate.js +33 -4
- package/src/commands/auth.js +272 -0
- package/src/commands/brain-query.js +44 -0
- package/src/commands/briefing.js +344 -0
- package/src/commands/commit-prepare.js +547 -0
- package/src/commands/compress-agents.js +416 -0
- package/src/commands/context-health.js +4 -2
- package/src/commands/context-trim.js +17 -11
- package/src/commands/design-hybrid-options.js +3 -3
- package/src/commands/devlog-process.js +6 -4
- package/src/commands/dossier.js +423 -0
- package/src/commands/feature-archive.js +513 -0
- package/src/commands/feature-close.js +123 -18
- package/src/commands/gate-approve.js +198 -0
- package/src/commands/gate-check.js +24 -5
- package/src/commands/genome-doctor.js +166 -9
- package/src/commands/git-guard.js +170 -0
- package/src/commands/harness.js +121 -0
- package/src/commands/implementation-plan.js +47 -20
- package/src/commands/init.js +6 -2
- package/src/commands/install.js +6 -2
- package/src/commands/live.js +497 -56
- package/src/commands/locale-apply.js +9 -6
- package/src/commands/locale-diff.js +11 -112
- package/src/commands/mcp-doctor.js +2 -1
- package/src/commands/mcp-init.js +4 -10
- package/src/commands/memory.js +234 -0
- package/src/commands/parallel-assign.js +107 -27
- package/src/commands/parallel-doctor.js +416 -3
- package/src/commands/parallel-guard.js +241 -0
- package/src/commands/parallel-init.js +66 -4
- package/src/commands/parallel-merge.js +299 -0
- package/src/commands/parallel-status.js +147 -3
- package/src/commands/preflight.js +63 -4
- package/src/commands/qa-init.js +10 -5
- package/src/commands/revision.js +235 -0
- package/src/commands/scaffold-complete.js +188 -0
- package/src/commands/security-audit.js +275 -0
- package/src/commands/security-scan.js +376 -0
- package/src/commands/self-implement-loop.js +46 -2
- package/src/commands/setup-context.js +11 -10
- package/src/commands/squad-agent-create.js +51 -9
- package/src/commands/squad-investigate.js +53 -0
- package/src/commands/squad-plan.js +33 -1
- package/src/commands/squad-scaffold.js +4 -3
- package/src/commands/squad-score.js +71 -14
- package/src/commands/squad-status.js +22 -1
- package/src/commands/squad-validate.js +93 -2
- package/src/commands/store-genome.js +304 -0
- package/src/commands/store-skill.js +247 -0
- package/src/commands/store-squad.js +431 -0
- package/src/commands/store-system.js +392 -0
- package/src/commands/tool-capabilities.js +63 -0
- package/src/commands/update.js +3 -3
- package/src/commands/verify-gate.js +40 -0
- package/src/commands/workflow-execute.js +644 -155
- package/src/commands/workflow-harden.js +231 -0
- package/src/commands/workflow-heal.js +136 -0
- package/src/commands/workflow-next.js +460 -22
- package/src/commands/workflow-status.js +328 -138
- package/src/commands/workspace.js +144 -0
- package/src/constants.js +42 -75
- package/src/context-memory.js +133 -4
- package/src/context-writer.js +2 -1
- package/src/context.js +32 -2
- package/src/doctor.js +46 -6
- package/src/dossier/codemap-store.js +267 -0
- package/src/dossier/dossier-bootstrap.js +222 -0
- package/src/dossier/dossier-compact.js +159 -0
- package/src/dossier/lock.js +128 -0
- package/src/dossier/revision-store.js +313 -0
- package/src/dossier/schema.js +155 -0
- package/src/dossier/store.js +400 -0
- package/src/execution-gateway.js +3 -0
- package/src/friction-scanner.js +202 -0
- package/src/genome-schema.js +24 -1
- package/src/genomes.js +33 -0
- package/src/handoff-contract.js +363 -0
- package/src/handoff-validator.js +45 -0
- package/src/harness/circuit-breaker.js +135 -0
- package/src/i18n/messages/en.js +317 -22
- package/src/i18n/messages/es.js +259 -18
- package/src/i18n/messages/fr.js +260 -18
- package/src/i18n/messages/pt-BR.js +313 -22
- package/src/install-profile.js +0 -16
- package/src/installer.js +70 -6
- package/src/lib/git-commit-guard.js +691 -0
- package/src/lib/security/artifact-reader.js +167 -0
- package/src/lib/security/exit-codes.js +51 -0
- package/src/lib/security/findings-writer.js +176 -0
- package/src/lib/security/runtime-events.js +77 -0
- package/src/lib/security/secrets-regex.js +115 -0
- package/src/lib/store/security-scan.js +173 -0
- package/src/lib/terminal-checkbox.js +130 -0
- package/src/lib/tmux-launcher.js +163 -0
- package/src/lib/tool-capabilities.js +102 -0
- package/src/locales.js +12 -8
- package/src/parallel-workspace.js +756 -0
- package/src/parser.js +8 -1
- package/src/path-guard.js +47 -0
- package/src/preflight-engine.js +237 -26
- package/src/self-healing.js +142 -0
- package/src/session-handoff.js +111 -1
- package/src/squad/squad-scaffold.js +183 -19
- package/src/test-briefing.js +226 -0
- package/src/updater.js +1 -1
- package/src/utils.js +3 -0
- package/src/workflow-gates.js +185 -0
- package/template/.aioson/agents/analyst.md +76 -130
- package/template/.aioson/agents/architect.md +53 -86
- package/template/.aioson/agents/committer.md +161 -0
- package/template/.aioson/agents/cypher.md +252 -0
- package/template/.aioson/agents/dev.md +112 -628
- package/template/.aioson/agents/deyvin.md +33 -236
- package/template/.aioson/agents/discover.md +235 -0
- package/template/.aioson/agents/discovery-design-doc.md +17 -252
- package/template/.aioson/agents/genome.md +76 -26
- package/template/.aioson/agents/manifests/analyst.manifest.json +26 -0
- package/template/.aioson/agents/manifests/architect.manifest.json +23 -0
- package/template/.aioson/agents/manifests/committer.manifest.json +23 -0
- package/template/.aioson/agents/manifests/dev.manifest.json +37 -0
- package/template/.aioson/agents/manifests/orchestrator.manifest.json +30 -0
- package/template/.aioson/agents/manifests/pentester.manifest.json +39 -0
- package/template/.aioson/agents/manifests/pm.manifest.json +26 -0
- package/template/.aioson/agents/manifests/product.manifest.json +23 -0
- package/template/.aioson/agents/manifests/qa.manifest.json +25 -0
- package/template/.aioson/agents/manifests/setup.manifest.json +20 -0
- package/template/.aioson/agents/manifests/ux-ui.manifest.json +24 -0
- package/template/.aioson/agents/neo.md +5 -7
- package/template/.aioson/agents/orache.md +2 -6
- package/template/.aioson/agents/orchestrator.md +81 -182
- package/template/.aioson/agents/pentester.md +235 -0
- package/template/.aioson/agents/pm.md +40 -104
- package/template/.aioson/agents/product.md +99 -344
- package/template/.aioson/agents/profiler-enricher.md +57 -6
- package/template/.aioson/agents/profiler-forge.md +17 -7
- package/template/.aioson/agents/profiler-researcher.md +29 -6
- package/template/.aioson/agents/qa.md +168 -514
- package/template/.aioson/agents/setup.md +52 -278
- package/template/.aioson/agents/sheldon.md +122 -754
- package/template/.aioson/agents/site-forge.md +111 -1583
- package/template/.aioson/agents/squad.md +139 -2010
- package/template/.aioson/agents/tester.md +10 -0
- package/template/.aioson/agents/ux-ui.md +104 -812
- package/template/.aioson/agents/validator.md +69 -0
- package/template/.aioson/brains/scripts/query.js +5 -1
- package/template/.aioson/config/autonomy-protocol.json +43 -0
- package/template/.aioson/config.md +43 -15
- package/template/.aioson/constitution.md +36 -33
- package/template/.aioson/context/design-doc.md +136 -0
- package/template/.aioson/context/project-map.md +57 -0
- package/template/.aioson/design-docs/code-reuse.md +48 -0
- package/template/.aioson/design-docs/componentization.md +47 -0
- package/template/.aioson/design-docs/file-size.md +52 -0
- package/template/.aioson/design-docs/folder-structure.md +51 -0
- package/template/.aioson/design-docs/naming.md +54 -0
- package/template/.aioson/docs/LAYERS.md +12 -2
- package/template/.aioson/docs/dev/execution-discipline.md +106 -0
- package/template/.aioson/docs/dev/stack-conventions.md +83 -0
- package/template/.aioson/docs/deyvin/continuity-recovery.md +57 -0
- package/template/.aioson/docs/deyvin/debugging-escalation.md +30 -0
- package/template/.aioson/docs/deyvin/pair-execution.md +44 -0
- package/template/.aioson/docs/deyvin/runtime-handoffs.md +36 -0
- package/template/.aioson/docs/product/conversation-playbook.md +116 -0
- package/template/.aioson/docs/product/prd-contract.md +107 -0
- package/template/.aioson/docs/product/quality-lens.md +57 -0
- package/template/.aioson/docs/product/research-loop.md +65 -0
- package/template/.aioson/docs/sheldon/enrichment-paths.md +134 -0
- package/template/.aioson/docs/sheldon/quality-lens.md +57 -0
- package/template/.aioson/docs/sheldon/research-loop.md +56 -0
- package/template/.aioson/docs/sheldon/web-intelligence.md +75 -0
- package/template/.aioson/docs/site-forge-build.md +195 -0
- package/template/.aioson/docs/site-forge-extraction.md +135 -0
- package/template/.aioson/docs/site-forge-qa.md +155 -0
- package/template/.aioson/docs/site-forge-recon.md +434 -0
- package/template/.aioson/docs/site-forge-transform.md +249 -0
- package/template/.aioson/docs/squad/content-output.md +91 -0
- package/template/.aioson/docs/squad/creation-flow.md +135 -0
- package/template/.aioson/docs/squad/domain-classification.md +117 -0
- package/template/.aioson/docs/squad/genome-bindings.md +47 -0
- package/template/.aioson/docs/squad/package-contract.md +234 -0
- package/template/.aioson/docs/squad/quality-lens.md +56 -0
- package/template/.aioson/docs/squad/research-loop.md +59 -0
- package/template/.aioson/docs/squad/session-operations.md +117 -0
- package/template/.aioson/docs/squad/workflow-quality.md +165 -0
- package/template/.aioson/docs/ux-ui/accessibility-audit.md +55 -0
- package/template/.aioson/docs/ux-ui/audit-mode.md +86 -0
- package/template/.aioson/docs/ux-ui/component-map.md +35 -0
- package/template/.aioson/docs/ux-ui/design-execution.md +111 -0
- package/template/.aioson/docs/ux-ui/design-gate.md +27 -0
- package/template/.aioson/docs/ux-ui/research-mode.md +39 -0
- package/template/.aioson/docs/ux-ui/site-delivery.md +156 -0
- package/template/.aioson/docs/ux-ui/token-contract.md +57 -0
- package/template/.aioson/genomes/copywriting.meta.json +48 -0
- package/template/.aioson/git-guard.json +11 -0
- package/template/.aioson/mcp/servers.md +0 -1
- package/template/.aioson/rules/agent-language-policy.md +93 -0
- package/template/.aioson/rules/aioson-context-boundary.md +63 -0
- package/template/.aioson/rules/canonical-path-contract.md +47 -0
- package/template/.aioson/rules/data-format-convention.md +24 -86
- package/template/.aioson/rules/disk-first-artifacts.md +44 -0
- package/template/.aioson/rules/output-brevity.md +44 -0
- package/template/.aioson/rules/prd-section-ownership.md +49 -0
- package/template/.aioson/rules/security-baseline.md +139 -0
- package/template/.aioson/rules/spec-level-ownership.md +61 -0
- package/template/.aioson/rules/squad-driver-pattern.md +81 -0
- package/template/.aioson/schemas/squad-blueprint.schema.json +24 -0
- package/template/.aioson/schemas/squad-manifest.schema.json +44 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +30 -0
- package/template/.aioson/skills/process/secure-tdd/SKILL.md +97 -0
- package/template/.aioson/skills/process/secure-tdd/references/nextjs.md +81 -0
- package/template/.aioson/skills/process/secure-tdd/references/node-express.md +91 -0
- package/template/.aioson/skills/process/secure-tdd/references/planned-stacks.md +33 -0
- package/template/.aioson/skills/static/harness-validate/SKILL.md +46 -0
- package/template/.aioson/skills/static/web-research-cache.md +3 -0
- package/template/.aioson/tasks/squad-create.md +35 -8
- package/template/.aioson/tasks/squad-design.md +50 -2
- package/template/.aioson/tasks/squad-investigate.md +14 -1
- package/template/.claude/commands/aioson/agent/committer.md +5 -0
- package/template/.claude/commands/aioson/agent/copywriter.md +5 -0
- package/template/.claude/commands/aioson/agent/cypher.md +5 -0
- package/template/.claude/commands/aioson/agent/pair.md +5 -0
- package/template/.claude/commands/aioson/agent/validator.md +5 -0
- package/template/.gemini/commands/aios-analyst.toml +6 -3
- package/template/.gemini/commands/aios-architect.toml +7 -6
- package/template/.gemini/commands/aios-committer.toml +7 -0
- package/template/.gemini/commands/aios-copywriter.toml +7 -0
- package/template/.gemini/commands/aios-cypher.toml +7 -0
- package/template/.gemini/commands/aios-dev.toml +8 -7
- package/template/.gemini/commands/aios-deyvin.toml +6 -5
- package/template/.gemini/commands/aios-discovery-design-doc.toml +6 -3
- package/template/.gemini/commands/aios-genome.toml +7 -0
- package/template/.gemini/commands/aios-neo.toml +5 -3
- package/template/.gemini/commands/aios-orache.toml +7 -0
- package/template/.gemini/commands/aios-orchestrator.toml +8 -7
- package/template/.gemini/commands/aios-pair.toml +6 -5
- package/template/.gemini/commands/aios-pm.toml +8 -7
- package/template/.gemini/commands/aios-product.toml +5 -3
- package/template/.gemini/commands/aios-qa.toml +6 -5
- package/template/.gemini/commands/aios-setup.toml +5 -2
- package/template/.gemini/commands/aios-sheldon.toml +7 -0
- package/template/.gemini/commands/aios-site-forge.toml +7 -0
- package/template/.gemini/commands/aios-squad.toml +7 -0
- package/template/.gemini/commands/aios-tester.toml +6 -5
- package/template/.gemini/commands/aios-ux-ui.toml +8 -7
- package/template/.gemini/commands/aios-validator.toml +7 -0
- package/template/AGENTS.md +12 -1
- package/template/CLAUDE.md +5 -1
- package/template/.aioson/locales/en/agents/analyst.md +0 -244
- package/template/.aioson/locales/en/agents/architect.md +0 -245
- package/template/.aioson/locales/en/agents/dev.md +0 -397
- package/template/.aioson/locales/en/agents/deyvin.md +0 -137
- package/template/.aioson/locales/en/agents/discovery-design-doc.md +0 -27
- package/template/.aioson/locales/en/agents/genome.md +0 -212
- package/template/.aioson/locales/en/agents/neo.md +0 -8
- package/template/.aioson/locales/en/agents/orache.md +0 -6
- package/template/.aioson/locales/en/agents/orchestrator.md +0 -189
- package/template/.aioson/locales/en/agents/pair.md +0 -5
- package/template/.aioson/locales/en/agents/pm.md +0 -84
- package/template/.aioson/locales/en/agents/product.md +0 -378
- package/template/.aioson/locales/en/agents/profiler-enricher.md +0 -5
- package/template/.aioson/locales/en/agents/profiler-forge.md +0 -5
- package/template/.aioson/locales/en/agents/profiler-researcher.md +0 -5
- package/template/.aioson/locales/en/agents/qa.md +0 -270
- package/template/.aioson/locales/en/agents/setup.md +0 -421
- package/template/.aioson/locales/en/agents/sheldon.md +0 -455
- package/template/.aioson/locales/en/agents/squad.md +0 -449
- package/template/.aioson/locales/en/agents/tester.md +0 -6
- package/template/.aioson/locales/en/agents/ux-ui.md +0 -668
- package/template/.aioson/locales/es/agents/analyst.md +0 -225
- package/template/.aioson/locales/es/agents/architect.md +0 -245
- package/template/.aioson/locales/es/agents/dev.md +0 -370
- package/template/.aioson/locales/es/agents/deyvin.md +0 -99
- package/template/.aioson/locales/es/agents/discovery-design-doc.md +0 -21
- package/template/.aioson/locales/es/agents/genome.md +0 -104
- package/template/.aioson/locales/es/agents/neo.md +0 -50
- package/template/.aioson/locales/es/agents/orache.md +0 -105
- package/template/.aioson/locales/es/agents/orchestrator.md +0 -194
- package/template/.aioson/locales/es/agents/pair.md +0 -7
- package/template/.aioson/locales/es/agents/pm.md +0 -90
- package/template/.aioson/locales/es/agents/product.md +0 -372
- package/template/.aioson/locales/es/agents/profiler-enricher.md +0 -7
- package/template/.aioson/locales/es/agents/profiler-forge.md +0 -7
- package/template/.aioson/locales/es/agents/profiler-researcher.md +0 -7
- package/template/.aioson/locales/es/agents/qa.md +0 -198
- package/template/.aioson/locales/es/agents/setup.md +0 -405
- package/template/.aioson/locales/es/agents/sheldon.md +0 -309
- package/template/.aioson/locales/es/agents/squad.md +0 -532
- package/template/.aioson/locales/es/agents/tester.md +0 -9
- package/template/.aioson/locales/es/agents/ux-ui.md +0 -212
- package/template/.aioson/locales/fr/agents/analyst.md +0 -225
- package/template/.aioson/locales/fr/agents/architect.md +0 -245
- package/template/.aioson/locales/fr/agents/dev.md +0 -370
- package/template/.aioson/locales/fr/agents/deyvin.md +0 -99
- package/template/.aioson/locales/fr/agents/discovery-design-doc.md +0 -21
- package/template/.aioson/locales/fr/agents/genome.md +0 -104
- package/template/.aioson/locales/fr/agents/neo.md +0 -50
- package/template/.aioson/locales/fr/agents/orache.md +0 -106
- package/template/.aioson/locales/fr/agents/orchestrator.md +0 -194
- package/template/.aioson/locales/fr/agents/pair.md +0 -7
- package/template/.aioson/locales/fr/agents/pm.md +0 -90
- package/template/.aioson/locales/fr/agents/product.md +0 -372
- package/template/.aioson/locales/fr/agents/profiler-enricher.md +0 -7
- package/template/.aioson/locales/fr/agents/profiler-forge.md +0 -7
- package/template/.aioson/locales/fr/agents/profiler-researcher.md +0 -7
- package/template/.aioson/locales/fr/agents/qa.md +0 -198
- package/template/.aioson/locales/fr/agents/setup.md +0 -405
- package/template/.aioson/locales/fr/agents/sheldon.md +0 -309
- package/template/.aioson/locales/fr/agents/squad.md +0 -532
- package/template/.aioson/locales/fr/agents/tester.md +0 -9
- package/template/.aioson/locales/fr/agents/ux-ui.md +0 -212
- package/template/.aioson/locales/pt-BR/agents/analyst.md +0 -319
- package/template/.aioson/locales/pt-BR/agents/architect.md +0 -284
- package/template/.aioson/locales/pt-BR/agents/dev.md +0 -483
- package/template/.aioson/locales/pt-BR/agents/deyvin.md +0 -184
- package/template/.aioson/locales/pt-BR/agents/discovery-design-doc.md +0 -198
- package/template/.aioson/locales/pt-BR/agents/genome.md +0 -297
- package/template/.aioson/locales/pt-BR/agents/neo.md +0 -208
- package/template/.aioson/locales/pt-BR/agents/orache.md +0 -137
- package/template/.aioson/locales/pt-BR/agents/orchestrator.md +0 -324
- package/template/.aioson/locales/pt-BR/agents/pair.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/pm.md +0 -182
- package/template/.aioson/locales/pt-BR/agents/product.md +0 -466
- package/template/.aioson/locales/pt-BR/agents/profiler-enricher.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/profiler-forge.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/profiler-researcher.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/qa.md +0 -300
- package/template/.aioson/locales/pt-BR/agents/setup.md +0 -533
- package/template/.aioson/locales/pt-BR/agents/sheldon.md +0 -323
- package/template/.aioson/locales/pt-BR/agents/squad.md +0 -1330
- package/template/.aioson/locales/pt-BR/agents/tester.md +0 -449
- package/template/.aioson/locales/pt-BR/agents/ux-ui.md +0 -669
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { execFileSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG_REL_PATH = '.aioson/git-guard.json';
|
|
8
|
+
const MANAGED_HOOK_MARKER = '# aioson-git-guard-hook';
|
|
9
|
+
const BACKUP_HOOK_GIT_PATH = 'hooks/pre-commit.aioson-backup';
|
|
10
|
+
const DEFAULT_POLICY = Object.freeze({
|
|
11
|
+
version: 1,
|
|
12
|
+
description: '',
|
|
13
|
+
allowPaths: [],
|
|
14
|
+
blockPaths: [],
|
|
15
|
+
allowExtensions: [],
|
|
16
|
+
blockExtensions: []
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const BLOCKED_PATH_RULES = [
|
|
20
|
+
{
|
|
21
|
+
id: 'dependency_dir',
|
|
22
|
+
reason: 'dependency/vendor directory should not be committed',
|
|
23
|
+
test: (relPath) => /(^|\/)(node_modules|vendor)(\/|$)/i.test(relPath)
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'build_output',
|
|
27
|
+
reason: 'generated build output should not be committed',
|
|
28
|
+
test: (relPath) => /(^|\/)(dist|build|coverage|\.next|\.nuxt|\.svelte-kit|\.turbo|\.cache|\.parcel-cache|tmp|temp)(\/|$)/i.test(relPath)
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'session_artifact',
|
|
32
|
+
reason: 'runtime/session artifact should not be committed',
|
|
33
|
+
test: (relPath) => /(^|\/)(aioson-logs|output|media)(\/|$)/i.test(relPath)
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'aioson_backup',
|
|
37
|
+
reason: 'AIOSON backup artifact should not be committed',
|
|
38
|
+
test: (relPath) => /(^|\/)\.aioson\/backups(\/|$)/i.test(relPath)
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'env_file',
|
|
42
|
+
reason: 'environment file may contain secrets',
|
|
43
|
+
test: (relPath) => {
|
|
44
|
+
const base = path.posix.basename(relPath).toLowerCase();
|
|
45
|
+
if (!base.startsWith('.env')) return false;
|
|
46
|
+
return !['.env.example', '.env.sample', '.env.template', '.env.dist'].includes(base);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'secret_file',
|
|
51
|
+
reason: 'secret or credential file should not be committed',
|
|
52
|
+
test: (relPath) => /\.(pem|key|p12|pfx|p8|keystore|mobileprovision|kdbx)$/i.test(relPath)
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'log_file',
|
|
56
|
+
reason: 'log/debug artifact should not be committed',
|
|
57
|
+
test: (relPath) => {
|
|
58
|
+
const base = path.posix.basename(relPath).toLowerCase();
|
|
59
|
+
return base === '.ds_store'
|
|
60
|
+
|| base === 'npm-debug.log'
|
|
61
|
+
|| base === 'yarn-error.log'
|
|
62
|
+
|| base === 'pnpm-debug.log'
|
|
63
|
+
|| /\.log$/i.test(base);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const WARNING_PATH_RULES = [
|
|
69
|
+
{
|
|
70
|
+
id: 'backup_suffix',
|
|
71
|
+
reason: 'backup or temporary file staged',
|
|
72
|
+
test: (relPath) => /\.(bak|tmp|orig|rej|swp|swo|old|save)$/i.test(relPath)
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'scratch_name',
|
|
76
|
+
reason: 'draft/scratch-style file name staged',
|
|
77
|
+
test: (relPath) => {
|
|
78
|
+
const base = path.posix.basename(relPath);
|
|
79
|
+
return /(^|[._-])(draft|scratch|wip|junk|trash|temp|tmp)([._-]|$)/i.test(base);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'local_database',
|
|
84
|
+
reason: 'local database or dump file staged',
|
|
85
|
+
test: (relPath) => /\.(sqlite|sqlite3|db|dump)$/i.test(relPath)
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const CONTENT_RULES = [
|
|
90
|
+
{
|
|
91
|
+
id: 'private_key_block',
|
|
92
|
+
severity: 'error',
|
|
93
|
+
reason: 'private key material detected',
|
|
94
|
+
pattern: /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/m
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'aws_access_key',
|
|
98
|
+
severity: 'error',
|
|
99
|
+
reason: 'AWS access key detected',
|
|
100
|
+
pattern: /\bAKIA[0-9A-Z]{16}\b/
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'github_token',
|
|
104
|
+
severity: 'error',
|
|
105
|
+
reason: 'GitHub token detected',
|
|
106
|
+
pattern: /\b(?:github_pat_[A-Za-z0-9_]{20,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|ghu_[A-Za-z0-9]{20,}|ghs_[A-Za-z0-9]{20,}|ghr_[A-Za-z0-9]{20,})\b/
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'slack_token',
|
|
110
|
+
severity: 'error',
|
|
111
|
+
reason: 'Slack token detected',
|
|
112
|
+
pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'google_api_key',
|
|
116
|
+
severity: 'error',
|
|
117
|
+
reason: 'Google API key detected',
|
|
118
|
+
pattern: /\bAIza[0-9A-Za-z\-_]{35}\b/
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'stripe_secret',
|
|
122
|
+
severity: 'error',
|
|
123
|
+
reason: 'Stripe secret key detected',
|
|
124
|
+
pattern: /\bsk_(?:live|test)_[0-9A-Za-z]{16,}\b/
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'openai_secret',
|
|
128
|
+
severity: 'error',
|
|
129
|
+
reason: 'OpenAI-style secret detected',
|
|
130
|
+
pattern: /\bsk-[A-Za-z0-9]{20,}\b/
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'npm_token',
|
|
134
|
+
severity: 'error',
|
|
135
|
+
reason: 'npm token detected',
|
|
136
|
+
pattern: /\bnpm_[A-Za-z0-9]{20,}\b/
|
|
137
|
+
}
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// Detects literal secret assignments. Quotes are required so that function
|
|
141
|
+
// calls (e.g. `const token = requireToken(config)`) are not flagged: only the
|
|
142
|
+
// value inside the matched quote pair counts toward the 8-char minimum.
|
|
143
|
+
const GENERIC_SECRET_ASSIGNMENT = /\b([A-Z0-9_]*(?:SECRET|TOKEN|API_KEY|ACCESS_KEY|PRIVATE_KEY|PASSWORD|PASSWD|CLIENT_SECRET)[A-Z0-9_]*)\b\s*[:=]\s*(['"`])([^'"`\n\r]{8,})\2/gi;
|
|
144
|
+
const PLACEHOLDER_VALUE = /^(?:example|sample|placeholder|dummy|changeme|change-me|replace[-_]?me|your[_-]?value|your[_-]?token|test|local|localhost|xxx+)$/i;
|
|
145
|
+
|
|
146
|
+
function runGit(gitRoot, args, options = {}) {
|
|
147
|
+
return execFileSync('git', args, {
|
|
148
|
+
cwd: gitRoot,
|
|
149
|
+
encoding: options.encoding || 'utf8',
|
|
150
|
+
maxBuffer: options.maxBuffer || 8 * 1024 * 1024,
|
|
151
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeRelPath(relPath) {
|
|
156
|
+
return String(relPath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveGitRoot(projectDir) {
|
|
160
|
+
return String(runGit(projectDir, ['rev-parse', '--show-toplevel'])).trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveGitPath(gitRoot, gitPath) {
|
|
164
|
+
const resolved = String(runGit(gitRoot, ['rev-parse', '--git-path', gitPath])).trim();
|
|
165
|
+
return path.isAbsolute(resolved) ? resolved : path.join(gitRoot, resolved);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function listStagedFiles(gitRoot) {
|
|
169
|
+
const output = runGit(gitRoot, ['diff', '--cached', '--name-only', '--diff-filter=ACMR', '-z'], {
|
|
170
|
+
encoding: 'buffer'
|
|
171
|
+
});
|
|
172
|
+
return String(output)
|
|
173
|
+
.split('\u0000')
|
|
174
|
+
.map((entry) => normalizeRelPath(entry.trim()))
|
|
175
|
+
.filter(Boolean);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readStagedBlob(gitRoot, relPath) {
|
|
179
|
+
return runGit(gitRoot, ['show', `:${relPath}`], {
|
|
180
|
+
encoding: 'buffer',
|
|
181
|
+
maxBuffer: 16 * 1024 * 1024
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function findLineFromIndex(text, index) {
|
|
186
|
+
if (index == null || index < 0) return null;
|
|
187
|
+
return text.slice(0, index).split('\n').length;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isBinaryBuffer(buffer) {
|
|
191
|
+
for (let i = 0; i < buffer.length; i += 1) {
|
|
192
|
+
if (buffer[i] === 0) return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function escapeRegExp(value) {
|
|
198
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function globToRegExp(pattern) {
|
|
202
|
+
const normalized = normalizeRelPath(pattern).replace(/^\/+/, '');
|
|
203
|
+
let source = '';
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
206
|
+
const char = normalized[i];
|
|
207
|
+
const next = normalized[i + 1];
|
|
208
|
+
|
|
209
|
+
if (char === '*') {
|
|
210
|
+
if (next === '*') {
|
|
211
|
+
source += '.*';
|
|
212
|
+
i += 1;
|
|
213
|
+
} else {
|
|
214
|
+
source += '[^/]*';
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (char === '?') {
|
|
220
|
+
source += '[^/]';
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
source += escapeRegExp(char);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return new RegExp(`^${source}$`, 'i');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function matchesAnyPattern(relPath, patterns) {
|
|
231
|
+
return patterns.some((pattern) => globToRegExp(pattern).test(relPath));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeExtension(value) {
|
|
235
|
+
const ext = String(value || '').trim().toLowerCase();
|
|
236
|
+
if (!ext) return '';
|
|
237
|
+
return ext.startsWith('.') ? ext : `.${ext}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function matchesAnyExtension(relPath, extensions) {
|
|
241
|
+
const lowered = relPath.toLowerCase();
|
|
242
|
+
return extensions.some((ext) => lowered.endsWith(ext));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validateStringArray(data, key) {
|
|
246
|
+
if (data[key] == null) return [];
|
|
247
|
+
if (!Array.isArray(data[key])) {
|
|
248
|
+
throw new Error(`Invalid git guard config: "${key}" must be an array of strings`);
|
|
249
|
+
}
|
|
250
|
+
return data[key].map((item, index) => {
|
|
251
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
252
|
+
throw new Error(`Invalid git guard config: "${key}[${index}]" must be a non-empty string`);
|
|
253
|
+
}
|
|
254
|
+
return key.endsWith('Extensions')
|
|
255
|
+
? normalizeExtension(item)
|
|
256
|
+
: normalizeRelPath(item).replace(/^\/+/, '');
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function resolveGuardConfigPath(projectDir, candidatePath = null) {
|
|
261
|
+
if (!candidatePath) return path.join(projectDir, DEFAULT_CONFIG_REL_PATH);
|
|
262
|
+
return path.isAbsolute(candidatePath)
|
|
263
|
+
? candidatePath
|
|
264
|
+
: path.resolve(projectDir, String(candidatePath));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function loadGuardConfig(projectDir, options = {}) {
|
|
268
|
+
const configPath = resolveGuardConfigPath(projectDir, options.configPath || options.config);
|
|
269
|
+
|
|
270
|
+
let raw;
|
|
271
|
+
try {
|
|
272
|
+
raw = await fs.readFile(configPath, 'utf8');
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error && error.code === 'ENOENT') {
|
|
275
|
+
return {
|
|
276
|
+
path: configPath,
|
|
277
|
+
loaded: false,
|
|
278
|
+
config: { ...DEFAULT_POLICY }
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let parsed;
|
|
285
|
+
try {
|
|
286
|
+
parsed = JSON.parse(raw);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
throw new Error(`Invalid git guard config at ${configPath}: ${error.message}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
292
|
+
throw new Error(`Invalid git guard config at ${configPath}: root value must be an object`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const version = parsed.version == null ? 1 : Number(parsed.version);
|
|
296
|
+
if (!Number.isInteger(version) || version !== 1) {
|
|
297
|
+
throw new Error(`Invalid git guard config at ${configPath}: unsupported version "${parsed.version}"`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (parsed.description != null && typeof parsed.description !== 'string') {
|
|
301
|
+
throw new Error(`Invalid git guard config at ${configPath}: "description" must be a string`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
path: configPath,
|
|
306
|
+
loaded: true,
|
|
307
|
+
config: {
|
|
308
|
+
version,
|
|
309
|
+
description: parsed.description || '',
|
|
310
|
+
allowPaths: validateStringArray(parsed, 'allowPaths'),
|
|
311
|
+
contentAllowPaths: validateStringArray(parsed, 'contentAllowPaths'),
|
|
312
|
+
blockPaths: validateStringArray(parsed, 'blockPaths'),
|
|
313
|
+
allowExtensions: validateStringArray(parsed, 'allowExtensions'),
|
|
314
|
+
blockExtensions: validateStringArray(parsed, 'blockExtensions')
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isAllowlistedPath(relPath, policy) {
|
|
320
|
+
return matchesAnyPattern(relPath, policy.allowPaths) || matchesAnyExtension(relPath, policy.allowExtensions);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isContentAllowlistedPath(relPath, policy) {
|
|
324
|
+
return matchesAnyPattern(relPath, policy.contentAllowPaths || []);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function collectPathFindings(relPath, rules, severity) {
|
|
328
|
+
const findings = [];
|
|
329
|
+
for (const rule of rules) {
|
|
330
|
+
if (!rule.test(relPath)) continue;
|
|
331
|
+
findings.push({
|
|
332
|
+
type: 'path',
|
|
333
|
+
severity,
|
|
334
|
+
id: rule.id,
|
|
335
|
+
path: relPath,
|
|
336
|
+
reason: rule.reason,
|
|
337
|
+
line: null
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return findings;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function collectConfiguredPathFindings(relPath, policy) {
|
|
344
|
+
const findings = [];
|
|
345
|
+
|
|
346
|
+
if (matchesAnyPattern(relPath, policy.blockPaths)) {
|
|
347
|
+
findings.push({
|
|
348
|
+
type: 'path',
|
|
349
|
+
severity: 'error',
|
|
350
|
+
id: 'config_block_path',
|
|
351
|
+
path: relPath,
|
|
352
|
+
reason: 'project git guard policy blocks this path',
|
|
353
|
+
line: null
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (matchesAnyExtension(relPath, policy.blockExtensions)) {
|
|
358
|
+
findings.push({
|
|
359
|
+
type: 'path',
|
|
360
|
+
severity: 'error',
|
|
361
|
+
id: 'config_block_extension',
|
|
362
|
+
path: relPath,
|
|
363
|
+
reason: 'project git guard policy blocks this file extension',
|
|
364
|
+
line: null
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return findings;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function collectContentFindings(relPath, text) {
|
|
372
|
+
const findings = [];
|
|
373
|
+
|
|
374
|
+
for (const rule of CONTENT_RULES) {
|
|
375
|
+
const match = text.match(rule.pattern);
|
|
376
|
+
if (!match || match.index == null) continue;
|
|
377
|
+
findings.push({
|
|
378
|
+
type: 'content',
|
|
379
|
+
severity: rule.severity,
|
|
380
|
+
id: rule.id,
|
|
381
|
+
path: relPath,
|
|
382
|
+
reason: rule.reason,
|
|
383
|
+
line: findLineFromIndex(text, match.index)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let genericMatch;
|
|
388
|
+
GENERIC_SECRET_ASSIGNMENT.lastIndex = 0;
|
|
389
|
+
while ((genericMatch = GENERIC_SECRET_ASSIGNMENT.exec(text)) !== null) {
|
|
390
|
+
const variableName = String(genericMatch[1] || '');
|
|
391
|
+
const value = String(genericMatch[3] || '');
|
|
392
|
+
const lowered = value.toLowerCase();
|
|
393
|
+
if (PLACEHOLDER_VALUE.test(value)) continue;
|
|
394
|
+
if (/(example|sample|dummy|placeholder|changeme|localhost|local[_-]?dev)/i.test(lowered)) continue;
|
|
395
|
+
if (/(public|publishable)/i.test(variableName)) continue;
|
|
396
|
+
findings.push({
|
|
397
|
+
type: 'content',
|
|
398
|
+
severity: 'warning',
|
|
399
|
+
id: 'generic_secret_assignment',
|
|
400
|
+
path: relPath,
|
|
401
|
+
reason: `possible secret assignment detected for ${variableName}`,
|
|
402
|
+
line: findLineFromIndex(text, genericMatch.index)
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return findings;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildSuggestedCommands(findings) {
|
|
410
|
+
const paths = [...new Set(findings.map((item) => item.path).filter(Boolean))];
|
|
411
|
+
if (paths.length === 0) return [];
|
|
412
|
+
return paths.map((relPath) => `git restore --staged -- "${relPath}"`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function summarizePolicy(policyState) {
|
|
416
|
+
return {
|
|
417
|
+
path: policyState.path,
|
|
418
|
+
loaded: policyState.loaded,
|
|
419
|
+
version: policyState.config.version,
|
|
420
|
+
allowPathsCount: policyState.config.allowPaths.length,
|
|
421
|
+
blockPathsCount: policyState.config.blockPaths.length,
|
|
422
|
+
allowExtensionsCount: policyState.config.allowExtensions.length,
|
|
423
|
+
blockExtensionsCount: policyState.config.blockExtensions.length
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function inspectStagedChanges(projectDir, options = {}) {
|
|
428
|
+
const gitRoot = resolveGitRoot(projectDir);
|
|
429
|
+
const policyState = await loadGuardConfig(gitRoot, options);
|
|
430
|
+
const stagedFiles = listStagedFiles(gitRoot);
|
|
431
|
+
const allowWarnings = Boolean(options.allowWarnings);
|
|
432
|
+
const findings = [];
|
|
433
|
+
const files = [];
|
|
434
|
+
|
|
435
|
+
for (const relPath of stagedFiles) {
|
|
436
|
+
const allowlisted = isAllowlistedPath(relPath, policyState.config);
|
|
437
|
+
const fileFindings = [
|
|
438
|
+
...(allowlisted ? [] : collectPathFindings(relPath, BLOCKED_PATH_RULES, 'error')),
|
|
439
|
+
...(allowlisted ? [] : collectPathFindings(relPath, WARNING_PATH_RULES, 'warning')),
|
|
440
|
+
...collectConfiguredPathFindings(relPath, policyState.config)
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
let size = 0;
|
|
444
|
+
let binary = false;
|
|
445
|
+
try {
|
|
446
|
+
const buffer = readStagedBlob(gitRoot, relPath);
|
|
447
|
+
size = buffer.length;
|
|
448
|
+
binary = isBinaryBuffer(buffer);
|
|
449
|
+
if (!binary && !isContentAllowlistedPath(relPath, policyState.config)) {
|
|
450
|
+
const text = buffer.toString('utf8');
|
|
451
|
+
fileFindings.push(...collectContentFindings(relPath, text));
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
fileFindings.push({
|
|
455
|
+
type: 'content',
|
|
456
|
+
severity: 'error',
|
|
457
|
+
id: 'staged_read_failed',
|
|
458
|
+
path: relPath,
|
|
459
|
+
reason: `failed to inspect staged content: ${error.message}`,
|
|
460
|
+
line: null
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
findings.push(...fileFindings);
|
|
465
|
+
files.push({
|
|
466
|
+
path: relPath,
|
|
467
|
+
size,
|
|
468
|
+
binary,
|
|
469
|
+
allowlisted,
|
|
470
|
+
findings: fileFindings
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const errors = findings.filter((item) => item.severity === 'error');
|
|
475
|
+
const warnings = findings.filter((item) => item.severity === 'warning');
|
|
476
|
+
const ok = stagedFiles.length > 0 && errors.length === 0 && (allowWarnings || warnings.length === 0);
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
ok,
|
|
480
|
+
gitRoot,
|
|
481
|
+
stagedFiles,
|
|
482
|
+
strict: !allowWarnings,
|
|
483
|
+
policy: summarizePolicy(policyState),
|
|
484
|
+
files,
|
|
485
|
+
errors,
|
|
486
|
+
warnings,
|
|
487
|
+
suggestedCommands: buildSuggestedCommands([...errors, ...warnings]),
|
|
488
|
+
summary: {
|
|
489
|
+
stagedCount: stagedFiles.length,
|
|
490
|
+
errorCount: errors.length,
|
|
491
|
+
warningCount: warnings.length
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function isManagedHook(content) {
|
|
497
|
+
return String(content || '').includes(MANAGED_HOOK_MARKER);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function buildPreCommitHookScript({ backupPath = '' } = {}) {
|
|
501
|
+
const backupLiteral = backupPath ? JSON.stringify(backupPath) : '""';
|
|
502
|
+
|
|
503
|
+
return `#!/bin/sh
|
|
504
|
+
${MANAGED_HOOK_MARKER}
|
|
505
|
+
# Managed by: aioson git:guard --install-hook
|
|
506
|
+
|
|
507
|
+
set -eu
|
|
508
|
+
|
|
509
|
+
GIT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
510
|
+
HOOK_BACKUP=${backupLiteral}
|
|
511
|
+
|
|
512
|
+
run_guard() {
|
|
513
|
+
if command -v aioson >/dev/null 2>&1; then
|
|
514
|
+
aioson git:guard "$GIT_ROOT"
|
|
515
|
+
return $?
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
if [ -x "$GIT_ROOT/node_modules/.bin/aioson" ]; then
|
|
519
|
+
"$GIT_ROOT/node_modules/.bin/aioson" git:guard "$GIT_ROOT"
|
|
520
|
+
return $?
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
if [ -f "$GIT_ROOT/bin/aioson.js" ]; then
|
|
524
|
+
node "$GIT_ROOT/bin/aioson.js" git:guard "$GIT_ROOT"
|
|
525
|
+
return $?
|
|
526
|
+
fi
|
|
527
|
+
|
|
528
|
+
echo "AIOSON pre-commit hook blocked this commit: aioson CLI was not found." >&2
|
|
529
|
+
echo "Install AIOSON CLI or remove the hook with 'aioson git:guard <project> --uninstall-hook'." >&2
|
|
530
|
+
return 1
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
run_guard
|
|
534
|
+
|
|
535
|
+
if [ -n "$HOOK_BACKUP" ] && [ -x "$HOOK_BACKUP" ]; then
|
|
536
|
+
"$HOOK_BACKUP" "$@"
|
|
537
|
+
fi
|
|
538
|
+
`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function exists(filePath) {
|
|
542
|
+
try {
|
|
543
|
+
await fs.access(filePath);
|
|
544
|
+
return true;
|
|
545
|
+
} catch {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function installPreCommitHook(projectDir, options = {}) {
|
|
551
|
+
const gitRoot = resolveGitRoot(projectDir);
|
|
552
|
+
const hookPath = resolveGitPath(gitRoot, 'hooks/pre-commit');
|
|
553
|
+
const backupPath = resolveGitPath(gitRoot, BACKUP_HOOK_GIT_PATH);
|
|
554
|
+
const dryRun = Boolean(options.dryRun || options['dry-run']);
|
|
555
|
+
const force = Boolean(options.force);
|
|
556
|
+
|
|
557
|
+
let existingContent = null;
|
|
558
|
+
try {
|
|
559
|
+
existingContent = await fs.readFile(hookPath, 'utf8');
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const hookExists = typeof existingContent === 'string';
|
|
565
|
+
const managedExistingHook = isManagedHook(existingContent);
|
|
566
|
+
const backupExists = await exists(backupPath);
|
|
567
|
+
|
|
568
|
+
if (hookExists && !managedExistingHook && !force) {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
error: 'hook_exists',
|
|
572
|
+
message: 'A non-AIOSON pre-commit hook already exists. Re-run with --force to back it up and chain it.',
|
|
573
|
+
gitRoot,
|
|
574
|
+
hookPath,
|
|
575
|
+
backupPath
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (hookExists && !managedExistingHook && force && backupExists) {
|
|
580
|
+
return {
|
|
581
|
+
ok: false,
|
|
582
|
+
error: 'hook_backup_exists',
|
|
583
|
+
message: 'Cannot back up the existing pre-commit hook because an AIOSON backup hook already exists.',
|
|
584
|
+
gitRoot,
|
|
585
|
+
hookPath,
|
|
586
|
+
backupPath
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const shouldChainBackup = backupExists || (hookExists && !managedExistingHook && force);
|
|
591
|
+
const script = buildPreCommitHookScript({
|
|
592
|
+
backupPath: shouldChainBackup ? backupPath : ''
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
if (!dryRun) {
|
|
596
|
+
await fs.mkdir(path.dirname(hookPath), { recursive: true });
|
|
597
|
+
if (hookExists && !managedExistingHook && force) {
|
|
598
|
+
await fs.rename(hookPath, backupPath);
|
|
599
|
+
}
|
|
600
|
+
await fs.writeFile(hookPath, script, 'utf8');
|
|
601
|
+
await fs.chmod(hookPath, 0o755);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
ok: true,
|
|
606
|
+
gitRoot,
|
|
607
|
+
hookPath,
|
|
608
|
+
backupPath,
|
|
609
|
+
installed: !dryRun,
|
|
610
|
+
dryRun,
|
|
611
|
+
replacedExistingHook: hookExists,
|
|
612
|
+
backedUpExistingHook: hookExists && !managedExistingHook && force,
|
|
613
|
+
chainedBackupHook: shouldChainBackup
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function uninstallPreCommitHook(projectDir, options = {}) {
|
|
618
|
+
const gitRoot = resolveGitRoot(projectDir);
|
|
619
|
+
const hookPath = resolveGitPath(gitRoot, 'hooks/pre-commit');
|
|
620
|
+
const backupPath = resolveGitPath(gitRoot, BACKUP_HOOK_GIT_PATH);
|
|
621
|
+
const dryRun = Boolean(options.dryRun || options['dry-run']);
|
|
622
|
+
|
|
623
|
+
let hookContent = null;
|
|
624
|
+
try {
|
|
625
|
+
hookContent = await fs.readFile(hookPath, 'utf8');
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (hookContent == null) {
|
|
631
|
+
return {
|
|
632
|
+
ok: true,
|
|
633
|
+
gitRoot,
|
|
634
|
+
hookPath,
|
|
635
|
+
backupPath,
|
|
636
|
+
removed: false,
|
|
637
|
+
restoredBackup: false,
|
|
638
|
+
message: 'No pre-commit hook is installed.'
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!isManagedHook(hookContent)) {
|
|
643
|
+
return {
|
|
644
|
+
ok: false,
|
|
645
|
+
error: 'hook_not_managed',
|
|
646
|
+
message: 'The current pre-commit hook is not managed by AIOSON.',
|
|
647
|
+
gitRoot,
|
|
648
|
+
hookPath,
|
|
649
|
+
backupPath
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const backupExists = await exists(backupPath);
|
|
654
|
+
if (!dryRun) {
|
|
655
|
+
await fs.unlink(hookPath);
|
|
656
|
+
if (backupExists) {
|
|
657
|
+
await fs.rename(backupPath, hookPath);
|
|
658
|
+
await fs.chmod(hookPath, 0o755);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
ok: true,
|
|
664
|
+
gitRoot,
|
|
665
|
+
hookPath,
|
|
666
|
+
backupPath,
|
|
667
|
+
removed: !dryRun,
|
|
668
|
+
restoredBackup: backupExists,
|
|
669
|
+
dryRun
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
module.exports = {
|
|
674
|
+
inspectStagedChanges,
|
|
675
|
+
resolveGitRoot,
|
|
676
|
+
resolveGitPath,
|
|
677
|
+
resolveGuardConfigPath,
|
|
678
|
+
loadGuardConfig,
|
|
679
|
+
listStagedFiles,
|
|
680
|
+
readStagedBlob,
|
|
681
|
+
normalizeRelPath,
|
|
682
|
+
installPreCommitHook,
|
|
683
|
+
uninstallPreCommitHook,
|
|
684
|
+
buildPreCommitHookScript,
|
|
685
|
+
isManagedHook,
|
|
686
|
+
BLOCKED_PATH_RULES,
|
|
687
|
+
WARNING_PATH_RULES,
|
|
688
|
+
DEFAULT_CONFIG_REL_PATH,
|
|
689
|
+
BACKUP_HOOK_GIT_PATH,
|
|
690
|
+
MANAGED_HOOK_MARKER
|
|
691
|
+
};
|