shared-things-daemon 1.0.4 → 1.1.0

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/dist/cli.js CHANGED
@@ -1,15 +1,75 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command } from "commander";
5
- import { input, select, password, confirm } from "@inquirer/prompts";
4
+ import { spawn } from "child_process";
5
+ import * as fs5 from "fs";
6
+ import * as path5 from "path";
7
+ import { confirm, input, password, select } from "@inquirer/prompts";
6
8
  import chalk from "chalk";
9
+ import { Command } from "commander";
7
10
  import updateNotifier from "update-notifier";
8
11
 
12
+ // src/api.ts
13
+ var ApiClient = class {
14
+ constructor(serverUrl, apiKey) {
15
+ this.serverUrl = serverUrl;
16
+ this.apiKey = apiKey;
17
+ }
18
+ async request(path6, options = {}) {
19
+ const url = `${this.serverUrl}${path6}`;
20
+ const headers = {
21
+ Authorization: `Bearer ${this.apiKey}`,
22
+ "Content-Type": "application/json",
23
+ ...options.headers
24
+ };
25
+ const response = await fetch(url, { ...options, headers });
26
+ if (!response.ok) {
27
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
28
+ throw new Error(`API error: ${error.error || response.statusText}`);
29
+ }
30
+ return response.json();
31
+ }
32
+ /**
33
+ * Get full project state
34
+ */
35
+ async getState() {
36
+ return this.request("/state");
37
+ }
38
+ /**
39
+ * Get changes since timestamp
40
+ */
41
+ async getDelta(since) {
42
+ return this.request(`/delta?since=${encodeURIComponent(since)}`);
43
+ }
44
+ /**
45
+ * Push local changes
46
+ */
47
+ async push(request) {
48
+ return this.request("/push", {
49
+ method: "POST",
50
+ body: JSON.stringify(request)
51
+ });
52
+ }
53
+ /**
54
+ * Health check
55
+ */
56
+ async health() {
57
+ return this.request("/health");
58
+ }
59
+ /**
60
+ * Reset all user data on server
61
+ */
62
+ async reset() {
63
+ return this.request("/reset", {
64
+ method: "DELETE"
65
+ });
66
+ }
67
+ };
68
+
9
69
  // src/config.ts
10
70
  import * as fs from "fs";
11
- import * as path from "path";
12
71
  import * as os from "os";
72
+ import * as path from "path";
13
73
  var CONFIG_DIR = path.join(os.homedir(), ".shared-things");
14
74
  var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
