@skalfa/skalfa-cli 1.0.6 → 1.0.8

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.
@@ -12,10 +12,12 @@ 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
14
  const pick_1 = require("../commands/pick");
15
+ const update_1 = require("../commands/update");
16
+ const agent_1 = require("../commands/agent");
15
17
  const fs_1 = require("../utils/fs");
16
18
  // Dynamic routing / forwarding logic
17
19
  const args = process.argv.slice(2);
18
- const knownCommands = ["create-api", "create-app", "add", "pick"];
20
+ const knownCommands = ["create:api", "create:app", "add", "pick", "update", "agent:install", "agent:update"];
19
21
  if (args.length > 0 && !knownCommands.includes(args[0]) && !["-h", "--help", "-v", "--version", "help"].includes(args[0])) {
20
22
  const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
21
23
  if (projectRoot) {
@@ -32,20 +34,39 @@ if (args.length > 0 && !knownCommands.includes(args[0]) && !["-h", "--help", "-v
32
34
  }
33
35
  }
34
36
  }
37
+ const packageJsonPath = node_path_1.default.join(__dirname, "..", "..", "package.json");
38
+ let version = "1.0.0";
39
+ try {
40
+ const packageJson = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
41
+ version = packageJson.version;
42
+ }
43
+ catch (e) {
44
+ // fallback if package.json is not found
45
+ }
35
46
  const program = new commander_1.Command();
47
+ const banner = `
48
+ ############ WELCOME TO ###############
49
+ _____ _____ _____ __ _____ _____
50
+ | __| | | _ | | | __| _ |
51
+ |__ | -| | |__| __| |
52
+ |_____|__|__|__|__|_____|__| |__|__|
53
+
54
+ #######################################
55
+ `;
36
56
  program
37
57
  .name("skalfa")
38
- .description("Create Skalfa API projects and install optional extensions.")
39
- .version("0.1.0");
58
+ .description("Start building with skalfa ecosystem.")
59
+ .version(version)
60
+ .addHelpText("before", banner);
40
61
  program
41
- .command("create-api")
62
+ .command("create:api")
42
63
  .description("Create a new Skalfa API project.")
43
64
  .argument("<name>", "project folder and package name")
44
65
  .action(async (name) => {
45
66
  await runCommand(() => (0, create_api_1.createApi)(name));
46
67
  });
47
68
  program
48
- .command("create-app")
69
+ .command("create:app")
49
70
  .description("Create a new Skalfa App Next.js project.")
50
71
  .argument("<name>", "project folder and package name")
51
72
  .action(async (name) => {
@@ -65,6 +86,25 @@ program
65
86
  .action(async (utility) => {
66
87
  await runCommand(() => (0, pick_1.pickUtility)(utility));
67
88
  });
89
+ program
90
+ .command("update")
91
+ .description("Update skalfa-cli to the latest version.")
92
+ .action(async () => {
93
+ await runCommand(() => (0, update_1.updateCli)());
94
+ });
95
+ program
96
+ .command("agent:install")
97
+ .description("Install the corresponding AI coding agent (agent-api or agent-app) into the current project.")
98
+ .option("-t, --type <type>", "Override project type detection: api or app")
99
+ .action(async (options) => {
100
+ await runCommand(() => (0, agent_1.installAgent)(options.type));
101
+ });
102
+ program
103
+ .command("agent:update")
104
+ .description("Update the installed AI coding agent to the latest version.")
105
+ .action(async () => {
106
+ await runCommand(() => (0, agent_1.updateAgent)());
107
+ });
68
108
  program.parse(process.argv);
69
109
  async function runCommand(command) {
70
110
  try {
@@ -0,0 +1,101 @@
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.installAgent = installAgent;
7
+ exports.updateAgent = updateAgent;
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
+ async function installAgent(overrideType) {
12
+ let type = overrideType;
13
+ // 1. Auto-detect project type if not overridden
14
+ if (!type) {
15
+ const pkgPath = node_path_1.default.join(process.cwd(), "package.json");
16
+ if (!node_fs_1.default.existsSync(pkgPath)) {
17
+ throw new Error("package.json not found. Please run this command in your project root.");
18
+ }
19
+ try {
20
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
21
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
22
+ const isApi = !!(deps["elysia"] || deps["knex"] || deps["@skalfa/skalfa-api-core"]);
23
+ const isApp = !!(deps["react"] || deps["next"] || deps["vite"] || deps["skalfa-app-core"]);
24
+ if (isApi) {
25
+ type = "api";
26
+ }
27
+ else if (isApp) {
28
+ type = "app";
29
+ }
30
+ else {
31
+ throw new Error("Could not auto-detect project type. Please specify using --type api or --type app.");
32
+ }
33
+ }
34
+ catch (e) {
35
+ throw new Error(`Failed to auto-detect project type: ${e.message}`);
36
+ }
37
+ }
38
+ if (type !== "api" && type !== "app") {
39
+ throw new Error("Invalid agent type. Must be 'api' or 'app'.");
40
+ }
41
+ const targetDir = node_path_1.default.join(process.cwd(), ".agents");
42
+ if (node_fs_1.default.existsSync(targetDir)) {
43
+ throw new Error(".agents folder already exists. Run 'skalfa agent:update' to pull the latest changes.");
44
+ }
45
+ const repoUrl = `https://github.com/skalfa-framework/agent-${type}.git`;
46
+ console.log(`Cloning agent-${type} from ${repoUrl}...`);
47
+ try {
48
+ // Clone repo directly into .agents/
49
+ (0, node_child_process_1.execSync)(`git clone ${repoUrl} "${targetDir}"`, { stdio: "inherit" });
50
+ }
51
+ catch (e) {
52
+ throw new Error(`Failed to clone agent repository: ${e.message}`);
53
+ }
54
+ // 2. Copy template files to records/
55
+ const templatesDir = node_path_1.default.join(targetDir, "templates");
56
+ const recordsDir = node_path_1.default.join(targetDir, "records");
57
+ if (node_fs_1.default.existsSync(templatesDir)) {
58
+ if (!node_fs_1.default.existsSync(recordsDir)) {
59
+ node_fs_1.default.mkdirSync(recordsDir, { recursive: true });
60
+ }
61
+ const files = node_fs_1.default.readdirSync(templatesDir);
62
+ for (const file of files) {
63
+ const srcFile = node_path_1.default.join(templatesDir, file);
64
+ const destFile = node_path_1.default.join(recordsDir, file);
65
+ if (!node_fs_1.default.existsSync(destFile)) {
66
+ node_fs_1.default.copyFileSync(srcFile, destFile);
67
+ console.log(`Created initial record: ${file}`);
68
+ }
69
+ }
70
+ }
71
+ // 3. Add /.agents/ to project's .gitignore
72
+ const gitignorePath = node_path_1.default.join(process.cwd(), ".gitignore");
73
+ let gitignoreContent = "";
74
+ if (node_fs_1.default.existsSync(gitignorePath)) {
75
+ gitignoreContent = node_fs_1.default.readFileSync(gitignorePath, "utf8");
76
+ }
77
+ if (!gitignoreContent.includes("/.agents/")) {
78
+ const separator = gitignoreContent.endsWith("\n") || gitignoreContent === "" ? "" : "\n";
79
+ node_fs_1.default.appendFileSync(gitignorePath, `${separator}/.agents/\n`);
80
+ console.log("Added /.agents/ to .gitignore");
81
+ }
82
+ console.log(`\nSuccessfully installed agent-${type}!`);
83
+ }
84
+ async function updateAgent() {
85
+ const targetDir = node_path_1.default.join(process.cwd(), ".agents");
86
+ if (!node_fs_1.default.existsSync(targetDir)) {
87
+ throw new Error("No agent installed in this project. Run 'skalfa agent:install' first.");
88
+ }
89
+ const gitDir = node_path_1.default.join(targetDir, ".git");
90
+ if (!node_fs_1.default.existsSync(gitDir)) {
91
+ throw new Error(".agents folder is not a Git repository.");
92
+ }
93
+ console.log("Updating agent to the latest version...");
94
+ try {
95
+ (0, node_child_process_1.execSync)("git pull", { cwd: targetDir, stdio: "inherit" });
96
+ console.log("Agent updated successfully!");
97
+ }
98
+ catch (e) {
99
+ throw new Error(`Failed to update agent: ${e.message}`);
100
+ }
101
+ }
@@ -11,6 +11,7 @@ const node_child_process_1 = require("node:child_process");
11
11
  const node_readline_1 = __importDefault(require("node:readline"));
12
12
  const npm_1 = require("../utils/npm");
13
13
  const installer_1 = require("../utils/installer");
14
+ const spinner_1 = require("../utils/spinner");
14
15
  const fs_1 = require("../utils/fs");
15
16
  const copier_1 = require("../utils/copier");
16
17
  const TEMPLATE_ENV_KEY = "SKALFA_API_TEMPLATE";
@@ -22,10 +23,10 @@ class Questioner {
22
23
  output: process.stdout,
23
24
  });
24
25
  }
25
- ask(query) {
26
+ ask(query, defaultValue = "") {
26
27
  return new Promise((resolve) => {
27
28
  this.rl.question(query, (answer) => {
28
- resolve(answer);
29
+ resolve(answer.trim() || defaultValue);
29
30
  });
30
31
  });
31
32
  }
@@ -50,85 +51,100 @@ async function createApi(projectName) {
50
51
  let hasDa = false;
51
52
  let hasSocket = false;
52
53
  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");
54
+ hasRedis = (await q.ask("Do you need Redis? (y/N): ", "No")).toLowerCase().startsWith("y");
55
+ hasQueue = (await q.ask("Do you need Queue? (y/N): ", "No")).toLowerCase().startsWith("y");
56
+ hasCache = (await q.ask("Do you need Cache? (y/N): ", "No")).toLowerCase().startsWith("y");
57
+ hasCron = (await q.ask("Do you need Cron? (y/N): ", "No")).toLowerCase().startsWith("y");
58
+ hasDa = (await q.ask("Do you need Data Analytics? (y/N): ", "No")).toLowerCase().startsWith("y");
59
+ hasSocket = (await q.ask("Do you need Socket? (y/N): ", "No")).toLowerCase().startsWith("y");
59
60
  }
60
61
  finally {
61
62
  q.close();
62
63
  }
63
64
  // Dependency relation: Queue or Cache requires Redis
64
65
  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}`);
66
+ const spinner = new spinner_1.Spinner("Preparing project...");
67
+ spinner.start();
68
+ try {
69
+ const envTemplateSource = process.env[TEMPLATE_ENV_KEY];
70
+ if (envTemplateSource) {
71
+ // Local copy mode (e.g. for development / override)
72
+ const templateSource = node_path_1.default.resolve(envTemplateSource);
73
+ spinner.update(`Copying template from ${templateSource}...`);
74
+ if (!(0, fs_1.exists)(templateSource)) {
75
+ throw new Error(`Template source override not found: ${templateSource}`);
76
+ }
77
+ (0, copier_1.copyTemplate)(templateSource, target);
72
78
  }
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)) {
79
+ else {
80
+ // Dynamic download from npm registry
81
+ const templatePackageName = "@skalfa/skalfa-api";
82
+ spinner.update(`Fetching latest template info for ${templatePackageName}...`);
83
+ const tarballUrl = await (0, npm_1.fetchLatestTarballUrl)(templatePackageName);
84
+ // Create a temporary extraction directory inside parent folder of target
85
+ const parentDir = node_path_1.default.dirname(target);
86
+ const tempExtractDir = node_path_1.default.join(parentDir, `${projectName}-temp-extract`);
87
+ if ((0, fs_1.exists)(tempExtractDir)) {
88
+ node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
89
+ }
90
+ node_fs_1.default.mkdirSync(tempExtractDir, { recursive: true });
91
+ const tarballPath = node_path_1.default.join(tempExtractDir, "template.tgz");
92
+ spinner.update("Downloading template tarball...");
93
+ await (0, npm_1.downloadTarball)(tarballUrl, tarballPath);
94
+ spinner.update("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
+ const packageDir = node_path_1.default.join(tempExtractDir, "package");
103
+ if (!(0, fs_1.exists)(packageDir)) {
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
+ // Rename/move extracted folder to target path
108
+ node_fs_1.default.renameSync(packageDir, target);
109
+ // Cleanup temp extract folder
84
110
  node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
85
111
  }
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" });
112
+ spinner.update("Customizing project files...");
113
+ // Cleanup git and github directories if present and rename package
114
+ (0, fs_1.removeDirectory)(node_path_1.default.join(target, ".git"));
115
+ (0, fs_1.removeDirectory)(node_path_1.default.join(target, ".github"));
116
+ const filesToDelete = ["CONTRIBUTING.md", "LICENSE"];
117
+ for (const file of filesToDelete) {
118
+ const filePath = node_path_1.default.join(target, file);
119
+ if (node_fs_1.default.existsSync(filePath)) {
120
+ node_fs_1.default.unlinkSync(filePath);
121
+ }
93
122
  }
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}`);
123
+ renamePackage(target, packageName);
124
+ // Rename .npmignore to .gitignore if it exists (npm renames .gitignore to .npmignore during pack/publish)
125
+ const npmignorePath = node_path_1.default.join(target, ".npmignore");
126
+ const gitignorePath = node_path_1.default.join(target, ".gitignore");
127
+ if (node_fs_1.default.existsSync(npmignorePath) && !node_fs_1.default.existsSync(gitignorePath)) {
128
+ node_fs_1.default.renameSync(npmignorePath, gitignorePath);
97
129
  }
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 });
130
+ // Customize project with selected options
131
+ customizeProject(target, {
132
+ redis: finalRedis,
133
+ queue: hasQueue,
134
+ cache: hasCache,
135
+ cron: hasCron,
136
+ da: hasDa,
137
+ socket: hasSocket
138
+ });
139
+ spinner.update("Installing dependencies (this may take a moment)...");
140
+ await (0, installer_1.installDependenciesAsync)(target);
141
+ spinner.stop(true, "Skalfa API project is ready.");
142
+ console.log(`\nNext steps:\n cd ${projectName}\n bun run dev`);
107
143
  }
