sneakoscope 3.0.1 → 3.0.3
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 +1 -1
- 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/core/agents/agent-message-bus.js +89 -2
- package/dist/core/agents/runtime-proof-summary.js +46 -2
- package/dist/core/codex/codex-cli-syntax-builder.js +4 -1
- package/dist/core/codex-control/codex-0139-capability.js +68 -12
- package/dist/core/codex-control/codex-multi-agent-event-normalizer.js +15 -0
- package/dist/core/codex-control/codex-tool-schema-fixtures.js +57 -0
- package/dist/core/commands/naruto-command.js +9 -4
- package/dist/core/fsx.js +1 -1
- package/dist/core/mcp/mcp-0-134-policy.js +3 -0
- package/dist/core/pipeline-internals/runtime-core.js +6 -1
- package/dist/core/release/release-cache-key.js +128 -0
- package/dist/core/release/release-gate-cache-v2.js +6 -7
- package/dist/core/release/release-proof-truth.js +63 -0
- package/dist/core/safety/side-effect-runtime-report.js +19 -4
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-capability.js +41 -0
- package/dist/core/zellij/zellij-command.js +14 -2
- package/dist/core/zellij/zellij-fake-adapter.js +163 -0
- package/dist/core/zellij/zellij-update.js +28 -3
- package/dist/core/zellij/zellij-worker-pane-manager.js +116 -5
- package/dist/core/zellij/zellij-worker-pane-summary.js +65 -0
- package/dist/scripts/github-release-body-helper.js +63 -0
- package/dist/scripts/release-speed-summary.js +11 -0
- package/package.json +33 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export function classifyReleaseCacheInputChange(input) {
|
|
2
|
+
if (input.before === input.after) {
|
|
3
|
+
return { neutralizable: true, reason: 'unchanged', behavior_affecting: false };
|
|
4
|
+
}
|
|
5
|
+
const before = normalizeReleaseCacheInputForBehavior(input.file, input.before);
|
|
6
|
+
const after = normalizeReleaseCacheInputForBehavior(input.file, input.after);
|
|
7
|
+
if (before === after) {
|
|
8
|
+
return {
|
|
9
|
+
neutralizable: true,
|
|
10
|
+
reason: neutralReason(input.file),
|
|
11
|
+
behavior_affecting: false
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
neutralizable: false,
|
|
16
|
+
reason: behaviorReason(input.file, input.before, input.after),
|
|
17
|
+
behavior_affecting: true
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function normalizeReleaseCacheInputForBehavior(file, text) {
|
|
21
|
+
const rel = normalizeRel(file);
|
|
22
|
+
if (rel === 'package.json')
|
|
23
|
+
return normalizePackageJson(text);
|
|
24
|
+
if (rel === 'package-lock.json')
|
|
25
|
+
return normalizePackageLock(text);
|
|
26
|
+
if (rel === 'src/core/version.ts' || rel === 'src/core/fsx.ts') {
|
|
27
|
+
return text.replace(/(PACKAGE_VERSION\s*=\s*['"])([^'"]+)(['"])/, '$1__SKS_RELEASE_VERSION__$3');
|
|
28
|
+
}
|
|
29
|
+
if (rel === 'src/bin/sks.ts') {
|
|
30
|
+
return text.replace(/(FAST_PACKAGE_VERSION\s*=\s*['"])([^'"]+)(['"])/, '$1__SKS_RELEASE_VERSION__$3');
|
|
31
|
+
}
|
|
32
|
+
if (rel === 'dist/build-manifest.json')
|
|
33
|
+
return normalizeBuildManifest(text);
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
function normalizePackageJson(text) {
|
|
37
|
+
return normalizeJson(text, (value) => {
|
|
38
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
39
|
+
value.version = '__SKS_RELEASE_VERSION__';
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function normalizePackageLock(text) {
|
|
45
|
+
return normalizeJson(text, (value) => {
|
|
46
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
47
|
+
value.version = '__SKS_RELEASE_VERSION__';
|
|
48
|
+
if (value.packages?.[''] && typeof value.packages[''] === 'object') {
|
|
49
|
+
value.packages[''].version = '__SKS_RELEASE_VERSION__';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function normalizeBuildManifest(text) {
|
|
56
|
+
return normalizeJson(text, (value) => {
|
|
57
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
58
|
+
if ('version' in value)
|
|
59
|
+
value.version = '__SKS_RELEASE_VERSION__';
|
|
60
|
+
if ('package_version' in value)
|
|
61
|
+
value.package_version = '__SKS_RELEASE_VERSION__';
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function normalizeJson(text, mutate) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(text);
|
|
69
|
+
return stableJson(mutate(parsed));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return text;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function stableJson(value) {
|
|
76
|
+
if (Array.isArray(value))
|
|
77
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
78
|
+
if (value && typeof value === 'object') {
|
|
79
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
80
|
+
}
|
|
81
|
+
return JSON.stringify(value);
|
|
82
|
+
}
|
|
83
|
+
function neutralReason(file) {
|
|
84
|
+
const rel = normalizeRel(file);
|
|
85
|
+
if (rel === 'package.json')
|
|
86
|
+
return 'package_json_version_only';
|
|
87
|
+
if (rel === 'package-lock.json')
|
|
88
|
+
return 'package_lock_root_version_only';
|
|
89
|
+
if (rel === 'src/bin/sks.ts')
|
|
90
|
+
return 'fast_package_version_only';
|
|
91
|
+
if (rel === 'src/core/version.ts' || rel === 'src/core/fsx.ts')
|
|
92
|
+
return 'package_version_constant_only';
|
|
93
|
+
if (rel === 'dist/build-manifest.json')
|
|
94
|
+
return 'build_manifest_version_only';
|
|
95
|
+
return 'version_surface_only';
|
|
96
|
+
}
|
|
97
|
+
function behaviorReason(file, before, after) {
|
|
98
|
+
const rel = normalizeRel(file);
|
|
99
|
+
if (rel === 'package.json') {
|
|
100
|
+
const changed = changedTopLevelJsonKeys(before, after);
|
|
101
|
+
const behaviorKeys = changed.filter((key) => ['scripts', 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', 'files', 'engines', 'publishConfig'].includes(key));
|
|
102
|
+
return behaviorKeys.length ? `package_json_behavior_keys:${behaviorKeys.join(',')}` : `package_json_non_version_keys:${changed.join(',') || 'unknown'}`;
|
|
103
|
+
}
|
|
104
|
+
if (rel === 'package-lock.json')
|
|
105
|
+
return 'package_lock_dependency_graph_changed';
|
|
106
|
+
if (rel === 'dist/build-manifest.json')
|
|
107
|
+
return 'build_manifest_artifact_hash_or_behavior_changed';
|
|
108
|
+
if (rel.startsWith('src/'))
|
|
109
|
+
return 'source_behavior_changed';
|
|
110
|
+
if (rel.startsWith('schemas/'))
|
|
111
|
+
return 'schema_behavior_changed';
|
|
112
|
+
return 'release_cache_input_behavior_changed';
|
|
113
|
+
}
|
|
114
|
+
function changedTopLevelJsonKeys(before, after) {
|
|
115
|
+
try {
|
|
116
|
+
const left = JSON.parse(before);
|
|
117
|
+
const right = JSON.parse(after);
|
|
118
|
+
const keys = [...new Set([...Object.keys(left || {}), ...Object.keys(right || {})])];
|
|
119
|
+
return keys.filter((key) => stableJson(left?.[key]) !== stableJson(right?.[key])).sort();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function normalizeRel(file) {
|
|
126
|
+
return String(file || '').replace(/\\/g, '/').replace(/^\.?\//, '');
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=release-cache-key.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
+
import { normalizeReleaseCacheInputForBehavior } from './release-cache-key.js';
|
|
4
5
|
export const RELEASE_GATE_CACHE_V2_SCHEMA = 'sks.release-gate-cache.v2';
|
|
5
6
|
export function releaseGateCacheFile(root) {
|
|
6
7
|
return path.join(root, '.sneakoscope', 'reports', 'release-gates', 'cache-v2.json');
|
|
@@ -21,7 +22,8 @@ const VERSION_NEUTRAL_CACHE_FILES = new Set([
|
|
|
21
22
|
'package-lock.json',
|
|
22
23
|
'src/core/version.ts',
|
|
23
24
|
'src/core/fsx.ts',
|
|
24
|
-
'src/bin/sks.ts'
|
|
25
|
+
'src/bin/sks.ts',
|
|
26
|
+
'dist/build-manifest.json'
|
|
25
27
|
]);
|
|
26
28
|
export function releaseGateCacheKey(root, gate) {
|
|
27
29
|
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
@@ -54,14 +56,14 @@ export function releaseGateCacheKey(root, gate) {
|
|
|
54
56
|
const rel = path.relative(root, file);
|
|
55
57
|
hash.update(rel);
|
|
56
58
|
if (!versionSensitive && VERSION_NEUTRAL_CACHE_FILES.has(rel))
|
|
57
|
-
hashVersionNeutralFile(hash, file, releaseVersion);
|
|
59
|
+
hashVersionNeutralFile(hash, rel, file, releaseVersion);
|
|
58
60
|
else
|
|
59
61
|
hashFileIfPresent(hash, file);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
return hash.digest('hex');
|
|
63
65
|
}
|
|
64
|
-
function hashVersionNeutralFile(hash, file, releaseVersion) {
|
|
66
|
+
function hashVersionNeutralFile(hash, rel, file, releaseVersion) {
|
|
65
67
|
if (!fs.existsSync(file) || !fs.statSync(file).isFile())
|
|
66
68
|
return;
|
|
67
69
|
const text = fs.readFileSync(file, 'utf8');
|
|
@@ -69,10 +71,7 @@ function hashVersionNeutralFile(hash, file, releaseVersion) {
|
|
|
69
71
|
hash.update(text);
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
|
-
|
|
73
|
-
// version-only bump hashes identically. Any other content change in these
|
|
74
|
-
// files still alters the key.
|
|
75
|
-
hash.update(text.split(releaseVersion).join('__SKS_RELEASE_VERSION__'));
|
|
74
|
+
hash.update(normalizeReleaseCacheInputForBehavior(rel, text));
|
|
76
75
|
}
|
|
77
76
|
export function expandGlob(root, input) {
|
|
78
77
|
const absolute = path.join(root, input);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { nowIso, readJson, runProcess, sha256, writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
export async function buildReleaseProofTruth(root) {
|
|
5
|
+
const pkg = await readJson(path.join(root, 'package.json'));
|
|
6
|
+
const gitCommit = await gitOutput(root, ['rev-parse', 'HEAD']);
|
|
7
|
+
const gitBranch = await gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
8
|
+
const gitStatus = await gitOutput(root, ['status', '--porcelain']);
|
|
9
|
+
const packlist = await readNpmPacklist(root);
|
|
10
|
+
return {
|
|
11
|
+
schema: 'sks.release-proof-truth.v1',
|
|
12
|
+
generated_at: nowIso(),
|
|
13
|
+
package_version: String(pkg.version || ''),
|
|
14
|
+
git_commit_sha: gitCommit || null,
|
|
15
|
+
git_branch: gitBranch || null,
|
|
16
|
+
git_status_clean: gitStatus === '',
|
|
17
|
+
package_json_sha256: await shaFile(root, 'package.json'),
|
|
18
|
+
package_lock_sha256: await shaFile(root, 'package-lock.json'),
|
|
19
|
+
version_ts_sha256: await shaFile(root, 'src/core/version.ts'),
|
|
20
|
+
changelog_sha256: await shaFile(root, 'CHANGELOG.md'),
|
|
21
|
+
release_gates_sha256: await shaFile(root, 'release-gates.v2.json'),
|
|
22
|
+
...(packlist ? {
|
|
23
|
+
npm_packlist_count: packlist.count,
|
|
24
|
+
npm_packlist_bytes: packlist.bytes
|
|
25
|
+
} : {})
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function writeReleaseProofTruth(root) {
|
|
29
|
+
const truth = await buildReleaseProofTruth(root);
|
|
30
|
+
await writeJsonAtomic(path.join(root, '.sneakoscope', 'release-proof-truth.json'), truth);
|
|
31
|
+
await writeJsonAtomic(path.join(root, 'dist', 'release-proof-truth.json'), truth);
|
|
32
|
+
return truth;
|
|
33
|
+
}
|
|
34
|
+
async function shaFile(root, rel) {
|
|
35
|
+
return sha256(await fs.readFile(path.join(root, rel)));
|
|
36
|
+
}
|
|
37
|
+
async function gitOutput(root, args) {
|
|
38
|
+
const result = await runProcess('git', args, { cwd: root, timeoutMs: 10000, maxOutputBytes: 64 * 1024 }).catch(() => null);
|
|
39
|
+
if (!result || result.code !== 0)
|
|
40
|
+
return null;
|
|
41
|
+
return String(result.stdout || '').trim();
|
|
42
|
+
}
|
|
43
|
+
async function readNpmPacklist(root) {
|
|
44
|
+
const result = await runProcess('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], {
|
|
45
|
+
cwd: root,
|
|
46
|
+
timeoutMs: 60000,
|
|
47
|
+
maxOutputBytes: 1024 * 1024
|
|
48
|
+
}).catch(() => null);
|
|
49
|
+
if (!result || result.code !== 0)
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(String(result.stdout || '[]'));
|
|
53
|
+
const files = Array.isArray(parsed?.[0]?.files) ? parsed[0].files : [];
|
|
54
|
+
return {
|
|
55
|
+
count: files.length,
|
|
56
|
+
bytes: files.reduce((sum, file) => sum + Number(file.size || 0), 0)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=release-proof-truth.js.map
|
|
@@ -34,10 +34,18 @@ export async function buildSideEffectRuntimeReport(root) {
|
|
|
34
34
|
async function discoverLedgerPaths(root) {
|
|
35
35
|
const found = new Set();
|
|
36
36
|
await addIfExists(found, mutationLedgerPath(root));
|
|
37
|
-
await walkForLedgers(path.join(root, '.sneakoscope', 'missions'), found
|
|
37
|
+
await walkForLedgers(path.join(root, '.sneakoscope', 'missions'), found, {
|
|
38
|
+
depth: 0,
|
|
39
|
+
maxDepth: positiveInt(process.env.SKS_SIDE_EFFECT_LEDGER_SCAN_MAX_DEPTH, 6),
|
|
40
|
+
visitedDirs: 0,
|
|
41
|
+
maxDirs: positiveInt(process.env.SKS_SIDE_EFFECT_LEDGER_SCAN_MAX_DIRS, 20000)
|
|
42
|
+
});
|
|
38
43
|
return [...found].sort();
|
|
39
44
|
}
|
|
40
|
-
async function walkForLedgers(dir, found) {
|
|
45
|
+
async function walkForLedgers(dir, found, budget) {
|
|
46
|
+
if (budget.depth > budget.maxDepth || budget.visitedDirs >= budget.maxDirs)
|
|
47
|
+
return;
|
|
48
|
+
budget.visitedDirs += 1;
|
|
41
49
|
let entries;
|
|
42
50
|
try {
|
|
43
51
|
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
@@ -47,12 +55,19 @@ async function walkForLedgers(dir, found) {
|
|
|
47
55
|
}
|
|
48
56
|
for (const entry of entries) {
|
|
49
57
|
const file = path.join(dir, entry.name);
|
|
50
|
-
if (entry.isDirectory())
|
|
51
|
-
await walkForLedgers(file, found);
|
|
58
|
+
if (entry.isDirectory() && !shouldSkipLedgerScanDir(entry.name))
|
|
59
|
+
await walkForLedgers(file, found, { ...budget, depth: budget.depth + 1 });
|
|
52
60
|
else if (entry.isFile() && entry.name === 'mutation-ledger.jsonl')
|
|
53
61
|
found.add(file);
|
|
54
62
|
}
|
|
55
63
|
}
|
|
64
|
+
function shouldSkipLedgerScanDir(name) {
|
|
65
|
+
return new Set(['node_modules', '.git', 'dist', 'vendor', '.next', 'coverage']).has(name);
|
|
66
|
+
}
|
|
67
|
+
function positiveInt(value, fallback) {
|
|
68
|
+
const parsed = Number(value);
|
|
69
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
70
|
+
}
|
|
56
71
|
async function addIfExists(found, file) {
|
|
57
72
|
try {
|
|
58
73
|
await fsp.access(file);
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '3.0.
|
|
1
|
+
export const PACKAGE_VERSION = '3.0.3';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -3,6 +3,47 @@ import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
|
3
3
|
import { compareVersionLike, parseZellijVersionText, runZellij } from './zellij-command.js';
|
|
4
4
|
export const ZELLIJ_CAPABILITY_SCHEMA = 'sks.zellij-capability.v1';
|
|
5
5
|
export const ZELLIJ_MIN_VERSION = '0.41.0';
|
|
6
|
+
export const ZELLIJ_STACKED_PANE_CAPABILITY_SCHEMA = 'sks.zellij-stacked-pane-capability.v1';
|
|
7
|
+
export const ZELLIJ_STACKED_PANE_MIN_VERSION = '0.43.0';
|
|
8
|
+
export function zellijSupportsStackedPanes(version) {
|
|
9
|
+
const parsed = parseZellijVersionText(version);
|
|
10
|
+
return Boolean(parsed && compareVersionLike(parsed, ZELLIJ_STACKED_PANE_MIN_VERSION) >= 0);
|
|
11
|
+
}
|
|
12
|
+
export function resolveZellijStackedPaneCapability(input = {}) {
|
|
13
|
+
const versionText = input.versionText == null ? null : String(input.versionText);
|
|
14
|
+
const parsedVersion = parseZellijVersionText(versionText);
|
|
15
|
+
const supports = zellijSupportsStackedPanes(parsedVersion);
|
|
16
|
+
const blockers = [...(input.blockers || [])].map(String);
|
|
17
|
+
const zellijMissing = blockers.includes('zellij_missing') || blockers.includes('zellij_missing_required');
|
|
18
|
+
if (!parsedVersion && !zellijMissing && input.ok === false)
|
|
19
|
+
blockers.push('zellij_version_unparsed');
|
|
20
|
+
return {
|
|
21
|
+
schema: ZELLIJ_STACKED_PANE_CAPABILITY_SCHEMA,
|
|
22
|
+
ok: blockers.length === 0 && supports,
|
|
23
|
+
zellij_bin: input.zellijBin === undefined ? 'zellij' : input.zellijBin,
|
|
24
|
+
version_text: versionText,
|
|
25
|
+
parsed_version: parsedVersion,
|
|
26
|
+
supports_stacked_panes: supports,
|
|
27
|
+
requires_update: Boolean(parsedVersion && !supports),
|
|
28
|
+
fallback_mode: supports ? 'native-stacked' : zellijMissing ? 'headless-only' : 'down-split-stack-emulation',
|
|
29
|
+
blockers
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function checkZellijStackedPaneCapability(opts = {}) {
|
|
33
|
+
const versionRun = await runZellij(['--version'], { optional: true, timeoutMs: 5000 });
|
|
34
|
+
const versionText = `${versionRun.stdout_tail}\n${versionRun.stderr_tail}`.trim();
|
|
35
|
+
const report = resolveZellijStackedPaneCapability({
|
|
36
|
+
ok: versionRun.ok,
|
|
37
|
+
zellijBin: 'zellij',
|
|
38
|
+
versionText,
|
|
39
|
+
blockers: versionRun.ok ? [] : versionRun.blockers
|
|
40
|
+
});
|
|
41
|
+
if (opts.writeReport !== false) {
|
|
42
|
+
const root = opts.root || process.cwd();
|
|
43
|
+
await writeJsonAtomic(path.join(root, '.sneakoscope', 'reports', 'zellij-stacked-pane-capability.json'), report);
|
|
44
|
+
}
|
|
45
|
+
return report;
|
|
46
|
+
}
|
|
6
47
|
export async function checkZellijCapability(opts = {}) {
|
|
7
48
|
const requireZellij = opts.require === true || process.env.SKS_REQUIRE_ZELLIJ === '1';
|
|
8
49
|
const versionRun = await runZellij(['--version'], { optional: !requireZellij, timeoutMs: 5000 });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { runProcess } from '../fsx.js';
|
|
4
|
+
import { runFakeZellij } from './zellij-fake-adapter.js';
|
|
4
5
|
export const ZELLIJ_UNIX_SOCKET_PATH_LIMIT = 103;
|
|
5
6
|
export async function runZellij(args = [], opts = {}) {
|
|
6
7
|
const started = Date.now();
|
|
@@ -16,7 +17,17 @@ export async function runZellij(args = [], opts = {}) {
|
|
|
16
17
|
runOpts.stdoutFile = opts.stdoutFile;
|
|
17
18
|
if (opts.stderrFile !== undefined)
|
|
18
19
|
runOpts.stderrFile = opts.stderrFile;
|
|
19
|
-
const
|
|
20
|
+
const fakeOpts = {
|
|
21
|
+
cwd: runOpts.cwd || process.cwd(),
|
|
22
|
+
env: preparedEnv.env
|
|
23
|
+
};
|
|
24
|
+
if (runOpts.timeoutMs !== undefined)
|
|
25
|
+
fakeOpts.timeoutMs = runOpts.timeoutMs;
|
|
26
|
+
if (runOpts.maxOutputBytes !== undefined)
|
|
27
|
+
fakeOpts.maxOutputBytes = runOpts.maxOutputBytes;
|
|
28
|
+
const result = process.env.SKS_ZELLIJ_FAKE_ADAPTER === '1' || preparedEnv.env.SKS_ZELLIJ_FAKE_ADAPTER === '1'
|
|
29
|
+
? await runFakeZellij(args, fakeOpts)
|
|
30
|
+
: await runProcess('zellij', args, runOpts);
|
|
20
31
|
const ok = result.code === 0;
|
|
21
32
|
const stderr = String(result.stderr || '');
|
|
22
33
|
const missing = result.code === -1 && /ENOENT|not found|spawn zellij/i.test(stderr);
|
|
@@ -88,9 +99,10 @@ export function isZellijSocketPathTooLong(text) {
|
|
|
88
99
|
return /IPC socket path is too long|socket path is too long/i.test(String(text || ''));
|
|
89
100
|
}
|
|
90
101
|
export function parseZellijVersionText(text) {
|
|
91
|
-
const match = String(text || '').match(
|
|
102
|
+
const match = String(text || '').match(/(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+)(?:[-+][0-9A-Za-z.-]+)?(?:$|[^0-9A-Za-z])/i);
|
|
92
103
|
return match?.[1] ?? null;
|
|
93
104
|
}
|
|
105
|
+
export const parseZellijVersion = parseZellijVersionText;
|
|
94
106
|
export function compareVersionLike(a, b) {
|
|
95
107
|
const pa = versionParts(a);
|
|
96
108
|
const pb = versionParts(b);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { appendJsonl, ensureDir, nowIso } from '../fsx.js';
|
|
3
|
+
const sessions = new Map();
|
|
4
|
+
export async function runFakeZellij(args = [], opts = {}) {
|
|
5
|
+
const env = { ...process.env, ...(opts.env || {}) };
|
|
6
|
+
const root = path.resolve(String(env.SKS_ZELLIJ_FAKE_ROOT || opts.cwd || process.cwd()));
|
|
7
|
+
const version = String(env.SKS_ZELLIJ_FAKE_VERSION || '0.43.1');
|
|
8
|
+
const delayMs = Math.max(0, Number(env.SKS_ZELLIJ_FAKE_DELAY_MS || 0) || 0);
|
|
9
|
+
const sessionName = sessionFromArgs(args);
|
|
10
|
+
const startedAt = Date.now();
|
|
11
|
+
let result;
|
|
12
|
+
if (args.length === 1 && args[0] === '--version') {
|
|
13
|
+
result = ok(`zellij ${version}\n`);
|
|
14
|
+
}
|
|
15
|
+
else if (args[0] === 'attach' && args[1] === '--create-background') {
|
|
16
|
+
getSession(String(args[2] || 'default'));
|
|
17
|
+
result = ok('');
|
|
18
|
+
}
|
|
19
|
+
else if (args.includes('new-pane')) {
|
|
20
|
+
if (args.includes('--stacked') && !supportsStacked(version)) {
|
|
21
|
+
result = fail('unknown option --stacked');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
if (delayMs > 0)
|
|
25
|
+
await sleep(delayMs);
|
|
26
|
+
const session = getSession(sessionName);
|
|
27
|
+
const name = optionValue(args, '--name') || `pane-${session.next_id}`;
|
|
28
|
+
const paneId = `terminal_${session.next_id++}`;
|
|
29
|
+
const command = commandAfter(args, '--') || '';
|
|
30
|
+
const pane = { pane_id: paneId, title: name, name, terminal_command: command, exited: false };
|
|
31
|
+
session.panes.push(pane);
|
|
32
|
+
session.focused_pane_id = paneId;
|
|
33
|
+
result = ok(`${JSON.stringify({ pane_id: paneId })}\n`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (args.includes('focus-pane-id')) {
|
|
37
|
+
const paneId = String(args[args.indexOf('focus-pane-id') + 1] || '');
|
|
38
|
+
const session = getSession(sessionName);
|
|
39
|
+
const pane = findPane(session, paneId);
|
|
40
|
+
if (!pane)
|
|
41
|
+
result = fail(`Pane ${paneId} not found`);
|
|
42
|
+
else {
|
|
43
|
+
session.focused_pane_id = pane.pane_id;
|
|
44
|
+
result = ok('');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (args.includes('list-panes')) {
|
|
48
|
+
const session = getSession(sessionName);
|
|
49
|
+
result = ok(`${JSON.stringify(session.panes)}\n`);
|
|
50
|
+
}
|
|
51
|
+
else if (args.includes('dump-screen')) {
|
|
52
|
+
const session = getSession(sessionName);
|
|
53
|
+
result = ok(session.panes.map((pane) => pane.title).join('\n') + '\n');
|
|
54
|
+
}
|
|
55
|
+
else if (args.includes('rename-pane')) {
|
|
56
|
+
const session = getSession(sessionName);
|
|
57
|
+
const paneId = optionValue(args, '--pane-id');
|
|
58
|
+
const name = String(args[args.length - 1] || '');
|
|
59
|
+
const pane = paneId ? findPane(session, paneId) : session.panes.find((row) => row.pane_id === session.focused_pane_id);
|
|
60
|
+
if (!pane)
|
|
61
|
+
result = fail(`Pane ${paneId || 'focused'} not found`);
|
|
62
|
+
else {
|
|
63
|
+
pane.title = name;
|
|
64
|
+
pane.name = name;
|
|
65
|
+
result = ok('');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (args.includes('close-pane')) {
|
|
69
|
+
const session = getSession(sessionName);
|
|
70
|
+
const paneId = optionValue(args, '--pane-id');
|
|
71
|
+
const pane = paneId ? findPane(session, paneId) : session.panes.find((row) => row.pane_id === session.focused_pane_id);
|
|
72
|
+
if (!pane)
|
|
73
|
+
result = fail(`Pane ${paneId || 'focused'} not found`);
|
|
74
|
+
else {
|
|
75
|
+
pane.exited = true;
|
|
76
|
+
result = ok('');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
result = ok('');
|
|
81
|
+
}
|
|
82
|
+
await recordFakeZellijCall(root, args, {
|
|
83
|
+
session_name: sessionName,
|
|
84
|
+
version,
|
|
85
|
+
exit_code: result.code,
|
|
86
|
+
duration_ms: Date.now() - startedAt
|
|
87
|
+
});
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
async function recordFakeZellijCall(root, args, meta) {
|
|
91
|
+
const file = path.join(root, '.sneakoscope', 'fake-zellij-calls.jsonl');
|
|
92
|
+
await ensureDir(path.dirname(file));
|
|
93
|
+
await appendJsonl(file, {
|
|
94
|
+
schema: 'sks.fake-zellij-call.v1',
|
|
95
|
+
ts: nowIso(),
|
|
96
|
+
args: [...args],
|
|
97
|
+
...meta
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function getSession(name) {
|
|
101
|
+
const key = String(name || 'default');
|
|
102
|
+
const existing = sessions.get(key);
|
|
103
|
+
if (existing)
|
|
104
|
+
return existing;
|
|
105
|
+
const next = { next_id: 1, focused_pane_id: null, panes: [] };
|
|
106
|
+
sessions.set(key, next);
|
|
107
|
+
return next;
|
|
108
|
+
}
|
|
109
|
+
function sessionFromArgs(args) {
|
|
110
|
+
const explicit = optionValue(args, '--session');
|
|
111
|
+
if (explicit)
|
|
112
|
+
return explicit;
|
|
113
|
+
if (args[0] === 'attach' && args[1] === '--create-background')
|
|
114
|
+
return String(args[2] || 'default');
|
|
115
|
+
return 'default';
|
|
116
|
+
}
|
|
117
|
+
function optionValue(args, flag) {
|
|
118
|
+
const index = args.indexOf(flag);
|
|
119
|
+
if (index < 0)
|
|
120
|
+
return null;
|
|
121
|
+
const value = args[index + 1];
|
|
122
|
+
return value == null ? null : String(value);
|
|
123
|
+
}
|
|
124
|
+
function commandAfter(args, marker) {
|
|
125
|
+
const index = args.indexOf(marker);
|
|
126
|
+
if (index < 0)
|
|
127
|
+
return null;
|
|
128
|
+
return args.slice(index + 1).map(String).join(' ');
|
|
129
|
+
}
|
|
130
|
+
function findPane(session, id) {
|
|
131
|
+
const normalized = String(id || '').replace(/^terminal_/, '');
|
|
132
|
+
return session.panes.find((pane) => pane.pane_id === id || pane.pane_id.replace(/^terminal_/, '') === normalized) || null;
|
|
133
|
+
}
|
|
134
|
+
function supportsStacked(version) {
|
|
135
|
+
const parts = String(version || '0.0.0').match(/(\d+)\.(\d+)\.(\d+)/)?.slice(1).map((part) => Number.parseInt(part, 10) || 0) || [0, 0, 0];
|
|
136
|
+
return parts[0] > 0 || parts[1] > 43 || (parts[1] === 43 && parts[2] >= 0);
|
|
137
|
+
}
|
|
138
|
+
function ok(stdout) {
|
|
139
|
+
return {
|
|
140
|
+
code: 0,
|
|
141
|
+
stdout,
|
|
142
|
+
stderr: '',
|
|
143
|
+
stdoutBytes: Buffer.byteLength(stdout),
|
|
144
|
+
stderrBytes: 0,
|
|
145
|
+
truncated: false,
|
|
146
|
+
timedOut: false
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function fail(stderr) {
|
|
150
|
+
return {
|
|
151
|
+
code: 1,
|
|
152
|
+
stdout: '',
|
|
153
|
+
stderr,
|
|
154
|
+
stdoutBytes: 0,
|
|
155
|
+
stderrBytes: Buffer.byteLength(stderr),
|
|
156
|
+
truncated: false,
|
|
157
|
+
timedOut: false
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function sleep(ms) {
|
|
161
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=zellij-fake-adapter.js.map
|
|
@@ -8,6 +8,18 @@ import { createRequestedScopeContract } from '../safety/requested-scope-contract
|
|
|
8
8
|
import { checkZellijCapability } from './zellij-capability.js';
|
|
9
9
|
import { compareVersionLike, parseZellijVersionText } from './zellij-command.js';
|
|
10
10
|
export const ZELLIJ_UPDATE_NOTICE_SCHEMA = 'sks.zellij-update-notice.v1';
|
|
11
|
+
export function resolveZellijUpdatePromptMode(input) {
|
|
12
|
+
const env = input.env || process.env;
|
|
13
|
+
if (input.skipFlag === true || env.SKS_SKIP_ZELLIJ_UPDATE === '1')
|
|
14
|
+
return 'skip';
|
|
15
|
+
if (input.ci === true || env.CI === '1' || /^true$/i.test(String(env.CI || '')))
|
|
16
|
+
return 'nonblocking-notice';
|
|
17
|
+
if (input.noQuestion === true || env.SKS_NO_QUESTION === '1' || /^true$/i.test(String(env.SKS_NO_QUESTION || '')))
|
|
18
|
+
return 'nonblocking-notice';
|
|
19
|
+
if (input.headless === true)
|
|
20
|
+
return 'nonblocking-notice';
|
|
21
|
+
return 'interactive-prompt';
|
|
22
|
+
}
|
|
11
23
|
const ZELLIJ_RELEASES_API_PATH = '/repos/zellij-org/zellij/releases/latest';
|
|
12
24
|
export function zellijUpgradeCommandHint(missing = false) {
|
|
13
25
|
if (process.platform === 'darwin')
|
|
@@ -194,10 +206,19 @@ export async function upgradeZellijToLatest(input = {}) {
|
|
|
194
206
|
export async function maybePromptZellijUpdateForLaunch(args = [], opts = {}) {
|
|
195
207
|
const env = opts.env || process.env;
|
|
196
208
|
const list = (args || []).map((arg) => String(arg));
|
|
197
|
-
|
|
209
|
+
const mode = resolveZellijUpdatePromptMode({
|
|
210
|
+
env,
|
|
211
|
+
skipFlag: list.includes('--json') || list.includes('--skip-cli-tools') || list.includes('--skip-zellij-update'),
|
|
212
|
+
noQuestion: list.includes('--no-question') || list.includes('--no-questions'),
|
|
213
|
+
headless: !(process.stdin.isTTY && process.stdout.isTTY)
|
|
214
|
+
});
|
|
215
|
+
if (mode === 'skip') {
|
|
198
216
|
return { status: 'skipped', current: null, latest: null, command: null };
|
|
199
217
|
}
|
|
200
|
-
const
|
|
218
|
+
const noticeInput = { env };
|
|
219
|
+
if (opts.missionDir !== undefined)
|
|
220
|
+
noticeInput.missionDir = opts.missionDir;
|
|
221
|
+
const notice = await checkZellijUpdateNotice(noticeInput).catch(() => null);
|
|
201
222
|
if (!notice)
|
|
202
223
|
return { status: 'skipped', current: null, latest: null, command: null };
|
|
203
224
|
if (notice.zellij_missing) {
|
|
@@ -211,6 +232,10 @@ export async function maybePromptZellijUpdateForLaunch(args = [], opts = {}) {
|
|
|
211
232
|
}
|
|
212
233
|
const label = opts.label || 'Zellij launch';
|
|
213
234
|
const autoYes = list.includes('--yes') || list.includes('-y');
|
|
235
|
+
if (mode === 'nonblocking-notice') {
|
|
236
|
+
console.log(`Zellij update available: ${notice.current_version} -> ${notice.latest_version}. Run: ${notice.upgrade_command}`);
|
|
237
|
+
return { status: 'available', current: notice.current_version, latest: notice.latest_version, command: notice.upgrade_command };
|
|
238
|
+
}
|
|
214
239
|
if (!autoYes && !canAskYesNo(env)) {
|
|
215
240
|
console.log(`Zellij update available: ${notice.current_version} -> ${notice.latest_version}. Run: ${notice.upgrade_command}`);
|
|
216
241
|
return { status: 'available', current: notice.current_version, latest: notice.latest_version, command: notice.upgrade_command };
|
|
@@ -284,7 +309,7 @@ function githubLatestTag(timeoutMs) {
|
|
|
284
309
|
});
|
|
285
310
|
}
|
|
286
311
|
function canAskYesNo(env) {
|
|
287
|
-
return
|
|
312
|
+
return resolveZellijUpdatePromptMode({ env, headless: !(process.stdin.isTTY && process.stdout.isTTY) }) === 'interactive-prompt';
|
|
288
313
|
}
|
|
289
314
|
async function askYesNoDefaultYes(question) {
|
|
290
315
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|