kaven-cli 0.4.0 → 0.5.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.
Files changed (57) hide show
  1. package/README.md +154 -215
  2. package/dist/EnvManager-NMS3NMIE.js +15 -0
  3. package/dist/MarketplaceClient-YCFH2VU4.js +1 -0
  4. package/dist/chunk-JHLQ46NG.js +1 -0
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.js +216 -286
  7. package/dist/tier-table-DQMPQSI2.js +6 -0
  8. package/package.json +26 -10
  9. package/dist/commands/auth/login.js +0 -122
  10. package/dist/commands/auth/logout.js +0 -23
  11. package/dist/commands/auth/whoami.js +0 -36
  12. package/dist/commands/cache/index.js +0 -43
  13. package/dist/commands/config/features.js +0 -1026
  14. package/dist/commands/config/index.js +0 -95
  15. package/dist/commands/index.js +0 -2
  16. package/dist/commands/init/index.js +0 -197
  17. package/dist/commands/init-ci/index.js +0 -153
  18. package/dist/commands/license/index.js +0 -10
  19. package/dist/commands/license/status.js +0 -44
  20. package/dist/commands/license/tier-table.js +0 -46
  21. package/dist/commands/marketplace/browse.js +0 -186
  22. package/dist/commands/marketplace/install.js +0 -263
  23. package/dist/commands/marketplace/list.js +0 -122
  24. package/dist/commands/module/activate.js +0 -206
  25. package/dist/commands/module/add.js +0 -69
  26. package/dist/commands/module/doctor.js +0 -175
  27. package/dist/commands/module/publish.js +0 -258
  28. package/dist/commands/module/remove.js +0 -58
  29. package/dist/commands/telemetry/view.js +0 -27
  30. package/dist/commands/upgrade/check.js +0 -162
  31. package/dist/commands/upgrade/index.js +0 -185
  32. package/dist/core/AuthService.js +0 -222
  33. package/dist/core/CacheManager.js +0 -154
  34. package/dist/core/ConfigManager.js +0 -166
  35. package/dist/core/EnvManager.js +0 -196
  36. package/dist/core/ErrorRecovery.js +0 -192
  37. package/dist/core/LicenseService.js +0 -83
  38. package/dist/core/ManifestParser.js +0 -52
  39. package/dist/core/MarkerService.js +0 -62
  40. package/dist/core/ModuleDoctor.js +0 -451
  41. package/dist/core/ModuleInstaller.js +0 -169
  42. package/dist/core/ProjectInitializer.js +0 -166
  43. package/dist/core/RegistryResolver.js +0 -95
  44. package/dist/core/SchemaActivator.js +0 -270
  45. package/dist/core/ScriptRunner.js +0 -73
  46. package/dist/core/SignatureVerifier.js +0 -75
  47. package/dist/core/index.js +0 -2
  48. package/dist/infrastructure/Container.js +0 -37
  49. package/dist/infrastructure/MarketplaceClient.js +0 -399
  50. package/dist/infrastructure/TelemetryBuffer.js +0 -73
  51. package/dist/infrastructure/TransactionalFileSystem.js +0 -77
  52. package/dist/infrastructure/errors.js +0 -63
  53. package/dist/infrastructure/index.js +0 -2
  54. package/dist/types/auth.js +0 -2
  55. package/dist/types/manifest.js +0 -45
  56. package/dist/types/markers.js +0 -10
  57. package/dist/types/marketplace.js +0 -2
