tokrepo 3.6.0 → 3.7.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 +770 -27
- 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.7.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 ───
|
|
@@ -304,6 +305,7 @@ function parseArgs(argv) {
|
|
|
304
305
|
'title', 'desc', 'tag', 'target', 'targets', 'keyword', 'types',
|
|
305
306
|
'kind', 'install-mode', 'install_mode', 'entrypoint', 'asset-kind', 'asset_kind',
|
|
306
307
|
'version',
|
|
308
|
+
'policy', 'session',
|
|
307
309
|
'page', 'page-size', 'page_size', 'sort-by', 'sort_by',
|
|
308
310
|
'time-window', 'time_window',
|
|
309
311
|
]);
|
|
@@ -841,8 +843,23 @@ async function cmdSearch() {
|
|
|
841
843
|
data = { ...data, list };
|
|
842
844
|
}
|
|
843
845
|
|
|
846
|
+
const originalCount = (data.list || []).length;
|
|
847
|
+
data = { ...data, list: applyAgentWorkflowFilters(data.list || [], args.flags) };
|
|
848
|
+
const filters = {
|
|
849
|
+
target: args.flags.target || undefined,
|
|
850
|
+
kind: args.flags.kind || args.flags.assetKind || undefined,
|
|
851
|
+
policy: args.flags.policy || undefined,
|
|
852
|
+
};
|
|
853
|
+
|
|
844
854
|
if (args.flags.json) {
|
|
845
|
-
outputJson({
|
|
855
|
+
outputJson({
|
|
856
|
+
query,
|
|
857
|
+
total: data.total || 0,
|
|
858
|
+
fetched: originalCount,
|
|
859
|
+
count: (data.list || []).length,
|
|
860
|
+
filters,
|
|
861
|
+
list: data.list || [],
|
|
862
|
+
});
|
|
846
863
|
return;
|
|
847
864
|
}
|
|
848
865
|
|
|
@@ -859,7 +876,8 @@ async function cmdSearch() {
|
|
|
859
876
|
return;
|
|
860
877
|
}
|
|
861
878
|
|
|
862
|
-
|
|
879
|
+
const filterText = [filters.target ? `target=${filters.target}` : '', filters.kind ? `kind=${filters.kind}` : '', filters.policy ? `policy=${filters.policy}` : ''].filter(Boolean).join(' · ');
|
|
880
|
+
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
881
|
|
|
864
882
|
for (let i = 0; i < data.list.length; i++) {
|
|
865
883
|
const wf = data.list[i];
|
|
@@ -874,6 +892,10 @@ async function cmdSearch() {
|
|
|
874
892
|
log(` ${C.dim}${String(i + 1).padStart(2)}.${C.reset} ${C.bold}${wf.title}${C.reset}`);
|
|
875
893
|
if (desc) log(` ${desc}`);
|
|
876
894
|
if (tags) log(` ${C.cyan}${tags}${C.reset} ${C.dim}★${votes} 👁${views}${C.reset}`);
|
|
895
|
+
if (wf.compatibility?.codex) {
|
|
896
|
+
const c = wf.compatibility.codex;
|
|
897
|
+
log(` ${C.dim}codex: ${c.status} · policy=${c.policyDecision.decision} · kind=${c.assetKind || 'unknown'}${C.reset}`);
|
|
898
|
+
}
|
|
877
899
|
log(` ${C.dim}tokrepo install ${wf.uuid}${C.reset}`);
|
|
878
900
|
log('');
|
|
879
901
|
}
|
|
@@ -984,6 +1006,40 @@ function getWorkflowAssetType(workflow) {
|
|
|
984
1006
|
return (workflow.tags[0].slug || workflow.tags[0].name || '').toLowerCase();
|
|
985
1007
|
}
|
|
986
1008
|
|
|
1009
|
+
function workflowAgentMetadata(workflow) {
|
|
1010
|
+
return workflow?.agent_metadata || workflow?.agentMetadata || {};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function normalizeCodexInstallMode(mode) {
|
|
1014
|
+
const normalized = String(mode || '').trim().toLowerCase().replace(/-/g, '_');
|
|
1015
|
+
return ['single', 'bundle', 'split', 'stage_only'].includes(normalized) ? normalized : '';
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function workflowAssetKind(workflow) {
|
|
1019
|
+
const metadata = workflowAgentMetadata(workflow);
|
|
1020
|
+
const explicit = workflow?.asset_kind || workflow?.assetKind || metadata.asset_kind || metadata.assetKind || '';
|
|
1021
|
+
if (explicit) return normalizeToolName(explicit);
|
|
1022
|
+
const assetType = getWorkflowAssetType(workflow);
|
|
1023
|
+
const aliases = {
|
|
1024
|
+
skills: 'skill',
|
|
1025
|
+
prompts: 'prompt',
|
|
1026
|
+
knowledge: 'knowledge',
|
|
1027
|
+
'mcp-configs': 'mcp_config',
|
|
1028
|
+
mcp: 'mcp_config',
|
|
1029
|
+
scripts: 'script',
|
|
1030
|
+
configs: 'config',
|
|
1031
|
+
tools: 'cli_tool',
|
|
1032
|
+
};
|
|
1033
|
+
return aliases[assetType] || normalizeToolName(assetType);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function workflowTargetTools(workflow) {
|
|
1037
|
+
const metadata = workflowAgentMetadata(workflow);
|
|
1038
|
+
return parseCsvList(workflow?.target_tools || workflow?.targetTools || metadata.target_tools || metadata.targetTools)
|
|
1039
|
+
.map(normalizeToolName)
|
|
1040
|
+
.filter(Boolean);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
987
1043
|
function extractInstallableContents(workflow, assetType) {
|
|
988
1044
|
const contents = [];
|
|
989
1045
|
const files = workflow.files || [];
|
|
@@ -1129,8 +1185,7 @@ function explicitInstallMode(workflow) {
|
|
|
1129
1185
|
workflow?.metadata?.installMode,
|
|
1130
1186
|
workflow?.metadata?.install_mode,
|
|
1131
1187
|
].filter(Boolean);
|
|
1132
|
-
|
|
1133
|
-
return ['single', 'bundle', 'split', 'stage_only'].includes(mode) ? mode : '';
|
|
1188
|
+
return normalizeCodexInstallMode(candidates[0]);
|
|
1134
1189
|
}
|
|
1135
1190
|
|
|
1136
1191
|
function inferCodexInstallMode(workflow, contents) {
|
|
@@ -1187,8 +1242,12 @@ function addPlanFile(plan, destPath, content, sourceName, type) {
|
|
|
1187
1242
|
}
|
|
1188
1243
|
|
|
1189
1244
|
function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
1190
|
-
const
|
|
1191
|
-
const
|
|
1245
|
+
const serverPlan = opts.serverPlan || null;
|
|
1246
|
+
const serverMetadata = serverPlan?.metadata || serverPlan?.agentMetadata || serverPlan?.agent_metadata || {};
|
|
1247
|
+
const installMode = normalizeCodexInstallMode(opts.installMode)
|
|
1248
|
+
|| normalizeCodexInstallMode(metadataValue(serverPlan, 'install_mode', 'installMode', ''))
|
|
1249
|
+
|| inferCodexInstallMode(workflow, contents);
|
|
1250
|
+
const agentMetadata = Object.keys(serverMetadata || {}).length > 0 ? serverMetadata : workflowAgentMetadata(workflow);
|
|
1192
1251
|
const plan = {
|
|
1193
1252
|
uuid: workflow.uuid,
|
|
1194
1253
|
title: workflow.title,
|
|
@@ -1199,7 +1258,8 @@ function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
|
1199
1258
|
files: [],
|
|
1200
1259
|
risks: [],
|
|
1201
1260
|
agentMetadata,
|
|
1202
|
-
contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || '',
|
|
1261
|
+
contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || agentMetadata.contentHash || metadataValue(serverPlan, 'content_hash', 'contentHash', ''),
|
|
1262
|
+
serverPlan,
|
|
1203
1263
|
};
|
|
1204
1264
|
|
|
1205
1265
|
if (installMode === 'stage_only') {
|
|
@@ -1313,7 +1373,36 @@ function mergedPlanRiskProfile(plan) {
|
|
|
1313
1373
|
};
|
|
1314
1374
|
}
|
|
1315
1375
|
|
|
1376
|
+
function policyDecisionFromServerPlan(plan) {
|
|
1377
|
+
const serverPlan = plan?.serverPlan;
|
|
1378
|
+
if (!serverPlan) return null;
|
|
1379
|
+
const raw = metadataValue(serverPlan, 'policy_decision', 'policyDecision', null);
|
|
1380
|
+
if (!raw) return null;
|
|
1381
|
+
if (typeof raw === 'string') {
|
|
1382
|
+
return {
|
|
1383
|
+
decision: raw,
|
|
1384
|
+
requiresConfirmation: raw === 'confirm',
|
|
1385
|
+
reasons: [],
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
const decision = String(raw.decision || raw.action || 'allow').trim().toLowerCase();
|
|
1389
|
+
const requiresConfirmation = Boolean(
|
|
1390
|
+
raw.requires_confirmation
|
|
1391
|
+
|| raw.requiresConfirmation
|
|
1392
|
+
|| metadataValue(serverPlan, 'requires_confirmation', 'requiresConfirmation', false)
|
|
1393
|
+
);
|
|
1394
|
+
const reasons = raw.reasons || raw.reason || [];
|
|
1395
|
+
return {
|
|
1396
|
+
decision,
|
|
1397
|
+
requiresConfirmation,
|
|
1398
|
+
reasons: Array.isArray(reasons) ? reasons : [String(reasons)].filter(Boolean),
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1316
1402
|
function decideCodexPolicy(plan) {
|
|
1403
|
+
const serverPolicy = policyDecisionFromServerPlan(plan);
|
|
1404
|
+
if (serverPolicy) return serverPolicy;
|
|
1405
|
+
|
|
1317
1406
|
const metadata = plan.agentMetadata || {};
|
|
1318
1407
|
const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
|
|
1319
1408
|
const assetKind = normalizeToolName(metadataValue(metadata, 'asset_kind', 'assetKind', ''));
|
|
@@ -1367,6 +1456,9 @@ function decideCodexPolicy(plan) {
|
|
|
1367
1456
|
}
|
|
1368
1457
|
|
|
1369
1458
|
function buildPublicPlanActions(plan) {
|
|
1459
|
+
const serverActions = metadataValue(plan.serverPlan, 'actions', 'actions', null);
|
|
1460
|
+
if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverActions) && serverActions.length > 0) return serverActions;
|
|
1461
|
+
|
|
1370
1462
|
const stage = plan.installMode === 'stage_only';
|
|
1371
1463
|
return plan.files.map(file => ({
|
|
1372
1464
|
type: stage ? 'stage_file' : 'write_file',
|
|
@@ -1380,7 +1472,23 @@ function buildPublicPlanActions(plan) {
|
|
|
1380
1472
|
}));
|
|
1381
1473
|
}
|
|
1382
1474
|
|
|
1475
|
+
function serverConcretePlanMatchesLocal(plan) {
|
|
1476
|
+
const serverActions = metadataValue(plan.serverPlan, 'actions', 'actions', null);
|
|
1477
|
+
if (!Array.isArray(serverActions) || serverActions.length !== (plan.files || []).length) return false;
|
|
1478
|
+
return serverActions.every((action, index) => {
|
|
1479
|
+
const file = plan.files[index];
|
|
1480
|
+
if (!file) return false;
|
|
1481
|
+
const serverPath = path.resolve(expandHomePath(action.path || ''));
|
|
1482
|
+
const localPath = path.resolve(file.path || '');
|
|
1483
|
+
const serverSha = action.sha256 || action.sha || '';
|
|
1484
|
+
return serverPath === localPath && (!serverSha || serverSha === file.sha256);
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1383
1488
|
function buildPublicPlanPreconditions(plan, policyDecision) {
|
|
1489
|
+
const serverPreconditions = metadataValue(plan.serverPlan, 'preconditions', 'preconditions', null);
|
|
1490
|
+
if (Array.isArray(serverPreconditions) && serverPreconditions.length > 0) return serverPreconditions;
|
|
1491
|
+
|
|
1384
1492
|
const metadata = plan.agentMetadata || {};
|
|
1385
1493
|
const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
|
|
1386
1494
|
const out = [
|
|
@@ -1405,6 +1513,9 @@ function buildPublicPlanPreconditions(plan, policyDecision) {
|
|
|
1405
1513
|
}
|
|
1406
1514
|
|
|
1407
1515
|
function buildPublicPlanRollback(plan) {
|
|
1516
|
+
const serverRollback = metadataValue(plan.serverPlan, 'rollback', 'rollback', null);
|
|
1517
|
+
if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverRollback) && serverRollback.length > 0) return serverRollback;
|
|
1518
|
+
|
|
1408
1519
|
const seen = new Set();
|
|
1409
1520
|
const rollback = [];
|
|
1410
1521
|
for (const file of plan.files) {
|
|
@@ -1416,11 +1527,18 @@ function buildPublicPlanRollback(plan) {
|
|
|
1416
1527
|
}
|
|
1417
1528
|
|
|
1418
1529
|
function buildPublicPlanPostVerify(plan) {
|
|
1530
|
+
const serverPostVerify = metadataValue(plan.serverPlan, 'post_verify', 'postVerify', null);
|
|
1531
|
+
if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverPostVerify) && serverPostVerify.length > 0) return serverPostVerify;
|
|
1532
|
+
|
|
1419
1533
|
const metadata = plan.agentMetadata || {};
|
|
1420
1534
|
const verification = metadataValue(metadata, 'verification', 'verification', {}) || {};
|
|
1421
1535
|
const out = plan.files.map(file => ({ type: 'file_sha256', path: file.path, sha256: file.sha256 }));
|
|
1536
|
+
const installedPaths = new Set(plan.files.map(file => path.resolve(file.path)));
|
|
1422
1537
|
for (const expected of (verification.expected_files || verification.expectedFiles || [])) {
|
|
1423
|
-
|
|
1538
|
+
const resolvedExpected = path.resolve(resolveVerifyPath(expected, { baseDir: plan.baseDir, files: plan.files }));
|
|
1539
|
+
if (installedPaths.has(resolvedExpected)) {
|
|
1540
|
+
out.push({ type: 'expected_file', path: expected });
|
|
1541
|
+
}
|
|
1424
1542
|
}
|
|
1425
1543
|
for (const command of (verification.commands || [])) {
|
|
1426
1544
|
out.push({ type: 'command', command });
|
|
@@ -1431,8 +1549,11 @@ function buildPublicPlanPostVerify(plan) {
|
|
|
1431
1549
|
function publicInstallPlan(plan) {
|
|
1432
1550
|
const policyDecision = decideCodexPolicy(plan);
|
|
1433
1551
|
const actions = buildPublicPlanActions(plan);
|
|
1552
|
+
const schemaVersion = Number(metadataValue(plan.serverPlan, 'schema_version', 'schemaVersion', 2)) || 2;
|
|
1434
1553
|
return {
|
|
1435
|
-
schemaVersion
|
|
1554
|
+
schemaVersion,
|
|
1555
|
+
sourceOfTruth: plan.serverPlan ? 'api_install_plan_v2' : 'local_fallback',
|
|
1556
|
+
concretePlanSource: serverConcretePlanMatchesLocal(plan) ? 'api_install_plan_v2' : 'local_fallback',
|
|
1436
1557
|
uuid: plan.uuid,
|
|
1437
1558
|
title: plan.title,
|
|
1438
1559
|
sourceUrl: plan.sourceUrl,
|
|
@@ -1460,6 +1581,95 @@ function publicInstallPlan(plan) {
|
|
|
1460
1581
|
};
|
|
1461
1582
|
}
|
|
1462
1583
|
|
|
1584
|
+
function workflowCodexCompatibility(workflow) {
|
|
1585
|
+
const metadata = workflowAgentMetadata(workflow);
|
|
1586
|
+
const assetKind = workflowAssetKind(workflow);
|
|
1587
|
+
const targetTools = workflowTargetTools(workflow);
|
|
1588
|
+
const installMode = normalizeCodexInstallMode(metadata.install_mode || metadata.installMode || workflow.install_mode || workflow.installMode) || 'single';
|
|
1589
|
+
const policy = decideCodexPolicy({
|
|
1590
|
+
agentMetadata: {
|
|
1591
|
+
...metadata,
|
|
1592
|
+
asset_kind: assetKind,
|
|
1593
|
+
target_tools: targetTools,
|
|
1594
|
+
install_mode: installMode,
|
|
1595
|
+
},
|
|
1596
|
+
risks: [],
|
|
1597
|
+
installMode,
|
|
1598
|
+
});
|
|
1599
|
+
const scores = { allow: 100, confirm: 70, stage_only: 40, deny: 0 };
|
|
1600
|
+
const statuses = {
|
|
1601
|
+
allow: 'native',
|
|
1602
|
+
confirm: 'requires_confirmation',
|
|
1603
|
+
stage_only: 'stage_only',
|
|
1604
|
+
deny: 'denied',
|
|
1605
|
+
};
|
|
1606
|
+
return {
|
|
1607
|
+
targetTool: 'codex',
|
|
1608
|
+
status: statuses[policy.decision] || 'unknown',
|
|
1609
|
+
score: scores[policy.decision] ?? 50,
|
|
1610
|
+
assetKind,
|
|
1611
|
+
targetTools,
|
|
1612
|
+
installMode,
|
|
1613
|
+
policyDecision: policy,
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function workflowMatchesAgentFilters(workflow, flags = {}) {
|
|
1618
|
+
const target = normalizeInstallTarget(flags.target || '');
|
|
1619
|
+
const requestedKinds = parseCsvList(flags.kind || flags.assetKind || flags.asset_kind).map(normalizeToolName);
|
|
1620
|
+
const requestedPolicies = parseCsvList(flags.policy).map(s => String(s).trim().toLowerCase());
|
|
1621
|
+
const assetKind = workflowAssetKind(workflow);
|
|
1622
|
+
const targetTools = workflowTargetTools(workflow);
|
|
1623
|
+
const compatibility = workflowCodexCompatibility(workflow);
|
|
1624
|
+
|
|
1625
|
+
if (target === 'codex') {
|
|
1626
|
+
if (targetTools.length > 0 && !targetTools.includes('codex')) return false;
|
|
1627
|
+
} else if (target && targetTools.length > 0 && !targetTools.includes(target)) {
|
|
1628
|
+
return false;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if (requestedKinds.length > 0) {
|
|
1632
|
+
const kindAliases = new Set([assetKind, `${assetKind}s`, assetKind.replace(/_/g, '-')]);
|
|
1633
|
+
const tags = (workflow.tags || []).flatMap(t => [t.slug, t.name]).filter(Boolean).map(normalizeToolName);
|
|
1634
|
+
const matchesKind = requestedKinds.some(kind => kindAliases.has(kind) || tags.includes(kind) || tags.includes(`${kind}s`));
|
|
1635
|
+
if (!matchesKind) return false;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (requestedPolicies.length > 0) {
|
|
1639
|
+
const decision = compatibility.policyDecision.decision;
|
|
1640
|
+
const aliases = {
|
|
1641
|
+
safe: 'allow',
|
|
1642
|
+
staged: 'stage_only',
|
|
1643
|
+
stage: 'stage_only',
|
|
1644
|
+
block: 'deny',
|
|
1645
|
+
blocked: 'deny',
|
|
1646
|
+
};
|
|
1647
|
+
const normalizedPolicies = requestedPolicies.map(policy => aliases[policy] || policy);
|
|
1648
|
+
if (!normalizedPolicies.includes(decision)) return false;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
return true;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function enrichWorkflowForAgent(workflow) {
|
|
1655
|
+
const compatibility = workflowCodexCompatibility(workflow);
|
|
1656
|
+
return {
|
|
1657
|
+
...workflow,
|
|
1658
|
+
assetKind: compatibility.assetKind,
|
|
1659
|
+
targetTools: compatibility.targetTools,
|
|
1660
|
+
compatibility: {
|
|
1661
|
+
codex: compatibility,
|
|
1662
|
+
},
|
|
1663
|
+
policyDecision: compatibility.policyDecision,
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function applyAgentWorkflowFilters(list, flags = {}) {
|
|
1668
|
+
const shouldEnrich = flags.target || flags.kind || flags.assetKind || flags.asset_kind || flags.policy;
|
|
1669
|
+
const filtered = (list || []).filter(item => workflowMatchesAgentFilters(item, flags));
|
|
1670
|
+
return shouldEnrich ? filtered.map(enrichWorkflowForAgent) : filtered;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1463
1673
|
function hasCodexInstallRisks(plan) {
|
|
1464
1674
|
const decision = decideCodexPolicy(plan).decision;
|
|
1465
1675
|
return decision === 'confirm' || decision === 'stage_only' || decision === 'deny';
|
|
@@ -1538,6 +1748,102 @@ function executeStageOnlyCodexPlan(plan) {
|
|
|
1538
1748
|
return { dryRun: true, staged: true, stageOnly: true, stagePath, plan: publicInstallPlan(plan), installedFiles };
|
|
1539
1749
|
}
|
|
1540
1750
|
|
|
1751
|
+
function expandHomePath(input) {
|
|
1752
|
+
const value = String(input || '');
|
|
1753
|
+
if (value === '~') return os.homedir();
|
|
1754
|
+
if (value.startsWith('~/')) return path.join(os.homedir(), value.slice(2));
|
|
1755
|
+
return value;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function resolveVerifyPath(checkPath, publicPlan) {
|
|
1759
|
+
const expanded = expandHomePath(checkPath);
|
|
1760
|
+
if (path.isAbsolute(expanded)) return expanded;
|
|
1761
|
+
const baseDir = publicPlan.baseDir || path.dirname(publicPlan.files?.[0]?.path || CODEX_SKILLS_DIR);
|
|
1762
|
+
return path.join(baseDir, expanded);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function runCodexPostVerify(publicPlan, opts = {}) {
|
|
1766
|
+
const checks = [];
|
|
1767
|
+
let ok = true;
|
|
1768
|
+
for (const check of (publicPlan.postVerify || [])) {
|
|
1769
|
+
if (check.type === 'file_sha256') {
|
|
1770
|
+
const filePath = resolveVerifyPath(check.path, publicPlan);
|
|
1771
|
+
const exists = fs.existsSync(filePath);
|
|
1772
|
+
const actualSha = exists ? currentFileSha(filePath) : '';
|
|
1773
|
+
const passed = Boolean(exists && actualSha === check.sha256);
|
|
1774
|
+
if (!passed) ok = false;
|
|
1775
|
+
checks.push({ ...check, path: filePath, status: passed ? 'pass' : 'fail', actualSha });
|
|
1776
|
+
} else if (check.type === 'expected_file') {
|
|
1777
|
+
const filePath = resolveVerifyPath(check.path, publicPlan);
|
|
1778
|
+
const passed = fs.existsSync(filePath);
|
|
1779
|
+
if (!passed) ok = false;
|
|
1780
|
+
checks.push({ ...check, path: filePath, status: passed ? 'pass' : 'fail' });
|
|
1781
|
+
} else if (check.type === 'command') {
|
|
1782
|
+
if (!opts.verifyCommands) {
|
|
1783
|
+
checks.push({ ...check, status: 'skipped', message: 'command verification is opt-in; re-run with --verify-commands' });
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
const childProcess = require('child_process');
|
|
1788
|
+
childProcess.execSync(String(check.command || ''), { stdio: 'pipe', shell: true, timeout: 30000 });
|
|
1789
|
+
checks.push({ ...check, status: 'pass' });
|
|
1790
|
+
} catch (e) {
|
|
1791
|
+
ok = false;
|
|
1792
|
+
checks.push({ ...check, status: 'fail', message: e.message });
|
|
1793
|
+
}
|
|
1794
|
+
} else {
|
|
1795
|
+
checks.push({ ...check, status: 'skipped', message: 'unknown verification check type' });
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return { ok, checks };
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function createCodexSessionId(operation = 'session') {
|
|
1802
|
+
const stamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '-').replace('Z', '');
|
|
1803
|
+
const random = crypto.randomBytes(4).toString('hex');
|
|
1804
|
+
return `${slugify(operation, 'session')}-${stamp}-${random}`;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function writeCodexSession(record) {
|
|
1808
|
+
if (!fs.existsSync(CODEX_SESSIONS_DIR)) {
|
|
1809
|
+
fs.mkdirSync(CODEX_SESSIONS_DIR, { recursive: true, mode: 0o700 });
|
|
1810
|
+
}
|
|
1811
|
+
const sessionId = record.sessionId || createCodexSessionId(record.operation || 'session');
|
|
1812
|
+
const sessionPath = path.join(CODEX_SESSIONS_DIR, `${sessionId}.json`);
|
|
1813
|
+
const payload = {
|
|
1814
|
+
schemaVersion: 1,
|
|
1815
|
+
sessionId,
|
|
1816
|
+
createdAt: new Date().toISOString(),
|
|
1817
|
+
cliVersion: CLI_VERSION,
|
|
1818
|
+
argv: process.argv.slice(2),
|
|
1819
|
+
...record,
|
|
1820
|
+
sessionId,
|
|
1821
|
+
};
|
|
1822
|
+
fs.writeFileSync(sessionPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
1823
|
+
return { sessionId, sessionPath };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function readCodexSessions() {
|
|
1827
|
+
try {
|
|
1828
|
+
if (!fs.existsSync(CODEX_SESSIONS_DIR)) return [];
|
|
1829
|
+
return fs.readdirSync(CODEX_SESSIONS_DIR)
|
|
1830
|
+
.filter(name => name.endsWith('.json'))
|
|
1831
|
+
.map(name => {
|
|
1832
|
+
const sessionPath = path.join(CODEX_SESSIONS_DIR, name);
|
|
1833
|
+
try {
|
|
1834
|
+
const parsed = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
|
|
1835
|
+
return { ...parsed, sessionPath };
|
|
1836
|
+
} catch {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
})
|
|
1840
|
+
.filter(Boolean)
|
|
1841
|
+
.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
|
|
1842
|
+
} catch {
|
|
1843
|
+
return [];
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1541
1847
|
function readCodexManifest() {
|
|
1542
1848
|
try {
|
|
1543
1849
|
const parsed = JSON.parse(fs.readFileSync(CODEX_MANIFEST_FILE, 'utf8'));
|
|
@@ -1546,7 +1852,15 @@ function readCodexManifest() {
|
|
|
1546
1852
|
return { schemaVersion: 1, installs: [] };
|
|
1547
1853
|
}
|
|
1548
1854
|
|
|
1549
|
-
function
|
|
1855
|
+
function writeCodexManifest(manifest) {
|
|
1856
|
+
if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
|
|
1857
|
+
fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
|
|
1858
|
+
}
|
|
1859
|
+
manifest.updatedAt = new Date().toISOString();
|
|
1860
|
+
fs.writeFileSync(CODEX_MANIFEST_FILE, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function writeCodexManifestRecord(plan, installedFiles, sessionInfo = {}, verification = null) {
|
|
1550
1864
|
if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
|
|
1551
1865
|
fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
|
|
1552
1866
|
}
|
|
@@ -1561,6 +1875,9 @@ function writeCodexManifestRecord(plan, installedFiles) {
|
|
|
1561
1875
|
installedAt,
|
|
1562
1876
|
contentHash: plan.contentHash || '',
|
|
1563
1877
|
agentMetadata: plan.agentMetadata || {},
|
|
1878
|
+
sessionId: sessionInfo.sessionId,
|
|
1879
|
+
sessionPath: sessionInfo.sessionPath,
|
|
1880
|
+
verification,
|
|
1564
1881
|
installedFiles: installedFiles.map(file => ({
|
|
1565
1882
|
path: file.path,
|
|
1566
1883
|
sourceName: file.sourceName,
|
|
@@ -1573,16 +1890,58 @@ function writeCodexManifestRecord(plan, installedFiles) {
|
|
|
1573
1890
|
manifest.installs = manifest.installs.filter(item => !(item.uuid === plan.uuid && item.targetTool === 'codex'));
|
|
1574
1891
|
manifest.installs.push(record);
|
|
1575
1892
|
manifest.updatedAt = installedAt;
|
|
1576
|
-
|
|
1893
|
+
writeCodexManifest(manifest);
|
|
1577
1894
|
return record;
|
|
1578
1895
|
}
|
|
1579
1896
|
|
|
1580
1897
|
function executeCodexInstallPlan(plan, opts = {}) {
|
|
1581
|
-
|
|
1582
|
-
if (
|
|
1898
|
+
const publicPlan = publicInstallPlan(plan);
|
|
1899
|
+
if (opts.dryRun) {
|
|
1900
|
+
const session = writeCodexSession({
|
|
1901
|
+
operation: 'install',
|
|
1902
|
+
status: 'dry_run',
|
|
1903
|
+
targetTool: 'codex',
|
|
1904
|
+
uuid: plan.uuid,
|
|
1905
|
+
title: plan.title,
|
|
1906
|
+
sourceUrl: plan.sourceUrl,
|
|
1907
|
+
policyDecision: publicPlan.policyDecision,
|
|
1908
|
+
plan: publicPlan,
|
|
1909
|
+
result: { dryRun: true, installedFiles: [] },
|
|
1910
|
+
});
|
|
1911
|
+
return { dryRun: true, plan: publicPlan, installedFiles: [], ...session };
|
|
1912
|
+
}
|
|
1913
|
+
if (plan.installMode === 'stage_only') {
|
|
1914
|
+
const result = executeStageOnlyCodexPlan(plan);
|
|
1915
|
+
const verification = runCodexPostVerify(result.plan, opts);
|
|
1916
|
+
const session = writeCodexSession({
|
|
1917
|
+
operation: 'install',
|
|
1918
|
+
status: 'stage_only',
|
|
1919
|
+
targetTool: 'codex',
|
|
1920
|
+
uuid: plan.uuid,
|
|
1921
|
+
title: plan.title,
|
|
1922
|
+
sourceUrl: plan.sourceUrl,
|
|
1923
|
+
policyDecision: result.plan.policyDecision,
|
|
1924
|
+
plan: result.plan,
|
|
1925
|
+
installedFiles: result.installedFiles,
|
|
1926
|
+
verification,
|
|
1927
|
+
result: { staged: true, stageOnly: true, stagePath: result.stagePath },
|
|
1928
|
+
});
|
|
1929
|
+
return { ...result, verification, ...session };
|
|
1930
|
+
}
|
|
1583
1931
|
if (opts.stage) {
|
|
1584
1932
|
const stagePath = stageCodexInstallPlan(plan);
|
|
1585
|
-
|
|
1933
|
+
const session = writeCodexSession({
|
|
1934
|
+
operation: 'install',
|
|
1935
|
+
status: 'staged',
|
|
1936
|
+
targetTool: 'codex',
|
|
1937
|
+
uuid: plan.uuid,
|
|
1938
|
+
title: plan.title,
|
|
1939
|
+
sourceUrl: plan.sourceUrl,
|
|
1940
|
+
policyDecision: publicPlan.policyDecision,
|
|
1941
|
+
plan: publicPlan,
|
|
1942
|
+
result: { staged: true, stagePath },
|
|
1943
|
+
});
|
|
1944
|
+
return { dryRun: true, staged: true, stagePath, plan: publicPlan, installedFiles: [], ...session };
|
|
1586
1945
|
}
|
|
1587
1946
|
|
|
1588
1947
|
const installedFiles = [];
|
|
@@ -1602,8 +1961,22 @@ function executeCodexInstallPlan(plan, opts = {}) {
|
|
|
1602
1961
|
});
|
|
1603
1962
|
}
|
|
1604
1963
|
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1964
|
+
const verification = runCodexPostVerify(publicPlan, opts);
|
|
1965
|
+
const session = writeCodexSession({
|
|
1966
|
+
operation: 'install',
|
|
1967
|
+
status: 'installed',
|
|
1968
|
+
targetTool: 'codex',
|
|
1969
|
+
uuid: plan.uuid,
|
|
1970
|
+
title: plan.title,
|
|
1971
|
+
sourceUrl: plan.sourceUrl,
|
|
1972
|
+
policyDecision: publicPlan.policyDecision,
|
|
1973
|
+
plan: publicPlan,
|
|
1974
|
+
installedFiles,
|
|
1975
|
+
verification,
|
|
1976
|
+
result: { installedFiles },
|
|
1977
|
+
});
|
|
1978
|
+
const manifestRecord = writeCodexManifestRecord(plan, installedFiles, session, verification);
|
|
1979
|
+
return { dryRun: false, plan: publicPlan, installedFiles, manifestRecord, verification, ...session };
|
|
1607
1980
|
}
|
|
1608
1981
|
|
|
1609
1982
|
async function installCodexAsset(workflow, contents, opts = {}) {
|
|
@@ -1631,6 +2004,7 @@ async function cmdInstall() {
|
|
|
1631
2004
|
dryRun: Boolean(args.flags.dryRun || args.flags.dry_run),
|
|
1632
2005
|
stage: Boolean(args.flags.stage),
|
|
1633
2006
|
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2007
|
+
verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
|
|
1634
2008
|
json: Boolean(args.flags.json),
|
|
1635
2009
|
manifest: Boolean(args.flags.manifest),
|
|
1636
2010
|
};
|
|
@@ -1807,7 +2181,8 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1807
2181
|
if (targetTool === 'codex') {
|
|
1808
2182
|
let result;
|
|
1809
2183
|
try {
|
|
1810
|
-
|
|
2184
|
+
const serverPlan = opts.serverPlan !== undefined ? opts.serverPlan : await fetchServerCodexInstallPlan(uuid, config, apiBase);
|
|
2185
|
+
result = await installCodexAsset(workflow, contents, { ...opts, serverPlan });
|
|
1811
2186
|
} catch (e) {
|
|
1812
2187
|
die(e.message);
|
|
1813
2188
|
}
|
|
@@ -1821,6 +2196,7 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1821
2196
|
} else {
|
|
1822
2197
|
info(`No Codex skill files were written. Re-run with --approve-mcp or --yes to install.`);
|
|
1823
2198
|
}
|
|
2199
|
+
if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
|
|
1824
2200
|
} else if (opts.dryRun) {
|
|
1825
2201
|
info(`Dry run: ${plan.files.length} file(s) would be installed to ${CODEX_SKILLS_DIR}`);
|
|
1826
2202
|
for (const file of plan.files) {
|
|
@@ -1828,6 +2204,7 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1828
2204
|
log(` ${C.dim}•${C.reset} ~/${rel}`);
|
|
1829
2205
|
if (file.riskFlags.length) log(` ${C.yellow}${file.riskFlags.join(', ')}${C.reset}`);
|
|
1830
2206
|
}
|
|
2207
|
+
if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
|
|
1831
2208
|
} else {
|
|
1832
2209
|
for (const file of result.installedFiles) {
|
|
1833
2210
|
const relPath = path.relative(os.homedir(), file.path);
|
|
@@ -1836,6 +2213,8 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1836
2213
|
log('');
|
|
1837
2214
|
success(`${result.installedFiles.length} file(s) installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1838
2215
|
log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
|
|
2216
|
+
if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
|
|
2217
|
+
if (result.verification && !result.verification.ok) log(` ${C.yellow}Verification: failed${C.reset}`);
|
|
1839
2218
|
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1840
2219
|
}
|
|
1841
2220
|
}
|
|
@@ -1851,6 +2230,9 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1851
2230
|
installedFiles: result.installedFiles || [],
|
|
1852
2231
|
plan: result.plan,
|
|
1853
2232
|
manifestPath: CODEX_MANIFEST_FILE,
|
|
2233
|
+
sessionId: result.sessionId,
|
|
2234
|
+
sessionPath: result.sessionPath,
|
|
2235
|
+
verification: result.verification,
|
|
1854
2236
|
};
|
|
1855
2237
|
}
|
|
1856
2238
|
|
|
@@ -2012,8 +2394,16 @@ async function cmdList() {
|
|
|
2012
2394
|
data = { ...data, list };
|
|
2013
2395
|
}
|
|
2014
2396
|
|
|
2397
|
+
const originalCount = (data.list || []).length;
|
|
2398
|
+
data = { ...data, list: applyAgentWorkflowFilters(data.list || [], args.flags) };
|
|
2399
|
+
const filters = {
|
|
2400
|
+
target: args.flags.target || undefined,
|
|
2401
|
+
kind: args.flags.kind || args.flags.assetKind || undefined,
|
|
2402
|
+
policy: args.flags.policy || undefined,
|
|
2403
|
+
};
|
|
2404
|
+
|
|
2015
2405
|
if (args.flags.json) {
|
|
2016
|
-
outputJson({ total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
|
|
2406
|
+
outputJson({ total: data.total || 0, fetched: originalCount, count: (data.list || []).length, filters, list: data.list || [] });
|
|
2017
2407
|
return;
|
|
2018
2408
|
}
|
|
2019
2409
|
|
|
@@ -2022,11 +2412,16 @@ async function cmdList() {
|
|
|
2022
2412
|
return;
|
|
2023
2413
|
}
|
|
2024
2414
|
|
|
2025
|
-
|
|
2415
|
+
const filterText = [filters.target ? `target=${filters.target}` : '', filters.kind ? `kind=${filters.kind}` : '', filters.policy ? `policy=${filters.policy}` : ''].filter(Boolean).join(' · ');
|
|
2416
|
+
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
2417
|
|
|
2027
2418
|
for (const wf of data.list) {
|
|
2028
2419
|
const views = wf.view_count || 0;
|
|
2029
2420
|
log(` ${C.cyan}${wf.uuid.substring(0,8)}${C.reset} ${C.bold}${wf.title}${C.reset}`);
|
|
2421
|
+
if (wf.compatibility?.codex) {
|
|
2422
|
+
const c = wf.compatibility.codex;
|
|
2423
|
+
log(` ${C.dim} codex=${c.status} · policy=${c.policyDecision.decision} · kind=${c.assetKind || 'unknown'}${C.reset}`);
|
|
2424
|
+
}
|
|
2030
2425
|
log(` ${C.dim} ${views} views · https://tokrepo.com/en/workflows/${wf.uuid}${C.reset}\n`);
|
|
2031
2426
|
}
|
|
2032
2427
|
} catch (e) {
|
|
@@ -2236,13 +2631,16 @@ async function cmdClone() {
|
|
|
2236
2631
|
const assetType = getWorkflowAssetType(workflow);
|
|
2237
2632
|
const contents = extractInstallableContents(workflow, assetType);
|
|
2238
2633
|
if (contents.length === 0) throw new Error('No installable content found');
|
|
2634
|
+
const serverPlan = await fetchServerCodexInstallPlan(workflow.uuid, config, apiBase);
|
|
2239
2635
|
const result = await installCodexAsset(workflow, contents, {
|
|
2240
2636
|
...args.flags,
|
|
2241
2637
|
dryRun,
|
|
2242
2638
|
stage: Boolean(args.flags.stage),
|
|
2243
2639
|
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2640
|
+
verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
|
|
2244
2641
|
json: true,
|
|
2245
2642
|
throwOnError: true,
|
|
2643
|
+
serverPlan,
|
|
2246
2644
|
});
|
|
2247
2645
|
if (!dryRun) installedCount += result.installedFiles.length;
|
|
2248
2646
|
results.push({
|
|
@@ -2255,6 +2653,9 @@ async function cmdClone() {
|
|
|
2255
2653
|
files: result.plan.files,
|
|
2256
2654
|
installedFiles: result.installedFiles || [],
|
|
2257
2655
|
risks: result.plan.risks,
|
|
2656
|
+
sessionId: result.sessionId,
|
|
2657
|
+
sessionPath: result.sessionPath,
|
|
2658
|
+
verification: result.verification,
|
|
2258
2659
|
});
|
|
2259
2660
|
if (!json) {
|
|
2260
2661
|
const fileCount = (dryRun || args.flags.stage) ? result.plan.files.length : result.installedFiles.length;
|
|
@@ -2383,6 +2784,15 @@ async function fetchWorkflowForInstall(uuid, config, apiBase) {
|
|
|
2383
2784
|
return { workflow, contents };
|
|
2384
2785
|
}
|
|
2385
2786
|
|
|
2787
|
+
async function fetchServerCodexInstallPlan(uuid, config, apiBase) {
|
|
2788
|
+
try {
|
|
2789
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/install-plan?uuid=${encodeURIComponent(uuid)}&target=codex`, null, config?.token, apiBase);
|
|
2790
|
+
return data?.plan || data || null;
|
|
2791
|
+
} catch {
|
|
2792
|
+
return null;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2386
2796
|
async function cmdSyncInstalled() {
|
|
2387
2797
|
const args = parseArgs(process.argv);
|
|
2388
2798
|
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
@@ -2417,7 +2827,8 @@ async function cmdSyncInstalled() {
|
|
|
2417
2827
|
|
|
2418
2828
|
try {
|
|
2419
2829
|
const { workflow, contents } = await fetchWorkflowForInstall(uuid, config, apiBase);
|
|
2420
|
-
const
|
|
2830
|
+
const serverPlan = await fetchServerCodexInstallPlan(uuid, config, apiBase);
|
|
2831
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
|
|
2421
2832
|
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2422
2833
|
const shouldWrite = force || diff.needsUpdate;
|
|
2423
2834
|
|
|
@@ -2455,8 +2866,10 @@ async function cmdSyncInstalled() {
|
|
|
2455
2866
|
stage,
|
|
2456
2867
|
installMode: record.installMode || record.install_mode,
|
|
2457
2868
|
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2869
|
+
verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
|
|
2458
2870
|
json: true,
|
|
2459
2871
|
throwOnError: true,
|
|
2872
|
+
serverPlan,
|
|
2460
2873
|
});
|
|
2461
2874
|
|
|
2462
2875
|
results.push({
|
|
@@ -2468,6 +2881,9 @@ async function cmdSyncInstalled() {
|
|
|
2468
2881
|
stagePath: installResult.stagePath,
|
|
2469
2882
|
installedFiles: installResult.installedFiles || [],
|
|
2470
2883
|
plan: installResult.plan,
|
|
2884
|
+
sessionId: installResult.sessionId,
|
|
2885
|
+
sessionPath: installResult.sessionPath,
|
|
2886
|
+
verification: installResult.verification,
|
|
2471
2887
|
});
|
|
2472
2888
|
if (!json) success(stage ? `Staged ${installResult.plan.files.length} file(s)` : `Updated ${(installResult.installedFiles || []).length} file(s)`);
|
|
2473
2889
|
} catch (e) {
|
|
@@ -2526,6 +2942,8 @@ async function cmdInstalled() {
|
|
|
2526
2942
|
installMode: record.installMode || record.install_mode,
|
|
2527
2943
|
installedAt: record.installedAt || record.installed_at,
|
|
2528
2944
|
contentHash: record.contentHash || record.content_hash || '',
|
|
2945
|
+
sessionId: record.sessionId || record.session_id,
|
|
2946
|
+
sessionPath: record.sessionPath || record.session_path,
|
|
2529
2947
|
risks: record.risks || [],
|
|
2530
2948
|
files,
|
|
2531
2949
|
status: files.some(file => !file.exists) ? 'missing-files' : files.some(file => file.changed) ? 'local-changes' : 'installed',
|
|
@@ -2549,6 +2967,281 @@ async function cmdInstalled() {
|
|
|
2549
2967
|
}
|
|
2550
2968
|
}
|
|
2551
2969
|
|
|
2970
|
+
function isCodexManagedPath(filePath) {
|
|
2971
|
+
const resolved = path.resolve(expandHomePath(filePath));
|
|
2972
|
+
return ensureInside(CODEX_SKILLS_DIR, resolved) || ensureInside(path.join(CODEX_TOKREPO_DIR, 'staged'), resolved);
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
function removeEmptyCodexDirs(startDir) {
|
|
2976
|
+
const roots = [CODEX_SKILLS_DIR, path.join(CODEX_TOKREPO_DIR, 'staged')].map(root => path.resolve(root));
|
|
2977
|
+
let dir = path.resolve(startDir);
|
|
2978
|
+
const root = roots.find(candidate => dir === candidate || dir.startsWith(candidate + path.sep));
|
|
2979
|
+
if (!root) return;
|
|
2980
|
+
while (dir !== root && dir.startsWith(root + path.sep)) {
|
|
2981
|
+
try {
|
|
2982
|
+
if (!fs.existsSync(dir) || fs.readdirSync(dir).length > 0) break;
|
|
2983
|
+
fs.rmdirSync(dir);
|
|
2984
|
+
} catch {
|
|
2985
|
+
break;
|
|
2986
|
+
}
|
|
2987
|
+
dir = path.dirname(dir);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function findCodexManifestRecord(selector) {
|
|
2992
|
+
const manifest = readCodexManifest();
|
|
2993
|
+
const records = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
|
|
2994
|
+
const needle = String(selector || '').trim();
|
|
2995
|
+
if (!needle) return null;
|
|
2996
|
+
const lower = needle.toLowerCase();
|
|
2997
|
+
const exact = records.find(record => String(record.uuid || '').toLowerCase() === lower);
|
|
2998
|
+
if (exact) return exact;
|
|
2999
|
+
|
|
3000
|
+
const prefixMatches = /^[a-f0-9-]{8,}$/i.test(needle)
|
|
3001
|
+
? records.filter(record => String(record.uuid || '').toLowerCase().startsWith(lower))
|
|
3002
|
+
: [];
|
|
3003
|
+
if (prefixMatches.length === 1) return prefixMatches[0];
|
|
3004
|
+
if (prefixMatches.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the full UUID.`);
|
|
3005
|
+
|
|
3006
|
+
const slugNeedle = slugify(needle, '');
|
|
3007
|
+
const titleMatches = records.filter(record => {
|
|
3008
|
+
const title = String(record.title || '').toLowerCase();
|
|
3009
|
+
const sourceUrl = String(record.sourceUrl || record.source_url || '').toLowerCase();
|
|
3010
|
+
return title === lower || slugify(record.title || '', '') === slugNeedle || sourceUrl.includes(lower);
|
|
3011
|
+
});
|
|
3012
|
+
if (titleMatches.length === 1) return titleMatches[0];
|
|
3013
|
+
if (titleMatches.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the UUID.`);
|
|
3014
|
+
|
|
3015
|
+
const fuzzy = records.filter(record => String(record.title || '').toLowerCase().includes(lower));
|
|
3016
|
+
if (fuzzy.length === 1) return fuzzy[0];
|
|
3017
|
+
if (fuzzy.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the UUID.`);
|
|
3018
|
+
return null;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
function buildCodexRemovalPlan(record, files, opts = {}) {
|
|
3022
|
+
const actions = (files || []).map(file => {
|
|
3023
|
+
const filePath = path.resolve(expandHomePath(file.path));
|
|
3024
|
+
const exists = fs.existsSync(filePath);
|
|
3025
|
+
const actualSha = exists ? currentFileSha(filePath) : '';
|
|
3026
|
+
const expectedSha = file.sha256 || '';
|
|
3027
|
+
const changed = Boolean(exists && expectedSha && actualSha !== expectedSha);
|
|
3028
|
+
const managed = isCodexManagedPath(filePath);
|
|
3029
|
+
const allowed = managed && (!changed || opts.force);
|
|
3030
|
+
const reason = !managed ? 'outside-managed-roots'
|
|
3031
|
+
: changed && !opts.force ? 'local-changes'
|
|
3032
|
+
: exists ? 'remove'
|
|
3033
|
+
: 'already-missing';
|
|
3034
|
+
return {
|
|
3035
|
+
type: 'remove_file',
|
|
3036
|
+
path: filePath,
|
|
3037
|
+
sourceName: file.sourceName || file.source_name,
|
|
3038
|
+
expectedSha,
|
|
3039
|
+
actualSha,
|
|
3040
|
+
exists,
|
|
3041
|
+
changed,
|
|
3042
|
+
allowed,
|
|
3043
|
+
reason,
|
|
3044
|
+
};
|
|
3045
|
+
});
|
|
3046
|
+
return {
|
|
3047
|
+
schemaVersion: 1,
|
|
3048
|
+
operation: opts.operation || 'uninstall',
|
|
3049
|
+
targetTool: 'codex',
|
|
3050
|
+
uuid: record.uuid,
|
|
3051
|
+
title: record.title,
|
|
3052
|
+
sourceUrl: record.sourceUrl || record.source_url,
|
|
3053
|
+
manifestPath: CODEX_MANIFEST_FILE,
|
|
3054
|
+
force: Boolean(opts.force),
|
|
3055
|
+
dryRun: Boolean(opts.dryRun),
|
|
3056
|
+
requiresConfirmation: actions.some(action => !action.allowed),
|
|
3057
|
+
actions,
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
function executeCodexRemovalPlan(plan, opts = {}) {
|
|
3062
|
+
const blocked = plan.actions.filter(action => !action.allowed);
|
|
3063
|
+
if (blocked.length > 0) {
|
|
3064
|
+
const first = blocked[0];
|
|
3065
|
+
throw new Error(`Refusing to remove ${first.path}: ${first.reason}. Use --force only if you want to remove local changes.`);
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
const removedFiles = [];
|
|
3069
|
+
const skippedFiles = [];
|
|
3070
|
+
for (const action of plan.actions) {
|
|
3071
|
+
if (!action.exists) {
|
|
3072
|
+
skippedFiles.push({ path: action.path, reason: 'already-missing' });
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
fs.unlinkSync(action.path);
|
|
3076
|
+
removedFiles.push({ path: action.path, sha256: action.actualSha || action.expectedSha });
|
|
3077
|
+
removeEmptyCodexDirs(path.dirname(action.path));
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
const session = writeCodexSession({
|
|
3081
|
+
operation: plan.operation,
|
|
3082
|
+
status: plan.operation === 'rollback' ? 'rolled_back' : 'uninstalled',
|
|
3083
|
+
targetTool: 'codex',
|
|
3084
|
+
uuid: plan.uuid,
|
|
3085
|
+
title: plan.title,
|
|
3086
|
+
sourceUrl: plan.sourceUrl,
|
|
3087
|
+
plan,
|
|
3088
|
+
result: { removedFiles, skippedFiles },
|
|
3089
|
+
});
|
|
3090
|
+
|
|
3091
|
+
if (opts.removeManifest !== false && plan.uuid) {
|
|
3092
|
+
const manifest = readCodexManifest();
|
|
3093
|
+
manifest.installs = (manifest.installs || []).filter(item => !((item.targetTool || item.target_tool) === 'codex' && item.uuid === plan.uuid));
|
|
3094
|
+
writeCodexManifest(manifest);
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
return { dryRun: false, plan, removedFiles, skippedFiles, ...session };
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
async function cmdUninstall() {
|
|
3101
|
+
const args = parseArgs(process.argv);
|
|
3102
|
+
const target = args.positional[0];
|
|
3103
|
+
if (!target) {
|
|
3104
|
+
showUninstallHelp();
|
|
3105
|
+
process.exit(1);
|
|
3106
|
+
}
|
|
3107
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
3108
|
+
if (targetTool !== 'codex') error(`uninstall currently supports --target codex only`);
|
|
3109
|
+
|
|
3110
|
+
const json = Boolean(args.flags.json);
|
|
3111
|
+
const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
|
|
3112
|
+
const force = Boolean(args.flags.force);
|
|
3113
|
+
if (!json) log(`\n${C.bold}tokrepo uninstall${C.reset}\n`);
|
|
3114
|
+
|
|
3115
|
+
try {
|
|
3116
|
+
const record = findCodexManifestRecord(target);
|
|
3117
|
+
if (!record) error(`No installed Codex asset found for "${target}". Run: tokrepo installed --target codex`);
|
|
3118
|
+
const files = record.installedFiles || record.installed_files || [];
|
|
3119
|
+
const plan = buildCodexRemovalPlan(record, files, { operation: 'uninstall', dryRun, force });
|
|
3120
|
+
if (dryRun) {
|
|
3121
|
+
const session = writeCodexSession({
|
|
3122
|
+
operation: 'uninstall',
|
|
3123
|
+
status: 'dry_run',
|
|
3124
|
+
targetTool: 'codex',
|
|
3125
|
+
uuid: record.uuid,
|
|
3126
|
+
title: record.title,
|
|
3127
|
+
sourceUrl: record.sourceUrl || record.source_url,
|
|
3128
|
+
plan,
|
|
3129
|
+
result: { dryRun: true },
|
|
3130
|
+
});
|
|
3131
|
+
const response = { dryRun: true, plan, removedFiles: [], ...session };
|
|
3132
|
+
if (json) outputJson(response);
|
|
3133
|
+
else {
|
|
3134
|
+
info(`Dry run: ${plan.actions.length} file(s) would be removed`);
|
|
3135
|
+
for (const action of plan.actions) {
|
|
3136
|
+
const rel = path.relative(os.homedir(), action.path);
|
|
3137
|
+
log(` ${action.allowed ? C.dim : C.yellow}•${C.reset} ~/${rel} ${C.dim}${action.reason}${C.reset}`);
|
|
3138
|
+
}
|
|
3139
|
+
log(` ${C.dim}Session: ${session.sessionPath}${C.reset}`);
|
|
3140
|
+
}
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
const result = executeCodexRemovalPlan(plan, { force });
|
|
3145
|
+
if (json) outputJson(result);
|
|
3146
|
+
else {
|
|
3147
|
+
for (const file of result.removedFiles) {
|
|
3148
|
+
success(`Removed: ~/${path.relative(os.homedir(), file.path)}`);
|
|
3149
|
+
}
|
|
3150
|
+
success(`Uninstalled ${record.title || record.uuid}`);
|
|
3151
|
+
log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
|
|
3152
|
+
log(` ${C.dim}Session: ${result.sessionPath}${C.reset}\n`);
|
|
3153
|
+
}
|
|
3154
|
+
} catch (e) {
|
|
3155
|
+
error(`Uninstall failed: ${e.message}`);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
function findRollbackSession(selector) {
|
|
3160
|
+
const sessions = readCodexSessions();
|
|
3161
|
+
if (selector === 'last') {
|
|
3162
|
+
return [...sessions].reverse().find(session => (
|
|
3163
|
+
session.operation === 'install'
|
|
3164
|
+
&& ['installed', 'staged', 'stage_only'].includes(session.status)
|
|
3165
|
+
&& (session.installedFiles?.length || session.result?.stagePath || session.plan?.rollback?.length)
|
|
3166
|
+
));
|
|
3167
|
+
}
|
|
3168
|
+
const needle = String(selector || '').trim();
|
|
3169
|
+
if (!needle) return null;
|
|
3170
|
+
return sessions.find(session => session.sessionId === needle || String(session.sessionId || '').startsWith(needle));
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
function filesFromRollbackSession(session) {
|
|
3174
|
+
if (!session) return [];
|
|
3175
|
+
if (session.status === 'staged' && session.result?.stagePath) {
|
|
3176
|
+
return [{ path: session.result.stagePath, sha256: currentFileSha(session.result.stagePath), sourceName: 'install-plan.json' }];
|
|
3177
|
+
}
|
|
3178
|
+
if (Array.isArray(session.installedFiles) && session.installedFiles.length > 0) return session.installedFiles;
|
|
3179
|
+
return (session.plan?.rollback || [])
|
|
3180
|
+
.filter(action => action.type === 'remove_file' && action.path)
|
|
3181
|
+
.map(action => ({ path: action.path, sha256: action.sha256 || '', sourceName: path.basename(action.path) }));
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
async function cmdRollback() {
|
|
3185
|
+
const args = parseArgs(process.argv);
|
|
3186
|
+
const selector = args.flags.last ? 'last' : (args.flags.session || args.positional[0]);
|
|
3187
|
+
if (!selector) {
|
|
3188
|
+
showRollbackHelp();
|
|
3189
|
+
process.exit(1);
|
|
3190
|
+
}
|
|
3191
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
3192
|
+
if (targetTool !== 'codex') error(`rollback currently supports --target codex only`);
|
|
3193
|
+
|
|
3194
|
+
const json = Boolean(args.flags.json);
|
|
3195
|
+
const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
|
|
3196
|
+
const force = Boolean(args.flags.force);
|
|
3197
|
+
if (!json) log(`\n${C.bold}tokrepo rollback${C.reset}\n`);
|
|
3198
|
+
|
|
3199
|
+
try {
|
|
3200
|
+
const session = findRollbackSession(selector);
|
|
3201
|
+
if (!session) error(`No rollback session found for "${selector}". Run: tokrepo installed --target codex --json`);
|
|
3202
|
+
const files = filesFromRollbackSession(session);
|
|
3203
|
+
const plan = buildCodexRemovalPlan(session, files, { operation: 'rollback', dryRun, force });
|
|
3204
|
+
plan.rollbackSessionId = session.sessionId;
|
|
3205
|
+
plan.rollbackSessionPath = session.sessionPath;
|
|
3206
|
+
|
|
3207
|
+
if (dryRun) {
|
|
3208
|
+
const audit = writeCodexSession({
|
|
3209
|
+
operation: 'rollback',
|
|
3210
|
+
status: 'dry_run',
|
|
3211
|
+
targetTool: 'codex',
|
|
3212
|
+
uuid: session.uuid,
|
|
3213
|
+
title: session.title,
|
|
3214
|
+
sourceUrl: session.sourceUrl,
|
|
3215
|
+
plan,
|
|
3216
|
+
result: { dryRun: true },
|
|
3217
|
+
});
|
|
3218
|
+
const response = { dryRun: true, plan, removedFiles: [], ...audit };
|
|
3219
|
+
if (json) outputJson(response);
|
|
3220
|
+
else {
|
|
3221
|
+
info(`Dry run: rollback ${session.sessionId} would remove ${plan.actions.length} file(s)`);
|
|
3222
|
+
for (const action of plan.actions) {
|
|
3223
|
+
const rel = path.relative(os.homedir(), action.path);
|
|
3224
|
+
log(` ${action.allowed ? C.dim : C.yellow}•${C.reset} ~/${rel} ${C.dim}${action.reason}${C.reset}`);
|
|
3225
|
+
}
|
|
3226
|
+
log(` ${C.dim}Session: ${audit.sessionPath}${C.reset}`);
|
|
3227
|
+
}
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
const result = executeCodexRemovalPlan(plan, { force, removeManifest: Boolean(session.uuid) });
|
|
3232
|
+
if (json) outputJson(result);
|
|
3233
|
+
else {
|
|
3234
|
+
for (const file of result.removedFiles) {
|
|
3235
|
+
success(`Removed: ~/${path.relative(os.homedir(), file.path)}`);
|
|
3236
|
+
}
|
|
3237
|
+
success(`Rolled back ${session.sessionId}`);
|
|
3238
|
+
log(` ${C.dim}Session: ${result.sessionPath}${C.reset}\n`);
|
|
3239
|
+
}
|
|
3240
|
+
} catch (e) {
|
|
3241
|
+
error(`Rollback failed: ${e.message}`);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
2552
3245
|
async function cmdOutdated() {
|
|
2553
3246
|
const args = parseArgs(process.argv);
|
|
2554
3247
|
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
@@ -2574,7 +3267,8 @@ async function cmdOutdated() {
|
|
|
2574
3267
|
for (const record of installed) {
|
|
2575
3268
|
try {
|
|
2576
3269
|
const { workflow, contents } = await fetchWorkflowForInstall(record.uuid, config, apiBase);
|
|
2577
|
-
const
|
|
3270
|
+
const serverPlan = await fetchServerCodexInstallPlan(record.uuid, config, apiBase);
|
|
3271
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
|
|
2578
3272
|
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2579
3273
|
if (diff.needsUpdate) {
|
|
2580
3274
|
list.push({
|
|
@@ -2743,6 +3437,8 @@ ${C.bold}DISCOVER & INSTALL${C.reset}
|
|
|
2743
3437
|
${C.cyan}installed${C.reset} List installed Codex assets from manifest
|
|
2744
3438
|
${C.cyan}outdated${C.reset} Check installed Codex assets for updates
|
|
2745
3439
|
${C.cyan}sync-installed${C.reset} Update installed Codex assets from manifest
|
|
3440
|
+
${C.cyan}uninstall${C.reset} <uuid> Remove a managed Codex install
|
|
3441
|
+
${C.cyan}rollback${C.reset} --last Roll back the latest Codex install session
|
|
2746
3442
|
|
|
2747
3443
|
${C.bold}PUBLISH${C.reset}
|
|
2748
3444
|
${C.cyan}push${C.reset} [files...] Push files/directory (idempotent upsert)
|
|
@@ -2780,7 +3476,7 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
|
2780
3476
|
|
|
2781
3477
|
${C.bold}EXAMPLES${C.reset}
|
|
2782
3478
|
tokrepo search "mcp server" # Find MCP configs
|
|
2783
|
-
tokrepo search video --
|
|
3479
|
+
tokrepo search video --target codex --kind skill --policy allow --json
|
|
2784
3480
|
tokrepo detail ca000374-f5d8-... --json # Machine-readable detail
|
|
2785
3481
|
tokrepo plan 91aeb22d-eff0-4310-... # Install plan v2 for agents
|
|
2786
3482
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
@@ -2791,6 +3487,8 @@ ${C.bold}EXAMPLES${C.reset}
|
|
|
2791
3487
|
tokrepo outdated --target codex --json
|
|
2792
3488
|
tokrepo update --target codex --all
|
|
2793
3489
|
tokrepo sync-installed --target codex --dry-run
|
|
3490
|
+
tokrepo uninstall 91aeb22d --target codex --dry-run
|
|
3491
|
+
tokrepo rollback --last --target codex --dry-run
|
|
2794
3492
|
tokrepo push --private my-rules.md # Save one file privately
|
|
2795
3493
|
tokrepo push . --kind skill --target codex --install-mode bundle
|
|
2796
3494
|
tokrepo push --public skill.md # Share one file publicly
|
|
@@ -2818,11 +3516,12 @@ function showSearchHelp() {
|
|
|
2818
3516
|
${C.bold}tokrepo search${C.reset}
|
|
2819
3517
|
|
|
2820
3518
|
USAGE
|
|
2821
|
-
tokrepo search <query> [--json] [--all] [--page-size N] [--sort-by views|latest|stars|popular]
|
|
3519
|
+
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
3520
|
|
|
2823
3521
|
EXAMPLES
|
|
2824
3522
|
tokrepo search video
|
|
2825
3523
|
tokrepo search video --json
|
|
3524
|
+
tokrepo search video --target codex --kind skill --policy allow --json
|
|
2826
3525
|
tokrepo search "mcp server" --json --all
|
|
2827
3526
|
`);
|
|
2828
3527
|
}
|
|
@@ -2845,7 +3544,7 @@ function showInstallHelp() {
|
|
|
2845
3544
|
${C.bold}tokrepo install${C.reset}
|
|
2846
3545
|
|
|
2847
3546
|
USAGE
|
|
2848
|
-
tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--json]
|
|
3547
|
+
tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--verify-commands] [--json]
|
|
2849
3548
|
|
|
2850
3549
|
TARGETS
|
|
2851
3550
|
codex Write Codex skills to ~/.codex/skills/<asset-slug>/SKILL.md
|
|
@@ -2884,11 +3583,11 @@ function showListHelp() {
|
|
|
2884
3583
|
${C.bold}tokrepo list${C.reset}
|
|
2885
3584
|
|
|
2886
3585
|
USAGE
|
|
2887
|
-
tokrepo list [--json] [--all] [--page-size N]
|
|
3586
|
+
tokrepo list [--json] [--all] [--target codex] [--kind skill] [--policy allow] [--page-size N]
|
|
2888
3587
|
|
|
2889
3588
|
EXAMPLES
|
|
2890
3589
|
tokrepo list
|
|
2891
|
-
tokrepo list --json --all
|
|
3590
|
+
tokrepo list --json --all --target codex
|
|
2892
3591
|
`);
|
|
2893
3592
|
}
|
|
2894
3593
|
|
|
@@ -2931,6 +3630,42 @@ EXAMPLES
|
|
|
2931
3630
|
`);
|
|
2932
3631
|
}
|
|
2933
3632
|
|
|
3633
|
+
function showUninstallHelp() {
|
|
3634
|
+
log(`
|
|
3635
|
+
${C.bold}tokrepo uninstall${C.reset}
|
|
3636
|
+
|
|
3637
|
+
USAGE
|
|
3638
|
+
tokrepo uninstall <uuid|uuid-prefix|title> --target codex [--dry-run] [--force] [--json]
|
|
3639
|
+
|
|
3640
|
+
BEHAVIOR
|
|
3641
|
+
Removes only files recorded in ~/.codex/tokrepo/install-manifest.json and only
|
|
3642
|
+
under ~/.codex/skills or ~/.codex/tokrepo/staged. Local changes are blocked
|
|
3643
|
+
unless --force is provided.
|
|
3644
|
+
|
|
3645
|
+
EXAMPLES
|
|
3646
|
+
tokrepo uninstall 91aeb22d --target codex --dry-run --json
|
|
3647
|
+
tokrepo uninstall 91aeb22d-eff0-4310-abc6-811d2394b420 --target codex
|
|
3648
|
+
`);
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
function showRollbackHelp() {
|
|
3652
|
+
log(`
|
|
3653
|
+
${C.bold}tokrepo rollback${C.reset}
|
|
3654
|
+
|
|
3655
|
+
USAGE
|
|
3656
|
+
tokrepo rollback --last --target codex [--dry-run] [--force] [--json]
|
|
3657
|
+
tokrepo rollback <session-id> --target codex [--dry-run] [--force] [--json]
|
|
3658
|
+
|
|
3659
|
+
BEHAVIOR
|
|
3660
|
+
Replays the rollback section from ~/.codex/tokrepo/sessions/<session-id>.json.
|
|
3661
|
+
Local changes are blocked unless --force is provided.
|
|
3662
|
+
|
|
3663
|
+
EXAMPLES
|
|
3664
|
+
tokrepo rollback --last --target codex --dry-run --json
|
|
3665
|
+
tokrepo rollback install-20260506-120000-abc123 --target codex
|
|
3666
|
+
`);
|
|
3667
|
+
}
|
|
3668
|
+
|
|
2934
3669
|
function showCommandHelp(command) {
|
|
2935
3670
|
switch (command) {
|
|
2936
3671
|
case 'search':
|
|
@@ -2952,6 +3687,12 @@ function showCommandHelp(command) {
|
|
|
2952
3687
|
case 'installed':
|
|
2953
3688
|
case 'outdated':
|
|
2954
3689
|
showSyncInstalledHelp(); break;
|
|
3690
|
+
case 'uninstall':
|
|
3691
|
+
case 'remove':
|
|
3692
|
+
case 'rm':
|
|
3693
|
+
showUninstallHelp(); break;
|
|
3694
|
+
case 'rollback':
|
|
3695
|
+
showRollbackHelp(); break;
|
|
2955
3696
|
default:
|
|
2956
3697
|
showHelp(); break;
|
|
2957
3698
|
}
|
|
@@ -2982,6 +3723,8 @@ async function main() {
|
|
|
2982
3723
|
case 'delete': await cmdDelete(); break;
|
|
2983
3724
|
case 'clone': await cmdClone(); break;
|
|
2984
3725
|
case 'installed': await cmdInstalled(); break;
|
|
3726
|
+
case 'uninstall': case 'remove': case 'rm': await cmdUninstall(); break;
|
|
3727
|
+
case 'rollback': await cmdRollback(); break;
|
|
2985
3728
|
case 'outdated': await cmdOutdated(); break;
|
|
2986
3729
|
case 'sync-installed': case 'sync': await cmdSyncInstalled(); break;
|
|
2987
3730
|
case 'tags': await cmdTags(); break;
|