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.
Files changed (2) hide show
  1. package/bin/tokrepo.js +708 -0
  2. 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
+ }