tokrepo 3.5.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 +986 -31
- 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') {
|
|
@@ -1278,8 +1338,222 @@ function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
|
1278
1338
|
return plan;
|
|
1279
1339
|
}
|
|
1280
1340
|
|
|
1341
|
+
function metadataValue(metadata, snakeName, camelName, fallback) {
|
|
1342
|
+
if (!metadata) return fallback;
|
|
1343
|
+
if (metadata[snakeName] !== undefined) return metadata[snakeName];
|
|
1344
|
+
if (metadata[camelName] !== undefined) return metadata[camelName];
|
|
1345
|
+
return fallback;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function normalizeToolName(value) {
|
|
1349
|
+
return String(value || '').trim().toLowerCase().replace(/-/g, '_');
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function riskProfileFromFlags(flags = []) {
|
|
1353
|
+
const set = new Set(flags || []);
|
|
1354
|
+
return {
|
|
1355
|
+
executes_code: set.has('executable'),
|
|
1356
|
+
modifies_global_config: set.has('mcp'),
|
|
1357
|
+
requires_secrets: set.has('env') ? ['ENV'] : [],
|
|
1358
|
+
uses_absolute_paths: set.has('absolute-path'),
|
|
1359
|
+
network_access: set.has('network'),
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function mergedPlanRiskProfile(plan) {
|
|
1364
|
+
const metadata = plan.agentMetadata || {};
|
|
1365
|
+
const rp = metadataValue(metadata, 'risk_profile', 'riskProfile', {}) || {};
|
|
1366
|
+
const flags = new Set(plan.risks || []);
|
|
1367
|
+
return {
|
|
1368
|
+
executes_code: Boolean(rp.executes_code || rp.executesCode || flags.has('executable')),
|
|
1369
|
+
modifies_global_config: Boolean(rp.modifies_global_config || rp.modifiesGlobalConfig || flags.has('mcp')),
|
|
1370
|
+
requires_secrets: rp.requires_secrets || rp.requiresSecrets || (flags.has('env') ? ['ENV'] : []),
|
|
1371
|
+
uses_absolute_paths: Boolean(rp.uses_absolute_paths || rp.usesAbsolutePaths || flags.has('absolute-path')),
|
|
1372
|
+
network_access: Boolean(rp.network_access || rp.networkAccess || flags.has('network')),
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
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
|
+
|
|
1402
|
+
function decideCodexPolicy(plan) {
|
|
1403
|
+
const serverPolicy = policyDecisionFromServerPlan(plan);
|
|
1404
|
+
if (serverPolicy) return serverPolicy;
|
|
1405
|
+
|
|
1406
|
+
const metadata = plan.agentMetadata || {};
|
|
1407
|
+
const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
|
|
1408
|
+
const assetKind = normalizeToolName(metadataValue(metadata, 'asset_kind', 'assetKind', ''));
|
|
1409
|
+
const risk = mergedPlanRiskProfile(plan);
|
|
1410
|
+
let decision = 'allow';
|
|
1411
|
+
const reasons = [];
|
|
1412
|
+
const raise = (next) => {
|
|
1413
|
+
const rank = { allow: 0, confirm: 1, stage_only: 2, deny: 3 };
|
|
1414
|
+
if ((rank[next] || 0) > (rank[decision] || 0)) decision = next;
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
if (targetTools.length && !targetTools.map(normalizeToolName).includes('codex')) {
|
|
1418
|
+
raise('confirm');
|
|
1419
|
+
reasons.push('metadata target_tools does not include codex');
|
|
1420
|
+
}
|
|
1421
|
+
if (['script', 'cli_tool', 'mcp_config'].includes(assetKind)) {
|
|
1422
|
+
raise('stage_only');
|
|
1423
|
+
reasons.push(`asset_kind ${assetKind} is not activated directly for Codex`);
|
|
1424
|
+
}
|
|
1425
|
+
if (plan.installMode === 'stage_only') {
|
|
1426
|
+
raise('stage_only');
|
|
1427
|
+
reasons.push('install_mode is stage_only');
|
|
1428
|
+
}
|
|
1429
|
+
if (risk.executes_code) {
|
|
1430
|
+
raise('stage_only');
|
|
1431
|
+
reasons.push('risk_profile.executes_code is true');
|
|
1432
|
+
}
|
|
1433
|
+
if (risk.modifies_global_config) {
|
|
1434
|
+
raise('stage_only');
|
|
1435
|
+
reasons.push('risk_profile.modifies_global_config is true');
|
|
1436
|
+
}
|
|
1437
|
+
if ((risk.requires_secrets || []).length) {
|
|
1438
|
+
raise('stage_only');
|
|
1439
|
+
reasons.push('risk_profile.requires_secrets is not empty');
|
|
1440
|
+
}
|
|
1441
|
+
if (risk.uses_absolute_paths) {
|
|
1442
|
+
raise('confirm');
|
|
1443
|
+
reasons.push('risk_profile.uses_absolute_paths is true');
|
|
1444
|
+
}
|
|
1445
|
+
if (risk.network_access) {
|
|
1446
|
+
raise('confirm');
|
|
1447
|
+
reasons.push('risk_profile.network_access is true');
|
|
1448
|
+
}
|
|
1449
|
+
if (reasons.length === 0) reasons.push('safe markdown-only Codex install');
|
|
1450
|
+
|
|
1451
|
+
return {
|
|
1452
|
+
decision,
|
|
1453
|
+
requiresConfirmation: decision === 'confirm',
|
|
1454
|
+
reasons: Array.from(new Set(reasons)),
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
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
|
+
|
|
1462
|
+
const stage = plan.installMode === 'stage_only';
|
|
1463
|
+
return plan.files.map(file => ({
|
|
1464
|
+
type: stage ? 'stage_file' : 'write_file',
|
|
1465
|
+
path: file.path,
|
|
1466
|
+
sourceName: file.sourceName,
|
|
1467
|
+
sha256: file.sha256,
|
|
1468
|
+
bytes: file.bytes,
|
|
1469
|
+
ifExists: 'overwrite',
|
|
1470
|
+
entrypoint: path.basename(file.path).toLowerCase() === 'skill.md',
|
|
1471
|
+
risk: riskProfileFromFlags(file.riskFlags || []),
|
|
1472
|
+
}));
|
|
1473
|
+
}
|
|
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
|
+
|
|
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
|
+
|
|
1492
|
+
const metadata = plan.agentMetadata || {};
|
|
1493
|
+
const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
|
|
1494
|
+
const out = [
|
|
1495
|
+
{ type: 'target_supported', status: 'pass', message: 'codex install target is supported' },
|
|
1496
|
+
{ type: 'install_root', status: 'pass', message: '~/.codex/skills for activated skills; ~/.codex/tokrepo/staged for staged assets' },
|
|
1497
|
+
];
|
|
1498
|
+
if (!targetTools.length || targetTools.map(normalizeToolName).includes('codex')) {
|
|
1499
|
+
out.push({ type: 'target_tool_metadata', status: 'pass', message: 'metadata allows codex' });
|
|
1500
|
+
} else {
|
|
1501
|
+
out.push({ type: 'target_tool_metadata', status: 'warn', message: 'metadata target_tools does not include codex' });
|
|
1502
|
+
}
|
|
1503
|
+
out.push({
|
|
1504
|
+
type: 'content_hash',
|
|
1505
|
+
status: plan.contentHash ? 'pass' : 'warn',
|
|
1506
|
+
message: plan.contentHash ? 'asset metadata includes content_hash' : 'asset metadata does not include content_hash',
|
|
1507
|
+
});
|
|
1508
|
+
const policyStatus = policyDecision.decision === 'deny' ? 'block'
|
|
1509
|
+
: policyDecision.decision === 'allow' ? 'pass'
|
|
1510
|
+
: 'warn';
|
|
1511
|
+
out.push({ type: 'policy_decision', status: policyStatus, message: `${policyDecision.decision} for ${plan.uuid}` });
|
|
1512
|
+
return out;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
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
|
+
|
|
1519
|
+
const seen = new Set();
|
|
1520
|
+
const rollback = [];
|
|
1521
|
+
for (const file of plan.files) {
|
|
1522
|
+
if (!file.path || seen.has(file.path)) continue;
|
|
1523
|
+
seen.add(file.path);
|
|
1524
|
+
rollback.push({ type: 'remove_file', path: file.path });
|
|
1525
|
+
}
|
|
1526
|
+
return rollback;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
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
|
+
|
|
1533
|
+
const metadata = plan.agentMetadata || {};
|
|
1534
|
+
const verification = metadataValue(metadata, 'verification', 'verification', {}) || {};
|
|
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)));
|
|
1537
|
+
for (const expected of (verification.expected_files || verification.expectedFiles || [])) {
|
|
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
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
for (const command of (verification.commands || [])) {
|
|
1544
|
+
out.push({ type: 'command', command });
|
|
1545
|
+
}
|
|
1546
|
+
return out;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1281
1549
|
function publicInstallPlan(plan) {
|
|
1550
|
+
const policyDecision = decideCodexPolicy(plan);
|
|
1551
|
+
const actions = buildPublicPlanActions(plan);
|
|
1552
|
+
const schemaVersion = Number(metadataValue(plan.serverPlan, 'schema_version', 'schemaVersion', 2)) || 2;
|
|
1282
1553
|
return {
|
|
1554
|
+
schemaVersion,
|
|
1555
|
+
sourceOfTruth: plan.serverPlan ? 'api_install_plan_v2' : 'local_fallback',
|
|
1556
|
+
concretePlanSource: serverConcretePlanMatchesLocal(plan) ? 'api_install_plan_v2' : 'local_fallback',
|
|
1283
1557
|
uuid: plan.uuid,
|
|
1284
1558
|
title: plan.title,
|
|
1285
1559
|
sourceUrl: plan.sourceUrl,
|
|
@@ -1288,7 +1562,12 @@ function publicInstallPlan(plan) {
|
|
|
1288
1562
|
manifestPath: plan.manifestPath,
|
|
1289
1563
|
baseDir: plan.baseDir,
|
|
1290
1564
|
risks: plan.risks,
|
|
1291
|
-
|
|
1565
|
+
preconditions: buildPublicPlanPreconditions(plan, policyDecision),
|
|
1566
|
+
actions,
|
|
1567
|
+
policyDecision,
|
|
1568
|
+
requiresConfirmation: policyDecision.requiresConfirmation,
|
|
1569
|
+
rollback: buildPublicPlanRollback(plan),
|
|
1570
|
+
postVerify: buildPublicPlanPostVerify(plan),
|
|
1292
1571
|
contentHash: plan.contentHash || '',
|
|
1293
1572
|
agentMetadata: plan.agentMetadata || {},
|
|
1294
1573
|
files: plan.files.map(file => ({
|
|
@@ -1302,8 +1581,98 @@ function publicInstallPlan(plan) {
|
|
|
1302
1581
|
};
|
|
1303
1582
|
}
|
|
1304
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
|
+
|
|
1305
1673
|
function hasCodexInstallRisks(plan) {
|
|
1306
|
-
|
|
1674
|
+
const decision = decideCodexPolicy(plan).decision;
|
|
1675
|
+
return decision === 'confirm' || decision === 'stage_only' || decision === 'deny';
|
|
1307
1676
|
}
|
|
1308
1677
|
|
|
1309
1678
|
function formatRiskLine(file) {
|
|
@@ -1312,15 +1681,19 @@ function formatRiskLine(file) {
|
|
|
1312
1681
|
}
|
|
1313
1682
|
|
|
1314
1683
|
async function confirmCodexInstallRisks(plan, opts = {}) {
|
|
1684
|
+
const policy = decideCodexPolicy(plan);
|
|
1685
|
+
if (policy.decision === 'deny') {
|
|
1686
|
+
throw new Error(`Install policy denied this asset: ${policy.reasons.join('; ')}`);
|
|
1687
|
+
}
|
|
1315
1688
|
if (plan.installMode === 'stage_only') return;
|
|
1316
|
-
if (opts.dryRun || opts.stage ||
|
|
1689
|
+
if (opts.dryRun || opts.stage || policy.decision === 'allow') return;
|
|
1317
1690
|
if (opts.approveMcp || opts.approve_mcp || opts.yes) return;
|
|
1318
1691
|
|
|
1319
1692
|
if (opts.json || opts.throwOnError || process.env.TOKREPO_NONINTERACTIVE === '1') {
|
|
1320
|
-
throw new Error(`Install
|
|
1693
|
+
throw new Error(`Install policy is ${policy.decision}: ${policy.reasons.join('; ')}. Re-run with --dry-run to inspect, --stage to stage the plan, or --approve-mcp to approve writing the Codex skill bundle.`);
|
|
1321
1694
|
}
|
|
1322
1695
|
|
|
1323
|
-
warn(`
|
|
1696
|
+
warn(`Install policy is ${policy.decision}: ${policy.reasons.join('; ')}`);
|
|
1324
1697
|
log(` ${C.dim}TokRepo will only write files under ${CODEX_SKILLS_DIR}; it will not merge MCP configs, modify PATH, or execute scripts.${C.reset}`);
|
|
1325
1698
|
const riskyFiles = plan.files
|
|
1326
1699
|
.map(formatRiskLine)
|
|
@@ -1375,6 +1748,102 @@ function executeStageOnlyCodexPlan(plan) {
|
|
|
1375
1748
|
return { dryRun: true, staged: true, stageOnly: true, stagePath, plan: publicInstallPlan(plan), installedFiles };
|
|
1376
1749
|
}
|
|
1377
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
|
+
|
|
1378
1847
|
function readCodexManifest() {
|
|
1379
1848
|
try {
|
|
1380
1849
|
const parsed = JSON.parse(fs.readFileSync(CODEX_MANIFEST_FILE, 'utf8'));
|
|
@@ -1383,7 +1852,15 @@ function readCodexManifest() {
|
|
|
1383
1852
|
return { schemaVersion: 1, installs: [] };
|
|
1384
1853
|
}
|
|
1385
1854
|
|
|
1386
|
-
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) {
|
|
1387
1864
|
if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
|
|
1388
1865
|
fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
|
|
1389
1866
|
}
|
|
@@ -1398,6 +1875,9 @@ function writeCodexManifestRecord(plan, installedFiles) {
|
|
|
1398
1875
|
installedAt,
|
|
1399
1876
|
contentHash: plan.contentHash || '',
|
|
1400
1877
|
agentMetadata: plan.agentMetadata || {},
|
|
1878
|
+
sessionId: sessionInfo.sessionId,
|
|
1879
|
+
sessionPath: sessionInfo.sessionPath,
|
|
1880
|
+
verification,
|
|
1401
1881
|
installedFiles: installedFiles.map(file => ({
|
|
1402
1882
|
path: file.path,
|
|
1403
1883
|
sourceName: file.sourceName,
|
|
@@ -1410,16 +1890,58 @@ function writeCodexManifestRecord(plan, installedFiles) {
|
|
|
1410
1890
|
manifest.installs = manifest.installs.filter(item => !(item.uuid === plan.uuid && item.targetTool === 'codex'));
|
|
1411
1891
|
manifest.installs.push(record);
|
|
1412
1892
|
manifest.updatedAt = installedAt;
|
|
1413
|
-
|
|
1893
|
+
writeCodexManifest(manifest);
|
|
1414
1894
|
return record;
|
|
1415
1895
|
}
|
|
1416
1896
|
|
|
1417
1897
|
function executeCodexInstallPlan(plan, opts = {}) {
|
|
1418
|
-
|
|
1419
|
-
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
|
+
}
|
|
1420
1931
|
if (opts.stage) {
|
|
1421
1932
|
const stagePath = stageCodexInstallPlan(plan);
|
|
1422
|
-
|
|
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 };
|
|
1423
1945
|
}
|
|
1424
1946
|
|
|
1425
1947
|
const installedFiles = [];
|
|
@@ -1439,8 +1961,22 @@ function executeCodexInstallPlan(plan, opts = {}) {
|
|
|
1439
1961
|
});
|
|
1440
1962
|
}
|
|
1441
1963
|
|
|
1442
|
-
const
|
|
1443
|
-
|
|
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 };
|
|
1444
1980
|
}
|
|
1445
1981
|
|
|
1446
1982
|
async function installCodexAsset(workflow, contents, opts = {}) {
|
|
@@ -1468,6 +2004,7 @@ async function cmdInstall() {
|
|
|
1468
2004
|
dryRun: Boolean(args.flags.dryRun || args.flags.dry_run),
|
|
1469
2005
|
stage: Boolean(args.flags.stage),
|
|
1470
2006
|
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2007
|
+
verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
|
|
1471
2008
|
json: Boolean(args.flags.json),
|
|
1472
2009
|
manifest: Boolean(args.flags.manifest),
|
|
1473
2010
|
};
|
|
@@ -1488,6 +2025,33 @@ async function cmdInstall() {
|
|
|
1488
2025
|
}
|
|
1489
2026
|
}
|
|
1490
2027
|
|
|
2028
|
+
async function cmdPlan() {
|
|
2029
|
+
const args = parseArgs(process.argv);
|
|
2030
|
+
const target = args.positional[0];
|
|
2031
|
+
if (!target) {
|
|
2032
|
+
showPlanHelp();
|
|
2033
|
+
process.exit(1);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const config = readConfig();
|
|
2037
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
2038
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
2039
|
+
if (targetTool !== 'codex') {
|
|
2040
|
+
error(`plan currently supports --target codex only`);
|
|
2041
|
+
}
|
|
2042
|
+
const result = await installOneAsset(target, config, apiBase, {
|
|
2043
|
+
targetTool,
|
|
2044
|
+
dryRun: true,
|
|
2045
|
+
stage: Boolean(args.flags.stage),
|
|
2046
|
+
installMode: args.flags.installMode,
|
|
2047
|
+
json: true,
|
|
2048
|
+
manifest: true,
|
|
2049
|
+
throwOnError: true,
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
outputJson(result.plan);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1491
2055
|
// Install all assets in a theme pack — sequentially, continue past per-item errors
|
|
1492
2056
|
async function installPack(slug, config, apiBase, opts) {
|
|
1493
2057
|
info(`Fetching pack ${C.bold}${slug}${C.reset}...`);
|
|
@@ -1617,7 +2181,8 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1617
2181
|
if (targetTool === 'codex') {
|
|
1618
2182
|
let result;
|
|
1619
2183
|
try {
|
|
1620
|
-
|
|
2184
|
+
const serverPlan = opts.serverPlan !== undefined ? opts.serverPlan : await fetchServerCodexInstallPlan(uuid, config, apiBase);
|
|
2185
|
+
result = await installCodexAsset(workflow, contents, { ...opts, serverPlan });
|
|
1621
2186
|
} catch (e) {
|
|
1622
2187
|
die(e.message);
|
|
1623
2188
|
}
|
|
@@ -1631,6 +2196,7 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1631
2196
|
} else {
|
|
1632
2197
|
info(`No Codex skill files were written. Re-run with --approve-mcp or --yes to install.`);
|
|
1633
2198
|
}
|
|
2199
|
+
if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
|
|
1634
2200
|
} else if (opts.dryRun) {
|
|
1635
2201
|
info(`Dry run: ${plan.files.length} file(s) would be installed to ${CODEX_SKILLS_DIR}`);
|
|
1636
2202
|
for (const file of plan.files) {
|
|
@@ -1638,6 +2204,7 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1638
2204
|
log(` ${C.dim}•${C.reset} ~/${rel}`);
|
|
1639
2205
|
if (file.riskFlags.length) log(` ${C.yellow}${file.riskFlags.join(', ')}${C.reset}`);
|
|
1640
2206
|
}
|
|
2207
|
+
if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
|
|
1641
2208
|
} else {
|
|
1642
2209
|
for (const file of result.installedFiles) {
|
|
1643
2210
|
const relPath = path.relative(os.homedir(), file.path);
|
|
@@ -1646,6 +2213,8 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1646
2213
|
log('');
|
|
1647
2214
|
success(`${result.installedFiles.length} file(s) installed from ${C.bold}${workflow.title}${C.reset}`);
|
|
1648
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}`);
|
|
1649
2218
|
log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
|
|
1650
2219
|
}
|
|
1651
2220
|
}
|
|
@@ -1661,6 +2230,9 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1661
2230
|
installedFiles: result.installedFiles || [],
|
|
1662
2231
|
plan: result.plan,
|
|
1663
2232
|
manifestPath: CODEX_MANIFEST_FILE,
|
|
2233
|
+
sessionId: result.sessionId,
|
|
2234
|
+
sessionPath: result.sessionPath,
|
|
2235
|
+
verification: result.verification,
|
|
1664
2236
|
};
|
|
1665
2237
|
}
|
|
1666
2238
|
|
|
@@ -1822,8 +2394,16 @@ async function cmdList() {
|
|
|
1822
2394
|
data = { ...data, list };
|
|
1823
2395
|
}
|
|
1824
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
|
+
|
|
1825
2405
|
if (args.flags.json) {
|
|
1826
|
-
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 || [] });
|
|
1827
2407
|
return;
|
|
1828
2408
|
}
|
|
1829
2409
|
|
|
@@ -1832,11 +2412,16 @@ async function cmdList() {
|
|
|
1832
2412
|
return;
|
|
1833
2413
|
}
|
|
1834
2414
|
|
|
1835
|
-
|
|
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`);
|
|
1836
2417
|
|
|
1837
2418
|
for (const wf of data.list) {
|
|
1838
2419
|
const views = wf.view_count || 0;
|
|
1839
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
|
+
}
|
|
1840
2425
|
log(` ${C.dim} ${views} views · https://tokrepo.com/en/workflows/${wf.uuid}${C.reset}\n`);
|
|
1841
2426
|
}
|
|
1842
2427
|
} catch (e) {
|
|
@@ -2046,13 +2631,16 @@ async function cmdClone() {
|
|
|
2046
2631
|
const assetType = getWorkflowAssetType(workflow);
|
|
2047
2632
|
const contents = extractInstallableContents(workflow, assetType);
|
|
2048
2633
|
if (contents.length === 0) throw new Error('No installable content found');
|
|
2634
|
+
const serverPlan = await fetchServerCodexInstallPlan(workflow.uuid, config, apiBase);
|
|
2049
2635
|
const result = await installCodexAsset(workflow, contents, {
|
|
2050
2636
|
...args.flags,
|
|
2051
2637
|
dryRun,
|
|
2052
2638
|
stage: Boolean(args.flags.stage),
|
|
2053
2639
|
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2640
|
+
verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
|
|
2054
2641
|
json: true,
|
|
2055
2642
|
throwOnError: true,
|
|
2643
|
+
serverPlan,
|
|
2056
2644
|
});
|
|
2057
2645
|
if (!dryRun) installedCount += result.installedFiles.length;
|
|
2058
2646
|
results.push({
|
|
@@ -2065,6 +2653,9 @@ async function cmdClone() {
|
|
|
2065
2653
|
files: result.plan.files,
|
|
2066
2654
|
installedFiles: result.installedFiles || [],
|
|
2067
2655
|
risks: result.plan.risks,
|
|
2656
|
+
sessionId: result.sessionId,
|
|
2657
|
+
sessionPath: result.sessionPath,
|
|
2658
|
+
verification: result.verification,
|
|
2068
2659
|
});
|
|
2069
2660
|
if (!json) {
|
|
2070
2661
|
const fileCount = (dryRun || args.flags.stage) ? result.plan.files.length : result.installedFiles.length;
|
|
@@ -2193,6 +2784,15 @@ async function fetchWorkflowForInstall(uuid, config, apiBase) {
|
|
|
2193
2784
|
return { workflow, contents };
|
|
2194
2785
|
}
|
|
2195
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
|
+
|
|
2196
2796
|
async function cmdSyncInstalled() {
|
|
2197
2797
|
const args = parseArgs(process.argv);
|
|
2198
2798
|
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
@@ -2227,7 +2827,8 @@ async function cmdSyncInstalled() {
|
|
|
2227
2827
|
|
|
2228
2828
|
try {
|
|
2229
2829
|
const { workflow, contents } = await fetchWorkflowForInstall(uuid, config, apiBase);
|
|
2230
|
-
const
|
|
2830
|
+
const serverPlan = await fetchServerCodexInstallPlan(uuid, config, apiBase);
|
|
2831
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
|
|
2231
2832
|
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2232
2833
|
const shouldWrite = force || diff.needsUpdate;
|
|
2233
2834
|
|
|
@@ -2265,8 +2866,10 @@ async function cmdSyncInstalled() {
|
|
|
2265
2866
|
stage,
|
|
2266
2867
|
installMode: record.installMode || record.install_mode,
|
|
2267
2868
|
approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
|
|
2869
|
+
verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
|
|
2268
2870
|
json: true,
|
|
2269
2871
|
throwOnError: true,
|
|
2872
|
+
serverPlan,
|
|
2270
2873
|
});
|
|
2271
2874
|
|
|
2272
2875
|
results.push({
|
|
@@ -2278,6 +2881,9 @@ async function cmdSyncInstalled() {
|
|
|
2278
2881
|
stagePath: installResult.stagePath,
|
|
2279
2882
|
installedFiles: installResult.installedFiles || [],
|
|
2280
2883
|
plan: installResult.plan,
|
|
2884
|
+
sessionId: installResult.sessionId,
|
|
2885
|
+
sessionPath: installResult.sessionPath,
|
|
2886
|
+
verification: installResult.verification,
|
|
2281
2887
|
});
|
|
2282
2888
|
if (!json) success(stage ? `Staged ${installResult.plan.files.length} file(s)` : `Updated ${(installResult.installedFiles || []).length} file(s)`);
|
|
2283
2889
|
} catch (e) {
|
|
@@ -2336,6 +2942,8 @@ async function cmdInstalled() {
|
|
|
2336
2942
|
installMode: record.installMode || record.install_mode,
|
|
2337
2943
|
installedAt: record.installedAt || record.installed_at,
|
|
2338
2944
|
contentHash: record.contentHash || record.content_hash || '',
|
|
2945
|
+
sessionId: record.sessionId || record.session_id,
|
|
2946
|
+
sessionPath: record.sessionPath || record.session_path,
|
|
2339
2947
|
risks: record.risks || [],
|
|
2340
2948
|
files,
|
|
2341
2949
|
status: files.some(file => !file.exists) ? 'missing-files' : files.some(file => file.changed) ? 'local-changes' : 'installed',
|
|
@@ -2359,6 +2967,281 @@ async function cmdInstalled() {
|
|
|
2359
2967
|
}
|
|
2360
2968
|
}
|
|
2361
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
|
+
|
|
2362
3245
|
async function cmdOutdated() {
|
|
2363
3246
|
const args = parseArgs(process.argv);
|
|
2364
3247
|
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
@@ -2384,7 +3267,8 @@ async function cmdOutdated() {
|
|
|
2384
3267
|
for (const record of installed) {
|
|
2385
3268
|
try {
|
|
2386
3269
|
const { workflow, contents } = await fetchWorkflowForInstall(record.uuid, config, apiBase);
|
|
2387
|
-
const
|
|
3270
|
+
const serverPlan = await fetchServerCodexInstallPlan(record.uuid, config, apiBase);
|
|
3271
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
|
|
2388
3272
|
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2389
3273
|
if (diff.needsUpdate) {
|
|
2390
3274
|
list.push({
|
|
@@ -2546,12 +3430,15 @@ ${C.bold}USAGE${C.reset}
|
|
|
2546
3430
|
${C.bold}DISCOVER & INSTALL${C.reset}
|
|
2547
3431
|
${C.cyan}search${C.reset} <query> Search assets by keyword
|
|
2548
3432
|
${C.cyan}detail${C.reset} <name|uuid> Show full asset metadata
|
|
3433
|
+
${C.cyan}plan${C.reset} <name|uuid> Print agent-native Codex install plan
|
|
2549
3434
|
${C.cyan}install${C.reset} <name|uuid> Smart install (auto-detects type & placement)
|
|
2550
3435
|
${C.cyan}pull${C.reset} <url|uuid|@u/n> Download raw asset files
|
|
2551
3436
|
${C.cyan}clone${C.reset} @username Clone all assets from a user
|
|
2552
3437
|
${C.cyan}installed${C.reset} List installed Codex assets from manifest
|
|
2553
3438
|
${C.cyan}outdated${C.reset} Check installed Codex assets for updates
|
|
2554
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
|
|
2555
3442
|
|
|
2556
3443
|
${C.bold}PUBLISH${C.reset}
|
|
2557
3444
|
${C.cyan}push${C.reset} [files...] Push files/directory (idempotent upsert)
|
|
@@ -2589,8 +3476,9 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
|
2589
3476
|
|
|
2590
3477
|
${C.bold}EXAMPLES${C.reset}
|
|
2591
3478
|
tokrepo search "mcp server" # Find MCP configs
|
|
2592
|
-
tokrepo search video --
|
|
3479
|
+
tokrepo search video --target codex --kind skill --policy allow --json
|
|
2593
3480
|
tokrepo detail ca000374-f5d8-... --json # Machine-readable detail
|
|
3481
|
+
tokrepo plan 91aeb22d-eff0-4310-... # Install plan v2 for agents
|
|
2594
3482
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
2595
3483
|
tokrepo install ca000374-f5d8-... --target codex
|
|
2596
3484
|
tokrepo install c4b18aeb --target gemini # Install for Gemini CLI
|
|
@@ -2599,6 +3487,8 @@ ${C.bold}EXAMPLES${C.reset}
|
|
|
2599
3487
|
tokrepo outdated --target codex --json
|
|
2600
3488
|
tokrepo update --target codex --all
|
|
2601
3489
|
tokrepo sync-installed --target codex --dry-run
|
|
3490
|
+
tokrepo uninstall 91aeb22d --target codex --dry-run
|
|
3491
|
+
tokrepo rollback --last --target codex --dry-run
|
|
2602
3492
|
tokrepo push --private my-rules.md # Save one file privately
|
|
2603
3493
|
tokrepo push . --kind skill --target codex --install-mode bundle
|
|
2604
3494
|
tokrepo push --public skill.md # Share one file publicly
|
|
@@ -2626,11 +3516,12 @@ function showSearchHelp() {
|
|
|
2626
3516
|
${C.bold}tokrepo search${C.reset}
|
|
2627
3517
|
|
|
2628
3518
|
USAGE
|
|
2629
|
-
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]
|
|
2630
3520
|
|
|
2631
3521
|
EXAMPLES
|
|
2632
3522
|
tokrepo search video
|
|
2633
3523
|
tokrepo search video --json
|
|
3524
|
+
tokrepo search video --target codex --kind skill --policy allow --json
|
|
2634
3525
|
tokrepo search "mcp server" --json --all
|
|
2635
3526
|
`);
|
|
2636
3527
|
}
|
|
@@ -2653,7 +3544,7 @@ function showInstallHelp() {
|
|
|
2653
3544
|
${C.bold}tokrepo install${C.reset}
|
|
2654
3545
|
|
|
2655
3546
|
USAGE
|
|
2656
|
-
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]
|
|
2657
3548
|
|
|
2658
3549
|
TARGETS
|
|
2659
3550
|
codex Write Codex skills to ~/.codex/skills/<asset-slug>/SKILL.md
|
|
@@ -2670,16 +3561,33 @@ EXAMPLES
|
|
|
2670
3561
|
`);
|
|
2671
3562
|
}
|
|
2672
3563
|
|
|
3564
|
+
function showPlanHelp() {
|
|
3565
|
+
log(`
|
|
3566
|
+
${C.bold}tokrepo plan${C.reset}
|
|
3567
|
+
|
|
3568
|
+
USAGE
|
|
3569
|
+
tokrepo plan <uuid|url|name> [--target codex] [--stage]
|
|
3570
|
+
|
|
3571
|
+
OUTPUT
|
|
3572
|
+
Machine-readable install plan v2 with preconditions, actions, policyDecision,
|
|
3573
|
+
rollback, postVerify, risk metadata, and destination file hashes.
|
|
3574
|
+
|
|
3575
|
+
EXAMPLES
|
|
3576
|
+
tokrepo plan 91aeb22d-eff0-4310-abc6-811d2394b420
|
|
3577
|
+
tokrepo plan https://tokrepo.com/en/workflows/91aeb22d-eff0-4310-abc6-811d2394b420
|
|
3578
|
+
`);
|
|
3579
|
+
}
|
|
3580
|
+
|
|
2673
3581
|
function showListHelp() {
|
|
2674
3582
|
log(`
|
|
2675
3583
|
${C.bold}tokrepo list${C.reset}
|
|
2676
3584
|
|
|
2677
3585
|
USAGE
|
|
2678
|
-
tokrepo list [--json] [--all] [--page-size N]
|
|
3586
|
+
tokrepo list [--json] [--all] [--target codex] [--kind skill] [--policy allow] [--page-size N]
|
|
2679
3587
|
|
|
2680
3588
|
EXAMPLES
|
|
2681
3589
|
tokrepo list
|
|
2682
|
-
tokrepo list --json --all
|
|
3590
|
+
tokrepo list --json --all --target codex
|
|
2683
3591
|
`);
|
|
2684
3592
|
}
|
|
2685
3593
|
|
|
@@ -2722,6 +3630,42 @@ EXAMPLES
|
|
|
2722
3630
|
`);
|
|
2723
3631
|
}
|
|
2724
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
|
+
|
|
2725
3669
|
function showCommandHelp(command) {
|
|
2726
3670
|
switch (command) {
|
|
2727
3671
|
case 'search':
|
|
@@ -2729,6 +3673,8 @@ function showCommandHelp(command) {
|
|
|
2729
3673
|
showSearchHelp(); break;
|
|
2730
3674
|
case 'detail':
|
|
2731
3675
|
showDetailHelp(); break;
|
|
3676
|
+
case 'plan':
|
|
3677
|
+
showPlanHelp(); break;
|
|
2732
3678
|
case 'install':
|
|
2733
3679
|
case 'i':
|
|
2734
3680
|
showInstallHelp(); break;
|
|
@@ -2741,6 +3687,12 @@ function showCommandHelp(command) {
|
|
|
2741
3687
|
case 'installed':
|
|
2742
3688
|
case 'outdated':
|
|
2743
3689
|
showSyncInstalledHelp(); break;
|
|
3690
|
+
case 'uninstall':
|
|
3691
|
+
case 'remove':
|
|
3692
|
+
case 'rm':
|
|
3693
|
+
showUninstallHelp(); break;
|
|
3694
|
+
case 'rollback':
|
|
3695
|
+
showRollbackHelp(); break;
|
|
2744
3696
|
default:
|
|
2745
3697
|
showHelp(); break;
|
|
2746
3698
|
}
|
|
@@ -2764,12 +3716,15 @@ async function main() {
|
|
|
2764
3716
|
case 'pull': await cmdPull(); break;
|
|
2765
3717
|
case 'search': case 'find': await cmdSearch(); break;
|
|
2766
3718
|
case 'detail': await cmdDetail(); break;
|
|
3719
|
+
case 'plan': await cmdPlan(); break;
|
|
2767
3720
|
case 'install': case 'i': await cmdInstall(); break;
|
|
2768
3721
|
case 'list': await cmdList(); break;
|
|
2769
3722
|
case 'update': await cmdUpdate(); break;
|
|
2770
3723
|
case 'delete': await cmdDelete(); break;
|
|
2771
3724
|
case 'clone': await cmdClone(); break;
|
|
2772
3725
|
case 'installed': await cmdInstalled(); break;
|
|
3726
|
+
case 'uninstall': case 'remove': case 'rm': await cmdUninstall(); break;
|
|
3727
|
+
case 'rollback': await cmdRollback(); break;
|
|
2773
3728
|
case 'outdated': await cmdOutdated(); break;
|
|
2774
3729
|
case 'sync-installed': case 'sync': await cmdSyncInstalled(); break;
|
|
2775
3730
|
case 'tags': await cmdTags(); break;
|
|
@@ -2784,7 +3739,7 @@ async function main() {
|
|
|
2784
3739
|
}
|
|
2785
3740
|
|
|
2786
3741
|
// Non-blocking update check after command completes
|
|
2787
|
-
if (!wantsJson(process.argv) && !args.flags.help) {
|
|
3742
|
+
if (command !== 'plan' && !wantsJson(process.argv) && !args.flags.help) {
|
|
2788
3743
|
checkForUpdate();
|
|
2789
3744
|
}
|
|
2790
3745
|
}
|