@opys/cli 0.1.2
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/README.md +82 -0
- package/dist/opys.mjs +446 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# opys CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for building and launching Minecraft client installations from declarative manifests.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -g @opys/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly without installing:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx @opys/cli <command>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `opys build`
|
|
20
|
+
|
|
21
|
+
Reads a JS config file, fetches Mojang metadata, and writes a `opys.json` manifest.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
opys build [--input opys.config.mjs] [--output opys.json]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Flag | Short | Default | Description |
|
|
28
|
+
| ---------- | ----- | ---------------------- | --------------------------------- |
|
|
29
|
+
| `--input` | `-i` | `opys.config.mjs` | Path to the JS config file |
|
|
30
|
+
| `--output` | `-o` | value from config file | Output path for the manifest JSON |
|
|
31
|
+
|
|
32
|
+
If `--output` is omitted and the config has no `output` field, the manifest is written to stdout.
|
|
33
|
+
|
|
34
|
+
### `opys launch`
|
|
35
|
+
|
|
36
|
+
Installs missing artifacts and spawns the JVM.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
opys launch [manifest] [--var key=value ...]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Common vars to pass at launch: `username`, `uuid`, `token`.
|
|
43
|
+
|
|
44
|
+
## Config file (`opys.config.mjs`)
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import {
|
|
48
|
+
defineConfig,
|
|
49
|
+
resolveMinecraft,
|
|
50
|
+
artifactScanner,
|
|
51
|
+
} from '@opys/minecraft';
|
|
52
|
+
|
|
53
|
+
export default defineConfig(async () => {
|
|
54
|
+
const mc = await resolveMinecraft({ version: '1.20.1' });
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
output: 'opys.json',
|
|
58
|
+
manifest: {
|
|
59
|
+
artifacts: [
|
|
60
|
+
mc.artifacts,
|
|
61
|
+
artifactScanner({
|
|
62
|
+
directory: 'mods',
|
|
63
|
+
url: 'https://cdn.example.com/mods/${path}',
|
|
64
|
+
path: '${root}/mods/${path}',
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
vars: mc.vars,
|
|
68
|
+
launch: mc.launch,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Exit codes
|
|
75
|
+
|
|
76
|
+
| Code | Meaning |
|
|
77
|
+
| ---- | ------------------------------- |
|
|
78
|
+
| 0 | Success |
|
|
79
|
+
| 1 | Usage error (bad args / config) |
|
|
80
|
+
| 2 | Network error |
|
|
81
|
+
| 3 | Integrity check failed |
|
|
82
|
+
| 4 | Extraction failure |
|
package/dist/opys.mjs
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { encodeManifest } from "@opys/core";
|
|
5
|
+
import { buildManifest, resolveConfig } from "@opys/dev";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { ExtractionError, IntegrityError, NetworkError, install, launch } from "@opys/runtime";
|
|
9
|
+
import { VersionFetchError } from "@opys/mojang";
|
|
10
|
+
|
|
11
|
+
//#region lib/errors.ts
|
|
12
|
+
var UsageError = class extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "UsageError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region lib/args.ts
|
|
21
|
+
function parseArgs$1(argv, specs) {
|
|
22
|
+
const options = {};
|
|
23
|
+
for (const s of specs) options[s.long] = {
|
|
24
|
+
type: s.type,
|
|
25
|
+
...s.short ? { short: s.short } : {}
|
|
26
|
+
};
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = parseArgs({
|
|
30
|
+
args: argv,
|
|
31
|
+
options,
|
|
32
|
+
allowPositionals: true,
|
|
33
|
+
strict: true
|
|
34
|
+
});
|
|
35
|
+
} catch (e) {
|
|
36
|
+
throw new UsageError(e.message);
|
|
37
|
+
}
|
|
38
|
+
return { getString: (flag) => parsed.values[flag] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region lib/load-config.ts
|
|
43
|
+
/** Import a config file, resolve it for the given mode, and report its directory. */
|
|
44
|
+
async function loadConfig(inputFile, mode) {
|
|
45
|
+
const absConfig = resolve(inputFile);
|
|
46
|
+
const configDir = dirname(absConfig);
|
|
47
|
+
const mod = await import(pathToFileURL(absConfig).href);
|
|
48
|
+
if (!mod.default) throw new UsageError(`${inputFile}: no default export`);
|
|
49
|
+
return {
|
|
50
|
+
config: await resolveConfig(mod.default, { mode }),
|
|
51
|
+
configDir
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region lib/commands/build.ts
|
|
57
|
+
async function cmdBuild(argv, logger, command) {
|
|
58
|
+
const args = parseArgs$1(argv, [
|
|
59
|
+
{
|
|
60
|
+
long: "input",
|
|
61
|
+
short: "i",
|
|
62
|
+
type: "string"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
long: "output",
|
|
66
|
+
short: "o",
|
|
67
|
+
type: "string"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
long: "mode",
|
|
71
|
+
type: "string"
|
|
72
|
+
}
|
|
73
|
+
]);
|
|
74
|
+
const inputFile = args.getString("input") ?? "opys.config.mjs";
|
|
75
|
+
const outputFile = args.getString("output");
|
|
76
|
+
const mode = args.getString("mode") ?? command;
|
|
77
|
+
const { config, configDir } = await loadConfig(inputFile, mode);
|
|
78
|
+
const manifest = await buildManifest(config, {
|
|
79
|
+
log: (scope, msg) => logger.info(`[${scope}] ${msg}`),
|
|
80
|
+
configDir,
|
|
81
|
+
mode
|
|
82
|
+
});
|
|
83
|
+
const json = JSON.stringify(encodeManifest(manifest), null, 2) + "\n";
|
|
84
|
+
const out = outputFile ?? config.output;
|
|
85
|
+
if (out) {
|
|
86
|
+
await writeFile(resolve(configDir, out), json);
|
|
87
|
+
logger.info(`Written to ${out}`);
|
|
88
|
+
} else process.stdout.write(json);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region lib/progress.ts
|
|
93
|
+
const BAR_WIDTH = 24;
|
|
94
|
+
const NON_TTY_INTERVAL_MS = 3e3;
|
|
95
|
+
const FILLED = "█";
|
|
96
|
+
const EMPTY = "░";
|
|
97
|
+
function progressBar(pct) {
|
|
98
|
+
const filled = Math.round(Math.min(pct, 1) * BAR_WIDTH);
|
|
99
|
+
return FILLED.repeat(filled) + EMPTY.repeat(BAR_WIDTH - filled);
|
|
100
|
+
}
|
|
101
|
+
function formatSpeed(filesPerSec) {
|
|
102
|
+
if (filesPerSec >= 1e3) return `${(filesPerSec / 1e3).toFixed(1)}k/s`;
|
|
103
|
+
return `${filesPerSec.toFixed(0)}/s`;
|
|
104
|
+
}
|
|
105
|
+
function formatDuration(ms) {
|
|
106
|
+
const s = ms / 1e3;
|
|
107
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
108
|
+
return `${Math.floor(s / 60)}m ${(s % 60).toFixed(0)}s`;
|
|
109
|
+
}
|
|
110
|
+
function formatEta(remaining, rate) {
|
|
111
|
+
if (rate <= 0) return "";
|
|
112
|
+
const secs = remaining / rate;
|
|
113
|
+
if (secs < 1) return "";
|
|
114
|
+
if (secs < 60) return ` eta ${secs.toFixed(0)}s`;
|
|
115
|
+
return ` eta ${(secs / 60).toFixed(1)}m`;
|
|
116
|
+
}
|
|
117
|
+
function elapsed(t0) {
|
|
118
|
+
return formatDuration(Date.now() - t0);
|
|
119
|
+
}
|
|
120
|
+
function formatBytes(bytes) {
|
|
121
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
122
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
123
|
+
return `${bytes} B`;
|
|
124
|
+
}
|
|
125
|
+
function basename(path) {
|
|
126
|
+
return path.replace(/\\/g, "/").split("/").at(-1) ?? path;
|
|
127
|
+
}
|
|
128
|
+
function initialProgress(total, t0) {
|
|
129
|
+
return {
|
|
130
|
+
fetched: 0,
|
|
131
|
+
total,
|
|
132
|
+
t0,
|
|
133
|
+
active: []
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function renderProgress(state) {
|
|
137
|
+
const { fetched, total, t0, active } = state;
|
|
138
|
+
const pct = total === 0 ? 1 : fetched / total;
|
|
139
|
+
const rate = fetched / (Math.max(Date.now() - t0, 1) / 1e3);
|
|
140
|
+
const overall = ` [${progressBar(pct)}] ${`${Math.round(pct * 100).toString().padStart(3)}%`} ${`${fetched}/${total}`}${rate > .1 ? ` @ ${formatSpeed(rate)}` : ""}${fetched < total ? formatEta(total - fetched, rate) : ""}`;
|
|
141
|
+
const cols = process.stderr.columns ?? 80;
|
|
142
|
+
return [overall, ...active.map((f, i) => {
|
|
143
|
+
const indent = i === active.length - 1 ? " └─ " : " ├─ ";
|
|
144
|
+
const name = basename(f.name);
|
|
145
|
+
const pctF = f.total > 0 ? f.bytes / f.total : 0;
|
|
146
|
+
const barF = progressBar(pctF);
|
|
147
|
+
const pctFStr = `${Math.round(pctF * 100).toString().padStart(3)}%`;
|
|
148
|
+
const totalFmt = f.total > 0 ? formatBytes(f.total) : "";
|
|
149
|
+
const fixed = ` [${barF}] ${pctFStr}${f.total > 0 ? ` ${formatBytes(f.bytes).padStart(totalFmt.length)}/${totalFmt}` : f.bytes > 0 ? ` ${formatBytes(f.bytes)}` : ""}`;
|
|
150
|
+
const nameBudget = cols - 1 - indent.length - fixed.length;
|
|
151
|
+
return `${indent}${nameBudget > 4 ? name.slice(0, nameBudget).padEnd(nameBudget) : ""}${fixed}`;
|
|
152
|
+
})];
|
|
153
|
+
}
|
|
154
|
+
var ProgressWriter = class {
|
|
155
|
+
lastLines = [];
|
|
156
|
+
lastNonTty = 0;
|
|
157
|
+
constructor(isTTY, nonTtyInterval = NON_TTY_INTERVAL_MS) {
|
|
158
|
+
this.isTTY = isTTY;
|
|
159
|
+
this.nonTtyInterval = nonTtyInterval;
|
|
160
|
+
}
|
|
161
|
+
clearLines() {
|
|
162
|
+
if (!this.isTTY || this.lastLines.length === 0) return;
|
|
163
|
+
process.stderr.write("\x1B[2K\r");
|
|
164
|
+
for (let i = 1; i < this.lastLines.length; i++) process.stderr.write("\x1B[1A\x1B[2K\r");
|
|
165
|
+
}
|
|
166
|
+
update(lines) {
|
|
167
|
+
if (this.isTTY) {
|
|
168
|
+
this.clearLines();
|
|
169
|
+
process.stderr.write(lines.join("\n"));
|
|
170
|
+
this.lastLines = lines;
|
|
171
|
+
} else {
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
if (now - this.lastNonTty >= this.nonTtyInterval) {
|
|
174
|
+
if (lines.length > 0) process.stderr.write(`${lines[0]}\n`);
|
|
175
|
+
this.lastNonTty = now;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
clear() {
|
|
180
|
+
if (this.isTTY && this.lastLines.length > 0) {
|
|
181
|
+
this.clearLines();
|
|
182
|
+
this.lastLines = [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
redraw() {
|
|
186
|
+
if (this.isTTY && this.lastLines.length > 0) process.stderr.write(this.lastLines.join("\n"));
|
|
187
|
+
}
|
|
188
|
+
log(line) {
|
|
189
|
+
this.clear();
|
|
190
|
+
process.stderr.write(`${line}\n`);
|
|
191
|
+
}
|
|
192
|
+
finish(lines) {
|
|
193
|
+
if (this.isTTY) {
|
|
194
|
+
this.clearLines();
|
|
195
|
+
const out = lines ?? this.lastLines;
|
|
196
|
+
if (out.length > 0) process.stderr.write(`${out.join("\n")}\n`);
|
|
197
|
+
this.lastLines = [];
|
|
198
|
+
} else if (lines && lines.length > 0) process.stderr.write(`${lines[0]}\n`);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region lib/commands/launch.ts
|
|
204
|
+
/** Minimum gap between progress redraws, in milliseconds. */
|
|
205
|
+
const RENDER_THROTTLE_MS = 80;
|
|
206
|
+
async function cmdLaunch(argv, logger, command) {
|
|
207
|
+
const args = parseArgs$1(argv, [{
|
|
208
|
+
long: "input",
|
|
209
|
+
short: "i",
|
|
210
|
+
type: "string"
|
|
211
|
+
}, {
|
|
212
|
+
long: "mode",
|
|
213
|
+
type: "string"
|
|
214
|
+
}]);
|
|
215
|
+
const inputFile = args.getString("input") ?? "opys.config.mjs";
|
|
216
|
+
const mode = args.getString("mode") ?? command;
|
|
217
|
+
const { config, configDir } = await loadConfig(inputFile, mode);
|
|
218
|
+
const baseManifest = await buildManifest(config, {
|
|
219
|
+
log: (scope, msg) => logger.info(`[${scope}] ${msg}`),
|
|
220
|
+
configDir,
|
|
221
|
+
mode
|
|
222
|
+
});
|
|
223
|
+
const manifest = config.runClient ? {
|
|
224
|
+
...baseManifest,
|
|
225
|
+
...config.runClient(baseManifest)
|
|
226
|
+
} : baseManifest;
|
|
227
|
+
const t0 = Date.now();
|
|
228
|
+
const pw = new ProgressWriter(process.stderr.isTTY ?? false);
|
|
229
|
+
logger.setProgressWriter(pw);
|
|
230
|
+
logger.info("Installing...");
|
|
231
|
+
const active = /* @__PURE__ */ new Map();
|
|
232
|
+
const state = initialProgress(0, t0);
|
|
233
|
+
let lastRender = 0;
|
|
234
|
+
const render = (force = false) => {
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
if (!force && now - lastRender < RENDER_THROTTLE_MS) return;
|
|
237
|
+
lastRender = now;
|
|
238
|
+
state.active = [...active.values()];
|
|
239
|
+
pw.update(renderProgress(state));
|
|
240
|
+
};
|
|
241
|
+
await install(manifest, { onProgress(p) {
|
|
242
|
+
switch (p.phase) {
|
|
243
|
+
case "download":
|
|
244
|
+
state.total = p.total;
|
|
245
|
+
state.fetched = p.fetched;
|
|
246
|
+
render(true);
|
|
247
|
+
break;
|
|
248
|
+
case "download:start":
|
|
249
|
+
active.set(p.path, {
|
|
250
|
+
name: p.path,
|
|
251
|
+
bytes: 0,
|
|
252
|
+
total: p.total
|
|
253
|
+
});
|
|
254
|
+
render();
|
|
255
|
+
break;
|
|
256
|
+
case "download:bytes": {
|
|
257
|
+
const entry = active.get(p.path);
|
|
258
|
+
if (entry) {
|
|
259
|
+
entry.bytes = p.bytes;
|
|
260
|
+
render();
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "download:done":
|
|
265
|
+
active.delete(p.path);
|
|
266
|
+
pw.log(` ✓ ${basename(p.path)}`);
|
|
267
|
+
break;
|
|
268
|
+
case "verify":
|
|
269
|
+
pw.finish();
|
|
270
|
+
pw.log(" Verifying...");
|
|
271
|
+
break;
|
|
272
|
+
case "extract":
|
|
273
|
+
pw.log(` Extracting ${p.count} archive${p.count === 1 ? "" : "s"}...`);
|
|
274
|
+
break;
|
|
275
|
+
case "sweep":
|
|
276
|
+
pw.log(` Swept ${p.removed} stale file${p.removed === 1 ? "" : "s"}`);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
} });
|
|
280
|
+
pw.finish();
|
|
281
|
+
logger.info(` Ready in ${elapsed(t0)}`);
|
|
282
|
+
logger.info("Launching...");
|
|
283
|
+
const child = await launch(manifest, { install: false });
|
|
284
|
+
logger.info(` PID ${child.pid}`);
|
|
285
|
+
await new Promise((res, rej) => {
|
|
286
|
+
child.on("exit", (code) => code === 0 || code === null ? res() : rej(/* @__PURE__ */ new Error(`exit ${code}`)));
|
|
287
|
+
child.on("error", rej);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region lib/logger.ts
|
|
293
|
+
const RANK = {
|
|
294
|
+
silent: 0,
|
|
295
|
+
error: 1,
|
|
296
|
+
warn: 2,
|
|
297
|
+
info: 3,
|
|
298
|
+
debug: 4
|
|
299
|
+
};
|
|
300
|
+
const LEVEL_PREFIX = {
|
|
301
|
+
error: "[error]",
|
|
302
|
+
warn: "[warn] ",
|
|
303
|
+
debug: "[debug]"
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* Level-aware logger that writes to stderr.
|
|
307
|
+
* Coordinates with a {@link ProgressWriter} to clear the progress bar before
|
|
308
|
+
* writing debug/warn lines so they don't mangle in-flight progress output.
|
|
309
|
+
*/
|
|
310
|
+
var Logger = class {
|
|
311
|
+
t0 = Date.now();
|
|
312
|
+
pw;
|
|
313
|
+
constructor(level) {
|
|
314
|
+
this.level = level;
|
|
315
|
+
}
|
|
316
|
+
/** Attach a ProgressWriter so log lines clear the bar before printing. */
|
|
317
|
+
setProgressWriter(pw) {
|
|
318
|
+
this.pw = pw;
|
|
319
|
+
}
|
|
320
|
+
error(msg) {
|
|
321
|
+
this.emit("error", msg);
|
|
322
|
+
}
|
|
323
|
+
warn(msg) {
|
|
324
|
+
this.emit("warn", msg);
|
|
325
|
+
}
|
|
326
|
+
info(msg) {
|
|
327
|
+
this.emit("info", msg);
|
|
328
|
+
}
|
|
329
|
+
debug(msg) {
|
|
330
|
+
this.emit("debug", msg);
|
|
331
|
+
}
|
|
332
|
+
/** Returns true when `level` is at or below this logger's threshold. */
|
|
333
|
+
enables(level) {
|
|
334
|
+
return RANK[level] <= RANK[this.level];
|
|
335
|
+
}
|
|
336
|
+
/** Returns an `InstallOptions`-compatible log callback. */
|
|
337
|
+
installerLog() {
|
|
338
|
+
return (level, msg) => this.emit(level, msg);
|
|
339
|
+
}
|
|
340
|
+
emit(level, msg) {
|
|
341
|
+
if (!this.enables(level)) return;
|
|
342
|
+
this.pw?.clear();
|
|
343
|
+
const prefix = LEVEL_PREFIX[level];
|
|
344
|
+
const line = prefix ? `${prefix} +${Date.now() - this.t0}ms ${msg}\n` : `${msg}\n`;
|
|
345
|
+
process.stderr.write(line);
|
|
346
|
+
this.pw?.redraw();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
/** Parse a log level from a string, defaulting to 'info' on unknown values. */
|
|
350
|
+
function parseLogLevel(raw) {
|
|
351
|
+
if (!raw) return "info";
|
|
352
|
+
if (Object.prototype.hasOwnProperty.call(RANK, raw)) return raw;
|
|
353
|
+
process.stderr.write(`[warn] Unknown log level '${raw}', defaulting to 'info'\n`);
|
|
354
|
+
return "info";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region bin/opys.ts
|
|
359
|
+
const USAGE = `\
|
|
360
|
+
opys — declarative manifest toolkit
|
|
361
|
+
|
|
362
|
+
USAGE
|
|
363
|
+
opys build [-i <opys.config.mjs>] [-o <out>] [--mode <m>] Build manifest
|
|
364
|
+
opys launch [-i <opys.config.mjs>] [--mode <m>] Build, install, launch
|
|
365
|
+
|
|
366
|
+
OPTIONS
|
|
367
|
+
-i, --input Config file (default: opys.config.mjs)
|
|
368
|
+
-o, --output Output file (default: stdout for build)
|
|
369
|
+
--mode <value> Mode passed to config function (default: command name)
|
|
370
|
+
--log-level <level> Log verbosity: silent|error|warn|info|debug (default: info)
|
|
371
|
+
-v Shorthand for --log-level debug
|
|
372
|
+
|
|
373
|
+
EXIT CODES
|
|
374
|
+
0 Success
|
|
375
|
+
1 Usage or config error
|
|
376
|
+
2 Network error
|
|
377
|
+
3 Integrity check failure
|
|
378
|
+
4 Extraction failure
|
|
379
|
+
`;
|
|
380
|
+
const COMMANDS = {
|
|
381
|
+
build: cmdBuild,
|
|
382
|
+
launch: cmdLaunch
|
|
383
|
+
};
|
|
384
|
+
/** Strip global flags from argv, returning the cleaned args and extracted values. */
|
|
385
|
+
function extractGlobals(argv) {
|
|
386
|
+
const args = [];
|
|
387
|
+
let logLevel = "info";
|
|
388
|
+
let i = 0;
|
|
389
|
+
while (i < argv.length) {
|
|
390
|
+
const token = argv[i++];
|
|
391
|
+
if (token === "--log-level") logLevel = parseLogLevel(argv[i++]);
|
|
392
|
+
else if (token === "-v") logLevel = "debug";
|
|
393
|
+
else args.push(token);
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
args,
|
|
397
|
+
logLevel
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async function main() {
|
|
401
|
+
const [, , ...allArgs] = process.argv;
|
|
402
|
+
const { args, logLevel } = extractGlobals(allArgs ?? []);
|
|
403
|
+
const [command, ...rest] = args;
|
|
404
|
+
const logger = new Logger(logLevel);
|
|
405
|
+
if (!command || command === "--help" || command === "-h") {
|
|
406
|
+
process.stdout.write(USAGE);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const handler = COMMANDS[command];
|
|
410
|
+
if (!handler) {
|
|
411
|
+
process.stderr.write(`Unknown command '${command}'\n\n${USAGE}`);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
await handler(rest, logger, command);
|
|
415
|
+
}
|
|
416
|
+
main().catch((err) => {
|
|
417
|
+
if (err instanceof UsageError) {
|
|
418
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
if (err instanceof NetworkError || err instanceof VersionFetchError) {
|
|
422
|
+
process.stderr.write(`Network error: ${err.message}\n`);
|
|
423
|
+
process.exit(2);
|
|
424
|
+
}
|
|
425
|
+
if (err instanceof IntegrityError) {
|
|
426
|
+
process.stderr.write(`Integrity check failed:\n`);
|
|
427
|
+
for (const p of err.paths) process.stderr.write(` ${p}\n`);
|
|
428
|
+
process.exit(3);
|
|
429
|
+
}
|
|
430
|
+
if (err instanceof ExtractionError) {
|
|
431
|
+
process.stderr.write(`Extraction failed: ${err.message}\n`);
|
|
432
|
+
if (err.cause instanceof Error) process.stderr.write(` caused by: ${err.cause.message}\n`);
|
|
433
|
+
process.exit(4);
|
|
434
|
+
}
|
|
435
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
436
|
+
process.stderr.write(`Unexpected error: ${msg}\n`);
|
|
437
|
+
if (err instanceof Error && err.cause instanceof Error) {
|
|
438
|
+
process.stderr.write(` caused by: ${err.cause.message}\n`);
|
|
439
|
+
if (err.cause.stack && !process.env.OPYS_QUIET) process.stderr.write(`${err.cause.stack}\n`);
|
|
440
|
+
}
|
|
441
|
+
if (err instanceof Error && err.stack && !process.env.OPYS_QUIET) process.stderr.write(`${err.stack}\n`);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
//#endregion
|
|
446
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opys/cli",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"bin": {
|
|
5
|
+
"opys": "dist/opys.mjs"
|
|
6
|
+
},
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsdown --config tsdown.config.ts",
|
|
12
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
13
|
+
"test": "vitest run tests/unit --passWithNoTests",
|
|
14
|
+
"test:int": "vitest run tests/integration --testTimeout=180000"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@opys/core": "^0.1.2",
|
|
19
|
+
"@opys/dev": "^0.1.2",
|
|
20
|
+
"@opys/runtime": "^0.1.2",
|
|
21
|
+
"@opys/minecraft": "^0.1.2",
|
|
22
|
+
"@opys/mojang": "^0.1.2"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"zod": "^4.0.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/harmoniya-net/opys.git",
|
|
33
|
+
"directory": "packages/cli"
|
|
34
|
+
}
|
|
35
|
+
}
|