tokrepo 3.3.1 → 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 +188 -20
- 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}`);
|
|
@@ -773,41 +784,175 @@ async function cmdSearch() {
|
|
|
773
784
|
|
|
774
785
|
// ─── Install (smart pull with correct placement) ───
|
|
775
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
|
+
|
|
776
841
|
async function cmdInstall() {
|
|
777
|
-
const
|
|
842
|
+
const args = parseArgs(process.argv);
|
|
843
|
+
const target = args.positional[0];
|
|
778
844
|
if (!target) {
|
|
779
|
-
error(`Usage: tokrepo install <
|
|
845
|
+
error(`Usage: tokrepo install <target> [--target gemini] [--yes]
|
|
780
846
|
|
|
781
847
|
Examples:
|
|
782
|
-
tokrepo install awesome-cursor-rules
|
|
783
|
-
tokrepo install ca000374-f5d8-4d75-a30c-460fda0b6b0e
|
|
784
|
-
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`);
|
|
785
853
|
}
|
|
786
854
|
|
|
787
855
|
log(`\n${C.bold}tokrepo install${C.reset}\n`);
|
|
788
856
|
|
|
789
857
|
const config = readConfig();
|
|
790
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); };
|
|
791
919
|
|
|
792
920
|
// Resolve target to UUID
|
|
793
921
|
let uuid = target;
|
|
794
922
|
|
|
795
923
|
// URL format
|
|
796
|
-
const urlMatch = target.match(/workflows\/([
|
|
924
|
+
const urlMatch = target.match(/workflows\/([^/?#]+)/);
|
|
797
925
|
if (urlMatch) {
|
|
926
|
+
// URL may carry either UUID or slug-uuid8 — pass through to detail resolver below
|
|
798
927
|
uuid = urlMatch[1];
|
|
799
928
|
}
|
|
800
|
-
|
|
801
|
-
|
|
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)) {
|
|
802
947
|
// Search by name (normalize separators for better matching)
|
|
803
|
-
const normalizedTarget = normalizeQuery(
|
|
948
|
+
const normalizedTarget = normalizeQuery(uuid);
|
|
804
949
|
info(`Searching for "${normalizedTarget}"...`);
|
|
805
950
|
try {
|
|
806
951
|
const encoded = encodeURIComponent(normalizedTarget);
|
|
807
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);
|
|
808
953
|
|
|
809
954
|
if (!searchData.list || searchData.list.length === 0) {
|
|
810
|
-
|
|
955
|
+
die(`No asset found matching "${target}". Try: tokrepo search ${target}`);
|
|
811
956
|
}
|
|
812
957
|
|
|
813
958
|
// If title contains all query words, prefer it
|
|
@@ -821,7 +966,7 @@ Examples:
|
|
|
821
966
|
uuid = chosen.uuid;
|
|
822
967
|
info(`Found: ${C.bold}${chosen.title}${C.reset}`);
|
|
823
968
|
} catch (e) {
|
|
824
|
-
|
|
969
|
+
die(`Search failed: ${e.message}`);
|
|
825
970
|
}
|
|
826
971
|
}
|
|
827
972
|
|
|
@@ -834,7 +979,7 @@ Examples:
|
|
|
834
979
|
workflow = data.workflow;
|
|
835
980
|
files = data.workflow.files || [];
|
|
836
981
|
} catch (e) {
|
|
837
|
-
|
|
982
|
+
die(`Fetch failed: ${e.message}`);
|
|
838
983
|
}
|
|
839
984
|
|
|
840
985
|
log(`\n ${C.bold}${workflow.title}${C.reset}`);
|
|
@@ -868,10 +1013,34 @@ Examples:
|
|
|
868
1013
|
}
|
|
869
1014
|
|
|
870
1015
|
if (contents.length === 0) {
|
|
871
|
-
|
|
1016
|
+
die('No installable content found in this asset.');
|
|
872
1017
|
}
|
|
873
1018
|
|
|
874
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
|
+
}
|
|
875
1044
|
|
|
876
1045
|
// Smart install based on asset type
|
|
877
1046
|
let installed = 0;
|
|
@@ -928,7 +1097,7 @@ Examples:
|
|
|
928
1097
|
}
|
|
929
1098
|
}
|
|
930
1099
|
|
|
931
|
-
|
|
1100
|
+
let destPath = path.join(destDir, fileName);
|
|
932
1101
|
|
|
933
1102
|
// Path traversal guard: ensure resolved path stays inside destDir
|
|
934
1103
|
if (!path.resolve(destPath).startsWith(path.resolve(destDir) + path.sep) && path.resolve(destPath) !== path.resolve(destDir)) {
|
|
@@ -936,10 +1105,7 @@ Examples:
|
|
|
936
1105
|
continue;
|
|
937
1106
|
}
|
|
938
1107
|
|
|
939
|
-
|
|
940
|
-
if (fs.existsSync(destPath)) {
|
|
941
|
-
warn(`File exists: ${path.relative(process.cwd(), destPath)} (overwriting)`);
|
|
942
|
-
}
|
|
1108
|
+
destPath = pickWritablePath(destPath, Boolean(opts.yes));
|
|
943
1109
|
|
|
944
1110
|
fs.writeFileSync(destPath, item.content);
|
|
945
1111
|
|
|
@@ -1308,6 +1474,7 @@ ${C.bold}PUSH OPTIONS${C.reset}
|
|
|
1308
1474
|
|
|
1309
1475
|
${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
1310
1476
|
Skills → .claude/skills/ (if .claude/ exists)
|
|
1477
|
+
Gemini → .gemini/GEMINI.md (with --target gemini)
|
|
1311
1478
|
Scripts → current dir (chmod +x)
|
|
1312
1479
|
Configs → project root
|
|
1313
1480
|
MCP → current dir (.json)
|
|
@@ -1316,6 +1483,7 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
|
1316
1483
|
${C.bold}EXAMPLES${C.reset}
|
|
1317
1484
|
tokrepo search "mcp server" # Find MCP configs
|
|
1318
1485
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
1486
|
+
tokrepo install c4b18aeb --target gemini # Install for Gemini CLI
|
|
1319
1487
|
tokrepo push --private my-rules.md # Save one file privately
|
|
1320
1488
|
tokrepo push --public skill.md # Share one file publicly
|
|
1321
1489
|
tokrepo push --private . # Push current dir as private
|