@skalfa/skalfa-cli 1.0.10 → 1.0.12

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.
@@ -62,7 +62,7 @@ program
62
62
  program
63
63
  .command("init")
64
64
  .description("Initialize a new Skalfa monorepo project containing both API and App.")
65
- .argument("<name>", "project folder name")
65
+ .argument("[name]", "project folder name")
66
66
  .action(async (name) => {
67
67
  await runCommand(() => (0, init_1.initProject)(name));
68
68
  });
@@ -89,10 +89,21 @@ program
89
89
  });
90
90
  program
91
91
  .command("pick")
92
- .description("Eject/copy a core utility from @skalfa/skalfa-api-core into your local utils folder for customization.")
93
- .argument("<utility>", `utility name: ${pick_1.UTILITIES.join(", ")}`)
94
- .action(async (utility) => {
95
- await runCommand(() => (0, pick_1.pickUtility)(utility));
92
+ .description("Eject/copy a core utility or component into your local project for customization.")
93
+ .argument("<name>", "utility or component name")
94
+ .argument("[newName]", "new name for the component (optional, component only)")
95
+ .action(async (name, newName) => {
96
+ await runCommand(() => {
97
+ if (pick_1.UTILITIES.includes(name)) {
98
+ if (newName) {
99
+ throw new Error("Renaming is only supported for components, not utilities.");
100
+ }
101
+ (0, pick_1.pickUtility)(name);
102
+ }
103
+ else {
104
+ (0, pick_1.pickComponent)(name, newName);
105
+ }
106
+ });
96
107
  });
97
108
  program
98
109
  .command("update")
@@ -43,6 +43,75 @@ async function addExtension(extensionName) {
43
43
  (0, installer_1.installPackage)(projectRoot, isDev ? "file:../skalfa-idb" : "@skalfa/skalfa-idb");
44
44
  addTsconfigPath(node_path_1.default.join(projectRoot, "tsconfig.json"), "@skalfa/skalfa-idb");
45
45
  addUtilExport(node_path_1.default.join(projectRoot, "utils", "index.ts"), "@skalfa/skalfa-idb");
46
+ // Scaffold IDBProvider
47
+ console.log("Scaffolding IDBProvider...");
48
+ const providerDir = node_path_1.default.join(projectRoot, "components", "base.components", "wrap");
49
+ if (!node_fs_1.default.existsSync(providerDir)) {
50
+ node_fs_1.default.mkdirSync(providerDir, { recursive: true });
51
+ }
52
+ const providerPath = node_path_1.default.join(providerDir, "IDBProvider.tsx");
53
+ const providerContent = `"use client"
54
+
55
+ import { useEffect } from "react"
56
+ import { idb } from "@skalfa/skalfa-idb"
57
+ import { AppSchema } from "@schema"
58
+ import { registry } from "@utils"
59
+
60
+ export function IDBProvider({ children }: { children: React.ReactNode }) {
61
+ useEffect(() => {
62
+ idb.setDefaultSchema(AppSchema);
63
+ registry.register("idb", idb);
64
+ }, []);
65
+
66
+ return <>{children}</>
67
+ }
68
+ `;
69
+ node_fs_1.default.writeFileSync(providerPath, providerContent, "utf8");
70
+ console.log(`Created: ${providerPath}`);
71
+ // Scaffold app.schema.ts
72
+ console.log("Scaffolding AppSchema...");
73
+ const schemaDir = node_path_1.default.join(projectRoot, "schema", "idb");
74
+ if (!node_fs_1.default.existsSync(schemaDir)) {
75
+ node_fs_1.default.mkdirSync(schemaDir, { recursive: true });
76
+ }
77
+ const schemaPath = node_path_1.default.join(schemaDir, "app.schema.ts");
78
+ const schemaContent = `import { DBSchema } from "@skalfa/skalfa-idb"
79
+
80
+ const name = String(process.env.NEXT_PUBLIC_APP_NAME || "").toLowerCase().trim().replace(/[^\\w\\s-]/g, "").replace(/[\\s_-]+/g, "-").replace(/^-+|-+$/g, "") + ".idb-app";
81
+
82
+ export const AppSchema: DBSchema = {
83
+ name: name,
84
+ version: 1,
85
+ stores: {}
86
+ }
87
+ `;
88
+ node_fs_1.default.writeFileSync(schemaPath, schemaContent, "utf8");
89
+ console.log(`Created: ${schemaPath}`);
90
+ // Ensure schema/index.ts exists so the @schema alias works
91
+ const schemaIndexPath = node_path_1.default.join(projectRoot, "schema", "index.ts");
92
+ if (!node_fs_1.default.existsSync(schemaIndexPath)) {
93
+ node_fs_1.default.writeFileSync(schemaIndexPath, `export * from "./idb/app.schema";\n`, "utf8");
94
+ console.log(`Created: ${schemaIndexPath}`);
95
+ }
96
+ // Update app/layout.tsx
97
+ console.log("Updating app/layout.tsx...");
98
+ const layoutPath = node_path_1.default.join(projectRoot, "app", "layout.tsx");
99
+ if (node_fs_1.default.existsSync(layoutPath)) {
100
+ let layoutContent = node_fs_1.default.readFileSync(layoutPath, "utf8");
101
+ // 1. Add IDBProvider to import
102
+ if (layoutContent.includes('import { ShortcutProvider } from "@components";')) {
103
+ layoutContent = layoutContent.replace('import { ShortcutProvider } from "@components";', 'import { IDBProvider, ShortcutProvider } from "@components";');
104
+ }
105
+ else if (layoutContent.includes('import { ShortcutProvider } from "@components"')) {
106
+ layoutContent = layoutContent.replace('import { ShortcutProvider } from "@components"', 'import { IDBProvider, ShortcutProvider } from "@components"');
107
+ }
108
+ // 2. Wrap {children} with <IDBProvider>
109
+ if (layoutContent.includes("{children}") && !layoutContent.includes("<IDBProvider>")) {
110
+ layoutContent = layoutContent.replace(/\{\s*children\s*\}/, `<IDBProvider>\n {children}\n </IDBProvider>`);
111
+ }
112
+ node_fs_1.default.writeFileSync(layoutPath, layoutContent, "utf8");
113
+ console.log(`Updated: ${layoutPath}`);
114
+ }
46
115
  }
47
116
  else if (extensionName === "socket") {
48
117
  console.log("Installing Skalfa Socket.io client extension...");
@@ -152,6 +152,12 @@ Here is the list of features and their development status:
152
152
  | :--- | :--- | :---: |
153
153
  | **Login** | API Login | \\\`[x] Completed\\\` |
154
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
+
155
161
  ## Development Setup
156
162
 
157
163
  ### Prerequisites
@@ -32,10 +32,19 @@ class Questioner {
32
32
  }
33
33
  async function initProject(projectName) {
34
34
  const cwd = process.cwd();
35
- const target = node_path_1.default.resolve(cwd, projectName);
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;
36
38
  (0, fs_1.assertInsideDirectory)(cwd, target);
37
- if ((0, fs_1.exists)(target)) {
38
- throw new Error(`Target directory already exists: ${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
+ }
39
48
  }
40
49
  // Ask interactive questions sequentially
41
50
  const q = new Questioner();
@@ -110,7 +119,7 @@ async function initProject(projectName) {
110
119
  console.log("\nConfiguring root files...");
111
120
  // 1. Root package.json
112
121
  const rootPackageJson = {
113
- name: projectName,
122
+ name: resolvedProjectName,
114
123
  private: true,
115
124
  workspaces: [
116
125
  "api",
@@ -124,7 +133,7 @@ async function initProject(projectName) {
124
133
  `;
125
134
  node_fs_1.default.writeFileSync(node_path_1.default.join(target, ".gitignore"), rootGitignore, "utf8");
126
135
  // 3. Root README.md
127
- const rootReadme = `# ${projectName}
136
+ const rootReadme = `# ${resolvedProjectName}
128
137
 
129
138
  This is a Skalfa monorepo project containing both the backend (API) and the frontend (App).
130
139
 
@@ -170,6 +179,11 @@ This is a combined Skalfa project containing both the backend (\`api\`) and fron
170
179
  3. Always ensure that API contracts and communication between the frontend and backend are aligned.
171
180
  `;
172
181
  node_fs_1.default.writeFileSync(node_path_1.default.join(agentsDir, "AGENTS.md"), agentsMd, "utf8");
173
- console.log(`\nSuccessfully initialized ${projectName}!`);
174
- 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)`);
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
+ }
175
189
  }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.UTILITIES = exports.UTILITY_EXPORTS = void 0;
7
7
  exports.pickUtility = pickUtility;
8
+ exports.pickComponent = pickComponent;
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  const fs_1 = require("../utils/fs");
@@ -38,7 +39,6 @@ function pickUtility(utilityName) {
38
39
  if (!(0, fs_1.exists)(utilsDir) || !(0, fs_1.exists)(indexPath)) {
39
40
  throw new Error("Folder utils or utils/index.ts not found. Make sure you are at the project root.");
40
41
  }
41
- // 1. Tentukan path source dari node_modules dan target di lokal proyek
42
42
  const corePackagePath = node_path_1.default.join(projectRoot, "node_modules", "@skalfa", "skalfa-api-core");
43
43
  const sourceDir = node_path_1.default.join(corePackagePath, "src", utilityName);
44
44
  const targetDir = node_path_1.default.join(utilsDir, utilityName);
@@ -48,7 +48,6 @@ function pickUtility(utilityName) {
48
48
  if ((0, fs_1.exists)(targetDir)) {
49
49
  throw new Error(`Utility folder "${utilityName}" is already present in your local utils folder.`);
50
50
  }
51
- // 2. Salin folder dari node_modules ke lokal proyek secara rekursif
52
51
  console.log(`Copying ${utilityName} folder from @skalfa/skalfa-api-core to utils/ ...`);
53
52
  node_fs_1.default.cpSync(sourceDir, targetDir, { recursive: true });
54
53
  const filesToDelete = ["CONTRIBUTING.md", "LICENSE"];
@@ -59,7 +58,6 @@ function pickUtility(utilityName) {
59
58
  }
60
59
  }
61
60
  console.log(`✓ Copied ${utilityName} folder`);
62
- // 3. Perbarui utils/index.ts untuk mereferensikan folder lokal
63
61
  console.log("Updating utils/index.ts with explicit local export override ...");
64
62
  let indexContent = node_fs_1.default.readFileSync(indexPath, "utf8").trim();
65
63
  const localExportLine = `export { ${utilitySymbols.join(", ")} } from "./${utilityName}";`;
@@ -73,3 +71,133 @@ function pickUtility(utilityName) {
73
71
  }
74
72
  console.log(`\nSuccess! You can now customize files under: utils/${utilityName}/`);
75
73
  }
74
+ function findComponentFolder(srcDir, componentName) {
75
+ if (!node_fs_1.default.existsSync(srcDir))
76
+ return null;
77
+ const folders = node_fs_1.default.readdirSync(srcDir);
78
+ for (const folder of folders) {
79
+ const folderPath = node_path_1.default.join(srcDir, folder);
80
+ if (!node_fs_1.default.statSync(folderPath).isDirectory())
81
+ continue;
82
+ const files = node_fs_1.default.readdirSync(folderPath);
83
+ for (const file of files) {
84
+ if (!file.endsWith(".tsx") && !file.endsWith(".ts"))
85
+ continue;
86
+ const filePath = node_path_1.default.join(folderPath, file);
87
+ const content = node_fs_1.default.readFileSync(filePath, "utf8");
88
+ // Match the component name or the component name with "Component" suffix (e.g. Button or ButtonComponent)
89
+ const regex = new RegExp(`export\\s+(async\\s+)?(function|const|class|type|interface)\\s+\\b(${componentName}|${componentName}Component)\\b`);
90
+ if (regex.test(content)) {
91
+ return { folderName: folder, fileName: file };
92
+ }
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ function pickComponent(componentName, newName) {
98
+ const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
99
+ if (!projectRoot) {
100
+ throw new Error("No package.json found. Run this command inside a Skalfa project.");
101
+ }
102
+ // 1. Locate the component source in @skalfa/skalfa-component
103
+ let componentSrcDir = node_path_1.default.join(projectRoot, "node_modules", "@skalfa", "skalfa-component", "src");
104
+ if (!node_fs_1.default.existsSync(componentSrcDir)) {
105
+ componentSrcDir = node_path_1.default.resolve(projectRoot, "..", "skalfa-component", "src");
106
+ }
107
+ // Clean the componentName if it has "Component" suffix for searching
108
+ const cleanComponentName = componentName.endsWith("Component") ? componentName.replace(/Component$/, "") : componentName;
109
+ const match = findComponentFolder(componentSrcDir, cleanComponentName);
110
+ if (!match) {
111
+ throw new Error(`Component "${componentName}" not found in @skalfa/skalfa-component.`);
112
+ }
113
+ const { folderName, fileName } = match;
114
+ const sourceFolder = node_path_1.default.join(componentSrcDir, folderName);
115
+ const sourceFilePath = node_path_1.default.join(sourceFolder, fileName);
116
+ // Extract base name of the component file (e.g. "Button" from "Button.component.tsx")
117
+ const fileBaseName = fileName.replace(/\.(component)?\.(tsx|ts)$/, "");
118
+ // Find all exported symbols in the file that start with or match the file base name
119
+ const sourceContent = node_fs_1.default.readFileSync(sourceFilePath, "utf8");
120
+ const exportRegex = new RegExp(`export\\s+(async\\s+)?(function|const|class|type|interface)\\s+\\b(${fileBaseName}\\w*)\\b`, "g");
121
+ const exportedTypes = [];
122
+ const exportedValues = [];
123
+ let exportMatch;
124
+ while ((exportMatch = exportRegex.exec(sourceContent)) !== null) {
125
+ const keyword = exportMatch[2];
126
+ const symbol = exportMatch[3];
127
+ if (keyword === "type" || keyword === "interface") {
128
+ exportedTypes.push(symbol);
129
+ }
130
+ else {
131
+ exportedValues.push(symbol);
132
+ }
133
+ }
134
+ const componentsDir = node_path_1.default.join(projectRoot, "components");
135
+ const indexPath = node_path_1.default.join(componentsDir, "index.ts");
136
+ if (!node_fs_1.default.existsSync(componentsDir)) {
137
+ node_fs_1.default.mkdirSync(componentsDir, { recursive: true });
138
+ }
139
+ // Ensure index.ts exists
140
+ if (!node_fs_1.default.existsSync(indexPath)) {
141
+ node_fs_1.default.writeFileSync(indexPath, `export * from "@skalfa/skalfa-component";\n`, "utf8");
142
+ }
143
+ // Determine target names
144
+ const targetFolderName = newName ? newName.charAt(0).toLowerCase() + newName.slice(1) : folderName;
145
+ const targetFolder = node_path_1.default.join(componentsDir, targetFolderName);
146
+ if (node_fs_1.default.existsSync(targetFolder)) {
147
+ throw new Error(`Component folder "${targetFolderName}" already exists in components/.`);
148
+ }
149
+ console.log(`Copying component "${cleanComponentName}" from ${sourceFolder} to components/${targetFolderName} ...`);
150
+ node_fs_1.default.cpSync(sourceFolder, targetFolder, { recursive: true });
151
+ let finalFileBaseName = fileBaseName;
152
+ let finalExportedTypes = [...exportedTypes];
153
+ let finalExportedValues = [...exportedValues];
154
+ // If renaming is requested, perform renaming of files and contents
155
+ if (newName) {
156
+ const cleanNewName = newName.endsWith("Component") ? newName.replace(/Component$/, "") : newName;
157
+ finalFileBaseName = cleanNewName;
158
+ finalExportedTypes = exportedTypes.map(sym => sym.replace(new RegExp(fileBaseName, "g"), cleanNewName));
159
+ finalExportedValues = exportedValues.map(sym => sym.replace(new RegExp(fileBaseName, "g"), cleanNewName));
160
+ const files = node_fs_1.default.readdirSync(targetFolder);
161
+ for (const file of files) {
162
+ const filePath = node_path_1.default.join(targetFolder, file);
163
+ if (node_fs_1.default.statSync(filePath).isDirectory())
164
+ continue;
165
+ // Read file content and replace component names
166
+ let content = node_fs_1.default.readFileSync(filePath, "utf8");
167
+ // Replace fileBaseName (e.g. Button) with cleanNewName (e.g. MyButton)
168
+ const nameRegex = new RegExp(fileBaseName, "g");
169
+ content = content.replace(nameRegex, cleanNewName);
170
+ node_fs_1.default.writeFileSync(filePath, content, "utf8");
171
+ // Rename the file if it contains the original file base name
172
+ if (file.includes(fileBaseName)) {
173
+ const newFile = file.replace(fileBaseName, cleanNewName);
174
+ node_fs_1.default.renameSync(filePath, node_path_1.default.join(targetFolder, newFile));
175
+ }
176
+ }
177
+ }
178
+ // Update components/index.ts to export the local component explicitly
179
+ let indexContent = node_fs_1.default.readFileSync(indexPath, "utf8");
180
+ // Find the exact filename of the main component file inside the target folder
181
+ const targetFiles = node_fs_1.default.readdirSync(targetFolder);
182
+ const componentFile = targetFiles.find(f => f.includes(finalFileBaseName) && (f.endsWith(".tsx") || f.endsWith(".ts")));
183
+ if (componentFile) {
184
+ const componentBaseFile = componentFile.replace(/\.(tsx|ts)$/, "");
185
+ const valueExport = finalExportedValues.length > 0
186
+ ? `export { ${finalExportedValues.join(", ")} } from "./${targetFolderName}/${componentBaseFile}";`
187
+ : "";
188
+ const typeExport = finalExportedTypes.length > 0
189
+ ? `export type { ${finalExportedTypes.join(", ")} } from "./${targetFolderName}/${componentBaseFile}";`
190
+ : "";
191
+ const localExportLine = [valueExport, typeExport].filter(Boolean).join("\n");
192
+ if (!indexContent.includes(`./${targetFolderName}/`)) {
193
+ indexContent = indexContent.trim() + `\n${localExportLine}\n`;
194
+ node_fs_1.default.writeFileSync(indexPath, indexContent, "utf8");
195
+ console.log(`✓ Updated components/index.ts with local export for: ${[...finalExportedValues, ...finalExportedTypes].join(", ")}`);
196
+ }
197
+ else {
198
+ console.log(`⚠️ Info: Local export for "${targetFolderName}" already exists in components/index.ts`);
199
+ }
200
+ }
201
+ const finalSymbols = [...finalExportedValues, ...finalExportedTypes];
202
+ console.log(`\nSuccess! Component "${finalSymbols.find((s) => !s.endsWith("Props")) || finalSymbols[0]}" is now available locally at components/${targetFolderName}/`);
203
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skalfa/skalfa-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
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": {