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