15
75
  function getConfigDir() {
@@ -39,11 +99,182 @@ function configExists() {
39
99
  return fs.existsSync(CONFIG_PATH);
40
100
  }
41
101
 
42
- // src/things.ts
102
+ // src/launchagent.ts
43
103
  import { execSync } from "child_process";
104
+ import * as fs2 from "fs";
105
+ import * as os2 from "os";
106
+ import * as path2 from "path";
107
+ var PLIST_NAME = "com.shared-things.daemon.plist";
108
+ var LAUNCH_AGENTS_DIR = path2.join(os2.homedir(), "Library", "LaunchAgents");
109
+ var PLIST_PATH = path2.join(LAUNCH_AGENTS_DIR, PLIST_NAME);
110
+ function installLaunchAgent() {
111
+ if (!fs2.existsSync(LAUNCH_AGENTS_DIR)) {
112
+ fs2.mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
113
+ }
114
+ let binPath;
115
+ try {
116
+ binPath = execSync("which shared-things", { encoding: "utf-8" }).trim();
117
+ } catch {
118
+ const npmPrefix = execSync("npm prefix -g", { encoding: "utf-8" }).trim();
119
+ binPath = path2.join(npmPrefix, "bin", "shared-things");
120
+ }
121
+ const logPath = path2.join(os2.homedir(), ".shared-things", "daemon.log");
122
+ let nodeBinDir;
123
+ try {
124
+ const nodePath = execSync("which node", { encoding: "utf-8" }).trim();
125
+ nodeBinDir = path2.dirname(nodePath);
126
+ } catch {
127
+ nodeBinDir = "/usr/local/bin";
128
+ }
129
+ const envPath = [
130
+ nodeBinDir,
131
+ "/usr/local/bin",
132
+ "/usr/bin",
133
+ "/bin",
134
+ "/opt/homebrew/bin"
135
+ ].filter((p, i, arr) => arr.indexOf(p) === i).join(":");
136
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
137
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
138
+ <plist version="1.0">
139
+ <dict>
140
+ <key>Label</key>
141
+ <string>com.shared-things.daemon</string>
142
+
143
+ <key>ProgramArguments</key>
144
+ <array>
145
+ <string>${binPath}</string>
146
+ <string>daemon</string>
147
+ </array>
148
+
149
+ <key>RunAtLoad</key>
150
+ <true/>
151
+
152
+ <key>KeepAlive</key>
153
+ <true/>
154
+
155
+ <key>StandardOutPath</key>
156
+ <string>${logPath}</string>
157
+
158
+ <key>StandardErrorPath</key>
159
+ <string>${logPath}</string>
160
+
161
+ <key>EnvironmentVariables</key>
162
+ <dict>
163
+ <key>PATH</key>
164
+ <string>${envPath}</string>
165
+ </dict>
166
+
167
+ <key>ThrottleInterval</key>
168
+ <integer>30</integer>
169
+ </dict>
170
+ </plist>
171
+ `;
172
+ fs2.writeFileSync(PLIST_PATH, plist);
173
+ console.log(`Created: ${PLIST_PATH}`);
174
+ try {
175
+ execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
176
+ execSync(`launchctl load "${PLIST_PATH}"`);
177
+ console.log("LaunchAgent installed and started.");
178
+ } catch (error) {
179
+ console.warn(`Warning: Could not load LaunchAgent: ${error}`);
180
+ console.warn("You may need to manually load it or restart your Mac.");
181
+ }
182
+ }
183
+ function uninstallLaunchAgent() {
184
+ if (!fs2.existsSync(PLIST_PATH)) {
185
+ console.log("LaunchAgent not installed.");
186
+ return;
187
+ }
188
+ try {
189
+ execSync(`launchctl unload "${PLIST_PATH}"`);
190
+ } catch {
191
+ }
192
+ fs2.unlinkSync(PLIST_PATH);
193
+ console.log("LaunchAgent uninstalled.");
194
+ }
195
+ function isLaunchAgentInstalled() {
196
+ return fs2.existsSync(PLIST_PATH);
197
+ }
198
+ function getLaunchAgentStatus() {
199
+ if (!isLaunchAgentInstalled()) {
200
+ return "not-installed";
201
+ }
202
+ try {
203
+ const result = execSync(`launchctl list | grep com.shared-things.daemon`, {
204
+ encoding: "utf-8"
205
+ });
206
+ return result.includes("com.shared-things.daemon") ? "running" : "stopped";
207
+ } catch {
208
+ return "stopped";
209
+ }
210
+ }
211
+
212
+ // src/logger.ts
213
+ import * as fs3 from "fs";
214
+ import * as path3 from "path";
215
+ var LOG_FILE = "daemon.log";
216
+ var MAX_LOG_SIZE = 1024 * 1024;
217
+ function getLogPath() {
218
+ return path3.join(getConfigDir(), LOG_FILE);
219
+ }
220
+ function rotateIfNeeded() {
221
+ const logPath = getLogPath();
222
+ if (!fs3.existsSync(logPath)) return;
223
+ const stats = fs3.statSync(logPath);
224
+ if (stats.size > MAX_LOG_SIZE) {
225
+ const oldLog = `${logPath}.1`;
226
+ if (fs3.existsSync(oldLog)) {
227
+ fs3.unlinkSync(oldLog);
228
+ }
229
+ fs3.renameSync(logPath, oldLog);
230
+ }
231
+ }
232
+ function log(message) {
233
+ ensureConfigDir();
234
+ rotateIfNeeded();
235
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
236
+ const line = `[${timestamp}] ${message}
237
+ `;
238
+ fs3.appendFileSync(getLogPath(), line);
239
+ }
240
+ function logError(message, error) {
241
+ const errorMsg = error instanceof Error ? error.message : String(error);
242
+ log(`ERROR: ${message}${error ? ` - ${errorMsg}` : ""}`);
243
+ }
244
+ function logSync(pushed, pulled, isFirstSync) {
245
+ if (isFirstSync) {
246
+ log(`First sync completed: pushed=${pushed}, pulled=${pulled}`);
247
+ } else if (pushed > 0 || pulled > 0) {
248
+ log(`Sync: pushed=${pushed}, pulled=${pulled}`);
249
+ } else {
250
+ log("Sync: no changes");
251
+ }
252
+ }
253
+ function logMapping(serverId, thingsId) {
254
+ log(`Mapped server ${serverId} -> local ${thingsId}`);
255
+ }
256
+ function logTodoCreated(title) {
257
+ log(`Created todo: "${title}"`);
258
+ }
259
+ function logTodoUpdated(thingsId, title) {
260
+ log(`Updated todo ${thingsId}: "${title}"`);
261
+ }
262
+ function logDaemonStart() {
263
+ log("Daemon started");
264
+ }
265
+ function logDaemonStop() {
266
+ log("Daemon stopped");
267
+ }
268
+
269
+ // src/sync.ts
270
+ import * as fs4 from "fs";
271
+ import * as path4 from "path";
272
+
273
+ // src/things.ts
274
+ import { execSync as execSync2 } from "child_process";
44
275
  function runAppleScript(script) {
45
276
  try {
46
- return execSync(`osascript -e '${script.replace(/'/g, `'"'"'`)}'`, {
277
+ return execSync2(`osascript -e '${script.replace(/'/g, `'"'"'`)}'`, {
47
278
  encoding: "utf-8",
48
279
  maxBuffer: 10 * 1024 * 1024
49
280
  // 10MB buffer
@@ -117,7 +348,7 @@ function createTodo(projectName, todo) {
117
348
  if (todo.tags?.length) params.set("tags", todo.tags.join(","));
118
349
  params.set("list", projectName);
119
350
  const url = `things:///add?${params.toString().replace(/\+/g, "%20")}`;
120
- execSync(`open -g "${url}"`);
351
+ execSync2(`open -g "${url}"`);
121
352
  }
122
353
  function updateTodo(authToken, thingsId, updates) {
123
354
  const params = new URLSearchParams();
@@ -126,10 +357,12 @@ function updateTodo(authToken, thingsId, updates) {
126
357
  if (updates.title !== void 0) params.set("title", updates.title);
127
358
  if (updates.notes !== void 0) params.set("notes", updates.notes);
128
359
  if (updates.dueDate !== void 0) params.set("when", updates.dueDate);
129
- if (updates.completed !== void 0) params.set("completed", updates.completed.toString());
130
- if (updates.canceled !== void 0) params.set("canceled", updates.canceled.toString());
360
+ if (updates.completed !== void 0)
361
+ params.set("completed", updates.completed.toString());
362
+ if (updates.canceled !== void 0)
363
+ params.set("canceled", updates.canceled.toString());
131
364
  const url = `things:///update?${params.toString().replace(/\+/g, "%20")}`;
132
- execSync(`open -g "${url}"`);
365
+ execSync2(`open -g "${url}"`);
133
366
  }
134
367
  function isThingsRunning() {
135
368
  try {
@@ -158,126 +391,10 @@ function listProjects() {
158
391
  return result.split("|||").filter(Boolean);
159
392
  }
160
393
 
161
- // src/api.ts
162
- var ApiClient = class {
163
- constructor(serverUrl, apiKey) {
164
- this.serverUrl = serverUrl;
165
- this.apiKey = apiKey;
166
- }
167
- async request(path6, options = {}) {
168
- const url = `${this.serverUrl}${path6}`;
169
- const headers = {
170
- "Authorization": `Bearer ${this.apiKey}`,
171
- "Content-Type": "application/json",
172
- ...options.headers
173
- };
174
- const response = await fetch(url, { ...options, headers });
175
- if (!response.ok) {
176
- const error = await response.json().catch(() => ({ error: "Unknown error" }));
177
- throw new Error(`API error: ${error.error || response.statusText}`);
178
- }
179
- return response.json();
180
- }
181
- /**
182
- * Get full project state
183
- */
184
- async getState() {
185
- return this.request("/state");
186
- }
187
- /**
188
- * Get changes since timestamp
189
- */
190
- async getDelta(since) {
191
- return this.request(`/delta?since=${encodeURIComponent(since)}`);
192
- }
193
- /**
194
- * Push local changes
195
- */
196
- async push(request) {
197
- return this.request("/push", {
198
- method: "POST",
199
- body: JSON.stringify(request)
200
- });
201
- }
202
- /**
203
- * Health check
204
- */
205
- async health() {
206
- return this.request("/health");
207
- }
208
- /**
209
- * Reset all user data on server
210
- */
211
- async reset() {
212
- return this.request("/reset", {
213
- method: "DELETE"
214
- });
215
- }
216
- };
217
-
218
- // src/logger.ts
219
- import * as fs2 from "fs";
220
- import * as path2 from "path";
221
- var LOG_FILE = "daemon.log";
222
- var MAX_LOG_SIZE = 1024 * 1024;
223
- function getLogPath() {
224
- return path2.join(getConfigDir(), LOG_FILE);
225
- }
226
- function rotateIfNeeded() {
227
- const logPath = getLogPath();
228
- if (!fs2.existsSync(logPath)) return;
229
- const stats = fs2.statSync(logPath);
230
- if (stats.size > MAX_LOG_SIZE) {
231
- const oldLog = logPath + ".1";
232
- if (fs2.existsSync(oldLog)) {
233
- fs2.unlinkSync(oldLog);
234
- }
235
- fs2.renameSync(logPath, oldLog);
236
- }
237
- }
238
- function log(message) {
239
- ensureConfigDir();
240
- rotateIfNeeded();
241
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
242
- const line = `[${timestamp}] ${message}
243
- `;
244
- fs2.appendFileSync(getLogPath(), line);
245
- }
246
- function logError(message, error) {
247
- const errorMsg = error instanceof Error ? error.message : String(error);
248
- log(`ERROR: ${message}${error ? ` - ${errorMsg}` : ""}`);
249
- }
250
- function logSync(pushed, pulled, isFirstSync) {
251
- if (isFirstSync) {
252
- log(`First sync completed: pushed=${pushed}, pulled=${pulled}`);
253
- } else if (pushed > 0 || pulled > 0) {
254
- log(`Sync: pushed=${pushed}, pulled=${pulled}`);
255
- } else {
256
- log("Sync: no changes");
257
- }
258
- }
259
- function logMapping(serverId, thingsId) {
260
- log(`Mapped server ${serverId} -> local ${thingsId}`);
261
- }
262
- function logTodoCreated(title) {
263
- log(`Created todo: "${title}"`);
264
- }
265
- function logTodoUpdated(thingsId, title) {
266
- log(`Updated todo ${thingsId}: "${title}"`);
267
- }
268
- function logDaemonStart() {
269
- log("Daemon started");
270
- }
271
- function logDaemonStop() {
272
- log("Daemon stopped");
273
- }
274
-
275
394
  // src/sync.ts
276
- import * as fs3 from "fs";
277
- import * as path3 from "path";
278
- var STATE_FILE = path3.join(getConfigDir(), "state.json");
395
+ var STATE_FILE = path4.join(getConfigDir(), "state.json");
279
396
  function loadLocalState() {
280
- if (!fs3.existsSync(STATE_FILE)) {
397
+ if (!fs4.existsSync(STATE_FILE)) {
281
398
  return {
282
399
  lastSyncedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
283
400
  todos: /* @__PURE__ */ new Map(),
@@ -285,12 +402,14 @@ function loadLocalState() {
285
402
  };
286
403
  }
287
404
  try {
288
- const content = fs3.readFileSync(STATE_FILE, "utf-8");
405
+ const content = fs4.readFileSync(STATE_FILE, "utf-8");
289
406
  const data = JSON.parse(content);
290
407
  return {
291
408
  lastSyncedAt: data.lastSyncedAt,
292
409
  todos: new Map(Object.entries(data.todos || {})),
293
- serverIdToThingsId: new Map(Object.entries(data.serverIdToThingsId || {}))
410
+ serverIdToThingsId: new Map(
411
+ Object.entries(data.serverIdToThingsId || {})
412
+ )
294
413
  };
295
414
  } catch {
296
415
  return {
@@ -306,7 +425,7 @@ function saveLocalState(state) {
306
425
  todos: Object.fromEntries(state.todos),
307
426
  serverIdToThingsId: Object.fromEntries(state.serverIdToThingsId)
308
427
  };
309
- fs3.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2));
428
+ fs4.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2));
310
429
  }
311
430
  async function runSync() {
312
431
  const config = loadConfig();
@@ -399,7 +518,10 @@ async function runSync() {
399
518
  currentTodosMap.set(newTodo.thingsId, newTodo);
400
519
  logMapping(remoteTodo.id, newTodo.thingsId);
401
520
  } else {
402
- logError(`Failed to find newly created todo for server ${remoteTodo.id}`, remoteTodo.title);
521
+ logError(
522
+ `Failed to find newly created todo for server ${remoteTodo.id}`,
523
+ remoteTodo.title
524
+ );
403
525
  }
404
526
  pulled++;
405
527
  } else if (hasRemoteChanged(localTodo, remoteTodo)) {
@@ -415,7 +537,9 @@ async function runSync() {
415
537
  }
416
538
  }
417
539
  if (delta.todos.deleted.length > 0) {
418
- log(`Remote deletions (manual action needed): ${delta.todos.deleted.join(", ")}`);
540
+ log(
541
+ `Remote deletions (manual action needed): ${delta.todos.deleted.join(", ")}`
542
+ );
419
543
  }
420
544
  localState.lastSyncedAt = delta.syncedAt;
421
545
  localState.todos = currentTodosMap;
@@ -430,121 +554,10 @@ function hasRemoteChanged(local, remote) {
430
554
  return local.title !== remote.title || local.notes !== remote.notes || local.dueDate !== remote.dueDate || local.status !== remote.status || JSON.stringify(local.tags) !== JSON.stringify(remote.tags);
431
555
  }
432
556
 
433
- // src/launchagent.ts
434
- import * as fs4 from "fs";
435
- import * as path4 from "path";
436
- import * as os2 from "os";
437
- import { execSync as execSync2 } from "child_process";
438
- var PLIST_NAME = "com.shared-things.daemon.plist";
439
- var LAUNCH_AGENTS_DIR = path4.join(os2.homedir(), "Library", "LaunchAgents");
440
- var PLIST_PATH = path4.join(LAUNCH_AGENTS_DIR, PLIST_NAME);
441
- function installLaunchAgent() {
442
- if (!fs4.existsSync(LAUNCH_AGENTS_DIR)) {
443
- fs4.mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
444
- }
445
- let binPath;
446
- try {
447
- binPath = execSync2("which shared-things", { encoding: "utf-8" }).trim();
448
- } catch {
449
- const npmPrefix = execSync2("npm prefix -g", { encoding: "utf-8" }).trim();
450
- binPath = path4.join(npmPrefix, "bin", "shared-things");
451
- }
452
- const logPath = path4.join(os2.homedir(), ".shared-things", "daemon.log");
453
- let nodeBinDir;
454
- try {
455
- const nodePath = execSync2("which node", { encoding: "utf-8" }).trim();
456
- nodeBinDir = path4.dirname(nodePath);
457
- } catch {
458
- nodeBinDir = "/usr/local/bin";
459
- }
460
- const envPath = [
461
- nodeBinDir,
462
- "/usr/local/bin",
463
- "/usr/bin",
464
- "/bin",
465
- "/opt/homebrew/bin"
466
- ].filter((p, i, arr) => arr.indexOf(p) === i).join(":");
467
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
468
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
469
- <plist version="1.0">
470
- <dict>
471
- <key>Label</key>
472
- <string>com.shared-things.daemon</string>
473
-
474
- <key>ProgramArguments</key>
475
- <array>
476
- <string>${binPath}</string>
477
- <string>daemon</string>
478
- </array>
479
-
480
- <key>RunAtLoad</key>
481
- <true/>
482
-
483
- <key>KeepAlive</key>
484
- <true/>
485
-
486
- <key>StandardOutPath</key>
487
- <string>${logPath}</string>
488
-
489
- <key>StandardErrorPath</key>
490
- <string>${logPath}</string>
491
-
492
- <key>EnvironmentVariables</key>
493
- <dict>
494
- <key>PATH</key>
495
- <string>${envPath}</string>
496
- </dict>
497
-
498
- <key>ThrottleInterval</key>
499
- <integer>30</integer>
500
- </dict>
501
- </plist>
502
- `;
503
- fs4.writeFileSync(PLIST_PATH, plist);
504
- console.log(`Created: ${PLIST_PATH}`);
505
- try {
506
- execSync2(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
507
- execSync2(`launchctl load "${PLIST_PATH}"`);
508
- console.log("LaunchAgent installed and started.");
509
- } catch (error) {
510
- console.warn(`Warning: Could not load LaunchAgent: ${error}`);
511
- console.warn("You may need to manually load it or restart your Mac.");
512
- }
513
- }
514
- function uninstallLaunchAgent() {
515
- if (!fs4.existsSync(PLIST_PATH)) {
516
- console.log("LaunchAgent not installed.");
517
- return;
518
- }
519
- try {
520
- execSync2(`launchctl unload "${PLIST_PATH}"`);
521
- } catch {
522
- }
523
- fs4.unlinkSync(PLIST_PATH);
524
- console.log("LaunchAgent uninstalled.");
525
- }
526
- function isLaunchAgentInstalled() {
527
- return fs4.existsSync(PLIST_PATH);
528
- }
529
- function getLaunchAgentStatus() {
530
- if (!isLaunchAgentInstalled()) {
531
- return "not-installed";
532
- }
533
- try {
534
- const result = execSync2(`launchctl list | grep com.shared-things.daemon`, {
535
- encoding: "utf-8"
536
- });
537
- return result.includes("com.shared-things.daemon") ? "running" : "stopped";
538
- } catch {
539
- return "stopped";
540
- }
541
- }
542
-
543
557
  // src/cli.ts
544
- import * as fs5 from "fs";
545
- import * as path5 from "path";
546
- import { spawn } from "child_process";
547
- var pkg = JSON.parse(fs5.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
558
+ var pkg = JSON.parse(
559
+ fs5.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
560
+ );
548
561
  var updateCheckInterval = 1e3 * 60 * 60;
549
562
  var notifier = updateNotifier({ pkg, updateCheckInterval });
550
563
  if (notifier.update) {
@@ -574,8 +587,10 @@ if (notifier.update && notifier.update.current !== notifier.update.latest) {
574
587
  process.on("exit", () => {
575
588
  if (notifier.update && notifier.update.current !== notifier.update.latest) {
576
589
  console.error(
577
- chalk.yellow(`
578
- Update available: ${notifier.update.current} \u2192 ${notifier.update.latest}`) + chalk.dim(`
590
+ chalk.yellow(
591
+ `
592
+ Update available: ${notifier.update.current} \u2192 ${notifier.update.latest}`
593
+ ) + chalk.dim(`
579
594
  Run: npm i -g ${pkg.name}
580
595
  `)
581
596
  );
@@ -638,7 +653,9 @@ program.command("init").description("Setup wizard").action(async () => {
638
653
  }
639
654
  const projects = listProjects();
640
655
  if (projects.length === 0) {
641
- console.error("\u274C No Things projects found. Create a project in Things first.");
656
+ console.error(
657
+ "\u274C No Things projects found. Create a project in Things first."
658
+ );
642
659
  process.exit(1);
643
660
  }
644
661
  const projectName = await select({
@@ -647,8 +664,10 @@ program.command("init").description("Setup wizard").action(async () => {
647
664
  });
648
665
  console.log("\n\u23F3 Checking Things project...");
649
666
  const todos = getTodosFromProject(projectName);
650
- console.log(`\u2705 Found ${todos.length} todo${todos.length === 1 ? "" : "s"} in "${projectName}"
651
- `);
667
+ console.log(
668
+ `\u2705 Found ${todos.length} todo${todos.length === 1 ? "" : "s"} in "${projectName}"
669
+ `
670
+ );
652
671
  console.log("\u{1F4CB} Find your Things Auth Token in:");
653
672
  console.log(" Things \u2192 Settings \u2192 General \u2192 Things URLs \u2192 Manage\n");
654
673
  const thingsAuthToken = await password({
@@ -656,7 +675,9 @@ program.command("init").description("Setup wizard").action(async () => {
656
675
  mask: "*",
657
676
  validate: (value) => value ? true : "Auth token is required for updating tasks"
658
677
  });
659
- console.log(chalk.yellow("\n\u26A0\uFE0F Auth token will be verified on first sync.\n"));
678
+ console.log(
679
+ chalk.yellow("\n\u26A0\uFE0F Auth token will be verified on first sync.\n")
680
+ );
660
681
  saveConfig({
661
682
  serverUrl,
662
683
  apiKey,
@@ -697,10 +718,16 @@ program.command("status").description("Show sync status").action(async () => {
697
718
  } catch {
698
719
  serverReachable = false;
699
720
  }
700
- console.log(`${chalk.dim("Server:")} ${chalk.cyan(config.serverUrl)} ${serverReachable ? chalk.green("\u25CF") : chalk.red("\u25CB")}`);
701
- console.log(`${chalk.dim("Project:")} ${chalk.white(config.projectName)}`);
721
+ console.log(
722
+ `${chalk.dim("Server:")} ${chalk.cyan(config.serverUrl)} ${serverReachable ? chalk.green("\u25CF") : chalk.red("\u25CB")}`
723
+ );
724
+ console.log(
725
+ `${chalk.dim("Project:")} ${chalk.white(config.projectName)}`
726
+ );
702
727
  console.log(`${chalk.dim("Interval:")} ${config.pollInterval}s`);
703
- console.log(`${chalk.dim("Daemon:")} ${isRunning ? chalk.green("\u25CF running") : chalk.red("\u25CB stopped")}`);
728
+ console.log(
729
+ `${chalk.dim("Daemon:")} ${isRunning ? chalk.green("\u25CF running") : chalk.red("\u25CB stopped")}`
730
+ );
704
731
  const statePath = path5.join(getConfigDir(), "state.json");
705
732
  if (fs5.existsSync(statePath)) {
706
733
  const state = JSON.parse(fs5.readFileSync(statePath, "utf-8"));
@@ -732,7 +759,11 @@ program.command("sync").description("Manual one-time sync").action(async () => {
732
759
  if (result.isFirstSync) {
733
760
  console.log(chalk.cyan("\u{1F4E5} First sync completed!"));
734
761
  }
735
- console.log(chalk.green(`\u2705 Done! Pushed: ${result.pushed}, Pulled: ${result.pulled}`));
762
+ console.log(
763
+ chalk.green(
764
+ `\u2705 Done! Pushed: ${result.pushed}, Pulled: ${result.pulled}`
765
+ )
766
+ );
736
767
  } catch (error) {
737
768
  logError("Manual sync failed", error);
738
769
  console.error(chalk.red(`\u274C Sync failed: ${error}`));
@@ -811,8 +842,14 @@ program.command("reset").description("Reset sync state (next sync will be a fres
811
842
  try {
812
843
  console.log(chalk.dim("Deleting server data..."));
813
844
  const result = await api.reset();
814
- log(`Server reset: deleted ${result.deleted.todos} todos, ${result.deleted.headings} headings`);
815
- console.log(chalk.green(`\u2705 Server data deleted (${result.deleted.todos} todos, ${result.deleted.headings} headings)`));
845
+ log(
846
+ `Server reset: deleted ${result.deleted.todos} todos, ${result.deleted.headings} headings`
847
+ );
848
+ console.log(
849
+ chalk.green(
850
+ `\u2705 Server data deleted (${result.deleted.todos} todos, ${result.deleted.headings} headings)`
851
+ )
852
+ );
816
853
  } catch (error) {
817
854
  logError("Server reset failed", error);
818
855
  console.error(chalk.red(`\u274C Failed to reset server: ${error}`));
@@ -823,7 +860,11 @@ program.command("reset").description("Reset sync state (next sync will be a fres
823
860
  fs5.unlinkSync(statePath);
824
861
  log("Sync state reset by user");
825
862
  }
826
- console.log(chalk.green('\u2705 Reset complete. Run "shared-things sync" for a fresh sync.'));
863
+ console.log(
864
+ chalk.green(
865
+ '\u2705 Reset complete. Run "shared-things sync" for a fresh sync.'
866
+ )
867
+ );
827
868
  });
828
869
  program.command("purge").description("Remove all local data (config, state, logs)").action(async () => {
829
870
  const configDir = getConfigDir();
@@ -833,7 +874,9 @@ program.command("purge").description("Remove all local data (config, state, logs
833
874
  }
834
875
  const daemonStatus = getLaunchAgentStatus();
835
876
  if (daemonStatus === "running") {
836
- console.log(chalk.yellow("\u26A0\uFE0F Daemon is still running. Stopping it first..."));
877
+ console.log(
878
+ chalk.yellow("\u26A0\uFE0F Daemon is still running. Stopping it first...")
879
+ );
837
880
  uninstallLaunchAgent();
838
881
  }
839
882
  const confirmed = await confirm({
@@ -845,6 +888,10 @@ program.command("purge").description("Remove all local data (config, state, logs
845
888
  return;
846
889
  }
847
890
  fs5.rmSync(configDir, { recursive: true, force: true });
848
- console.log(chalk.green('\u2705 All local data removed. Run "shared-things init" to start fresh.'));
891
+ console.log(
892
+ chalk.green(
893
+ '\u2705 All local data removed. Run "shared-things init" to start fresh.'
894
+ )
895
+ );
849
896
  });
850
897
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shared-things-daemon",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Sync Things 3 projects between multiple macOS users",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "scripts",
12
13
  "README.md"
13
14
  ],
14
15
  "keywords": [
@@ -44,6 +45,7 @@
44
45
  },
45
46
  "devDependencies": {
46
47
  "@types/node": "^20.0.0",
48
+ "@types/update-notifier": "^6.0.8",
47
49
  "tsup": "^8.5.1",
48
50
  "tsx": "^4.7.0",
49
51
  "typescript": "^5.3.0",
@@ -51,6 +53,8 @@
51
53
  },
52
54
  "scripts": {
53
55
  "build": "tsup",
54
- "dev": "tsx watch src/cli.ts"
56
+ "dev": "tsx watch src/cli.ts",
57
+ "typecheck": "tsc --noEmit",
58
+ "postinstall": "node scripts/postinstall.js || true"
55
59
  }
56
60
  }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script: Automatically restart the daemon if it's running.
5
+ * This ensures users get the new version after `npm update -g shared-things-daemon`.
6
+ *
7
+ * Safe behaviors:
8
+ * - Silent success if daemon is not installed/running
9
+ * - Never fails npm install (catches all errors)
10
+ * - Works with macOS launchd only (daemon is macOS-specific)
11
+ */
12
+
13
+ import { execSync } from "node:child_process";
14
+ import * as fs from "node:fs";
15
+ import * as os from "node:os";
16
+ import * as path from "node:path";
17
+
18
+ const LABEL = "com.shared-things.daemon";
19
+ const PLIST_PATH = path.join(
20
+ os.homedir(),
21
+ "Library",
22
+ "LaunchAgents",
23
+ `${LABEL}.plist`,
24
+ );
25
+
26
+ function main() {
27
+ // Only run on macOS
28
+ if (process.platform !== "darwin") {
29
+ return;
30
+ }
31
+
32
+ // Check if launchd agent is installed
33
+ if (!fs.existsSync(PLIST_PATH)) {
34
+ return;
35
+ }
36
+
37
+ // Check if agent is currently loaded/running
38
+ try {
39
+ const result = execSync(`launchctl list 2>/dev/null | grep ${LABEL}`, {
40
+ encoding: "utf-8",
41
+ stdio: ["pipe", "pipe", "pipe"],
42
+ });
43
+ if (!result.includes(LABEL)) {
44
+ return;
45
+ }
46
+ } catch {
47
+ // Not running, nothing to restart
48
+ return;
49
+ }
50
+
51
+ // Restart the agent using load/unload (same as launchagent.ts)
52
+ try {
53
+ // Unload (stop) the agent
54
+ execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`, {
55
+ stdio: "pipe",
56
+ });
57
+
58
+ // Load (start) the agent with updated binary
59
+ execSync(`launchctl load "${PLIST_PATH}"`, {
60
+ stdio: "pipe",
61
+ });
62
+
63
+ console.log(" ✅ Daemon restarted with new version");
64
+ } catch {
65
+ // Don't fail the install, just inform user
66
+ console.log(
67
+ ' ℹ️ Could not auto-restart daemon. Run "shared-things uninstall && shared-things install" manually.',
68
+ );
69
+ }
70
+ }
71
+
72
+ try {
73
+ main();
74
+ } catch {
75
+ // Never fail npm install
76
+ }