@kythin/stackydo 0.9.2
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 +368 -0
- package/binary-install.js +212 -0
- package/binary.js +128 -0
- package/install.js +4 -0
- package/npm-shrinkwrap.json +547 -0
- package/package.json +117 -0
- package/run-stackydo-mcp.js +4 -0
- package/run-stackydo.js +4 -0
package/.gitignore
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 kythin
|
|
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
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# stackydo
|
|
2
|
+
|
|
3
|
+
**One person's entire workload, in one place.**
|
|
4
|
+
|
|
5
|
+
Stackydo is a context-aware CLI task manager designed for individual engineers, leads, and makers who juggle work across many projects, teams, and responsibilities. Tasks are plain markdown files with YAML frontmatter — no database, no server, no vendor lock-in.
|
|
6
|
+
|
|
7
|
+
The core idea: your work doesn't live in one project. You might be debugging a production incident, reviewing a teammate's PR, prepping a conference talk, and planning next sprint — all in the same afternoon. Stackydo uses **stacks** to separate these workstreams while keeping everything in a single, searchable task store.
|
|
8
|
+
|
|
9
|
+
## Why This Exists
|
|
10
|
+
|
|
11
|
+
Most task managers are built for teams or for single projects. Stackydo is built for **you** — the individual who needs to:
|
|
12
|
+
|
|
13
|
+
- Track work across multiple projects, random ideas, ad-hoc tasks, team duties, and personal goals simultaneously
|
|
14
|
+
- Create tasks from wherever you are — terminal, editor, scripts, or AI agents — so fast it becomes muscle memory to offload every stray thought
|
|
15
|
+
- Search across everything at once ("what was that security thing last week?")
|
|
16
|
+
- Let AI tools triage, summarize, and report on your workload via the built-in MCP server
|
|
17
|
+
- Own your data as plain files you can grep, version, and back up
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Headless CLI** — create, list, search, update, complete, delete from scripts and pipelines
|
|
22
|
+
- **MCP server** — gives AI assistants (Claude Desktop, Claude Code, etc.) full access to your task store
|
|
23
|
+
- **Stacks** — organize tasks into named workstreams (e.g. "work", "personal", "sprint-12")
|
|
24
|
+
- **Short IDs** — human-friendly IDs like SD1, SD2 alongside ULIDs; all commands accept either
|
|
25
|
+
- **Automatic context capture** — records git branch/commit, working directory, and project context on task creation
|
|
26
|
+
- **Task graph** — subtasks, dependencies (blocked-by, blocks, related-to)
|
|
27
|
+
- **AI-friendly storage** — plain markdown+YAML files that any LLM or script can read and write
|
|
28
|
+
- **`.stackydo-context` files** — define project-level context that gets attached to new tasks automatically
|
|
29
|
+
- **Session chaining** — tracks the last task ID created per shell session via `$STACKYDO_LAST_ID`
|
|
30
|
+
- **Configurable storage** — set `dir` in `.stackydo-context` for per-project workspaces, or `$STACKYDO_DIR` for per-session overrides (defaults to `~/.stackydo/`)
|
|
31
|
+
- **Multi-workspace** — discover, list, and migrate tasks across workspaces
|
|
32
|
+
- **Doctor** — diagnose and auto-fix workspace issues (missing short IDs, orphan refs, corrupt files)
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
### Homebrew (macOS/Linux)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
brew tap kythin/homebrew-tap && brew install stackydo
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Shell (curl one-liner)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/kythin/stackydo-cli/releases/latest/download/stackydo-installer.sh | sh
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### PowerShell (Windows)
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
powershell -c "irm https://github.com/kythin/stackydo-cli/releases/latest/download/stackydo-installer.ps1 | iex"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### From source
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cargo install --git https://github.com/kythin/stackydo-cli
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Update
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Homebrew
|
|
64
|
+
brew upgrade stackydo
|
|
65
|
+
|
|
66
|
+
# Shell — re-run the curl installer, or use the built-in updater:
|
|
67
|
+
stackydo-update
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
All methods install two binaries: `stackydo` (CLI) and `stackydo-mcp` (MCP server).
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Create a task
|
|
76
|
+
stackydo create --title "Fix auth bug" --tags "backend,urgent" --priority high --stack work \
|
|
77
|
+
-- The login endpoint returns 500 when the token expires
|
|
78
|
+
|
|
79
|
+
# List tasks
|
|
80
|
+
stackydo list
|
|
81
|
+
stackydo list --status todo --sort priority
|
|
82
|
+
stackydo list --stack work
|
|
83
|
+
stackydo list --overdue
|
|
84
|
+
|
|
85
|
+
# Show task detail (prefix matching and short IDs work everywhere)
|
|
86
|
+
stackydo show SD1
|
|
87
|
+
stackydo show 01HQ
|
|
88
|
+
|
|
89
|
+
# Update a task
|
|
90
|
+
stackydo update SD1 --status in_progress --note "Investigating root cause"
|
|
91
|
+
|
|
92
|
+
# Complete a task
|
|
93
|
+
stackydo complete SD1
|
|
94
|
+
|
|
95
|
+
# Search
|
|
96
|
+
stackydo search "auth"
|
|
97
|
+
|
|
98
|
+
# Add a comment
|
|
99
|
+
stackydo comment SD1 "Turns out it was a timezone issue"
|
|
100
|
+
|
|
101
|
+
# Check workspace health
|
|
102
|
+
stackydo doctor
|
|
103
|
+
stackydo doctor --fix
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## CLI Commands
|
|
107
|
+
|
|
108
|
+
| Command | Description |
|
|
109
|
+
|---------|-------------|
|
|
110
|
+
| `create` | Create a new task with title, tags, priority, stack, body, due date, dependencies |
|
|
111
|
+
| `list` | List/filter tasks by status, stage, tag, priority, stack, due date; sort, group, paginate |
|
|
112
|
+
| `show` | Show a task's full details |
|
|
113
|
+
| `update` | Update fields, append body text, sed-style substitution, add timestamped notes |
|
|
114
|
+
| `complete` | Mark task(s) as done (single or bulk with `--all`) |
|
|
115
|
+
| `delete` | Permanently delete task(s) (single or bulk with `--all`) |
|
|
116
|
+
| `search` | Search title and body (case-insensitive) with same filters as `list` |
|
|
117
|
+
| `comment` | Add a comment to a task |
|
|
118
|
+
| `stats` | Summary statistics: totals, overdue count, breakdowns by status/stack/tag |
|
|
119
|
+
| `stacks` | All stacks with per-stack task counts and status breakdowns |
|
|
120
|
+
| `context` | Preview what context would be captured for a new task |
|
|
121
|
+
| `init` | Initialize a new workspace (optionally with `--here` and `--git`) |
|
|
122
|
+
| `doctor` | Diagnose and optionally fix workspace issues |
|
|
123
|
+
| `shuffle` | Randomise the order of tasks in a stack+status group |
|
|
124
|
+
| `draw` | Draw the top task from one group and move it to another |
|
|
125
|
+
| `list-workspaces` | Discover all stackydo workspaces on the system |
|
|
126
|
+
| `migrate` | Move or copy tasks between workspaces |
|
|
127
|
+
| `import` | Import tasks from stdin (JSON or YAML) |
|
|
128
|
+
| `mcp-setup` | Register `stackydo-mcp` with Claude Code via `claude mcp add` |
|
|
129
|
+
| `agent-setup` | Generate an AI agent playbook in your project's CLAUDE.md |
|
|
130
|
+
|
|
131
|
+
## Stacks
|
|
132
|
+
|
|
133
|
+
A task can belong to one **stack** — a named group like "work", "personal", or "sprint-12". Think of stacks as physical piles of tasks rather than flat database categories.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Create a task on a stack
|
|
137
|
+
stackydo create --title "Deploy v2" --stack work
|
|
138
|
+
|
|
139
|
+
# Filter tasks by stack
|
|
140
|
+
stackydo list --stack work
|
|
141
|
+
stackydo list --stack personal --status todo
|
|
142
|
+
|
|
143
|
+
# See all stacks with counts
|
|
144
|
+
stackydo stacks
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The manifest tracks known stack names. Tasks without a stack are unstacked and won't appear in stack-filtered results.
|
|
148
|
+
|
|
149
|
+
## Workflows
|
|
150
|
+
|
|
151
|
+
Workflows define the statuses available to tasks and how they map to lifecycle stages (backlog, active, archive). Each stack can use a different workflow.
|
|
152
|
+
|
|
153
|
+
### Built-in Workflows
|
|
154
|
+
|
|
155
|
+
**Kanban** (default) — traditional task board:
|
|
156
|
+
|
|
157
|
+
| Stage | Statuses |
|
|
158
|
+
|-------|----------|
|
|
159
|
+
| Backlog | `todo`, `on_hold` |
|
|
160
|
+
| Active | `in_progress`, `blocked`, `in_review` |
|
|
161
|
+
| Archive | `done`, `cancelled` |
|
|
162
|
+
|
|
163
|
+
**Deck** — card game metaphor for randomised/prioritised work:
|
|
164
|
+
|
|
165
|
+
| Stage | Statuses |
|
|
166
|
+
|-------|----------|
|
|
167
|
+
| Backlog | `deck` |
|
|
168
|
+
| Active | `hand`, `table` |
|
|
169
|
+
| Archive | `discard` |
|
|
170
|
+
|
|
171
|
+
### Per-Stack Assignment
|
|
172
|
+
|
|
173
|
+
Set a stack's workflow in `manifest.json`:
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"stack_workflows": {
|
|
177
|
+
"ideas": "deck",
|
|
178
|
+
"work": "kanban"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Stacks without a workflow inherit the workspace default (kanban).
|
|
184
|
+
|
|
185
|
+
### Index Ordering
|
|
186
|
+
|
|
187
|
+
Tasks within the same stack+status group have an `index` field for positional ordering. Indexes are maintained automatically when tasks are created, moved, or deleted. Use `shuffle` to randomise order, and `draw` to pull the top task from one group to another:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Randomise the deck
|
|
191
|
+
stackydo shuffle --stack ideas --status deck
|
|
192
|
+
|
|
193
|
+
# Draw the top card into your hand
|
|
194
|
+
stackydo draw --source ideas/deck --target ideas/hand
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## MCP Server (Claude Desktop / Claude Code)
|
|
198
|
+
|
|
199
|
+
Stackydo includes an MCP server that gives AI assistants full access to your task store.
|
|
200
|
+
|
|
201
|
+
### Setup
|
|
202
|
+
|
|
203
|
+
Install stackydo using any method from the [Install](#install) section. Both `stackydo` and `stackydo-mcp` are included.
|
|
204
|
+
|
|
205
|
+
**Claude Code** (quickest):
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
stackydo mcp-setup
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
This runs `claude mcp add` to register the server. Use `--scope user` for global access or `--scope project` (default) for per-project.
|
|
212
|
+
|
|
213
|
+
**Claude Desktop** — add to your config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"mcpServers": {
|
|
218
|
+
"stackydo": {
|
|
219
|
+
"command": "stackydo-mcp"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
To use a non-default storage directory, add an `env` key:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"mcpServers": {
|
|
230
|
+
"stackydo": {
|
|
231
|
+
"command": "stackydo-mcp",
|
|
232
|
+
"env": {
|
|
233
|
+
"STACKYDO_DIR": "/path/to/your/tasks"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Restart Claude Desktop after editing the config.
|
|
241
|
+
|
|
242
|
+
### Available MCP Tools
|
|
243
|
+
|
|
244
|
+
| Tool | Description |
|
|
245
|
+
|------|-------------|
|
|
246
|
+
| `list_tasks` | List/filter tasks by status, tag, priority, stack, due date; sort and group |
|
|
247
|
+
| `get_task` | Get a single task by ID (prefix matching) |
|
|
248
|
+
| `create_task` | Create a task with title, priority, tags, stack, body, due date |
|
|
249
|
+
| `update_task` | Update fields, append timestamped notes, body editing |
|
|
250
|
+
| `complete_task` | Mark a task as done |
|
|
251
|
+
| `delete_task` | Permanently delete a task |
|
|
252
|
+
| `search_tasks` | Search title and body (case-insensitive) |
|
|
253
|
+
| `add_comment` | Add a comment to a task |
|
|
254
|
+
| `get_stats` | Summary statistics: totals, overdue count, breakdowns by status/stack/tag |
|
|
255
|
+
| `get_stacks` | All stacks with per-stack task counts and status breakdowns |
|
|
256
|
+
| `list_workspaces` | Discover all stackydo workspaces on the system |
|
|
257
|
+
| `migrate_tasks` | Move or copy tasks between workspaces |
|
|
258
|
+
| `doctor` | Diagnose and optionally fix workspace issues |
|
|
259
|
+
| `shuffle` | Randomise task order in a stack+status group |
|
|
260
|
+
| `draw` | Draw top task from one group and move to another |
|
|
261
|
+
|
|
262
|
+
The server also exposes a `stackydo://guide` resource with a full agent guide, and prompt templates for triage, planning, and task extraction.
|
|
263
|
+
|
|
264
|
+
## AI Agent Setup
|
|
265
|
+
|
|
266
|
+
Stackydo is designed to be used by AI coding agents (Claude Code, Cursor, etc.) as their own task tracker:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# Generate a CLAUDE.md playbook for your project
|
|
270
|
+
stackydo agent-setup
|
|
271
|
+
|
|
272
|
+
# Register the MCP server with Claude Code
|
|
273
|
+
stackydo mcp-setup
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`agent-setup` writes instructions into your project's CLAUDE.md so agents know how to create, update, and complete tasks as they work.
|
|
277
|
+
|
|
278
|
+
## Environment Variables
|
|
279
|
+
|
|
280
|
+
| Variable | Description |
|
|
281
|
+
|----------|-------------|
|
|
282
|
+
| `STACKYDO_DIR` | Override the task storage directory (highest priority, overrides `.stackydo-context`; default: `~/.stackydo/`) |
|
|
283
|
+
| `STACKYDO_LAST_ID` | Set automatically by `stackydo create`; chains tasks in a shell session |
|
|
284
|
+
|
|
285
|
+
## Task Storage
|
|
286
|
+
|
|
287
|
+
Each task is a markdown file at `<STACKYDO_DIR>/<ULID>.md`:
|
|
288
|
+
|
|
289
|
+
```markdown
|
|
290
|
+
---
|
|
291
|
+
id: 01HQXYZ...
|
|
292
|
+
short_id: SD1
|
|
293
|
+
title: Fix auth bug
|
|
294
|
+
status: todo
|
|
295
|
+
priority: high
|
|
296
|
+
tags: [backend, urgent]
|
|
297
|
+
stack: work
|
|
298
|
+
index: 0
|
|
299
|
+
created: 2025-02-13T10:30:00Z
|
|
300
|
+
modified: 2025-02-13T10:30:00Z
|
|
301
|
+
context:
|
|
302
|
+
working_dir: /home/user/project
|
|
303
|
+
git_branch: main
|
|
304
|
+
git_commit: a3f7d92
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
The login endpoint returns 500 when the token expires.
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
A manifest at `<STACKYDO_DIR>/manifest.json` tracks tags, stacks, short ID counter, and settings.
|
|
311
|
+
|
|
312
|
+
## Context Discovery
|
|
313
|
+
|
|
314
|
+
On task creation, stackydo automatically captures:
|
|
315
|
+
|
|
316
|
+
1. Current working directory
|
|
317
|
+
2. Git branch, remote URL, and HEAD commit (if in a repo)
|
|
318
|
+
3. Content from the nearest `.stackydo-context` file (walks up from CWD, falls back to `~/.stackydo-context`)
|
|
319
|
+
4. `$STACKYDO_LAST_ID` — the ID of the previous task created in the same shell session
|
|
320
|
+
|
|
321
|
+
Use `stackydo context` to preview what would be captured without creating a task.
|
|
322
|
+
|
|
323
|
+
### Workspace Resolution
|
|
324
|
+
|
|
325
|
+
The `.stackydo-context` file can set the task store location via a `dir` field. This lets a project root define a shared workspace without requiring every user to set an environment variable.
|
|
326
|
+
|
|
327
|
+
Resolution priority:
|
|
328
|
+
1. `$STACKYDO_DIR` env var (highest — per-session override)
|
|
329
|
+
2. `dir` field in the nearest `.stackydo-context` (per-project)
|
|
330
|
+
3. `~/.stackydo/` (default)
|
|
331
|
+
|
|
332
|
+
Example `.stackydo-context`:
|
|
333
|
+
|
|
334
|
+
```yaml
|
|
335
|
+
dir: .stackydo-workspace
|
|
336
|
+
project: my-app
|
|
337
|
+
stack: dev
|
|
338
|
+
description: Project-level context captured on new tasks
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The `dir` path is resolved relative to the config file's location. Use `stackydo context` to see which source resolved the task store.
|
|
342
|
+
|
|
343
|
+
To set up a project workspace:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
stackydo init --here --dir .stackydo-workspace
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Contributing
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# Build
|
|
353
|
+
cargo build
|
|
354
|
+
|
|
355
|
+
# Run tests
|
|
356
|
+
cargo test # unit tests
|
|
357
|
+
cargo build && bash tests/smoke_test.sh # CLI smoke tests
|
|
358
|
+
bash tests/test_all.sh # full suite: clippy + unit + build + smoke + scenario + scale
|
|
359
|
+
|
|
360
|
+
# Lint
|
|
361
|
+
cargo clippy
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Tests use `$STACKYDO_DIR` to write to a local `tests/.test-data/` directory — they never touch `~/.stackydo/`.
|
|
365
|
+
|
|
366
|
+
## License
|
|
367
|
+
|
|
368
|
+
MIT
|
|
@@ -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;
|