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.
Files changed (2) hide show
  1. package/bin/tokrepo.js +94 -37
  2. 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.0.0';
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
- const title = guessAssetTitle(files, entry.name);
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
- // Step 1: Diff with remote
1067
- info('Comparing with remote...');
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
- let diffResults;
1071
- try {
1072
- const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
1073
- diffResults = data.results;
1074
- } catch (e) {
1075
- // Fallback: if diff endpoint not available, treat all as new
1076
- warn(`Diff API not available (${e.message}), treating all as new`);
1077
- diffResults = assets.map(a => ({ title: a.title, status: 'new' }));
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
- // Build status map
1081
- const statusMap = {};
1082
- for (const r of diffResults) {
1083
- statusMap[r.title] = r;
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 status = statusMap[asset.title]?.status || 'new';
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
- // Step 2: Upsert each changed asset
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 status = statusMap[asset.title]?.status || 'new';
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
- // Diff with remote
1180
- info('Comparing with remote...\n');
1181
- const diffPayload = assets.map(a => ({ title: a.title, content_hash: a.hash }));
1222
+ // Check local sync state first, remote for unknowns
1223
+ const syncState = readSyncState();
1224
+ const localStatus = {};
1225
+ const needsRemoteCheck = [];
1182
1226
 
1183
- let diffResults;
1184
- try {
1185
- const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
1186
- diffResults = data.results;
1187
- } catch (e) {
1188
- error(`Diff API error: ${e.message}`);
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
- const statusMap = {};
1192
- for (const r of diffResults) {
1193
- statusMap[r.title] = r;
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 status = statusMap[asset.title]?.status || 'new';
1200
- const uuid = statusMap[asset.title]?.remote_uuid || '';
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') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokrepo",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "AI assets for humans and agents — sync, search, install, push. Like git push for your AI skills.",
5
5
  "bin": {
6
6
  "tokrepo": "bin/tokrepo.js"