tokrepo 3.3.2 → 3.4.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 +1182 -158
- package/package.json +1 -1
package/bin/tokrepo.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const https = require('https');
|
|
7
7
|
const http = require('http');
|
|
8
|
+
const os = require('os');
|
|
8
9
|
const readline = require('readline');
|
|
9
10
|
|
|
10
11
|
// ANSI colors
|
|
@@ -20,21 +21,41 @@ const C = {
|
|
|
20
21
|
white: '\x1b[37m',
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
const CONFIG_DIR = path.join(
|
|
24
|
+
const CONFIG_DIR = path.join(os.homedir(), '.tokrepo');
|
|
24
25
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
25
26
|
const PROJECT_CONFIG = '.tokrepo.json';
|
|
26
27
|
const DEFAULT_API = 'https://api.tokrepo.com';
|
|
27
|
-
const CLI_VERSION = '3.
|
|
28
|
-
const VERSION_CHECK_FILE = path.join(
|
|
28
|
+
const CLI_VERSION = '3.4.0';
|
|
29
|
+
const VERSION_CHECK_FILE = path.join(os.homedir(), '.tokrepo', '.version-check');
|
|
30
|
+
const CODEX_DIR = path.join(os.homedir(), '.codex');
|
|
31
|
+
const CODEX_SKILLS_DIR = path.join(CODEX_DIR, 'skills');
|
|
32
|
+
const CODEX_TOKREPO_DIR = path.join(CODEX_DIR, 'tokrepo');
|
|
33
|
+
const CODEX_MANIFEST_FILE = path.join(CODEX_TOKREPO_DIR, 'install-manifest.json');
|
|
34
|
+
const SUPPORTED_INSTALL_TARGETS = ['gemini', 'codex'];
|
|
29
35
|
|
|
30
36
|
// ─── Helpers ───
|
|
31
37
|
|
|
38
|
+
function wantsJson(argv = process.argv) {
|
|
39
|
+
return argv.includes('--json') || argv.some(arg => arg.startsWith('--json=') && arg !== '--json=false');
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
function log(msg) { console.log(msg); }
|
|
33
43
|
function success(msg) { log(`${C.green}✓${C.reset} ${msg}`); }
|
|
34
|
-
function error(msg) {
|
|
44
|
+
function error(msg) {
|
|
45
|
+
if (wantsJson()) {
|
|
46
|
+
console.error(JSON.stringify({ error: msg }, null, 2));
|
|
47
|
+
} else {
|
|
48
|
+
log(`${C.red}✗${C.reset} ${msg}`);
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
35
52
|
function warn(msg) { log(`${C.yellow}!${C.reset} ${msg}`); }
|
|
36
53
|
function info(msg) { log(`${C.cyan}→${C.reset} ${msg}`); }
|
|
37
54
|
|
|
55
|
+
function outputJson(data) {
|
|
56
|
+
console.log(JSON.stringify(data, null, 2));
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
function readConfig() {
|
|
39
60
|
// P0: TOKREPO_TOKEN env var takes priority (enables Agent automation)
|
|
40
61
|
const envToken = process.env.TOKREPO_TOKEN;
|
|
@@ -272,32 +293,55 @@ function parseArgs(argv) {
|
|
|
272
293
|
args.command = argv[i];
|
|
273
294
|
i++;
|
|
274
295
|
}
|
|
296
|
+
|
|
297
|
+
const valueFlags = new Set([
|
|
298
|
+
'title', 'desc', 'tag', 'target', 'keyword', 'types',
|
|
299
|
+
'page', 'page-size', 'page_size', 'sort-by', 'sort_by',
|
|
300
|
+
'time-window', 'time_window',
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
const assignFlag = (rawName, value = true) => {
|
|
304
|
+
const name = rawName.replace(/^--?/, '');
|
|
305
|
+
const normalized = name.replace(/-/g, '_');
|
|
306
|
+
if (normalized === 'tag') {
|
|
307
|
+
if (!args.flags.tags) args.flags.tags = [];
|
|
308
|
+
args.flags.tags.push(value);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (normalized === 'page_size') {
|
|
312
|
+
args.flags.pageSize = value;
|
|
313
|
+
} else if (normalized === 'sort_by') {
|
|
314
|
+
args.flags.sortBy = value;
|
|
315
|
+
} else if (normalized === 'time_window') {
|
|
316
|
+
args.flags.timeWindow = value;
|
|
317
|
+
} else if (normalized === 'dry_run') {
|
|
318
|
+
args.flags.dryRun = value;
|
|
319
|
+
} else if (normalized === 'approve_mcp') {
|
|
320
|
+
args.flags.approveMcp = value;
|
|
321
|
+
}
|
|
322
|
+
args.flags[normalized] = value;
|
|
323
|
+
};
|
|
324
|
+
|
|
275
325
|
while (i < argv.length) {
|
|
276
326
|
const arg = argv[i];
|
|
277
|
-
if (arg === '--
|
|
278
|
-
args.flags.
|
|
279
|
-
} else if (arg === '--private') {
|
|
280
|
-
args.flags.private = true;
|
|
281
|
-
} else if (arg === '--title' && i + 1 < argv.length) {
|
|
282
|
-
args.flags.title = argv[++i];
|
|
283
|
-
} else if (arg.startsWith('--title=')) {
|
|
284
|
-
args.flags.title = arg.split('=').slice(1).join('=');
|
|
285
|
-
} else if (arg === '--desc' && i + 1 < argv.length) {
|
|
286
|
-
args.flags.desc = argv[++i];
|
|
287
|
-
} else if (arg.startsWith('--desc=')) {
|
|
288
|
-
args.flags.desc = arg.split('=').slice(1).join('=');
|
|
289
|
-
} else if (arg === '--tag' && i + 1 < argv.length) {
|
|
290
|
-
if (!args.flags.tags) args.flags.tags = [];
|
|
291
|
-
args.flags.tags.push(argv[++i]);
|
|
292
|
-
} else if (arg.startsWith('--tag=')) {
|
|
293
|
-
if (!args.flags.tags) args.flags.tags = [];
|
|
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('=');
|
|
327
|
+
if (arg === '-h' || arg === '--help') {
|
|
328
|
+
args.flags.help = true;
|
|
299
329
|
} else if (arg === '-y' || arg === '--yes') {
|
|
300
330
|
args.flags.yes = true;
|
|
331
|
+
} else if (arg.startsWith('--')) {
|
|
332
|
+
const eqIndex = arg.indexOf('=');
|
|
333
|
+
if (eqIndex !== -1) {
|
|
334
|
+
const name = arg.slice(2, eqIndex);
|
|
335
|
+
const value = arg.slice(eqIndex + 1);
|
|
336
|
+
assignFlag(name, value);
|
|
337
|
+
} else {
|
|
338
|
+
const name = arg.slice(2);
|
|
339
|
+
if (valueFlags.has(name) && i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
|
|
340
|
+
assignFlag(name, argv[++i]);
|
|
341
|
+
} else {
|
|
342
|
+
assignFlag(name, true);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
301
345
|
} else if (!arg.startsWith('-')) {
|
|
302
346
|
args.positional.push(arg);
|
|
303
347
|
}
|
|
@@ -388,10 +432,10 @@ async function cmdLogin() {
|
|
|
388
432
|
|
|
389
433
|
if (useToken) {
|
|
390
434
|
// Manual token flow
|
|
391
|
-
info('Paste your API
|
|
435
|
+
info('Paste your API key (from https://tokrepo.com/en/my/settings)');
|
|
392
436
|
log('');
|
|
393
|
-
const token = await ask('API
|
|
394
|
-
if (!token) error('
|
|
437
|
+
const token = await ask('API Key:');
|
|
438
|
+
if (!token) error('API key is required');
|
|
395
439
|
return await saveAndVerifyToken(token);
|
|
396
440
|
}
|
|
397
441
|
|
|
@@ -684,7 +728,10 @@ function normalizeQuery(q) {
|
|
|
684
728
|
// - Full URL: "https://tokrepo.com/en/workflows/ca000374-f5d8-..."
|
|
685
729
|
// - @username/asset-name: search by author + keyword
|
|
686
730
|
// - Plain name: search by keyword
|
|
687
|
-
async function resolveAssetId(input, config, apiBase) {
|
|
731
|
+
async function resolveAssetId(input, config, apiBase, opts = {}) {
|
|
732
|
+
const emitInfo = (msg) => { if (!opts.quiet) info(msg); };
|
|
733
|
+
const emitWarn = (msg) => { if (!opts.quiet) warn(msg); };
|
|
734
|
+
|
|
688
735
|
// Already a UUID
|
|
689
736
|
if (/^[a-f0-9-]{36}$/.test(input)) return input;
|
|
690
737
|
|
|
@@ -697,7 +744,7 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
697
744
|
if (atMatch) {
|
|
698
745
|
const [, username, assetName] = atMatch;
|
|
699
746
|
const normalizedName = normalizeQuery(assetName);
|
|
700
|
-
|
|
747
|
+
emitInfo(`Searching for "${normalizedName}" by @${username}...`);
|
|
701
748
|
// Search by keyword, then filter by author nickname
|
|
702
749
|
const encoded = encodeURIComponent(normalizedName);
|
|
703
750
|
try {
|
|
@@ -710,7 +757,7 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
710
757
|
if (match) return match.uuid;
|
|
711
758
|
// Fallback: return first result
|
|
712
759
|
if (items.length > 0) {
|
|
713
|
-
|
|
760
|
+
emitWarn(`No exact match for @${username}, using best match: "${items[0].title}"`);
|
|
714
761
|
return items[0].uuid;
|
|
715
762
|
}
|
|
716
763
|
} catch { /* fall through */ }
|
|
@@ -719,7 +766,7 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
719
766
|
|
|
720
767
|
// Plain name: search by keyword (normalize separators)
|
|
721
768
|
const normalizedInput = normalizeQuery(input);
|
|
722
|
-
|
|
769
|
+
emitInfo(`Searching for "${normalizedInput}"...`);
|
|
723
770
|
const encoded = encodeURIComponent(normalizedInput);
|
|
724
771
|
try {
|
|
725
772
|
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=1&page_size=5&sort_by=views`, null, config?.token, apiBase);
|
|
@@ -732,19 +779,43 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
732
779
|
// ─── Search ───
|
|
733
780
|
|
|
734
781
|
async function cmdSearch() {
|
|
735
|
-
const
|
|
736
|
-
|
|
782
|
+
const args = parseArgs(process.argv);
|
|
783
|
+
const rawQuery = args.flags.keyword || args.positional.join(' ');
|
|
784
|
+
if (!rawQuery) {
|
|
785
|
+
showSearchHelp();
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
737
788
|
|
|
738
789
|
const query = normalizeQuery(rawQuery);
|
|
739
790
|
const displayQuery = query !== rawQuery ? `"${rawQuery}" → "${query}"` : `"${query}"`;
|
|
740
|
-
log(`\n${C.bold}tokrepo search${C.reset} ${displayQuery}\n`);
|
|
791
|
+
if (!args.flags.json) log(`\n${C.bold}tokrepo search${C.reset} ${displayQuery}\n`);
|
|
741
792
|
|
|
742
793
|
const config = readConfig();
|
|
743
794
|
const apiBase = config?.api || DEFAULT_API;
|
|
744
795
|
|
|
745
796
|
try {
|
|
746
797
|
const encoded = encodeURIComponent(query);
|
|
747
|
-
const
|
|
798
|
+
const pageSize = Number(args.flags.pageSize || (args.flags.all ? 200 : 20)) || 20;
|
|
799
|
+
const sortBy = args.flags.sortBy || 'views';
|
|
800
|
+
let page = Number(args.flags.page || 1) || 1;
|
|
801
|
+
let data = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=${page}&page_size=${pageSize}&sort_by=${encodeURIComponent(sortBy)}`, null, config?.token, apiBase);
|
|
802
|
+
|
|
803
|
+
if (args.flags.all) {
|
|
804
|
+
const list = [...(data.list || [])];
|
|
805
|
+
while (list.length < (data.total || 0)) {
|
|
806
|
+
page++;
|
|
807
|
+
const next = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=${page}&page_size=${pageSize}&sort_by=${encodeURIComponent(sortBy)}`, null, config?.token, apiBase);
|
|
808
|
+
const items = next.list || [];
|
|
809
|
+
if (items.length === 0) break;
|
|
810
|
+
list.push(...items);
|
|
811
|
+
}
|
|
812
|
+
data = { ...data, list };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (args.flags.json) {
|
|
816
|
+
outputJson({ query, total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
748
819
|
|
|
749
820
|
if (!data.list || data.list.length === 0) {
|
|
750
821
|
info('No assets found.');
|
|
@@ -782,6 +853,44 @@ async function cmdSearch() {
|
|
|
782
853
|
}
|
|
783
854
|
}
|
|
784
855
|
|
|
856
|
+
async function cmdDetail() {
|
|
857
|
+
const args = parseArgs(process.argv);
|
|
858
|
+
const target = args.positional[0];
|
|
859
|
+
if (!target) {
|
|
860
|
+
showDetailHelp();
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const config = readConfig();
|
|
865
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
866
|
+
|
|
867
|
+
try {
|
|
868
|
+
const uuid = await resolveAssetId(target, config, apiBase, { quiet: Boolean(args.flags.json) });
|
|
869
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${encodeURIComponent(uuid)}`, null, config?.token, apiBase);
|
|
870
|
+
if (args.flags.json) {
|
|
871
|
+
outputJson(data);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const workflow = data.workflow;
|
|
876
|
+
log(`\n${C.bold}tokrepo detail${C.reset}\n`);
|
|
877
|
+
log(` ${C.bold}${workflow.title}${C.reset}`);
|
|
878
|
+
if (workflow.description) log(` ${C.dim}${workflow.description}${C.reset}`);
|
|
879
|
+
log(`\n ${C.bold}UUID:${C.reset} ${workflow.uuid}`);
|
|
880
|
+
log(` ${C.bold}URL:${C.reset} ${C.cyan}https://tokrepo.com/en/workflows/${workflow.uuid}${C.reset}`);
|
|
881
|
+
if (workflow.tags && workflow.tags.length) {
|
|
882
|
+
log(` ${C.bold}Tags:${C.reset} ${workflow.tags.map(t => t.name || t.slug).join(', ')}`);
|
|
883
|
+
}
|
|
884
|
+
const fileCount = (workflow.files || []).length;
|
|
885
|
+
const stepCount = (workflow.steps || []).length;
|
|
886
|
+
log(` ${C.bold}Files:${C.reset} ${fileCount}`);
|
|
887
|
+
log(` ${C.bold}Steps:${C.reset} ${stepCount}`);
|
|
888
|
+
log('');
|
|
889
|
+
} catch (e) {
|
|
890
|
+
error(`Detail failed: ${e.message}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
785
894
|
// ─── Install (smart pull with correct placement) ───
|
|
786
895
|
|
|
787
896
|
function normalizeInstallTarget(target) {
|
|
@@ -790,6 +899,9 @@ function normalizeInstallTarget(target) {
|
|
|
790
899
|
const aliases = {
|
|
791
900
|
gemini: 'gemini',
|
|
792
901
|
'gemini-cli': 'gemini',
|
|
902
|
+
codex: 'codex',
|
|
903
|
+
'codex-cli': 'codex',
|
|
904
|
+
'openai-codex': 'codex',
|
|
793
905
|
};
|
|
794
906
|
return aliases[normalized] || normalized;
|
|
795
907
|
}
|
|
@@ -797,8 +909,8 @@ function normalizeInstallTarget(target) {
|
|
|
797
909
|
function validateInstallTarget(target) {
|
|
798
910
|
if (!target) return '';
|
|
799
911
|
const normalized = normalizeInstallTarget(target);
|
|
800
|
-
if (normalized
|
|
801
|
-
error(`Unsupported install target: ${target}. Supported targets:
|
|
912
|
+
if (!SUPPORTED_INSTALL_TARGETS.includes(normalized)) {
|
|
913
|
+
error(`Unsupported install target: ${target}. Supported targets: ${SUPPORTED_INSTALL_TARGETS.join(', ')}`);
|
|
802
914
|
}
|
|
803
915
|
return normalized;
|
|
804
916
|
}
|
|
@@ -838,27 +950,449 @@ function formatGeminiContent(workflow, contents) {
|
|
|
838
950
|
return `${parts.join('\n\n').trim()}\n`;
|
|
839
951
|
}
|
|
840
952
|
|
|
953
|
+
function getWorkflowAssetType(workflow) {
|
|
954
|
+
if (!workflow || !workflow.tags || workflow.tags.length === 0) return 'other';
|
|
955
|
+
return (workflow.tags[0].slug || workflow.tags[0].name || '').toLowerCase();
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function extractInstallableContents(workflow, assetType) {
|
|
959
|
+
const contents = [];
|
|
960
|
+
const files = workflow.files || [];
|
|
961
|
+
|
|
962
|
+
if (files.length > 0) {
|
|
963
|
+
for (const f of files) {
|
|
964
|
+
if (f.content && !f.content.startsWith('PK')) {
|
|
965
|
+
contents.push({
|
|
966
|
+
name: f.name || 'SKILL.md',
|
|
967
|
+
content: f.content,
|
|
968
|
+
type: f.type || f.file_type || f.fileType || detectFileType(f.name || ''),
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (contents.length === 0 && workflow.steps) {
|
|
975
|
+
for (const step of workflow.steps) {
|
|
976
|
+
const content = step.prompt_template || step.promptTemplate;
|
|
977
|
+
if (content && !content.startsWith('PK')) {
|
|
978
|
+
const name = (step.title || `step-${step.step_order || contents.length + 1}`).replace(/[/\\?%*:|"<>]/g, '-');
|
|
979
|
+
contents.push({ name, content, type: assetType || 'other' });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return contents;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function sha256(content) {
|
|
988
|
+
return crypto.createHash('sha256').update(String(content || '')).digest('hex');
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function slugify(input, fallback = 'asset') {
|
|
992
|
+
const raw = String(input || '')
|
|
993
|
+
.normalize('NFKD')
|
|
994
|
+
.replace(/[^\x00-\x7F]/g, '')
|
|
995
|
+
.toLowerCase()
|
|
996
|
+
.replace(/['"]/g, '')
|
|
997
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
998
|
+
.replace(/^-+|-+$/g, '')
|
|
999
|
+
.replace(/-{2,}/g, '-');
|
|
1000
|
+
return raw || fallback;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function sanitizePathSegment(input, fallback = 'file') {
|
|
1004
|
+
const cleaned = String(input || '')
|
|
1005
|
+
.replace(/[/\\?%*:|"<>]/g, '-')
|
|
1006
|
+
.replace(/^\.+$/, '')
|
|
1007
|
+
.replace(/^\.+/, '')
|
|
1008
|
+
.trim();
|
|
1009
|
+
return cleaned || fallback;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function sanitizeRelativePath(input, fallback = 'file.md') {
|
|
1013
|
+
const normalized = String(input || fallback).replace(/\\/g, '/');
|
|
1014
|
+
const parts = normalized
|
|
1015
|
+
.split('/')
|
|
1016
|
+
.filter(Boolean)
|
|
1017
|
+
.map((part, index) => sanitizePathSegment(part, index === 0 ? fallback : 'file'));
|
|
1018
|
+
let rel = parts.join('/');
|
|
1019
|
+
if (!rel) rel = fallback;
|
|
1020
|
+
if (!path.extname(rel)) rel += '.md';
|
|
1021
|
+
return rel;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function ensureInside(baseDir, destPath) {
|
|
1025
|
+
const resolvedBase = path.resolve(baseDir);
|
|
1026
|
+
const resolvedDest = path.resolve(destPath);
|
|
1027
|
+
return resolvedDest === resolvedBase || resolvedDest.startsWith(resolvedBase + path.sep);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function getFrontmatter(content) {
|
|
1031
|
+
const text = String(content || '').replace(/^\uFEFF/, '');
|
|
1032
|
+
const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
1033
|
+
if (!match) return null;
|
|
1034
|
+
return { raw: match[0], body: match[1], rest: text.slice(match[0].length) };
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function getFrontmatterValue(content, key) {
|
|
1038
|
+
const fm = getFrontmatter(content);
|
|
1039
|
+
if (!fm) return '';
|
|
1040
|
+
const re = new RegExp(`^${key}\\s*:\\s*(.+)$`, 'im');
|
|
1041
|
+
const match = fm.body.match(re);
|
|
1042
|
+
if (!match) return '';
|
|
1043
|
+
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function isCodexSkillDocument(item) {
|
|
1047
|
+
const content = item?.content || '';
|
|
1048
|
+
const name = getFrontmatterValue(content, 'name');
|
|
1049
|
+
const description = getFrontmatterValue(content, 'description');
|
|
1050
|
+
return Boolean(name && description) || /^skill\.md$/i.test(path.basename(item?.name || ''));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function yamlQuoted(value) {
|
|
1054
|
+
return JSON.stringify(String(value || '').replace(/\s+/g, ' ').trim());
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function ensureCodexSkillFrontmatter(content, name, description) {
|
|
1058
|
+
const text = String(content || '').replace(/^\uFEFF/, '').trim();
|
|
1059
|
+
const fm = getFrontmatter(text);
|
|
1060
|
+
if (!fm) {
|
|
1061
|
+
return `---\nname: ${name}\ndescription: ${yamlQuoted(description)}\n---\n\n${text}\n`;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const lines = fm.body.split('\n');
|
|
1065
|
+
const hasName = lines.some(line => /^name\s*:/i.test(line));
|
|
1066
|
+
const hasDescription = lines.some(line => /^description\s*:/i.test(line));
|
|
1067
|
+
const next = [];
|
|
1068
|
+
if (!hasName) next.push(`name: ${name}`);
|
|
1069
|
+
if (!hasDescription) next.push(`description: ${yamlQuoted(description)}`);
|
|
1070
|
+
next.push(...lines);
|
|
1071
|
+
return `---\n${next.join('\n')}\n---\n${fm.rest.trim() ? `\n${fm.rest.trim()}\n` : '\n'}`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function getCodexDescription(workflow, item) {
|
|
1075
|
+
const fromItem = getFrontmatterValue(item?.content || '', 'description');
|
|
1076
|
+
if (fromItem) return fromItem;
|
|
1077
|
+
const title = workflow?.title || item?.name || 'TokRepo asset';
|
|
1078
|
+
const desc = workflow?.description || '';
|
|
1079
|
+
return desc ? desc.substring(0, 240) : `Use ${title} from TokRepo.`;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function codexSkillDirName(workflow, item, suffix = '') {
|
|
1083
|
+
const uuid8 = (workflow?.uuid || '').substring(0, 8) || 'asset';
|
|
1084
|
+
const fmName = getFrontmatterValue(item?.content || '', 'name');
|
|
1085
|
+
const base = fmName || item?.name || workflow?.slug || workflow?.title || uuid8;
|
|
1086
|
+
const baseSlug = slugify(base, uuid8).replace(/-md$/, '');
|
|
1087
|
+
const withSuffix = suffix ? `${baseSlug}-${slugify(suffix, 'part')}` : baseSlug;
|
|
1088
|
+
if (withSuffix.startsWith('tokrepo-') && withSuffix.endsWith(`-${uuid8}`)) return withSuffix;
|
|
1089
|
+
if (withSuffix.startsWith('tokrepo-')) return `${withSuffix}-${uuid8}`;
|
|
1090
|
+
if (withSuffix.endsWith(`-${uuid8}`)) return `tokrepo-${withSuffix}`;
|
|
1091
|
+
return `tokrepo-${withSuffix}-${uuid8}`;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function explicitInstallMode(workflow) {
|
|
1095
|
+
const candidates = [
|
|
1096
|
+
workflow?.installMode,
|
|
1097
|
+
workflow?.install_mode,
|
|
1098
|
+
workflow?.metadata?.installMode,
|
|
1099
|
+
workflow?.metadata?.install_mode,
|
|
1100
|
+
].filter(Boolean);
|
|
1101
|
+
const mode = String(candidates[0] || '').toLowerCase();
|
|
1102
|
+
return ['single', 'bundle', 'split'].includes(mode) ? mode : '';
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function inferCodexInstallMode(workflow, contents) {
|
|
1106
|
+
const explicit = explicitInstallMode(workflow);
|
|
1107
|
+
if (explicit) return explicit;
|
|
1108
|
+
if (contents.length <= 1) return 'single';
|
|
1109
|
+
const skillDocs = contents.filter(isCodexSkillDocument);
|
|
1110
|
+
if (skillDocs.length > 1 && skillDocs.length === contents.length) return 'split';
|
|
1111
|
+
return 'bundle';
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function analyzeInstallRisks(fileName, content, type) {
|
|
1115
|
+
const risks = new Set();
|
|
1116
|
+
const lowerName = String(fileName || '').toLowerCase();
|
|
1117
|
+
const text = String(content || '');
|
|
1118
|
+
if (type === 'script' || /\.(sh|py|js|mjs|ts|rb|go|rs|lua)$/.test(lowerName) || /^#!\//.test(text)) {
|
|
1119
|
+
risks.add('executable');
|
|
1120
|
+
}
|
|
1121
|
+
if (lowerName.endsWith('.mcp.json') || /"mcpServers"\s*:/.test(text) || /\bmcpServers\s*:/.test(text)) {
|
|
1122
|
+
risks.add('mcp');
|
|
1123
|
+
}
|
|
1124
|
+
if (/\b(PATH|HOME|TOKEN|API_KEY|SECRET|PASSWORD|OPENAI_API_KEY|ANTHROPIC_API_KEY)\b/.test(text)) {
|
|
1125
|
+
risks.add('env');
|
|
1126
|
+
}
|
|
1127
|
+
if (/(^|[\s"'=])\/(Users|opt|usr|var|etc|tmp)\//.test(text) || /[A-Za-z]:\\/.test(text)) {
|
|
1128
|
+
risks.add('absolute-path');
|
|
1129
|
+
}
|
|
1130
|
+
return Array.from(risks);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function buildBundleEntrypoint(workflow, contents, skillName) {
|
|
1134
|
+
const title = workflow.title || 'TokRepo Asset';
|
|
1135
|
+
const sourceUrl = `https://tokrepo.com/en/workflows/${workflow.uuid}`;
|
|
1136
|
+
const fileList = contents
|
|
1137
|
+
.map((item, index) => `- ${sanitizeRelativePath(item.name || `file-${index + 1}.md`)}`)
|
|
1138
|
+
.join('\n');
|
|
1139
|
+
const body = `# ${title}\n\nThis Codex skill was installed from TokRepo as a bundle. Use the files in this directory as the source material for the skill.\n\n${fileList}\n\nSource: ${sourceUrl}\n`;
|
|
1140
|
+
return ensureCodexSkillFrontmatter(body, skillName, getCodexDescription(workflow));
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function addPlanFile(plan, destPath, content, sourceName, type) {
|
|
1144
|
+
const riskFlags = analyzeInstallRisks(sourceName || destPath, content, type);
|
|
1145
|
+
plan.files.push({
|
|
1146
|
+
path: destPath,
|
|
1147
|
+
sourceName: sourceName || path.basename(destPath),
|
|
1148
|
+
sha256: sha256(content),
|
|
1149
|
+
bytes: Buffer.byteLength(String(content || '')),
|
|
1150
|
+
riskFlags,
|
|
1151
|
+
content,
|
|
1152
|
+
});
|
|
1153
|
+
for (const risk of riskFlags) {
|
|
1154
|
+
if (!plan.risks.includes(risk)) plan.risks.push(risk);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
1159
|
+
const installMode = opts.installMode || inferCodexInstallMode(workflow, contents);
|
|
1160
|
+
const plan = {
|
|
1161
|
+
uuid: workflow.uuid,
|
|
1162
|
+
title: workflow.title,
|
|
1163
|
+
sourceUrl: `https://tokrepo.com/en/workflows/${workflow.uuid}`,
|
|
1164
|
+
targetTool: 'codex',
|
|
1165
|
+
installMode,
|
|
1166
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
1167
|
+
files: [],
|
|
1168
|
+
risks: [],
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
if (installMode === 'split') {
|
|
1172
|
+
const usedDirs = new Set();
|
|
1173
|
+
contents.forEach((item, index) => {
|
|
1174
|
+
const skillName = slugify(getFrontmatterValue(item.content, 'name') || item.name || `${workflow.title}-${index + 1}`, `${workflow.uuid.substring(0, 8)}-${index + 1}`);
|
|
1175
|
+
const baseDirName = codexSkillDirName(workflow, item, contents.length > 1 && !getFrontmatterValue(item.content, 'name') ? String(index + 1) : '');
|
|
1176
|
+
let dirName = baseDirName;
|
|
1177
|
+
let duplicateIndex = 2;
|
|
1178
|
+
while (usedDirs.has(dirName)) {
|
|
1179
|
+
dirName = `${baseDirName}-${duplicateIndex}`;
|
|
1180
|
+
duplicateIndex++;
|
|
1181
|
+
}
|
|
1182
|
+
usedDirs.add(dirName);
|
|
1183
|
+
const destDir = path.join(CODEX_SKILLS_DIR, dirName);
|
|
1184
|
+
const destPath = path.join(destDir, 'SKILL.md');
|
|
1185
|
+
const content = ensureCodexSkillFrontmatter(item.content, skillName, getCodexDescription(workflow, item));
|
|
1186
|
+
addPlanFile(plan, destPath, content, item.name, item.type);
|
|
1187
|
+
});
|
|
1188
|
+
return plan;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const primaryItem = contents.find(item => /^skill\.md$/i.test(path.basename(item.name || ''))) || contents[0];
|
|
1192
|
+
const skillName = slugify(getFrontmatterValue(primaryItem?.content || '', 'name') || workflow.slug || workflow.title, workflow.uuid.substring(0, 8));
|
|
1193
|
+
const dirItem = getFrontmatterValue(primaryItem?.content || '', 'name') ? primaryItem : null;
|
|
1194
|
+
const destDir = path.join(CODEX_SKILLS_DIR, codexSkillDirName(workflow, dirItem));
|
|
1195
|
+
plan.baseDir = destDir;
|
|
1196
|
+
|
|
1197
|
+
if (installMode === 'single' || contents.length === 1) {
|
|
1198
|
+
const item = primaryItem;
|
|
1199
|
+
const content = ensureCodexSkillFrontmatter(item.content, skillName, getCodexDescription(workflow, item));
|
|
1200
|
+
addPlanFile(plan, path.join(destDir, 'SKILL.md'), content, item.name, item.type);
|
|
1201
|
+
return plan;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
let hasEntrypoint = false;
|
|
1205
|
+
const usedRelNames = new Set();
|
|
1206
|
+
for (let i = 0; i < contents.length; i++) {
|
|
1207
|
+
const item = contents[i];
|
|
1208
|
+
const relName = sanitizeRelativePath(item.name || `file-${i + 1}.md`);
|
|
1209
|
+
let destName = /^skill\.md$/i.test(path.basename(relName)) ? 'SKILL.md' : relName;
|
|
1210
|
+
if (usedRelNames.has(destName)) {
|
|
1211
|
+
const ext = path.extname(destName);
|
|
1212
|
+
const base = destName.slice(0, destName.length - ext.length);
|
|
1213
|
+
let duplicateIndex = 2;
|
|
1214
|
+
let candidate = `${base}-${duplicateIndex}${ext}`;
|
|
1215
|
+
while (usedRelNames.has(candidate)) {
|
|
1216
|
+
duplicateIndex++;
|
|
1217
|
+
candidate = `${base}-${duplicateIndex}${ext}`;
|
|
1218
|
+
}
|
|
1219
|
+
destName = candidate;
|
|
1220
|
+
}
|
|
1221
|
+
usedRelNames.add(destName);
|
|
1222
|
+
const destPath = path.join(destDir, destName);
|
|
1223
|
+
const content = destName === 'SKILL.md'
|
|
1224
|
+
? ensureCodexSkillFrontmatter(item.content, skillName, getCodexDescription(workflow, item))
|
|
1225
|
+
: `${String(item.content || '').trim()}\n`;
|
|
1226
|
+
if (destName === 'SKILL.md') hasEntrypoint = true;
|
|
1227
|
+
addPlanFile(plan, destPath, content, item.name, item.type);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (!hasEntrypoint) {
|
|
1231
|
+
addPlanFile(plan, path.join(destDir, 'SKILL.md'), buildBundleEntrypoint(workflow, contents, skillName), 'SKILL.md', 'skill');
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return plan;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function publicInstallPlan(plan) {
|
|
1238
|
+
return {
|
|
1239
|
+
uuid: plan.uuid,
|
|
1240
|
+
title: plan.title,
|
|
1241
|
+
sourceUrl: plan.sourceUrl,
|
|
1242
|
+
targetTool: plan.targetTool,
|
|
1243
|
+
installMode: plan.installMode,
|
|
1244
|
+
manifestPath: plan.manifestPath,
|
|
1245
|
+
baseDir: plan.baseDir,
|
|
1246
|
+
risks: plan.risks,
|
|
1247
|
+
files: plan.files.map(file => ({
|
|
1248
|
+
path: file.path,
|
|
1249
|
+
sourceName: file.sourceName,
|
|
1250
|
+
sha256: file.sha256,
|
|
1251
|
+
bytes: file.bytes,
|
|
1252
|
+
riskFlags: file.riskFlags,
|
|
1253
|
+
exists: fs.existsSync(file.path),
|
|
1254
|
+
})),
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function hasCodexInstallRisks(plan) {
|
|
1259
|
+
return (plan.risks || []).some(risk => ['mcp', 'executable', 'env', 'absolute-path'].includes(risk));
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function formatRiskLine(file) {
|
|
1263
|
+
if (!file.riskFlags || file.riskFlags.length === 0) return '';
|
|
1264
|
+
return `${file.sourceName || path.basename(file.path)}: ${file.riskFlags.join(', ')}`;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
async function confirmCodexInstallRisks(plan, opts = {}) {
|
|
1268
|
+
if (opts.dryRun || opts.stage || !hasCodexInstallRisks(plan)) return;
|
|
1269
|
+
if (opts.approveMcp || opts.approve_mcp || opts.yes) return;
|
|
1270
|
+
|
|
1271
|
+
if (opts.json || opts.throwOnError || process.env.TOKREPO_NONINTERACTIVE === '1') {
|
|
1272
|
+
throw new Error(`Install plan includes risky content (${plan.risks.join(', ')}). Re-run with --dry-run to inspect or --approve-mcp to approve writing the Codex skill bundle.`);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
warn(`This asset contains ${plan.risks.join(', ')} content.`);
|
|
1276
|
+
log(` ${C.dim}TokRepo will only write files under ${CODEX_SKILLS_DIR}; it will not merge MCP configs, modify PATH, or execute scripts.${C.reset}`);
|
|
1277
|
+
const riskyFiles = plan.files
|
|
1278
|
+
.map(formatRiskLine)
|
|
1279
|
+
.filter(Boolean)
|
|
1280
|
+
.slice(0, 8);
|
|
1281
|
+
for (const line of riskyFiles) {
|
|
1282
|
+
log(` ${C.yellow}!${C.reset} ${line}`);
|
|
1283
|
+
}
|
|
1284
|
+
if (plan.files.length > riskyFiles.length) {
|
|
1285
|
+
log(` ${C.dim}...and ${plan.files.length - riskyFiles.length} more file(s) in the plan${C.reset}`);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const answer = await ask('Write this Codex skill bundle anyway? (y/N):');
|
|
1289
|
+
if (answer.toLowerCase() !== 'y') {
|
|
1290
|
+
throw new Error('Install aborted.');
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function stageCodexInstallPlan(plan) {
|
|
1295
|
+
const stagedDir = path.join(CODEX_TOKREPO_DIR, 'staged');
|
|
1296
|
+
if (!fs.existsSync(stagedDir)) {
|
|
1297
|
+
fs.mkdirSync(stagedDir, { recursive: true, mode: 0o700 });
|
|
1298
|
+
}
|
|
1299
|
+
const stagePath = path.join(stagedDir, `${plan.uuid}.install-plan.json`);
|
|
1300
|
+
fs.writeFileSync(stagePath, `${JSON.stringify(publicInstallPlan(plan), null, 2)}\n`, { mode: 0o600 });
|
|
1301
|
+
return stagePath;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function readCodexManifest() {
|
|
1305
|
+
try {
|
|
1306
|
+
const parsed = JSON.parse(fs.readFileSync(CODEX_MANIFEST_FILE, 'utf8'));
|
|
1307
|
+
if (Array.isArray(parsed.installs)) return parsed;
|
|
1308
|
+
} catch {}
|
|
1309
|
+
return { schemaVersion: 1, installs: [] };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function writeCodexManifestRecord(plan, installedFiles) {
|
|
1313
|
+
if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
|
|
1314
|
+
fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
|
|
1315
|
+
}
|
|
1316
|
+
const manifest = readCodexManifest();
|
|
1317
|
+
const installedAt = new Date().toISOString();
|
|
1318
|
+
const record = {
|
|
1319
|
+
uuid: plan.uuid,
|
|
1320
|
+
title: plan.title,
|
|
1321
|
+
sourceUrl: plan.sourceUrl,
|
|
1322
|
+
targetTool: 'codex',
|
|
1323
|
+
installMode: plan.installMode,
|
|
1324
|
+
installedAt,
|
|
1325
|
+
installedFiles: installedFiles.map(file => ({
|
|
1326
|
+
path: file.path,
|
|
1327
|
+
sourceName: file.sourceName,
|
|
1328
|
+
sha256: file.sha256,
|
|
1329
|
+
bytes: file.bytes,
|
|
1330
|
+
riskFlags: file.riskFlags,
|
|
1331
|
+
})),
|
|
1332
|
+
risks: plan.risks,
|
|
1333
|
+
};
|
|
1334
|
+
manifest.installs = manifest.installs.filter(item => !(item.uuid === plan.uuid && item.targetTool === 'codex'));
|
|
1335
|
+
manifest.installs.push(record);
|
|
1336
|
+
manifest.updatedAt = installedAt;
|
|
1337
|
+
fs.writeFileSync(CODEX_MANIFEST_FILE, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
1338
|
+
return record;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function executeCodexInstallPlan(plan, opts = {}) {
|
|
1342
|
+
if (opts.dryRun) return { dryRun: true, plan: publicInstallPlan(plan), installedFiles: [] };
|
|
1343
|
+
if (opts.stage) {
|
|
1344
|
+
const stagePath = stageCodexInstallPlan(plan);
|
|
1345
|
+
return { dryRun: true, staged: true, stagePath, plan: publicInstallPlan(plan), installedFiles: [] };
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const installedFiles = [];
|
|
1349
|
+
for (const file of plan.files) {
|
|
1350
|
+
const destDir = path.dirname(file.path);
|
|
1351
|
+
if (!ensureInside(CODEX_SKILLS_DIR, file.path)) {
|
|
1352
|
+
throw new Error(`Install path escaped Codex skills directory: ${file.path}`);
|
|
1353
|
+
}
|
|
1354
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
1355
|
+
fs.writeFileSync(file.path, file.content);
|
|
1356
|
+
installedFiles.push({
|
|
1357
|
+
path: file.path,
|
|
1358
|
+
sourceName: file.sourceName,
|
|
1359
|
+
sha256: sha256(file.content),
|
|
1360
|
+
bytes: Buffer.byteLength(String(file.content || '')),
|
|
1361
|
+
riskFlags: file.riskFlags,
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const manifestRecord = writeCodexManifestRecord(plan, installedFiles);
|
|
1366
|
+
return { dryRun: false, plan: publicInstallPlan(plan), installedFiles, manifestRecord };
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function installCodexAsset(workflow, contents, opts = {}) {
|
|
1370
|
+
const plan = buildCodexInstallPlan(workflow, contents, opts);
|
|
1371
|
+
await confirmCodexInstallRisks(plan, opts);
|
|
1372
|
+
return executeCodexInstallPlan(plan, opts);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
841
1375
|
async function cmdInstall() {
|
|
842
1376
|
const args = parseArgs(process.argv);
|
|
843
1377
|
const target = args.positional[0];
|
|
844
1378
|
if (!target) {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
Examples:
|
|
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`);
|
|
1379
|
+
showInstallHelp();
|
|
1380
|
+
process.exit(1);
|
|
853
1381
|
}
|
|
854
1382
|
|
|
855
|
-
log(`\n${C.bold}tokrepo install${C.reset}\n`);
|
|
1383
|
+
if (!args.flags.json) log(`\n${C.bold}tokrepo install${C.reset}\n`);
|
|
856
1384
|
|
|
857
1385
|
const config = readConfig();
|
|
858
1386
|
const apiBase = config?.api || DEFAULT_API;
|
|
859
1387
|
const installOpts = {
|
|
860
1388
|
targetTool: validateInstallTarget(args.flags.target),
|
|
861
1389
|
yes: Boolean(args.flags.yes),
|
|
1390
|
+
update: Boolean(args.flags.update),
|
|
1391
|
+
dryRun: Boolean(args.flags.dryRun || args.flags.dry_run),
|
|
1392
|
+
stage: Boolean(args.flags.stage),
|
|
1393
|
+
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
1394
|
+
json: Boolean(args.flags.json),
|
|
1395
|
+
manifest: Boolean(args.flags.manifest),
|
|
862
1396
|
};
|
|
863
1397
|
|
|
864
1398
|
// pack/<slug> dispatch — install entire theme pack
|
|
@@ -871,7 +1405,10 @@ Examples:
|
|
|
871
1405
|
return;
|
|
872
1406
|
}
|
|
873
1407
|
|
|
874
|
-
await installOneAsset(target, config, apiBase, installOpts);
|
|
1408
|
+
const result = await installOneAsset(target, config, apiBase, installOpts);
|
|
1409
|
+
if (args.flags.json) {
|
|
1410
|
+
outputJson(result);
|
|
1411
|
+
}
|
|
875
1412
|
}
|
|
876
1413
|
|
|
877
1414
|
// Install all assets in a theme pack — sequentially, continue past per-item errors
|
|
@@ -916,6 +1453,7 @@ async function installPack(slug, config, apiBase, opts) {
|
|
|
916
1453
|
async function installOneAsset(target, config, apiBase, opts) {
|
|
917
1454
|
opts = opts || {};
|
|
918
1455
|
const die = (msg) => { if (opts.throwOnError) throw new Error(msg); error(msg); };
|
|
1456
|
+
const emitInfo = (msg) => { if (!opts.json) info(msg); };
|
|
919
1457
|
|
|
920
1458
|
// Resolve target to UUID
|
|
921
1459
|
let uuid = target;
|
|
@@ -946,7 +1484,7 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
946
1484
|
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
1485
|
// Search by name (normalize separators for better matching)
|
|
948
1486
|
const normalizedTarget = normalizeQuery(uuid);
|
|
949
|
-
|
|
1487
|
+
emitInfo(`Searching for "${normalizedTarget}"...`);
|
|
950
1488
|
try {
|
|
951
1489
|
const encoded = encodeURIComponent(normalizedTarget);
|
|
952
1490
|
const searchData = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=1&page_size=5&sort_by=views`, null, config?.token, apiBase);
|
|
@@ -964,61 +1502,87 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
964
1502
|
const chosen = exact || searchData.list[0];
|
|
965
1503
|
|
|
966
1504
|
uuid = chosen.uuid;
|
|
967
|
-
|
|
1505
|
+
emitInfo(`Found: ${C.bold}${chosen.title}${C.reset}`);
|
|
968
1506
|
} catch (e) {
|
|
969
1507
|
die(`Search failed: ${e.message}`);
|
|
970
1508
|
}
|
|
971
1509
|
}
|
|
972
1510
|
|
|
973
1511
|
// Fetch the asset
|
|
974
|
-
|
|
1512
|
+
emitInfo(`Fetching ${uuid.substring(0, 8)}...`);
|
|
975
1513
|
|
|
976
|
-
let workflow
|
|
1514
|
+
let workflow;
|
|
977
1515
|
try {
|
|
978
1516
|
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${uuid}`, null, config?.token, apiBase);
|
|
979
1517
|
workflow = data.workflow;
|
|
980
|
-
files = data.workflow.files || [];
|
|
981
1518
|
} catch (e) {
|
|
982
1519
|
die(`Fetch failed: ${e.message}`);
|
|
983
1520
|
}
|
|
984
1521
|
|
|
985
|
-
|
|
986
|
-
|
|
1522
|
+
if (!opts.json) {
|
|
1523
|
+
log(`\n ${C.bold}${workflow.title}${C.reset}`);
|
|
1524
|
+
if (workflow.description) log(` ${C.dim}${workflow.description.substring(0, 100)}${C.reset}`);
|
|
1525
|
+
}
|
|
987
1526
|
|
|
988
1527
|
// Determine asset type from tags
|
|
989
|
-
let assetType =
|
|
990
|
-
if (workflow.tags && workflow.tags.length > 0) {
|
|
991
|
-
assetType = (workflow.tags[0].slug || workflow.tags[0].name || '').toLowerCase();
|
|
992
|
-
}
|
|
1528
|
+
let assetType = getWorkflowAssetType(workflow);
|
|
993
1529
|
|
|
994
1530
|
// Get content — prefer files, fallback to steps
|
|
995
|
-
const contents =
|
|
1531
|
+
const contents = extractInstallableContents(workflow, assetType);
|
|
996
1532
|
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
if (f.content && !f.content.startsWith('PK')) {
|
|
1000
|
-
contents.push({ name: f.name, content: f.content, type: f.file_type || f.fileType || 'other' });
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1533
|
+
if (contents.length === 0) {
|
|
1534
|
+
die('No installable content found in this asset.');
|
|
1003
1535
|
}
|
|
1004
1536
|
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1537
|
+
if (!opts.json) log('');
|
|
1538
|
+
const targetTool = normalizeInstallTarget(opts.targetTool);
|
|
1539
|
+
|
|
1540
|
+
if (targetTool === 'codex') {
|
|
1541
|
+
let result;
|
|
1542
|
+
try {
|
|
1543
|
+
result = await installCodexAsset(workflow, contents, opts);
|
|
1544
|
+
} catch (e) {
|
|
1545
|
+
die(e.message);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (!opts.json) {
|
|
1549
|
+
const plan = result.plan;
|
|
1550
|
+
if (opts.stage) {
|
|
1551
|
+
info(`Staged install plan: ${result.stagePath}`);
|
|
1552
|
+
info(`No Codex skill files were written. Re-run with --approve-mcp or --yes to install.`);
|
|
1553
|
+
} else if (opts.dryRun) {
|
|
1554
|
+
info(`Dry run: ${plan.files.length} file(s) would be installed to ${CODEX_SKILLS_DIR}`);
|
|
1555
|
+
for (const file of plan.files) {
|
|
1556
|
+
const rel = path.relative(os.homedir(), file.path);
|
|
1557
|
+
log(` ${C.dim}•${C.reset} ~/${rel}`);
|
|
1558
|
+
if (file.riskFlags.length) log(` ${C.yellow}${file.riskFlags.join(', ')}${C.reset}`);
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
for (const file of result.installedFiles) {
|
|
1562
|
+
const relPath = path.relative(os.homedir(), file.path);
|
|
1563
|
+
success(`Installed: ~/${relPath}`);
|
|
1564
|
+
}
|
|
1565
|
+
log('');
|
|
1566
|
+
success(`${result.installedFiles.length} file(s) installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1567
|
+
log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
|
|
1568
|
+
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1011
1569
|
}
|
|
1012
1570
|
}
|
|
1013
|
-
}
|
|
1014
1571
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1572
|
+
return {
|
|
1573
|
+
uuid,
|
|
1574
|
+
title: workflow.title,
|
|
1575
|
+
targetTool: 'codex',
|
|
1576
|
+
dryRun: Boolean(opts.dryRun || opts.stage),
|
|
1577
|
+
staged: Boolean(result.staged),
|
|
1578
|
+
stagePath: result.stagePath,
|
|
1579
|
+
installMode: result.plan.installMode,
|
|
1580
|
+
installedFiles: result.installedFiles || [],
|
|
1581
|
+
plan: result.plan,
|
|
1582
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
1583
|
+
};
|
|
1017
1584
|
}
|
|
1018
1585
|
|
|
1019
|
-
log('');
|
|
1020
|
-
const targetTool = normalizeInstallTarget(opts.targetTool);
|
|
1021
|
-
|
|
1022
1586
|
if (targetTool === 'gemini') {
|
|
1023
1587
|
const destDir = path.join(process.cwd(), '.gemini');
|
|
1024
1588
|
if (!fs.existsSync(destDir)) {
|
|
@@ -1039,7 +1603,13 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1039
1603
|
log('');
|
|
1040
1604
|
success(`1 file installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1041
1605
|
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1042
|
-
return
|
|
1606
|
+
return {
|
|
1607
|
+
uuid,
|
|
1608
|
+
title: workflow.title,
|
|
1609
|
+
targetTool: 'gemini',
|
|
1610
|
+
installedFiles: [{ path: destPath }],
|
|
1611
|
+
sourceUrl: `https://tokrepo.com/en/workflows/${uuid}`,
|
|
1612
|
+
};
|
|
1043
1613
|
}
|
|
1044
1614
|
|
|
1045
1615
|
// Smart install based on asset type
|
|
@@ -1122,6 +1692,13 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1122
1692
|
log('');
|
|
1123
1693
|
success(`${installed} file(s) installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1124
1694
|
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1695
|
+
return {
|
|
1696
|
+
uuid,
|
|
1697
|
+
title: workflow.title,
|
|
1698
|
+
targetTool: targetTool || 'project',
|
|
1699
|
+
installed,
|
|
1700
|
+
sourceUrl: `https://tokrepo.com/en/workflows/${uuid}`,
|
|
1701
|
+
};
|
|
1125
1702
|
}
|
|
1126
1703
|
|
|
1127
1704
|
async function cmdWhoami() {
|
|
@@ -1141,13 +1718,33 @@ async function cmdWhoami() {
|
|
|
1141
1718
|
}
|
|
1142
1719
|
|
|
1143
1720
|
async function cmdList() {
|
|
1144
|
-
|
|
1721
|
+
const args = parseArgs(process.argv);
|
|
1722
|
+
if (!args.flags.json) log(`\n${C.bold}tokrepo list${C.reset}\n`);
|
|
1145
1723
|
|
|
1146
1724
|
const config = readConfig();
|
|
1147
1725
|
if (!config || !config.token) error('Not logged in. Run: tokrepo login');
|
|
1148
1726
|
|
|
1149
1727
|
try {
|
|
1150
|
-
const
|
|
1728
|
+
const pageSize = Number(args.flags.pageSize || (args.flags.all ? 200 : 50)) || 50;
|
|
1729
|
+
let page = Number(args.flags.page || 1) || 1;
|
|
1730
|
+
let data = await apiRequest('GET', `/api/v1/tokenboard/workflows/my?page=${page}&page_size=${pageSize}`, null, config.token, config.api);
|
|
1731
|
+
|
|
1732
|
+
if (args.flags.all) {
|
|
1733
|
+
const list = [...(data.list || [])];
|
|
1734
|
+
while (list.length < (data.total || 0)) {
|
|
1735
|
+
page++;
|
|
1736
|
+
const next = await apiRequest('GET', `/api/v1/tokenboard/workflows/my?page=${page}&page_size=${pageSize}`, null, config.token, config.api);
|
|
1737
|
+
const items = next.list || [];
|
|
1738
|
+
if (items.length === 0) break;
|
|
1739
|
+
list.push(...items);
|
|
1740
|
+
}
|
|
1741
|
+
data = { ...data, list };
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
if (args.flags.json) {
|
|
1745
|
+
outputJson({ total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1151
1748
|
|
|
1152
1749
|
if (!data.list || data.list.length === 0) {
|
|
1153
1750
|
info('No assets found. Run: tokrepo push');
|
|
@@ -1207,16 +1804,22 @@ async function cmdUpdate() {
|
|
|
1207
1804
|
}
|
|
1208
1805
|
|
|
1209
1806
|
async function cmdDelete() {
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1807
|
+
const args = parseArgs(process.argv);
|
|
1808
|
+
const uuid = args.positional[0];
|
|
1809
|
+
if (!uuid) error('Usage: tokrepo delete <uuid> [--yes]');
|
|
1212
1810
|
|
|
1213
1811
|
log(`\n${C.bold}tokrepo delete${C.reset}\n`);
|
|
1214
1812
|
|
|
1215
1813
|
const config = readConfig();
|
|
1216
1814
|
if (!config || !config.token) error('Not logged in. Run: tokrepo login');
|
|
1217
1815
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1816
|
+
// --yes / -y / TOKREPO_NONINTERACTIVE 跳过交互式确认(脚本/CI 友好)。
|
|
1817
|
+
// 没有这两个的话仍然要 y/N 防误删。
|
|
1818
|
+
const skipConfirm = Boolean(args.flags.yes) || Boolean(args.flags.y) || process.env.TOKREPO_NONINTERACTIVE === '1';
|
|
1819
|
+
if (!skipConfirm) {
|
|
1820
|
+
const confirm = await ask(`Delete ${uuid.substring(0,8)}...? (y/N):`);
|
|
1821
|
+
if (confirm.toLowerCase() !== 'y') { log('Aborted.'); return; }
|
|
1822
|
+
}
|
|
1220
1823
|
|
|
1221
1824
|
try {
|
|
1222
1825
|
await apiRequest('DELETE', '/api/v1/tokenboard/workflows/delete', { uuid }, config.token, config.api);
|
|
@@ -1226,107 +1829,393 @@ async function cmdDelete() {
|
|
|
1226
1829
|
}
|
|
1227
1830
|
}
|
|
1228
1831
|
|
|
1832
|
+
function tagMatchesTypes(workflow, requestedTypes) {
|
|
1833
|
+
if (!requestedTypes || requestedTypes.length === 0) return true;
|
|
1834
|
+
const tags = (workflow.tags || []).flatMap(t => [t.slug, t.name]).filter(Boolean).map(t => String(t).toLowerCase());
|
|
1835
|
+
const assetType = getWorkflowAssetType(workflow);
|
|
1836
|
+
return requestedTypes.some(type => {
|
|
1837
|
+
const needle = String(type).trim().toLowerCase();
|
|
1838
|
+
if (!needle) return false;
|
|
1839
|
+
if (assetType === needle || assetType === `${needle}s`) return true;
|
|
1840
|
+
return tags.some(tag => tag === needle || tag === `${needle}s` || tag.includes(needle));
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function itemMatchesKeyword(workflow, keyword) {
|
|
1845
|
+
if (!keyword) return true;
|
|
1846
|
+
const needle = normalizeQuery(keyword).toLowerCase();
|
|
1847
|
+
const fields = [
|
|
1848
|
+
workflow.title,
|
|
1849
|
+
workflow.slug,
|
|
1850
|
+
workflow.description,
|
|
1851
|
+
...(workflow.tags || []).flatMap(t => [t.name, t.slug]),
|
|
1852
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
1853
|
+
return needle.split(/\s+/).every(word => fields.includes(word));
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
async function fetchCloneItems(username, config, apiBase, args) {
|
|
1857
|
+
const pageSize = Number(args.flags.pageSize || 200) || 200;
|
|
1858
|
+
const keyword = args.flags.keyword || '';
|
|
1859
|
+
const requestedTypes = String(args.flags.types || '')
|
|
1860
|
+
.split(',')
|
|
1861
|
+
.map(s => s.trim())
|
|
1862
|
+
.filter(Boolean);
|
|
1863
|
+
|
|
1864
|
+
let effectiveUsername = username.startsWith('@') ? username.slice(1) : username;
|
|
1865
|
+
const result = { username: effectiveUsername, source: 'public', list: [], total: 0 };
|
|
1866
|
+
|
|
1867
|
+
let cloneSelf = effectiveUsername === 'me';
|
|
1868
|
+
if (config?.token) {
|
|
1869
|
+
try {
|
|
1870
|
+
const me = await apiRequest('GET', '/api/v1/tokenboard/auth/me', null, config.token, apiBase);
|
|
1871
|
+
if (effectiveUsername === 'me' || me.nickname?.toLowerCase() === effectiveUsername.toLowerCase()) {
|
|
1872
|
+
cloneSelf = true;
|
|
1873
|
+
effectiveUsername = me.nickname || effectiveUsername;
|
|
1874
|
+
result.username = effectiveUsername;
|
|
1875
|
+
}
|
|
1876
|
+
} catch { /* anonymous/public clone still works */ }
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
if (cloneSelf) {
|
|
1880
|
+
if (!config?.token) error('Cloning @me requires login or TOKREPO_TOKEN.');
|
|
1881
|
+
let page = 1;
|
|
1882
|
+
while (true) {
|
|
1883
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/my?page=${page}&page_size=${pageSize}`, null, config.token, apiBase);
|
|
1884
|
+
const items = data.list || [];
|
|
1885
|
+
result.total = data.total || result.total;
|
|
1886
|
+
result.list.push(...items);
|
|
1887
|
+
if (items.length < pageSize || result.list.length >= result.total) break;
|
|
1888
|
+
page++;
|
|
1889
|
+
}
|
|
1890
|
+
result.source = 'my';
|
|
1891
|
+
} else {
|
|
1892
|
+
let page = 1;
|
|
1893
|
+
while (true) {
|
|
1894
|
+
const params = [
|
|
1895
|
+
`author_name=${encodeURIComponent(effectiveUsername)}`,
|
|
1896
|
+
`page=${page}`,
|
|
1897
|
+
`page_size=${pageSize}`,
|
|
1898
|
+
'sort_by=latest',
|
|
1899
|
+
];
|
|
1900
|
+
if (keyword) params.push(`keyword=${encodeURIComponent(normalizeQuery(keyword))}`);
|
|
1901
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?${params.join('&')}`, null, config?.token, apiBase);
|
|
1902
|
+
const items = data.list || data.items || [];
|
|
1903
|
+
result.total = data.total || result.total;
|
|
1904
|
+
result.list.push(...items);
|
|
1905
|
+
if (items.length < pageSize || result.list.length >= result.total) break;
|
|
1906
|
+
page++;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
result.list = result.list.filter(item => itemMatchesKeyword(item, keyword) && tagMatchesTypes(item, requestedTypes));
|
|
1911
|
+
result.count = result.list.length;
|
|
1912
|
+
result.keyword = keyword || undefined;
|
|
1913
|
+
result.types = requestedTypes;
|
|
1914
|
+
return result;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1229
1917
|
async function cmdClone() {
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1918
|
+
const args = parseArgs(process.argv);
|
|
1919
|
+
const target = args.positional[0];
|
|
1920
|
+
if (!target) {
|
|
1921
|
+
showCloneHelp();
|
|
1922
|
+
process.exit(1);
|
|
1923
|
+
}
|
|
1232
1924
|
|
|
1233
|
-
|
|
1925
|
+
const json = Boolean(args.flags.json);
|
|
1926
|
+
if (!json) log(`\n${C.bold}tokrepo clone${C.reset}\n`);
|
|
1234
1927
|
|
|
1235
1928
|
const config = readConfig();
|
|
1236
1929
|
const apiBase = config?.api || DEFAULT_API;
|
|
1930
|
+
const targetTool = validateInstallTarget(args.flags.target);
|
|
1931
|
+
const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
|
|
1237
1932
|
|
|
1238
|
-
// Extract username from @username format
|
|
1239
|
-
let username = target;
|
|
1240
|
-
if (username.startsWith('@')) username = username.slice(1);
|
|
1241
|
-
|
|
1242
|
-
// Step 1: Find user's UUID by searching for their workflows
|
|
1243
|
-
info(`Finding user @${username}...`);
|
|
1244
|
-
let authorUuid = '';
|
|
1245
1933
|
try {
|
|
1246
|
-
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
if (
|
|
1251
|
-
|
|
1252
|
-
|
|
1934
|
+
if (!json) info(`Fetching assets from ${target}...`);
|
|
1935
|
+
const cloneItems = await fetchCloneItems(target, config, apiBase, args);
|
|
1936
|
+
|
|
1937
|
+
if (cloneItems.list.length === 0) {
|
|
1938
|
+
if (json) {
|
|
1939
|
+
outputJson({ target, count: 0, list: [] });
|
|
1940
|
+
} else {
|
|
1941
|
+
info(`${target} has no matching assets.`);
|
|
1253
1942
|
}
|
|
1943
|
+
return;
|
|
1254
1944
|
}
|
|
1255
|
-
} catch { /* fall through */ }
|
|
1256
1945
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
if (
|
|
1946
|
+
if (!json) log(` Found ${C.bold}${cloneItems.list.length}${C.reset} matching asset(s)\n`);
|
|
1947
|
+
|
|
1948
|
+
if (targetTool === 'codex') {
|
|
1949
|
+
const results = [];
|
|
1950
|
+
let installedCount = 0;
|
|
1951
|
+
for (let i = 0; i < cloneItems.list.length; i++) {
|
|
1952
|
+
const item = cloneItems.list[i];
|
|
1953
|
+
if (!json) log(`${C.dim}[${i + 1}/${cloneItems.list.length}]${C.reset} ${item.title}`);
|
|
1954
|
+
try {
|
|
1955
|
+
const detail = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${item.uuid}`, null, config?.token, apiBase);
|
|
1956
|
+
const workflow = detail.workflow;
|
|
1957
|
+
const assetType = getWorkflowAssetType(workflow);
|
|
1958
|
+
const contents = extractInstallableContents(workflow, assetType);
|
|
1959
|
+
if (contents.length === 0) throw new Error('No installable content found');
|
|
1960
|
+
const result = await installCodexAsset(workflow, contents, {
|
|
1961
|
+
...args.flags,
|
|
1962
|
+
dryRun,
|
|
1963
|
+
stage: Boolean(args.flags.stage),
|
|
1964
|
+
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
1965
|
+
json: true,
|
|
1966
|
+
throwOnError: true,
|
|
1967
|
+
});
|
|
1968
|
+
if (!dryRun) installedCount += result.installedFiles.length;
|
|
1969
|
+
results.push({
|
|
1970
|
+
uuid: workflow.uuid,
|
|
1971
|
+
title: workflow.title,
|
|
1972
|
+
dryRun: Boolean(dryRun || args.flags.stage),
|
|
1973
|
+
staged: Boolean(result.staged),
|
|
1974
|
+
stagePath: result.stagePath,
|
|
1975
|
+
installMode: result.plan.installMode,
|
|
1976
|
+
files: result.plan.files,
|
|
1977
|
+
installedFiles: result.installedFiles || [],
|
|
1978
|
+
risks: result.plan.risks,
|
|
1979
|
+
});
|
|
1980
|
+
if (!json) {
|
|
1981
|
+
const fileCount = (dryRun || args.flags.stage) ? result.plan.files.length : result.installedFiles.length;
|
|
1982
|
+
success(`${args.flags.stage ? 'Staged' : dryRun ? 'Planned' : 'Installed'} ${fileCount} file(s)`);
|
|
1983
|
+
}
|
|
1984
|
+
} catch (e) {
|
|
1985
|
+
results.push({ uuid: item.uuid, title: item.title, error: e.message });
|
|
1986
|
+
if (!json) warn(`Skipped "${item.title}": ${e.message}`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
const response = {
|
|
1991
|
+
target,
|
|
1992
|
+
username: cloneItems.username,
|
|
1993
|
+
targetTool: 'codex',
|
|
1994
|
+
dryRun,
|
|
1995
|
+
total: cloneItems.total,
|
|
1996
|
+
count: cloneItems.count,
|
|
1997
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
1998
|
+
results,
|
|
1999
|
+
};
|
|
2000
|
+
if (json) {
|
|
2001
|
+
outputJson(response);
|
|
2002
|
+
} else {
|
|
2003
|
+
log('');
|
|
2004
|
+
if (args.flags.stage) {
|
|
2005
|
+
success(`Staged ${results.filter(r => !r.error).length}/${cloneItems.list.length} asset install plan(s)`);
|
|
2006
|
+
} else if (dryRun) {
|
|
2007
|
+
success(`Dry run complete: ${results.filter(r => !r.error).length}/${cloneItems.list.length} assets planned`);
|
|
2008
|
+
} else {
|
|
2009
|
+
success(`Installed ${installedCount} Codex file(s) from ${results.filter(r => !r.error).length}/${cloneItems.list.length} assets`);
|
|
2010
|
+
log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}\n`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
if (targetTool && targetTool !== 'codex') {
|
|
2017
|
+
error(`clone --target ${targetTool} is not implemented yet. Supported clone target: codex`);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
if (json) {
|
|
2021
|
+
outputJson(cloneItems);
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// Legacy raw clone behavior for users who do not specify a target.
|
|
2026
|
+
const outDir = path.join(process.cwd(), cloneItems.username);
|
|
2027
|
+
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
2028
|
+
|
|
2029
|
+
let downloaded = 0;
|
|
2030
|
+
for (const item of cloneItems.list) {
|
|
2031
|
+
const title = item.title || item.uuid;
|
|
2032
|
+
const safeDirName = title.replace(/[/\\?%*:|"<>]/g, '-').substring(0, 80);
|
|
2033
|
+
const assetDir = path.join(outDir, safeDirName);
|
|
2034
|
+
|
|
1260
2035
|
try {
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
2036
|
+
const detail = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${item.uuid}`, null, config?.token, apiBase);
|
|
2037
|
+
const workflow = detail.workflow;
|
|
2038
|
+
const contents = extractInstallableContents(workflow, getWorkflowAssetType(workflow));
|
|
2039
|
+
|
|
2040
|
+
if (contents.length > 0) {
|
|
2041
|
+
if (!fs.existsSync(assetDir)) fs.mkdirSync(assetDir, { recursive: true });
|
|
2042
|
+
for (const contentItem of contents) {
|
|
2043
|
+
const safeName = sanitizeRelativePath(contentItem.name || 'content.md');
|
|
2044
|
+
fs.writeFileSync(path.join(assetDir, safeName), contentItem.content);
|
|
2045
|
+
}
|
|
2046
|
+
downloaded++;
|
|
2047
|
+
log(` ${C.green}✓${C.reset} ${safeDirName} ${C.dim}(${contents.length} files)${C.reset}`);
|
|
1264
2048
|
}
|
|
1265
|
-
} catch {
|
|
2049
|
+
} catch (e) {
|
|
2050
|
+
log(` ${C.yellow}!${C.reset} ${safeDirName} ${C.dim}(skipped: ${e.message})${C.reset}`);
|
|
2051
|
+
}
|
|
1266
2052
|
}
|
|
1267
|
-
|
|
2053
|
+
|
|
2054
|
+
log('');
|
|
2055
|
+
success(`Cloned ${downloaded}/${cloneItems.list.length} assets to ./${cloneItems.username}/`);
|
|
2056
|
+
} catch (e) {
|
|
2057
|
+
error(`Clone failed: ${e.message}`);
|
|
1268
2058
|
}
|
|
2059
|
+
}
|
|
1269
2060
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
2061
|
+
function currentFileSha(filePath) {
|
|
2062
|
+
try {
|
|
2063
|
+
return sha256(fs.readFileSync(filePath, 'utf8'));
|
|
2064
|
+
} catch {
|
|
2065
|
+
return '';
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function diffCodexPlanWithLocal(plan, manifestRecord = {}) {
|
|
2070
|
+
const reasons = [];
|
|
2071
|
+
const desired = new Map(plan.files.map(file => [file.path, file.sha256]));
|
|
2072
|
+
const recordedFiles = manifestRecord.installedFiles || manifestRecord.installed_files || [];
|
|
2073
|
+
|
|
2074
|
+
for (const file of plan.files) {
|
|
2075
|
+
if (!fs.existsSync(file.path)) {
|
|
2076
|
+
reasons.push({ type: 'missing', path: file.path });
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
const actualSha = currentFileSha(file.path);
|
|
2080
|
+
if (actualSha !== file.sha256) {
|
|
2081
|
+
reasons.push({ type: 'changed', path: file.path, actualSha, expectedSha: file.sha256 });
|
|
1284
2082
|
}
|
|
1285
2083
|
}
|
|
1286
2084
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
2085
|
+
for (const file of recordedFiles) {
|
|
2086
|
+
if (file.path && !desired.has(file.path)) {
|
|
2087
|
+
reasons.push({ type: 'obsolete-manifest-path', path: file.path });
|
|
2088
|
+
}
|
|
1290
2089
|
}
|
|
1291
2090
|
|
|
1292
|
-
|
|
2091
|
+
return {
|
|
2092
|
+
needsUpdate: reasons.length > 0,
|
|
2093
|
+
reasons,
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
1293
2096
|
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
2097
|
+
async function fetchWorkflowForInstall(uuid, config, apiBase) {
|
|
2098
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${encodeURIComponent(uuid)}`, null, config?.token, apiBase);
|
|
2099
|
+
const workflow = data.workflow;
|
|
2100
|
+
const contents = extractInstallableContents(workflow, getWorkflowAssetType(workflow));
|
|
2101
|
+
if (contents.length === 0) {
|
|
2102
|
+
throw new Error('No installable content found');
|
|
1298
2103
|
}
|
|
2104
|
+
return { workflow, contents };
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
async function cmdSyncInstalled() {
|
|
2108
|
+
const args = parseArgs(process.argv);
|
|
2109
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
2110
|
+
if (targetTool !== 'codex') {
|
|
2111
|
+
error(`sync-installed currently supports --target codex only`);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
const json = Boolean(args.flags.json);
|
|
2115
|
+
if (!json) log(`\n${C.bold}tokrepo sync-installed${C.reset}\n`);
|
|
2116
|
+
|
|
2117
|
+
const manifest = readCodexManifest();
|
|
2118
|
+
const installed = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
|
|
2119
|
+
if (installed.length === 0) {
|
|
2120
|
+
if (json) outputJson({ targetTool: 'codex', count: 0, results: [] });
|
|
2121
|
+
else info(`No Codex installs found in ${CODEX_MANIFEST_FILE}`);
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
const config = readConfig();
|
|
2126
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
2127
|
+
const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
|
|
2128
|
+
const stage = Boolean(args.flags.stage);
|
|
2129
|
+
const force = Boolean(args.flags.update || args.flags.force);
|
|
2130
|
+
const results = [];
|
|
1299
2131
|
|
|
1300
|
-
let
|
|
1301
|
-
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
2132
|
+
for (let i = 0; i < installed.length; i++) {
|
|
2133
|
+
const record = installed[i];
|
|
2134
|
+
const uuid = record.uuid;
|
|
2135
|
+
if (!uuid) continue;
|
|
2136
|
+
|
|
2137
|
+
if (!json) log(`${C.dim}[${i + 1}/${installed.length}]${C.reset} ${record.title || uuid}`);
|
|
1305
2138
|
|
|
1306
2139
|
try {
|
|
1307
|
-
const
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
2140
|
+
const { workflow, contents } = await fetchWorkflowForInstall(uuid, config, apiBase);
|
|
2141
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode });
|
|
2142
|
+
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2143
|
+
const shouldWrite = force || diff.needsUpdate;
|
|
2144
|
+
|
|
2145
|
+
if (dryRun) {
|
|
2146
|
+
results.push({
|
|
2147
|
+
uuid,
|
|
2148
|
+
title: workflow.title,
|
|
2149
|
+
status: shouldWrite ? 'would-update' : 'unchanged',
|
|
2150
|
+
needsUpdate: shouldWrite,
|
|
2151
|
+
reasons: diff.reasons,
|
|
2152
|
+
plan: publicInstallPlan(plan),
|
|
2153
|
+
});
|
|
2154
|
+
if (!json) {
|
|
2155
|
+
const label = shouldWrite ? `${C.yellow}would update${C.reset}` : `${C.green}unchanged${C.reset}`;
|
|
2156
|
+
log(` ${label}`);
|
|
1319
2157
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
if (!shouldWrite) {
|
|
2162
|
+
results.push({
|
|
2163
|
+
uuid,
|
|
2164
|
+
title: workflow.title,
|
|
2165
|
+
status: 'unchanged',
|
|
2166
|
+
needsUpdate: false,
|
|
2167
|
+
reasons: [],
|
|
2168
|
+
});
|
|
2169
|
+
if (!json) success('Unchanged');
|
|
2170
|
+
continue;
|
|
1322
2171
|
}
|
|
2172
|
+
|
|
2173
|
+
const installResult = await installCodexAsset(workflow, contents, {
|
|
2174
|
+
...args.flags,
|
|
2175
|
+
dryRun: false,
|
|
2176
|
+
stage,
|
|
2177
|
+
installMode: record.installMode || record.install_mode,
|
|
2178
|
+
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2179
|
+
json: true,
|
|
2180
|
+
throwOnError: true,
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
results.push({
|
|
2184
|
+
uuid,
|
|
2185
|
+
title: workflow.title,
|
|
2186
|
+
status: stage ? 'staged' : 'updated',
|
|
2187
|
+
needsUpdate: true,
|
|
2188
|
+
reasons: diff.reasons,
|
|
2189
|
+
stagePath: installResult.stagePath,
|
|
2190
|
+
installedFiles: installResult.installedFiles || [],
|
|
2191
|
+
plan: installResult.plan,
|
|
2192
|
+
});
|
|
2193
|
+
if (!json) success(stage ? `Staged ${installResult.plan.files.length} file(s)` : `Updated ${(installResult.installedFiles || []).length} file(s)`);
|
|
1323
2194
|
} catch (e) {
|
|
1324
|
-
|
|
2195
|
+
results.push({ uuid, title: record.title || uuid, status: 'failed', error: e.message });
|
|
2196
|
+
if (!json) warn(`Failed: ${e.message}`);
|
|
1325
2197
|
}
|
|
1326
2198
|
}
|
|
1327
2199
|
|
|
1328
|
-
|
|
1329
|
-
|
|
2200
|
+
const summary = {
|
|
2201
|
+
targetTool: 'codex',
|
|
2202
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
2203
|
+
dryRun,
|
|
2204
|
+
stage,
|
|
2205
|
+
count: results.length,
|
|
2206
|
+
updated: results.filter(item => item.status === 'updated').length,
|
|
2207
|
+
staged: results.filter(item => item.status === 'staged').length,
|
|
2208
|
+
unchanged: results.filter(item => item.status === 'unchanged').length,
|
|
2209
|
+
failed: results.filter(item => item.status === 'failed').length,
|
|
2210
|
+
results,
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
if (json) {
|
|
2214
|
+
outputJson(summary);
|
|
2215
|
+
} else {
|
|
2216
|
+
log('');
|
|
2217
|
+
success(`Sync complete: ${summary.updated} updated, ${summary.staged} staged, ${summary.unchanged} unchanged, ${summary.failed} failed`);
|
|
2218
|
+
}
|
|
1330
2219
|
}
|
|
1331
2220
|
|
|
1332
2221
|
async function cmdTags() {
|
|
@@ -1447,9 +2336,11 @@ ${C.bold}USAGE${C.reset}
|
|
|
1447
2336
|
|
|
1448
2337
|
${C.bold}DISCOVER & INSTALL${C.reset}
|
|
1449
2338
|
${C.cyan}search${C.reset} <query> Search assets by keyword
|
|
2339
|
+
${C.cyan}detail${C.reset} <name|uuid> Show full asset metadata
|
|
1450
2340
|
${C.cyan}install${C.reset} <name|uuid> Smart install (auto-detects type & placement)
|
|
1451
2341
|
${C.cyan}pull${C.reset} <url|uuid|@u/n> Download raw asset files
|
|
1452
2342
|
${C.cyan}clone${C.reset} @username Clone all assets from a user
|
|
2343
|
+
${C.cyan}sync-installed${C.reset} Update installed Codex assets from manifest
|
|
1453
2344
|
|
|
1454
2345
|
${C.bold}PUBLISH${C.reset}
|
|
1455
2346
|
${C.cyan}push${C.reset} [files...] Push files/directory (idempotent upsert)
|
|
@@ -1459,7 +2350,7 @@ ${C.bold}PUBLISH${C.reset}
|
|
|
1459
2350
|
${C.cyan}delete${C.reset} <uuid> Delete an asset
|
|
1460
2351
|
|
|
1461
2352
|
${C.bold}ACCOUNT${C.reset}
|
|
1462
|
-
${C.cyan}login${C.reset} Save API
|
|
2353
|
+
${C.cyan}login${C.reset} Save API key (or set TOKREPO_TOKEN env var)
|
|
1463
2354
|
${C.cyan}list${C.reset} List your published assets
|
|
1464
2355
|
${C.cyan}tags${C.reset} List available tags
|
|
1465
2356
|
${C.cyan}whoami${C.reset} Show current user
|
|
@@ -1475,6 +2366,7 @@ ${C.bold}PUSH OPTIONS${C.reset}
|
|
|
1475
2366
|
${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
1476
2367
|
Skills → .claude/skills/ (if .claude/ exists)
|
|
1477
2368
|
Gemini → .gemini/GEMINI.md (with --target gemini)
|
|
2369
|
+
Codex → ~/.codex/skills/ (with --target codex)
|
|
1478
2370
|
Scripts → current dir (chmod +x)
|
|
1479
2371
|
Configs → project root
|
|
1480
2372
|
MCP → current dir (.json)
|
|
@@ -1482,8 +2374,13 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
|
1482
2374
|
|
|
1483
2375
|
${C.bold}EXAMPLES${C.reset}
|
|
1484
2376
|
tokrepo search "mcp server" # Find MCP configs
|
|
2377
|
+
tokrepo search video --json # Machine-readable search
|
|
2378
|
+
tokrepo detail ca000374-f5d8-... --json # Machine-readable detail
|
|
1485
2379
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
2380
|
+
tokrepo install ca000374-f5d8-... --target codex
|
|
1486
2381
|
tokrepo install c4b18aeb --target gemini # Install for Gemini CLI
|
|
2382
|
+
tokrepo clone @henuwangkai --target codex --keyword video
|
|
2383
|
+
tokrepo sync-installed --target codex --dry-run
|
|
1487
2384
|
tokrepo push --private my-rules.md # Save one file privately
|
|
1488
2385
|
tokrepo push --public skill.md # Share one file publicly
|
|
1489
2386
|
tokrepo push --private . # Push current dir as private
|
|
@@ -1505,10 +2402,133 @@ ${C.bold}GET YOUR TOKEN${C.reset}
|
|
|
1505
2402
|
`);
|
|
1506
2403
|
}
|
|
1507
2404
|
|
|
2405
|
+
function showSearchHelp() {
|
|
2406
|
+
log(`
|
|
2407
|
+
${C.bold}tokrepo search${C.reset}
|
|
2408
|
+
|
|
2409
|
+
USAGE
|
|
2410
|
+
tokrepo search <query> [--json] [--all] [--page-size N] [--sort-by views|latest|stars|popular]
|
|
2411
|
+
|
|
2412
|
+
EXAMPLES
|
|
2413
|
+
tokrepo search video
|
|
2414
|
+
tokrepo search video --json
|
|
2415
|
+
tokrepo search "mcp server" --json --all
|
|
2416
|
+
`);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function showDetailHelp() {
|
|
2420
|
+
log(`
|
|
2421
|
+
${C.bold}tokrepo detail${C.reset}
|
|
2422
|
+
|
|
2423
|
+
USAGE
|
|
2424
|
+
tokrepo detail <uuid|url|name> [--json]
|
|
2425
|
+
|
|
2426
|
+
EXAMPLES
|
|
2427
|
+
tokrepo detail 91aeb22d-eff0-4310-abc6-811d2394b420 --json
|
|
2428
|
+
tokrepo detail https://tokrepo.com/en/workflows/91aeb22d-eff0-4310-abc6-811d2394b420
|
|
2429
|
+
`);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
function showInstallHelp() {
|
|
2433
|
+
log(`
|
|
2434
|
+
${C.bold}tokrepo install${C.reset}
|
|
2435
|
+
|
|
2436
|
+
USAGE
|
|
2437
|
+
tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--json]
|
|
2438
|
+
|
|
2439
|
+
TARGETS
|
|
2440
|
+
codex Write Codex skills to ~/.codex/skills/<asset-slug>/SKILL.md
|
|
2441
|
+
gemini Write project instructions to .gemini/GEMINI.md
|
|
2442
|
+
|
|
2443
|
+
EXAMPLES
|
|
2444
|
+
tokrepo install awesome-cursor-rules
|
|
2445
|
+
tokrepo install ca000374-f5d8-4d75-a30c-460fda0b6b0e
|
|
2446
|
+
tokrepo install https://tokrepo.com/en/workflows/ca000374-...
|
|
2447
|
+
tokrepo install 91aeb22d-eff0-4310-abc6-811d2394b420 --target codex
|
|
2448
|
+
tokrepo install 91aeb22d-eff0-4310-abc6-811d2394b420 --target codex --dry-run --json
|
|
2449
|
+
tokrepo install 20bc3ffd-1d7a-41d1-86d0-b668e8500cee --target codex --stage
|
|
2450
|
+
tokrepo install c4b18aeb --target gemini
|
|
2451
|
+
`);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
function showListHelp() {
|
|
2455
|
+
log(`
|
|
2456
|
+
${C.bold}tokrepo list${C.reset}
|
|
2457
|
+
|
|
2458
|
+
USAGE
|
|
2459
|
+
tokrepo list [--json] [--all] [--page-size N]
|
|
2460
|
+
|
|
2461
|
+
EXAMPLES
|
|
2462
|
+
tokrepo list
|
|
2463
|
+
tokrepo list --json --all
|
|
2464
|
+
`);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function showCloneHelp() {
|
|
2468
|
+
log(`
|
|
2469
|
+
${C.bold}tokrepo clone${C.reset}
|
|
2470
|
+
|
|
2471
|
+
USAGE
|
|
2472
|
+
tokrepo clone @username [--target codex] [--keyword query] [--types skill,prompt,knowledge] [--dry-run] [--stage] [--approve-mcp] [--json] [--manifest]
|
|
2473
|
+
|
|
2474
|
+
EXAMPLES
|
|
2475
|
+
tokrepo clone @henuwangkai --target codex --types skill,prompt,knowledge
|
|
2476
|
+
tokrepo clone @henuwangkai --target codex --keyword video
|
|
2477
|
+
tokrepo clone @me --target codex --dry-run --json --manifest
|
|
2478
|
+
`);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function showSyncInstalledHelp() {
|
|
2482
|
+
log(`
|
|
2483
|
+
${C.bold}tokrepo sync-installed${C.reset}
|
|
2484
|
+
|
|
2485
|
+
USAGE
|
|
2486
|
+
tokrepo sync-installed --target codex [--dry-run] [--stage] [--update] [--approve-mcp] [--json]
|
|
2487
|
+
|
|
2488
|
+
BEHAVIOR
|
|
2489
|
+
Reads ~/.codex/tokrepo/install-manifest.json, fetches each TokRepo asset again,
|
|
2490
|
+
rebuilds the Codex install plan, compares local files by sha256, then updates
|
|
2491
|
+
changed or missing files. Use --update to force reinstall unchanged assets.
|
|
2492
|
+
|
|
2493
|
+
EXAMPLES
|
|
2494
|
+
tokrepo sync-installed --target codex --dry-run --json
|
|
2495
|
+
tokrepo sync-installed --target codex --stage
|
|
2496
|
+
tokrepo sync-installed --target codex --update --approve-mcp
|
|
2497
|
+
`);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
function showCommandHelp(command) {
|
|
2501
|
+
switch (command) {
|
|
2502
|
+
case 'search':
|
|
2503
|
+
case 'find':
|
|
2504
|
+
showSearchHelp(); break;
|
|
2505
|
+
case 'detail':
|
|
2506
|
+
showDetailHelp(); break;
|
|
2507
|
+
case 'install':
|
|
2508
|
+
case 'i':
|
|
2509
|
+
showInstallHelp(); break;
|
|
2510
|
+
case 'list':
|
|
2511
|
+
showListHelp(); break;
|
|
2512
|
+
case 'clone':
|
|
2513
|
+
showCloneHelp(); break;
|
|
2514
|
+
case 'sync-installed':
|
|
2515
|
+
case 'sync':
|
|
2516
|
+
showSyncInstalledHelp(); break;
|
|
2517
|
+
default:
|
|
2518
|
+
showHelp(); break;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
|
|
1508
2522
|
// ─── Main ───
|
|
1509
2523
|
|
|
1510
2524
|
async function main() {
|
|
1511
2525
|
const command = process.argv[2];
|
|
2526
|
+
const args = parseArgs(process.argv);
|
|
2527
|
+
|
|
2528
|
+
if (args.flags.help && command && !['help', '--help', '-h'].includes(command)) {
|
|
2529
|
+
showCommandHelp(command);
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
1512
2532
|
|
|
1513
2533
|
switch (command) {
|
|
1514
2534
|
case 'login': await cmdLogin(); break;
|
|
@@ -1516,11 +2536,13 @@ async function main() {
|
|
|
1516
2536
|
case 'push': await cmdPush(); break;
|
|
1517
2537
|
case 'pull': await cmdPull(); break;
|
|
1518
2538
|
case 'search': case 'find': await cmdSearch(); break;
|
|
2539
|
+
case 'detail': await cmdDetail(); break;
|
|
1519
2540
|
case 'install': case 'i': await cmdInstall(); break;
|
|
1520
2541
|
case 'list': await cmdList(); break;
|
|
1521
2542
|
case 'update': await cmdUpdate(); break;
|
|
1522
2543
|
case 'delete': await cmdDelete(); break;
|
|
1523
2544
|
case 'clone': await cmdClone(); break;
|
|
2545
|
+
case 'sync-installed': case 'sync': await cmdSyncInstalled(); break;
|
|
1524
2546
|
case 'tags': await cmdTags(); break;
|
|
1525
2547
|
case 'status': case 'diff': await cmdStatus(); break;
|
|
1526
2548
|
case 'whoami': await cmdWhoami(); break;
|
|
@@ -1533,7 +2555,9 @@ async function main() {
|
|
|
1533
2555
|
}
|
|
1534
2556
|
|
|
1535
2557
|
// Non-blocking update check after command completes
|
|
1536
|
-
|
|
2558
|
+
if (!wantsJson(process.argv) && !args.flags.help) {
|
|
2559
|
+
checkForUpdate();
|
|
2560
|
+
}
|
|
1537
2561
|
}
|
|
1538
2562
|
|
|
1539
2563
|
main().catch((e) => { error(e.message); });
|