arkormx 0.2.5 → 0.2.6

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/dist/cli.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
3
- import path, { dirname, extname, join, relative } from "path";
4
- import { createRequire } from "module";
5
- import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
6
- import { dirname as dirname$1, extname as extname$1, join as join$1, resolve } from "node:path";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, extname, join, resolve } from "node:path";
7
4
  import { spawnSync } from "node:child_process";
8
5
  import { str } from "@h3ravel/support";
6
+ import path, { dirname as dirname$1, extname as extname$1, join as join$1, relative } from "path";
7
+ import { copyFileSync, existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
9
8
  import { fileURLToPath, pathToFileURL } from "url";
9
+ import { createRequire } from "module";
10
10
  import { Logger } from "@h3ravel/shared";
11
11
  import { Command, Kernel } from "@h3ravel/musket";
12
12
  import { createHash } from "node:crypto";
@@ -1008,14 +1008,14 @@ const generateMigrationFile = (name, options = {}) => {
1008
1008
  const fileSlug = toMigrationFileSlug(name);
1009
1009
  const className = resolveMigrationClassName(name);
1010
1010
  const extension = options.extension ?? "ts";
1011
- const directory = options.directory ?? join$1(process.cwd(), "database", "migrations");
1011
+ const directory = options.directory ?? join(process.cwd(), "database", "migrations");
1012
1012
  const fileName = `${timestamp}_${fileSlug}.${extension}`;
1013
- const filePath = join$1(directory, fileName);
1013
+ const filePath = join(directory, fileName);
1014
1014
  const content = buildMigrationSource(className, extension);
1015
1015
  if (options.write ?? true) {
1016
- if (!existsSync$1(directory)) mkdirSync$1(directory, { recursive: true });
1017
- if (existsSync$1(filePath)) throw new ArkormException(`Migration file already exists: ${filePath}`);
1018
- writeFileSync$1(filePath, content);
1016
+ if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
1017
+ if (existsSync(filePath)) throw new ArkormException(`Migration file already exists: ${filePath}`);
1018
+ writeFileSync(filePath, content);
1019
1019
  }
1020
1020
  return {
1021
1021
  fileName,
@@ -1048,12 +1048,32 @@ const getMigrationPlan = async (migration, direction = "up") => {
1048
1048
  * @returns A promise that resolves to an object containing the updated schema, schema path, and list of operations applied.
1049
1049
  */
1050
1050
  const applyMigrationToPrismaSchema = async (migration, options = {}) => {
1051
- const schemaPath = options.schemaPath ?? join$1(process.cwd(), "prisma", "schema.prisma");
1052
- if (!existsSync$1(schemaPath)) throw new ArkormException(`Prisma schema file not found: ${schemaPath}`);
1053
- const source = readFileSync$1(schemaPath, "utf-8");
1051
+ const schemaPath = options.schemaPath ?? join(process.cwd(), "prisma", "schema.prisma");
1052
+ if (!existsSync(schemaPath)) throw new ArkormException(`Prisma schema file not found: ${schemaPath}`);
1053
+ const source = readFileSync(schemaPath, "utf-8");
1054
1054
  const operations = await getMigrationPlan(migration, "up");
1055
1055
  const schema = applyOperationsToPrismaSchema(source, operations);
1056
- if (options.write ?? true) writeFileSync$1(schemaPath, schema);
1056
+ if (options.write ?? true) writeFileSync(schemaPath, schema);
1057
+ return {
1058
+ schema,
1059
+ schemaPath,
1060
+ operations
1061
+ };
1062
+ };
1063
+ /**
1064
+ * Apply the rollback (down) operations defined in a migration to a Prisma schema file.
1065
+ *
1066
+ * @param migration The migration class or instance to rollback.
1067
+ * @param options Options for applying the rollback, including schema path and write flag.
1068
+ * @returns A promise that resolves to an object containing the updated schema, schema path, and rollback operations applied.
1069
+ */
1070
+ const applyMigrationRollbackToPrismaSchema = async (migration, options = {}) => {
1071
+ const schemaPath = options.schemaPath ?? join(process.cwd(), "prisma", "schema.prisma");
1072
+ if (!existsSync(schemaPath)) throw new ArkormException(`Prisma schema file not found: ${schemaPath}`);
1073
+ const source = readFileSync(schemaPath, "utf-8");
1074
+ const operations = await getMigrationPlan(migration, "down");
1075
+ const schema = applyOperationsToPrismaSchema(source, operations);
1076
+ if (options.write ?? true) writeFileSync(schemaPath, schema);
1057
1077
  return {
1058
1078
  schema,
1059
1079
  schemaPath,
@@ -1068,7 +1088,7 @@ const resolveDefaultStubsPath = () => {
1068
1088
  while (true) {
1069
1089
  const packageJsonPath = path.join(current, "package.json");
1070
1090
  const stubsPath = path.join(current, "stubs");
1071
- if (existsSync(packageJsonPath) && existsSync(stubsPath)) return stubsPath;
1091
+ if (existsSync$1(packageJsonPath) && existsSync$1(stubsPath)) return stubsPath;
1072
1092
  const parent = path.dirname(current);
1073
1093
  if (parent === current) break;
1074
1094
  current = parent;
@@ -1167,7 +1187,7 @@ const loadRuntimeConfigSync = () => {
1167
1187
  const require = createRequire(import.meta.url);
1168
1188
  const syncConfigPaths = [path.join(process.cwd(), "arkormx.config.cjs")];
1169
1189
  for (const configPath of syncConfigPaths) {
1170
- if (!existsSync(configPath)) continue;
1190
+ if (!existsSync$1(configPath)) continue;
1171
1191
  try {
1172
1192
  resolveAndApplyConfig(require(configPath));
1173
1193
  return true;
@@ -1189,7 +1209,7 @@ const loadArkormConfig = async () => {
1189
1209
  runtimeConfigLoadingPromise = (async () => {
1190
1210
  const configPaths = [path.join(process.cwd(), "arkormx.config.js"), path.join(process.cwd(), "arkormx.config.ts")];
1191
1211
  for (const configPath of configPaths) {
1192
- if (!existsSync(configPath)) continue;
1212
+ if (!existsSync$1(configPath)) continue;
1193
1213
  try {
1194
1214
  resolveAndApplyConfig(await importConfigFile(configPath));
1195
1215
  return;
@@ -1233,8 +1253,8 @@ var CliApp = class {
1233
1253
  * @param filePath
1234
1254
  */
1235
1255
  ensureDirectory(filePath) {
1236
- const dir = dirname(filePath);
1237
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1256
+ const dir = dirname$1(filePath);
1257
+ if (!existsSync$1(dir)) mkdirSync$1(dir, { recursive: true });
1238
1258
  }
1239
1259
  /**
1240
1260
  * Convert absolute paths under current working directory into relative display paths.
@@ -1283,13 +1303,13 @@ var CliApp = class {
1283
1303
  * @returns
1284
1304
  */
1285
1305
  resolveRuntimeDirectoryPath(directoryPath) {
1286
- if (existsSync(directoryPath)) return directoryPath;
1306
+ if (existsSync$1(directoryPath)) return directoryPath;
1287
1307
  const { buildOutput } = this.getConfig("paths") || {};
1288
1308
  if (typeof buildOutput !== "string" || buildOutput.trim().length === 0) return directoryPath;
1289
1309
  const relativeSource = relative(process.cwd(), directoryPath);
1290
1310
  if (!relativeSource || relativeSource.startsWith("..")) return directoryPath;
1291
- const mappedDirectory = join(buildOutput, relativeSource);
1292
- return existsSync(mappedDirectory) ? mappedDirectory : directoryPath;
1311
+ const mappedDirectory = join$1(buildOutput, relativeSource);
1312
+ return existsSync$1(mappedDirectory) ? mappedDirectory : directoryPath;
1293
1313
  }
1294
1314
  /**
1295
1315
  * Resolve a script file path for runtime execution.
@@ -1300,7 +1320,7 @@ var CliApp = class {
1300
1320
  * @returns
1301
1321
  */
1302
1322
  resolveRuntimeScriptPath(filePath) {
1303
- const extension = extname(filePath).toLowerCase();
1323
+ const extension = extname$1(filePath).toLowerCase();
1304
1324
  const isTsFile = extension === ".ts" || extension === ".mts" || extension === ".cts";
1305
1325
  const candidates = [];
1306
1326
  if (isTsFile) {
@@ -1311,15 +1331,15 @@ var CliApp = class {
1311
1331
  if (typeof buildOutput === "string" && buildOutput.trim().length > 0) {
1312
1332
  const relativeSource = relative(process.cwd(), filePath);
1313
1333
  if (relativeSource && !relativeSource.startsWith("..")) {
1314
- const mappedFile = join(buildOutput, relativeSource);
1315
- const mappedExtension = extname(mappedFile).toLowerCase();
1334
+ const mappedFile = join$1(buildOutput, relativeSource);
1335
+ const mappedExtension = extname$1(mappedFile).toLowerCase();
1316
1336
  if (mappedExtension === ".ts" || mappedExtension === ".mts" || mappedExtension === ".cts") {
1317
1337
  const mappedBase = mappedFile.slice(0, -mappedExtension.length);
1318
1338
  candidates.push(`${mappedBase}.js`, `${mappedBase}.cjs`, `${mappedBase}.mjs`);
1319
1339
  } else candidates.push(mappedFile);
1320
1340
  }
1321
1341
  }
1322
- const runtimeMatch = candidates.find((path) => existsSync(path));
1342
+ const runtimeMatch = candidates.find((path) => existsSync$1(path));
1323
1343
  if (runtimeMatch) return runtimeMatch;
1324
1344
  return filePath;
1325
1345
  }
@@ -1331,14 +1351,14 @@ var CliApp = class {
1331
1351
  * @param replacements
1332
1352
  */
1333
1353
  generateFile(stubPath, outputPath, replacements, options) {
1334
- if (existsSync(outputPath) && !options?.force) {
1354
+ if (existsSync$1(outputPath) && !options?.force) {
1335
1355
  this.command.error(`Error: ${this.formatPathForLog(outputPath)} already exists.`);
1336
1356
  process.exit(1);
1337
- } else if (existsSync(outputPath) && options?.force) rmSync(outputPath);
1338
- let content = readFileSync(stubPath, "utf-8");
1357
+ } else if (existsSync$1(outputPath) && options?.force) rmSync$1(outputPath);
1358
+ let content = readFileSync$1(stubPath, "utf-8");
1339
1359
  for (const [key, value] of Object.entries(replacements)) content = content.replace(new RegExp(`{{${key}}}`, "g"), value);
1340
1360
  this.ensureDirectory(outputPath);
1341
- writeFileSync(outputPath, content);
1361
+ writeFileSync$1(outputPath, content);
1342
1362
  return outputPath;
1343
1363
  }
1344
1364
  /**
@@ -1360,7 +1380,7 @@ var CliApp = class {
1360
1380
  * @returns
1361
1381
  */
1362
1382
  resolveStubPath(stubName) {
1363
- return join(this.resolveConfigPath("stubs", getDefaultStubsPath()), stubName);
1383
+ return join$1(this.resolveConfigPath("stubs", getDefaultStubsPath()), stubName);
1364
1384
  }
1365
1385
  /**
1366
1386
  * Generate a factory file for a given model name.
@@ -1374,9 +1394,9 @@ var CliApp = class {
1374
1394
  const factoryName = `${baseName}Factory`;
1375
1395
  const modelName = options.modelName ? str(options.modelName).pascal() : baseName;
1376
1396
  const outputExt = this.resolveOutputExt();
1377
- const outputPath = join(this.resolveConfigPath("factories", join(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
1378
- const modelPath = join(this.resolveConfigPath("models", join(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
1379
- const relativeImport = options.modelImportPath ?? `./${this.stripKnownSourceExtension(relative(dirname(outputPath), modelPath).replace(/\\/g, "/"))}${outputExt === "js" ? ".js" : ""}`;
1397
+ const outputPath = join$1(this.resolveConfigPath("factories", join$1(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
1398
+ const modelPath = join$1(this.resolveConfigPath("models", join$1(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
1399
+ const relativeImport = options.modelImportPath ?? `./${this.stripKnownSourceExtension(relative(dirname$1(outputPath), modelPath).replace(/\\/g, "/"))}${outputExt === "js" ? ".js" : ""}`;
1380
1400
  const stubPath = this.resolveStubPath(outputExt === "js" ? "factory.js.stub" : "factory.stub");
1381
1401
  return {
1382
1402
  name: factoryName,
@@ -1397,7 +1417,7 @@ var CliApp = class {
1397
1417
  makeSeeder(name, options = {}) {
1398
1418
  const seederName = `${str(name.replace(/Seeder$/, "")).pascal()}Seeder`;
1399
1419
  const outputExt = this.resolveOutputExt();
1400
- const outputPath = join(this.resolveConfigPath("seeders", join(process.cwd(), "database", "seeders")), `${seederName}.${outputExt}`);
1420
+ const outputPath = join$1(this.resolveConfigPath("seeders", join$1(process.cwd(), "database", "seeders")), `${seederName}.${outputExt}`);
1401
1421
  const stubPath = this.resolveStubPath(outputExt === "js" ? "seeder.js.stub" : "seeder.stub");
1402
1422
  return {
1403
1423
  name: seederName,
@@ -1412,7 +1432,7 @@ var CliApp = class {
1412
1432
  */
1413
1433
  makeMigration(name) {
1414
1434
  const generated = generateMigrationFile(name, {
1415
- directory: this.resolveConfigPath("migrations", join(process.cwd(), "database", "migrations")),
1435
+ directory: this.resolveConfigPath("migrations", join$1(process.cwd(), "database", "migrations")),
1416
1436
  extension: this.resolveOutputExt()
1417
1437
  });
1418
1438
  return {
@@ -1432,13 +1452,13 @@ var CliApp = class {
1432
1452
  const modelName = `${baseName}`;
1433
1453
  const delegateName = str(baseName).camel().plural().toString();
1434
1454
  const outputExt = this.resolveOutputExt();
1435
- const outputPath = join(this.resolveConfigPath("models", join(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
1455
+ const outputPath = join$1(this.resolveConfigPath("models", join$1(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
1436
1456
  const shouldBuildFactory = options.all || options.factory;
1437
1457
  const shouldBuildSeeder = options.all || options.seeder;
1438
1458
  const shouldBuildMigration = options.all || options.migration;
1439
1459
  const factoryName = `${baseName}Factory`;
1440
- const factoryPath = join(this.resolveConfigPath("factories", join(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
1441
- const factoryImportPath = `./${relative(dirname(outputPath), factoryPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`;
1460
+ const factoryPath = join$1(this.resolveConfigPath("factories", join$1(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
1461
+ const factoryImportPath = `./${relative(dirname$1(outputPath), factoryPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`;
1442
1462
  const stubPath = this.resolveStubPath(outputExt === "js" ? "model.js.stub" : "model.stub");
1443
1463
  const modelPath = this.generateFile(stubPath, outputPath, {
1444
1464
  ModelName: modelName,
@@ -1460,7 +1480,7 @@ var CliApp = class {
1460
1480
  if (shouldBuildFactory) created.factory = this.makeFactory(baseName, {
1461
1481
  force: options.force,
1462
1482
  modelName,
1463
- modelImportPath: `./${relative(dirname(factoryPath), outputPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`
1483
+ modelImportPath: `./${relative(dirname$1(factoryPath), outputPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`
1464
1484
  });
1465
1485
  if (shouldBuildSeeder) created.seeder = this.makeSeeder(baseName, { force: options.force });
1466
1486
  if (shouldBuildMigration) created.migration = this.makeMigration(`create ${delegateName} table`);
@@ -1475,26 +1495,28 @@ var CliApp = class {
1475
1495
  * @param delegateName The name of the delegate (table) to ensure in the Prisma schema.
1476
1496
  */
1477
1497
  ensurePrismaModelEntry(modelName, delegateName) {
1478
- const schemaPath = join(process.cwd(), "prisma", "schema.prisma");
1479
- if (!existsSync(schemaPath)) return {
1498
+ const schemaPath = join$1(process.cwd(), "prisma", "schema.prisma");
1499
+ if (!existsSync$1(schemaPath)) return {
1480
1500
  path: schemaPath,
1481
1501
  updated: false
1482
1502
  };
1483
- const source = readFileSync(schemaPath, "utf-8");
1503
+ const source = readFileSync$1(schemaPath, "utf-8");
1484
1504
  const existingByTable = findModelBlock(source, delegateName);
1485
1505
  const existingByName = new RegExp(`model\\s+${modelName}\\s*\\{`, "m").test(source);
1486
1506
  if (existingByTable || existingByName) return {
1487
1507
  path: schemaPath,
1488
1508
  updated: false
1489
1509
  };
1490
- writeFileSync(schemaPath, applyCreateTableOperation(source, {
1510
+ writeFileSync$1(schemaPath, applyCreateTableOperation(source, {
1491
1511
  type: "createTable",
1492
1512
  table: delegateName,
1493
1513
  columns: [{
1494
1514
  name: "id",
1495
1515
  type: "id",
1496
1516
  primary: true
1497
- }]
1517
+ }],
1518
+ indexes: [],
1519
+ foreignKeys: []
1498
1520
  }));
1499
1521
  return {
1500
1522
  path: schemaPath,
@@ -1546,14 +1568,14 @@ var CliApp = class {
1546
1568
  body.split("\n").forEach((rawLine) => {
1547
1569
  const line = rawLine.trim();
1548
1570
  if (!line || line.startsWith("@@") || line.startsWith("//")) return;
1549
- const fieldMatch = line.match(/^(\w+)\s+([A-Za-z]+)(\?)?\b/);
1571
+ const fieldMatch = line.match(/^(\w+)\s+([A-Za-z]+)(\?)?(?:\s|$)/);
1550
1572
  if (!fieldMatch) return;
1551
1573
  const fieldType = fieldMatch[2];
1552
1574
  if (!scalarTypes.has(fieldType)) return;
1553
1575
  fields.push({
1554
1576
  name: fieldMatch[1],
1555
1577
  type: this.prismaTypeToTs(fieldType),
1556
- optional: Boolean(fieldMatch[3])
1578
+ nullable: Boolean(fieldMatch[3])
1557
1579
  });
1558
1580
  });
1559
1581
  models.push({
@@ -1620,18 +1642,18 @@ var CliApp = class {
1620
1642
  * @returns An object with details about the synchronization process, including updated and skipped files.
1621
1643
  */
1622
1644
  syncModelsFromPrisma(options = {}) {
1623
- const schemaPath = options.schemaPath ?? join(process.cwd(), "prisma", "schema.prisma");
1624
- const modelsDir = options.modelsDir ?? this.resolveConfigPath("models", join(process.cwd(), "src", "models"));
1625
- if (!existsSync(schemaPath)) throw new Error(`Prisma schema file not found: ${schemaPath}`);
1626
- if (!existsSync(modelsDir)) throw new Error(`Models directory not found: ${modelsDir}`);
1627
- const schema = readFileSync(schemaPath, "utf-8");
1645
+ const schemaPath = options.schemaPath ?? join$1(process.cwd(), "prisma", "schema.prisma");
1646
+ const modelsDir = options.modelsDir ?? this.resolveConfigPath("models", join$1(process.cwd(), "src", "models"));
1647
+ if (!existsSync$1(schemaPath)) throw new Error(`Prisma schema file not found: ${schemaPath}`);
1648
+ if (!existsSync$1(modelsDir)) throw new Error(`Models directory not found: ${modelsDir}`);
1649
+ const schema = readFileSync$1(schemaPath, "utf-8");
1628
1650
  const prismaModels = this.parsePrismaModels(schema);
1629
- const modelFiles = readdirSync(modelsDir).filter((file) => file.endsWith(".ts"));
1651
+ const modelFiles = readdirSync$1(modelsDir).filter((file) => file.endsWith(".ts"));
1630
1652
  const updated = [];
1631
1653
  const skipped = [];
1632
1654
  modelFiles.forEach((file) => {
1633
- const filePath = join(modelsDir, file);
1634
- const source = readFileSync(filePath, "utf-8");
1655
+ const filePath = join$1(modelsDir, file);
1656
+ const source = readFileSync$1(filePath, "utf-8");
1635
1657
  const classMatch = source.match(/export\s+class\s+(\w+)\s+extends\s+Model<'([^']+)'>/);
1636
1658
  if (!classMatch) {
1637
1659
  skipped.push(filePath);
@@ -1644,13 +1666,13 @@ var CliApp = class {
1644
1666
  skipped.push(filePath);
1645
1667
  return;
1646
1668
  }
1647
- const declarations = prismaModel.fields.map((field) => `declare ${field.name}${field.optional ? "?" : ""}: ${field.type}`);
1669
+ const declarations = prismaModel.fields.map((field) => `declare ${field.name}: ${field.type}${field.nullable ? " | null" : ""}`);
1648
1670
  const synced = this.syncModelDeclarations(source, declarations);
1649
1671
  if (!synced.updated) {
1650
1672
  skipped.push(filePath);
1651
1673
  return;
1652
1674
  }
1653
- writeFileSync(filePath, synced.content);
1675
+ writeFileSync$1(filePath, synced.content);
1654
1676
  updated.push(filePath);
1655
1677
  });
1656
1678
  return {
@@ -1682,23 +1704,23 @@ var InitCommand = class extends Command {
1682
1704
  */
1683
1705
  async handle() {
1684
1706
  this.app.command = this;
1685
- const outputDir = join$1(process.cwd(), "arkormx.config.js");
1707
+ const outputDir = join(process.cwd(), "arkormx.config.js");
1686
1708
  const { stubs } = getUserConfig("paths") ?? {};
1687
1709
  const stubsDir = typeof stubs === "string" && stubs.trim().length > 0 ? stubs : getDefaultStubsPath();
1688
- const preferredStubPath = join$1(stubsDir, "arkormx.config.stub");
1689
- const legacyStubPath = join$1(stubsDir, "arkorm.config.stub");
1690
- const stubPath = existsSync(preferredStubPath) ? preferredStubPath : legacyStubPath;
1691
- if (existsSync(outputDir) && !this.option("force")) {
1710
+ const preferredStubPath = join(stubsDir, "arkormx.config.stub");
1711
+ const legacyStubPath = join(stubsDir, "arkorm.config.stub");
1712
+ const stubPath = existsSync$1(preferredStubPath) ? preferredStubPath : legacyStubPath;
1713
+ if (existsSync$1(outputDir) && !this.option("force")) {
1692
1714
  this.error("Error: Arkormˣ has already been initialized. Use --force to reinitialize.");
1693
1715
  process.exit(1);
1694
1716
  }
1695
1717
  this.app.ensureDirectory(outputDir);
1696
- if (existsSync(outputDir) && this.option("force")) copyFileSync(outputDir, outputDir.replace(/\.js$/, `.backup.${Date.now()}.js`));
1697
- if (!existsSync(stubPath)) {
1718
+ if (existsSync$1(outputDir) && this.option("force")) copyFileSync(outputDir, outputDir.replace(/\.js$/, `.backup.${Date.now()}.js`));
1719
+ if (!existsSync$1(stubPath)) {
1698
1720
  this.error(`Error: Missing config stub at ${preferredStubPath} (or ${legacyStubPath})`);
1699
1721
  process.exit(1);
1700
1722
  }
1701
- writeFileSync(outputDir, readFileSync(stubPath, "utf-8"));
1723
+ writeFileSync$1(outputDir, readFileSync$1(stubPath, "utf-8"));
1702
1724
  this.success("Arkormˣ initialized successfully!");
1703
1725
  }
1704
1726
  };
@@ -1828,39 +1850,43 @@ var MakeSeederCommand = class extends Command {
1828
1850
  //#region src/helpers/migration-history.ts
1829
1851
  const DEFAULT_STATE = {
1830
1852
  version: 1,
1831
- migrations: []
1853
+ migrations: [],
1854
+ runs: []
1832
1855
  };
1833
1856
  const resolveMigrationStateFilePath = (cwd, configuredPath) => {
1834
1857
  if (configuredPath && configuredPath.trim().length > 0) return resolve(configuredPath);
1835
- return join$1(cwd, ".arkormx", "migrations.applied.json");
1858
+ return join(cwd, ".arkormx", "migrations.applied.json");
1836
1859
  };
1837
1860
  const buildMigrationIdentity = (filePath, className) => {
1838
1861
  const fileName = filePath.split("/").pop()?.split("\\").pop() ?? filePath;
1839
- return `${fileName.slice(0, fileName.length - extname$1(fileName).length)}:${className}`;
1862
+ return `${fileName.slice(0, fileName.length - extname(fileName).length)}:${className}`;
1840
1863
  };
1841
1864
  const computeMigrationChecksum = (filePath) => {
1842
- const source = readFileSync$1(filePath, "utf-8");
1865
+ const source = readFileSync(filePath, "utf-8");
1843
1866
  return createHash("sha256").update(source).digest("hex");
1844
1867
  };
1845
1868
  const readAppliedMigrationsState = (stateFilePath) => {
1846
- if (!existsSync$1(stateFilePath)) return { ...DEFAULT_STATE };
1869
+ if (!existsSync(stateFilePath)) return { ...DEFAULT_STATE };
1847
1870
  try {
1848
- const parsed = JSON.parse(readFileSync$1(stateFilePath, "utf-8"));
1871
+ const parsed = JSON.parse(readFileSync(stateFilePath, "utf-8"));
1849
1872
  if (!Array.isArray(parsed.migrations)) return { ...DEFAULT_STATE };
1850
1873
  return {
1851
1874
  version: 1,
1852
1875
  migrations: parsed.migrations.filter((migration) => {
1853
1876
  return typeof migration?.id === "string" && typeof migration?.file === "string" && typeof migration?.className === "string" && typeof migration?.appliedAt === "string" && (migration?.checksum === void 0 || typeof migration?.checksum === "string");
1854
- })
1877
+ }),
1878
+ runs: Array.isArray(parsed.runs) ? parsed.runs.filter((run) => {
1879
+ return typeof run?.id === "string" && typeof run?.appliedAt === "string" && Array.isArray(run?.migrationIds) && run.migrationIds.every((item) => typeof item === "string");
1880
+ }) : []
1855
1881
  };
1856
1882
  } catch {
1857
1883
  return { ...DEFAULT_STATE };
1858
1884
  }
1859
1885
  };
1860
1886
  const writeAppliedMigrationsState = (stateFilePath, state) => {
1861
- const directory = dirname$1(stateFilePath);
1862
- if (!existsSync$1(directory)) mkdirSync$1(directory, { recursive: true });
1863
- writeFileSync$1(stateFilePath, JSON.stringify(state, null, 2));
1887
+ const directory = dirname(stateFilePath);
1888
+ if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
1889
+ writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
1864
1890
  };
1865
1891
  const isMigrationApplied = (state, identity, checksum) => {
1866
1892
  const matched = state.migrations.find((migration) => migration.id === identity);
@@ -1877,9 +1903,40 @@ const markMigrationApplied = (state, entry) => {
1877
1903
  next.push(entry);
1878
1904
  return {
1879
1905
  version: 1,
1880
- migrations: next
1906
+ migrations: next,
1907
+ runs: state.runs ?? []
1881
1908
  };
1882
1909
  };
1910
+ const removeAppliedMigration = (state, identity) => {
1911
+ return {
1912
+ version: 1,
1913
+ migrations: state.migrations.filter((migration) => migration.id !== identity),
1914
+ runs: (state.runs ?? []).map((run) => ({
1915
+ ...run,
1916
+ migrationIds: run.migrationIds.filter((id) => id !== identity)
1917
+ })).filter((run) => run.migrationIds.length > 0)
1918
+ };
1919
+ };
1920
+ const buildMigrationRunId = () => {
1921
+ return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1922
+ };
1923
+ const markMigrationRun = (state, run) => {
1924
+ const nextRuns = (state.runs ?? []).filter((existing) => existing.id !== run.id);
1925
+ nextRuns.push(run);
1926
+ return {
1927
+ version: 1,
1928
+ migrations: state.migrations,
1929
+ runs: nextRuns
1930
+ };
1931
+ };
1932
+ const getLastMigrationRun = (state) => {
1933
+ const runs = state.runs ?? [];
1934
+ if (runs.length === 0) return void 0;
1935
+ return [...runs].sort((left, right) => right.appliedAt.localeCompare(left.appliedAt))[0];
1936
+ };
1937
+ const getLatestAppliedMigrations = (state, steps) => {
1938
+ return [...state.migrations].sort((left, right) => right.appliedAt.localeCompare(left.appliedAt)).slice(0, Math.max(0, steps));
1939
+ };
1883
1940
 
1884
1941
  //#endregion
1885
1942
  //#region src/database/Migration.ts
@@ -1928,15 +1985,14 @@ var MigrateCommand = class extends Command {
1928
1985
  */
1929
1986
  async handle() {
1930
1987
  this.app.command = this;
1931
- const configuredMigrationsDir = this.app.getConfig("paths")?.migrations ?? join$1(process.cwd(), "database", "migrations");
1988
+ const configuredMigrationsDir = this.app.getConfig("paths")?.migrations ?? join(process.cwd(), "database", "migrations");
1932
1989
  const migrationsDir = this.app.resolveRuntimeDirectoryPath(configuredMigrationsDir);
1933
- if (!existsSync$1(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
1934
- const schemaPath = this.option("schema") ? resolve(String(this.option("schema"))) : join$1(process.cwd(), "prisma", "schema.prisma");
1990
+ if (!existsSync(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
1991
+ const schemaPath = this.option("schema") ? resolve(String(this.option("schema"))) : join(process.cwd(), "prisma", "schema.prisma");
1935
1992
  const classes = this.option("all") || !this.argument("name") ? await this.loadAllMigrations(migrationsDir) : (await this.loadNamedMigration(migrationsDir, this.argument("name"))).filter(([cls]) => cls !== void 0);
1936
1993
  if (classes.length === 0) return void this.error("Error: No migration classes found to run.");
1937
- const shouldTrackApplied = Boolean(this.option("all") || !this.argument("name") || this.option("state-file"));
1938
1994
  const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
1939
- let appliedState = shouldTrackApplied ? readAppliedMigrationsState(stateFilePath) : void 0;
1995
+ let appliedState = readAppliedMigrationsState(stateFilePath);
1940
1996
  const skipped = [];
1941
1997
  const changed = [];
1942
1998
  const pending = classes.filter(([migrationClass, file]) => {
@@ -1963,6 +2019,7 @@ var MigrateCommand = class extends Command {
1963
2019
  write: true
1964
2020
  });
1965
2021
  if (appliedState) {
2022
+ const runAppliedIds = [];
1966
2023
  for (const [migrationClass, file] of pending) {
1967
2024
  const identity = buildMigrationIdentity(file, migrationClass.name);
1968
2025
  appliedState = markMigrationApplied(appliedState, {
@@ -1972,7 +2029,13 @@ var MigrateCommand = class extends Command {
1972
2029
  appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
1973
2030
  checksum: computeMigrationChecksum(file)
1974
2031
  });
2032
+ runAppliedIds.push(identity);
1975
2033
  }
2034
+ appliedState = markMigrationRun(appliedState, {
2035
+ id: buildMigrationRunId(),
2036
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
2037
+ migrationIds: runAppliedIds
2038
+ });
1976
2039
  writeAppliedMigrationsState(stateFilePath, appliedState);
1977
2040
  }
1978
2041
  if (!this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
@@ -1992,7 +2055,7 @@ var MigrateCommand = class extends Command {
1992
2055
  * @param migrationsDir The directory to load migration classes from.
1993
2056
  */
1994
2057
  async loadAllMigrations(migrationsDir) {
1995
- const files = readdirSync$1(migrationsDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).sort((left, right) => left.localeCompare(right)).map((file) => this.app.resolveRuntimeScriptPath(join$1(migrationsDir, file)));
2058
+ const files = readdirSync(migrationsDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).sort((left, right) => left.localeCompare(right)).map((file) => this.app.resolveRuntimeScriptPath(join(migrationsDir, file)));
1996
2059
  return (await Promise.all(files.map(async (file) => (await this.loadMigrationClassesFromFile(file)).map((cls) => [cls, file])))).flat();
1997
2060
  }
1998
2061
  /**
@@ -2014,7 +2077,7 @@ var MigrateCommand = class extends Command {
2014
2077
  `${base}Migration.js`,
2015
2078
  `${base}Migration.mjs`,
2016
2079
  `${base}Migration.cjs`
2017
- ].map((file) => join$1(migrationsDir, file)).find((file) => existsSync$1(file));
2080
+ ].map((file) => join(migrationsDir, file)).find((file) => existsSync(file));
2018
2081
  if (!target) return [[void 0, name]];
2019
2082
  const runtimeTarget = this.app.resolveRuntimeScriptPath(target);
2020
2083
  return (await this.loadMigrationClassesFromFile(runtimeTarget)).map((cls) => [cls, runtimeTarget]);
@@ -2036,6 +2099,91 @@ var MigrateCommand = class extends Command {
2036
2099
  }
2037
2100
  };
2038
2101
 
2102
+ //#endregion
2103
+ //#region src/cli/commands/MigrateRollbackCommand.ts
2104
+ /**
2105
+ * Rollback migration classes from the Prisma schema and run Prisma workflow.
2106
+ * By default, rolls back classes applied in the last migrate run.
2107
+ *
2108
+ * @author Legacy (3m1n3nc3)
2109
+ * @since 0.2.4
2110
+ */
2111
+ var MigrateRollbackCommand = class extends Command {
2112
+ signature = `migrate:rollback
2113
+ {--step= : Number of latest applied migration classes to rollback}
2114
+ {--dry-run : Preview rollback targets without applying changes}
2115
+ {--deploy : Use prisma migrate deploy instead of migrate dev}
2116
+ {--skip-generate : Skip prisma generate}
2117
+ {--skip-migrate : Skip prisma migrate command}
2118
+ {--state-file= : Path to applied migration state file}
2119
+ {--schema= : Explicit prisma schema path}
2120
+ {--migration-name= : Name for prisma migrate dev}
2121
+ `;
2122
+ description = "Rollback migration classes from schema.prisma and run Prisma workflow";
2123
+ async handle() {
2124
+ this.app.command = this;
2125
+ const configuredMigrationsDir = this.app.getConfig("paths")?.migrations ?? join(process.cwd(), "database", "migrations");
2126
+ const migrationsDir = this.app.resolveRuntimeDirectoryPath(configuredMigrationsDir);
2127
+ if (!existsSync(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
2128
+ const schemaPath = this.option("schema") ? resolve(String(this.option("schema"))) : join(process.cwd(), "prisma", "schema.prisma");
2129
+ const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
2130
+ let appliedState = readAppliedMigrationsState(stateFilePath);
2131
+ const stepOption = this.option("step");
2132
+ const stepCount = stepOption == null ? void 0 : Number(stepOption);
2133
+ if (stepCount != null && (!Number.isFinite(stepCount) || stepCount <= 0 || !Number.isInteger(stepCount))) return void this.error("Error: --step must be a positive integer.");
2134
+ const targets = stepCount ? getLatestAppliedMigrations(appliedState, stepCount) : (() => {
2135
+ const lastRun = getLastMigrationRun(appliedState);
2136
+ if (!lastRun) return [];
2137
+ return lastRun.migrationIds.map((id) => appliedState.migrations.find((migration) => migration.id === id)).filter((migration) => Boolean(migration));
2138
+ })();
2139
+ if (targets.length === 0) return void this.error("Error: No tracked migrations available to rollback.");
2140
+ const available = await this.loadAllMigrations(migrationsDir);
2141
+ const rollbackClasses = targets.map((target) => {
2142
+ return available.find(([migrationClass, file]) => {
2143
+ return buildMigrationIdentity(file, migrationClass.name) === target.id || migrationClass.name === target.className;
2144
+ });
2145
+ }).filter((entry) => Boolean(entry));
2146
+ if (rollbackClasses.length === 0) return void this.error("Error: Unable to resolve rollback migration classes from tracked history.");
2147
+ if (this.option("dry-run")) {
2148
+ this.success(`Dry run: ${rollbackClasses.length} migration(s) would be rolled back.`);
2149
+ rollbackClasses.forEach(([_, file]) => this.success(this.app.splitLogger("WouldRollback", file)));
2150
+ return;
2151
+ }
2152
+ for (const [MigrationClassItem] of rollbackClasses) await applyMigrationRollbackToPrismaSchema(MigrationClassItem, {
2153
+ schemaPath,
2154
+ write: true
2155
+ });
2156
+ for (const [migrationClass, file] of rollbackClasses) {
2157
+ const identity = buildMigrationIdentity(file, migrationClass.name);
2158
+ appliedState = removeAppliedMigration(appliedState, identity);
2159
+ }
2160
+ writeAppliedMigrationsState(stateFilePath, appliedState);
2161
+ if (!this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
2162
+ if (!this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
2163
+ else runPrismaCommand([
2164
+ "migrate",
2165
+ "dev",
2166
+ "--name",
2167
+ this.option("migration-name") ? String(this.option("migration-name")) : `arkorm_cli_rollback_${Date.now()}`
2168
+ ], process.cwd());
2169
+ this.success(`Rolled back ${rollbackClasses.length} migration(s).`);
2170
+ rollbackClasses.forEach(([_, file]) => this.success(this.app.splitLogger("RolledBack", file)));
2171
+ }
2172
+ async loadAllMigrations(migrationsDir) {
2173
+ const files = readdirSync(migrationsDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).sort((left, right) => left.localeCompare(right)).map((file) => this.app.resolveRuntimeScriptPath(join(migrationsDir, file)));
2174
+ return (await Promise.all(files.map(async (file) => (await this.loadMigrationClassesFromFile(file)).map((cls) => [cls, file])))).flat();
2175
+ }
2176
+ async loadMigrationClassesFromFile(filePath) {
2177
+ const imported = await import(`${pathToFileURL$1(resolve(filePath)).href}?arkorm_rollback=${Date.now()}`);
2178
+ return Object.values(imported).filter((value) => {
2179
+ if (typeof value !== "function") return false;
2180
+ const candidate = value;
2181
+ const prototype = candidate.prototype;
2182
+ return candidate[MIGRATION_BRAND] === true || typeof prototype?.up === "function" && typeof prototype?.down === "function";
2183
+ });
2184
+ }
2185
+ };
2186
+
2039
2187
  //#endregion
2040
2188
  //#region src/cli/commands/MigrationHistoryCommand.ts
2041
2189
  /**
@@ -2056,11 +2204,11 @@ var MigrationHistoryCommand = class extends Command {
2056
2204
  this.app.command = this;
2057
2205
  const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
2058
2206
  if (this.option("delete")) {
2059
- if (!existsSync$1(stateFilePath)) {
2207
+ if (!existsSync(stateFilePath)) {
2060
2208
  this.success(`No migration state file found at ${this.app.formatPathForLog(stateFilePath)}`);
2061
2209
  return;
2062
2210
  }
2063
- rmSync$1(stateFilePath);
2211
+ rmSync(stateFilePath);
2064
2212
  this.success(`Deleted migration state file: ${this.app.formatPathForLog(stateFilePath)}`);
2065
2213
  return;
2066
2214
  }
@@ -2187,9 +2335,9 @@ var SeedCommand = class extends Command {
2187
2335
  */
2188
2336
  async handle() {
2189
2337
  this.app.command = this;
2190
- const configuredSeedersDir = this.app.getConfig("paths")?.seeders ?? join$1(process.cwd(), "database", "seeders");
2338
+ const configuredSeedersDir = this.app.getConfig("paths")?.seeders ?? join(process.cwd(), "database", "seeders");
2191
2339
  const seedersDir = this.app.resolveRuntimeDirectoryPath(configuredSeedersDir);
2192
- if (!existsSync$1(seedersDir)) return void this.error(`ERROR: Seeders directory not found: ${this.app.formatPathForLog(configuredSeedersDir)}`);
2340
+ if (!existsSync(seedersDir)) return void this.error(`ERROR: Seeders directory not found: ${this.app.formatPathForLog(configuredSeedersDir)}`);
2193
2341
  const classes = this.option("all") ? await this.loadAllSeeders(seedersDir) : await this.loadNamedSeeder(seedersDir, this.argument("name") ?? "DatabaseSeeder");
2194
2342
  if (classes.length === 0) return void this.error("ERROR: No seeder classes found to run.");
2195
2343
  for (const SeederClassItem of classes) await new SeederClassItem().run();
@@ -2203,7 +2351,7 @@ var SeedCommand = class extends Command {
2203
2351
  * @returns
2204
2352
  */
2205
2353
  async loadAllSeeders(seedersDir) {
2206
- const files = readdirSync$1(seedersDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).map((file) => this.app.resolveRuntimeScriptPath(join$1(seedersDir, file)));
2354
+ const files = readdirSync(seedersDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).map((file) => this.app.resolveRuntimeScriptPath(join(seedersDir, file)));
2207
2355
  return (await Promise.all(files.map(async (file) => await this.loadSeederClassesFromFile(file)))).flat();
2208
2356
  }
2209
2357
  /**
@@ -2224,7 +2372,7 @@ var SeedCommand = class extends Command {
2224
2372
  `${base}Seeder.js`,
2225
2373
  `${base}Seeder.mjs`,
2226
2374
  `${base}Seeder.cjs`
2227
- ].map((file) => join$1(seedersDir, file)).find((file) => existsSync$1(file));
2375
+ ].map((file) => join(seedersDir, file)).find((file) => existsSync(file));
2228
2376
  if (!target) return [];
2229
2377
  const runtimeTarget = this.app.resolveRuntimeScriptPath(target);
2230
2378
  return await this.loadSeederClassesFromFile(runtimeTarget);
@@ -2272,6 +2420,7 @@ await Kernel.init(app, {
2272
2420
  ModelsSyncCommand,
2273
2421
  SeedCommand,
2274
2422
  MigrateCommand,
2423
+ MigrateRollbackCommand,
2275
2424
  MigrationHistoryCommand
2276
2425
  ],
2277
2426
  exceptionHandler(exception) {