tokrepo 2.0.0 → 3.0.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/bin/tokrepo.js +398 -6
- package/package.json +2 -2
package/bin/tokrepo.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
const https = require('https');
|
|
6
7
|
const http = require('http');
|
|
7
8
|
const readline = require('readline');
|
|
@@ -23,6 +24,8 @@ const CONFIG_DIR = path.join(require('os').homedir(), '.tokrepo');
|
|
|
23
24
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
24
25
|
const PROJECT_CONFIG = '.tokrepo.json';
|
|
25
26
|
const DEFAULT_API = 'https://api.tokrepo.com';
|
|
27
|
+
const CLI_VERSION = '3.0.0';
|
|
28
|
+
const VERSION_CHECK_FILE = path.join(require('os').homedir(), '.tokrepo', '.version-check');
|
|
26
29
|
|
|
27
30
|
// ─── Helpers ───
|
|
28
31
|
|
|
@@ -42,9 +45,64 @@ function readConfig() {
|
|
|
42
45
|
|
|
43
46
|
function writeConfig(config) {
|
|
44
47
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
45
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
48
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
46
49
|
}
|
|
47
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
50
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check npm registry for newer version (non-blocking, max once per day)
|
|
54
|
+
async function checkForUpdate() {
|
|
55
|
+
try {
|
|
56
|
+
// Only check once per 24 hours
|
|
57
|
+
if (fs.existsSync(VERSION_CHECK_FILE)) {
|
|
58
|
+
const stat = fs.statSync(VERSION_CHECK_FILE);
|
|
59
|
+
const hoursSinceCheck = (Date.now() - stat.mtimeMs) / 3600000;
|
|
60
|
+
if (hoursSinceCheck < 24) return;
|
|
61
|
+
}
|
|
62
|
+
// Touch the file to mark check time
|
|
63
|
+
if (!fs.existsSync(path.dirname(VERSION_CHECK_FILE))) {
|
|
64
|
+
fs.mkdirSync(path.dirname(VERSION_CHECK_FILE), { recursive: true, mode: 0o700 });
|
|
65
|
+
}
|
|
66
|
+
fs.writeFileSync(VERSION_CHECK_FILE, '', { mode: 0o600 });
|
|
67
|
+
|
|
68
|
+
const data = await new Promise((resolve, reject) => {
|
|
69
|
+
const req = https.get('https://registry.npmjs.org/tokrepo/latest', {
|
|
70
|
+
headers: { 'Accept': 'application/json' },
|
|
71
|
+
timeout: 3000,
|
|
72
|
+
}, (res) => {
|
|
73
|
+
let body = '';
|
|
74
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
75
|
+
res.on('end', () => {
|
|
76
|
+
try { resolve(JSON.parse(body)); } catch { reject(); }
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
req.on('error', reject);
|
|
80
|
+
req.on('timeout', () => { req.destroy(); reject(); });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const latest = data.version;
|
|
84
|
+
if (latest && latest !== CLI_VERSION) {
|
|
85
|
+
const cmp = compareVersions(latest, CLI_VERSION);
|
|
86
|
+
if (cmp > 0) {
|
|
87
|
+
log('');
|
|
88
|
+
log(`${C.yellow}!${C.reset} Update available: ${C.dim}${CLI_VERSION}${C.reset} → ${C.green}${latest}${C.reset}`);
|
|
89
|
+
log(` Run: ${C.cyan}npm install -g tokrepo${C.reset}`);
|
|
90
|
+
log('');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Silent fail — update check is best-effort
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function compareVersions(a, b) {
|
|
99
|
+
const pa = a.split('.').map(Number);
|
|
100
|
+
const pb = b.split('.').map(Number);
|
|
101
|
+
for (let i = 0; i < 3; i++) {
|
|
102
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
103
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
104
|
+
}
|
|
105
|
+
return 0;
|
|
48
106
|
}
|
|
49
107
|
|
|
50
108
|
function readProjectConfig(baseDir = process.cwd()) {
|
|
@@ -74,6 +132,10 @@ function apiRequest(method, urlPath, body, token, apiBase) {
|
|
|
74
132
|
return new Promise((resolve, reject) => {
|
|
75
133
|
const base = apiBase || DEFAULT_API;
|
|
76
134
|
const url = new URL(urlPath, base);
|
|
135
|
+
// Force HTTPS to prevent token transmission over plain HTTP
|
|
136
|
+
if (url.protocol === 'http:' && !url.hostname.match(/^(localhost|127\.0\.0\.1)$/)) {
|
|
137
|
+
url.protocol = 'https:';
|
|
138
|
+
}
|
|
77
139
|
const isHttps = url.protocol === 'https:';
|
|
78
140
|
const mod = isHttps ? https : http;
|
|
79
141
|
|
|
@@ -144,7 +206,8 @@ function detectFileType(filename) {
|
|
|
144
206
|
|
|
145
207
|
// Guess tag from file type
|
|
146
208
|
function guessTag(fileType) {
|
|
147
|
-
|
|
209
|
+
// Use lowercase singular names to match backend tag slugs
|
|
210
|
+
const map = { skill: 'skill', prompt: 'prompt', script: 'script', config: 'config', mcp: 'mcp' };
|
|
148
211
|
return map[fileType] || null;
|
|
149
212
|
}
|
|
150
213
|
|
|
@@ -253,6 +316,11 @@ function collectFiles(paths, baseDir) {
|
|
|
253
316
|
|
|
254
317
|
for (const p of paths) {
|
|
255
318
|
const resolved = path.resolve(baseDir, p);
|
|
319
|
+
// Prevent path traversal — resolved must be within baseDir
|
|
320
|
+
if (!resolved.startsWith(path.resolve(baseDir) + path.sep) && resolved !== path.resolve(baseDir)) {
|
|
321
|
+
warn(`Skipped (outside project): ${p}`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
256
324
|
if (!fs.existsSync(resolved)) {
|
|
257
325
|
warn(`Not found: ${p}`);
|
|
258
326
|
continue;
|
|
@@ -674,6 +742,9 @@ Examples:
|
|
|
674
742
|
}
|
|
675
743
|
case 'mcp':
|
|
676
744
|
case 'mcp configs': {
|
|
745
|
+
// Security warning for MCP configs
|
|
746
|
+
warn('MCP server config detected. Review the configuration carefully before adding to your project.');
|
|
747
|
+
warn('MCP servers can execute arbitrary code. Only install from trusted sources.');
|
|
677
748
|
// Save as mcp config, hint about manual merge
|
|
678
749
|
if (!fileName.endsWith('.json')) fileName = fileName.replace(/\.md$/, '.json');
|
|
679
750
|
break;
|
|
@@ -704,6 +775,8 @@ Examples:
|
|
|
704
775
|
}
|
|
705
776
|
}
|
|
706
777
|
|
|
778
|
+
// Sanitize fileName to prevent path traversal from API response
|
|
779
|
+
fileName = path.basename(fileName);
|
|
707
780
|
const destPath = path.join(destDir, fileName);
|
|
708
781
|
|
|
709
782
|
// Don't overwrite without warning
|
|
@@ -848,6 +921,306 @@ async function cmdTags() {
|
|
|
848
921
|
}
|
|
849
922
|
}
|
|
850
923
|
|
|
924
|
+
// ─── Sync: scan directory, diff with remote, upsert changes ───
|
|
925
|
+
|
|
926
|
+
function computeHash(files) {
|
|
927
|
+
const h = crypto.createHash('sha256');
|
|
928
|
+
for (const f of files) {
|
|
929
|
+
h.update(f.name);
|
|
930
|
+
h.update('\0');
|
|
931
|
+
h.update(f.content);
|
|
932
|
+
h.update('\0');
|
|
933
|
+
}
|
|
934
|
+
return h.digest('hex');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function scanDirectory(dirPath) {
|
|
938
|
+
const assets = [];
|
|
939
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'dist', 'build']);
|
|
940
|
+
const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db', 'package-lock.json', 'yarn.lock']);
|
|
941
|
+
|
|
942
|
+
// Each subdirectory = one asset; loose files = one asset per file
|
|
943
|
+
let entries;
|
|
944
|
+
try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return assets; }
|
|
945
|
+
|
|
946
|
+
const looseFiles = [];
|
|
947
|
+
|
|
948
|
+
for (const entry of entries) {
|
|
949
|
+
if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name) || SKIP_FILES.has(entry.name)) continue;
|
|
950
|
+
|
|
951
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
952
|
+
|
|
953
|
+
if (entry.isDirectory()) {
|
|
954
|
+
// Subdirectory = one asset (e.g., ~/.claude/skills/my-skill/)
|
|
955
|
+
const files = collectAssetFiles(fullPath);
|
|
956
|
+
if (files.length === 0) continue;
|
|
957
|
+
|
|
958
|
+
const title = guessAssetTitle(files, entry.name);
|
|
959
|
+
const hash = computeHash(files);
|
|
960
|
+
const detectedTags = new Set();
|
|
961
|
+
for (const f of files) {
|
|
962
|
+
const ft = detectFileType(f.name);
|
|
963
|
+
const tag = guessTag(ft);
|
|
964
|
+
if (tag) detectedTags.add(tag);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
assets.push({ title, files, hash, tags: Array.from(detectedTags), sourcePath: fullPath });
|
|
968
|
+
} else if (entry.isFile()) {
|
|
969
|
+
// Loose file
|
|
970
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
971
|
+
const validExts = ['.md', '.sh', '.py', '.js', '.mjs', '.ts', '.json', '.yaml', '.yml', '.toml', '.prompt'];
|
|
972
|
+
if (validExts.includes(ext) || entry.name === '.cursorrules' || entry.name === '.windsurfrules') {
|
|
973
|
+
looseFiles.push({ path: fullPath, name: entry.name });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Each loose file = one asset
|
|
979
|
+
for (const f of looseFiles) {
|
|
980
|
+
let content;
|
|
981
|
+
try { content = fs.readFileSync(f.path, 'utf8'); } catch { continue; }
|
|
982
|
+
if (!content.trim()) continue;
|
|
983
|
+
|
|
984
|
+
const files = [{ name: f.name, content, type: detectFileType(f.name) }];
|
|
985
|
+
const title = guessAssetTitle(files, path.basename(f.name, path.extname(f.name)));
|
|
986
|
+
const hash = computeHash(files);
|
|
987
|
+
const ft = detectFileType(f.name);
|
|
988
|
+
const tag = guessTag(ft);
|
|
989
|
+
|
|
990
|
+
assets.push({ title, files, hash, tags: tag ? [tag] : [], sourcePath: f.path });
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return assets;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function collectAssetFiles(dirPath) {
|
|
997
|
+
const files = [];
|
|
998
|
+
const SKIP = new Set(['.DS_Store', 'node_modules', '.git', '__pycache__']);
|
|
999
|
+
|
|
1000
|
+
function walk(dir, relBase) {
|
|
1001
|
+
let entries;
|
|
1002
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
1003
|
+
for (const entry of entries) {
|
|
1004
|
+
if (entry.name.startsWith('.') || SKIP.has(entry.name)) continue;
|
|
1005
|
+
const fullPath = path.join(dir, entry.name);
|
|
1006
|
+
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1007
|
+
if (entry.isDirectory()) {
|
|
1008
|
+
walk(fullPath, relPath);
|
|
1009
|
+
} else if (entry.isFile()) {
|
|
1010
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
1011
|
+
const validExts = ['.md', '.sh', '.py', '.js', '.mjs', '.ts', '.json', '.yaml', '.yml', '.toml', '.prompt', '.rb', '.go', '.rs'];
|
|
1012
|
+
if (validExts.includes(ext) || entry.name === '.cursorrules') {
|
|
1013
|
+
let content;
|
|
1014
|
+
try { content = fs.readFileSync(fullPath, 'utf8'); } catch { continue; }
|
|
1015
|
+
if (!content.trim()) continue;
|
|
1016
|
+
files.push({ name: relPath, content, type: detectFileType(relPath) });
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
walk(dirPath, '');
|
|
1023
|
+
return files;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function guessAssetTitle(files, fallbackName) {
|
|
1027
|
+
// Try to find a heading in the first .md file
|
|
1028
|
+
for (const f of files) {
|
|
1029
|
+
if (f.name.toLowerCase().endsWith('.md')) {
|
|
1030
|
+
const match = f.content.match(/^#\s+(.+)$/m);
|
|
1031
|
+
if (match) return match[1].trim();
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// Clean up fallback name
|
|
1035
|
+
return fallbackName
|
|
1036
|
+
.replace(/[-_]/g, ' ')
|
|
1037
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async function cmdSync() {
|
|
1041
|
+
const args = parseArgs(process.argv);
|
|
1042
|
+
const config = readConfig();
|
|
1043
|
+
if (!config || !config.token) error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
|
|
1044
|
+
|
|
1045
|
+
const targetDir = args.positional[0]
|
|
1046
|
+
? path.resolve(args.positional[0])
|
|
1047
|
+
: path.join(require('os').homedir(), '.claude', 'skills');
|
|
1048
|
+
|
|
1049
|
+
if (!fs.existsSync(targetDir)) {
|
|
1050
|
+
error(`Directory not found: ${targetDir}`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const visibility = args.flags.public ? 1 : 0; // default private for sync
|
|
1054
|
+
|
|
1055
|
+
log(`\n${C.bold}tokrepo sync${C.reset}\n`);
|
|
1056
|
+
info(`Scanning ${targetDir}...`);
|
|
1057
|
+
|
|
1058
|
+
const assets = scanDirectory(targetDir);
|
|
1059
|
+
if (assets.length === 0) {
|
|
1060
|
+
info('No assets found in directory.');
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
log(` Found ${C.bold}${assets.length}${C.reset} assets\n`);
|
|
1065
|
+
|
|
1066
|
+
// Step 1: Diff with remote
|
|
1067
|
+
info('Comparing with remote...');
|
|
1068
|
+
const diffPayload = assets.map(a => ({ title: a.title, content_hash: a.hash }));
|
|
1069
|
+
|
|
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' }));
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Build status map
|
|
1081
|
+
const statusMap = {};
|
|
1082
|
+
for (const r of diffResults) {
|
|
1083
|
+
statusMap[r.title] = r;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Show status
|
|
1087
|
+
let newCount = 0, updatedCount = 0, unchangedCount = 0;
|
|
1088
|
+
|
|
1089
|
+
for (const asset of assets) {
|
|
1090
|
+
const status = statusMap[asset.title]?.status || 'new';
|
|
1091
|
+
if (status === 'new') {
|
|
1092
|
+
log(` ${C.green}+ new${C.reset} ${asset.title} ${C.dim}(${asset.files.length} files)${C.reset}`);
|
|
1093
|
+
newCount++;
|
|
1094
|
+
} else if (status === 'updated') {
|
|
1095
|
+
log(` ${C.yellow}~ updated${C.reset} ${asset.title}`);
|
|
1096
|
+
updatedCount++;
|
|
1097
|
+
} else {
|
|
1098
|
+
log(` ${C.dim}= unchanged ${asset.title}${C.reset}`);
|
|
1099
|
+
unchangedCount++;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
log('');
|
|
1104
|
+
log(` ${C.green}${newCount} new${C.reset} ${C.yellow}${updatedCount} updated${C.reset} ${C.dim}${unchangedCount} unchanged${C.reset}`);
|
|
1105
|
+
|
|
1106
|
+
if (newCount === 0 && updatedCount === 0) {
|
|
1107
|
+
log('');
|
|
1108
|
+
success('Everything is up to date!');
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
log('');
|
|
1113
|
+
|
|
1114
|
+
// Confirm unless -y
|
|
1115
|
+
if (!args.flags.yes) {
|
|
1116
|
+
const confirm = await ask(`Push ${newCount + updatedCount} assets? (y/N):`);
|
|
1117
|
+
if (confirm.toLowerCase() !== 'y') { log('Aborted.'); return; }
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Step 2: Upsert each changed asset
|
|
1121
|
+
let successCount = 0;
|
|
1122
|
+
let failCount = 0;
|
|
1123
|
+
|
|
1124
|
+
for (const asset of assets) {
|
|
1125
|
+
const status = statusMap[asset.title]?.status || 'new';
|
|
1126
|
+
if (status === 'unchanged') continue;
|
|
1127
|
+
|
|
1128
|
+
const totalChars = asset.files.reduce((sum, f) => sum + f.content.length, 0);
|
|
1129
|
+
|
|
1130
|
+
try {
|
|
1131
|
+
const data = await apiRequest('POST', '/api/v1/tokenboard/push/upsert', {
|
|
1132
|
+
title: asset.title,
|
|
1133
|
+
files: asset.files,
|
|
1134
|
+
tags: asset.tags,
|
|
1135
|
+
token_cost: String(Math.round(totalChars / 4)),
|
|
1136
|
+
visibility,
|
|
1137
|
+
}, config.token, config.api);
|
|
1138
|
+
|
|
1139
|
+
const action = data.action === 'created' ? C.green + '+ created' : C.yellow + '~ updated';
|
|
1140
|
+
log(` ${action}${C.reset} ${asset.title} ${C.dim}${data.url}${C.reset}`);
|
|
1141
|
+
successCount++;
|
|
1142
|
+
} catch (e) {
|
|
1143
|
+
log(` ${C.red}✗ failed${C.reset} ${asset.title}: ${e.message}`);
|
|
1144
|
+
failCount++;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
log('');
|
|
1149
|
+
if (failCount === 0) {
|
|
1150
|
+
success(`Synced ${successCount} assets!`);
|
|
1151
|
+
} else {
|
|
1152
|
+
warn(`${successCount} synced, ${failCount} failed`);
|
|
1153
|
+
}
|
|
1154
|
+
log('');
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async function cmdStatus() {
|
|
1158
|
+
const args = parseArgs(process.argv);
|
|
1159
|
+
const config = readConfig();
|
|
1160
|
+
if (!config || !config.token) error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
|
|
1161
|
+
|
|
1162
|
+
const targetDir = args.positional[0]
|
|
1163
|
+
? path.resolve(args.positional[0])
|
|
1164
|
+
: path.join(require('os').homedir(), '.claude', 'skills');
|
|
1165
|
+
|
|
1166
|
+
if (!fs.existsSync(targetDir)) {
|
|
1167
|
+
error(`Directory not found: ${targetDir}`);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
log(`\n${C.bold}tokrepo status${C.reset}\n`);
|
|
1171
|
+
info(`Scanning ${targetDir}...`);
|
|
1172
|
+
|
|
1173
|
+
const assets = scanDirectory(targetDir);
|
|
1174
|
+
if (assets.length === 0) {
|
|
1175
|
+
info('No assets found in directory.');
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Diff with remote
|
|
1180
|
+
info('Comparing with remote...\n');
|
|
1181
|
+
const diffPayload = assets.map(a => ({ title: a.title, content_hash: a.hash }));
|
|
1182
|
+
|
|
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}`);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const statusMap = {};
|
|
1192
|
+
for (const r of diffResults) {
|
|
1193
|
+
statusMap[r.title] = r;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
let newCount = 0, updatedCount = 0, unchangedCount = 0;
|
|
1197
|
+
|
|
1198
|
+
for (const asset of assets) {
|
|
1199
|
+
const status = statusMap[asset.title]?.status || 'new';
|
|
1200
|
+
const uuid = statusMap[asset.title]?.remote_uuid || '';
|
|
1201
|
+
const uuidShort = uuid ? ` ${C.dim}(${uuid.substring(0, 8)})${C.reset}` : '';
|
|
1202
|
+
|
|
1203
|
+
if (status === 'new') {
|
|
1204
|
+
log(` ${C.green}+ new${C.reset} ${asset.title} ${C.dim}(${asset.files.length} files)${C.reset}`);
|
|
1205
|
+
newCount++;
|
|
1206
|
+
} else if (status === 'updated') {
|
|
1207
|
+
log(` ${C.yellow}~ modified${C.reset} ${asset.title}${uuidShort}`);
|
|
1208
|
+
updatedCount++;
|
|
1209
|
+
} else {
|
|
1210
|
+
log(` ${C.dim} unchanged ${asset.title}${uuidShort}${C.reset}`);
|
|
1211
|
+
unchangedCount++;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
log('');
|
|
1216
|
+
log(` ${C.bold}${assets.length}${C.reset} local assets: ${C.green}${newCount} new${C.reset} ${C.yellow}${updatedCount} modified${C.reset} ${C.dim}${unchangedCount} synced${C.reset}`);
|
|
1217
|
+
|
|
1218
|
+
if (newCount > 0 || updatedCount > 0) {
|
|
1219
|
+
log(`\n Run ${C.cyan}tokrepo sync ${args.positional[0] || ''}${C.reset} to push changes`);
|
|
1220
|
+
}
|
|
1221
|
+
log('');
|
|
1222
|
+
}
|
|
1223
|
+
|
|
851
1224
|
function showHelp() {
|
|
852
1225
|
log(`
|
|
853
1226
|
${C.bold}tokrepo${C.reset} — AI assets for humans and agents. Like GitHub, for AI experience.
|
|
@@ -865,10 +1238,12 @@ ${C.bold}DISCOVER & INSTALL${C.reset}
|
|
|
865
1238
|
${C.cyan}install${C.reset} <name|uuid> Smart install (auto-detects type & placement)
|
|
866
1239
|
${C.cyan}pull${C.reset} <url|uuid> Download raw asset files
|
|
867
1240
|
|
|
868
|
-
${C.bold}PUBLISH${C.reset}
|
|
869
|
-
${C.cyan}push${C.reset} [files...] Push files/directory
|
|
1241
|
+
${C.bold}PUBLISH & SYNC${C.reset}
|
|
1242
|
+
${C.cyan}push${C.reset} [files...] Push files/directory (creates new asset)
|
|
1243
|
+
${C.cyan}sync${C.reset} [dir] Sync directory to TokRepo (smart upsert)
|
|
1244
|
+
${C.cyan}status${C.reset} [dir] Show local vs remote diff
|
|
870
1245
|
${C.cyan}init${C.reset} Create .tokrepo.json project config
|
|
871
|
-
${C.cyan}update${C.reset} <uuid> [f] Update existing asset
|
|
1246
|
+
${C.cyan}update${C.reset} <uuid> [f] Update existing asset by UUID
|
|
872
1247
|
${C.cyan}delete${C.reset} <uuid> Delete an asset
|
|
873
1248
|
|
|
874
1249
|
${C.bold}ACCOUNT${C.reset}
|
|
@@ -892,6 +1267,16 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
|
892
1267
|
MCP → current dir (.json)
|
|
893
1268
|
Prompts → current dir (.md)
|
|
894
1269
|
|
|
1270
|
+
${C.bold}SYNC (the killer feature)${C.reset}
|
|
1271
|
+
${C.cyan}tokrepo sync ~/.claude/skills/${C.reset} # Sync all skills (default: private)
|
|
1272
|
+
${C.cyan}tokrepo sync ~/.claude/skills/ --public${C.reset} # Sync as public assets
|
|
1273
|
+
${C.cyan}tokrepo sync . -y${C.reset} # Sync current dir, skip confirm
|
|
1274
|
+
${C.cyan}tokrepo status${C.reset} # Show what would change
|
|
1275
|
+
|
|
1276
|
+
Sync scans a directory, detects new/modified assets, and pushes only
|
|
1277
|
+
what changed. Each subdirectory becomes one asset. Loose files become
|
|
1278
|
+
individual assets. Like ${C.bold}git push${C.reset} for your AI assets.
|
|
1279
|
+
|
|
895
1280
|
${C.bold}EXAMPLES${C.reset}
|
|
896
1281
|
tokrepo search "mcp server" # Find MCP configs
|
|
897
1282
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
@@ -919,6 +1304,8 @@ async function main() {
|
|
|
919
1304
|
case 'login': await cmdLogin(); break;
|
|
920
1305
|
case 'init': await cmdInit(); break;
|
|
921
1306
|
case 'push': await cmdPush(); break;
|
|
1307
|
+
case 'sync': await cmdSync(); break;
|
|
1308
|
+
case 'status': case 'st': await cmdStatus(); break;
|
|
922
1309
|
case 'pull': await cmdPull(); break;
|
|
923
1310
|
case 'search': case 'find': await cmdSearch(); break;
|
|
924
1311
|
case 'install': case 'i': await cmdInstall(); break;
|
|
@@ -927,11 +1314,16 @@ async function main() {
|
|
|
927
1314
|
case 'delete': await cmdDelete(); break;
|
|
928
1315
|
case 'tags': await cmdTags(); break;
|
|
929
1316
|
case 'whoami': await cmdWhoami(); break;
|
|
1317
|
+
case '--version': case '-v': case 'version':
|
|
1318
|
+
log(`tokrepo ${CLI_VERSION}`); break;
|
|
930
1319
|
case 'help': case '--help': case '-h': case undefined:
|
|
931
1320
|
showHelp(); break;
|
|
932
1321
|
default:
|
|
933
1322
|
error(`Unknown command: ${command}. Run: tokrepo help`);
|
|
934
1323
|
}
|
|
1324
|
+
|
|
1325
|
+
// Non-blocking update check after command completes
|
|
1326
|
+
checkForUpdate();
|
|
935
1327
|
}
|
|
936
1328
|
|
|
937
1329
|
main().catch((e) => { error(e.message); });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokrepo",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "AI assets for humans and agents — search, install, push. Like
|
|
3
|
+
"version": "3.0.0",
|
|
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"
|
|
7
7
|
},
|