@drewpayment/mink 0.2.2 → 0.3.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 +1019 -530
- package/package.json +1 -1
- package/src/cli.ts +16 -1
- package/src/commands/session-start.ts +10 -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/sync.ts +333 -0
- package/src/core/vault.ts +82 -3
- package/src/types/config.ts +30 -2
- package/src/types/note.ts +7 -0
package/src/core/sync.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { minkRoot } from "./paths";
|
|
5
|
+
import { resolveConfigValue, setConfigValue } from "./global-config";
|
|
6
|
+
|
|
7
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const GIT_TIMEOUT = 5_000;
|
|
10
|
+
const PUSH_TIMEOUT = 10_000;
|
|
11
|
+
const FETCH_TIMEOUT = 15_000;
|
|
12
|
+
|
|
13
|
+
const GITIGNORE_CONTENTS = `# Runtime state — machine-specific
|
|
14
|
+
scheduler.pid
|
|
15
|
+
scheduler.log
|
|
16
|
+
|
|
17
|
+
# Local backups — machine-specific snapshots
|
|
18
|
+
projects/*/backups/
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function git(args: string, timeoutMs: number = GIT_TIMEOUT): string {
|
|
24
|
+
return execSync(`git ${args}`, {
|
|
25
|
+
cwd: minkRoot(),
|
|
26
|
+
timeout: timeoutMs,
|
|
27
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
+
}).toString().trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function gitSafe(args: string, timeoutMs: number = GIT_TIMEOUT): string | null {
|
|
32
|
+
try {
|
|
33
|
+
return git(args, timeoutMs);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export function isSyncInitialized(): boolean {
|
|
42
|
+
const enabled = resolveConfigValue("sync.enabled").value;
|
|
43
|
+
if (enabled !== "true") return false;
|
|
44
|
+
return existsSync(join(minkRoot(), ".git"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ensureGitignore(): void {
|
|
48
|
+
const gitignorePath = join(minkRoot(), ".gitignore");
|
|
49
|
+
writeFileSync(gitignorePath, GITIGNORE_CONTENTS);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SyncStatusInfo {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
gitInitialized: boolean;
|
|
55
|
+
remoteUrl: string;
|
|
56
|
+
lastPush: string;
|
|
57
|
+
lastPull: string;
|
|
58
|
+
pendingChanges: number;
|
|
59
|
+
branch: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getSyncStatus(): SyncStatusInfo {
|
|
63
|
+
const enabled = resolveConfigValue("sync.enabled").value === "true";
|
|
64
|
+
const gitInitialized = existsSync(join(minkRoot(), ".git"));
|
|
65
|
+
const remoteUrl = resolveConfigValue("sync.remote-url").value;
|
|
66
|
+
const lastPush = resolveConfigValue("sync.last-push").value;
|
|
67
|
+
const lastPull = resolveConfigValue("sync.last-pull").value;
|
|
68
|
+
|
|
69
|
+
let pendingChanges = 0;
|
|
70
|
+
let branch = "";
|
|
71
|
+
|
|
72
|
+
if (gitInitialized) {
|
|
73
|
+
const status = gitSafe("status --porcelain");
|
|
74
|
+
if (status !== null) {
|
|
75
|
+
pendingChanges = status
|
|
76
|
+
.split("\n")
|
|
77
|
+
.filter((l) => l.trim().length > 0).length;
|
|
78
|
+
}
|
|
79
|
+
branch = gitSafe("rev-parse --abbrev-ref HEAD") ?? "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
enabled,
|
|
84
|
+
gitInitialized,
|
|
85
|
+
remoteUrl,
|
|
86
|
+
lastPush,
|
|
87
|
+
lastPull,
|
|
88
|
+
pendingChanges,
|
|
89
|
+
branch,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function initSync(remoteUrl: string): void {
|
|
94
|
+
const root = minkRoot();
|
|
95
|
+
const gitDir = join(root, ".git");
|
|
96
|
+
|
|
97
|
+
if (existsSync(gitDir)) {
|
|
98
|
+
console.log("[mink] sync is already initialized in " + root);
|
|
99
|
+
console.log("[mink] run 'mink sync disconnect' first to reinitialize");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Write .gitignore before any git operations
|
|
104
|
+
ensureGitignore();
|
|
105
|
+
|
|
106
|
+
// Initialize git repo
|
|
107
|
+
git("init");
|
|
108
|
+
git(`remote add origin ${remoteUrl}`);
|
|
109
|
+
|
|
110
|
+
// Try to fetch from remote
|
|
111
|
+
const fetchResult = gitSafe("fetch origin", FETCH_TIMEOUT);
|
|
112
|
+
|
|
113
|
+
if (fetchResult !== null) {
|
|
114
|
+
// Check if remote has any branches
|
|
115
|
+
const remoteBranches = gitSafe("branch -r");
|
|
116
|
+
if (remoteBranches && remoteBranches.trim().length > 0) {
|
|
117
|
+
// Remote has content — detect default branch and pull
|
|
118
|
+
const defaultBranch = detectRemoteDefaultBranch();
|
|
119
|
+
try {
|
|
120
|
+
git("add -A");
|
|
121
|
+
// Commit local content first so merge has a base
|
|
122
|
+
const status = gitSafe("status --porcelain");
|
|
123
|
+
if (status && status.trim().length > 0) {
|
|
124
|
+
git(`commit -m "mink: local state before sync"`);
|
|
125
|
+
}
|
|
126
|
+
git(`pull --rebase origin ${defaultBranch}`, FETCH_TIMEOUT);
|
|
127
|
+
} catch {
|
|
128
|
+
// Rebase failed — abort and warn
|
|
129
|
+
gitSafe("rebase --abort");
|
|
130
|
+
console.error(
|
|
131
|
+
"[mink] warning: could not merge remote content. Local state preserved."
|
|
132
|
+
);
|
|
133
|
+
console.error(
|
|
134
|
+
"[mink] you may need to resolve conflicts manually with 'mink sync pull'"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// Remote is empty — do initial push
|
|
139
|
+
git("add -A");
|
|
140
|
+
git(`commit -m "mink: initial sync"`);
|
|
141
|
+
git("branch -M main");
|
|
142
|
+
git("push -u origin main", PUSH_TIMEOUT);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
// Fetch failed (network or empty repo) — commit locally and try push
|
|
146
|
+
git("add -A");
|
|
147
|
+
git(`commit -m "mink: initial sync"`);
|
|
148
|
+
git("branch -M main");
|
|
149
|
+
try {
|
|
150
|
+
git("push -u origin main", PUSH_TIMEOUT);
|
|
151
|
+
} catch {
|
|
152
|
+
console.error(
|
|
153
|
+
"[mink] push failed — local commit preserved, will retry on next sync"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Save config
|
|
159
|
+
setConfigValue("sync.enabled", "true");
|
|
160
|
+
setConfigValue("sync.remote-url", remoteUrl);
|
|
161
|
+
setConfigValue("sync.last-push", new Date().toISOString());
|
|
162
|
+
|
|
163
|
+
console.log("[mink] sync initialized successfully");
|
|
164
|
+
console.log("[mink] remote: " + remoteUrl);
|
|
165
|
+
console.log("[mink] auto-sync: pull on session-start, push on session-stop");
|
|
166
|
+
console.log("[mink] manual sync: run 'mink sync' at any time");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function syncPull(
|
|
170
|
+
onMessage: (msg: string) => void = (msg) => console.error(msg)
|
|
171
|
+
): void {
|
|
172
|
+
if (!isSyncInitialized()) return;
|
|
173
|
+
|
|
174
|
+
const root = minkRoot();
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Stash any uncommitted local changes as safety net
|
|
178
|
+
const status = gitSafe("status --porcelain");
|
|
179
|
+
const hasLocalChanges = status !== null && status.trim().length > 0;
|
|
180
|
+
|
|
181
|
+
if (hasLocalChanges) {
|
|
182
|
+
gitSafe("stash push -m mink-sync-pull");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Determine branch
|
|
186
|
+
const branch = gitSafe("rev-parse --abbrev-ref HEAD") ?? "main";
|
|
187
|
+
|
|
188
|
+
// Pull with rebase
|
|
189
|
+
try {
|
|
190
|
+
git(`pull --rebase origin ${branch}`, FETCH_TIMEOUT);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
// Check if rebase is in progress and abort
|
|
193
|
+
if (existsSync(join(root, ".git", "rebase-merge")) ||
|
|
194
|
+
existsSync(join(root, ".git", "rebase-apply"))) {
|
|
195
|
+
gitSafe("rebase --abort");
|
|
196
|
+
onMessage(
|
|
197
|
+
"[mink] sync pull: rebase conflict detected — aborted rebase, local state preserved"
|
|
198
|
+
);
|
|
199
|
+
onMessage(
|
|
200
|
+
"[mink] resolve manually with 'mink sync pull' or 'cd ~/.mink && git pull --rebase origin main'"
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
onMessage(
|
|
204
|
+
`[mink] sync pull failed: ${err instanceof Error ? err.message : String(err)}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Pop stash if we stashed earlier
|
|
210
|
+
if (hasLocalChanges) {
|
|
211
|
+
try {
|
|
212
|
+
gitSafe("stash pop");
|
|
213
|
+
} catch {
|
|
214
|
+
onMessage(
|
|
215
|
+
"[mink] sync pull: stash pop had conflicts — your local changes are in git stash"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
setConfigValue("sync.last-pull", new Date().toISOString());
|
|
221
|
+
} catch (err) {
|
|
222
|
+
onMessage(
|
|
223
|
+
`[mink] sync pull error: ${err instanceof Error ? err.message : String(err)}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function syncPush(
|
|
229
|
+
onMessage: (msg: string) => void = (msg) => console.error(msg)
|
|
230
|
+
): void {
|
|
231
|
+
if (!isSyncInitialized()) return;
|
|
232
|
+
|
|
233
|
+
const root = minkRoot();
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
// Check for changes
|
|
237
|
+
const status = gitSafe("status --porcelain");
|
|
238
|
+
if (!status || !status.trim()) {
|
|
239
|
+
// No local changes — still try to push any unpushed commits
|
|
240
|
+
const branch = gitSafe("rev-parse --abbrev-ref HEAD") ?? "main";
|
|
241
|
+
try {
|
|
242
|
+
git(`push origin ${branch}`, PUSH_TIMEOUT);
|
|
243
|
+
setConfigValue("sync.last-push", new Date().toISOString());
|
|
244
|
+
} catch {
|
|
245
|
+
// No unpushed commits or network error — silent
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Stage all changes (respects .gitignore)
|
|
251
|
+
git("add -A");
|
|
252
|
+
|
|
253
|
+
// Commit
|
|
254
|
+
const now = new Date();
|
|
255
|
+
const timestamp = now.toISOString().replace("T", " ").slice(0, 16);
|
|
256
|
+
git(`commit -m "mink: sync ${timestamp}"`);
|
|
257
|
+
|
|
258
|
+
// Determine branch
|
|
259
|
+
const branch = gitSafe("rev-parse --abbrev-ref HEAD") ?? "main";
|
|
260
|
+
|
|
261
|
+
// Pull with rebase to reconcile any remote changes
|
|
262
|
+
try {
|
|
263
|
+
git(`pull --rebase origin ${branch}`, FETCH_TIMEOUT);
|
|
264
|
+
} catch {
|
|
265
|
+
// Check for rebase conflict
|
|
266
|
+
if (existsSync(join(root, ".git", "rebase-merge")) ||
|
|
267
|
+
existsSync(join(root, ".git", "rebase-apply"))) {
|
|
268
|
+
gitSafe("rebase --abort");
|
|
269
|
+
onMessage(
|
|
270
|
+
"[mink] sync: rebase conflict during push — local commit preserved, skipping push"
|
|
271
|
+
);
|
|
272
|
+
onMessage(
|
|
273
|
+
"[mink] resolve manually with 'mink sync pull' then 'mink sync push'"
|
|
274
|
+
);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Push (best-effort)
|
|
280
|
+
try {
|
|
281
|
+
git(`push origin ${branch}`, PUSH_TIMEOUT);
|
|
282
|
+
setConfigValue("sync.last-push", new Date().toISOString());
|
|
283
|
+
} catch {
|
|
284
|
+
onMessage(
|
|
285
|
+
"[mink] sync push failed — local commit preserved, will retry next session"
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
onMessage(
|
|
290
|
+
`[mink] sync push error: ${err instanceof Error ? err.message : String(err)}`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function disconnectSync(): void {
|
|
296
|
+
const root = minkRoot();
|
|
297
|
+
const gitDir = join(root, ".git");
|
|
298
|
+
|
|
299
|
+
if (!existsSync(gitDir)) {
|
|
300
|
+
console.log("[mink] sync is not initialized — nothing to disconnect");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Remove .git directory
|
|
305
|
+
const { rmSync } = require("fs");
|
|
306
|
+
rmSync(gitDir, { recursive: true, force: true });
|
|
307
|
+
|
|
308
|
+
// Clear sync config keys
|
|
309
|
+
setConfigValue("sync.enabled", "false");
|
|
310
|
+
setConfigValue("sync.remote-url", "");
|
|
311
|
+
setConfigValue("sync.last-push", "");
|
|
312
|
+
setConfigValue("sync.last-pull", "");
|
|
313
|
+
|
|
314
|
+
console.log("[mink] sync disconnected — git tracking removed, data preserved");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Internal Helpers ───────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
function detectRemoteDefaultBranch(): string {
|
|
320
|
+
// Try common default branch names
|
|
321
|
+
const remoteBranches = gitSafe("branch -r") ?? "";
|
|
322
|
+
if (remoteBranches.includes("origin/main")) return "main";
|
|
323
|
+
if (remoteBranches.includes("origin/master")) return "master";
|
|
324
|
+
|
|
325
|
+
// Fall back to first remote branch
|
|
326
|
+
const first = remoteBranches
|
|
327
|
+
.split("\n")
|
|
328
|
+
.map((b) => b.trim())
|
|
329
|
+
.filter((b) => b.startsWith("origin/") && !b.includes("HEAD"))
|
|
330
|
+
.map((b) => b.replace("origin/", ""))[0];
|
|
331
|
+
|
|
332
|
+
return first ?? "main";
|
|
333
|
+
}
|
package/src/core/vault.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { join } from "path";
|
|
1
|
+
import { join, basename, resolve } from "path";
|
|
2
2
|
import { homedir } from "os";
|
|
3
|
-
import { existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { existsSync, mkdirSync, symlinkSync, unlinkSync, lstatSync, readlinkSync } from "fs";
|
|
4
4
|
import { resolveConfigValue } from "./global-config";
|
|
5
5
|
import { safeReadJson } from "./fs-utils";
|
|
6
|
-
import
|
|
6
|
+
import { atomicWriteJson } from "./fs-utils";
|
|
7
|
+
import type { VaultManifest, VaultLink } from "../types/note";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_VAULT_PATH = join(homedir(), ".mink", "wiki");
|
|
9
10
|
|
|
@@ -130,3 +131,81 @@ export function categoryToDir(
|
|
|
130
131
|
return join(root, "inbox");
|
|
131
132
|
}
|
|
132
133
|
}
|
|
134
|
+
|
|
135
|
+
// ── Symlink management ────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function saveManifest(manifest: VaultManifest): void {
|
|
138
|
+
atomicWriteJson(vaultManifestPath(), manifest);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function linkExternal(targetPath: string, name?: string): { ok: true; linkName: string; linkPath: string } | { ok: false; error: string } {
|
|
142
|
+
const root = resolveVaultPath();
|
|
143
|
+
const absTarget = targetPath.startsWith("~/")
|
|
144
|
+
? join(homedir(), targetPath.slice(2))
|
|
145
|
+
: resolve(targetPath);
|
|
146
|
+
|
|
147
|
+
if (!existsSync(absTarget)) {
|
|
148
|
+
return { ok: false, error: `target does not exist: ${absTarget}` };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!lstatSync(absTarget).isDirectory()) {
|
|
152
|
+
return { ok: false, error: `target is not a directory: ${absTarget}` };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const linkName = name ?? basename(absTarget);
|
|
156
|
+
const linkPath = join(root, linkName);
|
|
157
|
+
|
|
158
|
+
// Don't overwrite existing vault directories
|
|
159
|
+
if (existsSync(linkPath)) {
|
|
160
|
+
if (lstatSync(linkPath).isSymbolicLink()) {
|
|
161
|
+
const existing = readlinkSync(linkPath);
|
|
162
|
+
if (existing === absTarget) {
|
|
163
|
+
return { ok: false, error: `already linked: ${linkName} -> ${absTarget}` };
|
|
164
|
+
}
|
|
165
|
+
return { ok: false, error: `a different link already exists at ${linkName} -> ${existing}` };
|
|
166
|
+
}
|
|
167
|
+
return { ok: false, error: `${linkName} already exists in the vault and is not a symlink` };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
symlinkSync(absTarget, linkPath, "dir");
|
|
171
|
+
|
|
172
|
+
// Record in manifest
|
|
173
|
+
const manifest = loadVaultManifest();
|
|
174
|
+
if (manifest) {
|
|
175
|
+
const links = manifest.links ?? [];
|
|
176
|
+
links.push({ name: linkName, target: absTarget, linkedAt: new Date().toISOString() });
|
|
177
|
+
manifest.links = links;
|
|
178
|
+
saveManifest(manifest);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { ok: true, linkName, linkPath };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function unlinkExternal(name: string): { ok: true } | { ok: false; error: string } {
|
|
185
|
+
const root = resolveVaultPath();
|
|
186
|
+
const linkPath = join(root, name);
|
|
187
|
+
|
|
188
|
+
if (!existsSync(linkPath)) {
|
|
189
|
+
return { ok: false, error: `no link named "${name}" in the vault` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!lstatSync(linkPath).isSymbolicLink()) {
|
|
193
|
+
return { ok: false, error: `"${name}" is not a symlink — refusing to remove` };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
unlinkSync(linkPath);
|
|
197
|
+
|
|
198
|
+
// Remove from manifest
|
|
199
|
+
const manifest = loadVaultManifest();
|
|
200
|
+
if (manifest && manifest.links) {
|
|
201
|
+
manifest.links = manifest.links.filter(l => l.name !== name);
|
|
202
|
+
saveManifest(manifest);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { ok: true };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function listLinks(): VaultLink[] {
|
|
209
|
+
const manifest = loadVaultManifest();
|
|
210
|
+
return manifest?.links ?? [];
|
|
211
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -5,6 +5,10 @@ export interface GlobalConfig {
|
|
|
5
5
|
"wiki.git-backup"?: string;
|
|
6
6
|
"wiki.git-remote"?: string;
|
|
7
7
|
"notes.default-category"?: string;
|
|
8
|
+
"sync.enabled"?: string;
|
|
9
|
+
"sync.remote-url"?: string;
|
|
10
|
+
"sync.last-push"?: string;
|
|
11
|
+
"sync.last-pull"?: string;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
export type ConfigKey = keyof GlobalConfig & string;
|
|
@@ -39,13 +43,13 @@ export const CONFIG_KEYS: ConfigKeyMeta[] = [
|
|
|
39
43
|
key: "wiki.git-backup",
|
|
40
44
|
default: "false",
|
|
41
45
|
envVar: "MINK_WIKI_GIT_BACKUP",
|
|
42
|
-
description: "
|
|
46
|
+
description: "Deprecated: use sync.enabled instead",
|
|
43
47
|
},
|
|
44
48
|
{
|
|
45
49
|
key: "wiki.git-remote",
|
|
46
50
|
default: "origin",
|
|
47
51
|
envVar: "MINK_WIKI_GIT_REMOTE",
|
|
48
|
-
description: "
|
|
52
|
+
description: "Deprecated: use sync.remote-url instead",
|
|
49
53
|
},
|
|
50
54
|
{
|
|
51
55
|
key: "notes.default-category",
|
|
@@ -53,6 +57,30 @@ export const CONFIG_KEYS: ConfigKeyMeta[] = [
|
|
|
53
57
|
envVar: "MINK_NOTES_DEFAULT_CATEGORY",
|
|
54
58
|
description: "Default category for notes captured via CLI",
|
|
55
59
|
},
|
|
60
|
+
{
|
|
61
|
+
key: "sync.enabled",
|
|
62
|
+
default: "false",
|
|
63
|
+
envVar: "MINK_SYNC_ENABLED",
|
|
64
|
+
description: "Enable/disable automatic git sync of ~/.mink",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "sync.remote-url",
|
|
68
|
+
default: "",
|
|
69
|
+
envVar: "MINK_SYNC_REMOTE_URL",
|
|
70
|
+
description: "Git remote URL for ~/.mink sync",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: "sync.last-push",
|
|
74
|
+
default: "",
|
|
75
|
+
envVar: "MINK_SYNC_LAST_PUSH",
|
|
76
|
+
description: "ISO timestamp of last successful sync push",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "sync.last-pull",
|
|
80
|
+
default: "",
|
|
81
|
+
envVar: "MINK_SYNC_LAST_PULL",
|
|
82
|
+
description: "ISO timestamp of last successful sync pull",
|
|
83
|
+
},
|
|
56
84
|
];
|
|
57
85
|
|
|
58
86
|
const VALID_KEYS = new Set<string>(CONFIG_KEYS.map((k) => k.key));
|
package/src/types/note.ts
CHANGED
|
@@ -35,12 +35,19 @@ export interface NoteFrontmatter {
|
|
|
35
35
|
[key: string]: unknown;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface VaultLink {
|
|
39
|
+
name: string;
|
|
40
|
+
target: string;
|
|
41
|
+
linkedAt: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
export interface VaultManifest {
|
|
39
45
|
version: number;
|
|
40
46
|
createdAt: string;
|
|
41
47
|
totalNotes: number;
|
|
42
48
|
categories: Record<NoteCategory, number>;
|
|
43
49
|
lastOrganized: string;
|
|
50
|
+
links?: VaultLink[];
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export interface VaultIndexEntry {
|