kaven-cli 0.1.0-alpha.1 → 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/README.md +221 -45
- package/dist/commands/auth/login.js +97 -19
- package/dist/commands/auth/logout.js +4 -6
- package/dist/commands/auth/whoami.js +12 -11
- package/dist/commands/cache/index.js +43 -0
- package/dist/commands/config/index.js +128 -0
- package/dist/commands/init/index.js +209 -0
- package/dist/commands/init-ci/index.js +153 -0
- package/dist/commands/license/index.js +10 -0
- package/dist/commands/license/status.js +44 -0
- package/dist/commands/license/tier-table.js +46 -0
- package/dist/commands/marketplace/browse.js +219 -0
- package/dist/commands/marketplace/install.js +233 -29
- package/dist/commands/marketplace/list.js +94 -16
- package/dist/commands/module/doctor.js +143 -38
- package/dist/commands/module/publish.js +291 -0
- package/dist/commands/upgrade/check.js +162 -0
- package/dist/commands/upgrade/index.js +218 -0
- package/dist/core/AuthService.js +207 -14
- package/dist/core/CacheManager.js +151 -0
- package/dist/core/ConfigManager.js +165 -0
- package/dist/core/EnvManager.js +196 -0
- package/dist/core/ErrorRecovery.js +191 -0
- package/dist/core/LicenseService.js +118 -0
- package/dist/core/ModuleDoctor.js +286 -0
- package/dist/core/ModuleInstaller.js +136 -2
- package/dist/core/ProjectInitializer.js +117 -0
- package/dist/core/RegistryResolver.js +94 -0
- package/dist/core/ScriptRunner.js +72 -0
- package/dist/core/SignatureVerifier.js +72 -0
- package/dist/index.js +265 -20
- package/dist/infrastructure/MarketplaceClient.js +388 -64
- package/dist/infrastructure/errors.js +61 -0
- package/dist/types/auth.js +2 -0
- package/dist/types/marketplace.js +2 -0
- package/package.json +15 -2
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ModuleDoctor = void 0;
|
|
7
7
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
9
10
|
class ModuleDoctor {
|
|
10
11
|
constructor(projectRoot, markerService, manifestParser) {
|
|
11
12
|
this.projectRoot = projectRoot;
|
|
@@ -17,6 +18,11 @@ class ModuleDoctor {
|
|
|
17
18
|
results.push(...(await this.checkAnchors()));
|
|
18
19
|
results.push(...(await this.checkMarkers()));
|
|
19
20
|
results.push(...(await this.checkDependencies()));
|
|
21
|
+
results.push(...(await this.checkSchemaMerge()));
|
|
22
|
+
results.push(...(await this.checkEnvCompleteness()));
|
|
23
|
+
results.push(...(await this.checkLicense()));
|
|
24
|
+
results.push(...(await this.checkFrameworkVersion()));
|
|
25
|
+
results.push(...(await this.checkPrismaClientSync()));
|
|
20
26
|
return results;
|
|
21
27
|
}
|
|
22
28
|
async checkAnchors() {
|
|
@@ -146,6 +152,286 @@ class ModuleDoctor {
|
|
|
146
152
|
}
|
|
147
153
|
return results;
|
|
148
154
|
}
|
|
155
|
+
// ──────────────────────────────────────────────────────────
|
|
156
|
+
// C2.4: New enhanced checks
|
|
157
|
+
// ──────────────────────────────────────────────────────────
|
|
158
|
+
/** Check that the base Prisma schema exists and has no merge conflicts. */
|
|
159
|
+
async checkSchemaMerge() {
|
|
160
|
+
const results = [];
|
|
161
|
+
const baseSchemaPath = path_1.default.join(this.projectRoot, "packages/database/prisma/schema.base.prisma");
|
|
162
|
+
if (!(await fs_extra_1.default.pathExists(baseSchemaPath))) {
|
|
163
|
+
results.push({
|
|
164
|
+
type: "dependency",
|
|
165
|
+
severity: "warning",
|
|
166
|
+
message: "Prisma base schema not found: packages/database/prisma/schema.base.prisma",
|
|
167
|
+
file: "packages/database/prisma/schema.base.prisma",
|
|
168
|
+
fixable: false,
|
|
169
|
+
});
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
// Check for git merge conflict markers
|
|
173
|
+
const schemaDir = path_1.default.dirname(baseSchemaPath);
|
|
174
|
+
try {
|
|
175
|
+
const schemaFiles = await fs_extra_1.default.readdir(schemaDir);
|
|
176
|
+
for (const schemaFile of schemaFiles) {
|
|
177
|
+
if (!schemaFile.endsWith(".prisma"))
|
|
178
|
+
continue;
|
|
179
|
+
const filePath = path_1.default.join(schemaDir, schemaFile);
|
|
180
|
+
const content = await fs_extra_1.default.readFile(filePath, "utf-8");
|
|
181
|
+
if (content.includes("<<<<<<<")) {
|
|
182
|
+
results.push({
|
|
183
|
+
type: "marker",
|
|
184
|
+
severity: "error",
|
|
185
|
+
message: `Merge conflict detected in schema file: ${schemaFile}`,
|
|
186
|
+
file: path_1.default.join("packages/database/prisma", schemaFile),
|
|
187
|
+
fixable: false,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Non-critical
|
|
194
|
+
}
|
|
195
|
+
if (results.length === 0) {
|
|
196
|
+
results.push({
|
|
197
|
+
type: "dependency",
|
|
198
|
+
severity: "info",
|
|
199
|
+
message: "Prisma schema integrity OK",
|
|
200
|
+
fixable: false,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
/** Check that .env has all keys defined in .env.example (non-optional). */
|
|
206
|
+
async checkEnvCompleteness() {
|
|
207
|
+
const results = [];
|
|
208
|
+
const envExamplePath = path_1.default.join(this.projectRoot, ".env.example");
|
|
209
|
+
const envPath = path_1.default.join(this.projectRoot, ".env");
|
|
210
|
+
if (!(await fs_extra_1.default.pathExists(envExamplePath))) {
|
|
211
|
+
results.push({
|
|
212
|
+
type: "dependency",
|
|
213
|
+
severity: "info",
|
|
214
|
+
message: ".env.example not found — skipping env completeness check",
|
|
215
|
+
fixable: false,
|
|
216
|
+
});
|
|
217
|
+
return results;
|
|
218
|
+
}
|
|
219
|
+
if (!(await fs_extra_1.default.pathExists(envPath))) {
|
|
220
|
+
results.push({
|
|
221
|
+
type: "dependency",
|
|
222
|
+
severity: "warning",
|
|
223
|
+
message: ".env file not found. Copy .env.example to .env and fill in values",
|
|
224
|
+
file: ".env",
|
|
225
|
+
fixable: false,
|
|
226
|
+
});
|
|
227
|
+
return results;
|
|
228
|
+
}
|
|
229
|
+
const exampleContent = await fs_extra_1.default.readFile(envExamplePath, "utf-8");
|
|
230
|
+
const envContent = await fs_extra_1.default.readFile(envPath, "utf-8");
|
|
231
|
+
// Parse keys: lines that are KEY=VALUE (not comments, not blank)
|
|
232
|
+
const parseKeys = (content) => {
|
|
233
|
+
const keys = new Set();
|
|
234
|
+
for (const line of content.split("\n")) {
|
|
235
|
+
const trimmed = line.trim();
|
|
236
|
+
if (trimmed.startsWith("#") || !trimmed.includes("="))
|
|
237
|
+
continue;
|
|
238
|
+
const key = trimmed.split("=")[0].trim();
|
|
239
|
+
if (key)
|
|
240
|
+
keys.add(key);
|
|
241
|
+
}
|
|
242
|
+
return keys;
|
|
243
|
+
};
|
|
244
|
+
const exampleKeys = parseKeys(exampleContent);
|
|
245
|
+
const envKeys = parseKeys(envContent);
|
|
246
|
+
const missingKeys = [];
|
|
247
|
+
for (const key of exampleKeys) {
|
|
248
|
+
if (!envKeys.has(key)) {
|
|
249
|
+
missingKeys.push(key);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (missingKeys.length > 0) {
|
|
253
|
+
results.push({
|
|
254
|
+
type: "dependency",
|
|
255
|
+
severity: "warning",
|
|
256
|
+
message: `Missing env vars in .env: ${missingKeys.join(", ")}`,
|
|
257
|
+
file: ".env",
|
|
258
|
+
fixable: true,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
results.push({
|
|
263
|
+
type: "dependency",
|
|
264
|
+
severity: "info",
|
|
265
|
+
message: "Env vars completeness OK",
|
|
266
|
+
fixable: false,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return results;
|
|
270
|
+
}
|
|
271
|
+
/** Check if the stored license is present and not expired. */
|
|
272
|
+
async checkLicense() {
|
|
273
|
+
const results = [];
|
|
274
|
+
const licensePath = path_1.default.join(os_1.default.homedir(), ".kaven", "license.json");
|
|
275
|
+
if (!(await fs_extra_1.default.pathExists(licensePath))) {
|
|
276
|
+
results.push({
|
|
277
|
+
type: "dependency",
|
|
278
|
+
severity: "warning",
|
|
279
|
+
message: "No license found at ~/.kaven/license.json. Run 'kaven license status' to set up.",
|
|
280
|
+
fixable: false,
|
|
281
|
+
});
|
|
282
|
+
return results;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const licenseData = await fs_extra_1.default.readJson(licensePath);
|
|
286
|
+
if (licenseData.expiresAt) {
|
|
287
|
+
const expiresAt = new Date(licenseData.expiresAt).getTime();
|
|
288
|
+
if (Date.now() > expiresAt) {
|
|
289
|
+
results.push({
|
|
290
|
+
type: "dependency",
|
|
291
|
+
severity: "error",
|
|
292
|
+
message: `License expired on ${licenseData.expiresAt}. Run 'kaven upgrade' to renew.`,
|
|
293
|
+
fixable: false,
|
|
294
|
+
});
|
|
295
|
+
return results;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
results.push({
|
|
299
|
+
type: "dependency",
|
|
300
|
+
severity: "info",
|
|
301
|
+
message: `License valid (tier: ${licenseData.tier || "unknown"})`,
|
|
302
|
+
fixable: false,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
results.push({
|
|
307
|
+
type: "dependency",
|
|
308
|
+
severity: "warning",
|
|
309
|
+
message: "Could not read license file. Try 'kaven license status'.",
|
|
310
|
+
fixable: false,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return results;
|
|
314
|
+
}
|
|
315
|
+
/** Check if the framework version in package.json is compatible. */
|
|
316
|
+
async checkFrameworkVersion() {
|
|
317
|
+
const results = [];
|
|
318
|
+
const packageJsonPath = path_1.default.join(this.projectRoot, "package.json");
|
|
319
|
+
if (!(await fs_extra_1.default.pathExists(packageJsonPath))) {
|
|
320
|
+
results.push({
|
|
321
|
+
type: "dependency",
|
|
322
|
+
severity: "info",
|
|
323
|
+
message: "package.json not found — skipping framework version check",
|
|
324
|
+
fixable: false,
|
|
325
|
+
});
|
|
326
|
+
return results;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const packageJson = await fs_extra_1.default.readJSON(packageJsonPath);
|
|
330
|
+
const kavenCoreVersion = packageJson.dependencies?.["@kaven/core"] ||
|
|
331
|
+
packageJson.devDependencies?.["@kaven/core"];
|
|
332
|
+
if (!kavenCoreVersion) {
|
|
333
|
+
results.push({
|
|
334
|
+
type: "dependency",
|
|
335
|
+
severity: "info",
|
|
336
|
+
message: "@kaven/core not found in dependencies — not a Kaven framework project",
|
|
337
|
+
fixable: false,
|
|
338
|
+
});
|
|
339
|
+
return results;
|
|
340
|
+
}
|
|
341
|
+
// Minimum required semver range
|
|
342
|
+
const MINIMUM_VERSION = "1.0.0";
|
|
343
|
+
const versionStr = kavenCoreVersion.replace(/[\^~>=<]/, "").split(" ")[0];
|
|
344
|
+
const parts = versionStr.split(".").map(Number);
|
|
345
|
+
const minParts = MINIMUM_VERSION.split(".").map(Number);
|
|
346
|
+
let compatible = true;
|
|
347
|
+
for (let i = 0; i < 3; i++) {
|
|
348
|
+
if ((parts[i] || 0) > (minParts[i] || 0))
|
|
349
|
+
break;
|
|
350
|
+
if ((parts[i] || 0) < (minParts[i] || 0)) {
|
|
351
|
+
compatible = false;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!compatible) {
|
|
356
|
+
results.push({
|
|
357
|
+
type: "dependency",
|
|
358
|
+
severity: "warning",
|
|
359
|
+
message: `@kaven/core version ${kavenCoreVersion} may be outdated. Minimum: ^${MINIMUM_VERSION}`,
|
|
360
|
+
fixable: false,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
results.push({
|
|
365
|
+
type: "dependency",
|
|
366
|
+
severity: "info",
|
|
367
|
+
message: `Framework version OK (${kavenCoreVersion})`,
|
|
368
|
+
fixable: false,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
results.push({
|
|
374
|
+
type: "dependency",
|
|
375
|
+
severity: "info",
|
|
376
|
+
message: "Could not determine framework version",
|
|
377
|
+
fixable: false,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return results;
|
|
381
|
+
}
|
|
382
|
+
/** Check if Prisma client is generated and up-to-date. */
|
|
383
|
+
async checkPrismaClientSync() {
|
|
384
|
+
const results = [];
|
|
385
|
+
const prismaClientPath = path_1.default.join(this.projectRoot, "node_modules/@prisma/client");
|
|
386
|
+
const schemaPath = path_1.default.join(this.projectRoot, "prisma/schema.prisma");
|
|
387
|
+
if (!(await fs_extra_1.default.pathExists(prismaClientPath))) {
|
|
388
|
+
results.push({
|
|
389
|
+
type: "dependency",
|
|
390
|
+
severity: "warning",
|
|
391
|
+
message: "@prisma/client not found. Run 'npx prisma generate' to generate the client.",
|
|
392
|
+
fixable: true,
|
|
393
|
+
});
|
|
394
|
+
return results;
|
|
395
|
+
}
|
|
396
|
+
if (!(await fs_extra_1.default.pathExists(schemaPath))) {
|
|
397
|
+
results.push({
|
|
398
|
+
type: "dependency",
|
|
399
|
+
severity: "info",
|
|
400
|
+
message: "prisma/schema.prisma not found — skipping Prisma sync check",
|
|
401
|
+
fixable: false,
|
|
402
|
+
});
|
|
403
|
+
return results;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const schemaStat = await fs_extra_1.default.stat(schemaPath);
|
|
407
|
+
const clientStat = await fs_extra_1.default.stat(prismaClientPath);
|
|
408
|
+
if (schemaStat.mtime > clientStat.mtime) {
|
|
409
|
+
results.push({
|
|
410
|
+
type: "dependency",
|
|
411
|
+
severity: "warning",
|
|
412
|
+
message: "Prisma schema was modified after client generation. Run 'npx prisma generate'.",
|
|
413
|
+
fixable: true,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
results.push({
|
|
418
|
+
type: "dependency",
|
|
419
|
+
severity: "info",
|
|
420
|
+
message: "Prisma client is up to date",
|
|
421
|
+
fixable: false,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
results.push({
|
|
427
|
+
type: "dependency",
|
|
428
|
+
severity: "info",
|
|
429
|
+
message: "Could not compare Prisma schema and client timestamps",
|
|
430
|
+
fixable: false,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return results;
|
|
434
|
+
}
|
|
149
435
|
async readKavenConfig() {
|
|
150
436
|
const configPath = path_1.default.join(this.projectRoot, "kaven.config.json");
|
|
151
437
|
if (!(await fs_extra_1.default.pathExists(configPath))) {
|
|
@@ -1,18 +1,93 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.ModuleInstaller = void 0;
|
|
7
40
|
const TransactionalFileSystem_1 = require("../infrastructure/TransactionalFileSystem");
|
|
41
|
+
const ScriptRunner_js_1 = require("./ScriptRunner.js");
|
|
8
42
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
43
|
const path_1 = __importDefault(require("path"));
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
45
|
class ModuleInstaller {
|
|
11
46
|
constructor(projectRoot, markerService) {
|
|
12
47
|
this.projectRoot = projectRoot;
|
|
13
48
|
this.markerService = markerService;
|
|
14
49
|
}
|
|
15
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Check whether the given module slug is already installed by scanning
|
|
52
|
+
* project files for its begin/end markers.
|
|
53
|
+
*/
|
|
54
|
+
async isModuleInstalled(moduleName) {
|
|
55
|
+
try {
|
|
56
|
+
const filesToCheck = await this.findProjectFiles();
|
|
57
|
+
for (const filePath of filesToCheck) {
|
|
58
|
+
try {
|
|
59
|
+
const content = await fs_extra_1.default.readFile(filePath, "utf-8");
|
|
60
|
+
if (this.markerService.hasModule(content, moduleName)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Skip unreadable files
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Find text-based project source files to check for markers. */
|
|
75
|
+
async findProjectFiles() {
|
|
76
|
+
const { glob } = await Promise.resolve().then(() => __importStar(require("glob")));
|
|
77
|
+
const patterns = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"];
|
|
78
|
+
const ignore = ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"];
|
|
79
|
+
const files = [];
|
|
80
|
+
for (const pattern of patterns) {
|
|
81
|
+
const found = await glob(pattern, {
|
|
82
|
+
cwd: this.projectRoot,
|
|
83
|
+
absolute: true,
|
|
84
|
+
ignore,
|
|
85
|
+
});
|
|
86
|
+
files.push(...found);
|
|
87
|
+
}
|
|
88
|
+
return [...new Set(files)];
|
|
89
|
+
}
|
|
90
|
+
async install(manifest, options) {
|
|
16
91
|
const tx = new TransactionalFileSystem_1.TransactionalFileSystem(this.projectRoot);
|
|
17
92
|
try {
|
|
18
93
|
const filesToModify = Array.from(new Set(manifest.injections.map((inj) => inj.file)));
|
|
@@ -21,6 +96,36 @@ class ModuleInstaller {
|
|
|
21
96
|
await this.injectCode(injection);
|
|
22
97
|
}
|
|
23
98
|
await tx.commit();
|
|
99
|
+
// Run postInstall lifecycle scripts if defined in module.json
|
|
100
|
+
const moduleJson = await this.readManifest(this.projectRoot);
|
|
101
|
+
if (moduleJson?.scripts?.postInstall?.length) {
|
|
102
|
+
const runner = new ScriptRunner_js_1.ScriptRunner();
|
|
103
|
+
try {
|
|
104
|
+
await runner.runScripts(moduleJson.scripts.postInstall.map((s) => ({ ...s, cwd: this.projectRoot })), 'postInstall', false);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
108
|
+
console.warn(chalk_1.default.yellow(`
|
|
109
|
+
⚠ PostInstall script failed: ${errMsg}`));
|
|
110
|
+
console.warn(chalk_1.default.dim(' The module is installed. Run the script manually if needed.'));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Inject environment variables if defined in module.json
|
|
114
|
+
if (!options?.skipEnv && moduleJson?.env?.length) {
|
|
115
|
+
const { EnvManager } = await Promise.resolve().then(() => __importStar(require('./EnvManager.js')));
|
|
116
|
+
const envManager = new EnvManager();
|
|
117
|
+
const envVarDefs = moduleJson.env.map((e) => ({
|
|
118
|
+
name: e.key,
|
|
119
|
+
description: e.example ?? e.key,
|
|
120
|
+
required: e.required ?? false,
|
|
121
|
+
}));
|
|
122
|
+
await envManager.injectEnvVars(manifest.name, envVarDefs, {
|
|
123
|
+
projectDir: this.projectRoot,
|
|
124
|
+
envFile: options?.envFile,
|
|
125
|
+
skipEnv: options?.skipEnv,
|
|
126
|
+
skipConfirmation: options?.yes,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
24
129
|
}
|
|
25
130
|
catch (error) {
|
|
26
131
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -30,11 +135,31 @@ class ModuleInstaller {
|
|
|
30
135
|
throw error;
|
|
31
136
|
}
|
|
32
137
|
}
|
|
33
|
-
async uninstall(manifest) {
|
|
138
|
+
async uninstall(manifest, options) {
|
|
34
139
|
const tx = new TransactionalFileSystem_1.TransactionalFileSystem(this.projectRoot);
|
|
35
140
|
try {
|
|
36
141
|
const filesToModify = Array.from(new Set(manifest.injections.map((inj) => inj.file)));
|
|
37
142
|
await tx.backup(filesToModify);
|
|
143
|
+
// Remove environment variables before removing files
|
|
144
|
+
const { EnvManager } = await Promise.resolve().then(() => __importStar(require('./EnvManager.js')));
|
|
145
|
+
const envManager = new EnvManager();
|
|
146
|
+
envManager.removeEnvVars(manifest.name, {
|
|
147
|
+
projectDir: this.projectRoot,
|
|
148
|
+
skipEnv: options?.skipEnv,
|
|
149
|
+
});
|
|
150
|
+
// Run preRemove lifecycle scripts if defined in module.json
|
|
151
|
+
const moduleJson = await this.readManifest(this.projectRoot);
|
|
152
|
+
if (moduleJson?.scripts?.preRemove?.length) {
|
|
153
|
+
const runner = new ScriptRunner_js_1.ScriptRunner();
|
|
154
|
+
try {
|
|
155
|
+
await runner.runScripts(moduleJson.scripts.preRemove.map((s) => ({ ...s, cwd: this.projectRoot })), 'preRemove', false);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
159
|
+
console.warn(chalk_1.default.yellow(`
|
|
160
|
+
⚠ PreRemove script failed: ${errMsg}`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
38
163
|
// Na desinstalação, removemos por arquivo para evitar múltiplas tentativas
|
|
39
164
|
// de remover marcadores que o regex global já removeu.
|
|
40
165
|
for (const fileName of filesToModify) {
|
|
@@ -62,5 +187,14 @@ class ModuleInstaller {
|
|
|
62
187
|
const updated = this.markerService.removeModule(content, moduleName);
|
|
63
188
|
await fs_extra_1.default.writeFile(filePath, updated);
|
|
64
189
|
}
|
|
190
|
+
async readManifest(dir) {
|
|
191
|
+
try {
|
|
192
|
+
const raw = await fs_extra_1.default.readFile(path_1.default.join(dir, 'module.json'), 'utf-8');
|
|
193
|
+
return JSON.parse(raw);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
65
199
|
}
|
|
66
200
|
exports.ModuleInstaller = ModuleInstaller;
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
/** Run a shell command via spawn, returning exit code. */
|
|
12
|
+
function runCommand(cmd, args, cwd, onData) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const proc = (0, child_process_1.spawn)(cmd, args, { cwd, stdio: onData ? "pipe" : "inherit" });
|
|
15
|
+
if (onData && proc.stdout) {
|
|
16
|
+
proc.stdout.on("data", (d) => onData(d.toString()));
|
|
17
|
+
}
|
|
18
|
+
if (onData && proc.stderr) {
|
|
19
|
+
proc.stderr.on("data", (d) => onData(d.toString()));
|
|
20
|
+
}
|
|
21
|
+
proc.on("error", reject);
|
|
22
|
+
proc.on("close", (code) => resolve(code ?? 0));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
class ProjectInitializer {
|
|
26
|
+
/** Validate that the project name only contains alphanumerics and hyphens. */
|
|
27
|
+
validateName(name) {
|
|
28
|
+
if (!name || name.trim().length === 0) {
|
|
29
|
+
return { valid: false, reason: "Project name cannot be empty" };
|
|
30
|
+
}
|
|
31
|
+
if (/\s/.test(name)) {
|
|
32
|
+
return { valid: false, reason: "Project name cannot contain spaces" };
|
|
33
|
+
}
|
|
34
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
35
|
+
return {
|
|
36
|
+
valid: false,
|
|
37
|
+
reason: "Project name must only contain lowercase letters, numbers, and hyphens",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { valid: true };
|
|
41
|
+
}
|
|
42
|
+
/** Clone the template repo with --depth 1 into targetDir. */
|
|
43
|
+
async cloneTemplate(targetDir) {
|
|
44
|
+
const exitCode = await runCommand("git", ["clone", "--depth", "1", TEMPLATE_REPO, targetDir], process.cwd());
|
|
45
|
+
if (exitCode !== 0) {
|
|
46
|
+
throw new Error(`git clone failed with exit code ${exitCode}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Remove the .git directory from the cloned project. */
|
|
50
|
+
async removeGitDir(targetDir) {
|
|
51
|
+
const gitDir = path_1.default.join(targetDir, ".git");
|
|
52
|
+
if (await fs_extra_1.default.pathExists(gitDir)) {
|
|
53
|
+
await fs_extra_1.default.remove(gitDir);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Replace placeholders in key project files. */
|
|
57
|
+
async replacePlaceholders(targetDir, values) {
|
|
58
|
+
const replacements = {
|
|
59
|
+
"{{PROJECT_NAME}}": values.projectName,
|
|
60
|
+
"{{DATABASE_URL}}": values.dbUrl,
|
|
61
|
+
"{{DEFAULT_LOCALE}}": values.locale,
|
|
62
|
+
"{{DEFAULT_CURRENCY}}": values.currency,
|
|
63
|
+
};
|
|
64
|
+
const filesToProcess = [
|
|
65
|
+
"package.json",
|
|
66
|
+
".env.example",
|
|
67
|
+
"prisma/schema.prisma",
|
|
68
|
+
"apps/api/package.json",
|
|
69
|
+
"apps/admin/package.json",
|
|
70
|
+
"apps/tenant/package.json",
|
|
71
|
+
];
|
|
72
|
+
for (const relFile of filesToProcess) {
|
|
73
|
+
const filePath = path_1.default.join(targetDir, relFile);
|
|
74
|
+
if (!(await fs_extra_1.default.pathExists(filePath)))
|
|
75
|
+
continue;
|
|
76
|
+
let content = await fs_extra_1.default.readFile(filePath, "utf-8");
|
|
77
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
78
|
+
content = content.split(placeholder).join(value);
|
|
79
|
+
}
|
|
80
|
+
await fs_extra_1.default.writeFile(filePath, content, "utf-8");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Run pnpm install in the target directory. */
|
|
84
|
+
async runInstall(targetDir) {
|
|
85
|
+
const exitCode = await runCommand("pnpm", ["install"], targetDir);
|
|
86
|
+
if (exitCode !== 0) {
|
|
87
|
+
throw new Error(`pnpm install failed with exit code ${exitCode}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Initialize git and create an initial commit. */
|
|
91
|
+
async initGit(targetDir) {
|
|
92
|
+
await runCommand("git", ["init"], targetDir);
|
|
93
|
+
await runCommand("git", ["add", "."], targetDir);
|
|
94
|
+
await runCommand("git", ["commit", "-m", "chore: initial kaven setup"], targetDir);
|
|
95
|
+
}
|
|
96
|
+
/** Health check after project initialization. */
|
|
97
|
+
async healthCheck(targetDir) {
|
|
98
|
+
const issues = [];
|
|
99
|
+
// Check key files exist
|
|
100
|
+
const requiredFiles = ["package.json", ".env.example", "prisma/schema.prisma"];
|
|
101
|
+
for (const file of requiredFiles) {
|
|
102
|
+
if (!(await fs_extra_1.default.pathExists(path_1.default.join(targetDir, file)))) {
|
|
103
|
+
issues.push(`Missing required file: ${file}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Check node_modules exists (if install was run)
|
|
107
|
+
const hasNodeModules = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "node_modules"));
|
|
108
|
+
if (!hasNodeModules) {
|
|
109
|
+
issues.push("Dependencies not installed. Run: pnpm install");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
healthy: issues.length === 0,
|
|
113
|
+
issues,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.ProjectInitializer = ProjectInitializer;
|