tokrepo 3.6.0 → 3.8.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 +1117 -34
- package/package.json +1 -1
package/bin/tokrepo.js
CHANGED
|
@@ -25,12 +25,13 @@ const CONFIG_DIR = path.join(os.homedir(), '.tokrepo');
|
|
|
25
25
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
26
26
|
const PROJECT_CONFIG = '.tokrepo.json';
|
|
27
27
|
const DEFAULT_API = 'https://api.tokrepo.com';
|
|
28
|
-
const CLI_VERSION = '3.
|
|
28
|
+
const CLI_VERSION = '3.8.0';
|
|
29
29
|
const VERSION_CHECK_FILE = path.join(os.homedir(), '.tokrepo', '.version-check');
|
|
30
30
|
const CODEX_DIR = path.join(os.homedir(), '.codex');
|
|
31
31
|
const CODEX_SKILLS_DIR = path.join(CODEX_DIR, 'skills');
|
|
32
32
|
const CODEX_TOKREPO_DIR = path.join(CODEX_DIR, 'tokrepo');
|
|
33
33
|
const CODEX_MANIFEST_FILE = path.join(CODEX_TOKREPO_DIR, 'install-manifest.json');
|
|
34
|
+
const CODEX_SESSIONS_DIR = path.join(CODEX_TOKREPO_DIR, 'sessions');
|
|
34
35
|
const SUPPORTED_INSTALL_TARGETS = ['gemini', 'codex'];
|
|
35
36
|
|
|
36
37
|
// ─── Helpers ───
|
|
@@ -301,9 +302,10 @@ function parseArgs(argv) {
|
|
|
301
302
|
}
|
|
302
303
|
|
|
303
304
|
const valueFlags = new Set([
|
|
304
|
-
'title', 'desc', 'tag', 'target', 'targets', 'keyword', 'types',
|
|
305
|
+
'title', 'desc', 'tag', 'target', 'targets', 'keyword', 'query', 'types',
|
|
305
306
|
'kind', 'install-mode', 'install_mode', 'entrypoint', 'asset-kind', 'asset_kind',
|
|
306
|
-
'version',
|
|
307
|
+
'version', 'uuid',
|
|
308
|
+
'policy', 'session',
|
|
307
309
|
'page', 'page-size', 'page_size', 'sort-by', 'sort_by',
|
|
308
310
|
'time-window', 'time_window',
|
|
309
311
|
]);
|
|
@@ -544,14 +546,153 @@ function browserAuthFlow() {
|
|
|
544
546
|
});
|
|
545
547
|
}
|
|
546
548
|
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
+
function inferAssetKindFromPushFiles(files = []) {
|
|
550
|
+
const normalized = files.map(file => ({
|
|
551
|
+
...file,
|
|
552
|
+
lowerName: String(file.name || '').toLowerCase(),
|
|
553
|
+
content: String(file.content || ''),
|
|
554
|
+
}));
|
|
555
|
+
if (normalized.some(file => /"mcpServers"\s*:/.test(file.content) || /\bmcpServers\s*:/.test(file.content))) return 'mcp_config';
|
|
556
|
+
if (normalized.some(file => file.type === 'script' || /\.(sh|py|js|mjs|ts|rb|go|rs|lua)$/.test(file.lowerName) || /^#!\//.test(file.content))) return 'script';
|
|
557
|
+
if (normalized.some(file => file.lowerName === 'package.json' && /"bin"\s*:/.test(file.content))) return 'cli_tool';
|
|
558
|
+
if (normalized.some(file => file.type === 'skill' || isCodexSkillDocument(file))) return 'skill';
|
|
559
|
+
if (normalized.some(file => file.type === 'prompt')) return 'prompt';
|
|
560
|
+
if (normalized.some(file => file.type === 'config')) return 'config';
|
|
561
|
+
if (normalized.some(file => file.lowerName.endsWith('.md'))) return 'knowledge';
|
|
562
|
+
return 'other';
|
|
563
|
+
}
|
|
549
564
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
565
|
+
function analyzePushMetadataQuality(files = [], metadata = {}) {
|
|
566
|
+
const issues = [];
|
|
567
|
+
const add = (severity, code, message, fix = '') => {
|
|
568
|
+
issues.push({ severity, code, message, fix });
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const normalizedKind = normalizeToolName(metadata.kind || '');
|
|
572
|
+
const inferredKind = inferAssetKindFromPushFiles(files);
|
|
573
|
+
const targetTools = parseCsvList(metadata.targetTools).map(normalizeToolName);
|
|
574
|
+
const installMode = normalizeCodexInstallMode(metadata.installMode);
|
|
575
|
+
const rawInstallMode = String(metadata.installMode || '').trim();
|
|
576
|
+
const entrypoint = String(metadata.entrypoint || '').trim();
|
|
577
|
+
const fileNames = new Set(files.map(file => String(file.name || '').replace(/\\/g, '/')));
|
|
578
|
+
const lowerFileNames = new Set(Array.from(fileNames).map(name => name.toLowerCase()));
|
|
579
|
+
const multiFile = files.length > 1;
|
|
580
|
+
const riskFlags = Array.from(new Set(files.flatMap(file => analyzeInstallRisks(file.name, file.content, file.type))));
|
|
581
|
+
const containsCodex = targetTools.includes('codex');
|
|
582
|
+
|
|
583
|
+
if (!normalizedKind) {
|
|
584
|
+
add('warning', 'missing_asset_kind', `No asset_kind was provided; inferred "${inferredKind}".`, `Recommended: --kind ${inferredKind}`);
|
|
585
|
+
} else if (!['skill', 'prompt', 'knowledge', 'mcp_config', 'script', 'cli_tool', 'config', 'pack', 'other'].includes(normalizedKind)) {
|
|
586
|
+
add('warning', 'unknown_asset_kind', `Unknown asset_kind "${metadata.kind}".`, 'Recommended: use skill, prompt, knowledge, mcp_config, script, cli_tool, config, or pack.');
|
|
553
587
|
}
|
|
554
588
|
|
|
589
|
+
if (targetTools.length === 0) {
|
|
590
|
+
add('warning', 'missing_target_tools', 'No target_tools metadata was provided.', 'Recommended: --target codex or --targets codex,claude_code,gemini_cli.');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const invalidTargets = targetTools.filter(tool => !['codex', 'claude_code', 'gemini_cli', 'cursor', 'windsurf', 'opencode'].includes(tool));
|
|
594
|
+
if (invalidTargets.length > 0) {
|
|
595
|
+
add('info', 'unknown_target_tool', `Unknown target tool(s): ${invalidTargets.join(', ')}.`, 'Use stable ids when possible: codex, claude_code, gemini_cli, cursor, windsurf.');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (rawInstallMode && !installMode) {
|
|
599
|
+
add('warning', 'unknown_install_mode', `Unknown install_mode "${metadata.installMode}".`, 'Recommended: single, bundle, split, or stage_only.');
|
|
600
|
+
} else if (!rawInstallMode && (multiFile || containsCodex)) {
|
|
601
|
+
add('warning', 'missing_install_mode', `No install_mode was provided for ${multiFile ? 'a multi-file asset' : 'a Codex-targeted asset'}.`, `Recommended: --install-mode ${multiFile ? 'bundle' : 'single'}.`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (entrypoint && !fileNames.has(entrypoint) && !lowerFileNames.has(entrypoint.toLowerCase())) {
|
|
605
|
+
add('warning', 'entrypoint_missing', `Entrypoint "${entrypoint}" is not included in the pushed files.`, 'Recommended: set --entrypoint to an included file, usually SKILL.md.');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!entrypoint && (installMode === 'single' || installMode === 'bundle' || containsCodex)) {
|
|
609
|
+
add('warning', 'missing_entrypoint', 'No entrypoint metadata was provided.', 'Recommended: --entrypoint SKILL.md or the main markdown file.');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const effectiveKind = normalizedKind || inferredKind;
|
|
613
|
+
if (containsCodex && ['script', 'cli_tool', 'mcp_config'].includes(effectiveKind) && installMode !== 'stage_only') {
|
|
614
|
+
add('warning', 'high_risk_codex_activation', `${effectiveKind} assets targeted at Codex should stage by default.`, 'Recommended: --install-mode stage_only or publish a markdown skill wrapper.');
|
|
615
|
+
}
|
|
616
|
+
if (riskFlags.includes('executable')) {
|
|
617
|
+
add('warning', 'executes_code_detected', 'Executable code was detected in the asset files.', 'Recommended: declare script/cli_tool or use stage_only for agent installs.');
|
|
618
|
+
}
|
|
619
|
+
if (riskFlags.includes('mcp')) {
|
|
620
|
+
add('warning', 'mcp_config_detected', 'MCP config content was detected.', 'Recommended: --kind mcp_config --install-mode stage_only.');
|
|
621
|
+
}
|
|
622
|
+
if (riskFlags.includes('env')) {
|
|
623
|
+
add('info', 'secret_or_env_mentions', 'Environment variable or secret-like words were mentioned.', 'Check this is documentation only and no real secret is included.');
|
|
624
|
+
}
|
|
625
|
+
if (riskFlags.includes('absolute-path')) {
|
|
626
|
+
add('warning', 'absolute_path_detected', 'Absolute local paths were detected.', 'Recommended: replace machine-specific paths with placeholders.');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (containsCodex && effectiveKind === 'skill') {
|
|
630
|
+
const skillDocs = files.filter(file => isCodexSkillDocument(file));
|
|
631
|
+
if (installMode === 'split' && skillDocs.length !== files.length) {
|
|
632
|
+
add('warning', 'split_requires_skill_docs', 'install_mode=split works best when every markdown file has skill frontmatter.', 'Recommended: use bundle or add name/description frontmatter to each split skill.');
|
|
633
|
+
}
|
|
634
|
+
if (skillDocs.length === 0) {
|
|
635
|
+
add('warning', 'missing_codex_skill_frontmatter', 'No Codex skill frontmatter was found.', 'Recommended: add YAML frontmatter with name and description.');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const severityPenalty = issues.reduce((sum, issue) => {
|
|
640
|
+
if (issue.severity === 'warning') return sum + 10;
|
|
641
|
+
return sum + 2;
|
|
642
|
+
}, 0);
|
|
643
|
+
const score = Math.max(0, 100 - severityPenalty);
|
|
644
|
+
const status = issues.some(issue => issue.severity === 'warning') ? 'warn' : 'pass';
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
score,
|
|
648
|
+
status,
|
|
649
|
+
inferredAssetKind: inferredKind,
|
|
650
|
+
assetKind: normalizedKind || '',
|
|
651
|
+
targetTools,
|
|
652
|
+
installMode: installMode || '',
|
|
653
|
+
entrypoint,
|
|
654
|
+
riskFlags,
|
|
655
|
+
issueCount: issues.length,
|
|
656
|
+
issues,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function formatMetadataQualityLabel(report) {
|
|
661
|
+
const color = report.status === 'pass' ? C.green : C.yellow;
|
|
662
|
+
return `${color}${report.status}${C.reset} ${C.bold}${report.score}/100${C.reset}`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function printMetadataQualityReport(report, opts = {}) {
|
|
666
|
+
const compact = Boolean(opts.compact);
|
|
667
|
+
if (!compact) {
|
|
668
|
+
log(`\n${C.bold}Agent metadata quality${C.reset}`);
|
|
669
|
+
log(` Status: ${formatMetadataQualityLabel(report)}`);
|
|
670
|
+
log(` Inferred kind: ${report.inferredAssetKind}`);
|
|
671
|
+
if (report.targetTools.length) log(` Targets: ${report.targetTools.join(', ')}`);
|
|
672
|
+
if (report.installMode) log(` Install mode: ${report.installMode}`);
|
|
673
|
+
if (report.riskFlags.length) log(` Risk flags: ${report.riskFlags.join(', ')}`);
|
|
674
|
+
} else {
|
|
675
|
+
log(`${C.bold}Agent metadata quality suggestions:${C.reset}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (report.issues.length === 0) {
|
|
679
|
+
if (!compact) success('Metadata is agent-ready.');
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
for (const issue of report.issues.slice(0, compact ? 8 : report.issues.length)) {
|
|
683
|
+
const color = issue.severity === 'warning' ? C.yellow : C.dim;
|
|
684
|
+
log(` ${color}${issue.severity.toUpperCase()}${C.reset} ${issue.code}: ${issue.message}`);
|
|
685
|
+
if (issue.fix) log(` ${C.dim}${issue.fix}${C.reset}`);
|
|
686
|
+
}
|
|
687
|
+
if (compact && report.issues.length > 8) {
|
|
688
|
+
log(` ${C.dim}...and ${report.issues.length - 8} more suggestion(s). Run --metadata-report for full detail.${C.reset}`);
|
|
689
|
+
}
|
|
690
|
+
if (!compact) log('');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function cmdPush() {
|
|
694
|
+
const args = parseArgs(process.argv);
|
|
695
|
+
|
|
555
696
|
const projectConfig = readProjectConfig();
|
|
556
697
|
const baseDir = process.cwd();
|
|
557
698
|
|
|
@@ -621,6 +762,31 @@ async function cmdPush() {
|
|
|
621
762
|
error('No readable text files found to push.');
|
|
622
763
|
}
|
|
623
764
|
|
|
765
|
+
const metadataQuality = analyzePushMetadataQuality(pushFiles, {
|
|
766
|
+
kind,
|
|
767
|
+
targetTools,
|
|
768
|
+
installMode,
|
|
769
|
+
entrypoint,
|
|
770
|
+
title,
|
|
771
|
+
description,
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (args.flags.metadata_report || args.flags.metadataReport) {
|
|
775
|
+
if (args.flags.json) {
|
|
776
|
+
outputJson({ schemaVersion: 1, metadataQuality, files: pushFiles.map(file => ({ name: file.name, type: file.type, bytes: Buffer.byteLength(file.content || '') })) });
|
|
777
|
+
} else {
|
|
778
|
+
printMetadataQualityReport(metadataQuality);
|
|
779
|
+
}
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Public registry policy: quality gates advise by default and never block user uploads.
|
|
784
|
+
// TokRepo-owned CI can opt into strict mode with TOKREPO_METADATA_STRICT=1.
|
|
785
|
+
if (process.env.TOKREPO_METADATA_STRICT === '1' && metadataQuality.issues.length > 0 && !args.flags.force) {
|
|
786
|
+
if (!args.flags.json) printMetadataQualityReport(metadataQuality);
|
|
787
|
+
error(`Internal metadata quality gate failed. Fix the issues or re-run with --force to push anyway.`);
|
|
788
|
+
}
|
|
789
|
+
|
|
624
790
|
// Show summary
|
|
625
791
|
log(`\n${C.bold}tokrepo push${C.reset}\n`);
|
|
626
792
|
log(` ${C.bold}Title:${C.reset} ${title}`);
|
|
@@ -638,6 +804,7 @@ async function cmdPush() {
|
|
|
638
804
|
if (metadataSummary.length > 0) {
|
|
639
805
|
log(` ${C.bold}Agent meta:${C.reset} ${metadataSummary.join(' · ')}`);
|
|
640
806
|
}
|
|
807
|
+
log(` ${C.bold}Agent quality:${C.reset} ${formatMetadataQualityLabel(metadataQuality)}`);
|
|
641
808
|
log('');
|
|
642
809
|
|
|
643
810
|
for (const f of pushFiles) {
|
|
@@ -648,9 +815,18 @@ async function cmdPush() {
|
|
|
648
815
|
|
|
649
816
|
const totalChars = pushFiles.reduce((sum, f) => sum + f.content.length, 0);
|
|
650
817
|
|
|
818
|
+
if (metadataQuality.issues.length > 0) {
|
|
819
|
+
printMetadataQualityReport(metadataQuality, { compact: true });
|
|
820
|
+
}
|
|
821
|
+
|
|
651
822
|
// Push
|
|
652
823
|
info('Pushing...');
|
|
653
824
|
|
|
825
|
+
const config = readConfig();
|
|
826
|
+
if (!config || !config.token) {
|
|
827
|
+
error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
|
|
828
|
+
}
|
|
829
|
+
|
|
654
830
|
try {
|
|
655
831
|
const data = await apiRequest('POST', '/api/v1/tokenboard/push/upsert', {
|
|
656
832
|
title,
|
|
@@ -841,8 +1017,23 @@ async function cmdSearch() {
|
|
|
841
1017
|
data = { ...data, list };
|
|
842
1018
|
}
|
|
843
1019
|
|
|
1020
|
+
const originalCount = (data.list || []).length;
|
|
1021
|
+
data = { ...data, list: applyAgentWorkflowFilters(data.list || [], args.flags) };
|
|
1022
|
+
const filters = {
|
|
1023
|
+
target: args.flags.target || undefined,
|
|
1024
|
+
kind: args.flags.kind || args.flags.assetKind || undefined,
|
|
1025
|
+
policy: args.flags.policy || undefined,
|
|
1026
|
+
};
|
|
1027
|
+
|
|
844
1028
|
if (args.flags.json) {
|
|
845
|
-
outputJson({
|
|
1029
|
+
outputJson({
|
|
1030
|
+
query,
|
|
1031
|
+
total: data.total || 0,
|
|
1032
|
+
fetched: originalCount,
|
|
1033
|
+
count: (data.list || []).length,
|
|
1034
|
+
filters,
|
|
1035
|
+
list: data.list || [],
|
|
1036
|
+
});
|
|
846
1037
|
return;
|
|
847
1038
|
}
|
|
848
1039
|
|
|
@@ -859,7 +1050,8 @@ async function cmdSearch() {
|
|
|
859
1050
|
return;
|
|
860
1051
|
}
|
|
861
1052
|
|
|
862
|
-
|
|
1053
|
+
const filterText = [filters.target ? `target=${filters.target}` : '', filters.kind ? `kind=${filters.kind}` : '', filters.policy ? `policy=${filters.policy}` : ''].filter(Boolean).join(' · ');
|
|
1054
|
+
log(` ${C.bold}${data.list.length}${C.reset} shown${filterText ? ` ${C.dim}(${filterText})${C.reset}` : ''}${data.total ? ` ${C.dim}from ${data.total} result(s)${C.reset}` : ''}:\n`);
|
|
863
1055
|
|
|
864
1056
|
for (let i = 0; i < data.list.length; i++) {
|
|
865
1057
|
const wf = data.list[i];
|
|
@@ -874,6 +1066,10 @@ async function cmdSearch() {
|
|
|
874
1066
|
log(` ${C.dim}${String(i + 1).padStart(2)}.${C.reset} ${C.bold}${wf.title}${C.reset}`);
|
|
875
1067
|
if (desc) log(` ${desc}`);
|
|
876
1068
|
if (tags) log(` ${C.cyan}${tags}${C.reset} ${C.dim}★${votes} 👁${views}${C.reset}`);
|
|
1069
|
+
if (wf.compatibility?.codex) {
|
|
1070
|
+
const c = wf.compatibility.codex;
|
|
1071
|
+
log(` ${C.dim}codex: ${c.status} · policy=${c.policyDecision.decision} · kind=${c.assetKind || 'unknown'}${C.reset}`);
|
|
1072
|
+
}
|
|
877
1073
|
log(` ${C.dim}tokrepo install ${wf.uuid}${C.reset}`);
|
|
878
1074
|
log('');
|
|
879
1075
|
}
|
|
@@ -984,6 +1180,40 @@ function getWorkflowAssetType(workflow) {
|
|
|
984
1180
|
return (workflow.tags[0].slug || workflow.tags[0].name || '').toLowerCase();
|
|
985
1181
|
}
|
|
986
1182
|
|
|
1183
|
+
function workflowAgentMetadata(workflow) {
|
|
1184
|
+
return workflow?.agent_metadata || workflow?.agentMetadata || {};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function normalizeCodexInstallMode(mode) {
|
|
1188
|
+
const normalized = String(mode || '').trim().toLowerCase().replace(/-/g, '_');
|
|
1189
|
+
return ['single', 'bundle', 'split', 'stage_only'].includes(normalized) ? normalized : '';
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function workflowAssetKind(workflow) {
|
|
1193
|
+
const metadata = workflowAgentMetadata(workflow);
|
|
1194
|
+
const explicit = workflow?.asset_kind || workflow?.assetKind || metadata.asset_kind || metadata.assetKind || '';
|
|
1195
|
+
if (explicit) return normalizeToolName(explicit);
|
|
1196
|
+
const assetType = getWorkflowAssetType(workflow);
|
|
1197
|
+
const aliases = {
|
|
1198
|
+
skills: 'skill',
|
|
1199
|
+
prompts: 'prompt',
|
|
1200
|
+
knowledge: 'knowledge',
|
|
1201
|
+
'mcp-configs': 'mcp_config',
|
|
1202
|
+
mcp: 'mcp_config',
|
|
1203
|
+
scripts: 'script',
|
|
1204
|
+
configs: 'config',
|
|
1205
|
+
tools: 'cli_tool',
|
|
1206
|
+
};
|
|
1207
|
+
return aliases[assetType] || normalizeToolName(assetType);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function workflowTargetTools(workflow) {
|
|
1211
|
+
const metadata = workflowAgentMetadata(workflow);
|
|
1212
|
+
return parseCsvList(workflow?.target_tools || workflow?.targetTools || metadata.target_tools || metadata.targetTools)
|
|
1213
|
+
.map(normalizeToolName)
|
|
1214
|
+
.filter(Boolean);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
987
1217
|
function extractInstallableContents(workflow, assetType) {
|
|
988
1218
|
const contents = [];
|
|
989
1219
|
const files = workflow.files || [];
|
|
@@ -1129,8 +1359,7 @@ function explicitInstallMode(workflow) {
|
|
|
1129
1359
|
workflow?.metadata?.installMode,
|
|
1130
1360
|
workflow?.metadata?.install_mode,
|
|
1131
1361
|
].filter(Boolean);
|
|
1132
|
-
|
|
1133
|
-
return ['single', 'bundle', 'split', 'stage_only'].includes(mode) ? mode : '';
|
|
1362
|
+
return normalizeCodexInstallMode(candidates[0]);
|
|
1134
1363
|
}
|
|
1135
1364
|
|
|
1136
1365
|
function inferCodexInstallMode(workflow, contents) {
|
|
@@ -1187,8 +1416,12 @@ function addPlanFile(plan, destPath, content, sourceName, type) {
|
|
|
1187
1416
|
}
|
|
1188
1417
|
|
|
1189
1418
|
function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
1190
|
-
const
|
|
1191
|
-
const
|
|
1419
|
+
const serverPlan = opts.serverPlan || null;
|
|
1420
|
+
const serverMetadata = serverPlan?.metadata || serverPlan?.agentMetadata || serverPlan?.agent_metadata || {};
|
|
1421
|
+
const installMode = normalizeCodexInstallMode(opts.installMode)
|
|
1422
|
+
|| normalizeCodexInstallMode(metadataValue(serverPlan, 'install_mode', 'installMode', ''))
|
|
1423
|
+
|| inferCodexInstallMode(workflow, contents);
|
|
1424
|
+
const agentMetadata = Object.keys(serverMetadata || {}).length > 0 ? serverMetadata : workflowAgentMetadata(workflow);
|
|
1192
1425
|
const plan = {
|
|
1193
1426
|
uuid: workflow.uuid,
|
|
1194
1427
|
title: workflow.title,
|
|
@@ -1199,7 +1432,8 @@ function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
|
1199
1432
|
files: [],
|
|
1200
1433
|
risks: [],
|
|
1201
1434
|
agentMetadata,
|
|
1202
|
-
contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || '',
|
|
1435
|
+
contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || agentMetadata.contentHash || metadataValue(serverPlan, 'content_hash', 'contentHash', ''),
|
|
1436
|
+
serverPlan,
|
|
1203
1437
|
};
|
|
1204
1438
|
|
|
1205
1439
|
if (installMode === 'stage_only') {
|
|
@@ -1313,7 +1547,36 @@ function mergedPlanRiskProfile(plan) {
|
|
|
1313
1547
|
};
|
|
1314
1548
|
}
|
|
1315
1549
|
|
|
1550
|
+
function policyDecisionFromServerPlan(plan) {
|
|
1551
|
+
const serverPlan = plan?.serverPlan;
|
|
1552
|
+
if (!serverPlan) return null;
|
|
1553
|
+
const raw = metadataValue(serverPlan, 'policy_decision', 'policyDecision', null);
|
|
1554
|
+
if (!raw) return null;
|
|
1555
|
+
if (typeof raw === 'string') {
|
|
1556
|
+
return {
|
|
1557
|
+
decision: raw,
|
|
1558
|
+
requiresConfirmation: raw === 'confirm',
|
|
1559
|
+
reasons: [],
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
const decision = String(raw.decision || raw.action || 'allow').trim().toLowerCase();
|
|
1563
|
+
const requiresConfirmation = Boolean(
|
|
1564
|
+
raw.requires_confirmation
|
|
1565
|
+
|| raw.requiresConfirmation
|
|
1566
|
+
|| metadataValue(serverPlan, 'requires_confirmation', 'requiresConfirmation', false)
|
|
1567
|
+
);
|
|
1568
|
+
const reasons = raw.reasons || raw.reason || [];
|
|
1569
|
+
return {
|
|
1570
|
+
decision,
|
|
1571
|
+
requiresConfirmation,
|
|
1572
|
+
reasons: Array.isArray(reasons) ? reasons : [String(reasons)].filter(Boolean),
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1316
1576
|
function decideCodexPolicy(plan) {
|
|
1577
|
+
const serverPolicy = policyDecisionFromServerPlan(plan);
|
|
1578
|
+
if (serverPolicy) return serverPolicy;
|
|
1579
|
+
|
|
1317
1580
|
const metadata = plan.agentMetadata || {};
|
|
1318
1581
|
const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
|
|
1319
1582
|
const assetKind = normalizeToolName(metadataValue(metadata, 'asset_kind', 'assetKind', ''));
|
|
@@ -1367,6 +1630,9 @@ function decideCodexPolicy(plan) {
|
|
|
1367
1630
|
}
|
|
1368
1631
|
|
|
1369
1632
|
function buildPublicPlanActions(plan) {
|
|
1633
|
+
const serverActions = metadataValue(plan.serverPlan, 'actions', 'actions', null);
|
|
1634
|
+
if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverActions) && serverActions.length > 0) return serverActions;
|
|
1635
|
+
|
|
1370
1636
|
const stage = plan.installMode === 'stage_only';
|
|
1371
1637
|
return plan.files.map(file => ({
|
|
1372
1638
|
type: stage ? 'stage_file' : 'write_file',
|
|
@@ -1380,7 +1646,23 @@ function buildPublicPlanActions(plan) {
|
|
|
1380
1646
|
}));
|
|
1381
1647
|
}
|
|
1382
1648
|
|
|
1649
|
+
function serverConcretePlanMatchesLocal(plan) {
|
|
1650
|
+
const serverActions = metadataValue(plan.serverPlan, 'actions', 'actions', null);
|
|
1651
|
+
if (!Array.isArray(serverActions) || serverActions.length !== (plan.files || []).length) return false;
|
|
1652
|
+
return serverActions.every((action, index) => {
|
|
1653
|
+
const file = plan.files[index];
|
|
1654
|
+
if (!file) return false;
|
|
1655
|
+
const serverPath = path.resolve(expandHomePath(action.path || ''));
|
|
1656
|
+
const localPath = path.resolve(file.path || '');
|
|
1657
|
+
const serverSha = action.sha256 || action.sha || '';
|
|
1658
|
+
return serverPath === localPath && (!serverSha || serverSha === file.sha256);
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1383
1662
|
function buildPublicPlanPreconditions(plan, policyDecision) {
|
|
1663
|
+
const serverPreconditions = metadataValue(plan.serverPlan, 'preconditions', 'preconditions', null);
|
|
1664
|
+
if (Array.isArray(serverPreconditions) && serverPreconditions.length > 0) return serverPreconditions;
|
|
1665
|
+
|
|
1384
1666
|
const metadata = plan.agentMetadata || {};
|
|
1385
1667
|
const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
|
|
1386
1668
|
const out = [
|
|
@@ -1405,6 +1687,9 @@ function buildPublicPlanPreconditions(plan, policyDecision) {
|
|
|
1405
1687
|
}
|
|
1406
1688
|
|
|
1407
1689
|
function buildPublicPlanRollback(plan) {
|
|
1690
|
+
const serverRollback = metadataValue(plan.serverPlan, 'rollback', 'rollback', null);
|
|
1691
|
+
if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverRollback) && serverRollback.length > 0) return serverRollback;
|
|
1692
|
+
|
|
1408
1693
|
const seen = new Set();
|
|
1409
1694
|
const rollback = [];
|
|
1410
1695
|
for (const file of plan.files) {
|
|
@@ -1416,11 +1701,18 @@ function buildPublicPlanRollback(plan) {
|
|
|
1416
1701
|
}
|
|
1417
1702
|
|
|
1418
1703
|
function buildPublicPlanPostVerify(plan) {
|
|
1704
|
+
const serverPostVerify = metadataValue(plan.serverPlan, 'post_verify', 'postVerify', null);
|
|
1705
|
+
if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverPostVerify) && serverPostVerify.length > 0) return serverPostVerify;
|
|
1706
|
+
|
|
1419
1707
|
const metadata = plan.agentMetadata || {};
|
|
1420
1708
|
const verification = metadataValue(metadata, 'verification', 'verification', {}) || {};
|
|
1421
1709
|
const out = plan.files.map(file => ({ type: 'file_sha256', path: file.path, sha256: file.sha256 }));
|
|
1710
|
+
const installedPaths = new Set(plan.files.map(file => path.resolve(file.path)));
|
|
1422
1711
|
for (const expected of (verification.expected_files || verification.expectedFiles || [])) {
|
|
1423
|
-
|
|
1712
|
+
const resolvedExpected = path.resolve(resolveVerifyPath(expected, { baseDir: plan.baseDir, files: plan.files }));
|
|
1713
|
+
if (installedPaths.has(resolvedExpected)) {
|
|
1714
|
+
out.push({ type: 'expected_file', path: expected });
|
|
1715
|
+
}
|
|
1424
1716
|
}
|
|
1425
1717
|
for (const command of (verification.commands || [])) {
|
|
1426
1718
|
out.push({ type: 'command', command });
|
|
@@ -1431,8 +1723,11 @@ function buildPublicPlanPostVerify(plan) {
|
|
|
1431
1723
|
function publicInstallPlan(plan) {
|
|
1432
1724
|
const policyDecision = decideCodexPolicy(plan);
|
|
1433
1725
|
const actions = buildPublicPlanActions(plan);
|
|
1726
|
+
const schemaVersion = Number(metadataValue(plan.serverPlan, 'schema_version', 'schemaVersion', 2)) || 2;
|
|
1434
1727
|
return {
|
|
1435
|
-
schemaVersion
|
|
1728
|
+
schemaVersion,
|
|
1729
|
+
sourceOfTruth: plan.serverPlan ? 'api_install_plan_v2' : 'local_fallback',
|
|
1730
|
+
concretePlanSource: serverConcretePlanMatchesLocal(plan) ? 'api_install_plan_v2' : 'local_fallback',
|
|
1436
1731
|
uuid: plan.uuid,
|
|
1437
1732
|
title: plan.title,
|
|
1438
1733
|
sourceUrl: plan.sourceUrl,
|
|
@@ -1460,6 +1755,95 @@ function publicInstallPlan(plan) {
|
|
|
1460
1755
|
};
|
|
1461
1756
|
}
|
|
1462
1757
|
|
|
1758
|
+
function workflowCodexCompatibility(workflow) {
|
|
1759
|
+
const metadata = workflowAgentMetadata(workflow);
|
|
1760
|
+
const assetKind = workflowAssetKind(workflow);
|
|
1761
|
+
const targetTools = workflowTargetTools(workflow);
|
|
1762
|
+
const installMode = normalizeCodexInstallMode(metadata.install_mode || metadata.installMode || workflow.install_mode || workflow.installMode) || 'single';
|
|
1763
|
+
const policy = decideCodexPolicy({
|
|
1764
|
+
agentMetadata: {
|
|
1765
|
+
...metadata,
|
|
1766
|
+
asset_kind: assetKind,
|
|
1767
|
+
target_tools: targetTools,
|
|
1768
|
+
install_mode: installMode,
|
|
1769
|
+
},
|
|
1770
|
+
risks: [],
|
|
1771
|
+
installMode,
|
|
1772
|
+
});
|
|
1773
|
+
const scores = { allow: 100, confirm: 70, stage_only: 40, deny: 0 };
|
|
1774
|
+
const statuses = {
|
|
1775
|
+
allow: 'native',
|
|
1776
|
+
confirm: 'requires_confirmation',
|
|
1777
|
+
stage_only: 'stage_only',
|
|
1778
|
+
deny: 'denied',
|
|
1779
|
+
};
|
|
1780
|
+
return {
|
|
1781
|
+
targetTool: 'codex',
|
|
1782
|
+
status: statuses[policy.decision] || 'unknown',
|
|
1783
|
+
score: scores[policy.decision] ?? 50,
|
|
1784
|
+
assetKind,
|
|
1785
|
+
targetTools,
|
|
1786
|
+
installMode,
|
|
1787
|
+
policyDecision: policy,
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function workflowMatchesAgentFilters(workflow, flags = {}) {
|
|
1792
|
+
const target = normalizeInstallTarget(flags.target || '');
|
|
1793
|
+
const requestedKinds = parseCsvList(flags.kind || flags.assetKind || flags.asset_kind).map(normalizeToolName);
|
|
1794
|
+
const requestedPolicies = parseCsvList(flags.policy).map(s => String(s).trim().toLowerCase());
|
|
1795
|
+
const assetKind = workflowAssetKind(workflow);
|
|
1796
|
+
const targetTools = workflowTargetTools(workflow);
|
|
1797
|
+
const compatibility = workflowCodexCompatibility(workflow);
|
|
1798
|
+
|
|
1799
|
+
if (target === 'codex') {
|
|
1800
|
+
if (targetTools.length > 0 && !targetTools.includes('codex')) return false;
|
|
1801
|
+
} else if (target && targetTools.length > 0 && !targetTools.includes(target)) {
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
if (requestedKinds.length > 0) {
|
|
1806
|
+
const kindAliases = new Set([assetKind, `${assetKind}s`, assetKind.replace(/_/g, '-')]);
|
|
1807
|
+
const tags = (workflow.tags || []).flatMap(t => [t.slug, t.name]).filter(Boolean).map(normalizeToolName);
|
|
1808
|
+
const matchesKind = requestedKinds.some(kind => kindAliases.has(kind) || tags.includes(kind) || tags.includes(`${kind}s`));
|
|
1809
|
+
if (!matchesKind) return false;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
if (requestedPolicies.length > 0) {
|
|
1813
|
+
const decision = compatibility.policyDecision.decision;
|
|
1814
|
+
const aliases = {
|
|
1815
|
+
safe: 'allow',
|
|
1816
|
+
staged: 'stage_only',
|
|
1817
|
+
stage: 'stage_only',
|
|
1818
|
+
block: 'deny',
|
|
1819
|
+
blocked: 'deny',
|
|
1820
|
+
};
|
|
1821
|
+
const normalizedPolicies = requestedPolicies.map(policy => aliases[policy] || policy);
|
|
1822
|
+
if (!normalizedPolicies.includes(decision)) return false;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
return true;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function enrichWorkflowForAgent(workflow) {
|
|
1829
|
+
const compatibility = workflowCodexCompatibility(workflow);
|
|
1830
|
+
return {
|
|
1831
|
+
...workflow,
|
|
1832
|
+
assetKind: compatibility.assetKind,
|
|
1833
|
+
targetTools: compatibility.targetTools,
|
|
1834
|
+
compatibility: {
|
|
1835
|
+
codex: compatibility,
|
|
1836
|
+
},
|
|
1837
|
+
policyDecision: compatibility.policyDecision,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
function applyAgentWorkflowFilters(list, flags = {}) {
|
|
1842
|
+
const shouldEnrich = flags.target || flags.kind || flags.assetKind || flags.asset_kind || flags.policy;
|
|
1843
|
+
const filtered = (list || []).filter(item => workflowMatchesAgentFilters(item, flags));
|
|
1844
|
+
return shouldEnrich ? filtered.map(enrichWorkflowForAgent) : filtered;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1463
1847
|
function hasCodexInstallRisks(plan) {
|
|
1464
1848
|
const decision = decideCodexPolicy(plan).decision;
|
|
1465
1849
|
return decision === 'confirm' || decision === 'stage_only' || decision === 'deny';
|
|
@@ -1538,6 +1922,102 @@ function executeStageOnlyCodexPlan(plan) {
|
|
|
1538
1922
|
return { dryRun: true, staged: true, stageOnly: true, stagePath, plan: publicInstallPlan(plan), installedFiles };
|
|
1539
1923
|
}
|
|
1540
1924
|
|
|
1925
|
+
function expandHomePath(input) {
|
|
1926
|
+
const value = String(input || '');
|
|
1927
|
+
if (value === '~') return os.homedir();
|
|
1928
|
+
if (value.startsWith('~/')) return path.join(os.homedir(), value.slice(2));
|
|
1929
|
+
return value;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function resolveVerifyPath(checkPath, publicPlan) {
|
|
1933
|
+
const expanded = expandHomePath(checkPath);
|
|
1934
|
+
if (path.isAbsolute(expanded)) return expanded;
|
|
1935
|
+
const baseDir = publicPlan.baseDir || path.dirname(publicPlan.files?.[0]?.path || CODEX_SKILLS_DIR);
|
|
1936
|
+
return path.join(baseDir, expanded);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function runCodexPostVerify(publicPlan, opts = {}) {
|
|
1940
|
+
const checks = [];
|
|
1941
|
+
let ok = true;
|
|
1942
|
+
for (const check of (publicPlan.postVerify || [])) {
|
|
1943
|
+
if (check.type === 'file_sha256') {
|
|
1944
|
+
const filePath = resolveVerifyPath(check.path, publicPlan);
|
|
1945
|
+
const exists = fs.existsSync(filePath);
|
|
1946
|
+
const actualSha = exists ? currentFileSha(filePath) : '';
|
|
1947
|
+
const passed = Boolean(exists && actualSha === check.sha256);
|
|
1948
|
+
if (!passed) ok = false;
|
|
1949
|
+
checks.push({ ...check, path: filePath, status: passed ? 'pass' : 'fail', actualSha });
|
|
1950
|
+
} else if (check.type === 'expected_file') {
|
|
1951
|
+
const filePath = resolveVerifyPath(check.path, publicPlan);
|
|
1952
|
+
const passed = fs.existsSync(filePath);
|
|
1953
|
+
if (!passed) ok = false;
|
|
1954
|
+
checks.push({ ...check, path: filePath, status: passed ? 'pass' : 'fail' });
|
|
1955
|
+
} else if (check.type === 'command') {
|
|
1956
|
+
if (!opts.verifyCommands) {
|
|
1957
|
+
checks.push({ ...check, status: 'skipped', message: 'command verification is opt-in; re-run with --verify-commands' });
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
try {
|
|
1961
|
+
const childProcess = require('child_process');
|
|
1962
|
+
childProcess.execSync(String(check.command || ''), { stdio: 'pipe', shell: true, timeout: 30000 });
|
|
1963
|
+
checks.push({ ...check, status: 'pass' });
|
|
1964
|
+
} catch (e) {
|
|
1965
|
+
ok = false;
|
|
1966
|
+
checks.push({ ...check, status: 'fail', message: e.message });
|
|
1967
|
+
}
|
|
1968
|
+
} else {
|
|
1969
|
+
checks.push({ ...check, status: 'skipped', message: 'unknown verification check type' });
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return { ok, checks };
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function createCodexSessionId(operation = 'session') {
|
|
1976
|
+
const stamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '-').replace('Z', '');
|
|
1977
|
+
const random = crypto.randomBytes(4).toString('hex');
|
|
1978
|
+
return `${slugify(operation, 'session')}-${stamp}-${random}`;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function writeCodexSession(record) {
|
|
1982
|
+
if (!fs.existsSync(CODEX_SESSIONS_DIR)) {
|
|
1983
|
+
fs.mkdirSync(CODEX_SESSIONS_DIR, { recursive: true, mode: 0o700 });
|
|
1984
|
+
}
|
|
1985
|
+
const sessionId = record.sessionId || createCodexSessionId(record.operation || 'session');
|
|
1986
|
+
const sessionPath = path.join(CODEX_SESSIONS_DIR, `${sessionId}.json`);
|
|
1987
|
+
const payload = {
|
|
1988
|
+
schemaVersion: 1,
|
|
1989
|
+
sessionId,
|
|
1990
|
+
createdAt: new Date().toISOString(),
|
|
1991
|
+
cliVersion: CLI_VERSION,
|
|
1992
|
+
argv: process.argv.slice(2),
|
|
1993
|
+
...record,
|
|
1994
|
+
sessionId,
|
|
1995
|
+
};
|
|
1996
|
+
fs.writeFileSync(sessionPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
1997
|
+
return { sessionId, sessionPath };
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function readCodexSessions() {
|
|
2001
|
+
try {
|
|
2002
|
+
if (!fs.existsSync(CODEX_SESSIONS_DIR)) return [];
|
|
2003
|
+
return fs.readdirSync(CODEX_SESSIONS_DIR)
|
|
2004
|
+
.filter(name => name.endsWith('.json'))
|
|
2005
|
+
.map(name => {
|
|
2006
|
+
const sessionPath = path.join(CODEX_SESSIONS_DIR, name);
|
|
2007
|
+
try {
|
|
2008
|
+
const parsed = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
|
|
2009
|
+
return { ...parsed, sessionPath };
|
|
2010
|
+
} catch {
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
})
|
|
2014
|
+
.filter(Boolean)
|
|
2015
|
+
.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
|
|
2016
|
+
} catch {
|
|
2017
|
+
return [];
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1541
2021
|
function readCodexManifest() {
|
|
1542
2022
|
try {
|
|
1543
2023
|
const parsed = JSON.parse(fs.readFileSync(CODEX_MANIFEST_FILE, 'utf8'));
|
|
@@ -1546,7 +2026,15 @@ function readCodexManifest() {
|
|
|
1546
2026
|
return { schemaVersion: 1, installs: [] };
|
|
1547
2027
|
}
|
|
1548
2028
|
|
|
1549
|
-
function
|
|
2029
|
+
function writeCodexManifest(manifest) {
|
|
2030
|
+
if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
|
|
2031
|
+
fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
|
|
2032
|
+
}
|
|
2033
|
+
manifest.updatedAt = new Date().toISOString();
|
|
2034
|
+
fs.writeFileSync(CODEX_MANIFEST_FILE, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function writeCodexManifestRecord(plan, installedFiles, sessionInfo = {}, verification = null) {
|
|
1550
2038
|
if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
|
|
1551
2039
|
fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
|
|
1552
2040
|
}
|
|
@@ -1561,6 +2049,9 @@ function writeCodexManifestRecord(plan, installedFiles) {
|
|
|
1561
2049
|
installedAt,
|
|
1562
2050
|
contentHash: plan.contentHash || '',
|
|
1563
2051
|
agentMetadata: plan.agentMetadata || {},
|
|
2052
|
+
sessionId: sessionInfo.sessionId,
|
|
2053
|
+
sessionPath: sessionInfo.sessionPath,
|
|
2054
|
+
verification,
|
|
1564
2055
|
installedFiles: installedFiles.map(file => ({
|
|
1565
2056
|
path: file.path,
|
|
1566
2057
|
sourceName: file.sourceName,
|
|
@@ -1573,16 +2064,58 @@ function writeCodexManifestRecord(plan, installedFiles) {
|
|
|
1573
2064
|
manifest.installs = manifest.installs.filter(item => !(item.uuid === plan.uuid && item.targetTool === 'codex'));
|
|
1574
2065
|
manifest.installs.push(record);
|
|
1575
2066
|
manifest.updatedAt = installedAt;
|
|
1576
|
-
|
|
2067
|
+
writeCodexManifest(manifest);
|
|
1577
2068
|
return record;
|
|
1578
2069
|
}
|
|
1579
2070
|
|
|
1580
2071
|
function executeCodexInstallPlan(plan, opts = {}) {
|
|
1581
|
-
|
|
1582
|
-
if (
|
|
2072
|
+
const publicPlan = publicInstallPlan(plan);
|
|
2073
|
+
if (opts.dryRun) {
|
|
2074
|
+
const session = writeCodexSession({
|
|
2075
|
+
operation: 'install',
|
|
2076
|
+
status: 'dry_run',
|
|
2077
|
+
targetTool: 'codex',
|
|
2078
|
+
uuid: plan.uuid,
|
|
2079
|
+
title: plan.title,
|
|
2080
|
+
sourceUrl: plan.sourceUrl,
|
|
2081
|
+
policyDecision: publicPlan.policyDecision,
|
|
2082
|
+
plan: publicPlan,
|
|
2083
|
+
result: { dryRun: true, installedFiles: [] },
|
|
2084
|
+
});
|
|
2085
|
+
return { dryRun: true, plan: publicPlan, installedFiles: [], ...session };
|
|
2086
|
+
}
|
|
2087
|
+
if (plan.installMode === 'stage_only') {
|
|
2088
|
+
const result = executeStageOnlyCodexPlan(plan);
|
|
2089
|
+
const verification = runCodexPostVerify(result.plan, opts);
|
|
2090
|
+
const session = writeCodexSession({
|
|
2091
|
+
operation: 'install',
|
|
2092
|
+
status: 'stage_only',
|
|
2093
|
+
targetTool: 'codex',
|
|
2094
|
+
uuid: plan.uuid,
|
|
2095
|
+
title: plan.title,
|
|
2096
|
+
sourceUrl: plan.sourceUrl,
|
|
2097
|
+
policyDecision: result.plan.policyDecision,
|
|
2098
|
+
plan: result.plan,
|
|
2099
|
+
installedFiles: result.installedFiles,
|
|
2100
|
+
verification,
|
|
2101
|
+
result: { staged: true, stageOnly: true, stagePath: result.stagePath },
|
|
2102
|
+
});
|
|
2103
|
+
return { ...result, verification, ...session };
|
|
2104
|
+
}
|
|
1583
2105
|
if (opts.stage) {
|
|
1584
2106
|
const stagePath = stageCodexInstallPlan(plan);
|
|
1585
|
-
|
|
2107
|
+
const session = writeCodexSession({
|
|
2108
|
+
operation: 'install',
|
|
2109
|
+
status: 'staged',
|
|
2110
|
+
targetTool: 'codex',
|
|
2111
|
+
uuid: plan.uuid,
|
|
2112
|
+
title: plan.title,
|
|
2113
|
+
sourceUrl: plan.sourceUrl,
|
|
2114
|
+
policyDecision: publicPlan.policyDecision,
|
|
2115
|
+
plan: publicPlan,
|
|
2116
|
+
result: { staged: true, stagePath },
|
|
2117
|
+
});
|
|
2118
|
+
return { dryRun: true, staged: true, stagePath, plan: publicPlan, installedFiles: [], ...session };
|
|
1586
2119
|
}
|
|
1587
2120
|
|
|
1588
2121
|
const installedFiles = [];
|
|
@@ -1602,8 +2135,22 @@ function executeCodexInstallPlan(plan, opts = {}) {
|
|
|
1602
2135
|
});
|
|
1603
2136
|
}
|
|
1604
2137
|
|
|
1605
|
-
const
|
|
1606
|
-
|
|
2138
|
+
const verification = runCodexPostVerify(publicPlan, opts);
|
|
2139
|
+
const session = writeCodexSession({
|
|
2140
|
+
operation: 'install',
|
|
2141
|
+
status: 'installed',
|
|
2142
|
+
targetTool: 'codex',
|
|
2143
|
+
uuid: plan.uuid,
|
|
2144
|
+
title: plan.title,
|
|
2145
|
+
sourceUrl: plan.sourceUrl,
|
|
2146
|
+
policyDecision: publicPlan.policyDecision,
|
|
2147
|
+
plan: publicPlan,
|
|
2148
|
+
installedFiles,
|
|
2149
|
+
verification,
|
|
2150
|
+
result: { installedFiles },
|
|
2151
|
+
});
|
|
2152
|
+
const manifestRecord = writeCodexManifestRecord(plan, installedFiles, session, verification);
|
|
2153
|
+
return { dryRun: false, plan: publicPlan, installedFiles, manifestRecord, verification, ...session };
|
|
1607
2154
|
}
|
|
1608
2155
|
|
|
1609
2156
|
async function installCodexAsset(workflow, contents, opts = {}) {
|
|
@@ -1631,6 +2178,7 @@ async function cmdInstall() {
|
|
|
1631
2178
|
dryRun: Boolean(args.flags.dryRun || args.flags.dry_run),
|
|
1632
2179
|
stage: Boolean(args.flags.stage),
|
|
1633
2180
|
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2181
|
+
verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
|
|
1634
2182
|
json: Boolean(args.flags.json),
|
|
1635
2183
|
manifest: Boolean(args.flags.manifest),
|
|
1636
2184
|
};
|
|
@@ -1807,7 +2355,8 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1807
2355
|
if (targetTool === 'codex') {
|
|
1808
2356
|
let result;
|
|
1809
2357
|
try {
|
|
1810
|
-
|
|
2358
|
+
const serverPlan = opts.serverPlan !== undefined ? opts.serverPlan : await fetchServerCodexInstallPlan(uuid, config, apiBase);
|
|
2359
|
+
result = await installCodexAsset(workflow, contents, { ...opts, serverPlan });
|
|
1811
2360
|
} catch (e) {
|
|
1812
2361
|
die(e.message);
|
|
1813
2362
|
}
|
|
@@ -1821,6 +2370,7 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1821
2370
|
} else {
|
|
1822
2371
|
info(`No Codex skill files were written. Re-run with --approve-mcp or --yes to install.`);
|
|
1823
2372
|
}
|
|
2373
|
+
if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
|
|
1824
2374
|
} else if (opts.dryRun) {
|
|
1825
2375
|
info(`Dry run: ${plan.files.length} file(s) would be installed to ${CODEX_SKILLS_DIR}`);
|
|
1826
2376
|
for (const file of plan.files) {
|
|
@@ -1828,6 +2378,7 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1828
2378
|
log(` ${C.dim}•${C.reset} ~/${rel}`);
|
|
1829
2379
|
if (file.riskFlags.length) log(` ${C.yellow}${file.riskFlags.join(', ')}${C.reset}`);
|
|
1830
2380
|
}
|
|
2381
|
+
if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
|
|
1831
2382
|
} else {
|
|
1832
2383
|
for (const file of result.installedFiles) {
|
|
1833
2384
|
const relPath = path.relative(os.homedir(), file.path);
|
|
@@ -1836,6 +2387,8 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1836
2387
|
log('');
|
|
1837
2388
|
success(`${result.installedFiles.length} file(s) installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1838
2389
|
log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
|
|
2390
|
+
if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
|
|
2391
|
+
if (result.verification && !result.verification.ok) log(` ${C.yellow}Verification: failed${C.reset}`);
|
|
1839
2392
|
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1840
2393
|
}
|
|
1841
2394
|
}
|
|
@@ -1851,6 +2404,9 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1851
2404
|
installedFiles: result.installedFiles || [],
|
|
1852
2405
|
plan: result.plan,
|
|
1853
2406
|
manifestPath: CODEX_MANIFEST_FILE,
|
|
2407
|
+
sessionId: result.sessionId,
|
|
2408
|
+
sessionPath: result.sessionPath,
|
|
2409
|
+
verification: result.verification,
|
|
1854
2410
|
};
|
|
1855
2411
|
}
|
|
1856
2412
|
|
|
@@ -2012,8 +2568,16 @@ async function cmdList() {
|
|
|
2012
2568
|
data = { ...data, list };
|
|
2013
2569
|
}
|
|
2014
2570
|
|
|
2571
|
+
const originalCount = (data.list || []).length;
|
|
2572
|
+
data = { ...data, list: applyAgentWorkflowFilters(data.list || [], args.flags) };
|
|
2573
|
+
const filters = {
|
|
2574
|
+
target: args.flags.target || undefined,
|
|
2575
|
+
kind: args.flags.kind || args.flags.assetKind || undefined,
|
|
2576
|
+
policy: args.flags.policy || undefined,
|
|
2577
|
+
};
|
|
2578
|
+
|
|
2015
2579
|
if (args.flags.json) {
|
|
2016
|
-
outputJson({ total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
|
|
2580
|
+
outputJson({ total: data.total || 0, fetched: originalCount, count: (data.list || []).length, filters, list: data.list || [] });
|
|
2017
2581
|
return;
|
|
2018
2582
|
}
|
|
2019
2583
|
|
|
@@ -2022,11 +2586,16 @@ async function cmdList() {
|
|
|
2022
2586
|
return;
|
|
2023
2587
|
}
|
|
2024
2588
|
|
|
2025
|
-
|
|
2589
|
+
const filterText = [filters.target ? `target=${filters.target}` : '', filters.kind ? `kind=${filters.kind}` : '', filters.policy ? `policy=${filters.policy}` : ''].filter(Boolean).join(' · ');
|
|
2590
|
+
log(` ${C.bold}${data.list.length}${C.reset} assets${filterText ? ` ${C.dim}(${filterText})${C.reset}` : ''}${data.total ? ` ${C.dim}from ${data.total}${C.reset}` : ''}:\n`);
|
|
2026
2591
|
|
|
2027
2592
|
for (const wf of data.list) {
|
|
2028
2593
|
const views = wf.view_count || 0;
|
|
2029
2594
|
log(` ${C.cyan}${wf.uuid.substring(0,8)}${C.reset} ${C.bold}${wf.title}${C.reset}`);
|
|
2595
|
+
if (wf.compatibility?.codex) {
|
|
2596
|
+
const c = wf.compatibility.codex;
|
|
2597
|
+
log(` ${C.dim} codex=${c.status} · policy=${c.policyDecision.decision} · kind=${c.assetKind || 'unknown'}${C.reset}`);
|
|
2598
|
+
}
|
|
2030
2599
|
log(` ${C.dim} ${views} views · https://tokrepo.com/en/workflows/${wf.uuid}${C.reset}\n`);
|
|
2031
2600
|
}
|
|
2032
2601
|
} catch (e) {
|
|
@@ -2236,13 +2805,16 @@ async function cmdClone() {
|
|
|
2236
2805
|
const assetType = getWorkflowAssetType(workflow);
|
|
2237
2806
|
const contents = extractInstallableContents(workflow, assetType);
|
|
2238
2807
|
if (contents.length === 0) throw new Error('No installable content found');
|
|
2808
|
+
const serverPlan = await fetchServerCodexInstallPlan(workflow.uuid, config, apiBase);
|
|
2239
2809
|
const result = await installCodexAsset(workflow, contents, {
|
|
2240
2810
|
...args.flags,
|
|
2241
2811
|
dryRun,
|
|
2242
2812
|
stage: Boolean(args.flags.stage),
|
|
2243
2813
|
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2814
|
+
verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
|
|
2244
2815
|
json: true,
|
|
2245
2816
|
throwOnError: true,
|
|
2817
|
+
serverPlan,
|
|
2246
2818
|
});
|
|
2247
2819
|
if (!dryRun) installedCount += result.installedFiles.length;
|
|
2248
2820
|
results.push({
|
|
@@ -2255,6 +2827,9 @@ async function cmdClone() {
|
|
|
2255
2827
|
files: result.plan.files,
|
|
2256
2828
|
installedFiles: result.installedFiles || [],
|
|
2257
2829
|
risks: result.plan.risks,
|
|
2830
|
+
sessionId: result.sessionId,
|
|
2831
|
+
sessionPath: result.sessionPath,
|
|
2832
|
+
verification: result.verification,
|
|
2258
2833
|
});
|
|
2259
2834
|
if (!json) {
|
|
2260
2835
|
const fileCount = (dryRun || args.flags.stage) ? result.plan.files.length : result.installedFiles.length;
|
|
@@ -2383,6 +2958,154 @@ async function fetchWorkflowForInstall(uuid, config, apiBase) {
|
|
|
2383
2958
|
return { workflow, contents };
|
|
2384
2959
|
}
|
|
2385
2960
|
|
|
2961
|
+
async function fetchServerCodexInstallPlan(uuid, config, apiBase) {
|
|
2962
|
+
try {
|
|
2963
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/install-plan?uuid=${encodeURIComponent(uuid)}&target=codex`, null, config?.token, apiBase);
|
|
2964
|
+
return data?.plan || data || null;
|
|
2965
|
+
} catch {
|
|
2966
|
+
return null;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
function runSelfCliJson(cliArgs, opts = {}) {
|
|
2971
|
+
const childProcess = require('child_process');
|
|
2972
|
+
const stdout = childProcess.execFileSync(process.execPath, [__filename, ...cliArgs], {
|
|
2973
|
+
env: { ...process.env, ...(opts.env || {}), TOKREPO_NONINTERACTIVE: '1' },
|
|
2974
|
+
cwd: opts.cwd || process.cwd(),
|
|
2975
|
+
encoding: 'utf8',
|
|
2976
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
2977
|
+
});
|
|
2978
|
+
return JSON.parse(stdout);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
function makeEvalResult(name, ok, details = {}) {
|
|
2982
|
+
return {
|
|
2983
|
+
name,
|
|
2984
|
+
ok: Boolean(ok),
|
|
2985
|
+
status: ok ? 'pass' : 'fail',
|
|
2986
|
+
...details,
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
function createTempDir(prefix) {
|
|
2991
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
async function cmdEvalAgent() {
|
|
2995
|
+
const args = parseArgs(process.argv);
|
|
2996
|
+
const json = Boolean(args.flags.json);
|
|
2997
|
+
const sampleUuid = args.flags.uuid || '91aeb22d-eff0-4310-abc6-811d2394b420';
|
|
2998
|
+
const query = args.flags.keyword || args.flags.query || 'video';
|
|
2999
|
+
const keepTemp = Boolean(args.flags.keep_temp || args.flags.keepTemp);
|
|
3000
|
+
const startedAt = new Date().toISOString();
|
|
3001
|
+
const results = [];
|
|
3002
|
+
const tempRoots = [];
|
|
3003
|
+
|
|
3004
|
+
if (!json) log(`\n${C.bold}tokrepo eval-agent${C.reset}\n`);
|
|
3005
|
+
|
|
3006
|
+
const runScenario = async (name, fn) => {
|
|
3007
|
+
const start = Date.now();
|
|
3008
|
+
try {
|
|
3009
|
+
const details = await fn();
|
|
3010
|
+
const result = makeEvalResult(name, true, { durationMs: Date.now() - start, ...details });
|
|
3011
|
+
results.push(result);
|
|
3012
|
+
if (!json) success(`${name} (${result.durationMs}ms)`);
|
|
3013
|
+
} catch (e) {
|
|
3014
|
+
const result = makeEvalResult(name, false, { durationMs: Date.now() - start, error: e.message });
|
|
3015
|
+
results.push(result);
|
|
3016
|
+
if (!json) warn(`${name}: ${e.message}`);
|
|
3017
|
+
}
|
|
3018
|
+
};
|
|
3019
|
+
|
|
3020
|
+
await runScenario('search_filters_codex_allow_skill', async () => {
|
|
3021
|
+
const data = runSelfCliJson(['search', query, '--target', 'codex', '--kind', 'skill', '--policy', 'allow', '--json', '--page-size', '10']);
|
|
3022
|
+
if (!data.count || !Array.isArray(data.list)) throw new Error('filtered search returned no list');
|
|
3023
|
+
const bad = data.list.find(item => item.policyDecision?.decision && item.policyDecision.decision !== 'allow');
|
|
3024
|
+
if (bad) throw new Error(`search returned non-allow asset ${bad.uuid}`);
|
|
3025
|
+
return { count: data.count, firstUuid: data.list[0]?.uuid, firstTitle: data.list[0]?.title };
|
|
3026
|
+
});
|
|
3027
|
+
|
|
3028
|
+
await runScenario('install_plan_contract', async () => {
|
|
3029
|
+
const plan = runSelfCliJson(['plan', sampleUuid, '--target', 'codex']);
|
|
3030
|
+
if (plan.schemaVersion !== 2) throw new Error(`expected schemaVersion 2, got ${plan.schemaVersion}`);
|
|
3031
|
+
if (!plan.policyDecision?.decision) throw new Error('missing policyDecision');
|
|
3032
|
+
if (!Array.isArray(plan.actions) || plan.actions.length === 0) throw new Error('missing actions');
|
|
3033
|
+
if (!Array.isArray(plan.rollback) || plan.rollback.length === 0) throw new Error('missing rollback');
|
|
3034
|
+
if (!Array.isArray(plan.postVerify) || plan.postVerify.length === 0) throw new Error('missing postVerify');
|
|
3035
|
+
return {
|
|
3036
|
+
sourceOfTruth: plan.sourceOfTruth,
|
|
3037
|
+
concretePlanSource: plan.concretePlanSource,
|
|
3038
|
+
policy: plan.policyDecision.decision,
|
|
3039
|
+
actions: plan.actions.length,
|
|
3040
|
+
};
|
|
3041
|
+
});
|
|
3042
|
+
|
|
3043
|
+
await runScenario('metadata_quality_report_non_blocking', async () => {
|
|
3044
|
+
const tmp = createTempDir('tokrepo-eval-quality');
|
|
3045
|
+
tempRoots.push(tmp);
|
|
3046
|
+
const skillPath = path.join(tmp, 'SKILL.md');
|
|
3047
|
+
fs.writeFileSync(skillPath, `---\nname: eval-agent-sample\ndescription: \"Sample skill used by tokrepo eval-agent.\"\n---\n\n# Eval Agent Sample\n\nUse this to test metadata quality reporting.\n`);
|
|
3048
|
+
const report = runSelfCliJson(['push', skillPath, '--metadata-report', '--json', '--kind', 'skill', '--target', 'codex', '--install-mode', 'single', '--entrypoint', 'SKILL.md']);
|
|
3049
|
+
if (!report.metadataQuality) throw new Error('missing metadataQuality');
|
|
3050
|
+
if (report.metadataQuality.status !== 'pass') throw new Error(`expected pass, got ${report.metadataQuality.status}`);
|
|
3051
|
+
return { score: report.metadataQuality.score, status: report.metadataQuality.status };
|
|
3052
|
+
});
|
|
3053
|
+
|
|
3054
|
+
await runScenario('codex_install_verify_and_rollback', async () => {
|
|
3055
|
+
const tmpHome = createTempDir('tokrepo-eval-home');
|
|
3056
|
+
tempRoots.push(tmpHome);
|
|
3057
|
+
const env = { HOME: tmpHome };
|
|
3058
|
+
const install = runSelfCliJson(['install', sampleUuid, '--target', 'codex', '--yes', '--json'], { env });
|
|
3059
|
+
if (!install.sessionId) throw new Error('install did not create sessionId');
|
|
3060
|
+
if (!install.verification?.ok) throw new Error('install verification failed');
|
|
3061
|
+
if (!install.installedFiles?.length) throw new Error('no files installed');
|
|
3062
|
+
const installed = runSelfCliJson(['installed', '--target', 'codex', '--json'], { env });
|
|
3063
|
+
if (installed.count < 1) throw new Error('installed manifest did not record asset');
|
|
3064
|
+
const rollback = runSelfCliJson(['rollback', '--last', '--target', 'codex', '--json'], { env });
|
|
3065
|
+
if (!rollback.removedFiles?.length) throw new Error('rollback removed no files');
|
|
3066
|
+
const after = runSelfCliJson(['installed', '--target', 'codex', '--json'], { env });
|
|
3067
|
+
if (after.count !== 0) throw new Error(`manifest still has ${after.count} install(s) after rollback`);
|
|
3068
|
+
return {
|
|
3069
|
+
installedFiles: install.installedFiles.length,
|
|
3070
|
+
sessionId: install.sessionId,
|
|
3071
|
+
removedFiles: rollback.removedFiles.length,
|
|
3072
|
+
};
|
|
3073
|
+
});
|
|
3074
|
+
|
|
3075
|
+
if (!keepTemp) {
|
|
3076
|
+
for (const dir of tempRoots) {
|
|
3077
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
const failed = results.filter(result => !result.ok);
|
|
3082
|
+
const summary = {
|
|
3083
|
+
schemaVersion: 1,
|
|
3084
|
+
startedAt,
|
|
3085
|
+
finishedAt: new Date().toISOString(),
|
|
3086
|
+
cliVersion: CLI_VERSION,
|
|
3087
|
+
targetTool: 'codex',
|
|
3088
|
+
sampleUuid,
|
|
3089
|
+
query,
|
|
3090
|
+
status: failed.length === 0 ? 'pass' : 'fail',
|
|
3091
|
+
passed: results.length - failed.length,
|
|
3092
|
+
failed: failed.length,
|
|
3093
|
+
count: results.length,
|
|
3094
|
+
tempRoots: keepTemp ? tempRoots : [],
|
|
3095
|
+
results,
|
|
3096
|
+
};
|
|
3097
|
+
|
|
3098
|
+
if (json) {
|
|
3099
|
+
outputJson(summary);
|
|
3100
|
+
} else {
|
|
3101
|
+
log('');
|
|
3102
|
+
if (summary.status === 'pass') success(`Agent eval passed: ${summary.passed}/${summary.count}`);
|
|
3103
|
+
else warn(`Agent eval failed: ${summary.failed}/${summary.count}`);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
2386
3109
|
async function cmdSyncInstalled() {
|
|
2387
3110
|
const args = parseArgs(process.argv);
|
|
2388
3111
|
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
@@ -2417,7 +3140,8 @@ async function cmdSyncInstalled() {
|
|
|
2417
3140
|
|
|
2418
3141
|
try {
|
|
2419
3142
|
const { workflow, contents } = await fetchWorkflowForInstall(uuid, config, apiBase);
|
|
2420
|
-
const
|
|
3143
|
+
const serverPlan = await fetchServerCodexInstallPlan(uuid, config, apiBase);
|
|
3144
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
|
|
2421
3145
|
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2422
3146
|
const shouldWrite = force || diff.needsUpdate;
|
|
2423
3147
|
|
|
@@ -2455,8 +3179,10 @@ async function cmdSyncInstalled() {
|
|
|
2455
3179
|
stage,
|
|
2456
3180
|
installMode: record.installMode || record.install_mode,
|
|
2457
3181
|
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
3182
|
+
verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
|
|
2458
3183
|
json: true,
|
|
2459
3184
|
throwOnError: true,
|
|
3185
|
+
serverPlan,
|
|
2460
3186
|
});
|
|
2461
3187
|
|
|
2462
3188
|
results.push({
|
|
@@ -2468,6 +3194,9 @@ async function cmdSyncInstalled() {
|
|
|
2468
3194
|
stagePath: installResult.stagePath,
|
|
2469
3195
|
installedFiles: installResult.installedFiles || [],
|
|
2470
3196
|
plan: installResult.plan,
|
|
3197
|
+
sessionId: installResult.sessionId,
|
|
3198
|
+
sessionPath: installResult.sessionPath,
|
|
3199
|
+
verification: installResult.verification,
|
|
2471
3200
|
});
|
|
2472
3201
|
if (!json) success(stage ? `Staged ${installResult.plan.files.length} file(s)` : `Updated ${(installResult.installedFiles || []).length} file(s)`);
|
|
2473
3202
|
} catch (e) {
|
|
@@ -2526,6 +3255,8 @@ async function cmdInstalled() {
|
|
|
2526
3255
|
installMode: record.installMode || record.install_mode,
|
|
2527
3256
|
installedAt: record.installedAt || record.installed_at,
|
|
2528
3257
|
contentHash: record.contentHash || record.content_hash || '',
|
|
3258
|
+
sessionId: record.sessionId || record.session_id,
|
|
3259
|
+
sessionPath: record.sessionPath || record.session_path,
|
|
2529
3260
|
risks: record.risks || [],
|
|
2530
3261
|
files,
|
|
2531
3262
|
status: files.some(file => !file.exists) ? 'missing-files' : files.some(file => file.changed) ? 'local-changes' : 'installed',
|
|
@@ -2549,6 +3280,281 @@ async function cmdInstalled() {
|
|
|
2549
3280
|
}
|
|
2550
3281
|
}
|
|
2551
3282
|
|
|
3283
|
+
function isCodexManagedPath(filePath) {
|
|
3284
|
+
const resolved = path.resolve(expandHomePath(filePath));
|
|
3285
|
+
return ensureInside(CODEX_SKILLS_DIR, resolved) || ensureInside(path.join(CODEX_TOKREPO_DIR, 'staged'), resolved);
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
function removeEmptyCodexDirs(startDir) {
|
|
3289
|
+
const roots = [CODEX_SKILLS_DIR, path.join(CODEX_TOKREPO_DIR, 'staged')].map(root => path.resolve(root));
|
|
3290
|
+
let dir = path.resolve(startDir);
|
|
3291
|
+
const root = roots.find(candidate => dir === candidate || dir.startsWith(candidate + path.sep));
|
|
3292
|
+
if (!root) return;
|
|
3293
|
+
while (dir !== root && dir.startsWith(root + path.sep)) {
|
|
3294
|
+
try {
|
|
3295
|
+
if (!fs.existsSync(dir) || fs.readdirSync(dir).length > 0) break;
|
|
3296
|
+
fs.rmdirSync(dir);
|
|
3297
|
+
} catch {
|
|
3298
|
+
break;
|
|
3299
|
+
}
|
|
3300
|
+
dir = path.dirname(dir);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
function findCodexManifestRecord(selector) {
|
|
3305
|
+
const manifest = readCodexManifest();
|
|
3306
|
+
const records = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
|
|
3307
|
+
const needle = String(selector || '').trim();
|
|
3308
|
+
if (!needle) return null;
|
|
3309
|
+
const lower = needle.toLowerCase();
|
|
3310
|
+
const exact = records.find(record => String(record.uuid || '').toLowerCase() === lower);
|
|
3311
|
+
if (exact) return exact;
|
|
3312
|
+
|
|
3313
|
+
const prefixMatches = /^[a-f0-9-]{8,}$/i.test(needle)
|
|
3314
|
+
? records.filter(record => String(record.uuid || '').toLowerCase().startsWith(lower))
|
|
3315
|
+
: [];
|
|
3316
|
+
if (prefixMatches.length === 1) return prefixMatches[0];
|
|
3317
|
+
if (prefixMatches.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the full UUID.`);
|
|
3318
|
+
|
|
3319
|
+
const slugNeedle = slugify(needle, '');
|
|
3320
|
+
const titleMatches = records.filter(record => {
|
|
3321
|
+
const title = String(record.title || '').toLowerCase();
|
|
3322
|
+
const sourceUrl = String(record.sourceUrl || record.source_url || '').toLowerCase();
|
|
3323
|
+
return title === lower || slugify(record.title || '', '') === slugNeedle || sourceUrl.includes(lower);
|
|
3324
|
+
});
|
|
3325
|
+
if (titleMatches.length === 1) return titleMatches[0];
|
|
3326
|
+
if (titleMatches.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the UUID.`);
|
|
3327
|
+
|
|
3328
|
+
const fuzzy = records.filter(record => String(record.title || '').toLowerCase().includes(lower));
|
|
3329
|
+
if (fuzzy.length === 1) return fuzzy[0];
|
|
3330
|
+
if (fuzzy.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the UUID.`);
|
|
3331
|
+
return null;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
function buildCodexRemovalPlan(record, files, opts = {}) {
|
|
3335
|
+
const actions = (files || []).map(file => {
|
|
3336
|
+
const filePath = path.resolve(expandHomePath(file.path));
|
|
3337
|
+
const exists = fs.existsSync(filePath);
|
|
3338
|
+
const actualSha = exists ? currentFileSha(filePath) : '';
|
|
3339
|
+
const expectedSha = file.sha256 || '';
|
|
3340
|
+
const changed = Boolean(exists && expectedSha && actualSha !== expectedSha);
|
|
3341
|
+
const managed = isCodexManagedPath(filePath);
|
|
3342
|
+
const allowed = managed && (!changed || opts.force);
|
|
3343
|
+
const reason = !managed ? 'outside-managed-roots'
|
|
3344
|
+
: changed && !opts.force ? 'local-changes'
|
|
3345
|
+
: exists ? 'remove'
|
|
3346
|
+
: 'already-missing';
|
|
3347
|
+
return {
|
|
3348
|
+
type: 'remove_file',
|
|
3349
|
+
path: filePath,
|
|
3350
|
+
sourceName: file.sourceName || file.source_name,
|
|
3351
|
+
expectedSha,
|
|
3352
|
+
actualSha,
|
|
3353
|
+
exists,
|
|
3354
|
+
changed,
|
|
3355
|
+
allowed,
|
|
3356
|
+
reason,
|
|
3357
|
+
};
|
|
3358
|
+
});
|
|
3359
|
+
return {
|
|
3360
|
+
schemaVersion: 1,
|
|
3361
|
+
operation: opts.operation || 'uninstall',
|
|
3362
|
+
targetTool: 'codex',
|
|
3363
|
+
uuid: record.uuid,
|
|
3364
|
+
title: record.title,
|
|
3365
|
+
sourceUrl: record.sourceUrl || record.source_url,
|
|
3366
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
3367
|
+
force: Boolean(opts.force),
|
|
3368
|
+
dryRun: Boolean(opts.dryRun),
|
|
3369
|
+
requiresConfirmation: actions.some(action => !action.allowed),
|
|
3370
|
+
actions,
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
function executeCodexRemovalPlan(plan, opts = {}) {
|
|
3375
|
+
const blocked = plan.actions.filter(action => !action.allowed);
|
|
3376
|
+
if (blocked.length > 0) {
|
|
3377
|
+
const first = blocked[0];
|
|
3378
|
+
throw new Error(`Refusing to remove ${first.path}: ${first.reason}. Use --force only if you want to remove local changes.`);
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
const removedFiles = [];
|
|
3382
|
+
const skippedFiles = [];
|
|
3383
|
+
for (const action of plan.actions) {
|
|
3384
|
+
if (!action.exists) {
|
|
3385
|
+
skippedFiles.push({ path: action.path, reason: 'already-missing' });
|
|
3386
|
+
continue;
|
|
3387
|
+
}
|
|
3388
|
+
fs.unlinkSync(action.path);
|
|
3389
|
+
removedFiles.push({ path: action.path, sha256: action.actualSha || action.expectedSha });
|
|
3390
|
+
removeEmptyCodexDirs(path.dirname(action.path));
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
const session = writeCodexSession({
|
|
3394
|
+
operation: plan.operation,
|
|
3395
|
+
status: plan.operation === 'rollback' ? 'rolled_back' : 'uninstalled',
|
|
3396
|
+
targetTool: 'codex',
|
|
3397
|
+
uuid: plan.uuid,
|
|
3398
|
+
title: plan.title,
|
|
3399
|
+
sourceUrl: plan.sourceUrl,
|
|
3400
|
+
plan,
|
|
3401
|
+
result: { removedFiles, skippedFiles },
|
|
3402
|
+
});
|
|
3403
|
+
|
|
3404
|
+
if (opts.removeManifest !== false && plan.uuid) {
|
|
3405
|
+
const manifest = readCodexManifest();
|
|
3406
|
+
manifest.installs = (manifest.installs || []).filter(item => !((item.targetTool || item.target_tool) === 'codex' && item.uuid === plan.uuid));
|
|
3407
|
+
writeCodexManifest(manifest);
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
return { dryRun: false, plan, removedFiles, skippedFiles, ...session };
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
async function cmdUninstall() {
|
|
3414
|
+
const args = parseArgs(process.argv);
|
|
3415
|
+
const target = args.positional[0];
|
|
3416
|
+
if (!target) {
|
|
3417
|
+
showUninstallHelp();
|
|
3418
|
+
process.exit(1);
|
|
3419
|
+
}
|
|
3420
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
3421
|
+
if (targetTool !== 'codex') error(`uninstall currently supports --target codex only`);
|
|
3422
|
+
|
|
3423
|
+
const json = Boolean(args.flags.json);
|
|
3424
|
+
const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
|
|
3425
|
+
const force = Boolean(args.flags.force);
|
|
3426
|
+
if (!json) log(`\n${C.bold}tokrepo uninstall${C.reset}\n`);
|
|
3427
|
+
|
|
3428
|
+
try {
|
|
3429
|
+
const record = findCodexManifestRecord(target);
|
|
3430
|
+
if (!record) error(`No installed Codex asset found for "${target}". Run: tokrepo installed --target codex`);
|
|
3431
|
+
const files = record.installedFiles || record.installed_files || [];
|
|
3432
|
+
const plan = buildCodexRemovalPlan(record, files, { operation: 'uninstall', dryRun, force });
|
|
3433
|
+
if (dryRun) {
|
|
3434
|
+
const session = writeCodexSession({
|
|
3435
|
+
operation: 'uninstall',
|
|
3436
|
+
status: 'dry_run',
|
|
3437
|
+
targetTool: 'codex',
|
|
3438
|
+
uuid: record.uuid,
|
|
3439
|
+
title: record.title,
|
|
3440
|
+
sourceUrl: record.sourceUrl || record.source_url,
|
|
3441
|
+
plan,
|
|
3442
|
+
result: { dryRun: true },
|
|
3443
|
+
});
|
|
3444
|
+
const response = { dryRun: true, plan, removedFiles: [], ...session };
|
|
3445
|
+
if (json) outputJson(response);
|
|
3446
|
+
else {
|
|
3447
|
+
info(`Dry run: ${plan.actions.length} file(s) would be removed`);
|
|
3448
|
+
for (const action of plan.actions) {
|
|
3449
|
+
const rel = path.relative(os.homedir(), action.path);
|
|
3450
|
+
log(` ${action.allowed ? C.dim : C.yellow}•${C.reset} ~/${rel} ${C.dim}${action.reason}${C.reset}`);
|
|
3451
|
+
}
|
|
3452
|
+
log(` ${C.dim}Session: ${session.sessionPath}${C.reset}`);
|
|
3453
|
+
}
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
const result = executeCodexRemovalPlan(plan, { force });
|
|
3458
|
+
if (json) outputJson(result);
|
|
3459
|
+
else {
|
|
3460
|
+
for (const file of result.removedFiles) {
|
|
3461
|
+
success(`Removed: ~/${path.relative(os.homedir(), file.path)}`);
|
|
3462
|
+
}
|
|
3463
|
+
success(`Uninstalled ${record.title || record.uuid}`);
|
|
3464
|
+
log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
|
|
3465
|
+
log(` ${C.dim}Session: ${result.sessionPath}${C.reset}\n`);
|
|
3466
|
+
}
|
|
3467
|
+
} catch (e) {
|
|
3468
|
+
error(`Uninstall failed: ${e.message}`);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
function findRollbackSession(selector) {
|
|
3473
|
+
const sessions = readCodexSessions();
|
|
3474
|
+
if (selector === 'last') {
|
|
3475
|
+
return [...sessions].reverse().find(session => (
|
|
3476
|
+
session.operation === 'install'
|
|
3477
|
+
&& ['installed', 'staged', 'stage_only'].includes(session.status)
|
|
3478
|
+
&& (session.installedFiles?.length || session.result?.stagePath || session.plan?.rollback?.length)
|
|
3479
|
+
));
|
|
3480
|
+
}
|
|
3481
|
+
const needle = String(selector || '').trim();
|
|
3482
|
+
if (!needle) return null;
|
|
3483
|
+
return sessions.find(session => session.sessionId === needle || String(session.sessionId || '').startsWith(needle));
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
function filesFromRollbackSession(session) {
|
|
3487
|
+
if (!session) return [];
|
|
3488
|
+
if (session.status === 'staged' && session.result?.stagePath) {
|
|
3489
|
+
return [{ path: session.result.stagePath, sha256: currentFileSha(session.result.stagePath), sourceName: 'install-plan.json' }];
|
|
3490
|
+
}
|
|
3491
|
+
if (Array.isArray(session.installedFiles) && session.installedFiles.length > 0) return session.installedFiles;
|
|
3492
|
+
return (session.plan?.rollback || [])
|
|
3493
|
+
.filter(action => action.type === 'remove_file' && action.path)
|
|
3494
|
+
.map(action => ({ path: action.path, sha256: action.sha256 || '', sourceName: path.basename(action.path) }));
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
async function cmdRollback() {
|
|
3498
|
+
const args = parseArgs(process.argv);
|
|
3499
|
+
const selector = args.flags.last ? 'last' : (args.flags.session || args.positional[0]);
|
|
3500
|
+
if (!selector) {
|
|
3501
|
+
showRollbackHelp();
|
|
3502
|
+
process.exit(1);
|
|
3503
|
+
}
|
|
3504
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
3505
|
+
if (targetTool !== 'codex') error(`rollback currently supports --target codex only`);
|
|
3506
|
+
|
|
3507
|
+
const json = Boolean(args.flags.json);
|
|
3508
|
+
const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
|
|
3509
|
+
const force = Boolean(args.flags.force);
|
|
3510
|
+
if (!json) log(`\n${C.bold}tokrepo rollback${C.reset}\n`);
|
|
3511
|
+
|
|
3512
|
+
try {
|
|
3513
|
+
const session = findRollbackSession(selector);
|
|
3514
|
+
if (!session) error(`No rollback session found for "${selector}". Run: tokrepo installed --target codex --json`);
|
|
3515
|
+
const files = filesFromRollbackSession(session);
|
|
3516
|
+
const plan = buildCodexRemovalPlan(session, files, { operation: 'rollback', dryRun, force });
|
|
3517
|
+
plan.rollbackSessionId = session.sessionId;
|
|
3518
|
+
plan.rollbackSessionPath = session.sessionPath;
|
|
3519
|
+
|
|
3520
|
+
if (dryRun) {
|
|
3521
|
+
const audit = writeCodexSession({
|
|
3522
|
+
operation: 'rollback',
|
|
3523
|
+
status: 'dry_run',
|
|
3524
|
+
targetTool: 'codex',
|
|
3525
|
+
uuid: session.uuid,
|
|
3526
|
+
title: session.title,
|
|
3527
|
+
sourceUrl: session.sourceUrl,
|
|
3528
|
+
plan,
|
|
3529
|
+
result: { dryRun: true },
|
|
3530
|
+
});
|
|
3531
|
+
const response = { dryRun: true, plan, removedFiles: [], ...audit };
|
|
3532
|
+
if (json) outputJson(response);
|
|
3533
|
+
else {
|
|
3534
|
+
info(`Dry run: rollback ${session.sessionId} would remove ${plan.actions.length} file(s)`);
|
|
3535
|
+
for (const action of plan.actions) {
|
|
3536
|
+
const rel = path.relative(os.homedir(), action.path);
|
|
3537
|
+
log(` ${action.allowed ? C.dim : C.yellow}•${C.reset} ~/${rel} ${C.dim}${action.reason}${C.reset}`);
|
|
3538
|
+
}
|
|
3539
|
+
log(` ${C.dim}Session: ${audit.sessionPath}${C.reset}`);
|
|
3540
|
+
}
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
const result = executeCodexRemovalPlan(plan, { force, removeManifest: Boolean(session.uuid) });
|
|
3545
|
+
if (json) outputJson(result);
|
|
3546
|
+
else {
|
|
3547
|
+
for (const file of result.removedFiles) {
|
|
3548
|
+
success(`Removed: ~/${path.relative(os.homedir(), file.path)}`);
|
|
3549
|
+
}
|
|
3550
|
+
success(`Rolled back ${session.sessionId}`);
|
|
3551
|
+
log(` ${C.dim}Session: ${result.sessionPath}${C.reset}\n`);
|
|
3552
|
+
}
|
|
3553
|
+
} catch (e) {
|
|
3554
|
+
error(`Rollback failed: ${e.message}`);
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
|
|
2552
3558
|
async function cmdOutdated() {
|
|
2553
3559
|
const args = parseArgs(process.argv);
|
|
2554
3560
|
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
@@ -2574,7 +3580,8 @@ async function cmdOutdated() {
|
|
|
2574
3580
|
for (const record of installed) {
|
|
2575
3581
|
try {
|
|
2576
3582
|
const { workflow, contents } = await fetchWorkflowForInstall(record.uuid, config, apiBase);
|
|
2577
|
-
const
|
|
3583
|
+
const serverPlan = await fetchServerCodexInstallPlan(record.uuid, config, apiBase);
|
|
3584
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
|
|
2578
3585
|
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2579
3586
|
if (diff.needsUpdate) {
|
|
2580
3587
|
list.push({
|
|
@@ -2743,6 +3750,9 @@ ${C.bold}DISCOVER & INSTALL${C.reset}
|
|
|
2743
3750
|
${C.cyan}installed${C.reset} List installed Codex assets from manifest
|
|
2744
3751
|
${C.cyan}outdated${C.reset} Check installed Codex assets for updates
|
|
2745
3752
|
${C.cyan}sync-installed${C.reset} Update installed Codex assets from manifest
|
|
3753
|
+
${C.cyan}uninstall${C.reset} <uuid> Remove a managed Codex install
|
|
3754
|
+
${C.cyan}rollback${C.reset} --last Roll back the latest Codex install session
|
|
3755
|
+
${C.cyan}eval-agent${C.reset} Run agent-native contract and lifecycle evals
|
|
2746
3756
|
|
|
2747
3757
|
${C.bold}PUBLISH${C.reset}
|
|
2748
3758
|
${C.cyan}push${C.reset} [files...] Push files/directory (idempotent upsert)
|
|
@@ -2768,6 +3778,7 @@ ${C.bold}PUSH OPTIONS${C.reset}
|
|
|
2768
3778
|
${C.cyan}--kind${C.reset} skill Set agent asset_kind
|
|
2769
3779
|
${C.cyan}--target${C.reset} codex Add target tool metadata on push
|
|
2770
3780
|
${C.cyan}--install-mode${C.reset} bundle Set install_mode metadata
|
|
3781
|
+
${C.cyan}--metadata-report${C.reset} Print agent metadata quality suggestions without pushing
|
|
2771
3782
|
|
|
2772
3783
|
${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
2773
3784
|
Skills → .claude/skills/ (if .claude/ exists)
|
|
@@ -2780,7 +3791,7 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
|
2780
3791
|
|
|
2781
3792
|
${C.bold}EXAMPLES${C.reset}
|
|
2782
3793
|
tokrepo search "mcp server" # Find MCP configs
|
|
2783
|
-
tokrepo search video --
|
|
3794
|
+
tokrepo search video --target codex --kind skill --policy allow --json
|
|
2784
3795
|
tokrepo detail ca000374-f5d8-... --json # Machine-readable detail
|
|
2785
3796
|
tokrepo plan 91aeb22d-eff0-4310-... # Install plan v2 for agents
|
|
2786
3797
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
@@ -2791,7 +3802,11 @@ ${C.bold}EXAMPLES${C.reset}
|
|
|
2791
3802
|
tokrepo outdated --target codex --json
|
|
2792
3803
|
tokrepo update --target codex --all
|
|
2793
3804
|
tokrepo sync-installed --target codex --dry-run
|
|
3805
|
+
tokrepo uninstall 91aeb22d --target codex --dry-run
|
|
3806
|
+
tokrepo rollback --last --target codex --dry-run
|
|
3807
|
+
tokrepo eval-agent --json
|
|
2794
3808
|
tokrepo push --private my-rules.md # Save one file privately
|
|
3809
|
+
tokrepo push . --metadata-report --json # Check agent metadata without uploading
|
|
2795
3810
|
tokrepo push . --kind skill --target codex --install-mode bundle
|
|
2796
3811
|
tokrepo push --public skill.md # Share one file publicly
|
|
2797
3812
|
tokrepo push --private . # Push current dir as private
|
|
@@ -2818,11 +3833,12 @@ function showSearchHelp() {
|
|
|
2818
3833
|
${C.bold}tokrepo search${C.reset}
|
|
2819
3834
|
|
|
2820
3835
|
USAGE
|
|
2821
|
-
tokrepo search <query> [--json] [--all] [--page-size N] [--sort-by views|latest|stars|popular]
|
|
3836
|
+
tokrepo search <query> [--json] [--all] [--target codex] [--kind skill] [--policy allow|confirm|stage_only|deny] [--page-size N] [--sort-by views|latest|stars|popular]
|
|
2822
3837
|
|
|
2823
3838
|
EXAMPLES
|
|
2824
3839
|
tokrepo search video
|
|
2825
3840
|
tokrepo search video --json
|
|
3841
|
+
tokrepo search video --target codex --kind skill --policy allow --json
|
|
2826
3842
|
tokrepo search "mcp server" --json --all
|
|
2827
3843
|
`);
|
|
2828
3844
|
}
|
|
@@ -2845,7 +3861,7 @@ function showInstallHelp() {
|
|
|
2845
3861
|
${C.bold}tokrepo install${C.reset}
|
|
2846
3862
|
|
|
2847
3863
|
USAGE
|
|
2848
|
-
tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--json]
|
|
3864
|
+
tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--verify-commands] [--json]
|
|
2849
3865
|
|
|
2850
3866
|
TARGETS
|
|
2851
3867
|
codex Write Codex skills to ~/.codex/skills/<asset-slug>/SKILL.md
|
|
@@ -2884,11 +3900,11 @@ function showListHelp() {
|
|
|
2884
3900
|
${C.bold}tokrepo list${C.reset}
|
|
2885
3901
|
|
|
2886
3902
|
USAGE
|
|
2887
|
-
tokrepo list [--json] [--all] [--page-size N]
|
|
3903
|
+
tokrepo list [--json] [--all] [--target codex] [--kind skill] [--policy allow] [--page-size N]
|
|
2888
3904
|
|
|
2889
3905
|
EXAMPLES
|
|
2890
3906
|
tokrepo list
|
|
2891
|
-
tokrepo list --json --all
|
|
3907
|
+
tokrepo list --json --all --target codex
|
|
2892
3908
|
`);
|
|
2893
3909
|
}
|
|
2894
3910
|
|
|
@@ -2931,6 +3947,61 @@ EXAMPLES
|
|
|
2931
3947
|
`);
|
|
2932
3948
|
}
|
|
2933
3949
|
|
|
3950
|
+
function showUninstallHelp() {
|
|
3951
|
+
log(`
|
|
3952
|
+
${C.bold}tokrepo uninstall${C.reset}
|
|
3953
|
+
|
|
3954
|
+
USAGE
|
|
3955
|
+
tokrepo uninstall <uuid|uuid-prefix|title> --target codex [--dry-run] [--force] [--json]
|
|
3956
|
+
|
|
3957
|
+
BEHAVIOR
|
|
3958
|
+
Removes only files recorded in ~/.codex/tokrepo/install-manifest.json and only
|
|
3959
|
+
under ~/.codex/skills or ~/.codex/tokrepo/staged. Local changes are blocked
|
|
3960
|
+
unless --force is provided.
|
|
3961
|
+
|
|
3962
|
+
EXAMPLES
|
|
3963
|
+
tokrepo uninstall 91aeb22d --target codex --dry-run --json
|
|
3964
|
+
tokrepo uninstall 91aeb22d-eff0-4310-abc6-811d2394b420 --target codex
|
|
3965
|
+
`);
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
function showRollbackHelp() {
|
|
3969
|
+
log(`
|
|
3970
|
+
${C.bold}tokrepo rollback${C.reset}
|
|
3971
|
+
|
|
3972
|
+
USAGE
|
|
3973
|
+
tokrepo rollback --last --target codex [--dry-run] [--force] [--json]
|
|
3974
|
+
tokrepo rollback <session-id> --target codex [--dry-run] [--force] [--json]
|
|
3975
|
+
|
|
3976
|
+
BEHAVIOR
|
|
3977
|
+
Replays the rollback section from ~/.codex/tokrepo/sessions/<session-id>.json.
|
|
3978
|
+
Local changes are blocked unless --force is provided.
|
|
3979
|
+
|
|
3980
|
+
EXAMPLES
|
|
3981
|
+
tokrepo rollback --last --target codex --dry-run --json
|
|
3982
|
+
tokrepo rollback install-20260506-120000-abc123 --target codex
|
|
3983
|
+
`);
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
function showEvalAgentHelp() {
|
|
3987
|
+
log(`
|
|
3988
|
+
${C.bold}tokrepo eval-agent${C.reset}
|
|
3989
|
+
|
|
3990
|
+
USAGE
|
|
3991
|
+
tokrepo eval-agent [--json] [--uuid <asset-uuid>] [--keyword video] [--keep-temp]
|
|
3992
|
+
|
|
3993
|
+
BEHAVIOR
|
|
3994
|
+
Runs agent-native smoke evals against search filters, install-plan contracts,
|
|
3995
|
+
metadata quality reporting, Codex install verification, manifest state, and rollback.
|
|
3996
|
+
Lifecycle tests use a temporary HOME and do not touch your real ~/.codex.
|
|
3997
|
+
|
|
3998
|
+
EXAMPLES
|
|
3999
|
+
tokrepo eval-agent
|
|
4000
|
+
tokrepo eval-agent --json
|
|
4001
|
+
tokrepo eval-agent --uuid 91aeb22d-eff0-4310-abc6-811d2394b420 --keyword video --json
|
|
4002
|
+
`);
|
|
4003
|
+
}
|
|
4004
|
+
|
|
2934
4005
|
function showCommandHelp(command) {
|
|
2935
4006
|
switch (command) {
|
|
2936
4007
|
case 'search':
|
|
@@ -2952,6 +4023,15 @@ function showCommandHelp(command) {
|
|
|
2952
4023
|
case 'installed':
|
|
2953
4024
|
case 'outdated':
|
|
2954
4025
|
showSyncInstalledHelp(); break;
|
|
4026
|
+
case 'uninstall':
|
|
4027
|
+
case 'remove':
|
|
4028
|
+
case 'rm':
|
|
4029
|
+
showUninstallHelp(); break;
|
|
4030
|
+
case 'rollback':
|
|
4031
|
+
showRollbackHelp(); break;
|
|
4032
|
+
case 'eval-agent':
|
|
4033
|
+
case 'eval':
|
|
4034
|
+
showEvalAgentHelp(); break;
|
|
2955
4035
|
default:
|
|
2956
4036
|
showHelp(); break;
|
|
2957
4037
|
}
|
|
@@ -2982,6 +4062,9 @@ async function main() {
|
|
|
2982
4062
|
case 'delete': await cmdDelete(); break;
|
|
2983
4063
|
case 'clone': await cmdClone(); break;
|
|
2984
4064
|
case 'installed': await cmdInstalled(); break;
|
|
4065
|
+
case 'uninstall': case 'remove': case 'rm': await cmdUninstall(); break;
|
|
4066
|
+
case 'rollback': await cmdRollback(); break;
|
|
4067
|
+
case 'eval-agent': case 'eval': await cmdEvalAgent(); break;
|
|
2985
4068
|
case 'outdated': await cmdOutdated(); break;
|
|
2986
4069
|
case 'sync-installed': case 'sync': await cmdSyncInstalled(); break;
|
|
2987
4070
|
case 'tags': await cmdTags(); break;
|