108
- // Cleanup git and github directories if present and rename package
109
- (0, fs_1.removeDirectory)(node_path_1.default.join(target, ".git"));
110
- (0, fs_1.removeDirectory)(node_path_1.default.join(target, ".github"));
111
- renamePackage(target, packageName);
112
- // Rename .npmignore to .gitignore if it exists (npm renames .gitignore to .npmignore during pack/publish)
113
- const npmignorePath = node_path_1.default.join(target, ".npmignore");
114
- const gitignorePath = node_path_1.default.join(target, ".gitignore");
115
- if (node_fs_1.default.existsSync(npmignorePath) && !node_fs_1.default.existsSync(gitignorePath)) {
116
- node_fs_1.default.renameSync(npmignorePath, gitignorePath);
144
+ catch (error) {
145
+ spinner.stop(false, `Failed to prepare project: ${error.message}`);
146
+ throw error;
117
147
  }
118
- // Customize project with selected options
119
- customizeProject(target, {
120
- redis: finalRedis,
121
- queue: hasQueue,
122
- cache: hasCache,
123
- cron: hasCron,
124
- da: hasDa,
125
- socket: hasSocket
126
- });
127
- console.log("Installing dependencies...");
128
- (0, installer_1.installDependencies)(target);
129
- console.log("");
130
- console.log("✓ Skalfa API project is ready.");
131
- console.log(`Next steps:\n cd ${projectName}\n bun run dev`);
132
148
  }
