@skalfa/skalfa-cli 1.0.2

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.
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const add_extension_1 = require("../commands/add-extension");
6
+ const create_api_1 = require("../commands/create-api");
7
+ const pick_1 = require("../commands/pick");
8
+ const program = new commander_1.Command();
9
+ program
10
+ .name("aluna")
11
+ .description("Create Aluna API projects and install optional extensions.")
12
+ .version("0.1.0");
13
+ program
14
+ .command("create-api")
15
+ .description("Create a new API project from the local aluna-api template.")
16
+ .argument("<name>", "project folder and package name")
17
+ .action((name) => {
18
+ runCommand(() => (0, create_api_1.createApi)(name));
19
+ });
20
+ program
21
+ .command("add")
22
+ .description("Install an optional Aluna extension into the current project.")
23
+ .argument("<extension>", `extension name: ${add_extension_1.extensionNames.join(", ")}`)
24
+ .action((extension) => {
25
+ runCommand(() => (0, add_extension_1.addExtension)(extension));
26
+ });
27
+ program
28
+ .command("pick")
29
+ .description("Eject/copy a core utility from @aluna/aluna-api-core into your local utils folder for customization.")
30
+ .argument("<utility>", `utility name: ${pick_1.UTILITIES.join(", ")}`)
31
+ .action((utility) => {
32
+ runCommand(() => (0, pick_1.pickUtility)(utility));
33
+ });
34
+ program.parse(process.argv);
35
+ function runCommand(command) {
36
+ try {
37
+ command();
38
+ }
39
+ catch (error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ console.error(`Error: ${message}`);
42
+ process.exitCode = 1;
43
+ }
44
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const add_extension_1 = require("../commands/add-extension");
12
+ const create_api_1 = require("../commands/create-api");
13
+ const pick_1 = require("../commands/pick");
14
+ const fs_1 = require("../utils/fs");
15
+ // Dynamic routing / forwarding logic
16
+ const args = process.argv.slice(2);
17
+ const knownCommands = ["create-api", "add", "pick"];
18
+ if (args.length > 0 && !knownCommands.includes(args[0]) && !["-h", "--help", "-v", "--version", "help"].includes(args[0])) {
19
+ const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
20
+ if (projectRoot) {
21
+ const localCliPath = node_path_1.default.join(projectRoot, "utils", "commands", "kava.ts");
22
+ if (node_fs_1.default.existsSync(localCliPath)) {
23
+ try {
24
+ // Forward arguments directly to local kava.ts using Bun
25
+ (0, node_child_process_1.execSync)(`bun run utils/commands/kava.ts ${args.join(" ")}`, { stdio: "inherit" });
26
+ process.exit(0);
27
+ }
28
+ catch (err) {
29
+ process.exit(1);
30
+ }
31
+ }
32
+ }
33
+ }
34
+ const program = new commander_1.Command();
35
+ program
36
+ .name("kava")
37
+ .description("Create Kava API projects and install optional extensions.")
38
+ .version("0.1.0");
39
+ program
40
+ .command("create-api")
41
+ .description("Create a new Kava API project.")
42
+ .argument("<name>", "project folder and package name")
43
+ .action(async (name) => {
44
+ await runCommand(() => (0, create_api_1.createApi)(name));
45
+ });
46
+ program
47
+ .command("add")
48
+ .description("Install an optional Kava extension into the current project.")
49
+ .argument("<extension>", `extension name: ${add_extension_1.extensionNames.join(", ")}`)
50
+ .action(async (extension) => {
51
+ await runCommand(() => (0, add_extension_1.addExtension)(extension));
52
+ });
53
+ program
54
+ .command("pick")
55
+ .description("Eject/copy a core utility from @kava/kava-api-core into your local utils folder for customization.")
56
+ .argument("<utility>", `utility name: ${pick_1.UTILITIES.join(", ")}`)
57
+ .action(async (utility) => {
58
+ await runCommand(() => (0, pick_1.pickUtility)(utility));
59
+ });
60
+ program.parse(process.argv);
61
+ async function runCommand(command) {
62
+ try {
63
+ await command();
64
+ }
65
+ catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ console.error(`Error: ${message}`);
68
+ process.exit(1);
69
+ }
70
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const add_extension_1 = require("../commands/add-extension");
12
+ const create_api_1 = require("../commands/create-api");
13
+ const pick_1 = require("../commands/pick");
14
+ const fs_1 = require("../utils/fs");
15
+ // Dynamic routing / forwarding logic
16
+ const args = process.argv.slice(2);
17
+ const knownCommands = ["create-api", "add", "pick"];
18
+ if (args.length > 0 && !knownCommands.includes(args[0]) && !["-h", "--help", "-v", "--version", "help"].includes(args[0])) {
19
+ const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
20
+ if (projectRoot) {
21
+ const localCliPath = node_path_1.default.join(projectRoot, "utils", "commands", "skalfa.ts");
22
+ if (node_fs_1.default.existsSync(localCliPath)) {
23
+ try {
24
+ // Forward arguments directly to local skalfa.ts using Bun
25
+ (0, node_child_process_1.execSync)(`bun run utils/commands/skalfa.ts ${args.join(" ")}`, { stdio: "inherit" });
26
+ process.exit(0);
27
+ }
28
+ catch (err) {
29
+ process.exit(1);
30
+ }
31
+ }
32
+ }
33
+ }
34
+ const program = new commander_1.Command();
35
+ program
36
+ .name("skalfa")
37
+ .description("Create Skalfa API projects and install optional extensions.")
38
+ .version("0.1.0");
39
+ program
40
+ .command("create-api")
41
+ .description("Create a new Skalfa API project.")
42
+ .argument("<name>", "project folder and package name")
43
+ .action(async (name) => {
44
+ await runCommand(() => (0, create_api_1.createApi)(name));
45
+ });
46
+ program
47
+ .command("add")
48
+ .description("Install an optional Skalfa extension into the current project.")
49
+ .argument("<extension>", `extension name: ${add_extension_1.extensionNames.join(", ")}`)
50
+ .action(async (extension) => {
51
+ await runCommand(() => (0, add_extension_1.addExtension)(extension));
52
+ });
53
+ program
54
+ .command("pick")
55
+ .description("Eject/copy a core utility from @skalfa/skalfa-api-core into your local utils folder for customization.")
56
+ .argument("<utility>", `utility name: ${pick_1.UTILITIES.join(", ")}`)
57
+ .action(async (utility) => {
58
+ await runCommand(() => (0, pick_1.pickUtility)(utility));
59
+ });
60
+ program.parse(process.argv);
61
+ async function runCommand(command) {
62
+ try {
63
+ await command();
64
+ }
65
+ catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ console.error(`Error: ${message}`);
68
+ process.exit(1);
69
+ }
70
+ }
@@ -0,0 +1,349 @@
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.extensionNames = exports.extensions = void 0;
7
+ exports.addExtension = addExtension;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_child_process_1 = require("node:child_process");
11
+ const npm_1 = require("../utils/npm");
12
+ const copier_1 = require("../utils/copier");
13
+ const fs_1 = require("../utils/fs");
14
+ const installer_1 = require("../utils/installer");
15
+ exports.extensions = {
16
+ mail: "@skalfa/mail",
17
+ redis: "@skalfa/skalfa-redis",
18
+ queue: "@skalfa/skalfa-queue",
19
+ cache: "@skalfa/skalfa-cache",
20
+ cron: "@skalfa/skalfa-cron",
21
+ da: "@skalfa/skalfa-da",
22
+ socket: "@skalfa/skalfa-socket",
23
+ notification: "@skalfa/notification",
24
+ orm: "@skalfa/skalfa-orm"
25
+ };
26
+ exports.extensionNames = Object.keys(exports.extensions);
27
+ async function addExtension(extensionName) {
28
+ const packageName = exports.extensions[extensionName];
29
+ if (!packageName) {
30
+ throw new Error(`Unknown extension "${extensionName}". Available extensions: ${exports.extensionNames.join(", ")}`);
31
+ }
32
+ const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
33
+ if (!projectRoot) {
34
+ throw new Error("No package.json found. Run this command inside a Skalfa API project.");
35
+ }
36
+ const isDev = !!process.env["SKALFA_API_TEMPLATE"];
37
+ if (extensionName === "orm") {
38
+ console.log("Installing Skalfa ORM and database dependencies...");
39
+ (0, installer_1.installPackage)(projectRoot, isDev ? "file:../skalfa-orm" : "@skalfa/skalfa-orm");
40
+ (0, installer_1.installPackage)(projectRoot, "knex");
41
+ (0, installer_1.installPackage)(projectRoot, "pg");
42
+ await scaffoldOrmExtension(projectRoot);
43
+ }
44
+ else if (["redis", "queue", "cache", "cron", "da", "socket"].includes(extensionName)) {
45
+ console.log(`Installing Skalfa extension: ${extensionName}...`);
46
+ // Install the main package
47
+ (0, installer_1.installPackage)(projectRoot, isDev ? `file:../${packageName.split("/")[1]}` : packageName);
48
+ // Install peer dependencies if any
49
+ if (extensionName === "redis") {
50
+ (0, installer_1.installPackage)(projectRoot, "ioredis");
51
+ }
52
+ else if (extensionName === "da") {
53
+ (0, installer_1.installPackage)(projectRoot, "@clickhouse/client");
54
+ }
55
+ else if (extensionName === "socket") {
56
+ (0, installer_1.installPackage)(projectRoot, "socket.io");
57
+ }
58
+ // Auto-install Redis if Queue or Cache is added
59
+ if (extensionName === "queue" || extensionName === "cache") {
60
+ console.log(`Extension ${extensionName} requires Redis. Automatically installing @skalfa/skalfa-redis...`);
61
+ (0, installer_1.installPackage)(projectRoot, isDev ? "file:../skalfa-redis" : "@skalfa/skalfa-redis");
62
+ (0, installer_1.installPackage)(projectRoot, "ioredis");
63
+ }
64
+ await scaffoldUtilityExtension(projectRoot, extensionName);
65
+ }
66
+ else {
67
+ console.log(`Installing Skalfa extension: ${extensionName}`);
68
+ (0, installer_1.installPackage)(projectRoot, packageName);
69
+ console.log(`Installed ${packageName}`);
70
+ }
71
+ }
72
+ async function getTemplateSource(projectRoot) {
73
+ const envTemplateSource = process.env["SKALFA_API_TEMPLATE"];
74
+ let tempExtractDir = null;
75
+ let templateSource = "";
76
+ if (envTemplateSource) {
77
+ templateSource = node_path_1.default.resolve(envTemplateSource);
78
+ if (!(0, fs_1.exists)(templateSource)) {
79
+ throw new Error(`Template source override not found: ${templateSource}`);
80
+ }
81
+ return { templateSource, cleanup: () => { } };
82
+ }
83
+ else {
84
+ // Dynamic download from npm registry
85
+ const templatePackageName = "@skalfa/skalfa-api";
86
+ console.log(`Fetching latest template info for ${templatePackageName} from npm registry...`);
87
+ const tarballUrl = await (0, npm_1.fetchLatestTarballUrl)(templatePackageName);
88
+ const parentDir = node_path_1.default.dirname(projectRoot);
89
+ tempExtractDir = node_path_1.default.join(parentDir, `skalfa-temp-extract-${Date.now()}`);
90
+ node_fs_1.default.mkdirSync(tempExtractDir, { recursive: true });
91
+ const tarballPath = node_path_1.default.join(tempExtractDir, "template.tgz");
92
+ console.log("Downloading template tarball...");
93
+ await (0, npm_1.downloadTarball)(tarballUrl, tarballPath);
94
+ console.log("Extracting template...");
95
+ try {
96
+ (0, node_child_process_1.execSync)(`tar -xzf "${tarballPath}" -C "${tempExtractDir}"`, { stdio: "ignore" });
97
+ }
98
+ catch (err) {
99
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
100
+ throw new Error(`Failed to extract template tarball. Please ensure 'tar' command is available: ${err.message}`);
101
+ }
102
+ templateSource = node_path_1.default.join(tempExtractDir, "package");
103
+ if (!(0, fs_1.exists)(templateSource)) {
104
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
105
+ throw new Error("Invalid template structure: 'package' folder not found inside tarball.");
106
+ }
107
+ return {
108
+ templateSource,
109
+ cleanup: () => {
110
+ if (tempExtractDir && (0, fs_1.exists)(tempExtractDir)) {
111
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
112
+ }
113
+ }
114
+ };
115
+ }
116
+ }
117
+ async function scaffoldOrmExtension(projectRoot) {
118
+ const { templateSource, cleanup } = await getTemplateSource(projectRoot);
119
+ try {
120
+ // 1. Copy database/ folder and app/models/ folder from template
121
+ console.log("Scaffolding database and model directories...");
122
+ const dbSrc = node_path_1.default.join(templateSource, "database");
123
+ const dbDest = node_path_1.default.join(projectRoot, "database");
124
+ if ((0, fs_1.exists)(dbSrc)) {
125
+ (0, copier_1.copyTemplate)(dbSrc, dbDest);
126
+ }
127
+ const modelsSrc = node_path_1.default.join(templateSource, "app", "models");
128
+ const modelsDest = node_path_1.default.join(projectRoot, "app", "models");
129
+ if ((0, fs_1.exists)(modelsSrc)) {
130
+ (0, copier_1.copyTemplate)(modelsSrc, modelsDest);
131
+ }
132
+ // 2. Copy database-enabled UserController and AuthController from template
133
+ console.log("Restoring database-enabled controllers...");
134
+ const authControllerSrc = node_path_1.default.join(templateSource, "app", "controllers", "iam", "auth.controller.ts");
135
+ const authControllerDest = node_path_1.default.join(projectRoot, "app", "controllers", "iam", "auth.controller.ts");
136
+ if ((0, fs_1.exists)(authControllerSrc)) {
137
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(authControllerDest), { recursive: true });
138
+ node_fs_1.default.copyFileSync(authControllerSrc, authControllerDest);
139
+ }
140
+ const userControllerSrc = node_path_1.default.join(templateSource, "app", "controllers", "iam", "user.controller.ts");
141
+ const userControllerDest = node_path_1.default.join(projectRoot, "app", "controllers", "iam", "user.controller.ts");
142
+ if ((0, fs_1.exists)(userControllerSrc)) {
143
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(userControllerDest), { recursive: true });
144
+ node_fs_1.default.copyFileSync(userControllerSrc, userControllerDest);
145
+ }
146
+ // 3. Restore database CLI commands loader if missing
147
+ console.log("Restoring database CLI commands...");
148
+ const commandsSrc = node_path_1.default.join(templateSource, "utils", "commands");
149
+ const commandsDest = node_path_1.default.join(projectRoot, "utils", "commands");
150
+ if ((0, fs_1.exists)(commandsSrc)) {
151
+ const skalfaCliSrc = node_path_1.default.join(commandsSrc, "skalfa.ts");
152
+ const skalfaCliDest = node_path_1.default.join(commandsDest, "skalfa.ts");
153
+ if ((0, fs_1.exists)(skalfaCliSrc) && !(0, fs_1.exists)(skalfaCliDest)) {
154
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(skalfaCliDest), { recursive: true });
155
+ node_fs_1.default.copyFileSync(skalfaCliSrc, skalfaCliDest);
156
+ }
157
+ }
158
+ // 4. Restore the database initialization block and imports in app/app.ts
159
+ console.log("Restoring database initialization in app/app.ts...");
160
+ const appTsSrc = node_path_1.default.join(templateSource, "app", "app.ts");
161
+ const appTsDest = node_path_1.default.join(projectRoot, "app", "app.ts");
162
+ if ((0, fs_1.exists)(appTsSrc) && (0, fs_1.exists)(appTsDest)) {
163
+ const templateAppTs = node_fs_1.default.readFileSync(appTsSrc, "utf8");
164
+ const regex = /(\/\/ ## Init: database\s*\r?\n\/\/ =====================================>\r?\n)([\s\S]*?)(\r?\n\/\/ =====================================>)/;
165
+ const match = templateAppTs.match(regex);
166
+ if (match) {
167
+ const dbBlock = match[2];
168
+ let targetAppTs = node_fs_1.default.readFileSync(appTsDest, "utf8");
169
+ targetAppTs = targetAppTs.replace(/(\/\/ ## Init: database\s*\r?\n\/\/ =====================================>\r?\n)([\s\S]*?)(\r?\n\/\/ =====================================>)/, `$1${dbBlock}$3`);
170
+ if (!targetAppTs.match(/\bdb\b/)) {
171
+ targetAppTs = targetAppTs.replace(/(\bcontroller\b|\blogger\b)/, "db, $1");
172
+ }
173
+ node_fs_1.default.writeFileSync(appTsDest, targetAppTs, "utf8");
174
+ }
175
+ }
176
+ // 5. Update tsconfig.json path mappings
177
+ console.log("Updating tsconfig.json paths...");
178
+ const tsconfigPath = node_path_1.default.join(projectRoot, "tsconfig.json");
179
+ if ((0, fs_1.exists)(tsconfigPath)) {
180
+ let content = node_fs_1.default.readFileSync(tsconfigPath, "utf8");
181
+ if (!content.includes("@skalfa/skalfa-orm")) {
182
+ content = content.replace(/("@utils\/\*"\s*:\s*\[\s*"utils\/\*",)/, `$1\n "node_modules/@skalfa/skalfa-orm/dist/*",`);
183
+ node_fs_1.default.writeFileSync(tsconfigPath, content, "utf8");
184
+ }
185
+ }
186
+ // 6. Update utils/index.ts exports
187
+ console.log("Updating utils/index.ts exports...");
188
+ const utilsIndexPath = node_path_1.default.join(projectRoot, "utils", "index.ts");
189
+ if ((0, fs_1.exists)(utilsIndexPath)) {
190
+ let content = node_fs_1.default.readFileSync(utilsIndexPath, "utf8");
191
+ if (!content.includes("@skalfa/skalfa-orm")) {
192
+ content = content.replace(/export \* from "@skalfa\/skalfa-api-core";/, `export * from "@skalfa/skalfa-api-core";\nexport * from "@skalfa/skalfa-orm";`);
193
+ content = content.replace(/export const db: any = null;\r?\n/, "");
194
+ content = content.replace(/export const Model: any = null;\r?\n/, "");
195
+ node_fs_1.default.writeFileSync(utilsIndexPath, content, "utf8");
196
+ }
197
+ }
198
+ }
199
+ finally {
200
+ cleanup();
201
+ }
202
+ console.log("✓ ORM initialization and scaffolding successfully restored!");
203
+ }
204
+ async function scaffoldUtilityExtension(projectRoot, ext) {
205
+ const { templateSource, cleanup } = await getTemplateSource(projectRoot);
206
+ try {
207
+ const tsconfigPath = node_path_1.default.join(projectRoot, "tsconfig.json");
208
+ const utilsIndexPath = node_path_1.default.join(projectRoot, "utils", "index.ts");
209
+ const appTsPath = node_path_1.default.join(projectRoot, "app", "app.ts");
210
+ // 1. Copy relevant template folders
211
+ if (ext === "queue") {
212
+ console.log("Copying Queue worker examples...");
213
+ const src = node_path_1.default.join(templateSource, "app", "jobs", "queues");
214
+ const dest = node_path_1.default.join(projectRoot, "app", "jobs", "queues");
215
+ (0, copier_1.copyTemplate)(src, dest);
216
+ // Check if project has DA or Notification packages
217
+ const packageJsonPath = node_path_1.default.join(projectRoot, "package.json");
218
+ let hasDa = false;
219
+ let hasNotification = false;
220
+ if (node_fs_1.default.existsSync(packageJsonPath)) {
221
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
222
+ const deps = pkg.dependencies || {};
223
+ hasDa = !!deps["@skalfa/skalfa-da"];
224
+ hasNotification = !!deps["@skalfa/notification"] || !!deps["skalfa-notification"];
225
+ }
226
+ cleanQueueWorkers(projectRoot, hasDa, hasNotification);
227
+ }
228
+ else if (ext === "cron") {
229
+ console.log("Copying Cron job examples...");
230
+ const src = node_path_1.default.join(templateSource, "app", "jobs", "crons");
231
+ const dest = node_path_1.default.join(projectRoot, "app", "jobs", "crons");
232
+ (0, copier_1.copyTemplate)(src, dest);
233
+ }
234
+ else if (ext === "socket") {
235
+ console.log("Copying Socket.io handler examples...");
236
+ const src = node_path_1.default.join(templateSource, "app", "jobs", "sockets");
237
+ const dest = node_path_1.default.join(projectRoot, "app", "jobs", "sockets");
238
+ (0, copier_1.copyTemplate)(src, dest);
239
+ }
240
+ else if (ext === "da") {
241
+ console.log("Copying Data Analytics OLAP migrations...");
242
+ const src = node_path_1.default.join(templateSource, "database", "da.migrations");
243
+ const dest = node_path_1.default.join(projectRoot, "database", "da.migrations");
244
+ (0, copier_1.copyTemplate)(src, dest);
245
+ }
246
+ // 2. Update tsconfig.json path mappings
247
+ addTsconfigPath(tsconfigPath, `@skalfa/skalfa-${ext}`);
248
+ if (ext === "queue" || ext === "cache") {
249
+ addTsconfigPath(tsconfigPath, "@skalfa/skalfa-redis");
250
+ }
251
+ // 3. Update utils/index.ts exports
252
+ addUtilExport(utilsIndexPath, `@skalfa/skalfa-${ext}`);
253
+ if (ext === "queue" || ext === "cache") {
254
+ addUtilExport(utilsIndexPath, "@skalfa/skalfa-redis");
255
+ }
256
+ // 4. Uncomment initialization blocks and update imports in app/app.ts
257
+ if (node_fs_1.default.existsSync(appTsPath)) {
258
+ let content = node_fs_1.default.readFileSync(appTsPath, "utf8");
259
+ const importsToAdd = [];
260
+ if (ext === "redis" || ext === "queue" || ext === "cache") {
261
+ importsToAdd.push("redis");
262
+ // Uncomment Redis block
263
+ content = content.replace(/\/\/ if \(process\.env\.REDIS_HOST[\s\S]*?\/\/ \}/g, (match) => match.replace(/^\/\/ ?/gm, ""));
264
+ }
265
+ if (ext === "da") {
266
+ importsToAdd.push("daClient");
267
+ // Uncomment DA block
268
+ content = content.replace(/\/\/ if \(process\.env\.DA_HOST[\s\S]*?\/\/ }/g, (match) => match.replace(/^\/\/ ?/gm, ""));
269
+ }
270
+ if (ext === "cron") {
271
+ importsToAdd.push("cron");
272
+ // Uncomment Cron block
273
+ content = content.replace(/\/\/ cron\.worker\(\)/g, "cron.worker()");
274
+ }
275
+ if (ext === "socket") {
276
+ importsToAdd.push("socket");
277
+ // Uncomment Socket block
278
+ content = content.replace(/\/\/ if \(process\.env\.SOCKET_PORT[\s\S]*?\/\/ }/g, (match) => match.replace(/^\/\/ ?/gm, ""));
279
+ }
280
+ // Update import statement at the top of app.ts
281
+ if (importsToAdd.length > 0) {
282
+ const importRegex = /import\s*\{\s*([\s\S]*?)\s*\}\s*from\s*["']@utils["']/;
283
+ const match = content.match(importRegex);
284
+ if (match) {
285
+ const currentImports = match[1].split(",").map(i => i.trim()).filter(Boolean);
286
+ const finalImports = Array.from(new Set([...currentImports, ...importsToAdd]));
287
+ const newImportLine = `import { ${finalImports.join(", ")} } from "@utils"`;
288
+ content = content.replace(importRegex, newImportLine);
289
+ }
290
+ }
291
+ node_fs_1.default.writeFileSync(appTsPath, content, "utf8");
292
+ }
293
+ }
294
+ finally {
295
+ cleanup();
296
+ }
297
+ console.log(`✓ Extension "${ext}" successfully configured!`);
298
+ }
299
+ function addTsconfigPath(tsconfigPath, packageName) {
300
+ if (!node_fs_1.default.existsSync(tsconfigPath))
301
+ return;
302
+ let content = node_fs_1.default.readFileSync(tsconfigPath, "utf8");
303
+ if (!content.includes(packageName)) {
304
+ content = content.replace(/("@utils\/\*"\s*:\s*\[\s*"utils\/\*",)/, `$1\n "node_modules/${packageName}/dist/*",`);
305
+ node_fs_1.default.writeFileSync(tsconfigPath, content, "utf8");
306
+ }
307
+ }
308
+ function addUtilExport(utilsIndexPath, packageName) {
309
+ if (!node_fs_1.default.existsSync(utilsIndexPath))
310
+ return;
311
+ let content = node_fs_1.default.readFileSync(utilsIndexPath, "utf8");
312
+ if (!content.includes(packageName)) {
313
+ content += `export * from "${packageName}";\n`;
314
+ node_fs_1.default.writeFileSync(utilsIndexPath, content, "utf8");
315
+ }
316
+ }
317
+ function cleanQueueWorkers(targetDir, hasDa, hasNotification) {
318
+ const queuesDir = node_path_1.default.join(targetDir, "app", "jobs", "queues");
319
+ const workerQueuePath = node_path_1.default.join(queuesDir, "worker.queue.ts");
320
+ if (!node_fs_1.default.existsSync(workerQueuePath))
321
+ return;
322
+ let content = node_fs_1.default.readFileSync(workerQueuePath, "utf8");
323
+ if (!hasDa) {
324
+ const daFiles = [
325
+ "access-log.queue.worker.ts",
326
+ "activity-log.queue.worker.ts",
327
+ "error-log.queue.worker.ts"
328
+ ];
329
+ for (const file of daFiles) {
330
+ const p = node_path_1.default.join(queuesDir, file);
331
+ if (node_fs_1.default.existsSync(p))
332
+ node_fs_1.default.unlinkSync(p);
333
+ }
334
+ content = content.replace(/import\s*\{\s*activityLogQueueWorker\s*\}\s*from\s*["']\.\/activity-log\.queue\.worker["'];?\r?\n?/g, "");
335
+ content = content.replace(/import\s*\{\s*accessLogQueueWorker\s*\}\s*from\s*["']\.\/access-log\.queue\.worker["'];?\r?\n?/g, "");
336
+ content = content.replace(/import\s*\{\s*errorLogQueueWorker\s*\}\s*from\s*["']\.\/error-log\.queue\.worker["'];?\r?\n?/g, "");
337
+ content = content.replace(/activityLogQueueWorker\(\)\r?\n?/g, "");
338
+ content = content.replace(/accessLogQueueWorker\(\)\r?\n?/g, "");
339
+ content = content.replace(/errorLogQueueWorker\(\)\r?\n?/g, "");
340
+ }
341
+ if (!hasNotification) {
342
+ const p = node_path_1.default.join(queuesDir, "notification.queue.worker.ts");
343
+ if (node_fs_1.default.existsSync(p))
344
+ node_fs_1.default.unlinkSync(p);
345
+ content = content.replace(/import\s*\{\s*notificationQueueWorker\s*\}\s*from\s*["']\.\/notification\.queue\.worker["'];?\r?\n?/g, "");
346
+ content = content.replace(/notificationQueueWorker\(\)\r?\n?/g, "");
347
+ }
348
+ node_fs_1.default.writeFileSync(workerQueuePath, content, "utf8");
349
+ }
@@ -0,0 +1,317 @@
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.createApi = createApi;
7
+ exports.renamePackage = renamePackage;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_readline_1 = __importDefault(require("node:readline"));
12
+ const npm_1 = require("../utils/npm");
13
+ const installer_1 = require("../utils/installer");
14
+ const fs_1 = require("../utils/fs");
15
+ const copier_1 = require("../utils/copier");
16
+ const TEMPLATE_ENV_KEY = "SKALFA_API_TEMPLATE";
17
+ class Questioner {
18
+ rl;
19
+ constructor() {
20
+ this.rl = node_readline_1.default.createInterface({
21
+ input: process.stdin,
22
+ output: process.stdout,
23
+ });
24
+ }
25
+ ask(query) {
26
+ return new Promise((resolve) => {
27
+ this.rl.question(query, (answer) => {
28
+ resolve(answer);
29
+ });
30
+ });
31
+ }
32
+ close() {
33
+ this.rl.close();
34
+ }
35
+ }
36
+ async function createApi(projectName) {
37
+ const cwd = process.cwd();
38
+ const target = node_path_1.default.resolve(cwd, projectName);
39
+ const packageName = node_path_1.default.basename(target);
40
+ (0, fs_1.assertInsideDirectory)(cwd, target);
41
+ if ((0, fs_1.exists)(target)) {
42
+ throw new Error(`Target directory already exists: ${target}`);
43
+ }
44
+ // Ask interactive questions sequentially using a single readline interface
45
+ const q = new Questioner();
46
+ let hasRedis = false;
47
+ let hasQueue = false;
48
+ let hasCache = false;
49
+ let hasCron = false;
50
+ let hasDa = false;
51
+ let hasSocket = false;
52
+ try {
53
+ hasRedis = (await q.ask("Do you need Redis? (y/N): ")).toLowerCase().startsWith("y");
54
+ hasQueue = (await q.ask("Do you need Queue? (y/N): ")).toLowerCase().startsWith("y");
55
+ hasCache = (await q.ask("Do you need Cache? (y/N): ")).toLowerCase().startsWith("y");
56
+ hasCron = (await q.ask("Do you need Cron? (y/N): ")).toLowerCase().startsWith("y");
57
+ hasDa = (await q.ask("Do you need Clickhouse Data Analytics? (y/N): ")).toLowerCase().startsWith("y");
58
+ hasSocket = (await q.ask("Do you need Socket.io? (y/N): ")).toLowerCase().startsWith("y");
59
+ }
60
+ finally {
61
+ q.close();
62
+ }
63
+ // Dependency relation: Queue or Cache requires Redis
64
+ const finalRedis = hasRedis || hasQueue || hasCache;
65
+ const envTemplateSource = process.env[TEMPLATE_ENV_KEY];
66
+ if (envTemplateSource) {
67
+ // Local copy mode (e.g. for development / override)
68
+ const templateSource = node_path_1.default.resolve(envTemplateSource);
69
+ console.log(`Creating Skalfa API project from local template override: ${templateSource}`);
70
+ if (!(0, fs_1.exists)(templateSource)) {
71
+ throw new Error(`Template source override not found: ${templateSource}`);
72
+ }
73
+ (0, copier_1.copyTemplate)(templateSource, target);
74
+ }
75
+ else {
76
+ // Dynamic download from npm registry
77
+ const templatePackageName = "@skalfa/skalfa-api";
78
+ console.log(`Fetching latest template info for ${templatePackageName} from npm registry...`);
79
+ const tarballUrl = await (0, npm_1.fetchLatestTarballUrl)(templatePackageName);
80
+ // Create a temporary extraction directory inside parent folder of target
81
+ const parentDir = node_path_1.default.dirname(target);
82
+ const tempExtractDir = node_path_1.default.join(parentDir, `${projectName}-temp-extract`);
83
+ if ((0, fs_1.exists)(tempExtractDir)) {
84
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
85
+ }
86
+ node_fs_1.default.mkdirSync(tempExtractDir, { recursive: true });
87
+ const tarballPath = node_path_1.default.join(tempExtractDir, "template.tgz");
88
+ console.log("Downloading template tarball...");
89
+ await (0, npm_1.downloadTarball)(tarballUrl, tarballPath);
90
+ console.log("Extracting template...");
91
+ try {
92
+ (0, node_child_process_1.execSync)(`tar -xzf "${tarballPath}" -C "${tempExtractDir}"`, { stdio: "ignore" });
93
+ }
94
+ catch (err) {
95
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
96
+ throw new Error(`Failed to extract template tarball. Please ensure 'tar' command is available: ${err.message}`);
97
+ }
98
+ const packageDir = node_path_1.default.join(tempExtractDir, "package");
99
+ if (!(0, fs_1.exists)(packageDir)) {
100
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
101
+ throw new Error("Invalid template structure: 'package' folder not found inside tarball.");
102
+ }
103
+ // Rename/move extracted folder to target path
104
+ node_fs_1.default.renameSync(packageDir, target);
105
+ // Cleanup temp extract folder
106
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
107
+ }
108
+ // Cleanup git directory if present and rename package
109
+ (0, fs_1.removeDirectory)(node_path_1.default.join(target, ".git"));
110
+ renamePackage(target, packageName);
111
+ // Customize project with selected options
112
+ customizeProject(target, {
113
+ redis: finalRedis,
114
+ queue: hasQueue,
115
+ cache: hasCache,
116
+ cron: hasCron,
117
+ da: hasDa,
118
+ socket: hasSocket
119
+ });
120
+ console.log("Installing dependencies...");
121
+ (0, installer_1.installDependencies)(target);
122
+ console.log("");
123
+ console.log("✓ Skalfa API project is ready.");
124
+ console.log(`Next steps:\n cd ${projectName}\n bun run dev`);
125
+ }
126
+ function renamePackage(target, packageName) {
127
+ const packageJsonPath = node_path_1.default.join(target, "package.json");
128
+ if (!(0, fs_1.exists)(packageJsonPath)) {
129
+ console.warn("Skipped package rename: package.json was not found.");
130
+ return;
131
+ }
132
+ const packageJson = (0, fs_1.readJsonFile)(packageJsonPath);
133
+ packageJson.name = packageName;
134
+ (0, fs_1.writeJsonFile)(packageJsonPath, packageJson);
135
+ }
136
+ function customizeProject(target, opts) {
137
+ const packageJsonPath = node_path_1.default.join(target, "package.json");
138
+ const tsconfigPath = node_path_1.default.join(target, "tsconfig.json");
139
+ const utilsIndexPath = node_path_1.default.join(target, "utils", "index.ts");
140
+ const appTsPath = node_path_1.default.join(target, "app", "app.ts");
141
+ const isDev = !!process.env[TEMPLATE_ENV_KEY];
142
+ // 1. Update dependencies in package.json
143
+ if (node_fs_1.default.existsSync(packageJsonPath)) {
144
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
145
+ pkg.dependencies = pkg.dependencies || {};
146
+ // Base ORM integration (always included)
147
+ pkg.dependencies["@skalfa/skalfa-orm"] = isDev ? "file:../skalfa-orm" : "^1.0.0";
148
+ if (isDev) {
149
+ pkg.dependencies["@skalfa/skalfa-api-core"] = "file:../skalfa-api-core";
150
+ }
151
+ if (opts.redis) {
152
+ pkg.dependencies["@skalfa/skalfa-redis"] = isDev ? "file:../skalfa-redis" : "^1.0.0";
153
+ pkg.dependencies["ioredis"] = "^5.4.1";
154
+ }
155
+ if (opts.queue) {
156
+ pkg.dependencies["@skalfa/skalfa-queue"] = isDev ? "file:../skalfa-queue" : "^1.0.0";
157
+ }
158
+ if (opts.cache) {
159
+ pkg.dependencies["@skalfa/skalfa-cache"] = isDev ? "file:../skalfa-cache" : "^1.0.0";
160
+ }
161
+ if (opts.cron) {
162
+ pkg.dependencies["@skalfa/skalfa-cron"] = isDev ? "file:../skalfa-cron" : "^1.0.0";
163
+ }
164
+ if (opts.da) {
165
+ pkg.dependencies["@skalfa/skalfa-da"] = isDev ? "file:../skalfa-da" : "^1.0.0";
166
+ pkg.dependencies["@clickhouse/client"] = "^1.6.0";
167
+ }
168
+ if (opts.socket) {
169
+ pkg.dependencies["@skalfa/skalfa-socket"] = isDev ? "file:../skalfa-socket" : "^1.0.0";
170
+ pkg.dependencies["socket.io"] = "^4.7.5";
171
+ }
172
+ node_fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), "utf8");
173
+ }
174
+ // 2. Add path mappings in tsconfig.json
175
+ addTsconfigPath(tsconfigPath, "@skalfa/skalfa-orm");
176
+ if (opts.redis)
177
+ addTsconfigPath(tsconfigPath, "@skalfa/skalfa-redis");
178
+ if (opts.queue)
179
+ addTsconfigPath(tsconfigPath, "@skalfa/skalfa-queue");
180
+ if (opts.cache)
181
+ addTsconfigPath(tsconfigPath, "@skalfa/skalfa-cache");
182
+ if (opts.cron)
183
+ addTsconfigPath(tsconfigPath, "@skalfa/skalfa-cron");
184
+ if (opts.da)
185
+ addTsconfigPath(tsconfigPath, "@skalfa/skalfa-da");
186
+ if (opts.socket)
187
+ addTsconfigPath(tsconfigPath, "@skalfa/skalfa-socket");
188
+ // 3. Write exports to utils/index.ts
189
+ if (node_fs_1.default.existsSync(utilsIndexPath)) {
190
+ let exports = `export * from "@skalfa/skalfa-api-core";\nexport * from "@skalfa/skalfa-orm";\n`;
191
+ if (opts.redis)
192
+ exports += `export * from "@skalfa/skalfa-redis";\n`;
193
+ if (opts.queue)
194
+ exports += `export * from "@skalfa/skalfa-queue";\n`;
195
+ if (opts.cache)
196
+ exports += `export * from "@skalfa/skalfa-cache";\n`;
197
+ if (opts.cron)
198
+ exports += `export * from "@skalfa/skalfa-cron";\n`;
199
+ if (opts.da)
200
+ exports += `export * from "@skalfa/skalfa-da";\n`;
201
+ if (opts.socket)
202
+ exports += `export * from "@skalfa/skalfa-socket";\n`;
203
+ node_fs_1.default.writeFileSync(utilsIndexPath, exports, "utf8");
204
+ }
205
+ // 4. Clean up folders/files that are not wanted
206
+ if (!opts.queue) {
207
+ const queueDir = node_path_1.default.join(target, "app", "jobs", "queues");
208
+ if (node_fs_1.default.existsSync(queueDir)) {
209
+ node_fs_1.default.rmSync(queueDir, { recursive: true, force: true });
210
+ }
211
+ }
212
+ else {
213
+ cleanQueueWorkers(target, opts.da, false);
214
+ }
215
+ if (!opts.cron) {
216
+ const cronDir = node_path_1.default.join(target, "app", "jobs", "crons");
217
+ if (node_fs_1.default.existsSync(cronDir)) {
218
+ node_fs_1.default.rmSync(cronDir, { recursive: true, force: true });
219
+ }
220
+ }
221
+ if (!opts.socket) {
222
+ const socketDir = node_path_1.default.join(target, "app", "jobs", "sockets");
223
+ if (node_fs_1.default.existsSync(socketDir)) {
224
+ node_fs_1.default.rmSync(socketDir, { recursive: true, force: true });
225
+ }
226
+ }
227
+ if (!opts.da) {
228
+ const daDir = node_path_1.default.join(target, "database", "da.migrations");
229
+ if (node_fs_1.default.existsSync(daDir)) {
230
+ node_fs_1.default.rmSync(daDir, { recursive: true, force: true });
231
+ }
232
+ }
233
+ // Clean up parent jobs folder if completely empty
234
+ const jobsDir = node_path_1.default.join(target, "app", "jobs");
235
+ if (node_fs_1.default.existsSync(jobsDir)) {
236
+ const files = node_fs_1.default.readdirSync(jobsDir);
237
+ if (files.length === 0) {
238
+ node_fs_1.default.rmSync(jobsDir, { recursive: true, force: true });
239
+ }
240
+ }
241
+ // 5. Uncomment initialization blocks and update imports in app/app.ts
242
+ if (node_fs_1.default.existsSync(appTsPath)) {
243
+ let content = node_fs_1.default.readFileSync(appTsPath, "utf8");
244
+ const importsToAdd = [];
245
+ if (opts.redis) {
246
+ importsToAdd.push("redis");
247
+ // Uncomment Redis block
248
+ content = content.replace(/\/\/ if \(process\.env\.REDIS_HOST[\s\S]*?\/\/ \}/g, (match) => match.replace(/^\/\/ ?/gm, ""));
249
+ }
250
+ if (opts.da) {
251
+ importsToAdd.push("daClient");
252
+ // Uncomment DA block
253
+ content = content.replace(/\/\/ if \(process\.env\.DA_HOST[\s\S]*?\/\/ }/g, (match) => match.replace(/^\/\/ ?/gm, ""));
254
+ }
255
+ if (opts.cron) {
256
+ importsToAdd.push("cron");
257
+ // Uncomment Cron block
258
+ content = content.replace(/\/\/ cron\.worker\(\)/g, "cron.worker()");
259
+ }
260
+ if (opts.socket) {
261
+ importsToAdd.push("socket");
262
+ // Uncomment Socket block
263
+ content = content.replace(/\/\/ if \(process\.env\.SOCKET_PORT[\s\S]*?\/\/ }/g, (match) => match.replace(/^\/\/ ?/gm, ""));
264
+ }
265
+ // Update import statement at the top of app.ts
266
+ if (importsToAdd.length > 0) {
267
+ const baseImports = ["controller", "db", "logger", "middleware", "storage", "registry"];
268
+ const finalImports = [...baseImports, ...importsToAdd];
269
+ const targetRegex = /import\s*\{\s*controller,\s*db,\s*logger,\s*middleware,\s*storage,\s*registry\s*\}\s*from\s*["']@utils["']/;
270
+ const newImportLine = `import { ${finalImports.join(", ")} } from "@utils"`;
271
+ content = content.replace(targetRegex, newImportLine);
272
+ }
273
+ node_fs_1.default.writeFileSync(appTsPath, content, "utf8");
274
+ }
275
+ }
276
+ function addTsconfigPath(tsconfigPath, packageName) {
277
+ if (!node_fs_1.default.existsSync(tsconfigPath))
278
+ return;
279
+ let content = node_fs_1.default.readFileSync(tsconfigPath, "utf8");
280
+ if (!content.includes(packageName)) {
281
+ content = content.replace(/("@utils\/\*"\s*:\s*\[\s*"utils\/\*",)/, `$1\n "node_modules/${packageName}/dist/*",`);
282
+ node_fs_1.default.writeFileSync(tsconfigPath, content, "utf8");
283
+ }
284
+ }
285
+ function cleanQueueWorkers(targetDir, hasDa, hasNotification) {
286
+ const queuesDir = node_path_1.default.join(targetDir, "app", "jobs", "queues");
287
+ const workerQueuePath = node_path_1.default.join(queuesDir, "worker.queue.ts");
288
+ if (!node_fs_1.default.existsSync(workerQueuePath))
289
+ return;
290
+ let content = node_fs_1.default.readFileSync(workerQueuePath, "utf8");
291
+ if (!hasDa) {
292
+ const daFiles = [
293
+ "access-log.queue.worker.ts",
294
+ "activity-log.queue.worker.ts",
295
+ "error-log.queue.worker.ts"
296
+ ];
297
+ for (const file of daFiles) {
298
+ const p = node_path_1.default.join(queuesDir, file);
299
+ if (node_fs_1.default.existsSync(p))
300
+ node_fs_1.default.unlinkSync(p);
301
+ }
302
+ content = content.replace(/import\s*\{\s*activityLogQueueWorker\s*\}\s*from\s*["']\.\/activity-log\.queue\.worker["'];?\r?\n?/g, "");
303
+ content = content.replace(/import\s*\{\s*accessLogQueueWorker\s*\}\s*from\s*["']\.\/access-log\.queue\.worker["'];?\r?\n?/g, "");
304
+ content = content.replace(/import\s*\{\s*errorLogQueueWorker\s*\}\s*from\s*["']\.\/error-log\.queue\.worker["'];?\r?\n?/g, "");
305
+ content = content.replace(/activityLogQueueWorker\(\)\r?\n?/g, "");
306
+ content = content.replace(/accessLogQueueWorker\(\)\r?\n?/g, "");
307
+ content = content.replace(/errorLogQueueWorker\(\)\r?\n?/g, "");
308
+ }
309
+ if (!hasNotification) {
310
+ const p = node_path_1.default.join(queuesDir, "notification.queue.worker.ts");
311
+ if (node_fs_1.default.existsSync(p))
312
+ node_fs_1.default.unlinkSync(p);
313
+ content = content.replace(/import\s*\{\s*notificationQueueWorker\s*\}\s*from\s*["']\.\/notification\.queue\.worker["'];?\r?\n?/g, "");
314
+ content = content.replace(/notificationQueueWorker\(\)\r?\n?/g, "");
315
+ }
316
+ node_fs_1.default.writeFileSync(workerQueuePath, content, "utf8");
317
+ }
@@ -0,0 +1,68 @@
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.UTILITIES = exports.UTILITY_EXPORTS = void 0;
7
+ exports.pickUtility = pickUtility;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const fs_1 = require("../utils/fs");
11
+ exports.UTILITY_EXPORTS = {
12
+ auth: ["auth"],
13
+ context: ["context", "AppContext"],
14
+ controller: ["controller", "ControllerContext", "ControllerSchema", "controllerSchema"],
15
+ conversion: ["conversion"],
16
+ db: ["db", "useDB", "closeAllDB"],
17
+ logger: ["logger"],
18
+ mail: ["sendMail", "renderMailTemplate", "SendMailOptions"],
19
+ middleware: ["middleware"],
20
+ model: ["Model", "softDelete", "foreignIdFor"],
21
+ permission: ["permission", "KeyPermission"],
22
+ route: ["route", "api"],
23
+ storage: ["storage"],
24
+ validation: ["validate", "ValidationRules", "ValidationRule"],
25
+ };
26
+ exports.UTILITIES = Object.keys(exports.UTILITY_EXPORTS);
27
+ function pickUtility(utilityName) {
28
+ const utilitySymbols = exports.UTILITY_EXPORTS[utilityName];
29
+ if (!utilitySymbols) {
30
+ throw new Error(`Unknown utility "${utilityName}". Available utilities: ${exports.UTILITIES.join(", ")}`);
31
+ }
32
+ const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
33
+ if (!projectRoot) {
34
+ throw new Error("No package.json found. Run this command inside a Skalfa API project.");
35
+ }
36
+ const utilsDir = node_path_1.default.join(projectRoot, "utils");
37
+ const indexPath = node_path_1.default.join(utilsDir, "index.ts");
38
+ if (!(0, fs_1.exists)(utilsDir) || !(0, fs_1.exists)(indexPath)) {
39
+ throw new Error("Folder utils or utils/index.ts not found. Make sure you are at the project root.");
40
+ }
41
+ // 1. Tentukan path source dari node_modules dan target di lokal proyek
42
+ const corePackagePath = node_path_1.default.join(projectRoot, "node_modules", "@skalfa", "skalfa-api-core");
43
+ const sourceDir = node_path_1.default.join(corePackagePath, "src", utilityName);
44
+ const targetDir = node_path_1.default.join(utilsDir, utilityName);
45
+ if (!(0, fs_1.exists)(sourceDir)) {
46
+ throw new Error(`Source folder for "${utilityName}" not found at ${sourceDir}. Make sure @skalfa/skalfa-api-core is installed.`);
47
+ }
48
+ if ((0, fs_1.exists)(targetDir)) {
49
+ throw new Error(`Utility folder "${utilityName}" is already present in your local utils folder.`);
50
+ }
51
+ // 2. Salin folder dari node_modules ke lokal proyek secara rekursif
52
+ console.log(`Copying ${utilityName} folder from @skalfa/skalfa-api-core to utils/ ...`);
53
+ node_fs_1.default.cpSync(sourceDir, targetDir, { recursive: true });
54
+ console.log(`✓ Copied ${utilityName} folder`);
55
+ // 3. Perbarui utils/index.ts untuk mereferensikan folder lokal
56
+ console.log("Updating utils/index.ts with explicit local export override ...");
57
+ let indexContent = node_fs_1.default.readFileSync(indexPath, "utf8").trim();
58
+ const localExportLine = `export { ${utilitySymbols.join(", ")} } from "./${utilityName}";`;
59
+ if (!indexContent.includes(`./${utilityName}`)) {
60
+ indexContent += `\n${localExportLine}\n`;
61
+ node_fs_1.default.writeFileSync(indexPath, indexContent, "utf8");
62
+ console.log("✓ Updated utils/index.ts with override.");
63
+ }
64
+ else {
65
+ console.log("⚠️ Info: Local export for this utility already exists in utils/index.ts");
66
+ }
67
+ console.log(`\nSuccess! You can now customize files under: utils/${utilityName}/`);
68
+ }
@@ -0,0 +1,41 @@
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.ignoredNames = void 0;
7
+ exports.copyTemplate = copyTemplate;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const fs_1 = require("./fs");
11
+ exports.ignoredNames = new Set(["node_modules", ".git"]);
12
+ function copyTemplate(source, target) {
13
+ copyRecursive(source, target);
14
+ }
15
+ function copyRecursive(source, target) {
16
+ const stat = node_fs_1.default.lstatSync(source);
17
+ if (stat.isDirectory()) {
18
+ copyDirectory(source, target);
19
+ return;
20
+ }
21
+ if (stat.isSymbolicLink()) {
22
+ copySymbolicLink(source, target);
23
+ return;
24
+ }
25
+ node_fs_1.default.copyFileSync(source, target);
26
+ }
27
+ function copyDirectory(source, target) {
28
+ (0, fs_1.ensureDirectoryExists)(target);
29
+ for (const entry of node_fs_1.default.readdirSync(source, { withFileTypes: true })) {
30
+ if (exports.ignoredNames.has(entry.name)) {
31
+ continue;
32
+ }
33
+ const sourcePath = node_path_1.default.join(source, entry.name);
34
+ const targetPath = node_path_1.default.join(target, entry.name);
35
+ copyRecursive(sourcePath, targetPath);
36
+ }
37
+ }
38
+ function copySymbolicLink(source, target) {
39
+ const linkTarget = node_fs_1.default.readlinkSync(source);
40
+ node_fs_1.default.symlinkSync(linkTarget, target);
41
+ }
@@ -0,0 +1,53 @@
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.exists = exists;
7
+ exports.ensureDirectoryExists = ensureDirectoryExists;
8
+ exports.readJsonFile = readJsonFile;
9
+ exports.writeJsonFile = writeJsonFile;
10
+ exports.removeDirectory = removeDirectory;
11
+ exports.assertInsideDirectory = assertInsideDirectory;
12
+ exports.findProjectRoot = findProjectRoot;
13
+ const node_fs_1 = __importDefault(require("node:fs"));
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ function exists(targetPath) {
16
+ return node_fs_1.default.existsSync(targetPath);
17
+ }
18
+ function ensureDirectoryExists(targetPath) {
19
+ node_fs_1.default.mkdirSync(targetPath, { recursive: true });
20
+ }
21
+ function readJsonFile(targetPath) {
22
+ return JSON.parse(node_fs_1.default.readFileSync(targetPath, "utf8"));
23
+ }
24
+ function writeJsonFile(targetPath, value) {
25
+ node_fs_1.default.writeFileSync(targetPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
26
+ }
27
+ function removeDirectory(targetPath) {
28
+ if (!exists(targetPath)) {
29
+ return;
30
+ }
31
+ node_fs_1.default.rmSync(targetPath, { recursive: true, force: true });
32
+ }
33
+ function assertInsideDirectory(parent, child) {
34
+ const resolvedParent = node_path_1.default.resolve(parent);
35
+ const resolvedChild = node_path_1.default.resolve(child);
36
+ const relativePath = node_path_1.default.relative(resolvedParent, resolvedChild);
37
+ if (relativePath.startsWith("..") || node_path_1.default.isAbsolute(relativePath)) {
38
+ throw new Error(`Path is outside expected directory: ${child}`);
39
+ }
40
+ }
41
+ function findProjectRoot(startPath) {
42
+ let currentPath = node_path_1.default.resolve(startPath);
43
+ while (true) {
44
+ if (exists(node_path_1.default.join(currentPath, "package.json"))) {
45
+ return currentPath;
46
+ }
47
+ const parentPath = node_path_1.default.dirname(currentPath);
48
+ if (parentPath === currentPath) {
49
+ return null;
50
+ }
51
+ currentPath = parentPath;
52
+ }
53
+ }
@@ -0,0 +1,30 @@
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.installDependencies = installDependencies;
7
+ exports.installPackage = installPackage;
8
+ exports.runInstall = runInstall;
9
+ const node_child_process_1 = require("node:child_process");
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ function installDependencies(target) {
13
+ runInstall(target);
14
+ }
15
+ function installPackage(target, packageName) {
16
+ runInstall(target, [packageName]);
17
+ }
18
+ function runInstall(target, packages = []) {
19
+ const useBun = node_fs_1.default.existsSync(node_path_1.default.join(target, "bun.lock"));
20
+ const pm = useBun ? "bun" : "npm";
21
+ const action = useBun ? "add" : "install";
22
+ const command = packages.length > 0
23
+ ? [pm, action, ...packages].join(" ")
24
+ : [pm, "install"].join(" ");
25
+ console.log(`Running: ${command}`);
26
+ (0, node_child_process_1.execSync)(command, {
27
+ cwd: target,
28
+ stdio: "inherit"
29
+ });
30
+ }
@@ -0,0 +1,63 @@
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.fetchLatestTarballUrl = fetchLatestTarballUrl;
7
+ exports.downloadTarball = downloadTarball;
8
+ const node_https_1 = __importDefault(require("node:https"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ function fetchLatestTarballUrl(packageName) {
11
+ return new Promise((resolve, reject) => {
12
+ const encodedPackageName = packageName.replace("/", "%2f");
13
+ const url = `https://registry.npmjs.org/${encodedPackageName}/latest`;
14
+ node_https_1.default.get(url, { headers: { "User-Agent": "skalfa-cli" } }, (res) => {
15
+ if (res.statusCode !== 200) {
16
+ reject(new Error(`Failed to fetch latest version info for ${packageName} from npm registry. Status: ${res.statusCode} ${res.statusMessage}`));
17
+ return;
18
+ }
19
+ let data = "";
20
+ res.on("data", (chunk) => {
21
+ data += chunk;
22
+ });
23
+ res.on("end", () => {
24
+ try {
25
+ const json = JSON.parse(data);
26
+ const tarballUrl = json.dist?.tarball;
27
+ if (!tarballUrl) {
28
+ reject(new Error(`Could not find tarball URL in registry response for ${packageName}.`));
29
+ return;
30
+ }
31
+ resolve(tarballUrl);
32
+ }
33
+ catch (err) {
34
+ reject(err);
35
+ }
36
+ });
37
+ }).on("error", (err) => {
38
+ reject(new Error(`Network error while contacting npm registry: ${err.message}`));
39
+ });
40
+ });
41
+ }
42
+ function downloadTarball(url, destPath) {
43
+ return new Promise((resolve, reject) => {
44
+ const file = node_fs_1.default.createWriteStream(destPath);
45
+ node_https_1.default.get(url, { headers: { "User-Agent": "skalfa-cli" } }, (res) => {
46
+ if (res.statusCode !== 200) {
47
+ file.close();
48
+ node_fs_1.default.unlink(destPath, () => { });
49
+ reject(new Error(`Failed to download tarball from ${url}. Status: ${res.statusCode} ${res.statusMessage}`));
50
+ return;
51
+ }
52
+ res.pipe(file);
53
+ file.on("finish", () => {
54
+ file.close();
55
+ resolve();
56
+ });
57
+ }).on("error", (err) => {
58
+ file.close();
59
+ node_fs_1.default.unlink(destPath, () => { });
60
+ reject(err);
61
+ });
62
+ });
63
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@skalfa/skalfa-cli",
3
+ "version": "1.0.2",
4
+ "description": "Skalfa API scaffolding and extension installer CLI.",
5
+ "main": "dist/bin/skalfa.js",
6
+ "bin": {
7
+ "skalfa": "./dist/bin/skalfa.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "start": "node ./dist/bin/skalfa.js",
15
+ "check": "npm run build && node ./dist/bin/skalfa.js --help",
16
+ "prepare": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "aluna",
20
+ "cli",
21
+ "scaffold"
22
+ ],
23
+ "author": "",
24
+ "license": "UNLICENSED",
25
+ "dependencies": {
26
+ "commander": "^12.1.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^26.0.0",
30
+ "typescript": "^6.0.3"
31
+ }
32
+ }