@skalfa/skalfa-cli 1.0.4 → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Skalfa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ <p align="center">
2
+ <img src="https://skalfa.sejedigital.com/images/logo-skalfa.png" alt="Skalfa Logo" width="300" />
3
+ </p>
4
+
5
+ # @skalfa/skalfa-cli
6
+
7
+ > Command Line Interface tool for scaffolding Skalfa projects, managing extensions, and ejecting core utilities.
8
+
9
+ ---
10
+
11
+ ## About this Package
12
+
13
+ This package is part of the **Skalfa Framework**, a premium development ecosystem designed to build high-performance, modular web applications and APIs.
14
+
15
+ ---
16
+
17
+ ## Documentation
18
+
19
+ See the usage documentation at [Documentation](https://skalfa.sejedigital.com).
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ You can install this command line tool globally using your preferred package manager:
26
+
27
+ ```bash
28
+ # Using npm (Global)
29
+ npm install -g @skalfa/skalfa-cli
30
+
31
+ # Using bun (Global)
32
+ bun install -g @skalfa/skalfa-cli
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Command Line Interface (CLI) Guide
38
+
39
+ This package provides the core `skalfa` developer CLI. The following commands are available:
40
+
41
+ ### 🚀 Scaffolding Commands
42
+ * **`skalfa create-api <name>`**: Scaffolds a new high-performance backend API project powered by Elysia, Bun, and modular utility extensions. It prompts sequentially for optional databases, queues, caches, and real-time sockets.
43
+ * **`skalfa create-app <name>`**: Scaffolds a new modern Next.js frontend application with pre-configured templates, styles, PWA integrations, and Tauri mobile/desktop wrappers.
44
+
45
+ ### 🔌 Extension Commands
46
+ * **`skalfa add <extension-name>`**: Automatically installs and configures an optional extension in the current project root. It is project-aware:
47
+ * In a backend project, it adds utilities like `redis`, `queue`, `cache`, `cron`, `da`, `socket`, or `orm`.
48
+ * In a frontend project, it adds extensions like `idb`, `socket`, `document`, `pwa`, `tauri-desktop`, or `tauri-mobile`.
49
+
50
+ ### 🎛️ Ejection Commands
51
+ * **`skalfa pick <utility-name>`**: Ejects a core utility from the compiled core engine directly into your local `utils/` folder, allowing full local customization while maintaining compatibility.
52
+
53
+ ---
54
+
55
+ ## Pre-installed Dependencies
56
+
57
+ The following key dependencies are packaged and managed within this project:
58
+
59
+ | Dependency | Scope | Version |
60
+ | :--- | :--- | :--- |
61
+ | `commander` | runtime | `^12.1.0` |
62
+ | `@types/node` | development | `^26.0.0` |
63
+ | `typescript` | development | `^6.0.3` |
64
+
65
+ ---
66
+
67
+ ## License
68
+
69
+ This package is licensed under the **MIT License**. For full license text, see the [LICENSE](LICENSE) file.
@@ -10,11 +10,12 @@ const node_path_1 = __importDefault(require("node:path"));
10
10
  const node_fs_1 = __importDefault(require("node:fs"));
11
11
  const add_extension_1 = require("../commands/add-extension");
12
12
  const create_api_1 = require("../commands/create-api");
13
+ const create_app_1 = require("../commands/create-app");
13
14
  const pick_1 = require("../commands/pick");
14
15
  const fs_1 = require("../utils/fs");
15
16
  // Dynamic routing / forwarding logic
16
17
  const args = process.argv.slice(2);
17
- const knownCommands = ["create-api", "add", "pick"];
18
+ const knownCommands = ["create-api", "create-app", "add", "pick"];
18
19
  if (args.length > 0 && !knownCommands.includes(args[0]) && !["-h", "--help", "-v", "--version", "help"].includes(args[0])) {
19
20
  const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
20
21
  if (projectRoot) {
@@ -43,6 +44,13 @@ program
43
44
  .action(async (name) => {
44
45
  await runCommand(() => (0, create_api_1.createApi)(name));
45
46
  });
47
+ program
48
+ .command("create-app")
49
+ .description("Create a new Skalfa App Next.js project.")
50
+ .argument("<name>", "project folder and package name")
51
+ .action(async (name) => {
52
+ await runCommand(() => (0, create_app_1.createApp)(name));
53
+ });
46
54
  program
47
55
  .command("add")
48
56
  .description("Install an optional Skalfa extension into the current project.")
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.extensionNames = exports.extensions = void 0;
6
+ exports.frontendExtensions = exports.extensionNames = exports.extensions = void 0;
7
7
  exports.addExtension = addExtension;
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -23,14 +23,135 @@ exports.extensions = {
23
23
  orm: "@skalfa/skalfa-orm"
24
24
  };
25
25
  exports.extensionNames = Object.keys(exports.extensions);
26
+ exports.frontendExtensions = ["idb", "socket", "document", "pwa", "tauri-desktop", "tauri-mobile"];
26
27
  async function addExtension(extensionName) {
27
- const packageName = exports.extensions[extensionName];
28
- if (!packageName) {
29
- throw new Error(`Unknown extension "${extensionName}". Available extensions: ${exports.extensionNames.join(", ")}`);
30
- }
31
28
  const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
32
29
  if (!projectRoot) {
33
- throw new Error("No package.json found. Run this command inside a Skalfa API project.");
30
+ throw new Error("No package.json found. Run this command inside a Skalfa project.");
31
+ }
32
+ // Detect project type by checking package.json dependencies
33
+ const packageJsonPath = node_path_1.default.join(projectRoot, "package.json");
34
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
35
+ const isFrontend = pkg.dependencies && pkg.dependencies["next"];
36
+ if (isFrontend) {
37
+ if (!exports.frontendExtensions.includes(extensionName)) {
38
+ throw new Error(`Unknown frontend extension "${extensionName}". Available frontend extensions: ${exports.frontendExtensions.join(", ")}`);
39
+ }
40
+ const isDev = !!process.env["SKALFA_APP_TEMPLATE"];
41
+ if (extensionName === "idb") {
42
+ console.log("Installing Skalfa IndexedDB extension...");
43
+ (0, installer_1.installPackage)(projectRoot, isDev ? "file:../skalfa-idb" : "@skalfa/skalfa-idb");
44
+ addTsconfigPath(node_path_1.default.join(projectRoot, "tsconfig.json"), "@skalfa/skalfa-idb");
45
+ addUtilExport(node_path_1.default.join(projectRoot, "utils", "index.ts"), "@skalfa/skalfa-idb");
46
+ }
47
+ else if (extensionName === "socket") {
48
+ console.log("Installing Skalfa Socket.io client extension...");
49
+ (0, installer_1.installPackage)(projectRoot, isDev ? "file:../skalfa-socket-client" : "@skalfa/skalfa-socket-client");
50
+ (0, installer_1.installPackage)(projectRoot, "socket.io-client");
51
+ addTsconfigPath(node_path_1.default.join(projectRoot, "tsconfig.json"), "@skalfa/skalfa-socket-client");
52
+ addUtilExport(node_path_1.default.join(projectRoot, "utils", "index.ts"), "@skalfa/skalfa-socket-client");
53
+ }
54
+ else if (extensionName === "document") {
55
+ console.log("Installing Skalfa Document export extension...");
56
+ (0, installer_1.installPackage)(projectRoot, isDev ? "file:../skalfa-document" : "@skalfa/skalfa-document");
57
+ (0, installer_1.installPackage)(projectRoot, "exceljs");
58
+ (0, installer_1.installPackage)(projectRoot, "pdf-lib");
59
+ (0, installer_1.installPackage)(projectRoot, "pdfjs-dist");
60
+ addTsconfigPath(node_path_1.default.join(projectRoot, "tsconfig.json"), "@skalfa/skalfa-document");
61
+ addUtilExport(node_path_1.default.join(projectRoot, "utils", "index.ts"), "@skalfa/skalfa-document");
62
+ // Copy public worker
63
+ console.log("Scaffolding pdf worker file...");
64
+ const { templateSource, cleanup } = await getAppTemplateSource(projectRoot);
65
+ try {
66
+ const workerSrc = node_path_1.default.join(templateSource, "public", "pdf.worker.min.mjs");
67
+ const workerDest = node_path_1.default.join(projectRoot, "public", "pdf.worker.min.mjs");
68
+ if ((0, fs_1.exists)(workerSrc)) {
69
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(workerDest), { recursive: true });
70
+ node_fs_1.default.copyFileSync(workerSrc, workerDest);
71
+ }
72
+ }
73
+ finally {
74
+ cleanup();
75
+ }
76
+ // Add exports back to components/base.components/index.ts to keep @components compatibility
77
+ const baseComponentsIndexPath = node_path_1.default.join(projectRoot, "components", "base.components", "index.ts");
78
+ if (node_fs_1.default.existsSync(baseComponentsIndexPath)) {
79
+ let content = node_fs_1.default.readFileSync(baseComponentsIndexPath, "utf8");
80
+ if (!content.includes("@skalfa/skalfa-document")) {
81
+ content += `\nexport * from "@skalfa/skalfa-document";\n`;
82
+ node_fs_1.default.writeFileSync(baseComponentsIndexPath, content, "utf8");
83
+ }
84
+ }
85
+ }
86
+ else if (extensionName === "pwa") {
87
+ console.log("Installing Skalfa PWA extension...");
88
+ (0, installer_1.installPackage)(projectRoot, "@ducanh2912/next-pwa");
89
+ // Copy manifest.ts from template if it doesn't exist
90
+ const manifestPath = node_path_1.default.join(projectRoot, "app", "manifest.ts");
91
+ if (!node_fs_1.default.existsSync(manifestPath)) {
92
+ console.log("Scaffolding PWA manifest file...");
93
+ const { templateSource, cleanup } = await getAppTemplateSource(projectRoot);
94
+ try {
95
+ const manifestSrc = node_path_1.default.join(templateSource, "app", "manifest.ts");
96
+ if ((0, fs_1.exists)(manifestSrc)) {
97
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(manifestPath), { recursive: true });
98
+ node_fs_1.default.copyFileSync(manifestSrc, manifestPath);
99
+ }
100
+ }
101
+ finally {
102
+ cleanup();
103
+ }
104
+ }
105
+ // Wrap next.config.ts with withPWA
106
+ const nextConfigPath = node_path_1.default.join(projectRoot, "next.config.ts");
107
+ if (node_fs_1.default.existsSync(nextConfigPath)) {
108
+ let content = node_fs_1.default.readFileSync(nextConfigPath, "utf8");
109
+ if (!content.includes("@ducanh2912/next-pwa")) {
110
+ content = `import withPWAInit from "@ducanh2912/next-pwa";\n` + content;
111
+ content = content.replace(/export default nextConfig;/, `const withPWA = withPWAInit({\n dest: "public",\n disable: process.env.NODE_ENV === "development",\n});\n\nexport default withPWA(nextConfig);`);
112
+ node_fs_1.default.writeFileSync(nextConfigPath, content, "utf8");
113
+ }
114
+ }
115
+ }
116
+ else if (extensionName === "tauri-desktop" || extensionName === "tauri-mobile") {
117
+ console.log(`Installing Skalfa ${extensionName === "tauri-desktop" ? "Tauri Desktop" : "Tauri Mobile"} extension...`);
118
+ (0, installer_1.installPackage)(projectRoot, "@tauri-apps/api");
119
+ (0, installer_1.installPackage)(projectRoot, "@tauri-apps/cli", true); // devDependency
120
+ (0, installer_1.installPackage)(projectRoot, "cross-env", true); // devDependency
121
+ // Copy src-tauri folder
122
+ console.log("Scaffolding Tauri configuration...");
123
+ const tauriDest = node_path_1.default.join(projectRoot, "src-tauri");
124
+ if (!node_fs_1.default.existsSync(tauriDest)) {
125
+ const { templateSource, cleanup } = await getAppTemplateSource(projectRoot);
126
+ try {
127
+ const tauriSrc = node_path_1.default.join(templateSource, "src-tauri");
128
+ if ((0, fs_1.exists)(tauriSrc)) {
129
+ (0, copier_1.copyTemplate)(tauriSrc, tauriDest);
130
+ }
131
+ }
132
+ finally {
133
+ cleanup();
134
+ }
135
+ }
136
+ // Add scripts to package.json
137
+ if (node_fs_1.default.existsSync(packageJsonPath)) {
138
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
139
+ pkg.scripts = pkg.scripts || {};
140
+ pkg.scripts["tauri"] = "cross-env IS_TAURI=true tauri";
141
+ if (extensionName === "tauri-mobile") {
142
+ pkg.scripts["tauri:android"] = "cross-env IS_TAURI=true tauri android";
143
+ pkg.scripts["tauri:ios"] = "cross-env IS_TAURI=true tauri ios";
144
+ }
145
+ node_fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), "utf8");
146
+ }
147
+ }
148
+ console.log(`✓ Frontend extension "${extensionName}" successfully installed and configured.`);
149
+ return;
150
+ }
151
+ // Backend scaffolding logic
152
+ const packageName = exports.extensions[extensionName];
153
+ if (!packageName) {
154
+ throw new Error(`Unknown backend extension "${extensionName}". Available extensions: ${exports.extensionNames.join(", ")}`);
34
155
  }
35
156
  const isDev = !!process.env["SKALFA_API_TEMPLATE"];
36
157
  if (extensionName === "orm") {
@@ -80,7 +201,6 @@ async function getTemplateSource(projectRoot) {
80
201
  return { templateSource, cleanup: () => { } };
81
202
  }
82
203
  else {
83
- // Dynamic download from npm registry
84
204
  const templatePackageName = "@skalfa/skalfa-api";
85
205
  console.log(`Fetching latest template info for ${templatePackageName} from npm registry...`);
86
206
  const tarballUrl = await (0, npm_1.fetchLatestTarballUrl)(templatePackageName);
@@ -113,10 +233,53 @@ async function getTemplateSource(projectRoot) {
113
233
  };
114
234
  }
115
235
  }
236
+ async function getAppTemplateSource(projectRoot) {
237
+ const envTemplateSource = process.env["SKALFA_APP_TEMPLATE"];
238
+ let tempExtractDir = null;
239
+ let templateSource = "";
240
+ if (envTemplateSource) {
241
+ templateSource = node_path_1.default.resolve(envTemplateSource);
242
+ if (!(0, fs_1.exists)(templateSource)) {
243
+ throw new Error(`Template source override not found: ${templateSource}`);
244
+ }
245
+ return { templateSource, cleanup: () => { } };
246
+ }
247
+ else {
248
+ const templatePackageName = "@skalfa/skalfa-app";
249
+ console.log(`Fetching latest template info for ${templatePackageName} from npm registry...`);
250
+ const tarballUrl = await (0, npm_1.fetchLatestTarballUrl)(templatePackageName);
251
+ const parentDir = node_path_1.default.dirname(projectRoot);
252
+ tempExtractDir = node_path_1.default.join(parentDir, `skalfa-app-temp-extract-${Date.now()}`);
253
+ node_fs_1.default.mkdirSync(tempExtractDir, { recursive: true });
254
+ const tarballPath = node_path_1.default.join(tempExtractDir, "template.tgz");
255
+ console.log("Downloading template tarball...");
256
+ await (0, npm_1.downloadTarball)(tarballUrl, tarballPath);
257
+ console.log("Extracting template...");
258
+ try {
259
+ (0, node_child_process_1.execSync)(`tar -xzf "${tarballPath}" -C "${tempExtractDir}"`, { stdio: "ignore" });
260
+ }
261
+ catch (err) {
262
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
263
+ throw new Error(`Failed to extract template tarball. Please ensure 'tar' command is available: ${err.message}`);
264
+ }
265
+ templateSource = node_path_1.default.join(tempExtractDir, "package");
266
+ if (!(0, fs_1.exists)(templateSource)) {
267
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
268
+ throw new Error("Invalid template structure: 'package' folder not found inside tarball.");
269
+ }
270
+ return {
271
+ templateSource,
272
+ cleanup: () => {
273
+ if (tempExtractDir && (0, fs_1.exists)(tempExtractDir)) {
274
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
275
+ }
276
+ }
277
+ };
278
+ }
279
+ }
116
280
  async function scaffoldOrmExtension(projectRoot) {
117
281
  const { templateSource, cleanup } = await getTemplateSource(projectRoot);
118
282
  try {
119
- // 1. Copy database/ folder and app/models/ folder from template
120
283
  console.log("Scaffolding database and model directories...");
121
284
  const dbSrc = node_path_1.default.join(templateSource, "database");
122
285
  const dbDest = node_path_1.default.join(projectRoot, "database");
@@ -128,7 +291,6 @@ async function scaffoldOrmExtension(projectRoot) {
128
291
  if ((0, fs_1.exists)(modelsSrc)) {
129
292
  (0, copier_1.copyTemplate)(modelsSrc, modelsDest);
130
293
  }
131
- // 2. Copy database-enabled UserController and AuthController from template
132
294
  console.log("Restoring database-enabled controllers...");
133
295
  const authControllerSrc = node_path_1.default.join(templateSource, "app", "controllers", "iam", "auth.controller.ts");
134
296
  const authControllerDest = node_path_1.default.join(projectRoot, "app", "controllers", "iam", "auth.controller.ts");
@@ -142,7 +304,6 @@ async function scaffoldOrmExtension(projectRoot) {
142
304
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(userControllerDest), { recursive: true });
143
305
  node_fs_1.default.copyFileSync(userControllerSrc, userControllerDest);
144
306
  }
145
- // 3. Restore database CLI commands loader if missing
146
307
  console.log("Restoring database CLI commands...");
147
308
  const commandsSrc = node_path_1.default.join(templateSource, "utils", "commands");
148
309
  const commandsDest = node_path_1.default.join(projectRoot, "utils", "commands");
@@ -154,7 +315,6 @@ async function scaffoldOrmExtension(projectRoot) {
154
315
  node_fs_1.default.copyFileSync(skalfaCliSrc, skalfaCliDest);
155
316
  }
156
317
  }
157
- // 4. Restore the database initialization block and imports in app/app.ts
158
318
  console.log("Restoring database initialization in app/app.ts...");
159
319
  const appTsSrc = node_path_1.default.join(templateSource, "app", "app.ts");
160
320
  const appTsDest = node_path_1.default.join(projectRoot, "app", "app.ts");
@@ -172,7 +332,6 @@ async function scaffoldOrmExtension(projectRoot) {
172
332
  node_fs_1.default.writeFileSync(appTsDest, targetAppTs, "utf8");
173
333
  }
174
334
  }
175
- // 5. Update tsconfig.json path mappings
176
335
  console.log("Updating tsconfig.json paths...");
177
336
  const tsconfigPath = node_path_1.default.join(projectRoot, "tsconfig.json");
178
337
  if ((0, fs_1.exists)(tsconfigPath)) {
@@ -182,7 +341,6 @@ async function scaffoldOrmExtension(projectRoot) {
182
341
  node_fs_1.default.writeFileSync(tsconfigPath, content, "utf8");
183
342
  }
184
343
  }
185
- // 6. Update utils/index.ts exports
186
344
  console.log("Updating utils/index.ts exports...");
187
345
  const utilsIndexPath = node_path_1.default.join(projectRoot, "utils", "index.ts");
188
346
  if ((0, fs_1.exists)(utilsIndexPath)) {
@@ -206,13 +364,11 @@ async function scaffoldUtilityExtension(projectRoot, ext) {
206
364
  const tsconfigPath = node_path_1.default.join(projectRoot, "tsconfig.json");
207
365
  const utilsIndexPath = node_path_1.default.join(projectRoot, "utils", "index.ts");
208
366
  const appTsPath = node_path_1.default.join(projectRoot, "app", "app.ts");
209
- // 1. Copy relevant template folders
210
367
  if (ext === "queue") {
211
368
  console.log("Copying Queue worker examples...");
212
369
  const src = node_path_1.default.join(templateSource, "app", "jobs", "queues");
213
370
  const dest = node_path_1.default.join(projectRoot, "app", "jobs", "queues");
214
371
  (0, copier_1.copyTemplate)(src, dest);
215
- // Check if project has DA or Notification packages
216
372
  const packageJsonPath = node_path_1.default.join(projectRoot, "package.json");
217
373
  let hasDa = false;
218
374
  let hasNotification = false;
@@ -242,31 +398,25 @@ async function scaffoldUtilityExtension(projectRoot, ext) {
242
398
  const dest = node_path_1.default.join(projectRoot, "database", "da.migrations");
243
399
  (0, copier_1.copyTemplate)(src, dest);
244
400
  }
245
- // 2. Update tsconfig.json path mappings
246
401
  addTsconfigPath(tsconfigPath, `@skalfa/skalfa-${ext}`);
247
402
  if (ext === "queue" || ext === "cache") {
248
403
  addTsconfigPath(tsconfigPath, "@skalfa/skalfa-redis");
249
404
  }
250
- // 3. Update utils/index.ts exports
251
405
  addUtilExport(utilsIndexPath, `@skalfa/skalfa-${ext}`);
252
406
  if (ext === "queue" || ext === "cache") {
253
407
  addUtilExport(utilsIndexPath, "@skalfa/skalfa-redis");
254
408
  }
255
- // 4. Uncomment initialization blocks and update imports in app/app.ts
256
409
  if (node_fs_1.default.existsSync(appTsPath)) {
257
410
  let content = node_fs_1.default.readFileSync(appTsPath, "utf8");
258
411
  const importsToAdd = [];
259
412
  if (ext === "redis" || ext === "queue" || ext === "cache") {
260
413
  importsToAdd.push("redis");
261
- // Uncomment Redis block
262
414
  content = content.replace(/\/\/ if \(process\.env\.REDIS_HOST[\s\S]*?\/\/ \}/g, (match) => match.replace(/^\/\/ ?/gm, ""));
263
415
  }
264
416
  if (ext === "da") {
265
417
  importsToAdd.push("daClient");
266
- // Uncomment DA block
267
418
  content = content.replace(/\/\/ if \(process\.env\.DA_HOST[\s\S]*?\/\/ }/g, (match) => match.replace(/^\/\/ ?/gm, ""));
268
419
  }
269
- // Update import statement at the top of app.ts
270
420
  if (importsToAdd.length > 0) {
271
421
  const importRegex = /import\s*\{\s*([\s\S]*?)\s*\}\s*from\s*["']@utils["']/;
272
422
  const match = content.match(importRegex);
@@ -279,7 +429,6 @@ async function scaffoldUtilityExtension(projectRoot, ext) {
279
429
  }
280
430
  node_fs_1.default.writeFileSync(appTsPath, content, "utf8");
281
431
  }
282
- // 5. Update package.json scripts for workers
283
432
  const packageJsonPath = node_path_1.default.join(projectRoot, "package.json");
284
433
  if (node_fs_1.default.existsSync(packageJsonPath) && ["cron", "queue", "socket"].includes(ext)) {
285
434
  const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
@@ -300,7 +449,6 @@ async function scaffoldUtilityExtension(projectRoot, ext) {
300
449
  }
301
450
  if (scriptKey) {
302
451
  pkg.scripts[scriptKey] = scriptVal;
303
- // Update dev concurrently command
304
452
  const devScript = pkg.scripts["dev"] || "";
305
453
  const runCmd = `bun ${scriptKey}`;
306
454
  if (devScript.includes("concurrently")) {
@@ -0,0 +1,257 @@
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.createApp = createApp;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_child_process_1 = require("node:child_process");
10
+ const node_readline_1 = __importDefault(require("node:readline"));
11
+ const npm_1 = require("../utils/npm");
12
+ const installer_1 = require("../utils/installer");
13
+ const fs_1 = require("../utils/fs");
14
+ const copier_1 = require("../utils/copier");
15
+ const TEMPLATE_ENV_KEY = "SKALFA_APP_TEMPLATE";
16
+ class Questioner {
17
+ rl;
18
+ constructor() {
19
+ this.rl = node_readline_1.default.createInterface({
20
+ input: process.stdin,
21
+ output: process.stdout,
22
+ });
23
+ }
24
+ ask(query) {
25
+ return new Promise((resolve) => {
26
+ this.rl.question(query, (answer) => {
27
+ resolve(answer);
28
+ });
29
+ });
30
+ }
31
+ close() {
32
+ this.rl.close();
33
+ }
34
+ }
35
+ async function createApp(projectName) {
36
+ const cwd = process.cwd();
37
+ const target = node_path_1.default.resolve(cwd, projectName);
38
+ const packageName = node_path_1.default.basename(target);
39
+ (0, fs_1.assertInsideDirectory)(cwd, target);
40
+ if ((0, fs_1.exists)(target)) {
41
+ throw new Error(`Target directory already exists: ${target}`);
42
+ }
43
+ // Ask interactive questions sequentially
44
+ const q = new Questioner();
45
+ let hasIdb = false;
46
+ let hasSocket = false;
47
+ let hasDocument = false;
48
+ let hasPwa = false;
49
+ let hasTauriDesktop = false;
50
+ let hasTauriMobile = false;
51
+ try {
52
+ hasIdb = (await q.ask("Do you need IndexedDB (IDB)? (y/N): ")).toLowerCase().startsWith("y");
53
+ hasSocket = (await q.ask("Do you need Socket.io Client? (y/N): ")).toLowerCase().startsWith("y");
54
+ hasDocument = (await q.ask("Do you need Document Export/Viewer (PDF/Excel)? (y/N): ")).toLowerCase().startsWith("y");
55
+ hasPwa = (await q.ask("Do you want to enable Progressive Web App (PWA)? (y/N): ")).toLowerCase().startsWith("y");
56
+ hasTauriDesktop = (await q.ask("Do you want to enable Tauri Desktop support (Windows/macOS/Linux)? (y/N): ")).toLowerCase().startsWith("y");
57
+ hasTauriMobile = (await q.ask("Do you want to enable Tauri Mobile support (Android/iOS)? (y/N): ")).toLowerCase().startsWith("y");
58
+ }
59
+ finally {
60
+ q.close();
61
+ }
62
+ const envTemplateSource = process.env[TEMPLATE_ENV_KEY];
63
+ if (envTemplateSource) {
64
+ // Local copy mode
65
+ const templateSource = node_path_1.default.resolve(envTemplateSource);
66
+ console.log(`Creating Skalfa App project from local template override: ${templateSource}`);
67
+ if (!(0, fs_1.exists)(templateSource)) {
68
+ throw new Error(`Template source override not found: ${templateSource}`);
69
+ }
70
+ (0, copier_1.copyTemplate)(templateSource, target);
71
+ }
72
+ else {
73
+ // Dynamic download from npm registry
74
+ const templatePackageName = "@skalfa/skalfa-app";
75
+ console.log(`Fetching latest template info for ${templatePackageName} from npm registry...`);
76
+ const tarballUrl = await (0, npm_1.fetchLatestTarballUrl)(templatePackageName);
77
+ const parentDir = node_path_1.default.dirname(target);
78
+ const tempExtractDir = node_path_1.default.join(parentDir, `${projectName}-temp-extract`);
79
+ if ((0, fs_1.exists)(tempExtractDir)) {
80
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
81
+ }
82
+ node_fs_1.default.mkdirSync(tempExtractDir, { recursive: true });
83
+ const tarballPath = node_path_1.default.join(tempExtractDir, "template.tgz");
84
+ console.log("Downloading template tarball...");
85
+ await (0, npm_1.downloadTarball)(tarballUrl, tarballPath);
86
+ console.log("Extracting template...");
87
+ try {
88
+ (0, node_child_process_1.execSync)(`tar -xzf "${tarballPath}" -C "${tempExtractDir}"`, { stdio: "ignore" });
89
+ }
90
+ catch (err) {
91
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
92
+ throw new Error(`Failed to extract template tarball. Please ensure 'tar' command is available: ${err.message}`);
93
+ }
94
+ const packageDir = node_path_1.default.join(tempExtractDir, "package");
95
+ if (!(0, fs_1.exists)(packageDir)) {
96
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
97
+ throw new Error("Invalid template structure: 'package' folder not found inside tarball.");
98
+ }
99
+ node_fs_1.default.renameSync(packageDir, target);
100
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
101
+ }
102
+ // Cleanup git and github directories
103
+ (0, fs_1.removeDirectory)(node_path_1.default.join(target, ".git"));
104
+ (0, fs_1.removeDirectory)(node_path_1.default.join(target, ".github"));
105
+ renamePackage(target, packageName);
106
+ // Rename .npmignore to .gitignore if it exists
107
+ const npmignorePath = node_path_1.default.join(target, ".npmignore");
108
+ const gitignorePath = node_path_1.default.join(target, ".gitignore");
109
+ if (node_fs_1.default.existsSync(npmignorePath) && !node_fs_1.default.existsSync(gitignorePath)) {
110
+ node_fs_1.default.renameSync(npmignorePath, gitignorePath);
111
+ }
112
+ // Customize project with selected options
113
+ customizeProject(target, {
114
+ idb: hasIdb,
115
+ socket: hasSocket,
116
+ document: hasDocument,
117
+ pwa: hasPwa,
118
+ tauriDesktop: hasTauriDesktop,
119
+ tauriMobile: hasTauriMobile
120
+ });
121
+ console.log("Installing dependencies...");
122
+ (0, installer_1.installDependencies)(target);
123
+ console.log("");
124
+ console.log("✓ Skalfa App Next.js project is ready.");
125
+ console.log(`Next steps:\n cd ${projectName}\n bun run dev`);
126
+ }
127
+ function renamePackage(target, packageName) {
128
+ const packageJsonPath = node_path_1.default.join(target, "package.json");
129
+ if (!(0, fs_1.exists)(packageJsonPath)) {
130
+ console.warn("Skipped package rename: package.json was not found.");
131
+ return;
132
+ }
133
+ const packageJson = (0, fs_1.readJsonFile)(packageJsonPath);
134
+ packageJson.name = packageName;
135
+ (0, fs_1.writeJsonFile)(packageJsonPath, packageJson);
136
+ }
137
+ function customizeProject(target, opts) {
138
+ const packageJsonPath = node_path_1.default.join(target, "package.json");
139
+ const baseComponentsIndexPath = node_path_1.default.join(target, "components", "base.components", "index.ts");
140
+ const isDev = !!process.env[TEMPLATE_ENV_KEY];
141
+ // 1. Update dependencies and scripts in package.json
142
+ if (node_fs_1.default.existsSync(packageJsonPath)) {
143
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
144
+ pkg.dependencies = pkg.dependencies || {};
145
+ pkg.devDependencies = pkg.devDependencies || {};
146
+ pkg.scripts = pkg.scripts || {};
147
+ // Core dependency
148
+ pkg.dependencies["@skalfa/skalfa-app-core"] = isDev ? "file:../skalfa-app-core" : "^1.0.0";
149
+ // A. IndexedDB Option
150
+ if (opts.idb) {
151
+ pkg.dependencies["@skalfa/skalfa-idb"] = isDev ? "file:../skalfa-idb" : "^1.0.0";
152
+ }
153
+ else {
154
+ // Delete schema directory
155
+ const schemaDir = node_path_1.default.join(target, "schema");
156
+ if (node_fs_1.default.existsSync(schemaDir)) {
157
+ node_fs_1.default.rmSync(schemaDir, { recursive: true, force: true });
158
+ }
159
+ // Delete IDBProvider component
160
+ const idbProviderPath = node_path_1.default.join(target, "components", "base.components", "wrap", "IDBProvider.tsx");
161
+ if (node_fs_1.default.existsSync(idbProviderPath)) {
162
+ node_fs_1.default.unlinkSync(idbProviderPath);
163
+ }
164
+ // Remove export from base.components/index.ts
165
+ if (node_fs_1.default.existsSync(baseComponentsIndexPath)) {
166
+ let content = node_fs_1.default.readFileSync(baseComponentsIndexPath, "utf8");
167
+ content = content.replace(/export \* from "\.\/wrap\/IDBProvider";\r?\n?/g, "");
168
+ node_fs_1.default.writeFileSync(baseComponentsIndexPath, content, "utf8");
169
+ }
170
+ // Modify app/layout.tsx to remove IDBProvider wrapper
171
+ const layoutPath = node_path_1.default.join(target, "app", "layout.tsx");
172
+ if (node_fs_1.default.existsSync(layoutPath)) {
173
+ let content = node_fs_1.default.readFileSync(layoutPath, "utf8");
174
+ content = content
175
+ .replace(/import\s*\{\s*IDBProvider\s*,\s*ShortcutProvider\s*\}\s*from\s*["']@components["'];?/g, 'import { ShortcutProvider } from "@components";')
176
+ .replace(/<IDBProvider>\s*\r?\n?/g, "")
177
+ .replace(/<\/IDBProvider>\s*\r?\n?/g, "");
178
+ node_fs_1.default.writeFileSync(layoutPath, content, "utf8");
179
+ }
180
+ }
181
+ // B. Socket Option
182
+ if (opts.socket) {
183
+ pkg.dependencies["@skalfa/skalfa-socket-client"] = isDev ? "file:../skalfa-socket-client" : "^1.0.0";
184
+ pkg.dependencies["socket.io-client"] = "^4.8.1";
185
+ }
186
+ // C. Document Option
187
+ if (opts.document) {
188
+ pkg.dependencies["@skalfa/skalfa-document"] = isDev ? "file:../skalfa-document" : "^1.0.0";
189
+ pkg.dependencies["exceljs"] = "^4.4.0";
190
+ pkg.dependencies["pdf-lib"] = "^1.17.1";
191
+ pkg.dependencies["pdfjs-dist"] = "^4.4.168";
192
+ }
193
+ else {
194
+ // Delete local document folder
195
+ const documentDir = node_path_1.default.join(target, "components", "base.components", "document");
196
+ if (node_fs_1.default.existsSync(documentDir)) {
197
+ node_fs_1.default.rmSync(documentDir, { recursive: true, force: true });
198
+ }
199
+ // Delete public pdf worker
200
+ const workerPath = node_path_1.default.join(target, "public", "pdf.worker.min.mjs");
201
+ if (node_fs_1.default.existsSync(workerPath)) {
202
+ node_fs_1.default.unlinkSync(workerPath);
203
+ }
204
+ // Remove exports from base.components/index.ts
205
+ if (node_fs_1.default.existsSync(baseComponentsIndexPath)) {
206
+ let content = node_fs_1.default.readFileSync(baseComponentsIndexPath, "utf8");
207
+ content = content
208
+ .replace(/export \* from "\.\/document\/DocumentViewer\.component";\r?\n?/g, "")
209
+ .replace(/export \* from "\.\/document\/ExportExcel\.component";\r?\n?/g, "")
210
+ .replace(/export \* from "\.\/document\/ImportExcel\.component";\r?\n?/g, "")
211
+ .replace(/export \* from "\.\/document\/PrintTable\.component";\r?\n?/g, "")
212
+ .replace(/export \* from "\.\/document\/RenderPDF\.component";\r?\n?/g, "");
213
+ node_fs_1.default.writeFileSync(baseComponentsIndexPath, content, "utf8");
214
+ }
215
+ }
216
+ // D. PWA Option
217
+ if (opts.pwa) {
218
+ pkg.dependencies["@ducanh2912/next-pwa"] = "^10.2.9";
219
+ // Wrap next.config.ts with withPWA
220
+ const nextConfigPath = node_path_1.default.join(target, "next.config.ts");
221
+ if (node_fs_1.default.existsSync(nextConfigPath)) {
222
+ let content = node_fs_1.default.readFileSync(nextConfigPath, "utf8");
223
+ if (!content.includes("@ducanh2912/next-pwa")) {
224
+ content = `import withPWAInit from "@ducanh2912/next-pwa";\n` + content;
225
+ content = content.replace(/export default nextConfig;/, `const withPWA = withPWAInit({\n dest: "public",\n disable: process.env.NODE_ENV === "development",\n});\n\nexport default withPWA(nextConfig);`);
226
+ node_fs_1.default.writeFileSync(nextConfigPath, content, "utf8");
227
+ }
228
+ }
229
+ }
230
+ else {
231
+ // Delete manifest.ts
232
+ const manifestPath = node_path_1.default.join(target, "app", "manifest.ts");
233
+ if (node_fs_1.default.existsSync(manifestPath)) {
234
+ node_fs_1.default.unlinkSync(manifestPath);
235
+ }
236
+ }
237
+ // E. Tauri Option
238
+ if (opts.tauriDesktop || opts.tauriMobile) {
239
+ pkg.dependencies["@tauri-apps/api"] = "^2.0.0";
240
+ pkg.devDependencies["@tauri-apps/cli"] = "^2.0.0";
241
+ pkg.devDependencies["cross-env"] = "^7.0.3";
242
+ pkg.scripts["tauri"] = "cross-env IS_TAURI=true tauri";
243
+ if (opts.tauriMobile) {
244
+ pkg.scripts["tauri:android"] = "cross-env IS_TAURI=true tauri android";
245
+ pkg.scripts["tauri:ios"] = "cross-env IS_TAURI=true tauri ios";
246
+ }
247
+ }
248
+ else {
249
+ // Delete src-tauri folder
250
+ const tauriDir = node_path_1.default.join(target, "src-tauri");
251
+ if (node_fs_1.default.existsSync(tauriDir)) {
252
+ node_fs_1.default.rmSync(tauriDir, { recursive: true, force: true });
253
+ }
254
+ }
255
+ node_fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), "utf8");
256
+ }
257
+ }
@@ -12,15 +12,16 @@ const node_path_1 = __importDefault(require("node:path"));
12
12
  function installDependencies(target) {
13
13
  runInstall(target);
14
14
  }
15
- function installPackage(target, packageName) {
16
- runInstall(target, [packageName]);
15
+ function installPackage(target, packageName, isDev = false) {
16
+ runInstall(target, [packageName], isDev);
17
17
  }
18
- function runInstall(target, packages = []) {
18
+ function runInstall(target, packages = [], isDev = false) {
19
19
  const useBun = node_fs_1.default.existsSync(node_path_1.default.join(target, "bun.lock"));
20
20
  const pm = useBun ? "bun" : "npm";
21
21
  const action = useBun ? "add" : "install";
22
+ const devFlag = isDev ? (useBun ? "-d" : "--save-dev") : "";
22
23
  const command = packages.length > 0
23
- ? [pm, action, ...packages].join(" ")
24
+ ? [pm, action, devFlag, ...packages].filter(Boolean).join(" ")
24
25
  : [pm, "install"].join(" ");
25
26
  console.log(`Running: ${command}`);
26
27
  (0, node_child_process_1.execSync)(command, {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@skalfa/skalfa-cli",
3
- "version": "1.0.4",
4
- "description": "Skalfa API scaffolding and extension installer CLI.",
3
+ "version": "1.0.6",
4
+ "description": "Command Line Interface tool for scaffolding Skalfa projects, managing extensions, and ejecting core utilities.",
5
5
  "main": "dist/bin/skalfa.js",
6
6
  "bin": {
7
7
  "skalfa": "./dist/bin/skalfa.js"
@@ -29,4 +29,4 @@
29
29
  "@types/node": "^26.0.0",
30
30
  "typescript": "^6.0.3"
31
31
  }
32
- }
32
+ }