@seedgrid/ai-toolkit 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +151 -2
- package/dist/adapters/claude.js +114 -0
- package/dist/adapters/codex.js +108 -0
- package/dist/adapters/index.js +17 -0
- package/dist/adapters/shared.js +142 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli.js +203 -0
- package/dist/commands/common.js +98 -0
- package/dist/commands/doctor.js +290 -0
- package/dist/commands/init.js +9 -0
- package/dist/commands/install.js +107 -0
- package/dist/commands/remove.js +54 -0
- package/dist/commands/review.js +127 -0
- package/dist/commands/status.js +73 -0
- package/dist/commands/update.js +8 -0
- package/dist/commands/validate.js +27 -0
- package/dist/core/apply.js +181 -0
- package/dist/core/blocks.js +64 -0
- package/dist/core/content.js +528 -0
- package/dist/core/env.js +40 -0
- package/dist/core/frontmatter.js +29 -0
- package/dist/core/hash.js +14 -0
- package/dist/core/inspect.js +51 -0
- package/dist/core/lock.js +124 -0
- package/dist/core/paths.js +65 -0
- package/dist/core/plan.js +242 -0
- package/dist/core/tools.js +62 -0
- package/dist/core/types.js +11 -0
- package/dist/core/updates.js +121 -0
- package/docs/arquitetura.md +80 -0
- package/docs/compatibilidade.md +50 -0
- package/docs/formato.md +155 -0
- package/docs/instalacao.md +105 -0
- package/docs/manutencao.md +115 -0
- package/package.json +43 -4
- package/seedgridKB/SeedGrid/Extensions/Catalogo-de-enums.md +161 -0
- package/seedgridKB/SeedGrid/Extensions/Catalogo-de-propriedades.md +971 -0
- package/seedgridKB/SeedGrid/Extensions/Indice.md +63 -0
- package/seedgridKB/SeedGrid/Extensions/ai-pricing/Configuracao.md +124 -0
- package/seedgridKB/SeedGrid/Extensions/ai-pricing/Enums.md +40 -0
- package/seedgridKB/SeedGrid/Extensions/ai-pricing/Exemplos.md +97 -0
- package/seedgridKB/SeedGrid/Extensions/ai-pricing/Visao-geral.md +114 -0
- package/seedgridKB/SeedGrid/Extensions/apikeys/Configuracao-Rust.md +69 -0
- package/seedgridKB/SeedGrid/Extensions/apikeys/Configuracao.md +88 -0
- package/seedgridKB/SeedGrid/Extensions/apikeys/Enums.md +33 -0
- package/seedgridKB/SeedGrid/Extensions/apikeys/Exemplos.md +121 -0
- package/seedgridKB/SeedGrid/Extensions/apikeys/Rust.md +126 -0
- package/seedgridKB/SeedGrid/Extensions/apikeys/Visao-geral.md +111 -0
- package/seedgridKB/SeedGrid/Extensions/audit/Configuracao-Rust.md +93 -0
- package/seedgridKB/SeedGrid/Extensions/audit/Configuracao.md +127 -0
- package/seedgridKB/SeedGrid/Extensions/audit/Enums.md +66 -0
- package/seedgridKB/SeedGrid/Extensions/audit/Exemplos.md +151 -0
- package/seedgridKB/SeedGrid/Extensions/audit/Rust.md +120 -0
- package/seedgridKB/SeedGrid/Extensions/audit/Visao-geral.md +114 -0
- package/seedgridKB/SeedGrid/Extensions/backup/Configuracao-Rust.md +136 -0
- package/seedgridKB/SeedGrid/Extensions/backup/Configuracao.md +319 -0
- package/seedgridKB/SeedGrid/Extensions/backup/Enums.md +131 -0
- package/seedgridKB/SeedGrid/Extensions/backup/Exemplos.md +210 -0
- package/seedgridKB/SeedGrid/Extensions/backup/Rust.md +135 -0
- package/seedgridKB/SeedGrid/Extensions/backup/Visao-geral.md +150 -0
- package/seedgridKB/SeedGrid/Extensions/billing/Configuracao-Rust.md +86 -0
- package/seedgridKB/SeedGrid/Extensions/billing/Configuracao.md +243 -0
- package/seedgridKB/SeedGrid/Extensions/billing/Enums.md +165 -0
- package/seedgridKB/SeedGrid/Extensions/billing/Exemplos.md +205 -0
- package/seedgridKB/SeedGrid/Extensions/billing/Rust.md +109 -0
- package/seedgridKB/SeedGrid/Extensions/billing/Visao-geral.md +181 -0
- package/seedgridKB/SeedGrid/Extensions/broker/Configuracao-Rust.md +40 -0
- package/seedgridKB/SeedGrid/Extensions/broker/Visao-geral.md +114 -0
- package/seedgridKB/SeedGrid/Extensions/cache/Configuracao-Rust.md +73 -0
- package/seedgridKB/SeedGrid/Extensions/cache/Configuracao.md +112 -0
- package/seedgridKB/SeedGrid/Extensions/cache/Enums.md +36 -0
- package/seedgridKB/SeedGrid/Extensions/cache/Exemplos.md +149 -0
- package/seedgridKB/SeedGrid/Extensions/cache/Rust.md +81 -0
- package/seedgridKB/SeedGrid/Extensions/cache/Visao-geral.md +90 -0
- package/seedgridKB/SeedGrid/Extensions/commons/Configuracao.md +107 -0
- package/seedgridKB/SeedGrid/Extensions/commons/Enums.md +105 -0
- package/seedgridKB/SeedGrid/Extensions/commons/Exemplos.md +141 -0
- package/seedgridKB/SeedGrid/Extensions/commons/Visao-geral.md +99 -0
- package/seedgridKB/SeedGrid/Extensions/exchange/Configuracao.md +122 -0
- package/seedgridKB/SeedGrid/Extensions/exchange/Enums.md +29 -0
- package/seedgridKB/SeedGrid/Extensions/exchange/Exemplos.md +82 -0
- package/seedgridKB/SeedGrid/Extensions/exchange/Visao-geral.md +118 -0
- package/seedgridKB/SeedGrid/Extensions/filemanagement/Configuracao-Rust.md +110 -0
- package/seedgridKB/SeedGrid/Extensions/filemanagement/Configuracao.md +157 -0
- package/seedgridKB/SeedGrid/Extensions/filemanagement/Exemplos.md +157 -0
- package/seedgridKB/SeedGrid/Extensions/filemanagement/Rust.md +70 -0
- package/seedgridKB/SeedGrid/Extensions/filemanagement/Visao-geral.md +88 -0
- package/seedgridKB/SeedGrid/Extensions/login-tenancy/Configuracao-Rust.md +124 -0
- package/seedgridKB/SeedGrid/Extensions/login-tenancy/Configuracao.md +195 -0
- package/seedgridKB/SeedGrid/Extensions/login-tenancy/Enums.md +71 -0
- package/seedgridKB/SeedGrid/Extensions/login-tenancy/Exemplos.md +149 -0
- package/seedgridKB/SeedGrid/Extensions/login-tenancy/Rust.md +139 -0
- package/seedgridKB/SeedGrid/Extensions/login-tenancy/Visao-geral.md +130 -0
- package/seedgridKB/SeedGrid/Extensions/notification/Configuracao.md +141 -0
- package/seedgridKB/SeedGrid/Extensions/notification/Enums.md +52 -0
- package/seedgridKB/SeedGrid/Extensions/notification/Exemplos.md +141 -0
- package/seedgridKB/SeedGrid/Extensions/notification/Visao-geral.md +113 -0
- package/seedgridKB/SeedGrid/Extensions/privacy/Configuracao-Rust.md +67 -0
- package/seedgridKB/SeedGrid/Extensions/privacy/Configuracao.md +94 -0
- package/seedgridKB/SeedGrid/Extensions/privacy/Enums.md +79 -0
- package/seedgridKB/SeedGrid/Extensions/privacy/Exemplos.md +172 -0
- package/seedgridKB/SeedGrid/Extensions/privacy/Rust.md +107 -0
- package/seedgridKB/SeedGrid/Extensions/privacy/Visao-geral.md +90 -0
- package/seedgridKB/SeedGrid/Extensions/realtime/Configuracao-Rust.md +72 -0
- package/seedgridKB/SeedGrid/Extensions/realtime/Configuracao.md +104 -0
- package/seedgridKB/SeedGrid/Extensions/realtime/Exemplos.md +104 -0
- package/seedgridKB/SeedGrid/Extensions/realtime/Rust.md +69 -0
- package/seedgridKB/SeedGrid/Extensions/realtime/Visao-geral.md +105 -0
- package/seedgridKB/SeedGrid/Extensions/resilience/Configuracao-Rust.md +68 -0
- package/seedgridKB/SeedGrid/Extensions/resilience/Configuracao.md +186 -0
- package/seedgridKB/SeedGrid/Extensions/resilience/Enums.md +96 -0
- package/seedgridKB/SeedGrid/Extensions/resilience/Exemplos.md +229 -0
- package/seedgridKB/SeedGrid/Extensions/resilience/Rust.md +80 -0
- package/seedgridKB/SeedGrid/Extensions/resilience/Visao-geral.md +112 -0
- package/seedgridKB/SeedGrid/Extensions/runtime/Configuracao-Rust.md +90 -0
- package/seedgridKB/SeedGrid/Extensions/runtime/Visao-geral.md +114 -0
- package/seedgridKB/SeedGrid/Extensions/security/Configuracao-Rust.md +327 -0
- package/seedgridKB/SeedGrid/Extensions/security/Configuracao.md +510 -0
- package/seedgridKB/SeedGrid/Extensions/security/Enums.md +95 -0
- package/seedgridKB/SeedGrid/Extensions/security/Exemplos.md +218 -0
- package/seedgridKB/SeedGrid/Extensions/security/Rust.md +203 -0
- package/seedgridKB/SeedGrid/Extensions/security/Visao-geral.md +131 -0
- package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Configuracao-Rust.md +188 -0
- package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Configuracao.md +345 -0
- package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Enums.md +117 -0
- package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Exemplos.md +205 -0
- package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Rust.md +161 -0
- package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Visao-geral.md +142 -0
- package/seedgridKB/SeedGrid/Extensions/whatsapp/Configuracao-Rust.md +75 -0
- package/seedgridKB/SeedGrid/Extensions/whatsapp/Configuracao.md +152 -0
- package/seedgridKB/SeedGrid/Extensions/whatsapp/Enums.md +198 -0
- package/seedgridKB/SeedGrid/Extensions/whatsapp/Exemplos.md +173 -0
- package/seedgridKB/SeedGrid/Extensions/whatsapp/Rust.md +83 -0
- package/seedgridKB/SeedGrid/Extensions/whatsapp/Visao-geral.md +99 -0
- package/seedgridKB/SeedGrid/Mapa-dos-repositorios.md +112 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/Indice.md +323 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/buttons/API.md +257 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/buttons/Exemplos.md +307 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/buttons/Visao-geral.md +51 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/datatable/API.md +174 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/datatable/Exemplos.md +236 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/datatable/Visao-geral.md +76 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/digits/API.md +339 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/digits/Exemplos.md +311 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/digits/Visao-geral.md +57 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/editor/API.md +125 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/editor/Exemplos.md +116 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/editor/Visao-geral.md +86 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/feedback-e-exibicao/API.md +490 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/feedback-e-exibicao/Exemplos.md +544 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/feedback-e-exibicao/Visao-geral.md +69 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/fundamentos/API.md +113 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/fundamentos/Exemplos.md +159 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/fundamentos/Visao-geral.md +92 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/gadgets/API.md +516 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/gadgets/Exemplos.md +629 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/gadgets/Visao-geral.md +80 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/inputs-selecao/API.md +615 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/inputs-selecao/Exemplos.md +384 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/inputs-selecao/Visao-geral.md +111 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/inputs-texto/API.md +661 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/inputs-texto/Exemplos.md +312 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/inputs-texto/Visao-geral.md +123 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/layout/API.md +1090 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/layout/Exemplos.md +1172 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/layout/Visao-geral.md +98 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/Visao-geral.md +98 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/fe-apikeys.md +118 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/fe-audit.md +88 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/fe-backup.md +131 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/fe-billing-platform.md +132 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/fe-billing-user.md +120 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/fe-login-tenancy.md +119 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/fe-security.md +427 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/fe-self-enter.md +104 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/modulos/fe-subdomain-tenancy.md +110 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/navegacao-e-overlay/API.md +490 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/navegacao-e-overlay/Exemplos.md +561 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/navegacao-e-overlay/Visao-geral.md +82 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/pacotes/Visao-geral.md +68 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/pacotes/fe-commons.md +61 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/pacotes/fe-core.md +140 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/pacotes/fe-playground.md +112 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/pacotes/fe-theme.md +97 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/providers/API.md +177 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/providers/Exemplos.md +332 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/providers/Visao-geral.md +106 -0
- package/seedgridKB/SeedGrid/SeedGrid-components/showcase.md +70 -0
- package/seedgridKB/SeedGrid/Toolkit/agents/arquiteto.md +53 -0
- package/seedgridKB/SeedGrid/Toolkit/agents/backend-java.md +45 -0
- package/seedgridKB/SeedGrid/Toolkit/agents/backend-rust.md +43 -0
- package/seedgridKB/SeedGrid/Toolkit/agents/frontend-react.md +41 -0
- package/seedgridKB/SeedGrid/Toolkit/agents/qa.md +44 -0
- package/seedgridKB/SeedGrid/Toolkit/agents/revisor.md +40 -0
- package/seedgridKB/SeedGrid/Toolkit/references/fluxo-de-trabalho.md +78 -0
- package/seedgridKB/SeedGrid/Toolkit/references/glossario.md +15 -0
- package/seedgridKB/SeedGrid/Toolkit/references/modos-shared-native.md +27 -0
- package/seedgridKB/SeedGrid/Toolkit/rules/backend-java-seedgrid.md +54 -0
- package/seedgridKB/SeedGrid/Toolkit/rules/coordenacao.md +39 -0
- package/seedgridKB/SeedGrid/Toolkit/rules/formato-entrega.md +29 -0
- package/seedgridKB/SeedGrid/Toolkit/rules/frontend-seedgrid.md +47 -0
- package/seedgridKB/SeedGrid/Toolkit/rules/personalizacao-local.md +22 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/planejar-mudanca/SKILL.md +38 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/revisar-codigo/SKILL.md +33 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/revisar-codigo/references/checklist.md +25 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-backend-crud/SKILL.md +29 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-bump/SKILL.md +26 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-crud/SKILL.md +221 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-crud/agents/openai.yaml +4 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-crud/references/checklist.md +31 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-crud/references/patterns.md +185 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-crud/references/routing.md +66 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-new-screen/SKILL.md +56 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-new-screen/agents/openai.yaml +4 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-new-screen/references/checklist.md +32 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-new-screen/references/steps.md +114 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-review/SKILL.md +61 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/validar-mudanca/SKILL.md +29 -0
- package/seedgridKB/SeedGrid/Toolkit/templates/decisao-arquitetura.md +29 -0
- package/seedgridKB/SeedGrid/Toolkit/templates/especificacao-tarefa.md +32 -0
- package/seedgridKB/SeedGrid/Toolkit/templates/relatorio-validacao.md +30 -0
- package/seedgridKB/SeedGrid/Versoes.md +64 -0
- package/seedgridKB/SeedGrid/Visao-geral.md +35 -0
- package/seedgridKB/manifest.json +142 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isSha256 } from './hash.js';
|
|
4
|
+
import { isSafeRelPath } from './paths.js';
|
|
5
|
+
import { ALL_MODES, ALL_TARGETS, BLOCK_FILES } from './types.js';
|
|
6
|
+
export const LOCK_PATH = '.ai/toolkit-lock.json';
|
|
7
|
+
export const LOCK_VERSION = 1;
|
|
8
|
+
export const TOOL_NAME = '@seedgrid/ai-toolkit';
|
|
9
|
+
/** Prefixos onde o toolkit pode gerenciar arquivos. Qualquer outro caminho no lock é rejeitado. */
|
|
10
|
+
export const MANAGED_PREFIXES = ['.ai/', '.claude/skills/', '.claude/agents/', '.agents/skills/', '.codex/agents/'];
|
|
11
|
+
/** Áreas de .ai/ que nunca são gerenciadas. */
|
|
12
|
+
export const UNMANAGED_AI_PREFIXES = ['.ai/local/', '.ai/backups/'];
|
|
13
|
+
export function isManagedPath(p) {
|
|
14
|
+
if (!isSafeRelPath(p) || p === LOCK_PATH)
|
|
15
|
+
return false;
|
|
16
|
+
if (UNMANAGED_AI_PREFIXES.some((u) => p.startsWith(u)))
|
|
17
|
+
return false;
|
|
18
|
+
return MANAGED_PREFIXES.some((m) => p.startsWith(m) && p.length > m.length);
|
|
19
|
+
}
|
|
20
|
+
function isRecord(v) {
|
|
21
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
22
|
+
}
|
|
23
|
+
/** Valida o lock como dado não confiável antes de usá-lo para escrever ou remover. */
|
|
24
|
+
export function validateLock(data) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
if (!isRecord(data))
|
|
27
|
+
return { errors: ['o lock deve ser um objeto JSON'] };
|
|
28
|
+
if (data.lockVersion !== LOCK_VERSION)
|
|
29
|
+
errors.push(`lockVersion não suportado: ${String(data.lockVersion)}`);
|
|
30
|
+
if (data.tool !== TOOL_NAME)
|
|
31
|
+
errors.push(`tool inesperado: ${String(data.tool)}`);
|
|
32
|
+
for (const k of ['cliVersion', 'contentVersion']) {
|
|
33
|
+
if (typeof data[k] !== 'string')
|
|
34
|
+
errors.push(`${k} ausente`);
|
|
35
|
+
}
|
|
36
|
+
if (typeof data.contentSchemaVersion !== 'number')
|
|
37
|
+
errors.push('contentSchemaVersion ausente');
|
|
38
|
+
if (!Array.isArray(data.targets) ||
|
|
39
|
+
data.targets.length === 0 ||
|
|
40
|
+
!data.targets.every((t) => ALL_TARGETS.includes(t))) {
|
|
41
|
+
errors.push('targets inválido');
|
|
42
|
+
}
|
|
43
|
+
if (!ALL_MODES.includes(data.mode))
|
|
44
|
+
errors.push('mode inválido');
|
|
45
|
+
const src = data.source;
|
|
46
|
+
if (!isRecord(src) || (src.type !== 'bundled' && src.type !== 'local'))
|
|
47
|
+
errors.push('source inválido');
|
|
48
|
+
else if (src.path !== undefined && !isSafeRelPath(src.path))
|
|
49
|
+
errors.push('source.path deve ser relativo ao projeto');
|
|
50
|
+
if (!isRecord(data.lastOperation))
|
|
51
|
+
errors.push('lastOperation ausente');
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
if (!Array.isArray(data.files))
|
|
54
|
+
errors.push('files deve ser uma lista');
|
|
55
|
+
else {
|
|
56
|
+
for (const f of data.files) {
|
|
57
|
+
if (!isRecord(f) || typeof f.path !== 'string' || !isManagedPath(f.path)) {
|
|
58
|
+
errors.push(`entrada de arquivo inválida ou fora das áreas gerenciadas: ${JSON.stringify(isRecord(f) ? f.path : f)}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!isSha256(f.sha256))
|
|
62
|
+
errors.push(`hash inválido para ${f.path}`);
|
|
63
|
+
if (typeof f.component !== 'string')
|
|
64
|
+
errors.push(`component ausente para ${f.path}`);
|
|
65
|
+
const key = f.path.toLowerCase(); // Windows/macOS não diferenciam maiúsculas
|
|
66
|
+
if (seen.has(key))
|
|
67
|
+
errors.push(`arquivo repetido no lock: ${f.path}`);
|
|
68
|
+
seen.add(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(data.blocks))
|
|
72
|
+
errors.push('blocks deve ser uma lista');
|
|
73
|
+
else {
|
|
74
|
+
const seenBlocks = new Set();
|
|
75
|
+
for (const b of data.blocks) {
|
|
76
|
+
if (!isRecord(b) || !BLOCK_FILES.includes(b.file)) {
|
|
77
|
+
errors.push(`bloco inválido: ${JSON.stringify(b)}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!isSha256(b.sha256))
|
|
81
|
+
errors.push(`hash inválido para bloco em ${String(b.file)}`);
|
|
82
|
+
if (typeof b.createdFile !== 'boolean')
|
|
83
|
+
errors.push(`createdFile ausente para ${String(b.file)}`);
|
|
84
|
+
if (seenBlocks.has(b.file))
|
|
85
|
+
errors.push(`bloco repetido: ${String(b.file)}`);
|
|
86
|
+
seenBlocks.add(b.file);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return errors.length > 0 ? { errors } : { lock: data, errors };
|
|
90
|
+
}
|
|
91
|
+
export async function readLock(projectRoot) {
|
|
92
|
+
let raw;
|
|
93
|
+
try {
|
|
94
|
+
raw = await readFile(path.join(projectRoot, ...LOCK_PATH.split('/')), 'utf8');
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err.code === 'ENOENT')
|
|
98
|
+
return { status: 'absent' };
|
|
99
|
+
return { status: 'invalid', errors: [`não foi possível ler o lock: ${err.message}`], raw: '' };
|
|
100
|
+
}
|
|
101
|
+
let data;
|
|
102
|
+
try {
|
|
103
|
+
data = JSON.parse(raw);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return { status: 'invalid', errors: [`JSON inválido: ${err.message}`], raw };
|
|
107
|
+
}
|
|
108
|
+
const { lock, errors } = validateLock(data);
|
|
109
|
+
return lock ? { status: 'ok', lock, raw } : { status: 'invalid', errors, raw };
|
|
110
|
+
}
|
|
111
|
+
export function serializeLock(lock) {
|
|
112
|
+
const sorted = {
|
|
113
|
+
...lock,
|
|
114
|
+
targets: [...lock.targets].sort(),
|
|
115
|
+
files: [...lock.files].sort((a, b) => a.path.localeCompare(b.path)),
|
|
116
|
+
blocks: [...lock.blocks].sort((a, b) => a.file.localeCompare(b.file)),
|
|
117
|
+
};
|
|
118
|
+
return `${JSON.stringify(sorted, null, 2)}\n`;
|
|
119
|
+
}
|
|
120
|
+
/** Igualdade semântica, ignorando a última operação e a versão do CLI. */
|
|
121
|
+
export function sameLockContent(a, b) {
|
|
122
|
+
const strip = (l) => serializeLock({ ...l, lastOperation: { id: '', kind: '', at: '' }, cliVersion: '' });
|
|
123
|
+
return strip(a) === strip(b);
|
|
124
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ToolkitError } from './types.js';
|
|
4
|
+
const MAX_REL_PATH = 400;
|
|
5
|
+
/**
|
|
6
|
+
* Caminho relativo portável: separador "/", sem raiz, sem letra de unidade,
|
|
7
|
+
* sem segmentos vazios, "." ou "..".
|
|
8
|
+
*/
|
|
9
|
+
export function isSafeRelPath(value) {
|
|
10
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_REL_PATH)
|
|
11
|
+
return false;
|
|
12
|
+
if (value.includes('\\') || value.includes('\0') || value.includes(':'))
|
|
13
|
+
return false;
|
|
14
|
+
if (value.startsWith('/'))
|
|
15
|
+
return false;
|
|
16
|
+
return value.split('/').every((seg) => seg !== '' && seg !== '.' && seg !== '..');
|
|
17
|
+
}
|
|
18
|
+
export function toPosix(p) {
|
|
19
|
+
return p.split(path.sep).join('/');
|
|
20
|
+
}
|
|
21
|
+
export function isInside(parent, child) {
|
|
22
|
+
const rel = path.relative(parent, child);
|
|
23
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
24
|
+
}
|
|
25
|
+
/** Resolve um caminho relativo do projeto, rejeitando escapes. */
|
|
26
|
+
export function resolveInProject(projectRoot, rel) {
|
|
27
|
+
if (!isSafeRelPath(rel)) {
|
|
28
|
+
throw new ToolkitError(`Caminho inválido ou fora do projeto: ${JSON.stringify(rel)}`);
|
|
29
|
+
}
|
|
30
|
+
const abs = path.resolve(projectRoot, ...rel.split('/'));
|
|
31
|
+
if (!isInside(projectRoot, abs) || abs === projectRoot) {
|
|
32
|
+
throw new ToolkitError(`Caminho fora do projeto: ${rel}`);
|
|
33
|
+
}
|
|
34
|
+
return abs;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Garante que nenhum componente existente do caminho seja um link simbólico
|
|
38
|
+
* que leve para fora do projeto e que o arquivo final não seja um link.
|
|
39
|
+
* Retorna a descrição do problema ou null.
|
|
40
|
+
*/
|
|
41
|
+
export async function checkSymlinkSafety(projectRoot, rel) {
|
|
42
|
+
const rootReal = await realpath(projectRoot);
|
|
43
|
+
const segments = rel.split('/');
|
|
44
|
+
let current = projectRoot;
|
|
45
|
+
for (let i = 0; i < segments.length; i++) {
|
|
46
|
+
current = path.join(current, segments[i]);
|
|
47
|
+
let st;
|
|
48
|
+
try {
|
|
49
|
+
st = await lstat(current);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null; // o restante do caminho ainda não existe
|
|
53
|
+
}
|
|
54
|
+
if (st.isSymbolicLink()) {
|
|
55
|
+
if (i === segments.length - 1) {
|
|
56
|
+
return `${rel}: o destino é um link simbólico; o toolkit não escreve através de links.`;
|
|
57
|
+
}
|
|
58
|
+
const target = await realpath(current).catch(() => null);
|
|
59
|
+
if (!target || !isInside(rootReal, target)) {
|
|
60
|
+
return `${rel}: ${segments.slice(0, i + 1).join('/')} é um link simbólico que aponta para fora do projeto.`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
2
|
+
import { canonicalInner, removeBlock, scanBlock, upsertBlock } from './blocks.js';
|
|
3
|
+
import { sha256 } from './hash.js';
|
|
4
|
+
import { isManagedPath, LOCK_PATH, LOCK_VERSION, sameLockContent, serializeLock, TOOL_NAME, } from './lock.js';
|
|
5
|
+
import { checkSymlinkSafety, resolveInProject } from './paths.js';
|
|
6
|
+
import { ToolkitError } from './types.js';
|
|
7
|
+
async function readCurrent(projectRoot, rel) {
|
|
8
|
+
const symlinkProblem = await checkSymlinkSafety(projectRoot, rel);
|
|
9
|
+
if (symlinkProblem)
|
|
10
|
+
return { exists: true, problem: symlinkProblem };
|
|
11
|
+
const abs = resolveInProject(projectRoot, rel);
|
|
12
|
+
try {
|
|
13
|
+
const st = await lstat(abs);
|
|
14
|
+
if (!st.isFile())
|
|
15
|
+
return { exists: true, problem: `${rel} existe e não é um arquivo regular` };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { exists: false };
|
|
19
|
+
}
|
|
20
|
+
const text = await readFile(abs, 'utf8');
|
|
21
|
+
return { exists: true, text, hash: sha256(text) };
|
|
22
|
+
}
|
|
23
|
+
const WRITE_FILE_ACTIONS = ['create', 'update', 'delete'];
|
|
24
|
+
const WRITE_BLOCK_ACTIONS = ['create-file', 'insert', 'update', 'remove', 'delete-file'];
|
|
25
|
+
function finish(plan) {
|
|
26
|
+
const changes = plan.fileOps.filter((o) => WRITE_FILE_ACTIONS.includes(o.action)).length +
|
|
27
|
+
plan.blockOps.filter((o) => WRITE_BLOCK_ACTIONS.includes(o.action)).length +
|
|
28
|
+
(plan.lockOp.action === 'unchanged' ? 0 : 1);
|
|
29
|
+
return { ...plan, changes };
|
|
30
|
+
}
|
|
31
|
+
async function planBlockRemoval(projectRoot, entry, conflicts) {
|
|
32
|
+
const cur = await readCurrent(projectRoot, entry.file);
|
|
33
|
+
if (cur.problem) {
|
|
34
|
+
conflicts.push({ path: entry.file, reason: cur.problem });
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (!cur.exists)
|
|
38
|
+
return { file: entry.file, action: 'forget', reason: 'arquivo já não existe', expectedHash: null };
|
|
39
|
+
const scan = scanBlock(cur.text);
|
|
40
|
+
if (scan.status === 'invalid') {
|
|
41
|
+
conflicts.push({ path: entry.file, reason: `bloco gerenciado inválido: ${scan.reason}` });
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (scan.status === 'none') {
|
|
45
|
+
return { file: entry.file, action: 'forget', reason: 'bloco já não existe', expectedHash: cur.hash };
|
|
46
|
+
}
|
|
47
|
+
if (sha256(scan.inner) !== entry.sha256) {
|
|
48
|
+
conflicts.push({ path: entry.file, reason: 'o bloco gerenciado foi editado localmente' });
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const newText = removeBlock(cur.text);
|
|
52
|
+
if (entry.createdFile && newText.trim() === '') {
|
|
53
|
+
return { file: entry.file, action: 'delete-file', reason: 'arquivo criado pelo toolkit ficou vazio', expectedHash: cur.hash };
|
|
54
|
+
}
|
|
55
|
+
return { file: entry.file, action: 'remove', reason: 'remove somente o bloco gerenciado', newText, expectedHash: cur.hash };
|
|
56
|
+
}
|
|
57
|
+
async function planFileRemoval(projectRoot, entry, conflicts, why) {
|
|
58
|
+
const cur = await readCurrent(projectRoot, entry.path);
|
|
59
|
+
if (cur.problem) {
|
|
60
|
+
conflicts.push({ path: entry.path, reason: cur.problem });
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (!cur.exists)
|
|
64
|
+
return { path: entry.path, action: 'forget', reason: 'arquivo já não existe', expectedHash: null };
|
|
65
|
+
if (cur.hash !== entry.sha256) {
|
|
66
|
+
conflicts.push({ path: entry.path, reason: `${why}, mas foi modificado localmente` });
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return { path: entry.path, action: 'delete', reason: why, expectedHash: cur.hash };
|
|
70
|
+
}
|
|
71
|
+
/** Planeja init/update. Não escreve nada. */
|
|
72
|
+
export async function planInstall(projectRoot, desired, lockState, meta) {
|
|
73
|
+
const conflicts = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
const fileOps = [];
|
|
76
|
+
const blockOps = [];
|
|
77
|
+
if (lockState.status === 'invalid') {
|
|
78
|
+
conflicts.push({ path: LOCK_PATH, reason: `lock inválido: ${lockState.errors.join('; ')}` });
|
|
79
|
+
}
|
|
80
|
+
const prevLock = lockState.status === 'ok' ? lockState.lock : null;
|
|
81
|
+
const prevFiles = new Map((prevLock?.files ?? []).map((f) => [f.path, f]));
|
|
82
|
+
const prevBlocks = new Map((prevLock?.blocks ?? []).map((b) => [b.file, b]));
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
for (const d of desired.files) {
|
|
85
|
+
if (!isManagedPath(d.path))
|
|
86
|
+
throw new ToolkitError(`Destino fora das áreas gerenciadas: ${d.path}`);
|
|
87
|
+
const key = d.path.toLowerCase();
|
|
88
|
+
if (seen.has(key))
|
|
89
|
+
throw new ToolkitError(`Destino duplicado no plano: ${d.path}`);
|
|
90
|
+
seen.add(key);
|
|
91
|
+
}
|
|
92
|
+
const newFiles = [];
|
|
93
|
+
for (const d of desired.files) {
|
|
94
|
+
const desiredHash = sha256(d.content);
|
|
95
|
+
newFiles.push({ path: d.path, sha256: desiredHash, component: d.component });
|
|
96
|
+
const cur = await readCurrent(projectRoot, d.path);
|
|
97
|
+
if (cur.problem) {
|
|
98
|
+
conflicts.push({ path: d.path, reason: cur.problem });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const prev = prevFiles.get(d.path);
|
|
102
|
+
if (!cur.exists) {
|
|
103
|
+
fileOps.push({ path: d.path, action: 'create', reason: prev ? 'arquivo gerenciado ausente' : 'novo', content: d.content, expectedHash: null });
|
|
104
|
+
}
|
|
105
|
+
else if (cur.hash === desiredHash) {
|
|
106
|
+
fileOps.push({
|
|
107
|
+
path: d.path,
|
|
108
|
+
action: prev ? 'unchanged' : 'adopt',
|
|
109
|
+
reason: prev ? 'sem mudanças' : 'arquivo existente idêntico passa a ser gerenciado',
|
|
110
|
+
expectedHash: cur.hash,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else if (prev && cur.hash === prev.sha256) {
|
|
114
|
+
fileOps.push({ path: d.path, action: 'update', reason: 'nova versão do conteúdo', content: d.content, expectedHash: cur.hash });
|
|
115
|
+
}
|
|
116
|
+
else if (prev) {
|
|
117
|
+
conflicts.push({ path: d.path, reason: 'modificado localmente desde a última instalação' });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
conflicts.push({ path: d.path, reason: 'arquivo existente não gerenciado pelo toolkit, com conteúdo diferente' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const desiredPaths = new Set(desired.files.map((d) => d.path));
|
|
124
|
+
for (const prev of prevFiles.values()) {
|
|
125
|
+
if (desiredPaths.has(prev.path))
|
|
126
|
+
continue;
|
|
127
|
+
const op = await planFileRemoval(projectRoot, prev, conflicts, 'não faz mais parte da instalação');
|
|
128
|
+
if (op)
|
|
129
|
+
fileOps.push(op);
|
|
130
|
+
}
|
|
131
|
+
const newBlocks = [];
|
|
132
|
+
for (const b of desired.blocks) {
|
|
133
|
+
const inner = canonicalInner(b.inner);
|
|
134
|
+
const desiredHash = sha256(inner);
|
|
135
|
+
const prev = prevBlocks.get(b.file);
|
|
136
|
+
const cur = await readCurrent(projectRoot, b.file);
|
|
137
|
+
if (cur.problem) {
|
|
138
|
+
conflicts.push({ path: b.file, reason: cur.problem });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const createdFile = prev?.createdFile ?? !cur.exists;
|
|
142
|
+
newBlocks.push({ file: b.file, sha256: desiredHash, createdFile });
|
|
143
|
+
if (!cur.exists) {
|
|
144
|
+
if (prev)
|
|
145
|
+
warnings.push(`${b.file} não existe mais e será recriado com o bloco gerenciado.`);
|
|
146
|
+
blockOps.push({ file: b.file, action: 'create-file', reason: 'novo arquivo com bloco gerenciado', newText: upsertBlock(null, inner), expectedHash: null });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const scan = scanBlock(cur.text);
|
|
150
|
+
if (scan.status === 'invalid') {
|
|
151
|
+
conflicts.push({ path: b.file, reason: `bloco gerenciado inválido: ${scan.reason}` });
|
|
152
|
+
}
|
|
153
|
+
else if (scan.status === 'none') {
|
|
154
|
+
if (prev)
|
|
155
|
+
warnings.push(`O bloco gerenciado foi removido de ${b.file} e será reinserido ao final do arquivo.`);
|
|
156
|
+
blockOps.push({ file: b.file, action: 'insert', reason: 'adiciona bloco ao final, preservando o texto existente', newText: upsertBlock(cur.text, inner), expectedHash: cur.hash });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const curInner = sha256(scan.inner);
|
|
160
|
+
if (curInner === desiredHash) {
|
|
161
|
+
blockOps.push({ file: b.file, action: 'unchanged', reason: 'sem mudanças', expectedHash: cur.hash });
|
|
162
|
+
}
|
|
163
|
+
else if (prev && curInner === prev.sha256) {
|
|
164
|
+
blockOps.push({ file: b.file, action: 'update', reason: 'atualiza somente o bloco gerenciado', newText: upsertBlock(cur.text, inner), expectedHash: cur.hash });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
conflicts.push({
|
|
168
|
+
path: b.file,
|
|
169
|
+
reason: prev ? 'o bloco gerenciado foi editado localmente' : 'já existe um bloco SEEDGRID AI TOOLKIT não registrado no lock',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const desiredBlockFiles = new Set(desired.blocks.map((b) => b.file));
|
|
175
|
+
for (const prev of prevBlocks.values()) {
|
|
176
|
+
if (desiredBlockFiles.has(prev.file))
|
|
177
|
+
continue;
|
|
178
|
+
const op = await planBlockRemoval(projectRoot, prev, conflicts);
|
|
179
|
+
if (op)
|
|
180
|
+
blockOps.push(op);
|
|
181
|
+
}
|
|
182
|
+
const lock = {
|
|
183
|
+
lockVersion: LOCK_VERSION,
|
|
184
|
+
tool: TOOL_NAME,
|
|
185
|
+
cliVersion: meta.cliVersion,
|
|
186
|
+
contentVersion: meta.contentVersion,
|
|
187
|
+
contentSchemaVersion: meta.contentSchemaVersion,
|
|
188
|
+
targets: [...meta.targets].sort(),
|
|
189
|
+
mode: meta.mode,
|
|
190
|
+
source: meta.source,
|
|
191
|
+
lastOperation: { id: meta.operationId, kind: meta.kind, at: meta.now },
|
|
192
|
+
files: newFiles,
|
|
193
|
+
blocks: newBlocks,
|
|
194
|
+
};
|
|
195
|
+
const lockCur = await readCurrent(projectRoot, LOCK_PATH);
|
|
196
|
+
if (lockCur.problem)
|
|
197
|
+
conflicts.push({ path: LOCK_PATH, reason: lockCur.problem });
|
|
198
|
+
const lockUnchanged = prevLock !== null && sameLockContent(prevLock, lock);
|
|
199
|
+
const lockOp = lockUnchanged
|
|
200
|
+
? { action: 'unchanged', expectedHash: lockCur.hash ?? null }
|
|
201
|
+
: { action: 'write', content: serializeLock(lock), expectedHash: lockCur.hash ?? null };
|
|
202
|
+
if (prevLock && prevLock.contentVersion !== meta.contentVersion) {
|
|
203
|
+
const cmp = prevLock.contentVersion.localeCompare(meta.contentVersion, undefined, { numeric: true });
|
|
204
|
+
if (cmp > 0)
|
|
205
|
+
warnings.push(`O conteúdo instalado (${prevLock.contentVersion}) é mais novo que o da fonte (${meta.contentVersion}).`);
|
|
206
|
+
}
|
|
207
|
+
return finish({ kind: meta.kind, projectRoot, fileOps, blockOps, lockOp, conflicts, warnings, lock: lockUnchanged ? prevLock : lock });
|
|
208
|
+
}
|
|
209
|
+
/** Planeja a remoção dos arquivos e blocos registrados no lock. Não escreve nada. */
|
|
210
|
+
export async function planRemove(projectRoot, lockState) {
|
|
211
|
+
if (lockState.status === 'absent')
|
|
212
|
+
throw new ToolkitError('Nenhuma instalação encontrada (sem .ai/toolkit-lock.json).');
|
|
213
|
+
if (lockState.status === 'invalid') {
|
|
214
|
+
throw new ToolkitError('O lock é inválido; nada foi removido.', lockState.errors);
|
|
215
|
+
}
|
|
216
|
+
const conflicts = [];
|
|
217
|
+
const fileOps = [];
|
|
218
|
+
const blockOps = [];
|
|
219
|
+
for (const f of lockState.lock.files) {
|
|
220
|
+
const op = await planFileRemoval(projectRoot, f, conflicts, 'arquivo gerenciado');
|
|
221
|
+
if (op)
|
|
222
|
+
fileOps.push(op);
|
|
223
|
+
}
|
|
224
|
+
for (const b of lockState.lock.blocks) {
|
|
225
|
+
const op = await planBlockRemoval(projectRoot, b, conflicts);
|
|
226
|
+
if (op)
|
|
227
|
+
blockOps.push(op);
|
|
228
|
+
}
|
|
229
|
+
const lockCur = await readCurrent(projectRoot, LOCK_PATH);
|
|
230
|
+
if (lockCur.problem)
|
|
231
|
+
conflicts.push({ path: LOCK_PATH, reason: lockCur.problem });
|
|
232
|
+
return finish({
|
|
233
|
+
kind: 'remove',
|
|
234
|
+
projectRoot,
|
|
235
|
+
fileOps,
|
|
236
|
+
blockOps,
|
|
237
|
+
lockOp: { action: 'delete', expectedHash: lockCur.hash ?? null },
|
|
238
|
+
conflicts,
|
|
239
|
+
warnings: [],
|
|
240
|
+
lock: null,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
function run(command, timeoutMs) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
// Comando fixo do adaptador, sem entrada do usuário. shell é necessário para .cmd no Windows.
|
|
5
|
+
exec(command, { timeout: timeoutMs, windowsHide: true }, (err, stdout) => {
|
|
6
|
+
resolve(err ? null : stdout.toString());
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Executa um comando de sonda (fixo, definido pelo adaptador) na pasta do projeto
|
|
12
|
+
* e devolve a saída padrão, ou null em caso de falha. A entrada padrão é fechada
|
|
13
|
+
* para o processo não esperar texto adicional.
|
|
14
|
+
*/
|
|
15
|
+
export function runProbe(command, cwd, timeoutMs = 60000) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const child = exec(command, { cwd, timeout: timeoutMs, windowsHide: true, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) => {
|
|
18
|
+
resolve(err ? null : stdout.toString());
|
|
19
|
+
});
|
|
20
|
+
child.stdin?.end();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export async function detectTool(adapter, timeoutMs = 15000) {
|
|
24
|
+
if (process.env.SEEDGRID_AI_SKIP_TOOL_DETECTION === '1') {
|
|
25
|
+
return {
|
|
26
|
+
target: adapter.target,
|
|
27
|
+
displayName: adapter.displayName,
|
|
28
|
+
installed: false,
|
|
29
|
+
version: null,
|
|
30
|
+
validated: false,
|
|
31
|
+
validatedVersions: adapter.validatedVersions,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const out = await run(adapter.versionCommand, timeoutMs);
|
|
35
|
+
const version = out?.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/)?.[0] ?? null;
|
|
36
|
+
return {
|
|
37
|
+
target: adapter.target,
|
|
38
|
+
displayName: adapter.displayName,
|
|
39
|
+
installed: out !== null,
|
|
40
|
+
version,
|
|
41
|
+
validated: version !== null && adapter.validatedVersions.includes(version),
|
|
42
|
+
validatedVersions: adapter.validatedVersions,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Executa o comando de revisão (fixo, montado pelo adaptador) na pasta do projeto e devolve a saída.
|
|
47
|
+
* A entrada padrão é fechada. Pode demorar: chama o modelo da ferramenta, com a conta do usuário.
|
|
48
|
+
*/
|
|
49
|
+
export function runReviewCommand(command, cwd, timeoutMs = 15 * 60 * 1000) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const child = exec(command, { cwd, timeout: timeoutMs, windowsHide: true, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
52
|
+
const e = err;
|
|
53
|
+
resolve({
|
|
54
|
+
code: e ? (typeof e.code === 'number' ? e.code : null) : 0,
|
|
55
|
+
stdout: stdout.toString(),
|
|
56
|
+
stderr: stderr.toString(),
|
|
57
|
+
timedOut: Boolean(e?.killed),
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
child.stdin?.end();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const ALL_TARGETS = ['claude', 'codex'];
|
|
2
|
+
export const ALL_MODES = ['shared', 'native'];
|
|
3
|
+
export const BLOCK_FILES = ['AGENTS.md', 'CLAUDE.md'];
|
|
4
|
+
export class ToolkitError extends Error {
|
|
5
|
+
details;
|
|
6
|
+
constructor(message, details = []) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.details = details;
|
|
9
|
+
this.name = 'ToolkitError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { exec, spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { TOOL_NAME } from './lock.js';
|
|
7
|
+
export const NO_UPDATE_CHECK_ENV = 'SEEDGRID_AI_NO_UPDATE_CHECK';
|
|
8
|
+
/** Compara versões x.y.z (sufixos de pré-release ficam abaixo da versão final). */
|
|
9
|
+
export function compareVersions(a, b) {
|
|
10
|
+
const parse = (v) => {
|
|
11
|
+
const [core = '', pre] = v.replace(/^v/, '').split('-', 2);
|
|
12
|
+
return { nums: core.split('.').map((n) => Number.parseInt(n, 10) || 0), pre };
|
|
13
|
+
};
|
|
14
|
+
const pa = parse(a);
|
|
15
|
+
const pb = parse(b);
|
|
16
|
+
for (let i = 0; i < 3; i++) {
|
|
17
|
+
const d = (pa.nums[i] ?? 0) - (pb.nums[i] ?? 0);
|
|
18
|
+
if (d !== 0)
|
|
19
|
+
return Math.sign(d);
|
|
20
|
+
}
|
|
21
|
+
if (pa.pre === pb.pre)
|
|
22
|
+
return 0;
|
|
23
|
+
if (!pa.pre)
|
|
24
|
+
return 1;
|
|
25
|
+
if (!pb.pre)
|
|
26
|
+
return -1;
|
|
27
|
+
return pa.pre.localeCompare(pb.pre, undefined, { numeric: true });
|
|
28
|
+
}
|
|
29
|
+
export function updateCheckDisabled() {
|
|
30
|
+
return process.env[NO_UPDATE_CHECK_ENV] === '1' || Boolean(process.env.CI);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Consulta a versão "latest" publicada no npm (somente leitura). Usa o próprio
|
|
34
|
+
* npm para respeitar registry, proxy e .npmrc. Retorna null sem rede, sem
|
|
35
|
+
* pacote publicado ou em caso de qualquer falha.
|
|
36
|
+
*/
|
|
37
|
+
export async function checkLatest(current, timeoutMs = 8000) {
|
|
38
|
+
const simulated = process.env.SEEDGRID_AI_TEST_LATEST; // somente testes: evita rede
|
|
39
|
+
if (simulated)
|
|
40
|
+
return { current, latest: simulated, newer: compareVersions(simulated, current) > 0 };
|
|
41
|
+
if (updateCheckDisabled())
|
|
42
|
+
return null;
|
|
43
|
+
// Cache por usuário: a consulta ao npm custa alguns segundos, então é feita no máximo a cada 12 h.
|
|
44
|
+
const cacheFile = path.join(os.homedir(), '.cache', 'seedgrid-ai', 'update-check.json');
|
|
45
|
+
try {
|
|
46
|
+
const cached = JSON.parse(await readFile(cacheFile, 'utf8'));
|
|
47
|
+
if (Date.now() - cached.checkedAt < CACHE_TTL_MS) {
|
|
48
|
+
return cached.latest ? { current, latest: cached.latest, newer: compareVersions(cached.latest, current) > 0 } : null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// sem cache válido
|
|
53
|
+
}
|
|
54
|
+
const latest = await queryNpm(timeoutMs);
|
|
55
|
+
try {
|
|
56
|
+
await mkdir(path.dirname(cacheFile), { recursive: true });
|
|
57
|
+
await writeFile(cacheFile, JSON.stringify({ checkedAt: Date.now(), latest }));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// cache é opcional
|
|
61
|
+
}
|
|
62
|
+
return latest ? { current, latest, newer: compareVersions(latest, current) > 0 } : null;
|
|
63
|
+
}
|
|
64
|
+
const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
65
|
+
function queryNpm(timeoutMs) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const child = exec(`npm view ${TOOL_NAME}@latest version --json`, { timeout: timeoutMs, windowsHide: true }, (err, stdout) => {
|
|
68
|
+
if (err)
|
|
69
|
+
return resolve(null);
|
|
70
|
+
try {
|
|
71
|
+
const latest = JSON.parse(stdout.toString().trim());
|
|
72
|
+
resolve(typeof latest === 'string' && /^\d+\.\d+\.\d+/.test(latest) ? latest : null);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
resolve(null);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
child.stdin?.end();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function isInteractive() {
|
|
82
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
83
|
+
}
|
|
84
|
+
/** Pergunta sim/não no terminal. Enter vazio usa o padrão. */
|
|
85
|
+
export async function confirm(question, defaultYes = true) {
|
|
86
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
87
|
+
try {
|
|
88
|
+
const answer = (await rl.question(`${question} ${defaultYes ? '[Y/n]' : '[y/N]'} `)).trim().toLowerCase();
|
|
89
|
+
if (answer === '')
|
|
90
|
+
return defaultYes;
|
|
91
|
+
return ['y', 'yes', 's', 'sim'].includes(answer);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
rl.close();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const UNSAFE_FOR_SHELL = /["%^&|<>!`$\r\n]/;
|
|
98
|
+
/** Linha de comando equivalente, para exibir ou para o usuário rodar manualmente. */
|
|
99
|
+
export function npxCommandLine(version, args) {
|
|
100
|
+
const quote = (a) => (/[\s'"]/.test(a) ? `"${a}"` : a);
|
|
101
|
+
return ['npx', `${TOOL_NAME}@${version}`, ...args].map(quote).join(' ');
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Executa a versão indicada do pacote via npx, repassando os argumentos.
|
|
105
|
+
* Retorna o código de saída, ou null se não for seguro montar o comando.
|
|
106
|
+
*/
|
|
107
|
+
export function runPublishedVersion(version, args) {
|
|
108
|
+
if (args.some((a) => UNSAFE_FOR_SHELL.test(a)))
|
|
109
|
+
return Promise.resolve(null);
|
|
110
|
+
const quoted = args.map((a) => (/\s/.test(a) ? `"${a}"` : a));
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
// shell é necessário para npx.cmd no Windows; os argumentos já foram filtrados acima.
|
|
113
|
+
const child = spawn('npx', ['--yes', `${TOOL_NAME}@${version}`, ...quoted], {
|
|
114
|
+
stdio: 'inherit',
|
|
115
|
+
shell: true,
|
|
116
|
+
env: { ...process.env, [NO_UPDATE_CHECK_ENV]: '1' }, // evita perguntar de novo na versão nova
|
|
117
|
+
});
|
|
118
|
+
child.on('error', () => resolve(1));
|
|
119
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
120
|
+
});
|
|
121
|
+
}
|