133
149
  function renamePackage(target, packageName) {
134
150
  const packageJsonPath = node_path_1.default.join(target, "package.json");
@@ -10,6 +10,7 @@ const node_child_process_1 = require("node:child_process");
10
10
  const node_readline_1 = __importDefault(require("node:readline"));
11
11
  const npm_1 = require("../utils/npm");
12
12
  const installer_1 = require("../utils/installer");
13
+ const spinner_1 = require("../utils/spinner");
13
14
  const fs_1 = require("../utils/fs");
14
15
  const copier_1 = require("../utils/copier");
15
16
  const TEMPLATE_ENV_KEY = "SKALFA_APP_TEMPLATE";
@@ -21,10 +22,10 @@ class Questioner {
21
22
  output: process.stdout,
22
23
  });
23
24
  }
24
- ask(query) {
25
+ ask(query, defaultValue = "") {
25
26
  return new Promise((resolve) => {
26
27
  this.rl.question(query, (answer) => {
27
- resolve(answer);
28
+ resolve(answer.trim() || defaultValue);
28
29
  });
29
30
  });
30
31
  }
@@ -49,80 +50,95 @@ async function createApp(projectName) {
49
50
  let hasTauriDesktop = false;
50
51
  let hasTauriMobile = false;
51
52
  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");
53
+ hasIdb = (await q.ask("Do you need IndexedDB (IDB)? (y/N): ", "No")).toLowerCase().startsWith("y");
54
+ hasSocket = (await q.ask("Do you need Socket Client? (y/N): ", "No")).toLowerCase().startsWith("y");
55
+ hasDocument = (await q.ask("Do you need Document Export/Viewer (PDF/Excel)? (y/N): ", "No")).toLowerCase().startsWith("y");
56
+ hasPwa = (await q.ask("Do you want to enable Progressive Web App (PWA)? (y/N): ", "No")).toLowerCase().startsWith("y");
57
+ hasTauriDesktop = (await q.ask("Do you want to enable Tauri Desktop support (Windows/macOS/Linux)? (y/N): ", "No")).toLowerCase().startsWith("y");
58
+ hasTauriMobile = (await q.ask("Do you want to enable Tauri Mobile support (Android/iOS)? (y/N): ", "No")).toLowerCase().startsWith("y");
58
59
  }
59
60
  finally {
60
61
  q.close();
61
62
  }
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}`);
63
+ const spinner = new spinner_1.Spinner("Preparing project...");
64
+ spinner.start();
65
+ try {
66
+ const envTemplateSource = process.env[TEMPLATE_ENV_KEY];
67
+ if (envTemplateSource) {
68
+ // Local copy mode
69
+ const templateSource = node_path_1.default.resolve(envTemplateSource);
70
+ spinner.update(`Copying template from ${templateSource}...`);
71
+ if (!(0, fs_1.exists)(templateSource)) {
72
+ throw new Error(`Template source override not found: ${templateSource}`);
73
+ }
74
+ (0, copier_1.copyTemplate)(templateSource, target);
69
75
  }
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)) {
76
+ else {
77
+ // Dynamic download from npm registry
78
+ const templatePackageName = "@skalfa/skalfa-app";
79
+ spinner.update(`Fetching latest template info for ${templatePackageName}...`);
80
+ const tarballUrl = await (0, npm_1.fetchLatestTarballUrl)(templatePackageName);
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
+ spinner.update("Downloading template tarball...");
89
+ await (0, npm_1.downloadTarball)(tarballUrl, tarballPath);
90
+ spinner.update("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
+ node_fs_1.default.renameSync(packageDir, target);
80
104
  node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
81
105
  }
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}`);
106
+ spinner.update("Customizing project files...");
107
+ // Cleanup git and github directories
108
+ (0, fs_1.removeDirectory)(node_path_1.default.join(target, ".git"));
109
+ (0, fs_1.removeDirectory)(node_path_1.default.join(target, ".github"));
110
+ const filesToDelete = ["CONTRIBUTING.md", "LICENSE"];
111
+ for (const file of filesToDelete) {
112
+ const filePath = node_path_1.default.join(target, file);
113
+ if (node_fs_1.default.existsSync(filePath)) {
114
+ node_fs_1.default.unlinkSync(filePath);
115
+ }
93
116
  }
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.");
117
+ renamePackage(target, packageName);
118
+ // Rename .npmignore to .gitignore if it exists
119
+ const npmignorePath = node_path_1.default.join(target, ".npmignore");
120
+ const gitignorePath = node_path_1.default.join(target, ".gitignore");
121
+ if (node_fs_1.default.existsSync(npmignorePath) && !node_fs_1.default.existsSync(gitignorePath)) {
122
+ node_fs_1.default.renameSync(npmignorePath, gitignorePath);
98
123
  }
