tokrepo 3.4.0 → 3.6.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 +456 -15
- package/package.json +1 -1
package/bin/tokrepo.js
CHANGED
|
@@ -25,7 +25,7 @@ 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.6.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');
|
|
@@ -236,6 +236,12 @@ function guessTag(fileType) {
|
|
|
236
236
|
return map[fileType] || null;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
function parseCsvList(value) {
|
|
240
|
+
if (!value) return [];
|
|
241
|
+
if (Array.isArray(value)) return value.flatMap(parseCsvList);
|
|
242
|
+
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
243
|
+
}
|
|
244
|
+
|
|
239
245
|
// ─── Glob matching ───
|
|
240
246
|
|
|
241
247
|
function matchGlob(pattern, filename) {
|
|
@@ -295,7 +301,9 @@ function parseArgs(argv) {
|
|
|
295
301
|
}
|
|
296
302
|
|
|
297
303
|
const valueFlags = new Set([
|
|
298
|
-
'title', 'desc', 'tag', 'target', 'keyword', 'types',
|
|
304
|
+
'title', 'desc', 'tag', 'target', 'targets', 'keyword', 'types',
|
|
305
|
+
'kind', 'install-mode', 'install_mode', 'entrypoint', 'asset-kind', 'asset_kind',
|
|
306
|
+
'version',
|
|
299
307
|
'page', 'page-size', 'page_size', 'sort-by', 'sort_by',
|
|
300
308
|
'time-window', 'time_window',
|
|
301
309
|
]);
|
|
@@ -318,6 +326,10 @@ function parseArgs(argv) {
|
|
|
318
326
|
args.flags.dryRun = value;
|
|
319
327
|
} else if (normalized === 'approve_mcp') {
|
|
320
328
|
args.flags.approveMcp = value;
|
|
329
|
+
} else if (normalized === 'install_mode') {
|
|
330
|
+
args.flags.installMode = value;
|
|
331
|
+
} else if (normalized === 'asset_kind') {
|
|
332
|
+
args.flags.assetKind = value;
|
|
321
333
|
}
|
|
322
334
|
args.flags[normalized] = value;
|
|
323
335
|
};
|
|
@@ -574,6 +586,10 @@ async function cmdPush() {
|
|
|
574
586
|
description = args.flags.desc || description || '';
|
|
575
587
|
visibility = args.flags.public ? 1 : (args.flags.private ? 0 : (projectConfig?.visibility ?? 0));
|
|
576
588
|
tags = args.flags.tags || tags || [];
|
|
589
|
+
const kind = args.flags.kind || args.flags.assetKind || projectConfig?.kind || projectConfig?.asset_kind || '';
|
|
590
|
+
const targetTools = parseCsvList(args.flags.targets || args.flags.target || projectConfig?.target_tools || projectConfig?.targetTools);
|
|
591
|
+
const installMode = args.flags.installMode || projectConfig?.install_mode || projectConfig?.installMode || '';
|
|
592
|
+
const entrypoint = args.flags.entrypoint || projectConfig?.entrypoint || '';
|
|
577
593
|
|
|
578
594
|
// Read files and detect types
|
|
579
595
|
const pushFiles = [];
|
|
@@ -613,6 +629,15 @@ async function cmdPush() {
|
|
|
613
629
|
if (detectedTags.size > 0) {
|
|
614
630
|
log(` ${C.bold}Tags:${C.reset} ${Array.from(detectedTags).join(', ')}`);
|
|
615
631
|
}
|
|
632
|
+
const metadataSummary = [
|
|
633
|
+
kind ? `kind=${kind}` : '',
|
|
634
|
+
targetTools.length ? `target_tools=${targetTools.join(',')}` : '',
|
|
635
|
+
installMode ? `install_mode=${installMode}` : '',
|
|
636
|
+
entrypoint ? `entrypoint=${entrypoint}` : '',
|
|
637
|
+
].filter(Boolean);
|
|
638
|
+
if (metadataSummary.length > 0) {
|
|
639
|
+
log(` ${C.bold}Agent meta:${C.reset} ${metadataSummary.join(' · ')}`);
|
|
640
|
+
}
|
|
616
641
|
log('');
|
|
617
642
|
|
|
618
643
|
for (const f of pushFiles) {
|
|
@@ -634,6 +659,10 @@ async function cmdPush() {
|
|
|
634
659
|
tags: Array.from(detectedTags),
|
|
635
660
|
token_cost: String(Math.round(totalChars / 4)),
|
|
636
661
|
visibility: visibility,
|
|
662
|
+
kind,
|
|
663
|
+
target_tools: targetTools,
|
|
664
|
+
install_mode: installMode,
|
|
665
|
+
entrypoint,
|
|
637
666
|
}, config.token, config.api);
|
|
638
667
|
|
|
639
668
|
log('');
|
|
@@ -1095,11 +1124,13 @@ function explicitInstallMode(workflow) {
|
|
|
1095
1124
|
const candidates = [
|
|
1096
1125
|
workflow?.installMode,
|
|
1097
1126
|
workflow?.install_mode,
|
|
1127
|
+
workflow?.agent_metadata?.install_mode,
|
|
1128
|
+
workflow?.agentMetadata?.installMode,
|
|
1098
1129
|
workflow?.metadata?.installMode,
|
|
1099
1130
|
workflow?.metadata?.install_mode,
|
|
1100
1131
|
].filter(Boolean);
|
|
1101
1132
|
const mode = String(candidates[0] || '').toLowerCase();
|
|
1102
|
-
return ['single', 'bundle', 'split'].includes(mode) ? mode : '';
|
|
1133
|
+
return ['single', 'bundle', 'split', 'stage_only'].includes(mode) ? mode : '';
|
|
1103
1134
|
}
|
|
1104
1135
|
|
|
1105
1136
|
function inferCodexInstallMode(workflow, contents) {
|
|
@@ -1157,6 +1188,7 @@ function addPlanFile(plan, destPath, content, sourceName, type) {
|
|
|
1157
1188
|
|
|
1158
1189
|
function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
1159
1190
|
const installMode = opts.installMode || inferCodexInstallMode(workflow, contents);
|
|
1191
|
+
const agentMetadata = workflow?.agent_metadata || workflow?.agentMetadata || {};
|
|
1160
1192
|
const plan = {
|
|
1161
1193
|
uuid: workflow.uuid,
|
|
1162
1194
|
title: workflow.title,
|
|
@@ -1166,8 +1198,20 @@ function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
|
1166
1198
|
manifestPath: CODEX_MANIFEST_FILE,
|
|
1167
1199
|
files: [],
|
|
1168
1200
|
risks: [],
|
|
1201
|
+
agentMetadata,
|
|
1202
|
+
contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || '',
|
|
1169
1203
|
};
|
|
1170
1204
|
|
|
1205
|
+
if (installMode === 'stage_only') {
|
|
1206
|
+
const stageDir = path.join(CODEX_TOKREPO_DIR, 'staged', workflow.uuid);
|
|
1207
|
+
plan.baseDir = stageDir;
|
|
1208
|
+
contents.forEach((item, index) => {
|
|
1209
|
+
const relName = sanitizeRelativePath(item.name || `file-${index + 1}.md`);
|
|
1210
|
+
addPlanFile(plan, path.join(stageDir, relName), `${String(item.content || '').trim()}\n`, item.name, item.type);
|
|
1211
|
+
});
|
|
1212
|
+
return plan;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1171
1215
|
if (installMode === 'split') {
|
|
1172
1216
|
const usedDirs = new Set();
|
|
1173
1217
|
contents.forEach((item, index) => {
|
|
@@ -1234,8 +1278,161 @@ function buildCodexInstallPlan(workflow, contents, opts = {}) {
|
|
|
1234
1278
|
return plan;
|
|
1235
1279
|
}
|
|
1236
1280
|
|
|
1281
|
+
function metadataValue(metadata, snakeName, camelName, fallback) {
|
|
1282
|
+
if (!metadata) return fallback;
|
|
1283
|
+
if (metadata[snakeName] !== undefined) return metadata[snakeName];
|
|
1284
|
+
if (metadata[camelName] !== undefined) return metadata[camelName];
|
|
1285
|
+
return fallback;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function normalizeToolName(value) {
|
|
1289
|
+
return String(value || '').trim().toLowerCase().replace(/-/g, '_');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function riskProfileFromFlags(flags = []) {
|
|
1293
|
+
const set = new Set(flags || []);
|
|
1294
|
+
return {
|
|
1295
|
+
executes_code: set.has('executable'),
|
|
1296
|
+
modifies_global_config: set.has('mcp'),
|
|
1297
|
+
requires_secrets: set.has('env') ? ['ENV'] : [],
|
|
1298
|
+
uses_absolute_paths: set.has('absolute-path'),
|
|
1299
|
+
network_access: set.has('network'),
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function mergedPlanRiskProfile(plan) {
|
|
1304
|
+
const metadata = plan.agentMetadata || {};
|
|
1305
|
+
const rp = metadataValue(metadata, 'risk_profile', 'riskProfile', {}) || {};
|
|
1306
|
+
const flags = new Set(plan.risks || []);
|
|
1307
|
+
return {
|
|
1308
|
+
executes_code: Boolean(rp.executes_code || rp.executesCode || flags.has('executable')),
|
|
1309
|
+
modifies_global_config: Boolean(rp.modifies_global_config || rp.modifiesGlobalConfig || flags.has('mcp')),
|
|
1310
|
+
requires_secrets: rp.requires_secrets || rp.requiresSecrets || (flags.has('env') ? ['ENV'] : []),
|
|
1311
|
+
uses_absolute_paths: Boolean(rp.uses_absolute_paths || rp.usesAbsolutePaths || flags.has('absolute-path')),
|
|
1312
|
+
network_access: Boolean(rp.network_access || rp.networkAccess || flags.has('network')),
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function decideCodexPolicy(plan) {
|
|
1317
|
+
const metadata = plan.agentMetadata || {};
|
|
1318
|
+
const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
|
|
1319
|
+
const assetKind = normalizeToolName(metadataValue(metadata, 'asset_kind', 'assetKind', ''));
|
|
1320
|
+
const risk = mergedPlanRiskProfile(plan);
|
|
1321
|
+
let decision = 'allow';
|
|
1322
|
+
const reasons = [];
|
|
1323
|
+
const raise = (next) => {
|
|
1324
|
+
const rank = { allow: 0, confirm: 1, stage_only: 2, deny: 3 };
|
|
1325
|
+
if ((rank[next] || 0) > (rank[decision] || 0)) decision = next;
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
if (targetTools.length && !targetTools.map(normalizeToolName).includes('codex')) {
|
|
1329
|
+
raise('confirm');
|
|
1330
|
+
reasons.push('metadata target_tools does not include codex');
|
|
1331
|
+
}
|
|
1332
|
+
if (['script', 'cli_tool', 'mcp_config'].includes(assetKind)) {
|
|
1333
|
+
raise('stage_only');
|
|
1334
|
+
reasons.push(`asset_kind ${assetKind} is not activated directly for Codex`);
|
|
1335
|
+
}
|
|
1336
|
+
if (plan.installMode === 'stage_only') {
|
|
1337
|
+
raise('stage_only');
|
|
1338
|
+
reasons.push('install_mode is stage_only');
|
|
1339
|
+
}
|
|
1340
|
+
if (risk.executes_code) {
|
|
1341
|
+
raise('stage_only');
|
|
1342
|
+
reasons.push('risk_profile.executes_code is true');
|
|
1343
|
+
}
|
|
1344
|
+
if (risk.modifies_global_config) {
|
|
1345
|
+
raise('stage_only');
|
|
1346
|
+
reasons.push('risk_profile.modifies_global_config is true');
|
|
1347
|
+
}
|
|
1348
|
+
if ((risk.requires_secrets || []).length) {
|
|
1349
|
+
raise('stage_only');
|
|
1350
|
+
reasons.push('risk_profile.requires_secrets is not empty');
|
|
1351
|
+
}
|
|
1352
|
+
if (risk.uses_absolute_paths) {
|
|
1353
|
+
raise('confirm');
|
|
1354
|
+
reasons.push('risk_profile.uses_absolute_paths is true');
|
|
1355
|
+
}
|
|
1356
|
+
if (risk.network_access) {
|
|
1357
|
+
raise('confirm');
|
|
1358
|
+
reasons.push('risk_profile.network_access is true');
|
|
1359
|
+
}
|
|
1360
|
+
if (reasons.length === 0) reasons.push('safe markdown-only Codex install');
|
|
1361
|
+
|
|
1362
|
+
return {
|
|
1363
|
+
decision,
|
|
1364
|
+
requiresConfirmation: decision === 'confirm',
|
|
1365
|
+
reasons: Array.from(new Set(reasons)),
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function buildPublicPlanActions(plan) {
|
|
1370
|
+
const stage = plan.installMode === 'stage_only';
|
|
1371
|
+
return plan.files.map(file => ({
|
|
1372
|
+
type: stage ? 'stage_file' : 'write_file',
|
|
1373
|
+
path: file.path,
|
|
1374
|
+
sourceName: file.sourceName,
|
|
1375
|
+
sha256: file.sha256,
|
|
1376
|
+
bytes: file.bytes,
|
|
1377
|
+
ifExists: 'overwrite',
|
|
1378
|
+
entrypoint: path.basename(file.path).toLowerCase() === 'skill.md',
|
|
1379
|
+
risk: riskProfileFromFlags(file.riskFlags || []),
|
|
1380
|
+
}));
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function buildPublicPlanPreconditions(plan, policyDecision) {
|
|
1384
|
+
const metadata = plan.agentMetadata || {};
|
|
1385
|
+
const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
|
|
1386
|
+
const out = [
|
|
1387
|
+
{ type: 'target_supported', status: 'pass', message: 'codex install target is supported' },
|
|
1388
|
+
{ type: 'install_root', status: 'pass', message: '~/.codex/skills for activated skills; ~/.codex/tokrepo/staged for staged assets' },
|
|
1389
|
+
];
|
|
1390
|
+
if (!targetTools.length || targetTools.map(normalizeToolName).includes('codex')) {
|
|
1391
|
+
out.push({ type: 'target_tool_metadata', status: 'pass', message: 'metadata allows codex' });
|
|
1392
|
+
} else {
|
|
1393
|
+
out.push({ type: 'target_tool_metadata', status: 'warn', message: 'metadata target_tools does not include codex' });
|
|
1394
|
+
}
|
|
1395
|
+
out.push({
|
|
1396
|
+
type: 'content_hash',
|
|
1397
|
+
status: plan.contentHash ? 'pass' : 'warn',
|
|
1398
|
+
message: plan.contentHash ? 'asset metadata includes content_hash' : 'asset metadata does not include content_hash',
|
|
1399
|
+
});
|
|
1400
|
+
const policyStatus = policyDecision.decision === 'deny' ? 'block'
|
|
1401
|
+
: policyDecision.decision === 'allow' ? 'pass'
|
|
1402
|
+
: 'warn';
|
|
1403
|
+
out.push({ type: 'policy_decision', status: policyStatus, message: `${policyDecision.decision} for ${plan.uuid}` });
|
|
1404
|
+
return out;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function buildPublicPlanRollback(plan) {
|
|
1408
|
+
const seen = new Set();
|
|
1409
|
+
const rollback = [];
|
|
1410
|
+
for (const file of plan.files) {
|
|
1411
|
+
if (!file.path || seen.has(file.path)) continue;
|
|
1412
|
+
seen.add(file.path);
|
|
1413
|
+
rollback.push({ type: 'remove_file', path: file.path });
|
|
1414
|
+
}
|
|
1415
|
+
return rollback;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function buildPublicPlanPostVerify(plan) {
|
|
1419
|
+
const metadata = plan.agentMetadata || {};
|
|
1420
|
+
const verification = metadataValue(metadata, 'verification', 'verification', {}) || {};
|
|
1421
|
+
const out = plan.files.map(file => ({ type: 'file_sha256', path: file.path, sha256: file.sha256 }));
|
|
1422
|
+
for (const expected of (verification.expected_files || verification.expectedFiles || [])) {
|
|
1423
|
+
out.push({ type: 'expected_file', path: expected });
|
|
1424
|
+
}
|
|
1425
|
+
for (const command of (verification.commands || [])) {
|
|
1426
|
+
out.push({ type: 'command', command });
|
|
1427
|
+
}
|
|
1428
|
+
return out;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1237
1431
|
function publicInstallPlan(plan) {
|
|
1432
|
+
const policyDecision = decideCodexPolicy(plan);
|
|
1433
|
+
const actions = buildPublicPlanActions(plan);
|
|
1238
1434
|
return {
|
|
1435
|
+
schemaVersion: 2,
|
|
1239
1436
|
uuid: plan.uuid,
|
|
1240
1437
|
title: plan.title,
|
|
1241
1438
|
sourceUrl: plan.sourceUrl,
|
|
@@ -1244,6 +1441,14 @@ function publicInstallPlan(plan) {
|
|
|
1244
1441
|
manifestPath: plan.manifestPath,
|
|
1245
1442
|
baseDir: plan.baseDir,
|
|
1246
1443
|
risks: plan.risks,
|
|
1444
|
+
preconditions: buildPublicPlanPreconditions(plan, policyDecision),
|
|
1445
|
+
actions,
|
|
1446
|
+
policyDecision,
|
|
1447
|
+
requiresConfirmation: policyDecision.requiresConfirmation,
|
|
1448
|
+
rollback: buildPublicPlanRollback(plan),
|
|
1449
|
+
postVerify: buildPublicPlanPostVerify(plan),
|
|
1450
|
+
contentHash: plan.contentHash || '',
|
|
1451
|
+
agentMetadata: plan.agentMetadata || {},
|
|
1247
1452
|
files: plan.files.map(file => ({
|
|
1248
1453
|
path: file.path,
|
|
1249
1454
|
sourceName: file.sourceName,
|
|
@@ -1256,7 +1461,8 @@ function publicInstallPlan(plan) {
|
|
|
1256
1461
|
}
|
|
1257
1462
|
|
|
1258
1463
|
function hasCodexInstallRisks(plan) {
|
|
1259
|
-
|
|
1464
|
+
const decision = decideCodexPolicy(plan).decision;
|
|
1465
|
+
return decision === 'confirm' || decision === 'stage_only' || decision === 'deny';
|
|
1260
1466
|
}
|
|
1261
1467
|
|
|
1262
1468
|
function formatRiskLine(file) {
|
|
@@ -1265,14 +1471,19 @@ function formatRiskLine(file) {
|
|
|
1265
1471
|
}
|
|
1266
1472
|
|
|
1267
1473
|
async function confirmCodexInstallRisks(plan, opts = {}) {
|
|
1268
|
-
|
|
1474
|
+
const policy = decideCodexPolicy(plan);
|
|
1475
|
+
if (policy.decision === 'deny') {
|
|
1476
|
+
throw new Error(`Install policy denied this asset: ${policy.reasons.join('; ')}`);
|
|
1477
|
+
}
|
|
1478
|
+
if (plan.installMode === 'stage_only') return;
|
|
1479
|
+
if (opts.dryRun || opts.stage || policy.decision === 'allow') return;
|
|
1269
1480
|
if (opts.approveMcp || opts.approve_mcp || opts.yes) return;
|
|
1270
1481
|
|
|
1271
1482
|
if (opts.json || opts.throwOnError || process.env.TOKREPO_NONINTERACTIVE === '1') {
|
|
1272
|
-
throw new Error(`Install
|
|
1483
|
+
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.`);
|
|
1273
1484
|
}
|
|
1274
1485
|
|
|
1275
|
-
warn(`
|
|
1486
|
+
warn(`Install policy is ${policy.decision}: ${policy.reasons.join('; ')}`);
|
|
1276
1487
|
log(` ${C.dim}TokRepo will only write files under ${CODEX_SKILLS_DIR}; it will not merge MCP configs, modify PATH, or execute scripts.${C.reset}`);
|
|
1277
1488
|
const riskyFiles = plan.files
|
|
1278
1489
|
.map(formatRiskLine)
|
|
@@ -1301,6 +1512,32 @@ function stageCodexInstallPlan(plan) {
|
|
|
1301
1512
|
return stagePath;
|
|
1302
1513
|
}
|
|
1303
1514
|
|
|
1515
|
+
function executeStageOnlyCodexPlan(plan) {
|
|
1516
|
+
const installedFiles = [];
|
|
1517
|
+
const stageRoot = path.join(CODEX_TOKREPO_DIR, 'staged', plan.uuid);
|
|
1518
|
+
if (!fs.existsSync(stageRoot)) fs.mkdirSync(stageRoot, { recursive: true, mode: 0o700 });
|
|
1519
|
+
|
|
1520
|
+
for (const file of plan.files) {
|
|
1521
|
+
if (!ensureInside(stageRoot, file.path)) {
|
|
1522
|
+
throw new Error(`Stage path escaped TokRepo staging directory: ${file.path}`);
|
|
1523
|
+
}
|
|
1524
|
+
const destDir = path.dirname(file.path);
|
|
1525
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true, mode: 0o700 });
|
|
1526
|
+
fs.writeFileSync(file.path, file.content, { mode: 0o600 });
|
|
1527
|
+
installedFiles.push({
|
|
1528
|
+
path: file.path,
|
|
1529
|
+
sourceName: file.sourceName,
|
|
1530
|
+
sha256: sha256(file.content),
|
|
1531
|
+
bytes: Buffer.byteLength(String(file.content || '')),
|
|
1532
|
+
riskFlags: file.riskFlags,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const stagePath = path.join(stageRoot, 'install-plan.json');
|
|
1537
|
+
fs.writeFileSync(stagePath, `${JSON.stringify(publicInstallPlan(plan), null, 2)}\n`, { mode: 0o600 });
|
|
1538
|
+
return { dryRun: true, staged: true, stageOnly: true, stagePath, plan: publicInstallPlan(plan), installedFiles };
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1304
1541
|
function readCodexManifest() {
|
|
1305
1542
|
try {
|
|
1306
1543
|
const parsed = JSON.parse(fs.readFileSync(CODEX_MANIFEST_FILE, 'utf8'));
|
|
@@ -1322,6 +1559,8 @@ function writeCodexManifestRecord(plan, installedFiles) {
|
|
|
1322
1559
|
targetTool: 'codex',
|
|
1323
1560
|
installMode: plan.installMode,
|
|
1324
1561
|
installedAt,
|
|
1562
|
+
contentHash: plan.contentHash || '',
|
|
1563
|
+
agentMetadata: plan.agentMetadata || {},
|
|
1325
1564
|
installedFiles: installedFiles.map(file => ({
|
|
1326
1565
|
path: file.path,
|
|
1327
1566
|
sourceName: file.sourceName,
|
|
@@ -1340,6 +1579,7 @@ function writeCodexManifestRecord(plan, installedFiles) {
|
|
|
1340
1579
|
|
|
1341
1580
|
function executeCodexInstallPlan(plan, opts = {}) {
|
|
1342
1581
|
if (opts.dryRun) return { dryRun: true, plan: publicInstallPlan(plan), installedFiles: [] };
|
|
1582
|
+
if (plan.installMode === 'stage_only') return executeStageOnlyCodexPlan(plan);
|
|
1343
1583
|
if (opts.stage) {
|
|
1344
1584
|
const stagePath = stageCodexInstallPlan(plan);
|
|
1345
1585
|
return { dryRun: true, staged: true, stagePath, plan: publicInstallPlan(plan), installedFiles: [] };
|
|
@@ -1411,6 +1651,33 @@ async function cmdInstall() {
|
|
|
1411
1651
|
}
|
|
1412
1652
|
}
|
|
1413
1653
|
|
|
1654
|
+
async function cmdPlan() {
|
|
1655
|
+
const args = parseArgs(process.argv);
|
|
1656
|
+
const target = args.positional[0];
|
|
1657
|
+
if (!target) {
|
|
1658
|
+
showPlanHelp();
|
|
1659
|
+
process.exit(1);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const config = readConfig();
|
|
1663
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
1664
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
1665
|
+
if (targetTool !== 'codex') {
|
|
1666
|
+
error(`plan currently supports --target codex only`);
|
|
1667
|
+
}
|
|
1668
|
+
const result = await installOneAsset(target, config, apiBase, {
|
|
1669
|
+
targetTool,
|
|
1670
|
+
dryRun: true,
|
|
1671
|
+
stage: Boolean(args.flags.stage),
|
|
1672
|
+
installMode: args.flags.installMode,
|
|
1673
|
+
json: true,
|
|
1674
|
+
manifest: true,
|
|
1675
|
+
throwOnError: true,
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
outputJson(result.plan);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1414
1681
|
// Install all assets in a theme pack — sequentially, continue past per-item errors
|
|
1415
1682
|
async function installPack(slug, config, apiBase, opts) {
|
|
1416
1683
|
info(`Fetching pack ${C.bold}${slug}${C.reset}...`);
|
|
@@ -1547,9 +1814,13 @@ async function installOneAsset(target, config, apiBase, opts) {
|
|
|
1547
1814
|
|
|
1548
1815
|
if (!opts.json) {
|
|
1549
1816
|
const plan = result.plan;
|
|
1550
|
-
if (opts.stage) {
|
|
1817
|
+
if (result.staged || opts.stage) {
|
|
1551
1818
|
info(`Staged install plan: ${result.stagePath}`);
|
|
1552
|
-
|
|
1819
|
+
if (result.stageOnly) {
|
|
1820
|
+
info(`stage_only asset: files were written only under ${path.dirname(result.stagePath)}; no Codex skill was activated.`);
|
|
1821
|
+
} else {
|
|
1822
|
+
info(`No Codex skill files were written. Re-run with --approve-mcp or --yes to install.`);
|
|
1823
|
+
}
|
|
1553
1824
|
} else if (opts.dryRun) {
|
|
1554
1825
|
info(`Dry run: ${plan.files.length} file(s) would be installed to ${CODEX_SKILLS_DIR}`);
|
|
1555
1826
|
for (const file of plan.files) {
|
|
@@ -1764,6 +2035,12 @@ async function cmdList() {
|
|
|
1764
2035
|
}
|
|
1765
2036
|
|
|
1766
2037
|
async function cmdUpdate() {
|
|
2038
|
+
const args = parseArgs(process.argv);
|
|
2039
|
+
if (args.flags.target || args.flags.all || args.flags.force) {
|
|
2040
|
+
await cmdSyncInstalled();
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1767
2044
|
const uuid = process.argv[3];
|
|
1768
2045
|
if (!uuid) error('Usage: tokrepo update <uuid> [file]');
|
|
1769
2046
|
|
|
@@ -1833,9 +2110,11 @@ function tagMatchesTypes(workflow, requestedTypes) {
|
|
|
1833
2110
|
if (!requestedTypes || requestedTypes.length === 0) return true;
|
|
1834
2111
|
const tags = (workflow.tags || []).flatMap(t => [t.slug, t.name]).filter(Boolean).map(t => String(t).toLowerCase());
|
|
1835
2112
|
const assetType = getWorkflowAssetType(workflow);
|
|
2113
|
+
const metadataKind = String(workflow.asset_kind || workflow.agent_metadata?.asset_kind || workflow.agentMetadata?.assetKind || '').toLowerCase();
|
|
1836
2114
|
return requestedTypes.some(type => {
|
|
1837
2115
|
const needle = String(type).trim().toLowerCase();
|
|
1838
2116
|
if (!needle) return false;
|
|
2117
|
+
if (metadataKind === needle || metadataKind === `${needle}s`) return true;
|
|
1839
2118
|
if (assetType === needle || assetType === `${needle}s`) return true;
|
|
1840
2119
|
return tags.some(tag => tag === needle || tag === `${needle}s` || tag.includes(needle));
|
|
1841
2120
|
});
|
|
@@ -2116,17 +2395,17 @@ async function cmdSyncInstalled() {
|
|
|
2116
2395
|
|
|
2117
2396
|
const manifest = readCodexManifest();
|
|
2118
2397
|
const installed = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
|
|
2398
|
+
const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
|
|
2399
|
+
const stage = Boolean(args.flags.stage);
|
|
2119
2400
|
if (installed.length === 0) {
|
|
2120
|
-
if (json) outputJson({ targetTool: 'codex', count: 0, results: [] });
|
|
2401
|
+
if (json) outputJson({ targetTool: 'codex', manifestPath: CODEX_MANIFEST_FILE, dryRun, stage, count: 0, results: [] });
|
|
2121
2402
|
else info(`No Codex installs found in ${CODEX_MANIFEST_FILE}`);
|
|
2122
2403
|
return;
|
|
2123
2404
|
}
|
|
2124
2405
|
|
|
2125
2406
|
const config = readConfig();
|
|
2126
2407
|
const apiBase = config?.api || DEFAULT_API;
|
|
2127
|
-
const
|
|
2128
|
-
const stage = Boolean(args.flags.stage);
|
|
2129
|
-
const force = Boolean(args.flags.update || args.flags.force);
|
|
2408
|
+
const force = Boolean(args.flags.update || args.flags.force || args.flags.all);
|
|
2130
2409
|
const results = [];
|
|
2131
2410
|
|
|
2132
2411
|
for (let i = 0; i < installed.length; i++) {
|
|
@@ -2218,6 +2497,126 @@ async function cmdSyncInstalled() {
|
|
|
2218
2497
|
}
|
|
2219
2498
|
}
|
|
2220
2499
|
|
|
2500
|
+
async function cmdInstalled() {
|
|
2501
|
+
const args = parseArgs(process.argv);
|
|
2502
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
2503
|
+
if (targetTool !== 'codex') error(`installed currently supports --target codex only`);
|
|
2504
|
+
|
|
2505
|
+
const json = Boolean(args.flags.json);
|
|
2506
|
+
if (!json) log(`\n${C.bold}tokrepo installed${C.reset}\n`);
|
|
2507
|
+
|
|
2508
|
+
const manifest = readCodexManifest();
|
|
2509
|
+
const records = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
|
|
2510
|
+
const list = records.map(record => {
|
|
2511
|
+
const files = (record.installedFiles || record.installed_files || []).map(file => {
|
|
2512
|
+
const actualSha = file.path && fs.existsSync(file.path) ? currentFileSha(file.path) : '';
|
|
2513
|
+
return {
|
|
2514
|
+
path: file.path,
|
|
2515
|
+
sourceName: file.sourceName || file.source_name,
|
|
2516
|
+
sha256: file.sha256,
|
|
2517
|
+
exists: Boolean(file.path && fs.existsSync(file.path)),
|
|
2518
|
+
changed: Boolean(actualSha && file.sha256 && actualSha !== file.sha256),
|
|
2519
|
+
};
|
|
2520
|
+
});
|
|
2521
|
+
return {
|
|
2522
|
+
uuid: record.uuid,
|
|
2523
|
+
title: record.title,
|
|
2524
|
+
sourceUrl: record.sourceUrl || record.source_url,
|
|
2525
|
+
targetTool: 'codex',
|
|
2526
|
+
installMode: record.installMode || record.install_mode,
|
|
2527
|
+
installedAt: record.installedAt || record.installed_at,
|
|
2528
|
+
contentHash: record.contentHash || record.content_hash || '',
|
|
2529
|
+
risks: record.risks || [],
|
|
2530
|
+
files,
|
|
2531
|
+
status: files.some(file => !file.exists) ? 'missing-files' : files.some(file => file.changed) ? 'local-changes' : 'installed',
|
|
2532
|
+
};
|
|
2533
|
+
});
|
|
2534
|
+
|
|
2535
|
+
if (json) {
|
|
2536
|
+
outputJson({ targetTool: 'codex', manifestPath: CODEX_MANIFEST_FILE, count: list.length, list });
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
if (list.length === 0) {
|
|
2541
|
+
info(`No Codex installs found in ${CODEX_MANIFEST_FILE}`);
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
for (const item of list) {
|
|
2546
|
+
const color = item.status === 'installed' ? C.green : C.yellow;
|
|
2547
|
+
log(` ${color}${item.status}${C.reset} ${C.bold}${item.title || item.uuid}${C.reset}`);
|
|
2548
|
+
log(` ${C.dim}${item.uuid} · ${item.installMode || 'unknown'} · ${item.files.length} file(s)${C.reset}\n`);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
async function cmdOutdated() {
|
|
2553
|
+
const args = parseArgs(process.argv);
|
|
2554
|
+
const targetTool = validateInstallTarget(args.flags.target || 'codex');
|
|
2555
|
+
if (targetTool !== 'codex') error(`outdated currently supports --target codex only`);
|
|
2556
|
+
|
|
2557
|
+
const json = Boolean(args.flags.json);
|
|
2558
|
+
if (!json) log(`\n${C.bold}tokrepo outdated${C.reset}\n`);
|
|
2559
|
+
|
|
2560
|
+
const manifest = readCodexManifest();
|
|
2561
|
+
const installed = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
|
|
2562
|
+
if (installed.length === 0) {
|
|
2563
|
+
if (json) outputJson({ targetTool: 'codex', count: 0, outdated: 0, list: [] });
|
|
2564
|
+
else info(`No Codex installs found in ${CODEX_MANIFEST_FILE}`);
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
const config = readConfig();
|
|
2569
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
2570
|
+
const list = [];
|
|
2571
|
+
let unchanged = 0;
|
|
2572
|
+
let failed = 0;
|
|
2573
|
+
|
|
2574
|
+
for (const record of installed) {
|
|
2575
|
+
try {
|
|
2576
|
+
const { workflow, contents } = await fetchWorkflowForInstall(record.uuid, config, apiBase);
|
|
2577
|
+
const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode });
|
|
2578
|
+
const diff = diffCodexPlanWithLocal(plan, record);
|
|
2579
|
+
if (diff.needsUpdate) {
|
|
2580
|
+
list.push({
|
|
2581
|
+
uuid: record.uuid,
|
|
2582
|
+
title: workflow.title,
|
|
2583
|
+
status: 'outdated',
|
|
2584
|
+
reasons: diff.reasons,
|
|
2585
|
+
plan: publicInstallPlan(plan),
|
|
2586
|
+
});
|
|
2587
|
+
} else {
|
|
2588
|
+
unchanged++;
|
|
2589
|
+
}
|
|
2590
|
+
} catch (e) {
|
|
2591
|
+
failed++;
|
|
2592
|
+
list.push({ uuid: record.uuid, title: record.title || record.uuid, status: 'failed', error: e.message });
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
if (json) {
|
|
2597
|
+
outputJson({ targetTool: 'codex', manifestPath: CODEX_MANIFEST_FILE, count: installed.length, outdated: list.filter(i => i.status === 'outdated').length, unchanged, failed, list });
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
const outdated = list.filter(item => item.status === 'outdated');
|
|
2602
|
+
if (outdated.length === 0 && failed === 0) {
|
|
2603
|
+
success(`All ${unchanged} Codex install(s) are up to date.`);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
for (const item of list) {
|
|
2607
|
+
if (item.status === 'failed') {
|
|
2608
|
+
warn(`${item.title}: ${item.error}`);
|
|
2609
|
+
} else {
|
|
2610
|
+
log(` ${C.yellow}outdated${C.reset} ${C.bold}${item.title}${C.reset}`);
|
|
2611
|
+
for (const reason of item.reasons.slice(0, 3)) {
|
|
2612
|
+
log(` ${C.dim}${reason.type}: ${reason.path || ''}${C.reset}`);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
log('');
|
|
2617
|
+
info(`Run ${C.cyan}tokrepo update --target codex --all${C.reset} to update installed Codex assets.`);
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2221
2620
|
async function cmdTags() {
|
|
2222
2621
|
log(`\n${C.bold}tokrepo tags${C.reset}\n`);
|
|
2223
2622
|
|
|
@@ -2337,16 +2736,20 @@ ${C.bold}USAGE${C.reset}
|
|
|
2337
2736
|
${C.bold}DISCOVER & INSTALL${C.reset}
|
|
2338
2737
|
${C.cyan}search${C.reset} <query> Search assets by keyword
|
|
2339
2738
|
${C.cyan}detail${C.reset} <name|uuid> Show full asset metadata
|
|
2739
|
+
${C.cyan}plan${C.reset} <name|uuid> Print agent-native Codex install plan
|
|
2340
2740
|
${C.cyan}install${C.reset} <name|uuid> Smart install (auto-detects type & placement)
|
|
2341
2741
|
${C.cyan}pull${C.reset} <url|uuid|@u/n> Download raw asset files
|
|
2342
2742
|
${C.cyan}clone${C.reset} @username Clone all assets from a user
|
|
2743
|
+
${C.cyan}installed${C.reset} List installed Codex assets from manifest
|
|
2744
|
+
${C.cyan}outdated${C.reset} Check installed Codex assets for updates
|
|
2343
2745
|
${C.cyan}sync-installed${C.reset} Update installed Codex assets from manifest
|
|
2344
2746
|
|
|
2345
2747
|
${C.bold}PUBLISH${C.reset}
|
|
2346
2748
|
${C.cyan}push${C.reset} [files...] Push files/directory (idempotent upsert)
|
|
2347
2749
|
${C.cyan}status${C.reset} Compare local vs remote (like git status)
|
|
2348
2750
|
${C.cyan}init${C.reset} Create .tokrepo.json project config
|
|
2349
|
-
${C.cyan}update${C.reset} <uuid> [f] Update existing asset
|
|
2751
|
+
${C.cyan}update${C.reset} <uuid> [f] Update existing remote asset
|
|
2752
|
+
${C.cyan}update${C.reset} --target codex --all Update installed Codex assets
|
|
2350
2753
|
${C.cyan}delete${C.reset} <uuid> Delete an asset
|
|
2351
2754
|
|
|
2352
2755
|
${C.bold}ACCOUNT${C.reset}
|
|
@@ -2362,6 +2765,9 @@ ${C.bold}PUSH OPTIONS${C.reset}
|
|
|
2362
2765
|
${C.cyan}--title${C.reset} "..." Set title (auto-detected from README or dir name)
|
|
2363
2766
|
${C.cyan}--desc${C.reset} "..." Set description
|
|
2364
2767
|
${C.cyan}--tag${C.reset} Skills Add tag (repeatable)
|
|
2768
|
+
${C.cyan}--kind${C.reset} skill Set agent asset_kind
|
|
2769
|
+
${C.cyan}--target${C.reset} codex Add target tool metadata on push
|
|
2770
|
+
${C.cyan}--install-mode${C.reset} bundle Set install_mode metadata
|
|
2365
2771
|
|
|
2366
2772
|
${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
2367
2773
|
Skills → .claude/skills/ (if .claude/ exists)
|
|
@@ -2376,12 +2782,17 @@ ${C.bold}EXAMPLES${C.reset}
|
|
|
2376
2782
|
tokrepo search "mcp server" # Find MCP configs
|
|
2377
2783
|
tokrepo search video --json # Machine-readable search
|
|
2378
2784
|
tokrepo detail ca000374-f5d8-... --json # Machine-readable detail
|
|
2785
|
+
tokrepo plan 91aeb22d-eff0-4310-... # Install plan v2 for agents
|
|
2379
2786
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
2380
2787
|
tokrepo install ca000374-f5d8-... --target codex
|
|
2381
2788
|
tokrepo install c4b18aeb --target gemini # Install for Gemini CLI
|
|
2382
2789
|
tokrepo clone @henuwangkai --target codex --keyword video
|
|
2790
|
+
tokrepo installed --target codex --json
|
|
2791
|
+
tokrepo outdated --target codex --json
|
|
2792
|
+
tokrepo update --target codex --all
|
|
2383
2793
|
tokrepo sync-installed --target codex --dry-run
|
|
2384
2794
|
tokrepo push --private my-rules.md # Save one file privately
|
|
2795
|
+
tokrepo push . --kind skill --target codex --install-mode bundle
|
|
2385
2796
|
tokrepo push --public skill.md # Share one file publicly
|
|
2386
2797
|
tokrepo push --private . # Push current dir as private
|
|
2387
2798
|
tokrepo push --public --title "My MCP" . # Push dir publicly with title
|
|
@@ -2451,6 +2862,23 @@ EXAMPLES
|
|
|
2451
2862
|
`);
|
|
2452
2863
|
}
|
|
2453
2864
|
|
|
2865
|
+
function showPlanHelp() {
|
|
2866
|
+
log(`
|
|
2867
|
+
${C.bold}tokrepo plan${C.reset}
|
|
2868
|
+
|
|
2869
|
+
USAGE
|
|
2870
|
+
tokrepo plan <uuid|url|name> [--target codex] [--stage]
|
|
2871
|
+
|
|
2872
|
+
OUTPUT
|
|
2873
|
+
Machine-readable install plan v2 with preconditions, actions, policyDecision,
|
|
2874
|
+
rollback, postVerify, risk metadata, and destination file hashes.
|
|
2875
|
+
|
|
2876
|
+
EXAMPLES
|
|
2877
|
+
tokrepo plan 91aeb22d-eff0-4310-abc6-811d2394b420
|
|
2878
|
+
tokrepo plan https://tokrepo.com/en/workflows/91aeb22d-eff0-4310-abc6-811d2394b420
|
|
2879
|
+
`);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2454
2882
|
function showListHelp() {
|
|
2455
2883
|
log(`
|
|
2456
2884
|
${C.bold}tokrepo list${C.reset}
|
|
@@ -2484,6 +2912,9 @@ ${C.bold}tokrepo sync-installed${C.reset}
|
|
|
2484
2912
|
|
|
2485
2913
|
USAGE
|
|
2486
2914
|
tokrepo sync-installed --target codex [--dry-run] [--stage] [--update] [--approve-mcp] [--json]
|
|
2915
|
+
tokrepo installed --target codex [--json]
|
|
2916
|
+
tokrepo outdated --target codex [--json]
|
|
2917
|
+
tokrepo update --target codex --all [--stage] [--approve-mcp] [--json]
|
|
2487
2918
|
|
|
2488
2919
|
BEHAVIOR
|
|
2489
2920
|
Reads ~/.codex/tokrepo/install-manifest.json, fetches each TokRepo asset again,
|
|
@@ -2491,6 +2922,9 @@ BEHAVIOR
|
|
|
2491
2922
|
changed or missing files. Use --update to force reinstall unchanged assets.
|
|
2492
2923
|
|
|
2493
2924
|
EXAMPLES
|
|
2925
|
+
tokrepo installed --target codex --json
|
|
2926
|
+
tokrepo outdated --target codex --json
|
|
2927
|
+
tokrepo update --target codex --all
|
|
2494
2928
|
tokrepo sync-installed --target codex --dry-run --json
|
|
2495
2929
|
tokrepo sync-installed --target codex --stage
|
|
2496
2930
|
tokrepo sync-installed --target codex --update --approve-mcp
|
|
@@ -2504,6 +2938,8 @@ function showCommandHelp(command) {
|
|
|
2504
2938
|
showSearchHelp(); break;
|
|
2505
2939
|
case 'detail':
|
|
2506
2940
|
showDetailHelp(); break;
|
|
2941
|
+
case 'plan':
|
|
2942
|
+
showPlanHelp(); break;
|
|
2507
2943
|
case 'install':
|
|
2508
2944
|
case 'i':
|
|
2509
2945
|
showInstallHelp(); break;
|
|
@@ -2513,6 +2949,8 @@ function showCommandHelp(command) {
|
|
|
2513
2949
|
showCloneHelp(); break;
|
|
2514
2950
|
case 'sync-installed':
|
|
2515
2951
|
case 'sync':
|
|
2952
|
+
case 'installed':
|
|
2953
|
+
case 'outdated':
|
|
2516
2954
|
showSyncInstalledHelp(); break;
|
|
2517
2955
|
default:
|
|
2518
2956
|
showHelp(); break;
|
|
@@ -2537,11 +2975,14 @@ async function main() {
|
|
|
2537
2975
|
case 'pull': await cmdPull(); break;
|
|
2538
2976
|
case 'search': case 'find': await cmdSearch(); break;
|
|
2539
2977
|
case 'detail': await cmdDetail(); break;
|
|
2978
|
+
case 'plan': await cmdPlan(); break;
|
|
2540
2979
|
case 'install': case 'i': await cmdInstall(); break;
|
|
2541
2980
|
case 'list': await cmdList(); break;
|
|
2542
2981
|
case 'update': await cmdUpdate(); break;
|
|
2543
2982
|
case 'delete': await cmdDelete(); break;
|
|
2544
2983
|
case 'clone': await cmdClone(); break;
|
|
2984
|
+
case 'installed': await cmdInstalled(); break;
|
|
2985
|
+
case 'outdated': await cmdOutdated(); break;
|
|
2545
2986
|
case 'sync-installed': case 'sync': await cmdSyncInstalled(); break;
|
|
2546
2987
|
case 'tags': await cmdTags(); break;
|
|
2547
2988
|
case 'status': case 'diff': await cmdStatus(); break;
|
|
@@ -2555,7 +2996,7 @@ async function main() {
|
|
|
2555
2996
|
}
|
|
2556
2997
|
|
|
2557
2998
|
// Non-blocking update check after command completes
|
|
2558
|
-
if (!wantsJson(process.argv) && !args.flags.help) {
|
|
2999
|
+
if (command !== 'plan' && !wantsJson(process.argv) && !args.flags.help) {
|
|
2559
3000
|
checkForUpdate();
|
|
2560
3001
|
}
|
|
2561
3002
|
}
|