set-prompt 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +13 -20
- package/dist/index.js +516 -135
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.8.0] - 2026-05-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Hermes integration** (`link hermes`) — generates a Hermes plugin at `~/.hermes/plugins/set-prompt/` that loads skills, commands, and hooks directly from the registered prompt repo at Hermes startup
|
|
11
|
+
- `plugin.yaml` — Hermes plugin manifest (`name`, `version`, `description`)
|
|
12
|
+
- `__init__.py` — Python adapter generated at link time. The repo's absolute path is baked into a `REPO_DIR` constant (JSON-encoded for Windows-safe backslash handling), so no symlinks are required. At Hermes startup, `register(ctx)` walks `<repo>/skills/<name>/SKILL.md` for `ctx.register_skill()`, `<repo>/commands/*.md` for `ctx.register_command()` (handler injects the markdown body via `ctx.inject_message()`), and `<repo>/hooks/hooks.json` for `ctx.register_hook()`.
|
|
13
|
+
- Shared `hooks.json` strategy — the same `hooks/hooks.json` used by Claude Code/Cursor is reused. Only Hermes-native event keys (`pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`, `on_session_end`, `on_session_finalize`, `on_session_reset`, `subagent_stop`, `pre_gateway_dispatch`) are picked up; other tools' keys are ignored, so a single file can drive multiple platforms.
|
|
14
|
+
- Hook command substitution: `${SET_PROMPT_REPO}` token + `SET_PROMPT_REPO` env var. Hermes hook callbacks receive a JSON payload on stdin: `{"event": "<event>", "args": [...], "kwargs": {...}}`.
|
|
15
|
+
- `~/.hermes/config.yaml` activation — created with `plugins.enabled: [set-prompt]` if absent. Existing files are never auto-modified; if `set-prompt` is missing from `enabled`, set-prompt prints the snippet for manual merge.
|
|
16
|
+
- `HermesConfigSchema` + `HermesConfig` type + `configManager.hermes` getter/setter + `isHermesEnabled()`
|
|
17
|
+
- `HERMES_DIR`, `HERMES_PLUGIN_DIR`, `HERMES_CONFIG_PATH` constants in `_defs`
|
|
18
|
+
- `linkHermes()` / `unlinkHermes()` in `src/link/hermes.ts`
|
|
19
|
+
- `SET_PROMPT_GUIDE` template: Hermes commands/hooks integration sections (set-prompt's adapter behavior, event whitelist table, payload format, decision-control caveat)
|
|
20
|
+
- Tests: `tests/commands/link-hermes.test.ts` (11 cases — REPO_DIR baking, Windows backslash JSON encoding, no-symlink assertion, three config.yaml branches, unlink behavior with auto-generated vs user-authored config)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `status-command.ts` — added missing branches for Cursor / OpenCode / Gemini CLI / Hermes (previously fell through silently after Antigravity)
|
|
24
|
+
- `uninstall-command.ts` — added missing unlink calls for OpenCode / Gemini CLI in addition to Hermes
|
|
25
|
+
- `link-command.ts` description in `index.ts` — agent list extended with `hermes`
|
|
26
|
+
- Test mocks updated across `link-command`, `status-command`, `uninstall-command` to cover all current `is*Enabled()` predicates and `unlink*` exports
|
|
27
|
+
|
|
28
|
+
### Notes / Caveats
|
|
29
|
+
- **Hermes loads plugins once at startup** — `register(ctx)` is called exactly once. Add/remove a skill, command, or hook in your repo and Hermes must be restarted to pick it up. Other tools' auto-discovery is not available here.
|
|
30
|
+
- **Hermes hooks are observation-only via this bridge** — block/allow decision semantics are not propagated back to Hermes. Use platform-native hooks if you need to gate tool calls.
|
|
31
|
+
- **Hermes does not auto-modify `~/.hermes/config.yaml`** — to avoid clobbering memory-provider / context-engine selections, set-prompt only writes the file when it's absent. Existing files are read-only; the user is asked to add `- set-prompt` under `plugins.enabled` manually if missing.
|
|
32
|
+
- **`AGENT_PROMPT_DIRS[HERMES]` is empty** — Hermes intentionally does **not** symlink directories. The Python adapter reads from `REPO_DIR` directly, sidestepping Windows symlink/junction permission edge cases.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## [0.7.1] - 2026-04-20
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- README hero: replaced ASCII structure diagram with hand-drawn architecture image (`docs/imgs/architecture.png`), loaded via GitHub raw URL so it renders on both GitHub and npmjs.com
|
|
40
|
+
- Added `docs/` directory for documentation assets (`docs/imgs/` for images); excluded from npm package via `.npmignore`
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
7
44
|
## [0.7.0] - 2026-04-20
|
|
8
45
|
|
|
9
46
|
### Added
|
package/README.md
CHANGED
|
@@ -12,25 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
Your skills, commands, and agents live in git. One command syncs them into every AI coding tool you use — so the prompts you built stay with you no matter which tool you're in.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
repo/ (git)
|
|
17
|
-
├── skills/
|
|
18
|
-
├── commands/
|
|
19
|
-
├── hooks/
|
|
20
|
-
├── agents/
|
|
21
|
-
├── rules/
|
|
22
|
-
├── .mcp.json
|
|
23
|
-
├── .app.json
|
|
24
|
-
├── .claude-plugin/plugin.json
|
|
25
|
-
└── .codex-plugin/plugin.json
|
|
26
|
-
│
|
|
27
|
-
┌──────────────────┼──────────────────────┐
|
|
28
|
-
▼ ▼ ▼
|
|
29
|
-
Claude Code Codex RooCode, OpenClaw,
|
|
30
|
-
(marketplace + (marketplace + Antigravity, Cursor,
|
|
31
|
-
repo symlink) cache symlink) OpenCode, Gemini CLI
|
|
32
|
-
(dir symlinks)
|
|
33
|
-
```
|
|
15
|
+

|
|
34
16
|
|
|
35
17
|
## Quick Start
|
|
36
18
|
|
|
@@ -112,6 +94,7 @@ set-prompt link antigravity # link Antigravity only
|
|
|
112
94
|
set-prompt link cursor # link Cursor only
|
|
113
95
|
set-prompt link opencode # link OpenCode only
|
|
114
96
|
set-prompt link geminicli # link Gemini CLI only
|
|
97
|
+
set-prompt link hermes # link Hermes only
|
|
115
98
|
```
|
|
116
99
|
|
|
117
100
|
The interactive mode shows all agents with their current state. **Check to link, uncheck to unlink** — existing directories are backed up before being replaced.
|
|
@@ -126,6 +109,7 @@ The interactive mode shows all agents with their current state. **Check to link,
|
|
|
126
109
|
| Cursor | dir symlinks into `~/.cursor/` | `skills/`, `agents/`, `commands/`, `hooks/`, `mcp.json` (hardlink) |
|
|
127
110
|
| OpenCode | dir symlinks into `~/.config/opencode/` | `skills/`, `commands/`, `agents/` |
|
|
128
111
|
| Gemini CLI | dir symlinks into `~/.gemini/` | `skills/`, `commands/`, `agents/` |
|
|
112
|
+
| Hermes | Python plugin adapter at `~/.hermes/plugins/set-prompt/` | `skills/`, `commands/`, `hooks/hooks.json` (read directly from repo at startup) |
|
|
129
113
|
|
|
130
114
|
> **Note on Claude Code**: Operates as a plugin. Restart Claude Code after linking for the plugin to be recognized.
|
|
131
115
|
|
|
@@ -139,6 +123,8 @@ The interactive mode shows all agents with their current state. **Check to link,
|
|
|
139
123
|
|
|
140
124
|
> **Note on Gemini CLI**: Skills follow the standard `skills/<name>/SKILL.md` pattern. Commands use `.toml` format (not `.md`) and agents use `.md` with YAML frontmatter. Files in your repo's `commands/` must be TOML for Gemini CLI to recognize them. **Agents have strict frontmatter validation** — unknown keys (e.g. `allowed-tools`, `color`, `mode` from other platforms) cause Gemini CLI to reject the agent. See `SET_PROMPT_GUIDE.md` for the allowed keys.
|
|
141
125
|
|
|
126
|
+
> **Note on Hermes**: Hermes plugins must register skills/commands/hooks programmatically — directory drop-ins are not auto-discovered. set-prompt generates a small Python adapter (`~/.hermes/plugins/set-prompt/__init__.py`) with the repo's absolute path baked in, so no symlinks are needed. **Restart Hermes after `link` (or after adding/removing a skill in your repo)** — `register()` runs only once at Hermes startup. Activation requires `set-prompt` listed under `plugins.enabled` in `~/.hermes/config.yaml` — set-prompt creates this file if absent but never modifies an existing one (it prints the snippet to add manually). Hooks are observation-only on the Hermes side: the same `hooks/hooks.json` is reused, but Hermes events (`pre_tool_call`, `on_session_start`, etc.) are picked up while Claude/Cursor keys are ignored. Hook scripts can reference `${SET_PROMPT_REPO}`.
|
|
127
|
+
|
|
142
128
|
---
|
|
143
129
|
|
|
144
130
|
### Step 3 — Keep in sync
|
|
@@ -161,7 +147,7 @@ set-prompt repo path # print repo location (e.g. cd "$(sppt rep
|
|
|
161
147
|
set-prompt repo open # open repo in OS file manager (--code VSCode, --stree Sourcetree)
|
|
162
148
|
```
|
|
163
149
|
|
|
164
|
-
Symlink-based agents (Claude Code, Codex, RooCode, OpenClaw, Antigravity, OpenCode, Gemini CLI) reflect changes immediately after pull. Cursor's `mcp.json` is a hardlink to repo's `.mcp.json`, so edits to either side are reflected automatically.
|
|
150
|
+
Symlink-based agents (Claude Code, Codex, RooCode, OpenClaw, Antigravity, OpenCode, Gemini CLI) reflect changes immediately after pull. Cursor's `mcp.json` is a hardlink to repo's `.mcp.json`, so edits to either side are reflected automatically. **Hermes** is the exception — its Python adapter only re-scans the repo at process startup, so restart Hermes after pulling new content.
|
|
165
151
|
|
|
166
152
|
---
|
|
167
153
|
|
|
@@ -249,6 +235,12 @@ set-prompt uninstall
|
|
|
249
235
|
├── commands/ → repo/commands (Gemini reads *.toml)
|
|
250
236
|
├── agents/ → repo/agents (Gemini reads *.md)
|
|
251
237
|
└── antigravity/ (Antigravity's own subtree, unrelated)
|
|
238
|
+
|
|
239
|
+
~/.hermes/ # Hermes (Python plugin adapter, no symlinks)
|
|
240
|
+
├── config.yaml # plugins.enabled: [set-prompt] (created if absent)
|
|
241
|
+
└── plugins/set-prompt/
|
|
242
|
+
├── plugin.yaml # Hermes manifest
|
|
243
|
+
└── __init__.py # REPO_DIR baked in; reads <repo>/{skills,commands,hooks/hooks.json} at startup
|
|
252
244
|
```
|
|
253
245
|
|
|
254
246
|
## Warning
|
|
@@ -263,6 +255,7 @@ set-prompt uninstall
|
|
|
263
255
|
- **Cursor** — replaces directories and `mcp.json` in `~/.cursor/`
|
|
264
256
|
- **OpenCode** — replaces directories in `~/.config/opencode/`
|
|
265
257
|
- **Gemini CLI** — replaces directories in `~/.gemini/` (`antigravity/` subtree untouched)
|
|
258
|
+
- **Hermes** — generates `~/.hermes/plugins/set-prompt/{plugin.yaml, __init__.py}` and writes `~/.hermes/config.yaml` only when absent (existing files are never auto-modified)
|
|
266
259
|
|
|
267
260
|
Before making any changes, `set-prompt` creates a backup and rolls back automatically on failure. However, you should be aware that:
|
|
268
261
|
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk24 from "chalk";
|
|
6
6
|
import figlet from "figlet";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import fs15 from "fs";
|
|
8
|
+
import path14 from "path";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
|
|
11
11
|
// src/commands/install-command.ts
|
|
@@ -38,6 +38,9 @@ var OPENCODE_DIR = path.join(os.homedir(), ".config", "opencode");
|
|
|
38
38
|
var OPENCODE_BACKUP_DIR = path.join(OPENCODE_DIR, "SET_PROMPT_BACKUP");
|
|
39
39
|
var GEMINICLI_DIR = path.join(os.homedir(), ".gemini");
|
|
40
40
|
var GEMINICLI_BACKUP_DIR = path.join(GEMINICLI_DIR, "SET_PROMPT_BACKUP");
|
|
41
|
+
var HERMES_DIR = path.join(os.homedir(), ".hermes");
|
|
42
|
+
var HERMES_PLUGIN_DIR = path.join(HERMES_DIR, "plugins", MARKET_NAME);
|
|
43
|
+
var HERMES_CONFIG_PATH = path.join(HERMES_DIR, "config.yaml");
|
|
41
44
|
var PROMPT_DIR_NAMES = ["skills", "commands", "hooks", "agents", "rules"];
|
|
42
45
|
var AGENT_PROMPT_DIRS = {
|
|
43
46
|
["claudecode" /* CLAUDECODE */]: ["skills", "commands", "hooks", "agents"],
|
|
@@ -47,7 +50,8 @@ var AGENT_PROMPT_DIRS = {
|
|
|
47
50
|
["antigravity" /* ANTIGRAVITY */]: ["skills"],
|
|
48
51
|
["cursor" /* CURSOR */]: ["skills", "agents", "commands", "hooks"],
|
|
49
52
|
["opencode" /* OPENCODE */]: ["skills", "commands", "agents"],
|
|
50
|
-
["geminicli" /* GEMINICLI */]: ["skills", "commands", "agents"]
|
|
53
|
+
["geminicli" /* GEMINICLI */]: ["skills", "commands", "agents"],
|
|
54
|
+
["hermes" /* HERMES */]: []
|
|
51
55
|
};
|
|
52
56
|
var ALL_AGENTS = [
|
|
53
57
|
{ name: "Claude Code", value: "claudecode" /* CLAUDECODE */ },
|
|
@@ -57,7 +61,8 @@ var ALL_AGENTS = [
|
|
|
57
61
|
{ name: "Antigravity", value: "antigravity" /* ANTIGRAVITY */ },
|
|
58
62
|
{ name: "Cursor", value: "cursor" /* CURSOR */ },
|
|
59
63
|
{ name: "OpenCode", value: "opencode" /* OPENCODE */ },
|
|
60
|
-
{ name: "Gemini CLI", value: "geminicli" /* GEMINICLI */ }
|
|
64
|
+
{ name: "Gemini CLI", value: "geminicli" /* GEMINICLI */ },
|
|
65
|
+
{ name: "Hermes", value: "hermes" /* HERMES */ }
|
|
61
66
|
];
|
|
62
67
|
|
|
63
68
|
// src/_libs/config.ts
|
|
@@ -99,6 +104,9 @@ var GeminicliConfigSchema = z.object({
|
|
|
99
104
|
path: z.string().nullable(),
|
|
100
105
|
backup_path: z.string().nullish().optional()
|
|
101
106
|
});
|
|
107
|
+
var HermesConfigSchema = z.object({
|
|
108
|
+
path: z.string().nullable()
|
|
109
|
+
});
|
|
102
110
|
var GlobalConfigSchema = z.object({
|
|
103
111
|
repo_path: z.string(),
|
|
104
112
|
remote_url: z.string().nullable(),
|
|
@@ -109,7 +117,8 @@ var GlobalConfigSchema = z.object({
|
|
|
109
117
|
antigravity: AntigravityConfigSchema.nullish().optional(),
|
|
110
118
|
cursor: CursorConfigSchema.nullish().optional(),
|
|
111
119
|
opencode: OpencodeConfigSchema.nullish().optional(),
|
|
112
|
-
geminicli: GeminicliConfigSchema.nullish().optional()
|
|
120
|
+
geminicli: GeminicliConfigSchema.nullish().optional(),
|
|
121
|
+
hermes: HermesConfigSchema.nullish().optional()
|
|
113
122
|
});
|
|
114
123
|
|
|
115
124
|
// src/_libs/config.ts
|
|
@@ -125,6 +134,7 @@ var ConfigManager = class {
|
|
|
125
134
|
this._cursor = null;
|
|
126
135
|
this._opencode = null;
|
|
127
136
|
this._geminicli = null;
|
|
137
|
+
this._hermes = null;
|
|
128
138
|
}
|
|
129
139
|
get repo_path() {
|
|
130
140
|
return this._repo_path;
|
|
@@ -156,6 +166,9 @@ var ConfigManager = class {
|
|
|
156
166
|
get geminicli() {
|
|
157
167
|
return this._geminicli;
|
|
158
168
|
}
|
|
169
|
+
get hermes() {
|
|
170
|
+
return this._hermes;
|
|
171
|
+
}
|
|
159
172
|
set repo_path(v) {
|
|
160
173
|
this._repo_path = v;
|
|
161
174
|
}
|
|
@@ -186,6 +199,9 @@ var ConfigManager = class {
|
|
|
186
199
|
set geminicli(v) {
|
|
187
200
|
this._geminicli = v;
|
|
188
201
|
}
|
|
202
|
+
set hermes(v) {
|
|
203
|
+
this._hermes = v;
|
|
204
|
+
}
|
|
189
205
|
init() {
|
|
190
206
|
this._loadFromDisk();
|
|
191
207
|
if (this._repo_path != null) {
|
|
@@ -209,7 +225,8 @@ var ConfigManager = class {
|
|
|
209
225
|
antigravity: this._antigravity,
|
|
210
226
|
cursor: this._cursor,
|
|
211
227
|
opencode: this._opencode,
|
|
212
|
-
geminicli: this._geminicli
|
|
228
|
+
geminicli: this._geminicli,
|
|
229
|
+
hermes: this._hermes
|
|
213
230
|
}, null, 4);
|
|
214
231
|
fs.writeFileSync(CONFIG_PATH, configStr, "utf-8");
|
|
215
232
|
console.log(chalk.green(`Config saved`) + chalk.dim(` \u2192 ${CONFIG_PATH}`));
|
|
@@ -252,6 +269,9 @@ var ConfigManager = class {
|
|
|
252
269
|
isGeminicliEnabled() {
|
|
253
270
|
return this._geminicli != null;
|
|
254
271
|
}
|
|
272
|
+
isHermesEnabled() {
|
|
273
|
+
return this._hermes != null;
|
|
274
|
+
}
|
|
255
275
|
_assign(config) {
|
|
256
276
|
this._repo_path = config.repo_path;
|
|
257
277
|
this._remote_url = config.remote_url;
|
|
@@ -263,6 +283,7 @@ var ConfigManager = class {
|
|
|
263
283
|
this._cursor = config.cursor ?? null;
|
|
264
284
|
this._opencode = config.opencode ?? null;
|
|
265
285
|
this._geminicli = config.geminicli ?? null;
|
|
286
|
+
this._hermes = config.hermes ?? null;
|
|
266
287
|
}
|
|
267
288
|
_loadFromDisk() {
|
|
268
289
|
if (fs.existsSync(CONFIG_PATH) === false) {
|
|
@@ -490,11 +511,46 @@ metadata:
|
|
|
490
511
|
# Gemini CLI
|
|
491
512
|
name: my-skill
|
|
492
513
|
description: "What this skill does and when Gemini should use it"
|
|
514
|
+
|
|
515
|
+
# Hermes
|
|
516
|
+
name: my-skill
|
|
517
|
+
description: "What this skill does and when to use it"
|
|
518
|
+
version: "1.0.0"
|
|
519
|
+
platforms:
|
|
520
|
+
- macos
|
|
521
|
+
- linux
|
|
522
|
+
- windows
|
|
523
|
+
metadata:
|
|
524
|
+
hermes:
|
|
525
|
+
tags:
|
|
526
|
+
- python
|
|
527
|
+
- automation
|
|
528
|
+
category: devops
|
|
529
|
+
requires_toolsets:
|
|
530
|
+
- terminal
|
|
531
|
+
fallback_for_toolsets:
|
|
532
|
+
- browser
|
|
533
|
+
requires_tools:
|
|
534
|
+
- rg
|
|
535
|
+
fallback_for_tools:
|
|
536
|
+
- grep
|
|
537
|
+
config:
|
|
538
|
+
- key: my.setting
|
|
539
|
+
description: "What this controls"
|
|
540
|
+
default: "value"
|
|
541
|
+
prompt: "Prompt for setup"
|
|
542
|
+
required_environment_variables:
|
|
543
|
+
- name: TENOR_API_KEY
|
|
544
|
+
prompt: "Tenor API key"
|
|
545
|
+
help: "Get a key from https://developers.google.com/tenor"
|
|
546
|
+
required_for: "full functionality"
|
|
493
547
|
---
|
|
494
548
|
\`\`\`
|
|
495
549
|
|
|
496
550
|
> **Gemini CLI note**: Only \`name\` and \`description\` are recognized. \`name\` must be lowercase with hyphens and match the directory name.
|
|
497
551
|
|
|
552
|
+
> **Hermes note (set-prompt integration)**: Hermes does not auto-discover files in standard directories \u2014 plugins must register skills programmatically. set-prompt generates \`~/.hermes/plugins/set-prompt/{plugin.yaml, __init__.py}\` on \`set-prompt link hermes\`. The \`__init__.py\` reads \`<repo>/skills/<skill-name>/SKILL.md\` directly (REPO_DIR is baked in at link time) and calls \`ctx.register_skill()\` at Hermes startup. The skill directory layout is the same as other platforms \u2014 no nested category folder.
|
|
553
|
+
|
|
498
554
|
| Field | Required | Platform | Description |
|
|
499
555
|
|-------|----------|----------|-------------|
|
|
500
556
|
| \`name\` | Yes | All | Display name. Claude Code / OpenCode / RooCode: lowercase, numbers, hyphens only (no underscores; Claude Code / OpenCode limit to 64 chars). Gemini CLI: lowercase with hyphens, must match the directory name. Antigravity: optional, defaults to folder name. |
|
|
@@ -519,7 +575,17 @@ description: "What this skill does and when Gemini should use it"
|
|
|
519
575
|
| \`command-arg-mode\` | No | OpenClaw | How arguments are forwarded to the tool. (default: \`"raw"\`) |
|
|
520
576
|
| \`license\` | No | Cursor, OpenCode | License name or reference to a bundled license file. |
|
|
521
577
|
| \`compatibility\` | No | Cursor, OpenCode | Environment requirements (system packages, network access, etc.) |
|
|
522
|
-
| \`metadata\` | No | Cursor, OpenCode | Arbitrary key-value mapping for additional metadata. OpenCode requires string-to-string values only. |
|
|
578
|
+
| \`metadata\` | No | Cursor, OpenCode, Hermes | Arbitrary key-value mapping for additional metadata. OpenCode requires string-to-string values only. Hermes uses the \`metadata.hermes.*\` namespace for platform-specific fields. |
|
|
579
|
+
| \`version\` | No | Hermes | Version number (e.g. \`"1.0.0"\`) |
|
|
580
|
+
| \`platforms\` | No | Hermes | OS allow-list. Values: \`macos\`, \`linux\`, \`windows\`. (default: all) |
|
|
581
|
+
| \`metadata.hermes.tags\` | No | Hermes | Categorization keywords array. |
|
|
582
|
+
| \`metadata.hermes.category\` | No | Hermes | Skill category \u2014 should match the parent directory name. |
|
|
583
|
+
| \`metadata.hermes.requires_toolsets\` | No | Hermes | Skill is hidden unless these toolsets are available. |
|
|
584
|
+
| \`metadata.hermes.fallback_for_toolsets\` | No | Hermes | Skill is hidden when these toolsets are already available (acts as a fallback). |
|
|
585
|
+
| \`metadata.hermes.requires_tools\` | No | Hermes | Skill is hidden unless these specific tools are available. |
|
|
586
|
+
| \`metadata.hermes.fallback_for_tools\` | No | Hermes | Skill is hidden when these specific tools are already available. |
|
|
587
|
+
| \`metadata.hermes.config\` | No | Hermes | Array of configuration declarations. Each entry: \`key\`, \`description\`, \`default\`, \`prompt\`. |
|
|
588
|
+
| \`required_environment_variables\` | No | Hermes | Array of required env vars. Each entry: \`name\`, \`prompt\`, \`help\`, \`required_for\`. |
|
|
523
589
|
|
|
524
590
|
---
|
|
525
591
|
|
|
@@ -591,6 +657,10 @@ Include: 1) Refactored code. 2) Explanation of changes.
|
|
|
591
657
|
|
|
592
658
|
**Namespacing**: subdirectories create namespaced commands \u2014 \`commands/git/commit.toml\` becomes \`/git:commit\`. After edits, run \`/commands reload\` in Gemini CLI.
|
|
593
659
|
|
|
660
|
+
#### Hermes commands (set-prompt integration)
|
|
661
|
+
|
|
662
|
+
Hermes commands are registered programmatically. set-prompt's generated \`__init__.py\` walks \`<repo>/commands/*.md\`, parses each file's YAML frontmatter for \`name\` / \`description\`, and calls \`ctx.register_command(name, handler, description)\`. The handler injects the markdown body as a user message via \`ctx.inject_message(body, role="user")\` when the user invokes the slash command. Hermes does **not** read \`allowed-tools\`, \`model\`, \`agent\`, or other platform-specific frontmatter \u2014 only \`name\` and \`description\` are honored.
|
|
663
|
+
|
|
594
664
|
| Field | Required | Platform | Description |
|
|
595
665
|
|-------|----------|----------|-------------|
|
|
596
666
|
| \`name\` | No | All | Display name \u2014 lowercase, numbers, hyphens only (max 64 chars). Defaults to directory name. |
|
|
@@ -877,6 +947,55 @@ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"
|
|
|
877
947
|
echo '{"permission":"deny","user_message":"Blocked by policy","agent_message":"Not allowed"}'
|
|
878
948
|
\`\`\`
|
|
879
949
|
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
#### Hermes Hooks (set-prompt integration)
|
|
953
|
+
|
|
954
|
+
Hermes uses Python callbacks rather than declarative JSON \u2014 set-prompt bridges the gap by reading the **same** \`hooks/hooks.json\` used by Claude Code/Cursor, but only picks up entries whose top-level key is a Hermes event name. Other tools' keys (\`UserPromptSubmit\`, \`afterFileEdit\`, etc.) are ignored, so the file can hold hooks for multiple platforms side-by-side.
|
|
955
|
+
|
|
956
|
+
\`\`\`json
|
|
957
|
+
{
|
|
958
|
+
"hooks": {
|
|
959
|
+
"UserPromptSubmit": [
|
|
960
|
+
{ "hooks": [{ "type": "command", "command": "...claude-only..." }] }
|
|
961
|
+
],
|
|
962
|
+
"pre_tool_call": [
|
|
963
|
+
{
|
|
964
|
+
"hooks": [
|
|
965
|
+
{
|
|
966
|
+
"type": "command",
|
|
967
|
+
"command": "node \\"\${SET_PROMPT_REPO}/hooks/audit.mjs\\"",
|
|
968
|
+
"timeout": 5
|
|
969
|
+
}
|
|
970
|
+
]
|
|
971
|
+
}
|
|
972
|
+
]
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
\`\`\`
|
|
976
|
+
|
|
977
|
+
**Hermes events** (whitelist applied by the generated \`__init__.py\`)
|
|
978
|
+
|
|
979
|
+
| Event | When it fires |
|
|
980
|
+
|-------|---------------|
|
|
981
|
+
| \`pre_tool_call\` / \`post_tool_call\` | Before / after a tool runs |
|
|
982
|
+
| \`pre_llm_call\` / \`post_llm_call\` | Before / after an LLM call |
|
|
983
|
+
| \`on_session_start\` / \`on_session_end\` / \`on_session_finalize\` / \`on_session_reset\` | Session lifecycle |
|
|
984
|
+
| \`subagent_stop\` | Subagent completes |
|
|
985
|
+
| \`pre_gateway_dispatch\` | Before a gateway dispatch |
|
|
986
|
+
|
|
987
|
+
**Handler fields** (only \`type: "command"\` is supported)
|
|
988
|
+
|
|
989
|
+
| Field | Description |
|
|
990
|
+
|-------|-------------|
|
|
991
|
+
| \`type\` | Must be \`"command"\`. Other types are ignored. |
|
|
992
|
+
| \`command\` | Shell command. \`\${SET_PROMPT_REPO}\` is substituted with the repo path; the \`SET_PROMPT_REPO\` env var is also set. |
|
|
993
|
+
| \`timeout\` | Seconds before the subprocess is killed. Optional. |
|
|
994
|
+
|
|
995
|
+
**Payload**: when the hook fires, set-prompt's runner pipes a JSON object on stdin to the command \u2014 \`{"event": "<event_name>", "args": [...], "kwargs": {...}}\`. \`args\`/\`kwargs\` mirror whatever Hermes passed to the callback (stringified via \`repr()\` for safety).
|
|
996
|
+
|
|
997
|
+
**Decision control**: Hermes does not currently expose block/allow semantics through this bridge \u2014 the subprocess return value is not propagated back. Hooks are observation-only on the Hermes side.
|
|
998
|
+
|
|
880
999
|
`;
|
|
881
1000
|
|
|
882
1001
|
// src/commands/scaffold-command.ts
|
|
@@ -1098,7 +1217,7 @@ var installCommand = async (target) => {
|
|
|
1098
1217
|
};
|
|
1099
1218
|
|
|
1100
1219
|
// src/commands/link-command.ts
|
|
1101
|
-
import
|
|
1220
|
+
import chalk15 from "chalk";
|
|
1102
1221
|
import { checkbox } from "@inquirer/prompts";
|
|
1103
1222
|
|
|
1104
1223
|
// src/link/claudecode.ts
|
|
@@ -2366,6 +2485,232 @@ Removing Gemini CLI integration...`));
|
|
|
2366
2485
|
configManager.save();
|
|
2367
2486
|
};
|
|
2368
2487
|
|
|
2488
|
+
// src/link/hermes.ts
|
|
2489
|
+
import path12 from "path";
|
|
2490
|
+
import fs13 from "fs";
|
|
2491
|
+
import chalk14 from "chalk";
|
|
2492
|
+
import { confirm as confirm11 } from "@inquirer/prompts";
|
|
2493
|
+
var PLUGIN_YAML = `name: ${MARKET_NAME}
|
|
2494
|
+
version: "1.0"
|
|
2495
|
+
description: Managed by set-prompt
|
|
2496
|
+
`;
|
|
2497
|
+
var buildInitPy = (repoPath) => {
|
|
2498
|
+
const repoLiteral = JSON.stringify(repoPath);
|
|
2499
|
+
return `"""Auto-generated by set-prompt. Do not edit by hand."""
|
|
2500
|
+
import json
|
|
2501
|
+
import os
|
|
2502
|
+
import subprocess
|
|
2503
|
+
from pathlib import Path
|
|
2504
|
+
|
|
2505
|
+
REPO_DIR = Path(${repoLiteral})
|
|
2506
|
+
|
|
2507
|
+
HERMES_EVENTS = {
|
|
2508
|
+
"pre_tool_call",
|
|
2509
|
+
"post_tool_call",
|
|
2510
|
+
"pre_llm_call",
|
|
2511
|
+
"post_llm_call",
|
|
2512
|
+
"on_session_start",
|
|
2513
|
+
"on_session_end",
|
|
2514
|
+
"on_session_finalize",
|
|
2515
|
+
"on_session_reset",
|
|
2516
|
+
"subagent_stop",
|
|
2517
|
+
"pre_gateway_dispatch",
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
|
|
2521
|
+
def _parse_frontmatter(text):
|
|
2522
|
+
if not text.startswith("---"):
|
|
2523
|
+
return {}, text
|
|
2524
|
+
end = text.find("\\n---", 3)
|
|
2525
|
+
if end == -1:
|
|
2526
|
+
return {}, text
|
|
2527
|
+
meta_block = text[3:end].strip()
|
|
2528
|
+
body = text[end + 4:].lstrip("\\n")
|
|
2529
|
+
meta = {}
|
|
2530
|
+
for line in meta_block.splitlines():
|
|
2531
|
+
if ":" not in line:
|
|
2532
|
+
continue
|
|
2533
|
+
key, _, value = line.partition(":")
|
|
2534
|
+
meta[key.strip()] = value.strip().strip('"').strip("'")
|
|
2535
|
+
return meta, body
|
|
2536
|
+
|
|
2537
|
+
|
|
2538
|
+
def _make_command_handler(ctx, body):
|
|
2539
|
+
def handler(_args=None):
|
|
2540
|
+
ctx.inject_message(body, role="user")
|
|
2541
|
+
return handler
|
|
2542
|
+
|
|
2543
|
+
|
|
2544
|
+
def _make_hook_runner(event, raw_cmd, timeout):
|
|
2545
|
+
def runner(*args, **kwargs):
|
|
2546
|
+
cmd = raw_cmd.replace("\${SET_PROMPT_REPO}", str(REPO_DIR))
|
|
2547
|
+
env = dict(os.environ)
|
|
2548
|
+
env["SET_PROMPT_REPO"] = str(REPO_DIR)
|
|
2549
|
+
payload = json.dumps({
|
|
2550
|
+
"event": event,
|
|
2551
|
+
"args": [repr(a) for a in args],
|
|
2552
|
+
"kwargs": {k: repr(v) for k, v in kwargs.items()},
|
|
2553
|
+
}).encode("utf-8")
|
|
2554
|
+
try:
|
|
2555
|
+
subprocess.run(
|
|
2556
|
+
cmd, shell=True, input=payload, env=env,
|
|
2557
|
+
timeout=timeout if isinstance(timeout, (int, float)) else None,
|
|
2558
|
+
check=False,
|
|
2559
|
+
)
|
|
2560
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
2561
|
+
pass
|
|
2562
|
+
return runner
|
|
2563
|
+
|
|
2564
|
+
|
|
2565
|
+
def _iter_hook_commands(entries):
|
|
2566
|
+
if not isinstance(entries, list):
|
|
2567
|
+
return
|
|
2568
|
+
for entry in entries:
|
|
2569
|
+
if not isinstance(entry, dict):
|
|
2570
|
+
continue
|
|
2571
|
+
sub = entry.get("hooks")
|
|
2572
|
+
if not isinstance(sub, list):
|
|
2573
|
+
continue
|
|
2574
|
+
for h in sub:
|
|
2575
|
+
if not isinstance(h, dict):
|
|
2576
|
+
continue
|
|
2577
|
+
if h.get("type") != "command":
|
|
2578
|
+
continue
|
|
2579
|
+
cmd = h.get("command")
|
|
2580
|
+
if not isinstance(cmd, str) or not cmd:
|
|
2581
|
+
continue
|
|
2582
|
+
yield cmd, h.get("timeout")
|
|
2583
|
+
|
|
2584
|
+
|
|
2585
|
+
def _register_hooks(ctx):
|
|
2586
|
+
hooks_json = REPO_DIR / "hooks" / "hooks.json"
|
|
2587
|
+
if not hooks_json.exists():
|
|
2588
|
+
return
|
|
2589
|
+
try:
|
|
2590
|
+
data = json.loads(hooks_json.read_text(encoding="utf-8"))
|
|
2591
|
+
except (OSError, ValueError):
|
|
2592
|
+
return
|
|
2593
|
+
block = data.get("hooks") if isinstance(data, dict) else None
|
|
2594
|
+
if not isinstance(block, dict):
|
|
2595
|
+
return
|
|
2596
|
+
for event, entries in block.items():
|
|
2597
|
+
if event not in HERMES_EVENTS:
|
|
2598
|
+
continue
|
|
2599
|
+
for cmd, timeout in _iter_hook_commands(entries):
|
|
2600
|
+
ctx.register_hook(event, _make_hook_runner(event, cmd, timeout))
|
|
2601
|
+
|
|
2602
|
+
|
|
2603
|
+
def register(ctx):
|
|
2604
|
+
skills_dir = REPO_DIR / "skills"
|
|
2605
|
+
if skills_dir.exists():
|
|
2606
|
+
for skill_path in sorted(skills_dir.iterdir()):
|
|
2607
|
+
if not skill_path.is_dir():
|
|
2608
|
+
continue
|
|
2609
|
+
if not (skill_path / "SKILL.md").exists():
|
|
2610
|
+
continue
|
|
2611
|
+
ctx.register_skill(skill_path.name, str(skill_path))
|
|
2612
|
+
|
|
2613
|
+
commands_dir = REPO_DIR / "commands"
|
|
2614
|
+
if commands_dir.exists():
|
|
2615
|
+
for cmd_file in sorted(commands_dir.glob("*.md")):
|
|
2616
|
+
try:
|
|
2617
|
+
text = cmd_file.read_text(encoding="utf-8")
|
|
2618
|
+
except OSError:
|
|
2619
|
+
continue
|
|
2620
|
+
meta, body = _parse_frontmatter(text)
|
|
2621
|
+
name = meta.get("name") or cmd_file.stem
|
|
2622
|
+
description = meta.get("description") or ""
|
|
2623
|
+
ctx.register_command(name, _make_command_handler(ctx, body), description)
|
|
2624
|
+
|
|
2625
|
+
_register_hooks(ctx)
|
|
2626
|
+
`;
|
|
2627
|
+
};
|
|
2628
|
+
var FRESH_CONFIG_YAML = `# Generated by set-prompt
|
|
2629
|
+
plugins:
|
|
2630
|
+
enabled:
|
|
2631
|
+
- ${MARKET_NAME}
|
|
2632
|
+
`;
|
|
2633
|
+
var ENABLE_SNIPPET = `plugins:
|
|
2634
|
+
enabled:
|
|
2635
|
+
- ${MARKET_NAME}`;
|
|
2636
|
+
var writePluginManifest = (repoPath) => {
|
|
2637
|
+
fs13.writeFileSync(path12.join(HERMES_PLUGIN_DIR, "plugin.yaml"), PLUGIN_YAML, "utf-8");
|
|
2638
|
+
fs13.writeFileSync(path12.join(HERMES_PLUGIN_DIR, "__init__.py"), buildInitPy(repoPath), "utf-8");
|
|
2639
|
+
};
|
|
2640
|
+
var ensureHermesConfigEnabled = () => {
|
|
2641
|
+
if (fs13.existsSync(HERMES_CONFIG_PATH) === false) {
|
|
2642
|
+
fs13.mkdirSync(path12.dirname(HERMES_CONFIG_PATH), { recursive: true });
|
|
2643
|
+
fs13.writeFileSync(HERMES_CONFIG_PATH, FRESH_CONFIG_YAML, "utf-8");
|
|
2644
|
+
console.log(chalk14.green(" \u2713 ") + chalk14.dim(`wrote ${HERMES_CONFIG_PATH}`));
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
const content = fs13.readFileSync(HERMES_CONFIG_PATH, "utf-8");
|
|
2648
|
+
const enabledPattern = new RegExp(`^\\s*-\\s+${MARKET_NAME}\\s*$`, "m");
|
|
2649
|
+
if (enabledPattern.test(content)) {
|
|
2650
|
+
console.log(chalk14.dim(` \u2713 ${MARKET_NAME} already listed in ${HERMES_CONFIG_PATH}`));
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
console.log(chalk14.yellow(`
|
|
2654
|
+
\u26A0 Hermes config exists but does not list "${MARKET_NAME}" as enabled.`));
|
|
2655
|
+
console.log(chalk14.dim(" Add the following entry under plugins.enabled to activate this plugin:\n"));
|
|
2656
|
+
console.log(chalk14.cyan(ENABLE_SNIPPET.split("\n").map((l) => ` ${l}`).join("\n")));
|
|
2657
|
+
console.log(chalk14.dim(`
|
|
2658
|
+
File: ${HERMES_CONFIG_PATH}`));
|
|
2659
|
+
};
|
|
2660
|
+
var linkHermes = async () => {
|
|
2661
|
+
const repoPath = resolveRepoPath();
|
|
2662
|
+
if (repoPath == null) {
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
console.log(chalk14.green(`
|
|
2666
|
+
Setting up Hermes plugin...`));
|
|
2667
|
+
console.log(chalk14.dim(HERMES_PLUGIN_DIR));
|
|
2668
|
+
try {
|
|
2669
|
+
fs13.mkdirSync(HERMES_PLUGIN_DIR, { recursive: true });
|
|
2670
|
+
writePluginManifest(repoPath);
|
|
2671
|
+
console.log(chalk14.dim(" \u251C\u2500\u2500 ") + chalk14.bold("plugin.yaml") + chalk14.green(" \u2713"));
|
|
2672
|
+
console.log(chalk14.dim(" \u2514\u2500\u2500 ") + chalk14.bold("__init__.py") + chalk14.dim(` (REPO_DIR \u2192 ${repoPath})`) + chalk14.green(" \u2713"));
|
|
2673
|
+
ensureHermesConfigEnabled();
|
|
2674
|
+
configManager.hermes = { path: HERMES_PLUGIN_DIR };
|
|
2675
|
+
configManager.save();
|
|
2676
|
+
console.log(chalk14.dim(`
|
|
2677
|
+
\u2139 Hermes loads plugins at startup \u2014 restart Hermes to pick up new skills/commands.`));
|
|
2678
|
+
} catch (ex) {
|
|
2679
|
+
console.error(chalk14.red(`\u274C Failed to set up Hermes plugin: ${ex.message}`));
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
var unlinkHermes = async (force = false) => {
|
|
2683
|
+
if (!force) {
|
|
2684
|
+
const ok = await confirm11({
|
|
2685
|
+
message: `Remove Hermes plugin dir (${HERMES_PLUGIN_DIR})?`,
|
|
2686
|
+
default: false
|
|
2687
|
+
});
|
|
2688
|
+
if (!ok) {
|
|
2689
|
+
console.log(chalk14.yellow("Cancelled."));
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
console.log(chalk14.red(`
|
|
2694
|
+
Removing Hermes plugin...`));
|
|
2695
|
+
console.log(chalk14.dim(HERMES_PLUGIN_DIR));
|
|
2696
|
+
if (fs13.existsSync(HERMES_PLUGIN_DIR)) {
|
|
2697
|
+
fs13.rmSync(HERMES_PLUGIN_DIR, { recursive: true, force: true });
|
|
2698
|
+
console.log(chalk14.red(" removed") + chalk14.dim(`: ${HERMES_PLUGIN_DIR}`));
|
|
2699
|
+
}
|
|
2700
|
+
if (fs13.existsSync(HERMES_CONFIG_PATH)) {
|
|
2701
|
+
const content = fs13.readFileSync(HERMES_CONFIG_PATH, "utf-8");
|
|
2702
|
+
if (content.trimStart().startsWith("# Generated by set-prompt")) {
|
|
2703
|
+
fs13.unlinkSync(HERMES_CONFIG_PATH);
|
|
2704
|
+
console.log(chalk14.red(" removed") + chalk14.dim(`: ${HERMES_CONFIG_PATH}`));
|
|
2705
|
+
} else if (new RegExp(`^\\s*-\\s+${MARKET_NAME}\\s*$`, "m").test(content)) {
|
|
2706
|
+
console.log(chalk14.yellow(` \u26A0 Hermes config still lists "${MARKET_NAME}" \u2014 remove it manually:`));
|
|
2707
|
+
console.log(chalk14.dim(` ${HERMES_CONFIG_PATH}`));
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
configManager.hermes = null;
|
|
2711
|
+
configManager.save();
|
|
2712
|
+
};
|
|
2713
|
+
|
|
2369
2714
|
// src/commands/link-command.ts
|
|
2370
2715
|
var LINK_MAP = {
|
|
2371
2716
|
["claudecode" /* CLAUDECODE */]: linkClaudeCode,
|
|
@@ -2375,7 +2720,8 @@ var LINK_MAP = {
|
|
|
2375
2720
|
["antigravity" /* ANTIGRAVITY */]: linkAntigravity,
|
|
2376
2721
|
["cursor" /* CURSOR */]: linkCursor,
|
|
2377
2722
|
["opencode" /* OPENCODE */]: linkOpencode,
|
|
2378
|
-
["geminicli" /* GEMINICLI */]: linkGeminicli
|
|
2723
|
+
["geminicli" /* GEMINICLI */]: linkGeminicli,
|
|
2724
|
+
["hermes" /* HERMES */]: linkHermes
|
|
2379
2725
|
};
|
|
2380
2726
|
var UNLINK_MAP = {
|
|
2381
2727
|
["claudecode" /* CLAUDECODE */]: unlinkClaudeCode,
|
|
@@ -2385,13 +2731,14 @@ var UNLINK_MAP = {
|
|
|
2385
2731
|
["antigravity" /* ANTIGRAVITY */]: unlinkAntigravity,
|
|
2386
2732
|
["cursor" /* CURSOR */]: unlinkCursor,
|
|
2387
2733
|
["opencode" /* OPENCODE */]: unlinkOpencode,
|
|
2388
|
-
["geminicli" /* GEMINICLI */]: unlinkGeminicli
|
|
2734
|
+
["geminicli" /* GEMINICLI */]: unlinkGeminicli,
|
|
2735
|
+
["hermes" /* HERMES */]: unlinkHermes
|
|
2389
2736
|
};
|
|
2390
2737
|
var linkCommand = async (tool) => {
|
|
2391
2738
|
if (tool != null) {
|
|
2392
2739
|
const known = ALL_AGENTS.some((a) => a.value === tool);
|
|
2393
2740
|
if (!known) {
|
|
2394
|
-
console.log(
|
|
2741
|
+
console.log(chalk15.red(`Unknown vendor: ${tool}`));
|
|
2395
2742
|
process.exit(1);
|
|
2396
2743
|
}
|
|
2397
2744
|
await LINK_MAP[tool]();
|
|
@@ -2405,12 +2752,13 @@ var linkCommand = async (tool) => {
|
|
|
2405
2752
|
["antigravity" /* ANTIGRAVITY */]: configManager.isAntigravityEnabled(),
|
|
2406
2753
|
["cursor" /* CURSOR */]: configManager.isCursorEnabled(),
|
|
2407
2754
|
["opencode" /* OPENCODE */]: configManager.isOpencodeEnabled(),
|
|
2408
|
-
["geminicli" /* GEMINICLI */]: configManager.isGeminicliEnabled()
|
|
2755
|
+
["geminicli" /* GEMINICLI */]: configManager.isGeminicliEnabled(),
|
|
2756
|
+
["hermes" /* HERMES */]: configManager.isHermesEnabled()
|
|
2409
2757
|
};
|
|
2410
2758
|
const selected = await checkbox({
|
|
2411
2759
|
message: "Which AI agent do you want to integrate?",
|
|
2412
2760
|
choices: ALL_AGENTS.map((a) => ({
|
|
2413
|
-
name: prevLinked[a.value] ? `${a.name} ${
|
|
2761
|
+
name: prevLinked[a.value] ? `${a.name} ${chalk15.dim("(applied)")}` : a.name,
|
|
2414
2762
|
value: a.value,
|
|
2415
2763
|
checked: prevLinked[a.value]
|
|
2416
2764
|
})),
|
|
@@ -2421,10 +2769,10 @@ var linkCommand = async (tool) => {
|
|
|
2421
2769
|
const toUnlink = ALL_AGENTS.filter((a) => prevLinked[a.value] && !selected.includes(a.value));
|
|
2422
2770
|
console.log();
|
|
2423
2771
|
if (toLink.length > 0) {
|
|
2424
|
-
console.log(
|
|
2772
|
+
console.log(chalk15.green(" Link ") + chalk15.dim("\u2192 ") + toLink.map((a) => chalk15.bold(a.name)).join(chalk15.dim(", ")));
|
|
2425
2773
|
}
|
|
2426
2774
|
if (toUnlink.length > 0) {
|
|
2427
|
-
console.log(
|
|
2775
|
+
console.log(chalk15.red(" Unlink ") + chalk15.dim("\u2192 ") + toUnlink.map((a) => chalk15.bold(a.name)).join(chalk15.dim(", ")));
|
|
2428
2776
|
}
|
|
2429
2777
|
console.log();
|
|
2430
2778
|
if (toLink.length === 0 && toUnlink.length === 0) {
|
|
@@ -2443,47 +2791,59 @@ var linkCommand = async (tool) => {
|
|
|
2443
2791
|
};
|
|
2444
2792
|
|
|
2445
2793
|
// src/commands/uninstall-command.ts
|
|
2446
|
-
import
|
|
2447
|
-
import
|
|
2448
|
-
import { confirm as
|
|
2794
|
+
import fs14 from "fs";
|
|
2795
|
+
import chalk16 from "chalk";
|
|
2796
|
+
import { confirm as confirm12 } from "@inquirer/prompts";
|
|
2449
2797
|
var uninstallCommand = async () => {
|
|
2450
2798
|
const targets = [
|
|
2451
|
-
{ label: `Config file ${
|
|
2452
|
-
{ label: `Home dir ${
|
|
2453
|
-
].filter((t) =>
|
|
2799
|
+
{ label: `Config file ${chalk16.dim(CONFIG_PATH)}`, path: CONFIG_PATH },
|
|
2800
|
+
{ label: `Home dir ${chalk16.dim(HOME_DIR)}`, path: HOME_DIR }
|
|
2801
|
+
].filter((t) => fs14.existsSync(t.path));
|
|
2454
2802
|
const hasClaudeCode = configManager.isClaudeCodeEnabled();
|
|
2455
2803
|
const hasRooCode = configManager.isRooCodeEnabled();
|
|
2456
2804
|
const hasOpenclaw = configManager.isOpenclawEnabled();
|
|
2457
2805
|
const hasAntigravity = configManager.isAntigravityEnabled();
|
|
2458
2806
|
const hasCodex = configManager.isCodexEnabled();
|
|
2459
2807
|
const hasCursor = configManager.isCursorEnabled();
|
|
2460
|
-
|
|
2461
|
-
|
|
2808
|
+
const hasOpencode = configManager.isOpencodeEnabled();
|
|
2809
|
+
const hasGeminicli = configManager.isGeminicliEnabled();
|
|
2810
|
+
const hasHermes = configManager.isHermesEnabled();
|
|
2811
|
+
if (targets.length === 0 && !hasClaudeCode && !hasRooCode && !hasOpenclaw && !hasAntigravity && !hasCodex && !hasCursor && !hasOpencode && !hasGeminicli && !hasHermes) {
|
|
2812
|
+
console.log(chalk16.yellow("Nothing to remove."));
|
|
2462
2813
|
return;
|
|
2463
2814
|
}
|
|
2464
|
-
console.log(
|
|
2815
|
+
console.log(chalk16.red("\nThe following will be removed:"));
|
|
2465
2816
|
targets.forEach((t) => console.log(` ${t.label}`));
|
|
2466
2817
|
if (hasClaudeCode) {
|
|
2467
|
-
console.log(` Claude Code plugin dir ${
|
|
2818
|
+
console.log(` Claude Code plugin dir ${chalk16.dim(CLAUDE_CODE_DIR)}`);
|
|
2468
2819
|
}
|
|
2469
2820
|
if (hasRooCode) {
|
|
2470
|
-
console.log(` RooCode symlinks ${
|
|
2821
|
+
console.log(` RooCode symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2471
2822
|
}
|
|
2472
2823
|
if (hasOpenclaw) {
|
|
2473
|
-
console.log(` OpenClaw symlinks ${
|
|
2824
|
+
console.log(` OpenClaw symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2474
2825
|
}
|
|
2475
2826
|
if (hasAntigravity) {
|
|
2476
|
-
console.log(` Antigravity symlinks ${
|
|
2827
|
+
console.log(` Antigravity symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2477
2828
|
}
|
|
2478
2829
|
if (hasCodex) {
|
|
2479
|
-
console.log(` Codex symlinks ${
|
|
2830
|
+
console.log(` Codex symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2480
2831
|
}
|
|
2481
2832
|
if (hasCursor) {
|
|
2482
|
-
console.log(` Cursor plugin dir ${
|
|
2833
|
+
console.log(` Cursor plugin dir ${chalk16.dim("(symlink will be removed)")}`);
|
|
2483
2834
|
}
|
|
2484
|
-
|
|
2835
|
+
if (hasOpencode) {
|
|
2836
|
+
console.log(` OpenCode symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2837
|
+
}
|
|
2838
|
+
if (hasGeminicli) {
|
|
2839
|
+
console.log(` Gemini CLI symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2840
|
+
}
|
|
2841
|
+
if (hasHermes) {
|
|
2842
|
+
console.log(` Hermes plugin dir ${chalk16.dim("(plugin folder will be removed)")}`);
|
|
2843
|
+
}
|
|
2844
|
+
const ok = await confirm12({ message: "Proceed?", default: false });
|
|
2485
2845
|
if (!ok) {
|
|
2486
|
-
console.log(
|
|
2846
|
+
console.log(chalk16.yellow("Cancelled."));
|
|
2487
2847
|
return;
|
|
2488
2848
|
}
|
|
2489
2849
|
if (hasClaudeCode) {
|
|
@@ -2504,27 +2864,36 @@ var uninstallCommand = async () => {
|
|
|
2504
2864
|
if (hasCursor) {
|
|
2505
2865
|
await unlinkCursor(true);
|
|
2506
2866
|
}
|
|
2867
|
+
if (hasOpencode) {
|
|
2868
|
+
await unlinkOpencode(true);
|
|
2869
|
+
}
|
|
2870
|
+
if (hasGeminicli) {
|
|
2871
|
+
await unlinkGeminicli(true);
|
|
2872
|
+
}
|
|
2873
|
+
if (hasHermes) {
|
|
2874
|
+
await unlinkHermes(true);
|
|
2875
|
+
}
|
|
2507
2876
|
for (const t of targets) {
|
|
2508
|
-
|
|
2509
|
-
console.log(
|
|
2877
|
+
fs14.rmSync(t.path, { recursive: true, force: true });
|
|
2878
|
+
console.log(chalk16.dim(` removed: ${t.path}`));
|
|
2510
2879
|
}
|
|
2511
|
-
console.log(
|
|
2880
|
+
console.log(chalk16.green("\nUninstalled."));
|
|
2512
2881
|
};
|
|
2513
2882
|
|
|
2514
2883
|
// src/commands/status-command.ts
|
|
2515
|
-
import
|
|
2884
|
+
import chalk17 from "chalk";
|
|
2516
2885
|
var statusCommand = () => {
|
|
2517
2886
|
if (configManager.repo_path == null) {
|
|
2518
|
-
console.log(
|
|
2519
|
-
console.log(
|
|
2887
|
+
console.log(chalk17.yellow("\u274C No repo installed."));
|
|
2888
|
+
console.log(chalk17.dim(` Run: set-prompt install <repo-url>`));
|
|
2520
2889
|
return;
|
|
2521
2890
|
}
|
|
2522
|
-
console.log(
|
|
2523
|
-
console.log(`${TAB}path ${
|
|
2891
|
+
console.log(chalk17.bold("\nRepo"));
|
|
2892
|
+
console.log(`${TAB}path ${chalk17.cyan(configManager.repo_path)}`);
|
|
2524
2893
|
if (configManager.remote_url != null) {
|
|
2525
|
-
console.log(`${TAB}remote ${
|
|
2894
|
+
console.log(`${TAB}remote ${chalk17.dim(configManager.remote_url)}`);
|
|
2526
2895
|
}
|
|
2527
|
-
console.log(
|
|
2896
|
+
console.log(chalk17.bold("\nLinked agents"));
|
|
2528
2897
|
for (const agent of ALL_AGENTS) {
|
|
2529
2898
|
let linked = false;
|
|
2530
2899
|
let agentPath = null;
|
|
@@ -2543,9 +2912,21 @@ var statusCommand = () => {
|
|
|
2543
2912
|
} else if (agent.value === "antigravity" /* ANTIGRAVITY */) {
|
|
2544
2913
|
linked = configManager.isAntigravityEnabled();
|
|
2545
2914
|
agentPath = configManager.antigravity?.path;
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2548
|
-
|
|
2915
|
+
} else if (agent.value === "cursor" /* CURSOR */) {
|
|
2916
|
+
linked = configManager.isCursorEnabled();
|
|
2917
|
+
agentPath = configManager.cursor?.path;
|
|
2918
|
+
} else if (agent.value === "opencode" /* OPENCODE */) {
|
|
2919
|
+
linked = configManager.isOpencodeEnabled();
|
|
2920
|
+
agentPath = configManager.opencode?.path;
|
|
2921
|
+
} else if (agent.value === "geminicli" /* GEMINICLI */) {
|
|
2922
|
+
linked = configManager.isGeminicliEnabled();
|
|
2923
|
+
agentPath = configManager.geminicli?.path;
|
|
2924
|
+
} else if (agent.value === "hermes" /* HERMES */) {
|
|
2925
|
+
linked = configManager.isHermesEnabled();
|
|
2926
|
+
agentPath = configManager.hermes?.path;
|
|
2927
|
+
}
|
|
2928
|
+
const label = linked ? chalk17.green("linked") : chalk17.dim("not linked");
|
|
2929
|
+
const pathStr = linked && agentPath ? chalk17.dim(` \u2192 ${agentPath}`) : "";
|
|
2549
2930
|
console.log(`${TAB}${agent.name.padEnd(12)} ${label}${pathStr}`);
|
|
2550
2931
|
}
|
|
2551
2932
|
console.log("");
|
|
@@ -2553,92 +2934,92 @@ var statusCommand = () => {
|
|
|
2553
2934
|
|
|
2554
2935
|
// src/commands/repo/pull-command.ts
|
|
2555
2936
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2556
|
-
import
|
|
2937
|
+
import chalk18 from "chalk";
|
|
2557
2938
|
var repoPullCommand = () => {
|
|
2558
2939
|
const repoPath = configManager.repo_path;
|
|
2559
2940
|
if (repoPath == null) {
|
|
2560
|
-
console.error(
|
|
2561
|
-
console.log(
|
|
2941
|
+
console.error(chalk18.red("\u274C No repo installed."));
|
|
2942
|
+
console.log(chalk18.yellow("Run: set-prompt install <git-url>"));
|
|
2562
2943
|
return;
|
|
2563
2944
|
}
|
|
2564
2945
|
if (configManager.remote_url == null) {
|
|
2565
|
-
console.error(
|
|
2946
|
+
console.error(chalk18.red("\u274C No remote URL registered. Cannot pull."));
|
|
2566
2947
|
return;
|
|
2567
2948
|
}
|
|
2568
|
-
console.log(
|
|
2569
|
-
console.log(
|
|
2949
|
+
console.log(chalk18.green("\nPulling prompt repo..."));
|
|
2950
|
+
console.log(chalk18.dim(repoPath));
|
|
2570
2951
|
const fetch = spawnSync4("git", ["fetch"], { cwd: repoPath, stdio: "inherit" });
|
|
2571
2952
|
if (fetch.status !== 0) {
|
|
2572
|
-
console.error(
|
|
2953
|
+
console.error(chalk18.red("\u274C git fetch failed."));
|
|
2573
2954
|
return;
|
|
2574
2955
|
}
|
|
2575
2956
|
const pull = spawnSync4("git", ["pull"], { cwd: repoPath, stdio: "inherit" });
|
|
2576
2957
|
if (pull.status !== 0) {
|
|
2577
|
-
console.error(
|
|
2958
|
+
console.error(chalk18.red("\u274C git pull failed."));
|
|
2578
2959
|
return;
|
|
2579
2960
|
}
|
|
2580
|
-
console.log(
|
|
2961
|
+
console.log(chalk18.green("\u2705 Repo pulled."));
|
|
2581
2962
|
};
|
|
2582
2963
|
|
|
2583
2964
|
// src/commands/repo/commit-command.ts
|
|
2584
2965
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
2585
|
-
import
|
|
2966
|
+
import chalk19 from "chalk";
|
|
2586
2967
|
var repoCommitCommand = (options = {}) => {
|
|
2587
2968
|
const repoPath = configManager.repo_path;
|
|
2588
2969
|
if (repoPath == null) {
|
|
2589
|
-
console.error(
|
|
2590
|
-
console.log(
|
|
2970
|
+
console.error(chalk19.red("\u274C No repo installed."));
|
|
2971
|
+
console.log(chalk19.yellow("Run: set-prompt install <git-url>"));
|
|
2591
2972
|
return false;
|
|
2592
2973
|
}
|
|
2593
2974
|
let message = options.message;
|
|
2594
2975
|
if (message == null || message.trim() === "") {
|
|
2595
2976
|
const generated = generateCommitMessage(repoPath);
|
|
2596
2977
|
if (generated == null) {
|
|
2597
|
-
console.error(
|
|
2978
|
+
console.error(chalk19.red("\u274C Nothing to commit \u2014 working tree is clean."));
|
|
2598
2979
|
return false;
|
|
2599
2980
|
}
|
|
2600
2981
|
message = generated;
|
|
2601
2982
|
const subject = message.split("\n")[0];
|
|
2602
|
-
console.log(
|
|
2983
|
+
console.log(chalk19.dim(` (auto-generated: ${subject})`));
|
|
2603
2984
|
}
|
|
2604
|
-
console.log(
|
|
2605
|
-
console.log(
|
|
2985
|
+
console.log(chalk19.green("\nCommitting prompt repo changes..."));
|
|
2986
|
+
console.log(chalk19.dim(repoPath));
|
|
2606
2987
|
const add = spawnSync5("git", ["add", "-A"], { cwd: repoPath, stdio: "inherit" });
|
|
2607
2988
|
if (add.status !== 0) {
|
|
2608
|
-
console.error(
|
|
2989
|
+
console.error(chalk19.red("\u274C git add failed."));
|
|
2609
2990
|
return false;
|
|
2610
2991
|
}
|
|
2611
2992
|
const commit = spawnSync5("git", ["commit", "-m", message], { cwd: repoPath, stdio: "inherit" });
|
|
2612
2993
|
if (commit.status !== 0) {
|
|
2613
|
-
console.error(
|
|
2994
|
+
console.error(chalk19.red("\u274C git commit failed \u2014 nothing to commit, or commit rejected."));
|
|
2614
2995
|
return false;
|
|
2615
2996
|
}
|
|
2616
|
-
console.log(
|
|
2997
|
+
console.log(chalk19.green("\u2705 Committed."));
|
|
2617
2998
|
return true;
|
|
2618
2999
|
};
|
|
2619
3000
|
|
|
2620
3001
|
// src/commands/repo/push-command.ts
|
|
2621
3002
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
2622
|
-
import
|
|
3003
|
+
import chalk20 from "chalk";
|
|
2623
3004
|
var repoPushCommand = () => {
|
|
2624
3005
|
const repoPath = configManager.repo_path;
|
|
2625
3006
|
if (repoPath == null) {
|
|
2626
|
-
console.error(
|
|
2627
|
-
console.log(
|
|
3007
|
+
console.error(chalk20.red("\u274C No repo installed."));
|
|
3008
|
+
console.log(chalk20.yellow("Run: set-prompt install <git-url>"));
|
|
2628
3009
|
return false;
|
|
2629
3010
|
}
|
|
2630
3011
|
if (configManager.remote_url == null) {
|
|
2631
|
-
console.error(
|
|
3012
|
+
console.error(chalk20.red("\u274C No remote URL registered. Cannot push."));
|
|
2632
3013
|
return false;
|
|
2633
3014
|
}
|
|
2634
|
-
console.log(
|
|
2635
|
-
console.log(
|
|
3015
|
+
console.log(chalk20.green("\nPushing prompt repo..."));
|
|
3016
|
+
console.log(chalk20.dim(repoPath));
|
|
2636
3017
|
const push = spawnSync6("git", ["push"], { cwd: repoPath, stdio: "inherit" });
|
|
2637
3018
|
if (push.status !== 0) {
|
|
2638
|
-
console.error(
|
|
3019
|
+
console.error(chalk20.red("\u274C git push failed."));
|
|
2639
3020
|
return false;
|
|
2640
3021
|
}
|
|
2641
|
-
console.log(
|
|
3022
|
+
console.log(chalk20.green("\u2705 Pushed."));
|
|
2642
3023
|
return true;
|
|
2643
3024
|
};
|
|
2644
3025
|
|
|
@@ -2651,7 +3032,7 @@ var repoSaveCommand = (options = {}) => {
|
|
|
2651
3032
|
|
|
2652
3033
|
// src/commands/repo/status-command.ts
|
|
2653
3034
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
2654
|
-
import
|
|
3035
|
+
import chalk21 from "chalk";
|
|
2655
3036
|
var parseBranchLine = (line) => {
|
|
2656
3037
|
const body = line.replace(/^## /, "");
|
|
2657
3038
|
if (body.startsWith("HEAD ") || body.includes("(no branch)")) {
|
|
@@ -2679,27 +3060,27 @@ var parseFileLine = (line) => {
|
|
|
2679
3060
|
const arrowIdx = name.indexOf(" -> ");
|
|
2680
3061
|
if (arrowIdx >= 0) name = name.slice(arrowIdx + 4);
|
|
2681
3062
|
if (name.startsWith('"') && name.endsWith('"')) name = name.slice(1, -1);
|
|
2682
|
-
if (status.includes("?")) return { label: "untracked", color:
|
|
2683
|
-
if (status.includes("D")) return { label: "deleted", color:
|
|
2684
|
-
if (status.includes("R")) return { label: "renamed", color:
|
|
2685
|
-
if (status.includes("A")) return { label: "added", color:
|
|
2686
|
-
if (status.includes("M")) return { label: "modified", color:
|
|
3063
|
+
if (status.includes("?")) return { label: "untracked", color: chalk21.gray, path: name };
|
|
3064
|
+
if (status.includes("D")) return { label: "deleted", color: chalk21.red, path: name };
|
|
3065
|
+
if (status.includes("R")) return { label: "renamed", color: chalk21.cyan, path: name };
|
|
3066
|
+
if (status.includes("A")) return { label: "added", color: chalk21.green, path: name };
|
|
3067
|
+
if (status.includes("M")) return { label: "modified", color: chalk21.yellow, path: name };
|
|
2687
3068
|
return null;
|
|
2688
3069
|
};
|
|
2689
3070
|
var formatUpstream = (info) => {
|
|
2690
|
-
if (info.branch == null) return
|
|
2691
|
-
if (info.upstream == null) return `${info.branch} ${
|
|
3071
|
+
if (info.branch == null) return chalk21.red("(detached HEAD)");
|
|
3072
|
+
if (info.upstream == null) return `${info.branch} ${chalk21.yellow("(no upstream)")}`;
|
|
2692
3073
|
const segs = [];
|
|
2693
|
-
if (info.ahead > 0) segs.push(
|
|
2694
|
-
if (info.behind > 0) segs.push(
|
|
2695
|
-
const trailing = segs.length > 0 ? ` (${segs.join(", ")})` :
|
|
2696
|
-
return `${info.branch} ${
|
|
3074
|
+
if (info.ahead > 0) segs.push(chalk21.green(`ahead ${info.ahead}`));
|
|
3075
|
+
if (info.behind > 0) segs.push(chalk21.red(`behind ${info.behind}`));
|
|
3076
|
+
const trailing = segs.length > 0 ? ` (${segs.join(", ")})` : chalk21.dim(" (up to date)");
|
|
3077
|
+
return `${info.branch} ${chalk21.dim("\u2192")} ${info.upstream}${trailing}`;
|
|
2697
3078
|
};
|
|
2698
3079
|
var repoStatusCommand = () => {
|
|
2699
3080
|
const repoPath = configManager.repo_path;
|
|
2700
3081
|
if (repoPath == null) {
|
|
2701
|
-
console.error(
|
|
2702
|
-
console.log(
|
|
3082
|
+
console.error(chalk21.red("\u274C No repo installed."));
|
|
3083
|
+
console.log(chalk21.yellow("Run: set-prompt install <git-url>"));
|
|
2703
3084
|
return;
|
|
2704
3085
|
}
|
|
2705
3086
|
const result = spawnSync7("git", ["status", "--porcelain=v1", "--branch", "--untracked-files=all"], {
|
|
@@ -2707,8 +3088,8 @@ var repoStatusCommand = () => {
|
|
|
2707
3088
|
encoding: "utf8"
|
|
2708
3089
|
});
|
|
2709
3090
|
if (result.status !== 0) {
|
|
2710
|
-
console.error(
|
|
2711
|
-
if (result.stderr) console.error(
|
|
3091
|
+
console.error(chalk21.red("\u274C git status failed."));
|
|
3092
|
+
if (result.stderr) console.error(chalk21.dim(result.stderr));
|
|
2712
3093
|
return;
|
|
2713
3094
|
}
|
|
2714
3095
|
const lines = result.stdout.split("\n").filter((l) => l.length > 0);
|
|
@@ -2716,14 +3097,14 @@ var repoStatusCommand = () => {
|
|
|
2716
3097
|
const fileLines = lines.slice(1);
|
|
2717
3098
|
const branchInfo = parseBranchLine(branchLine);
|
|
2718
3099
|
const changes = fileLines.map(parseFileLine).filter((f) => f !== null);
|
|
2719
|
-
console.log(`${
|
|
2720
|
-
console.log(`${
|
|
3100
|
+
console.log(`${chalk21.cyan("\u{1F4C2}")} ${chalk21.dim(repoPath)}`);
|
|
3101
|
+
console.log(`${chalk21.cyan("\u{1F33F}")} ${formatUpstream(branchInfo)}`);
|
|
2721
3102
|
console.log("");
|
|
2722
3103
|
if (changes.length === 0) {
|
|
2723
|
-
console.log(
|
|
3104
|
+
console.log(chalk21.green("\u2705 Working tree clean"));
|
|
2724
3105
|
return;
|
|
2725
3106
|
}
|
|
2726
|
-
console.log(
|
|
3107
|
+
console.log(chalk21.bold(`\u{1F4DD} Changes (${changes.length}):`));
|
|
2727
3108
|
const labelWidth = Math.max(...changes.map((c) => c.label.length));
|
|
2728
3109
|
for (const c of changes) {
|
|
2729
3110
|
const label = c.color(c.label.padEnd(labelWidth));
|
|
@@ -2732,12 +3113,12 @@ var repoStatusCommand = () => {
|
|
|
2732
3113
|
};
|
|
2733
3114
|
|
|
2734
3115
|
// src/commands/repo/path-command.ts
|
|
2735
|
-
import
|
|
3116
|
+
import chalk22 from "chalk";
|
|
2736
3117
|
var repoPathCommand = () => {
|
|
2737
3118
|
const repoPath = configManager.repo_path;
|
|
2738
3119
|
if (repoPath == null) {
|
|
2739
|
-
console.error(
|
|
2740
|
-
console.error(
|
|
3120
|
+
console.error(chalk22.red("\u274C No repo installed."));
|
|
3121
|
+
console.error(chalk22.yellow("Run: set-prompt install <git-url>"));
|
|
2741
3122
|
process.exitCode = 1;
|
|
2742
3123
|
return;
|
|
2743
3124
|
}
|
|
@@ -2746,8 +3127,8 @@ var repoPathCommand = () => {
|
|
|
2746
3127
|
|
|
2747
3128
|
// src/commands/repo/open-command.ts
|
|
2748
3129
|
import { spawn } from "child_process";
|
|
2749
|
-
import
|
|
2750
|
-
import
|
|
3130
|
+
import path13 from "path";
|
|
3131
|
+
import chalk23 from "chalk";
|
|
2751
3132
|
var resolveVscodeTarget = (repoPath) => {
|
|
2752
3133
|
if (isOnPath("code") === false) return null;
|
|
2753
3134
|
return { bin: "code", args: [repoPath] };
|
|
@@ -2758,9 +3139,9 @@ var resolveSourcetreeTarget = (repoPath) => {
|
|
|
2758
3139
|
}
|
|
2759
3140
|
if (process.platform === "win32") {
|
|
2760
3141
|
const exe = firstExistingPath([
|
|
2761
|
-
process.env.LOCALAPPDATA &&
|
|
2762
|
-
process.env["ProgramFiles(x86)"] &&
|
|
2763
|
-
process.env.ProgramFiles &&
|
|
3142
|
+
process.env.LOCALAPPDATA && path13.join(process.env.LOCALAPPDATA, "SourceTree", "SourceTree.exe"),
|
|
3143
|
+
process.env["ProgramFiles(x86)"] && path13.join(process.env["ProgramFiles(x86)"], "Atlassian", "SourceTree", "SourceTree.exe"),
|
|
3144
|
+
process.env.ProgramFiles && path13.join(process.env.ProgramFiles, "Atlassian", "SourceTree", "SourceTree.exe")
|
|
2764
3145
|
]);
|
|
2765
3146
|
if (exe != null) return { bin: exe, args: ["-f", repoPath] };
|
|
2766
3147
|
}
|
|
@@ -2777,22 +3158,22 @@ var sourcetreeInstallHint = () => {
|
|
|
2777
3158
|
return "Sourcetree is not available on Linux \u2014 try a native Git GUI instead";
|
|
2778
3159
|
};
|
|
2779
3160
|
var runLaunch = (label, target) => {
|
|
2780
|
-
console.log(
|
|
3161
|
+
console.log(chalk23.green(`Opening in ${label}: ${chalk23.dim(target.args[target.args.length - 1])}`));
|
|
2781
3162
|
const child = spawn(target.bin, target.args, { stdio: "ignore", detached: true, shell: true });
|
|
2782
3163
|
child.unref();
|
|
2783
3164
|
};
|
|
2784
3165
|
var repoOpenCommand = (options = {}) => {
|
|
2785
3166
|
const repoPath = configManager.repo_path;
|
|
2786
3167
|
if (repoPath == null) {
|
|
2787
|
-
console.error(
|
|
2788
|
-
console.log(
|
|
3168
|
+
console.error(chalk23.red("\u274C No repo installed."));
|
|
3169
|
+
console.log(chalk23.yellow("Run: set-prompt install <git-url>"));
|
|
2789
3170
|
return;
|
|
2790
3171
|
}
|
|
2791
3172
|
if (options.code === true) {
|
|
2792
3173
|
const target = resolveVscodeTarget(repoPath);
|
|
2793
3174
|
if (target == null) {
|
|
2794
|
-
console.error(
|
|
2795
|
-
console.log(
|
|
3175
|
+
console.error(chalk23.red("\u274C VSCode CLI (`code`) not found on PATH."));
|
|
3176
|
+
console.log(chalk23.dim(` ${VSCODE_INSTALL_HINT}`));
|
|
2796
3177
|
return;
|
|
2797
3178
|
}
|
|
2798
3179
|
runLaunch("VSCode", target);
|
|
@@ -2801,8 +3182,8 @@ var repoOpenCommand = (options = {}) => {
|
|
|
2801
3182
|
if (options.stree === true) {
|
|
2802
3183
|
const target = resolveSourcetreeTarget(repoPath);
|
|
2803
3184
|
if (target == null) {
|
|
2804
|
-
console.error(
|
|
2805
|
-
console.log(
|
|
3185
|
+
console.error(chalk23.red("\u274C Sourcetree not found."));
|
|
3186
|
+
console.log(chalk23.dim(` ${sourcetreeInstallHint()}`));
|
|
2806
3187
|
return;
|
|
2807
3188
|
}
|
|
2808
3189
|
runLaunch("Sourcetree", target);
|
|
@@ -2810,46 +3191,46 @@ var repoOpenCommand = (options = {}) => {
|
|
|
2810
3191
|
}
|
|
2811
3192
|
const platform = process.platform;
|
|
2812
3193
|
const opener = platform === "win32" ? "explorer" : platform === "darwin" ? "open" : "xdg-open";
|
|
2813
|
-
console.log(
|
|
3194
|
+
console.log(chalk23.green(`Opening: ${chalk23.dim(repoPath)}`));
|
|
2814
3195
|
const child = spawn(opener, [repoPath], { stdio: "ignore", detached: true });
|
|
2815
3196
|
child.on("error", (ex) => {
|
|
2816
|
-
console.error(
|
|
3197
|
+
console.error(chalk23.red(`\u274C Failed to open: ${ex.message}`));
|
|
2817
3198
|
});
|
|
2818
3199
|
child.unref();
|
|
2819
3200
|
};
|
|
2820
3201
|
|
|
2821
3202
|
// src/index.ts
|
|
2822
3203
|
process.on("SIGINT", () => {
|
|
2823
|
-
console.log(
|
|
3204
|
+
console.log(chalk24.yellow("\nCancelled."));
|
|
2824
3205
|
process.exit(0);
|
|
2825
3206
|
});
|
|
2826
3207
|
process.on("unhandledRejection", (reason) => {
|
|
2827
3208
|
if (reason instanceof Error && reason.name === "ExitPromptError") {
|
|
2828
|
-
console.log(
|
|
3209
|
+
console.log(chalk24.yellow("\nCancelled."));
|
|
2829
3210
|
process.exit(0);
|
|
2830
3211
|
}
|
|
2831
3212
|
throw reason;
|
|
2832
3213
|
});
|
|
2833
|
-
var __dirname =
|
|
2834
|
-
var pkg = JSON.parse(
|
|
3214
|
+
var __dirname = path14.dirname(fileURLToPath(import.meta.url));
|
|
3215
|
+
var pkg = JSON.parse(fs15.readFileSync(path14.join(__dirname, "../package.json"), "utf-8"));
|
|
2835
3216
|
configManager.init();
|
|
2836
3217
|
var program = new Command();
|
|
2837
|
-
var banner =
|
|
3218
|
+
var banner = chalk24.cyan(figlet.textSync("Set-Prompt", { horizontalLayout: "full" }));
|
|
2838
3219
|
program.name("set-prompt").description(pkg.description).version(pkg.version).addHelpText("beforeAll", ({ command }) => command === program ? banner + "\n" : "");
|
|
2839
|
-
program.command("install").description(`\u{1F4E6} Clone a ${
|
|
3220
|
+
program.command("install").description(`\u{1F4E6} Clone a ${chalk24.cyan("git repo")} into ${chalk24.dim("~/.set-prompt/repo/")} and register it as your prompt source`).argument("<url>", "remote git URL").action(async (source) => {
|
|
2840
3221
|
await installCommand(source);
|
|
2841
3222
|
});
|
|
2842
|
-
program.command("link").description(`\u{1F517} Symlink your prompt repo into an ${
|
|
3223
|
+
program.command("link").description(`\u{1F517} Symlink your prompt repo into an ${chalk24.cyan("AI agent")} plugin dir ${chalk24.dim("(claudecode | roocode | openclaw | codex | antigravity | cursor | opencode | geminicli | hermes)")}`).argument("[agent]", `target agent ${chalk24.dim("(omit for interactive selection)")}`).action(async (agent) => {
|
|
2843
3224
|
await linkCommand(agent);
|
|
2844
3225
|
});
|
|
2845
|
-
program.command("scaffold").description(`\u{1F6E0}\uFE0F Verify and create ${
|
|
3226
|
+
program.command("scaffold").description(`\u{1F6E0}\uFE0F Verify and create ${chalk24.cyan("required directories")} in a prompt repo ${chalk24.dim("(-f to force overwrite)")}`).argument("[path]", `path to repo ${chalk24.dim("(defaults to installed source)")}`).action(async (localPath) => {
|
|
2846
3227
|
await scaffoldCommand(localPath);
|
|
2847
3228
|
});
|
|
2848
|
-
program.command("status").description(`\u{1F4CB} Show registered ${
|
|
3229
|
+
program.command("status").description(`\u{1F4CB} Show registered ${chalk24.cyan("repo")} and which ${chalk24.cyan("agents")} are linked`).action(() => {
|
|
2849
3230
|
statusCommand();
|
|
2850
3231
|
});
|
|
2851
|
-
var repo = program.command("repo").description(`\u{1F5C2}\uFE0F Manage the installed prompt repo ${
|
|
2852
|
-
repo.command("status").description(`\u{1F4CB} Show VCS status of the repo ${
|
|
3232
|
+
var repo = program.command("repo").description(`\u{1F5C2}\uFE0F Manage the installed prompt repo ${chalk24.dim("(status | pull/update | commit | push | save | path | open)")}`);
|
|
3233
|
+
repo.command("status").description(`\u{1F4CB} Show VCS status of the repo ${chalk24.dim("(branch, ahead/behind, changed files)")}`).addHelpText("after", `
|
|
2853
3234
|
Example output:
|
|
2854
3235
|
\u{1F4C2} ~/.set-prompt/repo
|
|
2855
3236
|
\u{1F33F} main \u2192 origin/main (ahead 2)
|
|
@@ -2860,47 +3241,47 @@ Example output:
|
|
|
2860
3241
|
`).action(() => {
|
|
2861
3242
|
repoStatusCommand();
|
|
2862
3243
|
});
|
|
2863
|
-
repo.command("pull").description(`\u{1F504} Fetch and pull the latest changes from the ${
|
|
3244
|
+
repo.command("pull").alias("update").description(`\u{1F504} Fetch and pull the latest changes from the ${chalk24.cyan("remote repo")}`).action(() => {
|
|
2864
3245
|
repoPullCommand();
|
|
2865
3246
|
});
|
|
2866
|
-
repo.command("commit").description(`\u{1F4DD} Stage all changes and commit ${
|
|
3247
|
+
repo.command("commit").description(`\u{1F4DD} Stage all changes and commit ${chalk24.dim("(auto-generates message if -m omitted; does not push)")}`).option("-m, --message <msg>", "commit message (auto-generated from changed files if omitted)").addHelpText("after", `
|
|
2867
3248
|
Examples:
|
|
2868
3249
|
$ sppt repo commit -m "edit dbml skill"
|
|
2869
|
-
$ sppt repo commit ${
|
|
3250
|
+
$ sppt repo commit ${chalk24.dim('# auto-generates "update N files" + file list')}
|
|
2870
3251
|
`).action((opts) => {
|
|
2871
3252
|
repoCommitCommand({ message: opts.message });
|
|
2872
3253
|
});
|
|
2873
3254
|
repo.command("push").description(`\u2B06\uFE0F Push local commits to the remote`).action(() => {
|
|
2874
3255
|
repoPushCommand();
|
|
2875
3256
|
});
|
|
2876
|
-
repo.command("save").description(`\u{1F4BE} Stage + commit + push in one step ${
|
|
3257
|
+
repo.command("save").description(`\u{1F4BE} Stage + commit + push in one step ${chalk24.dim("(macro for commit \u2192 push; auto-generates message if -m omitted)")}`).option("-m, --message <msg>", "commit message (auto-generated from changed files if omitted)").addHelpText("after", `
|
|
2877
3258
|
Examples:
|
|
2878
3259
|
$ sppt repo save -m "edit dbml skill"
|
|
2879
|
-
$ sppt repo save ${
|
|
3260
|
+
$ sppt repo save ${chalk24.dim("# auto-generates message and pushes")}
|
|
2880
3261
|
|
|
2881
3262
|
Equivalent to: sppt repo commit && sppt repo push
|
|
2882
3263
|
`).action((opts) => {
|
|
2883
3264
|
repoSaveCommand({ message: opts.message });
|
|
2884
3265
|
});
|
|
2885
|
-
repo.command("path").description(`\u{1F4CD} Print the repo path to stdout ${
|
|
3266
|
+
repo.command("path").description(`\u{1F4CD} Print the repo path to stdout ${chalk24.dim("(e.g. cd $(sppt repo path))")}`).addHelpText("after", `
|
|
2886
3267
|
Examples:
|
|
2887
3268
|
$ sppt repo path
|
|
2888
|
-
${
|
|
3269
|
+
${chalk24.dim("/Users/me/.set-prompt/repo")}
|
|
2889
3270
|
|
|
2890
|
-
$ cd "$(sppt repo path)" ${
|
|
2891
|
-
$ code "$(sppt repo path)" ${
|
|
3271
|
+
$ cd "$(sppt repo path)" ${chalk24.dim("# jump into the repo")}
|
|
3272
|
+
$ code "$(sppt repo path)" ${chalk24.dim("# open in VSCode")}
|
|
2892
3273
|
`).action(() => {
|
|
2893
3274
|
repoPathCommand();
|
|
2894
3275
|
});
|
|
2895
|
-
repo.command("open").description(`\u{1F4C2} Open the repo in the OS file manager ${
|
|
3276
|
+
repo.command("open").description(`\u{1F4C2} Open the repo in the OS file manager ${chalk24.dim("(--code: VSCode, --stree: Sourcetree)")}`).option("--code", "open with VSCode (`code` CLI)").option("--stree", "open with Sourcetree (`stree` CLI)").addHelpText("after", `
|
|
2896
3277
|
Examples:
|
|
2897
|
-
$ sppt repo open ${
|
|
2898
|
-
$ sppt repo open --code ${
|
|
2899
|
-
$ sppt repo open --stree ${
|
|
3278
|
+
$ sppt repo open ${chalk24.dim("# Explorer / Finder / xdg-open")}
|
|
3279
|
+
$ sppt repo open --code ${chalk24.dim("# open in VSCode")}
|
|
3280
|
+
$ sppt repo open --stree ${chalk24.dim("# open in Sourcetree")}
|
|
2900
3281
|
`).action((opts) => {
|
|
2901
3282
|
repoOpenCommand({ code: opts.code, stree: opts.stree });
|
|
2902
3283
|
});
|
|
2903
|
-
program.command("uninstall").description(`\u{1F5D1}\uFE0F Remove all set-prompt data ${
|
|
3284
|
+
program.command("uninstall").description(`\u{1F5D1}\uFE0F Remove all set-prompt data ${chalk24.dim("(~/.set-prompt/, plugin dirs, settings entries)")}`).action(async () => {
|
|
2904
3285
|
await uninstallCommand();
|
|
2905
3286
|
});
|
|
2906
3287
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "set-prompt",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "One repo. Every AI coding tool. Always in sync.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"set-prompt": "./dist/index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start:dev": "tsx src/index.ts",
|
|
16
16
|
"build": "rimraf dist && tsup",
|
|
17
|
+
"publish": "npm run build && npm publish",
|
|
17
18
|
"test": "vitest --reporter=verbose"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|