@@ -1,166 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ProjectInitializer = void 0;
7
- const fs_extra_1 = __importDefault(require("fs-extra"));
8
- const path_1 = __importDefault(require("path"));
9
- const child_process_1 = require("child_process");
10
- const TEMPLATE_REPO = "https://github.com/kaven-co/kaven-template.git";
11
- const KAVEN_SQUAD_REPO = "https://github.com/bychrisr/kaven-squad";
12
- /** Run a shell command via spawn, returning exit code. */
13
- function runCommand(cmd, args, cwd, onData) {
14
- return new Promise((resolve, reject) => {
15
- const proc = (0, child_process_1.spawn)(cmd, args, { cwd, stdio: onData ? "pipe" : "inherit" });
16
- if (onData && proc.stdout) {
17
- proc.stdout.on("data", (d) => onData(d.toString()));
18
- }
19
- if (onData && proc.stderr) {
20
- proc.stderr.on("data", (d) => onData(d.toString()));
21
- }
22
- proc.on("error", reject);
23
- proc.on("close", (code) => resolve(code ?? 0));
24
- });
25
- }
26
- class ProjectInitializer {
27
- /** Validate that the project name only contains alphanumerics and hyphens. */
28
- validateName(name) {
29
- if (!name || name.trim().length === 0) {
30
- return { valid: false, reason: "Project name cannot be empty" };
31
- }
32
- if (/\s/.test(name)) {
33
- return { valid: false, reason: "Project name cannot contain spaces" };
34
- }
35
- if (!/^[a-z0-9-]+$/.test(name)) {
36
- return {
37
- valid: false,
38
- reason: "Project name must only contain lowercase letters, numbers, and hyphens",
39
- };
40
- }
41
- return { valid: true };
42
- }
43
- /** Clone the template (from Git or local path) into targetDir. */
44
- async cloneTemplate(targetDir, templateSource) {
45
- const source = templateSource || TEMPLATE_REPO;
46
- console.log(`[INIT] Clone Source: ${source}`);
47
- console.log(`[INIT] Target Dir: ${targetDir}`);
48
- // If it's a local path that exists, copy it instead of cloning
49
- if (await fs_extra_1.default.pathExists(source) && (source.startsWith("/") || source.startsWith("./") || source.startsWith("../"))) {
50
- console.log(`[INIT] Local Path Detected. Copying...`);
51
- await fs_extra_1.default.copy(source, targetDir, {
52
- filter: (src) => !src.includes("node_modules") && !src.includes(".git") && !src.includes(".turbo")
53
- });
54
- console.log(`[INIT] Local Copy Done.`);
55
- return;
56
- }
57
- const exitCode = await runCommand("git", ["clone", "--depth", "1", source, targetDir], process.cwd());
58
- if (exitCode !== 0) {
59
- throw new Error(`git clone failed with exit code ${exitCode}`);
60
- }
61
- }
62
- /** Remove the .git directory from the cloned project. */
63
- async removeGitDir(targetDir) {
64
- const gitDir = path_1.default.join(targetDir, ".git");
65
- if (await fs_extra_1.default.pathExists(gitDir)) {
66
- await fs_extra_1.default.remove(gitDir);
67
- }
68
- }
69
- /** Replace placeholders in key project files. */
70
- async replacePlaceholders(targetDir, values) {
71
- const replacements = {
72
- "{{PROJECT_NAME}}": values.projectName,
73
- "{{DATABASE_URL}}": values.dbUrl,
74
- "{{DEFAULT_LOCALE}}": values.locale,
75
- "{{DEFAULT_CURRENCY}}": values.currency,
76
- };
77
- const filesToProcess = [
78
- "package.json",
79
- ".env.example",
80
- "packages/database/prisma/schema.prisma",
81
- "apps/api/package.json",
82
- "apps/admin/package.json",
83
- "apps/tenant/package.json",
84
- ];
85
- for (const relFile of filesToProcess) {
86
- const filePath = path_1.default.join(targetDir, relFile);
87
- if (!(await fs_extra_1.default.pathExists(filePath)))
88
- continue;
89
- let content = await fs_extra_1.default.readFile(filePath, "utf-8");
90
- for (const [placeholder, value] of Object.entries(replacements)) {
91
- content = content.split(placeholder).join(value);
92
- }
93
- await fs_extra_1.default.writeFile(filePath, content, "utf-8");
94
- }
95
- // Safety net: directly update root package.json name field regardless of placeholder
96
- const pkgPath = path_1.default.join(targetDir, "package.json");
97
- if (await fs_extra_1.default.pathExists(pkgPath)) {
98
- const pkg = await fs_extra_1.default.readJson(pkgPath);
99
- if (pkg.name !== values.projectName) {
100
- pkg.name = values.projectName;
101
- await fs_extra_1.default.writeJson(pkgPath, pkg, { spaces: 2 });
102
- }
103
- }
104
- }
105
- /** Run pnpm install in the target directory. */
106
- async runInstall(targetDir) {
107
- const exitCode = await runCommand("pnpm", ["install"], targetDir);
108
- if (exitCode !== 0) {
109
- throw new Error(`pnpm install failed with exit code ${exitCode}`);
110
- }
111
- }
112
- /** Initialize git and create an initial commit. */
113
- async initGit(targetDir) {
114
- await runCommand("git", ["init"], targetDir);
115
- await runCommand("git", ["add", "."], targetDir);
116
- await runCommand("git", ["commit", "-m", "chore: initial kaven setup"], targetDir);
117
- }
118
- /**
119
- * Clone kaven-squad into squads/kaven-squad/ inside the project.
120
- * Returns { installed: true } on success, { installed: false, reason } on failure.
121
- * Never throws — squad installation is non-fatal.
122
- */
123
- async installSquad(targetDir) {
124
- const squadsDir = path_1.default.join(targetDir, "squads");
125
- const squadDir = path_1.default.join(squadsDir, "kaven-squad");
126
- // Squad already present — skip
127
- if (await fs_extra_1.default.pathExists(squadDir)) {
128
- return { installed: false, reason: "already-exists" };
129
- }
130
- await fs_extra_1.default.ensureDir(squadsDir);
131
- const exitCode = await runCommand("git", ["clone", "--depth", "1", KAVEN_SQUAD_REPO, squadDir], process.cwd());
132
- if (exitCode !== 0) {
133
- return {
134
- installed: false,
135
- reason: `git clone exited with code ${exitCode}`,
136
- };
137
- }
138
- // Remove .git — squad history not needed in user project
139
- const squadGitDir = path_1.default.join(squadDir, ".git");
140
- if (await fs_extra_1.default.pathExists(squadGitDir)) {
141
- await fs_extra_1.default.remove(squadGitDir);
142
- }
143
- return { installed: true };
144
- }
145
- /** Health check after project initialization. */
146
- async healthCheck(targetDir) {
147
- const issues = [];
148
- // Check key files exist
149
- const requiredFiles = ["package.json", ".env.example", "packages/database/prisma/schema.prisma"];
150
- for (const file of requiredFiles) {
151
- if (!(await fs_extra_1.default.pathExists(path_1.default.join(targetDir, file)))) {
152
- issues.push(`Missing required file: ${file}`);
153
- }
154
- }
155
- // Check node_modules exists (if install was run)
156
- const hasNodeModules = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "node_modules"));
157
- if (!hasNodeModules) {
158
- issues.push("Dependencies not installed. Run: pnpm install");
159
- }
160
- return {
161
- healthy: issues.length === 0,
162
- issues,
163
- };
164
- }
165
- }
166
- exports.ProjectInitializer = ProjectInitializer;
@@ -1,95 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RegistryResolver = void 0;
4
- const ConfigManager_1 = require("./ConfigManager");
5
- const MarketplaceClient_1 = require("../infrastructure/MarketplaceClient");
6
- const AuthService_1 = require("./AuthService");
7
- /**
8
- * C2.5: Registry resolver — handles both official and custom registries
9
- */
10
- class RegistryResolver {
11
- authService;
12
- constructor(authService) {
13
- this.authService = authService || new AuthService_1.AuthService();
14
- }
15
- /**
16
- * Get the active registry URL (custom or default)
17
- */
18
- async getActiveRegistry() {
19
- await ConfigManager_1.configManager.initialize();
20
- return ConfigManager_1.configManager.getRegistry();
21
- }
22
- /**
23
- * Get marketplace client for active registry
24
- */
25
- async getMarketplaceClient() {
26
- const registry = await this.getActiveRegistry();
27
- const client = new MarketplaceClient_1.MarketplaceClient(this.authService);
28
- // Set custom registry if configured
29
- if (registry !== "https://marketplace.kaven.site") {
30
- client.baseUrl = registry;
31
- }
32
- return client;
33
- }
34
- /**
35
- * Validate registry URL is accessible
36
- */
37
- async validateRegistry(url) {
38
- try {
39
- const response = await fetch(`${url}/health`);
40
- if (!response.ok) {
41
- return {
42
- valid: false,
43
- error: `Registry returned ${response.status} ${response.statusText}`,
44
- };
45
- }
46
- return { valid: true };
47
- }
48
- catch (error) {
49
- return {
50
- valid: false,
51
- error: `Failed to connect: ${error instanceof Error ? error.message : String(error)}`,
52
- };
53
- }
54
- }
55
- /**
56
- * Set custom registry
57
- */
58
- async setCustomRegistry(url) {
59
- // Validate URL format
60
- try {
61
- new URL(url);
62
- }
63
- catch {
64
- throw new Error(`Invalid URL format: ${url}`);
65
- }
66
- // Validate registry is accessible
67
- const validation = await this.validateRegistry(url);
68
- if (!validation.valid) {
69
- throw new Error(`Registry validation failed: ${validation.error}`);
70
- }
71
- // Save to config
72
- await ConfigManager_1.configManager.initialize();
73
- await ConfigManager_1.configManager.set("customRegistry", url);
74
- }
75
- /**
76
- * Reset to default registry
77
- */
78
- async resetToDefaultRegistry() {
79
- await ConfigManager_1.configManager.initialize();
80
- await ConfigManager_1.configManager.set("customRegistry", undefined);
81
- }
82
- /**
83
- * List all available registries (default + custom)
84
- */
85
- async listRegistries() {
86
- await ConfigManager_1.configManager.initialize();
87
- const config = ConfigManager_1.configManager.getAll();
88
- return {
89
- default: config.registry || "https://marketplace.kaven.site",
90
- custom: config.customRegistry,
91
- active: await this.getActiveRegistry(),
92
- };
93
- }
94
- }
95
- exports.RegistryResolver = RegistryResolver;
@@ -1,270 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SchemaActivator = exports.KAVEN_MODULES = void 0;
7
- const fs_extra_1 = __importDefault(require("fs-extra"));
8
- const path_1 = __importDefault(require("path"));
9
- exports.KAVEN_MODULES = [
10
- {
11
- id: "auth",
12
- label: "Auth & Identity",
13
- description: "Gestão de usuários, permissões e sessões",
14
- models: ["User", "Role", "Capability", "AuthSession", "AuditLog"],
15
- enums: ["UserRole"],
16
- dependsOn: [],
17
- },
18
- {
19
- id: "billing",
20
- label: "Billing",
21
- description: "Faturamento, assinaturas e pagamentos",
22
- models: ["Invoice", "Order", "Subscription", "Plan", "Payment", "Product"],
23
- enums: [],
24
- dependsOn: [],
25
- },
26
- {
27
- id: "projects",
28
- label: "Projects",
29
- description: "Gestão de projetos e tasks",
30
- models: ["Project", "Task"],
31
- enums: ["ProjectStatus", "TaskStatus", "TaskPriority"],
32
- dependsOn: [],
33
- },
34
- {
35
- id: "notifications",
36
- label: "Notifications",
37
- description: "Notificações e preferências de usuário",
38
- models: ["Notification", "UserPreference"],
39
- enums: [],
40
- dependsOn: [],
41
- },
42
- {
43
- id: "marketing-tracking",
44
- label: "Marketing Tracking",
45
- description: "Observabilidade de anúncios, GTM, GA4 e Meta CAPI",
46
- models: ["TrackingEvent"],
47
- enums: ["TrackingSource"],
48
- dependsOn: [],
49
- },
50
- ];
51
- // ============================================================
52
- // Marcadores de seção no schema
53
- // ============================================================
54
- const BEGIN_MARKER = (moduleId) => `// [KAVEN_MODULE:${moduleId.toUpperCase()} BEGIN]`;
55
- const END_MARKER = (moduleId) => `// [KAVEN_MODULE:${moduleId.toUpperCase()} END]`;
56
- // ============================================================
57
- // SchemaActivator — lê/escreve schema.extended.prisma
58
- // ============================================================
59
- class SchemaActivator {
60
- schemaPath;
61
- constructor(projectRoot) {
62
- this.schemaPath = path_1.default.join(projectRoot, "packages", "database", "prisma", "schema.extended.prisma");
63
- }
64
- /** Verifica se o schema existe no projeto */
65
- async exists() {
66
- return fs_extra_1.default.pathExists(this.schemaPath);
67
- }
68
- /** Caminho absoluto do schema */
69
- get path() {
70
- return this.schemaPath;
71
- }
72
- /** Lê o conteúdo atual do schema */
73
- async readSchema() {
74
- return fs_extra_1.default.readFile(this.schemaPath, "utf-8");
75
- }
76
- /** Persiste o conteúdo no schema */
77
- async writeSchema(content) {
78
- await fs_extra_1.default.writeFile(this.schemaPath, content, "utf-8");
79
- }
80
- /**
81
- * Detecta se um módulo está ativo no schema.
82
- *
83
- * Estratégia:
84
- * 1. Se existirem marcadores BEGIN/END: verifica se o conteúdo dentro
85
- * dos marcadores NÃO está completamente comentado.
86
- * 2. Se não houver marcadores: verifica se algum dos models principais
87
- * do módulo está presente e não comentado no arquivo.
88
- */
89
- async getModuleStatus(def) {
90
- const content = await this.readSchema();
91
- const begin = BEGIN_MARKER(def.id);
92
- const end = END_MARKER(def.id);
93
- const hasMarkers = content.includes(begin) && content.includes(end);
94
- let active = false;
95
- if (hasMarkers) {
96
- const block = this.extractBlock(content, def.id);
97
- active = block !== null && this.isBlockActive(block);
98
- }
99
- else {
100
- // Sem marcadores: verifica presença de pelo menos um model descomentado
101
- active = def.models.some((modelName) => this.isModelActive(content, modelName));
102
- }
103
- return {
104
- id: def.id,
105
- label: def.label,
106
- description: def.description,
107
- models: def.models,
108
- dependsOn: def.dependsOn,
109
- active,
110
- hasMarkers,
111
- };
112
- }
113
- /** Ativa um módulo: se tem marcadores, descomenta o bloco; senão, injeta o bloco */
114
- async activateModule(def) {
115
- const content = await this.readSchema();
116
- const begin = BEGIN_MARKER(def.id);
117
- const end = END_MARKER(def.id);
118
- if (content.includes(begin) && content.includes(end)) {
119
- const updated = this.uncommentBlock(content, def.id);
120
- await this.writeSchema(updated);
121
- return;
122
- }
123
- // Módulo não tem seção marcada — sem template para injetar
124
- throw new Error(`O módulo "${def.id}" não possui uma seção marcada (BEGIN/END) no schema.\n` +
125
- `Adicione o bloco do módulo manualmente com os marcadores:\n` +
126
- ` ${begin}\n` +
127
- ` ... models do módulo ...\n` +
128
- ` ${end}`);
129
- }
130
- /** Desativa um módulo: comenta todos os models do bloco */
131
- async deactivateModule(def) {
132
- const content = await this.readSchema();
133
- const begin = BEGIN_MARKER(def.id);
134
- const end = END_MARKER(def.id);
135
- if (!content.includes(begin) || !content.includes(end)) {
136
- throw new Error(`O módulo "${def.id}" não possui marcadores BEGIN/END no schema. ` +
137
- `Não é possível desativar automaticamente.`);
138
- }
139
- const updated = this.commentBlock(content, def.id);
140
- await this.writeSchema(updated);
141
- }
142
- // ──────────────────────────────────────────────
143
- // Helpers de manipulação de blocos
144
- // ──────────────────────────────────────────────
145
- /**
146
- * Extrai o conteúdo entre os marcadores BEGIN e END (exclusive).
147
- * Retorna null se os marcadores não forem encontrados.
148
- */
149
- extractBlock(content, moduleId) {
150
- const begin = BEGIN_MARKER(moduleId);
151
- const end = END_MARKER(moduleId);
152
- const lines = content.split("\n");
153
- let beginIdx = -1;
154
- let endIdx = -1;
155
- for (let i = 0; i < lines.length; i++) {
156
- if (lines[i].includes(begin))
157
- beginIdx = i;
158
- if (lines[i].includes(end) && beginIdx !== -1) {
159
- endIdx = i;
160
- break;
161
- }
162
- }
163
- if (beginIdx === -1 || endIdx === -1)
164
- return null;
165
- return lines.slice(beginIdx + 1, endIdx).join("\n");
166
- }
167
- /**
168
- * Verifica se um bloco tem ao menos uma linha não-comentada relevante
169
- * (ignora linhas vazias e comentários simples).
170
- */
171
- isBlockActive(block) {
172
- return block.split("\n").some((line) => {
173
- const trimmed = line.trim();
174
- return (trimmed.length > 0 &&
175
- !trimmed.startsWith("//") &&
176
- !trimmed.startsWith("/*") &&
177
- !trimmed.startsWith("*"));
178
- });
179
- }
180
- /**
181
- * Verifica se um model específico está ativo (não comentado) no schema.
182
- * Procura pela linha `model ModelName {` sem `//` antes.
183
- */
184
- isModelActive(content, modelName) {
185
- const lines = content.split("\n");
186
- for (const line of lines) {
187
- const trimmed = line.trim();
188
- if (trimmed.startsWith(`model ${modelName}`) &&
189
- !line.trimStart().startsWith("//")) {
190
- return true;
191
- }
192
- }
193
- return false;
194
- }
195
- /**
196
- * Comenta todas as linhas não-comentadas dentro do bloco BEGIN/END.
197
- */
198
- commentBlock(content, moduleId) {
199
- const begin = BEGIN_MARKER(moduleId);
200
- const end = END_MARKER(moduleId);
201
- const lines = content.split("\n");
202
- let inBlock = false;
203
- const result = [];
204
- for (const line of lines) {
205
- if (line.includes(begin)) {
206
- inBlock = true;
207
- result.push(line);
208
- continue;
209
- }
210
- if (line.includes(end)) {
211
- inBlock = false;
212
- result.push(line);
213
- continue;
214
- }
215
- if (inBlock) {
216
- const trimmed = line.trim();
217
- if (trimmed.length === 0) {
218
- result.push(line);
219
- }
220
- else if (trimmed.startsWith("//")) {
221
- result.push(line); // já comentado
222
- }
223
- else {
224
- result.push(`// ${line}`);
225
- }
226
- }
227
- else {
228
- result.push(line);
229
- }
230
- }
231
- return result.join("\n");
232
- }
233
- /**
234
- * Remove `// ` do início das linhas dentro do bloco BEGIN/END.
235
- */
236
- uncommentBlock(content, moduleId) {
237
- const begin = BEGIN_MARKER(moduleId);
238
- const end = END_MARKER(moduleId);
239
- const lines = content.split("\n");
240
- let inBlock = false;
241
- const result = [];
242
- for (const line of lines) {
243
- if (line.includes(begin)) {
244
- inBlock = true;
245
- result.push(line);
246
- continue;
247
- }
248
- if (line.includes(end)) {
249
- inBlock = false;
250
- result.push(line);
251
- continue;
252
- }
253
- if (inBlock) {
254
- // Remove exatamente um nível de comentário preservando identação
255
- const match = line.match(/^(\s*)\/\/\s?(.*)$/);
256
- if (match) {
257
- result.push(match[1] + match[2]);
258
- }
259
- else {
260
- result.push(line);
261
- }
262
- }
263
- else {
264
- result.push(line);
265
- }
266
- }
267
- return result.join("\n");
268
- }
269
- }
270
- exports.SchemaActivator = SchemaActivator;
@@ -1,73 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ScriptRunner = void 0;
7
- const child_process_1 = require("child_process");
8
- const readline_1 = __importDefault(require("readline"));
9
- const chalk_1 = __importDefault(require("chalk"));
10
- class ScriptRunner {
11
- timeoutMs;
12
- constructor(timeoutMs = 60_000) {
13
- this.timeoutMs = timeoutMs;
14
- }
15
- async runScript(script, label, skipConfirmation = false) {
16
- if (!skipConfirmation) {
17
- const confirmed = await this.confirm(`Run ${label} script: ${script.command} ${(script.args ?? []).join(' ')}?`);
18
- if (!confirmed) {
19
- console.log(chalk_1.default.dim(` Skipping ${label} script.`));
20
- return;
21
- }
22
- }
23
- return new Promise((resolve, reject) => {
24
- const child = (0, child_process_1.spawn)(script.command, script.args ?? [], {
25
- cwd: script.cwd,
26
- stdio: ['ignore', 'pipe', 'pipe'],
27
- shell: true,
28
- });
29
- const prefix = chalk_1.default.dim(`[${label}] `);
30
- child.stdout?.on('data', (data) => {
31
- process.stdout.write(prefix + data.toString());
32
- });
33
- child.stderr?.on('data', (data) => {
34
- process.stderr.write(prefix + chalk_1.default.yellow(data.toString()));
35
- });
36
- const timer = setTimeout(() => {
37
- console.warn(chalk_1.default.yellow(`\n ⚠ ${label} script timed out after ${this.timeoutMs / 1000}s, sending SIGTERM...`));
38
- child.kill('SIGTERM');
39
- setTimeout(() => {
40
- child.kill('SIGKILL');
41
- }, 5_000);
42
- }, this.timeoutMs);
43
- child.on('close', (code) => {
44
- clearTimeout(timer);
45
- if (code === 0 || code === null) {
46
- resolve();
47
- }
48
- else {
49
- reject(new Error(`${label} script exited with code ${code}`));
50
- }
51
- });
52
- child.on('error', (err) => {
53
- clearTimeout(timer);
54
- reject(err);
55
- });
56
- });
57
- }
58
- async runScripts(scripts, label, skipConfirmation = false) {
59
- for (const script of scripts) {
60
- await this.runScript(script, label, skipConfirmation);
61
- }
62
- }
63
- confirm(message) {
64
- return new Promise((resolve) => {
65
- const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
66
- rl.question(`\n ${message} [y/N] `, (answer) => {
67
- rl.close();
68
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
69
- });
70
- });
71
- }
72
- }
73
- exports.ScriptRunner = ScriptRunner;
@@ -1,75 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.computeFileChecksum = computeFileChecksum;
7
- exports.verifyEd25519Signature = verifyEd25519Signature;
8
- exports.verifyDownload = verifyDownload;
9
- const crypto_1 = __importDefault(require("crypto"));
10
- const fs_extra_1 = __importDefault(require("fs-extra"));
11
- const errors_1 = require("../infrastructure/errors");
12
- /**
13
- * Compute SHA-256 hex checksum of a file.
14
- */
15
- async function computeFileChecksum(filePath) {
16
- const data = await fs_extra_1.default.readFile(filePath);
17
- return crypto_1.default.createHash("sha256").update(data).digest("hex");
18
- }
19
- const HEX_PATTERN = /^[0-9a-fA-F]+$/;
20
- /**
21
- * Decode a signature string that may be hex or base64 encoded.
22
- * Ed25519 signatures are always 64 bytes:
23
- * - hex: 128 chars, only [0-9a-fA-F]
24
- * - base64: 88 chars, may contain +/=
25
- */
26
- function decodeSignature(encoded) {
27
- if (HEX_PATTERN.test(encoded) && encoded.length === 128) {
28
- return Buffer.from(encoded, "hex");
29
- }
30
- return Buffer.from(encoded, "base64");
31
- }
32
- /**
33
- * Verify Ed25519 signature against a SHA-256 checksum.
34
- *
35
- * Accepts signature in either hex or base64 encoding.
36
- * Tolerates signatures made over checksum with trailing newline
37
- * (common when signing via `echo checksum > file && openssl sign`).
38
- */
39
- function verifyEd25519Signature(checksum, signature, publicKeyBase64) {
40
- try {
41
- const publicKey = crypto_1.default.createPublicKey({
42
- key: Buffer.from(publicKeyBase64, "base64"),
43
- type: "spki",
44
- format: "der",
45
- });
46
- const sigBuffer = decodeSignature(signature);
47
- if (crypto_1.default.verify(null, Buffer.from(checksum), publicKey, sigBuffer)) {
48
- return true;
49
- }
50
- // Tolerate trailing newline from shell-based signing
51
- return crypto_1.default.verify(null, Buffer.from(checksum + "\n"), publicKey, sigBuffer);
52
- }
53
- catch {
54
- return false;
55
- }
56
- }
57
- /**
58
- * Verify a downloaded module tarball:
59
- * 1. Compute SHA-256 checksum and compare to expected
60
- * 2. Verify Ed25519 signature of checksum with publisher key
61
- *
62
- * Throws SignatureVerificationError on failure.
63
- */
64
- async function verifyDownload(opts) {
65
- const actualChecksum = await computeFileChecksum(opts.filePath);
66
- if (actualChecksum !== opts.expectedChecksum) {
67
- throw new errors_1.SignatureVerificationError(`Checksum mismatch: expected ${opts.expectedChecksum.substring(0, 16)}..., ` +
68
- `got ${actualChecksum.substring(0, 16)}...`);
69
- }
70
- const valid = verifyEd25519Signature(opts.expectedChecksum, opts.signature, opts.publicKeyBase64);
71
- if (!valid) {
72
- throw new errors_1.SignatureVerificationError("Ed25519 signature verification failed. " +
73
- "The package may have been tampered with.");
74
- }
75
- }
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });