tokrepo 3.0.0 → 3.1.1
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/bin/tokrepo.js +94 -37
- package/package.json +1 -1
package/bin/tokrepo.js
CHANGED
|
@@ -22,9 +22,10 @@ const C = {
|
|
|
22
22
|
|
|
23
23
|
const CONFIG_DIR = path.join(require('os').homedir(), '.tokrepo');
|
|
24
24
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
25
|
+
const SYNC_STATE_FILE = path.join(CONFIG_DIR, 'sync-state.json');
|
|
25
26
|
const PROJECT_CONFIG = '.tokrepo.json';
|
|
26
27
|
const DEFAULT_API = 'https://api.tokrepo.com';
|
|
27
|
-
const CLI_VERSION = '3.
|
|
28
|
+
const CLI_VERSION = '3.1.1';
|
|
28
29
|
const VERSION_CHECK_FILE = path.join(require('os').homedir(), '.tokrepo', '.version-check');
|
|
29
30
|
|
|
30
31
|
// ─── Helpers ───
|
|
@@ -105,6 +106,15 @@ function compareVersions(a, b) {
|
|
|
105
106
|
return 0;
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// Sync state: maps "dirPath:title" → { uuid, hash, lastSync }
|
|
110
|
+
function readSyncState() {
|
|
111
|
+
try { return JSON.parse(fs.readFileSync(SYNC_STATE_FILE, 'utf8')); } catch { return {}; }
|
|
112
|
+
}
|
|
113
|
+
function writeSyncState(state) {
|
|
114
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
115
|
+
fs.writeFileSync(SYNC_STATE_FILE, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
function readProjectConfig(baseDir = process.cwd()) {
|
|
109
119
|
const configPath = path.join(baseDir, PROJECT_CONFIG);
|
|
110
120
|
try {
|
|
@@ -955,7 +965,9 @@ function scanDirectory(dirPath) {
|
|
|
955
965
|
const files = collectAssetFiles(fullPath);
|
|
956
966
|
if (files.length === 0) continue;
|
|
957
967
|
|
|
958
|
-
|
|
968
|
+
// Use directory name as title (matches what `push` uses for remote title)
|
|
969
|
+
// NOT the markdown heading, which can be completely different
|
|
970
|
+
const title = entry.name;
|
|
959
971
|
const hash = computeHash(files);
|
|
960
972
|
const detectedTags = new Set();
|
|
961
973
|
for (const f of files) {
|
|
@@ -1063,35 +1075,54 @@ async function cmdSync() {
|
|
|
1063
1075
|
|
|
1064
1076
|
log(` Found ${C.bold}${assets.length}${C.reset} assets\n`);
|
|
1065
1077
|
|
|
1066
|
-
//
|
|
1067
|
-
|
|
1068
|
-
const diffPayload = assets.map(a => ({ title: a.title, content_hash: a.hash }));
|
|
1078
|
+
// Load local sync state for fast local-only diff
|
|
1079
|
+
const syncState = readSyncState();
|
|
1069
1080
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1081
|
+
// Classify each asset: check local state first, then remote diff for unknowns
|
|
1082
|
+
const needsRemoteCheck = [];
|
|
1083
|
+
const localStatus = {};
|
|
1084
|
+
|
|
1085
|
+
for (const asset of assets) {
|
|
1086
|
+
const key = asset.sourcePath;
|
|
1087
|
+
const cached = syncState[key];
|
|
1088
|
+
if (cached && cached.hash === asset.hash) {
|
|
1089
|
+
// Local hash matches last sync — unchanged
|
|
1090
|
+
localStatus[asset.title] = { status: 'unchanged', uuid: cached.uuid };
|
|
1091
|
+
} else if (cached && cached.hash !== asset.hash) {
|
|
1092
|
+
// Local hash differs from last sync — updated
|
|
1093
|
+
localStatus[asset.title] = { status: 'updated', uuid: cached.uuid };
|
|
1094
|
+
} else {
|
|
1095
|
+
// Not in local state — check remote
|
|
1096
|
+
needsRemoteCheck.push(asset);
|
|
1097
|
+
}
|
|
1078
1098
|
}
|
|
1079
1099
|
|
|
1080
|
-
//
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1100
|
+
// Remote diff for assets not in local state
|
|
1101
|
+
if (needsRemoteCheck.length > 0) {
|
|
1102
|
+
info('Comparing with remote...');
|
|
1103
|
+
const diffPayload = needsRemoteCheck.map(a => ({ title: a.title, content_hash: a.hash }));
|
|
1104
|
+
try {
|
|
1105
|
+
const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
|
|
1106
|
+
for (const r of data.results) {
|
|
1107
|
+
localStatus[r.title] = { status: r.status, uuid: r.remote_uuid || '' };
|
|
1108
|
+
}
|
|
1109
|
+
} catch (e) {
|
|
1110
|
+
warn(`Diff API: ${e.message} — treating unknowns as new`);
|
|
1111
|
+
for (const a of needsRemoteCheck) {
|
|
1112
|
+
localStatus[a.title] = { status: 'new', uuid: '' };
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1084
1115
|
}
|
|
1085
1116
|
|
|
1086
1117
|
// Show status
|
|
1087
1118
|
let newCount = 0, updatedCount = 0, unchangedCount = 0;
|
|
1088
1119
|
|
|
1089
1120
|
for (const asset of assets) {
|
|
1090
|
-
const
|
|
1091
|
-
if (status === 'new') {
|
|
1121
|
+
const st = localStatus[asset.title] || { status: 'new' };
|
|
1122
|
+
if (st.status === 'new') {
|
|
1092
1123
|
log(` ${C.green}+ new${C.reset} ${asset.title} ${C.dim}(${asset.files.length} files)${C.reset}`);
|
|
1093
1124
|
newCount++;
|
|
1094
|
-
} else if (status === 'updated') {
|
|
1125
|
+
} else if (st.status === 'updated') {
|
|
1095
1126
|
log(` ${C.yellow}~ updated${C.reset} ${asset.title}`);
|
|
1096
1127
|
updatedCount++;
|
|
1097
1128
|
} else {
|
|
@@ -1117,13 +1148,13 @@ async function cmdSync() {
|
|
|
1117
1148
|
if (confirm.toLowerCase() !== 'y') { log('Aborted.'); return; }
|
|
1118
1149
|
}
|
|
1119
1150
|
|
|
1120
|
-
//
|
|
1151
|
+
// Upsert each changed asset + save state
|
|
1121
1152
|
let successCount = 0;
|
|
1122
1153
|
let failCount = 0;
|
|
1123
1154
|
|
|
1124
1155
|
for (const asset of assets) {
|
|
1125
|
-
const
|
|
1126
|
-
if (status === 'unchanged') continue;
|
|
1156
|
+
const st = localStatus[asset.title] || { status: 'new' };
|
|
1157
|
+
if (st.status === 'unchanged') continue;
|
|
1127
1158
|
|
|
1128
1159
|
const totalChars = asset.files.reduce((sum, f) => sum + f.content.length, 0);
|
|
1129
1160
|
|
|
@@ -1138,6 +1169,15 @@ async function cmdSync() {
|
|
|
1138
1169
|
|
|
1139
1170
|
const action = data.action === 'created' ? C.green + '+ created' : C.yellow + '~ updated';
|
|
1140
1171
|
log(` ${action}${C.reset} ${asset.title} ${C.dim}${data.url}${C.reset}`);
|
|
1172
|
+
|
|
1173
|
+
// Save to local sync state
|
|
1174
|
+
syncState[asset.sourcePath] = {
|
|
1175
|
+
uuid: data.uuid,
|
|
1176
|
+
hash: asset.hash,
|
|
1177
|
+
title: asset.title,
|
|
1178
|
+
url: data.url,
|
|
1179
|
+
lastSync: new Date().toISOString(),
|
|
1180
|
+
};
|
|
1141
1181
|
successCount++;
|
|
1142
1182
|
} catch (e) {
|
|
1143
1183
|
log(` ${C.red}✗ failed${C.reset} ${asset.title}: ${e.message}`);
|
|
@@ -1145,6 +1185,9 @@ async function cmdSync() {
|
|
|
1145
1185
|
}
|
|
1146
1186
|
}
|
|
1147
1187
|
|
|
1188
|
+
// Persist sync state
|
|
1189
|
+
writeSyncState(syncState);
|
|
1190
|
+
|
|
1148
1191
|
log('');
|
|
1149
1192
|
if (failCount === 0) {
|
|
1150
1193
|
success(`Synced ${successCount} assets!`);
|
|
@@ -1176,28 +1219,42 @@ async function cmdStatus() {
|
|
|
1176
1219
|
return;
|
|
1177
1220
|
}
|
|
1178
1221
|
|
|
1179
|
-
//
|
|
1180
|
-
|
|
1181
|
-
const
|
|
1222
|
+
// Check local sync state first, remote for unknowns
|
|
1223
|
+
const syncState = readSyncState();
|
|
1224
|
+
const localStatus = {};
|
|
1225
|
+
const needsRemoteCheck = [];
|
|
1182
1226
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1227
|
+
for (const asset of assets) {
|
|
1228
|
+
const cached = syncState[asset.sourcePath];
|
|
1229
|
+
if (cached && cached.hash === asset.hash) {
|
|
1230
|
+
localStatus[asset.title] = { status: 'unchanged', uuid: cached.uuid };
|
|
1231
|
+
} else if (cached && cached.hash !== asset.hash) {
|
|
1232
|
+
localStatus[asset.title] = { status: 'updated', uuid: cached.uuid };
|
|
1233
|
+
} else {
|
|
1234
|
+
needsRemoteCheck.push(asset);
|
|
1235
|
+
}
|
|
1189
1236
|
}
|
|
1190
1237
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1238
|
+
if (needsRemoteCheck.length > 0) {
|
|
1239
|
+
info('Comparing with remote...');
|
|
1240
|
+
const diffPayload = needsRemoteCheck.map(a => ({ title: a.title, content_hash: a.hash }));
|
|
1241
|
+
try {
|
|
1242
|
+
const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
|
|
1243
|
+
for (const r of data.results) {
|
|
1244
|
+
localStatus[r.title] = { status: r.status, uuid: r.remote_uuid || '' };
|
|
1245
|
+
}
|
|
1246
|
+
} catch (e) {
|
|
1247
|
+
error(`Diff API error: ${e.message}`);
|
|
1248
|
+
}
|
|
1194
1249
|
}
|
|
1195
1250
|
|
|
1251
|
+
log('');
|
|
1196
1252
|
let newCount = 0, updatedCount = 0, unchangedCount = 0;
|
|
1197
1253
|
|
|
1198
1254
|
for (const asset of assets) {
|
|
1199
|
-
const
|
|
1200
|
-
const
|
|
1255
|
+
const st = localStatus[asset.title] || { status: 'new', uuid: '' };
|
|
1256
|
+
const status = st.status;
|
|
1257
|
+
const uuid = st.uuid || '';
|
|
1201
1258
|
const uuidShort = uuid ? ` ${C.dim}(${uuid.substring(0, 8)})${C.reset}` : '';
|
|
1202
1259
|
|
|
1203
1260
|
if (status === 'new') {
|