tokrepo 1.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.
- package/bin/tokrepo.js +708 -0
- package/package.json +33 -0
package/bin/tokrepo.js
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
|
|
9
|
+
// ANSI colors
|
|
10
|
+
const C = {
|
|
11
|
+
reset: '\x1b[0m',
|
|
12
|
+
bold: '\x1b[1m',
|
|
13
|
+
dim: '\x1b[2m',
|
|
14
|
+
red: '\x1b[31m',
|
|
15
|
+
green: '\x1b[32m',
|
|
16
|
+
yellow: '\x1b[33m',
|
|
17
|
+
blue: '\x1b[34m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
white: '\x1b[37m',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const CONFIG_DIR = path.join(require('os').homedir(), '.tokrepo');
|
|
23
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
24
|
+
const PROJECT_CONFIG = '.tokrepo.json';
|
|
25
|
+
const DEFAULT_API = 'https://api.tokrepo.com';
|
|
26
|
+
|
|
27
|
+
// ─── Helpers ───
|
|
28
|
+
|
|
29
|
+
function log(msg) { console.log(msg); }
|
|
30
|
+
function success(msg) { log(`${C.green}✓${C.reset} ${msg}`); }
|
|
31
|
+
function error(msg) { log(`${C.red}✗${C.reset} ${msg}`); process.exit(1); }
|
|
32
|
+
function warn(msg) { log(`${C.yellow}!${C.reset} ${msg}`); }
|
|
33
|
+
function info(msg) { log(`${C.cyan}→${C.reset} ${msg}`); }
|
|
34
|
+
|
|
35
|
+
function readConfig() {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeConfig(config) {
|
|
44
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
45
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readProjectConfig(baseDir = process.cwd()) {
|
|
51
|
+
const configPath = path.join(baseDir, PROJECT_CONFIG);
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ask(question) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
62
|
+
rl.question(`${C.cyan}?${C.reset} ${question} `, (answer) => {
|
|
63
|
+
rl.close();
|
|
64
|
+
resolve(answer.trim());
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fetchCurrentUser(config) {
|
|
70
|
+
return apiRequest('GET', '/api/v1/tokenboard/auth/me', null, config.token, config.api);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function apiRequest(method, urlPath, body, token, apiBase) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const base = apiBase || DEFAULT_API;
|
|
76
|
+
const url = new URL(urlPath, base);
|
|
77
|
+
const isHttps = url.protocol === 'https:';
|
|
78
|
+
const mod = isHttps ? https : http;
|
|
79
|
+
|
|
80
|
+
const headers = {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
'User-Agent': 'tokrepo-cli/1.1.0',
|
|
83
|
+
};
|
|
84
|
+
if (token) {
|
|
85
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const bodyStr = body ? JSON.stringify(body) : null;
|
|
89
|
+
if (bodyStr) {
|
|
90
|
+
headers['Content-Length'] = Buffer.byteLength(bodyStr);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const options = {
|
|
94
|
+
hostname: url.hostname,
|
|
95
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
96
|
+
path: url.pathname + url.search,
|
|
97
|
+
method,
|
|
98
|
+
headers,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const req = mod.request(options, (res) => {
|
|
102
|
+
let data = '';
|
|
103
|
+
res.on('data', (chunk) => data += chunk);
|
|
104
|
+
res.on('end', () => {
|
|
105
|
+
try {
|
|
106
|
+
const json = JSON.parse(data);
|
|
107
|
+
if (json.code === 200) {
|
|
108
|
+
resolve(json.data);
|
|
109
|
+
} else {
|
|
110
|
+
reject(new Error(json.message || `API error: ${json.code}`));
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
reject(new Error(`Invalid response: ${data.substring(0, 200)}`));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
req.on('error', reject);
|
|
119
|
+
if (bodyStr) req.write(bodyStr);
|
|
120
|
+
req.end();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── File type detection (auto from extension) ───
|
|
125
|
+
|
|
126
|
+
function detectFileType(filename) {
|
|
127
|
+
const lower = filename.toLowerCase();
|
|
128
|
+
// Skills
|
|
129
|
+
if (lower.endsWith('.skill.md') || lower === 'skill.md') return 'skill';
|
|
130
|
+
// Prompts
|
|
131
|
+
if (lower.endsWith('.prompt') || lower.endsWith('.prompt.md')) return 'prompt';
|
|
132
|
+
// Configs
|
|
133
|
+
if (lower === 'claude.md' || lower === '.claude.md' || lower === 'agents.md' || lower === '.agents.md') return 'config';
|
|
134
|
+
if (lower === 'gemini.md' || lower === '.gemini.md') return 'config';
|
|
135
|
+
if (lower === '.cursorrules' || lower === '.windsurfrules') return 'config';
|
|
136
|
+
if (lower.endsWith('.mcp.json') || lower.endsWith('.yaml') || lower.endsWith('.yml') || lower.endsWith('.toml')) return 'config';
|
|
137
|
+
if (lower.endsWith('.json') && !lower.endsWith('package.json') && !lower.endsWith('package-lock.json')) return 'config';
|
|
138
|
+
// Scripts
|
|
139
|
+
if (/\.(sh|py|js|mjs|ts|rb|go|rs|lua)$/.test(lower)) return 'script';
|
|
140
|
+
// Markdown defaults to other (content)
|
|
141
|
+
if (lower.endsWith('.md')) return 'other';
|
|
142
|
+
return 'other';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Guess tag from file type
|
|
146
|
+
function guessTag(fileType) {
|
|
147
|
+
const map = { skill: 'Skills', prompt: 'Prompts', script: 'Scripts', config: 'Configs' };
|
|
148
|
+
return map[fileType] || null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Glob matching ───
|
|
152
|
+
|
|
153
|
+
function matchGlob(pattern, filename) {
|
|
154
|
+
const regex = pattern
|
|
155
|
+
.replace(/\./g, '\\.')
|
|
156
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
157
|
+
.replace(/\*/g, '[^/]*')
|
|
158
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*')
|
|
159
|
+
.replace(/\?/g, '.');
|
|
160
|
+
return new RegExp(`^${regex}$`).test(filename);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function findFiles(patterns, baseDir) {
|
|
164
|
+
const results = new Set();
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
|
|
167
|
+
function walk(dir, relBase) {
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
171
|
+
} catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
176
|
+
const fullPath = path.join(dir, entry.name);
|
|
177
|
+
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
178
|
+
|
|
179
|
+
if (entry.isDirectory()) {
|
|
180
|
+
walk(fullPath, relPath);
|
|
181
|
+
} else if (entry.isFile()) {
|
|
182
|
+
for (const pattern of patterns) {
|
|
183
|
+
if (matchGlob(pattern, relPath) || matchGlob(pattern, entry.name)) {
|
|
184
|
+
if (!seen.has(relPath)) {
|
|
185
|
+
seen.add(relPath);
|
|
186
|
+
results.add({ path: fullPath, relPath });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
walk(baseDir, '');
|
|
195
|
+
return Array.from(results);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Parse CLI args ───
|
|
199
|
+
|
|
200
|
+
function parseArgs(argv) {
|
|
201
|
+
const args = { flags: {}, positional: [] };
|
|
202
|
+
let i = 2; // skip node and script
|
|
203
|
+
// skip command name
|
|
204
|
+
if (argv[i] && !argv[i].startsWith('-')) {
|
|
205
|
+
args.command = argv[i];
|
|
206
|
+
i++;
|
|
207
|
+
}
|
|
208
|
+
while (i < argv.length) {
|
|
209
|
+
const arg = argv[i];
|
|
210
|
+
if (arg === '--public') {
|
|
211
|
+
args.flags.public = true;
|
|
212
|
+
} else if (arg === '--private') {
|
|
213
|
+
args.flags.private = true;
|
|
214
|
+
} else if (arg === '--title' && i + 1 < argv.length) {
|
|
215
|
+
args.flags.title = argv[++i];
|
|
216
|
+
} else if (arg.startsWith('--title=')) {
|
|
217
|
+
args.flags.title = arg.split('=').slice(1).join('=');
|
|
218
|
+
} else if (arg === '--desc' && i + 1 < argv.length) {
|
|
219
|
+
args.flags.desc = argv[++i];
|
|
220
|
+
} else if (arg.startsWith('--desc=')) {
|
|
221
|
+
args.flags.desc = arg.split('=').slice(1).join('=');
|
|
222
|
+
} else if (arg === '--tag' && i + 1 < argv.length) {
|
|
223
|
+
if (!args.flags.tags) args.flags.tags = [];
|
|
224
|
+
args.flags.tags.push(argv[++i]);
|
|
225
|
+
} else if (arg.startsWith('--tag=')) {
|
|
226
|
+
if (!args.flags.tags) args.flags.tags = [];
|
|
227
|
+
args.flags.tags.push(arg.split('=').slice(1).join('='));
|
|
228
|
+
} else if (arg === '-y' || arg === '--yes') {
|
|
229
|
+
args.flags.yes = true;
|
|
230
|
+
} else if (!arg.startsWith('-')) {
|
|
231
|
+
args.positional.push(arg);
|
|
232
|
+
}
|
|
233
|
+
i++;
|
|
234
|
+
}
|
|
235
|
+
return args;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Collect files from paths ───
|
|
239
|
+
|
|
240
|
+
function collectFiles(paths, baseDir) {
|
|
241
|
+
const files = [];
|
|
242
|
+
const DEFAULT_PATTERNS = [
|
|
243
|
+
'*.md', '*.skill.md', '*.prompt', '*.prompt.md',
|
|
244
|
+
'*.sh', '*.py', '*.js', '*.mjs', '*.ts',
|
|
245
|
+
'*.json', '*.yaml', '*.yml', '*.toml',
|
|
246
|
+
];
|
|
247
|
+
// Skip binary/irrelevant files
|
|
248
|
+
const SKIP = new Set([
|
|
249
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
|
|
250
|
+
'.DS_Store', 'Thumbs.db', '.gitignore', '.npmignore',
|
|
251
|
+
]);
|
|
252
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.next', '.nuxt', 'dist', 'build', '__pycache__', '.venv', 'venv']);
|
|
253
|
+
|
|
254
|
+
for (const p of paths) {
|
|
255
|
+
const resolved = path.resolve(baseDir, p);
|
|
256
|
+
if (!fs.existsSync(resolved)) {
|
|
257
|
+
warn(`Not found: ${p}`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const stat = fs.statSync(resolved);
|
|
262
|
+
if (stat.isFile()) {
|
|
263
|
+
// Direct file
|
|
264
|
+
if (SKIP.has(path.basename(resolved))) continue;
|
|
265
|
+
files.push({ path: resolved, relPath: path.basename(resolved) });
|
|
266
|
+
} else if (stat.isDirectory()) {
|
|
267
|
+
// Scan directory
|
|
268
|
+
function walk(dir, relBase) {
|
|
269
|
+
let entries;
|
|
270
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name) || SKIP.has(entry.name)) continue;
|
|
273
|
+
const fullPath = path.join(dir, entry.name);
|
|
274
|
+
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
275
|
+
if (entry.isDirectory()) {
|
|
276
|
+
walk(fullPath, relPath);
|
|
277
|
+
} else if (entry.isFile()) {
|
|
278
|
+
// Check extension matches
|
|
279
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
280
|
+
const validExts = ['.md', '.sh', '.py', '.js', '.mjs', '.ts', '.json', '.yaml', '.yml', '.toml', '.prompt', '.rb', '.go', '.rs'];
|
|
281
|
+
if (validExts.includes(ext) || entry.name === '.cursorrules' || entry.name === '.windsurfrules') {
|
|
282
|
+
if (!SKIP.has(entry.name)) {
|
|
283
|
+
files.push({ path: fullPath, relPath });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
walk(resolved, '');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return files;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Guess title from directory or README ───
|
|
296
|
+
|
|
297
|
+
function guessTitle(files, baseDir) {
|
|
298
|
+
// Try README first line
|
|
299
|
+
const readme = files.find(f => /^readme\.md$/i.test(path.basename(f.relPath)));
|
|
300
|
+
if (readme) {
|
|
301
|
+
const content = fs.readFileSync(readme.path, 'utf8');
|
|
302
|
+
const firstHeading = content.match(/^#\s+(.+)$/m);
|
|
303
|
+
if (firstHeading) return firstHeading[1].trim();
|
|
304
|
+
}
|
|
305
|
+
// Fall back to directory name
|
|
306
|
+
return path.basename(baseDir);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ─── Commands ───
|
|
310
|
+
|
|
311
|
+
async function cmdLogin() {
|
|
312
|
+
log(`\n${C.bold}tokrepo login${C.reset}\n`);
|
|
313
|
+
info('Get your API token from https://tokrepo.com/en/workflows/submit');
|
|
314
|
+
log('');
|
|
315
|
+
|
|
316
|
+
const token = await ask('API Token:');
|
|
317
|
+
if (!token) error('Token is required');
|
|
318
|
+
|
|
319
|
+
writeConfig({ token, api: DEFAULT_API });
|
|
320
|
+
success(`Config saved to ${CONFIG_FILE}`);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const config = readConfig();
|
|
324
|
+
const data = await fetchCurrentUser(config);
|
|
325
|
+
success(`Logged in as ${C.bold}${data.nickname}${C.reset} (${data.email})`);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
warn(`Token saved but verification failed: ${e.message}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function cmdPush() {
|
|
332
|
+
const args = parseArgs(process.argv);
|
|
333
|
+
|
|
334
|
+
const config = readConfig();
|
|
335
|
+
if (!config || !config.token) {
|
|
336
|
+
error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const projectConfig = readProjectConfig();
|
|
340
|
+
const baseDir = process.cwd();
|
|
341
|
+
|
|
342
|
+
// Determine what to push
|
|
343
|
+
let filesToPush;
|
|
344
|
+
let title;
|
|
345
|
+
let description;
|
|
346
|
+
let visibility;
|
|
347
|
+
let tags;
|
|
348
|
+
|
|
349
|
+
if (args.positional.length > 0) {
|
|
350
|
+
// Direct mode: tokrepo push [files/dirs...] --public --title "..."
|
|
351
|
+
filesToPush = collectFiles(args.positional, baseDir);
|
|
352
|
+
} else if (projectConfig) {
|
|
353
|
+
// Config mode: use .tokrepo.json
|
|
354
|
+
const patterns = projectConfig.files || ['*.md'];
|
|
355
|
+
filesToPush = findFiles(patterns, baseDir);
|
|
356
|
+
title = projectConfig.title;
|
|
357
|
+
description = projectConfig.description;
|
|
358
|
+
tags = projectConfig.tags;
|
|
359
|
+
} else {
|
|
360
|
+
// No config, no files specified: push current directory
|
|
361
|
+
filesToPush = collectFiles(['.'], baseDir);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (filesToPush.length === 0) {
|
|
365
|
+
error('No pushable files found. Specify files or run in a directory with .md/.py/.js/.sh files.');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Flags override config
|
|
369
|
+
title = args.flags.title || title || guessTitle(filesToPush, baseDir);
|
|
370
|
+
description = args.flags.desc || description || '';
|
|
371
|
+
visibility = args.flags.public ? 1 : (args.flags.private ? 0 : (projectConfig?.visibility ?? 1));
|
|
372
|
+
tags = args.flags.tags || tags || [];
|
|
373
|
+
|
|
374
|
+
// Read files and detect types
|
|
375
|
+
const pushFiles = [];
|
|
376
|
+
const detectedTags = new Set(tags);
|
|
377
|
+
|
|
378
|
+
for (const f of filesToPush) {
|
|
379
|
+
let content;
|
|
380
|
+
try {
|
|
381
|
+
content = fs.readFileSync(f.path, 'utf8');
|
|
382
|
+
} catch {
|
|
383
|
+
warn(`Cannot read: ${f.relPath} (binary or unreadable, skipping)`);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
// Skip empty files
|
|
387
|
+
if (!content.trim()) continue;
|
|
388
|
+
|
|
389
|
+
const fileType = detectFileType(f.relPath);
|
|
390
|
+
const tag = guessTag(fileType);
|
|
391
|
+
if (tag) detectedTags.add(tag);
|
|
392
|
+
|
|
393
|
+
pushFiles.push({
|
|
394
|
+
name: f.relPath,
|
|
395
|
+
content,
|
|
396
|
+
type: fileType,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (pushFiles.length === 0) {
|
|
401
|
+
error('No readable text files found to push.');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Show summary
|
|
405
|
+
log(`\n${C.bold}tokrepo push${C.reset}\n`);
|
|
406
|
+
log(` ${C.bold}Title:${C.reset} ${title}`);
|
|
407
|
+
log(` ${C.bold}Visibility:${C.reset} ${visibility === 1 ? `${C.green}public${C.reset}` : `${C.yellow}private${C.reset}`}`);
|
|
408
|
+
log(` ${C.bold}Files:${C.reset} ${pushFiles.length}`);
|
|
409
|
+
if (detectedTags.size > 0) {
|
|
410
|
+
log(` ${C.bold}Tags:${C.reset} ${Array.from(detectedTags).join(', ')}`);
|
|
411
|
+
}
|
|
412
|
+
log('');
|
|
413
|
+
|
|
414
|
+
for (const f of pushFiles) {
|
|
415
|
+
const sizeKb = (Buffer.byteLength(f.content) / 1024).toFixed(1);
|
|
416
|
+
log(` ${C.dim}•${C.reset} ${f.name} ${C.dim}(${f.type}, ${sizeKb}KB)${C.reset}`);
|
|
417
|
+
}
|
|
418
|
+
log('');
|
|
419
|
+
|
|
420
|
+
const totalChars = pushFiles.reduce((sum, f) => sum + f.content.length, 0);
|
|
421
|
+
|
|
422
|
+
// Push
|
|
423
|
+
info('Pushing...');
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const data = await apiRequest('POST', '/api/v1/tokenboard/push/create', {
|
|
427
|
+
title,
|
|
428
|
+
description,
|
|
429
|
+
files: pushFiles,
|
|
430
|
+
tags: Array.from(detectedTags),
|
|
431
|
+
token_cost: String(Math.round(totalChars / 4)),
|
|
432
|
+
visibility: visibility,
|
|
433
|
+
}, config.token, config.api);
|
|
434
|
+
|
|
435
|
+
log('');
|
|
436
|
+
success(`Pushed!`);
|
|
437
|
+
log(`\n ${C.bold}URL:${C.reset} ${C.cyan}${data.url}${C.reset}`);
|
|
438
|
+
log(` ${C.bold}UUID:${C.reset} ${data.uuid}\n`);
|
|
439
|
+
} catch (e) {
|
|
440
|
+
error(`Push failed: ${e.message}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function cmdInit() {
|
|
445
|
+
log(`\n${C.bold}tokrepo init${C.reset}\n`);
|
|
446
|
+
|
|
447
|
+
const existing = readProjectConfig();
|
|
448
|
+
if (existing) {
|
|
449
|
+
warn(`${PROJECT_CONFIG} already exists.`);
|
|
450
|
+
const overwrite = await ask('Overwrite? (y/N):');
|
|
451
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
452
|
+
log('Aborted.');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const dirName = path.basename(process.cwd());
|
|
458
|
+
const title = await ask(`Title (${dirName}):`);
|
|
459
|
+
const description = await ask('Description:');
|
|
460
|
+
|
|
461
|
+
const config = {
|
|
462
|
+
title: title || dirName,
|
|
463
|
+
description: description || '',
|
|
464
|
+
files: ['*.md', '*.sh', '*.py', '*.js', '*.mjs', '*.ts', '*.json', '*.yaml'],
|
|
465
|
+
visibility: 1,
|
|
466
|
+
tags: [],
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
fs.writeFileSync(
|
|
470
|
+
path.join(process.cwd(), PROJECT_CONFIG),
|
|
471
|
+
JSON.stringify(config, null, 2) + '\n'
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
success(`Created ${PROJECT_CONFIG}`);
|
|
475
|
+
log(`\n${C.dim}Then run: tokrepo push${C.reset}\n`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function cmdPull() {
|
|
479
|
+
const urlOrUuid = process.argv[3];
|
|
480
|
+
if (!urlOrUuid) error('Usage: tokrepo pull <url-or-uuid>');
|
|
481
|
+
|
|
482
|
+
log(`\n${C.bold}tokrepo pull${C.reset}\n`);
|
|
483
|
+
|
|
484
|
+
let uuid = urlOrUuid;
|
|
485
|
+
const urlMatch = urlOrUuid.match(/workflows\/([a-f0-9-]+)/);
|
|
486
|
+
if (urlMatch) uuid = urlMatch[1];
|
|
487
|
+
|
|
488
|
+
const config = readConfig();
|
|
489
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
490
|
+
|
|
491
|
+
info(`Fetching ${uuid}...`);
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${uuid}`, null, config?.token, apiBase);
|
|
495
|
+
const workflow = data.workflow;
|
|
496
|
+
log(`\n ${C.bold}${workflow.title}${C.reset}`);
|
|
497
|
+
|
|
498
|
+
if (workflow.steps && workflow.steps.length > 0) {
|
|
499
|
+
for (const step of workflow.steps) {
|
|
500
|
+
const content = step.prompt_template || step.promptTemplate;
|
|
501
|
+
if (content) {
|
|
502
|
+
const fileName = `${step.title || 'step-' + step.step_order}.md`;
|
|
503
|
+
const safeName = fileName.replace(/[/\\?%*:|"<>]/g, '-');
|
|
504
|
+
fs.writeFileSync(path.join(process.cwd(), safeName), content);
|
|
505
|
+
success(`Downloaded: ${safeName}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
log('');
|
|
510
|
+
success('Pull complete!');
|
|
511
|
+
} catch (e) {
|
|
512
|
+
error(`Pull failed: ${e.message}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function cmdWhoami() {
|
|
517
|
+
const config = readConfig();
|
|
518
|
+
if (!config || !config.token) error('Not logged in. Run: tokrepo login');
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const data = await fetchCurrentUser(config);
|
|
522
|
+
log(`\n${C.bold}Logged in as:${C.reset}`);
|
|
523
|
+
log(` ${C.bold}Name:${C.reset} ${data.nickname}`);
|
|
524
|
+
log(` ${C.bold}Email:${C.reset} ${data.email}`);
|
|
525
|
+
log(` ${C.bold}UUID:${C.reset} ${data.uuid}`);
|
|
526
|
+
log(` ${C.bold}API:${C.reset} ${config.api}\n`);
|
|
527
|
+
} catch (e) {
|
|
528
|
+
error(`Auth failed: ${e.message}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function cmdList() {
|
|
533
|
+
log(`\n${C.bold}tokrepo list${C.reset}\n`);
|
|
534
|
+
|
|
535
|
+
const config = readConfig();
|
|
536
|
+
if (!config || !config.token) error('Not logged in. Run: tokrepo login');
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const data = await apiRequest('GET', '/api/v1/tokenboard/workflows/my?page=1&page_size=50', null, config.token, config.api);
|
|
540
|
+
|
|
541
|
+
if (!data.list || data.list.length === 0) {
|
|
542
|
+
info('No assets found. Run: tokrepo push');
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
log(` ${C.bold}${data.total}${C.reset} assets:\n`);
|
|
547
|
+
|
|
548
|
+
for (const wf of data.list) {
|
|
549
|
+
const views = wf.view_count || 0;
|
|
550
|
+
log(` ${C.cyan}${wf.uuid.substring(0,8)}${C.reset} ${C.bold}${wf.title}${C.reset}`);
|
|
551
|
+
log(` ${C.dim} ${views} views · https://tokrepo.com/en/workflows/${wf.uuid}${C.reset}\n`);
|
|
552
|
+
}
|
|
553
|
+
} catch (e) {
|
|
554
|
+
error(`Failed: ${e.message}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function cmdUpdate() {
|
|
559
|
+
const uuid = process.argv[3];
|
|
560
|
+
if (!uuid) error('Usage: tokrepo update <uuid> [file]');
|
|
561
|
+
|
|
562
|
+
log(`\n${C.bold}tokrepo update${C.reset}\n`);
|
|
563
|
+
|
|
564
|
+
const config = readConfig();
|
|
565
|
+
if (!config || !config.token) error('Not logged in. Run: tokrepo login');
|
|
566
|
+
|
|
567
|
+
const filePath = process.argv[4];
|
|
568
|
+
let body = { uuid };
|
|
569
|
+
|
|
570
|
+
if (filePath) {
|
|
571
|
+
const fullPath = path.resolve(filePath);
|
|
572
|
+
if (!fs.existsSync(fullPath)) error(`File not found: ${filePath}`);
|
|
573
|
+
|
|
574
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
575
|
+
body.steps = [{
|
|
576
|
+
id: 0,
|
|
577
|
+
step_order: 1,
|
|
578
|
+
title: body.title || path.basename(filePath),
|
|
579
|
+
description: '',
|
|
580
|
+
prompt_template: content,
|
|
581
|
+
variables: '{}',
|
|
582
|
+
depends_on: '',
|
|
583
|
+
expected_output: '',
|
|
584
|
+
}];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
info(`Updating ${uuid.substring(0,8)}...`);
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
await apiRequest('PUT', '/api/v1/tokenboard/workflows/update', body, config.token, config.api);
|
|
591
|
+
success('Updated!');
|
|
592
|
+
log(` ${C.dim}https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
593
|
+
} catch (e) {
|
|
594
|
+
error(`Update failed: ${e.message}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function cmdDelete() {
|
|
599
|
+
const uuid = process.argv[3];
|
|
600
|
+
if (!uuid) error('Usage: tokrepo delete <uuid>');
|
|
601
|
+
|
|
602
|
+
log(`\n${C.bold}tokrepo delete${C.reset}\n`);
|
|
603
|
+
|
|
604
|
+
const config = readConfig();
|
|
605
|
+
if (!config || !config.token) error('Not logged in. Run: tokrepo login');
|
|
606
|
+
|
|
607
|
+
const confirm = await ask(`Delete ${uuid.substring(0,8)}...? (y/N):`);
|
|
608
|
+
if (confirm.toLowerCase() !== 'y') { log('Aborted.'); return; }
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
await apiRequest('DELETE', '/api/v1/tokenboard/workflows/delete', { uuid }, config.token, config.api);
|
|
612
|
+
success('Deleted!');
|
|
613
|
+
} catch (e) {
|
|
614
|
+
error(`Delete failed: ${e.message}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function cmdTags() {
|
|
619
|
+
log(`\n${C.bold}tokrepo tags${C.reset}\n`);
|
|
620
|
+
|
|
621
|
+
const config = readConfig();
|
|
622
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
const data = await apiRequest('GET', '/api/v1/tokenboard/tags/list', null, null, apiBase);
|
|
626
|
+
log(` Available tags:\n`);
|
|
627
|
+
for (const tag of data.tags) {
|
|
628
|
+
log(` ${C.cyan}${tag.name}${C.reset}${tag.count ? ` ${C.dim}(${tag.count} assets)${C.reset}` : ''}`);
|
|
629
|
+
}
|
|
630
|
+
log('');
|
|
631
|
+
} catch (e) {
|
|
632
|
+
error(`Failed: ${e.message}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function showHelp() {
|
|
637
|
+
log(`
|
|
638
|
+
${C.bold}tokrepo${C.reset} — Push AI assets to tokrepo.com
|
|
639
|
+
|
|
640
|
+
${C.bold}QUICK START${C.reset}
|
|
641
|
+
${C.cyan}tokrepo login${C.reset} # one-time: paste your token
|
|
642
|
+
${C.cyan}tokrepo push --public .${C.reset} # push current directory
|
|
643
|
+
${C.cyan}tokrepo push --public README.md script.py${C.reset} # push specific files
|
|
644
|
+
|
|
645
|
+
${C.bold}USAGE${C.reset}
|
|
646
|
+
tokrepo push [files/dirs...] [options]
|
|
647
|
+
|
|
648
|
+
${C.bold}OPTIONS${C.reset}
|
|
649
|
+
${C.cyan}--public${C.reset} Make asset publicly visible (default)
|
|
650
|
+
${C.cyan}--private${C.reset} Make asset private
|
|
651
|
+
${C.cyan}--title${C.reset} "..." Set title (auto-detected from README or dir name)
|
|
652
|
+
${C.cyan}--desc${C.reset} "..." Set description
|
|
653
|
+
${C.cyan}--tag${C.reset} Skills Add tag (repeatable: --tag Skills --tag MCP)
|
|
654
|
+
${C.cyan}-y, --yes${C.reset} Skip confirmation prompts
|
|
655
|
+
|
|
656
|
+
${C.bold}COMMANDS${C.reset}
|
|
657
|
+
${C.cyan}login${C.reset} Save API token
|
|
658
|
+
${C.cyan}push${C.reset} [files...] Push files/directory (default: current dir)
|
|
659
|
+
${C.cyan}init${C.reset} Create .tokrepo.json project config
|
|
660
|
+
${C.cyan}pull${C.reset} <url> Download asset to local files
|
|
661
|
+
${C.cyan}list${C.reset} List your assets
|
|
662
|
+
${C.cyan}update${C.reset} <uuid> [f] Update existing asset
|
|
663
|
+
${C.cyan}delete${C.reset} <uuid> Delete an asset
|
|
664
|
+
${C.cyan}tags${C.reset} List available tags
|
|
665
|
+
${C.cyan}whoami${C.reset} Show current user
|
|
666
|
+
${C.cyan}help${C.reset} Show this help
|
|
667
|
+
|
|
668
|
+
${C.bold}EXAMPLES${C.reset}
|
|
669
|
+
tokrepo push --public . # Push all files in current dir
|
|
670
|
+
tokrepo push --public --title "My MCP" . # Push with custom title
|
|
671
|
+
tokrepo push --public src/ README.md # Push specific paths
|
|
672
|
+
tokrepo push # Uses .tokrepo.json if exists
|
|
673
|
+
|
|
674
|
+
${C.bold}FILE TYPE AUTO-DETECTION${C.reset}
|
|
675
|
+
.sh .py .js .ts .mjs .go .rs → script
|
|
676
|
+
.json .yaml .yml .toml → config
|
|
677
|
+
.skill.md → skill
|
|
678
|
+
.prompt .prompt.md → prompt
|
|
679
|
+
.md (other) → other
|
|
680
|
+
|
|
681
|
+
${C.bold}GET YOUR TOKEN${C.reset}
|
|
682
|
+
https://tokrepo.com/en/workflows/submit
|
|
683
|
+
`);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ─── Main ───
|
|
687
|
+
|
|
688
|
+
async function main() {
|
|
689
|
+
const command = process.argv[2];
|
|
690
|
+
|
|
691
|
+
switch (command) {
|
|
692
|
+
case 'login': await cmdLogin(); break;
|
|
693
|
+
case 'init': await cmdInit(); break;
|
|
694
|
+
case 'push': await cmdPush(); break;
|
|
695
|
+
case 'pull': await cmdPull(); break;
|
|
696
|
+
case 'list': await cmdList(); break;
|
|
697
|
+
case 'update': await cmdUpdate(); break;
|
|
698
|
+
case 'delete': await cmdDelete(); break;
|
|
699
|
+
case 'tags': await cmdTags(); break;
|
|
700
|
+
case 'whoami': await cmdWhoami(); break;
|
|
701
|
+
case 'help': case '--help': case '-h': case undefined:
|
|
702
|
+
showHelp(); break;
|
|
703
|
+
default:
|
|
704
|
+
error(`Unknown command: ${command}. Run: tokrepo help`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
main().catch((e) => { error(e.message); });
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tokrepo",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Push AI assets to tokrepo.com — Skills, Prompts, MCP Configs, Scripts",
|
|
5
|
+
"bin": {
|
|
6
|
+
"tokrepo": "bin/tokrepo.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/tokrepo/cli.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://tokrepo.com",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai",
|
|
19
|
+
"prompt",
|
|
20
|
+
"skill",
|
|
21
|
+
"workflow",
|
|
22
|
+
"mcp",
|
|
23
|
+
"tokrepo",
|
|
24
|
+
"claude",
|
|
25
|
+
"codex",
|
|
26
|
+
"cursor",
|
|
27
|
+
"gemini",
|
|
28
|
+
"agent"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=16"
|
|
32
|
+
}
|
|
33
|
+
}
|