@sunstack/cli 0.0.0 → 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/.gitignore +2 -0
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +282 -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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
This pre-1.0 minor release cuts over the lint configuration and rulebook format.
|
|
6
|
+
|
|
7
|
+
- Replace lint's `rule_sets` configuration with marked Markdown `rulebook` pairs,
|
|
8
|
+
checked-file globs, an ast-grep evidence project and `decision_model`. Rule
|
|
9
|
+
entries run explicit mechanical and model decision steps on evidence pieces;
|
|
10
|
+
asking rules always require a configured model and credential.
|
|
11
|
+
- Add `sunstack lint --check` for credential-free admission and rule listing,
|
|
12
|
+
and `--evidence [KIND]` for inspecting picked evidence without model requests.
|
|
13
|
+
`--init` writes a generic configuration skeleton.
|
|
14
|
+
- Report uncertain decisions with their step, sent state and answers. Exit 5
|
|
15
|
+
means uncertainty when cancellation, incomplete work or failing findings do
|
|
16
|
+
not take precedence.
|
|
17
|
+
- Remove native RuleSet `rule_sources`, `marker_prefix` and `max_question_words`.
|
|
18
|
+
Native JSONL `rule_paths` and conversation operations remain supported. Existing
|
|
19
|
+
lint configurations must migrate to the [current configuration](README.md#repository-lint);
|
|
20
|
+
removed keys are refused with replacement guidance.
|
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,284 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sunstack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lint repository documents with a marked rulebook, ast-grep evidence and explicit
|
|
4
|
+
decision steps. Use `sunstack exec` for the native JSON protocol.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Repository lint
|
|
7
|
+
|
|
8
|
+
Create a configuration skeleton with `sunstack lint --init`, then add the
|
|
9
|
+
referenced Markdown rulebook, JSON rule entries and ast-grep project. Init writes
|
|
10
|
+
only `sunstack.lint.json` and refuses to overwrite an existing file.
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"rulebook": [{ "markdown": "rulebook.md", "rules": "rulebook.rules.json" }],
|
|
15
|
+
"marker_prefix": "K",
|
|
16
|
+
"check": { "files": ["docs/**/*.md"], "ignore": ["docs/archive/**"] },
|
|
17
|
+
"evidence": {
|
|
18
|
+
"ast_grep": "evidence/sgconfig.yml",
|
|
19
|
+
"kinds": {
|
|
20
|
+
"piece": {
|
|
21
|
+
"rule": "your-rule-id",
|
|
22
|
+
"fields": { "body": { "from": "remainder" } }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"decision_model": { "model": "jev-latest" }
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Replace `your-rule-id` with a rule in your ast-grep project. An end-of-line marker
|
|
31
|
+
such as `<!-- K001 -->` binds a Markdown standard to its `K001` JSON entry.
|
|
32
|
+
Each entry names an evidence kind and a decision graph. Steps can check fields
|
|
33
|
+
mechanically, ask authored questions, combine answers and route to pass or a
|
|
34
|
+
finding. Questions reach the model as authored, with one piece's bounded field
|
|
35
|
+
view. The binary embeds six generic Markdown utilities; rulebooks, questions and
|
|
36
|
+
domain-specific evidence rules belong to the caller.
|
|
37
|
+
|
|
38
|
+
Rulebook and ast-grep project paths resolve relative to the configuration file.
|
|
39
|
+
Checked-file globs resolve from its Git repository root, or from the config
|
|
40
|
+
directory outside Git. Without `--config`, lint searches the current directory
|
|
41
|
+
and its parents for `sunstack.lint.json`.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
sunstack lint --config sunstack.lint.json --check
|
|
45
|
+
sunstack lint --config sunstack.lint.json --all --evidence
|
|
46
|
+
sunstack lint --config sunstack.lint.json --all --evidence piece
|
|
47
|
+
sunstack lint --config sunstack.lint.json --all --format json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`--check` validates configuration, the rulebook and evidence project, then lists
|
|
51
|
+
the assembled rules as JSON. It reads no checked files and sends no model
|
|
52
|
+
requests. `--evidence [KIND]` prints picked evidence and its fields as JSON without
|
|
53
|
+
asking the model. Both routes work without a credential.
|
|
54
|
+
|
|
55
|
+
An ordinary lint run uses changed evidence by default and requires Git for that
|
|
56
|
+
default scope. Use `--all` for every configured target, `--since REV` for changes
|
|
57
|
+
since a revision including worktree edits, or name files or directories for full
|
|
58
|
+
scope within those paths. Findings retain their rule, marker and source
|
|
59
|
+
location. Available output formats are `default`, `json`, `github` and `sarif`.
|
|
60
|
+
The JSON report includes `results`, one entry per evaluated rule/piece pair in
|
|
61
|
+
logical report order. Each entry has the rule's `name` and marker, the piece's
|
|
62
|
+
file and first and last line, its outcome (`pass`, `finding`, `not_applicable`,
|
|
63
|
+
`uncertain` or `incomplete`) and its ending `step` (null before any step ran).
|
|
64
|
+
|
|
65
|
+
The decision model is always configured and enabled for asking rules; there is
|
|
66
|
+
no model-off switch. Ordinary runs with asking rules require an OpenRouter
|
|
67
|
+
credential before checked-file acquisition, even if a later mechanical branch
|
|
68
|
+
would avoid an ask. Mechanical-only rules need no key. Credential lookup and
|
|
69
|
+
saving are described below under the native protocol.
|
|
70
|
+
|
|
71
|
+
Lint exits 0 for clean or nothing selected, 1 for error findings or warnings over
|
|
72
|
+
`--max-warnings`, 2 for invalid input, 3 for incomplete work, 4 for cancellation,
|
|
73
|
+
and 5 for uncertain decisions. Uncertainty retains the decision step, sent state
|
|
74
|
+
and answers for the caller to inspect. Cancellation takes precedence over
|
|
75
|
+
incomplete work, then findings, then uncertainty. A warning alone does not fail
|
|
76
|
+
unless it exceeds `--max-warnings`.
|
|
77
|
+
|
|
78
|
+
See the checked-in [worked configuration](tests/fixtures/rulebook-lint/skill-rules.config.json)
|
|
79
|
+
and its [rulebook entries](tests/fixtures/rulebook-lint/rulebook/steering.rules.json)
|
|
80
|
+
for a complete test example. Copy the fixture directory to an isolated root to
|
|
81
|
+
try it; its checked-file globs are relative to that root. The example's
|
|
82
|
+
skill-specific rules are test content.
|
|
83
|
+
|
|
84
|
+
## Native JSON protocol
|
|
85
|
+
|
|
86
|
+
Run `sunstack exec` (or `cargo run -p sunstack -- exec` from the repository root).
|
|
87
|
+
One process consumes one JSON request on stdin and writes one JSON response on
|
|
88
|
+
stdout. Relative input paths resolve from the process working directory. Run
|
|
89
|
+
from the repository root when using the reviewed examples.
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{ "protocol_version": 1, "operation": "evaluate_rules", "input": {} }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Replace `input` with the native operation input. Supported operations are
|
|
96
|
+
`prepare_evidence`, `run_judgments`, `check_rules`, `prepare_rules` and `evaluate_rules`.
|
|
97
|
+
The response is either `protocol_error` with diagnostics, or `operation_result`
|
|
98
|
+
whose `result` contains the operation and its unchanged native outcome.
|
|
99
|
+
The raw stdin limit is 8,388,608 bytes. Bind large evidence by path.
|
|
100
|
+
|
|
101
|
+
Exit 0 means the operation completed, including an unfavorable rule verdict.
|
|
102
|
+
Exit 2 means invalid input or protocol refusal; 3 means failed or incomplete
|
|
103
|
+
execution; 4 means caller cancellation. SIGINT and SIGTERM cancel owned work
|
|
104
|
+
and await its settlement before publishing the terminal response. The static
|
|
105
|
+
`sunstack: ready` stderr line witnesses signal registration, and
|
|
106
|
+
`sunstack: cancellation observed` witnesses signal consumption.
|
|
107
|
+
|
|
108
|
+
Change-scoped work retains the same protocol version. Native inputs
|
|
109
|
+
accept non-Full `change_scope` values (dirty layers, revision ranges, commit lists,
|
|
110
|
+
time windows and unions), bounded discovery, ChangeSet sources and explicit
|
|
111
|
+
package bindings. A relative repository root resolves from this process's CWD.
|
|
112
|
+
For example, select current unstaged changes on a repository binding with:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "change_scope": { "kind": "uncommitted", "layers": ["unstaged"] } }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This is a binding fragment, not a complete invocation. ChangeSet selectors need
|
|
119
|
+
`discovery.kind = "bounded"` with native discovery limits. A package binding's
|
|
120
|
+
empty `target_path_prefix` maps to the target repository root. Use the reviewed
|
|
121
|
+
[invocation schema](../../docs/specs/2026-10-12-rust-rule-engine/schemas/rule-invocation.schema.json)
|
|
122
|
+
for complete inputs.
|
|
123
|
+
|
|
124
|
+
Preparation and reports expose native `change_selections`; Full-only work has
|
|
125
|
+
an empty list. Rule occurrences retain selection reasons such as
|
|
126
|
+
`target_changed` or `full_scope`, pending comparisons, and outside-scope results.
|
|
127
|
+
Preparing rules sends no model request. A waiting gate leaves target comparison
|
|
128
|
+
pending; evaluation resolves it only when the native gate permits the work.
|
|
129
|
+
The CLI forwards these native outcomes and uses the same exit meanings above.
|
|
130
|
+
|
|
131
|
+
The binary uses a non-empty `OPENROUTER_API_KEY` first, otherwise the OS credential
|
|
132
|
+
store (`sunstack` / `openrouter-api-key`): macOS login Keychain or Linux Secret
|
|
133
|
+
Service. When the environment key is present, a run never reads or writes the store.
|
|
134
|
+
Runs never save credentials automatically. Use `sunstack credentials save` to save
|
|
135
|
+
the current environment key explicitly; it is listed in `sunstack --help`.
|
|
136
|
+
The command reports its destination without printing the key and refuses a missing
|
|
137
|
+
or empty variable. Set
|
|
138
|
+
`SUNSTACK_CREDENTIAL_STORE=off` to disable both lookup and saving (for CI or a one-off
|
|
139
|
+
key). Library callers pass the key as a value to `sunstack_lint::run`.
|
|
140
|
+
Without a terminal on stdin, macOS access that needs Keychain approval is treated
|
|
141
|
+
as unavailable immediately. Run once from a terminal to approve a changed build,
|
|
142
|
+
or supply the environment key. Interactive terminal runs may show the Keychain dialog.
|
|
143
|
+
On Linux, a locked Secret Service collection may prompt during fallback lookup or explicit saving;
|
|
144
|
+
use `SUNSTACK_CREDENTIAL_STORE=off` for unattended runs.
|
|
145
|
+
|
|
146
|
+
Credentials never come from stdin. Rule preparation uses the selected backend for encoding and accounting
|
|
147
|
+
when model work is reachable; it sends no provider request. Local-only rule
|
|
148
|
+
preparation and evaluation construct no backend and need no credential. A
|
|
149
|
+
model-based judgment, applicability gate or Router still requires valid backend
|
|
150
|
+
configuration when it becomes reachable. Evidence preparation needs no credential.
|
|
151
|
+
|
|
152
|
+
`check_rules` assembles every requested RuleSet and validates its rules without
|
|
153
|
+
constructing a backend, reading reviewed documents or sending model requests.
|
|
154
|
+
Its input contains `definition_root`, `definitions`, `repositories`,
|
|
155
|
+
`payload_schemas`, `limits`, optional `discovery` and `output` (`diagnostics` or `assembled_rules`).
|
|
156
|
+
Discovery defaults to `none`, as in runtime input. Connected rules need bounded
|
|
157
|
+
discovery settings for admission; check never executes discovery or acquisition.
|
|
158
|
+
Repository-root metadata and declared payload schemas are still validated.
|
|
159
|
+
It accepts no backend, attempt or request-budget settings. A clean check exits 0;
|
|
160
|
+
refusals exit 2 with definition file, line and column. Printed rules are returned
|
|
161
|
+
only after a clean check and include each native definition and its source.
|
|
162
|
+
|
|
163
|
+
Native RuleSets load `rule_paths` JSONL files, with paths relative to the RuleSet
|
|
164
|
+
file. Use the repository lint route above for marked Markdown rulebooks. Native
|
|
165
|
+
preparation and evaluation retain their coverage, rule evaluations, skipped rules
|
|
166
|
+
and counters. JSONL, RuleSet-header and invocation errors remain strict refusals.
|
|
167
|
+
|
|
168
|
+
Document targets can select Markdown units independently:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"kind": "markdown_units",
|
|
173
|
+
"mode": "call_site",
|
|
174
|
+
"units": ["paragraph", "list_item"],
|
|
175
|
+
"pattern": "\\bMUST\\s+load\\b"
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`mode: unit` produces one occurrence per requested section, paragraph or list
|
|
180
|
+
item, with an optional `starts_with` or `pattern` filter. Requested unit kinds
|
|
181
|
+
can overlap. `mode: call_site` requires exactly one nonempty filter and produces
|
|
182
|
+
one occurrence per match, owned by the innermost requested unit; list items own
|
|
183
|
+
their contained paragraphs. Regexes that can match empty text are refused.
|
|
184
|
+
Filters use visible text with inline code excluded; fenced content remains.
|
|
185
|
+
|
|
186
|
+
Occurrence keys include source identity, raw unit ranges, and call-site raw and
|
|
187
|
+
visible match ranges. Raw spans use original document bytes and character
|
|
188
|
+
coordinates. Several visible matches may share a raw entity span; generated-only
|
|
189
|
+
matches use zero-width raw boundary anchors. Question owners carry the full key,
|
|
190
|
+
so budget splitting preserves each target's evidence and answer references.
|
|
191
|
+
|
|
192
|
+
Preparation exposes `rule_evaluations`, including `waiting_for_expansion` behind
|
|
193
|
+
a staged different-input gate. No child keys or target selections are invented
|
|
194
|
+
while it waits. Evaluation reports `expanded`, `zero_matches`, stopped, failed
|
|
195
|
+
or cancelled work explicitly. Zero matches is complete when other work completes;
|
|
196
|
+
it is never a passed verdict. Same-input speculative carriers remain permitted.
|
|
197
|
+
Existing change-scope repository snapshots may already hold the reviewed bytes;
|
|
198
|
+
canonical target selection and expansion still wait for their gate frontier.
|
|
199
|
+
|
|
200
|
+
The evidence library also supports `frontmatter_field` with a literal top-level
|
|
201
|
+
`field`. An initial YAML `---` block yields a decoded JSON value or an absent
|
|
202
|
+
field. Missing or malformed blocks yield explicit unavailable data without an
|
|
203
|
+
invented span. Noul and composite rules reject this mechanical-only target.
|
|
204
|
+
|
|
205
|
+
A rules entry can use a native mechanical verdict. Put the check under
|
|
206
|
+
`verdict.check` and select the checked field in the evidence declaration:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"rule_id": "skill.description",
|
|
211
|
+
"evidence": [
|
|
212
|
+
{
|
|
213
|
+
"id": "description",
|
|
214
|
+
"input": "subject",
|
|
215
|
+
"requirement": "required",
|
|
216
|
+
"select": { "kind": "frontmatter_field", "field": "description" }
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
"target": "description",
|
|
220
|
+
"applicability": [],
|
|
221
|
+
"questions": {},
|
|
222
|
+
"verdict": {
|
|
223
|
+
"kind": "mechanical",
|
|
224
|
+
"explanation": "Description names when to use the skill.",
|
|
225
|
+
"check": { "kind": "expect", "expect": { "starts_with": "Use when" } }
|
|
226
|
+
},
|
|
227
|
+
"severity": "error"
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Save each native definition as one JSONL line in a RuleSet's `rule_paths`, and
|
|
232
|
+
declare its `subject` input as a document in the RuleSet header. Bind that input
|
|
233
|
+
to the document when calling `check_rules` or `evaluate_rules`.
|
|
234
|
+
|
|
235
|
+
`verdict.check.expect` accepts exactly one of `matches`, `not_matches`, `starts_with` or
|
|
236
|
+
`present: true`. Text predicates use the unit's rendered-visible text, excluding
|
|
237
|
+
inline code. A frontmatter string uses its decoded text; other values use
|
|
238
|
+
canonical JSON text. Null is present. Every predicate fails on an absent field;
|
|
239
|
+
a missing or malformed header is unresolved.
|
|
240
|
+
|
|
241
|
+
For an AST check, use `verdict.check: {"kind":"ast_grep","ast_grep":{...}}`.
|
|
242
|
+
`ast_grep` accepts a native rule object validated by ast-grep-config 0.51.0.
|
|
243
|
+
It sees the full original Markdown syntax tree, including ancestors outside
|
|
244
|
+
the unit. A match fully contained in the target unit is a failure; sibling
|
|
245
|
+
matches outside it are excluded. Frontmatter fields use `expect`; runtime unit
|
|
246
|
+
kinds do not select the metadata block. Local checks send no model request.
|
|
247
|
+
|
|
248
|
+
Reports use `checked` occurrences with `completed` pass/fail, `unresolved` or
|
|
249
|
+
`cancelled` local outcomes. A failed check retains its declared severity and
|
|
250
|
+
can complete with exit 0. Unresolved checks make the report incomplete. Local
|
|
251
|
+
checks create no model answers, references, receipts or confidence and consume
|
|
252
|
+
no model question/request budget. Applicability and routing still govern them.
|
|
253
|
+
|
|
254
|
+
Rust callers supply `RuleBackendFactory` in `RuleExecutionContext`. Selection
|
|
255
|
+
is credential-free; the invocation calls `prepare` once when its first model
|
|
256
|
+
request reaches encoding and caches the result. Concrete built backends already
|
|
257
|
+
implement the factory trait and can still be passed with `Arc::new(backend)`.
|
|
258
|
+
|
|
259
|
+
The backend-free
|
|
260
|
+
[unit-target check request](../../docs/specs/2026-10-12-rust-rule-engine/examples/unit-targets.check.request.json)
|
|
261
|
+
checks a rule that selects every paragraph of the existing example document.
|
|
262
|
+
Like other checks, it validates the rule without reading that document.
|
|
263
|
+
|
|
264
|
+
Build with the pinned root toolchain and the SDK environment described in the
|
|
265
|
+
project verification skill. For a local development invocation, pass a request
|
|
266
|
+
file to `scripts/cargo-debug-signing-runner.sh target/debug/sunstack exec`.
|
|
267
|
+
The reviewed skill-lint input lives in
|
|
268
|
+
`docs/specs/2026-10-12-rust-rule-engine/examples/skill-lint.invocation.json`.
|
|
269
|
+
The handoff example contains illustrative transcript/schema paths; the permanent
|
|
270
|
+
journey fixture supplies the accepted generated schema and a documented Codex
|
|
271
|
+
transcript composition.
|
|
272
|
+
|
|
273
|
+
Run `cargo nextest run -p sunstack` for local process checks.
|
|
274
|
+
The explicitly ignored `journey_measurements` test runs the fixed 40-process
|
|
275
|
+
controlled workload. The two ignored `hosted_journeys` cases make four genuine
|
|
276
|
+
provider invocations and require a command-local credential. They are deliberate
|
|
277
|
+
paid observations; preserve failures and obtain a new campaign decision rather
|
|
278
|
+
than repeating until favorable. The ignored `hosted_request_diagnosis` is an
|
|
279
|
+
offline encoder observation for a preserved failed invocation, with no dispatch.
|
|
280
|
+
|
|
281
|
+
See [milestone 2 proof](tests/MILESTONE2-PROOF.md) for change-scoped and connected
|
|
282
|
+
process coverage and its proof limits. See [phase 2 proof](tests/PHASE2-PROOF.md) for the observed gates, measurements,
|
|
283
|
+
fixture provenance, failed campaign and proof limits. [Phase 1 proof](tests/PHASE1-PROOF.md)
|
|
284
|
+
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.2.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.2.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.2.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.2.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
|
+
});
|