sneakoscope 1.18.13 → 1.19.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/README.md +36 -10
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/build-manifest.json +11 -9
- package/dist/cli/command-registry.d.ts +3 -0
- package/dist/cli/command-registry.js +3 -1
- package/dist/cli/install-helpers.d.ts +148 -3
- package/dist/cli/install-helpers.js +311 -121
- package/dist/commands/codex-lb.js +77 -1
- package/dist/commands/doctor.js +24 -0
- package/dist/commands/mad-sks.d.ts +1 -0
- package/dist/commands/mad-sks.js +3 -3
- package/dist/commands/versioning.js +2 -1
- package/dist/core/agents/agent-effort-policy.d.ts +6 -0
- package/dist/core/agents/agent-effort-policy.js +42 -0
- package/dist/core/agents/agent-orchestrator.js +9 -5
- package/dist/core/agents/agent-roster.d.ts +52 -4
- package/dist/core/agents/agent-roster.js +121 -11
- package/dist/core/agents/agent-scheduler.d.ts +2 -1
- package/dist/core/agents/agent-scheduler.js +7 -5
- package/dist/core/agents/agent-schema.d.ts +5 -0
- package/dist/core/agents/agent-schema.js +5 -0
- package/dist/core/codex/codex-config-eperm-repair.d.ts +1 -0
- package/dist/core/codex/codex-config-eperm-repair.js +20 -2
- package/dist/core/codex/codex-config-readability.js +31 -1
- package/dist/core/codex/codex-project-config-policy.d.ts +23 -0
- package/dist/core/codex/codex-project-config-policy.js +191 -8
- package/dist/core/commands/basic-cli.js +22 -5
- package/dist/core/commands/mad-sks-command.d.ts +1 -0
- package/dist/core/commands/mad-sks-command.js +17 -1
- package/dist/core/commands/naruto-command.d.ts +2 -0
- package/dist/core/commands/naruto-command.js +189 -0
- package/dist/core/feature-fixtures.js +3 -0
- package/dist/core/fsx.d.ts +1 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +1 -1
- package/dist/core/preflight/parallel-preflight-engine.d.ts +1 -0
- package/dist/core/routes.js +17 -1
- package/dist/core/version-manager.js +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-launcher.js +4 -7
- package/dist/core/zellij/zellij-layout-builder.js +1 -1
- package/dist/scripts/release-parallel-check.js +5 -0
- package/package.json +8 -2
- package/scripts/codex-config-load-probe.mjs +245 -0
|
@@ -25,7 +25,7 @@ const DEFAULT_CODEX_APP_PLUGINS = [
|
|
|
25
25
|
function packagedSksEntrypoint() {
|
|
26
26
|
return path.join(packageRoot(), 'dist', 'bin', 'sks.js');
|
|
27
27
|
}
|
|
28
|
-
export async function postinstall({ bootstrap }) {
|
|
28
|
+
export async function postinstall({ bootstrap, args = [] }) {
|
|
29
29
|
const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
|
|
30
30
|
const conflictScan = await scanHarnessConflicts(installRoot);
|
|
31
31
|
if (conflictScan.hard_block) {
|
|
@@ -33,88 +33,104 @@ export async function postinstall({ bootstrap }) {
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
const codexLbConfigSnapshot = await capturePostinstallCodexLbConfigSnapshot();
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
console.log(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
36
|
+
// A failed setup side-effect must never fail `npm i`. Wrap the whole flow; always
|
|
37
|
+
// restore the codex-lb snapshot in finally (even on the early bootstrap return / on throw).
|
|
38
|
+
try {
|
|
39
|
+
console.log('\nSKS installed.');
|
|
40
|
+
const shim = await ensureSksCommandDuringInstall();
|
|
41
|
+
if (shim.status === 'present')
|
|
42
|
+
console.log(`SKS command: available (${shim.command ?? 'unknown'}).`);
|
|
43
|
+
else if (shim.status === 'repaired')
|
|
44
|
+
console.log(`SKS command: stale PATH shim repaired (${shim.command ?? 'unknown'}).`);
|
|
45
|
+
else if (shim.status === 'created')
|
|
46
|
+
console.log(`SKS command: shim created at ${shim.command ?? 'unknown'}.`);
|
|
47
|
+
else if (shim.status === 'created_not_on_path')
|
|
48
|
+
console.log(`SKS command: shim created at ${shim.command ?? 'unknown'}. Add ${path.dirname(shim.command ?? '')} to PATH, or run npx -y -p sneakoscope sks.`);
|
|
49
|
+
else if (shim.status === 'skipped')
|
|
50
|
+
console.log(`SKS command: skipped (${shim.reason}).`);
|
|
51
|
+
else
|
|
52
|
+
console.log(`SKS command: shim unavailable. Use npx -y -p sneakoscope sks. ${shim.error || ''}`.trim());
|
|
53
|
+
const context7Install = await ensureGlobalContext7DuringInstall();
|
|
54
|
+
if (context7Install.status === 'present')
|
|
55
|
+
console.log('Context7 MCP: already configured for Codex.');
|
|
56
|
+
else if (context7Install.status === 'installed')
|
|
57
|
+
console.log('Context7 MCP: configured for Codex.');
|
|
58
|
+
else if (context7Install.status === 'codex_missing')
|
|
59
|
+
console.log('Context7 MCP: Codex CLI missing. Install @openai/codex or set SKS_CODEX_BIN, then run `sks context7 setup --scope global` or `sks setup` in a project.');
|
|
60
|
+
else if (context7Install.status === 'skipped')
|
|
61
|
+
console.log(`Context7 MCP: skipped (${context7Install.reason}).`);
|
|
62
|
+
else if (context7Install.status === 'failed')
|
|
63
|
+
console.log(`Context7 MCP: auto setup failed. Run \`sks context7 setup --scope global\` or \`sks setup\`. ${context7Install.error || ''}`.trim());
|
|
64
|
+
const fastModeRepair = await ensureGlobalCodexFastModeDuringInstall();
|
|
65
|
+
if (fastModeRepair.status === 'updated')
|
|
66
|
+
console.log(`Codex App Fast mode: updated ${fastModeRepair.config_path}${fastModeRepair.backup_path ? ` (backup ${fastModeRepair.backup_path})` : ''}.`);
|
|
67
|
+
else if (fastModeRepair.status === 'present')
|
|
68
|
+
console.log('Codex App Fast mode: config already compatible.');
|
|
69
|
+
else if (fastModeRepair.status === 'unparseable_config_preserved')
|
|
70
|
+
console.log(`Codex App Fast mode: existing ${fastModeRepair.config_path} is not valid TOML — left untouched, backed up to ${fastModeRepair.backup_path}. Run \`sks doctor --fix\` to recover it.`);
|
|
71
|
+
else if (fastModeRepair.status === 'skipped_unsafe_rewrite')
|
|
72
|
+
console.log(`Codex App Fast mode: skipped (managed rewrite would not parse; ${fastModeRepair.config_path} left untouched).`);
|
|
73
|
+
else if (fastModeRepair.status === 'skipped')
|
|
74
|
+
console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
|
|
75
|
+
else if (fastModeRepair.status === 'failed')
|
|
76
|
+
console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
|
|
77
|
+
// Terminating a third-party app's processes during `npm i` is unsafe by default; opt-in only.
|
|
78
|
+
const appProcessRepair = process.env.SKS_POSTINSTALL_RECONCILE_APP_PROCESSES === '1'
|
|
79
|
+
? await reconcileCodexAppUpgradeProcesses()
|
|
80
|
+
: { status: 'skipped', reason: 'opt_in_required', killed: [] };
|
|
81
|
+
if (appProcessRepair.status === 'repaired')
|
|
82
|
+
console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es). Restart Codex App to reconnect cleanly.`);
|
|
83
|
+
else if (appProcessRepair.status === 'partial')
|
|
84
|
+
console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es); ${(appProcessRepair.failed ?? []).length} could not be stopped. Restart Codex App if reconnecting continues.`);
|
|
85
|
+
else if (appProcessRepair.status === 'skipped' && appProcessRepair.reason === 'opt_in_required')
|
|
86
|
+
console.log('Codex App reconnect repair: not run (set SKS_POSTINSTALL_RECONCILE_APP_PROCESSES=1 to allow postinstall to stop stale orphan app-server processes; otherwise run `sks doctor --fix`).');
|
|
87
|
+
else if (appProcessRepair.status === 'skipped' && appProcessRepair.reason !== 'platform')
|
|
88
|
+
console.log(`Codex App reconnect repair: skipped (${appProcessRepair.reason}).`);
|
|
89
|
+
else if (appProcessRepair.status === 'failed')
|
|
90
|
+
console.log(`Codex App reconnect repair: skipped (${appProcessRepair.error || appProcessRepair.reason || 'process check failed'}).`);
|
|
91
|
+
const globalSkills = await ensureGlobalCodexSkillsDuringInstall();
|
|
92
|
+
if (globalSkills.status === 'installed') {
|
|
93
|
+
const removed = globalSkills.removed_stale_generated_skills || [];
|
|
94
|
+
const cleanup = removed.length ? ` Removed stale generated skill shadow(s): ${removed.join(', ')}.` : '';
|
|
95
|
+
console.log(`Codex App global $ skills: installed in ${globalSkills.root} (${globalSkills.installed_count} skills).${cleanup}`);
|
|
96
|
+
}
|
|
97
|
+
else if (globalSkills.status === 'partial')
|
|
98
|
+
console.log(`Codex App global $ skills: partial in ${globalSkills.root}; missing ${(globalSkills.missing_skills ?? []).join(', ')}. Run \`sks doctor --fix\`.`);
|
|
99
|
+
else if (globalSkills.status === 'skipped')
|
|
100
|
+
console.log(`Codex App global $ skills: skipped (${globalSkills.reason}).`);
|
|
101
|
+
else if (globalSkills.status === 'failed')
|
|
102
|
+
console.log(`Codex App global $ skills: auto setup failed. Run \`sks doctor --fix\`. ${globalSkills.error || ''}`.trim());
|
|
103
|
+
const getdesignSkill = await ensureGlobalGetdesignSkillDuringInstall();
|
|
104
|
+
if (getdesignSkill.status === 'installed')
|
|
105
|
+
console.log('getdesign Codex skill: installed.');
|
|
106
|
+
else if (getdesignSkill.status === 'present')
|
|
107
|
+
console.log('getdesign Codex skill: already available.');
|
|
108
|
+
else if (getdesignSkill.status === 'skills_cli_missing')
|
|
109
|
+
console.log(`getdesign Codex skill: skills CLI missing; generated getdesign-reference skill is installed. Later run \`${getdesignSkill.install}\` if the skills CLI is available.`);
|
|
110
|
+
else if (getdesignSkill.status === 'skipped')
|
|
111
|
+
console.log(`getdesign Codex skill: skipped (${getdesignSkill.reason}).`);
|
|
112
|
+
else if (getdesignSkill.status === 'failed')
|
|
113
|
+
console.log(`getdesign Codex skill: auto setup failed; generated getdesign-reference skill remains available. ${getdesignSkill.error || ''}`.trim());
|
|
114
|
+
const bootstrapDecision = await postinstallBootstrapDecision(installRoot);
|
|
115
|
+
if (bootstrapDecision.run) {
|
|
116
|
+
console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
|
|
117
|
+
await runPostinstallBootstrap(installRoot, bootstrap);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.log('\nNext:');
|
|
121
|
+
console.log(' sks bootstrap');
|
|
122
|
+
console.log(`\nSKS bootstrap was not run automatically: ${bootstrapDecision.reason}.`);
|
|
123
|
+
console.log('This initializes the current project, installs SKS Codex App skills, verifies Codex App/Context7 readiness, and checks Zellij runtime dependencies.');
|
|
124
|
+
console.log('Dependency repair: sks bootstrap --yes, sks deps check --yes, or sks --mad --yes. Postinstall reports missing CLI tools but does not mutate Homebrew/npm globals unless SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS=1 is set.');
|
|
125
|
+
console.log('Open runtime after readiness is green: sks\n');
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.log(`\nSKS postinstall: a setup step did not complete; installation continues. Run \`sks doctor --fix\` afterward. (${err?.message || err})`);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot).catch(() => { });
|
|
132
|
+
await reportPostinstallCodexLbAuth().catch(() => { });
|
|
109
133
|
}
|
|
110
|
-
await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot);
|
|
111
|
-
await reportPostinstallCodexLbAuth();
|
|
112
|
-
console.log('\nNext:');
|
|
113
|
-
console.log(' sks bootstrap');
|
|
114
|
-
console.log(`\nSKS bootstrap was not run automatically: ${bootstrapDecision.reason}.`);
|
|
115
|
-
console.log('This initializes the current project, installs SKS Codex App skills, verifies Codex App/Context7 readiness, and checks Zellij runtime dependencies.');
|
|
116
|
-
console.log('Dependency repair: sks deps check; install Zellij with brew install zellij when needed.');
|
|
117
|
-
console.log('Open runtime after readiness is green: sks\n');
|
|
118
134
|
}
|
|
119
135
|
async function reportPostinstallCodexLbAuth() {
|
|
120
136
|
const codexLbAuth = await ensureCodexLbAuthDuringInstall();
|
|
@@ -128,6 +144,8 @@ async function reportPostinstallCodexLbAuth() {
|
|
|
128
144
|
console.log('codex-lb auth: stored key missing. Run `sks codex-lb setup --host <domain> --api-key <key>` to repair.');
|
|
129
145
|
else if (codexLbAuth.status === 'missing_base_url')
|
|
130
146
|
console.log('codex-lb auth: stored key has no recoverable base URL. Run `sks codex-lb reconfigure --host <domain> --api-key <key>` once.');
|
|
147
|
+
else if (codexLbAuth.status === 'not_configured')
|
|
148
|
+
console.log('codex-lb (optional multi-account load balancer): not configured — opt in anytime with `sks codex-lb setup` (your choice; never applied automatically, never edits your Codex config without it). Swap key later: `sks codex-lb set-key`; switch auth: `sks codex-lb use-oauth` / `use-codex-lb`.');
|
|
131
149
|
else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured')
|
|
132
150
|
console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
|
|
133
151
|
const reconcile = codexLbAuth.auth_reconcile;
|
|
@@ -197,6 +215,12 @@ export async function postinstallBootstrapDecision(root) {
|
|
|
197
215
|
const target = candidate ? installRoot : globalSksRoot();
|
|
198
216
|
if (process.env.SKS_POSTINSTALL_BOOTSTRAP === '1')
|
|
199
217
|
return { run: true, target, reason: 'forced by SKS_POSTINSTALL_BOOTSTRAP=1' };
|
|
218
|
+
// A global `npm i -g sneakoscope` must NOT initialize whatever project the user's shell
|
|
219
|
+
// happened to be in (that would scribble AGENTS.md/.codex/.agents into an unrelated repo).
|
|
220
|
+
// Only bootstrap the global runtime root; the user runs `sks setup` inside a project explicitly.
|
|
221
|
+
if (process.env.npm_config_global === 'true' && candidate) {
|
|
222
|
+
return { run: true, target: globalSksRoot(), reason: 'global install: bootstrapping global SKS runtime only (run `sks setup` inside a project to initialize it)' };
|
|
223
|
+
}
|
|
200
224
|
if (candidate)
|
|
201
225
|
return { run: true, target, reason: 'auto-running sks setup --bootstrap --install-scope global --force' };
|
|
202
226
|
return { run: true, target, reason: 'no project marker found; auto-running global SKS runtime bootstrap' };
|
|
@@ -267,8 +291,8 @@ async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
|
|
|
267
291
|
const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, snapshot.base_url));
|
|
268
292
|
const alreadyOk = next === ensureTrailingNewline(current) && codexLbProviderBaseUrl(current);
|
|
269
293
|
if (!alreadyOk) {
|
|
270
|
-
await
|
|
271
|
-
configRestored = true;
|
|
294
|
+
const safeWrite = await safeWriteCodexConfigToml(snapshot.config_path, current, next, 'codex-lb-restore');
|
|
295
|
+
configRestored = safeWrite.ok && safeWrite.changed === true;
|
|
272
296
|
}
|
|
273
297
|
}
|
|
274
298
|
// Restore auth.json only if bootstrap accidentally wiped or emptied a pre-existing auth.json.
|
|
@@ -347,8 +371,10 @@ export async function configureCodexLb(opts = {}) {
|
|
|
347
371
|
await ensureDir(path.dirname(configPath));
|
|
348
372
|
const current = await readText(configPath, '');
|
|
349
373
|
const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl, useDefaultProvider));
|
|
350
|
-
await
|
|
351
|
-
|
|
374
|
+
const safeWrite = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb');
|
|
375
|
+
if (!safeWrite.ok)
|
|
376
|
+
return { ok: false, status: safeWrite.status, config_path: configPath, env_path: envPath, backup_path: safeWrite.backup_path };
|
|
377
|
+
appliedActions.push({ type: 'write_config_provider', target: configPath, ok: true, backup_path: safeWrite.backup_path });
|
|
352
378
|
if (useDefaultProvider)
|
|
353
379
|
appliedActions.push({ type: 'select_default_provider', target: configPath, ok: true });
|
|
354
380
|
if (writeEnvFile) {
|
|
@@ -789,8 +815,8 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
789
815
|
if (status.env_key_configured && status.base_url && (!status.ok || !status.selected || !status.provider_uses_codex_lb_env_auth || legacyAuthMigrated || hasTopLevelCodexModeLock(currentConfig))) {
|
|
790
816
|
await ensureDir(path.dirname(status.config_path));
|
|
791
817
|
const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(currentConfig, status.base_url));
|
|
792
|
-
await
|
|
793
|
-
configRepaired = true;
|
|
818
|
+
const safeWrite = await safeWriteCodexConfigToml(status.config_path, currentConfig, next, 'codex-lb-repair');
|
|
819
|
+
configRepaired = safeWrite.ok && safeWrite.changed === true;
|
|
794
820
|
status = await codexLbStatus(opts);
|
|
795
821
|
}
|
|
796
822
|
if (!status.ok) {
|
|
@@ -1352,7 +1378,7 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
|
|
|
1352
1378
|
return { ok: true, status: 'synced' };
|
|
1353
1379
|
return { ok: false, status: 'login_failed', error: redactSecretText(login.stderr || login.stdout || 'codex login failed', [apiKey]).trim() };
|
|
1354
1380
|
}
|
|
1355
|
-
function upsertCodexLbConfig(text = '', baseUrl, selectDefault = true) {
|
|
1381
|
+
export function upsertCodexLbConfig(text = '', baseUrl, selectDefault = true) {
|
|
1356
1382
|
let next = selectDefault
|
|
1357
1383
|
? upsertTopLevelTomlString(text, 'model_provider', 'codex-lb')
|
|
1358
1384
|
: removeTopLevelTomlKeyIfValue(text, 'model_provider', 'codex-lb');
|
|
@@ -1442,41 +1468,61 @@ export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
|
|
|
1442
1468
|
try {
|
|
1443
1469
|
await ensureDir(path.dirname(configPath));
|
|
1444
1470
|
const current = await readText(configPath, '');
|
|
1471
|
+
// Safety gate 1: never blind-overwrite an unparseable user config — that would
|
|
1472
|
+
// entrench corruption on the file Codex actually loads. Back it up and bail.
|
|
1473
|
+
if (current.trim()) {
|
|
1474
|
+
const currentSmoke = codexConfigParseSmoke(current);
|
|
1475
|
+
if (!currentSmoke.ok) {
|
|
1476
|
+
const backupPath = await backupCodexConfig(configPath, current, 'unparseable');
|
|
1477
|
+
return { status: 'unparseable_config_preserved', config_path: configPath, backup_path: backupPath, parse_smoke: currentSmoke };
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1445
1480
|
const next = normalizeCodexFastModeUiConfig(current);
|
|
1446
1481
|
if (next === ensureTrailingNewline(current))
|
|
1447
1482
|
return { status: 'present', config_path: configPath };
|
|
1483
|
+
// Safety gate 2: never WRITE a config that would not parse.
|
|
1484
|
+
const nextSmoke = codexConfigParseSmoke(next);
|
|
1485
|
+
if (!nextSmoke.ok)
|
|
1486
|
+
return { status: 'skipped_unsafe_rewrite', config_path: configPath, parse_smoke: nextSmoke };
|
|
1487
|
+
// Safety gate 3: back up the user's good config before mutating it.
|
|
1488
|
+
const backupPath = current.trim() ? await backupCodexConfig(configPath, current, 'pre-update') : null;
|
|
1448
1489
|
await writeTextAtomic(configPath, next);
|
|
1449
|
-
return { status: 'updated', config_path: configPath };
|
|
1490
|
+
return { status: 'updated', config_path: configPath, backup_path: backupPath };
|
|
1450
1491
|
}
|
|
1451
1492
|
catch (err) {
|
|
1452
1493
|
return { status: 'failed', config_path: configPath, error: err.message };
|
|
1453
1494
|
}
|
|
1454
1495
|
}
|
|
1455
1496
|
export function normalizeCodexFastModeUiConfig(text = '') {
|
|
1456
|
-
|
|
1497
|
+
// Run to a fixed point so a second install is a true no-op (idempotent). The per-pass
|
|
1498
|
+
// table/whitespace normalization converges within one extra pass.
|
|
1499
|
+
return normalizeCodexFastModeUiConfigOnce(normalizeCodexFastModeUiConfigOnce(text));
|
|
1500
|
+
}
|
|
1501
|
+
function normalizeCodexFastModeUiConfigOnce(text = '') {
|
|
1502
|
+
// Preserve user-owned top-level scalars (model / service_tier / model_reasoning_effort):
|
|
1503
|
+
// SKS only supplies a default when the user has not chosen one, and never strips the
|
|
1504
|
+
// user's own reasoning effort. SKS continues to manage its own namespaced tables below
|
|
1505
|
+
// ([features], [profiles.sks-*], [user.fast_mode], [plugins]).
|
|
1506
|
+
let next = String(text || '');
|
|
1457
1507
|
next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
|
|
1458
1508
|
next = removeTomlTableKey(next, 'features', 'codex_hooks');
|
|
1459
|
-
next =
|
|
1460
|
-
next =
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
next =
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
next =
|
|
1474
|
-
next =
|
|
1475
|
-
next =
|
|
1476
|
-
next = upsertTomlTableKey(next, 'features', 'plugins = true');
|
|
1477
|
-
next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
|
|
1478
|
-
next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
|
|
1479
|
-
next = upsertTomlTableKey(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
|
|
1509
|
+
next = upsertTopLevelTomlStringIfAbsent(next, 'model', 'gpt-5.5');
|
|
1510
|
+
next = upsertTopLevelTomlStringIfAbsent(next, 'service_tier', 'fast');
|
|
1511
|
+
// Codex App feature flags / fast-mode UI / suppress-warning are SET-IF-ABSENT: a fresh
|
|
1512
|
+
// config still gets SKS's defaults, but SKS NEVER overrides (re-enables) a feature the
|
|
1513
|
+
// user disabled in the App, and never rejects-then-hides UI by forcing an unrecognized
|
|
1514
|
+
// flag on an older App build. This is what stops SKS from "removing/blocking" the App UI.
|
|
1515
|
+
next = upsertTopLevelTomlBooleanIfAbsent(next, 'suppress_unstable_features_warning', true);
|
|
1516
|
+
for (const featureLine of [
|
|
1517
|
+
'hooks = true', 'remote_control = true', 'multi_agent = true', 'fast_mode = true',
|
|
1518
|
+
'fast_mode_ui = true', 'codex_git_commit = true', 'computer_use = true', 'browser_use = true',
|
|
1519
|
+
'browser_use_external = true', 'image_generation = true', 'in_app_browser = true',
|
|
1520
|
+
'guardian_approval = true', 'tool_suggest = true', 'apps = true', 'plugins = true'
|
|
1521
|
+
])
|
|
1522
|
+
next = upsertTomlTableKeyIfAbsent(next, 'features', featureLine);
|
|
1523
|
+
next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'visible = true');
|
|
1524
|
+
next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'enabled = true');
|
|
1525
|
+
next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
|
|
1480
1526
|
next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model = "gpt-5.5"');
|
|
1481
1527
|
next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'service_tier = "fast"');
|
|
1482
1528
|
next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'approval_policy = "on-request"');
|
|
@@ -1492,9 +1538,17 @@ export function normalizeCodexFastModeUiConfig(text = '') {
|
|
|
1492
1538
|
next = upsertTomlTableKey(next, 'profiles.sks-research', 'approval_policy = "never"');
|
|
1493
1539
|
next = upsertTomlTableKey(next, 'profiles.sks-research', 'sandbox_mode = "workspace-write"');
|
|
1494
1540
|
next = upsertTomlTableKey(next, 'profiles.sks-research', 'model_reasoning_effort = "xhigh"');
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1541
|
+
// Plugin auto-enable is OPT-IN only. Force-writing `[plugins."name@marketplace"] enabled =
|
|
1542
|
+
// true` for marketplace plugins the App may not have installed (different build/channel)
|
|
1543
|
+
// makes the App reference plugins it cannot load -> broken/blocked plugin UI. It also
|
|
1544
|
+
// replaced the user's whole plugin table, reverting any `enabled = false` they set. By
|
|
1545
|
+
// default SKS leaves the user's [plugins] alone; opt in with SKS_MANAGE_CODEX_APP_PLUGINS=1.
|
|
1546
|
+
if (process.env.SKS_MANAGE_CODEX_APP_PLUGINS === '1') {
|
|
1547
|
+
for (const [name, marketplace] of DEFAULT_CODEX_APP_PLUGINS) {
|
|
1548
|
+
const table = `plugins."${name}@${marketplace}"`;
|
|
1549
|
+
if (!hasTomlTable(next, table))
|
|
1550
|
+
next = upsertTomlTable(next, table, `[${table}]\nenabled = true`);
|
|
1551
|
+
}
|
|
1498
1552
|
}
|
|
1499
1553
|
return ensureTrailingNewline(next);
|
|
1500
1554
|
}
|
|
@@ -1568,6 +1622,35 @@ function upsertTomlTableKey(text, table, line) {
|
|
|
1568
1622
|
lines.splice(end, 0, line);
|
|
1569
1623
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
1570
1624
|
}
|
|
1625
|
+
// True if [table] already declares `key` (so we never override a user's explicit value).
|
|
1626
|
+
function hasTomlTableKey(text, table, key) {
|
|
1627
|
+
const lines = String(text || '').split('\n');
|
|
1628
|
+
const header = `[${table}]`;
|
|
1629
|
+
const start = lines.findIndex((x) => x.trim() === header);
|
|
1630
|
+
if (start === -1)
|
|
1631
|
+
return false;
|
|
1632
|
+
const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
1633
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
1634
|
+
const ln = lines[i];
|
|
1635
|
+
if (ln === undefined)
|
|
1636
|
+
continue;
|
|
1637
|
+
if (/^\s*\[.+\]\s*$/.test(ln))
|
|
1638
|
+
break;
|
|
1639
|
+
if (keyRe.test(ln))
|
|
1640
|
+
return true;
|
|
1641
|
+
}
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
// Set a [table] key only when absent — preserves a Codex App feature the user toggled off
|
|
1645
|
+
// (so SKS never re-enables / re-surfaces UI the user hid). On a fresh config the key/table
|
|
1646
|
+
// is still created, preserving fresh-install enablement.
|
|
1647
|
+
function upsertTomlTableKeyIfAbsent(text, table, line) {
|
|
1648
|
+
const key = String(line).split('=')[0]?.trim() ?? '';
|
|
1649
|
+
return hasTomlTableKey(text, table, key) ? String(text || '') : upsertTomlTableKey(text, table, line);
|
|
1650
|
+
}
|
|
1651
|
+
function upsertTopLevelTomlBooleanIfAbsent(text, key, value) {
|
|
1652
|
+
return hasTopLevelTomlKey(text, key) ? String(text || '') : upsertTopLevelTomlBoolean(text, key, value);
|
|
1653
|
+
}
|
|
1571
1654
|
function ensureTrailingNewline(text = '') {
|
|
1572
1655
|
const value = String(text || '').trimEnd();
|
|
1573
1656
|
return value ? `${value}\n` : '';
|
|
@@ -1589,6 +1672,65 @@ function upsertTopLevelTomlString(text, key, value) {
|
|
|
1589
1672
|
lines.splice(end, 0, line);
|
|
1590
1673
|
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
1591
1674
|
}
|
|
1675
|
+
function hasTopLevelTomlKey(text, key) {
|
|
1676
|
+
const lines = String(text || '').split('\n');
|
|
1677
|
+
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
1678
|
+
const end = firstTable === -1 ? lines.length : firstTable;
|
|
1679
|
+
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
1680
|
+
for (let i = 0; i < end; i += 1) {
|
|
1681
|
+
if (typeof lines[i] === 'string' && pattern.test(lines[i]))
|
|
1682
|
+
return true;
|
|
1683
|
+
}
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
// Preserve a user's deliberate top-level scalar (model/service_tier/reasoning); only set
|
|
1687
|
+
// the SKS default when the key is ABSENT. This is what stops `npm i -g` from clobbering
|
|
1688
|
+
// a user's global Codex config on every update.
|
|
1689
|
+
function upsertTopLevelTomlStringIfAbsent(text, key, value) {
|
|
1690
|
+
return hasTopLevelTomlKey(text, key) ? String(text || '') : upsertTopLevelTomlString(text, key, value);
|
|
1691
|
+
}
|
|
1692
|
+
// Lightweight safety gate: detect clearly-broken TOML so we never overwrite (or produce)
|
|
1693
|
+
// an unparseable config that Codex itself would reject. Mirrors the project-config smoke.
|
|
1694
|
+
function codexConfigParseSmoke(text = '') {
|
|
1695
|
+
const str = String(text || '');
|
|
1696
|
+
const tripleTokens = (str.match(/"""|'''/g) || []).length;
|
|
1697
|
+
const unterminatedTriple = tripleTokens % 2 !== 0;
|
|
1698
|
+
const invalidHeader = str.split('\n').find((line) => /^\s*\[/.test(line) && !/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(line)) || null;
|
|
1699
|
+
return { ok: !unterminatedTriple && !invalidHeader, unterminated_multiline_string: unterminatedTriple, invalid_table_header: invalidHeader };
|
|
1700
|
+
}
|
|
1701
|
+
async function backupCodexConfig(configPath, text, tag) {
|
|
1702
|
+
try {
|
|
1703
|
+
const stamp = `${PACKAGE_VERSION}-${Date.now().toString(36)}`;
|
|
1704
|
+
const backupPath = `${configPath}.sks-${tag}-${stamp}.bak`;
|
|
1705
|
+
await writeTextAtomic(backupPath, text);
|
|
1706
|
+
return backupPath;
|
|
1707
|
+
}
|
|
1708
|
+
catch {
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
// Single TOML-safe gate for every codex-lb config write. Mirrors the fast-mode safety so the
|
|
1713
|
+
// codex-lb path can NEVER corrupt ~/.codex/config.toml on install (esp. a fresh/initial one):
|
|
1714
|
+
// - refuse to overwrite an existing config that is already unparseable (back it up, bail),
|
|
1715
|
+
// - refuse to WRITE a result that would not parse (e.g. a regex helper mangled a multiline
|
|
1716
|
+
// string), leaving the existing config untouched,
|
|
1717
|
+
// - otherwise back up the prior config before mutating.
|
|
1718
|
+
export async function safeWriteCodexConfigToml(configPath, current, next, tag = 'codex-lb') {
|
|
1719
|
+
const cur = String(current || '');
|
|
1720
|
+
if (cur.trim() && !codexConfigParseSmoke(cur).ok) {
|
|
1721
|
+
const backupPath = await backupCodexConfig(configPath, cur, `${tag}-unparseable`);
|
|
1722
|
+
return { ok: false, status: 'unparseable_config_preserved', config_path: configPath, backup_path: backupPath };
|
|
1723
|
+
}
|
|
1724
|
+
if (!codexConfigParseSmoke(next).ok) {
|
|
1725
|
+
return { ok: false, status: 'skipped_unsafe_rewrite', config_path: configPath, backup_path: null };
|
|
1726
|
+
}
|
|
1727
|
+
if (next === ensureTrailingNewline(cur)) {
|
|
1728
|
+
return { ok: true, status: 'present', config_path: configPath, backup_path: null, changed: false };
|
|
1729
|
+
}
|
|
1730
|
+
const backupPath = cur.trim() ? await backupCodexConfig(configPath, cur, tag) : null;
|
|
1731
|
+
await writeTextAtomic(configPath, next);
|
|
1732
|
+
return { ok: true, status: 'written', config_path: configPath, backup_path: backupPath, changed: true };
|
|
1733
|
+
}
|
|
1592
1734
|
function upsertTopLevelTomlBoolean(text, key, value) {
|
|
1593
1735
|
const line = `${key} = ${value ? 'true' : 'false'}`;
|
|
1594
1736
|
const lines = String(text || '').split('\n');
|
|
@@ -1606,6 +1748,10 @@ function upsertTopLevelTomlBoolean(text, key, value) {
|
|
|
1606
1748
|
lines.splice(end, 0, line);
|
|
1607
1749
|
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
1608
1750
|
}
|
|
1751
|
+
function hasTomlTable(text, table) {
|
|
1752
|
+
const header = `[${table}]`;
|
|
1753
|
+
return String(text || '').split('\n').some((line) => String(line).trim() === header);
|
|
1754
|
+
}
|
|
1609
1755
|
function upsertTomlTable(text, table, block) {
|
|
1610
1756
|
let lines = String(text || '').trimEnd().split('\n');
|
|
1611
1757
|
if (lines.length === 1 && lines[0] === '')
|
|
@@ -1915,7 +2061,7 @@ async function ensureGlobalGetdesignSkillDuringInstall() {
|
|
|
1915
2061
|
}
|
|
1916
2062
|
export async function ensureRelatedCliTools(args = []) {
|
|
1917
2063
|
const skip = args.includes('--skip-cli-tools') || process.env.SKS_SKIP_CLI_TOOLS === '1';
|
|
1918
|
-
const codex = await ensureCodexCliTool({ skip });
|
|
2064
|
+
const codex = await ensureCodexCliTool({ skip, args });
|
|
1919
2065
|
const zellijRepair = skip ? { status: 'skipped', reason: 'SKS_SKIP_CLI_TOOLS=1 or --skip-cli-tools' } : await ensureZellijCliTool(args);
|
|
1920
2066
|
const zellij = await checkZellijCapability({ require: false, writeReport: false });
|
|
1921
2067
|
return {
|
|
@@ -1928,11 +2074,42 @@ export async function ensureRelatedCliTools(args = []) {
|
|
|
1928
2074
|
current_session: false,
|
|
1929
2075
|
repair: zellijRepair,
|
|
1930
2076
|
install_hint: zellij.status === 'ok' ? null : zellijInstallHint(),
|
|
1931
|
-
error: zellij.blockers[0] || zellij.warnings[0] || null
|
|
2077
|
+
error: zellijRepair.error || zellij.blockers[0] || zellij.warnings[0] || null
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
export async function ensureMadLaunchDependencies(args = []) {
|
|
2082
|
+
const skip = args.includes('--skip-cli-tools') || process.env.SKS_SKIP_CLI_TOOLS === '1';
|
|
2083
|
+
const zellijRepair = skip ? { target: 'zellij', status: 'skipped', reason: 'SKS_SKIP_CLI_TOOLS=1 or --skip-cli-tools' } : await ensureZellijCliTool(args);
|
|
2084
|
+
const zellij = await checkZellijCapability({ require: false, writeReport: false });
|
|
2085
|
+
const ready = zellij.status === 'ok';
|
|
2086
|
+
return {
|
|
2087
|
+
ready,
|
|
2088
|
+
actions: ready ? [] : [{
|
|
2089
|
+
target: 'zellij',
|
|
2090
|
+
status: zellijRepair.status,
|
|
2091
|
+
command: zellijRepair.command || zellijInstallHint(),
|
|
2092
|
+
error: zellijRepair.error || zellij.blockers[0] || zellij.warnings[0] || null,
|
|
2093
|
+
repair: zellijRepair
|
|
2094
|
+
}],
|
|
2095
|
+
status: {
|
|
2096
|
+
zellij: {
|
|
2097
|
+
ok: ready,
|
|
2098
|
+
status: zellij.status,
|
|
2099
|
+
version: zellij.version,
|
|
2100
|
+
min_version: zellij.min_version,
|
|
2101
|
+
repair: zellijRepair,
|
|
2102
|
+
install_hint: ready ? null : zellijInstallHint()
|
|
2103
|
+
}
|
|
1932
2104
|
}
|
|
1933
2105
|
};
|
|
1934
2106
|
}
|
|
1935
|
-
export
|
|
2107
|
+
export function formatMadLaunchDependencyAction(action = {}) {
|
|
2108
|
+
const command = action.command ? ` Run: ${action.command}.` : '';
|
|
2109
|
+
const error = action.error ? ` ${action.error}` : '';
|
|
2110
|
+
return `${action.target || 'dependency'} ${action.status || 'blocked'}.${command}${error}`.trim();
|
|
2111
|
+
}
|
|
2112
|
+
export async function ensureCodexCliTool({ skip = false, args = [] } = {}) {
|
|
1936
2113
|
if (skip)
|
|
1937
2114
|
return { status: 'skipped', reason: 'SKS_SKIP_CLI_TOOLS=1 or --skip-cli-tools' };
|
|
1938
2115
|
const before = await getCodexInfo().catch(() => EMPTY_CODEX_INFO);
|
|
@@ -1941,6 +2118,12 @@ export async function ensureCodexCliTool({ skip = false } = {}) {
|
|
|
1941
2118
|
const npmBin = await which('npm');
|
|
1942
2119
|
if (!npmBin)
|
|
1943
2120
|
return { status: 'failed', error: 'npm not found on PATH; install Codex CLI manually with npm i -g @openai/codex@latest.' };
|
|
2121
|
+
const command = 'npm i -g @openai/codex@latest';
|
|
2122
|
+
if (args.includes('--dry-run'))
|
|
2123
|
+
return { status: 'dry_run', command, error: 'Codex CLI not found on PATH.' };
|
|
2124
|
+
if (!await confirmInstallYesDefault(`Codex CLI is missing. Install latest Codex CLI with ${command}?`, args)) {
|
|
2125
|
+
return { status: 'needs_approval', command, error: 'Codex CLI not found on PATH.' };
|
|
2126
|
+
}
|
|
1944
2127
|
const install = await runProcess(npmBin, ['i', '-g', '@openai/codex@latest'], {
|
|
1945
2128
|
timeoutMs: 120000,
|
|
1946
2129
|
maxOutputBytes: 128 * 1024
|
|
@@ -1969,24 +2152,27 @@ export async function ensureZellijCliTool(args = [], opts = {}) {
|
|
|
1969
2152
|
const repairCommand = command;
|
|
1970
2153
|
if (args.includes('--dry-run') || opts.dryRun)
|
|
1971
2154
|
return { target: 'zellij', status: 'dry_run', command: repairCommand, error: before.blockers[0] || before.warnings[0] || null };
|
|
1972
|
-
const
|
|
2155
|
+
const hasInstalledZellij = Boolean(before.version);
|
|
2156
|
+
const question = hasInstalledZellij
|
|
1973
2157
|
? `Homebrew Zellij ${before.version || 'unknown'} is not ready. Upgrade to latest Zellij with ${repairCommand}?`
|
|
1974
2158
|
: `Zellij is missing. Install latest Zellij with ${repairCommand}?`;
|
|
1975
2159
|
if (!await confirmInstallYesDefault(question, args))
|
|
1976
2160
|
return { target: 'zellij', status: 'needs_approval', command: repairCommand, error: before.blockers[0] || before.warnings[0] || null };
|
|
1977
|
-
const brewArgs =
|
|
2161
|
+
const brewArgs = hasInstalledZellij ? ['upgrade', 'zellij'] : ['install', 'zellij'];
|
|
1978
2162
|
const install = await runProcess(brew, brewArgs, { timeoutMs: 180000, maxOutputBytes: 128 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
1979
2163
|
if (install.code !== 0)
|
|
1980
2164
|
return { target: 'zellij', status: 'failed', command: repairCommand, error: `${install.stderr || install.stdout || repairCommand + ' failed'}`.trim() };
|
|
1981
2165
|
const after = await checkZellijCapability({ require: false, writeReport: false });
|
|
1982
2166
|
if (after.status !== 'ok')
|
|
1983
2167
|
return { target: 'zellij', status: 'installed_not_ready', command: repairCommand, error: after.blockers[0] || after.warnings[0] || 'zellij installed but not ready' };
|
|
1984
|
-
return { target: 'zellij', status:
|
|
2168
|
+
return { target: 'zellij', status: hasInstalledZellij ? 'upgraded' : 'installed', command: repairCommand, bin: after.bin, version: after.version || null };
|
|
1985
2169
|
}
|
|
1986
2170
|
function zellijInstallHint() {
|
|
1987
2171
|
return process.platform === 'darwin' ? 'brew install zellij' : 'Install Zellij from https://zellij.dev/documentation/installation.html';
|
|
1988
2172
|
}
|
|
1989
2173
|
async function confirmInstallYesDefault(question, args = []) {
|
|
2174
|
+
if (hasFlag(args, '--from-postinstall') && process.env.SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS !== '1')
|
|
2175
|
+
return false;
|
|
1990
2176
|
if (shouldAutoApproveInstall(args))
|
|
1991
2177
|
return true;
|
|
1992
2178
|
if (!canAskYesNo())
|
|
@@ -2023,6 +2209,10 @@ export async function maybePromptCodexUpdateForLaunch(args = [], opts = {}) {
|
|
|
2023
2209
|
return installCodexLatest(command, latest.version, current);
|
|
2024
2210
|
}
|
|
2025
2211
|
export function shouldAutoApproveInstall(args = [], env = process.env) {
|
|
2212
|
+
if (hasFlag(args, '--from-postinstall') && env.SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS !== '1')
|
|
2213
|
+
return false;
|
|
2214
|
+
if (hasFlag(args, '--from-postinstall') && env.SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS === '1')
|
|
2215
|
+
return true;
|
|
2026
2216
|
return hasFlag(args, '--yes') || hasFlag(args, '-y') || isAgentRuntime(env);
|
|
2027
2217
|
}
|
|
2028
2218
|
function canAskYesNo() {
|