99
- node_fs_1.default.renameSync(packageDir, target);
100
- node_fs_1.default.rmSync(tempExtractDir, { recursive: true, force: true });
124
+ // Customize project with selected options
125
+ customizeProject(target, {
126
+ idb: hasIdb,
127
+ socket: hasSocket,
128
+ document: hasDocument,
129
+ pwa: hasPwa,
130
+ tauriDesktop: hasTauriDesktop,
131
+ tauriMobile: hasTauriMobile
132
+ });
133
+ spinner.update("Installing dependencies (this may take a moment)...");
134
+ await (0, installer_1.installDependenciesAsync)(target);
135
+ spinner.stop(true, "Skalfa App Next.js project is ready.");
136
+ console.log(`\nNext steps:\n cd ${projectName}\n bun run dev`);
101
137
  }
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);
138
+ catch (error) {
139
+ spinner.stop(false, `Failed to prepare project: ${error.message}`);
140
+ throw error;
111
141
  }
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
142
  }
127
143
  function renamePackage(target, packageName) {
128
144
  const packageJsonPath = node_path_1.default.join(target, "package.json");
@@ -51,6 +51,13 @@ function pickUtility(utilityName) {
51
51
  // 2. Salin folder dari node_modules ke lokal proyek secara rekursif
52
52
  console.log(`Copying ${utilityName} folder from @skalfa/skalfa-api-core to utils/ ...`);
53
53
  node_fs_1.default.cpSync(sourceDir, targetDir, { recursive: true });
54
+ const filesToDelete = ["CONTRIBUTING.md", "LICENSE"];
55
+ for (const file of filesToDelete) {
56
+ const filePath = node_path_1.default.join(targetDir, file);
57
+ if (node_fs_1.default.existsSync(filePath)) {
58
+ node_fs_1.default.unlinkSync(filePath);
59
+ }
60
+ }
54
61
  console.log(`✓ Copied ${utilityName} folder`);
55
62
  // 3. Perbarui utils/index.ts untuk mereferensikan folder lokal
56
63
  console.log("Updating utils/index.ts with explicit local export override ...");
@@ -0,0 +1,76 @@
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.updateCli = updateCli;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const npm_1 = require("../utils/npm");
11
+ const PACKAGE_NAME = "@skalfa/skalfa-cli";
12
+ async function updateCli() {
13
+ // 1. Get current version
14
+ const packageJsonPath = node_path_1.default.join(__dirname, "..", "..", "package.json");
15
+ let currentVersion = "0.0.0";
16
+ try {
17
+ const packageJson = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
18
+ currentVersion = packageJson.version;
19
+ }
20
+ catch (err) {
21
+ throw new Error(`Failed to read local package.json from ${packageJsonPath}: ${err.message}`);
22
+ }
23
+ console.log(`Current version: v${currentVersion}`);
24
+ console.log("Checking for updates...");
25
+ // 2. Fetch latest version from registry
26
+ let latestVersion;
27
+ try {
28
+ latestVersion = await (0, npm_1.fetchLatestVersion)(PACKAGE_NAME);
29
+ }
30
+ catch (err) {
31
+ throw new Error(`Failed to fetch latest version: ${err.message}`);
32
+ }
33
+ console.log(`Latest version: v${latestVersion}`);
34
+ if (currentVersion === latestVersion) {
35
+ console.log(`\n✓ ${PACKAGE_NAME} is already up to date.`);
36
+ return;
37
+ }
38
+ console.log(`\nUpdating ${PACKAGE_NAME} from v${currentVersion} to v${latestVersion}...`);
39
+ // 3. Detect package manager and install
40
+ const scriptPath = process.argv[1] || "";
41
+ const normalizedPath = scriptPath.toLowerCase();
42
+ let packageManager = "npm";
43
+ if (normalizedPath.includes(".bun") || normalizedPath.includes("bun")) {
44
+ packageManager = "bun";
45
+ }
46
+ else if (normalizedPath.includes("yarn")) {
47
+ packageManager = "yarn";
48
+ }
49
+ else if (normalizedPath.includes("pnpm")) {
50
+ packageManager = "pnpm";
51
+ }
52
+ else if (typeof process.versions?.bun !== "undefined") {
53
+ packageManager = "bun";
54
+ }
55
+ let updateCommand = "";
56
+ if (packageManager === "bun") {
57
+ updateCommand = `bun install -g ${PACKAGE_NAME}@latest`;
58
+ }
59
+ else if (packageManager === "yarn") {
60
+ updateCommand = `yarn global add ${PACKAGE_NAME}@latest`;
61
+ }
62
+ else if (packageManager === "pnpm") {
63
+ updateCommand = `pnpm add -g ${PACKAGE_NAME}@latest`;
64
+ }
65
+ else {
66
+ updateCommand = `npm install -g ${PACKAGE_NAME}@latest`;
67
+ }
68
+ console.log(`Running: ${updateCommand}`);
69
+ try {
70
+ (0, node_child_process_1.execSync)(updateCommand, { stdio: "inherit" });
71
+ console.log(`\n✓ Successfully updated ${PACKAGE_NAME} to v${latestVersion}!`);
72
+ }
73
+ catch (err) {
74
+ throw new Error(`Failed to run update command: ${err.message}`);
75
+ }
76
+ }
@@ -4,14 +4,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.installDependencies = installDependencies;
7
+ exports.installDependenciesAsync = installDependenciesAsync;
7
8
  exports.installPackage = installPackage;
8
9
  exports.runInstall = runInstall;
10
+ exports.runInstallAsync = runInstallAsync;
9
11
  const node_child_process_1 = require("node:child_process");
10
12
  const node_fs_1 = __importDefault(require("node:fs"));
11
13
  const node_path_1 = __importDefault(require("node:path"));
12
14
  function installDependencies(target) {
13
15
  runInstall(target);
14
16
  }
17
+ function installDependenciesAsync(target) {
18
+ return runInstallAsync(target);
19
+ }
15
20
  function installPackage(target, packageName, isDev = false) {
16
21
  runInstall(target, [packageName], isDev);
17
22
  }
@@ -29,3 +34,21 @@ function runInstall(target, packages = [], isDev = false) {
29
34
  stdio: "inherit"
30
35
  });
31
36
  }
