@squirreljetpack/im 0.0.0 β 0.2.6
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 +155 -0
- package/README.md +190 -1
- package/binary-install.js +348 -0
- package/binary.js +124 -0
- package/install.js +4 -0
- package/npm-shrinkwrap.json +51 -0
- package/package.json +72 -5
- package/run-im.js +4 -0
package/.gitignore
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
## [0.2.6] - 2026-10-29
|
|
2
|
+
|
|
3
|
+
### π Features (sync)
|
|
4
|
+
|
|
5
|
+
- `im :auth register|login|status|logout` and `im :sync` (plus
|
|
6
|
+
`im :sync --reset`) sync a database against a Cloudflare Worker
|
|
7
|
+
(`sync-server/`): a per-user Durable Object holds the log, D1 the accounts.
|
|
8
|
+
- Concurrent edits merge per field (last write wins) instead of per entity; a
|
|
9
|
+
push breaks at entity boundaries, so a create carries all of its cells.
|
|
10
|
+
- Deletes are terminal and slot losers become tombstones, so two devices that
|
|
11
|
+
edit the same field converge instead of duplicating rows.
|
|
12
|
+
- Entity ids are UUIDv7, so a row created on any device is unique without a
|
|
13
|
+
round trip; local `short_id`s stay a per-device projection.
|
|
14
|
+
- The server address comes from `[sync] server` in the client config
|
|
15
|
+
(`IM_SYNC_URL` overrides it for one command); the account is stored apart
|
|
16
|
+
from the sync state, so `im :sync --reset` does not sign you out.
|
|
17
|
+
- A database from an older version (INTEGER keys) is copied aside and
|
|
18
|
+
recreated empty instead of being written with the wrong key type; an
|
|
19
|
+
unreadable file is quarantined the same way.
|
|
20
|
+
|
|
21
|
+
### π₯ Breaking changes (tracker rework)
|
|
22
|
+
|
|
23
|
+
- `min`/`max` renamed to `low`/`high`; `kind = "number"` renamed to
|
|
24
|
+
`"integer"`; new `kind = "duration"` (duration-string values stored as
|
|
25
|
+
seconds, displayed like `6m 30s`).
|
|
26
|
+
- `interval` now takes a table `{ anchor, span, cumulative = false }`
|
|
27
|
+
- Value and bound forms are strict per kind/mode: `float` takes plain
|
|
28
|
+
numbers, `integer` plain whole numbers, `duration` duration strings only,
|
|
29
|
+
`null` no value at all; `low`/`high` mirror the value form of the kind
|
|
30
|
+
(duration strings for `duration` and `null`+replace, whole numbers for
|
|
31
|
+
`text` and `null`+cumulative). Invalid bound forms are dropped with a
|
|
32
|
+
warning at config load.
|
|
33
|
+
- New `strict` setting gates logging/updating: numeric kinds gate the value
|
|
34
|
+
against the inclusive `low`..`high` span (inverted bounds gate the span
|
|
35
|
+
between them), `text` gates the message length in characters, `null`
|
|
36
|
+
trackers gate *when* they may be logged (the circular `low`/`high` time
|
|
37
|
+
offset zone; requires both bounds, replace mode only).
|
|
38
|
+
- `null` trackers require an interval (dropped with a warning otherwise) and
|
|
39
|
+
store score 0 in every mode β the payload column is unused and today-view
|
|
40
|
+
rows show no payload (the count lives in the grid, the timestamp in the
|
|
41
|
+
entry's own time). Negative durations are rejected at log time.
|
|
42
|
+
- Replace-mode insertion is one shared strategy for all kinds: a CLI log in
|
|
43
|
+
an occupied slot drops the slot's previous rows (including rows logged
|
|
44
|
+
with a mood β the mood row itself survives) and inserts the new one.
|
|
45
|
+
- The TUI update action on a `null` row only moves its timestamp to now,
|
|
46
|
+
within its current interval slot (cross-slot moves are rejected, the
|
|
47
|
+
strict zone still applies); on value-bearing kinds it runs the same strict
|
|
48
|
+
pipeline as the CLI.
|
|
49
|
+
- `:db doctor` prunes any nonzero row of a `null` tracker (all modes).
|
|
50
|
+
|
|
51
|
+
## [0.2.5] - 2026-08-14
|
|
52
|
+
|
|
53
|
+
### π Features
|
|
54
|
+
|
|
55
|
+
- Editor no longer opens automatically for interactive flows.
|
|
56
|
+
- Config adjustments
|
|
57
|
+
- Body templates
|
|
58
|
+
- Adjust today view filters
|
|
59
|
+
- Config.tasks.overdue_color + badge::task_label_color
|
|
60
|
+
- Today-view preview polish + overdue date column + task children
|
|
61
|
+
- Link actions in the today view (Ctrl+L)
|
|
62
|
+
- Improve jiff-english robustness
|
|
63
|
+
- TUI migration to matchmaker
|
|
64
|
+
|
|
65
|
+
## [0.2.4] - 2026-08-07
|
|
66
|
+
|
|
67
|
+
### π Features
|
|
68
|
+
|
|
69
|
+
- Db :doctor
|
|
70
|
+
- Jiff-english
|
|
71
|
+
- Duration trackers
|
|
72
|
+
- Tui interaction for trackers
|
|
73
|
+
|
|
74
|
+
## [Unreleased]
|
|
75
|
+
|
|
76
|
+
### π Features
|
|
77
|
+
|
|
78
|
+
- `[badges]` config section: journal badge (glyph and/or color β a bare
|
|
79
|
+
char, a color string, or `{ badge = 'Β·', color = "red" }`; color defaults
|
|
80
|
+
to `Reset`), plus `tracker` and `mood` glyph overrides for the today
|
|
81
|
+
view and previews. `journal_badge` moved from `[today_view]`.
|
|
82
|
+
- Today view task filter: `TasksFilter` (`none` / `all` / `overdue` /
|
|
83
|
+
`pending` / `horizon`) replaces `config.today_view.include_overdue` as
|
|
84
|
+
`initial_tasks_filter`, bound to the view variant β `All` uses the
|
|
85
|
+
configured filter, `A` pins `Horizon`, `B` pins `Overdue`; `none` hides
|
|
86
|
+
the whole task section (journal-only view). Undated
|
|
87
|
+
oneshots created before today are no longer dropped from the `All`
|
|
88
|
+
variant. `CycleShow` renamed `CycleFilter`; TUI headers label the
|
|
89
|
+
variants per app (`all | journal | tasks` in the today view,
|
|
90
|
+
`all | oneshot | other` in the tasks view).
|
|
91
|
+
|
|
92
|
+
- Renamed: the binary is now `im` (previously `feeling`), and the logged
|
|
93
|
+
mood entries are consistently called moods β DB schema (`mood` table,
|
|
94
|
+
`tracker.mood`, `task_moods.mood_id`), code identifiers, CLI usage
|
|
95
|
+
strings, and docs. Data paths moved to `~/.config/im` / `im.db`;
|
|
96
|
+
existing `feeling` databases and config dirs are not migrated and must
|
|
97
|
+
be moved/deleted by hand.
|
|
98
|
+
- Taskβmood links: `im <mood> -<short id>` records a link; the task
|
|
99
|
+
preview shows a `moods:` field with colored badges
|
|
100
|
+
- `:db` command replaces `:prune` β `:db prune` (old behavior) and
|
|
101
|
+
`:db backfill` (persist missing mood embeddings/scores)
|
|
102
|
+
- `:db doctor` β checks tracker entries against their configured kinds and
|
|
103
|
+
prunes entries whose storage class no longer matches (orphaned tracker
|
|
104
|
+
types and stale nonzero time-marker entries included), after an
|
|
105
|
+
interactive confirm
|
|
106
|
+
- Tracker intervals are calendar-aware (`interval = ["2020-01-01 00:00", "1 day"]`)
|
|
107
|
+
- New `null` tracker kind: valueless timestamp markers with time-of-day
|
|
108
|
+
coloring (e.g. sleep start) or per-interval counting
|
|
109
|
+
|
|
110
|
+
### π Bug Fixes
|
|
111
|
+
|
|
112
|
+
- Recurring-task and tracker intervals are calendar-based (jiff): DST and
|
|
113
|
+
variable month lengths no longer drift interval boundaries
|
|
114
|
+
- Interval storage switched from seconds to packed jiff spans
|
|
115
|
+
|
|
116
|
+
## [0.2.3] - 2026-08-07
|
|
117
|
+
|
|
118
|
+
### π Bug Fixes
|
|
119
|
+
|
|
120
|
+
- Reorganize parse, fix overflow
|
|
121
|
+
|
|
122
|
+
## [0.2.2] - 2026-08-07
|
|
123
|
+
|
|
124
|
+
### π Features
|
|
125
|
+
|
|
126
|
+
- Show name of attached parent during interactive task creation
|
|
127
|
+
|
|
128
|
+
## [0.2.1] - 2026-08-06
|
|
129
|
+
|
|
130
|
+
- Release pipeline updates
|
|
131
|
+
- misc
|
|
132
|
+
|
|
133
|
+
## [0.2.0] - 2026-08-06
|
|
134
|
+
|
|
135
|
+
### π Features
|
|
136
|
+
|
|
137
|
+
- Nomic embedder, ort runtime
|
|
138
|
+
- Reorganize views
|
|
139
|
+
- Reorganize config
|
|
140
|
+
|
|
141
|
+
### πΌ Other
|
|
142
|
+
|
|
143
|
+
- CliOpts flag counts threaded into handlers
|
|
144
|
+
- ColorAxesSettings flattened into MoodConfig; :color verbose axes dump
|
|
145
|
+
- Scheduled task syntax β ':' description, '@' duration markers
|
|
146
|
+
- Lock invalid-timestamp behavior with regression tests
|
|
147
|
+
- Task creation confirmation polish
|
|
148
|
+
- Cliclack intro for task flows; prefill fields logged
|
|
149
|
+
- 'feeling @<date>' β today view anchored to arbitrary days
|
|
150
|
+
- Grid titles + verbose gating; text-tracker entry timestamps
|
|
151
|
+
- Shared Enter-action across both TUIs
|
|
152
|
+
|
|
153
|
+
## [0.1.0] - 2026-08-01
|
|
154
|
+
|
|
155
|
+
Initial commit
|
package/README.md
CHANGED
|
@@ -1 +1,190 @@
|
|
|
1
|
-
|
|
1
|
+
# [Im](https://www.youtube.com/watch?v=Ts0XSyWpMnU)
|
|
2
|
+
|
|
3
|
+
`im` is a CLI tool for recording all the small moments, feelings, thoughts and setbacks and accomplishments that make up your day[^1].
|
|
4
|
+
|
|
5
|
+
```shell
|
|
6
|
+
# record moods and notes
|
|
7
|
+
im playing chess against stockfish and im winning # mood
|
|
8
|
+
im confused . why did i lose? # mood + note
|
|
9
|
+
im . turns out i was playing antichess # note
|
|
10
|
+
|
|
11
|
+
im worried about school tomorrow
|
|
12
|
+
im sad that my dog ate my bones
|
|
13
|
+
im sorry that my cat ate my dog
|
|
14
|
+
|
|
15
|
+
im abe on the next level
|
|
16
|
+
im abe rockin over that base treble
|
|
17
|
+
im aswing it this way
|
|
18
|
+
im aswing it that-a-way
|
|
19
|
+
|
|
20
|
+
# a trailing . opens your editor for writing the note
|
|
21
|
+
im terrified that someone will see me writing this down .
|
|
22
|
+
|
|
23
|
+
# add a task
|
|
24
|
+
|
|
25
|
+
# build habits with recurring tasks
|
|
26
|
+
|
|
27
|
+
# schedule future events
|
|
28
|
+
|
|
29
|
+
# show today's agenda
|
|
30
|
+
im @due | cat
|
|
31
|
+
|
|
32
|
+
# log custom trackers
|
|
33
|
+
im full -food 7
|
|
34
|
+
im going to bed -sleep -acc finished blog post
|
|
35
|
+
im still up -sleep
|
|
36
|
+
im still up -sleep
|
|
37
|
+
|
|
38
|
+
# visualize your progress
|
|
39
|
+
im :
|
|
40
|
+
|
|
41
|
+
im :month pet_cat cat_food_spend wins
|
|
42
|
+
|
|
43
|
+
# reflect and repeat
|
|
44
|
+
im still alive . i think
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- Log moods and journal entries in simple, natural syntax.
|
|
50
|
+
- Today view: show a ordered summary of all that you have logged and planned today -- an interactive, auto-generated daily note.
|
|
51
|
+
- Grid view: Display your mood, task and tracker histories as dots.
|
|
52
|
+
- Moods are colored by how much they match to various emotions or topics you have configured.
|
|
53
|
+
- Change the topics, timespans, and filters, and watch as interesting patterns emerge.
|
|
54
|
+
- Task Management: Support for oneshot, recurring tasks and scheduled tasks.
|
|
55
|
+
- Interactive views over upcoming / completed / due tasks.
|
|
56
|
+
- Full featured data model supporting priorities, subtasks, optional tasks, overdue tasks and multiple completions.
|
|
57
|
+
- Custom Trackers: Track custom metrics with configurable intervals, ranges and colors.
|
|
58
|
+
|
|
59
|
+
## Gallery
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
<img src="https://raw.githubusercontent.com/Squirreljetpack/im/main/.README.assets/image-20260814134849943.png" alt="month" style="width:200px;"/>
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
<!-- HELP_START -->
|
|
69
|
+
```
|
|
70
|
+
im β immediate mood, journal, and task tracker
|
|
71
|
+
|
|
72
|
+
Usage:
|
|
73
|
+
im <mood> [. [body]] log a mood (with an optional body)
|
|
74
|
+
im %<duration> start a timed session
|
|
75
|
+
im [-<tracker> [value]]... [mood] add one or more custom tracker records
|
|
76
|
+
(optionally attached to a mood)
|
|
77
|
+
|
|
78
|
+
im ! [name] [@<time>] create a task (interactive without a name)
|
|
79
|
+
im ! %[<interval>] [[:]name] create a recurring task (interactive)
|
|
80
|
+
im ! @[<time>] [:<name>] [%<duration>] scheduled a task (interactive if partial)
|
|
81
|
+
im +[task] [count] edit or update a task by its reference
|
|
82
|
+
(see `im -vh`)
|
|
83
|
+
|
|
84
|
+
Views:
|
|
85
|
+
im @[date] today view
|
|
86
|
+
im @due[:t|:w] due view
|
|
87
|
+
(today / tomorrow / this week)
|
|
88
|
+
im @[:o|:O] pending tasks
|
|
89
|
+
im @done[:o|:O] completed tasks
|
|
90
|
+
(all / oneshot / recurring+scheduled)
|
|
91
|
+
|
|
92
|
+
Trackers and grids:
|
|
93
|
+
im :[week|month|year] [ids] dot-sequence tracker grid
|
|
94
|
+
ids: <tracker> or @<recurring name>
|
|
95
|
+
period defaults to "week"
|
|
96
|
+
|
|
97
|
+
Other:
|
|
98
|
+
im :config | :c [file] open the config in $VISUAL / $EDITOR
|
|
99
|
+
file: moods | colors
|
|
100
|
+
im - browse trackers
|
|
101
|
+
im :embed embed stdin lines (one vector/line)
|
|
102
|
+
im :color <mood> projected mood color diagnostic
|
|
103
|
+
im :clear [@<date>] clear all mood entries from a day
|
|
104
|
+
im :db prune delete completed and expired tasks
|
|
105
|
+
im :db backfill compute and persist missing mood embeddings
|
|
106
|
+
im :db doctor check and repair database
|
|
107
|
+
|
|
108
|
+
Sync:
|
|
109
|
+
im :auth register|login|status|logout manage this device's syncing account
|
|
110
|
+
im :sync push local changes, pull remote changes
|
|
111
|
+
im :sync --reset rebuild database from remote state
|
|
112
|
+
|
|
113
|
+
Flags:
|
|
114
|
+
im -q | -v <command> quiet / verbose; flags go first
|
|
115
|
+
im --help | -h show this help (-v applies)
|
|
116
|
+
```
|
|
117
|
+
<!-- HELP_END -->
|
|
118
|
+
|
|
119
|
+
## Installation
|
|
120
|
+
|
|
121
|
+
##### Homebrew
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
brew install Squirreljetpack/tap/im
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
##### AUR
|
|
128
|
+
|
|
129
|
+
Unavailable
|
|
130
|
+
|
|
131
|
+
##### npm
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
npm install -g @squirreljetpack/im
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
Run `im :config` to open the configuration file in your `$EDITOR` or `$VISUAL`.
|
|
140
|
+
|
|
141
|
+
The default locations are in order:
|
|
142
|
+
|
|
143
|
+
- `~/.config/matchmaker/config.toml` (If the folder exists already).
|
|
144
|
+
- `{PLATFORM_SPECIFIC_CONFIG_DIRECTORY}/matchmaker` (Generally the same as above when on linux)
|
|
145
|
+
|
|
146
|
+
## Advanced
|
|
147
|
+
|
|
148
|
+
[`im`](https://www.youtube.com/watch?v=xP26YedHWxc) stores three types of objects:
|
|
149
|
+
- moods entries: which compute an embedding that captures their semantic meaning.
|
|
150
|
+
- tasks: oneshot, recurring, or scheduled.
|
|
151
|
+
- trackers: defined in your configuration.
|
|
152
|
+
|
|
153
|
+
##### Trackers
|
|
154
|
+
|
|
155
|
+
Trackers are defined in your configuration (`im :config`); each `[tracker.<name>]`
|
|
156
|
+
section is logged through `im -<name> <value>` (no value for `null` trackers):
|
|
157
|
+
|
|
158
|
+
- Kinds: `text`, `integer`, `float`, `duration` (strings like `6m 30s`), `null` (timestamp/count markers β requires an interval).
|
|
159
|
+
- `interval = { anchor = "2026-01-01T00:00:00-04:00", span = "1 day" }` bins
|
|
160
|
+
entries into calendar slots. By default, a new log in a slot
|
|
161
|
+
replaces the slot's previous entry; `interval.cumulative = true` keeps every log and
|
|
162
|
+
the slot's grid dot shows the aggregate.
|
|
163
|
+
|
|
164
|
+
## FAQ
|
|
165
|
+
|
|
166
|
+
### Why is the binary size so large?
|
|
167
|
+
|
|
168
|
+
`im` bundles a quant of `nomic-ai/nomic-embed-text-v1.5` to categorize your entries. This was the most effective, reasonably sized approach tested out of several options. The size is a bit abnormal, but was pleasantly surprised with how much better it worked compared to my other attempts. If you have any interesting uses for an embedding model that `im` can help with, `im` open to them!
|
|
169
|
+
|
|
170
|
+
### What does im stand for?
|
|
171
|
+
|
|
172
|
+
What it stands for is immaterial[^2].
|
|
173
|
+
|
|
174
|
+
### How can I contribute?
|
|
175
|
+
Open to suggestions. Helping implement more filters, or a sensible configurable spec for exporting to markdown would be helpful. Documentation, always.
|
|
176
|
+
|
|
177
|
+
## See also
|
|
178
|
+
|
|
179
|
+
- https://github.com/qiz-li/im
|
|
180
|
+
- https://docs.rs/jiff/latest/jiff/
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
[^1]: `im` can also help you:
|
|
184
|
+
- catalogue all the goals and hopes you never got around to
|
|
185
|
+
- all the tasks you missed the deadline for
|
|
186
|
+
- reinforce negative thoughts
|
|
187
|
+
- rationalize bad decisions
|
|
188
|
+
- realize that you don't actually have any thoughts interesting enough to write down.
|
|
189
|
+
|
|
190
|
+
[^2]: important, immense, immaculate, importune, imagine all the people .. immolatedβ«
|
|
@@ -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,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 3,
|
|
3
|
+
"name": "@squirreljetpack/im",
|
|
4
|
+
"packages": {
|
|
5
|
+
"": {
|
|
6
|
+
"bin": {
|
|
7
|
+
"im": "run-im.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
|
+
"name": "@squirreljetpack/im",
|
|
21
|
+
"version": "0.2.6"
|
|
22
|
+
},
|
|
23
|
+
"node_modules/detect-libc": {
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=8"
|
|
26
|
+
},
|
|
27
|
+
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
28
|
+
"license": "Apache-2.0",
|
|
29
|
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
30
|
+
"version": "2.1.2"
|
|
31
|
+
},
|
|
32
|
+
"node_modules/prettier": {
|
|
33
|
+
"bin": {
|
|
34
|
+
"prettier": "bin/prettier.cjs"
|
|
35
|
+
},
|
|
36
|
+
"dev": true,
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=14"
|
|
39
|
+
},
|
|
40
|
+
"funding": {
|
|
41
|
+
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
42
|
+
},
|
|
43
|
+
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
|
46
|
+
"version": "3.8.3"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"requires": true,
|
|
50
|
+
"version": "0.2.6"
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,74 @@
|
|
|
1
1
|
{
|
|
2
|
+
"artifactDownloadUrls": [
|
|
3
|
+
"https://github.com/Squirreljetpack/im/releases/download/v0.2.6"
|
|
4
|
+
],
|
|
5
|
+
"bin": {
|
|
6
|
+
"im": "run-im.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"detect-libc": "^2.1.2"
|
|
10
|
+
},
|
|
11
|
+
"description": "A CLI tool for tracking moods, journaling, habits, metrics, and for managing oneshot, recurring and scheduled tasks",
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"prettier": "^3.8.3"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=14.14",
|
|
17
|
+
"npm": ">=6"
|
|
18
|
+
},
|
|
19
|
+
"glibcMinimum": {
|
|
20
|
+
"major": 2,
|
|
21
|
+
"series": 39
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Squirreljetpack/im",
|
|
2
24
|
"name": "@squirreljetpack/im",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
25
|
+
"preferUnplugged": true,
|
|
26
|
+
"repository": "https://github.com/Squirreljetpack/im",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"fmt": "prettier --write **/*.js",
|
|
29
|
+
"fmt:check": "prettier --check **/*.js",
|
|
30
|
+
"postinstall": "node ./install.js"
|
|
31
|
+
},
|
|
32
|
+
"supportedPlatforms": {
|
|
33
|
+
"aarch64-apple-darwin": {
|
|
34
|
+
"artifactName": "im-aarch64-apple-darwin.tar.xz",
|
|
35
|
+
"bins": {
|
|
36
|
+
"im": "im"
|
|
37
|
+
},
|
|
38
|
+
"zipExt": ".tar.xz"
|
|
39
|
+
},
|
|
40
|
+
"aarch64-pc-windows-msvc": {
|
|
41
|
+
"artifactName": "im-x86_64-pc-windows-msvc.zip",
|
|
42
|
+
"bins": {
|
|
43
|
+
"im": "im.exe"
|
|
44
|
+
},
|
|
45
|
+
"zipExt": ".zip"
|
|
46
|
+
},
|
|
47
|
+
"x86_64-pc-windows-gnu": {
|
|
48
|
+
"artifactName": "im-x86_64-pc-windows-msvc.zip",
|
|
49
|
+
"bins": {
|
|
50
|
+
"im": "im.exe"
|
|
51
|
+
},
|
|
52
|
+
"zipExt": ".zip"
|
|
53
|
+
},
|
|
54
|
+
"x86_64-pc-windows-msvc": {
|
|
55
|
+
"artifactName": "im-x86_64-pc-windows-msvc.zip",
|
|
56
|
+
"bins": {
|
|
57
|
+
"im": "im.exe"
|
|
58
|
+
},
|
|
59
|
+
"zipExt": ".zip"
|
|
60
|
+
},
|
|
61
|
+
"x86_64-unknown-linux-gnu": {
|
|
62
|
+
"artifactName": "im-x86_64-unknown-linux-gnu.tar.xz",
|
|
63
|
+
"bins": {
|
|
64
|
+
"im": "im"
|
|
65
|
+
},
|
|
66
|
+
"zipExt": ".tar.xz"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"version": "0.2.6",
|
|
70
|
+
"volta": {
|
|
71
|
+
"node": "18.14.1",
|
|
72
|
+
"npm": "9.5.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/run-im.js
ADDED