argsbarg 5.0.1 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -1
- package/docs/distribution-homebrew.md +2 -0
- package/examples/full-example/scripts/formula-shared.ts +1 -0
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +23 -0
- package/src/builtins/config.test.ts +8 -0
- package/src/cli-tool/cli-smoke.test.ts +8 -0
- package/src/cli-tool/create.test.ts +13 -0
- package/src/cli-tool/full-example-capabilities.test.ts +8 -0
- package/src/cli-tool/main.ts +0 -0
- package/src/config/context.test.ts +8 -0
- package/src/config/file.test.ts +12 -0
- package/src/config/resolve.test.ts +18 -0
- package/src/config/validate.test.ts +11 -0
- package/src/config.integration.test.ts +5 -0
- package/src/configure/configure.test.ts +22 -0
- package/src/configure/index.ts +14 -0
- package/src/configure/prompt.ts +7 -0
- package/src/docs/api-guide.test.ts +10 -0
- package/src/docs/docs.test.ts +27 -0
- package/src/docs/mcp-resources.test.ts +10 -0
- package/src/formats.test.ts +9 -0
- package/src/headless.test.ts +10 -0
- package/src/hidden-mcpb.test.ts +22 -0
- package/src/install/binary-placement.test.ts +14 -0
- package/src/install/gh-release-update.test.ts +9 -0
- package/src/install/install-validate.test.ts +10 -0
- package/src/install/mcp-codex.test.ts +7 -0
- package/src/install/mcp-openclaw.test.ts +6 -0
- package/src/install/mcp-opencode.test.ts +10 -0
- package/src/install/status.test.ts +9 -0
- package/src/install/targets.test.ts +19 -0
- package/src/invoke.test.ts +14 -0
- package/src/mcp/claude.test.ts +10 -0
- package/src/mcp/env.test.ts +12 -0
- package/src/mcp/zip.test.ts +5 -0
- package/src/mcp.integration.test.ts +39 -0
- package/src/parse.test.ts +71 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for configure/configure module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
6
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
7
|
import { tmpdir } from "node:os";
|
|
@@ -42,30 +46,37 @@ afterEach(() => {
|
|
|
42
46
|
rmSync(home, { recursive: true, force: true });
|
|
43
47
|
});
|
|
44
48
|
|
|
49
|
+
/** Tests for configure opts. */
|
|
45
50
|
describe("configure opts", () => {
|
|
51
|
+
/** Validate rejects multiple modes. */
|
|
46
52
|
test("validate rejects multiple modes", () => {
|
|
47
53
|
const opts = parseConfigureOpts({ sync: "1", status: "1" });
|
|
48
54
|
expect(validateConfigureOpts(opts)).toContain("only one");
|
|
49
55
|
});
|
|
50
56
|
|
|
57
|
+
/** Sync requires --yes. */
|
|
51
58
|
test("sync requires --yes", () => {
|
|
52
59
|
const opts = parseConfigureOpts({ sync: "1" });
|
|
53
60
|
expect(validateConfigureOpts(opts)).toContain("--yes");
|
|
54
61
|
});
|
|
55
62
|
|
|
63
|
+
/** Remove-all requires --yes. */
|
|
56
64
|
test("remove-all requires --yes", () => {
|
|
57
65
|
const opts = parseConfigureOpts({ "remove-all": "1" });
|
|
58
66
|
expect(validateConfigureOpts(opts)).toContain("--yes");
|
|
59
67
|
});
|
|
60
68
|
});
|
|
61
69
|
|
|
70
|
+
/** Tests for install paths. */
|
|
62
71
|
describe("install paths", () => {
|
|
72
|
+
/** ResolveInstallPaths includes skill and mcp paths. */
|
|
63
73
|
test("resolveInstallPaths includes skill and mcp paths", () => {
|
|
64
74
|
const paths = resolveInstallPaths(fixture);
|
|
65
75
|
expect(paths.cursorSkillDir).toContain(".cursor/skills");
|
|
66
76
|
expect(paths.cursorMcpPath).toContain("mcp.json");
|
|
67
77
|
});
|
|
68
78
|
|
|
79
|
+
/** Claude desktop mcp path on darwin. */
|
|
69
80
|
test("claude desktop mcp path on darwin", () => {
|
|
70
81
|
const prev = process.platform;
|
|
71
82
|
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
@@ -79,7 +90,9 @@ describe("install paths", () => {
|
|
|
79
90
|
});
|
|
80
91
|
});
|
|
81
92
|
|
|
93
|
+
/** Tests for detect installed. */
|
|
82
94
|
describe("detect installed", () => {
|
|
95
|
+
/** Detects cursor mcp when configured. */
|
|
83
96
|
test("detects cursor mcp when configured", () => {
|
|
84
97
|
const paths = resolveInstallPaths(fixture);
|
|
85
98
|
mkdirSync(join(home, ".cursor"), { recursive: true });
|
|
@@ -95,7 +108,9 @@ describe("detect installed", () => {
|
|
|
95
108
|
});
|
|
96
109
|
});
|
|
97
110
|
|
|
111
|
+
/** Tests for sync plan. */
|
|
98
112
|
describe("sync plan", () => {
|
|
113
|
+
/** BuildUpdatePlan greenfield includes agent targets. */
|
|
99
114
|
test("buildUpdatePlan greenfield includes agent targets", () => {
|
|
100
115
|
const paths = resolveInstallPaths(fixture);
|
|
101
116
|
const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
|
|
@@ -103,6 +118,7 @@ describe("sync plan", () => {
|
|
|
103
118
|
expect(plan.some((a) => a.kind === "app")).toBe(false);
|
|
104
119
|
});
|
|
105
120
|
|
|
121
|
+
/** BuildInstallPlan --all omits app self-install. */
|
|
106
122
|
test("buildInstallPlan --all omits app self-install", () => {
|
|
107
123
|
const paths = resolveInstallPaths(fixture);
|
|
108
124
|
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
|
|
@@ -110,6 +126,7 @@ describe("sync plan", () => {
|
|
|
110
126
|
expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
|
|
111
127
|
});
|
|
112
128
|
|
|
129
|
+
/** BuildInstallPlan respects configure.agentIntegration skill. */
|
|
113
130
|
test("buildInstallPlan respects configure.agentIntegration skill", () => {
|
|
114
131
|
const skillApp: CliProgram = {
|
|
115
132
|
...fixture,
|
|
@@ -123,7 +140,9 @@ describe("sync plan", () => {
|
|
|
123
140
|
});
|
|
124
141
|
});
|
|
125
142
|
|
|
143
|
+
/** Tests for remove plan. */
|
|
126
144
|
describe("remove plan", () => {
|
|
145
|
+
/** BuildUninstallPlan --all with nothing installed is empty. */
|
|
127
146
|
test("buildUninstallPlan --all with nothing installed is empty", () => {
|
|
128
147
|
const paths = resolveInstallPaths(fixture);
|
|
129
148
|
const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
|
|
@@ -131,7 +150,9 @@ describe("remove plan", () => {
|
|
|
131
150
|
});
|
|
132
151
|
});
|
|
133
152
|
|
|
153
|
+
/** Tests for app config wizard. */
|
|
134
154
|
describe("app config wizard", () => {
|
|
155
|
+
/** Tests that appConfigHasEntries when entries exist. */
|
|
135
156
|
test("appConfigHasEntries when entries exist", () => {
|
|
136
157
|
expect(
|
|
137
158
|
appConfigHasEntries({
|
|
@@ -141,6 +162,7 @@ describe("app config wizard", () => {
|
|
|
141
162
|
).toBe(true);
|
|
142
163
|
});
|
|
143
164
|
|
|
165
|
+
/** Tests that appConfigHasEntries false for empty entries or missing appConfig. */
|
|
144
166
|
test("appConfigHasEntries false for empty entries or missing appConfig", () => {
|
|
145
167
|
expect(appConfigHasEntries({ ...fixture, appConfig: { entries: {} } })).toBe(false);
|
|
146
168
|
expect(appConfigHasEntries(fixture)).toBe(false);
|
package/src/configure/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Interactive and automated `configure` command orchestration (agent artifacts and app config).
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { resolveCapabilities } from "../capabilities.ts";
|
|
2
6
|
import { displayAppConfigPath, runConfigure } from "../config/bootstrap.ts";
|
|
3
7
|
import { resolveInstallPaths } from "../install/paths.ts";
|
|
@@ -36,6 +40,7 @@ export function appConfigHasEntries(program: CliProgram): boolean {
|
|
|
36
40
|
return !!entries && Object.keys(entries).length > 0;
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
/** Parsed flags for the top-level `configure` built-in. */
|
|
39
44
|
export interface ConfigureOpts {
|
|
40
45
|
sync?: boolean;
|
|
41
46
|
removeAll?: boolean;
|
|
@@ -46,6 +51,7 @@ export interface ConfigureOpts {
|
|
|
46
51
|
json?: boolean;
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
/** Maps raw argv flags into {@link ConfigureOpts}. */
|
|
49
55
|
export function parseConfigureOpts(raw: Record<string, string>): ConfigureOpts {
|
|
50
56
|
const flag = (name: string) => raw[name] === "1";
|
|
51
57
|
return {
|
|
@@ -59,6 +65,7 @@ export function parseConfigureOpts(raw: Record<string, string>): ConfigureOpts {
|
|
|
59
65
|
};
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
/** Returns an error message when configure flags are inconsistent; otherwise null. */
|
|
62
69
|
export function validateConfigureOpts(opts: ConfigureOpts): string | null {
|
|
63
70
|
const flags = [opts.sync, opts.removeAll, opts.removeConfig, opts.status].filter(Boolean);
|
|
64
71
|
if (flags.length > 1) {
|
|
@@ -73,6 +80,7 @@ export function validateConfigureOpts(opts: ConfigureOpts): string | null {
|
|
|
73
80
|
return null;
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
/** Adapts configure flags to internal install-plan option shape. */
|
|
76
84
|
function configureToInstallOpts(opts: ConfigureOpts): InstallOpts {
|
|
77
85
|
if (opts.status) {
|
|
78
86
|
return { status: true, yes: opts.yes, dry: opts.dry, json: opts.json };
|
|
@@ -89,6 +97,7 @@ function configureToInstallOpts(opts: ConfigureOpts): InstallOpts {
|
|
|
89
97
|
return { dry: opts.dry, json: opts.json };
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
/** Installs a skill target and returns changed paths. */
|
|
92
101
|
function runSkillAction(root: CliProgram, kind: InstallActionKind, opts: InstallOpts): string[] {
|
|
93
102
|
const target = skillTargetFromActionKind(kind);
|
|
94
103
|
if (!target) return [];
|
|
@@ -99,6 +108,7 @@ function runSkillAction(root: CliProgram, kind: InstallActionKind, opts: Install
|
|
|
99
108
|
});
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
/** Runs install or uninstall actions and collects changed paths. */
|
|
102
112
|
function executePlan(
|
|
103
113
|
root: CliProgram,
|
|
104
114
|
actions: Array<InstallAction | UninstallAction>,
|
|
@@ -130,6 +140,7 @@ function executePlan(
|
|
|
130
140
|
return changed;
|
|
131
141
|
}
|
|
132
142
|
|
|
143
|
+
/** Builds plan context limited to a single artifact key. */
|
|
133
144
|
function buildSingleTargetContext(
|
|
134
145
|
root: CliProgram,
|
|
135
146
|
paths: ReturnType<typeof resolveInstallPaths>,
|
|
@@ -147,6 +158,7 @@ function buildSingleTargetContext(
|
|
|
147
158
|
};
|
|
148
159
|
}
|
|
149
160
|
|
|
161
|
+
/** Resolves install or uninstall actions for one artifact target. */
|
|
150
162
|
function actionsForTarget(
|
|
151
163
|
root: CliProgram,
|
|
152
164
|
paths: ReturnType<typeof resolveInstallPaths>,
|
|
@@ -164,6 +176,7 @@ function actionsForTarget(
|
|
|
164
176
|
return target.planUninstall(ctx);
|
|
165
177
|
}
|
|
166
178
|
|
|
179
|
+
/** Walks enabled targets with per-target prompts (TTY required). */
|
|
167
180
|
async function runInteractiveConfigure(root: CliProgram, opts: ConfigureOpts): Promise<string[]> {
|
|
168
181
|
if (!process.stdin.isTTY) {
|
|
169
182
|
throw new Error("Interactive configure requires a TTY. Use flags such as --sync --yes.");
|
|
@@ -218,6 +231,7 @@ async function runInteractiveConfigure(root: CliProgram, opts: ConfigureOpts): P
|
|
|
218
231
|
return changed;
|
|
219
232
|
}
|
|
220
233
|
|
|
234
|
+
/** Runs sync, remove, or status modes without per-target prompts. */
|
|
221
235
|
async function runAutomatedConfigure(root: CliProgram, opts: ConfigureOpts): Promise<string[]> {
|
|
222
236
|
const installOpts = configureToInstallOpts(opts);
|
|
223
237
|
const paths = resolveInstallPaths(root);
|
package/src/configure/prompt.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
TTY prompts for per-target install, skip, or uninstall during interactive `configure`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { readSync } from "node:fs";
|
|
2
6
|
import type { CliInstallArtifactKey } from "../install/target-types.ts";
|
|
3
7
|
|
|
8
|
+
/** Human-readable labels for each install artifact key in interactive prompts. */
|
|
4
9
|
const LABELS: Record<CliInstallArtifactKey, string> = {
|
|
5
10
|
app: "App binary",
|
|
6
11
|
cursorSkill: "Cursor skill",
|
|
@@ -18,10 +23,12 @@ const LABELS: Record<CliInstallArtifactKey, string> = {
|
|
|
18
23
|
configure: "App config",
|
|
19
24
|
};
|
|
20
25
|
|
|
26
|
+
/** Returns the prompt label for an install artifact key. */
|
|
21
27
|
export function artifactPromptLabel(key: CliInstallArtifactKey): string {
|
|
22
28
|
return LABELS[key];
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
/** User choice from a per-target Y/n or y/N prompt. */
|
|
25
32
|
export type TargetPromptAction = "install" | "skip" | "uninstall";
|
|
26
33
|
|
|
27
34
|
/** Prompt per target: Y/n when not installed, y/N when installed. */
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for docs/api-guide module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { expect, test } from "bun:test";
|
|
2
6
|
import { cliSchemaExport } from "../schema.ts";
|
|
3
7
|
import type { CliProgram } from "../types.ts";
|
|
@@ -45,6 +49,7 @@ const nestedFixture: CliProgram = {
|
|
|
45
49
|
],
|
|
46
50
|
};
|
|
47
51
|
|
|
52
|
+
/** Tests that generateApiGuideBody matches command section of full API guide. */
|
|
48
53
|
test("generateApiGuideBody matches command section of full API guide", () => {
|
|
49
54
|
const body = generateApiGuideBody(nestedFixture);
|
|
50
55
|
const full = generateApiGuide(nestedFixture);
|
|
@@ -53,6 +58,7 @@ test("generateApiGuideBody matches command section of full API guide", () => {
|
|
|
53
58
|
expect(body).not.toContain("CLI API reference");
|
|
54
59
|
});
|
|
55
60
|
|
|
61
|
+
/** Tests that generateApiGuide covers the same command keys as cliSchemaExport. */
|
|
56
62
|
test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
|
|
57
63
|
const md = generateApiGuide(nestedFixture);
|
|
58
64
|
const schema = cliSchemaExport(nestedFixture);
|
|
@@ -62,6 +68,7 @@ test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
|
|
|
62
68
|
expect(schema.commands?.map((c) => c.key)).toEqual(["stat"]);
|
|
63
69
|
});
|
|
64
70
|
|
|
71
|
+
/** Tests that generateApiGuide resolves program key in install notes. */
|
|
65
72
|
test("generateApiGuide resolves program key in install notes", () => {
|
|
66
73
|
const fixture: CliProgram = {
|
|
67
74
|
key: "myapp",
|
|
@@ -75,6 +82,7 @@ test("generateApiGuide resolves program key in install notes", () => {
|
|
|
75
82
|
expect(md).not.toContain("Upgrade to latest release");
|
|
76
83
|
});
|
|
77
84
|
|
|
85
|
+
/** Tests that generateApiGuide mentions Homebrew upgrade. */
|
|
78
86
|
test("generateApiGuide mentions Homebrew upgrade", () => {
|
|
79
87
|
const fixture: CliProgram = {
|
|
80
88
|
key: "myapp",
|
|
@@ -87,6 +95,7 @@ test("generateApiGuide mentions Homebrew upgrade", () => {
|
|
|
87
95
|
expect(md).not.toContain("install --update");
|
|
88
96
|
});
|
|
89
97
|
|
|
98
|
+
/** Tests that generateApiGuide resolves {argsbarg:program} in consumer notes. */
|
|
90
99
|
test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
|
|
91
100
|
const fixture: CliProgram = {
|
|
92
101
|
key: "myapp",
|
|
@@ -105,6 +114,7 @@ test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
|
|
|
105
114
|
expect(md).toContain("Invoke `myapp run`.");
|
|
106
115
|
});
|
|
107
116
|
|
|
117
|
+
/** Tests that generateApiGuide and cliSchemaExport include leaf outputSchema. */
|
|
108
118
|
test("generateApiGuide and cliSchemaExport include leaf outputSchema", () => {
|
|
109
119
|
const fixture: CliProgram = {
|
|
110
120
|
key: "myapp",
|
package/src/docs/docs.test.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for docs/docs module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
6
|
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
7
|
import { tmpdir } from "node:os";
|
|
@@ -49,6 +53,7 @@ function docsFixture(mcp = true): CliProgram {
|
|
|
49
53
|
};
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/** Docs reserved when enabled. */
|
|
52
57
|
test("docs reserved when enabled", () => {
|
|
53
58
|
const root: CliProgram = {
|
|
54
59
|
...docsFixture(),
|
|
@@ -63,6 +68,7 @@ test("docs reserved when enabled", () => {
|
|
|
63
68
|
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: docs/);
|
|
64
69
|
});
|
|
65
70
|
|
|
71
|
+
/** Docs rejects reserved topic keys. */
|
|
66
72
|
test("docs rejects reserved topic keys", () => {
|
|
67
73
|
const root = docsFixture();
|
|
68
74
|
const docs = root.docs;
|
|
@@ -77,22 +83,26 @@ test("docs rejects reserved topic keys", () => {
|
|
|
77
83
|
expect(() => cliValidateProgram(root)).toThrow(/reserved/);
|
|
78
84
|
});
|
|
79
85
|
|
|
86
|
+
/** DocsEffectiveDefaultTopic uses first topic key. */
|
|
80
87
|
test("docsEffectiveDefaultTopic uses first topic key", () => {
|
|
81
88
|
expect(docsEffectiveDefaultTopic(docsFixture().docs!)).toBe("readme");
|
|
82
89
|
});
|
|
83
90
|
|
|
91
|
+
/** Bare docs prints first topic via Cli.invoke. */
|
|
84
92
|
test("bare docs prints first topic via Cli.invoke", async () => {
|
|
85
93
|
const result = await new Cli(docsFixture()).invoke(["docs"]);
|
|
86
94
|
expect(result.exitCode).toBe(0);
|
|
87
95
|
expect(result.stdout).toContain("Hello README");
|
|
88
96
|
});
|
|
89
97
|
|
|
98
|
+
/** Docs readme prints bundled text. */
|
|
90
99
|
test("docs readme prints bundled text", async () => {
|
|
91
100
|
const result = await new Cli(docsFixture()).invoke(["docs", "readme"]);
|
|
92
101
|
expect(result.exitCode).toBe(0);
|
|
93
102
|
expect(result.stdout).toContain("Hello README");
|
|
94
103
|
});
|
|
95
104
|
|
|
105
|
+
/** Docs defaultTopic override. */
|
|
96
106
|
test("docs defaultTopic override", async () => {
|
|
97
107
|
const root = docsFixture();
|
|
98
108
|
root.docs!.defaultTopic = "arch";
|
|
@@ -100,6 +110,7 @@ test("docs defaultTopic override", async () => {
|
|
|
100
110
|
expect(result.stdout).toContain("Architecture");
|
|
101
111
|
});
|
|
102
112
|
|
|
113
|
+
/** Docs mcp when MCP enabled. */
|
|
103
114
|
test("docs mcp when MCP enabled", async () => {
|
|
104
115
|
const result = await new Cli(docsFixture(true)).invoke(["docs", "mcp"]);
|
|
105
116
|
expect(result.exitCode).toBe(0);
|
|
@@ -109,11 +120,13 @@ test("docs mcp when MCP enabled", async () => {
|
|
|
109
120
|
expect(result.stdout).toContain("configure --sync --yes");
|
|
110
121
|
});
|
|
111
122
|
|
|
123
|
+
/** Docs rejects unknown subcommand. */
|
|
112
124
|
test("docs rejects unknown subcommand", async () => {
|
|
113
125
|
const result = await new Cli(docsFixture()).invoke(["docs", "all"]);
|
|
114
126
|
expect(result.exitCode).not.toBe(0);
|
|
115
127
|
});
|
|
116
128
|
|
|
129
|
+
/** Docs mcp absent from router when MCP disabled. */
|
|
117
130
|
test("docs mcp absent from router when MCP disabled", async () => {
|
|
118
131
|
const root = docsFixture(false);
|
|
119
132
|
const presentation = cliPresentationRoot(root);
|
|
@@ -126,6 +139,7 @@ test("docs mcp absent from router when MCP disabled", async () => {
|
|
|
126
139
|
expect(result.exitCode).not.toBe(0);
|
|
127
140
|
});
|
|
128
141
|
|
|
142
|
+
/** Presentation includes docs subtree. */
|
|
129
143
|
test("presentation includes docs subtree", () => {
|
|
130
144
|
const presentation = cliPresentationRoot(docsFixture());
|
|
131
145
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
@@ -135,6 +149,7 @@ test("presentation includes docs subtree", () => {
|
|
|
135
149
|
).toBe(true);
|
|
136
150
|
});
|
|
137
151
|
|
|
152
|
+
/** Docs schema prints JSON. */
|
|
138
153
|
test("docs schema prints JSON", async () => {
|
|
139
154
|
const result = await new Cli(docsFixture()).invoke(["docs", "schema"]);
|
|
140
155
|
expect(result.exitCode).toBe(0);
|
|
@@ -143,6 +158,7 @@ test("docs schema prints JSON", async () => {
|
|
|
143
158
|
expect(schema.commands.some((c: { key: string }) => c.key === "run")).toBe(true);
|
|
144
159
|
});
|
|
145
160
|
|
|
161
|
+
/** Docs api prints markdown reference. */
|
|
146
162
|
test("docs api prints markdown reference", async () => {
|
|
147
163
|
const result = await new Cli(docsFixture()).invoke(["docs", "api"]);
|
|
148
164
|
expect(result.exitCode).toBe(0);
|
|
@@ -152,6 +168,7 @@ test("docs api prints markdown reference", async () => {
|
|
|
152
168
|
expect(result.stdout).toContain("myapp docs schema");
|
|
153
169
|
});
|
|
154
170
|
|
|
171
|
+
/** Tests that skipsRequiredAppConfigExit includes docs and config builtins. */
|
|
155
172
|
test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
|
|
156
173
|
const program = {
|
|
157
174
|
...docsFixture(),
|
|
@@ -165,6 +182,7 @@ test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
|
|
|
165
182
|
expect(skipsRequiredAppConfigExit(["run"], caps)).toBe(false);
|
|
166
183
|
});
|
|
167
184
|
|
|
185
|
+
/** Docs skill prints Cursor SKILL.md. */
|
|
168
186
|
test("docs skill prints Cursor SKILL.md", async () => {
|
|
169
187
|
const result = await new Cli(docsFixture()).invoke(["docs", "skill"]);
|
|
170
188
|
expect(result.exitCode).toBe(0);
|
|
@@ -176,6 +194,7 @@ test("docs skill prints Cursor SKILL.md", async () => {
|
|
|
176
194
|
expect(result.stdout).not.toContain("mcp.json");
|
|
177
195
|
});
|
|
178
196
|
|
|
197
|
+
/** Docs skill help recommends configure. */
|
|
179
198
|
test("docs skill help recommends configure", async () => {
|
|
180
199
|
const presentation = cliPresentationRoot(docsFixture());
|
|
181
200
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
@@ -190,6 +209,7 @@ test("docs skill help recommends configure", async () => {
|
|
|
190
209
|
}
|
|
191
210
|
});
|
|
192
211
|
|
|
212
|
+
/** Presentation includes docs schema and skill. */
|
|
193
213
|
test("presentation includes docs schema and skill", () => {
|
|
194
214
|
const presentation = cliPresentationRoot(docsFixture());
|
|
195
215
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
@@ -201,6 +221,7 @@ test("presentation includes docs schema and skill", () => {
|
|
|
201
221
|
}
|
|
202
222
|
});
|
|
203
223
|
|
|
224
|
+
/** Completions offer docs subcommands. */
|
|
204
225
|
test("completions offer docs subcommands", () => {
|
|
205
226
|
const bash = completionBashScript(cliPresentationRoot(docsFixture()));
|
|
206
227
|
expect(bash).toContain("docs) echo");
|
|
@@ -210,6 +231,7 @@ test("completions offer docs subcommands", () => {
|
|
|
210
231
|
expect(bash).toContain("skill) echo");
|
|
211
232
|
});
|
|
212
233
|
|
|
234
|
+
/** Tests that generateMcpGuide includes schema URI and configure sync. */
|
|
213
235
|
test("generateMcpGuide includes schema URI and configure sync", () => {
|
|
214
236
|
const guide = generateMcpGuide(docsFixture(true));
|
|
215
237
|
expect(guide).toContain("myapp://schema");
|
|
@@ -223,6 +245,7 @@ test("generateMcpGuide includes schema URI and configure sync", () => {
|
|
|
223
245
|
expect(guide).toContain("ChatGPT");
|
|
224
246
|
});
|
|
225
247
|
|
|
248
|
+
/** Docs --save writes topic file. */
|
|
226
249
|
test("docs --save writes topic file", async () => {
|
|
227
250
|
const result = await new Cli(docsFixture()).invoke(["docs", "readme", "--save"]);
|
|
228
251
|
expect(result.exitCode).toBe(0);
|
|
@@ -232,6 +255,7 @@ test("docs --save writes topic file", async () => {
|
|
|
232
255
|
expect(text).not.toContain("Generated by");
|
|
233
256
|
});
|
|
234
257
|
|
|
258
|
+
/** Docs api --save prepends generated hint. */
|
|
235
259
|
test("docs api --save prepends generated hint", async () => {
|
|
236
260
|
const result = await new Cli(docsFixture()).invoke(["docs", "api", "--save"]);
|
|
237
261
|
expect(result.exitCode).toBe(0);
|
|
@@ -242,6 +266,7 @@ test("docs api --save prepends generated hint", async () => {
|
|
|
242
266
|
expect(text).toContain("CLI API reference");
|
|
243
267
|
});
|
|
244
268
|
|
|
269
|
+
/** Docs skill --save keeps frontmatter first. */
|
|
245
270
|
test("docs skill --save keeps frontmatter first", async () => {
|
|
246
271
|
const result = await new Cli(docsFixture()).invoke(["docs", "skill", "--save"]);
|
|
247
272
|
expect(result.exitCode).toBe(0);
|
|
@@ -252,6 +277,7 @@ test("docs skill --save keeps frontmatter first", async () => {
|
|
|
252
277
|
expect(text.indexOf(hint)).toBeGreaterThan(text.indexOf("---\n", 4));
|
|
253
278
|
});
|
|
254
279
|
|
|
280
|
+
/** Docs schema --save writes JSON file. */
|
|
255
281
|
test("docs schema --save writes JSON file", async () => {
|
|
256
282
|
const result = await new Cli(docsFixture()).invoke(["docs", "schema", "--save"]);
|
|
257
283
|
expect(result.exitCode).toBe(0);
|
|
@@ -262,6 +288,7 @@ test("docs schema --save writes JSON file", async () => {
|
|
|
262
288
|
expect(schema.key).toBe("myapp");
|
|
263
289
|
});
|
|
264
290
|
|
|
291
|
+
/** Tests that saveDocsTopic returns relative path. */
|
|
265
292
|
test("saveDocsTopic returns relative path", () => {
|
|
266
293
|
const path = saveDocsTopic(docsFixture(), "api");
|
|
267
294
|
expect(path).toBe("docs/api.md");
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for docs/mcp-resources module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { expect, test } from "bun:test";
|
|
2
6
|
import type { CliProgram } from "../types.ts";
|
|
3
7
|
import {
|
|
@@ -30,14 +34,17 @@ function fixture(opts?: { docs?: boolean; mcp?: boolean }): CliProgram {
|
|
|
30
34
|
};
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
/** DefaultDocsTopicResourceUri. */
|
|
33
38
|
test("defaultDocsTopicResourceUri", () => {
|
|
34
39
|
expect(defaultDocsTopicResourceUri("my_app", "readme")).toBe("my_app://docs/readme");
|
|
35
40
|
});
|
|
36
41
|
|
|
42
|
+
/** ResolveDocsTopicResourceUri sanitizes program key. */
|
|
37
43
|
test("resolveDocsTopicResourceUri sanitizes program key", () => {
|
|
38
44
|
expect(resolveDocsTopicResourceUri(fixture(), "readme")).toBe("my_app://docs/readme");
|
|
39
45
|
});
|
|
40
46
|
|
|
47
|
+
/** DocsMcpResources when docs and MCP enabled. */
|
|
41
48
|
test("docsMcpResources when docs and MCP enabled", () => {
|
|
42
49
|
const resources = docsMcpResources(fixture());
|
|
43
50
|
expect(resources.map((r) => r.uri)).toEqual(["my_app://docs/readme", "my_app://docs/arch"]);
|
|
@@ -47,14 +54,17 @@ test("docsMcpResources when docs and MCP enabled", () => {
|
|
|
47
54
|
expect(resources[0]?.load()).toBe("# Readme\n");
|
|
48
55
|
});
|
|
49
56
|
|
|
57
|
+
/** DocsMcpResources empty when docs disabled. */
|
|
50
58
|
test("docsMcpResources empty when docs disabled", () => {
|
|
51
59
|
expect(docsMcpResources(fixture({ docs: false }))).toEqual([]);
|
|
52
60
|
});
|
|
53
61
|
|
|
62
|
+
/** DocsMcpResources empty when MCP disabled. */
|
|
54
63
|
test("docsMcpResources empty when MCP disabled", () => {
|
|
55
64
|
expect(docsMcpResources(fixture({ mcp: false }))).toEqual([]);
|
|
56
65
|
});
|
|
57
66
|
|
|
67
|
+
/** Tests that reservedDocsTopicResourceUris matches docsMcpResources URIs. */
|
|
58
68
|
test("reservedDocsTopicResourceUris matches docsMcpResources URIs", () => {
|
|
59
69
|
const program = fixture();
|
|
60
70
|
expect(reservedDocsTopicResourceUris(program)).toEqual(
|
package/src/formats.test.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for formats module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { expect, test } from "bun:test";
|
|
2
6
|
import {
|
|
3
7
|
parseCommaList,
|
|
@@ -8,6 +12,7 @@ import {
|
|
|
8
12
|
} from "./formats.ts";
|
|
9
13
|
import { CliValueFormat } from "./types.ts";
|
|
10
14
|
|
|
15
|
+
/** ParseDurationMs parses minutes and hours. */
|
|
11
16
|
test("parseDurationMs parses minutes and hours", () => {
|
|
12
17
|
expect(parseDurationMs("30s")).toBe(30_000);
|
|
13
18
|
expect(parseDurationMs("5m")).toBe(5 * 60 * 1000);
|
|
@@ -15,21 +20,25 @@ test("parseDurationMs parses minutes and hours", () => {
|
|
|
15
20
|
expect(parseDurationMs("1d")).toBe(24 * 60 * 60 * 1000);
|
|
16
21
|
});
|
|
17
22
|
|
|
23
|
+
/** ParseCommaList splits and trims. */
|
|
18
24
|
test("parseCommaList splits and trims", () => {
|
|
19
25
|
expect(parseCommaList("a,b")).toEqual(["a", "b"]);
|
|
20
26
|
expect(parseCommaList(" a , b , ")).toEqual(["a", "b"]);
|
|
21
27
|
});
|
|
22
28
|
|
|
29
|
+
/** ParseDate validates calendar dates. */
|
|
23
30
|
test("parseDate validates calendar dates", () => {
|
|
24
31
|
expect(parseDate("2026-06-22")).toBe("2026-06-22");
|
|
25
32
|
expect(() => parseDate("2026-02-30")).toThrow();
|
|
26
33
|
});
|
|
27
34
|
|
|
35
|
+
/** ParseDateTime normalizes to UTC ISO. */
|
|
28
36
|
test("parseDateTime normalizes to UTC ISO", () => {
|
|
29
37
|
expect(parseDateTime("2026-06-22T15:00:00Z")).toBe("2026-06-22T15:00:00.000Z");
|
|
30
38
|
expect(() => parseDateTime("2026-06-22")).toThrow();
|
|
31
39
|
});
|
|
32
40
|
|
|
41
|
+
/** ValidateFormatValue rejects invalid duration. */
|
|
33
42
|
test("validateFormatValue rejects invalid duration", () => {
|
|
34
43
|
expect(() => validateFormatValue("nope", CliValueFormat.Duration)).toThrow();
|
|
35
44
|
});
|
package/src/headless.test.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for headless module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { expect, test } from "bun:test";
|
|
2
6
|
import {
|
|
3
7
|
formatDryRunMessage,
|
|
@@ -8,12 +12,14 @@ import {
|
|
|
8
12
|
wantsExplicitJson,
|
|
9
13
|
} from "./headless.ts";
|
|
10
14
|
|
|
15
|
+
/** WantsExplicitJson includes MCP invocation. */
|
|
11
16
|
test("wantsExplicitJson includes MCP invocation", () => {
|
|
12
17
|
expect(wantsExplicitJson({ invocation: "cli" }, false)).toBe(false);
|
|
13
18
|
expect(wantsExplicitJson({ invocation: "mcp" }, false)).toBe(true);
|
|
14
19
|
expect(wantsExplicitJson({ invocation: "cli" }, true)).toBe(true);
|
|
15
20
|
});
|
|
16
21
|
|
|
22
|
+
/** Tests that shouldRunHeadless is true for MCP and json. */
|
|
17
23
|
test("shouldRunHeadless is true for MCP and json", () => {
|
|
18
24
|
expect(shouldRunHeadless({ invocation: "mcp" }, false)).toBe(true);
|
|
19
25
|
expect(shouldRunHeadless({ invocation: "cli" }, true)).toBe(true);
|
|
@@ -21,6 +27,7 @@ test("shouldRunHeadless is true for MCP and json", () => {
|
|
|
21
27
|
expect(shouldRunHeadless({ invocation: "cli" }, false, false, false)).toBe(true);
|
|
22
28
|
});
|
|
23
29
|
|
|
30
|
+
/** Tests that shouldRunHeadlessWithPositionals requires positionals in non-tty. */
|
|
24
31
|
test("shouldRunHeadlessWithPositionals requires positionals in non-tty", () => {
|
|
25
32
|
expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, [], false, false)).toBe(
|
|
26
33
|
false,
|
|
@@ -30,6 +37,7 @@ test("shouldRunHeadlessWithPositionals requires positionals in non-tty", () => {
|
|
|
30
37
|
);
|
|
31
38
|
});
|
|
32
39
|
|
|
40
|
+
/** Tests that shouldRunHeadlessWithYes requires yes in non-tty. */
|
|
33
41
|
test("shouldRunHeadlessWithYes requires yes in non-tty", () => {
|
|
34
42
|
expect(
|
|
35
43
|
shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: true, hasRequiredArgs: true }, false),
|
|
@@ -46,11 +54,13 @@ test("shouldRunHeadlessWithYes requires yes in non-tty", () => {
|
|
|
46
54
|
).toBe(true);
|
|
47
55
|
});
|
|
48
56
|
|
|
57
|
+
/** Tests that formatDryRunMessage prefixes dry-run output. */
|
|
49
58
|
test("formatDryRunMessage prefixes dry-run output", () => {
|
|
50
59
|
expect(formatDryRunMessage("hello", false)).toBe("hello");
|
|
51
60
|
expect(formatDryRunMessage("hello", true)).toBe("[DRY RUN] hello");
|
|
52
61
|
});
|
|
53
62
|
|
|
63
|
+
/** Tests that requireYesInNonTty exits without yes in non-tty. */
|
|
54
64
|
test("requireYesInNonTty exits without yes in non-tty", () => {
|
|
55
65
|
const originalExit = process.exit;
|
|
56
66
|
let code: number | undefined;
|