@drewpayment/mink 0.2.2 → 0.4.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/README.md +80 -11
- package/dist/cli.js +1455 -703
- package/package.json +1 -1
- package/src/cli.ts +25 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/device.ts +65 -0
- package/src/commands/session-start.ts +26 -0
- package/src/commands/session-stop.ts +4 -59
- package/src/commands/sync.ts +111 -0
- package/src/commands/wiki.ts +106 -1
- package/src/core/device.ts +72 -0
- package/src/core/global-config.ts +72 -11
- package/src/core/paths.ts +12 -0
- package/src/core/sync.ts +345 -0
- package/src/core/vault.ts +82 -3
- package/src/types/config.ts +55 -2
- package/src/types/note.ts +7 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -137,6 +137,18 @@ switch (command) {
|
|
|
137
137
|
break;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
case "sync": {
|
|
141
|
+
const { sync } = await import("./commands/sync");
|
|
142
|
+
await sync(process.argv.slice(3));
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case "device": {
|
|
147
|
+
const { device } = await import("./commands/device");
|
|
148
|
+
device(process.argv.slice(3));
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
140
152
|
case "bug-search": {
|
|
141
153
|
const { bugSearch } = await import("./commands/bug-search");
|
|
142
154
|
bugSearch(cwd, process.argv.slice(3).join(" "));
|
|
@@ -189,13 +201,25 @@ switch (command) {
|
|
|
189
201
|
console.log(" config [key] [value] Manage global user settings");
|
|
190
202
|
console.log();
|
|
191
203
|
console.log("Notes & Wiki:");
|
|
192
|
-
console.log(" wiki <cmd> Manage the notes/wiki vault (init|status|rebuild-index|organize)");
|
|
204
|
+
console.log(" wiki <cmd> Manage the notes/wiki vault (init|status|link|unlink|links|rebuild-index|organize)");
|
|
193
205
|
console.log(" note \"text\" Capture a note to the vault");
|
|
194
206
|
console.log(" note --daily [text] Create or append to today's daily note");
|
|
195
207
|
console.log(" note list [filters] List notes (--category, --tag, --recent)");
|
|
196
208
|
console.log(" note search <term> Full-text search across the vault");
|
|
197
209
|
console.log(" skill install Install /mink:note skill for Claude Code");
|
|
198
210
|
console.log();
|
|
211
|
+
console.log("Devices & Sync:");
|
|
212
|
+
console.log(" device Show current device info");
|
|
213
|
+
console.log(" device list List all registered devices");
|
|
214
|
+
console.log(" device rename <name> Set a friendly name for this device");
|
|
215
|
+
console.log(" sync Full manual sync (pull then push)");
|
|
216
|
+
console.log(" sync init <remote-url> Connect ~/.mink to a git remote for cross-device sync");
|
|
217
|
+
console.log(" sync status Show sync state (remote, last sync, pending changes)");
|
|
218
|
+
console.log(" sync push Manually push local changes");
|
|
219
|
+
console.log(" sync pull Manually pull remote changes");
|
|
220
|
+
console.log(" sync pause / resume Temporarily disable/enable auto-sync");
|
|
221
|
+
console.log(" sync disconnect Remove git tracking (data preserved)");
|
|
222
|
+
console.log();
|
|
199
223
|
console.log("Automation & Analysis:");
|
|
200
224
|
console.log(" dashboard [--port=N] Open the real-time web dashboard");
|
|
201
225
|
console.log(" daemon <cmd> Manage the background daemon (start|stop|restart|logs)");
|
package/src/commands/config.ts
CHANGED
|
@@ -69,7 +69,7 @@ export async function config(args: string[]): Promise<void> {
|
|
|
69
69
|
const all = resolveAllConfig();
|
|
70
70
|
console.log("[mink] configuration:");
|
|
71
71
|
for (const entry of all) {
|
|
72
|
-
let line = ` ${entry.key} = ${entry.value} (source: ${entry.source})`;
|
|
72
|
+
let line = ` ${entry.key} = ${entry.value} (${entry.scope}, source: ${entry.source})`;
|
|
73
73
|
if (
|
|
74
74
|
entry.source === "environment variable" &&
|
|
75
75
|
entry.configFileValue !== undefined
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getOrCreateDeviceId,
|
|
3
|
+
listDevices,
|
|
4
|
+
setDeviceName,
|
|
5
|
+
} from "../core/device";
|
|
6
|
+
import { hostname, platform } from "os";
|
|
7
|
+
|
|
8
|
+
export function device(args: string[]): void {
|
|
9
|
+
const sub = args[0] ?? "status";
|
|
10
|
+
|
|
11
|
+
switch (sub) {
|
|
12
|
+
case "status": {
|
|
13
|
+
const id = getOrCreateDeviceId();
|
|
14
|
+
const devices = listDevices();
|
|
15
|
+
const current = devices.find((d) => d.id === id);
|
|
16
|
+
console.log("[mink] device info:");
|
|
17
|
+
console.log(` id: ${id}`);
|
|
18
|
+
console.log(` name: ${current?.name ?? hostname()}`);
|
|
19
|
+
console.log(` hostname: ${hostname()}`);
|
|
20
|
+
console.log(` platform: ${platform()}`);
|
|
21
|
+
if (current?.firstSeen) {
|
|
22
|
+
console.log(` first seen: ${current.firstSeen}`);
|
|
23
|
+
}
|
|
24
|
+
if (current?.lastSeen) {
|
|
25
|
+
console.log(` last seen: ${current.lastSeen}`);
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
case "list": {
|
|
31
|
+
const devices = listDevices();
|
|
32
|
+
const currentId = getOrCreateDeviceId();
|
|
33
|
+
if (devices.length === 0) {
|
|
34
|
+
console.log("[mink] no devices registered yet");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log("[mink] registered devices:");
|
|
38
|
+
for (const d of devices) {
|
|
39
|
+
const marker = d.id === currentId ? " (this device)" : "";
|
|
40
|
+
console.log(` ${d.name}${marker}`);
|
|
41
|
+
console.log(` id: ${d.id}`);
|
|
42
|
+
console.log(` hostname: ${d.hostname}`);
|
|
43
|
+
console.log(` platform: ${d.platform}`);
|
|
44
|
+
console.log(` last seen: ${d.lastSeen}`);
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case "rename": {
|
|
50
|
+
const name = args.slice(1).join(" ");
|
|
51
|
+
if (!name) {
|
|
52
|
+
console.error("Usage: mink device rename <name>");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
setDeviceName(name);
|
|
56
|
+
console.log(`[mink] device renamed to "${name}"`);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
default:
|
|
61
|
+
console.error(`[mink] unknown device subcommand: ${sub}`);
|
|
62
|
+
console.error("Usage: mink device [status|list|rename <name>]");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -7,6 +7,32 @@ import { isWikiEnabled, isVaultInitialized, isInsideVault } from "../core/vault"
|
|
|
7
7
|
import { loadVaultIndex } from "../core/note-index";
|
|
8
8
|
|
|
9
9
|
export function sessionStart(cwd: string): void {
|
|
10
|
+
// Migrate config to shared/local split if needed (before sync pull)
|
|
11
|
+
try {
|
|
12
|
+
const { migrateConfigIfNeeded } = require("../core/global-config");
|
|
13
|
+
migrateConfigIfNeeded();
|
|
14
|
+
} catch {
|
|
15
|
+
// Never crash hooks
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Register/update this device in the registry
|
|
19
|
+
try {
|
|
20
|
+
const { updateDeviceHeartbeat } = require("../core/device");
|
|
21
|
+
updateDeviceHeartbeat();
|
|
22
|
+
} catch {
|
|
23
|
+
// Never crash hooks
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Sync pull before session begins (if enabled)
|
|
27
|
+
try {
|
|
28
|
+
const { isSyncInitialized, syncPull } = require("../core/sync");
|
|
29
|
+
if (isSyncInitialized()) {
|
|
30
|
+
syncPull((msg: string) => console.error(msg));
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Never crash hooks
|
|
34
|
+
}
|
|
35
|
+
|
|
10
36
|
const dir = projectDir(cwd);
|
|
11
37
|
mkdirSync(dir, { recursive: true });
|
|
12
38
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { statSync, existsSync, readFileSync } from "fs";
|
|
2
2
|
import { join, dirname } from "path";
|
|
3
|
-
import { execSync } from "child_process";
|
|
4
3
|
import { safeReadJson, atomicWriteJson, atomicWriteText } from "../core/fs-utils";
|
|
5
4
|
import { isSessionState, buildSummary } from "../core/session";
|
|
6
5
|
import { reflect } from "./reflect";
|
|
@@ -10,10 +9,8 @@ import { createActionLogWriter, consolidateLog } from "../core/action-log";
|
|
|
10
9
|
import {
|
|
11
10
|
isWikiEnabled,
|
|
12
11
|
isVaultInitialized,
|
|
13
|
-
resolveVaultPath,
|
|
14
12
|
vaultProjects,
|
|
15
13
|
} from "../core/vault";
|
|
16
|
-
import { resolveConfigValue } from "../core/global-config";
|
|
17
14
|
import type { SessionState, SessionFinalizer } from "../types/session";
|
|
18
15
|
import type { ProjectConfig } from "../types/file-index";
|
|
19
16
|
|
|
@@ -128,13 +125,11 @@ export function sessionStop(
|
|
|
128
125
|
// Never crash hooks
|
|
129
126
|
}
|
|
130
127
|
|
|
131
|
-
//
|
|
128
|
+
// Full mink sync (subsumes wiki git-backup)
|
|
132
129
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
gitBackupVault(onReminder);
|
|
137
|
-
}
|
|
130
|
+
const { isSyncInitialized, syncPush } = require("../core/sync");
|
|
131
|
+
if (isSyncInitialized()) {
|
|
132
|
+
syncPush(onReminder);
|
|
138
133
|
}
|
|
139
134
|
} catch {
|
|
140
135
|
// Never crash hooks
|
|
@@ -209,53 +204,3 @@ function writeSessionToWiki(
|
|
|
209
204
|
}
|
|
210
205
|
}
|
|
211
206
|
|
|
212
|
-
function gitBackupVault(
|
|
213
|
-
onReminder: (msg: string) => void
|
|
214
|
-
): void {
|
|
215
|
-
const vaultPath = resolveVaultPath();
|
|
216
|
-
|
|
217
|
-
// Check if vault is a git repo
|
|
218
|
-
const gitDir = join(vaultPath, ".git");
|
|
219
|
-
if (!existsSync(gitDir)) {
|
|
220
|
-
onReminder(
|
|
221
|
-
"[mink] wiki git-backup enabled but vault is not a git repo — run 'git init' in " +
|
|
222
|
-
vaultPath
|
|
223
|
-
);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
// Check for changes
|
|
229
|
-
const status = execSync("git status --porcelain", {
|
|
230
|
-
cwd: vaultPath,
|
|
231
|
-
timeout: 5000,
|
|
232
|
-
}).toString();
|
|
233
|
-
|
|
234
|
-
if (!status.trim()) return; // Nothing to commit
|
|
235
|
-
|
|
236
|
-
// Stage and commit
|
|
237
|
-
execSync("git add -A", { cwd: vaultPath, timeout: 5000 });
|
|
238
|
-
const msg = `mink: vault update ${new Date().toISOString().split("T")[0]}`;
|
|
239
|
-
execSync(`git commit -m "${msg}"`, {
|
|
240
|
-
cwd: vaultPath,
|
|
241
|
-
timeout: 5000,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Push (best-effort with timeout)
|
|
245
|
-
const remote = resolveConfigValue("wiki.git-remote").value;
|
|
246
|
-
try {
|
|
247
|
-
execSync(`git push ${remote}`, {
|
|
248
|
-
cwd: vaultPath,
|
|
249
|
-
timeout: 10000,
|
|
250
|
-
});
|
|
251
|
-
} catch {
|
|
252
|
-
onReminder(
|
|
253
|
-
`[mink] wiki git push to '${remote}' failed — local commit preserved, will retry next session`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
} catch (err) {
|
|
257
|
-
onReminder(
|
|
258
|
-
`[mink] wiki git backup error: ${err instanceof Error ? err.message : String(err)}`
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initSync,
|
|
3
|
+
syncPull,
|
|
4
|
+
syncPush,
|
|
5
|
+
getSyncStatus,
|
|
6
|
+
disconnectSync,
|
|
7
|
+
isSyncInitialized,
|
|
8
|
+
} from "../core/sync";
|
|
9
|
+
import { setConfigValue } from "../core/global-config";
|
|
10
|
+
|
|
11
|
+
export async function sync(args: string[]): Promise<void> {
|
|
12
|
+
const subcommand = args[0];
|
|
13
|
+
|
|
14
|
+
switch (subcommand) {
|
|
15
|
+
case undefined:
|
|
16
|
+
// No args: full manual sync (pull then push)
|
|
17
|
+
return handleManualSync();
|
|
18
|
+
|
|
19
|
+
case "init":
|
|
20
|
+
return handleInit(args.slice(1));
|
|
21
|
+
|
|
22
|
+
case "status":
|
|
23
|
+
return handleStatus();
|
|
24
|
+
|
|
25
|
+
case "push":
|
|
26
|
+
syncPush((msg) => console.error(msg));
|
|
27
|
+
return;
|
|
28
|
+
|
|
29
|
+
case "pull":
|
|
30
|
+
syncPull((msg) => console.error(msg));
|
|
31
|
+
return;
|
|
32
|
+
|
|
33
|
+
case "pause":
|
|
34
|
+
return handlePause();
|
|
35
|
+
|
|
36
|
+
case "resume":
|
|
37
|
+
return handleResume();
|
|
38
|
+
|
|
39
|
+
case "disconnect":
|
|
40
|
+
return handleDisconnect();
|
|
41
|
+
|
|
42
|
+
default:
|
|
43
|
+
console.error(`[mink] unknown sync subcommand: ${subcommand}`);
|
|
44
|
+
console.error("Usage: mink sync [init|status|push|pull|pause|resume|disconnect]");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handleManualSync(): void {
|
|
50
|
+
if (!isSyncInitialized()) {
|
|
51
|
+
console.error("[mink] sync is not initialized");
|
|
52
|
+
console.error("Run 'mink sync init <remote-url>' to set up sync");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log("[mink] pulling remote changes...");
|
|
57
|
+
syncPull((msg) => console.error(msg));
|
|
58
|
+
|
|
59
|
+
console.log("[mink] pushing local changes...");
|
|
60
|
+
syncPush((msg) => console.error(msg));
|
|
61
|
+
|
|
62
|
+
console.log("[mink] sync complete");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleInit(args: string[]): void {
|
|
66
|
+
const remoteUrl = args[0];
|
|
67
|
+
if (!remoteUrl) {
|
|
68
|
+
console.error("[mink] missing remote URL");
|
|
69
|
+
console.error("Usage: mink sync init <remote-url>");
|
|
70
|
+
console.error("Example: mink sync init git@github.com:user/mink-data.git");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
initSync(remoteUrl);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleStatus(): void {
|
|
78
|
+
const status = getSyncStatus();
|
|
79
|
+
|
|
80
|
+
console.log("Mink Sync Status");
|
|
81
|
+
console.log("─".repeat(40));
|
|
82
|
+
console.log(` Enabled: ${status.enabled ? "yes" : "no"}`);
|
|
83
|
+
console.log(` Git initialized: ${status.gitInitialized ? "yes" : "no"}`);
|
|
84
|
+
console.log(` Remote URL: ${status.remoteUrl || "(not set)"}`);
|
|
85
|
+
console.log(` Branch: ${status.branch || "(none)"}`);
|
|
86
|
+
console.log(` Pending changes: ${status.pendingChanges}`);
|
|
87
|
+
console.log(` Last push: ${status.lastPush || "(never)"}`);
|
|
88
|
+
console.log(` Last pull: ${status.lastPull || "(never)"}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handlePause(): void {
|
|
92
|
+
setConfigValue("sync.enabled", "false");
|
|
93
|
+
console.log("[mink] sync paused — auto-sync disabled");
|
|
94
|
+
console.log("[mink] run 'mink sync resume' to re-enable");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleResume(): void {
|
|
98
|
+
const status = getSyncStatus();
|
|
99
|
+
if (!status.gitInitialized) {
|
|
100
|
+
console.error("[mink] sync has not been initialized yet");
|
|
101
|
+
console.error("Run 'mink sync init <remote-url>' first");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setConfigValue("sync.enabled", "true");
|
|
106
|
+
console.log("[mink] sync resumed — auto-sync re-enabled");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleDisconnect(): void {
|
|
110
|
+
disconnectSync();
|
|
111
|
+
}
|
package/src/commands/wiki.ts
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
isVaultInitialized,
|
|
8
8
|
vaultManifestPath,
|
|
9
9
|
vaultTemplates,
|
|
10
|
+
linkExternal,
|
|
11
|
+
unlinkExternal,
|
|
12
|
+
listLinks,
|
|
10
13
|
} from "../core/vault";
|
|
11
14
|
import { atomicWriteJson } from "../core/fs-utils";
|
|
12
15
|
import { setConfigValue } from "../core/global-config";
|
|
@@ -34,13 +37,25 @@ export async function wiki(
|
|
|
34
37
|
case "organize":
|
|
35
38
|
wikiOrganize();
|
|
36
39
|
break;
|
|
40
|
+
case "link":
|
|
41
|
+
wikiLink(args.slice(1));
|
|
42
|
+
break;
|
|
43
|
+
case "unlink":
|
|
44
|
+
wikiUnlink(args.slice(1));
|
|
45
|
+
break;
|
|
46
|
+
case "links":
|
|
47
|
+
wikiLinks();
|
|
48
|
+
break;
|
|
37
49
|
default:
|
|
38
|
-
console.log("Usage: mink wiki <
|
|
50
|
+
console.log("Usage: mink wiki <command>");
|
|
39
51
|
console.log();
|
|
40
52
|
console.log(" init Initialize the notes/wiki vault");
|
|
41
53
|
console.log(" status Show vault statistics");
|
|
42
54
|
console.log(" rebuild-index Full rescan and reindex of vault");
|
|
43
55
|
console.log(" organize List inbox notes needing categorization");
|
|
56
|
+
console.log(" link <path> [name] Symlink external notes into the vault");
|
|
57
|
+
console.log(" unlink <name> Remove a symlinked directory from the vault");
|
|
58
|
+
console.log(" links List all linked directories");
|
|
44
59
|
break;
|
|
45
60
|
}
|
|
46
61
|
}
|
|
@@ -188,6 +203,15 @@ function wikiStatus(): void {
|
|
|
188
203
|
` last indexed: ${index.lastScanTimestamp || "never"}`
|
|
189
204
|
);
|
|
190
205
|
|
|
206
|
+
const links = listLinks();
|
|
207
|
+
if (links.length > 0) {
|
|
208
|
+
console.log();
|
|
209
|
+
console.log(" Linked directories:");
|
|
210
|
+
for (const link of links) {
|
|
211
|
+
console.log(` ${link.name} -> ${link.target}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
191
215
|
if (categoryCounts.inbox > 0) {
|
|
192
216
|
console.log();
|
|
193
217
|
console.log(
|
|
@@ -242,6 +266,87 @@ function wikiOrganize(): void {
|
|
|
242
266
|
);
|
|
243
267
|
}
|
|
244
268
|
|
|
269
|
+
function wikiLink(args: string[]): void {
|
|
270
|
+
if (!isVaultInitialized()) {
|
|
271
|
+
console.log("[mink] no vault initialized");
|
|
272
|
+
console.log(" Run 'mink wiki init' first.");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const targetPath = args[0];
|
|
277
|
+
if (!targetPath) {
|
|
278
|
+
console.log("Usage: mink wiki link <path> [name]");
|
|
279
|
+
console.log();
|
|
280
|
+
console.log(" Symlinks an external directory into the vault so it appears");
|
|
281
|
+
console.log(" alongside Mink's content in Obsidian.");
|
|
282
|
+
console.log();
|
|
283
|
+
console.log(" Examples:");
|
|
284
|
+
console.log(" mink wiki link ~/dev/notes");
|
|
285
|
+
console.log(" mink wiki link ~/dev/notes my-notes");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const name = args[1]; // optional override
|
|
290
|
+
const result = linkExternal(targetPath, name);
|
|
291
|
+
|
|
292
|
+
if (!result.ok) {
|
|
293
|
+
console.error(`[mink] ${result.error}`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log(`[mink] linked: ${result.linkName} -> ${targetPath}`);
|
|
298
|
+
console.log(` symlink: ${result.linkPath}`);
|
|
299
|
+
console.log();
|
|
300
|
+
console.log(" Open ~/.mink/wiki/ as your Obsidian vault to see everything together.");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function wikiUnlink(args: string[]): void {
|
|
304
|
+
if (!isVaultInitialized()) {
|
|
305
|
+
console.log("[mink] no vault initialized");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const name = args[0];
|
|
310
|
+
if (!name) {
|
|
311
|
+
console.log("Usage: mink wiki unlink <name>");
|
|
312
|
+
console.log();
|
|
313
|
+
console.log(" Run 'mink wiki links' to see linked directories.");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const result = unlinkExternal(name);
|
|
318
|
+
|
|
319
|
+
if (!result.ok) {
|
|
320
|
+
console.error(`[mink] ${result.error}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
console.log(`[mink] unlinked: ${name}`);
|
|
325
|
+
console.log(" (original directory was not modified)");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function wikiLinks(): void {
|
|
329
|
+
if (!isVaultInitialized()) {
|
|
330
|
+
console.log("[mink] no vault initialized");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const links = listLinks();
|
|
335
|
+
|
|
336
|
+
if (links.length === 0) {
|
|
337
|
+
console.log("[mink] no linked directories");
|
|
338
|
+
console.log(" Use 'mink wiki link <path>' to symlink external notes into the vault.");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log("[mink] linked directories:");
|
|
343
|
+
console.log();
|
|
344
|
+
for (const link of links) {
|
|
345
|
+
console.log(` ${link.name} -> ${link.target}`);
|
|
346
|
+
console.log(` linked: ${link.linkedAt}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
245
350
|
function expandPath(raw: string): string {
|
|
246
351
|
if (raw.startsWith("~/")) {
|
|
247
352
|
return resolve(homedir(), raw.slice(2));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import { hostname, platform } from "os";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { deviceIdPath, deviceRegistryPath } from "./paths";
|
|
6
|
+
import { safeReadJson, atomicWriteJson } from "./fs-utils";
|
|
7
|
+
import type { DeviceInfo, DeviceRegistry } from "../types/config";
|
|
8
|
+
|
|
9
|
+
export function getOrCreateDeviceId(): string {
|
|
10
|
+
const idPath = deviceIdPath();
|
|
11
|
+
if (existsSync(idPath)) {
|
|
12
|
+
return readFileSync(idPath, "utf-8").trim();
|
|
13
|
+
}
|
|
14
|
+
const id = randomUUID();
|
|
15
|
+
mkdirSync(dirname(idPath), { recursive: true });
|
|
16
|
+
writeFileSync(idPath, id + "\n");
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadDeviceRegistry(): DeviceRegistry {
|
|
21
|
+
const raw = safeReadJson(deviceRegistryPath());
|
|
22
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw) && "devices" in (raw as object)) {
|
|
23
|
+
return raw as DeviceRegistry;
|
|
24
|
+
}
|
|
25
|
+
return { devices: {} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function saveDeviceRegistry(registry: DeviceRegistry): void {
|
|
29
|
+
atomicWriteJson(deviceRegistryPath(), registry);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function updateDeviceHeartbeat(): void {
|
|
33
|
+
const id = getOrCreateDeviceId();
|
|
34
|
+
const registry = loadDeviceRegistry();
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
const existing = registry.devices[id];
|
|
37
|
+
|
|
38
|
+
registry.devices[id] = {
|
|
39
|
+
name: existing?.name ?? hostname(),
|
|
40
|
+
hostname: hostname(),
|
|
41
|
+
platform: platform(),
|
|
42
|
+
firstSeen: existing?.firstSeen ?? now,
|
|
43
|
+
lastSeen: now,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
saveDeviceRegistry(registry);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function listDevices(): Array<DeviceInfo & { id: string }> {
|
|
50
|
+
const registry = loadDeviceRegistry();
|
|
51
|
+
return Object.entries(registry.devices).map(([id, info]) => ({
|
|
52
|
+
id,
|
|
53
|
+
...info,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function setDeviceName(name: string): void {
|
|
58
|
+
const id = getOrCreateDeviceId();
|
|
59
|
+
const registry = loadDeviceRegistry();
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
const existing = registry.devices[id];
|
|
62
|
+
|
|
63
|
+
registry.devices[id] = {
|
|
64
|
+
name,
|
|
65
|
+
hostname: hostname(),
|
|
66
|
+
platform: platform(),
|
|
67
|
+
firstSeen: existing?.firstSeen ?? now,
|
|
68
|
+
lastSeen: now,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
saveDeviceRegistry(registry);
|
|
72
|
+
}
|