37
+ function runInstallAsync(target, packages = [], isDev = false) {
38
+ return new Promise((resolve, reject) => {
39
+ const useBun = node_fs_1.default.existsSync(node_path_1.default.join(target, "bun.lock"));
40
+ const pm = useBun ? "bun" : "npm";
41
+ const action = useBun ? "add" : "install";
42
+ const devFlag = isDev ? (useBun ? "-d" : "--save-dev") : "";
43
+ const command = packages.length > 0
44
+ ? [pm, action, devFlag, ...packages].filter(Boolean).join(" ")
45
+ : [pm, "install"].join(" ");
46
+ (0, node_child_process_1.exec)(command, { cwd: target }, (error, stdout, stderr) => {
47
+ if (error) {
48
+ reject(new Error(`Installation failed: ${stderr || stdout || error.message}`));
49
+ return;
50
+ }
51
+ resolve();
52
+ });
53
+ });
54
+ }
package/dist/utils/npm.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.fetchLatestTarballUrl = fetchLatestTarballUrl;
7
7
  exports.downloadTarball = downloadTarball;
8
+ exports.fetchLatestVersion = fetchLatestVersion;
8
9
  const node_https_1 = __importDefault(require("node:https"));
9
10
  const node_fs_1 = __importDefault(require("node:fs"));
