argsbarg 3.3.1 → 3.3.3
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 +15 -1
- package/docs/bundled-docs.md +18 -0
- package/docs/cli-program.md +17 -2
- package/docs/install.md +6 -4
- package/docs/templates/cursor/rules/cli-program.mdc +1 -1
- package/package.json +1 -1
- package/src/builtins/install.ts +3 -3
- package/src/docs/builtin.ts +22 -3
- package/src/docs/docs.test.ts +68 -1
- package/src/docs/resolve.ts +19 -30
- package/src/docs/save.ts +60 -0
- package/src/install/detect-installed.ts +0 -1
- package/src/install/index.ts +7 -9
- package/src/install/install.test.ts +64 -1
- package/src/install/mcp-config.ts +0 -1
- package/src/install/plan.ts +8 -8
- package/src/install/uninstall.ts +17 -15
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.3.3] - 2026-06-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`docs --save`** — write bundled docs to `./docs/`; `docs all --save` writes one file per topic instead of combined stdout output. Argsbarg-generated markdown (`mcp`, `api`, `skill`) is prefixed with a `Generated by … docs … --save` HTML comment.
|
|
15
|
+
|
|
16
|
+
## [3.3.2] - 2026-06-21
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`install --uninstall`** — symmetric with install: requires `--all` or scoped flags; `--uninstall --all` removes everything argsbarg installed; empty scope succeeds without error.
|
|
21
|
+
|
|
10
22
|
## [3.3.1] - 2026-06-21
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -243,7 +255,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
243
255
|
- Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
|
|
244
256
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
245
257
|
|
|
246
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.3.
|
|
258
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.3.3...HEAD
|
|
259
|
+
[3.3.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.3
|
|
260
|
+
[3.3.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.2
|
|
247
261
|
[3.3.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.1
|
|
248
262
|
[3.3.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.0
|
|
249
263
|
[3.2.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.2.0
|
package/docs/bundled-docs.md
CHANGED
|
@@ -32,6 +32,8 @@ myapp docs api # command tree as markdown
|
|
|
32
32
|
myapp docs skill # generated Cursor SKILL.md
|
|
33
33
|
myapp docs all # all user topics; includes auto mcp when MCP enabled
|
|
34
34
|
myapp docs mcp # auto-generated when mcpServer.enabled
|
|
35
|
+
myapp docs readme --save # write ./docs/readme.md
|
|
36
|
+
myapp docs all --save # write ./docs/<topic>.md for each bundled topic
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
## Configuration
|
|
@@ -89,3 +91,19 @@ All `docs` subcommands are hidden from MCP `tools/list` (`mcpTool: { enabled: fa
|
|
|
89
91
|
| `mcp` | Callable tools + schema resource |
|
|
90
92
|
|
|
91
93
|
Do not declare a top-level command named **`docs`** when `docs.enabled` is `true` — it is reserved.
|
|
94
|
+
|
|
95
|
+
## Save to disk (`--save`)
|
|
96
|
+
|
|
97
|
+
Pass **`--save`** on `docs` or any docs subcommand to write files under **`./docs/`** (relative to the current working directory). Each saved path is printed on stdout.
|
|
98
|
+
|
|
99
|
+
| Command | Output |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `docs readme --save` | `./docs/readme.md` |
|
|
102
|
+
| `docs schema --save` | `./docs/schema.json` |
|
|
103
|
+
| `docs api --save` | `./docs/api.md` |
|
|
104
|
+
| `docs skill --save` | `./docs/skill.md` |
|
|
105
|
+
| `docs all --save` | one file per bundled topic (`readme.md`, `mcp.md`, …) — not a combined file |
|
|
106
|
+
|
|
107
|
+
`docs all --save` uses the same topic set as stdout `docs all` (user topics plus auto `mcp` when enabled). It does not include `schema`, `api`, or `skill` unless you invoke those subcommands separately.
|
|
108
|
+
|
|
109
|
+
Argsbarg-generated markdown (`mcp`, `api`, `skill`) is prefixed with an HTML comment: `<!-- Generated by myapp docs api --save; do not edit. -->`. Consumer topic files and `schema.json` are written as-is.
|
package/docs/cli-program.md
CHANGED
|
@@ -52,12 +52,27 @@ ArgsBarg is **schema-first** — the program tree is the product. **Keep `CliPro
|
|
|
52
52
|
| --- | --- |
|
|
53
53
|
| Shared option objects (`DRY_RUN_OPTION`, `JSON_OPTION`) | Identical flag reused on many leaves |
|
|
54
54
|
| Shared spreads (`...MCP_TOOL_MUTATOR`) | Same `mcpTool` metadata on a family of commands |
|
|
55
|
-
| `
|
|
55
|
+
| `commands/<name>/command.tsx` module | Entry file is large; handler/body is substantial (Ink page, headless dispatch) |
|
|
56
56
|
| `docs.topics` text imports | Compile-time markdown bundling — not schema shape |
|
|
57
57
|
|
|
58
58
|
**Avoid extracting** thin indirection: a file that only re-exports `{ key, description, options }` with no logic, or splitting every leaf into its own module when the handler is a few lines. If extraction does not reduce duplication or file size materially, keep it inline.
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
When you extract a leaf or router, prefer a **plain exported object** — not a zero-arg wrapper function:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// commands/reserve/command.tsx
|
|
64
|
+
export const reserveCommand = {
|
|
65
|
+
key: "reserve",
|
|
66
|
+
description: "Reserve a QA environment.",
|
|
67
|
+
options: [YES_OPTION, DRY_RUN_OPTION],
|
|
68
|
+
positionals: [/* … */],
|
|
69
|
+
handler: async (ctx) => { /* … */ },
|
|
70
|
+
} satisfies CliLeaf;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use a **parameterized factory** only when the schema truly depends on inputs (e.g. `createUpsertCommand(deps)` for tests or injected config). A `reserveCommand()` that returns a static literal adds indirection without benefit.
|
|
74
|
+
|
|
75
|
+
**`satisfies CliProgram`** on the root (or **`satisfies CliLeaf`** / router type on extracted modules) preserves type-checking whether inline or not.
|
|
61
76
|
|
|
62
77
|
## Descriptions
|
|
63
78
|
|
package/docs/install.md
CHANGED
|
@@ -17,8 +17,8 @@ myapp update
|
|
|
17
17
|
# See what is installed
|
|
18
18
|
myapp install --status
|
|
19
19
|
|
|
20
|
-
# Remove everything
|
|
21
|
-
myapp install --uninstall --yes
|
|
20
|
+
# Remove everything installed with --all
|
|
21
|
+
myapp install --uninstall --all --yes
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
## What gets installed
|
|
@@ -33,7 +33,9 @@ myapp install --uninstall --yes
|
|
|
33
33
|
| Claude skill | `--skill` | `~/.claude/skills/<dir>/` when `~/.claude` exists |
|
|
34
34
|
| MCP config | `--mcp` | `~/.cursor/mcp.json` and `~/.claude.json` when MCP is enabled |
|
|
35
35
|
|
|
36
|
-
`--all` expands to `--bin`, `--completions`, `--skill`, and `--mcp` (when `mcpServer.enabled` is `true`).
|
|
36
|
+
`--all` expands to `--bin`, `--completions`, `--skill`, and `--mcp` (when `mcpServer.enabled` is `true`) for both install and uninstall. Missing targets are skipped silently (no error if nothing is on disk or a shell/agent directory does not exist).
|
|
37
|
+
|
|
38
|
+
`install --uninstall` requires the same target flags as install (`--all`, `--bin`, etc.) — bare `--uninstall` alone is an error.
|
|
37
39
|
|
|
38
40
|
Shells not on PATH are skipped silently (no warnings).
|
|
39
41
|
|
|
@@ -105,7 +107,7 @@ Environment:
|
|
|
105
107
|
| `--reinstall` | Reinstall artifacts already on disk (implies `--bin` + `--yes`) |
|
|
106
108
|
| `--from <path>` | Binary to copy with `--reinstall` (default: running executable) |
|
|
107
109
|
| `--status` | Read-only inventory |
|
|
108
|
-
| `--uninstall` | Remove
|
|
110
|
+
| `--uninstall` | Remove artifacts in scope (`--all`, `--bin`, `--completions`, `--skill`, `--mcp`); skips targets not installed |
|
|
109
111
|
|
|
110
112
|
`--update` is accepted as a deprecated alias for `--reinstall`.
|
|
111
113
|
|
|
@@ -13,7 +13,7 @@ alwaysApply: false
|
|
|
13
13
|
- Reserved root commands (do not declare): `completion`, `install`, `mcp`, `version`, `docs`, `update`
|
|
14
14
|
|
|
15
15
|
## Schema
|
|
16
|
-
- **Inline** options, positionals, and handler schema; extract
|
|
16
|
+
- **Inline** options, positionals, and handler schema; extract to `export const …Command = { … } satisfies CliLeaf` (or router type) when shared flags, `mcpTool` spreads, or large handlers justify a module — not zero-arg wrapper functions
|
|
17
17
|
- Leaf descriptions: action-oriented, not UI jargon
|
|
18
18
|
- Prefer option names `yes`, `dry-run`, `json` when semantics match; describe non-interactive use on the option
|
|
19
19
|
|
package/package.json
CHANGED
package/src/builtins/install.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: "uninstall",
|
|
45
|
-
description: "Remove installed artifacts (all
|
|
45
|
+
description: "Remove installed artifacts (use --all or scoped flags; skips targets not on disk).",
|
|
46
46
|
kind: CliOptionKind.Presence,
|
|
47
47
|
},
|
|
48
48
|
{
|
|
@@ -96,8 +96,8 @@ export function cliBuiltinInstallCommand(root: CliProgram): CliLeaf {
|
|
|
96
96
|
` {app} update\n\n` +
|
|
97
97
|
"See what is installed:\n" +
|
|
98
98
|
` {app} install --status\n\n` +
|
|
99
|
-
"Remove everything:\n" +
|
|
100
|
-
` {app} install --uninstall --yes\n\n` +
|
|
99
|
+
"Remove everything installed with --all:\n" +
|
|
100
|
+
` {app} install --uninstall --all --yes\n\n` +
|
|
101
101
|
"Use --dry to preview changes, --json for machine-readable output.",
|
|
102
102
|
options: installBuiltinOptions(root),
|
|
103
103
|
handler: () => {},
|
package/src/docs/builtin.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliFallbackMode, type CliLeaf, type CliProgram, type CliRouter } from "../types.ts";
|
|
1
|
+
import { CliFallbackMode, CliOptionKind, type CliLeaf, type CliOption, type CliProgram, type CliRouter } from "../types.ts";
|
|
2
2
|
import {
|
|
3
3
|
DOCS_ROUTER_DESCRIPTION,
|
|
4
4
|
docsEffectiveDefaultTopic,
|
|
@@ -8,14 +8,32 @@ import {
|
|
|
8
8
|
docsUserTopicKeys,
|
|
9
9
|
printDocsTopic,
|
|
10
10
|
} from "./resolve.ts";
|
|
11
|
+
import { saveDocsTopics } from "./save.ts";
|
|
12
|
+
|
|
13
|
+
const DOCS_SAVE_OPTION: CliOption = {
|
|
14
|
+
name: "save",
|
|
15
|
+
description: "Write documentation to ./docs/ (`docs all` writes one file per topic).",
|
|
16
|
+
kind: CliOptionKind.Presence,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function runDocsTopic(program: CliProgram, topic: string, ctx: { hasFlag(name: string): boolean }): void {
|
|
20
|
+
if (ctx.hasFlag("save")) {
|
|
21
|
+
for (const path of saveDocsTopics(program, topic)) {
|
|
22
|
+
process.stdout.write(`${path}\n`);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
printDocsTopic(program, topic);
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
function docsLeaf(program: CliProgram, key: string, description: string): CliLeaf {
|
|
13
30
|
return {
|
|
14
31
|
key,
|
|
15
32
|
description,
|
|
33
|
+
options: [DOCS_SAVE_OPTION],
|
|
16
34
|
mcpTool: { enabled: false },
|
|
17
|
-
handler: () => {
|
|
18
|
-
|
|
35
|
+
handler: (ctx) => {
|
|
36
|
+
runDocsTopic(program, key, ctx);
|
|
19
37
|
},
|
|
20
38
|
};
|
|
21
39
|
}
|
|
@@ -46,6 +64,7 @@ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
|
|
|
46
64
|
return {
|
|
47
65
|
key: "docs",
|
|
48
66
|
description: docs.description ?? DOCS_ROUTER_DESCRIPTION,
|
|
67
|
+
options: [DOCS_SAVE_OPTION],
|
|
49
68
|
fallbackCommand: docsEffectiveDefaultTopic(docs),
|
|
50
69
|
fallbackMode: CliFallbackMode.MissingOnly,
|
|
51
70
|
commands: leaves,
|
package/src/docs/docs.test.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
2
5
|
import { cliPresentationRoot } from "../builtins/presentation.ts";
|
|
3
6
|
import { completionBashScript } from "../completion.ts";
|
|
4
7
|
import { cliInvoke } from "../index.ts";
|
|
@@ -6,6 +9,21 @@ import type { CliProgram } from "../types.ts";
|
|
|
6
9
|
import { cliValidateProgram } from "../validate.ts";
|
|
7
10
|
import { combineAllDocs, docsEffectiveDefaultTopic } from "./resolve.ts";
|
|
8
11
|
import { generateMcpGuide } from "./mcp-guide.ts";
|
|
12
|
+
import { docsTopicsToSave, saveDocsTopics } from "./save.ts";
|
|
13
|
+
|
|
14
|
+
let workDir: string;
|
|
15
|
+
let prevCwd: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
workDir = mkdtempSync(join(tmpdir(), "argsbarg-docs-save-"));
|
|
19
|
+
prevCwd = process.cwd();
|
|
20
|
+
process.chdir(workDir);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
process.chdir(prevCwd);
|
|
25
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
9
27
|
|
|
10
28
|
function docsFixture(mcp = true): CliProgram {
|
|
11
29
|
return {
|
|
@@ -165,3 +183,52 @@ test("generateMcpGuide includes schema URI", () => {
|
|
|
165
183
|
const guide = generateMcpGuide(docsFixture(true));
|
|
166
184
|
expect(guide).toContain("myapp://schema");
|
|
167
185
|
});
|
|
186
|
+
|
|
187
|
+
test("docs --save writes topic file", async () => {
|
|
188
|
+
const result = await cliInvoke(docsFixture(), ["docs", "readme", "--save"]);
|
|
189
|
+
expect(result.exitCode).toBe(0);
|
|
190
|
+
expect(result.stdout.trim()).toBe("docs/readme.md");
|
|
191
|
+
const text = readFileSync(join(workDir, "docs/readme.md"), "utf8");
|
|
192
|
+
expect(text).toContain("Hello README");
|
|
193
|
+
expect(text).not.toContain("Generated by");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("docs api --save prepends generated hint", async () => {
|
|
197
|
+
const result = await cliInvoke(docsFixture(), ["docs", "api", "--save"]);
|
|
198
|
+
expect(result.exitCode).toBe(0);
|
|
199
|
+
const text = readFileSync(join(workDir, "docs/api.md"), "utf8");
|
|
200
|
+
expect(text.startsWith("<!-- Generated by myapp docs api --save; do not edit. -->\n\n")).toBe(
|
|
201
|
+
true,
|
|
202
|
+
);
|
|
203
|
+
expect(text).toContain("CLI API reference");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("docs schema --save writes JSON file", async () => {
|
|
207
|
+
const result = await cliInvoke(docsFixture(), ["docs", "schema", "--save"]);
|
|
208
|
+
expect(result.exitCode).toBe(0);
|
|
209
|
+
expect(result.stdout.trim()).toBe("docs/schema.json");
|
|
210
|
+
const text = readFileSync(join(workDir, "docs/schema.json"), "utf8");
|
|
211
|
+
expect(text).not.toContain("Generated by");
|
|
212
|
+
const schema = JSON.parse(text);
|
|
213
|
+
expect(schema.key).toBe("myapp");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("docs all --save writes separate topic files", async () => {
|
|
217
|
+
const program = docsFixture(true);
|
|
218
|
+
const result = await cliInvoke(program, ["docs", "all", "--save"]);
|
|
219
|
+
expect(result.exitCode).toBe(0);
|
|
220
|
+
const lines = result.stdout.trim().split("\n");
|
|
221
|
+
expect(lines).toEqual(docsTopicsToSave(program, "all").map((key) => `docs/${key}.md`));
|
|
222
|
+
expect(readFileSync(join(workDir, "docs/readme.md"), "utf8")).not.toContain("Generated by");
|
|
223
|
+
expect(readFileSync(join(workDir, "docs/mcp.md"), "utf8")).toContain(
|
|
224
|
+
"<!-- Generated by myapp docs mcp --save; do not edit. -->",
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("saveDocsTopics returns relative paths", () => {
|
|
229
|
+
const paths = saveDocsTopics(docsFixture(), "api");
|
|
230
|
+
expect(paths).toEqual(["docs/api.md"]);
|
|
231
|
+
const text = readFileSync(join(workDir, "docs/api.md"), "utf8");
|
|
232
|
+
expect(text).toContain("<!-- Generated by myapp docs api --save; do not edit. -->");
|
|
233
|
+
expect(text).toContain("CLI API reference");
|
|
234
|
+
});
|
package/src/docs/resolve.ts
CHANGED
|
@@ -77,6 +77,24 @@ export function docsTopicText(program: CliProgram, topic: string): string {
|
|
|
77
77
|
return entry.text;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** Full file body for a docs topic (stdout or `--save`). */
|
|
81
|
+
export function docsTopicContent(program: CliProgram, topic: string): string {
|
|
82
|
+
if (topic === "schema") {
|
|
83
|
+
return cliSchemaJson(program);
|
|
84
|
+
}
|
|
85
|
+
if (topic === "api") {
|
|
86
|
+
return generateApiGuide(program);
|
|
87
|
+
}
|
|
88
|
+
if (topic === "skill") {
|
|
89
|
+
return `${generateSkillBundle(program, "cursor").skillMd}\n`;
|
|
90
|
+
}
|
|
91
|
+
if (topic === "all") {
|
|
92
|
+
return `${combineAllDocs(program)}\n`;
|
|
93
|
+
}
|
|
94
|
+
const text = docsTopicText(program, topic);
|
|
95
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
/** All bundled docs concatenated with horizontal rules. */
|
|
81
99
|
export function combineAllDocs(program: CliProgram): string {
|
|
82
100
|
return docsPrintOrder(program)
|
|
@@ -84,36 +102,7 @@ export function combineAllDocs(program: CliProgram): string {
|
|
|
84
102
|
.join("\n\n---\n\n");
|
|
85
103
|
}
|
|
86
104
|
|
|
87
|
-
/** Writes CLI schema JSON to stdout (`docs schema`). */
|
|
88
|
-
export function printDocsSchema(program: CliProgram): void {
|
|
89
|
-
process.stdout.write(cliSchemaJson(program));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Writes markdown API reference to stdout (`docs api`). */
|
|
93
|
-
export function printDocsApi(program: CliProgram): void {
|
|
94
|
-
process.stdout.write(generateApiGuide(program));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Writes generated Cursor SKILL.md to stdout (`docs skill`). */
|
|
98
|
-
export function printDocsSkill(program: CliProgram): void {
|
|
99
|
-
const bundle = generateSkillBundle(program, "cursor");
|
|
100
|
-
process.stdout.write(`${bundle.skillMd}\n`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
105
|
/** Writes one docs topic (or `all`) to stdout. */
|
|
104
106
|
export function printDocsTopic(program: CliProgram, topic: string): void {
|
|
105
|
-
|
|
106
|
-
printDocsSchema(program);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (topic === "api") {
|
|
110
|
-
printDocsApi(program);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (topic === "skill") {
|
|
114
|
-
printDocsSkill(program);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const content = topic === "all" ? combineAllDocs(program) : docsTopicText(program, topic);
|
|
118
|
-
process.stdout.write(`${content}\n`);
|
|
107
|
+
process.stdout.write(docsTopicContent(program, topic));
|
|
119
108
|
}
|
package/src/docs/save.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { CliProgram } from "../types.ts";
|
|
4
|
+
import { docsPrintOrder, docsTopicContent } from "./resolve.ts";
|
|
5
|
+
|
|
6
|
+
/** Relative output directory for `docs --save`. */
|
|
7
|
+
export const DOCS_SAVE_DIR = "docs";
|
|
8
|
+
|
|
9
|
+
/** Builtin docs topics generated by argsbarg (not consumer `docs.topics`). */
|
|
10
|
+
export const DOCS_GENERATED_SAVE_TOPICS = ["mcp", "api", "skill"] as const;
|
|
11
|
+
|
|
12
|
+
/** Whether `--save` should prepend a generated-file hint (argsbarg writers only). */
|
|
13
|
+
export function docsTopicIsGeneratedByArgsbarg(topic: string): boolean {
|
|
14
|
+
return (DOCS_GENERATED_SAVE_TOPICS as readonly string[]).includes(topic);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** HTML comment prepended to generated markdown saved with `--save`. */
|
|
18
|
+
export function docsSaveGeneratedHint(program: CliProgram, topic: string): string {
|
|
19
|
+
return `<!-- Generated by ${program.key} docs ${topic} --save; do not edit. -->\n\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** File body for `--save` (hint on argsbarg-generated markdown only). */
|
|
23
|
+
export function docsTopicContentForSave(program: CliProgram, topic: string): string {
|
|
24
|
+
const content = docsTopicContent(program, topic);
|
|
25
|
+
if (!docsTopicIsGeneratedByArgsbarg(topic)) {
|
|
26
|
+
return content;
|
|
27
|
+
}
|
|
28
|
+
return `${docsSaveGeneratedHint(program, topic)}${content}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Filename for a saved docs topic. */
|
|
32
|
+
export function docsSaveFilename(topic: string): string {
|
|
33
|
+
if (topic === "schema") {
|
|
34
|
+
return "schema.json";
|
|
35
|
+
}
|
|
36
|
+
return `${topic}.md`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Topic keys to write for one `docs` invocation (`all` expands to bundled topics). */
|
|
40
|
+
export function docsTopicsToSave(program: CliProgram, topic: string): string[] {
|
|
41
|
+
if (topic === "all") {
|
|
42
|
+
return docsPrintOrder(program);
|
|
43
|
+
}
|
|
44
|
+
return [topic];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Writes docs topic(s) under `./docs/`; returns relative paths written. */
|
|
48
|
+
export function saveDocsTopics(program: CliProgram, topic: string): string[] {
|
|
49
|
+
const dir = join(process.cwd(), DOCS_SAVE_DIR);
|
|
50
|
+
mkdirSync(dir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const saved: string[] = [];
|
|
53
|
+
for (const key of docsTopicsToSave(program, topic)) {
|
|
54
|
+
const rel = join(DOCS_SAVE_DIR, docsSaveFilename(key));
|
|
55
|
+
const abs = join(process.cwd(), rel);
|
|
56
|
+
writeFileSync(abs, docsTopicContentForSave(program, key), "utf8");
|
|
57
|
+
saved.push(rel);
|
|
58
|
+
}
|
|
59
|
+
return saved;
|
|
60
|
+
}
|
package/src/install/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { resolveInstallPaths } from "./paths.ts";
|
|
|
13
13
|
import { installErr, installInfo, installOut, printInstallStatus } from "./status.ts";
|
|
14
14
|
import { buildUninstallPlan, uninstallSkillDir, type UninstallAction } from "./uninstall.ts";
|
|
15
15
|
|
|
16
|
-
function parseInstallOpts(raw: Record<string, string>): InstallOpts {
|
|
16
|
+
export function parseInstallOpts(raw: Record<string, string>): InstallOpts {
|
|
17
17
|
const flag = (name: string) => raw[name] === "1";
|
|
18
18
|
const reinstall = flag("reinstall") || flag("update");
|
|
19
19
|
return {
|
|
@@ -34,7 +34,7 @@ function parseInstallOpts(raw: Record<string, string>): InstallOpts {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function
|
|
37
|
+
export function validateInstallOpts(opts: InstallOpts): string | null {
|
|
38
38
|
if (opts.quiet && opts.dry) {
|
|
39
39
|
return "--quiet cannot be combined with --dry.";
|
|
40
40
|
}
|
|
@@ -60,10 +60,10 @@ function validateOpts(opts: InstallOpts): string | null {
|
|
|
60
60
|
) {
|
|
61
61
|
return "--reinstall cannot be combined with other target flags.";
|
|
62
62
|
}
|
|
63
|
-
if (opts.uninstall && (opts.
|
|
64
|
-
return "--uninstall cannot be combined with --
|
|
63
|
+
if (opts.uninstall && (opts.reinstall || opts.status)) {
|
|
64
|
+
return "--uninstall cannot be combined with --reinstall or --status.";
|
|
65
65
|
}
|
|
66
|
-
if (!opts.status && !opts.reinstall
|
|
66
|
+
if (!opts.status && !opts.reinstall) {
|
|
67
67
|
const hasTarget = opts.all || opts.bin || opts.completions || opts.skill || opts.mcp;
|
|
68
68
|
if (!hasTarget) {
|
|
69
69
|
return "Specify at least one target: --all, --bin, --completions, --skill, or --mcp.";
|
|
@@ -126,7 +126,7 @@ export async function runInstallMutation(
|
|
|
126
126
|
rawOpts: Record<string, string>,
|
|
127
127
|
): Promise<{ changed: string[]; opts: InstallOpts; paths: ReturnType<typeof resolveInstallPaths> }> {
|
|
128
128
|
const opts = parseInstallOpts(rawOpts);
|
|
129
|
-
const err =
|
|
129
|
+
const err = validateInstallOpts(opts);
|
|
130
130
|
if (err) {
|
|
131
131
|
throw new Error(err);
|
|
132
132
|
}
|
|
@@ -159,7 +159,7 @@ export async function runInstallMutation(
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
if (actions.length === 0) {
|
|
162
|
-
|
|
162
|
+
return { changed: [], opts, paths };
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
if (!opts.quiet && !opts.json) {
|
|
@@ -218,5 +218,3 @@ export async function cliInstall(root: CliProgram, rawOpts: Record<string, strin
|
|
|
218
218
|
|
|
219
219
|
process.exit(0);
|
|
220
220
|
}
|
|
221
|
-
|
|
222
|
-
export { parseInstallOpts };
|
|
@@ -6,8 +6,9 @@ import { CliProgram } from "../types.ts";
|
|
|
6
6
|
import { detectInstalledArtifacts } from "./detect-installed.ts";
|
|
7
7
|
import { resolveInstallPaths } from "./paths.ts";
|
|
8
8
|
import { buildInstallPlan } from "./plan.ts";
|
|
9
|
+
import { buildUninstallPlan } from "./uninstall.ts";
|
|
9
10
|
import { printInstallStatus } from "./status.ts";
|
|
10
|
-
import { parseInstallOpts } from "./index.ts";
|
|
11
|
+
import { parseInstallOpts, runInstallMutation, validateInstallOpts } from "./index.ts";
|
|
11
12
|
|
|
12
13
|
const fixture: CliProgram = {
|
|
13
14
|
key: "testapp",
|
|
@@ -123,3 +124,65 @@ describe("parseInstallOpts", () => {
|
|
|
123
124
|
expect(opts.all).toBe(true);
|
|
124
125
|
});
|
|
125
126
|
});
|
|
127
|
+
|
|
128
|
+
describe("validateInstallOpts", () => {
|
|
129
|
+
test("uninstall requires a target flag", () => {
|
|
130
|
+
const opts = parseInstallOpts({ uninstall: "1", yes: "1" });
|
|
131
|
+
expect(validateInstallOpts(opts)).toContain("Specify at least one target");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("uninstall allows --all", () => {
|
|
135
|
+
const opts = parseInstallOpts({ uninstall: "1", all: "1", yes: "1" });
|
|
136
|
+
expect(validateInstallOpts(opts)).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("uninstall rejects --reinstall", () => {
|
|
140
|
+
const opts = parseInstallOpts({ uninstall: "1", reinstall: "1", all: "1" });
|
|
141
|
+
expect(validateInstallOpts(opts)).toContain("--reinstall");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("install mutation", () => {
|
|
146
|
+
test("uninstall --all with nothing installed succeeds", async () => {
|
|
147
|
+
const result = await runInstallMutation(fixture, { uninstall: "1", all: "1", yes: "1" });
|
|
148
|
+
expect(result.changed).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("install --skill with no agent dirs succeeds", async () => {
|
|
152
|
+
const result = await runInstallMutation(fixture, { skill: "1", yes: "1", dry: "1" });
|
|
153
|
+
expect(result.changed).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("uninstall --all removes detected binary", async () => {
|
|
157
|
+
const paths = resolveInstallPaths(fixture, {});
|
|
158
|
+
mkdirSync(paths.bindir, { recursive: true });
|
|
159
|
+
writeFileSync(paths.binaryPath, "fake", "utf8");
|
|
160
|
+
|
|
161
|
+
const result = await runInstallMutation(fixture, { uninstall: "1", all: "1", yes: "1" });
|
|
162
|
+
expect(result.changed).toContain(paths.binaryPath);
|
|
163
|
+
expect(existsSync(paths.binaryPath)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("uninstall plan", () => {
|
|
168
|
+
test("buildUninstallPlan --all scopes like install --all", () => {
|
|
169
|
+
const paths = resolveInstallPaths(fixture, {});
|
|
170
|
+
mkdirSync(paths.bindir, { recursive: true });
|
|
171
|
+
writeFileSync(paths.binaryPath, "fake", "utf8");
|
|
172
|
+
|
|
173
|
+
const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
|
|
174
|
+
expect(plan.some((a) => a.summary.startsWith("binary:"))).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("buildUninstallPlan --bin ignores completions", () => {
|
|
178
|
+
const paths = resolveInstallPaths(fixture, {});
|
|
179
|
+
mkdirSync(paths.bindir, { recursive: true });
|
|
180
|
+
writeFileSync(paths.binaryPath, "fake", "utf8");
|
|
181
|
+
mkdirSync(join(home, ".bash_completion.d"), { recursive: true });
|
|
182
|
+
writeFileSync(paths.bashCompletion, "# bash", "utf8");
|
|
183
|
+
|
|
184
|
+
const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", bin: "1" }));
|
|
185
|
+
expect(plan).toHaveLength(1);
|
|
186
|
+
expect(plan[0]!.summary.startsWith("binary:")).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
});
|
package/src/install/plan.ts
CHANGED
|
@@ -35,19 +35,19 @@ export interface InstallAction {
|
|
|
35
35
|
run: () => string[];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function
|
|
38
|
+
export function wantsInstallBin(opts: InstallOpts): boolean {
|
|
39
39
|
return !!(opts.all || opts.bin || opts.reinstall);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function
|
|
42
|
+
export function wantsInstallCompletions(opts: InstallOpts): boolean {
|
|
43
43
|
return !!(opts.all || opts.completions);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function
|
|
46
|
+
export function wantsInstallSkill(opts: InstallOpts): boolean {
|
|
47
47
|
return !!(opts.all || opts.skill);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function
|
|
50
|
+
export function wantsInstallMcp(opts: InstallOpts, root: CliProgram): boolean {
|
|
51
51
|
return !!(opts.mcp || opts.all) && resolveCapabilities(root).mcp;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -56,7 +56,7 @@ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: In
|
|
|
56
56
|
const actions: InstallAction[] = [];
|
|
57
57
|
const dry = !!opts.dry;
|
|
58
58
|
|
|
59
|
-
if (
|
|
59
|
+
if (wantsInstallBin(opts)) {
|
|
60
60
|
const sourcePath = opts.from ?? process.execPath;
|
|
61
61
|
actions.push({
|
|
62
62
|
kind: "binary",
|
|
@@ -66,7 +66,7 @@ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: In
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
if (
|
|
69
|
+
if (wantsInstallCompletions(opts)) {
|
|
70
70
|
const shells = detectShells();
|
|
71
71
|
if (shells.bash) {
|
|
72
72
|
actions.push({
|
|
@@ -103,7 +103,7 @@ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: In
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
if (
|
|
106
|
+
if (wantsInstallSkill(opts)) {
|
|
107
107
|
const home = userHome();
|
|
108
108
|
if (existsSync(join(home, ".cursor"))) {
|
|
109
109
|
actions.push({
|
|
@@ -123,7 +123,7 @@ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: In
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
if (
|
|
126
|
+
if (wantsInstallMcp(opts, root)) {
|
|
127
127
|
const entry = expectedMcpEntry(root);
|
|
128
128
|
if (existsSync(join(userHome(), ".cursor"))) {
|
|
129
129
|
actions.push({
|
package/src/install/uninstall.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { existsSync, rmSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { resolveCapabilities } from "../capabilities.ts";
|
|
4
2
|
import { CliProgram } from "../types.ts";
|
|
5
3
|
import { uninstallBinary } from "./binary.ts";
|
|
6
4
|
import { uninstallCompletions } from "./completions.ts";
|
|
7
5
|
import { detectInstalledArtifacts } from "./detect-installed.ts";
|
|
8
6
|
import { removeMcpConfig } from "./mcp-config.ts";
|
|
9
|
-
import { InstallPaths
|
|
10
|
-
import
|
|
7
|
+
import { InstallPaths } from "./paths.ts";
|
|
8
|
+
import {
|
|
9
|
+
wantsInstallBin,
|
|
10
|
+
wantsInstallCompletions,
|
|
11
|
+
wantsInstallMcp,
|
|
12
|
+
wantsInstallSkill,
|
|
13
|
+
type InstallOpts,
|
|
14
|
+
} from "./plan.ts";
|
|
11
15
|
|
|
12
16
|
export interface UninstallAction {
|
|
13
17
|
summary: string;
|
|
@@ -15,22 +19,17 @@ export interface UninstallAction {
|
|
|
15
19
|
run: () => string[];
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
return !opts.bin && !opts.completions && !opts.skill && !opts.mcp;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Builds uninstall actions from detected artifacts. */
|
|
22
|
+
/** Builds uninstall actions for scoped targets (--all mirrors install --all). */
|
|
23
23
|
export function buildUninstallPlan(
|
|
24
24
|
root: CliProgram,
|
|
25
25
|
paths: InstallPaths,
|
|
26
26
|
opts: InstallOpts,
|
|
27
27
|
): UninstallAction[] {
|
|
28
28
|
const detected = detectInstalledArtifacts(paths);
|
|
29
|
-
const all = scopeAll(opts);
|
|
30
29
|
const dry = !!opts.dry;
|
|
31
30
|
const actions: UninstallAction[] = [];
|
|
32
31
|
|
|
33
|
-
if ((
|
|
32
|
+
if (wantsInstallBin(opts) && detected.binary) {
|
|
34
33
|
actions.push({
|
|
35
34
|
summary: `binary: ${paths.binaryPath}`,
|
|
36
35
|
message: `Removing binary ${paths.binaryPath}`,
|
|
@@ -38,7 +37,10 @@ export function buildUninstallPlan(
|
|
|
38
37
|
});
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
if (
|
|
40
|
+
if (
|
|
41
|
+
wantsInstallCompletions(opts) &&
|
|
42
|
+
(detected.bashCompletion || detected.zshCompletion || detected.fishCompletion)
|
|
43
|
+
) {
|
|
42
44
|
if (detected.bashCompletion) {
|
|
43
45
|
actions.push({
|
|
44
46
|
summary: `bash completion: ${paths.bashCompletion}`,
|
|
@@ -62,7 +64,7 @@ export function buildUninstallPlan(
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
if ((
|
|
67
|
+
if (wantsInstallSkill(opts) && detected.cursorSkill) {
|
|
66
68
|
actions.push({
|
|
67
69
|
summary: `cursor skill: ${paths.cursorSkillDir}/`,
|
|
68
70
|
message: `Removing Cursor skill ${paths.cursorSkillDir}/`,
|
|
@@ -70,7 +72,7 @@ export function buildUninstallPlan(
|
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
if ((
|
|
75
|
+
if (wantsInstallSkill(opts) && detected.claudeSkill) {
|
|
74
76
|
actions.push({
|
|
75
77
|
summary: `claude skill: ${paths.claudeSkillDir}/`,
|
|
76
78
|
message: `Removing Claude Code skill ${paths.claudeSkillDir}/`,
|
|
@@ -78,7 +80,7 @@ export function buildUninstallPlan(
|
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
if ((
|
|
83
|
+
if (wantsInstallMcp(opts, root)) {
|
|
82
84
|
if (detected.cursorMcp) {
|
|
83
85
|
actions.push({
|
|
84
86
|
summary: `cursor mcp: ${paths.cursorMcpPath}`,
|