@sunstack/cli 0.0.0 → 0.1.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/.gitignore +2 -0
- package/LICENSE +21 -0
- package/README.md +188 -3
- package/binary-install.js +348 -0
- package/binary.js +124 -0
- package/install.js +4 -0
- package/npm-shrinkwrap.json +52 -0
- package/package.json +93 -9
- package/run-sunstack.js +33 -0
package/.gitignore
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shravan Sunder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,190 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sunstack exec
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Run `sunstack exec` (or `cargo run -p sunstack -- exec` from the repository root).
|
|
4
|
+
One process consumes one JSON request on stdin and writes one JSON response on
|
|
5
|
+
stdout. Relative input paths resolve from the process working directory. Run
|
|
6
|
+
from the repository root when using the reviewed examples.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
```json
|
|
9
|
+
{"protocol_version":1,"operation":"evaluate_rules","input":{}}
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Replace `input` with the native operation input. Supported operations are
|
|
13
|
+
`prepare_evidence`, `run_judgments`, `check_rules`, `prepare_rules` and `evaluate_rules`.
|
|
14
|
+
The response is either `protocol_error` with diagnostics, or `operation_result`
|
|
15
|
+
whose `result` contains the operation and its unchanged native outcome.
|
|
16
|
+
The raw stdin limit is 8,388,608 bytes. Bind large evidence by path.
|
|
17
|
+
|
|
18
|
+
Exit 0 means the operation completed, including an unfavorable rule verdict.
|
|
19
|
+
Exit 2 means invalid input or protocol refusal; 3 means failed or incomplete
|
|
20
|
+
execution; 4 means caller cancellation. SIGINT and SIGTERM cancel owned work
|
|
21
|
+
and await its settlement before publishing the terminal response. The static
|
|
22
|
+
`sunstack: ready` stderr line witnesses signal registration, and
|
|
23
|
+
`sunstack: cancellation observed` witnesses signal consumption.
|
|
24
|
+
|
|
25
|
+
Change-scoped work retains the same protocol version. Native inputs
|
|
26
|
+
accept non-Full `change_scope` values (dirty layers, revision ranges, commit lists,
|
|
27
|
+
time windows and unions), bounded discovery, ChangeSet sources and explicit
|
|
28
|
+
package bindings. A relative repository root resolves from this process's CWD.
|
|
29
|
+
For example, select current unstaged changes on a repository binding with:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{"change_scope":{"kind":"uncommitted","layers":["unstaged"]}}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This is a binding fragment, not a complete invocation. ChangeSet selectors need
|
|
36
|
+
`discovery.kind = "bounded"` with native discovery limits. A package binding's
|
|
37
|
+
empty `target_path_prefix` maps to the target repository root. Use the reviewed
|
|
38
|
+
[invocation schema](../../docs/specs/2026-10-12-rust-rule-engine/schemas/rule-invocation.schema.json)
|
|
39
|
+
for complete inputs.
|
|
40
|
+
|
|
41
|
+
Preparation and reports expose native `change_selections`; Full-only work has
|
|
42
|
+
an empty list. Rule occurrences retain selection reasons such as
|
|
43
|
+
`target_changed` or `full_scope`, pending comparisons, and outside-scope results.
|
|
44
|
+
Preparing rules sends no model request. A waiting gate leaves target comparison
|
|
45
|
+
pending; evaluation resolves it only when the native gate permits the work.
|
|
46
|
+
The CLI forwards these native outcomes and uses the same exit meanings above.
|
|
47
|
+
|
|
48
|
+
The Jev backend reads `OPENROUTER_API_KEY` from the process environment, never
|
|
49
|
+
stdin. Rule preparation uses the selected backend for encoding and accounting
|
|
50
|
+
when model work is reachable; it sends no provider request. Local-only rule
|
|
51
|
+
preparation and evaluation construct no backend and need no credential. A
|
|
52
|
+
model-based judgment, applicability gate or Router still requires valid backend
|
|
53
|
+
configuration when it becomes reachable. Evidence preparation needs no credential.
|
|
54
|
+
|
|
55
|
+
`check_rules` assembles every requested RuleSet and validates its rules without
|
|
56
|
+
constructing a backend, reading reviewed documents or sending model requests.
|
|
57
|
+
Its input contains `definition_root`, `definitions`, `repositories`,
|
|
58
|
+
`payload_schemas`, `limits`, optional `discovery` and `output` (`diagnostics` or `assembled_rules`).
|
|
59
|
+
Discovery defaults to `none`, as in runtime input. Connected rules need bounded
|
|
60
|
+
discovery settings for admission; check never executes discovery or acquisition.
|
|
61
|
+
Repository-root metadata and declared payload schemas are still validated.
|
|
62
|
+
It accepts no backend, attempt or request-budget settings. A clean check exits 0;
|
|
63
|
+
refusals exit 2 with definition file, line and column. Printed rules are returned
|
|
64
|
+
only after a clean check and include each native definition and its source.
|
|
65
|
+
|
|
66
|
+
RuleSets can combine existing `rule_paths` JSONL files with
|
|
67
|
+
`rule_sources: [{"markdown":"standards.md","rules":"standards.rules.json"}]`.
|
|
68
|
+
Paths resolve relative to the RuleSet file. A RuleSet with sources needs a `marker_prefix`
|
|
69
|
+
such as `K`; an end-of-line `<!-- K014 -->` binds its standard to the `K014`
|
|
70
|
+
entry in the strict rules object. Entry `name` becomes the native rule ID;
|
|
71
|
+
`target.input` is always explicit, and optional `context` uses native evidence
|
|
72
|
+
declarations. The target declaration reserves ID `target`. The assembled question
|
|
73
|
+
uses visible Markdown wording, with a default cap of 400 words configurable by
|
|
74
|
+
`max_question_words`. Both the Markdown and rules file are definition dependencies.
|
|
75
|
+
|
|
76
|
+
At runtime, invalid source entries are listed in `skipped_rules`, while valid
|
|
77
|
+
rules continue. An unreadable or malformed source contributes one source-level
|
|
78
|
+
skip. `rule_coverage` is `full`, `partial` or `none`; partial runs can complete
|
|
79
|
+
with exit 0, while no valid rule yields incomplete exit 3. Measurements keep
|
|
80
|
+
`invalid_rule_entries` and `skipped_rule_sources` separate from applicability's
|
|
81
|
+
`rules_skipped`. Existing JSONL, RuleSet-header and invocation errors retain
|
|
82
|
+
their strict refusal behavior.
|
|
83
|
+
|
|
84
|
+
Run the complete credential-free check/print example from the repository root:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
scripts/cargo-debug-signing-runner.sh target/debug/sunstack exec \
|
|
88
|
+
< docs/specs/2026-10-12-rust-rule-engine/examples/marked-standards.check.request.json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The [check input schema](../../docs/specs/2026-10-12-rust-rule-engine/schemas/check-rules-input.schema.json)
|
|
92
|
+
and [rules-object schema](../../docs/specs/2026-10-12-rust-rule-engine/schemas/rule-source.schema.json)
|
|
93
|
+
describe these boundaries. Preparation and evaluation reports now require coverage,
|
|
94
|
+
rule evaluations, skipped rules and their counters; strict report consumers must
|
|
95
|
+
update together.
|
|
96
|
+
|
|
97
|
+
Document targets can select Markdown units independently:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{"kind":"markdown_units","mode":"call_site","units":["paragraph","list_item"],"pattern":"\\bMUST\\s+load\\b"}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`mode: unit` produces one occurrence per requested section, paragraph or list
|
|
104
|
+
item, with an optional `starts_with` or `pattern` filter. Requested unit kinds
|
|
105
|
+
can overlap. `mode: call_site` requires exactly one nonempty filter and produces
|
|
106
|
+
one occurrence per match, owned by the innermost requested unit; list items own
|
|
107
|
+
their contained paragraphs. Regexes that can match empty text are refused.
|
|
108
|
+
Filters use visible text with inline code excluded; fenced content remains.
|
|
109
|
+
|
|
110
|
+
Occurrence keys include source identity, raw unit ranges, and call-site raw and
|
|
111
|
+
visible match ranges. Raw spans use original document bytes and character
|
|
112
|
+
coordinates. Several visible matches may share a raw entity span; generated-only
|
|
113
|
+
matches use zero-width raw boundary anchors. Question owners carry the full key,
|
|
114
|
+
so budget splitting preserves each target's evidence and answer references.
|
|
115
|
+
|
|
116
|
+
Preparation exposes `rule_evaluations`, including `waiting_for_expansion` behind
|
|
117
|
+
a staged different-input gate. No child keys or target selections are invented
|
|
118
|
+
while it waits. Evaluation reports `expanded`, `zero_matches`, stopped, failed
|
|
119
|
+
or cancelled work explicitly. Zero matches is complete when other work completes;
|
|
120
|
+
it is never a passed verdict. Same-input speculative carriers remain permitted.
|
|
121
|
+
Existing change-scope repository snapshots may already hold the reviewed bytes;
|
|
122
|
+
canonical target selection and expansion still wait for their gate frontier.
|
|
123
|
+
|
|
124
|
+
The evidence library also supports `frontmatter_field` with a literal top-level
|
|
125
|
+
`field`. An initial YAML `---` block yields a decoded JSON value or an absent
|
|
126
|
+
field. Missing or malformed blocks yield explicit unavailable data without an
|
|
127
|
+
invented span. Noul and composite rules reject this mechanical-only target.
|
|
128
|
+
|
|
129
|
+
A rules entry can replace its judgment threshold with one local check:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{"name":"skill.description","target":{"input":"skill","select":{"kind":"frontmatter_field","field":"description"}},"expect":{"starts_with":"Use when"},"severity":"error"}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`expect` accepts exactly one of `matches`, `not_matches`, `starts_with` or
|
|
136
|
+
`present: true`. Text predicates use the unit's rendered-visible text, excluding
|
|
137
|
+
inline code. A frontmatter string uses its decoded text; other values use
|
|
138
|
+
canonical JSON text. Null is present. Every predicate fails on an absent field;
|
|
139
|
+
a missing or malformed header is unresolved.
|
|
140
|
+
|
|
141
|
+
`ast_grep` accepts a native rule object validated by ast-grep-config 0.51.0.
|
|
142
|
+
It sees the full original Markdown syntax tree, including ancestors outside
|
|
143
|
+
the unit. A match fully contained in the target unit is a failure; sibling
|
|
144
|
+
matches outside it are excluded. Frontmatter fields use `expect`; runtime unit
|
|
145
|
+
kinds do not select the metadata block. The marked standard becomes the local
|
|
146
|
+
result's explanation and is not sent to a model.
|
|
147
|
+
|
|
148
|
+
Reports use `checked` occurrences with `completed` pass/fail, `unresolved` or
|
|
149
|
+
`cancelled` local outcomes. A failed check retains its declared severity and
|
|
150
|
+
can complete with exit 0. Unresolved checks make the report incomplete. Local
|
|
151
|
+
checks create no model answers, references, receipts or confidence and consume
|
|
152
|
+
no model question/request budget. Applicability and routing still govern them.
|
|
153
|
+
|
|
154
|
+
The [mechanical invocation](../../docs/specs/2026-10-12-rust-rule-engine/examples/mechanical.invocation.json)
|
|
155
|
+
checks a description and a load instruction without a credential. Wrap that
|
|
156
|
+
native input in the usual `evaluate_rules` protocol frame. The companion
|
|
157
|
+
[check request](../../docs/specs/2026-10-12-rust-rule-engine/examples/mechanical.check.request.json)
|
|
158
|
+
validates and prints both standards without reading the reviewed skill.
|
|
159
|
+
|
|
160
|
+
Rust callers supply `RuleBackendFactory` in `RuleExecutionContext`. Selection
|
|
161
|
+
is credential-free; the invocation calls `prepare` once when its first model
|
|
162
|
+
request reaches encoding and caches the result. Concrete built backends already
|
|
163
|
+
implement the factory trait and can still be passed with `Arc::new(backend)`.
|
|
164
|
+
|
|
165
|
+
The backend-free
|
|
166
|
+
[unit-target check request](../../docs/specs/2026-10-12-rust-rule-engine/examples/unit-targets.check.request.json)
|
|
167
|
+
checks a rule that selects every paragraph of the existing example document.
|
|
168
|
+
Like other checks, it validates the rule without reading that document.
|
|
169
|
+
|
|
170
|
+
Build with the pinned root toolchain and the SDK environment described in the
|
|
171
|
+
project verification skill. For a local development invocation, pass a request
|
|
172
|
+
file to `scripts/cargo-debug-signing-runner.sh target/debug/sunstack exec`.
|
|
173
|
+
The reviewed skill-lint input lives in
|
|
174
|
+
`docs/specs/2026-10-12-rust-rule-engine/examples/skill-lint.invocation.json`.
|
|
175
|
+
The handoff example contains illustrative transcript/schema paths; the permanent
|
|
176
|
+
journey fixture supplies the accepted generated schema and a documented Codex
|
|
177
|
+
transcript composition.
|
|
178
|
+
|
|
179
|
+
Run `cargo nextest run -p sunstack` for local process checks.
|
|
180
|
+
The explicitly ignored `journey_measurements` test runs the fixed 40-process
|
|
181
|
+
controlled workload. The two ignored `hosted_journeys` cases make four genuine
|
|
182
|
+
provider invocations and require a command-local credential. They are deliberate
|
|
183
|
+
paid observations; preserve failures and obtain a new campaign decision rather
|
|
184
|
+
than repeating until favorable. The ignored `hosted_request_diagnosis` is an
|
|
185
|
+
offline encoder observation for a preserved failed invocation, with no dispatch.
|
|
186
|
+
|
|
187
|
+
See [milestone 2 proof](tests/MILESTONE2-PROOF.md) for change-scoped and connected
|
|
188
|
+
process coverage and its proof limits. See [phase 2 proof](tests/PHASE2-PROOF.md) for the observed gates, measurements,
|
|
189
|
+
fixture provenance, failed campaign and proof limits. [Phase 1 proof](tests/PHASE1-PROOF.md)
|
|
190
|
+
is a historical account of the earlier two-operation target.
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createWriteStream,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
rmSync,
|
|
7
|
+
} = require("fs");
|
|
8
|
+
const { join, sep } = require("path");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
const { tmpdir } = require("os");
|
|
11
|
+
|
|
12
|
+
const https = require("node:https");
|
|
13
|
+
const http = require("node:http");
|
|
14
|
+
|
|
15
|
+
const tmpDir = tmpdir();
|
|
16
|
+
|
|
17
|
+
const error = (msg) => {
|
|
18
|
+
console.error(msg);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getProxyForUrl(urlString) {
|
|
23
|
+
const url = new URL(urlString);
|
|
24
|
+
const isHttps = url.protocol === "https:";
|
|
25
|
+
|
|
26
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
27
|
+
if (noProxy === "*") return null;
|
|
28
|
+
if (noProxy) {
|
|
29
|
+
const hostname = url.hostname.toLowerCase();
|
|
30
|
+
const noProxyList = noProxy.split(",").map((s) => s.trim().toLowerCase());
|
|
31
|
+
for (const entry of noProxyList) {
|
|
32
|
+
if (hostname === entry || hostname.endsWith("." + entry)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const proxyEnv = isHttps
|
|
39
|
+
? process.env.HTTPS_PROXY || process.env.https_proxy
|
|
40
|
+
: process.env.HTTP_PROXY || process.env.http_proxy;
|
|
41
|
+
|
|
42
|
+
if (!proxyEnv) return null;
|
|
43
|
+
|
|
44
|
+
const proxyUrl = new URL(proxyEnv);
|
|
45
|
+
|
|
46
|
+
let auth = null;
|
|
47
|
+
if (proxyUrl.username || proxyUrl.password) {
|
|
48
|
+
auth = `${proxyUrl.username}:${proxyUrl.password}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
hostname: proxyUrl.hostname,
|
|
53
|
+
port: proxyUrl.port || (proxyUrl.protocol === "https:" ? 443 : 80),
|
|
54
|
+
auth: auth,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function connectThroughProxy(proxy, target) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const headers = {};
|
|
61
|
+
if (proxy.auth) {
|
|
62
|
+
headers["Proxy-Authorization"] =
|
|
63
|
+
"Basic " + Buffer.from(proxy.auth).toString("base64");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const connectReq = http.request({
|
|
67
|
+
hostname: proxy.hostname,
|
|
68
|
+
port: proxy.port,
|
|
69
|
+
method: "CONNECT",
|
|
70
|
+
path: `${target.hostname}:${target.port || 443}`,
|
|
71
|
+
headers,
|
|
72
|
+
});
|
|
73
|
+
connectReq.on("connect", (res, socket) => {
|
|
74
|
+
if (res.statusCode === 200) {
|
|
75
|
+
resolve(socket);
|
|
76
|
+
} else {
|
|
77
|
+
reject(new Error(`Proxy CONNECT failed with status ${res.statusCode}`));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
connectReq.on("error", reject);
|
|
81
|
+
connectReq.end();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function download(urlString, maxRedirects) {
|
|
86
|
+
if (maxRedirects === undefined) maxRedirects = 5;
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
if (maxRedirects < 0) {
|
|
89
|
+
return reject(new Error("Too many redirects"));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parsed = new URL(urlString);
|
|
93
|
+
const isHttps = parsed.protocol === "https:";
|
|
94
|
+
const mod = isHttps ? https : http;
|
|
95
|
+
const proxy = getProxyForUrl(urlString);
|
|
96
|
+
|
|
97
|
+
const doRequest = (extraOptions) => {
|
|
98
|
+
const options = Object.assign(
|
|
99
|
+
{
|
|
100
|
+
hostname: parsed.hostname,
|
|
101
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
102
|
+
path: parsed.pathname + parsed.search,
|
|
103
|
+
method: "GET",
|
|
104
|
+
headers: { "User-Agent": "cargo-dist-npm-installer" },
|
|
105
|
+
},
|
|
106
|
+
extraOptions || {},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (proxy && !isHttps) {
|
|
110
|
+
// HTTP through HTTP proxy: request the full URL via the proxy
|
|
111
|
+
options.hostname = proxy.hostname;
|
|
112
|
+
options.port = proxy.port;
|
|
113
|
+
options.path = urlString;
|
|
114
|
+
if (proxy.auth) {
|
|
115
|
+
options.headers["Proxy-Authorization"] =
|
|
116
|
+
"Basic " + Buffer.from(proxy.auth).toString("base64");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const req = mod.request(options, (res) => {
|
|
121
|
+
if (
|
|
122
|
+
res.statusCode >= 300 &&
|
|
123
|
+
res.statusCode < 400 &&
|
|
124
|
+
res.headers.location
|
|
125
|
+
) {
|
|
126
|
+
res.resume();
|
|
127
|
+
const nextUrl = new URL(res.headers.location, urlString).toString();
|
|
128
|
+
return download(nextUrl, maxRedirects - 1).then(resolve, reject);
|
|
129
|
+
}
|
|
130
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
131
|
+
res.resume();
|
|
132
|
+
return reject(new Error(`HTTP ${res.statusCode} from ${urlString}`));
|
|
133
|
+
}
|
|
134
|
+
resolve(res);
|
|
135
|
+
});
|
|
136
|
+
req.on("error", reject);
|
|
137
|
+
req.end();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (proxy && isHttps) {
|
|
141
|
+
connectThroughProxy(proxy, parsed).then(
|
|
142
|
+
(socket) => doRequest({ socket, agent: false }),
|
|
143
|
+
reject,
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
doRequest();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
class Package {
|
|
152
|
+
constructor(platform, name, url, filename, zipExt, binaries) {
|
|
153
|
+
let errors = [];
|
|
154
|
+
if (typeof url !== "string") {
|
|
155
|
+
errors.push("url must be a string");
|
|
156
|
+
} else {
|
|
157
|
+
try {
|
|
158
|
+
new URL(url);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
errors.push(e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (name && typeof name !== "string") {
|
|
164
|
+
errors.push("package name must be a string");
|
|
165
|
+
}
|
|
166
|
+
if (!name) {
|
|
167
|
+
errors.push("You must specify the name of your package");
|
|
168
|
+
}
|
|
169
|
+
if (binaries && typeof binaries !== "object") {
|
|
170
|
+
errors.push("binaries must be a string => string map");
|
|
171
|
+
}
|
|
172
|
+
if (!binaries) {
|
|
173
|
+
errors.push("You must specify the binaries in the package");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (errors.length > 0) {
|
|
177
|
+
let errorMsg =
|
|
178
|
+
"One or more of the parameters you passed to the Binary constructor are invalid:\n";
|
|
179
|
+
errors.forEach((error) => {
|
|
180
|
+
errorMsg += error;
|
|
181
|
+
});
|
|
182
|
+
errorMsg +=
|
|
183
|
+
'\n\nCorrect usage: new Package("my-binary", "https://example.com/binary/download.tar.gz", {"my-binary": "my-binary"})';
|
|
184
|
+
error(errorMsg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.platform = platform;
|
|
188
|
+
this.url = url;
|
|
189
|
+
this.name = name;
|
|
190
|
+
this.filename = filename;
|
|
191
|
+
this.zipExt = zipExt;
|
|
192
|
+
this.installDirectory = join(__dirname, "node_modules", ".bin_real");
|
|
193
|
+
this.binaries = binaries;
|
|
194
|
+
|
|
195
|
+
if (!existsSync(this.installDirectory)) {
|
|
196
|
+
mkdirSync(this.installDirectory, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
exists() {
|
|
201
|
+
for (const binaryName in this.binaries) {
|
|
202
|
+
const binRelPath = this.binaries[binaryName];
|
|
203
|
+
const binPath = join(this.installDirectory, binRelPath);
|
|
204
|
+
if (!existsSync(binPath)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
install(suppressLogs = false) {
|
|
212
|
+
if (this.exists()) {
|
|
213
|
+
if (!suppressLogs) {
|
|
214
|
+
console.error(
|
|
215
|
+
`${this.name} is already installed, skipping installation.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return Promise.resolve();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
rmSync(this.installDirectory, { recursive: true, force: true });
|
|
223
|
+
} catch {
|
|
224
|
+
// ignore - directory may not exist
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
mkdirSync(this.installDirectory, { recursive: true });
|
|
228
|
+
|
|
229
|
+
if (!suppressLogs) {
|
|
230
|
+
console.error(`Downloading release from ${this.url}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return download(this.url)
|
|
234
|
+
.then((res) => {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
|
|
237
|
+
if (err) return reject(err);
|
|
238
|
+
let tempFile = join(directory, this.filename);
|
|
239
|
+
const sink = res.pipe(createWriteStream(tempFile));
|
|
240
|
+
sink.on("error", (err) => reject(err));
|
|
241
|
+
sink.on("close", () => {
|
|
242
|
+
if (/\.tar\.*/.test(this.zipExt)) {
|
|
243
|
+
const result = spawnSync("tar", [
|
|
244
|
+
"xf",
|
|
245
|
+
tempFile,
|
|
246
|
+
// The tarballs are stored with a leading directory
|
|
247
|
+
// component; we strip one component in the
|
|
248
|
+
// shell installers too.
|
|
249
|
+
"--strip-components",
|
|
250
|
+
"1",
|
|
251
|
+
"-C",
|
|
252
|
+
this.installDirectory,
|
|
253
|
+
]);
|
|
254
|
+
if (result.status == 0) {
|
|
255
|
+
resolve();
|
|
256
|
+
} else if (result.error) {
|
|
257
|
+
reject(result.error);
|
|
258
|
+
} else {
|
|
259
|
+
reject(
|
|
260
|
+
new Error(
|
|
261
|
+
`An error occurred untarring the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} else if (this.zipExt == ".zip") {
|
|
266
|
+
let result;
|
|
267
|
+
if (this.platform.artifactName.includes("windows")) {
|
|
268
|
+
// Windows does not have "unzip" by default on many installations, instead
|
|
269
|
+
// we use Expand-Archive from powershell
|
|
270
|
+
result = spawnSync("powershell.exe", [
|
|
271
|
+
"-NoProfile",
|
|
272
|
+
"-NonInteractive",
|
|
273
|
+
"-Command",
|
|
274
|
+
`& {
|
|
275
|
+
param([string]$LiteralPath, [string]$DestinationPath)
|
|
276
|
+
Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force
|
|
277
|
+
}`,
|
|
278
|
+
tempFile,
|
|
279
|
+
this.installDirectory,
|
|
280
|
+
]);
|
|
281
|
+
} else {
|
|
282
|
+
result = spawnSync("unzip", [
|
|
283
|
+
"-q",
|
|
284
|
+
tempFile,
|
|
285
|
+
"-d",
|
|
286
|
+
this.installDirectory,
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (result.status == 0) {
|
|
291
|
+
resolve();
|
|
292
|
+
} else if (result.error) {
|
|
293
|
+
reject(result.error);
|
|
294
|
+
} else {
|
|
295
|
+
reject(
|
|
296
|
+
new Error(
|
|
297
|
+
`An error occurred unzipping the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
reject(
|
|
303
|
+
new Error(`Unrecognized file extension: ${this.zipExt}`),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
})
|
|
310
|
+
.then(() => {
|
|
311
|
+
if (!suppressLogs) {
|
|
312
|
+
console.error(`${this.name} has been installed!`);
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
.catch((e) => {
|
|
316
|
+
error(`Error fetching release: ${e.message}`);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
run(binaryName) {
|
|
321
|
+
const promise = !this.exists() ? this.install(true) : Promise.resolve();
|
|
322
|
+
|
|
323
|
+
promise
|
|
324
|
+
.then(() => {
|
|
325
|
+
const [, , ...args] = process.argv;
|
|
326
|
+
|
|
327
|
+
const options = { cwd: process.cwd(), stdio: "inherit" };
|
|
328
|
+
|
|
329
|
+
const binRelPath = this.binaries[binaryName];
|
|
330
|
+
if (!binRelPath) {
|
|
331
|
+
error(`${binaryName} is not a known binary in ${this.name}`);
|
|
332
|
+
}
|
|
333
|
+
const binPath = join(this.installDirectory, binRelPath);
|
|
334
|
+
const result = spawnSync(binPath, args, options);
|
|
335
|
+
|
|
336
|
+
if (result.error) {
|
|
337
|
+
error(result.error);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
process.exit(result.status);
|
|
341
|
+
})
|
|
342
|
+
.catch((e) => {
|
|
343
|
+
error(e.message);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports.Package = Package;
|
package/binary.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const { Package } = require("./binary-install");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const libc = require("detect-libc");
|
|
4
|
+
|
|
5
|
+
const error = (msg) => {
|
|
6
|
+
console.error(msg);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
name,
|
|
12
|
+
artifactDownloadUrls,
|
|
13
|
+
supportedPlatforms,
|
|
14
|
+
glibcMinimum,
|
|
15
|
+
} = require("./package.json");
|
|
16
|
+
|
|
17
|
+
// FIXME: implement NPM installer handling of fallback download URLs
|
|
18
|
+
const artifactDownloadUrl = artifactDownloadUrls[0];
|
|
19
|
+
const builderGlibcMajorVersion = glibcMinimum.major;
|
|
20
|
+
const builderGlibcMinorVersion = glibcMinimum.series;
|
|
21
|
+
|
|
22
|
+
const getPlatform = () => {
|
|
23
|
+
const rawOsType = os.type();
|
|
24
|
+
const rawArchitecture = os.arch();
|
|
25
|
+
|
|
26
|
+
// We want to use rust-style target triples as the canonical key
|
|
27
|
+
// for a platform, so translate the "os" library's concepts into rust ones
|
|
28
|
+
let osType = "";
|
|
29
|
+
switch (rawOsType) {
|
|
30
|
+
case "Windows_NT":
|
|
31
|
+
osType = "pc-windows-msvc";
|
|
32
|
+
break;
|
|
33
|
+
case "Darwin":
|
|
34
|
+
osType = "apple-darwin";
|
|
35
|
+
break;
|
|
36
|
+
case "Linux":
|
|
37
|
+
osType = "unknown-linux-gnu";
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let arch = "";
|
|
42
|
+
switch (rawArchitecture) {
|
|
43
|
+
case "x64":
|
|
44
|
+
arch = "x86_64";
|
|
45
|
+
break;
|
|
46
|
+
case "arm64":
|
|
47
|
+
arch = "aarch64";
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (rawOsType === "Linux") {
|
|
52
|
+
if (libc.familySync() == "musl") {
|
|
53
|
+
osType = "unknown-linux-musl-dynamic";
|
|
54
|
+
} else if (libc.isNonGlibcLinuxSync()) {
|
|
55
|
+
console.warn(
|
|
56
|
+
"Your libc is neither glibc nor musl; trying static musl binary instead",
|
|
57
|
+
);
|
|
58
|
+
osType = "unknown-linux-musl-static";
|
|
59
|
+
} else {
|
|
60
|
+
let libcVersion = libc.versionSync();
|
|
61
|
+
let splitLibcVersion = libcVersion.split(".");
|
|
62
|
+
let libcMajorVersion = splitLibcVersion[0];
|
|
63
|
+
let libcMinorVersion = splitLibcVersion[1];
|
|
64
|
+
if (
|
|
65
|
+
libcMajorVersion != builderGlibcMajorVersion ||
|
|
66
|
+
libcMinorVersion < builderGlibcMinorVersion
|
|
67
|
+
) {
|
|
68
|
+
// We can't run the glibc binaries, but we can run the static musl ones
|
|
69
|
+
// if they exist
|
|
70
|
+
console.warn(
|
|
71
|
+
"Your glibc isn't compatible; trying static musl binary instead",
|
|
72
|
+
);
|
|
73
|
+
osType = "unknown-linux-musl-static";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Assume the above succeeded and build a target triple to look things up with.
|
|
79
|
+
// If any of it failed, this lookup will fail and we'll handle it like normal.
|
|
80
|
+
let targetTriple = `${arch}-${osType}`;
|
|
81
|
+
let platform = supportedPlatforms[targetTriple];
|
|
82
|
+
|
|
83
|
+
if (!platform) {
|
|
84
|
+
error(
|
|
85
|
+
`Platform with type "${rawOsType}" and architecture "${rawArchitecture}" is not supported by ${name}.\nYour system must be one of the following:\n\n${Object.keys(
|
|
86
|
+
supportedPlatforms,
|
|
87
|
+
).join(",")}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return platform;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const getPackage = () => {
|
|
95
|
+
const platform = getPlatform();
|
|
96
|
+
const url = `${artifactDownloadUrl}/${platform.artifactName}`;
|
|
97
|
+
let filename = platform.artifactName;
|
|
98
|
+
let ext = platform.zipExt;
|
|
99
|
+
let binary = new Package(platform, name, url, filename, ext, platform.bins);
|
|
100
|
+
|
|
101
|
+
return binary;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const install = (suppressLogs) => {
|
|
105
|
+
if (!artifactDownloadUrl || artifactDownloadUrl.length === 0) {
|
|
106
|
+
console.warn("in demo mode, not installing binaries");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const pkg = getPackage();
|
|
110
|
+
|
|
111
|
+
return pkg.install(suppressLogs);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const run = (binaryName) => {
|
|
115
|
+
const pkg = getPackage();
|
|
116
|
+
|
|
117
|
+
pkg.run(binaryName);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
install,
|
|
122
|
+
run,
|
|
123
|
+
getPackage,
|
|
124
|
+
};
|
package/install.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 3,
|
|
3
|
+
"name": "@sunstack/cli",
|
|
4
|
+
"packages": {
|
|
5
|
+
"": {
|
|
6
|
+
"bin": {
|
|
7
|
+
"sunstack": "run-sunstack.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"detect-libc": "^2.1.2"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"prettier": "^3.8.3"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=14.14",
|
|
17
|
+
"npm": ">=6"
|
|
18
|
+
},
|
|
19
|
+
"hasInstallScript": true,
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"name": "@sunstack/cli",
|
|
22
|
+
"version": "0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"node_modules/detect-libc": {
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=8"
|
|
27
|
+
},
|
|
28
|
+
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
31
|
+
"version": "2.1.2"
|
|
32
|
+
},
|
|
33
|
+
"node_modules/prettier": {
|
|
34
|
+
"bin": {
|
|
35
|
+
"prettier": "bin/prettier.cjs"
|
|
36
|
+
},
|
|
37
|
+
"dev": true,
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=14"
|
|
40
|
+
},
|
|
41
|
+
"funding": {
|
|
42
|
+
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
43
|
+
},
|
|
44
|
+
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
|
47
|
+
"version": "3.8.3"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"requires": true,
|
|
51
|
+
"version": "0.1.0"
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,96 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
2
|
+
"artifactDownloadUrls": [
|
|
3
|
+
"https://github.com/ShravanSunder/sunstack-tools/releases/download/v0.1.0"
|
|
4
|
+
],
|
|
5
|
+
"bin": {
|
|
6
|
+
"sunstack": "run-sunstack.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"detect-libc": "^2.1.2"
|
|
10
|
+
},
|
|
11
|
+
"description": "Lint repository documents and execute native Sunstack checks",
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"prettier": "^3.8.3"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=14.14",
|
|
17
|
+
"npm": ">=6"
|
|
8
18
|
},
|
|
19
|
+
"glibcMinimum": {
|
|
20
|
+
"major": 2,
|
|
21
|
+
"series": 31
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/ShravanSunder/sunstack-tools",
|
|
9
24
|
"license": "MIT",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
25
|
+
"name": "@sunstack/cli",
|
|
26
|
+
"preferUnplugged": true,
|
|
27
|
+
"repository": "https://github.com/ShravanSunder/sunstack-tools",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"fmt": "prettier --write **/*.js",
|
|
30
|
+
"fmt:check": "prettier --check **/*.js",
|
|
31
|
+
"postinstall": "node ./install.js"
|
|
32
|
+
},
|
|
33
|
+
"supportedPlatforms": {
|
|
34
|
+
"aarch64-apple-darwin": {
|
|
35
|
+
"artifactName": "sunstack-aarch64-apple-darwin.tar.gz",
|
|
36
|
+
"bins": {
|
|
37
|
+
"sunstack": "sunstack"
|
|
38
|
+
},
|
|
39
|
+
"zipExt": ".tar.gz"
|
|
40
|
+
},
|
|
41
|
+
"aarch64-unknown-linux-gnu": {
|
|
42
|
+
"artifactName": "sunstack-aarch64-unknown-linux-musl.tar.gz",
|
|
43
|
+
"bins": {
|
|
44
|
+
"sunstack": "sunstack"
|
|
45
|
+
},
|
|
46
|
+
"zipExt": ".tar.gz"
|
|
47
|
+
},
|
|
48
|
+
"aarch64-unknown-linux-musl-dynamic": {
|
|
49
|
+
"artifactName": "sunstack-aarch64-unknown-linux-musl.tar.gz",
|
|
50
|
+
"bins": {
|
|
51
|
+
"sunstack": "sunstack"
|
|
52
|
+
},
|
|
53
|
+
"zipExt": ".tar.gz"
|
|
54
|
+
},
|
|
55
|
+
"aarch64-unknown-linux-musl-static": {
|
|
56
|
+
"artifactName": "sunstack-aarch64-unknown-linux-musl.tar.gz",
|
|
57
|
+
"bins": {
|
|
58
|
+
"sunstack": "sunstack"
|
|
59
|
+
},
|
|
60
|
+
"zipExt": ".tar.gz"
|
|
61
|
+
},
|
|
62
|
+
"x86_64-apple-darwin": {
|
|
63
|
+
"artifactName": "sunstack-x86_64-apple-darwin.tar.gz",
|
|
64
|
+
"bins": {
|
|
65
|
+
"sunstack": "sunstack"
|
|
66
|
+
},
|
|
67
|
+
"zipExt": ".tar.gz"
|
|
68
|
+
},
|
|
69
|
+
"x86_64-unknown-linux-gnu": {
|
|
70
|
+
"artifactName": "sunstack-x86_64-unknown-linux-musl.tar.gz",
|
|
71
|
+
"bins": {
|
|
72
|
+
"sunstack": "sunstack"
|
|
73
|
+
},
|
|
74
|
+
"zipExt": ".tar.gz"
|
|
75
|
+
},
|
|
76
|
+
"x86_64-unknown-linux-musl-dynamic": {
|
|
77
|
+
"artifactName": "sunstack-x86_64-unknown-linux-musl.tar.gz",
|
|
78
|
+
"bins": {
|
|
79
|
+
"sunstack": "sunstack"
|
|
80
|
+
},
|
|
81
|
+
"zipExt": ".tar.gz"
|
|
82
|
+
},
|
|
83
|
+
"x86_64-unknown-linux-musl-static": {
|
|
84
|
+
"artifactName": "sunstack-x86_64-unknown-linux-musl.tar.gz",
|
|
85
|
+
"bins": {
|
|
86
|
+
"sunstack": "sunstack"
|
|
87
|
+
},
|
|
88
|
+
"zipExt": ".tar.gz"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"version": "0.1.0",
|
|
92
|
+
"volta": {
|
|
93
|
+
"node": "18.14.1",
|
|
94
|
+
"npm": "9.5.0"
|
|
95
|
+
}
|
|
96
|
+
}
|
package/run-sunstack.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Keep cargo-dist installation; async execution preserves native cancellation and exits.
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
const { join } = require('node:path');
|
|
5
|
+
const { getPackage } = require('./binary');
|
|
6
|
+
|
|
7
|
+
async function runSunstack() {
|
|
8
|
+
const binaryPackage = getPackage();
|
|
9
|
+
if (!binaryPackage.exists()) await binaryPackage.install(true);
|
|
10
|
+
const child = spawn(
|
|
11
|
+
join(binaryPackage.installDirectory, binaryPackage.binaries.sunstack),
|
|
12
|
+
process.argv.slice(2),
|
|
13
|
+
{ stdio: 'inherit' },
|
|
14
|
+
);
|
|
15
|
+
/** @type {NodeJS.Signals[]} */
|
|
16
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
17
|
+
const handlers = new Map(signals.map((signal) => [signal, () => child.kill(signal)]));
|
|
18
|
+
for (const [signal, handler] of handlers) process.on(signal, handler);
|
|
19
|
+
child.on('error', (error) => {
|
|
20
|
+
process.stderr.write(`${error.message}\n`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
});
|
|
23
|
+
child.on('close', (code, signal) => {
|
|
24
|
+
for (const [forwarded, handler] of handlers) process.removeListener(forwarded, handler);
|
|
25
|
+
if (signal) process.kill(process.pid, signal);
|
|
26
|
+
else process.exitCode = code ?? 1;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
runSunstack().catch((error) => {
|
|
31
|
+
process.stderr.write(`${error.message}\n`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
});
|