harness-everything 0.2.0 → 0.2.1
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/CHANGELOG.md +7 -0
- package/build-multi-agent-system/SKILL.md +68 -68
- package/create-agent-launcher/SKILL.md +48 -48
- package/docs/workflows/build-multi-agent-system.md +62 -62
- package/docs/workflows/create-agent-launcher.md +62 -62
- package/docs/workflows/environment-detection.md +63 -63
- package/docs/workflows/eval-harness.md +57 -57
- package/docs/workflows/fable-discipline.md +59 -59
- package/docs/workflows/fable-mode.md +32 -32
- package/docs/workflows/git-commit.md +62 -62
- package/docs/workflows/grill-me.md +62 -62
- package/docs/workflows/grill-with-docs.md +62 -62
- package/docs/workflows/improve-codebase-architecture.md +60 -60
- package/docs/workflows/repo-docs.md +59 -59
- package/docs/workflows/rewrite-commits.md +60 -60
- package/docs/workflows/security-review.md +58 -58
- package/docs/workflows/self-evolve.md +59 -59
- package/docs/workflows/tdd.md +39 -39
- package/docs/workflows/todo-driven-workflow.md +36 -36
- package/docs/workflows/using-git-worktrees.md +63 -63
- package/docs/workflows/verification-loop.md +69 -69
- package/docs/workflows/verify-before-claim.md +60 -60
- package/docs/workflows/zoom-out.md +62 -62
- package/environment-detection/SKILL.md +77 -77
- package/eval-framework/behavioral-test.js +105 -105
- package/eval-framework/mechanism-test.js +251 -251
- package/eval-harness/SKILL.md +59 -59
- package/fable-discipline/SKILL.md +38 -38
- package/fable-mode/SKILL.md +51 -51
- package/git-commit/SKILL.md +55 -55
- package/grill-me/SKILL.md +36 -36
- package/grill-with-docs/SKILL.md +99 -99
- package/harness-everything/SKILL.md +141 -141
- package/harness-everything/examples/README.md +9 -9
- package/harness-everything/examples/todo-workflow.md +22 -22
- package/harness-everything/scripts/todo-cli.js +157 -157
- package/harness-everything/scripts/verify-gate.js +31 -31
- package/hooks/scripts/atomic-commit-check.js +68 -68
- package/hooks/scripts/context-compact.js +114 -114
- package/hooks/scripts/lib/harness-state.js +29 -3
- package/hooks/scripts/lib/platforms/claude.js +42 -42
- package/hooks/scripts/lib/platforms/codex.js +46 -46
- package/hooks/scripts/lib/platforms/continue.js +41 -41
- package/hooks/scripts/lib/platforms/copilot.js +41 -41
- package/hooks/scripts/lib/platforms/cursor.js +41 -41
- package/hooks/scripts/rule-of-3.js +176 -176
- package/improve-codebase-architecture/SKILL.md +44 -44
- package/install-cognitive-os/SKILL.md +95 -95
- package/package.json +1 -1
- package/repo-docs/SKILL.md +45 -45
- package/rewrite-commits/SKILL.md +32 -32
- package/scripts/installer.js +649 -649
- package/scripts/lib/advisory-text.js +163 -163
- package/scripts/lib/claude-hooks.js +71 -71
- package/scripts/lib/gitignore.js +80 -58
- package/scripts/lib/manifest.js +82 -82
- package/scripts/lib/prompts.js +241 -241
- package/scripts/lib/skills.js +172 -172
- package/scripts/lib/workspace.js +28 -28
- package/security-review/SKILL.md +510 -510
- package/self-evolve/SKILL.md +54 -54
- package/skill-creator/SKILL.md +91 -91
- package/skill-creator/references/testing-workflow.md +32 -32
- package/skill-style/SKILL.md +48 -48
- package/tdd/SKILL.md +53 -53
- package/todo-driven-workflow/SKILL.md +54 -54
- package/using-git-worktrees/SKILL.md +161 -161
- package/verification-loop/SKILL.md +138 -138
- package/verify-before-claim/SKILL.md +86 -86
- package/zoom-out/SKILL.md +90 -90
package/scripts/lib/manifest.js
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
// Tracks exactly what this package installed, so uninstall can remove
|
|
2
|
-
// precisely those paths instead of sweeping shared directories that may also
|
|
3
|
-
// hold content from other tools, the user's own manual additions, or
|
|
4
|
-
// self-evolve's locally-generated skills.
|
|
5
|
-
//
|
|
6
|
-
// The manifest always lives at <platform-home>/harness-everything/manifest.json
|
|
7
|
-
// - e.g. .claude/harness-everything/manifest.json, .cursor/harness-everything/
|
|
8
|
-
// manifest.json, or (global scope) ~/.agents/harness-everything/manifest.json.
|
|
9
|
-
// harness-everything/ is a subfolder this package exclusively owns inside a
|
|
10
|
-
// directory that already belongs to that platform (or, for global scope, the
|
|
11
|
-
// shared ~/.agents convention) - nothing else creates a directory literally
|
|
12
|
-
// named "harness-everything" there, so nesting under it is what makes "the
|
|
13
|
-
// manifest can only describe our own installs" actually true.
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
|
|
17
|
-
const PACKAGE_NAME = 'harness-everything';
|
|
18
|
-
const HARNESS_AUTHOR = 'Miya Daniel | Harness Core Team';
|
|
19
|
-
const HARNESS_DIR_NAME = 'harness-everything';
|
|
20
|
-
|
|
21
|
-
function getHarnessDir(homeDir) {
|
|
22
|
-
return path.join(homeDir, HARNESS_DIR_NAME);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getManifestPath(homeDir) {
|
|
26
|
-
return path.join(getHarnessDir(homeDir), 'manifest.json');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function readManifest(manifestPath) {
|
|
30
|
-
if (!fs.existsSync(manifestPath)) {
|
|
31
|
-
return { package: PACKAGE_NAME, skills: [] };
|
|
32
|
-
}
|
|
33
|
-
try {
|
|
34
|
-
const data = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
35
|
-
if (!Array.isArray(data.skills)) data.skills = [];
|
|
36
|
-
return data;
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return { package: PACKAGE_NAME, skills: [] };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function writeManifest(manifestPath, data) {
|
|
43
|
-
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
44
|
-
fs.writeFileSync(manifestPath, JSON.stringify(data, null, 2), 'utf8');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function recordSkillInstall(manifestPath, packageVersion, skillId, dirPath) {
|
|
48
|
-
const data = readManifest(manifestPath);
|
|
49
|
-
data.package = PACKAGE_NAME;
|
|
50
|
-
data.version = packageVersion;
|
|
51
|
-
data.updatedAt = new Date().toISOString();
|
|
52
|
-
const idx = data.skills.findIndex(s => s.dirPath === dirPath);
|
|
53
|
-
const entry = { id: skillId, dirPath, installedAt: new Date().toISOString() };
|
|
54
|
-
if (idx !== -1) data.skills[idx] = entry;
|
|
55
|
-
else data.skills.push(entry);
|
|
56
|
-
writeManifest(manifestPath, data);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Removes one skill entry, and - once no skills remain in it - deletes the
|
|
60
|
-
// manifest file itself rather than leaving an empty bookkeeping file behind.
|
|
61
|
-
function removeSkillFromManifest(manifestPath, dirPath) {
|
|
62
|
-
if (!fs.existsSync(manifestPath)) return;
|
|
63
|
-
const data = readManifest(manifestPath);
|
|
64
|
-
data.skills = data.skills.filter(s => s.dirPath !== dirPath);
|
|
65
|
-
if (data.skills.length === 0) {
|
|
66
|
-
fs.unlinkSync(manifestPath);
|
|
67
|
-
} else {
|
|
68
|
-
writeManifest(manifestPath, data);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
module.exports = {
|
|
73
|
-
PACKAGE_NAME,
|
|
74
|
-
HARNESS_AUTHOR,
|
|
75
|
-
HARNESS_DIR_NAME,
|
|
76
|
-
getHarnessDir,
|
|
77
|
-
getManifestPath,
|
|
78
|
-
readManifest,
|
|
79
|
-
writeManifest,
|
|
80
|
-
recordSkillInstall,
|
|
81
|
-
removeSkillFromManifest,
|
|
82
|
-
};
|
|
1
|
+
// Tracks exactly what this package installed, so uninstall can remove
|
|
2
|
+
// precisely those paths instead of sweeping shared directories that may also
|
|
3
|
+
// hold content from other tools, the user's own manual additions, or
|
|
4
|
+
// self-evolve's locally-generated skills.
|
|
5
|
+
//
|
|
6
|
+
// The manifest always lives at <platform-home>/harness-everything/manifest.json
|
|
7
|
+
// - e.g. .claude/harness-everything/manifest.json, .cursor/harness-everything/
|
|
8
|
+
// manifest.json, or (global scope) ~/.agents/harness-everything/manifest.json.
|
|
9
|
+
// harness-everything/ is a subfolder this package exclusively owns inside a
|
|
10
|
+
// directory that already belongs to that platform (or, for global scope, the
|
|
11
|
+
// shared ~/.agents convention) - nothing else creates a directory literally
|
|
12
|
+
// named "harness-everything" there, so nesting under it is what makes "the
|
|
13
|
+
// manifest can only describe our own installs" actually true.
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const PACKAGE_NAME = 'harness-everything';
|
|
18
|
+
const HARNESS_AUTHOR = 'Miya Daniel | Harness Core Team';
|
|
19
|
+
const HARNESS_DIR_NAME = 'harness-everything';
|
|
20
|
+
|
|
21
|
+
function getHarnessDir(homeDir) {
|
|
22
|
+
return path.join(homeDir, HARNESS_DIR_NAME);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getManifestPath(homeDir) {
|
|
26
|
+
return path.join(getHarnessDir(homeDir), 'manifest.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readManifest(manifestPath) {
|
|
30
|
+
if (!fs.existsSync(manifestPath)) {
|
|
31
|
+
return { package: PACKAGE_NAME, skills: [] };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const data = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
35
|
+
if (!Array.isArray(data.skills)) data.skills = [];
|
|
36
|
+
return data;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return { package: PACKAGE_NAME, skills: [] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeManifest(manifestPath, data) {
|
|
43
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
44
|
+
fs.writeFileSync(manifestPath, JSON.stringify(data, null, 2), 'utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function recordSkillInstall(manifestPath, packageVersion, skillId, dirPath) {
|
|
48
|
+
const data = readManifest(manifestPath);
|
|
49
|
+
data.package = PACKAGE_NAME;
|
|
50
|
+
data.version = packageVersion;
|
|
51
|
+
data.updatedAt = new Date().toISOString();
|
|
52
|
+
const idx = data.skills.findIndex(s => s.dirPath === dirPath);
|
|
53
|
+
const entry = { id: skillId, dirPath, installedAt: new Date().toISOString() };
|
|
54
|
+
if (idx !== -1) data.skills[idx] = entry;
|
|
55
|
+
else data.skills.push(entry);
|
|
56
|
+
writeManifest(manifestPath, data);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Removes one skill entry, and - once no skills remain in it - deletes the
|
|
60
|
+
// manifest file itself rather than leaving an empty bookkeeping file behind.
|
|
61
|
+
function removeSkillFromManifest(manifestPath, dirPath) {
|
|
62
|
+
if (!fs.existsSync(manifestPath)) return;
|
|
63
|
+
const data = readManifest(manifestPath);
|
|
64
|
+
data.skills = data.skills.filter(s => s.dirPath !== dirPath);
|
|
65
|
+
if (data.skills.length === 0) {
|
|
66
|
+
fs.unlinkSync(manifestPath);
|
|
67
|
+
} else {
|
|
68
|
+
writeManifest(manifestPath, data);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
PACKAGE_NAME,
|
|
74
|
+
HARNESS_AUTHOR,
|
|
75
|
+
HARNESS_DIR_NAME,
|
|
76
|
+
getHarnessDir,
|
|
77
|
+
getManifestPath,
|
|
78
|
+
readManifest,
|
|
79
|
+
writeManifest,
|
|
80
|
+
recordSkillInstall,
|
|
81
|
+
removeSkillFromManifest,
|
|
82
|
+
};
|
package/scripts/lib/prompts.js
CHANGED
|
@@ -1,241 +1,241 @@
|
|
|
1
|
-
// Terminal TUI primitives used by the interactive installer flow. No install
|
|
2
|
-
// or filesystem logic lives here - purely input/output.
|
|
3
|
-
const readline = require('readline');
|
|
4
|
-
|
|
5
|
-
function interactiveSelect(items, pageSize = items.length) {
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
let cursor = 0;
|
|
8
|
-
let scrollIndex = 0;
|
|
9
|
-
let visibleItems = [];
|
|
10
|
-
const isScrollable = items.length > pageSize;
|
|
11
|
-
|
|
12
|
-
function render() {
|
|
13
|
-
readline.cursorTo(process.stdout, 0);
|
|
14
|
-
|
|
15
|
-
if (isScrollable) {
|
|
16
|
-
if (cursor >= scrollIndex + pageSize) {
|
|
17
|
-
scrollIndex = cursor - pageSize + 1;
|
|
18
|
-
} else if (cursor < scrollIndex) {
|
|
19
|
-
scrollIndex = cursor;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
visibleItems = items.slice(scrollIndex, scrollIndex + pageSize);
|
|
24
|
-
const writeLines = [];
|
|
25
|
-
|
|
26
|
-
if (isScrollable) {
|
|
27
|
-
if (scrollIndex > 0) {
|
|
28
|
-
writeLines.push(` ▲ (more above)`);
|
|
29
|
-
} else {
|
|
30
|
-
writeLines.push(``); // Spacer
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
for (let i = 0; i < visibleItems.length; i++) {
|
|
35
|
-
const itemIndex = scrollIndex + i;
|
|
36
|
-
const item = visibleItems[i];
|
|
37
|
-
const isSelected = item.checked ? '[x]' : '[ ]';
|
|
38
|
-
const isCurrent = itemIndex === cursor ? '> ' : ' ';
|
|
39
|
-
const desc = item.description ? ` - ${item.description}` : '';
|
|
40
|
-
const lineText = `${isCurrent}${isSelected} ${item.name || item.id}${desc}`;
|
|
41
|
-
const maxCols = process.stdout.columns || 80;
|
|
42
|
-
const truncated = lineText.length > maxCols - 5
|
|
43
|
-
? lineText.slice(0, maxCols - 8) + '...'
|
|
44
|
-
: lineText;
|
|
45
|
-
writeLines.push(truncated);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (isScrollable) {
|
|
49
|
-
if (scrollIndex + pageSize < items.length) {
|
|
50
|
-
writeLines.push(` ▼ (more below)`);
|
|
51
|
-
} else {
|
|
52
|
-
writeLines.push(``); // Spacer
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
writeLines.push(``);
|
|
57
|
-
writeLines.push(`(Use Up/Down Arrow keys to navigate, Space to toggle, Enter to confirm)`);
|
|
58
|
-
|
|
59
|
-
for (const line of writeLines) {
|
|
60
|
-
readline.clearLine(process.stdout, 0);
|
|
61
|
-
process.stdout.write(`${line}\n`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
readline.emitKeypressEvents(process.stdin);
|
|
66
|
-
if (process.stdin.isTTY) {
|
|
67
|
-
process.stdin.setRawMode(true);
|
|
68
|
-
}
|
|
69
|
-
process.stdin.resume();
|
|
70
|
-
|
|
71
|
-
// Hide cursor
|
|
72
|
-
process.stdout.write('[?25l');
|
|
73
|
-
|
|
74
|
-
render();
|
|
75
|
-
|
|
76
|
-
function cleanup() {
|
|
77
|
-
// Show cursor again
|
|
78
|
-
process.stdout.write('[?25h');
|
|
79
|
-
process.stdin.removeListener('keypress', handleKeypress);
|
|
80
|
-
if (process.stdin.isTTY) {
|
|
81
|
-
process.stdin.setRawMode(false);
|
|
82
|
-
}
|
|
83
|
-
process.stdin.pause();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function handleKeypress(str, key) {
|
|
87
|
-
if (key) {
|
|
88
|
-
if (key.ctrl && key.name === 'c') {
|
|
89
|
-
cleanup();
|
|
90
|
-
process.exit(130);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const linesToMove = pageSize + (isScrollable ? 4 : 2);
|
|
94
|
-
|
|
95
|
-
if (key.name === 'up') {
|
|
96
|
-
cursor = (cursor - 1 + items.length) % items.length;
|
|
97
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
98
|
-
render();
|
|
99
|
-
} else if (key.name === 'down') {
|
|
100
|
-
cursor = (cursor + 1) % items.length;
|
|
101
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
102
|
-
render();
|
|
103
|
-
} else if (key.name === 'space' || str === ' ') {
|
|
104
|
-
const itemIndex = cursor;
|
|
105
|
-
items[itemIndex].checked = !items[itemIndex].checked;
|
|
106
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
107
|
-
render();
|
|
108
|
-
} else if (key.name === 'return' || key.name === 'enter') {
|
|
109
|
-
cleanup();
|
|
110
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
111
|
-
for (let i = 0; i < linesToMove; i++) {
|
|
112
|
-
readline.clearLine(process.stdout, 0);
|
|
113
|
-
process.stdout.write('\n');
|
|
114
|
-
}
|
|
115
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
116
|
-
|
|
117
|
-
for (let i = 0; i < visibleItems.length; i++) {
|
|
118
|
-
readline.clearLine(process.stdout, 0);
|
|
119
|
-
const item = visibleItems[i];
|
|
120
|
-
const isSelected = item.checked ? '✔' : ' ';
|
|
121
|
-
process.stdout.write(` ${isSelected} ${item.name || item.id}\n`);
|
|
122
|
-
}
|
|
123
|
-
if (isScrollable) {
|
|
124
|
-
readline.clearLine(process.stdout, 0);
|
|
125
|
-
process.stdout.write('\n');
|
|
126
|
-
}
|
|
127
|
-
readline.clearLine(process.stdout, 0);
|
|
128
|
-
process.stdout.write('\n');
|
|
129
|
-
resolve(items);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
process.stdin.on('keypress', handleKeypress);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function askQuestion(query) {
|
|
139
|
-
const rl = readline.createInterface({
|
|
140
|
-
input: process.stdin,
|
|
141
|
-
output: process.stdout,
|
|
142
|
-
});
|
|
143
|
-
return new Promise(resolve => {
|
|
144
|
-
rl.question(query, answer => {
|
|
145
|
-
rl.close();
|
|
146
|
-
resolve(answer.trim().toLowerCase());
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function interactiveSingleSelect(items) {
|
|
152
|
-
return new Promise((resolve) => {
|
|
153
|
-
let cursor = 0;
|
|
154
|
-
if (!items.some(i => i.checked)) {
|
|
155
|
-
items[0].checked = true;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function render() {
|
|
159
|
-
readline.cursorTo(process.stdout, 0);
|
|
160
|
-
for (let i = 0; i < items.length; i++) {
|
|
161
|
-
const item = items[i];
|
|
162
|
-
const isSelected = item.checked ? '(o)' : '( )';
|
|
163
|
-
const isCurrent = i === cursor ? '> ' : ' ';
|
|
164
|
-
readline.clearLine(process.stdout, 0);
|
|
165
|
-
process.stdout.write(`${isCurrent}${isSelected} ${item.name}\n`);
|
|
166
|
-
}
|
|
167
|
-
readline.clearLine(process.stdout, 0);
|
|
168
|
-
process.stdout.write(`\n`);
|
|
169
|
-
readline.clearLine(process.stdout, 0);
|
|
170
|
-
process.stdout.write(`(Use Up/Down Arrow keys to navigate, Space to select, Enter to confirm)\n`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
readline.emitKeypressEvents(process.stdin);
|
|
174
|
-
if (process.stdin.isTTY) {
|
|
175
|
-
process.stdin.setRawMode(true);
|
|
176
|
-
}
|
|
177
|
-
process.stdin.resume();
|
|
178
|
-
process.stdout.write('[?25l');
|
|
179
|
-
render();
|
|
180
|
-
|
|
181
|
-
function cleanup() {
|
|
182
|
-
process.stdout.write('[?25h');
|
|
183
|
-
process.stdin.removeListener('keypress', handleKeypress);
|
|
184
|
-
if (process.stdin.isTTY) {
|
|
185
|
-
process.stdin.setRawMode(false);
|
|
186
|
-
}
|
|
187
|
-
process.stdin.pause();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function handleKeypress(str, key) {
|
|
191
|
-
if (key) {
|
|
192
|
-
if (key.ctrl && key.name === 'c') {
|
|
193
|
-
cleanup();
|
|
194
|
-
process.exit(130);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const linesToMove = items.length + 2;
|
|
198
|
-
|
|
199
|
-
if (key.name === 'up') {
|
|
200
|
-
cursor = (cursor - 1 + items.length) % items.length;
|
|
201
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
202
|
-
render();
|
|
203
|
-
} else if (key.name === 'down') {
|
|
204
|
-
cursor = (cursor + 1) % items.length;
|
|
205
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
206
|
-
render();
|
|
207
|
-
} else if (key.name === 'space' || str === ' ') {
|
|
208
|
-
items.forEach((item, idx) => {
|
|
209
|
-
item.checked = (idx === cursor);
|
|
210
|
-
});
|
|
211
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
212
|
-
render();
|
|
213
|
-
} else if (key.name === 'return' || key.name === 'enter') {
|
|
214
|
-
items.forEach((item, idx) => {
|
|
215
|
-
item.checked = (idx === cursor);
|
|
216
|
-
});
|
|
217
|
-
cleanup();
|
|
218
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
219
|
-
for (let i = 0; i < linesToMove; i++) {
|
|
220
|
-
readline.clearLine(process.stdout, 0);
|
|
221
|
-
process.stdout.write('\n');
|
|
222
|
-
}
|
|
223
|
-
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
224
|
-
|
|
225
|
-
for (let i = 0; i < items.length; i++) {
|
|
226
|
-
readline.clearLine(process.stdout, 0);
|
|
227
|
-
const item = items[i];
|
|
228
|
-
const isSelected = item.checked ? '✔' : ' ';
|
|
229
|
-
process.stdout.write(` ${isSelected} ${item.name}\n`);
|
|
230
|
-
}
|
|
231
|
-
readline.clearLine(process.stdout, 0);
|
|
232
|
-
process.stdout.write('\n');
|
|
233
|
-
resolve(items.find(item => item.checked).id);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
process.stdin.on('keypress', handleKeypress);
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
module.exports = { interactiveSelect, interactiveSingleSelect, askQuestion };
|
|
1
|
+
// Terminal TUI primitives used by the interactive installer flow. No install
|
|
2
|
+
// or filesystem logic lives here - purely input/output.
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
function interactiveSelect(items, pageSize = items.length) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
let cursor = 0;
|
|
8
|
+
let scrollIndex = 0;
|
|
9
|
+
let visibleItems = [];
|
|
10
|
+
const isScrollable = items.length > pageSize;
|
|
11
|
+
|
|
12
|
+
function render() {
|
|
13
|
+
readline.cursorTo(process.stdout, 0);
|
|
14
|
+
|
|
15
|
+
if (isScrollable) {
|
|
16
|
+
if (cursor >= scrollIndex + pageSize) {
|
|
17
|
+
scrollIndex = cursor - pageSize + 1;
|
|
18
|
+
} else if (cursor < scrollIndex) {
|
|
19
|
+
scrollIndex = cursor;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
visibleItems = items.slice(scrollIndex, scrollIndex + pageSize);
|
|
24
|
+
const writeLines = [];
|
|
25
|
+
|
|
26
|
+
if (isScrollable) {
|
|
27
|
+
if (scrollIndex > 0) {
|
|
28
|
+
writeLines.push(` ▲ (more above)`);
|
|
29
|
+
} else {
|
|
30
|
+
writeLines.push(``); // Spacer
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
35
|
+
const itemIndex = scrollIndex + i;
|
|
36
|
+
const item = visibleItems[i];
|
|
37
|
+
const isSelected = item.checked ? '[x]' : '[ ]';
|
|
38
|
+
const isCurrent = itemIndex === cursor ? '> ' : ' ';
|
|
39
|
+
const desc = item.description ? ` - ${item.description}` : '';
|
|
40
|
+
const lineText = `${isCurrent}${isSelected} ${item.name || item.id}${desc}`;
|
|
41
|
+
const maxCols = process.stdout.columns || 80;
|
|
42
|
+
const truncated = lineText.length > maxCols - 5
|
|
43
|
+
? lineText.slice(0, maxCols - 8) + '...'
|
|
44
|
+
: lineText;
|
|
45
|
+
writeLines.push(truncated);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isScrollable) {
|
|
49
|
+
if (scrollIndex + pageSize < items.length) {
|
|
50
|
+
writeLines.push(` ▼ (more below)`);
|
|
51
|
+
} else {
|
|
52
|
+
writeLines.push(``); // Spacer
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
writeLines.push(``);
|
|
57
|
+
writeLines.push(`(Use Up/Down Arrow keys to navigate, Space to toggle, Enter to confirm)`);
|
|
58
|
+
|
|
59
|
+
for (const line of writeLines) {
|
|
60
|
+
readline.clearLine(process.stdout, 0);
|
|
61
|
+
process.stdout.write(`${line}\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
readline.emitKeypressEvents(process.stdin);
|
|
66
|
+
if (process.stdin.isTTY) {
|
|
67
|
+
process.stdin.setRawMode(true);
|
|
68
|
+
}
|
|
69
|
+
process.stdin.resume();
|
|
70
|
+
|
|
71
|
+
// Hide cursor
|
|
72
|
+
process.stdout.write('[?25l');
|
|
73
|
+
|
|
74
|
+
render();
|
|
75
|
+
|
|
76
|
+
function cleanup() {
|
|
77
|
+
// Show cursor again
|
|
78
|
+
process.stdout.write('[?25h');
|
|
79
|
+
process.stdin.removeListener('keypress', handleKeypress);
|
|
80
|
+
if (process.stdin.isTTY) {
|
|
81
|
+
process.stdin.setRawMode(false);
|
|
82
|
+
}
|
|
83
|
+
process.stdin.pause();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleKeypress(str, key) {
|
|
87
|
+
if (key) {
|
|
88
|
+
if (key.ctrl && key.name === 'c') {
|
|
89
|
+
cleanup();
|
|
90
|
+
process.exit(130);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const linesToMove = pageSize + (isScrollable ? 4 : 2);
|
|
94
|
+
|
|
95
|
+
if (key.name === 'up') {
|
|
96
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
97
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
98
|
+
render();
|
|
99
|
+
} else if (key.name === 'down') {
|
|
100
|
+
cursor = (cursor + 1) % items.length;
|
|
101
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
102
|
+
render();
|
|
103
|
+
} else if (key.name === 'space' || str === ' ') {
|
|
104
|
+
const itemIndex = cursor;
|
|
105
|
+
items[itemIndex].checked = !items[itemIndex].checked;
|
|
106
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
107
|
+
render();
|
|
108
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
109
|
+
cleanup();
|
|
110
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
111
|
+
for (let i = 0; i < linesToMove; i++) {
|
|
112
|
+
readline.clearLine(process.stdout, 0);
|
|
113
|
+
process.stdout.write('\n');
|
|
114
|
+
}
|
|
115
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
118
|
+
readline.clearLine(process.stdout, 0);
|
|
119
|
+
const item = visibleItems[i];
|
|
120
|
+
const isSelected = item.checked ? '✔' : ' ';
|
|
121
|
+
process.stdout.write(` ${isSelected} ${item.name || item.id}\n`);
|
|
122
|
+
}
|
|
123
|
+
if (isScrollable) {
|
|
124
|
+
readline.clearLine(process.stdout, 0);
|
|
125
|
+
process.stdout.write('\n');
|
|
126
|
+
}
|
|
127
|
+
readline.clearLine(process.stdout, 0);
|
|
128
|
+
process.stdout.write('\n');
|
|
129
|
+
resolve(items);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
process.stdin.on('keypress', handleKeypress);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function askQuestion(query) {
|
|
139
|
+
const rl = readline.createInterface({
|
|
140
|
+
input: process.stdin,
|
|
141
|
+
output: process.stdout,
|
|
142
|
+
});
|
|
143
|
+
return new Promise(resolve => {
|
|
144
|
+
rl.question(query, answer => {
|
|
145
|
+
rl.close();
|
|
146
|
+
resolve(answer.trim().toLowerCase());
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function interactiveSingleSelect(items) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
if (!items.some(i => i.checked)) {
|
|
155
|
+
items[0].checked = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function render() {
|
|
159
|
+
readline.cursorTo(process.stdout, 0);
|
|
160
|
+
for (let i = 0; i < items.length; i++) {
|
|
161
|
+
const item = items[i];
|
|
162
|
+
const isSelected = item.checked ? '(o)' : '( )';
|
|
163
|
+
const isCurrent = i === cursor ? '> ' : ' ';
|
|
164
|
+
readline.clearLine(process.stdout, 0);
|
|
165
|
+
process.stdout.write(`${isCurrent}${isSelected} ${item.name}\n`);
|
|
166
|
+
}
|
|
167
|
+
readline.clearLine(process.stdout, 0);
|
|
168
|
+
process.stdout.write(`\n`);
|
|
169
|
+
readline.clearLine(process.stdout, 0);
|
|
170
|
+
process.stdout.write(`(Use Up/Down Arrow keys to navigate, Space to select, Enter to confirm)\n`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
readline.emitKeypressEvents(process.stdin);
|
|
174
|
+
if (process.stdin.isTTY) {
|
|
175
|
+
process.stdin.setRawMode(true);
|
|
176
|
+
}
|
|
177
|
+
process.stdin.resume();
|
|
178
|
+
process.stdout.write('[?25l');
|
|
179
|
+
render();
|
|
180
|
+
|
|
181
|
+
function cleanup() {
|
|
182
|
+
process.stdout.write('[?25h');
|
|
183
|
+
process.stdin.removeListener('keypress', handleKeypress);
|
|
184
|
+
if (process.stdin.isTTY) {
|
|
185
|
+
process.stdin.setRawMode(false);
|
|
186
|
+
}
|
|
187
|
+
process.stdin.pause();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handleKeypress(str, key) {
|
|
191
|
+
if (key) {
|
|
192
|
+
if (key.ctrl && key.name === 'c') {
|
|
193
|
+
cleanup();
|
|
194
|
+
process.exit(130);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const linesToMove = items.length + 2;
|
|
198
|
+
|
|
199
|
+
if (key.name === 'up') {
|
|
200
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
201
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
202
|
+
render();
|
|
203
|
+
} else if (key.name === 'down') {
|
|
204
|
+
cursor = (cursor + 1) % items.length;
|
|
205
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
206
|
+
render();
|
|
207
|
+
} else if (key.name === 'space' || str === ' ') {
|
|
208
|
+
items.forEach((item, idx) => {
|
|
209
|
+
item.checked = (idx === cursor);
|
|
210
|
+
});
|
|
211
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
212
|
+
render();
|
|
213
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
214
|
+
items.forEach((item, idx) => {
|
|
215
|
+
item.checked = (idx === cursor);
|
|
216
|
+
});
|
|
217
|
+
cleanup();
|
|
218
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
219
|
+
for (let i = 0; i < linesToMove; i++) {
|
|
220
|
+
readline.clearLine(process.stdout, 0);
|
|
221
|
+
process.stdout.write('\n');
|
|
222
|
+
}
|
|
223
|
+
readline.moveCursor(process.stdout, 0, -linesToMove);
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < items.length; i++) {
|
|
226
|
+
readline.clearLine(process.stdout, 0);
|
|
227
|
+
const item = items[i];
|
|
228
|
+
const isSelected = item.checked ? '✔' : ' ';
|
|
229
|
+
process.stdout.write(` ${isSelected} ${item.name}\n`);
|
|
230
|
+
}
|
|
231
|
+
readline.clearLine(process.stdout, 0);
|
|
232
|
+
process.stdout.write('\n');
|
|
233
|
+
resolve(items.find(item => item.checked).id);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
process.stdin.on('keypress', handleKeypress);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { interactiveSelect, interactiveSingleSelect, askQuestion };
|