tibodog 0.1.2 → 0.1.3
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/app/Sources/TiboDog/App.swift +5 -2
- package/app/Sources/TiboDog/CodexUsageAPI.swift +12 -2
- package/app/Sources/TiboDog/Credentials.swift +56 -10
- package/app/Sources/TiboDog/DogMonitor.swift +55 -25
- package/app/Sources/TiboDog/Models.swift +2 -2
- package/app/Sources/TiboDog/NotchGeometry.swift +5 -2
- package/app/Sources/TiboDog/NotchView.swift +22 -11
- package/app/Sources/TiboDog/NowPlayingMonitor.swift +7 -4
- package/app/Sources/TiboDog/ResetTracker.swift +23 -6
- package/app/Sources/TiboDog/UsageStore.swift +14 -7
- package/app/Tests/TiboDogTests/CodexUsageTests.swift +77 -0
- package/bin/tibodog.js +65 -29
- package/package.json +3 -2
- package/src/bark.js +18 -9
- package/src/sources.js +145 -36
- package/src/widget.js +44 -15
package/src/widget.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
import { spawn, execFileSync } from 'node:child_process';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
|
-
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, rmSync,
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, rmSync, renameSync } from 'node:fs';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
-
import { join } from 'node:path';
|
|
8
|
+
import { join, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { isPostUrl } from './sources.js';
|
|
10
11
|
|
|
11
12
|
const APP_SRC = fileURLToPath(new URL('../app', import.meta.url));
|
|
12
|
-
|
|
13
|
+
// Where tibodog keeps its state, builds and the alert the widget reads.
|
|
14
|
+
export const DIR = process.env.TIBODOG_HOME ? resolve(process.env.TIBODOG_HOME) : join(homedir(), '.tibodog');
|
|
13
15
|
const INSTALLED = '/Applications/TiboDog.app';
|
|
14
16
|
|
|
15
17
|
export const widgetSupported = () => process.platform === 'darwin' && existsSync(APP_SRC);
|
|
@@ -28,7 +30,7 @@ function sourceHash() {
|
|
|
28
30
|
hash.update(p.slice(APP_SRC.length)).update(readFileSync(p));
|
|
29
31
|
}
|
|
30
32
|
};
|
|
31
|
-
for (const part of ['Package.swift', 'Sources', 'Resources', 'Scripts', 'Vendor']) walk(join(APP_SRC, part));
|
|
33
|
+
for (const part of ['Package.swift', 'Sources', 'Resources', 'Scripts', 'Vendor', '../sounds']) walk(join(APP_SRC, part));
|
|
32
34
|
return hash.digest('hex').slice(0, 10);
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -50,25 +52,42 @@ function build(out) {
|
|
|
50
52
|
});
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
// Builds the widget into ~/.tibodog/widget-<version
|
|
55
|
+
// Builds the widget into ~/.tibodog/widget-<version>. Older builds go only once this one worked, so a
|
|
56
|
+
// failed upgrade keeps the last good widget.
|
|
54
57
|
async function buildInto(app, onBuild) {
|
|
55
58
|
onBuild?.();
|
|
56
|
-
if (existsSync(DIR)) {
|
|
57
|
-
for (const name of readdirSync(DIR)) if (name.startsWith('widget-')) rmSync(join(DIR, name), { recursive: true, force: true });
|
|
58
|
-
}
|
|
59
59
|
const out = join(app, '..');
|
|
60
60
|
mkdirSync(out, { recursive: true });
|
|
61
61
|
const { ok, output } = await build(out);
|
|
62
|
-
if (ok && existsSync(app))
|
|
62
|
+
if (ok && existsSync(app)) {
|
|
63
|
+
for (const name of readdirSync(DIR)) {
|
|
64
|
+
if (name.startsWith('widget-') && join(DIR, name) !== out) rmSync(join(DIR, name), { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
return { app };
|
|
67
|
+
}
|
|
68
|
+
rmSync(out, { recursive: true, force: true });
|
|
63
69
|
return { error: output.trim().split('\n').slice(-5).join('\n') };
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
// The newest earlier build, if a new version failed to build.
|
|
73
|
+
function previousBuild() {
|
|
74
|
+
if (!existsSync(DIR)) return null;
|
|
75
|
+
const builds = readdirSync(DIR)
|
|
76
|
+
.filter((n) => n.startsWith('widget-') && existsSync(join(DIR, n, 'TiboDog.app')))
|
|
77
|
+
.map((n) => join(DIR, n, 'TiboDog.app'))
|
|
78
|
+
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
79
|
+
return builds[0] ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
// The app to run: the installed one if there is one, else a build in ~/.tibodog (made on first run).
|
|
67
83
|
// Resolves to { app } or { error }.
|
|
68
84
|
export async function ensureWidget({ onBuild } = {}) {
|
|
69
85
|
if (existsSync(INSTALLED)) return { app: INSTALLED };
|
|
70
86
|
const app = builtApp();
|
|
71
|
-
|
|
87
|
+
if (existsSync(app)) return { app };
|
|
88
|
+
const r = await buildInto(app, onBuild);
|
|
89
|
+
const fallback = r.error && previousBuild();
|
|
90
|
+
return fallback ? { app: fallback, error: r.error, fallback: true } : r;
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
export function widgetRunning() {
|
|
@@ -82,7 +101,9 @@ export function widgetRunning() {
|
|
|
82
101
|
|
|
83
102
|
// Opens it in the background; it keeps running after tibodog exits.
|
|
84
103
|
export function launchWidget(app) {
|
|
85
|
-
|
|
104
|
+
// `open` doesn't pass our environment on; the widget needs TIBODOG_HOME to find the alert.
|
|
105
|
+
const env = process.env.TIBODOG_HOME ? ['--env', `TIBODOG_HOME=${DIR}`] : [];
|
|
106
|
+
spawn('open', ['-g', ...env, app], { stdio: 'ignore', detached: true }).unref();
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
// Optional: copy the widget to /Applications so it can start at login without tibodog.
|
|
@@ -97,8 +118,13 @@ export async function installApp({ onBuild } = {}) {
|
|
|
97
118
|
} catch {
|
|
98
119
|
// wasn't running
|
|
99
120
|
}
|
|
100
|
-
|
|
101
|
-
|
|
121
|
+
try {
|
|
122
|
+
// ditto keeps the framework's symlinks relative and the code signature intact.
|
|
123
|
+
rmSync(INSTALLED, { recursive: true, force: true });
|
|
124
|
+
execFileSync('ditto', [app, INSTALLED], { stdio: 'ignore' });
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { error: `couldn't copy to ${INSTALLED}: ${e.message}` };
|
|
127
|
+
}
|
|
102
128
|
launchWidget(INSTALLED);
|
|
103
129
|
return { app: INSTALLED };
|
|
104
130
|
}
|
|
@@ -108,8 +134,11 @@ export async function installApp({ onBuild } = {}) {
|
|
|
108
134
|
export function writeAlert({ id, title, body, url, woof, open, postedAt }) {
|
|
109
135
|
mkdirSync(DIR, { recursive: true });
|
|
110
136
|
const posted = Date.parse(postedAt ?? '');
|
|
111
|
-
const alert = { id: String(id), title, body, url: url
|
|
137
|
+
const alert = { id: String(id), title, body, url: isPostUrl(url) ? url : null, woof, open, at: Date.now() / 1000 };
|
|
112
138
|
// When Tibo posted it: the widget checks whether the reset reached the account after this.
|
|
113
139
|
if (Number.isFinite(posted)) alert.postedAt = posted / 1000;
|
|
114
|
-
|
|
140
|
+
// Write then rename, so the widget (polling every 2s) never reads half a file.
|
|
141
|
+
const tmp = join(DIR, `alert.${process.pid}.tmp`);
|
|
142
|
+
writeFileSync(tmp, JSON.stringify(alert));
|
|
143
|
+
renameSync(tmp, join(DIR, 'alert.json'));
|
|
115
144
|
}
|