catylst 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/postinstall.js +127 -166
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catylst",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Generate customized Kotlin Multiplatform projects with an interactive wizard",
5
5
  "keywords": [
6
6
  "kotlin",
package/postinstall.js CHANGED
@@ -4,18 +4,20 @@
4
4
  // 2. Clones (or updates) the Catylst template to ~/.catylst/template
5
5
  // 3. Downloads the CLI JAR to ~/.catylst/catylst-cli.jar
6
6
 
7
- const { spawnSync } = require("child_process");
8
- const https = require("https");
7
+ // NOTE: all progress output goes to stderr.
8
+ // npm buffers stdout during install and only shows it on error.
9
+ // stderr is streamed to the terminal in real-time.
10
+
11
+ const { spawnSync, spawn } = require("child_process");
12
+ const https = require("https");
9
13
  const crypto = require("crypto");
10
- const fs = require("fs");
11
- const path = require("path");
12
- const os = require("os");
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const os = require("os");
13
17
 
14
18
  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";
19
+ const JAR_URL = "https://github.com/rohit-554/Catylst/releases/latest/download/catylst-cli.jar";
17
20
 
18
- // Trusted hosts for redirect following — no other host is allowed
19
21
  const TRUSTED_HOSTS = [
20
22
  "github.com",
21
23
  "objects.githubusercontent.com",
@@ -24,9 +26,11 @@ const TRUSTED_HOSTS = [
24
26
  "codeload.github.com",
25
27
  ];
26
28
 
27
- const CATYLST_DIR = path.join(os.homedir(), ".catylst");
29
+ const CATYLST_DIR = path.join(os.homedir(), ".catylst");
28
30
  const TEMPLATE_DIR = path.join(CATYLST_DIR, "template");
29
- const JAR_PATH = path.join(CATYLST_DIR, "catylst-cli.jar");
31
+ const JAR_PATH = path.join(CATYLST_DIR, "catylst-cli.jar");
32
+
33
+ // ── colours ───────────────────────────────────────────────────────────────────
30
34
 
31
35
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
32
36
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
@@ -35,207 +39,160 @@ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
35
39
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
36
40
  const purple = (s) => `\x1b[35m${s}\x1b[0m`;
37
41
 
38
- // ── Tips & jokes shown while cloning / downloading ───────────────────────────
42
+ const print = (s) => process.stderr.write(s + "\n");
43
+ const printRaw = (s) => process.stderr.write(s);
44
+
45
+ // ── tips & jokes ──────────────────────────────────────────────────────────────
39
46
 
40
47
  const MESSAGES = [
41
- "tip Room 3.1 auto-generates all your DAO queries at compile time.",
42
- "tip Navigation3 uses type-safe routes — no more string typos in nav graphs.",
43
- "tip Swap AI providers by changing one line in AppModule.kt.",
44
- "tip bloom-build helps you scaffold a full feature in seconds with Claude Code.",
45
- "tip Material 3 Expressive ships spring-based motion out of the box.",
46
- "tip Run ./gradlew :composeApp:kspAndroidMain after every Entity change.",
47
- "tip Koin multiplatform means one DI graph for Android, iOS, and Desktop.",
48
- "tip Use bloom-navigate to cleanly remove any feature you do not need.",
49
- "tip AGP 9 brings predictive back gesture support by default.",
50
- "tip commonMain code compiles to all targets — write once, ship everywhere.",
51
- "joke Why do Kotlin developers stay calm? Because they know how to handle exceptions.",
52
- "joke A null pointer walks into a bar. The bartender says: we don't serve your type here.",
53
- "joke Why did the Android developer quit? Too many fragments.",
54
- "joke Kotlin: where semicolons go to retire.",
55
- "joke iOS dev asks: what is Gradle? Android dev weeps softly.",
56
- "joke There are only 10 types of developers: those who understand binary and those who do not.",
57
- "joke A git push a day keeps the merge conflicts away. Usually.",
58
- "joke My code works. I have no idea why. Shipping it anyway.",
48
+ ["tip", "Room 3.1 auto-generates all your DAO queries at compile time."],
49
+ ["tip", "Navigation3 uses type-safe routes — no more string typos in nav graphs."],
50
+ ["tip", "Swap AI providers by changing one line in AppModule.kt."],
51
+ ["tip", "bloom-build scaffolds a full screen Entity, DAO, ViewModel, UI — in seconds."],
52
+ ["tip", "Material 3 Expressive ships spring-based motion out of the box."],
53
+ ["tip", "Run ./gradlew :composeApp:kspAndroidMain after every Room Entity change."],
54
+ ["tip", "Koin multiplatform means one DI graph for Android, iOS, and Desktop."],
55
+ ["tip", "bloom-navigate cleanly removes any feature you do not need."],
56
+ ["tip", "AGP 9 brings predictive back gesture support by default."],
57
+ ["tip", "commonMain code compiles to all targets — write once, ship everywhere."],
58
+ ["joke", "Why do Kotlin developers stay calm? They know how to handle exceptions."],
59
+ ["joke", "A null pointer walks into a bar. Bartender: we don't serve your type here."],
60
+ ["joke", "Why did the Android dev quit? Too many fragments."],
61
+ ["joke", "Kotlin: where semicolons go to retire."],
62
+ ["joke", "iOS dev asks what Gradle is. Android dev weeps softly."],
63
+ ["joke", "There are 10 types of developers: those who get binary and those who don't."],
64
+ ["joke", "A git push a day keeps the merge conflicts away. Usually."],
65
+ ["joke", "My code works. I have no idea why. Shipping it anyway."],
59
66
  ];
60
67
 
61
68
  let tipTimer = null;
62
- let tipIndex = 0;
63
69
 
64
70
  function startTips() {
65
- // Shuffle so order is different each install
66
71
  const msgs = [...MESSAGES].sort(() => Math.random() - 0.5);
67
- tipIndex = 0;
68
-
69
- function showNext() {
70
- const msg = msgs[tipIndex % msgs.length];
71
- const kind = msg.startsWith("joke") ? purple("joke ") : cyan("tip ");
72
- const text = msg.replace(/^(tip|joke)\s+/, "");
73
- process.stdout.write(`\r ${kind} ${dim(text)}${" ".repeat(10)}`);
74
- tipIndex++;
75
- tipTimer = setTimeout(showNext, 3000);
72
+ let i = 0;
73
+ function next() {
74
+ const [kind, text] = msgs[i++ % msgs.length];
75
+ const label = kind === "joke" ? purple("joke") : cyan("tip ");
76
+ printRaw(`\r ${label} ${dim(text)}${" ".repeat(6)}`);
77
+ tipTimer = setTimeout(next, 3200);
76
78
  }
77
-
78
- showNext();
79
+ next();
79
80
  }
80
81
 
81
- function stopTips(finalLine) {
82
+ function stopTips(line) {
82
83
  if (tipTimer) { clearTimeout(tipTimer); tipTimer = null; }
83
- process.stdout.write(`\r${finalLine}${" ".repeat(30)}\n`);
84
+ printRaw(`\r${line}${" ".repeat(40)}\n`);
84
85
  }
85
86
 
86
- // ── Header ───────────────────────────────────────────────────────────────────
87
+ // ── helpers ───────────────────────────────────────────────────────────────────
88
+
89
+ // Async wrapper for spawn — keeps event loop alive so timers fire during git ops
90
+ function runAsync(cmd, args, opts = {}) {
91
+ return new Promise((resolve, reject) => {
92
+ const child = spawn(cmd, args, { stdio: "pipe", ...opts });
93
+ const stderr = [];
94
+ child.stderr && child.stderr.on("data", (d) => stderr.push(d));
95
+ child.on("close", (code) => resolve({ status: code, stderr: Buffer.concat(stderr) }));
96
+ child.on("error", reject);
97
+ });
98
+ }
87
99
 
88
- console.log("");
89
- console.log(bold(" Catylst KMP Project Generator"));
90
- console.log(dim(" ────────────────────────────────────────"));
91
- console.log("");
100
+ function isTrustedHost(urlString) {
101
+ try {
102
+ const { hostname } = new URL(urlString);
103
+ return TRUSTED_HOSTS.some((h) => hostname === h || hostname.endsWith("." + h));
104
+ } catch { return false; }
105
+ }
92
106
 
93
- // ── 1. Check Java ────────────────────────────────────────────────────────────
107
+ // ── 1. Check Java ─────────────────────────────────────────────────────────────
94
108
 
95
109
  function checkJava() {
96
110
  const result = spawnSync("java", ["-version"], { encoding: "utf8" });
97
111
  const output = result.stderr || result.stdout || "";
98
- const match = output.match(/version "(\d+)/);
112
+ const match = output.match(/version "(\d+)/);
99
113
  if (!match) {
100
- console.error(yellow(" Java not found. Install JDK 17+ from https://adoptium.net"));
114
+ print(yellow(" x Java not found. Install JDK 17+ from https://adoptium.net"));
101
115
  process.exit(1);
102
116
  }
103
117
  const major = parseInt(match[1], 10);
104
118
  if (major < 17) {
105
- console.error(
106
- yellow(` ✗ JDK 17+ required (found ${major}). Install from https://adoptium.net`)
107
- );
119
+ print(yellow(` x JDK 17+ required (found ${major}). https://adoptium.net`));
108
120
  process.exit(1);
109
121
  }
110
- console.log(green(` Java ${major}`));
122
+ print(green(` ok Java ${major}`));
111
123
  }
112
124
 
113
- // ── 2. Clone / update template ───────────────────────────────────────────────
125
+ // ── 2. Clone / update template ────────────────────────────────────────────────
114
126
 
115
- function setupTemplate() {
127
+ async function setupTemplate() {
116
128
  fs.mkdirSync(CATYLST_DIR, { recursive: true });
117
129
 
118
- const isGitRepo = fs.existsSync(path.join(TEMPLATE_DIR, ".git"));
119
-
120
- if (isGitRepo) {
130
+ if (fs.existsSync(path.join(TEMPLATE_DIR, ".git"))) {
121
131
  startTips();
122
- const result = spawnSync("git", ["pull", "--quiet", "--rebase"], {
123
- cwd: TEMPLATE_DIR,
124
- stdio: "pipe",
125
- });
126
- if (result.status === 0) {
127
- stopTips(green(" ✓ Template updated"));
128
- } else {
129
- stopTips(yellow(" ⚠ Could not update template (offline?). Using existing."));
130
- }
132
+ const r = await runAsync("git", ["pull", "--quiet", "--rebase"], { cwd: TEMPLATE_DIR });
133
+ stopTips(r.status === 0
134
+ ? green(" ok Template updated")
135
+ : yellow(" !! Could not update template (offline?). Using existing."));
131
136
  } else {
132
- console.log(dim(" Cloning template — hang tight...\n"));
137
+ print(dim(" .. Cloning template\n"));
133
138
  startTips();
134
139
  fs.rmSync(TEMPLATE_DIR, { recursive: true, force: true });
135
- const result = spawnSync(
136
- "git",
137
- ["clone", "--depth", "1", "--quiet", REPO_URL, TEMPLATE_DIR],
138
- { stdio: "pipe" }
139
- );
140
- if (result.status !== 0) {
140
+ const r = await runAsync("git", ["clone", "--depth", "1", "--quiet", REPO_URL, TEMPLATE_DIR]);
141
+ if (r.status !== 0) {
141
142
  stopTips("");
142
- const msg = result.stderr ? result.stderr.toString().trim() : "unknown error";
143
- console.error(yellow(" ✗ Failed to clone template. Is git installed?"));
144
- console.error(dim(` ${msg}`));
143
+ print(yellow(" x Failed to clone. Is git installed?"));
144
+ print(dim(" " + r.stderr.toString().trim()));
145
145
  process.exit(1);
146
146
  }
147
- stopTips(green(" Template ready"));
147
+ stopTips(green(" ok Template ready"));
148
148
  }
149
149
  }
150
150
 
151
- // ── 3. Download JAR ──────────────────────────────────────────────────────────
152
-
153
- function isTrustedHost(urlString) {
154
- try {
155
- const { hostname } = new URL(urlString);
156
- return TRUSTED_HOSTS.some((h) => hostname === h || hostname.endsWith("." + h));
157
- } catch {
158
- return false;
159
- }
160
- }
151
+ // ── 3. Download JAR ───────────────────────────────────────────────────────────
161
152
 
162
153
  function downloadJar() {
163
- // Dev mode use local build if available
164
- const localJar = path.join(
165
- TEMPLATE_DIR,
166
- "cli-generator",
167
- "build",
168
- "libs",
169
- "cli-generator-1.0.0.jar"
170
- );
154
+ const localJar = path.join(TEMPLATE_DIR, "cli-generator", "build", "libs", "cli-generator-1.0.0.jar");
171
155
  if (fs.existsSync(localJar)) {
172
156
  fs.copyFileSync(localJar, JAR_PATH);
173
- console.log(green(" Using local build"));
157
+ print(green(" ok Using local build"));
174
158
  return Promise.resolve();
175
159
  }
176
160
 
177
161
  return new Promise((resolve, reject) => {
178
- console.log(dim(" Downloading CLI — almost there...\n"));
162
+ print(dim(" .. Downloading CLI\n"));
179
163
  startTips();
180
-
181
- // Write to a temp file first — atomic rename prevents race conditions
182
164
  const tmpPath = JAR_PATH + ".tmp." + process.pid;
183
165
 
184
- function get(url, redirectCount = 0) {
185
- if (redirectCount > 5) return reject(new Error("Too many redirects"));
186
-
187
- // Validate redirect destination stays on trusted hosts
188
- if (!isTrustedHost(url)) {
189
- return reject(new Error(`Redirect to untrusted host blocked: ${url}`));
190
- }
191
-
192
- https
193
- .get(url, { headers: { "User-Agent": "catylst-npm-installer" } }, (res) => {
194
- if (res.statusCode === 301 || res.statusCode === 302) {
195
- const location = res.headers.location;
196
- if (!location) return reject(new Error("Redirect with no Location header"));
197
- return get(location, redirectCount + 1);
198
- }
199
- if (res.statusCode !== 200) {
200
- return reject(new Error(`HTTP ${res.statusCode}`));
201
- }
202
-
203
- // Stream to temp file with restricted permissions (owner read/write only)
204
- const file = fs.createWriteStream(tmpPath, { mode: 0o600 });
205
- const hash = crypto.createHash("sha256");
206
-
207
- res.on("data", (chunk) => hash.update(chunk));
208
- res.pipe(file);
209
-
210
- file.on("finish", () => {
211
- file.close(() => {
212
- // Atomic rename — prevents TOCTOU race where another process
213
- // could read a partially-written file
214
- try {
215
- fs.renameSync(tmpPath, JAR_PATH);
216
- } catch (e) {
217
- fs.unlinkSync(tmpPath);
218
- return reject(e);
219
- }
220
-
221
- const digest = hash.digest("hex");
222
- stopTips(green(" ✓ CLI ready"));
223
- console.log(dim(` SHA-256: ${digest}`));
224
- resolve();
225
- });
226
- });
227
-
228
- file.on("error", (err) => {
229
- stopTips("");
230
- fs.unlink(tmpPath, () => {});
231
- reject(err);
166
+ function get(url, hops = 0) {
167
+ if (hops > 5) return reject(new Error("Too many redirects"));
168
+ if (!isTrustedHost(url)) return reject(new Error(`Blocked redirect to: ${url}`));
169
+
170
+ https.get(url, { headers: { "User-Agent": "catylst-npm-installer" } }, (res) => {
171
+ if (res.statusCode === 301 || res.statusCode === 302) {
172
+ const loc = res.headers.location;
173
+ if (!loc) return reject(new Error("Redirect missing Location header"));
174
+ return get(loc, hops + 1);
175
+ }
176
+ if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
177
+
178
+ const file = fs.createWriteStream(tmpPath, { mode: 0o600 });
179
+ const hash = crypto.createHash("sha256");
180
+
181
+ res.on("data", (chunk) => hash.update(chunk));
182
+ res.pipe(file);
183
+
184
+ file.on("finish", () => {
185
+ file.close(() => {
186
+ try { fs.renameSync(tmpPath, JAR_PATH); }
187
+ catch (e) { fs.unlink(tmpPath, () => {}); return reject(e); }
188
+ stopTips(green(" ok CLI ready"));
189
+ print(dim(` sha256: ${hash.digest("hex")}`));
190
+ resolve();
232
191
  });
233
- })
234
- .on("error", (err) => {
235
- stopTips("");
236
- fs.unlink(tmpPath, () => {});
237
- reject(err);
238
192
  });
193
+
194
+ file.on("error", (e) => { stopTips(""); fs.unlink(tmpPath, () => {}); reject(e); });
195
+ }).on("error", (e) => { stopTips(""); fs.unlink(tmpPath, () => {}); reject(e); });
239
196
  }
240
197
 
241
198
  get(JAR_URL);
@@ -245,17 +202,21 @@ function downloadJar() {
245
202
  // ── run ───────────────────────────────────────────────────────────────────────
246
203
 
247
204
  (async () => {
205
+ print("");
206
+ print(bold(" Catylst KMP Project Generator"));
207
+ print(dim(" ──────────────────────────────────────"));
208
+ print("");
209
+
248
210
  checkJava();
249
- setupTemplate();
211
+ await setupTemplate();
250
212
  await downloadJar();
251
213
 
252
- console.log("");
253
- console.log(dim(" ────────────────────────────────────────"));
254
- console.log(" " + green(bold("All done!")) + " Start your project:");
255
- console.log("");
256
- console.log(" " + cyan("catylst --interactive"));
257
- console.log("");
258
- console.log(" " + dim("Or non-interactive:"));
259
- console.log(" " + dim("catylst --package com.example.app --name MyApp"));
260
- console.log("");
214
+ print("");
215
+ print(dim(" ──────────────────────────────────────"));
216
+ print(" " + green(bold("Done.")) + " Start your project:");
217
+ print("");
218
+ print(" " + cyan("catylst --interactive"));
219
+ print("");
220
+ print(" " + dim("catylst --package com.example.app --name MyApp"));
221
+ print("");
261
222
  })();