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/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, cpSync } from 'node:fs';
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
- const DIR = process.env.TIBODOG_HOME ?? join(homedir(), '.tibodog');
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>, dropping builds of older versions.
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)) return { 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
- return existsSync(app) ? { app } : buildInto(app, onBuild);
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
- spawn('open', ['-g', app], { stdio: 'ignore', detached: true }).unref();
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
- rmSync(INSTALLED, { recursive: true, force: true });
101
- cpSync(app, INSTALLED, { recursive: true });
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 || null, woof, open, at: Date.now() / 1000 };
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
- writeFileSync(join(DIR, 'alert.json'), JSON.stringify(alert));
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
  }