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 +308 -261
- package/package.json +6 -2
- package/scripts/postinstall.js +76 -0
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,75 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
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/
|
|
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
|
|
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
|
-
|
|
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)
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
701
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
815
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
+
}
|