tokrepo 3.0.0 → 3.1.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.
Files changed (2) hide show
  1. package/bin/tokrepo.js +91 -36
  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.0';
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 {
@@ -1063,35 +1073,54 @@ async function cmdSync() {
1063
1073
 
1064
1074
  log(` Found ${C.bold}${assets.length}${C.reset} assets\n`);
1065
1075
 
1066
- // Step 1: Diff with remote
1067
- info('Comparing with remote...');
1068
- const diffPayload = assets.map(a => ({ title: a.title, content_hash: a.hash }));
1076
+ // Load local sync state for fast local-only diff
1077
+ const syncState = readSyncState();
1069
1078
 
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' }));
1079
+ // Classify each asset: check local state first, then remote diff for unknowns
1080
+ const needsRemoteCheck = [];
1081
+ const localStatus = {};
1082
+
1083
+ for (const asset of assets) {
1084
+ const key = asset.sourcePath;
1085
+ const cached = syncState[key];
1086
+ if (cached && cached.hash === asset.hash) {
1087
+ // Local hash matches last sync — unchanged
1088
+ localStatus[asset.title] = { status: 'unchanged', uuid: cached.uuid };
1089
+ } else if (cached && cached.hash !== asset.hash) {
1090
+ // Local hash differs from last sync — updated
1091
+ localStatus[asset.title] = { status: 'updated', uuid: cached.uuid };
1092
+ } else {
1093
+ // Not in local state — check remote
1094
+ needsRemoteCheck.push(asset);
1095
+ }
1078
1096
  }
1079
1097
 
1080
- // Build status map
1081
- const statusMap = {};
1082
- for (const r of diffResults) {
1083
- statusMap[r.title] = r;
1098
+ // Remote diff for assets not in local state
1099
+ if (needsRemoteCheck.length > 0) {
1100
+ info('Comparing with remote...');
1101
+ const diffPayload = needsRemoteCheck.map(a => ({ title: a.title, content_hash: a.hash }));
1102
+ try {
1103
+ const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
1104
+ for (const r of data.results) {
1105
+ localStatus[r.title] = { status: r.status, uuid: r.remote_uuid || '' };
1106
+ }
1107
+ } catch (e) {
1108
+ warn(`Diff API: ${e.message} — treating unknowns as new`);
1109
+ for (const a of needsRemoteCheck) {
1110
+ localStatus[a.title] = { status: 'new', uuid: '' };
1111
+ }
1112
+ }
1084
1113
  }
1085
1114
 
1086
1115
  // Show status
1087
1116
  let newCount = 0, updatedCount = 0, unchangedCount = 0;
1088
1117
 
1089
1118
  for (const asset of assets) {
1090
- const status = statusMap[asset.title]?.status || 'new';
1091
- if (status === 'new') {
1119
+ const st = localStatus[asset.title] || { status: 'new' };
1120
+ if (st.status === 'new') {
1092
1121
  log(` ${C.green}+ new${C.reset} ${asset.title} ${C.dim}(${asset.files.length} files)${C.reset}`);
1093
1122
  newCount++;
1094
- } else if (status === 'updated') {
1123
+ } else if (st.status === 'updated') {
1095
1124
  log(` ${C.yellow}~ updated${C.reset} ${asset.title}`);
1096
1125
  updatedCount++;
1097
1126
  } else {
@@ -1117,13 +1146,13 @@ async function cmdSync() {
1117
1146
  if (confirm.toLowerCase() !== 'y') { log('Aborted.'); return; }
1118
1147
  }
1119
1148
 
1120
- // Step 2: Upsert each changed asset
1149
+ // Upsert each changed asset + save state
1121
1150
  let successCount = 0;
1122
1151
  let failCount = 0;
1123
1152
 
1124
1153
  for (const asset of assets) {
1125
- const status = statusMap[asset.title]?.status || 'new';
1126
- if (status === 'unchanged') continue;
1154
+ const st = localStatus[asset.title] || { status: 'new' };
1155
+ if (st.status === 'unchanged') continue;
1127
1156
 
1128
1157
  const totalChars = asset.files.reduce((sum, f) => sum + f.content.length, 0);
1129
1158
 
@@ -1138,6 +1167,15 @@ async function cmdSync() {
1138
1167
 
1139
1168
  const action = data.action === 'created' ? C.green + '+ created' : C.yellow + '~ updated';
1140
1169
  log(` ${action}${C.reset} ${asset.title} ${C.dim}${data.url}${C.reset}`);
1170
+
1171
+ // Save to local sync state
1172
+ syncState[asset.sourcePath] = {
1173
+ uuid: data.uuid,
1174
+ hash: asset.hash,
1175
+ title: asset.title,
1176
+ url: data.url,
1177
+ lastSync: new Date().toISOString(),
1178
+ };
1141
1179
  successCount++;
1142
1180
  } catch (e) {
1143
1181
  log(` ${C.red}✗ failed${C.reset} ${asset.title}: ${e.message}`);
@@ -1145,6 +1183,9 @@ async function cmdSync() {
1145
1183
  }
1146
1184
  }
1147
1185
 
1186
+ // Persist sync state
1187
+ writeSyncState(syncState);
1188
+
1148
1189
  log('');
1149
1190
  if (failCount === 0) {
1150
1191
  success(`Synced ${successCount} assets!`);
@@ -1176,28 +1217,42 @@ async function cmdStatus() {
1176
1217
  return;
1177
1218
  }
1178
1219
 
1179
- // Diff with remote
1180
- info('Comparing with remote...\n');
1181
- const diffPayload = assets.map(a => ({ title: a.title, content_hash: a.hash }));
1220
+ // Check local sync state first, remote for unknowns
1221
+ const syncState = readSyncState();
1222
+ const localStatus = {};
1223
+ const needsRemoteCheck = [];
1182
1224
 
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}`);
1225
+ for (const asset of assets) {
1226
+ const cached = syncState[asset.sourcePath];
1227
+ if (cached && cached.hash === asset.hash) {
1228
+ localStatus[asset.title] = { status: 'unchanged', uuid: cached.uuid };
1229
+ } else if (cached && cached.hash !== asset.hash) {
1230
+ localStatus[asset.title] = { status: 'updated', uuid: cached.uuid };
1231
+ } else {
1232
+ needsRemoteCheck.push(asset);
1233
+ }
1189
1234
  }
1190
1235
 
1191
- const statusMap = {};
1192
- for (const r of diffResults) {
1193
- statusMap[r.title] = r;
1236
+ if (needsRemoteCheck.length > 0) {
1237
+ info('Comparing with remote...');
1238
+ const diffPayload = needsRemoteCheck.map(a => ({ title: a.title, content_hash: a.hash }));
1239
+ try {
1240
+ const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
1241
+ for (const r of data.results) {
1242
+ localStatus[r.title] = { status: r.status, uuid: r.remote_uuid || '' };
1243
+ }
1244
+ } catch (e) {
1245
+ error(`Diff API error: ${e.message}`);
1246
+ }
1194
1247
  }
1195
1248
 
1249
+ log('');
1196
1250
  let newCount = 0, updatedCount = 0, unchangedCount = 0;
1197
1251
 
1198
1252
  for (const asset of assets) {
1199
- const status = statusMap[asset.title]?.status || 'new';
1200
- const uuid = statusMap[asset.title]?.remote_uuid || '';
1253
+ const st = localStatus[asset.title] || { status: 'new', uuid: '' };
1254
+ const status = st.status;
1255
+ const uuid = st.uuid || '';
1201
1256
  const uuidShort = uuid ? ` ${C.dim}(${uuid.substring(0, 8)})${C.reset}` : '';
1202
1257
 
1203
1258
  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.0",
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"