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.
- package/package.json +1 -1
- package/postinstall.js +127 -166
package/package.json
CHANGED
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
|
-
|
|
8
|
-
|
|
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
|
|
11
|
-
const path
|
|
12
|
-
const 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
|
|
29
|
+
const CATYLST_DIR = path.join(os.homedir(), ".catylst");
|
|
28
30
|
const TEMPLATE_DIR = path.join(CATYLST_DIR, "template");
|
|
29
|
-
const JAR_PATH
|
|
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
|
-
|
|
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
|
|
42
|
-
"tip
|
|
43
|
-
"tip
|
|
44
|
-
"tip
|
|
45
|
-
"tip
|
|
46
|
-
"tip
|
|
47
|
-
"tip
|
|
48
|
-
"tip
|
|
49
|
-
"tip
|
|
50
|
-
"tip
|
|
51
|
-
"joke
|
|
52
|
-
"joke
|
|
53
|
-
"joke
|
|
54
|
-
"joke
|
|
55
|
-
"joke
|
|
56
|
-
"joke
|
|
57
|
-
"joke
|
|
58
|
-
"joke
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
82
|
+
function stopTips(line) {
|
|
82
83
|
if (tipTimer) { clearTimeout(tipTimer); tipTimer = null; }
|
|
83
|
-
|
|
84
|
+
printRaw(`\r${line}${" ".repeat(40)}\n`);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
// ──
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
112
|
+
const match = output.match(/version "(\d+)/);
|
|
99
113
|
if (!match) {
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
if (isGitRepo) {
|
|
130
|
+
if (fs.existsSync(path.join(TEMPLATE_DIR, ".git"))) {
|
|
121
131
|
startTips();
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
137
|
+
print(dim(" .. Cloning template\n"));
|
|
133
138
|
startTips();
|
|
134
139
|
fs.rmSync(TEMPLATE_DIR, { recursive: true, force: true });
|
|
135
|
-
const
|
|
136
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
print(green(" ok Using local build"));
|
|
174
158
|
return Promise.resolve();
|
|
175
159
|
}
|
|
176
160
|
|
|
177
161
|
return new Promise((resolve, reject) => {
|
|
178
|
-
|
|
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,
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
})();
|