catylst 1.0.0

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.
Files changed (3) hide show
  1. package/catylst.js +35 -0
  2. package/package.json +21 -0
  3. package/postinstall.js +206 -0
package/catylst.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // catylst.js — bin entry point
3
+ // Launches the Catylst CLI JAR from ~/.catylst/
4
+
5
+ const { spawn } = require("child_process");
6
+ const path = require("path");
7
+ const os = require("os");
8
+ const fs = require("fs");
9
+
10
+ const TEMPLATE_DIR = path.join(os.homedir(), ".catylst", "template");
11
+ const JAR_PATH = path.join(os.homedir(), ".catylst", "catylst-cli.jar");
12
+ const WORK_DIR = path.join(TEMPLATE_DIR, "cli-generator");
13
+
14
+ if (!fs.existsSync(JAR_PATH)) {
15
+ console.error(
16
+ "\x1b[33m✗ catylst-cli.jar not found. Re-run: npm install -g catylst\x1b[0m"
17
+ );
18
+ process.exit(1);
19
+ }
20
+
21
+ if (!fs.existsSync(WORK_DIR)) {
22
+ console.error(
23
+ "\x1b[33m✗ Template not found at ~/.catylst/template. Re-run: npm install -g catylst\x1b[0m"
24
+ );
25
+ process.exit(1);
26
+ }
27
+
28
+ const args = process.argv.slice(2);
29
+
30
+ const child = spawn("java", ["-jar", JAR_PATH, ...args], {
31
+ cwd: WORK_DIR,
32
+ stdio: "inherit",
33
+ });
34
+
35
+ child.on("exit", (code) => process.exit(code ?? 0));
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "catylst",
3
+ "version": "1.0.0",
4
+ "description": "Generate customized Kotlin Multiplatform projects with an interactive wizard",
5
+ "keywords": ["kotlin", "kmp", "android", "ios", "multiplatform", "compose", "generator"],
6
+ "homepage": "https://github.com/rohit-554/Catylst",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/rohit-554/Catylst.git"
10
+ },
11
+ "license": "MIT",
12
+ "bin": {
13
+ "catylst": "catylst.js"
14
+ },
15
+ "scripts": {
16
+ "postinstall": "node postinstall.js"
17
+ },
18
+ "engines": {
19
+ "node": ">=14"
20
+ }
21
+ }
package/postinstall.js ADDED
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ // postinstall.js — runs after `npm install -g catylst`
3
+ // 1. Checks for Java 17+
4
+ // 2. Clones (or updates) the Catylst template to ~/.catylst/template
5
+ // 3. Downloads the CLI JAR to ~/.catylst/catylst-cli.jar
6
+
7
+ const { spawnSync } = require("child_process");
8
+ const https = require("https");
9
+ const crypto = require("crypto");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const os = require("os");
13
+
14
+ const REPO_URL = "https://github.com/rohit-554/Catylst.git";
15
+ const JAR_URL =
16
+ "https://github.com/rohit-554/Catylst/releases/latest/download/catylst-cli.jar";
17
+
18
+ // Trusted hosts for redirect following — no other host is allowed
19
+ const TRUSTED_HOSTS = [
20
+ "github.com",
21
+ "objects.githubusercontent.com",
22
+ "releases.githubusercontent.com",
23
+ "codeload.github.com",
24
+ ];
25
+
26
+ const CATYLST_DIR = path.join(os.homedir(), ".catylst");
27
+ const TEMPLATE_DIR = path.join(CATYLST_DIR, "template");
28
+ const JAR_PATH = path.join(CATYLST_DIR, "catylst-cli.jar");
29
+
30
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
31
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
32
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
33
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
34
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
35
+
36
+ console.log("");
37
+ console.log(bold(" Catylst KMP Project Generator"));
38
+ console.log(dim(" ────────────────────────────────────────"));
39
+ console.log("");
40
+
41
+ // ── 1. Check Java ────────────────────────────────────────────────────────────
42
+
43
+ function checkJava() {
44
+ const result = spawnSync("java", ["-version"], { encoding: "utf8" });
45
+ const output = result.stderr || result.stdout || "";
46
+ const match = output.match(/version "(\d+)/);
47
+ if (!match) {
48
+ console.error(yellow(" ✗ Java not found. Install JDK 17+ from https://adoptium.net"));
49
+ process.exit(1);
50
+ }
51
+ const major = parseInt(match[1], 10);
52
+ if (major < 17) {
53
+ console.error(
54
+ yellow(` ✗ JDK 17+ required (found ${major}). Install from https://adoptium.net`)
55
+ );
56
+ process.exit(1);
57
+ }
58
+ console.log(green(` ✓ Java ${major}`));
59
+ }
60
+
61
+ // ── 2. Clone / update template ───────────────────────────────────────────────
62
+
63
+ function setupTemplate() {
64
+ fs.mkdirSync(CATYLST_DIR, { recursive: true });
65
+
66
+ const isGitRepo = fs.existsSync(path.join(TEMPLATE_DIR, ".git"));
67
+
68
+ if (isGitRepo) {
69
+ process.stdout.write(cyan(" ↻ Updating template..."));
70
+ // Use spawn array form — no shell interpolation, no injection risk
71
+ const result = spawnSync("git", ["pull", "--quiet", "--rebase"], {
72
+ cwd: TEMPLATE_DIR,
73
+ stdio: "pipe",
74
+ });
75
+ if (result.status === 0) {
76
+ console.log("\r" + green(" ✓ Template updated "));
77
+ } else {
78
+ console.log("\r" + yellow(" ⚠ Could not update template (offline?). Using existing."));
79
+ }
80
+ } else {
81
+ console.log(cyan(" ↓ Cloning template to ~/.catylst/template ..."));
82
+ fs.rmSync(TEMPLATE_DIR, { recursive: true, force: true });
83
+ // Use spawn array form — REPO_URL and TEMPLATE_DIR are never shell-interpolated
84
+ const result = spawnSync(
85
+ "git",
86
+ ["clone", "--depth", "1", "--quiet", REPO_URL, TEMPLATE_DIR],
87
+ { stdio: "pipe" }
88
+ );
89
+ if (result.status !== 0) {
90
+ const msg = result.stderr ? result.stderr.toString().trim() : "unknown error";
91
+ console.error(yellow(" ✗ Failed to clone template. Is git installed?"));
92
+ console.error(dim(` ${msg}`));
93
+ process.exit(1);
94
+ }
95
+ console.log(green(" ✓ Template ready"));
96
+ }
97
+ }
98
+
99
+ // ── 3. Download JAR ──────────────────────────────────────────────────────────
100
+
101
+ function isTrustedHost(urlString) {
102
+ try {
103
+ const { hostname } = new URL(urlString);
104
+ return TRUSTED_HOSTS.some((h) => hostname === h || hostname.endsWith("." + h));
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ function downloadJar() {
111
+ // Dev mode — use local build if available
112
+ const localJar = path.join(
113
+ TEMPLATE_DIR,
114
+ "cli-generator",
115
+ "build",
116
+ "libs",
117
+ "cli-generator-1.0.0.jar"
118
+ );
119
+ if (fs.existsSync(localJar)) {
120
+ fs.copyFileSync(localJar, JAR_PATH);
121
+ console.log(green(" ✓ Using local build"));
122
+ return Promise.resolve();
123
+ }
124
+
125
+ return new Promise((resolve, reject) => {
126
+ process.stdout.write(cyan(" ↓ Downloading catylst-cli.jar ..."));
127
+
128
+ // Write to a temp file first — atomic rename prevents race conditions
129
+ const tmpPath = JAR_PATH + ".tmp." + process.pid;
130
+
131
+ function get(url, redirectCount = 0) {
132
+ if (redirectCount > 5) return reject(new Error("Too many redirects"));
133
+
134
+ // Validate redirect destination stays on trusted hosts
135
+ if (!isTrustedHost(url)) {
136
+ return reject(new Error(`Redirect to untrusted host blocked: ${url}`));
137
+ }
138
+
139
+ https
140
+ .get(url, { headers: { "User-Agent": "catylst-npm-installer" } }, (res) => {
141
+ if (res.statusCode === 301 || res.statusCode === 302) {
142
+ const location = res.headers.location;
143
+ if (!location) return reject(new Error("Redirect with no Location header"));
144
+ return get(location, redirectCount + 1);
145
+ }
146
+ if (res.statusCode !== 200) {
147
+ return reject(new Error(`HTTP ${res.statusCode}`));
148
+ }
149
+
150
+ // Stream to temp file with restricted permissions (owner read/write only)
151
+ const file = fs.createWriteStream(tmpPath, { mode: 0o600 });
152
+ const hash = crypto.createHash("sha256");
153
+
154
+ res.on("data", (chunk) => hash.update(chunk));
155
+ res.pipe(file);
156
+
157
+ file.on("finish", () => {
158
+ file.close(() => {
159
+ // Atomic rename — prevents TOCTOU race where another process
160
+ // could read a partially-written file
161
+ try {
162
+ fs.renameSync(tmpPath, JAR_PATH);
163
+ } catch (e) {
164
+ fs.unlinkSync(tmpPath);
165
+ return reject(e);
166
+ }
167
+
168
+ const digest = hash.digest("hex");
169
+ console.log("\r" + green(" ✓ CLI ready "));
170
+ console.log(dim(` SHA-256: ${digest}`));
171
+ resolve();
172
+ });
173
+ });
174
+
175
+ file.on("error", (err) => {
176
+ fs.unlink(tmpPath, () => {});
177
+ reject(err);
178
+ });
179
+ })
180
+ .on("error", (err) => {
181
+ fs.unlink(tmpPath, () => {});
182
+ reject(err);
183
+ });
184
+ }
185
+
186
+ get(JAR_URL);
187
+ });
188
+ }
189
+
190
+ // ── run ───────────────────────────────────────────────────────────────────────
191
+
192
+ (async () => {
193
+ checkJava();
194
+ setupTemplate();
195
+ await downloadJar();
196
+
197
+ console.log("");
198
+ console.log(dim(" ────────────────────────────────────────"));
199
+ console.log(" " + green(bold("All done!")) + " Start your project:");
200
+ console.log("");
201
+ console.log(" " + cyan("catylst --interactive"));
202
+ console.log("");
203
+ console.log(" " + dim("Or non-interactive:"));
204
+ console.log(" " + dim("catylst --package com.example.app --name MyApp"));
205
+ console.log("");
206
+ })();