@colony2/c2j 0.0.1 → 0.0.3
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/LICENSE +661 -0
- package/README.md +453 -0
- package/install.js +6 -0
- package/lib.js +259 -0
- package/package.json +88 -9
- package/run-c2j.js +6 -0
- package/index.js +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# c2j
|
|
2
|
+
|
|
3
|
+
`c2j` is the local job-oriented CLI for submitting and running recipe jobs through an SWF runtime.
|
|
4
|
+
|
|
5
|
+
Use it when you want to:
|
|
6
|
+
|
|
7
|
+
- submit a recipe job from a named recipe or a local recipe file
|
|
8
|
+
- run or continue an existing job
|
|
9
|
+
- use the embedded local runtime for fast iteration
|
|
10
|
+
- inspect the current cell configuration used for job targeting
|
|
11
|
+
- list jobs for a cell
|
|
12
|
+
|
|
13
|
+
Examples below assume you are running from the repo root.
|
|
14
|
+
|
|
15
|
+
## Command Summary
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
c2j self
|
|
19
|
+
c2j cells
|
|
20
|
+
c2j init
|
|
21
|
+
c2j submit
|
|
22
|
+
c2j exec
|
|
23
|
+
c2j list
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use `go run ./cmd/c2j --help` or `c2j --help` to see the full command tree.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### 1. Check current-cell resolution
|
|
31
|
+
|
|
32
|
+
`submit` targets the current cell by default. That usually comes from `.c2j/config.yaml`, but supported project types can also be auto-detected.
|
|
33
|
+
|
|
34
|
+
Inspect the resolved config:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
c2j self
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
List allowed dependent cells:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
c2j cells
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Generate a starter config if needed:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
c2j init --stdout
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If the current directory does not resolve as a cell, either:
|
|
53
|
+
|
|
54
|
+
- create `.c2j/config.yaml`, or
|
|
55
|
+
- pass `--cell <repo-or-path>` explicitly to `submit` or `list`
|
|
56
|
+
|
|
57
|
+
### 2. Submit and run a local recipe file
|
|
58
|
+
|
|
59
|
+
For local authoring, this is the default loop:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
c2j submit \
|
|
63
|
+
--recipe-file ./recipes/my-recipe.yaml \
|
|
64
|
+
--run \
|
|
65
|
+
--embed
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
That does all of the following:
|
|
69
|
+
|
|
70
|
+
- loads the local YAML file
|
|
71
|
+
- embeds that recipe into the submitted job
|
|
72
|
+
- starts an embedded SWF runtime
|
|
73
|
+
- submits the job
|
|
74
|
+
- immediately executes it
|
|
75
|
+
|
|
76
|
+
### 3. Continue or inspect a job later
|
|
77
|
+
|
|
78
|
+
Find recent jobs for the current cell:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
c2j list --self --embed
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Continue a submitted job:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
c2j exec --job-id <job-id> --embed
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Current Cell Commands
|
|
91
|
+
|
|
92
|
+
### `c2j self`
|
|
93
|
+
|
|
94
|
+
Shows how `c2j` resolves the current cell from `.c2j/config.yaml` or supported auto-detection.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
c2j self
|
|
98
|
+
c2j self --json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Fields include:
|
|
102
|
+
|
|
103
|
+
- `short_name`
|
|
104
|
+
- `repo`
|
|
105
|
+
- `ref`
|
|
106
|
+
- `root_repo`
|
|
107
|
+
- `root_ref`
|
|
108
|
+
- `pattern`
|
|
109
|
+
|
|
110
|
+
### `c2j cells`
|
|
111
|
+
|
|
112
|
+
Lists dependent cells allowed by the current config.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
c2j cells
|
|
116
|
+
c2j cells --json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This is mainly useful when you want to target another cell by short name and need to verify how config expands it.
|
|
120
|
+
|
|
121
|
+
### `c2j init`
|
|
122
|
+
|
|
123
|
+
Writes a commented `.c2j/config.yaml` template.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
c2j init
|
|
127
|
+
c2j init --stdout
|
|
128
|
+
c2j init --force
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The generated template can derive values from the Go module in the current repo when `base: go` is appropriate.
|
|
132
|
+
|
|
133
|
+
## Submitting Jobs
|
|
134
|
+
|
|
135
|
+
### Basic forms
|
|
136
|
+
|
|
137
|
+
Submit a named recipe:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
c2j submit --recipe default --embed
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`--recipe` accepts a recipe name or git selector. For local files, prefer `--recipe-file`.
|
|
144
|
+
|
|
145
|
+
Submit a local recipe file:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
c2j submit --recipe-file ./recipes/my-recipe.yaml --embed
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Submit and run immediately:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
c2j submit --recipe-file ./recipes/my-recipe.yaml --run --embed
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
By default, if neither `--recipe` nor `--recipe-file` is set, `c2j` submits the recipe named `default`.
|
|
158
|
+
|
|
159
|
+
### Passing inputs
|
|
160
|
+
|
|
161
|
+
Inline JSON:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
c2j submit \
|
|
165
|
+
--recipe-file ./recipes/my-recipe.yaml \
|
|
166
|
+
--inputs-json '{"message":"hello"}' \
|
|
167
|
+
--run \
|
|
168
|
+
--embed
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Inputs file in JSON or YAML:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
c2j submit \
|
|
175
|
+
--recipe-file ./recipes/my-recipe.yaml \
|
|
176
|
+
--inputs-file ./recipes/test-inputs.yaml \
|
|
177
|
+
--run \
|
|
178
|
+
--embed
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Positional prompt shortcut:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
c2j submit "Summarize the repo" --recipe my-prompt-recipe --embed
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The positional argument is merged as `inputs.prompt`.
|
|
188
|
+
|
|
189
|
+
Rules:
|
|
190
|
+
|
|
191
|
+
- `--inputs-json` and `--inputs-file` are mutually exclusive
|
|
192
|
+
- the positional prompt cannot also be provided as `inputs.prompt`
|
|
193
|
+
|
|
194
|
+
### Choosing the target cell
|
|
195
|
+
|
|
196
|
+
Use the current cell:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
c2j submit --recipe-file ./recipes/my-recipe.yaml --self --embed
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Use another cell explicitly:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
c2j submit \
|
|
206
|
+
--recipe-file ./recipes/my-recipe.yaml \
|
|
207
|
+
--cell github.com/colony-2/root \
|
|
208
|
+
--embed
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`--cell` accepts:
|
|
212
|
+
|
|
213
|
+
- a canonical repo string
|
|
214
|
+
- a clone URL
|
|
215
|
+
- a local repository path
|
|
216
|
+
- a configured short name when `.c2j/config.yaml` defines a pattern
|
|
217
|
+
|
|
218
|
+
Rules:
|
|
219
|
+
|
|
220
|
+
- `--self` and `--cell` are mutually exclusive
|
|
221
|
+
- if no `--cell` is given, `c2j` behaves as if you targeted `--self`
|
|
222
|
+
|
|
223
|
+
### Getting machine-readable output
|
|
224
|
+
|
|
225
|
+
If you only want the submitted job identity:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
c2j submit \
|
|
229
|
+
--recipe-file ./recipes/my-recipe.yaml \
|
|
230
|
+
--json \
|
|
231
|
+
--embed
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
This emits:
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"tenant_id": "0",
|
|
239
|
+
"job_id": "job-...",
|
|
240
|
+
"recipe": "my_recipe_id"
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Note:
|
|
245
|
+
|
|
246
|
+
- `--json` and `--run` are mutually exclusive
|
|
247
|
+
|
|
248
|
+
## Running Jobs
|
|
249
|
+
|
|
250
|
+
`c2j exec` executes or continues an existing job and prints live story progress to stdout.
|
|
251
|
+
|
|
252
|
+
Basic usage:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
c2j exec --job-id <job-id> --embed
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Common variants:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
c2j exec --job-id <job-id> --wait-timeout 30m --embed
|
|
262
|
+
c2j exec --job-id <job-id> --input-mode fail --embed
|
|
263
|
+
c2j exec --job-id <job-id> --ci --embed
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Important behavior:
|
|
267
|
+
|
|
268
|
+
- completed jobs return successfully
|
|
269
|
+
- failed jobs return a non-zero exit code
|
|
270
|
+
- suspended jobs may wait, prompt, or fail depending on flags
|
|
271
|
+
- when input is pending, interactive terminals default to prompting
|
|
272
|
+
- in CI or non-terminal mode, input handling defaults to `ops`
|
|
273
|
+
|
|
274
|
+
### Input handling
|
|
275
|
+
|
|
276
|
+
`--input-mode` controls what happens when a job is blocked on user input:
|
|
277
|
+
|
|
278
|
+
- `prompt`
|
|
279
|
+
Prompt on stdin/stdout and submit the response
|
|
280
|
+
- `ops`
|
|
281
|
+
Emit machine-readable `input_required` JSON and exit non-zero
|
|
282
|
+
- `fail`
|
|
283
|
+
Exit immediately when input is required
|
|
284
|
+
|
|
285
|
+
`--ci` enables machine-readable input-required behavior without prompting.
|
|
286
|
+
|
|
287
|
+
### Not-ready handling
|
|
288
|
+
|
|
289
|
+
`--on-not-ready` controls how `exec` reacts when a job is not runnable yet:
|
|
290
|
+
|
|
291
|
+
- `wait`
|
|
292
|
+
- `fail`
|
|
293
|
+
- `fail-on-lease`
|
|
294
|
+
- `fail-on-pending-jobs`
|
|
295
|
+
- `fail-on-future`
|
|
296
|
+
- `fail-on-missing-capability`
|
|
297
|
+
|
|
298
|
+
With the default `wait`, `exec` will print `waiting: ...` lines and poll until the job becomes runnable or the wait timeout is reached.
|
|
299
|
+
|
|
300
|
+
### Exit codes
|
|
301
|
+
|
|
302
|
+
`c2j exec` uses distinct exit codes:
|
|
303
|
+
|
|
304
|
+
- `1`: general failure or job failure
|
|
305
|
+
- `2`: wait timeout
|
|
306
|
+
- `3`: input required
|
|
307
|
+
- `4`: job not runnable under the selected policy
|
|
308
|
+
- `5`: invalid job identity or invalid exec arguments
|
|
309
|
+
|
|
310
|
+
## Listing Jobs
|
|
311
|
+
|
|
312
|
+
List jobs for the current cell:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
c2j list --self --embed
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
List as JSON:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
c2j list --self --json --embed
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Filter by status:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
c2j list --self --status pending_jobs --status active --embed
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
List jobs for another cell:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
c2j list --cell github.com/colony-2/root --embed
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Useful filters:
|
|
337
|
+
|
|
338
|
+
- `--job-id`
|
|
339
|
+
- `--job-type`
|
|
340
|
+
- `--status`
|
|
341
|
+
- `--waiting-for`
|
|
342
|
+
- `--created-after`
|
|
343
|
+
- `--created-before`
|
|
344
|
+
- `--page-size`
|
|
345
|
+
- `--page-token`
|
|
346
|
+
- `--all`
|
|
347
|
+
|
|
348
|
+
## Embedded Runtime
|
|
349
|
+
|
|
350
|
+
`--embed` is shorthand for:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
--swf-url embed:///
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Use it when you want a local self-contained runtime instead of an external SWF server.
|
|
357
|
+
|
|
358
|
+
Behavior:
|
|
359
|
+
|
|
360
|
+
- starts embedded Postgres and Strata as needed
|
|
361
|
+
- uses a persistent runtime root on disk
|
|
362
|
+
- works well for local recipe authoring and debugging
|
|
363
|
+
|
|
364
|
+
Defaults:
|
|
365
|
+
|
|
366
|
+
- runtime URL: `embed:///`
|
|
367
|
+
- runtime root: `~/.c2j/embed/default`
|
|
368
|
+
|
|
369
|
+
You can override the root with:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
export C2J_EMBED_ROOT=/absolute/path/to/embed-root
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Notes:
|
|
376
|
+
|
|
377
|
+
- `C2J_EMBED_ROOT` must be an absolute path
|
|
378
|
+
- only one `c2j` process can own a given embedded runtime root at a time
|
|
379
|
+
- if you need parallel embedded runtimes, give each process a different `C2J_EMBED_ROOT`
|
|
380
|
+
|
|
381
|
+
## Runtime and Tenant Defaults
|
|
382
|
+
|
|
383
|
+
`c2j` reads these environment variables:
|
|
384
|
+
|
|
385
|
+
- `C2J_SWF_URL`
|
|
386
|
+
- `C2J_TENANT_ID`
|
|
387
|
+
- `C2J_EMBED_ROOT`
|
|
388
|
+
|
|
389
|
+
Built-in defaults:
|
|
390
|
+
|
|
391
|
+
- `C2J_SWF_URL`: `http://localhost:9047`
|
|
392
|
+
- `C2J_TENANT_ID`: `0`
|
|
393
|
+
|
|
394
|
+
Examples:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
export C2J_SWF_URL=http://localhost:9047
|
|
398
|
+
export C2J_TENANT_ID=123
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
When `--embed` is present, it overrides `C2J_SWF_URL` for that command.
|
|
402
|
+
|
|
403
|
+
## Common Workflows
|
|
404
|
+
|
|
405
|
+
### Local recipe authoring loop
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
c2j self
|
|
409
|
+
c2j submit --recipe-file ./recipes/my-recipe.yaml --run --embed
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Detached submit, then later run
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
c2j submit --recipe-file ./recipes/my-recipe.yaml --json --embed
|
|
416
|
+
c2j exec --job-id <job-id> --embed
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Run against a remote runtime instead of embed
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
c2j submit \
|
|
423
|
+
--recipe-file ./recipes/my-recipe.yaml \
|
|
424
|
+
--swf-url http://localhost:9047 \
|
|
425
|
+
--run
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Target another cell explicitly
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
c2j submit \
|
|
432
|
+
--recipe-file ./recipes/my-recipe.yaml \
|
|
433
|
+
--cell github.com/colony-2/root \
|
|
434
|
+
--run \
|
|
435
|
+
--embed
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Gotchas
|
|
439
|
+
|
|
440
|
+
- `--recipe` and `--recipe-file` are mutually exclusive
|
|
441
|
+
- `--json` and `--run` are mutually exclusive on `submit`
|
|
442
|
+
- `--inputs-json` and `--inputs-file` are mutually exclusive
|
|
443
|
+
- `--self` and `--cell` are mutually exclusive
|
|
444
|
+
- `self`, `cells`, and implicit current-cell submission depend on config or supported auto-detection succeeding
|
|
445
|
+
- short cell names require a config pattern; without config, use an explicit repo or path
|
|
446
|
+
- `--recipe-file` is clearer than passing a local file path through `--recipe`
|
|
447
|
+
|
|
448
|
+
## Related Files
|
|
449
|
+
|
|
450
|
+
- command entrypoint: [main.go](main.go)
|
|
451
|
+
- embedded runtime notes: [embed-swf-mode-spec.md](embed-swf-mode-spec.md)
|
|
452
|
+
- recipe authoring docs: [RECIPE_AUTHORING_GUIDE.md](../../recipes/guides/RECIPE_AUTHORING_GUIDE.md)
|
|
453
|
+
|
package/install.js
ADDED
package/lib.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// This file was generated by GoReleaser. DO NOT EDIT.
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import https from "https";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import JSZip from "jszip";
|
|
8
|
+
import { x as tarExtract } from "tar";
|
|
9
|
+
import { ProxyAgent } from "proxy-agent";
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
const { archives, name, version } = JSON.parse(
|
|
14
|
+
fs.readFileSync(new URL("./package.json", import.meta.url), "utf8"),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const agent = new ProxyAgent();
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
const getArchive = () => {
|
|
21
|
+
let target = `${process.platform}-${process.arch}`;
|
|
22
|
+
const archive = archives[target];
|
|
23
|
+
if (!archive) {
|
|
24
|
+
throw new Error(`No archive available for ${target}`);
|
|
25
|
+
}
|
|
26
|
+
return archive;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const binDir = path.join(__dirname, "bin");
|
|
30
|
+
|
|
31
|
+
async function extractTar(tarPath, binaries, dir, wrappedIn) {
|
|
32
|
+
try {
|
|
33
|
+
const filesToExtract = wrappedIn
|
|
34
|
+
? binaries.map((bin) =>
|
|
35
|
+
path.join(wrappedIn, bin).replace(/\\/g, "/"),
|
|
36
|
+
)
|
|
37
|
+
: binaries;
|
|
38
|
+
|
|
39
|
+
await tarExtract({
|
|
40
|
+
file: tarPath,
|
|
41
|
+
cwd: dir,
|
|
42
|
+
filter: (path) => filesToExtract.includes(path),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// If wrapped, move files from wrapped directory to bin directory
|
|
46
|
+
if (wrappedIn) {
|
|
47
|
+
const wrappedDir = path.join(dir, wrappedIn);
|
|
48
|
+
for (const binary of binaries) {
|
|
49
|
+
const srcPath = path.join(wrappedDir, binary);
|
|
50
|
+
const destPath = path.join(dir, binary);
|
|
51
|
+
if (fs.existsSync(srcPath)) {
|
|
52
|
+
fs.renameSync(srcPath, destPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Clean up empty wrapped directory
|
|
56
|
+
try {
|
|
57
|
+
fs.rmSync(wrappedDir, { recursive: true, force: true });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// Ignore cleanup errors
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`Successfully extracted ${binaries} to "${dir}"`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
throw new Error(`Extraction failed: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function extractZip(zipPath, binaries, dir, wrappedIn) {
|
|
70
|
+
try {
|
|
71
|
+
const zipData = fs.readFileSync(zipPath);
|
|
72
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
73
|
+
|
|
74
|
+
for (const binary of binaries) {
|
|
75
|
+
const binaryPath = wrappedIn
|
|
76
|
+
? path.join(wrappedIn, binary).replace(/\\/g, "/")
|
|
77
|
+
: binary;
|
|
78
|
+
|
|
79
|
+
if (!zip.files[binaryPath]) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Error: ${binaryPath} does not exist in ${zipPath}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const content = await zip.files[binaryPath].async("nodebuffer");
|
|
86
|
+
if (!fs.existsSync(dir)) {
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
const file = path.join(dir, binary);
|
|
90
|
+
fs.writeFileSync(file, content);
|
|
91
|
+
fs.chmodSync(file, "755");
|
|
92
|
+
console.log(`Successfully extracted "${binary}" to "${dir}"`);
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(`Extraction failed: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const run = async (bin) => {
|
|
100
|
+
await install();
|
|
101
|
+
if (process.platform === "win32") {
|
|
102
|
+
bin += ".exe";
|
|
103
|
+
}
|
|
104
|
+
const [, , ...args] = process.argv;
|
|
105
|
+
let result = spawnSync(path.join(binDir, bin), args, {
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
stdio: "inherit",
|
|
108
|
+
});
|
|
109
|
+
if (result.error) {
|
|
110
|
+
console.error(result.error);
|
|
111
|
+
}
|
|
112
|
+
return result.status;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const install = async () => {
|
|
116
|
+
try {
|
|
117
|
+
let archive = getArchive();
|
|
118
|
+
if (await exists(archive)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let tmp = fs.mkdtempSync("archive-");
|
|
122
|
+
let archivePath = path.join(tmp, archive.name);
|
|
123
|
+
await download(archive.url, archivePath);
|
|
124
|
+
verify(archivePath, archive.checksum);
|
|
125
|
+
|
|
126
|
+
if (!fs.existsSync(binDir)) {
|
|
127
|
+
fs.mkdirSync(binDir);
|
|
128
|
+
}
|
|
129
|
+
switch (archive.format) {
|
|
130
|
+
case "binary":
|
|
131
|
+
const bin = path.join(binDir, archive.bins[0]);
|
|
132
|
+
fs.copyFileSync(archivePath, bin);
|
|
133
|
+
fs.chmodSync(bin, 0o755);
|
|
134
|
+
break;
|
|
135
|
+
case "zip":
|
|
136
|
+
await extractZip(
|
|
137
|
+
archivePath,
|
|
138
|
+
archive.bins,
|
|
139
|
+
binDir,
|
|
140
|
+
archive.wrappedIn,
|
|
141
|
+
);
|
|
142
|
+
break;
|
|
143
|
+
case "tar":
|
|
144
|
+
case "tar.gz":
|
|
145
|
+
case "tgz":
|
|
146
|
+
await extractTar(
|
|
147
|
+
archivePath,
|
|
148
|
+
archive.bins,
|
|
149
|
+
binDir,
|
|
150
|
+
archive.wrappedIn,
|
|
151
|
+
);
|
|
152
|
+
break;
|
|
153
|
+
case "tar.zst":
|
|
154
|
+
case "tzst":
|
|
155
|
+
case "tar.xz":
|
|
156
|
+
case "txz":
|
|
157
|
+
default:
|
|
158
|
+
throw new Error(`unsupported format: ${archive.format}`);
|
|
159
|
+
}
|
|
160
|
+
console.log(`Installed ${name} ${version} to ${binDir}`);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
throw new Error(`Installation failed: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const verify = (filename, checksum) => {
|
|
167
|
+
if (checksum.algorithm == "" || checksum.digest == "") {
|
|
168
|
+
console.warn("Warning: No checksum provided for verification");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
let digest = crypto
|
|
172
|
+
.createHash(checksum.algorithm)
|
|
173
|
+
.update(fs.readFileSync(filename))
|
|
174
|
+
.digest("hex");
|
|
175
|
+
if (digest != checksum.digest) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`${filename}: ${checksum.algorithm} does not match, expected ${checksum.digest}, got ${digest}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const download = async (url, filename, maxRedirects = 10) => {
|
|
183
|
+
try {
|
|
184
|
+
console.log(`Downloading ${url} to ${filename}...`);
|
|
185
|
+
const dir = path.dirname(filename);
|
|
186
|
+
if (!fs.existsSync(dir)) {
|
|
187
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const parsedUrl = new URL(url);
|
|
192
|
+
const mod = parsedUrl.protocol === "https:" ? https : http;
|
|
193
|
+
|
|
194
|
+
const request = mod.get(url, { agent }, (response) => {
|
|
195
|
+
if (
|
|
196
|
+
response.statusCode >= 300 &&
|
|
197
|
+
response.statusCode < 400 &&
|
|
198
|
+
response.headers.location
|
|
199
|
+
) {
|
|
200
|
+
if (maxRedirects <= 0) {
|
|
201
|
+
reject(new Error("Too many redirects"));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
download(response.headers.location, filename, maxRedirects - 1)
|
|
205
|
+
.then(resolve)
|
|
206
|
+
.catch(reject);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (response.statusCode !== 200) {
|
|
211
|
+
reject(
|
|
212
|
+
new Error(
|
|
213
|
+
`HTTP ${response.statusCode}: ${response.statusMessage}`,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const writer = fs.createWriteStream(filename);
|
|
220
|
+
response.pipe(writer);
|
|
221
|
+
|
|
222
|
+
writer.on("finish", () => {
|
|
223
|
+
console.log(`Download complete: ${filename}`);
|
|
224
|
+
resolve(dir);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
writer.on("error", (err) => {
|
|
228
|
+
console.error(`Error writing file: ${err.message}`);
|
|
229
|
+
reject(err);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
request.on("error", (err) => {
|
|
234
|
+
reject(new Error(`Request failed: ${err.message}`));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
request.setTimeout(300000, () => {
|
|
238
|
+
request.destroy();
|
|
239
|
+
reject(new Error("Request timed out"));
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
} catch (err) {
|
|
243
|
+
throw new Error(`Download failed: ${err.message}`);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
function exists(archive) {
|
|
248
|
+
if (!fs.existsSync(binDir)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return archive.bins.every((bin) => fs.existsSync(path.join(binDir, bin)));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export {
|
|
255
|
+
install,
|
|
256
|
+
run,
|
|
257
|
+
getArchive,
|
|
258
|
+
download,
|
|
259
|
+
};
|