10
11
  function fetchLatestTarballUrl(packageName) {
@@ -61,3 +62,35 @@ function downloadTarball(url, destPath) {
61
62
  });
62
63
  });
63
64
  }
65
+ function fetchLatestVersion(packageName) {
66
+ return new Promise((resolve, reject) => {
67
+ const encodedPackageName = packageName.replace("/", "%2f");
68
+ const url = `https://registry.npmjs.org/${encodedPackageName}/latest`;
69
+ node_https_1.default.get(url, { headers: { "User-Agent": "skalfa-cli" } }, (res) => {
70
+ if (res.statusCode !== 200) {
71
+ reject(new Error(`Failed to fetch latest version info for ${packageName} from npm registry. Status: ${res.statusCode} ${res.statusMessage}`));
72
+ return;
73
+ }
74
+ let data = "";
75
+ res.on("data", (chunk) => {
76
+ data += chunk;
77
+ });
78
+ res.on("end", () => {
79
+ try {
80
+ const json = JSON.parse(data);
81
+ const version = json.version;
82
+ if (!version) {
83
+ reject(new Error(`Could not find version in registry response for ${packageName}.`));
84
+ return;
85
+ }
86
+ resolve(version);
87
+ }
88
+ catch (err) {
89
+ reject(err);
90
+ }
91
+ });
92
+ }).on("error", (err) => {
93
+ reject(new Error(`Network error while contacting npm registry: ${err.message}`));
94
+ });
95
+ });
96
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Spinner = void 0;
4
+ class Spinner {
5
+ timer = null;
6
+ message;
7
+ frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
8
+ currentFrame = 0;
9
+ constructor(message) {
10
+ this.message = message;
11
+ }
12
+ start() {
13
+ // Hide cursor
14
+ process.stdout.write("\x1B[?25l");
15
+ this.timer = setInterval(() => {
16
+ const frame = this.frames[this.currentFrame];
17
+ process.stdout.write(`\r\x1B[35m${frame}\x1B[0m ${this.message}`);
18
+ this.currentFrame = (this.currentFrame + 1) % this.frames.length;
19
+ }, 80);
20
+ }
21
+ update(message) {
22
+ this.message = message;
23
+ }
24
+ stop(success = true, statusMessage) {
25
+ if (this.timer) {
26
+ clearInterval(this.timer);
27
+ this.timer = null;
28
+ }
29
+ // Clear line
30
+ process.stdout.write("\r\x1B[K");
31
+ // Show cursor
32
+ process.stdout.write("\x1B[?25h");
33
+ if (success) {
34
+ console.log(`\x1B[32m✓\x1B[0m ${statusMessage || this.message}`);
35
+ }
36
+ else {
37
+ console.log(`\x1B[31m✗\x1B[0m ${statusMessage || this.message}`);
38
+ }
39
+ }
40
+ }
41
+ exports.Spinner = Spinner;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skalfa/skalfa-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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": {
@@ -29,4 +29,4 @@
29
29
  "@types/node": "^26.0.0",
30
30
  "typescript": "^6.0.3"
31
31
  }
32
- }
32
+ }