@kyaukyuai/linear-cli 2.0.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 +477 -0
- package/LICENSE +15 -0
- package/README.md +338 -0
- package/binary-install.js +212 -0
- package/binary.js +128 -0
- package/install.js +4 -0
- package/npm-shrinkwrap.json +546 -0
- package/package.json +93 -0
- package/run-linear.js +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# kyaukyuai/linear-cli
|
|
2
|
+
|
|
3
|
+
`kyaukyuai/linear-cli` is a fork of [`schpet/linear-cli`](https://github.com/schpet/linear-cli) for teams that want a git-first Linear CLI with additional automation, scripting, and documentation support. it remains git and [jj](https://www.jj-vcs.dev/) aware so you can stay in the right Linear context without leaving the terminal.
|
|
4
|
+
|
|
5
|
+
**works great with AI agents** — the CLI includes a [skill](#skills) that lets agents create issues, update status, and manage your Linear workflow alongside your code.
|
|
6
|
+
|
|
7
|
+
here's how it works:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
linear config # setup your repo, it writes a config file
|
|
11
|
+
|
|
12
|
+
linear issue list # list unstarted issues assigned to you
|
|
13
|
+
linear issue list -A # list unstarted issues assigned to anyone
|
|
14
|
+
linear issue start # choose an issue to start, creates a branch
|
|
15
|
+
linear issue start ABC-123 # start a specific issue
|
|
16
|
+
linear issue view # see current branch's issue as markdown
|
|
17
|
+
linear issue pr # makes a PR with title/body preset, using gh cli
|
|
18
|
+
linear issue create # create a new issue
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
it aims to be a complement to the web and desktop apps that lets you stay on the command line in an interactive or scripted way.
|
|
22
|
+
|
|
23
|
+
## screencast demos
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary><code>linear issue create</code></summary>
|
|
27
|
+
|
|
28
|
+
<img width="600" src="docs/cast-issue-create.svg?1" alt="screencast showing the linear issue create command, interactively adding issue details">
|
|
29
|
+
|
|
30
|
+
</details>
|
|
31
|
+
|
|
32
|
+
<details>
|
|
33
|
+
<summary><code>linear issue start</code></summary>
|
|
34
|
+
|
|
35
|
+
<img width="600" src="docs/cast-issue-start.svg?1" alt="screencast showing the linear issue start command, interactively choosing an issue to start">
|
|
36
|
+
|
|
37
|
+
</details>
|
|
38
|
+
|
|
39
|
+
## install
|
|
40
|
+
|
|
41
|
+
### homebrew
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
brew install kyaukyuai/tap/linear
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### deno via jsr
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
deno install -A --reload -f -g -n linear jsr:@kyaukyuai/linear-cli
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### npm / bun / pnpm
|
|
54
|
+
|
|
55
|
+
install as a dev dependency to pin a version in your project:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install -D @kyaukyuai/linear-cli
|
|
59
|
+
# or
|
|
60
|
+
bun add -D @kyaukyuai/linear-cli
|
|
61
|
+
# or
|
|
62
|
+
pnpm add -D @kyaukyuai/linear-cli
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
then run via your package manager:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx linear issue list
|
|
69
|
+
bunx linear issue list
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> **note:** this package ships pre-built binaries
|
|
73
|
+
|
|
74
|
+
package on npm: [@kyaukyuai/linear-cli](https://www.npmjs.com/package/@kyaukyuai/linear-cli)
|
|
75
|
+
|
|
76
|
+
### binaries
|
|
77
|
+
|
|
78
|
+
https://github.com/kyaukyuai/linear-cli/releases/latest
|
|
79
|
+
|
|
80
|
+
### local dev
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/kyaukyuai/linear-cli
|
|
84
|
+
cd linear-cli
|
|
85
|
+
deno task install
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## fork-specific features
|
|
89
|
+
|
|
90
|
+
compared to upstream, this fork adds and maintains several capabilities aimed at automation-heavy workflows:
|
|
91
|
+
|
|
92
|
+
- cycle workflows beyond listing and viewing, including `cycle current`, `cycle next`, `cycle create`, `cycle add`, and `cycle remove`
|
|
93
|
+
- issue workflow commands for `search`, `assign`, `move`, `priority`, `estimate`, `label add/remove`, comment delete, relations, and attachments
|
|
94
|
+
- JSON output for scripting across issue, cycle, project, and document commands
|
|
95
|
+
- workspace-aware auth management with keyring migration and default workspace support
|
|
96
|
+
- generated AI-agent skill docs, Claude plugin metadata, npm publishing, and Homebrew tap release plumbing
|
|
97
|
+
|
|
98
|
+
## differences from upstream
|
|
99
|
+
|
|
100
|
+
this fork is intentionally diverging from upstream in a few ways:
|
|
101
|
+
|
|
102
|
+
- package and publishing identity use `kyaukyuai/linear-cli`, `@kyaukyuai/linear-cli`, and `kyaukyuai/tap/linear`
|
|
103
|
+
- maintainer workflows are standardized around git-based release automation, even though the CLI itself still supports both git and jj at runtime
|
|
104
|
+
- documentation and release assets are tailored for this fork's roadmap, including agent-facing docs and additional release infrastructure
|
|
105
|
+
- changelog and README content track fork-specific features separately from upstream history
|
|
106
|
+
|
|
107
|
+
## setup
|
|
108
|
+
|
|
109
|
+
1. create an API key at [linear.app/settings/account/security](https://linear.app/settings/account/security)[^1]
|
|
110
|
+
|
|
111
|
+
2. authenticate with the CLI:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
linear auth login
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
3. configure your project:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
cd my-project-repo
|
|
121
|
+
linear config
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
see [docs/authentication.md](docs/authentication.md) for multi-workspace support and other authentication options.
|
|
125
|
+
|
|
126
|
+
the CLI works with both git and jj version control systems:
|
|
127
|
+
|
|
128
|
+
- **git**: works best when your branches include Linear issue IDs (e.g. `eng-123-my-feature`). use `linear issue start` or linear UI's 'copy git branch name' button and [related automations](https://linear.app/docs/account-preferences#git-related-automations).
|
|
129
|
+
- **jj**: detects issues from `Linear-issue` trailers in your commit descriptions. use `linear issue start` to automatically add the trailer, or add it manually with `jj describe`, e.g. `jj describe "$(linear issue describe ABC-123)"`
|
|
130
|
+
|
|
131
|
+
## commands
|
|
132
|
+
|
|
133
|
+
### issue commands
|
|
134
|
+
|
|
135
|
+
the current issue is determined by:
|
|
136
|
+
|
|
137
|
+
- **git**: the issue id in the current branch name (e.g. `eng-123-my-feature`)
|
|
138
|
+
- **jj**: the `Linear-issue` trailer in the current or ancestor commits
|
|
139
|
+
|
|
140
|
+
note that [Linear's GitHub integration](https://linear.app/docs/github#branch-format) will suggest git branch names.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
linear issue view # view current issue details in terminal
|
|
144
|
+
linear issue view ABC-123
|
|
145
|
+
linear issue view 123
|
|
146
|
+
linear issue view -w # open issue in web browser
|
|
147
|
+
linear issue view -a # open issue in Linear.app
|
|
148
|
+
linear issue id # prints the issue id from current branch (e.g., "ENG-123")
|
|
149
|
+
linear issue title # prints just the issue title
|
|
150
|
+
linear issue url # prints the Linear.app URL for the issue
|
|
151
|
+
linear issue pr # creates a GitHub PR with issue details via `gh pr create`
|
|
152
|
+
linear issue list # list your issues in a table view (supports -s/--state and --sort)
|
|
153
|
+
linear issue list --project "My Project" --milestone "Phase 1" # filter by milestone
|
|
154
|
+
linear issue list -w # open issue list in web browser
|
|
155
|
+
linear issue list -a # open issue list in Linear.app
|
|
156
|
+
linear issue start # create/switch to issue branch and mark as started
|
|
157
|
+
linear issue create # create a new issue (interactive prompts)
|
|
158
|
+
linear issue create -t "title" -d "description" # create with flags
|
|
159
|
+
linear issue create --project "My Project" --milestone "Phase 1" # create with milestone
|
|
160
|
+
linear issue update # update an issue (interactive prompts)
|
|
161
|
+
linear issue update ENG-123 --milestone "Phase 2" # set milestone on existing issue
|
|
162
|
+
linear issue delete # delete an issue
|
|
163
|
+
linear issue comment list # list comments on current issue
|
|
164
|
+
linear issue comment add # add a comment to current issue
|
|
165
|
+
linear issue comment add -p <id> # reply to a specific comment
|
|
166
|
+
linear issue comment update <id> # update a comment
|
|
167
|
+
linear issue commits # show all commits for an issue (jj only)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### team commands
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
linear team list # list teams
|
|
174
|
+
linear team id # print out the team id (e.g. for scripts)
|
|
175
|
+
linear team members # list team members
|
|
176
|
+
linear team create # create a new team
|
|
177
|
+
linear team autolinks # configure GitHub repository autolinks for Linear issues
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### project commands
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
linear project list # list projects
|
|
184
|
+
linear project view # view project details
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### milestone commands
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
linear milestone list --project <projectId> # list milestones for a project
|
|
191
|
+
linear m list --project <projectId> # list milestones (alias)
|
|
192
|
+
linear milestone view <milestoneId> # view milestone details
|
|
193
|
+
linear m view <milestoneId> # view milestone (alias)
|
|
194
|
+
linear milestone create --project <projectId> --name "Q1 Goals" --target-date "2026-03-31" # create a milestone
|
|
195
|
+
linear m create --project <projectId> # create a milestone (interactive)
|
|
196
|
+
linear milestone update <milestoneId> --name "New Name" # update milestone name
|
|
197
|
+
linear m update <milestoneId> --target-date "2026-04-15" # update target date
|
|
198
|
+
linear milestone delete <milestoneId> # delete a milestone
|
|
199
|
+
linear m delete <milestoneId> --force # delete without confirmation
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### document commands
|
|
203
|
+
|
|
204
|
+
manage Linear documents from the command line. documents can be attached to projects or issues, or exist at the workspace level.
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# list documents
|
|
208
|
+
linear document list # list all accessible documents
|
|
209
|
+
linear docs list # alias for document
|
|
210
|
+
linear document list --project <projectId> # filter by project
|
|
211
|
+
linear document list --issue TC-123 # filter by issue
|
|
212
|
+
linear document list --json # output as JSON
|
|
213
|
+
|
|
214
|
+
# view a document
|
|
215
|
+
linear document view <slug> # view document rendered in terminal
|
|
216
|
+
linear document view <slug> --raw # output raw markdown (for piping)
|
|
217
|
+
linear document view <slug> --web # open in browser
|
|
218
|
+
linear document view <slug> --json # output as JSON
|
|
219
|
+
|
|
220
|
+
# create a document
|
|
221
|
+
linear document create --title "My Doc" --content "# Hello" # inline content
|
|
222
|
+
linear document create --title "Spec" --content-file ./spec.md # from file
|
|
223
|
+
linear document create --title "Doc" --project <projectId> # attach to project
|
|
224
|
+
linear document create --title "Notes" --issue TC-123 # attach to issue
|
|
225
|
+
cat spec.md | linear document create --title "Spec" # from stdin
|
|
226
|
+
|
|
227
|
+
# update a document
|
|
228
|
+
linear document update <slug> --title "New Title" # update title
|
|
229
|
+
linear document update <slug> --content-file ./updated.md # update content
|
|
230
|
+
linear document update <slug> --edit # open in $EDITOR
|
|
231
|
+
|
|
232
|
+
# delete a document
|
|
233
|
+
linear document delete <slug> # soft delete (move to trash)
|
|
234
|
+
linear document delete <slug> --permanent # permanent delete
|
|
235
|
+
linear document delete --bulk <slug1> <slug2> # bulk delete
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### other commands
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
linear --help # show all commands
|
|
242
|
+
linear --version # show version
|
|
243
|
+
linear config # setup the project
|
|
244
|
+
linear completions # generate shell completions
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## configuration options
|
|
248
|
+
|
|
249
|
+
the CLI supports configuration via environment variables or a `.linear.toml` config file. environment variables take precedence over config file values.
|
|
250
|
+
|
|
251
|
+
| option | env var | toml key | example | description |
|
|
252
|
+
| --------------- | ------------------------ | ----------------- | -------------------------- | ------------------------------------- |
|
|
253
|
+
| Team ID | `LINEAR_TEAM_ID` | `team_id` | `"ENG"` | default team for operations |
|
|
254
|
+
| Workspace | `LINEAR_WORKSPACE` | `workspace` | `"mycompany"` | workspace slug for web/app URLs |
|
|
255
|
+
| Issue sort | `LINEAR_ISSUE_SORT` | `issue_sort` | `"priority"` or `"manual"` | how to sort issue lists |
|
|
256
|
+
| VCS | `LINEAR_VCS` | `vcs` | `"git"` or `"jj"` | version control system (default: git) |
|
|
257
|
+
| Download images | `LINEAR_DOWNLOAD_IMAGES` | `download_images` | `true` or `false` | download images when viewing issues |
|
|
258
|
+
|
|
259
|
+
the config file can be placed at (checked in order, first found is used):
|
|
260
|
+
|
|
261
|
+
- `./linear.toml` or `./.linear.toml` (current directory)
|
|
262
|
+
- `<repo-root>/linear.toml` or `<repo-root>/.linear.toml` (repository root)
|
|
263
|
+
- `<repo-root>/.config/linear.toml`
|
|
264
|
+
- `$XDG_CONFIG_HOME/linear/linear.toml` or `~/.config/linear/linear.toml` (Unix)
|
|
265
|
+
- `%APPDATA%\linear\linear.toml` (Windows)
|
|
266
|
+
|
|
267
|
+
## skills
|
|
268
|
+
|
|
269
|
+
linear-cli includes a skill that helps AI agents use the CLI effectively. for use cases outside the CLI, it includes instructions to interact directly with the graphql api, including authentication.
|
|
270
|
+
|
|
271
|
+
### claude code
|
|
272
|
+
|
|
273
|
+
install the skill using [claude code's plugin system](https://code.claude.com/docs/en/skills):
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# from claude code
|
|
277
|
+
/plugin marketplace add kyaukyuai/linear-cli
|
|
278
|
+
/plugin install linear-cli@linear-cli
|
|
279
|
+
|
|
280
|
+
# from bash
|
|
281
|
+
claude plugin marketplace add kyaukyuai/linear-cli
|
|
282
|
+
claude plugin install linear-cli@linear-cli
|
|
283
|
+
|
|
284
|
+
# to update
|
|
285
|
+
claude plugin marketplace update linear-cli
|
|
286
|
+
claude plugin update linear-cli@linear-cli
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### skills.sh for other agents
|
|
290
|
+
|
|
291
|
+
install the skill using [skills.sh](https://skills.sh):
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
npx skills add kyaukyuai/linear-cli
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
view the skill at [skills.sh/kyaukyuai/linear-cli/linear-cli](https://skills.sh/kyaukyuai/linear-cli/linear-cli)
|
|
298
|
+
|
|
299
|
+
## development
|
|
300
|
+
|
|
301
|
+
### updating skill documentation
|
|
302
|
+
|
|
303
|
+
the skill documentation in `skills/linear-cli/` is automatically generated from the CLI help text. after making changes to commands or help text, regenerate the docs:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
deno task generate-skill-docs
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
this will:
|
|
310
|
+
|
|
311
|
+
- discover all commands and subcommands from `linear --help`
|
|
312
|
+
- generate reference documentation for each command
|
|
313
|
+
- update the `SKILL.md` file from `SKILL.template.md`
|
|
314
|
+
|
|
315
|
+
**important:** the CI checks will fail if the generated docs are out of date, so make sure to run this before committing changes that affect command structure or help text.
|
|
316
|
+
|
|
317
|
+
### code formatting
|
|
318
|
+
|
|
319
|
+
ensure code is formatted consistently:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
deno fmt
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
the project uses deno's built-in formatter with configuration in `deno.json`. formatting is checked in CI.
|
|
326
|
+
|
|
327
|
+
## why
|
|
328
|
+
|
|
329
|
+
linear's UI is incredibly good but it slows me down. i find the following pretty grating to experience frequently:
|
|
330
|
+
|
|
331
|
+
- switching context from my repo to linear
|
|
332
|
+
- not being on the right view when i open linear
|
|
333
|
+
- linear suggests a git branch, but i have to do the work of creating or switching to that branch
|
|
334
|
+
- linear's suggested git branch doesn't account for it already existing or having a merged pull request
|
|
335
|
+
|
|
336
|
+
this cli solves this. it knows what you're working on (via git branches or jj commit trailers), does the work of managing your version control state, and will write your pull request details for you.
|
|
337
|
+
|
|
338
|
+
[^1]: creating an API key requires member access, it is not available for guest accounts.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const { createWriteStream, existsSync, mkdirSync, mkdtemp } = require("fs");
|
|
2
|
+
const { join, sep } = require("path");
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const { tmpdir } = require("os");
|
|
5
|
+
|
|
6
|
+
const axios = require("axios");
|
|
7
|
+
const rimraf = require("rimraf");
|
|
8
|
+
const tmpDir = tmpdir();
|
|
9
|
+
|
|
10
|
+
const error = (msg) => {
|
|
11
|
+
console.error(msg);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class Package {
|
|
16
|
+
constructor(platform, name, url, filename, zipExt, binaries) {
|
|
17
|
+
let errors = [];
|
|
18
|
+
if (typeof url !== "string") {
|
|
19
|
+
errors.push("url must be a string");
|
|
20
|
+
} else {
|
|
21
|
+
try {
|
|
22
|
+
new URL(url);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
errors.push(e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (name && typeof name !== "string") {
|
|
28
|
+
errors.push("package name must be a string");
|
|
29
|
+
}
|
|
30
|
+
if (!name) {
|
|
31
|
+
errors.push("You must specify the name of your package");
|
|
32
|
+
}
|
|
33
|
+
if (binaries && typeof binaries !== "object") {
|
|
34
|
+
errors.push("binaries must be a string => string map");
|
|
35
|
+
}
|
|
36
|
+
if (!binaries) {
|
|
37
|
+
errors.push("You must specify the binaries in the package");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (errors.length > 0) {
|
|
41
|
+
let errorMsg =
|
|
42
|
+
"One or more of the parameters you passed to the Binary constructor are invalid:\n";
|
|
43
|
+
errors.forEach((error) => {
|
|
44
|
+
errorMsg += error;
|
|
45
|
+
});
|
|
46
|
+
errorMsg +=
|
|
47
|
+
'\n\nCorrect usage: new Package("my-binary", "https://example.com/binary/download.tar.gz", {"my-binary": "my-binary"})';
|
|
48
|
+
error(errorMsg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.platform = platform;
|
|
52
|
+
this.url = url;
|
|
53
|
+
this.name = name;
|
|
54
|
+
this.filename = filename;
|
|
55
|
+
this.zipExt = zipExt;
|
|
56
|
+
this.installDirectory = join(__dirname, "node_modules", ".bin_real");
|
|
57
|
+
this.binaries = binaries;
|
|
58
|
+
|
|
59
|
+
if (!existsSync(this.installDirectory)) {
|
|
60
|
+
mkdirSync(this.installDirectory, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exists() {
|
|
65
|
+
for (const binaryName in this.binaries) {
|
|
66
|
+
const binRelPath = this.binaries[binaryName];
|
|
67
|
+
const binPath = join(this.installDirectory, binRelPath);
|
|
68
|
+
if (!existsSync(binPath)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
install(fetchOptions, suppressLogs = false) {
|
|
76
|
+
if (this.exists()) {
|
|
77
|
+
if (!suppressLogs) {
|
|
78
|
+
console.error(
|
|
79
|
+
`${this.name} is already installed, skipping installation.`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return Promise.resolve();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (existsSync(this.installDirectory)) {
|
|
86
|
+
rimraf.sync(this.installDirectory);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
mkdirSync(this.installDirectory, { recursive: true });
|
|
90
|
+
|
|
91
|
+
if (!suppressLogs) {
|
|
92
|
+
console.error(`Downloading release from ${this.url}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return axios({ ...fetchOptions, url: this.url, responseType: "stream" })
|
|
96
|
+
.then((res) => {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
|
|
99
|
+
let tempFile = join(directory, this.filename);
|
|
100
|
+
const sink = res.data.pipe(createWriteStream(tempFile));
|
|
101
|
+
sink.on("error", (err) => reject(err));
|
|
102
|
+
sink.on("close", () => {
|
|
103
|
+
if (/\.tar\.*/.test(this.zipExt)) {
|
|
104
|
+
const result = spawnSync("tar", [
|
|
105
|
+
"xf",
|
|
106
|
+
tempFile,
|
|
107
|
+
// The tarballs are stored with a leading directory
|
|
108
|
+
// component; we strip one component in the
|
|
109
|
+
// shell installers too.
|
|
110
|
+
"--strip-components",
|
|
111
|
+
"1",
|
|
112
|
+
"-C",
|
|
113
|
+
this.installDirectory,
|
|
114
|
+
]);
|
|
115
|
+
if (result.status == 0) {
|
|
116
|
+
resolve();
|
|
117
|
+
} else if (result.error) {
|
|
118
|
+
reject(result.error);
|
|
119
|
+
} else {
|
|
120
|
+
reject(
|
|
121
|
+
new Error(
|
|
122
|
+
`An error occurred untarring the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
} else if (this.zipExt == ".zip") {
|
|
127
|
+
let result;
|
|
128
|
+
if (this.platform.artifactName.includes("windows")) {
|
|
129
|
+
// Windows does not have "unzip" by default on many installations, instead
|
|
130
|
+
// we use Expand-Archive from powershell
|
|
131
|
+
result = spawnSync("powershell.exe", [
|
|
132
|
+
"-NoProfile",
|
|
133
|
+
"-NonInteractive",
|
|
134
|
+
"-Command",
|
|
135
|
+
`& {
|
|
136
|
+
param([string]$LiteralPath, [string]$DestinationPath)
|
|
137
|
+
Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force
|
|
138
|
+
}`,
|
|
139
|
+
tempFile,
|
|
140
|
+
this.installDirectory,
|
|
141
|
+
]);
|
|
142
|
+
} else {
|
|
143
|
+
result = spawnSync("unzip", [
|
|
144
|
+
"-q",
|
|
145
|
+
tempFile,
|
|
146
|
+
"-d",
|
|
147
|
+
this.installDirectory,
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.status == 0) {
|
|
152
|
+
resolve();
|
|
153
|
+
} else if (result.error) {
|
|
154
|
+
reject(result.error);
|
|
155
|
+
} else {
|
|
156
|
+
reject(
|
|
157
|
+
new Error(
|
|
158
|
+
`An error occurred unzipping the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
reject(
|
|
164
|
+
new Error(`Unrecognized file extension: ${this.zipExt}`),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
})
|
|
171
|
+
.then(() => {
|
|
172
|
+
if (!suppressLogs) {
|
|
173
|
+
console.error(`${this.name} has been installed!`);
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.catch((e) => {
|
|
177
|
+
error(`Error fetching release: ${e.message}`);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
run(binaryName, fetchOptions) {
|
|
182
|
+
const promise = !this.exists()
|
|
183
|
+
? this.install(fetchOptions, true)
|
|
184
|
+
: Promise.resolve();
|
|
185
|
+
|
|
186
|
+
promise
|
|
187
|
+
.then(() => {
|
|
188
|
+
const [, , ...args] = process.argv;
|
|
189
|
+
|
|
190
|
+
const options = { cwd: process.cwd(), stdio: "inherit" };
|
|
191
|
+
|
|
192
|
+
const binRelPath = this.binaries[binaryName];
|
|
193
|
+
if (!binRelPath) {
|
|
194
|
+
error(`${binaryName} is not a known binary in ${this.name}`);
|
|
195
|
+
}
|
|
196
|
+
const binPath = join(this.installDirectory, binRelPath);
|
|
197
|
+
const result = spawnSync(binPath, args, options);
|
|
198
|
+
|
|
199
|
+
if (result.error) {
|
|
200
|
+
error(result.error);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
process.exit(result.status);
|
|
204
|
+
})
|
|
205
|
+
.catch((e) => {
|
|
206
|
+
error(e.message);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports.Package = Package;
|
package/binary.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const { Package } = require("./binary-install");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const cTable = require("console.table");
|
|
4
|
+
const libc = require("detect-libc");
|
|
5
|
+
const { configureProxy } = require("axios-proxy-builder");
|
|
6
|
+
|
|
7
|
+
const error = (msg) => {
|
|
8
|
+
console.error(msg);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
name,
|
|
14
|
+
artifactDownloadUrls,
|
|
15
|
+
supportedPlatforms,
|
|
16
|
+
glibcMinimum,
|
|
17
|
+
} = require("./package.json");
|
|
18
|
+
|
|
19
|
+
// FIXME: implement NPM installer handling of fallback download URLs
|
|
20
|
+
const artifactDownloadUrl = artifactDownloadUrls[0];
|
|
21
|
+
const builderGlibcMajorVersion = glibcMinimum.major;
|
|
22
|
+
const builderGlibcMinorVersion = glibcMinimum.series;
|
|
23
|
+
|
|
24
|
+
const getPlatform = () => {
|
|
25
|
+
const rawOsType = os.type();
|
|
26
|
+
const rawArchitecture = os.arch();
|
|
27
|
+
|
|
28
|
+
// We want to use rust-style target triples as the canonical key
|
|
29
|
+
// for a platform, so translate the "os" library's concepts into rust ones
|
|
30
|
+
let osType = "";
|
|
31
|
+
switch (rawOsType) {
|
|
32
|
+
case "Windows_NT":
|
|
33
|
+
osType = "pc-windows-msvc";
|
|
34
|
+
break;
|
|
35
|
+
case "Darwin":
|
|
36
|
+
osType = "apple-darwin";
|
|
37
|
+
break;
|
|
38
|
+
case "Linux":
|
|
39
|
+
osType = "unknown-linux-gnu";
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let arch = "";
|
|
44
|
+
switch (rawArchitecture) {
|
|
45
|
+
case "x64":
|
|
46
|
+
arch = "x86_64";
|
|
47
|
+
break;
|
|
48
|
+
case "arm64":
|
|
49
|
+
arch = "aarch64";
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (rawOsType === "Linux") {
|
|
54
|
+
if (libc.familySync() == "musl") {
|
|
55
|
+
osType = "unknown-linux-musl-dynamic";
|
|
56
|
+
} else if (libc.isNonGlibcLinuxSync()) {
|
|
57
|
+
console.warn(
|
|
58
|
+
"Your libc is neither glibc nor musl; trying static musl binary instead",
|
|
59
|
+
);
|
|
60
|
+
osType = "unknown-linux-musl-static";
|
|
61
|
+
} else {
|
|
62
|
+
let libcVersion = libc.versionSync();
|
|
63
|
+
let splitLibcVersion = libcVersion.split(".");
|
|
64
|
+
let libcMajorVersion = splitLibcVersion[0];
|
|
65
|
+
let libcMinorVersion = splitLibcVersion[1];
|
|
66
|
+
if (
|
|
67
|
+
libcMajorVersion != builderGlibcMajorVersion ||
|
|
68
|
+
libcMinorVersion < builderGlibcMinorVersion
|
|
69
|
+
) {
|
|
70
|
+
// We can't run the glibc binaries, but we can run the static musl ones
|
|
71
|
+
// if they exist
|
|
72
|
+
console.warn(
|
|
73
|
+
"Your glibc isn't compatible; trying static musl binary instead",
|
|
74
|
+
);
|
|
75
|
+
osType = "unknown-linux-musl-static";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Assume the above succeeded and build a target triple to look things up with.
|
|
81
|
+
// If any of it failed, this lookup will fail and we'll handle it like normal.
|
|
82
|
+
let targetTriple = `${arch}-${osType}`;
|
|
83
|
+
let platform = supportedPlatforms[targetTriple];
|
|
84
|
+
|
|
85
|
+
if (!platform) {
|
|
86
|
+
error(
|
|
87
|
+
`Platform with type "${rawOsType}" and architecture "${rawArchitecture}" is not supported by ${name}.\nYour system must be one of the following:\n\n${Object.keys(
|
|
88
|
+
supportedPlatforms,
|
|
89
|
+
).join(",")}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return platform;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const getPackage = () => {
|
|
97
|
+
const platform = getPlatform();
|
|
98
|
+
const url = `${artifactDownloadUrl}/${platform.artifactName}`;
|
|
99
|
+
let filename = platform.artifactName;
|
|
100
|
+
let ext = platform.zipExt;
|
|
101
|
+
let binary = new Package(platform, name, url, filename, ext, platform.bins);
|
|
102
|
+
|
|
103
|
+
return binary;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const install = (suppressLogs) => {
|
|
107
|
+
if (!artifactDownloadUrl || artifactDownloadUrl.length === 0) {
|
|
108
|
+
console.warn("in demo mode, not installing binaries");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const package = getPackage();
|
|
112
|
+
const proxy = configureProxy(package.url);
|
|
113
|
+
|
|
114
|
+
return package.install(proxy, suppressLogs);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const run = (binaryName) => {
|
|
118
|
+
const package = getPackage();
|
|
119
|
+
const proxy = configureProxy(package.url);
|
|
120
|
+
|
|
121
|
+
package.run(binaryName, proxy);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
install,
|
|
126
|
+
run,
|
|
127
|
+
getPackage,
|
|
128
|
+
};
|
package/install.js
ADDED