@mohammadhprp/system-prompt 0.13.1 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/system-prompt.js +1 -1
- package/package.json +1 -1
- package/src/cli/args.js +51 -0
- package/src/cli/clack-ui.js +13 -0
- package/src/cli/index.js +47 -0
- package/src/cli/interactive.js +213 -0
- package/src/cli/plan.js +85 -0
- package/src/cli/tui-preferences.js +118 -0
- package/src/config/mcp.js +25 -0
- package/src/config/opencode.js +47 -0
- package/src/{agent-configs.js → config/tui.js} +0 -76
- package/src/doctor.js +10 -7
- package/src/hash.js +5 -0
- package/src/install/env.js +69 -0
- package/src/install/files.js +151 -0
- package/src/install/index.js +124 -0
- package/src/install/lock.js +159 -0
- package/src/install/merge.js +62 -0
- package/src/install/templates.js +76 -0
- package/src/item-layout.js +5 -0
- package/src/paths.js +59 -0
- package/src/ui.js +32 -0
- package/src/cli.js +0 -508
- package/src/installer.js +0 -653
package/bin/system-prompt.js
CHANGED
package/package.json
CHANGED
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { categories } from '../catalog.js';
|
|
2
|
+
|
|
3
|
+
const CATEGORY_FLAGS = new Set(Object.keys(categories));
|
|
4
|
+
|
|
5
|
+
export function parseArgs(argv) {
|
|
6
|
+
const options = {
|
|
7
|
+
targetDir: '.opencode',
|
|
8
|
+
selections: {},
|
|
9
|
+
all: false,
|
|
10
|
+
dryRun: false,
|
|
11
|
+
force: false,
|
|
12
|
+
doctor: false,
|
|
13
|
+
includeAgentsMd: true,
|
|
14
|
+
};
|
|
15
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
16
|
+
const arg = argv[index];
|
|
17
|
+
if (arg === '--all') options.all = true;
|
|
18
|
+
else if (arg === '--dry-run') options.dryRun = true;
|
|
19
|
+
else if (arg === '--force') options.force = true;
|
|
20
|
+
else if (arg === '--doctor') options.doctor = true;
|
|
21
|
+
else if (arg === '--no-agents-md') options.includeAgentsMd = false;
|
|
22
|
+
else if (arg === '--target') {
|
|
23
|
+
const value = argv[++index];
|
|
24
|
+
if (!value || value.startsWith('--')) throw new Error('Missing value for --target');
|
|
25
|
+
options.targetDir = value;
|
|
26
|
+
}
|
|
27
|
+
else if (arg.startsWith('--target=')) {
|
|
28
|
+
const value = arg.slice('--target='.length);
|
|
29
|
+
if (!value) throw new Error('Missing value for --target');
|
|
30
|
+
options.targetDir = value;
|
|
31
|
+
}
|
|
32
|
+
else if (arg.startsWith('--')) {
|
|
33
|
+
const [flag, inlineValue] = arg.slice(2).split('=', 2);
|
|
34
|
+
if (!CATEGORY_FLAGS.has(flag)) throw new Error(`Unknown option: ${arg}`);
|
|
35
|
+
const value = inlineValue ?? argv[++index];
|
|
36
|
+
if (!value) throw new Error(`Missing value for --${flag}`);
|
|
37
|
+
options.selections[flag] = value.split(',').filter(Boolean);
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
options.nonInteractive = options.all || Object.keys(options.selections).length > 0;
|
|
43
|
+
return options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function allSelections() {
|
|
47
|
+
return Object.fromEntries(Object.entries(categories).map(([category, config]) => [
|
|
48
|
+
category,
|
|
49
|
+
config.items.filter(item => !item.removed).map(item => item.id),
|
|
50
|
+
]));
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { confirm, intro, isCancel, multiselect, note, outro, select, spinner } from '@clack/prompts';
|
|
2
|
+
|
|
3
|
+
export const defaultUi = {
|
|
4
|
+
intro,
|
|
5
|
+
outro,
|
|
6
|
+
confirm,
|
|
7
|
+
multiselect,
|
|
8
|
+
spinner,
|
|
9
|
+
select,
|
|
10
|
+
note,
|
|
11
|
+
isCancel,
|
|
12
|
+
log: (...args) => console.log(...args),
|
|
13
|
+
};
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { doctor } from '../doctor.js';
|
|
4
|
+
import { install } from '../install/index.js';
|
|
5
|
+
import { loadLockFile, lockToSelections } from '../install/lock.js';
|
|
6
|
+
import { getPackageVersion } from '../paths.js';
|
|
7
|
+
import { status } from '../ui.js';
|
|
8
|
+
import { allSelections, parseArgs } from './args.js';
|
|
9
|
+
import { defaultUi } from './clack-ui.js';
|
|
10
|
+
import { runInteractive } from './interactive.js';
|
|
11
|
+
|
|
12
|
+
export async function runNonInteractive({ targetDir, selections, all, includeAgentsMd, force = false, dryRun = false, agentType = 'opencode', ui = defaultUi }) {
|
|
13
|
+
const resolvedSelections = all ? allSelections() : selections;
|
|
14
|
+
const absTarget = resolve(process.cwd(), targetDir);
|
|
15
|
+
const oldLock = await loadLockFile(absTarget);
|
|
16
|
+
if (!dryRun) ui.log(status('info', `Installing selected components into ${absTarget}`));
|
|
17
|
+
await install({
|
|
18
|
+
targetDir,
|
|
19
|
+
agentType,
|
|
20
|
+
selections: resolvedSelections,
|
|
21
|
+
includeAgentsMd,
|
|
22
|
+
oldSelections: oldLock ? lockToSelections(oldLock) : undefined,
|
|
23
|
+
oldLock,
|
|
24
|
+
force,
|
|
25
|
+
dryRun,
|
|
26
|
+
});
|
|
27
|
+
ui.log(status(dryRun ? 'info' : 'success', dryRun ? 'Dry run complete.' : 'Installation complete.'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
31
|
+
const args = parseArgs(argv);
|
|
32
|
+
if (args.doctor) {
|
|
33
|
+
const healthy = await doctor(args.targetDir);
|
|
34
|
+
if (!healthy) process.exitCode = 1;
|
|
35
|
+
return healthy;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const version = await getPackageVersion();
|
|
39
|
+
defaultUi.intro(`System prompt · v${version}`);
|
|
40
|
+
|
|
41
|
+
const agentType = 'opencode';
|
|
42
|
+
|
|
43
|
+
if (args.nonInteractive) {
|
|
44
|
+
return runNonInteractive({ ...args, agentType });
|
|
45
|
+
}
|
|
46
|
+
return runInteractive({ ...args, agentType });
|
|
47
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { categories } from '../catalog.js';
|
|
4
|
+
import { install } from '../install/index.js';
|
|
5
|
+
import { loadLockFile, lockToSelections } from '../install/lock.js';
|
|
6
|
+
import { defaultUi } from './clack-ui.js';
|
|
7
|
+
import {
|
|
8
|
+
buildSummary,
|
|
9
|
+
computeDiff,
|
|
10
|
+
fileList,
|
|
11
|
+
formatDiff,
|
|
12
|
+
generatedFiles,
|
|
13
|
+
initialItemValues,
|
|
14
|
+
shouldPreselectAll,
|
|
15
|
+
} from './plan.js';
|
|
16
|
+
import { collectTuiPreferences, readTuiPreferences } from './tui-preferences.js';
|
|
17
|
+
|
|
18
|
+
function cancelled(ui) {
|
|
19
|
+
ui.outro('Cancelled.');
|
|
20
|
+
return { status: 'cancelled' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function reportPlan({ ui, oldLock, selections, includeAgentsMd }) {
|
|
24
|
+
if (oldLock) {
|
|
25
|
+
const diffText = formatDiff(computeDiff(oldLock, selections));
|
|
26
|
+
ui.note(diffText || 'No changes — same selections as before.', 'Changes from previous installation');
|
|
27
|
+
} else {
|
|
28
|
+
ui.note(buildSummary(selections), 'What will be installed');
|
|
29
|
+
}
|
|
30
|
+
ui.note(fileList(generatedFiles(selections, includeAgentsMd)), 'Generated files');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function collectSelections({ ui, selectedCategories, existingSelections }) {
|
|
34
|
+
const selections = {};
|
|
35
|
+
for (const cat of selectedCategories) {
|
|
36
|
+
const catConfig = categories[cat];
|
|
37
|
+
const visibleItems = catConfig.items.filter(item => !item.removed);
|
|
38
|
+
const existingIds = initialItemValues(visibleItems, existingSelections[cat] || []);
|
|
39
|
+
|
|
40
|
+
const all = await ui.confirm({
|
|
41
|
+
message: `Install all ${catConfig.title.toLowerCase()}?`,
|
|
42
|
+
initialValue: shouldPreselectAll(visibleItems, existingIds),
|
|
43
|
+
});
|
|
44
|
+
if (ui.isCancel(all)) return null;
|
|
45
|
+
|
|
46
|
+
if (all) {
|
|
47
|
+
selections[cat] = visibleItems.map(item => item.id);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const picked = await ui.multiselect({
|
|
52
|
+
message: `Which ${catConfig.title.toLowerCase()} do you want?`,
|
|
53
|
+
options: visibleItems.map(item => ({
|
|
54
|
+
value: item.id,
|
|
55
|
+
label: item.deprecated ? `${item.name} (deprecated)` : item.name,
|
|
56
|
+
hint: item.deprecated ? '⚠ Deprecated — consider alternatives' : item.description,
|
|
57
|
+
})),
|
|
58
|
+
initialValues: existingIds,
|
|
59
|
+
required: true,
|
|
60
|
+
});
|
|
61
|
+
if (ui.isCancel(picked)) return null;
|
|
62
|
+
selections[cat] = picked;
|
|
63
|
+
}
|
|
64
|
+
return selections;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function installGeneratedFiles({ ui, targetDir, agentType, force, dryRun, oldLock, existingSelections, existingTuiPreferences }) {
|
|
68
|
+
const includeAgentsMd = await ui.confirm({
|
|
69
|
+
message: 'Generate AGENTS.md?',
|
|
70
|
+
initialValue: oldLock ? oldLock.includeAgentsMd : true,
|
|
71
|
+
});
|
|
72
|
+
if (ui.isCancel(includeAgentsMd)) return cancelled(ui);
|
|
73
|
+
|
|
74
|
+
const hasExisting = Object.keys(existingSelections).length > 0;
|
|
75
|
+
let keepExisting = false;
|
|
76
|
+
if (hasExisting) {
|
|
77
|
+
const removeAll = await ui.confirm({
|
|
78
|
+
message: 'Remove all previously installed components?',
|
|
79
|
+
initialValue: false,
|
|
80
|
+
});
|
|
81
|
+
if (ui.isCancel(removeAll)) return cancelled(ui);
|
|
82
|
+
keepExisting = !removeAll;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const progress = ui.spinner();
|
|
86
|
+
progress.start('Writing files...');
|
|
87
|
+
await install({
|
|
88
|
+
targetDir,
|
|
89
|
+
agentType,
|
|
90
|
+
selections: keepExisting ? existingSelections : {},
|
|
91
|
+
includeAgentsMd,
|
|
92
|
+
oldSelections: hasExisting ? existingSelections : undefined,
|
|
93
|
+
oldLock,
|
|
94
|
+
tuiPreferences: existingTuiPreferences,
|
|
95
|
+
force,
|
|
96
|
+
dryRun,
|
|
97
|
+
});
|
|
98
|
+
progress.stop('Done.');
|
|
99
|
+
|
|
100
|
+
ui.outro(includeAgentsMd ? 'AGENTS.md and configuration written.' : 'Configuration written.');
|
|
101
|
+
return { status: 'installed' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function installSelectedItems({ ui, targetDir, agentType, force, dryRun, oldLock, existingSelections, existingTuiPreferences }, selectedCategories) {
|
|
105
|
+
const includeAgentsMd = await ui.confirm({
|
|
106
|
+
message: 'Generate AGENTS.md?',
|
|
107
|
+
initialValue: oldLock ? oldLock.includeAgentsMd : true,
|
|
108
|
+
});
|
|
109
|
+
if (ui.isCancel(includeAgentsMd)) return cancelled(ui);
|
|
110
|
+
|
|
111
|
+
const selections = await collectSelections({ ui, selectedCategories, existingSelections });
|
|
112
|
+
if (!selections) return cancelled(ui);
|
|
113
|
+
|
|
114
|
+
let tuiPreferences = existingTuiPreferences;
|
|
115
|
+
const customizeTui = await ui.confirm({
|
|
116
|
+
message: 'Customize OpenCode TUI settings?',
|
|
117
|
+
initialValue: false,
|
|
118
|
+
});
|
|
119
|
+
if (ui.isCancel(customizeTui)) return cancelled(ui);
|
|
120
|
+
if (customizeTui) {
|
|
121
|
+
const custom = await collectTuiPreferences(existingTuiPreferences, ui);
|
|
122
|
+
if (!custom) return cancelled(ui);
|
|
123
|
+
tuiPreferences = custom;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (oldLock) {
|
|
127
|
+
const initialDiff = computeDiff(oldLock, selections);
|
|
128
|
+
if (Object.keys(initialDiff.removed).length) {
|
|
129
|
+
const removeDeselected = await ui.confirm({
|
|
130
|
+
message: 'Remove deselected items from the previous installation?',
|
|
131
|
+
initialValue: false,
|
|
132
|
+
});
|
|
133
|
+
if (ui.isCancel(removeDeselected)) return cancelled(ui);
|
|
134
|
+
if (!removeDeselected) {
|
|
135
|
+
for (const [cat, data] of Object.entries(initialDiff.removed)) {
|
|
136
|
+
selections[cat] = [...new Set([...(selections[cat] || []), ...data.ids])];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
reportPlan({ ui, oldLock, selections, includeAgentsMd });
|
|
143
|
+
|
|
144
|
+
const confirmed = await ui.confirm({
|
|
145
|
+
message: 'Proceed with installation?',
|
|
146
|
+
initialValue: true,
|
|
147
|
+
});
|
|
148
|
+
if (ui.isCancel(confirmed) || !confirmed) {
|
|
149
|
+
ui.outro('Installation cancelled.');
|
|
150
|
+
return { status: 'cancelled' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const progress = ui.spinner();
|
|
154
|
+
progress.start(oldLock ? 'Updating files...' : 'Installing files...');
|
|
155
|
+
|
|
156
|
+
const finalTarget = await install({
|
|
157
|
+
targetDir,
|
|
158
|
+
agentType,
|
|
159
|
+
selections,
|
|
160
|
+
includeAgentsMd,
|
|
161
|
+
oldSelections: oldLock ? existingSelections : undefined,
|
|
162
|
+
oldLock,
|
|
163
|
+
tuiPreferences,
|
|
164
|
+
force,
|
|
165
|
+
dryRun,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
progress.stop('Installation complete!');
|
|
169
|
+
|
|
170
|
+
const fileCount = Object.values(selections).reduce((sum, ids) => sum + (ids?.length || 0), 0);
|
|
171
|
+
const verb = oldLock ? 'Updated' : 'Installed';
|
|
172
|
+
ui.note(
|
|
173
|
+
[
|
|
174
|
+
agentType === 'opencode'
|
|
175
|
+
? 'Open your project in OpenCode — it reads opencode.json and AGENTS.md automatically.'
|
|
176
|
+
: 'Point your AI coding agent to AGENTS.md as the entry point.',
|
|
177
|
+
'Run /help in your agent to see available commands.',
|
|
178
|
+
].join('\n'),
|
|
179
|
+
'Next steps',
|
|
180
|
+
);
|
|
181
|
+
ui.outro(`${verb} ${fileCount} components to ${finalTarget}`);
|
|
182
|
+
return { status: 'installed', target: finalTarget };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function runInteractive({ targetDir, force = false, dryRun = false, agentType = 'opencode', ui = defaultUi }) {
|
|
186
|
+
const absTarget = resolve(process.cwd(), targetDir);
|
|
187
|
+
const oldLock = await loadLockFile(absTarget);
|
|
188
|
+
const existingSelections = lockToSelections(oldLock);
|
|
189
|
+
const existingTuiPreferences = await readTuiPreferences(absTarget);
|
|
190
|
+
const context = { ui, targetDir, agentType, force, dryRun, oldLock, existingSelections, existingTuiPreferences };
|
|
191
|
+
|
|
192
|
+
const selectedCategories = await ui.multiselect({
|
|
193
|
+
message: 'What would you like to install?',
|
|
194
|
+
options: buildCategoryOptions(),
|
|
195
|
+
initialValues: Object.keys(existingSelections),
|
|
196
|
+
required: false,
|
|
197
|
+
});
|
|
198
|
+
if (ui.isCancel(selectedCategories)) return cancelled(ui);
|
|
199
|
+
|
|
200
|
+
if (!selectedCategories?.length) return installGeneratedFiles(context);
|
|
201
|
+
return installSelectedItems(context, selectedCategories);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildCategoryOptions() {
|
|
205
|
+
return Object.entries(categories).map(([key, cat]) => {
|
|
206
|
+
const visible = cat.items.filter(item => !item.removed);
|
|
207
|
+
return {
|
|
208
|
+
value: key,
|
|
209
|
+
label: cat.title,
|
|
210
|
+
hint: `${visible.length} ${key === 'mcps' ? 'MCPs' : cat.title.toLowerCase()}`,
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
package/src/cli/plan.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { categories } from '../catalog.js';
|
|
2
|
+
import { lockToSelections } from '../install/lock.js';
|
|
3
|
+
|
|
4
|
+
export function computeDiff(oldLock, selections) {
|
|
5
|
+
const oldSels = lockToSelections(oldLock);
|
|
6
|
+
const added = {};
|
|
7
|
+
const removed = {};
|
|
8
|
+
const kept = {};
|
|
9
|
+
|
|
10
|
+
const allCats = [...new Set([...Object.keys(oldSels), ...Object.keys(selections)])];
|
|
11
|
+
|
|
12
|
+
for (const cat of allCats) {
|
|
13
|
+
const catConfig = categories[cat] || null;
|
|
14
|
+
const oldIds = new Set(oldSels[cat] || []);
|
|
15
|
+
const newIds = new Set(selections[cat] || []);
|
|
16
|
+
|
|
17
|
+
const addedIds = [...newIds].filter(id => !oldIds.has(id));
|
|
18
|
+
const removedIds = [...oldIds].filter(id => !newIds.has(id));
|
|
19
|
+
const keptIds = [...newIds].filter(id => oldIds.has(id));
|
|
20
|
+
|
|
21
|
+
if (addedIds.length) added[cat] = { config: catConfig, ids: addedIds };
|
|
22
|
+
if (removedIds.length) removed[cat] = { config: catConfig, ids: removedIds };
|
|
23
|
+
if (keptIds.length) kept[cat] = { config: catConfig, ids: keptIds };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { added, removed, kept };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatDiff(diff) {
|
|
30
|
+
const sections = [
|
|
31
|
+
['+ Added', diff.added],
|
|
32
|
+
['- Removed', diff.removed],
|
|
33
|
+
['~ Unchanged', diff.kept],
|
|
34
|
+
];
|
|
35
|
+
const lines = [];
|
|
36
|
+
|
|
37
|
+
for (const [label, group] of sections) {
|
|
38
|
+
const entries = Object.entries(group);
|
|
39
|
+
if (!entries.length) continue;
|
|
40
|
+
if (lines.length) lines.push('');
|
|
41
|
+
lines.push(` ${label}:`);
|
|
42
|
+
for (const [cat, data] of entries) {
|
|
43
|
+
lines.push(` ${data.config?.title || cat}: ${itemNames(data.config, data.ids).join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function initialItemValues(visibleItems, existingIds = []) {
|
|
51
|
+
const visible = new Set(visibleItems.map(item => item.id));
|
|
52
|
+
return existingIds.filter(id => visible.has(id));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function shouldPreselectAll(visibleItems, existingIds = []) {
|
|
56
|
+
if (existingIds.length) return existingIds.length === visibleItems.length;
|
|
57
|
+
return visibleItems.length <= 12;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildSummary(selections) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
for (const [cat, ids] of Object.entries(selections)) {
|
|
63
|
+
if (!ids?.length) continue;
|
|
64
|
+
const catConfig = categories[cat];
|
|
65
|
+
lines.push(`• ${catConfig?.title || cat}: ${itemNames(catConfig, ids).join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
return lines.join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function generatedFiles(selections, includeAgentsMd) {
|
|
71
|
+
const files = [];
|
|
72
|
+
if (includeAgentsMd) files.push('AGENTS.md');
|
|
73
|
+
files.push('opencode.json', 'tui.json', '.gitignore');
|
|
74
|
+
if (selections.mcps?.length) files.push('.env');
|
|
75
|
+
if (selections.memory?.length) files.push('memory/');
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function fileList(files) {
|
|
80
|
+
return files.map(file => `• ${file}`).join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function itemNames(config, ids) {
|
|
84
|
+
return ids.map(id => config?.items?.find(item => item.id === id)?.name || id);
|
|
85
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { normalizeTuiPreferences, TUI_THEMES, tuiPreferencesFromConfig } from '../config/tui.js';
|
|
5
|
+
|
|
6
|
+
export async function readTuiPreferences(absTarget) {
|
|
7
|
+
try {
|
|
8
|
+
return tuiPreferencesFromConfig(JSON.parse(await readFile(resolve(absTarget, 'tui.json'), 'utf-8')));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (error.code === 'ENOENT' || error instanceof SyntaxError) return {};
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function collectTuiPreferences(existing, ui) {
|
|
16
|
+
const base = normalizeTuiPreferences(existing);
|
|
17
|
+
|
|
18
|
+
const theme = await ui.select({
|
|
19
|
+
message: 'TUI theme',
|
|
20
|
+
options: TUI_THEMES.map(value => ({
|
|
21
|
+
value,
|
|
22
|
+
label: value,
|
|
23
|
+
hint: value === 'system' ? 'Adapts to your terminal' : undefined,
|
|
24
|
+
})),
|
|
25
|
+
initialValue: base.theme,
|
|
26
|
+
});
|
|
27
|
+
if (ui.isCancel(theme)) return null;
|
|
28
|
+
|
|
29
|
+
const diffStyle = await ui.select({
|
|
30
|
+
message: 'Diff style',
|
|
31
|
+
options: [
|
|
32
|
+
{ value: 'auto', label: 'auto', hint: 'Adapts to terminal width' },
|
|
33
|
+
{ value: 'stacked', label: 'stacked', hint: 'Always single column' },
|
|
34
|
+
],
|
|
35
|
+
initialValue: base.diff_style,
|
|
36
|
+
});
|
|
37
|
+
if (ui.isCancel(diffStyle)) return null;
|
|
38
|
+
|
|
39
|
+
const cursorStyle = await ui.select({
|
|
40
|
+
message: 'Cursor style',
|
|
41
|
+
options: ['block', 'underline', 'line', 'default'].map(value => ({ value, label: value })),
|
|
42
|
+
initialValue: base.cursor.style,
|
|
43
|
+
});
|
|
44
|
+
if (ui.isCancel(cursorStyle)) return null;
|
|
45
|
+
|
|
46
|
+
const scrollSpeed = await ui.select({
|
|
47
|
+
message: 'Scroll speed',
|
|
48
|
+
options: [
|
|
49
|
+
{ value: 1, label: '1', hint: 'Slow' },
|
|
50
|
+
{ value: 2, label: '2' },
|
|
51
|
+
{ value: 3, label: '3', hint: 'Default' },
|
|
52
|
+
{ value: 4, label: '4' },
|
|
53
|
+
{ value: 5, label: '5', hint: 'Fast' },
|
|
54
|
+
],
|
|
55
|
+
initialValue: base.scroll_speed,
|
|
56
|
+
});
|
|
57
|
+
if (ui.isCancel(scrollSpeed)) return null;
|
|
58
|
+
|
|
59
|
+
const scrollAcceleration = await ui.confirm({
|
|
60
|
+
message: 'Enable scroll acceleration?',
|
|
61
|
+
initialValue: base.scroll_acceleration,
|
|
62
|
+
});
|
|
63
|
+
if (ui.isCancel(scrollAcceleration)) return null;
|
|
64
|
+
|
|
65
|
+
const mouse = await ui.confirm({
|
|
66
|
+
message: 'Enable mouse support?',
|
|
67
|
+
initialValue: base.mouse,
|
|
68
|
+
});
|
|
69
|
+
if (ui.isCancel(mouse)) return null;
|
|
70
|
+
|
|
71
|
+
const attentionEnabled = await ui.confirm({
|
|
72
|
+
message: 'Enable attention notifications and sounds?',
|
|
73
|
+
initialValue: base.attention.enabled,
|
|
74
|
+
});
|
|
75
|
+
if (ui.isCancel(attentionEnabled)) return null;
|
|
76
|
+
|
|
77
|
+
const attention = { ...base.attention, enabled: attentionEnabled };
|
|
78
|
+
if (attentionEnabled) {
|
|
79
|
+
const notifications = await ui.confirm({
|
|
80
|
+
message: 'Desktop notifications?',
|
|
81
|
+
initialValue: base.attention.notifications,
|
|
82
|
+
});
|
|
83
|
+
if (ui.isCancel(notifications)) return null;
|
|
84
|
+
|
|
85
|
+
const sound = await ui.confirm({
|
|
86
|
+
message: 'Sound alerts?',
|
|
87
|
+
initialValue: base.attention.sound,
|
|
88
|
+
});
|
|
89
|
+
if (ui.isCancel(sound)) return null;
|
|
90
|
+
|
|
91
|
+
const volume = await ui.select({
|
|
92
|
+
message: 'Alert volume',
|
|
93
|
+
options: [
|
|
94
|
+
{ value: 0.2, label: '20%' },
|
|
95
|
+
{ value: 0.4, label: '40%', hint: 'Default' },
|
|
96
|
+
{ value: 0.6, label: '60%' },
|
|
97
|
+
{ value: 0.8, label: '80%' },
|
|
98
|
+
{ value: 1, label: '100%' },
|
|
99
|
+
],
|
|
100
|
+
initialValue: base.attention.volume,
|
|
101
|
+
});
|
|
102
|
+
if (ui.isCancel(volume)) return null;
|
|
103
|
+
|
|
104
|
+
attention.notifications = notifications;
|
|
105
|
+
attention.sound = sound;
|
|
106
|
+
attention.volume = volume;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return normalizeTuiPreferences({
|
|
110
|
+
theme,
|
|
111
|
+
diff_style: diffStyle,
|
|
112
|
+
cursor: { style: cursorStyle, blinking: base.cursor.blinking },
|
|
113
|
+
scroll_speed: scrollSpeed,
|
|
114
|
+
scroll_acceleration: scrollAcceleration,
|
|
115
|
+
mouse,
|
|
116
|
+
attention,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { isMissing, resolveSource } from '../paths.js';
|
|
4
|
+
import { status } from '../ui.js';
|
|
5
|
+
|
|
6
|
+
export async function loadMcpConfigs(selectedIds) {
|
|
7
|
+
const entries = {};
|
|
8
|
+
for (const id of selectedIds) {
|
|
9
|
+
try {
|
|
10
|
+
const configPath = resolveSource(`framework/mcps/${id}/configs/opencode.json`);
|
|
11
|
+
const raw = await readFile(configPath, 'utf-8');
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if (parsed.mcp) {
|
|
14
|
+
Object.assign(entries, parsed.mcp);
|
|
15
|
+
}
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (isMissing(error)) {
|
|
18
|
+
console.warn(status('warn', `No opencode.json config found for MCP: ${id}`));
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return entries;
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function generateOpenCodeConfig({ selections, mcpEntries, includeAgentsMd }) {
|
|
2
|
+
const plugins = [];
|
|
3
|
+
if (selections.plugins?.includes('opencode-goal-plugin')) {
|
|
4
|
+
plugins.push('@prevalentware/opencode-goal-plugin');
|
|
5
|
+
}
|
|
6
|
+
if (selections.plugins?.includes('ponytail')) {
|
|
7
|
+
plugins.push('@dietrichgebert/ponytail');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const instructions = [];
|
|
11
|
+
if (includeAgentsMd) instructions.push('AGENTS.md');
|
|
12
|
+
if (selections.memory?.length) instructions.push('.opencode/memory/*.md');
|
|
13
|
+
|
|
14
|
+
const config = {
|
|
15
|
+
$schema: 'https://opencode.ai/config.json',
|
|
16
|
+
formatter: true,
|
|
17
|
+
lsp: false,
|
|
18
|
+
instructions,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (plugins.length > 0) {
|
|
22
|
+
config.plugin = plugins;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const references = {};
|
|
26
|
+
if (selections.standards?.length) {
|
|
27
|
+
references.standards = {
|
|
28
|
+
path: 'references/standards',
|
|
29
|
+
description: 'Canonical engineering standards referenced by skills.',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (selections.templates?.length) {
|
|
33
|
+
references.templates = {
|
|
34
|
+
path: 'references/templates',
|
|
35
|
+
description: 'Ready-to-use workflow documents.',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (Object.keys(references).length > 0) {
|
|
39
|
+
config.references = references;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Object.keys(mcpEntries).length > 0) {
|
|
43
|
+
config.mcp = mcpEntries;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return JSON.stringify(config, null, 4);
|
|
47
|
+
}
|