skillwiki 0.5.5 → 0.6.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 +288 -52
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
- package/skills/wiki-sync/SKILL.md +115 -16
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
} from "./chunk-TPS5XD2J.js";
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
|
11
|
-
import { readFileSync as
|
|
12
|
-
import { join as
|
|
11
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
12
|
+
import { join as join41 } from "path";
|
|
13
13
|
import { Command as Command2 } from "commander";
|
|
14
14
|
|
|
15
15
|
// ../shared/src/exit-codes.ts
|
|
@@ -61,7 +61,8 @@ var ExitCode = {
|
|
|
61
61
|
BACKUP_SYNC_FAILED: 44,
|
|
62
62
|
BACKUP_RESTORE_CONFLICTS: 45,
|
|
63
63
|
USAGE: 46,
|
|
64
|
-
BODY_TRUNCATION_GUARD: 47
|
|
64
|
+
BODY_TRUNCATION_GUARD: 47,
|
|
65
|
+
SYNC_LOCK_HELD: 48
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
// ../shared/src/json-output.ts
|
|
@@ -3469,9 +3470,9 @@ function readCacheRaw(home) {
|
|
|
3469
3470
|
function readCache(home) {
|
|
3470
3471
|
const cache = readCacheRaw(home);
|
|
3471
3472
|
if (!cache) return { cache: null, hasUpdate: false, isStale: true };
|
|
3472
|
-
const
|
|
3473
|
+
const isStale2 = Date.now() - cache.lastCheck >= CHECK_INTERVAL_MS;
|
|
3473
3474
|
const hasUpdate = !!cache.latestVersion && semverGt(cache.latestVersion, cache.currentVersion);
|
|
3474
|
-
return { cache, hasUpdate, isStale };
|
|
3475
|
+
return { cache, hasUpdate, isStale: isStale2 };
|
|
3475
3476
|
}
|
|
3476
3477
|
function writeCache(home, cache) {
|
|
3477
3478
|
const p = cachePath(home);
|
|
@@ -3491,8 +3492,8 @@ function isDisabled() {
|
|
|
3491
3492
|
}
|
|
3492
3493
|
function triggerAutoUpdate(home, currentVersion) {
|
|
3493
3494
|
if (isDisabled()) return;
|
|
3494
|
-
const { isStale } = readCache(home);
|
|
3495
|
-
if (!
|
|
3495
|
+
const { isStale: isStale2 } = readCache(home);
|
|
3496
|
+
if (!isStale2) return;
|
|
3496
3497
|
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
3497
3498
|
if (!existsSync5(bgScript)) return;
|
|
3498
3499
|
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
@@ -5983,11 +5984,105 @@ ${body}`;
|
|
|
5983
5984
|
}
|
|
5984
5985
|
|
|
5985
5986
|
// src/commands/sync.ts
|
|
5986
|
-
import { existsSync as
|
|
5987
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5988
|
+
import { join as join34 } from "path";
|
|
5989
|
+
|
|
5990
|
+
// src/utils/sync-lock.ts
|
|
5991
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
5987
5992
|
import { join as join33 } from "path";
|
|
5993
|
+
import { createHash as createHash6 } from "crypto";
|
|
5994
|
+
function getSessionId() {
|
|
5995
|
+
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
5996
|
+
if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
|
|
5997
|
+
return process.pid.toString();
|
|
5998
|
+
}
|
|
5999
|
+
function lockPath(vault) {
|
|
6000
|
+
return join33(vault, ".skillwiki", "sync.lock");
|
|
6001
|
+
}
|
|
6002
|
+
function readLock(vault) {
|
|
6003
|
+
const path = lockPath(vault);
|
|
6004
|
+
if (!existsSync11(path)) return null;
|
|
6005
|
+
try {
|
|
6006
|
+
const raw = readFileSync9(path, "utf8");
|
|
6007
|
+
return JSON.parse(raw);
|
|
6008
|
+
} catch {
|
|
6009
|
+
return null;
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
6012
|
+
function isStale(lock, now) {
|
|
6013
|
+
const nowTime = (now ?? /* @__PURE__ */ new Date()).getTime();
|
|
6014
|
+
const expiresTime = new Date(lock.expires).getTime();
|
|
6015
|
+
return expiresTime < nowTime;
|
|
6016
|
+
}
|
|
6017
|
+
function acquireLock(vault, opts = {}) {
|
|
6018
|
+
const path = lockPath(vault);
|
|
6019
|
+
const dir = join33(vault, ".skillwiki");
|
|
6020
|
+
if (!existsSync11(dir)) {
|
|
6021
|
+
mkdirSync3(dir, { recursive: true });
|
|
6022
|
+
}
|
|
6023
|
+
const sessionId = opts.sessionId ?? getSessionId();
|
|
6024
|
+
const summary = opts.summary ?? "skillwiki sync";
|
|
6025
|
+
const ttlMinutes = opts.ttlMinutes ?? 30;
|
|
6026
|
+
const force = opts.force ?? false;
|
|
6027
|
+
const now = /* @__PURE__ */ new Date();
|
|
6028
|
+
const acquired = now.toISOString();
|
|
6029
|
+
const expires = new Date(now.getTime() + ttlMinutes * 60 * 1e3).toISOString();
|
|
6030
|
+
const lock = {
|
|
6031
|
+
session_id: sessionId,
|
|
6032
|
+
pid: process.pid,
|
|
6033
|
+
cwd: process.cwd(),
|
|
6034
|
+
summary,
|
|
6035
|
+
acquired,
|
|
6036
|
+
expires
|
|
6037
|
+
};
|
|
6038
|
+
try {
|
|
6039
|
+
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
6040
|
+
writeFileSync5(path, content, { flag: "wx" });
|
|
6041
|
+
return { ok: true, lock };
|
|
6042
|
+
} catch (e) {
|
|
6043
|
+
const err3 = e;
|
|
6044
|
+
if (err3.code !== "EEXIST") throw err3;
|
|
6045
|
+
}
|
|
6046
|
+
const existing = readLock(vault);
|
|
6047
|
+
if (!existing) {
|
|
6048
|
+
writeLockedFile(path, lock);
|
|
6049
|
+
return { ok: true, lock };
|
|
6050
|
+
}
|
|
6051
|
+
if (force || isStale(existing)) {
|
|
6052
|
+
writeLockedFile(path, lock);
|
|
6053
|
+
return { ok: true, lock };
|
|
6054
|
+
}
|
|
6055
|
+
return { ok: false, held: existing };
|
|
6056
|
+
}
|
|
6057
|
+
function writeLockedFile(path, lock) {
|
|
6058
|
+
const tmp = path + ".tmp";
|
|
6059
|
+
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
6060
|
+
writeFileSync5(tmp, content);
|
|
6061
|
+
renameSync(tmp, path);
|
|
6062
|
+
}
|
|
6063
|
+
function releaseLock(vault, opts = {}) {
|
|
6064
|
+
const path = lockPath(vault);
|
|
6065
|
+
if (!existsSync11(path)) {
|
|
6066
|
+
return { released: false };
|
|
6067
|
+
}
|
|
6068
|
+
const sessionId = opts.sessionId ?? getSessionId();
|
|
6069
|
+
const existing = readLock(vault);
|
|
6070
|
+
if (!existing || existing.session_id !== sessionId) {
|
|
6071
|
+
return { released: false };
|
|
6072
|
+
}
|
|
6073
|
+
try {
|
|
6074
|
+
unlinkSync4(path);
|
|
6075
|
+
return { released: true };
|
|
6076
|
+
} catch {
|
|
6077
|
+
return { released: false };
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
|
|
6081
|
+
// src/commands/sync.ts
|
|
5988
6082
|
function runSyncStatus(input) {
|
|
5989
6083
|
const vault = input.vault;
|
|
5990
|
-
|
|
6084
|
+
const includeStashes = input.includeStashes ?? false;
|
|
6085
|
+
if (!existsSync12(join34(vault, ".git"))) {
|
|
5991
6086
|
return {
|
|
5992
6087
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
5993
6088
|
result: ok({
|
|
@@ -6041,22 +6136,30 @@ function runSyncStatus(input) {
|
|
|
6041
6136
|
`last_commit: ${last_commit}`
|
|
6042
6137
|
];
|
|
6043
6138
|
const exitCode = status === "clean" ? ExitCode.OK : ExitCode.LINT_HAS_WARNINGS;
|
|
6139
|
+
let stashes;
|
|
6140
|
+
if (includeStashes) {
|
|
6141
|
+
stashes = enumerateStashes(vault);
|
|
6142
|
+
}
|
|
6143
|
+
const output = {
|
|
6144
|
+
is_git_repo: true,
|
|
6145
|
+
dirty,
|
|
6146
|
+
ahead,
|
|
6147
|
+
behind,
|
|
6148
|
+
last_commit,
|
|
6149
|
+
status,
|
|
6150
|
+
humanHint: hintLines.join("\n")
|
|
6151
|
+
};
|
|
6152
|
+
if (stashes !== void 0) {
|
|
6153
|
+
output.stashes = stashes;
|
|
6154
|
+
}
|
|
6044
6155
|
return {
|
|
6045
6156
|
exitCode,
|
|
6046
|
-
result: ok(
|
|
6047
|
-
is_git_repo: true,
|
|
6048
|
-
dirty,
|
|
6049
|
-
ahead,
|
|
6050
|
-
behind,
|
|
6051
|
-
last_commit,
|
|
6052
|
-
status,
|
|
6053
|
-
humanHint: hintLines.join("\n")
|
|
6054
|
-
})
|
|
6157
|
+
result: ok(output)
|
|
6055
6158
|
};
|
|
6056
6159
|
}
|
|
6057
6160
|
async function runSyncPush(input) {
|
|
6058
6161
|
const vault = input.vault;
|
|
6059
|
-
if (!
|
|
6162
|
+
if (!existsSync12(join34(vault, ".git"))) {
|
|
6060
6163
|
return {
|
|
6061
6164
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6062
6165
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -6139,9 +6242,28 @@ async function runSyncPush(input) {
|
|
|
6139
6242
|
})
|
|
6140
6243
|
};
|
|
6141
6244
|
}
|
|
6245
|
+
function enumerateStashes(vault) {
|
|
6246
|
+
const output = git(vault, ["log", "--format=%gd%x09%s%x09%ct", "-g", "stash"]);
|
|
6247
|
+
if (!output) return [];
|
|
6248
|
+
const now = Date.now();
|
|
6249
|
+
const stashes = [];
|
|
6250
|
+
const lines = output.split("\n").filter((l) => l.trim().length > 0);
|
|
6251
|
+
for (const line of lines) {
|
|
6252
|
+
const parts = line.split(" ");
|
|
6253
|
+
if (parts.length < 3) continue;
|
|
6254
|
+
const ref = parts[0];
|
|
6255
|
+
const message = parts[1];
|
|
6256
|
+
const ctStr = parts[2];
|
|
6257
|
+
const ct = parseInt(ctStr, 10);
|
|
6258
|
+
if (isNaN(ct)) continue;
|
|
6259
|
+
const age_minutes = Math.floor((now - ct * 1e3) / (60 * 1e3));
|
|
6260
|
+
stashes.push({ ref, message, age_minutes });
|
|
6261
|
+
}
|
|
6262
|
+
return stashes;
|
|
6263
|
+
}
|
|
6142
6264
|
async function runSyncPull(input) {
|
|
6143
6265
|
const vault = input.vault;
|
|
6144
|
-
if (!
|
|
6266
|
+
if (!existsSync12(join34(vault, ".git"))) {
|
|
6145
6267
|
return {
|
|
6146
6268
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6147
6269
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -6214,10 +6336,106 @@ async function runSyncPull(input) {
|
|
|
6214
6336
|
})
|
|
6215
6337
|
};
|
|
6216
6338
|
}
|
|
6339
|
+
function runSyncPeers(input) {
|
|
6340
|
+
const vault = input.vault;
|
|
6341
|
+
const locks = [];
|
|
6342
|
+
const existingLock = readLock(vault);
|
|
6343
|
+
if (existingLock) {
|
|
6344
|
+
const self = existingLock.session_id === getSessionId();
|
|
6345
|
+
locks.push({ ...existingLock, is_self: self });
|
|
6346
|
+
}
|
|
6347
|
+
const allStashes = enumerateStashes(vault);
|
|
6348
|
+
const stashes = [];
|
|
6349
|
+
for (const stash of allStashes) {
|
|
6350
|
+
let actualMessage = stash.message;
|
|
6351
|
+
const prefixMatch = stash.message.match(/^On [^:]+:\s*(.*)/);
|
|
6352
|
+
if (prefixMatch) {
|
|
6353
|
+
actualMessage = prefixMatch[1];
|
|
6354
|
+
}
|
|
6355
|
+
const match = actualMessage.match(/^wiki-sync:([^:]+):([^:]+):(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z):(.*)$/);
|
|
6356
|
+
if (!match) continue;
|
|
6357
|
+
const session_id = match[1];
|
|
6358
|
+
const cwd_hash = match[2];
|
|
6359
|
+
const timestamp = match[3];
|
|
6360
|
+
const summary = match[4];
|
|
6361
|
+
stashes.push({
|
|
6362
|
+
ref: stash.ref,
|
|
6363
|
+
session_id,
|
|
6364
|
+
cwd_hash,
|
|
6365
|
+
timestamp,
|
|
6366
|
+
summary,
|
|
6367
|
+
age_minutes: stash.age_minutes
|
|
6368
|
+
});
|
|
6369
|
+
}
|
|
6370
|
+
const hintParts = [];
|
|
6371
|
+
if (locks.length > 0) hintParts.push(`${locks.length} lock(s)`);
|
|
6372
|
+
if (stashes.length > 0) hintParts.push(`${stashes.length} wiki-sync stash(es)`);
|
|
6373
|
+
const humanHint = hintParts.length > 0 ? hintParts.join(", ") : "no peers detected";
|
|
6374
|
+
return {
|
|
6375
|
+
exitCode: ExitCode.OK,
|
|
6376
|
+
result: ok({
|
|
6377
|
+
locks,
|
|
6378
|
+
stashes,
|
|
6379
|
+
humanHint
|
|
6380
|
+
})
|
|
6381
|
+
};
|
|
6382
|
+
}
|
|
6383
|
+
function runSyncLock(input) {
|
|
6384
|
+
const vault = input.vault;
|
|
6385
|
+
if (!existsSync12(vault)) {
|
|
6386
|
+
return {
|
|
6387
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6388
|
+
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
6389
|
+
};
|
|
6390
|
+
}
|
|
6391
|
+
const result = acquireLock(vault, {
|
|
6392
|
+
sessionId: input.sessionId,
|
|
6393
|
+
summary: input.summary,
|
|
6394
|
+
ttlMinutes: input.ttlMinutes,
|
|
6395
|
+
force: input.force
|
|
6396
|
+
});
|
|
6397
|
+
if (result.ok) {
|
|
6398
|
+
return {
|
|
6399
|
+
exitCode: ExitCode.OK,
|
|
6400
|
+
result: ok({
|
|
6401
|
+
acquired: true,
|
|
6402
|
+
lock: result.lock,
|
|
6403
|
+
humanHint: `lock acquired for ${result.lock.summary} (expires ${result.lock.expires})`
|
|
6404
|
+
})
|
|
6405
|
+
};
|
|
6406
|
+
} else {
|
|
6407
|
+
return {
|
|
6408
|
+
exitCode: ExitCode.SYNC_LOCK_HELD,
|
|
6409
|
+
result: ok({
|
|
6410
|
+
acquired: false,
|
|
6411
|
+
lock: result.held,
|
|
6412
|
+
held_by: result.held,
|
|
6413
|
+
humanHint: `lock held by session ${result.held.session_id} (PID ${result.held.pid}) for ${result.held.summary}`
|
|
6414
|
+
})
|
|
6415
|
+
};
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
function runSyncUnlock(input) {
|
|
6419
|
+
const vault = input.vault;
|
|
6420
|
+
if (!existsSync12(vault)) {
|
|
6421
|
+
return {
|
|
6422
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6423
|
+
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
6424
|
+
};
|
|
6425
|
+
}
|
|
6426
|
+
const result = releaseLock(vault, { sessionId: input.sessionId });
|
|
6427
|
+
return {
|
|
6428
|
+
exitCode: ExitCode.OK,
|
|
6429
|
+
result: ok({
|
|
6430
|
+
released: result.released,
|
|
6431
|
+
humanHint: result.released ? "lock released" : "lock not held by this session (no-op)"
|
|
6432
|
+
})
|
|
6433
|
+
};
|
|
6434
|
+
}
|
|
6217
6435
|
|
|
6218
6436
|
// src/commands/backup.ts
|
|
6219
|
-
import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as
|
|
6220
|
-
import { join as
|
|
6437
|
+
import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync10, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
6438
|
+
import { join as join35, relative as relative3, dirname as dirname11 } from "path";
|
|
6221
6439
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
6222
6440
|
|
|
6223
6441
|
// src/utils/s3-client.ts
|
|
@@ -6241,7 +6459,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
|
|
|
6241
6459
|
function* walkMarkdown(dir, base) {
|
|
6242
6460
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
6243
6461
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
6244
|
-
const full =
|
|
6462
|
+
const full = join35(dir, entry.name);
|
|
6245
6463
|
if (entry.isDirectory()) {
|
|
6246
6464
|
yield* walkMarkdown(full, base);
|
|
6247
6465
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -6264,7 +6482,7 @@ async function runBackupSync(input) {
|
|
|
6264
6482
|
let failed = 0;
|
|
6265
6483
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
6266
6484
|
for (const relPath of files) {
|
|
6267
|
-
const absPath =
|
|
6485
|
+
const absPath = join35(input.vault, relPath);
|
|
6268
6486
|
const localStat = statSync4(absPath);
|
|
6269
6487
|
let needsUpload = true;
|
|
6270
6488
|
try {
|
|
@@ -6283,7 +6501,7 @@ async function runBackupSync(input) {
|
|
|
6283
6501
|
continue;
|
|
6284
6502
|
}
|
|
6285
6503
|
try {
|
|
6286
|
-
const body =
|
|
6504
|
+
const body = readFileSync10(absPath);
|
|
6287
6505
|
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
6288
6506
|
uploaded++;
|
|
6289
6507
|
} catch {
|
|
@@ -6340,7 +6558,7 @@ async function runBackupRestore(input) {
|
|
|
6340
6558
|
const objects = list.Contents ?? [];
|
|
6341
6559
|
for (const obj of objects) {
|
|
6342
6560
|
if (!obj.Key) continue;
|
|
6343
|
-
const localPath =
|
|
6561
|
+
const localPath = join35(target, obj.Key);
|
|
6344
6562
|
try {
|
|
6345
6563
|
const localStat = statSync4(localPath);
|
|
6346
6564
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
@@ -6353,8 +6571,8 @@ async function runBackupRestore(input) {
|
|
|
6353
6571
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
6354
6572
|
const body = await resp.Body?.transformToByteArray();
|
|
6355
6573
|
if (body) {
|
|
6356
|
-
|
|
6357
|
-
|
|
6574
|
+
mkdirSync4(dirname11(localPath), { recursive: true });
|
|
6575
|
+
writeFileSync6(localPath, Buffer.from(body));
|
|
6358
6576
|
downloaded++;
|
|
6359
6577
|
}
|
|
6360
6578
|
} catch {
|
|
@@ -6386,11 +6604,11 @@ async function runBackupRestore(input) {
|
|
|
6386
6604
|
}
|
|
6387
6605
|
|
|
6388
6606
|
// src/commands/status.ts
|
|
6389
|
-
import { existsSync as
|
|
6607
|
+
import { existsSync as existsSync13, statSync as statSync5 } from "fs";
|
|
6390
6608
|
import { readFile as readFile23 } from "fs/promises";
|
|
6391
|
-
import { join as
|
|
6609
|
+
import { join as join36 } from "path";
|
|
6392
6610
|
async function runStatus(input) {
|
|
6393
|
-
if (!
|
|
6611
|
+
if (!existsSync13(input.vault)) {
|
|
6394
6612
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
6395
6613
|
}
|
|
6396
6614
|
const scan = await scanVault(input.vault);
|
|
@@ -6415,7 +6633,7 @@ async function runStatus(input) {
|
|
|
6415
6633
|
const compound = scan.data.compound.length;
|
|
6416
6634
|
let schemaVersion = "v1";
|
|
6417
6635
|
try {
|
|
6418
|
-
const schemaContent = await readFile23(
|
|
6636
|
+
const schemaContent = await readFile23(join36(input.vault, "SCHEMA.md"), "utf8");
|
|
6419
6637
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
6420
6638
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
6421
6639
|
} catch {
|
|
@@ -6476,7 +6694,7 @@ async function runStatus(input) {
|
|
|
6476
6694
|
|
|
6477
6695
|
// src/commands/seed.ts
|
|
6478
6696
|
import { mkdir as mkdir13, writeFile as writeFile14, stat as stat7 } from "fs/promises";
|
|
6479
|
-
import { join as
|
|
6697
|
+
import { join as join37 } from "path";
|
|
6480
6698
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6481
6699
|
var EXAMPLE_PAGES = {
|
|
6482
6700
|
"entities/example-project.md": `---
|
|
@@ -6545,29 +6763,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
6545
6763
|
`;
|
|
6546
6764
|
async function runSeed(input) {
|
|
6547
6765
|
try {
|
|
6548
|
-
await stat7(
|
|
6766
|
+
await stat7(join37(input.vault, "SCHEMA.md"));
|
|
6549
6767
|
} catch {
|
|
6550
6768
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
6551
6769
|
}
|
|
6552
6770
|
const created = [];
|
|
6553
6771
|
const skipped = [];
|
|
6554
6772
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
6555
|
-
const absPath =
|
|
6773
|
+
const absPath = join37(input.vault, relPath);
|
|
6556
6774
|
try {
|
|
6557
6775
|
await stat7(absPath);
|
|
6558
6776
|
skipped.push(relPath);
|
|
6559
6777
|
} catch {
|
|
6560
|
-
await mkdir13(
|
|
6778
|
+
await mkdir13(join37(absPath, ".."), { recursive: true });
|
|
6561
6779
|
await writeFile14(absPath, content, "utf8");
|
|
6562
6780
|
created.push(relPath);
|
|
6563
6781
|
}
|
|
6564
6782
|
}
|
|
6565
|
-
const rawPath =
|
|
6783
|
+
const rawPath = join37(input.vault, "raw", "articles", "example-source.md");
|
|
6566
6784
|
try {
|
|
6567
6785
|
await stat7(rawPath);
|
|
6568
6786
|
skipped.push("raw/articles/example-source.md");
|
|
6569
6787
|
} catch {
|
|
6570
|
-
await mkdir13(
|
|
6788
|
+
await mkdir13(join37(rawPath, ".."), { recursive: true });
|
|
6571
6789
|
await writeFile14(rawPath, EXAMPLE_RAW, "utf8");
|
|
6572
6790
|
created.push("raw/articles/example-source.md");
|
|
6573
6791
|
}
|
|
@@ -6591,8 +6809,8 @@ async function runSeed(input) {
|
|
|
6591
6809
|
|
|
6592
6810
|
// src/commands/canvas.ts
|
|
6593
6811
|
import { readFile as readFile24, writeFile as writeFile15 } from "fs/promises";
|
|
6594
|
-
import { existsSync as
|
|
6595
|
-
import { join as
|
|
6812
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6813
|
+
import { join as join38 } from "path";
|
|
6596
6814
|
var NODE_WIDTH = 240;
|
|
6597
6815
|
var NODE_HEIGHT = 60;
|
|
6598
6816
|
var COLUMN_SPACING = 400;
|
|
@@ -6670,8 +6888,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
6670
6888
|
return edges;
|
|
6671
6889
|
}
|
|
6672
6890
|
async function runCanvasGenerate(input) {
|
|
6673
|
-
const graphPath = input.graphPath ??
|
|
6674
|
-
if (!
|
|
6891
|
+
const graphPath = input.graphPath ?? join38(input.vault, ".skillwiki", "graph.json");
|
|
6892
|
+
if (!existsSync14(graphPath)) {
|
|
6675
6893
|
return {
|
|
6676
6894
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
6677
6895
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -6708,7 +6926,7 @@ async function runCanvasGenerate(input) {
|
|
|
6708
6926
|
const nodes = buildCanvasNodes(paths);
|
|
6709
6927
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
6710
6928
|
const canvas = { nodes, edges };
|
|
6711
|
-
const outPath =
|
|
6929
|
+
const outPath = join38(input.vault, "vault-graph.canvas");
|
|
6712
6930
|
try {
|
|
6713
6931
|
await writeFile15(outPath, JSON.stringify(canvas, null, 2));
|
|
6714
6932
|
} catch (e) {
|
|
@@ -6731,7 +6949,7 @@ written: ${outPath}`
|
|
|
6731
6949
|
|
|
6732
6950
|
// src/commands/query.ts
|
|
6733
6951
|
import { readFile as readFile25, stat as stat8 } from "fs/promises";
|
|
6734
|
-
import { join as
|
|
6952
|
+
import { join as join39 } from "path";
|
|
6735
6953
|
var W_KEYWORD = 2;
|
|
6736
6954
|
var W_SOURCE_OVERLAP = 4;
|
|
6737
6955
|
var W_WIKILINK = 3;
|
|
@@ -6852,7 +7070,7 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
6852
7070
|
return score;
|
|
6853
7071
|
}
|
|
6854
7072
|
async function loadOrBuildGraph(vault) {
|
|
6855
|
-
const graphPath =
|
|
7073
|
+
const graphPath = join39(vault, ".skillwiki", "graph.json");
|
|
6856
7074
|
let needsBuild = false;
|
|
6857
7075
|
try {
|
|
6858
7076
|
const fileStat = await stat8(graphPath);
|
|
@@ -6874,14 +7092,14 @@ async function loadOrBuildGraph(vault) {
|
|
|
6874
7092
|
}
|
|
6875
7093
|
|
|
6876
7094
|
// src/utils/auto-commit.ts
|
|
6877
|
-
import { existsSync as
|
|
6878
|
-
import { join as
|
|
7095
|
+
import { existsSync as existsSync15 } from "fs";
|
|
7096
|
+
import { join as join40 } from "path";
|
|
6879
7097
|
async function postCommit(vault, exitCode) {
|
|
6880
7098
|
if (exitCode !== 0) return;
|
|
6881
7099
|
const home = process.env.HOME ?? "";
|
|
6882
7100
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
6883
7101
|
if (dotenv["AUTO_COMMIT"] === "false") return;
|
|
6884
|
-
if (!
|
|
7102
|
+
if (!existsSync15(join40(vault, ".git"))) return;
|
|
6885
7103
|
const lastOps = readLastOp(vault);
|
|
6886
7104
|
if (lastOps.length === 0) return;
|
|
6887
7105
|
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
@@ -6910,7 +7128,7 @@ async function postCommit(vault, exitCode) {
|
|
|
6910
7128
|
}
|
|
6911
7129
|
|
|
6912
7130
|
// src/cli.ts
|
|
6913
|
-
var pkg = JSON.parse(
|
|
7131
|
+
var pkg = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf8"));
|
|
6914
7132
|
var program = new Command2();
|
|
6915
7133
|
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
6916
7134
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
@@ -6932,7 +7150,7 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
6932
7150
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
6933
7151
|
});
|
|
6934
7152
|
program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6935
|
-
const out = opts.out ??
|
|
7153
|
+
const out = opts.out ?? join41(vault, ".skillwiki", "graph.json");
|
|
6936
7154
|
emit(await runGraphBuild({ vault, out }), vault);
|
|
6937
7155
|
});
|
|
6938
7156
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -7157,10 +7375,10 @@ program.command("tag-sync [vault]").description("mirror frontmatter enum values
|
|
|
7157
7375
|
else emit(await runTagSync({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
|
|
7158
7376
|
});
|
|
7159
7377
|
var syncCmd = program.command("sync").description("manage vault sync");
|
|
7160
|
-
syncCmd.command("status [vault]").description("check vault git sync status").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7378
|
+
syncCmd.command("status [vault]").description("check vault git sync status").option("--wiki <name>", "wiki profile name").option("--include-stashes", "enumerate all stashes in output", false).action(async (vault, opts) => {
|
|
7161
7379
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7162
7380
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7163
|
-
else emit(runSyncStatus({ vault: v.vault }));
|
|
7381
|
+
else emit(runSyncStatus({ vault: v.vault, includeStashes: !!opts.includeStashes }));
|
|
7164
7382
|
});
|
|
7165
7383
|
syncCmd.command("push [vault]").description("lint, commit, and push vault changes to remote").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7166
7384
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
@@ -7172,6 +7390,24 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
|
|
|
7172
7390
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7173
7391
|
else emit(await runSyncPull({ vault: v.vault }), v.vault);
|
|
7174
7392
|
});
|
|
7393
|
+
syncCmd.command("lock [vault]").description("acquire advisory lock on vault").option("--summary <text>", "lock description", "skillwiki sync").option("--ttl-minutes <n>", "lock time-to-live in minutes", "30").option("--force", "overwrite existing lock", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7394
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7395
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7396
|
+
else {
|
|
7397
|
+
const ttl = parseInt(opts.ttlMinutes, 10) || 30;
|
|
7398
|
+
emit(runSyncLock({ vault: v.vault, summary: opts.summary, ttlMinutes: ttl, force: !!opts.force }));
|
|
7399
|
+
}
|
|
7400
|
+
});
|
|
7401
|
+
syncCmd.command("unlock [vault]").description("release advisory lock on vault").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7402
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7403
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7404
|
+
else emit(runSyncUnlock({ vault: v.vault }));
|
|
7405
|
+
});
|
|
7406
|
+
syncCmd.command("peers [vault]").description("list active locks and recent wiki-sync stashes").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7407
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7408
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7409
|
+
else emit(runSyncPeers({ vault: v.vault }));
|
|
7410
|
+
});
|
|
7175
7411
|
var backupCmd = program.command("backup").description("manage S3-compatible remote backup");
|
|
7176
7412
|
backupCmd.command("sync [vault]").description("sync vault to S3-compatible remote backup").option("--dry-run", "list actions without executing").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--prune", "delete orphaned S3 objects not in vault", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7177
7413
|
const v = await resolveVaultArg(vault, opts.wiki);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"skills": "./",
|
|
5
5
|
"description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
|
|
6
6
|
"author": {
|
package/skills/package.json
CHANGED
|
@@ -1,47 +1,124 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: 0.
|
|
2
|
+
version: 0.3.0
|
|
3
3
|
name: wiki-sync
|
|
4
|
-
description: Safely sync the vault git repository. Runs skillwiki sync status, then guides push or pull with lint guards and conflict resolution.
|
|
4
|
+
description: Safely sync the vault git repository — multi-session safe via advisory lockfile. Runs skillwiki sync status, then guides push or pull with lint guards and conflict resolution.
|
|
5
5
|
---
|
|
6
6
|
# wiki-sync
|
|
7
7
|
## When This Skill Activates
|
|
8
8
|
- User wants to push local vault changes to the remote.
|
|
9
9
|
- User wants to pull remote changes into their local vault.
|
|
10
10
|
- User asks about vault sync status, git state, or multi-device coordination.
|
|
11
|
+
- Multiple Claude Code sessions targeting the same vault.
|
|
11
12
|
- Periodic maintenance before or after editing sessions.
|
|
12
13
|
## Pre-orientation reads
|
|
13
14
|
Standard four reads.
|
|
14
15
|
## Steps
|
|
15
16
|
0. Resolve vault: `skillwiki path` (record source for context).
|
|
17
|
+
|
|
18
|
+
## Pre-flight peer check (multi-session safe)
|
|
19
|
+
|
|
20
|
+
**Before any git stash or pull/push operation**, check for peer sessions:
|
|
21
|
+
|
|
22
|
+
1. Run `skillwiki sync peers <vault>` to detect other sessions with active locks or recent `wiki-sync:*` stashes.
|
|
23
|
+
2. If any non-self peer is present (locked or has stashes newer than 5 minutes):
|
|
24
|
+
- Surface the peer's session_id, PID, and summary to the user
|
|
25
|
+
- Ask the user to wait for the peer to finish, or pass `--force` to proceed anyway
|
|
26
|
+
- If `--force` is not given and peer is detected, **abort and exit**
|
|
27
|
+
3. Acquire an advisory lock: `skillwiki sync lock <vault> --summary "wiki-sync <op>"` (where `<op>` is "pull" or "push")
|
|
28
|
+
- If lock is held (exit code 48), surface the holder (session_id, PID, summary) and abort
|
|
29
|
+
4. **Always pair with unlock on exit** (success or error):
|
|
30
|
+
- `skillwiki sync unlock <vault>` in a finally block or error handler
|
|
31
|
+
|
|
32
|
+
### Stash backlog warning
|
|
33
|
+
|
|
34
|
+
On every invocation, count `wiki-sync:*` stashes older than 24 hours via `skillwiki sync peers`:
|
|
35
|
+
- If any old stashes exist, warn the user: "Found N wiki-sync stash(es) older than 24h — audit and clean before proceeding"
|
|
36
|
+
- **Do not auto-drop old stashes** — the user audits each one
|
|
37
|
+
|
|
38
|
+
## Sync workflow
|
|
39
|
+
|
|
16
40
|
1. Run `skillwiki sync status <vault>`. Read the JSON output.
|
|
17
|
-
- Exit code 0: vault is clean (nothing to sync).
|
|
18
|
-
- Exit code 22: warnings — dirty/ahead/behind (needs action).
|
|
41
|
+
- Exit code 0: vault is clean (nothing to sync).
|
|
42
|
+
- Exit code 22: warnings — dirty/ahead/behind (needs action).
|
|
19
43
|
2. Present the current state: `status`, `dirty`, `ahead`, `behind`, `last_commit`.
|
|
20
44
|
3. Ask the user which operation they want: **push**, **pull**, or **both** (pull then push).
|
|
45
|
+
|
|
21
46
|
### Push workflow
|
|
22
47
|
4. If vault is dirty, ask the user to review uncommitted changes before proceeding.
|
|
23
48
|
5. Run `skillwiki lint <vault>`. If errors exist, stop and report — do not push lint errors to remote.
|
|
24
49
|
6. If lint passes (errors = 0), stage and commit:
|
|
25
|
-
- `git -C <vault> add -A`
|
|
26
|
-
- `git -C <vault> commit -m "sync: vault update $(date -u +%Y-%m-%dT%H:%MZ)"`
|
|
50
|
+
- `git -C <vault> add -A`
|
|
51
|
+
- `git -C <vault> commit -m "sync: vault update $(date -u +%Y-%m-%dT%H:%MZ)"`
|
|
27
52
|
7. Run `git -C <vault> push origin HEAD`. Report result.
|
|
28
53
|
8. Append one `log.md` entry summarizing: files pushed, lint result, commit hash.
|
|
54
|
+
|
|
29
55
|
### Pull workflow
|
|
30
|
-
9.
|
|
31
|
-
10.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
9. Run `skillwiki sync status <vault> --include-stashes` to check for untracked file collisions (see Untracked file fingerprint below).
|
|
57
|
+
10. If vault is dirty, stash first with the identifiable name format:
|
|
58
|
+
```bash
|
|
59
|
+
VAULT="<vault>"
|
|
60
|
+
SESSION_ID="$(echo $CLAUDE_SESSION_ID)" # or fallback to PID/hostname
|
|
61
|
+
CWD_HASH="$(echo -n "$VAULT" | sha256sum | cut -c1-8)"
|
|
62
|
+
ISO_TS="$(date -u +%Y-%m-%dT%H:%MZ)"
|
|
63
|
+
MSG="wiki-sync:${SESSION_ID}:${CWD_HASH}:${ISO_TS}:pre-pull"
|
|
64
|
+
git -C "$VAULT" stash push -m "$MSG"
|
|
65
|
+
```
|
|
66
|
+
11. Run `git -C <vault> pull --rebase origin HEAD`. Report result.
|
|
67
|
+
12. If a stash was created, pop it: `git -C <vault> stash pop`.
|
|
68
|
+
13. If conflicts occur during stash pop, identify them and present to the user for resolution (see Conflict Resolution below).
|
|
69
|
+
14. Run `skillwiki lint <vault>` after pull to verify vault integrity.
|
|
70
|
+
15. Append one `log.md` entry summarizing: commits pulled, lint result, any conflicts.
|
|
71
|
+
|
|
36
72
|
### Pull-then-push workflow
|
|
37
|
-
|
|
38
|
-
|
|
73
|
+
16. Execute the pull workflow (steps 9-14) first.
|
|
74
|
+
17. Then execute the push workflow (steps 4-8).
|
|
75
|
+
|
|
76
|
+
## Stash naming convention
|
|
77
|
+
|
|
78
|
+
When `wiki-sync` creates a stash, use the identifiable message format:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
wiki-sync:{session_id}:{cwd_hash}:{iso8601_timestamp}:{summary}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- **session_id**: prefer `$CLAUDE_SESSION_ID` env var if set, else `$$` (shell PID), else `unknown`
|
|
85
|
+
- **cwd_hash**: first 8 chars of sha256(`$VAULT` path)
|
|
86
|
+
- **iso8601_timestamp**: e.g., `2026-05-23T03:25:00Z` (UTC)
|
|
87
|
+
- **summary**: short label like `pre-pull`, `pre-push`, or custom reason
|
|
88
|
+
|
|
89
|
+
This allows any session to list `git stash list` and identify which stash came from which session/working directory.
|
|
90
|
+
|
|
91
|
+
## Untracked file fingerprint (pre-pull)
|
|
92
|
+
|
|
93
|
+
Before `git pull --rebase`, check for untracked files that exist on the remote and may collide:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
for f in $(git -C "$VAULT" ls-files --others --exclude-standard); do
|
|
97
|
+
if git -C "$VAULT" cat-file -e "origin/main:$f" 2>/dev/null; then
|
|
98
|
+
# File exists on remote; check if identical
|
|
99
|
+
if diff -q <(git -C "$VAULT" show "origin/main:$f") "$VAULT/$f" >/dev/null 2>&1; then
|
|
100
|
+
# Byte-identical — safe to remove (presync artifact)
|
|
101
|
+
rm "$VAULT/$f"
|
|
102
|
+
else
|
|
103
|
+
# DIFFERENT — surface to user, DO NOT silently --include-untracked
|
|
104
|
+
echo "UNTRACKED COLLISION: $f differs from origin/main — surface to user for resolution"
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
# If file does not exist on remote, leave it alone (pull won't touch it)
|
|
108
|
+
done
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If collisions are found (different content), ask the user to resolve manually before pulling.
|
|
112
|
+
|
|
39
113
|
## Conflict Resolution
|
|
114
|
+
|
|
40
115
|
When merge conflicts are detected:
|
|
41
|
-
|
|
116
|
+
|
|
117
|
+
### Frontmatter conflicts
|
|
42
118
|
- For `updated:` fields: always take the newer timestamp (compare both sides, keep the later one).
|
|
43
119
|
- For all other frontmatter fields: present both versions to the user and ask which to keep.
|
|
44
|
-
|
|
120
|
+
|
|
121
|
+
### Body conflicts
|
|
45
122
|
- Do not auto-resolve body conflicts.
|
|
46
123
|
- Mark unresolved regions with `???` on a line by itself between the conflicting versions, so the user can see both sides and decide.
|
|
47
124
|
- Example:
|
|
@@ -51,12 +128,28 @@ Content from local version
|
|
|
51
128
|
Content from remote version
|
|
52
129
|
```
|
|
53
130
|
- After resolving conflicts, run `skillwiki lint <vault>` to verify before committing.
|
|
131
|
+
|
|
132
|
+
### Modify/delete conflicts
|
|
133
|
+
|
|
134
|
+
When `git pull --rebase` reports `CONFLICT (modify/delete)`:
|
|
135
|
+
|
|
136
|
+
1. Identify the commit that deleted the file:
|
|
137
|
+
```bash
|
|
138
|
+
git -C "$VAULT" log --diff-filter=D --pretty=oneline -- <path>
|
|
139
|
+
```
|
|
140
|
+
2. Read the commit message and any retro / log entry referencing it to determine if the deletion was intentional or accidental.
|
|
141
|
+
3. Decide:
|
|
142
|
+
- `git -C "$VAULT" rm <path>` — accept the deletion (rebase continues)
|
|
143
|
+
- `git -C "$VAULT" add <path>` — keep the local restoration (rebase continues)
|
|
144
|
+
4. `git -C "$VAULT" rebase --continue`.
|
|
145
|
+
|
|
54
146
|
## Multi-device coordination
|
|
55
147
|
When the user mentions editing from Obsidian desktop and Claude Code on a server (or any two-device setup):
|
|
56
148
|
- Recommend pulling before every editing session on each device.
|
|
57
149
|
- Recommend pushing after every editing session on each device.
|
|
58
150
|
- If both devices edit the same page between syncs, conflicts are inevitable — the Conflict Resolution section handles this.
|
|
59
151
|
- Suggest enabling auto-commit in Obsidian (Community Plugins: `obsidian-git`) to reduce dirty-state drift.
|
|
152
|
+
|
|
60
153
|
## Rclone-backed vault with git snapshotting (cron pattern)
|
|
61
154
|
Some deployments use a cloud-backed vault (`rclone mount`) with a separate git repository for versioned snapshots. This pattern separates "live working vault" from "versioned backup".
|
|
62
155
|
### Architecture
|
|
@@ -105,12 +198,18 @@ bash ~/.hermes/scripts/wiki-snapshot.sh # Re-sync fresh
|
|
|
105
198
|
2. **Slow rsync on rclone mounts**: The rclone FUSE mount can be slow for large directory listings. Use `rsync -q` (quiet) to reduce output overhead, and consider `--delete-delay` instead of `--delete` if file churn is high. The rclone mount latency can cause `du` and `find` operations to timeout — this is normal, not an error.
|
|
106
199
|
3. **Golden Rule violation**: Never mix sync methods on the same vault. If using rclone mount + git snapshotting, do NOT also enable Obsidian Sync, Syncthing, or iCloud on `~/wiki`. The rclone mount IS the sync mechanism.
|
|
107
200
|
4. **Credential exposure**: The rclone mount and git remote use different credentials. Ensure git credentials are cached or use HTTPS with token, but never commit rclone config to git.
|
|
201
|
+
|
|
108
202
|
## Stop conditions
|
|
109
203
|
- `skillwiki sync status` reports `not_a_repo` — the vault is not a git repository. Advise the user to initialize one.
|
|
110
204
|
- Lint errors are found before a push — do not push until resolved.
|
|
111
205
|
- `git push` or `git pull` fails with a network error — report and stop.
|
|
206
|
+
- Peer lock is held or peer stashes exist — abort and ask the user to wait or pass `--force`.
|
|
207
|
+
- Untracked file collision detected on pull — surface to user for manual resolution.
|
|
208
|
+
|
|
112
209
|
## Forbidden
|
|
113
210
|
- Pushing when lint errors exist.
|
|
114
211
|
- Auto-resolving body conflicts without user review.
|
|
115
212
|
- Force-pushing (`git push --force`).
|
|
116
213
|
- Modifying files in `raw/` to resolve conflicts (N9 — archive and re-ingest instead).
|
|
214
|
+
- Stashing without the `wiki-sync:...` name format (breaks peer detection).
|
|
215
|
+
- Force-deleting a peer's lockfile (use `--force` only if peer is confirmed dead).
|