tokrepo 3.3.0 → 3.3.2
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 +242 -42
- package/package.json +1 -1
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,7 +24,7 @@ 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';
|
|
26
|
-
const CLI_VERSION = '3.3.
|
|
27
|
+
const CLI_VERSION = '3.3.2';
|
|
27
28
|
const VERSION_CHECK_FILE = path.join(require('os').homedir(), '.tokrepo', '.version-check');
|
|
28
29
|
|
|
29
30
|
// ─── Helpers ───
|
|
@@ -291,6 +292,10 @@ function parseArgs(argv) {
|
|
|
291
292
|
} else if (arg.startsWith('--tag=')) {
|
|
292
293
|
if (!args.flags.tags) args.flags.tags = [];
|
|
293
294
|
args.flags.tags.push(arg.split('=').slice(1).join('='));
|
|
295
|
+
} else if (arg === '--target' && i + 1 < argv.length) {
|
|
296
|
+
args.flags.target = argv[++i];
|
|
297
|
+
} else if (arg.startsWith('--target=')) {
|
|
298
|
+
args.flags.target = arg.split('=').slice(1).join('=');
|
|
294
299
|
} else if (arg === '-y' || arg === '--yes') {
|
|
295
300
|
args.flags.yes = true;
|
|
296
301
|
} else if (!arg.startsWith('-')) {
|
|
@@ -414,6 +419,7 @@ async function saveAndVerifyToken(token) {
|
|
|
414
419
|
|
|
415
420
|
function browserAuthFlow() {
|
|
416
421
|
return new Promise((resolve) => {
|
|
422
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
417
423
|
const server = http.createServer((req, res) => {
|
|
418
424
|
// CORS headers for browser fetch
|
|
419
425
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -432,6 +438,11 @@ function browserAuthFlow() {
|
|
|
432
438
|
req.on('end', () => {
|
|
433
439
|
try {
|
|
434
440
|
const data = JSON.parse(body);
|
|
441
|
+
if (!data.token || data.state !== state) {
|
|
442
|
+
res.writeHead(403);
|
|
443
|
+
res.end('Invalid authorization state');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
435
446
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
436
447
|
res.end(JSON.stringify({ ok: true }));
|
|
437
448
|
server.close();
|
|
@@ -452,7 +463,7 @@ function browserAuthFlow() {
|
|
|
452
463
|
// Listen on random port
|
|
453
464
|
server.listen(0, '127.0.0.1', () => {
|
|
454
465
|
const port = server.address().port;
|
|
455
|
-
const authUrl = `https://tokrepo.com/en/cli-auth?port=${port}`;
|
|
466
|
+
const authUrl = `https://tokrepo.com/en/cli-auth?port=${port}&state=${state}`;
|
|
456
467
|
|
|
457
468
|
info(`Listening on http://127.0.0.1:${port}`);
|
|
458
469
|
log(` ${C.dim}If browser doesn't open, visit:${C.reset}`);
|
|
@@ -517,7 +528,7 @@ async function cmdPush() {
|
|
|
517
528
|
// Flags override config
|
|
518
529
|
title = args.flags.title || title || guessTitle(filesToPush, baseDir);
|
|
519
530
|
description = args.flags.desc || description || '';
|
|
520
|
-
visibility = args.flags.public ? 1 : (args.flags.private ? 0 : (projectConfig?.visibility ??
|
|
531
|
+
visibility = args.flags.public ? 1 : (args.flags.private ? 0 : (projectConfig?.visibility ?? 0));
|
|
521
532
|
tags = args.flags.tags || tags || [];
|
|
522
533
|
|
|
523
534
|
// Read files and detect types
|
|
@@ -553,8 +564,8 @@ async function cmdPush() {
|
|
|
553
564
|
// Show summary
|
|
554
565
|
log(`\n${C.bold}tokrepo push${C.reset}\n`);
|
|
555
566
|
log(` ${C.bold}Title:${C.reset} ${title}`);
|
|
556
|
-
log(` ${C.bold}Visibility:${C.reset} ${visibility === 1 ? `${C.green}public${C.reset}` : `${C.yellow}private${C.reset}`}`);
|
|
557
|
-
log(` ${C.bold}Files:${C.reset} ${pushFiles.length}`);
|
|
567
|
+
log(` ${C.bold}Visibility:${C.reset} ${visibility === 1 ? `${C.green}public${C.reset} (visible to everyone)` : `${C.yellow}private${C.reset} (only you can see)`}`);
|
|
568
|
+
log(` ${C.bold}Files:${C.reset} ${pushFiles.length} (only these files will be uploaded)`);
|
|
558
569
|
if (detectedTags.size > 0) {
|
|
559
570
|
log(` ${C.bold}Tags:${C.reset} ${Array.from(detectedTags).join(', ')}`);
|
|
560
571
|
}
|
|
@@ -614,7 +625,7 @@ async function cmdInit() {
|
|
|
614
625
|
title: title || dirName,
|
|
615
626
|
description: description || '',
|
|
616
627
|
files: ['*.md', '*.sh', '*.py', '*.js', '*.mjs', '*.ts', '*.json', '*.yaml'],
|
|
617
|
-
visibility:
|
|
628
|
+
visibility: 0,
|
|
618
629
|
tags: [],
|
|
619
630
|
};
|
|
620
631
|
|
|
@@ -663,6 +674,11 @@ async function cmdPull() {
|
|
|
663
674
|
}
|
|
664
675
|
}
|
|
665
676
|
|
|
677
|
+
// Normalize query: replace hyphens/underscores/dots with spaces for better matching
|
|
678
|
+
function normalizeQuery(q) {
|
|
679
|
+
return q.replace(/[-_.]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
680
|
+
}
|
|
681
|
+
|
|
666
682
|
// Resolve various input formats to a UUID:
|
|
667
683
|
// - UUID directly: "ca000374-f5d8-..."
|
|
668
684
|
// - Full URL: "https://tokrepo.com/en/workflows/ca000374-f5d8-..."
|
|
@@ -680,9 +696,10 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
680
696
|
const atMatch = input.match(/^@([^/]+)\/(.+)$/);
|
|
681
697
|
if (atMatch) {
|
|
682
698
|
const [, username, assetName] = atMatch;
|
|
683
|
-
|
|
699
|
+
const normalizedName = normalizeQuery(assetName);
|
|
700
|
+
info(`Searching for "${normalizedName}" by @${username}...`);
|
|
684
701
|
// Search by keyword, then filter by author nickname
|
|
685
|
-
const encoded = encodeURIComponent(
|
|
702
|
+
const encoded = encodeURIComponent(normalizedName);
|
|
686
703
|
try {
|
|
687
704
|
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=1&page_size=20&sort_by=views`, null, config?.token, apiBase);
|
|
688
705
|
const items = data.list || data.items || [];
|
|
@@ -700,9 +717,10 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
700
717
|
error(`Asset not found: ${input}`);
|
|
701
718
|
}
|
|
702
719
|
|
|
703
|
-
// Plain name: search by keyword
|
|
704
|
-
|
|
705
|
-
|
|
720
|
+
// Plain name: search by keyword (normalize separators)
|
|
721
|
+
const normalizedInput = normalizeQuery(input);
|
|
722
|
+
info(`Searching for "${normalizedInput}"...`);
|
|
723
|
+
const encoded = encodeURIComponent(normalizedInput);
|
|
706
724
|
try {
|
|
707
725
|
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=1&page_size=5&sort_by=views`, null, config?.token, apiBase);
|
|
708
726
|
const items = data.list || data.items || [];
|
|
@@ -714,10 +732,12 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
714
732
|
// ─── Search ───
|
|
715
733
|
|
|
716
734
|
async function cmdSearch() {
|
|
717
|
-
const
|
|
718
|
-
if (!
|
|
735
|
+
const rawQuery = process.argv.slice(3).join(' ');
|
|
736
|
+
if (!rawQuery) error('Usage: tokrepo search <keyword>');
|
|
719
737
|
|
|
720
|
-
|
|
738
|
+
const query = normalizeQuery(rawQuery);
|
|
739
|
+
const displayQuery = query !== rawQuery ? `"${rawQuery}" → "${query}"` : `"${query}"`;
|
|
740
|
+
log(`\n${C.bold}tokrepo search${C.reset} ${displayQuery}\n`);
|
|
721
741
|
|
|
722
742
|
const config = readConfig();
|
|
723
743
|
const apiBase = config?.api || DEFAULT_API;
|
|
@@ -728,7 +748,14 @@ async function cmdSearch() {
|
|
|
728
748
|
|
|
729
749
|
if (!data.list || data.list.length === 0) {
|
|
730
750
|
info('No assets found.');
|
|
731
|
-
|
|
751
|
+
// Suggest broader search terms
|
|
752
|
+
const words = query.split(' ');
|
|
753
|
+
if (words.length > 1) {
|
|
754
|
+
log(`\n ${C.dim}Try fewer keywords:${C.reset}`);
|
|
755
|
+
log(` ${C.cyan}tokrepo search ${words[0]}${C.reset}`);
|
|
756
|
+
log(` ${C.cyan}tokrepo search ${words.slice(0, 2).join(' ')}${C.reset}`);
|
|
757
|
+
}
|
|
758
|
+
log(`\n ${C.dim}Browse all: https://tokrepo.com/en/featured${C.reset}\n`);
|
|
732
759
|
return;
|
|
733
760
|
}
|
|
734
761
|
|
|
@@ -739,8 +766,13 @@ async function cmdSearch() {
|
|
|
739
766
|
const tags = (wf.tags || []).map(t => t.name).join(', ');
|
|
740
767
|
const views = wf.view_count || 0;
|
|
741
768
|
const votes = wf.vote_count || 0;
|
|
769
|
+
// Truncate long descriptions for readability
|
|
770
|
+
const desc = (wf.description || '').length > 80
|
|
771
|
+
? wf.description.substring(0, 77) + '...'
|
|
772
|
+
: (wf.description || '');
|
|
742
773
|
|
|
743
774
|
log(` ${C.dim}${String(i + 1).padStart(2)}.${C.reset} ${C.bold}${wf.title}${C.reset}`);
|
|
775
|
+
if (desc) log(` ${desc}`);
|
|
744
776
|
if (tags) log(` ${C.cyan}${tags}${C.reset} ${C.dim}★${votes} 👁${views}${C.reset}`);
|
|
745
777
|
log(` ${C.dim}tokrepo install ${wf.uuid}${C.reset}`);
|
|
746
778
|
log('');
|
|
@@ -752,50 +784,189 @@ async function cmdSearch() {
|
|
|
752
784
|
|
|
753
785
|
// ─── Install (smart pull with correct placement) ───
|
|
754
786
|
|
|
787
|
+
function normalizeInstallTarget(target) {
|
|
788
|
+
if (!target) return '';
|
|
789
|
+
const normalized = String(target).trim().toLowerCase();
|
|
790
|
+
const aliases = {
|
|
791
|
+
gemini: 'gemini',
|
|
792
|
+
'gemini-cli': 'gemini',
|
|
793
|
+
};
|
|
794
|
+
return aliases[normalized] || normalized;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function validateInstallTarget(target) {
|
|
798
|
+
if (!target) return '';
|
|
799
|
+
const normalized = normalizeInstallTarget(target);
|
|
800
|
+
if (normalized !== 'gemini') {
|
|
801
|
+
error(`Unsupported install target: ${target}. Supported targets: gemini`);
|
|
802
|
+
}
|
|
803
|
+
return normalized;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function pickWritablePath(destPath, overwrite) {
|
|
807
|
+
if (!fs.existsSync(destPath)) return destPath;
|
|
808
|
+
if (overwrite) {
|
|
809
|
+
warn(`File exists: ${path.relative(process.cwd(), destPath)} (overwriting)`);
|
|
810
|
+
return destPath;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const dir = path.dirname(destPath);
|
|
814
|
+
const ext = path.extname(destPath);
|
|
815
|
+
const base = path.basename(destPath, ext);
|
|
816
|
+
let index = 2;
|
|
817
|
+
let candidate = path.join(dir, `${base}.${index}${ext}`);
|
|
818
|
+
while (fs.existsSync(candidate)) {
|
|
819
|
+
index++;
|
|
820
|
+
candidate = path.join(dir, `${base}.${index}${ext}`);
|
|
821
|
+
}
|
|
822
|
+
warn(`File exists: ${path.relative(process.cwd(), destPath)}; writing ${path.relative(process.cwd(), candidate)} instead. Use --yes to overwrite.`);
|
|
823
|
+
return candidate;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function formatGeminiContent(workflow, contents) {
|
|
827
|
+
const parts = [
|
|
828
|
+
`# ${workflow.title || 'TokRepo Asset'}`,
|
|
829
|
+
workflow.description ? workflow.description : '',
|
|
830
|
+
'<!-- Installed from TokRepo. Gemini CLI reads GEMINI.md as project instructions. -->',
|
|
831
|
+
].filter(Boolean);
|
|
832
|
+
|
|
833
|
+
for (const item of contents) {
|
|
834
|
+
const title = item.name ? `## ${item.name}` : '## Instructions';
|
|
835
|
+
parts.push(`${title}\n\n${String(item.content || '').trim()}`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return `${parts.join('\n\n').trim()}\n`;
|
|
839
|
+
}
|
|
840
|
+
|
|
755
841
|
async function cmdInstall() {
|
|
756
|
-
const
|
|
842
|
+
const args = parseArgs(process.argv);
|
|
843
|
+
const target = args.positional[0];
|
|
757
844
|
if (!target) {
|
|
758
|
-
error(`Usage: tokrepo install <
|
|
845
|
+
error(`Usage: tokrepo install <target> [--target gemini] [--yes]
|
|
759
846
|
|
|
760
847
|
Examples:
|
|
761
|
-
tokrepo install awesome-cursor-rules
|
|
762
|
-
tokrepo install ca000374-f5d8-4d75-a30c-460fda0b6b0e
|
|
763
|
-
tokrepo install https://tokrepo.com/en/workflows/ca000374
|
|
848
|
+
tokrepo install awesome-cursor-rules # by name
|
|
849
|
+
tokrepo install ca000374-f5d8-4d75-a30c-460fda0b6b0e # by uuid
|
|
850
|
+
tokrepo install https://tokrepo.com/en/workflows/ca000374-...
|
|
851
|
+
tokrepo install pack/seo-geo # install whole theme pack
|
|
852
|
+
tokrepo install c4b18aeb --target gemini # write .gemini/GEMINI.md`);
|
|
764
853
|
}
|
|
765
854
|
|
|
766
855
|
log(`\n${C.bold}tokrepo install${C.reset}\n`);
|
|
767
856
|
|
|
768
857
|
const config = readConfig();
|
|
769
858
|
const apiBase = config?.api || DEFAULT_API;
|
|
859
|
+
const installOpts = {
|
|
860
|
+
targetTool: validateInstallTarget(args.flags.target),
|
|
861
|
+
yes: Boolean(args.flags.yes),
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
// pack/<slug> dispatch — install entire theme pack
|
|
865
|
+
if (target.startsWith('pack/')) {
|
|
866
|
+
const slug = target.slice('pack/'.length).trim();
|
|
867
|
+
if (!slug) {
|
|
868
|
+
error('Pack slug is required, e.g. tokrepo install pack/seo-geo');
|
|
869
|
+
}
|
|
870
|
+
await installPack(slug, config, apiBase, installOpts);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
await installOneAsset(target, config, apiBase, installOpts);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Install all assets in a theme pack — sequentially, continue past per-item errors
|
|
878
|
+
async function installPack(slug, config, apiBase, opts) {
|
|
879
|
+
info(`Fetching pack ${C.bold}${slug}${C.reset}...`);
|
|
880
|
+
let pack;
|
|
881
|
+
try {
|
|
882
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/homepage/packs/${encodeURIComponent(slug)}`, null, config?.token, apiBase);
|
|
883
|
+
pack = data.pack;
|
|
884
|
+
} catch (e) {
|
|
885
|
+
error(`Pack not found: ${slug} (${e.message})`);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
log(`\n ${C.bold}${pack.icon} ${pack.title}${C.reset}`);
|
|
889
|
+
if (pack.description) log(` ${C.dim}${pack.description.substring(0, 140)}${C.reset}`);
|
|
890
|
+
log(` ${C.dim}${pack.items.length} asset(s) in this pack${C.reset}\n`);
|
|
891
|
+
|
|
892
|
+
let ok = 0, fail = 0;
|
|
893
|
+
for (let i = 0; i < pack.items.length; i++) {
|
|
894
|
+
const it = pack.items[i];
|
|
895
|
+
log(`${C.dim}[${i + 1}/${pack.items.length}]${C.reset}`);
|
|
896
|
+
try {
|
|
897
|
+
await installOneAsset(it.uuid, config, apiBase, { ...(opts || {}), silent: false, throwOnError: true });
|
|
898
|
+
ok++;
|
|
899
|
+
} catch (e) {
|
|
900
|
+
warn(`Skipped "${it.title}": ${e.message}`);
|
|
901
|
+
fail++;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
log('');
|
|
906
|
+
if (fail === 0) {
|
|
907
|
+
success(`${ok} asset(s) installed from pack ${C.bold}${pack.title}${C.reset}`);
|
|
908
|
+
} else {
|
|
909
|
+
log(` ${C.dim}${ok} ok, ${fail} failed${C.reset}`);
|
|
910
|
+
}
|
|
911
|
+
log(` ${C.dim}Pack page: https://tokrepo.com/packs/${slug}${C.reset}\n`);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Single asset install — extracted so `pack/` flow can reuse.
|
|
915
|
+
// opts.throwOnError: pack flow wants to throw and continue; single-cli flow uses error() (which exits)
|
|
916
|
+
async function installOneAsset(target, config, apiBase, opts) {
|
|
917
|
+
opts = opts || {};
|
|
918
|
+
const die = (msg) => { if (opts.throwOnError) throw new Error(msg); error(msg); };
|
|
770
919
|
|
|
771
920
|
// Resolve target to UUID
|
|
772
921
|
let uuid = target;
|
|
773
922
|
|
|
774
923
|
// URL format
|
|
775
|
-
const urlMatch = target.match(/workflows\/([
|
|
924
|
+
const urlMatch = target.match(/workflows\/([^/?#]+)/);
|
|
776
925
|
if (urlMatch) {
|
|
926
|
+
// URL may carry either UUID or slug-uuid8 — pass through to detail resolver below
|
|
777
927
|
uuid = urlMatch[1];
|
|
778
928
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
929
|
+
|
|
930
|
+
// 已经是完整 UUID — 直接用
|
|
931
|
+
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(uuid)) {
|
|
932
|
+
// ok
|
|
933
|
+
}
|
|
934
|
+
// SEO slug 形态:结尾是 -<8 hex>,先尝试 /detail?slug= 直查,避免走 search 超时
|
|
935
|
+
else if (/-[a-f0-9]{8}$/i.test(uuid)) {
|
|
936
|
+
try {
|
|
937
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?slug=${encodeURIComponent(uuid)}`, null, config?.token, apiBase);
|
|
938
|
+
if (data && data.workflow && data.workflow.uuid) {
|
|
939
|
+
uuid = data.workflow.uuid;
|
|
940
|
+
}
|
|
941
|
+
} catch (_) {
|
|
942
|
+
// 404 → 回落到 search
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(uuid)) {
|
|
947
|
+
// Search by name (normalize separators for better matching)
|
|
948
|
+
const normalizedTarget = normalizeQuery(uuid);
|
|
949
|
+
info(`Searching for "${normalizedTarget}"...`);
|
|
783
950
|
try {
|
|
784
|
-
const encoded = encodeURIComponent(
|
|
951
|
+
const encoded = encodeURIComponent(normalizedTarget);
|
|
785
952
|
const searchData = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=1&page_size=5&sort_by=views`, null, config?.token, apiBase);
|
|
786
953
|
|
|
787
954
|
if (!searchData.list || searchData.list.length === 0) {
|
|
788
|
-
|
|
955
|
+
die(`No asset found matching "${target}". Try: tokrepo search ${target}`);
|
|
789
956
|
}
|
|
790
957
|
|
|
791
|
-
// If
|
|
792
|
-
const
|
|
958
|
+
// If title contains all query words, prefer it
|
|
959
|
+
const queryWords = normalizedTarget.toLowerCase().split(' ');
|
|
960
|
+
const exact = searchData.list.find(w => {
|
|
961
|
+
const title = w.title.toLowerCase();
|
|
962
|
+
return queryWords.every(word => title.includes(word));
|
|
963
|
+
});
|
|
793
964
|
const chosen = exact || searchData.list[0];
|
|
794
965
|
|
|
795
966
|
uuid = chosen.uuid;
|
|
796
967
|
info(`Found: ${C.bold}${chosen.title}${C.reset}`);
|
|
797
968
|
} catch (e) {
|
|
798
|
-
|
|
969
|
+
die(`Search failed: ${e.message}`);
|
|
799
970
|
}
|
|
800
971
|
}
|
|
801
972
|
|
|
@@ -808,7 +979,7 @@ Examples:
|
|
|
808
979
|
workflow = data.workflow;
|
|
809
980
|
files = data.workflow.files || [];
|
|
810
981
|
} catch (e) {
|
|
811
|
-
|
|
982
|
+
die(`Fetch failed: ${e.message}`);
|
|
812
983
|
}
|
|
813
984
|
|
|
814
985
|
log(`\n ${C.bold}${workflow.title}${C.reset}`);
|
|
@@ -842,10 +1013,34 @@ Examples:
|
|
|
842
1013
|
}
|
|
843
1014
|
|
|
844
1015
|
if (contents.length === 0) {
|
|
845
|
-
|
|
1016
|
+
die('No installable content found in this asset.');
|
|
846
1017
|
}
|
|
847
1018
|
|
|
848
1019
|
log('');
|
|
1020
|
+
const targetTool = normalizeInstallTarget(opts.targetTool);
|
|
1021
|
+
|
|
1022
|
+
if (targetTool === 'gemini') {
|
|
1023
|
+
const destDir = path.join(process.cwd(), '.gemini');
|
|
1024
|
+
if (!fs.existsSync(destDir)) {
|
|
1025
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
1026
|
+
}
|
|
1027
|
+
const destPath = pickWritablePath(path.join(destDir, 'GEMINI.md'), Boolean(opts.yes));
|
|
1028
|
+
const resolvedDir = path.resolve(destDir);
|
|
1029
|
+
const resolvedDest = path.resolve(destPath);
|
|
1030
|
+
if (!resolvedDest.startsWith(resolvedDir + path.sep) && resolvedDest !== resolvedDir) {
|
|
1031
|
+
die('Install path escaped .gemini directory.');
|
|
1032
|
+
}
|
|
1033
|
+
fs.writeFileSync(destPath, formatGeminiContent(workflow, contents));
|
|
1034
|
+
const relPath = path.relative(process.cwd(), destPath);
|
|
1035
|
+
success(`Installed: ${relPath}`);
|
|
1036
|
+
if (path.basename(destPath) !== 'GEMINI.md') {
|
|
1037
|
+
warn('Gemini CLI automatically reads GEMINI.md. Merge this file if you want it loaded by default.');
|
|
1038
|
+
}
|
|
1039
|
+
log('');
|
|
1040
|
+
success(`1 file installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1041
|
+
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
849
1044
|
|
|
850
1045
|
// Smart install based on asset type
|
|
851
1046
|
let installed = 0;
|
|
@@ -902,7 +1097,7 @@ Examples:
|
|
|
902
1097
|
}
|
|
903
1098
|
}
|
|
904
1099
|
|
|
905
|
-
|
|
1100
|
+
let destPath = path.join(destDir, fileName);
|
|
906
1101
|
|
|
907
1102
|
// Path traversal guard: ensure resolved path stays inside destDir
|
|
908
1103
|
if (!path.resolve(destPath).startsWith(path.resolve(destDir) + path.sep) && path.resolve(destPath) !== path.resolve(destDir)) {
|
|
@@ -910,10 +1105,7 @@ Examples:
|
|
|
910
1105
|
continue;
|
|
911
1106
|
}
|
|
912
1107
|
|
|
913
|
-
|
|
914
|
-
if (fs.existsSync(destPath)) {
|
|
915
|
-
warn(`File exists: ${path.relative(process.cwd(), destPath)} (overwriting)`);
|
|
916
|
-
}
|
|
1108
|
+
destPath = pickWritablePath(destPath, Boolean(opts.yes));
|
|
917
1109
|
|
|
918
1110
|
fs.writeFileSync(destPath, item.content);
|
|
919
1111
|
|
|
@@ -1241,10 +1433,14 @@ function showHelp() {
|
|
|
1241
1433
|
log(`
|
|
1242
1434
|
${C.bold}tokrepo${C.reset} — AI assets for humans and agents. Like GitHub, for AI experience.
|
|
1243
1435
|
|
|
1436
|
+
${C.dim}You control what gets pushed. Each push uploads only the files you specify.
|
|
1437
|
+
Nothing is shared without your explicit action. Private by default.${C.reset}
|
|
1438
|
+
|
|
1244
1439
|
${C.bold}QUICK START${C.reset}
|
|
1245
1440
|
${C.cyan}tokrepo search cursor rules${C.reset} # find assets
|
|
1246
1441
|
${C.cyan}tokrepo install awesome-cursor-rules${C.reset} # install to your project
|
|
1247
|
-
${C.cyan}tokrepo push --
|
|
1442
|
+
${C.cyan}tokrepo push --private my-skill.md${C.reset} # save privately (only you can see)
|
|
1443
|
+
${C.cyan}tokrepo push --public my-skill.md${C.reset} # share publicly
|
|
1248
1444
|
|
|
1249
1445
|
${C.bold}USAGE${C.reset}
|
|
1250
1446
|
tokrepo <command> [args] [options]
|
|
@@ -1270,14 +1466,15 @@ ${C.bold}ACCOUNT${C.reset}
|
|
|
1270
1466
|
${C.cyan}help${C.reset} Show this help
|
|
1271
1467
|
|
|
1272
1468
|
${C.bold}PUSH OPTIONS${C.reset}
|
|
1273
|
-
${C.cyan}--
|
|
1274
|
-
${C.cyan}--
|
|
1469
|
+
${C.cyan}--private${C.reset} Keep asset private — only you can see it (recommended for personal assets)
|
|
1470
|
+
${C.cyan}--public${C.reset} Share asset publicly with the community
|
|
1275
1471
|
${C.cyan}--title${C.reset} "..." Set title (auto-detected from README or dir name)
|
|
1276
1472
|
${C.cyan}--desc${C.reset} "..." Set description
|
|
1277
1473
|
${C.cyan}--tag${C.reset} Skills Add tag (repeatable)
|
|
1278
1474
|
|
|
1279
1475
|
${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
1280
1476
|
Skills → .claude/skills/ (if .claude/ exists)
|
|
1477
|
+
Gemini → .gemini/GEMINI.md (with --target gemini)
|
|
1281
1478
|
Scripts → current dir (chmod +x)
|
|
1282
1479
|
Configs → project root
|
|
1283
1480
|
MCP → current dir (.json)
|
|
@@ -1286,8 +1483,11 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
|
1286
1483
|
${C.bold}EXAMPLES${C.reset}
|
|
1287
1484
|
tokrepo search "mcp server" # Find MCP configs
|
|
1288
1485
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
1289
|
-
tokrepo
|
|
1290
|
-
tokrepo push --
|
|
1486
|
+
tokrepo install c4b18aeb --target gemini # Install for Gemini CLI
|
|
1487
|
+
tokrepo push --private my-rules.md # Save one file privately
|
|
1488
|
+
tokrepo push --public skill.md # Share one file publicly
|
|
1489
|
+
tokrepo push --private . # Push current dir as private
|
|
1490
|
+
tokrepo push --public --title "My MCP" . # Push dir publicly with title
|
|
1291
1491
|
|
|
1292
1492
|
${C.bold}FILE TYPE AUTO-DETECTION${C.reset}
|
|
1293
1493
|
.sh .py .js .ts .mjs .go .rs → script
|