@squirreljetpack/im 0.0.0 β†’ 0.2.7

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 ADDED
@@ -0,0 +1,2 @@
1
+ /node_modules
2
+
package/CHANGELOG.md ADDED
@@ -0,0 +1,173 @@
1
+ ## [0.2.7] - 2026-10-30
2
+
3
+ ### πŸš€ Features
4
+
5
+ - `im :color` without a mood lists every anchor mood of the moods file with
6
+ a swatch of its color, one per line; `im :color <mood>` keeps the full
7
+ pipeline diagnostic.
8
+ - Today view: `[today_view] swap_all_and_due_recurring_task_views` trades the
9
+ `All` and `Due` recurring-task displays. `Due` is anchored at now (earliest
10
+ window intersecting the horizon, skipping closed windows), and previews are
11
+ scoped to the row's window instead of sharing the whole day.
12
+ - No phantom availability windows past a recurring task's expiry, and
13
+ today-view `ends` shows the task's real expiry again.
14
+
15
+ ### πŸ› Bug Fixes
16
+
17
+ - *(embedding)* Pin the CPU EP on macOS
18
+
19
+ ## [0.2.6] - 2026-10-29
20
+
21
+ ### πŸš€ Features (sync)
22
+
23
+ - `im :auth register|login|status|logout` and `im :sync` (plus
24
+ `im :sync --reset`) sync a database against a Cloudflare Worker
25
+ (`sync-server/`): a per-user Durable Object holds the log, D1 the accounts.
26
+ - Concurrent edits merge per field (last write wins) instead of per entity; a
27
+ push breaks at entity boundaries, so a create carries all of its cells.
28
+ - Deletes are terminal and slot losers become tombstones, so two devices that
29
+ edit the same field converge instead of duplicating rows.
30
+ - Entity ids are UUIDv7, so a row created on any device is unique without a
31
+ round trip; local `short_id`s stay a per-device projection.
32
+ - The server address comes from `[sync] server` in the client config
33
+ (`IM_SYNC_URL` overrides it for one command); the account is stored apart
34
+ from the sync state, so `im :sync --reset` does not sign you out.
35
+ - A database from an older version (INTEGER keys) is copied aside and
36
+ recreated empty instead of being written with the wrong key type; an
37
+ unreadable file is quarantined the same way.
38
+
39
+ ### πŸ’₯ Breaking changes (tracker rework)
40
+
41
+ - `min`/`max` renamed to `low`/`high`; `kind = "number"` renamed to
42
+ `"integer"`; new `kind = "duration"` (duration-string values stored as
43
+ seconds, displayed like `6m 30s`).
44
+ - `interval` now takes a table `{ anchor, span, cumulative = false }`
45
+ - Value and bound forms are strict per kind/mode: `float` takes plain
46
+ numbers, `integer` plain whole numbers, `duration` duration strings only,
47
+ `null` no value at all; `low`/`high` mirror the value form of the kind
48
+ (duration strings for `duration` and `null`+replace, whole numbers for
49
+ `text` and `null`+cumulative). Invalid bound forms are dropped with a
50
+ warning at config load.
51
+ - New `strict` setting gates logging/updating: numeric kinds gate the value
52
+ against the inclusive `low`..`high` span (inverted bounds gate the span
53
+ between them), `text` gates the message length in characters, `null`
54
+ trackers gate *when* they may be logged (the circular `low`/`high` time
55
+ offset zone; requires both bounds, replace mode only).
56
+ - `null` trackers require an interval (dropped with a warning otherwise) and
57
+ store score 0 in every mode β€” the payload column is unused and today-view
58
+ rows show no payload (the count lives in the grid, the timestamp in the
59
+ entry's own time). Negative durations are rejected at log time.
60
+ - Replace-mode insertion is one shared strategy for all kinds: a CLI log in
61
+ an occupied slot drops the slot's previous rows (including rows logged
62
+ with a mood β€” the mood row itself survives) and inserts the new one.
63
+ - The TUI update action on a `null` row only moves its timestamp to now,
64
+ within its current interval slot (cross-slot moves are rejected, the
65
+ strict zone still applies); on value-bearing kinds it runs the same strict
66
+ pipeline as the CLI.
67
+ - `:db doctor` prunes any nonzero row of a `null` tracker (all modes).
68
+
69
+ ## [0.2.5] - 2026-08-14
70
+
71
+ ### πŸš€ Features
72
+
73
+ - Editor no longer opens automatically for interactive flows.
74
+ - Config adjustments
75
+ - Body templates
76
+ - Adjust today view filters
77
+ - Config.tasks.overdue_color + badge::task_label_color
78
+ - Today-view preview polish + overdue date column + task children
79
+ - Link actions in the today view (Ctrl+L)
80
+ - Improve jiff-english robustness
81
+ - TUI migration to matchmaker
82
+
83
+ ## [0.2.4] - 2026-08-07
84
+
85
+ ### πŸš€ Features
86
+
87
+ - Db :doctor
88
+ - Jiff-english
89
+ - Duration trackers
90
+ - Tui interaction for trackers
91
+
92
+ ## [Unreleased]
93
+
94
+ ### πŸš€ Features
95
+
96
+ - `[badges]` config section: journal badge (glyph and/or color β€” a bare
97
+ char, a color string, or `{ badge = 'Β·', color = "red" }`; color defaults
98
+ to `Reset`), plus `tracker` and `mood` glyph overrides for the today
99
+ view and previews. `journal_badge` moved from `[today_view]`.
100
+ - Today view task filter: `TasksFilter` (`none` / `all` / `overdue` /
101
+ `pending` / `horizon`) replaces `config.today_view.include_overdue` as
102
+ `initial_tasks_filter`, bound to the view variant β€” `All` uses the
103
+ configured filter, `A` pins `Horizon`, `B` pins `Overdue`; `none` hides
104
+ the whole task section (journal-only view). Undated
105
+ oneshots created before today are no longer dropped from the `All`
106
+ variant. `CycleShow` renamed `CycleFilter`; TUI headers label the
107
+ variants per app (`all | journal | tasks` in the today view,
108
+ `all | oneshot | other` in the tasks view).
109
+
110
+ - Renamed: the binary is now `im` (previously `feeling`), and the logged
111
+ mood entries are consistently called moods β€” DB schema (`mood` table,
112
+ `tracker.mood`, `task_moods.mood_id`), code identifiers, CLI usage
113
+ strings, and docs. Data paths moved to `~/.config/im` / `im.db`;
114
+ existing `feeling` databases and config dirs are not migrated and must
115
+ be moved/deleted by hand.
116
+ - Task↔mood links: `im <mood> -<short id>` records a link; the task
117
+ preview shows a `moods:` field with colored badges
118
+ - `:db` command replaces `:prune` β€” `:db prune` (old behavior) and
119
+ `:db backfill` (persist missing mood embeddings/scores)
120
+ - `:db doctor` β€” checks tracker entries against their configured kinds and
121
+ prunes entries whose storage class no longer matches (orphaned tracker
122
+ types and stale nonzero time-marker entries included), after an
123
+ interactive confirm
124
+ - Tracker intervals are calendar-aware (`interval = ["2020-01-01 00:00", "1 day"]`)
125
+ - New `null` tracker kind: valueless timestamp markers with time-of-day
126
+ coloring (e.g. sleep start) or per-interval counting
127
+
128
+ ### πŸ› Bug Fixes
129
+
130
+ - Recurring-task and tracker intervals are calendar-based (jiff): DST and
131
+ variable month lengths no longer drift interval boundaries
132
+ - Interval storage switched from seconds to packed jiff spans
133
+
134
+ ## [0.2.3] - 2026-08-07
135
+
136
+ ### πŸ› Bug Fixes
137
+
138
+ - Reorganize parse, fix overflow
139
+
140
+ ## [0.2.2] - 2026-08-07
141
+
142
+ ### πŸš€ Features
143
+
144
+ - Show name of attached parent during interactive task creation
145
+
146
+ ## [0.2.1] - 2026-08-06
147
+
148
+ - Release pipeline updates
149
+ - misc
150
+
151
+ ## [0.2.0] - 2026-08-06
152
+
153
+ ### πŸš€ Features
154
+
155
+ - Nomic embedder, ort runtime
156
+ - Reorganize views
157
+ - Reorganize config
158
+
159
+ ### πŸ’Ό Other
160
+
161
+ - CliOpts flag counts threaded into handlers
162
+ - ColorAxesSettings flattened into MoodConfig; :color verbose axes dump
163
+ - Scheduled task syntax β€” ':' description, '@' duration markers
164
+ - Lock invalid-timestamp behavior with regression tests
165
+ - Task creation confirmation polish
166
+ - Cliclack intro for task flows; prefill fields logged
167
+ - 'feeling @<date>' β€” today view anchored to arbitrary days
168
+ - Grid titles + verbose gating; text-tracker entry timestamps
169
+ - Shared Enter-action across both TUIs
170
+
171
+ ## [0.1.0] - 2026-08-01
172
+
173
+ Initial commit
package/README.md CHANGED
@@ -1 +1,188 @@
1
- Placeholder package for @squirreljetpack/im. Real releases are published by cargo-dist.
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
+ ![image-20260814134534039](.README.assets/image-20260814134534039.png)
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] palette swatches / mood diagnostic
103
+ im :clear [@<date>] clear all mood entries from a day
104
+ im :db prune|backfill|doctor database utilities
105
+
106
+ Sync:
107
+ im :auth register|login|status|logout manage this device's syncing account
108
+ im :sync push local changes, pull remote changes
109
+ im :sync --reset rebuild database from remote state
110
+
111
+ Flags:
112
+ im -q | -v <command> quiet / verbose; flags go first
113
+ im --help | -h show this help (-v applies)
114
+ ```
115
+ <!-- HELP_END -->
116
+
117
+ ## Installation
118
+
119
+ ##### Homebrew
120
+
121
+ ```sh
122
+ brew install Squirreljetpack/tap/im
123
+ ```
124
+
125
+ ##### AUR
126
+
127
+ Unavailable
128
+
129
+ ##### npm
130
+
131
+ ```sh
132
+ npm install -g @squirreljetpack/im
133
+ ```
134
+
135
+ ## Configuration
136
+
137
+ Run `im :config` to open the configuration file in your `$EDITOR` or `$VISUAL`.
138
+
139
+ The default locations are in order:
140
+
141
+ - `~/.config/matchmaker/config.toml` (If the folder exists already).
142
+ - `{PLATFORM_SPECIFIC_CONFIG_DIRECTORY}/matchmaker` (Generally the same as above when on linux)
143
+
144
+ ## Advanced
145
+
146
+ [`im`](https://www.youtube.com/watch?v=xP26YedHWxc) stores three types of objects:
147
+ - moods entries: which compute an embedding that captures their semantic meaning.
148
+ - tasks: oneshot, recurring, or scheduled.
149
+ - trackers: defined in your configuration.
150
+
151
+ ##### Trackers
152
+
153
+ Trackers are defined in your configuration (`im :config`); each `[tracker.<name>]`
154
+ section is logged through `im -<name> <value>` (no value for `null` trackers):
155
+
156
+ - Kinds: `text`, `integer`, `float`, `duration` (strings like `6m 30s`), `null` (timestamp/count markers β€” requires an interval).
157
+ - `interval = { anchor = "2026-01-01T00:00:00-04:00", span = "1 day" }` bins
158
+ entries into calendar slots. By default, a new log in a slot
159
+ replaces the slot's previous entry; `interval.cumulative = true` keeps every log and
160
+ the slot's grid dot shows the aggregate.
161
+
162
+ ## FAQ
163
+
164
+ ### Why is the binary size so large?
165
+
166
+ `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!
167
+
168
+ ### What does im stand for?
169
+
170
+ What it stands for is immaterial[^2].
171
+
172
+ ### How can I contribute?
173
+ Open to suggestions. Helping implement more filters, or a sensible configurable spec for exporting to markdown would be helpful. Documentation, always.
174
+
175
+ ## See also
176
+
177
+ - https://github.com/qiz-li/im
178
+ - https://docs.rs/jiff/latest/jiff/
179
+
180
+
181
+ [^1]: `im` can also help you:
182
+ - catalogue all the goals and hopes you never got around to
183
+ - all the tasks you missed the deadline for
184
+ - reinforce negative thoughts
185
+ - rationalize bad decisions
186
+ - realize that you don't actually have any thoughts interesting enough to write down.
187
+
188
+ [^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,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { install } = require("./binary");
4
+ install(false);
@@ -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.7"
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.7"
51
+ }
package/package.json CHANGED
@@ -1,7 +1,74 @@
1
1
  {
2
+ "artifactDownloadUrls": [
3
+ "https://github.com/Squirreljetpack/im/releases/download/v0.2.7"
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
- "version": "0.0.0",
4
- "description": "Placeholder. Bootstraps the package so OIDC trusted publishing can be configured.",
5
- "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/Squirreljetpack/im.git" }
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.7",
70
+ "volta": {
71
+ "node": "18.14.1",
72
+ "npm": "9.5.0"
73
+ }
74
+ }
package/run-im.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { run } = require("./binary");
4
+ run("im");