set-prompt 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +12 -1
- package/dist/index.js +614 -135
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.8.1] - 2026-05-02
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Hermes auto-enable** — `link hermes` now writes `set-prompt` into the existing `~/.hermes/config.yaml` automatically, matching Claude Code / Codex behavior. Previously the file was left untouched and only a manual-merge snippet was printed, which left the plugin detected-but-disabled (`× set-prompt — not enabled in config`).
|
|
11
|
+
- Existing `plugins.enabled` list → `set-prompt` is appended (other entries preserved).
|
|
12
|
+
- `plugins` map exists but no `enabled` key → `enabled: [set-prompt]` is added.
|
|
13
|
+
- `plugins` key missing entirely → `plugins.enabled: [set-prompt]` is added.
|
|
14
|
+
- YAML comments and formatting are preserved (AST-based modification via `yaml` package).
|
|
15
|
+
- Atomic write with timestamped backup + rollback on failure (same pattern as `link claudecode` / `link codex`).
|
|
16
|
+
- **`unlinkHermes` surgical removal** — for user-authored `config.yaml` (no `# Generated by set-prompt` header), only the `- set-prompt` entry is removed; the rest of the file is preserved. Auto-generated configs are still deleted whole.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `yaml` (eemeli/yaml) dependency for AST-based YAML editing.
|
|
20
|
+
- Tests for the new auto-enable branches: `plugins` key absent, `plugins.enabled` absent, comment preservation, and `unlinkHermes` surgical removal (17 cases total in `link-hermes.test.ts`).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [0.8.0] - 2026-05-02
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **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
|
|
28
|
+
- `plugin.yaml` — Hermes plugin manifest (`name`, `version`, `description`)
|
|
29
|
+
- `__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()`.
|
|
30
|
+
- 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.
|
|
31
|
+
- 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": {...}}`.
|
|
32
|
+
- `~/.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.
|
|
33
|
+
- `HermesConfigSchema` + `HermesConfig` type + `configManager.hermes` getter/setter + `isHermesEnabled()`
|
|
34
|
+
- `HERMES_DIR`, `HERMES_PLUGIN_DIR`, `HERMES_CONFIG_PATH` constants in `_defs`
|
|
35
|
+
- `linkHermes()` / `unlinkHermes()` in `src/link/hermes.ts`
|
|
36
|
+
- `SET_PROMPT_GUIDE` template: Hermes commands/hooks integration sections (set-prompt's adapter behavior, event whitelist table, payload format, decision-control caveat)
|
|
37
|
+
- 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)
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- `status-command.ts` — added missing branches for Cursor / OpenCode / Gemini CLI / Hermes (previously fell through silently after Antigravity)
|
|
41
|
+
- `uninstall-command.ts` — added missing unlink calls for OpenCode / Gemini CLI in addition to Hermes
|
|
42
|
+
- `link-command.ts` description in `index.ts` — agent list extended with `hermes`
|
|
43
|
+
- Test mocks updated across `link-command`, `status-command`, `uninstall-command` to cover all current `is*Enabled()` predicates and `unlink*` exports
|
|
44
|
+
|
|
45
|
+
### Notes / Caveats
|
|
46
|
+
- **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.
|
|
47
|
+
- **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.
|
|
48
|
+
- **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.
|
|
49
|
+
- **`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.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
7
53
|
## [0.7.1] - 2026-04-20
|
|
8
54
|
|
|
9
55
|
### Changed
|
package/README.md
CHANGED
|
@@ -94,6 +94,7 @@ set-prompt link antigravity # link Antigravity only
|
|
|
94
94
|
set-prompt link cursor # link Cursor only
|
|
95
95
|
set-prompt link opencode # link OpenCode only
|
|
96
96
|
set-prompt link geminicli # link Gemini CLI only
|
|
97
|
+
set-prompt link hermes # link Hermes only
|
|
97
98
|
```
|
|
98
99
|
|
|
99
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.
|
|
@@ -108,6 +109,7 @@ The interactive mode shows all agents with their current state. **Check to link,
|
|
|
108
109
|
| Cursor | dir symlinks into `~/.cursor/` | `skills/`, `agents/`, `commands/`, `hooks/`, `mcp.json` (hardlink) |
|
|
109
110
|
| OpenCode | dir symlinks into `~/.config/opencode/` | `skills/`, `commands/`, `agents/` |
|
|
110
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) |
|
|
111
113
|
|
|
112
114
|
> **Note on Claude Code**: Operates as a plugin. Restart Claude Code after linking for the plugin to be recognized.
|
|
113
115
|
|
|
@@ -121,6 +123,8 @@ The interactive mode shows all agents with their current state. **Check to link,
|
|
|
121
123
|
|
|
122
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.
|
|
123
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 writes this entry automatically (creating the file when absent, or appending to the existing list while preserving other entries and comments via AST-based YAML editing with backup/rollback). 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
|
+
|
|
124
128
|
---
|
|
125
129
|
|
|
126
130
|
### Step 3 — Keep in sync
|
|
@@ -143,7 +147,7 @@ set-prompt repo path # print repo location (e.g. cd "$(sppt rep
|
|
|
143
147
|
set-prompt repo open # open repo in OS file manager (--code VSCode, --stree Sourcetree)
|
|
144
148
|
```
|
|
145
149
|
|
|
146
|
-
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.
|
|
147
151
|
|
|
148
152
|
---
|
|
149
153
|
|
|
@@ -231,6 +235,12 @@ set-prompt uninstall
|
|
|
231
235
|
├── commands/ → repo/commands (Gemini reads *.toml)
|
|
232
236
|
├── agents/ → repo/agents (Gemini reads *.md)
|
|
233
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
|
|
234
244
|
```
|
|
235
245
|
|
|
236
246
|
## Warning
|
|
@@ -245,6 +255,7 @@ set-prompt uninstall
|
|
|
245
255
|
- **Cursor** — replaces directories and `mcp.json` in `~/.cursor/`
|
|
246
256
|
- **OpenCode** — replaces directories in `~/.config/opencode/`
|
|
247
257
|
- **Gemini CLI** — replaces directories in `~/.gemini/` (`antigravity/` subtree untouched)
|
|
258
|
+
- **Hermes** — generates `~/.hermes/plugins/set-prompt/{plugin.yaml, __init__.py}` and adds `set-prompt` to `~/.hermes/config.yaml` under `plugins.enabled` (creates the file when absent; appends to the existing list while preserving other entries and comments)
|
|
248
259
|
|
|
249
260
|
Before making any changes, `set-prompt` creates a backup and rolls back automatically on failure. However, you should be aware that:
|
|
250
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,330 @@ 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
|
+
import YAML from "yaml";
|
|
2494
|
+
var PLUGIN_YAML = `name: ${MARKET_NAME}
|
|
2495
|
+
version: "1.0"
|
|
2496
|
+
description: Managed by set-prompt
|
|
2497
|
+
`;
|
|
2498
|
+
var buildInitPy = (repoPath) => {
|
|
2499
|
+
const repoLiteral = JSON.stringify(repoPath);
|
|
2500
|
+
return `"""Auto-generated by set-prompt. Do not edit by hand."""
|
|
2501
|
+
import json
|
|
2502
|
+
import os
|
|
2503
|
+
import subprocess
|
|
2504
|
+
from pathlib import Path
|
|
2505
|
+
|
|
2506
|
+
REPO_DIR = Path(${repoLiteral})
|
|
2507
|
+
|
|
2508
|
+
HERMES_EVENTS = {
|
|
2509
|
+
"pre_tool_call",
|
|
2510
|
+
"post_tool_call",
|
|
2511
|
+
"pre_llm_call",
|
|
2512
|
+
"post_llm_call",
|
|
2513
|
+
"on_session_start",
|
|
2514
|
+
"on_session_end",
|
|
2515
|
+
"on_session_finalize",
|
|
2516
|
+
"on_session_reset",
|
|
2517
|
+
"subagent_stop",
|
|
2518
|
+
"pre_gateway_dispatch",
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
|
|
2522
|
+
def _parse_frontmatter(text):
|
|
2523
|
+
if not text.startswith("---"):
|
|
2524
|
+
return {}, text
|
|
2525
|
+
end = text.find("\\n---", 3)
|
|
2526
|
+
if end == -1:
|
|
2527
|
+
return {}, text
|
|
2528
|
+
meta_block = text[3:end].strip()
|
|
2529
|
+
body = text[end + 4:].lstrip("\\n")
|
|
2530
|
+
meta = {}
|
|
2531
|
+
for line in meta_block.splitlines():
|
|
2532
|
+
if ":" not in line:
|
|
2533
|
+
continue
|
|
2534
|
+
key, _, value = line.partition(":")
|
|
2535
|
+
meta[key.strip()] = value.strip().strip('"').strip("'")
|
|
2536
|
+
return meta, body
|
|
2537
|
+
|
|
2538
|
+
|
|
2539
|
+
def _make_command_handler(ctx, body):
|
|
2540
|
+
def handler(_args=None):
|
|
2541
|
+
ctx.inject_message(body, role="user")
|
|
2542
|
+
return handler
|
|
2543
|
+
|
|
2544
|
+
|
|
2545
|
+
def _make_hook_runner(event, raw_cmd, timeout):
|
|
2546
|
+
def runner(*args, **kwargs):
|
|
2547
|
+
cmd = raw_cmd.replace("\${SET_PROMPT_REPO}", str(REPO_DIR))
|
|
2548
|
+
env = dict(os.environ)
|
|
2549
|
+
env["SET_PROMPT_REPO"] = str(REPO_DIR)
|
|
2550
|
+
payload = json.dumps({
|
|
2551
|
+
"event": event,
|
|
2552
|
+
"args": [repr(a) for a in args],
|
|
2553
|
+
"kwargs": {k: repr(v) for k, v in kwargs.items()},
|
|
2554
|
+
}).encode("utf-8")
|
|
2555
|
+
try:
|
|
2556
|
+
subprocess.run(
|
|
2557
|
+
cmd, shell=True, input=payload, env=env,
|
|
2558
|
+
timeout=timeout if isinstance(timeout, (int, float)) else None,
|
|
2559
|
+
check=False,
|
|
2560
|
+
)
|
|
2561
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
2562
|
+
pass
|
|
2563
|
+
return runner
|
|
2564
|
+
|
|
2565
|
+
|
|
2566
|
+
def _iter_hook_commands(entries):
|
|
2567
|
+
if not isinstance(entries, list):
|
|
2568
|
+
return
|
|
2569
|
+
for entry in entries:
|
|
2570
|
+
if not isinstance(entry, dict):
|
|
2571
|
+
continue
|
|
2572
|
+
sub = entry.get("hooks")
|
|
2573
|
+
if not isinstance(sub, list):
|
|
2574
|
+
continue
|
|
2575
|
+
for h in sub:
|
|
2576
|
+
if not isinstance(h, dict):
|
|
2577
|
+
continue
|
|
2578
|
+
if h.get("type") != "command":
|
|
2579
|
+
continue
|
|
2580
|
+
cmd = h.get("command")
|
|
2581
|
+
if not isinstance(cmd, str) or not cmd:
|
|
2582
|
+
continue
|
|
2583
|
+
yield cmd, h.get("timeout")
|
|
2584
|
+
|
|
2585
|
+
|
|
2586
|
+
def _register_hooks(ctx):
|
|
2587
|
+
hooks_json = REPO_DIR / "hooks" / "hooks.json"
|
|
2588
|
+
if not hooks_json.exists():
|
|
2589
|
+
return
|
|
2590
|
+
try:
|
|
2591
|
+
data = json.loads(hooks_json.read_text(encoding="utf-8"))
|
|
2592
|
+
except (OSError, ValueError):
|
|
2593
|
+
return
|
|
2594
|
+
block = data.get("hooks") if isinstance(data, dict) else None
|
|
2595
|
+
if not isinstance(block, dict):
|
|
2596
|
+
return
|
|
2597
|
+
for event, entries in block.items():
|
|
2598
|
+
if event not in HERMES_EVENTS:
|
|
2599
|
+
continue
|
|
2600
|
+
for cmd, timeout in _iter_hook_commands(entries):
|
|
2601
|
+
ctx.register_hook(event, _make_hook_runner(event, cmd, timeout))
|
|
2602
|
+
|
|
2603
|
+
|
|
2604
|
+
def register(ctx):
|
|
2605
|
+
skills_dir = REPO_DIR / "skills"
|
|
2606
|
+
if skills_dir.exists():
|
|
2607
|
+
for skill_path in sorted(skills_dir.iterdir()):
|
|
2608
|
+
if not skill_path.is_dir():
|
|
2609
|
+
continue
|
|
2610
|
+
if not (skill_path / "SKILL.md").exists():
|
|
2611
|
+
continue
|
|
2612
|
+
ctx.register_skill(skill_path.name, str(skill_path))
|
|
2613
|
+
|
|
2614
|
+
commands_dir = REPO_DIR / "commands"
|
|
2615
|
+
if commands_dir.exists():
|
|
2616
|
+
for cmd_file in sorted(commands_dir.glob("*.md")):
|
|
2617
|
+
try:
|
|
2618
|
+
text = cmd_file.read_text(encoding="utf-8")
|
|
2619
|
+
except OSError:
|
|
2620
|
+
continue
|
|
2621
|
+
meta, body = _parse_frontmatter(text)
|
|
2622
|
+
name = meta.get("name") or cmd_file.stem
|
|
2623
|
+
description = meta.get("description") or ""
|
|
2624
|
+
ctx.register_command(name, _make_command_handler(ctx, body), description)
|
|
2625
|
+
|
|
2626
|
+
_register_hooks(ctx)
|
|
2627
|
+
`;
|
|
2628
|
+
};
|
|
2629
|
+
var FRESH_CONFIG_YAML = `# Generated by set-prompt
|
|
2630
|
+
plugins:
|
|
2631
|
+
enabled:
|
|
2632
|
+
- ${MARKET_NAME}
|
|
2633
|
+
`;
|
|
2634
|
+
var writePluginManifest = (repoPath) => {
|
|
2635
|
+
fs13.writeFileSync(path12.join(HERMES_PLUGIN_DIR, "plugin.yaml"), PLUGIN_YAML, "utf-8");
|
|
2636
|
+
fs13.writeFileSync(path12.join(HERMES_PLUGIN_DIR, "__init__.py"), buildInitPy(repoPath), "utf-8");
|
|
2637
|
+
};
|
|
2638
|
+
var writeWithBackup = (filePath, content) => {
|
|
2639
|
+
let backupPath = null;
|
|
2640
|
+
if (fs13.existsSync(filePath)) {
|
|
2641
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2642
|
+
backupPath = `${filePath}.bak.${timestamp}`;
|
|
2643
|
+
try {
|
|
2644
|
+
fs13.copyFileSync(filePath, backupPath);
|
|
2645
|
+
} catch {
|
|
2646
|
+
backupPath = null;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
try {
|
|
2650
|
+
fs13.mkdirSync(path12.dirname(filePath), { recursive: true });
|
|
2651
|
+
fs13.writeFileSync(filePath, content, "utf-8");
|
|
2652
|
+
} catch (ex) {
|
|
2653
|
+
if (backupPath !== null) {
|
|
2654
|
+
try {
|
|
2655
|
+
fs13.copyFileSync(backupPath, filePath);
|
|
2656
|
+
fs13.unlinkSync(backupPath);
|
|
2657
|
+
console.warn(chalk14.yellow(" \u26A0 Write failed \u2014 rolled back to original."));
|
|
2658
|
+
} catch {
|
|
2659
|
+
console.error(chalk14.red(` \u274C Rollback failed. Backup preserved at: ${backupPath}`));
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
throw ex;
|
|
2663
|
+
}
|
|
2664
|
+
if (backupPath !== null) {
|
|
2665
|
+
try {
|
|
2666
|
+
fs13.unlinkSync(backupPath);
|
|
2667
|
+
} catch {
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
};
|
|
2671
|
+
var ensureHermesConfigEnabled = () => {
|
|
2672
|
+
if (fs13.existsSync(HERMES_CONFIG_PATH) === false) {
|
|
2673
|
+
fs13.mkdirSync(path12.dirname(HERMES_CONFIG_PATH), { recursive: true });
|
|
2674
|
+
fs13.writeFileSync(HERMES_CONFIG_PATH, FRESH_CONFIG_YAML, "utf-8");
|
|
2675
|
+
console.log(chalk14.green(" \u2713 ") + chalk14.dim(`wrote ${HERMES_CONFIG_PATH}`));
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const content = fs13.readFileSync(HERMES_CONFIG_PATH, "utf-8");
|
|
2679
|
+
let doc;
|
|
2680
|
+
try {
|
|
2681
|
+
doc = YAML.parseDocument(content);
|
|
2682
|
+
} catch (ex) {
|
|
2683
|
+
console.warn(chalk14.yellow(` \u26A0 Failed to parse Hermes config (${ex.message}) \u2014 leaving file untouched.`));
|
|
2684
|
+
console.log(chalk14.dim(` File: ${HERMES_CONFIG_PATH}`));
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
if (doc.errors.length > 0) {
|
|
2688
|
+
console.warn(chalk14.yellow(` \u26A0 Hermes config has YAML errors \u2014 leaving file untouched.`));
|
|
2689
|
+
console.log(chalk14.dim(` File: ${HERMES_CONFIG_PATH}`));
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
let plugins = doc.get("plugins");
|
|
2693
|
+
if (plugins == null) {
|
|
2694
|
+
doc.set("plugins", doc.createNode({ enabled: [MARKET_NAME] }));
|
|
2695
|
+
writeWithBackup(HERMES_CONFIG_PATH, String(doc));
|
|
2696
|
+
console.log(chalk14.green(" \u2713 ") + chalk14.dim(`added plugins.enabled to ${HERMES_CONFIG_PATH}`));
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
if (!YAML.isMap(plugins)) {
|
|
2700
|
+
console.warn(chalk14.yellow(` \u26A0 "plugins" is not a map \u2014 leaving file untouched.`));
|
|
2701
|
+
console.log(chalk14.dim(` File: ${HERMES_CONFIG_PATH}`));
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
let enabled = plugins.get("enabled");
|
|
2705
|
+
if (enabled == null) {
|
|
2706
|
+
plugins.set("enabled", doc.createNode([MARKET_NAME]));
|
|
2707
|
+
writeWithBackup(HERMES_CONFIG_PATH, String(doc));
|
|
2708
|
+
console.log(chalk14.green(" \u2713 ") + chalk14.dim(`added plugins.enabled to ${HERMES_CONFIG_PATH}`));
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
if (!YAML.isSeq(enabled)) {
|
|
2712
|
+
console.warn(chalk14.yellow(` \u26A0 "plugins.enabled" is not a list \u2014 leaving file untouched.`));
|
|
2713
|
+
console.log(chalk14.dim(` File: ${HERMES_CONFIG_PATH}`));
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
const alreadyListed = enabled.items.some((item) => {
|
|
2717
|
+
const v = YAML.isScalar(item) ? item.value : item;
|
|
2718
|
+
return v === MARKET_NAME;
|
|
2719
|
+
});
|
|
2720
|
+
if (alreadyListed) {
|
|
2721
|
+
console.log(chalk14.dim(` \u2713 ${MARKET_NAME} already listed in ${HERMES_CONFIG_PATH}`));
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
enabled.add(MARKET_NAME);
|
|
2725
|
+
writeWithBackup(HERMES_CONFIG_PATH, String(doc));
|
|
2726
|
+
console.log(chalk14.green(" \u2713 ") + chalk14.dim(`enabled "${MARKET_NAME}" in ${HERMES_CONFIG_PATH}`));
|
|
2727
|
+
};
|
|
2728
|
+
var linkHermes = async () => {
|
|
2729
|
+
const repoPath = resolveRepoPath();
|
|
2730
|
+
if (repoPath == null) {
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
console.log(chalk14.green(`
|
|
2734
|
+
Setting up Hermes plugin...`));
|
|
2735
|
+
console.log(chalk14.dim(HERMES_PLUGIN_DIR));
|
|
2736
|
+
try {
|
|
2737
|
+
fs13.mkdirSync(HERMES_PLUGIN_DIR, { recursive: true });
|
|
2738
|
+
writePluginManifest(repoPath);
|
|
2739
|
+
console.log(chalk14.dim(" \u251C\u2500\u2500 ") + chalk14.bold("plugin.yaml") + chalk14.green(" \u2713"));
|
|
2740
|
+
console.log(chalk14.dim(" \u2514\u2500\u2500 ") + chalk14.bold("__init__.py") + chalk14.dim(` (REPO_DIR \u2192 ${repoPath})`) + chalk14.green(" \u2713"));
|
|
2741
|
+
ensureHermesConfigEnabled();
|
|
2742
|
+
configManager.hermes = { path: HERMES_PLUGIN_DIR };
|
|
2743
|
+
configManager.save();
|
|
2744
|
+
console.log(chalk14.dim(`
|
|
2745
|
+
\u2139 Hermes loads plugins at startup \u2014 restart Hermes to pick up new skills/commands.`));
|
|
2746
|
+
} catch (ex) {
|
|
2747
|
+
console.error(chalk14.red(`\u274C Failed to set up Hermes plugin: ${ex.message}`));
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
var unlinkHermes = async (force = false) => {
|
|
2751
|
+
if (!force) {
|
|
2752
|
+
const ok = await confirm11({
|
|
2753
|
+
message: `Remove Hermes plugin dir (${HERMES_PLUGIN_DIR})?`,
|
|
2754
|
+
default: false
|
|
2755
|
+
});
|
|
2756
|
+
if (!ok) {
|
|
2757
|
+
console.log(chalk14.yellow("Cancelled."));
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
console.log(chalk14.red(`
|
|
2762
|
+
Removing Hermes plugin...`));
|
|
2763
|
+
console.log(chalk14.dim(HERMES_PLUGIN_DIR));
|
|
2764
|
+
if (fs13.existsSync(HERMES_PLUGIN_DIR)) {
|
|
2765
|
+
fs13.rmSync(HERMES_PLUGIN_DIR, { recursive: true, force: true });
|
|
2766
|
+
console.log(chalk14.red(" removed") + chalk14.dim(`: ${HERMES_PLUGIN_DIR}`));
|
|
2767
|
+
}
|
|
2768
|
+
if (fs13.existsSync(HERMES_CONFIG_PATH)) {
|
|
2769
|
+
const content = fs13.readFileSync(HERMES_CONFIG_PATH, "utf-8");
|
|
2770
|
+
if (content.trimStart().startsWith("# Generated by set-prompt")) {
|
|
2771
|
+
fs13.unlinkSync(HERMES_CONFIG_PATH);
|
|
2772
|
+
console.log(chalk14.red(" removed") + chalk14.dim(`: ${HERMES_CONFIG_PATH}`));
|
|
2773
|
+
} else {
|
|
2774
|
+
removeMarketEntryFromConfig();
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
configManager.hermes = null;
|
|
2778
|
+
configManager.save();
|
|
2779
|
+
};
|
|
2780
|
+
var removeMarketEntryFromConfig = () => {
|
|
2781
|
+
const content = fs13.readFileSync(HERMES_CONFIG_PATH, "utf-8");
|
|
2782
|
+
let doc;
|
|
2783
|
+
try {
|
|
2784
|
+
doc = YAML.parseDocument(content);
|
|
2785
|
+
} catch (ex) {
|
|
2786
|
+
console.warn(chalk14.yellow(` \u26A0 Failed to parse Hermes config (${ex.message}) \u2014 leaving file untouched.`));
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (doc.errors.length > 0) {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
const plugins = doc.get("plugins");
|
|
2793
|
+
if (!YAML.isMap(plugins)) {
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
const enabled = plugins.get("enabled");
|
|
2797
|
+
if (!YAML.isSeq(enabled)) {
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
const idx = enabled.items.findIndex((item) => {
|
|
2801
|
+
const v = YAML.isScalar(item) ? item.value : item;
|
|
2802
|
+
return v === MARKET_NAME;
|
|
2803
|
+
});
|
|
2804
|
+
if (idx === -1) {
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
enabled.delete(idx);
|
|
2808
|
+
writeWithBackup(HERMES_CONFIG_PATH, String(doc));
|
|
2809
|
+
console.log(chalk14.red(" removed") + chalk14.dim(` "${MARKET_NAME}" from: ${HERMES_CONFIG_PATH}`));
|
|
2810
|
+
};
|
|
2811
|
+
|
|
2369
2812
|
// src/commands/link-command.ts
|
|
2370
2813
|
var LINK_MAP = {
|
|
2371
2814
|
["claudecode" /* CLAUDECODE */]: linkClaudeCode,
|
|
@@ -2375,7 +2818,8 @@ var LINK_MAP = {
|
|
|
2375
2818
|
["antigravity" /* ANTIGRAVITY */]: linkAntigravity,
|
|
2376
2819
|
["cursor" /* CURSOR */]: linkCursor,
|
|
2377
2820
|
["opencode" /* OPENCODE */]: linkOpencode,
|
|
2378
|
-
["geminicli" /* GEMINICLI */]: linkGeminicli
|
|
2821
|
+
["geminicli" /* GEMINICLI */]: linkGeminicli,
|
|
2822
|
+
["hermes" /* HERMES */]: linkHermes
|
|
2379
2823
|
};
|
|
2380
2824
|
var UNLINK_MAP = {
|
|
2381
2825
|
["claudecode" /* CLAUDECODE */]: unlinkClaudeCode,
|
|
@@ -2385,13 +2829,14 @@ var UNLINK_MAP = {
|
|
|
2385
2829
|
["antigravity" /* ANTIGRAVITY */]: unlinkAntigravity,
|
|
2386
2830
|
["cursor" /* CURSOR */]: unlinkCursor,
|
|
2387
2831
|
["opencode" /* OPENCODE */]: unlinkOpencode,
|
|
2388
|
-
["geminicli" /* GEMINICLI */]: unlinkGeminicli
|
|
2832
|
+
["geminicli" /* GEMINICLI */]: unlinkGeminicli,
|
|
2833
|
+
["hermes" /* HERMES */]: unlinkHermes
|
|
2389
2834
|
};
|
|
2390
2835
|
var linkCommand = async (tool) => {
|
|
2391
2836
|
if (tool != null) {
|
|
2392
2837
|
const known = ALL_AGENTS.some((a) => a.value === tool);
|
|
2393
2838
|
if (!known) {
|
|
2394
|
-
console.log(
|
|
2839
|
+
console.log(chalk15.red(`Unknown vendor: ${tool}`));
|
|
2395
2840
|
process.exit(1);
|
|
2396
2841
|
}
|
|
2397
2842
|
await LINK_MAP[tool]();
|
|
@@ -2405,12 +2850,13 @@ var linkCommand = async (tool) => {
|
|
|
2405
2850
|
["antigravity" /* ANTIGRAVITY */]: configManager.isAntigravityEnabled(),
|
|
2406
2851
|
["cursor" /* CURSOR */]: configManager.isCursorEnabled(),
|
|
2407
2852
|
["opencode" /* OPENCODE */]: configManager.isOpencodeEnabled(),
|
|
2408
|
-
["geminicli" /* GEMINICLI */]: configManager.isGeminicliEnabled()
|
|
2853
|
+
["geminicli" /* GEMINICLI */]: configManager.isGeminicliEnabled(),
|
|
2854
|
+
["hermes" /* HERMES */]: configManager.isHermesEnabled()
|
|
2409
2855
|
};
|
|
2410
2856
|
const selected = await checkbox({
|
|
2411
2857
|
message: "Which AI agent do you want to integrate?",
|
|
2412
2858
|
choices: ALL_AGENTS.map((a) => ({
|
|
2413
|
-
name: prevLinked[a.value] ? `${a.name} ${
|
|
2859
|
+
name: prevLinked[a.value] ? `${a.name} ${chalk15.dim("(applied)")}` : a.name,
|
|
2414
2860
|
value: a.value,
|
|
2415
2861
|
checked: prevLinked[a.value]
|
|
2416
2862
|
})),
|
|
@@ -2421,10 +2867,10 @@ var linkCommand = async (tool) => {
|
|
|
2421
2867
|
const toUnlink = ALL_AGENTS.filter((a) => prevLinked[a.value] && !selected.includes(a.value));
|
|
2422
2868
|
console.log();
|
|
2423
2869
|
if (toLink.length > 0) {
|
|
2424
|
-
console.log(
|
|
2870
|
+
console.log(chalk15.green(" Link ") + chalk15.dim("\u2192 ") + toLink.map((a) => chalk15.bold(a.name)).join(chalk15.dim(", ")));
|
|
2425
2871
|
}
|
|
2426
2872
|
if (toUnlink.length > 0) {
|
|
2427
|
-
console.log(
|
|
2873
|
+
console.log(chalk15.red(" Unlink ") + chalk15.dim("\u2192 ") + toUnlink.map((a) => chalk15.bold(a.name)).join(chalk15.dim(", ")));
|
|
2428
2874
|
}
|
|
2429
2875
|
console.log();
|
|
2430
2876
|
if (toLink.length === 0 && toUnlink.length === 0) {
|
|
@@ -2443,47 +2889,59 @@ var linkCommand = async (tool) => {
|
|
|
2443
2889
|
};
|
|
2444
2890
|
|
|
2445
2891
|
// src/commands/uninstall-command.ts
|
|
2446
|
-
import
|
|
2447
|
-
import
|
|
2448
|
-
import { confirm as
|
|
2892
|
+
import fs14 from "fs";
|
|
2893
|
+
import chalk16 from "chalk";
|
|
2894
|
+
import { confirm as confirm12 } from "@inquirer/prompts";
|
|
2449
2895
|
var uninstallCommand = async () => {
|
|
2450
2896
|
const targets = [
|
|
2451
|
-
{ label: `Config file ${
|
|
2452
|
-
{ label: `Home dir ${
|
|
2453
|
-
].filter((t) =>
|
|
2897
|
+
{ label: `Config file ${chalk16.dim(CONFIG_PATH)}`, path: CONFIG_PATH },
|
|
2898
|
+
{ label: `Home dir ${chalk16.dim(HOME_DIR)}`, path: HOME_DIR }
|
|
2899
|
+
].filter((t) => fs14.existsSync(t.path));
|
|
2454
2900
|
const hasClaudeCode = configManager.isClaudeCodeEnabled();
|
|
2455
2901
|
const hasRooCode = configManager.isRooCodeEnabled();
|
|
2456
2902
|
const hasOpenclaw = configManager.isOpenclawEnabled();
|
|
2457
2903
|
const hasAntigravity = configManager.isAntigravityEnabled();
|
|
2458
2904
|
const hasCodex = configManager.isCodexEnabled();
|
|
2459
2905
|
const hasCursor = configManager.isCursorEnabled();
|
|
2460
|
-
|
|
2461
|
-
|
|
2906
|
+
const hasOpencode = configManager.isOpencodeEnabled();
|
|
2907
|
+
const hasGeminicli = configManager.isGeminicliEnabled();
|
|
2908
|
+
const hasHermes = configManager.isHermesEnabled();
|
|
2909
|
+
if (targets.length === 0 && !hasClaudeCode && !hasRooCode && !hasOpenclaw && !hasAntigravity && !hasCodex && !hasCursor && !hasOpencode && !hasGeminicli && !hasHermes) {
|
|
2910
|
+
console.log(chalk16.yellow("Nothing to remove."));
|
|
2462
2911
|
return;
|
|
2463
2912
|
}
|
|
2464
|
-
console.log(
|
|
2913
|
+
console.log(chalk16.red("\nThe following will be removed:"));
|
|
2465
2914
|
targets.forEach((t) => console.log(` ${t.label}`));
|
|
2466
2915
|
if (hasClaudeCode) {
|
|
2467
|
-
console.log(` Claude Code plugin dir ${
|
|
2916
|
+
console.log(` Claude Code plugin dir ${chalk16.dim(CLAUDE_CODE_DIR)}`);
|
|
2468
2917
|
}
|
|
2469
2918
|
if (hasRooCode) {
|
|
2470
|
-
console.log(` RooCode symlinks ${
|
|
2919
|
+
console.log(` RooCode symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2471
2920
|
}
|
|
2472
2921
|
if (hasOpenclaw) {
|
|
2473
|
-
console.log(` OpenClaw symlinks ${
|
|
2922
|
+
console.log(` OpenClaw symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2474
2923
|
}
|
|
2475
2924
|
if (hasAntigravity) {
|
|
2476
|
-
console.log(` Antigravity symlinks ${
|
|
2925
|
+
console.log(` Antigravity symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2477
2926
|
}
|
|
2478
2927
|
if (hasCodex) {
|
|
2479
|
-
console.log(` Codex symlinks ${
|
|
2928
|
+
console.log(` Codex symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2480
2929
|
}
|
|
2481
2930
|
if (hasCursor) {
|
|
2482
|
-
console.log(` Cursor plugin dir ${
|
|
2931
|
+
console.log(` Cursor plugin dir ${chalk16.dim("(symlink will be removed)")}`);
|
|
2483
2932
|
}
|
|
2484
|
-
|
|
2933
|
+
if (hasOpencode) {
|
|
2934
|
+
console.log(` OpenCode symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2935
|
+
}
|
|
2936
|
+
if (hasGeminicli) {
|
|
2937
|
+
console.log(` Gemini CLI symlinks ${chalk16.dim("(backup will be restored)")}`);
|
|
2938
|
+
}
|
|
2939
|
+
if (hasHermes) {
|
|
2940
|
+
console.log(` Hermes plugin dir ${chalk16.dim("(plugin folder will be removed)")}`);
|
|
2941
|
+
}
|
|
2942
|
+
const ok = await confirm12({ message: "Proceed?", default: false });
|
|
2485
2943
|
if (!ok) {
|
|
2486
|
-
console.log(
|
|
2944
|
+
console.log(chalk16.yellow("Cancelled."));
|
|
2487
2945
|
return;
|
|
2488
2946
|
}
|
|
2489
2947
|
if (hasClaudeCode) {
|
|
@@ -2504,27 +2962,36 @@ var uninstallCommand = async () => {
|
|
|
2504
2962
|
if (hasCursor) {
|
|
2505
2963
|
await unlinkCursor(true);
|
|
2506
2964
|
}
|
|
2965
|
+
if (hasOpencode) {
|
|
2966
|
+
await unlinkOpencode(true);
|
|
2967
|
+
}
|
|
2968
|
+
if (hasGeminicli) {
|
|
2969
|
+
await unlinkGeminicli(true);
|
|
2970
|
+
}
|
|
2971
|
+
if (hasHermes) {
|
|
2972
|
+
await unlinkHermes(true);
|
|
2973
|
+
}
|
|
2507
2974
|
for (const t of targets) {
|
|
2508
|
-
|
|
2509
|
-
console.log(
|
|
2975
|
+
fs14.rmSync(t.path, { recursive: true, force: true });
|
|
2976
|
+
console.log(chalk16.dim(` removed: ${t.path}`));
|
|
2510
2977
|
}
|
|
2511
|
-
console.log(
|
|
2978
|
+
console.log(chalk16.green("\nUninstalled."));
|
|
2512
2979
|
};
|
|
2513
2980
|
|
|
2514
2981
|
// src/commands/status-command.ts
|
|
2515
|
-
import
|
|
2982
|
+
import chalk17 from "chalk";
|
|
2516
2983
|
var statusCommand = () => {
|
|
2517
2984
|
if (configManager.repo_path == null) {
|
|
2518
|
-
console.log(
|
|
2519
|
-
console.log(
|
|
2985
|
+
console.log(chalk17.yellow("\u274C No repo installed."));
|
|
2986
|
+
console.log(chalk17.dim(` Run: set-prompt install <repo-url>`));
|
|
2520
2987
|
return;
|
|
2521
2988
|
}
|
|
2522
|
-
console.log(
|
|
2523
|
-
console.log(`${TAB}path ${
|
|
2989
|
+
console.log(chalk17.bold("\nRepo"));
|
|
2990
|
+
console.log(`${TAB}path ${chalk17.cyan(configManager.repo_path)}`);
|
|
2524
2991
|
if (configManager.remote_url != null) {
|
|
2525
|
-
console.log(`${TAB}remote ${
|
|
2992
|
+
console.log(`${TAB}remote ${chalk17.dim(configManager.remote_url)}`);
|
|
2526
2993
|
}
|
|
2527
|
-
console.log(
|
|
2994
|
+
console.log(chalk17.bold("\nLinked agents"));
|
|
2528
2995
|
for (const agent of ALL_AGENTS) {
|
|
2529
2996
|
let linked = false;
|
|
2530
2997
|
let agentPath = null;
|
|
@@ -2543,9 +3010,21 @@ var statusCommand = () => {
|
|
|
2543
3010
|
} else if (agent.value === "antigravity" /* ANTIGRAVITY */) {
|
|
2544
3011
|
linked = configManager.isAntigravityEnabled();
|
|
2545
3012
|
agentPath = configManager.antigravity?.path;
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2548
|
-
|
|
3013
|
+
} else if (agent.value === "cursor" /* CURSOR */) {
|
|
3014
|
+
linked = configManager.isCursorEnabled();
|
|
3015
|
+
agentPath = configManager.cursor?.path;
|
|
3016
|
+
} else if (agent.value === "opencode" /* OPENCODE */) {
|
|
3017
|
+
linked = configManager.isOpencodeEnabled();
|
|
3018
|
+
agentPath = configManager.opencode?.path;
|
|
3019
|
+
} else if (agent.value === "geminicli" /* GEMINICLI */) {
|
|
3020
|
+
linked = configManager.isGeminicliEnabled();
|
|
3021
|
+
agentPath = configManager.geminicli?.path;
|
|
3022
|
+
} else if (agent.value === "hermes" /* HERMES */) {
|
|
3023
|
+
linked = configManager.isHermesEnabled();
|
|
3024
|
+
agentPath = configManager.hermes?.path;
|
|
3025
|
+
}
|
|
3026
|
+
const label = linked ? chalk17.green("linked") : chalk17.dim("not linked");
|
|
3027
|
+
const pathStr = linked && agentPath ? chalk17.dim(` \u2192 ${agentPath}`) : "";
|
|
2549
3028
|
console.log(`${TAB}${agent.name.padEnd(12)} ${label}${pathStr}`);
|
|
2550
3029
|
}
|
|
2551
3030
|
console.log("");
|
|
@@ -2553,92 +3032,92 @@ var statusCommand = () => {
|
|
|
2553
3032
|
|
|
2554
3033
|
// src/commands/repo/pull-command.ts
|
|
2555
3034
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2556
|
-
import
|
|
3035
|
+
import chalk18 from "chalk";
|
|
2557
3036
|
var repoPullCommand = () => {
|
|
2558
3037
|
const repoPath = configManager.repo_path;
|
|
2559
3038
|
if (repoPath == null) {
|
|
2560
|
-
console.error(
|
|
2561
|
-
console.log(
|
|
3039
|
+
console.error(chalk18.red("\u274C No repo installed."));
|
|
3040
|
+
console.log(chalk18.yellow("Run: set-prompt install <git-url>"));
|
|
2562
3041
|
return;
|
|
2563
3042
|
}
|
|
2564
3043
|
if (configManager.remote_url == null) {
|
|
2565
|
-
console.error(
|
|
3044
|
+
console.error(chalk18.red("\u274C No remote URL registered. Cannot pull."));
|
|
2566
3045
|
return;
|
|
2567
3046
|
}
|
|
2568
|
-
console.log(
|
|
2569
|
-
console.log(
|
|
3047
|
+
console.log(chalk18.green("\nPulling prompt repo..."));
|
|
3048
|
+
console.log(chalk18.dim(repoPath));
|
|
2570
3049
|
const fetch = spawnSync4("git", ["fetch"], { cwd: repoPath, stdio: "inherit" });
|
|
2571
3050
|
if (fetch.status !== 0) {
|
|
2572
|
-
console.error(
|
|
3051
|
+
console.error(chalk18.red("\u274C git fetch failed."));
|
|
2573
3052
|
return;
|
|
2574
3053
|
}
|
|
2575
3054
|
const pull = spawnSync4("git", ["pull"], { cwd: repoPath, stdio: "inherit" });
|
|
2576
3055
|
if (pull.status !== 0) {
|
|
2577
|
-
console.error(
|
|
3056
|
+
console.error(chalk18.red("\u274C git pull failed."));
|
|
2578
3057
|
return;
|
|
2579
3058
|
}
|
|
2580
|
-
console.log(
|
|
3059
|
+
console.log(chalk18.green("\u2705 Repo pulled."));
|
|
2581
3060
|
};
|
|
2582
3061
|
|
|
2583
3062
|
// src/commands/repo/commit-command.ts
|
|
2584
3063
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
2585
|
-
import
|
|
3064
|
+
import chalk19 from "chalk";
|
|
2586
3065
|
var repoCommitCommand = (options = {}) => {
|
|
2587
3066
|
const repoPath = configManager.repo_path;
|
|
2588
3067
|
if (repoPath == null) {
|
|
2589
|
-
console.error(
|
|
2590
|
-
console.log(
|
|
3068
|
+
console.error(chalk19.red("\u274C No repo installed."));
|
|
3069
|
+
console.log(chalk19.yellow("Run: set-prompt install <git-url>"));
|
|
2591
3070
|
return false;
|
|
2592
3071
|
}
|
|
2593
3072
|
let message = options.message;
|
|
2594
3073
|
if (message == null || message.trim() === "") {
|
|
2595
3074
|
const generated = generateCommitMessage(repoPath);
|
|
2596
3075
|
if (generated == null) {
|
|
2597
|
-
console.error(
|
|
3076
|
+
console.error(chalk19.red("\u274C Nothing to commit \u2014 working tree is clean."));
|
|
2598
3077
|
return false;
|
|
2599
3078
|
}
|
|
2600
3079
|
message = generated;
|
|
2601
3080
|
const subject = message.split("\n")[0];
|
|
2602
|
-
console.log(
|
|
3081
|
+
console.log(chalk19.dim(` (auto-generated: ${subject})`));
|
|
2603
3082
|
}
|
|
2604
|
-
console.log(
|
|
2605
|
-
console.log(
|
|
3083
|
+
console.log(chalk19.green("\nCommitting prompt repo changes..."));
|
|
3084
|
+
console.log(chalk19.dim(repoPath));
|
|
2606
3085
|
const add = spawnSync5("git", ["add", "-A"], { cwd: repoPath, stdio: "inherit" });
|
|
2607
3086
|
if (add.status !== 0) {
|
|
2608
|
-
console.error(
|
|
3087
|
+
console.error(chalk19.red("\u274C git add failed."));
|
|
2609
3088
|
return false;
|
|
2610
3089
|
}
|
|
2611
3090
|
const commit = spawnSync5("git", ["commit", "-m", message], { cwd: repoPath, stdio: "inherit" });
|
|
2612
3091
|
if (commit.status !== 0) {
|
|
2613
|
-
console.error(
|
|
3092
|
+
console.error(chalk19.red("\u274C git commit failed \u2014 nothing to commit, or commit rejected."));
|
|
2614
3093
|
return false;
|
|
2615
3094
|
}
|
|
2616
|
-
console.log(
|
|
3095
|
+
console.log(chalk19.green("\u2705 Committed."));
|
|
2617
3096
|
return true;
|
|
2618
3097
|
};
|
|
2619
3098
|
|
|
2620
3099
|
// src/commands/repo/push-command.ts
|
|
2621
3100
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
2622
|
-
import
|
|
3101
|
+
import chalk20 from "chalk";
|
|
2623
3102
|
var repoPushCommand = () => {
|
|
2624
3103
|
const repoPath = configManager.repo_path;
|
|
2625
3104
|
if (repoPath == null) {
|
|
2626
|
-
console.error(
|
|
2627
|
-
console.log(
|
|
3105
|
+
console.error(chalk20.red("\u274C No repo installed."));
|
|
3106
|
+
console.log(chalk20.yellow("Run: set-prompt install <git-url>"));
|
|
2628
3107
|
return false;
|
|
2629
3108
|
}
|
|
2630
3109
|
if (configManager.remote_url == null) {
|
|
2631
|
-
console.error(
|
|
3110
|
+
console.error(chalk20.red("\u274C No remote URL registered. Cannot push."));
|
|
2632
3111
|
return false;
|
|
2633
3112
|
}
|
|
2634
|
-
console.log(
|
|
2635
|
-
console.log(
|
|
3113
|
+
console.log(chalk20.green("\nPushing prompt repo..."));
|
|
3114
|
+
console.log(chalk20.dim(repoPath));
|
|
2636
3115
|
const push = spawnSync6("git", ["push"], { cwd: repoPath, stdio: "inherit" });
|
|
2637
3116
|
if (push.status !== 0) {
|
|
2638
|
-
console.error(
|
|
3117
|
+
console.error(chalk20.red("\u274C git push failed."));
|
|
2639
3118
|
return false;
|
|
2640
3119
|
}
|
|
2641
|
-
console.log(
|
|
3120
|
+
console.log(chalk20.green("\u2705 Pushed."));
|
|
2642
3121
|
return true;
|
|
2643
3122
|
};
|
|
2644
3123
|
|
|
@@ -2651,7 +3130,7 @@ var repoSaveCommand = (options = {}) => {
|
|
|
2651
3130
|
|
|
2652
3131
|
// src/commands/repo/status-command.ts
|
|
2653
3132
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
2654
|
-
import
|
|
3133
|
+
import chalk21 from "chalk";
|
|
2655
3134
|
var parseBranchLine = (line) => {
|
|
2656
3135
|
const body = line.replace(/^## /, "");
|
|
2657
3136
|
if (body.startsWith("HEAD ") || body.includes("(no branch)")) {
|
|
@@ -2679,27 +3158,27 @@ var parseFileLine = (line) => {
|
|
|
2679
3158
|
const arrowIdx = name.indexOf(" -> ");
|
|
2680
3159
|
if (arrowIdx >= 0) name = name.slice(arrowIdx + 4);
|
|
2681
3160
|
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:
|
|
3161
|
+
if (status.includes("?")) return { label: "untracked", color: chalk21.gray, path: name };
|
|
3162
|
+
if (status.includes("D")) return { label: "deleted", color: chalk21.red, path: name };
|
|
3163
|
+
if (status.includes("R")) return { label: "renamed", color: chalk21.cyan, path: name };
|
|
3164
|
+
if (status.includes("A")) return { label: "added", color: chalk21.green, path: name };
|
|
3165
|
+
if (status.includes("M")) return { label: "modified", color: chalk21.yellow, path: name };
|
|
2687
3166
|
return null;
|
|
2688
3167
|
};
|
|
2689
3168
|
var formatUpstream = (info) => {
|
|
2690
|
-
if (info.branch == null) return
|
|
2691
|
-
if (info.upstream == null) return `${info.branch} ${
|
|
3169
|
+
if (info.branch == null) return chalk21.red("(detached HEAD)");
|
|
3170
|
+
if (info.upstream == null) return `${info.branch} ${chalk21.yellow("(no upstream)")}`;
|
|
2692
3171
|
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} ${
|
|
3172
|
+
if (info.ahead > 0) segs.push(chalk21.green(`ahead ${info.ahead}`));
|
|
3173
|
+
if (info.behind > 0) segs.push(chalk21.red(`behind ${info.behind}`));
|
|
3174
|
+
const trailing = segs.length > 0 ? ` (${segs.join(", ")})` : chalk21.dim(" (up to date)");
|
|
3175
|
+
return `${info.branch} ${chalk21.dim("\u2192")} ${info.upstream}${trailing}`;
|
|
2697
3176
|
};
|
|
2698
3177
|
var repoStatusCommand = () => {
|
|
2699
3178
|
const repoPath = configManager.repo_path;
|
|
2700
3179
|
if (repoPath == null) {
|
|
2701
|
-
console.error(
|
|
2702
|
-
console.log(
|
|
3180
|
+
console.error(chalk21.red("\u274C No repo installed."));
|
|
3181
|
+
console.log(chalk21.yellow("Run: set-prompt install <git-url>"));
|
|
2703
3182
|
return;
|
|
2704
3183
|
}
|
|
2705
3184
|
const result = spawnSync7("git", ["status", "--porcelain=v1", "--branch", "--untracked-files=all"], {
|
|
@@ -2707,8 +3186,8 @@ var repoStatusCommand = () => {
|
|
|
2707
3186
|
encoding: "utf8"
|
|
2708
3187
|
});
|
|
2709
3188
|
if (result.status !== 0) {
|
|
2710
|
-
console.error(
|
|
2711
|
-
if (result.stderr) console.error(
|
|
3189
|
+
console.error(chalk21.red("\u274C git status failed."));
|
|
3190
|
+
if (result.stderr) console.error(chalk21.dim(result.stderr));
|
|
2712
3191
|
return;
|
|
2713
3192
|
}
|
|
2714
3193
|
const lines = result.stdout.split("\n").filter((l) => l.length > 0);
|
|
@@ -2716,14 +3195,14 @@ var repoStatusCommand = () => {
|
|
|
2716
3195
|
const fileLines = lines.slice(1);
|
|
2717
3196
|
const branchInfo = parseBranchLine(branchLine);
|
|
2718
3197
|
const changes = fileLines.map(parseFileLine).filter((f) => f !== null);
|
|
2719
|
-
console.log(`${
|
|
2720
|
-
console.log(`${
|
|
3198
|
+
console.log(`${chalk21.cyan("\u{1F4C2}")} ${chalk21.dim(repoPath)}`);
|
|
3199
|
+
console.log(`${chalk21.cyan("\u{1F33F}")} ${formatUpstream(branchInfo)}`);
|
|
2721
3200
|
console.log("");
|
|
2722
3201
|
if (changes.length === 0) {
|
|
2723
|
-
console.log(
|
|
3202
|
+
console.log(chalk21.green("\u2705 Working tree clean"));
|
|
2724
3203
|
return;
|
|
2725
3204
|
}
|
|
2726
|
-
console.log(
|
|
3205
|
+
console.log(chalk21.bold(`\u{1F4DD} Changes (${changes.length}):`));
|
|
2727
3206
|
const labelWidth = Math.max(...changes.map((c) => c.label.length));
|
|
2728
3207
|
for (const c of changes) {
|
|
2729
3208
|
const label = c.color(c.label.padEnd(labelWidth));
|
|
@@ -2732,12 +3211,12 @@ var repoStatusCommand = () => {
|
|
|
2732
3211
|
};
|
|
2733
3212
|
|
|
2734
3213
|
// src/commands/repo/path-command.ts
|
|
2735
|
-
import
|
|
3214
|
+
import chalk22 from "chalk";
|
|
2736
3215
|
var repoPathCommand = () => {
|
|
2737
3216
|
const repoPath = configManager.repo_path;
|
|
2738
3217
|
if (repoPath == null) {
|
|
2739
|
-
console.error(
|
|
2740
|
-
console.error(
|
|
3218
|
+
console.error(chalk22.red("\u274C No repo installed."));
|
|
3219
|
+
console.error(chalk22.yellow("Run: set-prompt install <git-url>"));
|
|
2741
3220
|
process.exitCode = 1;
|
|
2742
3221
|
return;
|
|
2743
3222
|
}
|
|
@@ -2746,8 +3225,8 @@ var repoPathCommand = () => {
|
|
|
2746
3225
|
|
|
2747
3226
|
// src/commands/repo/open-command.ts
|
|
2748
3227
|
import { spawn } from "child_process";
|
|
2749
|
-
import
|
|
2750
|
-
import
|
|
3228
|
+
import path13 from "path";
|
|
3229
|
+
import chalk23 from "chalk";
|
|
2751
3230
|
var resolveVscodeTarget = (repoPath) => {
|
|
2752
3231
|
if (isOnPath("code") === false) return null;
|
|
2753
3232
|
return { bin: "code", args: [repoPath] };
|
|
@@ -2758,9 +3237,9 @@ var resolveSourcetreeTarget = (repoPath) => {
|
|
|
2758
3237
|
}
|
|
2759
3238
|
if (process.platform === "win32") {
|
|
2760
3239
|
const exe = firstExistingPath([
|
|
2761
|
-
process.env.LOCALAPPDATA &&
|
|
2762
|
-
process.env["ProgramFiles(x86)"] &&
|
|
2763
|
-
process.env.ProgramFiles &&
|
|
3240
|
+
process.env.LOCALAPPDATA && path13.join(process.env.LOCALAPPDATA, "SourceTree", "SourceTree.exe"),
|
|
3241
|
+
process.env["ProgramFiles(x86)"] && path13.join(process.env["ProgramFiles(x86)"], "Atlassian", "SourceTree", "SourceTree.exe"),
|
|
3242
|
+
process.env.ProgramFiles && path13.join(process.env.ProgramFiles, "Atlassian", "SourceTree", "SourceTree.exe")
|
|
2764
3243
|
]);
|
|
2765
3244
|
if (exe != null) return { bin: exe, args: ["-f", repoPath] };
|
|
2766
3245
|
}
|
|
@@ -2777,22 +3256,22 @@ var sourcetreeInstallHint = () => {
|
|
|
2777
3256
|
return "Sourcetree is not available on Linux \u2014 try a native Git GUI instead";
|
|
2778
3257
|
};
|
|
2779
3258
|
var runLaunch = (label, target) => {
|
|
2780
|
-
console.log(
|
|
3259
|
+
console.log(chalk23.green(`Opening in ${label}: ${chalk23.dim(target.args[target.args.length - 1])}`));
|
|
2781
3260
|
const child = spawn(target.bin, target.args, { stdio: "ignore", detached: true, shell: true });
|
|
2782
3261
|
child.unref();
|
|
2783
3262
|
};
|
|
2784
3263
|
var repoOpenCommand = (options = {}) => {
|
|
2785
3264
|
const repoPath = configManager.repo_path;
|
|
2786
3265
|
if (repoPath == null) {
|
|
2787
|
-
console.error(
|
|
2788
|
-
console.log(
|
|
3266
|
+
console.error(chalk23.red("\u274C No repo installed."));
|
|
3267
|
+
console.log(chalk23.yellow("Run: set-prompt install <git-url>"));
|
|
2789
3268
|
return;
|
|
2790
3269
|
}
|
|
2791
3270
|
if (options.code === true) {
|
|
2792
3271
|
const target = resolveVscodeTarget(repoPath);
|
|
2793
3272
|
if (target == null) {
|
|
2794
|
-
console.error(
|
|
2795
|
-
console.log(
|
|
3273
|
+
console.error(chalk23.red("\u274C VSCode CLI (`code`) not found on PATH."));
|
|
3274
|
+
console.log(chalk23.dim(` ${VSCODE_INSTALL_HINT}`));
|
|
2796
3275
|
return;
|
|
2797
3276
|
}
|
|
2798
3277
|
runLaunch("VSCode", target);
|
|
@@ -2801,8 +3280,8 @@ var repoOpenCommand = (options = {}) => {
|
|
|
2801
3280
|
if (options.stree === true) {
|
|
2802
3281
|
const target = resolveSourcetreeTarget(repoPath);
|
|
2803
3282
|
if (target == null) {
|
|
2804
|
-
console.error(
|
|
2805
|
-
console.log(
|
|
3283
|
+
console.error(chalk23.red("\u274C Sourcetree not found."));
|
|
3284
|
+
console.log(chalk23.dim(` ${sourcetreeInstallHint()}`));
|
|
2806
3285
|
return;
|
|
2807
3286
|
}
|
|
2808
3287
|
runLaunch("Sourcetree", target);
|
|
@@ -2810,46 +3289,46 @@ var repoOpenCommand = (options = {}) => {
|
|
|
2810
3289
|
}
|
|
2811
3290
|
const platform = process.platform;
|
|
2812
3291
|
const opener = platform === "win32" ? "explorer" : platform === "darwin" ? "open" : "xdg-open";
|
|
2813
|
-
console.log(
|
|
3292
|
+
console.log(chalk23.green(`Opening: ${chalk23.dim(repoPath)}`));
|
|
2814
3293
|
const child = spawn(opener, [repoPath], { stdio: "ignore", detached: true });
|
|
2815
3294
|
child.on("error", (ex) => {
|
|
2816
|
-
console.error(
|
|
3295
|
+
console.error(chalk23.red(`\u274C Failed to open: ${ex.message}`));
|
|
2817
3296
|
});
|
|
2818
3297
|
child.unref();
|
|
2819
3298
|
};
|
|
2820
3299
|
|
|
2821
3300
|
// src/index.ts
|
|
2822
3301
|
process.on("SIGINT", () => {
|
|
2823
|
-
console.log(
|
|
3302
|
+
console.log(chalk24.yellow("\nCancelled."));
|
|
2824
3303
|
process.exit(0);
|
|
2825
3304
|
});
|
|
2826
3305
|
process.on("unhandledRejection", (reason) => {
|
|
2827
3306
|
if (reason instanceof Error && reason.name === "ExitPromptError") {
|
|
2828
|
-
console.log(
|
|
3307
|
+
console.log(chalk24.yellow("\nCancelled."));
|
|
2829
3308
|
process.exit(0);
|
|
2830
3309
|
}
|
|
2831
3310
|
throw reason;
|
|
2832
3311
|
});
|
|
2833
|
-
var __dirname =
|
|
2834
|
-
var pkg = JSON.parse(
|
|
3312
|
+
var __dirname = path14.dirname(fileURLToPath(import.meta.url));
|
|
3313
|
+
var pkg = JSON.parse(fs15.readFileSync(path14.join(__dirname, "../package.json"), "utf-8"));
|
|
2835
3314
|
configManager.init();
|
|
2836
3315
|
var program = new Command();
|
|
2837
|
-
var banner =
|
|
3316
|
+
var banner = chalk24.cyan(figlet.textSync("Set-Prompt", { horizontalLayout: "full" }));
|
|
2838
3317
|
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 ${
|
|
3318
|
+
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
3319
|
await installCommand(source);
|
|
2841
3320
|
});
|
|
2842
|
-
program.command("link").description(`\u{1F517} Symlink your prompt repo into an ${
|
|
3321
|
+
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
3322
|
await linkCommand(agent);
|
|
2844
3323
|
});
|
|
2845
|
-
program.command("scaffold").description(`\u{1F6E0}\uFE0F Verify and create ${
|
|
3324
|
+
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
3325
|
await scaffoldCommand(localPath);
|
|
2847
3326
|
});
|
|
2848
|
-
program.command("status").description(`\u{1F4CB} Show registered ${
|
|
3327
|
+
program.command("status").description(`\u{1F4CB} Show registered ${chalk24.cyan("repo")} and which ${chalk24.cyan("agents")} are linked`).action(() => {
|
|
2849
3328
|
statusCommand();
|
|
2850
3329
|
});
|
|
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 ${
|
|
3330
|
+
var repo = program.command("repo").description(`\u{1F5C2}\uFE0F Manage the installed prompt repo ${chalk24.dim("(status | pull/update | commit | push | save | path | open)")}`);
|
|
3331
|
+
repo.command("status").description(`\u{1F4CB} Show VCS status of the repo ${chalk24.dim("(branch, ahead/behind, changed files)")}`).addHelpText("after", `
|
|
2853
3332
|
Example output:
|
|
2854
3333
|
\u{1F4C2} ~/.set-prompt/repo
|
|
2855
3334
|
\u{1F33F} main \u2192 origin/main (ahead 2)
|
|
@@ -2860,47 +3339,47 @@ Example output:
|
|
|
2860
3339
|
`).action(() => {
|
|
2861
3340
|
repoStatusCommand();
|
|
2862
3341
|
});
|
|
2863
|
-
repo.command("pull").description(`\u{1F504} Fetch and pull the latest changes from the ${
|
|
3342
|
+
repo.command("pull").alias("update").description(`\u{1F504} Fetch and pull the latest changes from the ${chalk24.cyan("remote repo")}`).action(() => {
|
|
2864
3343
|
repoPullCommand();
|
|
2865
3344
|
});
|
|
2866
|
-
repo.command("commit").description(`\u{1F4DD} Stage all changes and commit ${
|
|
3345
|
+
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
3346
|
Examples:
|
|
2868
3347
|
$ sppt repo commit -m "edit dbml skill"
|
|
2869
|
-
$ sppt repo commit ${
|
|
3348
|
+
$ sppt repo commit ${chalk24.dim('# auto-generates "update N files" + file list')}
|
|
2870
3349
|
`).action((opts) => {
|
|
2871
3350
|
repoCommitCommand({ message: opts.message });
|
|
2872
3351
|
});
|
|
2873
3352
|
repo.command("push").description(`\u2B06\uFE0F Push local commits to the remote`).action(() => {
|
|
2874
3353
|
repoPushCommand();
|
|
2875
3354
|
});
|
|
2876
|
-
repo.command("save").description(`\u{1F4BE} Stage + commit + push in one step ${
|
|
3355
|
+
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
3356
|
Examples:
|
|
2878
3357
|
$ sppt repo save -m "edit dbml skill"
|
|
2879
|
-
$ sppt repo save ${
|
|
3358
|
+
$ sppt repo save ${chalk24.dim("# auto-generates message and pushes")}
|
|
2880
3359
|
|
|
2881
3360
|
Equivalent to: sppt repo commit && sppt repo push
|
|
2882
3361
|
`).action((opts) => {
|
|
2883
3362
|
repoSaveCommand({ message: opts.message });
|
|
2884
3363
|
});
|
|
2885
|
-
repo.command("path").description(`\u{1F4CD} Print the repo path to stdout ${
|
|
3364
|
+
repo.command("path").description(`\u{1F4CD} Print the repo path to stdout ${chalk24.dim("(e.g. cd $(sppt repo path))")}`).addHelpText("after", `
|
|
2886
3365
|
Examples:
|
|
2887
3366
|
$ sppt repo path
|
|
2888
|
-
${
|
|
3367
|
+
${chalk24.dim("/Users/me/.set-prompt/repo")}
|
|
2889
3368
|
|
|
2890
|
-
$ cd "$(sppt repo path)" ${
|
|
2891
|
-
$ code "$(sppt repo path)" ${
|
|
3369
|
+
$ cd "$(sppt repo path)" ${chalk24.dim("# jump into the repo")}
|
|
3370
|
+
$ code "$(sppt repo path)" ${chalk24.dim("# open in VSCode")}
|
|
2892
3371
|
`).action(() => {
|
|
2893
3372
|
repoPathCommand();
|
|
2894
3373
|
});
|
|
2895
|
-
repo.command("open").description(`\u{1F4C2} Open the repo in the OS file manager ${
|
|
3374
|
+
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
3375
|
Examples:
|
|
2897
|
-
$ sppt repo open ${
|
|
2898
|
-
$ sppt repo open --code ${
|
|
2899
|
-
$ sppt repo open --stree ${
|
|
3376
|
+
$ sppt repo open ${chalk24.dim("# Explorer / Finder / xdg-open")}
|
|
3377
|
+
$ sppt repo open --code ${chalk24.dim("# open in VSCode")}
|
|
3378
|
+
$ sppt repo open --stree ${chalk24.dim("# open in Sourcetree")}
|
|
2900
3379
|
`).action((opts) => {
|
|
2901
3380
|
repoOpenCommand({ code: opts.code, stree: opts.stree });
|
|
2902
3381
|
});
|
|
2903
|
-
program.command("uninstall").description(`\u{1F5D1}\uFE0F Remove all set-prompt data ${
|
|
3382
|
+
program.command("uninstall").description(`\u{1F5D1}\uFE0F Remove all set-prompt data ${chalk24.dim("(~/.set-prompt/, plugin dirs, settings entries)")}`).action(async () => {
|
|
2904
3383
|
await uninstallCommand();
|
|
2905
3384
|
});
|
|
2906
3385
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "set-prompt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "One repo. Every AI coding tool. Always in sync.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -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": [
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"ora": "^9.3.0",
|
|
52
53
|
"smol-toml": "^1.6.0",
|
|
53
54
|
"typia": "^11.0.3",
|
|
55
|
+
"yaml": "^2.8.4",
|
|
54
56
|
"zod": "^4.3.6"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|