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.
Files changed (2) hide show
  1. package/bin/tokrepo.js +188 -20
  2. 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.0';
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 target = process.argv[3];
842
+ const args = parseArgs(process.argv);
843
+ const target = args.positional[0];
778
844
  if (!target) {
779
- error(`Usage: tokrepo install <name-or-uuid>
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\/([a-f0-9-]+)/);
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
- // UUID format check
801
- else if (!/^[a-f0-9-]{36}$/.test(target)) {
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(target);
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
- error(`No asset found matching "${target}". Try: tokrepo search ${target}`);
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
- error(`Search failed: ${e.message}`);
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
- error(`Fetch failed: ${e.message}`);
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
- error('No installable content found in this asset.');
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
- const destPath = path.join(destDir, fileName);
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
- // Don't overwrite without warning
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokrepo",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
4
4
  "description": "AI assets for humans and agents — search, install, push. Like GitHub, for AI experience.",
5
5
  "bin": {
6
6
  "tokrepo": "bin/tokrepo.js"