@colony2/c2j 0.0.41 → 0.0.51
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 +77 -38
- package/bin/cli.js +30 -0
- package/package.json +30 -72
- package/scripts/postinstall.js +152 -0
- package/install.js +0 -6
- package/lib.js +0 -259
- package/run-c2j.js +0 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# c2j
|
|
2
2
|
|
|
3
|
-
`c2j` is the local job-oriented CLI for submitting and running recipe jobs through
|
|
3
|
+
`c2j` is the local job-oriented CLI for submitting and running recipe jobs through JobDB.
|
|
4
4
|
|
|
5
5
|
Use it when you want to:
|
|
6
6
|
|
|
@@ -14,6 +14,14 @@ Use it when you want to:
|
|
|
14
14
|
|
|
15
15
|
Examples below assume you are running from the repo root.
|
|
16
16
|
|
|
17
|
+
## JobDB upgrade compatibility
|
|
18
|
+
|
|
19
|
+
The typed-route JobDB update requires fresh format-3 databases/artifact storage
|
|
20
|
+
and matching server/worker versions; existing jobs are not migrated. Job and
|
|
21
|
+
task types now travel separately, preserving existing task names. Read
|
|
22
|
+
[the upgrade notes](JOBDB_UPGRADE_NOTES.md) before deploying it. No existing data
|
|
23
|
+
is automatically reset.
|
|
24
|
+
|
|
17
25
|
## Command Summary
|
|
18
26
|
|
|
19
27
|
```bash
|
|
@@ -91,7 +99,7 @@ That does all of the following:
|
|
|
91
99
|
|
|
92
100
|
- loads the local YAML file
|
|
93
101
|
- embeds that recipe into the submitted job
|
|
94
|
-
- starts an embedded
|
|
102
|
+
- starts an embedded JobDB runtime
|
|
95
103
|
- submits the job
|
|
96
104
|
- immediately executes it
|
|
97
105
|
|
|
@@ -142,15 +150,34 @@ This is mainly useful when you want to target another cell by short name and nee
|
|
|
142
150
|
|
|
143
151
|
### `c2j init`
|
|
144
152
|
|
|
145
|
-
Writes a commented `.c2j/config.yaml` template.
|
|
153
|
+
Writes a commented `.c2j/config.yaml` template and installs bundled c2j skills for Codex-compatible agents.
|
|
146
154
|
|
|
147
155
|
```bash
|
|
148
156
|
c2j init
|
|
149
157
|
c2j init --stdout
|
|
150
158
|
c2j init --force
|
|
159
|
+
c2j init --no-skills
|
|
160
|
+
c2j init --with-op-skills
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The generated template can derive values from the Go module in the current repo when `base: go` is appropriate. `--stdout` prints only the config and does not install skills.
|
|
164
|
+
|
|
165
|
+
Bundled skill install controls:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
c2j init --skills-scope auto
|
|
169
|
+
c2j init --skills-scope project
|
|
170
|
+
c2j init --skills-scope user
|
|
171
|
+
c2j init --skills-force
|
|
151
172
|
```
|
|
152
173
|
|
|
153
|
-
|
|
174
|
+
`auto` currently installs to the project-local `.agents/skills` directory. `user` installs to `${CODEX_HOME:-$HOME/.codex}/skills`. Install metadata is recorded in `.c2j/skills-lock.yaml`.
|
|
175
|
+
|
|
176
|
+
You can also install the repo skills directly with the public `skills` tool:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx skills add colony-2/c2j
|
|
180
|
+
```
|
|
154
181
|
|
|
155
182
|
## Submitting Jobs
|
|
156
183
|
|
|
@@ -333,7 +360,7 @@ Useful flags:
|
|
|
333
360
|
- `--case <id>` filters suite mode to selected cases
|
|
334
361
|
- `--parallelism <n>` controls local case concurrency
|
|
335
362
|
- `--out-dir <dir>` defaults to `.c2j/test-results/<timestamp>/`
|
|
336
|
-
- passthrough cases use a disposable embedded runtime
|
|
363
|
+
- passthrough cases use a disposable embedded runtime automatically
|
|
337
364
|
|
|
338
365
|
## Running Jobs
|
|
339
366
|
|
|
@@ -403,21 +430,20 @@ the job becomes runnable or the wait timeout is reached.
|
|
|
403
430
|
## Worker Modes
|
|
404
431
|
|
|
405
432
|
`c2j run loop` runs a long-lived worker for one tenant. It leases available jobs
|
|
406
|
-
from
|
|
433
|
+
from a remote JobDB runtime and executes up to the configured local concurrency.
|
|
407
434
|
|
|
408
435
|
Basic usage:
|
|
409
436
|
|
|
410
437
|
```bash
|
|
411
|
-
c2j run loop --
|
|
438
|
+
c2j run loop --jobdb http://localhost:9047/<tenant-id> --concurrency 4
|
|
412
439
|
```
|
|
413
440
|
|
|
414
441
|
Important behavior:
|
|
415
442
|
|
|
416
443
|
- `--concurrency <n>` controls how many jobs this process can run at once
|
|
417
|
-
- `--
|
|
418
|
-
- `--swf-url` must be an external `http://` or `https://` runtime
|
|
444
|
+
- `--jobdb` must be a remote `http://host/tenant` or `https://host/tenant` URI
|
|
419
445
|
- `--embed` is not available for `run loop`
|
|
420
|
-
- `--
|
|
446
|
+
- `--jobdb embed:///` is rejected for `run loop`
|
|
421
447
|
- `run loop` is non-interactive; use `run` or an ops surface for jobs that need input
|
|
422
448
|
|
|
423
449
|
### Loose scheduling with `ready` and `run any`
|
|
@@ -425,7 +451,7 @@ Important behavior:
|
|
|
425
451
|
`c2j ready` prints the number of currently ready recipe jobs for one tenant:
|
|
426
452
|
|
|
427
453
|
```bash
|
|
428
|
-
c2j ready --
|
|
454
|
+
c2j ready --jobdb http://localhost:9047/<tenant-id>
|
|
429
455
|
```
|
|
430
456
|
|
|
431
457
|
`c2j run any` atomically polls for one available item of recipe work, leases it,
|
|
@@ -433,23 +459,23 @@ runs it, and exits. If no lease is available, it exits successfully after
|
|
|
433
459
|
printing `no jobs found`.
|
|
434
460
|
|
|
435
461
|
```bash
|
|
436
|
-
c2j run any --
|
|
462
|
+
c2j run any --jobdb http://localhost:9047/<tenant-id>
|
|
437
463
|
```
|
|
438
464
|
|
|
439
465
|
These can be composed by an external scheduler:
|
|
440
466
|
|
|
441
467
|
```bash
|
|
442
|
-
count=$(c2j ready --
|
|
468
|
+
count=$(c2j ready --jobdb http://localhost:9047/<tenant-id>)
|
|
443
469
|
if [ "$count" -gt 0 ]; then
|
|
444
470
|
for _ in $(seq 1 "$count"); do
|
|
445
|
-
c2j run any --
|
|
471
|
+
c2j run any --jobdb http://localhost:9047/<tenant-id> &
|
|
446
472
|
done
|
|
447
473
|
wait
|
|
448
474
|
fi
|
|
449
475
|
```
|
|
450
476
|
|
|
451
477
|
`ready` is only a non-mutating snapshot and can become stale under competing
|
|
452
|
-
workers. `run any` uses
|
|
478
|
+
workers. `run any` uses JobDB polling so finding available work and acquiring the
|
|
453
479
|
lease happen in one runtime operation.
|
|
454
480
|
|
|
455
481
|
## Listing Jobs
|
|
@@ -490,20 +516,35 @@ Useful filters:
|
|
|
490
516
|
- `--page-token`
|
|
491
517
|
- `--all`
|
|
492
518
|
|
|
519
|
+
Filter by a pending task using a JSON route:
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
c2j list --self --waiting-for '{"jobType":"recipe","taskType":"input:collect_user_input"}' --embed
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Repeat `--waiting-for` for multiple routes and `--job-type` for multiple job
|
|
526
|
+
types. These flags accept literal identifiers, not comma-separated lists;
|
|
527
|
+
`--waiting-for JOBTYPE:TASKTYPE` is no longer supported because either type may
|
|
528
|
+
contain colons. Identifiers are case-sensitive and are not trimmed or encoded.
|
|
529
|
+
List JSON exposes `next_route` (`jobType`, optional `taskType`) and `task_wait`
|
|
530
|
+
instead of the old `next_need` and flat `task_wait_*` fields. The human-readable
|
|
531
|
+
`NEXT` column also shows the route as JSON.
|
|
532
|
+
|
|
493
533
|
## Embedded Runtime
|
|
494
534
|
|
|
495
535
|
`--embed` is shorthand for:
|
|
496
536
|
|
|
497
537
|
```bash
|
|
498
|
-
--
|
|
538
|
+
--jobdb embed:///
|
|
499
539
|
```
|
|
500
540
|
|
|
501
|
-
Use it when you want a local self-contained runtime instead of
|
|
541
|
+
Use it when you want a local self-contained runtime instead of a remote JobDB server.
|
|
502
542
|
|
|
503
543
|
Behavior:
|
|
504
544
|
|
|
505
545
|
- starts embedded Postgres and Strata as needed
|
|
506
546
|
- uses a persistent runtime root on disk
|
|
547
|
+
- always uses tenant `0`
|
|
507
548
|
- works well for local recipe authoring and debugging
|
|
508
549
|
|
|
509
550
|
Defaults:
|
|
@@ -511,41 +552,39 @@ Defaults:
|
|
|
511
552
|
- runtime URL: `embed:///`
|
|
512
553
|
- runtime root: `~/.c2j/embed/default`
|
|
513
554
|
|
|
514
|
-
You can override the root with:
|
|
515
|
-
|
|
516
|
-
```bash
|
|
517
|
-
export C2J_EMBED_ROOT=/absolute/path/to/embed-root
|
|
518
|
-
```
|
|
519
|
-
|
|
520
555
|
Notes:
|
|
521
556
|
|
|
522
|
-
- `C2J_EMBED_ROOT` must be an absolute path
|
|
523
557
|
- only one `c2j` process can own a given embedded runtime root at a time
|
|
524
|
-
- if you need parallel embedded runtimes, give each process a different `C2J_EMBED_ROOT`
|
|
525
558
|
|
|
526
|
-
##
|
|
559
|
+
## JobDB Configuration
|
|
527
560
|
|
|
528
561
|
`c2j` reads these environment variables:
|
|
529
562
|
|
|
530
|
-
- `
|
|
531
|
-
- `C2J_TENANT_ID`
|
|
532
|
-
- `C2J_EMBED_ROOT`
|
|
563
|
+
- `C2J_JOBDB`
|
|
533
564
|
|
|
534
|
-
|
|
565
|
+
JobDB URI forms:
|
|
535
566
|
|
|
536
|
-
- `
|
|
537
|
-
- `
|
|
567
|
+
- `https://jobdb.example.com/<tenant-id>`
|
|
568
|
+
- `http://localhost:9047/<tenant-id>`
|
|
569
|
+
- `embed:///`
|
|
538
570
|
|
|
539
571
|
Examples:
|
|
540
572
|
|
|
541
573
|
```bash
|
|
542
|
-
export
|
|
543
|
-
export C2J_TENANT_ID=123
|
|
574
|
+
export C2J_JOBDB=http://localhost:9047/my-tenant
|
|
544
575
|
```
|
|
545
576
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
577
|
+
`--jobdb` overrides `C2J_JOBDB` for that command. Project config may also define:
|
|
578
|
+
|
|
579
|
+
```yaml
|
|
580
|
+
jobdb: https://jobdb.example.com/my-tenant
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
For local embedded mode in project config:
|
|
584
|
+
|
|
585
|
+
```yaml
|
|
586
|
+
jobdb: embed:///
|
|
587
|
+
```
|
|
549
588
|
|
|
550
589
|
## Common Workflows
|
|
551
590
|
|
|
@@ -568,7 +607,7 @@ c2j run --job-id <job-id> --embed
|
|
|
568
607
|
```bash
|
|
569
608
|
c2j submit \
|
|
570
609
|
--recipe-file ./recipes/my-recipe.yaml \
|
|
571
|
-
--
|
|
610
|
+
--jobdb http://localhost:9047/my-tenant \
|
|
572
611
|
--run
|
|
573
612
|
```
|
|
574
613
|
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const packageJson = require(path.join(packageRoot, "package.json"));
|
|
11
|
+
const packageName = packageJson.name.split("/").pop();
|
|
12
|
+
const binaryPath = path.join(packageRoot, "vendor", packageName);
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(binaryPath)) {
|
|
15
|
+
console.error(
|
|
16
|
+
`${packageName} binary is missing. Reinstall the package or run npm rebuild ${packageJson.name}.`
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (result.error) {
|
|
26
|
+
console.error(result.error.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exit(result.status === null ? 1 : result.status);
|
package/package.json
CHANGED
|
@@ -1,82 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colony2/c2j",
|
|
3
|
-
"
|
|
4
|
-
"version": "0.0.41",
|
|
3
|
+
"version": "0.0.51",
|
|
5
4
|
"description": "c2j job system",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"run": "node run-c2j.js"
|
|
9
|
-
},
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://github.com/colony-2/c2j",
|
|
10
7
|
"repository": {
|
|
11
8
|
"type": "git",
|
|
12
9
|
"url": "git+https://github.com/colony-2/c2j.git"
|
|
13
10
|
},
|
|
14
|
-
"author": {
|
|
15
|
-
"email": "noreply@colony2.com",
|
|
16
|
-
"name": "Colony 2"
|
|
17
|
-
},
|
|
18
|
-
"license": "Apache-2.0",
|
|
19
11
|
"bugs": {
|
|
20
12
|
"url": "https://github.com/colony-2/c2j/issues"
|
|
21
13
|
},
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"jszip": "3.10.1",
|
|
28
|
-
"proxy-agent": "8.0.1",
|
|
29
|
-
"tar": "7.5.15"
|
|
30
|
-
},
|
|
31
|
-
"archives": {
|
|
32
|
-
"darwin-arm64": {
|
|
33
|
-
"name": "c2j_0.0.41_Darwin_arm64.tar.gz",
|
|
34
|
-
"url": "https://github.com/colony-2/c2j-release/releases/download/v0.0.41/c2j_0.0.41_Darwin_arm64.tar.gz",
|
|
35
|
-
"bins": [
|
|
36
|
-
"c2j"
|
|
37
|
-
],
|
|
38
|
-
"format": "tar.gz",
|
|
39
|
-
"checksum": {
|
|
40
|
-
"algorithm": "sha256",
|
|
41
|
-
"digest": "13de517cad797c93023be1d7f03d191432fe2a3167fd614e67d0af37f666435b"
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"darwin-x64": {
|
|
45
|
-
"name": "c2j_0.0.41_Darwin_x86_64.tar.gz",
|
|
46
|
-
"url": "https://github.com/colony-2/c2j-release/releases/download/v0.0.41/c2j_0.0.41_Darwin_x86_64.tar.gz",
|
|
47
|
-
"bins": [
|
|
48
|
-
"c2j"
|
|
49
|
-
],
|
|
50
|
-
"format": "tar.gz",
|
|
51
|
-
"checksum": {
|
|
52
|
-
"algorithm": "sha256",
|
|
53
|
-
"digest": "167d6ef9e7cfd2fb03fa132cecbec52d25fcaa2b5d4fc6a46d767f10e1895362"
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
"linux-arm64": {
|
|
57
|
-
"name": "c2j_0.0.41_Linux_arm64.tar.gz",
|
|
58
|
-
"url": "https://github.com/colony-2/c2j-release/releases/download/v0.0.41/c2j_0.0.41_Linux_arm64.tar.gz",
|
|
59
|
-
"bins": [
|
|
60
|
-
"c2j"
|
|
61
|
-
],
|
|
62
|
-
"format": "tar.gz",
|
|
63
|
-
"checksum": {
|
|
64
|
-
"algorithm": "sha256",
|
|
65
|
-
"digest": "0c9f35b66643466f5d4919e209c8b0b49feca80f87de6b9ea579ed781392dd5a"
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
"linux-x64": {
|
|
69
|
-
"name": "c2j_0.0.41_Linux_x86_64.tar.gz",
|
|
70
|
-
"url": "https://github.com/colony-2/c2j-release/releases/download/v0.0.41/c2j_0.0.41_Linux_x86_64.tar.gz",
|
|
71
|
-
"bins": [
|
|
72
|
-
"c2j"
|
|
73
|
-
],
|
|
74
|
-
"format": "tar.gz",
|
|
75
|
-
"checksum": {
|
|
76
|
-
"algorithm": "sha256",
|
|
77
|
-
"digest": "e87a96e1bdf4c26fdd818b6a40a3b1edcc20ffe1137dec6afda4b88729c92897"
|
|
78
|
-
}
|
|
79
|
-
}
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Colony 2",
|
|
16
|
+
"email": "noreply@colony2.com"
|
|
80
17
|
},
|
|
81
18
|
"keywords": [
|
|
82
19
|
"sandbox",
|
|
@@ -86,7 +23,28 @@
|
|
|
86
23
|
"cli",
|
|
87
24
|
"security"
|
|
88
25
|
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"c2j": "bin/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"postinstall": "node scripts/postinstall.js"
|
|
31
|
+
},
|
|
32
|
+
"os": [
|
|
33
|
+
"darwin",
|
|
34
|
+
"linux"
|
|
35
|
+
],
|
|
36
|
+
"cpu": [
|
|
37
|
+
"x64",
|
|
38
|
+
"arm64"
|
|
39
|
+
],
|
|
89
40
|
"engines": {
|
|
90
|
-
"node": "
|
|
91
|
-
}
|
|
92
|
-
|
|
41
|
+
"node": ">=14.0.0"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"bin/",
|
|
45
|
+
"scripts/",
|
|
46
|
+
"vendor/",
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { execFileSync } = require("child_process");
|
|
10
|
+
const { get } = require("https");
|
|
11
|
+
|
|
12
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
13
|
+
const packageJson = require(path.join(packageRoot, "package.json"));
|
|
14
|
+
const packageName = packageJson.name.split("/").pop();
|
|
15
|
+
const version = packageJson.version;
|
|
16
|
+
|
|
17
|
+
const platforms = {
|
|
18
|
+
"linux:x64": ["Linux", "x86_64"],
|
|
19
|
+
"linux:arm64": ["Linux", "arm64"],
|
|
20
|
+
"darwin:x64": ["Darwin", "x86_64"],
|
|
21
|
+
"darwin:arm64": ["Darwin", "arm64"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const platformKey = `${process.platform}:${process.arch}`;
|
|
25
|
+
const target = platforms[platformKey];
|
|
26
|
+
|
|
27
|
+
if (!target) {
|
|
28
|
+
throw new Error(`Unsupported platform for ${packageJson.name}: ${platformKey}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const repo = repositoryPath(packageJson);
|
|
32
|
+
const tag = `v${version}`;
|
|
33
|
+
const assetName = `${packageName}_${version}_${target[0]}_${target[1]}.tar.gz`;
|
|
34
|
+
const baseUrl = `https://github.com/${repo}/releases/download/${tag}`;
|
|
35
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${packageName}-`));
|
|
36
|
+
const archivePath = path.join(tempDir, assetName);
|
|
37
|
+
const checksumsPath = path.join(tempDir, "checksums.txt");
|
|
38
|
+
const extractDir = path.join(tempDir, "extract");
|
|
39
|
+
const vendorDir = path.join(packageRoot, "vendor");
|
|
40
|
+
const installedBinary = path.join(vendorDir, packageName);
|
|
41
|
+
|
|
42
|
+
fs.mkdirSync(extractDir);
|
|
43
|
+
fs.mkdirSync(vendorDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
downloadWithRetry(`${baseUrl}/${assetName}`, archivePath)
|
|
46
|
+
.then(() => downloadWithRetry(`${baseUrl}/checksums.txt`, checksumsPath))
|
|
47
|
+
.then(() => {
|
|
48
|
+
verifyChecksum(archivePath, checksumsPath, assetName);
|
|
49
|
+
execFileSync("tar", ["-xzf", archivePath, "-C", extractDir], {
|
|
50
|
+
stdio: "inherit",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const extractedBinary = path.join(extractDir, packageName);
|
|
54
|
+
if (!fs.existsSync(extractedBinary)) {
|
|
55
|
+
throw new Error(`Release archive did not contain ${packageName}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.copyFileSync(extractedBinary, installedBinary);
|
|
59
|
+
fs.chmodSync(installedBinary, 0o755);
|
|
60
|
+
})
|
|
61
|
+
.catch((error) => {
|
|
62
|
+
console.error(error.message || error);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
})
|
|
65
|
+
.finally(() => {
|
|
66
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function repositoryPath(pkg) {
|
|
70
|
+
const raw =
|
|
71
|
+
typeof pkg.repository === "string" ? pkg.repository : pkg.repository && pkg.repository.url;
|
|
72
|
+
|
|
73
|
+
if (!raw) {
|
|
74
|
+
throw new Error("package.json repository is required to locate release assets");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const match = raw.match(/github\.com[:/]([^/]+\/[^/.]+)(?:\.git)?/);
|
|
78
|
+
if (!match) {
|
|
79
|
+
throw new Error(`Unsupported repository URL for release assets: ${raw}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return match[1];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function downloadWithRetry(url, destination) {
|
|
86
|
+
let lastError;
|
|
87
|
+
|
|
88
|
+
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
89
|
+
try {
|
|
90
|
+
await download(url, destination);
|
|
91
|
+
return;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
lastError = error;
|
|
94
|
+
if (attempt === 5) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
await delay(attempt * 1000);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw lastError;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function download(url, destination) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const request = get(url, (response) => {
|
|
107
|
+
if (
|
|
108
|
+
response.statusCode >= 300 &&
|
|
109
|
+
response.statusCode < 400 &&
|
|
110
|
+
response.headers.location
|
|
111
|
+
) {
|
|
112
|
+
response.resume();
|
|
113
|
+
download(response.headers.location, destination).then(resolve, reject);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (response.statusCode !== 200) {
|
|
118
|
+
response.resume();
|
|
119
|
+
reject(new Error(`Failed to download ${url}: HTTP ${response.statusCode}`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const file = fs.createWriteStream(destination, { mode: 0o600 });
|
|
124
|
+
response.pipe(file);
|
|
125
|
+
file.on("finish", () => file.close(resolve));
|
|
126
|
+
file.on("error", reject);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
request.on("error", reject);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function delay(ms) {
|
|
134
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function verifyChecksum(archive, checksums, assetName) {
|
|
138
|
+
const expected = fs
|
|
139
|
+
.readFileSync(checksums, "utf8")
|
|
140
|
+
.split(/\r?\n/)
|
|
141
|
+
.map((line) => line.trim().split(/\s+/))
|
|
142
|
+
.find((parts) => parts[1] === assetName);
|
|
143
|
+
|
|
144
|
+
if (!expected) {
|
|
145
|
+
throw new Error(`No checksum found for ${assetName}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const actual = crypto.createHash("sha256").update(fs.readFileSync(archive)).digest("hex");
|
|
149
|
+
if (actual !== expected[0]) {
|
|
150
|
+
throw new Error(`Checksum mismatch for ${assetName}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
package/install.js
DELETED
package/lib.js
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
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
|
-
};
|