@mohammadhprp/system-prompt 0.13.0 → 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/framework/skills/README.md +1 -0
- package/framework/skills/grilling/SKILL.md +34 -0
- package/framework/skills/grilling/examples.md +47 -0
- package/package.json +1 -1
- package/src/catalog.js +13 -0
- 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/config/tui.js +92 -0
- package/src/doctor.js +23 -24
- 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 +31 -0
- package/src/paths.js +59 -0
- package/src/ui.js +32 -0
- package/src/agent-configs.js +0 -100
- package/src/cli.js +0 -334
- package/src/installer.js +0 -672
package/bin/system-prompt.js
CHANGED
|
@@ -21,6 +21,7 @@ This catalog is framework-agnostic: each skill defines when to activate, a step-
|
|
|
21
21
|
| [effective-html](./effective-html/SKILL.md) | Create self-contained HTML artifacts with routed guidance for design, wireframes, prototypes, plans, and diagrams. | Standalone HTML reports, explainers, interfaces, wireframes, prototypes, plans, and diagrams. |
|
|
22
22
|
| [find-skills](./find-skills/SKILL.md) | Discover, evaluate, and install skills from the open agent skills ecosystem. | Finding an installable skill for a specialized task or extending an agent's capabilities. |
|
|
23
23
|
| [frontend-design](./frontend-design/SKILL.md) | Distinctive, intentional visual design for new UI or reshaping existing UI — aesthetic direction, typography, and choices that don't read as templated defaults. | Building new interfaces, reshaping existing UI, or escaping generic AI-generated design looks. |
|
|
24
|
+
| [grilling](./grilling/SKILL.md) | Grill plans and decisions relentlessly through design-tree rounds until shared understanding is reached. | Stress-testing ideas, surfacing hidden assumptions, and scoping plans before implementation. |
|
|
24
25
|
| [glab](./glab/SKILL.md) | Use the GitLab CLI (glab) to manage merge requests, issues, pipelines, and repositories from the command line. | Any project hosted on GitLab (SaaS or self-hosted). |
|
|
25
26
|
| [humanizer](./humanizer/SKILL.md) | Remove signs of AI-generated writing from text — inflated importance, promotional language, em dash overuse, rule of three, AI vocabulary, and filler phrases. | Editing or reviewing prose to make it sound more natural and human-written. |
|
|
26
27
|
| [improve](./improve/SKILL.md) | Audit repositories as a read-only senior advisor and produce prioritized implementation plans for another agent. | Repository-wide audits, improvement roadmaps, and implementation handoffs. |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grilling
|
|
3
|
+
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# grilling
|
|
7
|
+
|
|
8
|
+
Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
|
|
9
|
+
|
|
10
|
+
Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
|
|
11
|
+
|
|
12
|
+
Format a round like so:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
|
16
|
+
|
|
17
|
+
➡️ <your recommended answer>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
❓ **Q2** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
|
22
|
+
|
|
23
|
+
➡️ <your recommended answer>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
|
|
27
|
+
|
|
28
|
+
Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait.
|
|
29
|
+
|
|
30
|
+
The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.
|
|
31
|
+
|
|
32
|
+
## Source
|
|
33
|
+
|
|
34
|
+
Adapted from [skills](https://github.com/mattpocock/skills/blob/main/skills/productivity/grilling/SKILL.md) by Matt Pocock, MIT License.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Grilling Skill — Example Outputs
|
|
2
|
+
|
|
3
|
+
Realistic examples of how the `grilling` skill stress-tests thinking through design-tree rounds.
|
|
4
|
+
|
|
5
|
+
## Example 1: New feature plan (frontier round, recommendations, wait)
|
|
6
|
+
|
|
7
|
+
**User:** "Grill me on my plan to add Redis caching to the API."
|
|
8
|
+
|
|
9
|
+
**What the agent does:**
|
|
10
|
+
|
|
11
|
+
Maps the design tree (invalidation strategy branches off what is cached; TTL branches off invalidation; cache key shape branches off endpoints covered), computes the frontier, and asks round 1:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
❓ **Q1** - **What gets cached**: Which endpoints or queries go through Redis — everything, read-heavy endpoints only, or specific hot queries?
|
|
15
|
+
|
|
16
|
+
➡️ Read-heavy endpoints only. Caching everything multiplies invalidation bugs for little gain.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
❓ **Q2** - **Where Redis lives**: Managed service (Upstash/ElastiCache), self-hosted container, or reuse existing infrastructure?
|
|
21
|
+
|
|
22
|
+
➡️ Managed service. One less stateful thing to operate for a first caching pass.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then waits. Q3 (TTL values) stays out of this round — it depends on Q1's answer.
|
|
26
|
+
|
|
27
|
+
**Result:** Whole frontier asked at once, each with a recommendation, no guessing at unsettled answers.
|
|
28
|
+
|
|
29
|
+
## Example 2: Facts via sub-agent, decisions via user (don't block)
|
|
30
|
+
|
|
31
|
+
**User:** Answers Q1 with "specific hot queries" and Q2 with "self-hosted container."
|
|
32
|
+
|
|
33
|
+
**What the agent does:**
|
|
34
|
+
|
|
35
|
+
Settled decisions push the frontier outward. Q3 (TTL per query) is now askable. But "which queries are actually hot" is a fact, not a decision — dispatches a sub-agent to check access logs and slow-query stats instead of asking the user. Asks the rest of the frontier now (eviction policy, invalidation trigger); only the TTL question waits for the sub-agent's report.
|
|
36
|
+
|
|
37
|
+
**Result:** Facts looked up, decisions put to the user, no blocking on exploration.
|
|
38
|
+
|
|
39
|
+
## Example 3: Session close (frontier empty, confirm before acting)
|
|
40
|
+
|
|
41
|
+
**User:** Answers the final round on invalidation triggers.
|
|
42
|
+
|
|
43
|
+
**What the agent does:**
|
|
44
|
+
|
|
45
|
+
Frontier is empty — every branch visited (scope, hosting, TTLs, eviction, invalidation). States the shared understanding back in one summary and asks for confirmation. Does not write any code or config until the user confirms.
|
|
46
|
+
|
|
47
|
+
**Result:** Nothing silently assumed, no action before confirmed understanding.
|
package/package.json
CHANGED
package/src/catalog.js
CHANGED
|
@@ -3,6 +3,7 @@ export const categories = {
|
|
|
3
3
|
title: 'Skills',
|
|
4
4
|
description: 'Task-specific procedures for AI coding agents',
|
|
5
5
|
sourceDir: 'framework/skills',
|
|
6
|
+
itemLayout: 'directory',
|
|
6
7
|
items: [
|
|
7
8
|
{ id: 'adhd', name: 'ADHD', description: 'Shape output for ADHD readers: action first, numbered steps, restated state, no tangents' },
|
|
8
9
|
{ id: 'agent-browser', name: 'Agent Browser', description: 'Automate browser and Electron workflows for navigation, testing, screenshots, and data extraction' },
|
|
@@ -17,6 +18,7 @@ export const categories = {
|
|
|
17
18
|
{ id: 'effective-html', name: 'Effective HTML', description: 'Create self-contained HTML artifacts with routed guidance for design, wireframes, prototypes, plans, and diagrams' },
|
|
18
19
|
{ id: 'find-skills', name: 'Find Skills', description: 'Discover, evaluate, and install agent skills for specialized tasks' },
|
|
19
20
|
{ id: 'frontend-design', name: 'Frontend Design', description: 'Distinctive, intentional visual design for new UI or reshaping existing UI' },
|
|
21
|
+
{ id: 'grilling', name: 'Grilling', description: 'Grill plans and decisions relentlessly through design-tree rounds until shared understanding is reached' },
|
|
20
22
|
{ id: 'gh', name: 'GitHub CLI', description: 'Work with GitHub via the gh CLI for repositories, issues, pull requests, Actions, releases, and APIs' },
|
|
21
23
|
{ id: 'glab', name: 'Glab', description: 'Work with GitLab via the glab CLI for MRs, issues, and pipelines' },
|
|
22
24
|
{ id: 'humanizer', name: 'Humanizer', description: 'Remove signs of AI-generated writing to make text sound more natural and human' },
|
|
@@ -46,6 +48,7 @@ export const categories = {
|
|
|
46
48
|
title: 'Subagents',
|
|
47
49
|
description: 'Specialized subagents for security, architecture, review, and research',
|
|
48
50
|
sourceDir: 'framework/agents',
|
|
51
|
+
itemLayout: 'file',
|
|
49
52
|
items: [
|
|
50
53
|
{ id: 'researcher', name: 'Researcher', description: 'Fetch and analyze web content from URLs' },
|
|
51
54
|
{ id: 'reviewer', name: 'Reviewer', description: 'Review code for correctness and best practices' },
|
|
@@ -57,6 +60,7 @@ export const categories = {
|
|
|
57
60
|
title: 'Slash Commands',
|
|
58
61
|
description: 'Slash command workflows for repeatable tasks',
|
|
59
62
|
sourceDir: 'framework/commands',
|
|
63
|
+
itemLayout: 'file',
|
|
60
64
|
items: [
|
|
61
65
|
{ id: 'audit-your-codebase', name: 'Audit Your Codebase', description: 'Audit for materially useful simplifications in structure, state, algorithms, and ownership' },
|
|
62
66
|
{ id: 'explain-codebase', name: 'Explain Codebase', description: 'Map a codebase and teach it interactively, from overview to focused deep-dives' },
|
|
@@ -69,6 +73,8 @@ export const categories = {
|
|
|
69
73
|
title: 'MCPs',
|
|
70
74
|
description: 'Model Context Protocol servers for AI coding agents',
|
|
71
75
|
sourceDir: 'framework/mcps',
|
|
76
|
+
itemLayout: 'directory',
|
|
77
|
+
copyItems: false,
|
|
72
78
|
items: [
|
|
73
79
|
{ id: 'excalidraw', name: 'Excalidraw MCP', description: 'Stream hand-drawn diagrams with interactive editing' },
|
|
74
80
|
{ id: 'figma-mcp-go', name: 'Figma MCP Go', description: 'Read/write access to Figma designs via plugin bridge' },
|
|
@@ -82,6 +88,8 @@ export const categories = {
|
|
|
82
88
|
title: 'Plugins',
|
|
83
89
|
description: 'OpenCode plugins that extend the core agent',
|
|
84
90
|
sourceDir: 'framework/plugins',
|
|
91
|
+
itemLayout: 'directory',
|
|
92
|
+
copyItems: false,
|
|
85
93
|
items: [
|
|
86
94
|
{ id: 'opencode-goal-plugin', name: 'OpenCode Goal Plugin', description: 'Goal-driven long-running tasks with persistence' },
|
|
87
95
|
{ id: 'ponytail', name: 'Ponytail', description: 'Lazy senior dev mode — YAGNI-first, reuse-first ladder that reduces code bloat and cost' },
|
|
@@ -92,6 +100,7 @@ export const categories = {
|
|
|
92
100
|
title: 'Styles',
|
|
93
101
|
description: 'Design system references from Refero Styles',
|
|
94
102
|
sourceDir: 'framework/styles',
|
|
103
|
+
itemLayout: 'directory',
|
|
95
104
|
items: [
|
|
96
105
|
{ id: 'factory', name: 'Factory', description: 'Terminal war room design system' },
|
|
97
106
|
{ id: 'huly', name: 'Huly', description: 'Midnight observatory design system' },
|
|
@@ -105,6 +114,7 @@ export const categories = {
|
|
|
105
114
|
title: 'Modes',
|
|
106
115
|
description: 'Behavior, tool, and prompt presets for different use cases',
|
|
107
116
|
sourceDir: 'framework/modes',
|
|
117
|
+
itemLayout: 'file',
|
|
108
118
|
items: [
|
|
109
119
|
{ id: 'audit', name: 'Audit', description: 'Read-only high-scrutiny review mode for evaluating artifacts' },
|
|
110
120
|
],
|
|
@@ -114,6 +124,7 @@ export const categories = {
|
|
|
114
124
|
title: 'Memory',
|
|
115
125
|
description: 'Persistent agent memory files for cross-session context',
|
|
116
126
|
sourceDir: 'framework/memory',
|
|
127
|
+
itemLayout: 'file',
|
|
117
128
|
items: [
|
|
118
129
|
{ id: 'codebase-insights', name: 'Codebase Insights', description: 'Non-obvious facts, gotchas, past decisions, and architecture quirks' },
|
|
119
130
|
{ id: 'user-preferences', name: 'User Preferences', description: 'Coding style, naming conventions, and architectural preferences' },
|
|
@@ -124,6 +135,7 @@ export const categories = {
|
|
|
124
135
|
title: 'Standards',
|
|
125
136
|
description: 'Canonical engineering standards',
|
|
126
137
|
sourceDir: 'framework/references/standards',
|
|
138
|
+
itemLayout: 'file',
|
|
127
139
|
items: [
|
|
128
140
|
{ id: 'api', name: 'API Design', description: 'REST API design standards' },
|
|
129
141
|
{ id: 'architecture', name: 'Architecture', description: 'System architecture standards' },
|
|
@@ -144,6 +156,7 @@ export const categories = {
|
|
|
144
156
|
title: 'Templates',
|
|
145
157
|
description: 'Fillable workflow documents',
|
|
146
158
|
sourceDir: 'framework/references/templates',
|
|
159
|
+
itemLayout: 'file',
|
|
147
160
|
items: [
|
|
148
161
|
{ id: 'adr', name: 'ADR', description: 'Architecture Decision Record' },
|
|
149
162
|
{ id: 'api-spec', name: 'API Spec', description: 'API specification document' },
|
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
|
+
}
|