@chaja/cli 0.0.0-stage → 0.2.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.
- package/README.md +13 -2
- package/dist/main.mjs +249 -0
- package/package.json +40 -3
package/README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @chaja/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create releases and upload sourcemaps to a self-hosted [Chajá](https://github.com/emiliodominguez/chaja) server from CI.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add -D @chaja/cli
|
|
7
|
+
|
|
8
|
+
export CHAJA_URL=https://chaja.example.com CHAJA_TOKEN=<API token> CHAJA_PROJECT=<project slug>
|
|
9
|
+
chaja releases new "$RELEASE"
|
|
10
|
+
chaja sourcemaps upload ./dist --release "$RELEASE"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
API tokens are created under Account in the Chajá web app. Every `*.map` under the directory is uploaded as `~/<path>`, which
|
|
14
|
+
matches the bundle on any origin; change it with `--url-prefix`. Requires Node.js 22+.
|
package/dist/main.mjs
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { join, posix, relative, sep } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
//#region src/messages.ts
|
|
6
|
+
/**
|
|
7
|
+
* Every line the CLI prints. Kept in one catalog so the CLI can be translated without touching command code.
|
|
8
|
+
*/
|
|
9
|
+
const messages = {
|
|
10
|
+
usage: [
|
|
11
|
+
"Usage: chaja <command> [options]",
|
|
12
|
+
"",
|
|
13
|
+
"Commands:",
|
|
14
|
+
" releases new <version> Create a release",
|
|
15
|
+
" sourcemaps upload <dir> --release <v> Upload every *.map under <dir>",
|
|
16
|
+
"",
|
|
17
|
+
"Options:",
|
|
18
|
+
" --url <url> Chajá server (CHAJA_URL)",
|
|
19
|
+
" --token <token> API token (CHAJA_TOKEN)",
|
|
20
|
+
" --project <slug> Project slug (CHAJA_PROJECT)",
|
|
21
|
+
" --url-prefix <p> Prefix for uploaded file names, default ~/ (any origin)"
|
|
22
|
+
].join("\n"),
|
|
23
|
+
missingOption(option, variable) {
|
|
24
|
+
return `Missing --${option} (or ${variable}).`;
|
|
25
|
+
},
|
|
26
|
+
missingArgument(name) {
|
|
27
|
+
return `Missing <${name}>.`;
|
|
28
|
+
},
|
|
29
|
+
unknownCommand(command) {
|
|
30
|
+
return `Unknown command: ${command}. Run "chaja help".`;
|
|
31
|
+
},
|
|
32
|
+
releaseCreated(version) {
|
|
33
|
+
return `Release ${version} is ready.`;
|
|
34
|
+
},
|
|
35
|
+
noSourcemaps(directory) {
|
|
36
|
+
return `No *.map files found under ${directory}.`;
|
|
37
|
+
},
|
|
38
|
+
uploaded(name, bytes) {
|
|
39
|
+
return `Uploaded ${name} (${bytes} bytes)`;
|
|
40
|
+
},
|
|
41
|
+
uploadSummary(count, version) {
|
|
42
|
+
return `${count} sourcemap(s) uploaded to release ${version}.`;
|
|
43
|
+
},
|
|
44
|
+
requestFailed(status, code) {
|
|
45
|
+
return `Request failed with ${status}${code ? ` (${code})` : ""}.`;
|
|
46
|
+
},
|
|
47
|
+
unreachable(url) {
|
|
48
|
+
return `Could not reach ${url}.`;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/cli.ts
|
|
53
|
+
const EXIT = {
|
|
54
|
+
ok: 0,
|
|
55
|
+
failed: 1,
|
|
56
|
+
usage: 2
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Raised for problems the user can fix; printed without a stack.
|
|
60
|
+
*/
|
|
61
|
+
var CliError = class extends Error {
|
|
62
|
+
exitCode;
|
|
63
|
+
/**
|
|
64
|
+
* Create a user-facing failure.
|
|
65
|
+
*
|
|
66
|
+
* @param message - Line to print.
|
|
67
|
+
* @param exitCode - Process exit code.
|
|
68
|
+
*/
|
|
69
|
+
constructor(message, exitCode) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.exitCode = exitCode;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Find every `.map` file under a directory.
|
|
76
|
+
*
|
|
77
|
+
* @param directory - Root directory.
|
|
78
|
+
* @returns Absolute paths.
|
|
79
|
+
*/
|
|
80
|
+
async function findSourcemaps(directory) {
|
|
81
|
+
return (await readdir(directory, {
|
|
82
|
+
recursive: true,
|
|
83
|
+
withFileTypes: true
|
|
84
|
+
})).filter(function isMap(entry) {
|
|
85
|
+
return entry.isFile() && entry.name.endsWith(".map");
|
|
86
|
+
}).map(function path(entry) {
|
|
87
|
+
return join(entry.parentPath, entry.name);
|
|
88
|
+
}).toSorted();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The name a map is stored under: the URL path of the generated file it describes, under the prefix.
|
|
92
|
+
*
|
|
93
|
+
* @param directory - Upload root.
|
|
94
|
+
* @param mapPath - Absolute map path.
|
|
95
|
+
* @param map - Parsed map, whose `file` names the generated file.
|
|
96
|
+
* @param prefix - URL prefix such as `~/` or `~/static`.
|
|
97
|
+
* @returns Artifact name like `~/assets/index-abc.js`.
|
|
98
|
+
*/
|
|
99
|
+
function artifactName(directory, mapPath, map, prefix) {
|
|
100
|
+
const relativeMap = relative(directory, mapPath).split(sep).join("/");
|
|
101
|
+
const folder = posix.dirname(relativeMap);
|
|
102
|
+
const generated = typeof map.file === "string" && map.file.length > 0 ? posix.join(folder, posix.basename(map.file)) : relativeMap.replace(/\.map$/u, "");
|
|
103
|
+
return `${prefix.endsWith("/") ? prefix : `${prefix}/`}${generated.replace(/^\.\//u, "")}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Call the Chajá API with the token, turning failures into CLI errors.
|
|
107
|
+
*
|
|
108
|
+
* @param io - Injected I/O.
|
|
109
|
+
* @param connection - Server, token and project.
|
|
110
|
+
* @param path - Path under `/api/v1`.
|
|
111
|
+
* @param init - Fetch options.
|
|
112
|
+
* @returns Parsed JSON body.
|
|
113
|
+
*/
|
|
114
|
+
async function call(io, connection, path, init) {
|
|
115
|
+
const url = `${connection.url}/api/v1${path}`;
|
|
116
|
+
let response;
|
|
117
|
+
try {
|
|
118
|
+
const headers = new Headers(init.headers);
|
|
119
|
+
headers.set("authorization", `Bearer ${connection.token}`);
|
|
120
|
+
response = await io.fetch(url, {
|
|
121
|
+
...init,
|
|
122
|
+
headers
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
throw new CliError(messages.unreachable(connection.url), EXIT.failed);
|
|
126
|
+
}
|
|
127
|
+
const text = await response.text();
|
|
128
|
+
const body = text ? JSON.parse(text) : {};
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const code = typeof body === "object" && body !== null && "code" in body && typeof body.code === "string" ? body.code : "";
|
|
131
|
+
throw new CliError(messages.requestFailed(response.status, code), EXIT.failed);
|
|
132
|
+
}
|
|
133
|
+
return body;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Run the CLI.
|
|
137
|
+
*
|
|
138
|
+
* @param argv - Arguments after the executable.
|
|
139
|
+
* @param io - Fetch, environment and output.
|
|
140
|
+
* @returns Exit code.
|
|
141
|
+
*/
|
|
142
|
+
async function run(argv, io) {
|
|
143
|
+
try {
|
|
144
|
+
const { values, positionals } = parseArgs({
|
|
145
|
+
args: argv,
|
|
146
|
+
allowPositionals: true,
|
|
147
|
+
options: {
|
|
148
|
+
url: { type: "string" },
|
|
149
|
+
token: { type: "string" },
|
|
150
|
+
project: { type: "string" },
|
|
151
|
+
release: { type: "string" },
|
|
152
|
+
"url-prefix": {
|
|
153
|
+
type: "string",
|
|
154
|
+
default: "~/"
|
|
155
|
+
},
|
|
156
|
+
help: {
|
|
157
|
+
type: "boolean",
|
|
158
|
+
short: "h"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
const [group, command, argument] = positionals;
|
|
163
|
+
if (values.help || !group || group === "help") {
|
|
164
|
+
io.stdout(messages.usage);
|
|
165
|
+
return EXIT.ok;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Read a required setting from a flag or environment variable.
|
|
169
|
+
*
|
|
170
|
+
* @param option - Flag name.
|
|
171
|
+
* @param variable - Environment variable.
|
|
172
|
+
* @param value - Flag value.
|
|
173
|
+
* @returns The setting.
|
|
174
|
+
*/
|
|
175
|
+
function required(option, variable, value) {
|
|
176
|
+
const resolved = value ?? io.env[variable];
|
|
177
|
+
if (!resolved) throw new CliError(messages.missingOption(option, variable), EXIT.usage);
|
|
178
|
+
return resolved;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Resolve server, token and project, failing on the first missing one.
|
|
182
|
+
*
|
|
183
|
+
* @returns Connection settings.
|
|
184
|
+
*/
|
|
185
|
+
function connection() {
|
|
186
|
+
return {
|
|
187
|
+
url: required("url", "CHAJA_URL", values.url).replace(/\/+$/u, ""),
|
|
188
|
+
token: required("token", "CHAJA_TOKEN", values.token),
|
|
189
|
+
project: required("project", "CHAJA_PROJECT", values.project)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (group === "releases" && command === "new") {
|
|
193
|
+
if (!argument) throw new CliError(messages.missingArgument("version"), EXIT.usage);
|
|
194
|
+
const target = connection();
|
|
195
|
+
await call(io, target, `/projects/${encodeURIComponent(target.project)}/releases`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: { "content-type": "application/json" },
|
|
198
|
+
body: JSON.stringify({ version: argument })
|
|
199
|
+
});
|
|
200
|
+
io.stdout(messages.releaseCreated(argument));
|
|
201
|
+
return EXIT.ok;
|
|
202
|
+
}
|
|
203
|
+
if (group === "sourcemaps" && command === "upload") {
|
|
204
|
+
if (!argument) throw new CliError(messages.missingArgument("dir"), EXIT.usage);
|
|
205
|
+
const target = connection();
|
|
206
|
+
const version = required("release", "CHAJA_RELEASE", values.release);
|
|
207
|
+
const maps = await findSourcemaps(argument);
|
|
208
|
+
if (maps.length === 0) throw new CliError(messages.noSourcemaps(argument), EXIT.failed);
|
|
209
|
+
for (const mapPath of maps) {
|
|
210
|
+
const bytes = await readFile(mapPath);
|
|
211
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
212
|
+
const name = artifactName(argument, mapPath, typeof parsed === "object" && parsed !== null ? parsed : {}, values["url-prefix"]);
|
|
213
|
+
await call(io, target, `/projects/${encodeURIComponent(target.project)}/releases/${encodeURIComponent(version)}/files?name=${encodeURIComponent(name)}`, {
|
|
214
|
+
method: "PUT",
|
|
215
|
+
headers: { "content-type": "application/json" },
|
|
216
|
+
body: bytes
|
|
217
|
+
});
|
|
218
|
+
io.stdout(messages.uploaded(name, bytes.byteLength));
|
|
219
|
+
}
|
|
220
|
+
io.stdout(messages.uploadSummary(maps.length, version));
|
|
221
|
+
return EXIT.ok;
|
|
222
|
+
}
|
|
223
|
+
throw new CliError(messages.unknownCommand(positionals.join(" ")), EXIT.usage);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof CliError) {
|
|
226
|
+
io.stderr(error.message);
|
|
227
|
+
return error.exitCode;
|
|
228
|
+
}
|
|
229
|
+
if (error instanceof TypeError && "code" in error && typeof error.code === "string" && error.code.startsWith("ERR_PARSE_ARGS")) {
|
|
230
|
+
io.stderr(error.message);
|
|
231
|
+
return EXIT.usage;
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/main.ts
|
|
238
|
+
process.exitCode = await run(process.argv.slice(2), {
|
|
239
|
+
fetch,
|
|
240
|
+
env: process.env,
|
|
241
|
+
stdout(line) {
|
|
242
|
+
process.stdout.write(`${line}\n`);
|
|
243
|
+
},
|
|
244
|
+
stderr(line) {
|
|
245
|
+
process.stderr.write(`${line}\n`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
//#endregion
|
|
249
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaja/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Chajá command line: create releases and upload sourcemaps from CI.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"chaja",
|
|
7
|
+
"sourcemaps",
|
|
8
|
+
"cli"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/emiliodominguez/chaja#readme",
|
|
11
|
+
"bugs": "https://github.com/emiliodominguez/chaja/issues",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/emiliodominguez/chaja.git",
|
|
15
|
+
"directory": "apps/cli"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"nx": {
|
|
19
|
+
"tags": [
|
|
20
|
+
"scope:app",
|
|
21
|
+
"type:cli"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"chaja": "./dist/main.mjs"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"tsdown": "0.23.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown src/main.ts --format esm --platform node --target node22 --out-dir dist --no-dts --clean",
|
|
41
|
+
"typecheck": "tsc --noEmit -p ."
|
|
42
|
+
}
|
|
6
43
|
}
|