@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 ADDED
@@ -0,0 +1,2 @@
1
+ /node_modules
2
+
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
- 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> 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,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.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
- "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.6",
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");