tokrepo 3.3.3 → 3.5.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 +1399 -152
- 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.5.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;
|
|
@@ -215,6 +236,12 @@ function guessTag(fileType) {
|
|
|
215
236
|
return map[fileType] || null;
|
|
216
237
|
}
|
|
217
238
|
|
|
239
|
+
function parseCsvList(value) {
|
|
240
|
+
if (!value) return [];
|
|
241
|
+
if (Array.isArray(value)) return value.flatMap(parseCsvList);
|
|
242
|
+
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
243
|
+
}
|
|
244
|
+
|
|
218
245
|
// ─── Glob matching ───
|
|
219
246
|
|
|
220
247
|
function matchGlob(pattern, filename) {
|
|
@@ -272,32 +299,61 @@ function parseArgs(argv) {
|
|
|
272
299
|
args.command = argv[i];
|
|
273
300
|
i++;
|
|
274
301
|
}
|
|
302
|
+
|
|
303
|
+
const valueFlags = new Set([
|
|
304
|
+
'title', 'desc', 'tag', 'target', 'targets', 'keyword', 'types',
|
|
305
|
+
'kind', 'install-mode', 'install_mode', 'entrypoint', 'asset-kind', 'asset_kind',
|
|
306
|
+
'version',
|
|
307
|
+
'page', 'page-size', 'page_size', 'sort-by', 'sort_by',
|
|
308
|
+
'time-window', 'time_window',
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
const assignFlag = (rawName, value = true) => {
|
|
312
|
+
const name = rawName.replace(/^--?/, '');
|
|
313
|
+
const normalized = name.replace(/-/g, '_');
|
|
314
|
+
if (normalized === 'tag') {
|
|
315
|
+
if (!args.flags.tags) args.flags.tags = [];
|
|
316
|
+
args.flags.tags.push(value);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (normalized === 'page_size') {
|
|
320
|
+
args.flags.pageSize = value;
|
|
321
|
+
} else if (normalized === 'sort_by') {
|
|
322
|
+
args.flags.sortBy = value;
|
|
323
|
+
} else if (normalized === 'time_window') {
|
|
324
|
+
args.flags.timeWindow = value;
|
|
325
|
+
} else if (normalized === 'dry_run') {
|
|
326
|
+
args.flags.dryRun = value;
|
|
327
|
+
} else if (normalized === 'approve_mcp') {
|
|
328
|
+
args.flags.approveMcp = value;
|
|
329
|
+
} else if (normalized === 'install_mode') {
|
|
330
|
+
args.flags.installMode = value;
|
|
331
|
+
} else if (normalized === 'asset_kind') {
|
|
332
|
+
args.flags.assetKind = value;
|
|
333
|
+
}
|
|
334
|
+
args.flags[normalized] = value;
|
|
335
|
+
};
|
|
336
|
+
|
|
275
337
|
while (i < argv.length) {
|
|
276
338
|
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('=');
|
|
339
|
+
if (arg === '-h' || arg === '--help') {
|
|
340
|
+
args.flags.help = true;
|
|
299
341
|
} else if (arg === '-y' || arg === '--yes') {
|
|
300
342
|
args.flags.yes = true;
|
|
343
|
+
} else if (arg.startsWith('--')) {
|
|
344
|
+
const eqIndex = arg.indexOf('=');
|
|
345
|
+
if (eqIndex !== -1) {
|
|
346
|
+
const name = arg.slice(2, eqIndex);
|
|
347
|
+
const value = arg.slice(eqIndex + 1);
|
|
348
|
+
assignFlag(name, value);
|
|
349
|
+
} else {
|
|
350
|
+
const name = arg.slice(2);
|
|
351
|
+
if (valueFlags.has(name) && i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
|
|
352
|
+
assignFlag(name, argv[++i]);
|
|
353
|
+
} else {
|
|
354
|
+
assignFlag(name, true);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
301
357
|
} else if (!arg.startsWith('-')) {
|
|
302
358
|
args.positional.push(arg);
|
|
303
359
|
}
|
|
@@ -388,10 +444,10 @@ async function cmdLogin() {
|
|
|
388
444
|
|
|
389
445
|
if (useToken) {
|
|
390
446
|
// Manual token flow
|
|
391
|
-
info('Paste your API
|
|
447
|
+
info('Paste your API key (from https://tokrepo.com/en/my/settings)');
|
|
392
448
|
log('');
|
|
393
|
-
const token = await ask('API
|
|
394
|
-
if (!token) error('
|
|
449
|
+
const token = await ask('API Key:');
|
|
450
|
+
if (!token) error('API key is required');
|
|
395
451
|
return await saveAndVerifyToken(token);
|
|
396
452
|
}
|
|
397
453
|
|
|
@@ -530,6 +586,10 @@ async function cmdPush() {
|
|
|
530
586
|
description = args.flags.desc || description || '';
|
|
531
587
|
visibility = args.flags.public ? 1 : (args.flags.private ? 0 : (projectConfig?.visibility ?? 0));
|
|
532
588
|
tags = args.flags.tags || tags || [];
|
|
589
|
+
const kind = args.flags.kind || args.flags.assetKind || projectConfig?.kind || projectConfig?.asset_kind || '';
|
|
590
|
+
const targetTools = parseCsvList(args.flags.targets || args.flags.target || projectConfig?.target_tools || projectConfig?.targetTools);
|
|
591
|
+
const installMode = args.flags.installMode || projectConfig?.install_mode || projectConfig?.installMode || '';
|
|
592
|
+
const entrypoint = args.flags.entrypoint || projectConfig?.entrypoint || '';
|
|
533
593
|
|
|
534
594
|
// Read files and detect types
|
|
535
595
|
const pushFiles = [];
|
|
@@ -569,6 +629,15 @@ async function cmdPush() {
|
|
|
569
629
|
if (detectedTags.size > 0) {
|
|
570
630
|
log(` ${C.bold}Tags:${C.reset} ${Array.from(detectedTags).join(', ')}`);
|
|
571
631
|
}
|
|
632
|
+
const metadataSummary = [
|
|
633
|
+
kind ? `kind=${kind}` : '',
|
|
634
|
+
targetTools.length ? `target_tools=${targetTools.join(',')}` : '',
|
|
635
|
+
installMode ? `install_mode=${installMode}` : '',
|
|
636
|
+
entrypoint ? `entrypoint=${entrypoint}` : '',
|
|
637
|
+
].filter(Boolean);
|
|
638
|
+
if (metadataSummary.length > 0) {
|
|
639
|
+
log(` ${C.bold}Agent meta:${C.reset} ${metadataSummary.join(' · ')}`);
|
|
640
|
+
}
|
|
572
641
|
log('');
|
|
573
642
|
|
|
574
643
|
for (const f of pushFiles) {
|
|
@@ -590,6 +659,10 @@ async function cmdPush() {
|
|
|
590
659
|
tags: Array.from(detectedTags),
|
|
591
660
|
token_cost: String(Math.round(totalChars / 4)),
|
|
592
661
|
visibility: visibility,
|
|
662
|
+
kind,
|
|
663
|
+
target_tools: targetTools,
|
|
664
|
+
install_mode: installMode,
|
|
665
|
+
entrypoint,
|
|
593
666
|
}, config.token, config.api);
|
|
594
667
|
|
|
595
668
|
log('');
|
|
@@ -684,7 +757,10 @@ function normalizeQuery(q) {
|
|
|
684
757
|
// - Full URL: "https://tokrepo.com/en/workflows/ca000374-f5d8-..."
|
|
685
758
|
// - @username/asset-name: search by author + keyword
|
|
686
759
|
// - Plain name: search by keyword
|
|
687
|
-
async function resolveAssetId(input, config, apiBase) {
|
|
760
|
+
async function resolveAssetId(input, config, apiBase, opts = {}) {
|
|
761
|
+
const emitInfo = (msg) => { if (!opts.quiet) info(msg); };
|
|
762
|
+
const emitWarn = (msg) => { if (!opts.quiet) warn(msg); };
|
|
763
|
+
|
|
688
764
|
// Already a UUID
|
|
689
765
|
if (/^[a-f0-9-]{36}$/.test(input)) return input;
|
|
690
766
|
|
|
@@ -697,7 +773,7 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
697
773
|
if (atMatch) {
|
|
698
774
|
const [, username, assetName] = atMatch;
|
|
699
775
|
const normalizedName = normalizeQuery(assetName);
|
|
700
|
-
|
|
776
|
+
emitInfo(`Searching for "${normalizedName}" by @${username}...`);
|
|
701
777
|
// Search by keyword, then filter by author nickname
|
|
702
778
|
const encoded = encodeURIComponent(normalizedName);
|
|
703
779
|
try {
|
|
@@ -710,7 +786,7 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
710
786
|
if (match) return match.uuid;
|
|
711
787
|
// Fallback: return first result
|
|
712
788
|
if (items.length > 0) {
|
|
713
|
-
|
|
789
|
+
emitWarn(`No exact match for @${username}, using best match: "${items[0].title}"`);
|
|
714
790
|
return items[0].uuid;
|
|
715
791
|
}
|
|
716
792
|
} catch { /* fall through */ }
|
|
@@ -719,7 +795,7 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
719
795
|
|
|
720
796
|
// Plain name: search by keyword (normalize separators)
|
|
721
797
|
const normalizedInput = normalizeQuery(input);
|
|
722
|
-
|
|
798
|
+
emitInfo(`Searching for "${normalizedInput}"...`);
|
|
723
799
|
const encoded = encodeURIComponent(normalizedInput);
|
|
724
800
|
try {
|
|
725
801
|
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 +808,43 @@ async function resolveAssetId(input, config, apiBase) {
|
|
|
732
808
|
// ─── Search ───
|
|
733
809
|
|
|
734
810
|
async function cmdSearch() {
|
|
735
|
-
const
|
|
736
|
-
|
|
811
|
+
const args = parseArgs(process.argv);
|
|
812
|
+
const rawQuery = args.flags.keyword || args.positional.join(' ');
|
|
813
|
+
if (!rawQuery) {
|
|
814
|
+
showSearchHelp();
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
737
817
|
|
|
738
818
|
const query = normalizeQuery(rawQuery);
|
|
739
819
|
const displayQuery = query !== rawQuery ? `"${rawQuery}" → "${query}"` : `"${query}"`;
|
|
740
|
-
log(`\n${C.bold}tokrepo search${C.reset} ${displayQuery}\n`);
|
|
820
|
+
if (!args.flags.json) log(`\n${C.bold}tokrepo search${C.reset} ${displayQuery}\n`);
|
|
741
821
|
|
|
742
822
|
const config = readConfig();
|
|
743
823
|
const apiBase = config?.api || DEFAULT_API;
|
|
744
824
|
|
|
745
825
|
try {
|
|
746
826
|
const encoded = encodeURIComponent(query);
|
|
747
|
-
const
|
|
827
|
+
const pageSize = Number(args.flags.pageSize || (args.flags.all ? 200 : 20)) || 20;
|
|
828
|
+
const sortBy = args.flags.sortBy || 'views';
|
|
829
|
+
let page = Number(args.flags.page || 1) || 1;
|
|
830
|
+
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);
|
|
831
|
+
|
|
832
|
+
if (args.flags.all) {
|
|
833
|
+
const list = [...(data.list || [])];
|
|
834
|
+
while (list.length < (data.total || 0)) {
|
|
835
|
+
page++;
|
|
836
|
+
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);
|
|
837
|
+
const items = next.list || [];
|
|
838
|
+
if (items.length === 0) break;
|
|
839
|
+
list.push(...items);
|
|
840
|
+
}
|
|
841
|
+
data = { ...data, list };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (args.flags.json) {
|
|
845
|
+
outputJson({ query, total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
748
848
|
|
|
749
849
|
if (!data.list || data.list.length === 0) {
|
|
750
850
|
info('No assets found.');
|
|
@@ -782,6 +882,44 @@ async function cmdSearch() {
|
|
|
782
882
|
}
|
|
783
883
|
}
|
|
784
884
|
|
|
885
|
+
async function cmdDetail() {
|
|
886
|
+
const args = parseArgs(process.argv);
|
|
887
|
+
const target = args.positional[0];
|
|
888
|
+
if (!target) {
|
|
889
|
+
showDetailHelp();
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const config = readConfig();
|
|
894
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
895
|
+
|
|
896
|
+
try {
|
|
897
|
+
const uuid = await resolveAssetId(target, config, apiBase, { quiet: Boolean(args.flags.json) });
|
|
898
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${encodeURIComponent(uuid)}`, null, config?.token, apiBase);
|
|
899
|
+
if (args.flags.json) {
|
|
900
|
+
outputJson(data);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const workflow = data.workflow;
|
|
905
|
+
log(`\n${C.bold}tokrepo detail${C.reset}\n`);
|
|
906
|
+
log(` ${C.bold}${workflow.title}${C.reset}`);
|
|
907
|
+
if (workflow.description) log(` ${C.dim}${workflow.description}${C.reset}`);
|
|
908
|
+
log(`\n ${C.bold}UUID:${C.reset} ${workflow.uuid}`);
|
|
909
|
+
log(` ${C.bold}URL:${C.reset} ${C.cyan}https://tokrepo.com/en/workflows/${workflow.uuid}${C.reset}`);
|
|
910
|
+
if (workflow.tags && workflow.tags.length) {
|
|
911
|
+
log(` ${C.bold}Tags:${C.reset} ${workflow.tags.map(t => t.name || t.slug).join(', ')}`);
|
|
912
|
+
}
|
|
913
|
+
const fileCount = (workflow.files || []).length;
|
|
914
|
+
const stepCount = (workflow.steps || []).length;
|
|
915
|
+
log(` ${C.bold}Files:${C.reset} ${fileCount}`);
|
|
916
|
+
log(` ${C.bold}Steps:${C.reset} ${stepCount}`);
|
|
917
|
+
log('');
|
|
918
|
+
} catch (e) {
|
|
919
|
+
error(`Detail failed: ${e.message}`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
785
923
|
// ─── Install (smart pull with correct placement) ───
|
|
786
924
|
|
|
787
925
|
function normalizeInstallTarget(target) {
|
|
@@ -790,6 +928,9 @@ function normalizeInstallTarget(target) {
|
|
|
790
928
|
const aliases = {
|
|
791
929
|
gemini: 'gemini',
|
|
792
930
|
'gemini-cli': 'gemini',
|
|
931
|
+
codex: 'codex',
|
|
932
|
+
'codex-cli': 'codex',
|
|
933
|
+
'openai-codex': 'codex',
|
|
793
934
|
};
|
|
794
935
|
return aliases[normalized] || normalized;
|
|
795
936
|
}
|
|
@@ -797,8 +938,8 @@ function normalizeInstallTarget(target) {
|
|
|
797
938
|
function validateInstallTarget(target) {
|
|
798
939
|
if (!target) return '';
|
|
799
940
|
const normalized = normalizeInstallTarget(target);
|
|
800
|
-
if (normalized
|
|
801
|
-
error(`Unsupported install target: ${target}. Supported targets:
|
|
941
|
+
if (!SUPPORTED_INSTALL_TARGETS.includes(normalized)) {
|
|
942
|
+
error(`Unsupported install target: ${target}. Supported targets: ${SUPPORTED_INSTALL_TARGETS.join(', ')}`);
|
|
802
943
|
}
|
|
803
944
|
return normalized;
|
|
804
945
|
}
|
|
@@ -838,27 +979,497 @@ function formatGeminiContent(workflow, contents) {
|
|
|
838
979
|
return `${parts.join('\n\n').trim()}\n`;
|
|
839
980
|
}
|
|
840
981
|
|
|
982
|
+
function getWorkflowAssetType(workflow) {
|
|
983
|
+
if (!workflow || !workflow.tags || workflow.tags.length === 0) return 'other';
|
|
984
|
+
return (workflow.tags[0].slug || workflow.tags[0].name || '').toLowerCase();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function extractInstallableContents(workflow, assetType) {
|
|
988
|
+
const contents = [];
|
|
989
|
+
const files = workflow.files || [];
|
|
990
|
+
|
|
991
|
+
if (files.length > 0) {
|
|
992
|
+
for (const f of files) {
|
|
993
|
+
if (f.content && !f.content.startsWith('PK')) {
|
|
994
|
+
contents.push({
|
|
995
|
+
name: f.name || 'SKILL.md',
|
|
996
|
+
content: f.content,
|
|
997
|
+
type: f.type || f.file_type || f.fileType || detectFileType(f.name || ''),
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (contents.length === 0 && workflow.steps) {
|
|
1004
|
+
for (const step of workflow.steps) {
|
|
1005
|
+
const content = step.prompt_template || step.promptTemplate;
|
|
1006
|
+
if (content && !content.startsWith('PK')) {
|
|
1007
|
+
const name = (step.title || `step-${step.step_order || contents.length + 1}`).replace(/[/\\?%*:|"<>]/g, '-');
|
|
1008
|
+
contents.push({ name, content, type: assetType || 'other' });
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return contents;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function sha256(content) {
|
|
1017
|
+
return crypto.createHash('sha256').update(String(content || '')).digest('hex');
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function slugify(input, fallback = 'asset') {
|
|
1021
|
+
const raw = String(input || '')
|
|
1022
|
+
.normalize('NFKD')
|
|
1023
|
+
.replace(/[^\x00-\x7F]/g, '')
|
|
1024
|
+
.toLowerCase()
|
|
1025
|
+
.replace(/['"]/g, '')
|
|
1026
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1027
|
+
.replace(/^-+|-+$/g, '')
|
|
1028
|
+
.replace(/-{2,}/g, '-');
|
|
1029
|
+
return raw || fallback;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function sanitizePathSegment(input, fallback = 'file') {
|
|
1033
|
+
const cleaned = String(input || '')
|
|
1034
|
+
.replace(/[/\\?%*:|"<>]/g, '-')
|
|
1035
|
+
.replace(/^\.+$/, '')
|
|
1036
|
+
.replace(/^\.+/, '')
|
|
1037
|
+
.trim();
|
|
1038
|
+
return cleaned || fallback;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function sanitizeRelativePath(input, fallback = 'file.md') {
|
|
1042
|
+
const normalized = String(input || fallback).replace(/\\/g, '/');
|
|
1043
|
+
const parts = normalized
|
|
1044
|
+
.split('/')
|
|
1045
|
+
.filter(Boolean)
|
|
1046
|
+
.map((part, index) => sanitizePathSegment(part, index === 0 ? fallback : 'file'));
|
|
1047
|
+
let rel = parts.join('/');
|
|
1048
|
+
if (!rel) rel = fallback;
|
|
1049
|
+
if (!path.extname(rel)) rel += '.md';
|
|
1050
|
+
return rel;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function ensureInside(baseDir, destPath) {
|
|
1054
|
+
const resolvedBase = path.resolve(baseDir);
|
|
1055
|
+
const resolvedDest = path.resolve(destPath);
|
|
1056
|
+
return resolvedDest === resolvedBase || resolvedDest.startsWith(resolvedBase + path.sep);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function getFrontmatter(content) {
|
|
1060
|
+
const text = String(content || '').replace(/^\uFEFF/, '');
|
|
1061
|
+
const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
1062
|
+
if (!match) return null;
|
|
1063
|
+
return { raw: match[0], body: match[1], rest: text.slice(match[0].length) };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function getFrontmatterValue(content, key) {
|
|
1067
|
+
const fm = getFrontmatter(content);
|
|
1068
|
+
if (!fm) return '';
|
|
1069
|
+
const re = new RegExp(`^${key}\\s*:\\s*(.+)$`, 'im');
|
|
1070
|
+
const match = fm.body.match(re);
|
|
1071
|
+
if (!match) return '';
|
|
1072
|
+
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function isCodexSkillDocument(item) {
|
|
1076
|
+
const content = item?.content || '';
|
|
1077
|
+
const name = getFrontmatterValue(content, 'name');
|
|
1078
|
+
const description = getFrontmatterValue(content, 'description');
|
|
1079
|
+
return Boolean(name && description) || /^skill\.md$/i.test(path.basename(item?.name || ''));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function yamlQuoted(value) {
|
|
1083
|
+
return JSON.stringify(String(value || '').replace(/\s+/g, ' ').trim());
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function ensureCodexSkillFrontmatter(content, name, description) {
|
|
1087
|
+
const text = String(content || '').replace(/^\uFEFF/, '').trim();
|
|
1088
|
+
const fm = getFrontmatter(text);
|
|
1089
|
+
if (!fm) {
|
|
1090
|
+
return `---\nname: ${name}\ndescription: ${yamlQuoted(description)}\n---\n\n${text}\n`;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const lines = fm.body.split('\n');
|
|
1094
|
+
const hasName = lines.some(line => /^name\s*:/i.test(line));
|
|
1095
|
+
const hasDescription = lines.some(line => /^description\s*:/i.test(line));
|
|
1096
|
+
const next = [];
|
|
1097
|
+
if (!hasName) next.push(`name: ${name}`);
|
|
1098
|
+
if (!hasDescription) next.push(`description: ${yamlQuoted(description)}`);
|
|
1099
|
+
next.push(...lines);
|
|
1100
|
+
return `---\n${next.join('\n')}\n---\n${fm.rest.trim() ? `\n${fm.rest.trim()}\n` : '\n'}`;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function getCodexDescription(workflow, item) {
|
|
1104
|
+
const fromItem = getFrontmatterValue(item?.content || '', 'description');
|
|
1105
|
+
if (fromItem) return fromItem;
|
|
1106
|
+
const title = workflow?.title || item?.name || 'TokRepo asset';
|
|
1107
|
+
const desc = workflow?.description || '';
|
|
1108
|
+
return desc ? desc.substring(0, 240) : `Use ${title} from TokRepo.`;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function codexSkillDirName(workflow, item, suffix = '') {
|
|
1112
|
+
const uuid8 = (workflow?.uuid || '').substring(0, 8) || 'asset';
|
|
1113
|
+
const fmName = getFrontmatterValue(item?.content || '', 'name');
|
|
1114
|
+
const base = fmName || item?.name || workflow?.slug || workflow?.title || uuid8;
|
|
1115
|
+
const baseSlug = slugify(base, uuid8).replace(/-md$/, '');
|
|
1116
|
+
const withSuffix = suffix ? `${baseSlug}-${slugify(suffix, 'part')}` : baseSlug;
|
|
1117
|
+
if (withSuffix.startsWith('tokrepo-') && withSuffix.endsWith(`-${uuid8}`)) return withSuffix;
|
|
1118
|
+
if (withSuffix.startsWith('tokrepo-')) return `${withSuffix}-${uuid8}`;
|
|
1119
|
+
if (withSuffix.endsWith(`-${uuid8}`)) return `tokrepo-${withSuffix}`;
|
|
1120
|
+
return `tokrepo-${withSuffix}-${uuid8}`;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function explicitInstallMode(workflow) {
|
|
1124
|
+
const candidates = [
|
|
1125
|
+
workflow?.installMode,
|
|
1126
|
+
workflow?.install_mode,
|
|
1127
|
+
workflow?.agent_metadata?.install_mode,
|
|
1128
|
+
workflow?.agentMetadata?.installMode,
|
|
1129
|
+
workflow?.metadata?.installMode,
|
|
1130
|
+
workflow?.metadata?.install_mode,
|
|
1131
|
+
].filter(Boolean);
|
|
1132
|
+
const mode = String(candidates[0] || '').toLowerCase();
|
|
1133
|
+
return ['single', 'bundle', 'split', 'stage_only'].includes(mode) ? mode : '';
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function inferCodexInstallMode(workflow, contents) {
|
|
1137
|
+
const explicit = explicitInstallMode(workflow);
|
|
1138
|
+
if (explicit) return explicit;
|
|
1139
|
+
if (contents.length <= 1) return 'single';
|
|
1140
|
+
const skillDocs = contents.filter(isCodexSkillDocument);
|
|
1141
|
+
if (skillDocs.length > 1 && skillDocs.length === contents.length) return 'split';
|
|
1142
|
+
return 'bundle';
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function analyzeInstallRisks(fileName, content, type) {
|
|
1146
|
+
const risks = new Set();
|
|
1147
|
+
const lowerName = String(fileName || '').toLowerCase();
|
|
1148
|
+
const text = String(content || '');
|
|
1149
|
+
if (type === 'script' || /\.(sh|py|js|mjs|ts|rb|go|rs|lua)$/.test(lowerName) || /^#!\//.test(text)) {
|
|
1150
|
+
risks.add('executable');
|
|
1151
|
+
}
|
|
1152
|
+
if (lowerName.endsWith('.mcp.json') || /"mcpServers"\s*:/.test(text) || /\bmcpServers\s*:/.test(text)) {
|
|
1153
|
+
risks.add('mcp');
|
|
1154
|
+
}
|
|
1155
|
+
if (/\b(PATH|HOME|TOKEN|API_KEY|SECRET|PASSWORD|OPENAI_API_KEY|ANTHROPIC_API_KEY)\b/.test(text)) {
|
|
1156
|
+
risks.add('env');
|
|
1157
|
+
}
|
|
1158
|
+
if (/(^|[\s"'=])\/(Users|opt|usr|var|etc|tmp)\//.test(text) || /[A-Za-z]:\\/.test(text)) {
|
|
1159
|
+
risks.add('absolute-path');
|
|
1160
|
+
}
|
|
1161
|
+
return Array.from(risks);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function buildBundleEntrypoint(workflow, contents, skillName) {
|
|
1165
|
+
const title = workflow.title || 'TokRepo Asset';
|
|
1166
|
+
const sourceUrl = `https://tokrepo.com/en/workflows/${workflow.uuid}`;
|
|
1167
|
+
const fileList = contents
|
|
1168
|
+
.map((item, index) => `- ${sanitizeRelativePath(item.name || `file-${index + 1}.md`)}`)
|
|
1169
|
+
.join('\n');
|
|
1170
|
+
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`;
|
|
1171
|
+
return ensureCodexSkillFrontmatter(body, skillName, getCodexDescription(workflow));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function addPlanFile(plan, destPath, content, sourceName, type) {
|
|
1175
|
+
const riskFlags = analyzeInstallRisks(sourceName || destPath, content, type);
|
|
1176
|
+
plan.files.push({
|
|
1177
|
+
path: destPath,
|
|
1178
|
+
sourceName: sourceName || path.basename(destPath),
|
|
1179
|
+
sha256: sha256(content),
|
|
1180
|
+
bytes: Buffer.byteLength(String(content || '')),
|
|
1181
|
+
riskFlags,
|
|
1182
|
+
content,
|
|
1183
|
+
});
|
|
1184
|
+
for (const risk of riskFlags) {
|
|
1185
|
+
if (!plan.risks.includes(risk)) plan.risks.push(risk);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
1190
|
+
const installMode = opts.installMode || inferCodexInstallMode(workflow, contents);
|
|
1191
|
+
const agentMetadata = workflow?.agent_metadata || workflow?.agentMetadata || {};
|
|
1192
|
+
const plan = {
|
|
1193
|
+
uuid: workflow.uuid,
|
|
1194
|
+
title: workflow.title,
|
|
1195
|
+
sourceUrl: `https://tokrepo.com/en/workflows/${workflow.uuid}`,
|
|
1196
|
+
targetTool: 'codex',
|
|
1197
|
+
installMode,
|
|
1198
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
1199
|
+
files: [],
|
|
1200
|
+
risks: [],
|
|
1201
|
+
agentMetadata,
|
|
1202
|
+
contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || '',
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
if (installMode === 'stage_only') {
|
|
1206
|
+
const stageDir = path.join(CODEX_TOKREPO_DIR, 'staged', workflow.uuid);
|
|
1207
|
+
plan.baseDir = stageDir;
|
|
1208
|
+
contents.forEach((item, index) => {
|
|
1209
|
+
const relName = sanitizeRelativePath(item.name || `file-${index + 1}.md`);
|
|
1210
|
+
addPlanFile(plan, path.join(stageDir, relName), `${String(item.content || '').trim()}\n`, item.name, item.type);
|
|
1211
|
+
});
|
|
1212
|
+
return plan;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (installMode === 'split') {
|
|
1216
|
+
const usedDirs = new Set();
|
|
1217
|
+
contents.forEach((item, index) => {
|
|
1218
|
+
const skillName = slugify(getFrontmatterValue(item.content, 'name') || item.name || `${workflow.title}-${index + 1}`, `${workflow.uuid.substring(0, 8)}-${index + 1}`);
|
|
1219
|
+
const baseDirName = codexSkillDirName(workflow, item, contents.length > 1 && !getFrontmatterValue(item.content, 'name') ? String(index + 1) : '');
|
|
1220
|
+
let dirName = baseDirName;
|
|
1221
|
+
let duplicateIndex = 2;
|
|
1222
|
+
while (usedDirs.has(dirName)) {
|
|
1223
|
+
dirName = `${baseDirName}-${duplicateIndex}`;
|
|
1224
|
+
duplicateIndex++;
|
|
1225
|
+
}
|
|
1226
|
+
usedDirs.add(dirName);
|
|
1227
|
+
const destDir = path.join(CODEX_SKILLS_DIR, dirName);
|
|
1228
|
+
const destPath = path.join(destDir, 'SKILL.md');
|
|
1229
|
+
const content = ensureCodexSkillFrontmatter(item.content, skillName, getCodexDescription(workflow, item));
|
|
1230
|
+
addPlanFile(plan, destPath, content, item.name, item.type);
|
|
1231
|
+
});
|
|
1232
|
+
return plan;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const primaryItem = contents.find(item => /^skill\.md$/i.test(path.basename(item.name || ''))) || contents[0];
|
|
1236
|
+
const skillName = slugify(getFrontmatterValue(primaryItem?.content || '', 'name') || workflow.slug || workflow.title, workflow.uuid.substring(0, 8));
|
|
1237
|
+
const dirItem = getFrontmatterValue(primaryItem?.content || '', 'name') ? primaryItem : null;
|
|
1238
|
+
const destDir = path.join(CODEX_SKILLS_DIR, codexSkillDirName(workflow, dirItem));
|
|
1239
|
+
plan.baseDir = destDir;
|
|
1240
|
+
|
|
1241
|
+
if (installMode === 'single' || contents.length === 1) {
|
|
1242
|
+
const item = primaryItem;
|
|
1243
|
+
const content = ensureCodexSkillFrontmatter(item.content, skillName, getCodexDescription(workflow, item));
|
|
1244
|
+
addPlanFile(plan, path.join(destDir, 'SKILL.md'), content, item.name, item.type);
|
|
1245
|
+
return plan;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let hasEntrypoint = false;
|
|
1249
|
+
const usedRelNames = new Set();
|
|
1250
|
+
for (let i = 0; i < contents.length; i++) {
|
|
1251
|
+
const item = contents[i];
|
|
1252
|
+
const relName = sanitizeRelativePath(item.name || `file-${i + 1}.md`);
|
|
1253
|
+
let destName = /^skill\.md$/i.test(path.basename(relName)) ? 'SKILL.md' : relName;
|
|
1254
|
+
if (usedRelNames.has(destName)) {
|
|
1255
|
+
const ext = path.extname(destName);
|
|
1256
|
+
const base = destName.slice(0, destName.length - ext.length);
|
|
1257
|
+
let duplicateIndex = 2;
|
|
1258
|
+
let candidate = `${base}-${duplicateIndex}${ext}`;
|
|
1259
|
+
while (usedRelNames.has(candidate)) {
|
|
1260
|
+
duplicateIndex++;
|
|
1261
|
+
candidate = `${base}-${duplicateIndex}${ext}`;
|
|
1262
|
+
}
|
|
1263
|
+
destName = candidate;
|
|
1264
|
+
}
|
|
1265
|
+
usedRelNames.add(destName);
|
|
1266
|
+
const destPath = path.join(destDir, destName);
|
|
1267
|
+
const content = destName === 'SKILL.md'
|
|
1268
|
+
? ensureCodexSkillFrontmatter(item.content, skillName, getCodexDescription(workflow, item))
|
|
1269
|
+
: `${String(item.content || '').trim()}\n`;
|
|
1270
|
+
if (destName === 'SKILL.md') hasEntrypoint = true;
|
|
1271
|
+
addPlanFile(plan, destPath, content, item.name, item.type);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (!hasEntrypoint) {
|
|
1275
|
+
addPlanFile(plan, path.join(destDir, 'SKILL.md'), buildBundleEntrypoint(workflow, contents, skillName), 'SKILL.md', 'skill');
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return plan;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function publicInstallPlan(plan) {
|
|
1282
|
+
return {
|
|
1283
|
+
uuid: plan.uuid,
|
|
1284
|
+
title: plan.title,
|
|
1285
|
+
sourceUrl: plan.sourceUrl,
|
|
1286
|
+
targetTool: plan.targetTool,
|
|
1287
|
+
installMode: plan.installMode,
|
|
1288
|
+
manifestPath: plan.manifestPath,
|
|
1289
|
+
baseDir: plan.baseDir,
|
|
1290
|
+
risks: plan.risks,
|
|
1291
|
+
requiresConfirmation: hasCodexInstallRisks(plan),
|
|
1292
|
+
contentHash: plan.contentHash || '',
|
|
1293
|
+
agentMetadata: plan.agentMetadata || {},
|
|
1294
|
+
files: plan.files.map(file => ({
|
|
1295
|
+
path: file.path,
|
|
1296
|
+
sourceName: file.sourceName,
|
|
1297
|
+
sha256: file.sha256,
|
|
1298
|
+
bytes: file.bytes,
|
|
1299
|
+
riskFlags: file.riskFlags,
|
|
1300
|
+
exists: fs.existsSync(file.path),
|
|
1301
|
+
})),
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function hasCodexInstallRisks(plan) {
|
|
1306
|
+
return (plan.risks || []).some(risk => ['mcp', 'executable', 'env', 'absolute-path'].includes(risk));
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function formatRiskLine(file) {
|
|
1310
|
+
if (!file.riskFlags || file.riskFlags.length === 0) return '';
|
|
1311
|
+
return `${file.sourceName || path.basename(file.path)}: ${file.riskFlags.join(', ')}`;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async function confirmCodexInstallRisks(plan, opts = {}) {
|
|
1315
|
+
if (plan.installMode === 'stage_only') return;
|
|
1316
|
+
if (opts.dryRun || opts.stage || !hasCodexInstallRisks(plan)) return;
|
|
1317
|
+
if (opts.approveMcp || opts.approve_mcp || opts.yes) return;
|
|
1318
|
+
|
|
1319
|
+
if (opts.json || opts.throwOnError || process.env.TOKREPO_NONINTERACTIVE === '1') {
|
|
1320
|
+
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.`);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
warn(`This asset contains ${plan.risks.join(', ')} content.`);
|
|
1324
|
+
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}`);
|
|
1325
|
+
const riskyFiles = plan.files
|
|
1326
|
+
.map(formatRiskLine)
|
|
1327
|
+
.filter(Boolean)
|
|
1328
|
+
.slice(0, 8);
|
|
1329
|
+
for (const line of riskyFiles) {
|
|
1330
|
+
log(` ${C.yellow}!${C.reset} ${line}`);
|
|
1331
|
+
}
|
|
1332
|
+
if (plan.files.length > riskyFiles.length) {
|
|
1333
|
+
log(` ${C.dim}...and ${plan.files.length - riskyFiles.length} more file(s) in the plan${C.reset}`);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const answer = await ask('Write this Codex skill bundle anyway? (y/N):');
|
|
1337
|
+
if (answer.toLowerCase() !== 'y') {
|
|
1338
|
+
throw new Error('Install aborted.');
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function stageCodexInstallPlan(plan) {
|
|
1343
|
+
const stagedDir = path.join(CODEX_TOKREPO_DIR, 'staged');
|
|
1344
|
+
if (!fs.existsSync(stagedDir)) {
|
|
1345
|
+
fs.mkdirSync(stagedDir, { recursive: true, mode: 0o700 });
|
|
1346
|
+
}
|
|
1347
|
+
const stagePath = path.join(stagedDir, `${plan.uuid}.install-plan.json`);
|
|
1348
|
+
fs.writeFileSync(stagePath, `${JSON.stringify(publicInstallPlan(plan), null, 2)}\n`, { mode: 0o600 });
|
|
1349
|
+
return stagePath;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function executeStageOnlyCodexPlan(plan) {
|
|
1353
|
+
const installedFiles = [];
|
|
1354
|
+
const stageRoot = path.join(CODEX_TOKREPO_DIR, 'staged', plan.uuid);
|
|
1355
|
+
if (!fs.existsSync(stageRoot)) fs.mkdirSync(stageRoot, { recursive: true, mode: 0o700 });
|
|
1356
|
+
|
|
1357
|
+
for (const file of plan.files) {
|
|
1358
|
+
if (!ensureInside(stageRoot, file.path)) {
|
|
1359
|
+
throw new Error(`Stage path escaped TokRepo staging directory: ${file.path}`);
|
|
1360
|
+
}
|
|
1361
|
+
const destDir = path.dirname(file.path);
|
|
1362
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true, mode: 0o700 });
|
|
1363
|
+
fs.writeFileSync(file.path, file.content, { mode: 0o600 });
|
|
1364
|
+
installedFiles.push({
|
|
1365
|
+
path: file.path,
|
|
1366
|
+
sourceName: file.sourceName,
|
|
1367
|
+
sha256: sha256(file.content),
|
|
1368
|
+
bytes: Buffer.byteLength(String(file.content || '')),
|
|
1369
|
+
riskFlags: file.riskFlags,
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const stagePath = path.join(stageRoot, 'install-plan.json');
|
|
1374
|
+
fs.writeFileSync(stagePath, `${JSON.stringify(publicInstallPlan(plan), null, 2)}\n`, { mode: 0o600 });
|
|
1375
|
+
return { dryRun: true, staged: true, stageOnly: true, stagePath, plan: publicInstallPlan(plan), installedFiles };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function readCodexManifest() {
|
|
1379
|
+
try {
|
|
1380
|
+
const parsed = JSON.parse(fs.readFileSync(CODEX_MANIFEST_FILE, 'utf8'));
|
|
1381
|
+
if (Array.isArray(parsed.installs)) return parsed;
|
|
1382
|
+
} catch {}
|
|
1383
|
+
return { schemaVersion: 1, installs: [] };
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function writeCodexManifestRecord(plan, installedFiles) {
|
|
1387
|
+
if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
|
|
1388
|
+
fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
|
|
1389
|
+
}
|
|
1390
|
+
const manifest = readCodexManifest();
|
|
1391
|
+
const installedAt = new Date().toISOString();
|
|
1392
|
+
const record = {
|
|
1393
|
+
uuid: plan.uuid,
|
|
1394
|
+
title: plan.title,
|
|
1395
|
+
sourceUrl: plan.sourceUrl,
|
|
1396
|
+
targetTool: 'codex',
|
|
1397
|
+
installMode: plan.installMode,
|
|
1398
|
+
installedAt,
|
|
1399
|
+
contentHash: plan.contentHash || '',
|
|
1400
|
+
agentMetadata: plan.agentMetadata || {},
|
|
1401
|
+
installedFiles: installedFiles.map(file => ({
|
|
1402
|
+
path: file.path,
|
|
1403
|
+
sourceName: file.sourceName,
|
|
1404
|
+
sha256: file.sha256,
|
|
1405
|
+
bytes: file.bytes,
|
|
1406
|
+
riskFlags: file.riskFlags,
|
|
1407
|
+
})),
|
|
1408
|
+
risks: plan.risks,
|
|
1409
|
+
};
|
|
1410
|
+
manifest.installs = manifest.installs.filter(item => !(item.uuid === plan.uuid && item.targetTool === 'codex'));
|
|
1411
|
+
manifest.installs.push(record);
|
|
1412
|
+
manifest.updatedAt = installedAt;
|
|
1413
|
+
fs.writeFileSync(CODEX_MANIFEST_FILE, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
1414
|
+
return record;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function executeCodexInstallPlan(plan, opts = {}) {
|
|
1418
|
+
if (opts.dryRun) return { dryRun: true, plan: publicInstallPlan(plan), installedFiles: [] };
|
|
1419
|
+
if (plan.installMode === 'stage_only') return executeStageOnlyCodexPlan(plan);
|
|
1420
|
+
if (opts.stage) {
|
|
1421
|
+
const stagePath = stageCodexInstallPlan(plan);
|
|
1422
|
+
return { dryRun: true, staged: true, stagePath, plan: publicInstallPlan(plan), installedFiles: [] };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const installedFiles = [];
|
|
1426
|
+
for (const file of plan.files) {
|
|
1427
|
+
const destDir = path.dirname(file.path);
|
|
1428
|
+
if (!ensureInside(CODEX_SKILLS_DIR, file.path)) {
|
|
1429
|
+
throw new Error(`Install path escaped Codex skills directory: ${file.path}`);
|
|
1430
|
+
}
|
|
1431
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
1432
|
+
fs.writeFileSync(file.path, file.content);
|
|
1433
|
+
installedFiles.push({
|
|
1434
|
+
path: file.path,
|
|
1435
|
+
sourceName: file.sourceName,
|
|
1436
|
+
sha256: sha256(file.content),
|
|
1437
|
+
bytes: Buffer.byteLength(String(file.content || '')),
|
|
1438
|
+
riskFlags: file.riskFlags,
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const manifestRecord = writeCodexManifestRecord(plan, installedFiles);
|
|
1443
|
+
return { dryRun: false, plan: publicInstallPlan(plan), installedFiles, manifestRecord };
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function installCodexAsset(workflow, contents, opts = {}) {
|
|
1447
|
+
const plan = buildCodexInstallPlan(workflow, contents, opts);
|
|
1448
|
+
await confirmCodexInstallRisks(plan, opts);
|
|
1449
|
+
return executeCodexInstallPlan(plan, opts);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
841
1452
|
async function cmdInstall() {
|
|
842
1453
|
const args = parseArgs(process.argv);
|
|
843
1454
|
const target = args.positional[0];
|
|
844
1455
|
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`);
|
|
1456
|
+
showInstallHelp();
|
|
1457
|
+
process.exit(1);
|
|
853
1458
|
}
|
|
854
1459
|
|
|
855
|
-
log(`\n${C.bold}tokrepo install${C.reset}\n`);
|
|
1460
|
+
if (!args.flags.json) log(`\n${C.bold}tokrepo install${C.reset}\n`);
|
|
856
1461
|
|
|
857
1462
|
const config = readConfig();
|
|
858
1463
|
const apiBase = config?.api || DEFAULT_API;
|
|
859
1464
|
const installOpts = {
|
|
860
1465
|
targetTool: validateInstallTarget(args.flags.target),
|
|
861
1466
|
yes: Boolean(args.flags.yes),
|
|
1467
|
+
update: Boolean(args.flags.update),
|
|
1468
|
+
dryRun: Boolean(args.flags.dryRun || args.flags.dry_run),
|
|
1469
|
+
stage: Boolean(args.flags.stage),
|
|
1470
|
+
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
1471
|
+
json: Boolean(args.flags.json),
|
|
1472
|
+
manifest: Boolean(args.flags.manifest),
|
|
862
1473
|
};
|
|
863
1474
|
|
|
864
1475
|
// pack/<slug> dispatch — install entire theme pack
|
|
@@ -871,7 +1482,10 @@ Examples:
|
|
|
871
1482
|
return;
|
|
872
1483
|
}
|
|
873
1484
|
|
|
874
|
-
await installOneAsset(target, config, apiBase, installOpts);
|
|
1485
|
+
const result = await installOneAsset(target, config, apiBase, installOpts);
|
|
1486
|
+
if (args.flags.json) {
|
|
1487
|
+
outputJson(result);
|
|
1488
|
+
}
|
|
875
1489
|
}
|
|
876
1490
|
|
|
877
1491
|
// Install all assets in a theme pack — sequentially, continue past per-item errors
|
|
@@ -916,6 +1530,7 @@ async function installPack(slug, config, apiBase, opts) {
|
|
|
916
1530
|
async function installOneAsset(target, config, apiBase, opts) {
|
|
917
1531
|
opts = opts || {};
|
|
918
1532
|
const die = (msg) => { if (opts.throwOnError) throw new Error(msg); error(msg); };
|
|
1533
|
+
const emitInfo = (msg) => { if (!opts.json) info(msg); };
|
|
919
1534
|
|
|
920
1535
|
// Resolve target to UUID
|
|
921
1536
|
let uuid = target;
|
|
@@ -946,7 +1561,7 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
946
1561
|
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
1562
|
// Search by name (normalize separators for better matching)
|
|
948
1563
|
const normalizedTarget = normalizeQuery(uuid);
|
|
949
|
-
|
|
1564
|
+
emitInfo(`Searching for "${normalizedTarget}"...`);
|
|
950
1565
|
try {
|
|
951
1566
|
const encoded = encodeURIComponent(normalizedTarget);
|
|
952
1567
|
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 +1579,91 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
964
1579
|
const chosen = exact || searchData.list[0];
|
|
965
1580
|
|
|
966
1581
|
uuid = chosen.uuid;
|
|
967
|
-
|
|
1582
|
+
emitInfo(`Found: ${C.bold}${chosen.title}${C.reset}`);
|
|
968
1583
|
} catch (e) {
|
|
969
1584
|
die(`Search failed: ${e.message}`);
|
|
970
1585
|
}
|
|
971
1586
|
}
|
|
972
1587
|
|
|
973
1588
|
// Fetch the asset
|
|
974
|
-
|
|
1589
|
+
emitInfo(`Fetching ${uuid.substring(0, 8)}...`);
|
|
975
1590
|
|
|
976
|
-
let workflow
|
|
1591
|
+
let workflow;
|
|
977
1592
|
try {
|
|
978
1593
|
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${uuid}`, null, config?.token, apiBase);
|
|
979
1594
|
workflow = data.workflow;
|
|
980
|
-
files = data.workflow.files || [];
|
|
981
1595
|
} catch (e) {
|
|
982
1596
|
die(`Fetch failed: ${e.message}`);
|
|
983
1597
|
}
|
|
984
1598
|
|
|
985
|
-
|
|
986
|
-
|
|
1599
|
+
if (!opts.json) {
|
|
1600
|
+
log(`\n ${C.bold}${workflow.title}${C.reset}`);
|
|
1601
|
+
if (workflow.description) log(` ${C.dim}${workflow.description.substring(0, 100)}${C.reset}`);
|
|
1602
|
+
}
|
|
987
1603
|
|
|
988
1604
|
// 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
|
-
}
|
|
1605
|
+
let assetType = getWorkflowAssetType(workflow);
|
|
993
1606
|
|
|
994
1607
|
// Get content — prefer files, fallback to steps
|
|
995
|
-
const contents =
|
|
1608
|
+
const contents = extractInstallableContents(workflow, assetType);
|
|
996
1609
|
|
|
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
|
-
}
|
|
1610
|
+
if (contents.length === 0) {
|
|
1611
|
+
die('No installable content found in this asset.');
|
|
1003
1612
|
}
|
|
1004
1613
|
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1614
|
+
if (!opts.json) log('');
|
|
1615
|
+
const targetTool = normalizeInstallTarget(opts.targetTool);
|
|
1616
|
+
|
|
1617
|
+
if (targetTool === 'codex') {
|
|
1618
|
+
let result;
|
|
1619
|
+
try {
|
|
1620
|
+
result = await installCodexAsset(workflow, contents, opts);
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
die(e.message);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (!opts.json) {
|
|
1626
|
+
const plan = result.plan;
|
|
1627
|
+
if (result.staged || opts.stage) {
|
|
1628
|
+
info(`Staged install plan: ${result.stagePath}`);
|
|
1629
|
+
if (result.stageOnly) {
|
|
1630
|
+
info(`stage_only asset: files were written only under ${path.dirname(result.stagePath)}; no Codex skill was activated.`);
|
|
1631
|
+
} else {
|
|
1632
|
+
info(`No Codex skill files were written. Re-run with --approve-mcp or --yes to install.`);
|
|
1633
|
+
}
|
|
1634
|
+
} else if (opts.dryRun) {
|
|
1635
|
+
info(`Dry run: ${plan.files.length} file(s) would be installed to ${CODEX_SKILLS_DIR}`);
|
|
1636
|
+
for (const file of plan.files) {
|
|
1637
|
+
const rel = path.relative(os.homedir(), file.path);
|
|
1638
|
+
log(` ${C.dim}•${C.reset} ~/${rel}`);
|
|
1639
|
+
if (file.riskFlags.length) log(` ${C.yellow}${file.riskFlags.join(', ')}${C.reset}`);
|
|
1640
|
+
}
|
|
1641
|
+
} else {
|
|
1642
|
+
for (const file of result.installedFiles) {
|
|
1643
|
+
const relPath = path.relative(os.homedir(), file.path);
|
|
1644
|
+
success(`Installed: ~/${relPath}`);
|
|
1645
|
+
}
|
|
1646
|
+
log('');
|
|
1647
|
+
success(`${result.installedFiles.length} file(s) installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1648
|
+
log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
|
|
1649
|
+
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1011
1650
|
}
|
|
1012
1651
|
}
|
|
1013
|
-
}
|
|
1014
1652
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1653
|
+
return {
|
|
1654
|
+
uuid,
|
|
1655
|
+
title: workflow.title,
|
|
1656
|
+
targetTool: 'codex',
|
|
1657
|
+
dryRun: Boolean(opts.dryRun || opts.stage),
|
|
1658
|
+
staged: Boolean(result.staged),
|
|
1659
|
+
stagePath: result.stagePath,
|
|
1660
|
+
installMode: result.plan.installMode,
|
|
1661
|
+
installedFiles: result.installedFiles || [],
|
|
1662
|
+
plan: result.plan,
|
|
1663
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
1664
|
+
};
|
|
1017
1665
|
}
|
|
1018
1666
|
|
|
1019
|
-
log('');
|
|
1020
|
-
const targetTool = normalizeInstallTarget(opts.targetTool);
|
|
1021
|
-
|
|
1022
1667
|
if (targetTool === 'gemini') {
|
|
1023
1668
|
const destDir = path.join(process.cwd(), '.gemini');
|
|
1024
1669
|
if (!fs.existsSync(destDir)) {
|
|
@@ -1039,7 +1684,13 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1039
1684
|
log('');
|
|
1040
1685
|
success(`1 file installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1041
1686
|
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1042
|
-
return
|
|
1687
|
+
return {
|
|
1688
|
+
uuid,
|
|
1689
|
+
title: workflow.title,
|
|
1690
|
+
targetTool: 'gemini',
|
|
1691
|
+
installedFiles: [{ path: destPath }],
|
|
1692
|
+
sourceUrl: `https://tokrepo.com/en/workflows/${uuid}`,
|
|
1693
|
+
};
|
|
1043
1694
|
}
|
|
1044
1695
|
|
|
1045
1696
|
// Smart install based on asset type
|
|
@@ -1122,6 +1773,13 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1122
1773
|
log('');
|
|
1123
1774
|
success(`${installed} file(s) installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1124
1775
|
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1776
|
+
return {
|
|
1777
|
+
uuid,
|
|
1778
|
+
title: workflow.title,
|
|
1779
|
+
targetTool: targetTool || 'project',
|
|
1780
|
+
installed,
|
|
1781
|
+
sourceUrl: `https://tokrepo.com/en/workflows/${uuid}`,
|
|
1782
|
+
};
|
|
1125
1783
|
}
|
|
1126
1784
|
|
|
1127
1785
|
async function cmdWhoami() {
|
|
@@ -1141,13 +1799,33 @@ async function cmdWhoami() {
|
|
|
1141
1799
|
}
|
|
1142
1800
|
|
|
1143
1801
|
async function cmdList() {
|
|
1144
|
-
|
|
1802
|
+
const args = parseArgs(process.argv);
|
|
1803
|
+
if (!args.flags.json) log(`\n${C.bold}tokrepo list${C.reset}\n`);
|
|
1145
1804
|
|
|
1146
1805
|
const config = readConfig();
|
|
1147
1806
|
if (!config || !config.token) error('Not logged in. Run: tokrepo login');
|
|
1148
1807
|
|
|
1149
1808
|
try {
|
|
1150
|
-
const
|
|
1809
|
+
const pageSize = Number(args.flags.pageSize || (args.flags.all ? 200 : 50)) || 50;
|
|
1810
|
+
let page = Number(args.flags.page || 1) || 1;
|
|
1811
|
+
let data = await apiRequest('GET', `/api/v1/tokenboard/workflows/my?page=${page}&page_size=${pageSize}`, null, config.token, config.api);
|
|
1812
|
+
|
|
1813
|
+
if (args.flags.all) {
|
|
1814
|
+
const list = [...(data.list || [])];
|
|
1815
|
+
while (list.length < (data.total || 0)) {
|
|
1816
|
+
page++;
|
|
1817
|
+
const next = await apiRequest('GET', `/api/v1/tokenboard/workflows/my?page=${page}&page_size=${pageSize}`, null, config.token, config.api);
|
|
1818
|
+
const items = next.list || [];
|
|
1819
|
+
if (items.length === 0) break;
|
|
1820
|
+
list.push(...items);
|
|
1821
|
+
}
|
|
1822
|
+
data = { ...data, list };
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
if (args.flags.json) {
|
|
1826
|
+
outputJson({ total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1151
1829
|
|
|
1152
1830
|
if (!data.list || data.list.length === 0) {
|
|
1153
1831
|
info('No assets found. Run: tokrepo push');
|
|
@@ -1167,6 +1845,12 @@ async function cmdList() {
|
|
|
1167
1845
|
}
|
|
1168
1846
|
|
|
1169
1847
|
async function cmdUpdate() {
|
|
1848
|
+
const args = parseArgs(process.argv);
|
|
1849
|
+
if (args.flags.target || args.flags.all || args.flags.force) {
|
|
1850
|
+
await cmdSyncInstalled();
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1170
1854
|
const uuid = process.argv[3];
|
|
1171
1855
|
if (!uuid) error('Usage: tokrepo update <uuid> [file]');
|
|
1172
1856
|
|
|
@@ -1232,107 +1916,515 @@ async function cmdDelete() {
|
|
|
1232
1916
|
}
|
|
1233
1917
|
}
|
|
1234
1918
|
|
|
1919
|
+
function tagMatchesTypes(workflow, requestedTypes) {
|
|
1920
|
+
if (!requestedTypes || requestedTypes.length === 0) return true;
|
|
1921
|
+
const tags = (workflow.tags || []).flatMap(t => [t.slug, t.name]).filter(Boolean).map(t => String(t).toLowerCase());
|
|
1922
|
+
const assetType = getWorkflowAssetType(workflow);
|
|
1923
|
+
const metadataKind = String(workflow.asset_kind || workflow.agent_metadata?.asset_kind || workflow.agentMetadata?.assetKind || '').toLowerCase();
|
|
1924
|
+
return requestedTypes.some(type => {
|
|
1925
|
+
const needle = String(type).trim().toLowerCase();
|
|
1926
|
+
if (!needle) return false;
|
|
1927
|
+
if (metadataKind === needle || metadataKind === `${needle}s`) return true;
|
|
1928
|
+
if (assetType === needle || assetType === `${needle}s`) return true;
|
|
1929
|
+
return tags.some(tag => tag === needle || tag === `${needle}s` || tag.includes(needle));
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function itemMatchesKeyword(workflow, keyword) {
|
|
1934
|
+
if (!keyword) return true;
|
|
1935
|
+
const needle = normalizeQuery(keyword).toLowerCase();
|
|
1936
|
+
const fields = [
|
|
1937
|
+
workflow.title,
|
|
1938
|
+
workflow.slug,
|
|
1939
|
+
workflow.description,
|
|
1940
|
+
...(workflow.tags || []).flatMap(t => [t.name, t.slug]),
|
|
1941
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
1942
|
+
return needle.split(/\s+/).every(word => fields.includes(word));
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
async function fetchCloneItems(username, config, apiBase, args) {
|
|
1946
|
+
const pageSize = Number(args.flags.pageSize || 200) || 200;
|
|
1947
|
+
const keyword = args.flags.keyword || '';
|
|
1948
|
+
const requestedTypes = String(args.flags.types || '')
|
|
1949
|
+
.split(',')
|
|
1950
|
+
.map(s => s.trim())
|
|
1951
|
+
.filter(Boolean);
|
|
1952
|
+
|
|
1953
|
+
let effectiveUsername = username.startsWith('@') ? username.slice(1) : username;
|
|
1954
|
+
const result = { username: effectiveUsername, source: 'public', list: [], total: 0 };
|
|
1955
|
+
|
|
1956
|
+
let cloneSelf = effectiveUsername === 'me';
|
|
1957
|
+
if (config?.token) {
|
|
1958
|
+
try {
|
|
1959
|
+
const me = await apiRequest('GET', '/api/v1/tokenboard/auth/me', null, config.token, apiBase);
|
|
1960
|
+
if (effectiveUsername === 'me' || me.nickname?.toLowerCase() === effectiveUsername.toLowerCase()) {
|
|
1961
|
+
cloneSelf = true;
|
|
1962
|
+
effectiveUsername = me.nickname || effectiveUsername;
|
|
1963
|
+
result.username = effectiveUsername;
|
|
1964
|
+
}
|
|
1965
|
+
} catch { /* anonymous/public clone still works */ }
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
if (cloneSelf) {
|
|
1969
|
+
if (!config?.token) error('Cloning @me requires login or TOKREPO_TOKEN.');
|
|
1970
|
+
let page = 1;
|
|
1971
|
+
while (true) {
|
|
1972
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/my?page=${page}&page_size=${pageSize}`, null, config.token, apiBase);
|
|
1973
|
+
const items = data.list || [];
|
|
1974
|
+
result.total = data.total || result.total;
|
|
1975
|
+
result.list.push(...items);
|
|
1976
|
+
if (items.length < pageSize || result.list.length >= result.total) break;
|
|
1977
|
+
page++;
|
|
1978
|
+
}
|
|
1979
|
+
result.source = 'my';
|
|
1980
|
+
} else {
|
|
1981
|
+
let page = 1;
|
|
1982
|
+
while (true) {
|
|
1983
|
+
const params = [
|
|
1984
|
+
`author_name=${encodeURIComponent(effectiveUsername)}`,
|
|
1985
|
+
`page=${page}`,
|
|
1986
|
+
`page_size=${pageSize}`,
|
|
1987
|
+
'sort_by=latest',
|
|
1988
|
+
];
|
|
1989
|
+
if (keyword) params.push(`keyword=${encodeURIComponent(normalizeQuery(keyword))}`);
|
|
1990
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?${params.join('&')}`, null, config?.token, apiBase);
|
|
1991
|
+
const items = data.list || data.items || [];
|
|
1992
|
+
result.total = data.total || result.total;
|
|
1993
|
+
result.list.push(...items);
|
|
1994
|
+
if (items.length < pageSize || result.list.length >= result.total) break;
|
|
1995
|
+
page++;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
result.list = result.list.filter(item => itemMatchesKeyword(item, keyword) && tagMatchesTypes(item, requestedTypes));
|
|
2000
|
+
result.count = result.list.length;
|
|
2001
|
+
result.keyword = keyword || undefined;
|
|
2002
|
+
result.types = requestedTypes;
|
|
2003
|
+
return result;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1235
2006
|
async function cmdClone() {
|
|
1236
|
-
const
|
|
1237
|
-
|
|
2007
|
+
const args = parseArgs(process.argv);
|
|
2008
|
+
const target = args.positional[0];
|
|
2009
|
+
if (!target) {
|
|
2010
|
+
showCloneHelp();
|
|
2011
|
+
process.exit(1);
|
|
2012
|
+
}
|
|
1238
2013
|
|
|
1239
|
-
|
|
2014
|
+
const json = Boolean(args.flags.json);
|
|
2015
|
+
if (!json) log(`\n${C.bold}tokrepo clone${C.reset}\n`);
|
|
1240
2016
|
|
|
1241
2017
|
const config = readConfig();
|
|
1242
2018
|
const apiBase = config?.api || DEFAULT_API;
|
|
2019
|
+
const targetTool = validateInstallTarget(args.flags.target);
|
|
2020
|
+
const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
|
|
1243
2021
|
|
|
1244
|
-
// Extract username from @username format
|
|
1245
|
-
let username = target;
|
|
1246
|
-
if (username.startsWith('@')) username = username.slice(1);
|
|
1247
|
-
|
|
1248
|
-
// Step 1: Find user's UUID by searching for their workflows
|
|
1249
|
-
info(`Finding user @${username}...`);
|
|
1250
|
-
let authorUuid = '';
|
|
1251
2022
|
try {
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
if (
|
|
1257
|
-
|
|
1258
|
-
|
|
2023
|
+
if (!json) info(`Fetching assets from ${target}...`);
|
|
2024
|
+
const cloneItems = await fetchCloneItems(target, config, apiBase, args);
|
|
2025
|
+
|
|
2026
|
+
if (cloneItems.list.length === 0) {
|
|
2027
|
+
if (json) {
|
|
2028
|
+
outputJson({ target, count: 0, list: [] });
|
|
2029
|
+
} else {
|
|
2030
|
+
info(`${target} has no matching assets.`);
|
|
1259
2031
|
}
|
|
2032
|
+
return;
|
|
1260
2033
|
}
|
|
1261
|
-
} catch { /* fall through */ }
|
|
1262
2034
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
if (
|
|
2035
|
+
if (!json) log(` Found ${C.bold}${cloneItems.list.length}${C.reset} matching asset(s)\n`);
|
|
2036
|
+
|
|
2037
|
+
if (targetTool === 'codex') {
|
|
2038
|
+
const results = [];
|
|
2039
|
+
let installedCount = 0;
|
|
2040
|
+
for (let i = 0; i < cloneItems.list.length; i++) {
|
|
2041
|
+
const item = cloneItems.list[i];
|
|
2042
|
+
if (!json) log(`${C.dim}[${i + 1}/${cloneItems.list.length}]${C.reset} ${item.title}`);
|
|
2043
|
+
try {
|
|
2044
|
+
const detail = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${item.uuid}`, null, config?.token, apiBase);
|
|
2045
|
+
const workflow = detail.workflow;
|
|
2046
|
+
const assetType = getWorkflowAssetType(workflow);
|
|
2047
|
+
const contents = extractInstallableContents(workflow, assetType);
|
|
2048
|
+
if (contents.length === 0) throw new Error('No installable content found');
|
|
2049
|
+
const result = await installCodexAsset(workflow, contents, {
|
|
2050
|
+
...args.flags,
|
|
2051
|
+
dryRun,
|
|
2052
|
+
stage: Boolean(args.flags.stage),
|
|
2053
|
+
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2054
|
+
json: true,
|
|
2055
|
+
throwOnError: true,
|
|
2056
|
+
});
|
|
2057
|
+
if (!dryRun) installedCount += result.installedFiles.length;
|
|
2058
|
+
results.push({
|
|
2059
|
+
uuid: workflow.uuid,
|
|
2060
|
+
title: workflow.title,
|
|
2061
|
+
dryRun: Boolean(dryRun || args.flags.stage),
|
|
2062
|
+
staged: Boolean(result.staged),
|
|
2063
|
+
stagePath: result.stagePath,
|
|
2064
|
+
installMode: result.plan.installMode,
|
|
2065
|
+
files: result.plan.files,
|
|
2066
|
+
installedFiles: result.installedFiles || [],
|
|
2067
|
+
risks: result.plan.risks,
|
|
2068
|
+
});
|
|
2069
|
+
if (!json) {
|
|
2070
|
+
const fileCount = (dryRun || args.flags.stage) ? result.plan.files.length : result.installedFiles.length;
|
|
2071
|
+
success(`${args.flags.stage ? 'Staged' : dryRun ? 'Planned' : 'Installed'} ${fileCount} file(s)`);
|
|
2072
|
+
}
|
|
2073
|
+
} catch (e) {
|
|
2074
|
+
results.push({ uuid: item.uuid, title: item.title, error: e.message });
|
|
2075
|
+
if (!json) warn(`Skipped "${item.title}": ${e.message}`);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const response = {
|
|
2080
|
+
target,
|
|
2081
|
+
username: cloneItems.username,
|
|
2082
|
+
targetTool: 'codex',
|
|
2083
|
+
dryRun,
|
|
2084
|
+
total: cloneItems.total,
|
|
2085
|
+
count: cloneItems.count,
|
|
2086
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
2087
|
+
results,
|
|
2088
|
+
};
|
|
2089
|
+
if (json) {
|
|
2090
|
+
outputJson(response);
|
|
2091
|
+
} else {
|
|
2092
|
+
log('');
|
|
2093
|
+
if (args.flags.stage) {
|
|
2094
|
+
success(`Staged ${results.filter(r => !r.error).length}/${cloneItems.list.length} asset install plan(s)`);
|
|
2095
|
+
} else if (dryRun) {
|
|
2096
|
+
success(`Dry run complete: ${results.filter(r => !r.error).length}/${cloneItems.list.length} assets planned`);
|
|
2097
|
+
} else {
|
|
2098
|
+
success(`Installed ${installedCount} Codex file(s) from ${results.filter(r => !r.error).length}/${cloneItems.list.length} assets`);
|
|
2099
|
+
log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}\n`);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
if (targetTool && targetTool !== 'codex') {
|
|
2106
|
+
error(`clone --target ${targetTool} is not implemented yet. Supported clone target: codex`);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
if (json) {
|
|
2110
|
+
outputJson(cloneItems);
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// Legacy raw clone behavior for users who do not specify a target.
|
|
2115
|
+
const outDir = path.join(process.cwd(), cloneItems.username);
|
|
2116
|
+
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
2117
|
+
|
|
2118
|
+
let downloaded = 0;
|
|
2119
|
+
for (const item of cloneItems.list) {
|
|
2120
|
+
const title = item.title || item.uuid;
|
|
2121
|
+
const safeDirName = title.replace(/[/\\?%*:|"<>]/g, '-').substring(0, 80);
|
|
2122
|
+
const assetDir = path.join(outDir, safeDirName);
|
|
2123
|
+
|
|
1266
2124
|
try {
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
2125
|
+
const detail = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${item.uuid}`, null, config?.token, apiBase);
|
|
2126
|
+
const workflow = detail.workflow;
|
|
2127
|
+
const contents = extractInstallableContents(workflow, getWorkflowAssetType(workflow));
|
|
2128
|
+
|
|
2129
|
+
if (contents.length > 0) {
|
|
2130
|
+
if (!fs.existsSync(assetDir)) fs.mkdirSync(assetDir, { recursive: true });
|
|
2131
|
+
for (const contentItem of contents) {
|
|
2132
|
+
const safeName = sanitizeRelativePath(contentItem.name || 'content.md');
|
|
2133
|
+
fs.writeFileSync(path.join(assetDir, safeName), contentItem.content);
|
|
2134
|
+
}
|
|
2135
|
+
downloaded++;
|
|
2136
|
+
log(` ${C.green}✓${C.reset} ${safeDirName} ${C.dim}(${contents.length} files)${C.reset}`);
|
|
1270
2137
|
}
|
|
1271
|
-
} catch {
|
|
2138
|
+
} catch (e) {
|
|
2139
|
+
log(` ${C.yellow}!${C.reset} ${safeDirName} ${C.dim}(skipped: ${e.message})${C.reset}`);
|
|
2140
|
+
}
|
|
1272
2141
|
}
|
|
1273
|
-
|
|
2142
|
+
|
|
2143
|
+
log('');
|
|
2144
|
+
success(`Cloned ${downloaded}/${cloneItems.list.length} assets to ./${cloneItems.username}/`);
|
|
2145
|
+
} catch (e) {
|
|
2146
|
+
error(`Clone failed: ${e.message}`);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
function currentFileSha(filePath) {
|
|
2151
|
+
try {
|
|
2152
|
+
return sha256(fs.readFileSync(filePath, 'utf8'));
|
|
2153
|
+
} catch {
|
|
2154
|
+
return '';
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function diffCodexPlanWithLocal(plan, manifestRecord = {}) {
|
|
2159
|
+
const reasons = [];
|
|
2160
|
+
const desired = new Map(plan.files.map(file => [file.path, file.sha256]));
|
|
2161
|
+
const recordedFiles = manifestRecord.installedFiles || manifestRecord.installed_files || [];
|
|
2162
|
+
|
|
2163
|
+
for (const file of plan.files) {
|
|
2164
|
+
if (!fs.existsSync(file.path)) {
|
|
2165
|
+
reasons.push({ type: 'missing', path: file.path });
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
const actualSha = currentFileSha(file.path);
|
|
2169
|
+
if (actualSha !== file.sha256) {
|
|
2170
|
+
reasons.push({ type: 'changed', path: file.path, actualSha, expectedSha: file.sha256 });
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
for (const file of recordedFiles) {
|
|
2175
|
+
if (file.path && !desired.has(file.path)) {
|
|
2176
|
+
reasons.push({ type: 'obsolete-manifest-path', path: file.path });
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
return {
|
|
2181
|
+
needsUpdate: reasons.length > 0,
|
|
2182
|
+
reasons,
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
async function fetchWorkflowForInstall(uuid, config, apiBase) {
|
|
2187
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${encodeURIComponent(uuid)}`, null, config?.token, apiBase);
|
|
2188
|
+
const workflow = data.workflow;
|
|
2189
|
+
const contents = extractInstallableContents(workflow, getWorkflowAssetType(workflow));
|
|
2190
|
+
if (contents.length === 0) {
|
|
2191
|
+
throw new Error('No installable content found');
|
|
2192
|
+
}
|
|
2193
|
+
return { workflow, contents };
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
async function cmdSyncInstalled() {
|
|
2197
|
+
const args = parseArgs(process.argv);
|
|
2198
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
2199
|
+
if (targetTool !== 'codex') {
|
|
2200
|
+
error(`sync-installed currently supports --target codex only`);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
const json = Boolean(args.flags.json);
|
|
2204
|
+
if (!json) log(`\n${C.bold}tokrepo sync-installed${C.reset}\n`);
|
|
2205
|
+
|
|
2206
|
+
const manifest = readCodexManifest();
|
|
2207
|
+
const installed = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
|
|
2208
|
+
const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
|
|
2209
|
+
const stage = Boolean(args.flags.stage);
|
|
2210
|
+
if (installed.length === 0) {
|
|
2211
|
+
if (json) outputJson({ targetTool: 'codex', manifestPath: CODEX_MANIFEST_FILE, dryRun, stage, count: 0, results: [] });
|
|
2212
|
+
else info(`No Codex installs found in ${CODEX_MANIFEST_FILE}`);
|
|
2213
|
+
return;
|
|
1274
2214
|
}
|
|
1275
2215
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
2216
|
+
const config = readConfig();
|
|
2217
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
2218
|
+
const force = Boolean(args.flags.update || args.flags.force || args.flags.all);
|
|
2219
|
+
const results = [];
|
|
2220
|
+
|
|
2221
|
+
for (let i = 0; i < installed.length; i++) {
|
|
2222
|
+
const record = installed[i];
|
|
2223
|
+
const uuid = record.uuid;
|
|
2224
|
+
if (!uuid) continue;
|
|
2225
|
+
|
|
2226
|
+
if (!json) log(`${C.dim}[${i + 1}/${installed.length}]${C.reset} ${record.title || uuid}`);
|
|
2227
|
+
|
|
1281
2228
|
try {
|
|
1282
|
-
const
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
2229
|
+
const { workflow, contents } = await fetchWorkflowForInstall(uuid, config, apiBase);
|
|
2230
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode });
|
|
2231
|
+
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2232
|
+
const shouldWrite = force || diff.needsUpdate;
|
|
2233
|
+
|
|
2234
|
+
if (dryRun) {
|
|
2235
|
+
results.push({
|
|
2236
|
+
uuid,
|
|
2237
|
+
title: workflow.title,
|
|
2238
|
+
status: shouldWrite ? 'would-update' : 'unchanged',
|
|
2239
|
+
needsUpdate: shouldWrite,
|
|
2240
|
+
reasons: diff.reasons,
|
|
2241
|
+
plan: publicInstallPlan(plan),
|
|
2242
|
+
});
|
|
2243
|
+
if (!json) {
|
|
2244
|
+
const label = shouldWrite ? `${C.yellow}would update${C.reset}` : `${C.green}unchanged${C.reset}`;
|
|
2245
|
+
log(` ${label}`);
|
|
2246
|
+
}
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
if (!shouldWrite) {
|
|
2251
|
+
results.push({
|
|
2252
|
+
uuid,
|
|
2253
|
+
title: workflow.title,
|
|
2254
|
+
status: 'unchanged',
|
|
2255
|
+
needsUpdate: false,
|
|
2256
|
+
reasons: [],
|
|
2257
|
+
});
|
|
2258
|
+
if (!json) success('Unchanged');
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
const installResult = await installCodexAsset(workflow, contents, {
|
|
2263
|
+
...args.flags,
|
|
2264
|
+
dryRun: false,
|
|
2265
|
+
stage,
|
|
2266
|
+
installMode: record.installMode || record.install_mode,
|
|
2267
|
+
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2268
|
+
json: true,
|
|
2269
|
+
throwOnError: true,
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
results.push({
|
|
2273
|
+
uuid,
|
|
2274
|
+
title: workflow.title,
|
|
2275
|
+
status: stage ? 'staged' : 'updated',
|
|
2276
|
+
needsUpdate: true,
|
|
2277
|
+
reasons: diff.reasons,
|
|
2278
|
+
stagePath: installResult.stagePath,
|
|
2279
|
+
installedFiles: installResult.installedFiles || [],
|
|
2280
|
+
plan: installResult.plan,
|
|
2281
|
+
});
|
|
2282
|
+
if (!json) success(stage ? `Staged ${installResult.plan.files.length} file(s)` : `Updated ${(installResult.installedFiles || []).length} file(s)`);
|
|
1288
2283
|
} catch (e) {
|
|
1289
|
-
|
|
2284
|
+
results.push({ uuid, title: record.title || uuid, status: 'failed', error: e.message });
|
|
2285
|
+
if (!json) warn(`Failed: ${e.message}`);
|
|
1290
2286
|
}
|
|
1291
2287
|
}
|
|
1292
2288
|
|
|
1293
|
-
|
|
1294
|
-
|
|
2289
|
+
const summary = {
|
|
2290
|
+
targetTool: 'codex',
|
|
2291
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
2292
|
+
dryRun,
|
|
2293
|
+
stage,
|
|
2294
|
+
count: results.length,
|
|
2295
|
+
updated: results.filter(item => item.status === 'updated').length,
|
|
2296
|
+
staged: results.filter(item => item.status === 'staged').length,
|
|
2297
|
+
unchanged: results.filter(item => item.status === 'unchanged').length,
|
|
2298
|
+
failed: results.filter(item => item.status === 'failed').length,
|
|
2299
|
+
results,
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
if (json) {
|
|
2303
|
+
outputJson(summary);
|
|
2304
|
+
} else {
|
|
2305
|
+
log('');
|
|
2306
|
+
success(`Sync complete: ${summary.updated} updated, ${summary.staged} staged, ${summary.unchanged} unchanged, ${summary.failed} failed`);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
async function cmdInstalled() {
|
|
2311
|
+
const args = parseArgs(process.argv);
|
|
2312
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
2313
|
+
if (targetTool !== 'codex') error(`installed currently supports --target codex only`);
|
|
2314
|
+
|
|
2315
|
+
const json = Boolean(args.flags.json);
|
|
2316
|
+
if (!json) log(`\n${C.bold}tokrepo installed${C.reset}\n`);
|
|
2317
|
+
|
|
2318
|
+
const manifest = readCodexManifest();
|
|
2319
|
+
const records = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
|
|
2320
|
+
const list = records.map(record => {
|
|
2321
|
+
const files = (record.installedFiles || record.installed_files || []).map(file => {
|
|
2322
|
+
const actualSha = file.path && fs.existsSync(file.path) ? currentFileSha(file.path) : '';
|
|
2323
|
+
return {
|
|
2324
|
+
path: file.path,
|
|
2325
|
+
sourceName: file.sourceName || file.source_name,
|
|
2326
|
+
sha256: file.sha256,
|
|
2327
|
+
exists: Boolean(file.path && fs.existsSync(file.path)),
|
|
2328
|
+
changed: Boolean(actualSha && file.sha256 && actualSha !== file.sha256),
|
|
2329
|
+
};
|
|
2330
|
+
});
|
|
2331
|
+
return {
|
|
2332
|
+
uuid: record.uuid,
|
|
2333
|
+
title: record.title,
|
|
2334
|
+
sourceUrl: record.sourceUrl || record.source_url,
|
|
2335
|
+
targetTool: 'codex',
|
|
2336
|
+
installMode: record.installMode || record.install_mode,
|
|
2337
|
+
installedAt: record.installedAt || record.installed_at,
|
|
2338
|
+
contentHash: record.contentHash || record.content_hash || '',
|
|
2339
|
+
risks: record.risks || [],
|
|
2340
|
+
files,
|
|
2341
|
+
status: files.some(file => !file.exists) ? 'missing-files' : files.some(file => file.changed) ? 'local-changes' : 'installed',
|
|
2342
|
+
};
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
if (json) {
|
|
2346
|
+
outputJson({ targetTool: 'codex', manifestPath: CODEX_MANIFEST_FILE, count: list.length, list });
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
if (list.length === 0) {
|
|
2351
|
+
info(`No Codex installs found in ${CODEX_MANIFEST_FILE}`);
|
|
1295
2352
|
return;
|
|
1296
2353
|
}
|
|
1297
2354
|
|
|
1298
|
-
|
|
2355
|
+
for (const item of list) {
|
|
2356
|
+
const color = item.status === 'installed' ? C.green : C.yellow;
|
|
2357
|
+
log(` ${color}${item.status}${C.reset} ${C.bold}${item.title || item.uuid}${C.reset}`);
|
|
2358
|
+
log(` ${C.dim}${item.uuid} · ${item.installMode || 'unknown'} · ${item.files.length} file(s)${C.reset}\n`);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
async function cmdOutdated() {
|
|
2363
|
+
const args = parseArgs(process.argv);
|
|
2364
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
2365
|
+
if (targetTool !== 'codex') error(`outdated currently supports --target codex only`);
|
|
2366
|
+
|
|
2367
|
+
const json = Boolean(args.flags.json);
|
|
2368
|
+
if (!json) log(`\n${C.bold}tokrepo outdated${C.reset}\n`);
|
|
1299
2369
|
|
|
1300
|
-
|
|
1301
|
-
const
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
2370
|
+
const manifest = readCodexManifest();
|
|
2371
|
+
const installed = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
|
|
2372
|
+
if (installed.length === 0) {
|
|
2373
|
+
if (json) outputJson({ targetTool: 'codex', count: 0, outdated: 0, list: [] });
|
|
2374
|
+
else info(`No Codex installs found in ${CODEX_MANIFEST_FILE}`);
|
|
2375
|
+
return;
|
|
1304
2376
|
}
|
|
1305
2377
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
2378
|
+
const config = readConfig();
|
|
2379
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
2380
|
+
const list = [];
|
|
2381
|
+
let unchanged = 0;
|
|
2382
|
+
let failed = 0;
|
|
1311
2383
|
|
|
2384
|
+
for (const record of installed) {
|
|
1312
2385
|
try {
|
|
1313
|
-
const
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
if (
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
downloaded++;
|
|
1327
|
-
log(` ${C.green}✓${C.reset} ${safeDirName} ${C.dim}(${workflow.steps.length} files)${C.reset}`);
|
|
2386
|
+
const { workflow, contents } = await fetchWorkflowForInstall(record.uuid, config, apiBase);
|
|
2387
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode });
|
|
2388
|
+
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2389
|
+
if (diff.needsUpdate) {
|
|
2390
|
+
list.push({
|
|
2391
|
+
uuid: record.uuid,
|
|
2392
|
+
title: workflow.title,
|
|
2393
|
+
status: 'outdated',
|
|
2394
|
+
reasons: diff.reasons,
|
|
2395
|
+
plan: publicInstallPlan(plan),
|
|
2396
|
+
});
|
|
2397
|
+
} else {
|
|
2398
|
+
unchanged++;
|
|
1328
2399
|
}
|
|
1329
2400
|
} catch (e) {
|
|
1330
|
-
|
|
2401
|
+
failed++;
|
|
2402
|
+
list.push({ uuid: record.uuid, title: record.title || record.uuid, status: 'failed', error: e.message });
|
|
1331
2403
|
}
|
|
1332
2404
|
}
|
|
1333
2405
|
|
|
2406
|
+
if (json) {
|
|
2407
|
+
outputJson({ targetTool: 'codex', manifestPath: CODEX_MANIFEST_FILE, count: installed.length, outdated: list.filter(i => i.status === 'outdated').length, unchanged, failed, list });
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
const outdated = list.filter(item => item.status === 'outdated');
|
|
2412
|
+
if (outdated.length === 0 && failed === 0) {
|
|
2413
|
+
success(`All ${unchanged} Codex install(s) are up to date.`);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
for (const item of list) {
|
|
2417
|
+
if (item.status === 'failed') {
|
|
2418
|
+
warn(`${item.title}: ${item.error}`);
|
|
2419
|
+
} else {
|
|
2420
|
+
log(` ${C.yellow}outdated${C.reset} ${C.bold}${item.title}${C.reset}`);
|
|
2421
|
+
for (const reason of item.reasons.slice(0, 3)) {
|
|
2422
|
+
log(` ${C.dim}${reason.type}: ${reason.path || ''}${C.reset}`);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
1334
2426
|
log('');
|
|
1335
|
-
|
|
2427
|
+
info(`Run ${C.cyan}tokrepo update --target codex --all${C.reset} to update installed Codex assets.`);
|
|
1336
2428
|
}
|
|
1337
2429
|
|
|
1338
2430
|
async function cmdTags() {
|
|
@@ -1453,19 +2545,24 @@ ${C.bold}USAGE${C.reset}
|
|
|
1453
2545
|
|
|
1454
2546
|
${C.bold}DISCOVER & INSTALL${C.reset}
|
|
1455
2547
|
${C.cyan}search${C.reset} <query> Search assets by keyword
|
|
2548
|
+
${C.cyan}detail${C.reset} <name|uuid> Show full asset metadata
|
|
1456
2549
|
${C.cyan}install${C.reset} <name|uuid> Smart install (auto-detects type & placement)
|
|
1457
2550
|
${C.cyan}pull${C.reset} <url|uuid|@u/n> Download raw asset files
|
|
1458
2551
|
${C.cyan}clone${C.reset} @username Clone all assets from a user
|
|
2552
|
+
${C.cyan}installed${C.reset} List installed Codex assets from manifest
|
|
2553
|
+
${C.cyan}outdated${C.reset} Check installed Codex assets for updates
|
|
2554
|
+
${C.cyan}sync-installed${C.reset} Update installed Codex assets from manifest
|
|
1459
2555
|
|
|
1460
2556
|
${C.bold}PUBLISH${C.reset}
|
|
1461
2557
|
${C.cyan}push${C.reset} [files...] Push files/directory (idempotent upsert)
|
|
1462
2558
|
${C.cyan}status${C.reset} Compare local vs remote (like git status)
|
|
1463
2559
|
${C.cyan}init${C.reset} Create .tokrepo.json project config
|
|
1464
|
-
${C.cyan}update${C.reset} <uuid> [f] Update existing asset
|
|
2560
|
+
${C.cyan}update${C.reset} <uuid> [f] Update existing remote asset
|
|
2561
|
+
${C.cyan}update${C.reset} --target codex --all Update installed Codex assets
|
|
1465
2562
|
${C.cyan}delete${C.reset} <uuid> Delete an asset
|
|
1466
2563
|
|
|
1467
2564
|
${C.bold}ACCOUNT${C.reset}
|
|
1468
|
-
${C.cyan}login${C.reset} Save API
|
|
2565
|
+
${C.cyan}login${C.reset} Save API key (or set TOKREPO_TOKEN env var)
|
|
1469
2566
|
${C.cyan}list${C.reset} List your published assets
|
|
1470
2567
|
${C.cyan}tags${C.reset} List available tags
|
|
1471
2568
|
${C.cyan}whoami${C.reset} Show current user
|
|
@@ -1477,10 +2574,14 @@ ${C.bold}PUSH OPTIONS${C.reset}
|
|
|
1477
2574
|
${C.cyan}--title${C.reset} "..." Set title (auto-detected from README or dir name)
|
|
1478
2575
|
${C.cyan}--desc${C.reset} "..." Set description
|
|
1479
2576
|
${C.cyan}--tag${C.reset} Skills Add tag (repeatable)
|
|
2577
|
+
${C.cyan}--kind${C.reset} skill Set agent asset_kind
|
|
2578
|
+
${C.cyan}--target${C.reset} codex Add target tool metadata on push
|
|
2579
|
+
${C.cyan}--install-mode${C.reset} bundle Set install_mode metadata
|
|
1480
2580
|
|
|
1481
2581
|
${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
1482
2582
|
Skills → .claude/skills/ (if .claude/ exists)
|
|
1483
2583
|
Gemini → .gemini/GEMINI.md (with --target gemini)
|
|
2584
|
+
Codex → ~/.codex/skills/ (with --target codex)
|
|
1484
2585
|
Scripts → current dir (chmod +x)
|
|
1485
2586
|
Configs → project root
|
|
1486
2587
|
MCP → current dir (.json)
|
|
@@ -1488,9 +2589,18 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
|
1488
2589
|
|
|
1489
2590
|
${C.bold}EXAMPLES${C.reset}
|
|
1490
2591
|
tokrepo search "mcp server" # Find MCP configs
|
|
2592
|
+
tokrepo search video --json # Machine-readable search
|
|
2593
|
+
tokrepo detail ca000374-f5d8-... --json # Machine-readable detail
|
|
1491
2594
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
2595
|
+
tokrepo install ca000374-f5d8-... --target codex
|
|
1492
2596
|
tokrepo install c4b18aeb --target gemini # Install for Gemini CLI
|
|
2597
|
+
tokrepo clone @henuwangkai --target codex --keyword video
|
|
2598
|
+
tokrepo installed --target codex --json
|
|
2599
|
+
tokrepo outdated --target codex --json
|
|
2600
|
+
tokrepo update --target codex --all
|
|
2601
|
+
tokrepo sync-installed --target codex --dry-run
|
|
1493
2602
|
tokrepo push --private my-rules.md # Save one file privately
|
|
2603
|
+
tokrepo push . --kind skill --target codex --install-mode bundle
|
|
1494
2604
|
tokrepo push --public skill.md # Share one file publicly
|
|
1495
2605
|
tokrepo push --private . # Push current dir as private
|
|
1496
2606
|
tokrepo push --public --title "My MCP" . # Push dir publicly with title
|
|
@@ -1511,10 +2621,141 @@ ${C.bold}GET YOUR TOKEN${C.reset}
|
|
|
1511
2621
|
`);
|
|
1512
2622
|
}
|
|
1513
2623
|
|
|
2624
|
+
function showSearchHelp() {
|
|
2625
|
+
log(`
|
|
2626
|
+
${C.bold}tokrepo search${C.reset}
|
|
2627
|
+
|
|
2628
|
+
USAGE
|
|
2629
|
+
tokrepo search <query> [--json] [--all] [--page-size N] [--sort-by views|latest|stars|popular]
|
|
2630
|
+
|
|
2631
|
+
EXAMPLES
|
|
2632
|
+
tokrepo search video
|
|
2633
|
+
tokrepo search video --json
|
|
2634
|
+
tokrepo search "mcp server" --json --all
|
|
2635
|
+
`);
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
function showDetailHelp() {
|
|
2639
|
+
log(`
|
|
2640
|
+
${C.bold}tokrepo detail${C.reset}
|
|
2641
|
+
|
|
2642
|
+
USAGE
|
|
2643
|
+
tokrepo detail <uuid|url|name> [--json]
|
|
2644
|
+
|
|
2645
|
+
EXAMPLES
|
|
2646
|
+
tokrepo detail 91aeb22d-eff0-4310-abc6-811d2394b420 --json
|
|
2647
|
+
tokrepo detail https://tokrepo.com/en/workflows/91aeb22d-eff0-4310-abc6-811d2394b420
|
|
2648
|
+
`);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
function showInstallHelp() {
|
|
2652
|
+
log(`
|
|
2653
|
+
${C.bold}tokrepo install${C.reset}
|
|
2654
|
+
|
|
2655
|
+
USAGE
|
|
2656
|
+
tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--json]
|
|
2657
|
+
|
|
2658
|
+
TARGETS
|
|
2659
|
+
codex Write Codex skills to ~/.codex/skills/<asset-slug>/SKILL.md
|
|
2660
|
+
gemini Write project instructions to .gemini/GEMINI.md
|
|
2661
|
+
|
|
2662
|
+
EXAMPLES
|
|
2663
|
+
tokrepo install awesome-cursor-rules
|
|
2664
|
+
tokrepo install ca000374-f5d8-4d75-a30c-460fda0b6b0e
|
|
2665
|
+
tokrepo install https://tokrepo.com/en/workflows/ca000374-...
|
|
2666
|
+
tokrepo install 91aeb22d-eff0-4310-abc6-811d2394b420 --target codex
|
|
2667
|
+
tokrepo install 91aeb22d-eff0-4310-abc6-811d2394b420 --target codex --dry-run --json
|
|
2668
|
+
tokrepo install 20bc3ffd-1d7a-41d1-86d0-b668e8500cee --target codex --stage
|
|
2669
|
+
tokrepo install c4b18aeb --target gemini
|
|
2670
|
+
`);
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
function showListHelp() {
|
|
2674
|
+
log(`
|
|
2675
|
+
${C.bold}tokrepo list${C.reset}
|
|
2676
|
+
|
|
2677
|
+
USAGE
|
|
2678
|
+
tokrepo list [--json] [--all] [--page-size N]
|
|
2679
|
+
|
|
2680
|
+
EXAMPLES
|
|
2681
|
+
tokrepo list
|
|
2682
|
+
tokrepo list --json --all
|
|
2683
|
+
`);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
function showCloneHelp() {
|
|
2687
|
+
log(`
|
|
2688
|
+
${C.bold}tokrepo clone${C.reset}
|
|
2689
|
+
|
|
2690
|
+
USAGE
|
|
2691
|
+
tokrepo clone @username [--target codex] [--keyword query] [--types skill,prompt,knowledge] [--dry-run] [--stage] [--approve-mcp] [--json] [--manifest]
|
|
2692
|
+
|
|
2693
|
+
EXAMPLES
|
|
2694
|
+
tokrepo clone @henuwangkai --target codex --types skill,prompt,knowledge
|
|
2695
|
+
tokrepo clone @henuwangkai --target codex --keyword video
|
|
2696
|
+
tokrepo clone @me --target codex --dry-run --json --manifest
|
|
2697
|
+
`);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function showSyncInstalledHelp() {
|
|
2701
|
+
log(`
|
|
2702
|
+
${C.bold}tokrepo sync-installed${C.reset}
|
|
2703
|
+
|
|
2704
|
+
USAGE
|
|
2705
|
+
tokrepo sync-installed --target codex [--dry-run] [--stage] [--update] [--approve-mcp] [--json]
|
|
2706
|
+
tokrepo installed --target codex [--json]
|
|
2707
|
+
tokrepo outdated --target codex [--json]
|
|
2708
|
+
tokrepo update --target codex --all [--stage] [--approve-mcp] [--json]
|
|
2709
|
+
|
|
2710
|
+
BEHAVIOR
|
|
2711
|
+
Reads ~/.codex/tokrepo/install-manifest.json, fetches each TokRepo asset again,
|
|
2712
|
+
rebuilds the Codex install plan, compares local files by sha256, then updates
|
|
2713
|
+
changed or missing files. Use --update to force reinstall unchanged assets.
|
|
2714
|
+
|
|
2715
|
+
EXAMPLES
|
|
2716
|
+
tokrepo installed --target codex --json
|
|
2717
|
+
tokrepo outdated --target codex --json
|
|
2718
|
+
tokrepo update --target codex --all
|
|
2719
|
+
tokrepo sync-installed --target codex --dry-run --json
|
|
2720
|
+
tokrepo sync-installed --target codex --stage
|
|
2721
|
+
tokrepo sync-installed --target codex --update --approve-mcp
|
|
2722
|
+
`);
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
function showCommandHelp(command) {
|
|
2726
|
+
switch (command) {
|
|
2727
|
+
case 'search':
|
|
2728
|
+
case 'find':
|
|
2729
|
+
showSearchHelp(); break;
|
|
2730
|
+
case 'detail':
|
|
2731
|
+
showDetailHelp(); break;
|
|
2732
|
+
case 'install':
|
|
2733
|
+
case 'i':
|
|
2734
|
+
showInstallHelp(); break;
|
|
2735
|
+
case 'list':
|
|
2736
|
+
showListHelp(); break;
|
|
2737
|
+
case 'clone':
|
|
2738
|
+
showCloneHelp(); break;
|
|
2739
|
+
case 'sync-installed':
|
|
2740
|
+
case 'sync':
|
|
2741
|
+
case 'installed':
|
|
2742
|
+
case 'outdated':
|
|
2743
|
+
showSyncInstalledHelp(); break;
|
|
2744
|
+
default:
|
|
2745
|
+
showHelp(); break;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
1514
2749
|
// ─── Main ───
|
|
1515
2750
|
|
|
1516
2751
|
async function main() {
|
|
1517
2752
|
const command = process.argv[2];
|
|
2753
|
+
const args = parseArgs(process.argv);
|
|
2754
|
+
|
|
2755
|
+
if (args.flags.help && command && !['help', '--help', '-h'].includes(command)) {
|
|
2756
|
+
showCommandHelp(command);
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
1518
2759
|
|
|
1519
2760
|
switch (command) {
|
|
1520
2761
|
case 'login': await cmdLogin(); break;
|
|
@@ -1522,11 +2763,15 @@ async function main() {
|
|
|
1522
2763
|
case 'push': await cmdPush(); break;
|
|
1523
2764
|
case 'pull': await cmdPull(); break;
|
|
1524
2765
|
case 'search': case 'find': await cmdSearch(); break;
|
|
2766
|
+
case 'detail': await cmdDetail(); break;
|
|
1525
2767
|
case 'install': case 'i': await cmdInstall(); break;
|
|
1526
2768
|
case 'list': await cmdList(); break;
|
|
1527
2769
|
case 'update': await cmdUpdate(); break;
|
|
1528
2770
|
case 'delete': await cmdDelete(); break;
|
|
1529
2771
|
case 'clone': await cmdClone(); break;
|
|
2772
|
+
case 'installed': await cmdInstalled(); break;
|
|
2773
|
+
case 'outdated': await cmdOutdated(); break;
|
|
2774
|
+
case 'sync-installed': case 'sync': await cmdSyncInstalled(); break;
|
|
1530
2775
|
case 'tags': await cmdTags(); break;
|
|
1531
2776
|
case 'status': case 'diff': await cmdStatus(); break;
|
|
1532
2777
|
case 'whoami': await cmdWhoami(); break;
|
|
@@ -1539,7 +2784,9 @@ async function main() {
|
|
|
1539
2784
|
}
|
|
1540
2785
|
|
|
1541
2786
|
// Non-blocking update check after command completes
|
|
1542
|
-
|
|
2787
|
+
if (!wantsJson(process.argv) && !args.flags.help) {
|
|
2788
|
+
checkForUpdate();
|
|
2789
|
+
}
|
|
1543
2790
|
}
|
|
1544
2791
|
|
|
1545
2792
|
main().catch((e) => { error(e.message); });
|