@skalfa/skalfa-cli 1.0.8 → 1.0.11
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/bin/skalfa.js +9 -1
- package/dist/commands/agent.js +30 -4
- package/dist/commands/create-api.js +74 -20
- package/dist/commands/create-app.js +66 -20
- package/dist/commands/init.js +189 -0
- package/package.json +1 -1
package/dist/bin/skalfa.js
CHANGED
|
@@ -11,13 +11,14 @@ 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
13
|
const create_app_1 = require("../commands/create-app");
|
|
14
|
+
const init_1 = require("../commands/init");
|
|
14
15
|
const pick_1 = require("../commands/pick");
|
|
15
16
|
const update_1 = require("../commands/update");
|
|
16
17
|
const agent_1 = require("../commands/agent");
|
|
17
18
|
const fs_1 = require("../utils/fs");
|
|
18
19
|
// Dynamic routing / forwarding logic
|
|
19
20
|
const args = process.argv.slice(2);
|
|
20
|
-
const knownCommands = ["create:api", "create:app", "add", "pick", "update", "agent:install", "agent:update"];
|
|
21
|
+
const knownCommands = ["init", "create:api", "create:app", "add", "pick", "update", "agent:install", "agent:update"];
|
|
21
22
|
if (args.length > 0 && !knownCommands.includes(args[0]) && !["-h", "--help", "-v", "--version", "help"].includes(args[0])) {
|
|
22
23
|
const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
|
|
23
24
|
if (projectRoot) {
|
|
@@ -58,6 +59,13 @@ program
|
|
|
58
59
|
.description("Start building with skalfa ecosystem.")
|
|
59
60
|
.version(version)
|
|
60
61
|
.addHelpText("before", banner);
|
|
62
|
+
program
|
|
63
|
+
.command("init")
|
|
64
|
+
.description("Initialize a new Skalfa monorepo project containing both API and App.")
|
|
65
|
+
.argument("[name]", "project folder name")
|
|
66
|
+
.action(async (name) => {
|
|
67
|
+
await runCommand(() => (0, init_1.initProject)(name));
|
|
68
|
+
});
|
|
61
69
|
program
|
|
62
70
|
.command("create:api")
|
|
63
71
|
.description("Create a new Skalfa API project.")
|
package/dist/commands/agent.js
CHANGED
|
@@ -8,11 +8,11 @@ exports.updateAgent = updateAgent;
|
|
|
8
8
|
const node_child_process_1 = require("node:child_process");
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
-
async function installAgent(overrideType) {
|
|
11
|
+
async function installAgent(overrideType, targetProjectDir = process.cwd()) {
|
|
12
12
|
let type = overrideType;
|
|
13
13
|
// 1. Auto-detect project type if not overridden
|
|
14
14
|
if (!type) {
|
|
15
|
-
const pkgPath = node_path_1.default.join(
|
|
15
|
+
const pkgPath = node_path_1.default.join(targetProjectDir, "package.json");
|
|
16
16
|
if (!node_fs_1.default.existsSync(pkgPath)) {
|
|
17
17
|
throw new Error("package.json not found. Please run this command in your project root.");
|
|
18
18
|
}
|
|
@@ -38,7 +38,7 @@ async function installAgent(overrideType) {
|
|
|
38
38
|
if (type !== "api" && type !== "app") {
|
|
39
39
|
throw new Error("Invalid agent type. Must be 'api' or 'app'.");
|
|
40
40
|
}
|
|
41
|
-
const targetDir = node_path_1.default.join(
|
|
41
|
+
const targetDir = node_path_1.default.join(targetProjectDir, ".agents");
|
|
42
42
|
if (node_fs_1.default.existsSync(targetDir)) {
|
|
43
43
|
throw new Error(".agents folder already exists. Run 'skalfa agent:update' to pull the latest changes.");
|
|
44
44
|
}
|
|
@@ -69,7 +69,7 @@ async function installAgent(overrideType) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
// 3. Add /.agents/ to project's .gitignore
|
|
72
|
-
const gitignorePath = node_path_1.default.join(
|
|
72
|
+
const gitignorePath = node_path_1.default.join(targetProjectDir, ".gitignore");
|
|
73
73
|
let gitignoreContent = "";
|
|
74
74
|
if (node_fs_1.default.existsSync(gitignorePath)) {
|
|
75
75
|
gitignoreContent = node_fs_1.default.readFileSync(gitignorePath, "utf8");
|
|
@@ -84,6 +84,32 @@ async function installAgent(overrideType) {
|
|
|
84
84
|
async function updateAgent() {
|
|
85
85
|
const targetDir = node_path_1.default.join(process.cwd(), ".agents");
|
|
86
86
|
if (!node_fs_1.default.existsSync(targetDir)) {
|
|
87
|
+
// Check if it's a monorepo
|
|
88
|
+
const apiAgentDir = node_path_1.default.join(process.cwd(), "api", ".agents");
|
|
89
|
+
const appAgentDir = node_path_1.default.join(process.cwd(), "app", ".agents");
|
|
90
|
+
if (node_fs_1.default.existsSync(apiAgentDir) || node_fs_1.default.existsSync(appAgentDir)) {
|
|
91
|
+
if (node_fs_1.default.existsSync(apiAgentDir)) {
|
|
92
|
+
console.log("Updating API agent...");
|
|
93
|
+
try {
|
|
94
|
+
(0, node_child_process_1.execSync)("git pull", { cwd: apiAgentDir, stdio: "inherit" });
|
|
95
|
+
console.log("API Agent updated successfully!");
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.error(`Failed to update API agent: ${e.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (node_fs_1.default.existsSync(appAgentDir)) {
|
|
102
|
+
console.log("Updating APP agent...");
|
|
103
|
+
try {
|
|
104
|
+
(0, node_child_process_1.execSync)("git pull", { cwd: appAgentDir, stdio: "inherit" });
|
|
105
|
+
console.log("APP Agent updated successfully!");
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.error(`Failed to update APP agent: ${e.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
87
113
|
throw new Error("No agent installed in this project. Run 'skalfa agent:install' first.");
|
|
88
114
|
}
|
|
89
115
|
const gitDir = node_path_1.default.join(targetDir, ".git");
|
|
@@ -34,7 +34,7 @@ class Questioner {
|
|
|
34
34
|
this.rl.close();
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
async function createApi(projectName) {
|
|
37
|
+
async function createApi(projectName, options) {
|
|
38
38
|
const cwd = process.cwd();
|
|
39
39
|
const target = node_path_1.default.resolve(cwd, projectName);
|
|
40
40
|
const packageName = node_path_1.default.basename(target);
|
|
@@ -42,24 +42,26 @@ async function createApi(projectName) {
|
|
|
42
42
|
if ((0, fs_1.exists)(target)) {
|
|
43
43
|
throw new Error(`Target directory already exists: ${target}`);
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
let
|
|
49
|
-
let
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
let hasRedis = options?.redis ?? false;
|
|
46
|
+
let hasQueue = options?.queue ?? false;
|
|
47
|
+
let hasCache = options?.cache ?? false;
|
|
48
|
+
let hasCron = options?.cron ?? false;
|
|
49
|
+
let hasDa = options?.da ?? false;
|
|
50
|
+
let hasSocket = options?.socket ?? false;
|
|
51
|
+
if (!options) {
|
|
52
|
+
// Ask interactive questions sequentially using a single readline interface
|
|
53
|
+
const q = new Questioner();
|
|
54
|
+
try {
|
|
55
|
+
hasRedis = (await q.ask("Do you need Redis? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
56
|
+
hasQueue = (await q.ask("Do you need Queue? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
57
|
+
hasCache = (await q.ask("Do you need Cache? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
58
|
+
hasCron = (await q.ask("Do you need Cron? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
59
|
+
hasDa = (await q.ask("Do you need Data Analytics? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
60
|
+
hasSocket = (await q.ask("Do you need Socket? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
q.close();
|
|
64
|
+
}
|
|
63
65
|
}
|
|
64
66
|
// Dependency relation: Queue or Cache requires Redis
|
|
65
67
|
const finalRedis = hasRedis || hasQueue || hasCache;
|
|
@@ -83,7 +85,7 @@ async function createApi(projectName) {
|
|
|
83
85
|
const tarballUrl = await (0, npm_1.fetchLatestTarballUrl)(templatePackageName);
|
|
84
86
|
// Create a temporary extraction directory inside parent folder of target
|
|
85
87
|
const parentDir = node_path_1.default.dirname(target);
|
|
86
|
-
const tempExtractDir = node_path_1.default.join(parentDir, `${
|
|
88
|
+
const tempExtractDir = node_path_1.default.join(parentDir, `${packageName}-temp-extract`);
|
|
87
89
|
if ((0, fs_1.exists)(tempExtractDir)) {
|
|
88
90
|
node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
|
|
89
91
|
}
|
|
@@ -121,6 +123,58 @@ async function createApi(projectName) {
|
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
renamePackage(target, packageName);
|
|
126
|
+
// Write template README.md file
|
|
127
|
+
const readmePath = node_path_1.default.join(target, "README.md");
|
|
128
|
+
const readmeContent = `# [Project Name] - Backend Service
|
|
129
|
+
|
|
130
|
+
## Overview
|
|
131
|
+
- **Description:** [Provide a short description of this project]
|
|
132
|
+
- **What it does:** [Briefly explain what this project does]
|
|
133
|
+
- **Why it exists:** [Explain why this project was created]
|
|
134
|
+
- **Main Goals:** [List the main goals of this service]
|
|
135
|
+
- **Target Users:** [Specify the target users/consumers of this service]
|
|
136
|
+
|
|
137
|
+
## Skalfa Stack
|
|
138
|
+
This backend service is built on top of **@skalfa/skalfa-api** with the following configured utilities:
|
|
139
|
+
- **Core:** \\\`@skalfa/skalfa-api-core\\\`
|
|
140
|
+
- **Database / ORM:** \\\`@skalfa/skalfa-orm\\\` (Knex)
|
|
141
|
+
- **Redis Connection:** \`${finalRedis ? "Enabled (@skalfa/skalfa-redis)" : "Disabled"}\`
|
|
142
|
+
- **Queue Worker:** \`${hasQueue ? "Enabled (@skalfa/skalfa-queue)" : "Disabled"}\`
|
|
143
|
+
- **Cache Manager:** \`${hasCache ? "Enabled (@skalfa/skalfa-cache)" : "Disabled"}\`
|
|
144
|
+
- **Cron Scheduler:** \`${hasCron ? "Enabled (@skalfa/skalfa-cron)" : "Disabled"}\`
|
|
145
|
+
- **Data Analytics:** \`${hasDa ? "Enabled (@skalfa/skalfa-da / ClickHouse)" : "Disabled"}\`
|
|
146
|
+
- **WebSocket Server:** \`${hasSocket ? "Enabled (@skalfa/skalfa-socket)" : "Disabled"}\`
|
|
147
|
+
|
|
148
|
+
## Features & Status
|
|
149
|
+
Here is the list of features and their development status:
|
|
150
|
+
|
|
151
|
+
| Feature | Description | Status |
|
|
152
|
+
| :--- | :--- | :---: |
|
|
153
|
+
| **Login** | API Login | \\\`[x] Completed\\\` |
|
|
154
|
+
|
|
155
|
+
## API Documentation
|
|
156
|
+
The API documentation is automatically generated and updated in the \\\`./docs/\\\` folder.
|
|
157
|
+
|
|
158
|
+
To generate or update the documentation, run:
|
|
159
|
+
\\\`bun skalfa generate:docs\\\`
|
|
160
|
+
|
|
161
|
+
## Development Setup
|
|
162
|
+
|
|
163
|
+
### Prerequisites
|
|
164
|
+
Ensure you have [Bun](https://bun.sh) or [Node.js](https://nodejs.org) installed.
|
|
165
|
+
|
|
166
|
+
### Commands
|
|
167
|
+
| Task | Bun | npm |
|
|
168
|
+
| :--- | :--- | :--- |
|
|
169
|
+
| **Install Dependencies** | \\\`bun install\\\` | \\\`npm install\\\` |
|
|
170
|
+
| **Start Dev Server** | \\\`bun run dev\\\` | \\\`npm run dev\\\` |
|
|
171
|
+
| **Build Production** | \\\`bun run build\\\` | \\\`npm run build\\\` |
|
|
172
|
+
| **Start Production** | \\\`bun run start\\\` | \\\`npm run start\\\` |
|
|
173
|
+
|
|
174
|
+
## Agent Instructions
|
|
175
|
+
If you are an AI coding agent assisting with this project, please make sure to read the workspace instructions and guidelines located in the [API Agent Rules](file:///.agents/AGENTS.md) or the root monorepo [Agent Rules](file:///../.agents/AGENTS.md) folder before making modifications.
|
|
176
|
+
`;
|
|
177
|
+
node_fs_1.default.writeFileSync(readmePath, readmeContent, "utf8");
|
|
124
178
|
// Rename .npmignore to .gitignore if it exists (npm renames .gitignore to .npmignore during pack/publish)
|
|
125
179
|
const npmignorePath = node_path_1.default.join(target, ".npmignore");
|
|
126
180
|
const gitignorePath = node_path_1.default.join(target, ".gitignore");
|
|
@@ -33,7 +33,7 @@ class Questioner {
|
|
|
33
33
|
this.rl.close();
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
async function createApp(projectName) {
|
|
36
|
+
async function createApp(projectName, options) {
|
|
37
37
|
const cwd = process.cwd();
|
|
38
38
|
const target = node_path_1.default.resolve(cwd, projectName);
|
|
39
39
|
const packageName = node_path_1.default.basename(target);
|
|
@@ -41,24 +41,26 @@ async function createApp(projectName) {
|
|
|
41
41
|
if ((0, fs_1.exists)(target)) {
|
|
42
42
|
throw new Error(`Target directory already exists: ${target}`);
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let
|
|
47
|
-
let
|
|
48
|
-
let
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
44
|
+
let hasIdb = options?.idb ?? false;
|
|
45
|
+
let hasSocket = options?.socket ?? false;
|
|
46
|
+
let hasDocument = options?.document ?? false;
|
|
47
|
+
let hasPwa = options?.pwa ?? false;
|
|
48
|
+
let hasTauriDesktop = options?.tauriDesktop ?? false;
|
|
49
|
+
let hasTauriMobile = options?.tauriMobile ?? false;
|
|
50
|
+
if (!options) {
|
|
51
|
+
// Ask interactive questions sequentially
|
|
52
|
+
const q = new Questioner();
|
|
53
|
+
try {
|
|
54
|
+
hasIdb = (await q.ask("Do you need IndexedDB (IDB)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
55
|
+
hasSocket = (await q.ask("Do you need Socket Client? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
56
|
+
hasDocument = (await q.ask("Do you need Document Export/Viewer (PDF/Excel)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
57
|
+
hasPwa = (await q.ask("Do you want to enable Progressive Web App (PWA)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
58
|
+
hasTauriDesktop = (await q.ask("Do you want to enable Tauri Desktop support (Windows/macOS/Linux)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
59
|
+
hasTauriMobile = (await q.ask("Do you want to enable Tauri Mobile support (Android/iOS)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
q.close();
|
|
63
|
+
}
|
|
62
64
|
}
|
|
63
65
|
const spinner = new spinner_1.Spinner("Preparing project...");
|
|
64
66
|
spinner.start();
|
|
@@ -79,7 +81,7 @@ async function createApp(projectName) {
|
|
|
79
81
|
spinner.update(`Fetching latest template info for ${templatePackageName}...`);
|
|
80
82
|
const tarballUrl = await (0, npm_1.fetchLatestTarballUrl)(templatePackageName);
|
|
81
83
|
const parentDir = node_path_1.default.dirname(target);
|
|
82
|
-
const tempExtractDir = node_path_1.default.join(parentDir, `${
|
|
84
|
+
const tempExtractDir = node_path_1.default.join(parentDir, `${packageName}-temp-extract`);
|
|
83
85
|
if ((0, fs_1.exists)(tempExtractDir)) {
|
|
84
86
|
node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
|
|
85
87
|
}
|
|
@@ -115,6 +117,50 @@ async function createApp(projectName) {
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
renamePackage(target, packageName);
|
|
120
|
+
// Write template README.md file
|
|
121
|
+
const readmePath = node_path_1.default.join(target, "README.md");
|
|
122
|
+
const readmeContent = `# [Project Name] - Frontend Application
|
|
123
|
+
|
|
124
|
+
## Overview
|
|
125
|
+
- **Description:** [Provide a short description of this project]
|
|
126
|
+
- **What it does:** [Briefly explain what this project does]
|
|
127
|
+
- **Why it exists:** [Explain why this project was created]
|
|
128
|
+
- **Main Goals:** [List the main goals of this application]
|
|
129
|
+
- **Target Users:** [Specify the target users of this application]
|
|
130
|
+
|
|
131
|
+
## Skalfa Stack
|
|
132
|
+
This frontend application is built on top of **@skalfa/skalfa-app** with the following configured utilities:
|
|
133
|
+
- **Core:** \\\`@skalfa/skalfa-app-core\\\`
|
|
134
|
+
- **IndexedDB Storage:** \`${hasIdb ? "Enabled (@skalfa/skalfa-idb)" : "Disabled"}\`
|
|
135
|
+
- **Socket Client:** \`${hasSocket ? "Enabled (@skalfa/skalfa-socket-client)" : "Disabled"}\`
|
|
136
|
+
- **Document Export:** \`${hasDocument ? "Enabled (@skalfa/skalfa-document)" : "Disabled"}\`
|
|
137
|
+
- **PWA Support:** \`${hasPwa ? "Enabled (@ducanh2912/next-pwa)" : "Disabled"}\`
|
|
138
|
+
- **Tauri Support:** \`${hasTauriDesktop || hasTauriMobile ? `Enabled (@tauri-apps/api) - ${[hasTauriDesktop ? "Desktop" : "", hasTauriMobile ? "Mobile" : ""].filter(Boolean).join("/")}` : "Disabled"}\`
|
|
139
|
+
|
|
140
|
+
## Features & Status
|
|
141
|
+
Here is the list of features and their development status:
|
|
142
|
+
|
|
143
|
+
| Feature | Description | Status |
|
|
144
|
+
| :--- | :--- | :---: |
|
|
145
|
+
| **Login** | Login pages | \\\`[x] Completed\\\` |
|
|
146
|
+
|
|
147
|
+
## Development Setup
|
|
148
|
+
|
|
149
|
+
### Prerequisites
|
|
150
|
+
Make sure you have [Bun](https://bun.sh) or [Node.js](https://nodejs.org) installed.
|
|
151
|
+
|
|
152
|
+
### Commands
|
|
153
|
+
| Task | Bun | npm |
|
|
154
|
+
| :--- | :--- | :--- |
|
|
155
|
+
| **Install Dependencies** | \\\`bun install\\\` | \\\`npm install\\\` |
|
|
156
|
+
| **Start Dev Server** | \\\`bun run dev\\\` | \\\`npm run dev\\\` |
|
|
157
|
+
| **Build Production** | \\\`bun run build\\\` | \\\`npm run build\\\` |
|
|
158
|
+
| **Start Production** | \\\`bun run start\\\` | \\\`npm run start\\\` |
|
|
159
|
+
|
|
160
|
+
## Agent Instructions
|
|
161
|
+
If you are an AI coding agent assisting with this project, please make sure to read the workspace instructions and guidelines located in the [App Agent Rules](file:///.agents/AGENTS.md) or the root monorepo [Agent Rules](file:///../.agents/AGENTS.md) folder before making modifications.
|
|
162
|
+
`;
|
|
163
|
+
node_fs_1.default.writeFileSync(readmePath, readmeContent, "utf8");
|
|
118
164
|
// Rename .npmignore to .gitignore if it exists
|
|
119
165
|
const npmignorePath = node_path_1.default.join(target, ".npmignore");
|
|
120
166
|
const gitignorePath = node_path_1.default.join(target, ".gitignore");
|
|
@@ -0,0 +1,189 @@
|
|
|
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.initProject = initProject;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
10
|
+
const create_api_1 = require("./create-api");
|
|
11
|
+
const create_app_1 = require("./create-app");
|
|
12
|
+
const agent_1 = require("./agent");
|
|
13
|
+
const fs_1 = require("../utils/fs");
|
|
14
|
+
class Questioner {
|
|
15
|
+
rl;
|
|
16
|
+
constructor() {
|
|
17
|
+
this.rl = node_readline_1.default.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stdout,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
ask(query, defaultValue = "") {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
this.rl.question(query, (answer) => {
|
|
25
|
+
resolve(answer.trim() || defaultValue);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
close() {
|
|
30
|
+
this.rl.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function initProject(projectName) {
|
|
34
|
+
const cwd = process.cwd();
|
|
35
|
+
const isCurrentDir = !projectName || projectName === ".";
|
|
36
|
+
const target = isCurrentDir ? cwd : node_path_1.default.resolve(cwd, projectName);
|
|
37
|
+
const resolvedProjectName = isCurrentDir ? (node_path_1.default.basename(cwd) || "skalfa-project") : projectName;
|
|
38
|
+
(0, fs_1.assertInsideDirectory)(cwd, target);
|
|
39
|
+
if (isCurrentDir) {
|
|
40
|
+
if ((0, fs_1.exists)(node_path_1.default.join(target, "api")) || (0, fs_1.exists)(node_path_1.default.join(target, "app")) || (0, fs_1.exists)(node_path_1.default.join(target, "package.json"))) {
|
|
41
|
+
throw new Error(`Current directory already contains conflicting files/folders (api, app, or package.json).`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
if ((0, fs_1.exists)(target)) {
|
|
46
|
+
throw new Error(`Target directory already exists: ${target}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Ask interactive questions sequentially
|
|
50
|
+
const q = new Questioner();
|
|
51
|
+
// API Options
|
|
52
|
+
let apiRedis = false;
|
|
53
|
+
let apiQueue = false;
|
|
54
|
+
let apiCache = false;
|
|
55
|
+
let apiCron = false;
|
|
56
|
+
let apiDa = false;
|
|
57
|
+
let apiSocket = false;
|
|
58
|
+
// APP Options
|
|
59
|
+
let appIdb = false;
|
|
60
|
+
let appSocket = false;
|
|
61
|
+
let appDocument = false;
|
|
62
|
+
let appPwa = false;
|
|
63
|
+
let appTauriDesktop = false;
|
|
64
|
+
let appTauriMobile = false;
|
|
65
|
+
try {
|
|
66
|
+
console.log("\n--- Configure API (Backend) ---");
|
|
67
|
+
apiRedis = (await q.ask("Do you need Redis? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
68
|
+
apiQueue = (await q.ask("Do you need Queue? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
69
|
+
apiCache = (await q.ask("Do you need Cache? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
70
|
+
apiCron = (await q.ask("Do you need Cron? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
71
|
+
apiDa = (await q.ask("Do you need Data Analytics? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
72
|
+
apiSocket = (await q.ask("Do you need Socket? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
73
|
+
console.log("\n--- Configure App (Frontend) ---");
|
|
74
|
+
appIdb = (await q.ask("Do you need IndexedDB (IDB)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
75
|
+
appSocket = (await q.ask("Do you need Socket Client? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
76
|
+
appDocument = (await q.ask("Do you need Document Export/Viewer (PDF/Excel)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
77
|
+
appPwa = (await q.ask("Do you want to enable Progressive Web App (PWA)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
78
|
+
appTauriDesktop = (await q.ask("Do you want to enable Tauri Desktop support (Windows/macOS/Linux)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
79
|
+
appTauriMobile = (await q.ask("Do you want to enable Tauri Mobile support (Android/iOS)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
q.close();
|
|
83
|
+
}
|
|
84
|
+
console.log(`\nInitializing Skalfa project in ${target}...`);
|
|
85
|
+
node_fs_1.default.mkdirSync(target, { recursive: true });
|
|
86
|
+
const apiDir = node_path_1.default.join(target, "api");
|
|
87
|
+
const appDir = node_path_1.default.join(target, "app");
|
|
88
|
+
console.log("\nCreating API...");
|
|
89
|
+
await (0, create_api_1.createApi)(apiDir, {
|
|
90
|
+
redis: apiRedis,
|
|
91
|
+
queue: apiQueue,
|
|
92
|
+
cache: apiCache,
|
|
93
|
+
cron: apiCron,
|
|
94
|
+
da: apiDa,
|
|
95
|
+
socket: apiSocket,
|
|
96
|
+
});
|
|
97
|
+
console.log("\nCreating App...");
|
|
98
|
+
await (0, create_app_1.createApp)(appDir, {
|
|
99
|
+
idb: appIdb,
|
|
100
|
+
socket: appSocket,
|
|
101
|
+
document: appDocument,
|
|
102
|
+
pwa: appPwa,
|
|
103
|
+
tauriDesktop: appTauriDesktop,
|
|
104
|
+
tauriMobile: appTauriMobile,
|
|
105
|
+
});
|
|
106
|
+
console.log("\nInstalling AI Agents...");
|
|
107
|
+
try {
|
|
108
|
+
await (0, agent_1.installAgent)("api", apiDir);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.error(`Failed to install API agent: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await (0, agent_1.installAgent)("app", appDir);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
console.error(`Failed to install App agent: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
console.log("\nConfiguring root files...");
|
|
120
|
+
// 1. Root package.json
|
|
121
|
+
const rootPackageJson = {
|
|
122
|
+
name: resolvedProjectName,
|
|
123
|
+
private: true,
|
|
124
|
+
workspaces: [
|
|
125
|
+
"api",
|
|
126
|
+
"app"
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(target, "package.json"), JSON.stringify(rootPackageJson, null, 2), "utf8");
|
|
130
|
+
// 2. Root .gitignore
|
|
131
|
+
const rootGitignore = `node_modules/
|
|
132
|
+
.agents/
|
|
133
|
+
`;
|
|
134
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(target, ".gitignore"), rootGitignore, "utf8");
|
|
135
|
+
// 3. Root README.md
|
|
136
|
+
const rootReadme = `# ${resolvedProjectName}
|
|
137
|
+
|
|
138
|
+
This is a Skalfa monorepo project containing both the backend (API) and the frontend (App).
|
|
139
|
+
|
|
140
|
+
## Structure
|
|
141
|
+
- \`api/\` - Backend service (Elysia, Knex, etc.)
|
|
142
|
+
- \`app/\` - Frontend application (Next.js)
|
|
143
|
+
|
|
144
|
+
## Getting Started
|
|
145
|
+
|
|
146
|
+
To install dependencies for both projects:
|
|
147
|
+
\`\`\`bash
|
|
148
|
+
bun install
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
To run the development servers:
|
|
152
|
+
- **API**: \`cd api && bun run dev\`
|
|
153
|
+
- **App**: \`cd app && bun run dev\`
|
|
154
|
+
`;
|
|
155
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(target, "README.md"), rootReadme, "utf8");
|
|
156
|
+
// 4. Root .agents folder
|
|
157
|
+
const agentsDir = node_path_1.default.join(target, ".agents");
|
|
158
|
+
node_fs_1.default.mkdirSync(agentsDir, { recursive: true });
|
|
159
|
+
// 5. Root .agents/skills.json
|
|
160
|
+
const skillsJson = {
|
|
161
|
+
entries: [
|
|
162
|
+
{ path: "../api/.agents" },
|
|
163
|
+
{ path: "../app/.agents" }
|
|
164
|
+
]
|
|
165
|
+
};
|
|
166
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(agentsDir, "skills.json"), JSON.stringify(skillsJson, null, 2), "utf8");
|
|
167
|
+
// 6. Root .agents/AGENTS.md
|
|
168
|
+
const agentsMd = `# Monorepo Rules
|
|
169
|
+
|
|
170
|
+
This is a combined Skalfa project containing both the backend (\`api\`) and frontend (\`app\`).
|
|
171
|
+
|
|
172
|
+
## Project Structure
|
|
173
|
+
- **Backend (API)**: Located in the [api](file:///api) directory.
|
|
174
|
+
- **Frontend (APP)**: Located in the [app](file:///app) directory.
|
|
175
|
+
|
|
176
|
+
## Instructions
|
|
177
|
+
1. When working on backend features, APIs, database migrations, or models, make changes inside the [api](file:///api) directory. Refer to [api/.agents](file:///api/.agents) for backend-specific guidelines.
|
|
178
|
+
2. When working on frontend features, pages, components, or state management, make changes inside the [app](file:///app) directory. Refer to [app/.agents](file:///app/.agents) for frontend-specific guidelines.
|
|
179
|
+
3. Always ensure that API contracts and communication between the frontend and backend are aligned.
|
|
180
|
+
`;
|
|
181
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(agentsDir, "AGENTS.md"), agentsMd, "utf8");
|
|
182
|
+
console.log(`\nSuccessfully initialized ${resolvedProjectName}!`);
|
|
183
|
+
if (isCurrentDir) {
|
|
184
|
+
console.log(`\nNext steps:\n bun install\n bun run --cwd api dev (or cd api && bun run dev)\n bun run --cwd app dev (or cd app && bun run dev)`);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.log(`\nNext steps:\n cd ${projectName}\n bun install\n bun run --cwd api dev (or cd api && bun run dev)\n bun run --cwd app dev (or cd app && bun run dev)`);
|
|
188
|
+
}
|
|
189
|
+
}
|
package/package.json
CHANGED