argsbarg 5.0.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -2
- package/docs/cli-program.md +4 -4
- package/docs/config-schema.md +9 -9
- package/docs/configure.md +4 -0
- package/docs/mcp.md +1 -1
- package/examples/full-example/README.md +2 -2
- package/examples/full-example/src/program.ts +0 -3
- package/index.d.ts +12 -7
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +9 -14
- package/src/builtins/config.test.ts +4 -4
- package/src/builtins/config.ts +19 -22
- package/src/builtins/configure.ts +35 -4
- package/src/builtins/dispatch.ts +10 -15
- package/src/builtins/registry.ts +0 -2
- package/src/capabilities.ts +6 -12
- package/src/cli-tool/cli-smoke.test.ts +0 -3
- package/src/cli-tool/create.test.ts +0 -3
- package/src/cli-tool/full-example-capabilities.test.ts +0 -2
- package/src/config/context.test.ts +0 -1
- package/src/config/entry.ts +2 -2
- package/src/config/file.test.ts +0 -5
- package/src/config/resolve.test.ts +0 -7
- package/src/config/resolve.ts +2 -2
- package/src/config/validate.test.ts +0 -6
- package/src/configure/configure.test.ts +0 -11
- package/src/docs/api-guide.test.ts +0 -4
- package/src/docs/docs.test.ts +1 -21
- package/src/docs/mcp-resources.test.ts +0 -6
- package/src/formats.test.ts +0 -5
- package/src/headless.test.ts +0 -4
- package/src/headless.ts +5 -4
- package/src/hidden-mcpb.test.ts +0 -12
- package/src/install/binary-placement.test.ts +0 -5
- package/src/install/gh-release-update.test.ts +0 -5
- package/src/install/install-validate.test.ts +0 -5
- package/src/install/mcp-opencode.test.ts +0 -4
- package/src/install/targets.test.ts +0 -10
- package/src/invoke.test.ts +0 -7
- package/src/mcp/claude.test.ts +0 -4
- package/src/mcp/env.test.ts +0 -5
- package/src/mcp/zip.test.ts +0 -1
- package/src/mcp.integration.test.ts +0 -22
- package/src/parse.test.ts +0 -28
- package/src/schema.ts +1 -1
- package/src/types.ts +2 -2
|
@@ -59,13 +59,11 @@ describe("full-example template", () => {
|
|
|
59
59
|
expect(programSource).toContain("appConfig:");
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
/** Status command defines outputSchema. */
|
|
63
62
|
test("status command defines outputSchema", () => {
|
|
64
63
|
const statusSource = readFileSync(join(exampleRoot, "src/commands/status/command.ts"), "utf8");
|
|
65
64
|
expect(statusSource).toContain("outputSchema:");
|
|
66
65
|
});
|
|
67
66
|
|
|
68
|
-
/** ResolveCapabilities matches full sink shape. */
|
|
69
67
|
test("resolveCapabilities matches full sink shape", () => {
|
|
70
68
|
expect(resolveCapabilities(sinkProgram)).toEqual({
|
|
71
69
|
completion: true,
|
|
@@ -72,7 +72,6 @@ describe("config/context", () => {
|
|
|
72
72
|
expect(empty.dir).toBe(dirname(empty.path));
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
/** Tests that AppConfigSnapshot path uses OS default from program key. */
|
|
76
75
|
test("AppConfigSnapshot path uses OS default from program key", () => {
|
|
77
76
|
const ctx = createAppConfigSnapshot(program, {}, {});
|
|
78
77
|
expect(ctx.path).toContain("ctx_test");
|
package/src/config/entry.ts
CHANGED
|
@@ -29,7 +29,7 @@ export function configEntryRequired(
|
|
|
29
29
|
return true;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Whether prompts and `
|
|
32
|
+
/** Whether prompts and `configure get` should redact this entry. */
|
|
33
33
|
export function configEntrySensitive(key: string, entry: CliAppConfigEntry): boolean {
|
|
34
34
|
return entry.sensitive ?? defaultConfigEntrySensitive(key);
|
|
35
35
|
}
|
|
@@ -56,7 +56,7 @@ export function jsonSchemaRequiredKeys(
|
|
|
56
56
|
return new Set(required.filter((k): k is string => typeof k === "string"));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/** Whether built-in
|
|
59
|
+
/** Whether built-in `configure get` / `configure set` subcommands are enabled. */
|
|
60
60
|
export function configCommandsEnabled(program: CliProgram): boolean {
|
|
61
61
|
if (!program.appConfig) {
|
|
62
62
|
return false;
|
package/src/config/file.test.ts
CHANGED
|
@@ -46,7 +46,6 @@ function withHome<T>(fn: (home: string) => T): T {
|
|
|
46
46
|
|
|
47
47
|
/** Tests for config/file. */
|
|
48
48
|
describe("config/file", () => {
|
|
49
|
-
/** BuildProgramUserConfig from program.appConfig env entries. */
|
|
50
49
|
test("buildProgramUserConfig from program.appConfig env entries", () => {
|
|
51
50
|
const cfg = buildProgramUserConfig(program);
|
|
52
51
|
expect(cfg?.api_token).toMatchObject({
|
|
@@ -57,7 +56,6 @@ describe("config/file", () => {
|
|
|
57
56
|
expect(cfg?.port).toBeUndefined();
|
|
58
57
|
});
|
|
59
58
|
|
|
60
|
-
/** ResolveAppConfigPath uses config.json. */
|
|
61
59
|
test("resolveAppConfigPath uses config.json", () => {
|
|
62
60
|
withHome((home) => {
|
|
63
61
|
expect(resolveAppConfigPath(program)).toBe(
|
|
@@ -100,7 +98,6 @@ describe("config/file", () => {
|
|
|
100
98
|
}
|
|
101
99
|
});
|
|
102
100
|
|
|
103
|
-
/** Rejects unknown keys on read. */
|
|
104
101
|
test("rejects unknown keys on read", () => {
|
|
105
102
|
withHome(() => {
|
|
106
103
|
const configPath = resolveAppConfigPath(program);
|
|
@@ -110,7 +107,6 @@ describe("config/file", () => {
|
|
|
110
107
|
});
|
|
111
108
|
});
|
|
112
109
|
|
|
113
|
-
/** WriteAppConfigFile round-trip. */
|
|
114
110
|
test("writeAppConfigFile round-trip", () => {
|
|
115
111
|
withHome(() => {
|
|
116
112
|
writeAppConfigFile(program, { apiToken: "saved" });
|
|
@@ -119,7 +115,6 @@ describe("config/file", () => {
|
|
|
119
115
|
});
|
|
120
116
|
});
|
|
121
117
|
|
|
122
|
-
/** Tests that uninstallAppConfig removes config directory recursively. */
|
|
123
118
|
test("uninstallAppConfig removes config directory recursively", () => {
|
|
124
119
|
withHome(() => {
|
|
125
120
|
writeAppConfigFile(program, { apiToken: "saved" });
|
|
@@ -31,7 +31,6 @@ const program: CliProgram = {
|
|
|
31
31
|
|
|
32
32
|
/** Tests for config/resolve. */
|
|
33
33
|
describe("config/resolve", () => {
|
|
34
|
-
/** Tests that prefers env over file for mapped keys. */
|
|
35
34
|
test("prefers env over file for mapped keys", () => {
|
|
36
35
|
const prev = process.env.API_TOKEN;
|
|
37
36
|
process.env.API_TOKEN = "from-env";
|
|
@@ -44,7 +43,6 @@ describe("config/resolve", () => {
|
|
|
44
43
|
}
|
|
45
44
|
});
|
|
46
45
|
|
|
47
|
-
/** Tests that uses file when env empty. */
|
|
48
46
|
test("uses file when env empty", () => {
|
|
49
47
|
const prev = process.env.API_TOKEN;
|
|
50
48
|
delete process.env.API_TOKEN;
|
|
@@ -56,7 +54,6 @@ describe("config/resolve", () => {
|
|
|
56
54
|
}
|
|
57
55
|
});
|
|
58
56
|
|
|
59
|
-
/** Tests that empty string in env or file counts as missing. */
|
|
60
57
|
test("empty string in env or file counts as missing", () => {
|
|
61
58
|
const prev = process.env.API_TOKEN;
|
|
62
59
|
process.env.API_TOKEN = "";
|
|
@@ -69,7 +66,6 @@ describe("config/resolve", () => {
|
|
|
69
66
|
}
|
|
70
67
|
});
|
|
71
68
|
|
|
72
|
-
/** Applies jsonSchema default when file and env absent. */
|
|
73
69
|
test("applies jsonSchema default when file and env absent", () => {
|
|
74
70
|
const prev = process.env.API_TOKEN;
|
|
75
71
|
delete process.env.API_TOKEN;
|
|
@@ -82,7 +78,6 @@ describe("config/resolve", () => {
|
|
|
82
78
|
}
|
|
83
79
|
});
|
|
84
80
|
|
|
85
|
-
/** All-string mode uses entry.default. */
|
|
86
81
|
test("all-string mode uses entry.default", () => {
|
|
87
82
|
const stringProgram: CliProgram = {
|
|
88
83
|
...program,
|
|
@@ -96,7 +91,6 @@ describe("config/resolve", () => {
|
|
|
96
91
|
expect(resolved.greeting).toBe("world");
|
|
97
92
|
});
|
|
98
93
|
|
|
99
|
-
/** Tests that prefers captured host env over file when process.env was exported from file. */
|
|
100
94
|
test("prefers captured host env over file when process.env was exported from file", () => {
|
|
101
95
|
const hostEnv = { API_TOKEN: "from-host" };
|
|
102
96
|
process.env.API_TOKEN = "from-file-export";
|
|
@@ -108,7 +102,6 @@ describe("config/resolve", () => {
|
|
|
108
102
|
}
|
|
109
103
|
});
|
|
110
104
|
|
|
111
|
-
/** ExportConfigToEnv does not overwrite host env. */
|
|
112
105
|
test("exportConfigToEnv does not overwrite host env", () => {
|
|
113
106
|
const prev = process.env.API_TOKEN;
|
|
114
107
|
process.env.API_TOKEN = "from-host";
|
package/src/config/resolve.ts
CHANGED
|
@@ -227,7 +227,7 @@ export function formatMissingConfigMessage(program: CliProgram, keys: string[]):
|
|
|
227
227
|
return [
|
|
228
228
|
`Missing required configuration: ${list}`,
|
|
229
229
|
`Configure interactively: ${program.key} configure`,
|
|
230
|
-
`Or set via: ${program.key}
|
|
230
|
+
`Or set via: ${program.key} configure set <key> <value>`,
|
|
231
231
|
`Config file: ${path}`,
|
|
232
232
|
`See: ${program.key} docs mcp`,
|
|
233
233
|
].join("\n");
|
|
@@ -240,7 +240,7 @@ export function formatMcpMissingConfigMessage(program: CliProgram, keys: string[
|
|
|
240
240
|
return [
|
|
241
241
|
`Missing required configuration: ${list}`,
|
|
242
242
|
`Configure: ${program.key} configure`,
|
|
243
|
-
`Or set via: ${program.key}
|
|
243
|
+
`Or set via: ${program.key} configure set`,
|
|
244
244
|
`Config file: ${path}`,
|
|
245
245
|
].join("\n");
|
|
246
246
|
}
|
|
@@ -23,7 +23,6 @@ const rootSchema = {
|
|
|
23
23
|
|
|
24
24
|
/** Tests for config/validate. */
|
|
25
25
|
describe("config/validate", () => {
|
|
26
|
-
/** Tests that accepts valid document. */
|
|
27
26
|
test("accepts valid document", () => {
|
|
28
27
|
const result = validateConfigDocument(
|
|
29
28
|
{ apiToken: "x", maxRetries: 3, prefs: { ttl: 3600 } },
|
|
@@ -33,14 +32,12 @@ describe("config/validate", () => {
|
|
|
33
32
|
expect(result.errors).toEqual([]);
|
|
34
33
|
});
|
|
35
34
|
|
|
36
|
-
/** Rejects missing required property. */
|
|
37
35
|
test("rejects missing required property", () => {
|
|
38
36
|
const result = validateConfigDocument({ apiToken: "x" }, rootSchema);
|
|
39
37
|
expect(result.valid).toBe(false);
|
|
40
38
|
expect(result.errors.some((e) => e.includes("maxRetries"))).toBe(true);
|
|
41
39
|
});
|
|
42
40
|
|
|
43
|
-
/** Rejects unknown property when additionalProperties is false. */
|
|
44
41
|
test("rejects unknown property when additionalProperties is false", () => {
|
|
45
42
|
const result = validateConfigDocument(
|
|
46
43
|
{ apiToken: "x", maxRetries: 1, extra: true },
|
|
@@ -50,19 +47,16 @@ describe("config/validate", () => {
|
|
|
50
47
|
expect(result.errors.some((e) => e.includes("extra"))).toBe(true);
|
|
51
48
|
});
|
|
52
49
|
|
|
53
|
-
/** Rejects type mismatch. */
|
|
54
50
|
test("rejects type mismatch", () => {
|
|
55
51
|
const result = validateConfigDocument({ apiToken: "x", maxRetries: "nope" }, rootSchema);
|
|
56
52
|
expect(result.valid).toBe(false);
|
|
57
53
|
});
|
|
58
54
|
|
|
59
|
-
/** ParseConfigSetValue coerces number and boolean. */
|
|
60
55
|
test("parseConfigSetValue coerces number and boolean", () => {
|
|
61
56
|
expect(parseConfigSetValue("5", { type: "integer" }, rootSchema, false)).toBe(5);
|
|
62
57
|
expect(parseConfigSetValue("true", { type: "boolean" }, rootSchema, false)).toBe(true);
|
|
63
58
|
});
|
|
64
59
|
|
|
65
|
-
/** ParseConfigSetValue requires --json for objects. */
|
|
66
60
|
test("parseConfigSetValue requires --json for objects", () => {
|
|
67
61
|
expect(() => parseConfigSetValue('{"ttl":1}', { type: "object" }, rootSchema, false)).toThrow(
|
|
68
62
|
/--json/,
|
|
@@ -48,19 +48,16 @@ afterEach(() => {
|
|
|
48
48
|
|
|
49
49
|
/** Tests for configure opts. */
|
|
50
50
|
describe("configure opts", () => {
|
|
51
|
-
/** Validate rejects multiple modes. */
|
|
52
51
|
test("validate rejects multiple modes", () => {
|
|
53
52
|
const opts = parseConfigureOpts({ sync: "1", status: "1" });
|
|
54
53
|
expect(validateConfigureOpts(opts)).toContain("only one");
|
|
55
54
|
});
|
|
56
55
|
|
|
57
|
-
/** Sync requires --yes. */
|
|
58
56
|
test("sync requires --yes", () => {
|
|
59
57
|
const opts = parseConfigureOpts({ sync: "1" });
|
|
60
58
|
expect(validateConfigureOpts(opts)).toContain("--yes");
|
|
61
59
|
});
|
|
62
60
|
|
|
63
|
-
/** Remove-all requires --yes. */
|
|
64
61
|
test("remove-all requires --yes", () => {
|
|
65
62
|
const opts = parseConfigureOpts({ "remove-all": "1" });
|
|
66
63
|
expect(validateConfigureOpts(opts)).toContain("--yes");
|
|
@@ -69,14 +66,12 @@ describe("configure opts", () => {
|
|
|
69
66
|
|
|
70
67
|
/** Tests for install paths. */
|
|
71
68
|
describe("install paths", () => {
|
|
72
|
-
/** ResolveInstallPaths includes skill and mcp paths. */
|
|
73
69
|
test("resolveInstallPaths includes skill and mcp paths", () => {
|
|
74
70
|
const paths = resolveInstallPaths(fixture);
|
|
75
71
|
expect(paths.cursorSkillDir).toContain(".cursor/skills");
|
|
76
72
|
expect(paths.cursorMcpPath).toContain("mcp.json");
|
|
77
73
|
});
|
|
78
74
|
|
|
79
|
-
/** Claude desktop mcp path on darwin. */
|
|
80
75
|
test("claude desktop mcp path on darwin", () => {
|
|
81
76
|
const prev = process.platform;
|
|
82
77
|
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
@@ -110,7 +105,6 @@ describe("detect installed", () => {
|
|
|
110
105
|
|
|
111
106
|
/** Tests for sync plan. */
|
|
112
107
|
describe("sync plan", () => {
|
|
113
|
-
/** BuildUpdatePlan greenfield includes agent targets. */
|
|
114
108
|
test("buildUpdatePlan greenfield includes agent targets", () => {
|
|
115
109
|
const paths = resolveInstallPaths(fixture);
|
|
116
110
|
const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
|
|
@@ -118,7 +112,6 @@ describe("sync plan", () => {
|
|
|
118
112
|
expect(plan.some((a) => a.kind === "app")).toBe(false);
|
|
119
113
|
});
|
|
120
114
|
|
|
121
|
-
/** BuildInstallPlan --all omits app self-install. */
|
|
122
115
|
test("buildInstallPlan --all omits app self-install", () => {
|
|
123
116
|
const paths = resolveInstallPaths(fixture);
|
|
124
117
|
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
|
|
@@ -126,7 +119,6 @@ describe("sync plan", () => {
|
|
|
126
119
|
expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
|
|
127
120
|
});
|
|
128
121
|
|
|
129
|
-
/** BuildInstallPlan respects configure.agentIntegration skill. */
|
|
130
122
|
test("buildInstallPlan respects configure.agentIntegration skill", () => {
|
|
131
123
|
const skillApp: CliProgram = {
|
|
132
124
|
...fixture,
|
|
@@ -142,7 +134,6 @@ describe("sync plan", () => {
|
|
|
142
134
|
|
|
143
135
|
/** Tests for remove plan. */
|
|
144
136
|
describe("remove plan", () => {
|
|
145
|
-
/** BuildUninstallPlan --all with nothing installed is empty. */
|
|
146
137
|
test("buildUninstallPlan --all with nothing installed is empty", () => {
|
|
147
138
|
const paths = resolveInstallPaths(fixture);
|
|
148
139
|
const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
|
|
@@ -152,7 +143,6 @@ describe("remove plan", () => {
|
|
|
152
143
|
|
|
153
144
|
/** Tests for app config wizard. */
|
|
154
145
|
describe("app config wizard", () => {
|
|
155
|
-
/** Tests that appConfigHasEntries when entries exist. */
|
|
156
146
|
test("appConfigHasEntries when entries exist", () => {
|
|
157
147
|
expect(
|
|
158
148
|
appConfigHasEntries({
|
|
@@ -162,7 +152,6 @@ describe("app config wizard", () => {
|
|
|
162
152
|
).toBe(true);
|
|
163
153
|
});
|
|
164
154
|
|
|
165
|
-
/** Tests that appConfigHasEntries false for empty entries or missing appConfig. */
|
|
166
155
|
test("appConfigHasEntries false for empty entries or missing appConfig", () => {
|
|
167
156
|
expect(appConfigHasEntries({ ...fixture, appConfig: { entries: {} } })).toBe(false);
|
|
168
157
|
expect(appConfigHasEntries(fixture)).toBe(false);
|
|
@@ -49,7 +49,6 @@ const nestedFixture: CliProgram = {
|
|
|
49
49
|
],
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
/** Tests that generateApiGuideBody matches command section of full API guide. */
|
|
53
52
|
test("generateApiGuideBody matches command section of full API guide", () => {
|
|
54
53
|
const body = generateApiGuideBody(nestedFixture);
|
|
55
54
|
const full = generateApiGuide(nestedFixture);
|
|
@@ -58,7 +57,6 @@ test("generateApiGuideBody matches command section of full API guide", () => {
|
|
|
58
57
|
expect(body).not.toContain("CLI API reference");
|
|
59
58
|
});
|
|
60
59
|
|
|
61
|
-
/** Tests that generateApiGuide covers the same command keys as cliSchemaExport. */
|
|
62
60
|
test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
|
|
63
61
|
const md = generateApiGuide(nestedFixture);
|
|
64
62
|
const schema = cliSchemaExport(nestedFixture);
|
|
@@ -68,7 +66,6 @@ test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
|
|
|
68
66
|
expect(schema.commands?.map((c) => c.key)).toEqual(["stat"]);
|
|
69
67
|
});
|
|
70
68
|
|
|
71
|
-
/** Tests that generateApiGuide resolves program key in install notes. */
|
|
72
69
|
test("generateApiGuide resolves program key in install notes", () => {
|
|
73
70
|
const fixture: CliProgram = {
|
|
74
71
|
key: "myapp",
|
|
@@ -82,7 +79,6 @@ test("generateApiGuide resolves program key in install notes", () => {
|
|
|
82
79
|
expect(md).not.toContain("Upgrade to latest release");
|
|
83
80
|
});
|
|
84
81
|
|
|
85
|
-
/** Tests that generateApiGuide mentions Homebrew upgrade. */
|
|
86
82
|
test("generateApiGuide mentions Homebrew upgrade", () => {
|
|
87
83
|
const fixture: CliProgram = {
|
|
88
84
|
key: "myapp",
|
package/src/docs/docs.test.ts
CHANGED
|
@@ -83,26 +83,22 @@ test("docs rejects reserved topic keys", () => {
|
|
|
83
83
|
expect(() => cliValidateProgram(root)).toThrow(/reserved/);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
/** DocsEffectiveDefaultTopic uses first topic key. */
|
|
87
86
|
test("docsEffectiveDefaultTopic uses first topic key", () => {
|
|
88
87
|
expect(docsEffectiveDefaultTopic(docsFixture().docs!)).toBe("readme");
|
|
89
88
|
});
|
|
90
89
|
|
|
91
|
-
/** Bare docs prints first topic via Cli.invoke. */
|
|
92
90
|
test("bare docs prints first topic via Cli.invoke", async () => {
|
|
93
91
|
const result = await new Cli(docsFixture()).invoke(["docs"]);
|
|
94
92
|
expect(result.exitCode).toBe(0);
|
|
95
93
|
expect(result.stdout).toContain("Hello README");
|
|
96
94
|
});
|
|
97
95
|
|
|
98
|
-
/** Docs readme prints bundled text. */
|
|
99
96
|
test("docs readme prints bundled text", async () => {
|
|
100
97
|
const result = await new Cli(docsFixture()).invoke(["docs", "readme"]);
|
|
101
98
|
expect(result.exitCode).toBe(0);
|
|
102
99
|
expect(result.stdout).toContain("Hello README");
|
|
103
100
|
});
|
|
104
101
|
|
|
105
|
-
/** Docs defaultTopic override. */
|
|
106
102
|
test("docs defaultTopic override", async () => {
|
|
107
103
|
const root = docsFixture();
|
|
108
104
|
root.docs!.defaultTopic = "arch";
|
|
@@ -110,7 +106,6 @@ test("docs defaultTopic override", async () => {
|
|
|
110
106
|
expect(result.stdout).toContain("Architecture");
|
|
111
107
|
});
|
|
112
108
|
|
|
113
|
-
/** Docs mcp when MCP enabled. */
|
|
114
109
|
test("docs mcp when MCP enabled", async () => {
|
|
115
110
|
const result = await new Cli(docsFixture(true)).invoke(["docs", "mcp"]);
|
|
116
111
|
expect(result.exitCode).toBe(0);
|
|
@@ -120,13 +115,11 @@ test("docs mcp when MCP enabled", async () => {
|
|
|
120
115
|
expect(result.stdout).toContain("configure --sync --yes");
|
|
121
116
|
});
|
|
122
117
|
|
|
123
|
-
/** Docs rejects unknown subcommand. */
|
|
124
118
|
test("docs rejects unknown subcommand", async () => {
|
|
125
119
|
const result = await new Cli(docsFixture()).invoke(["docs", "all"]);
|
|
126
120
|
expect(result.exitCode).not.toBe(0);
|
|
127
121
|
});
|
|
128
122
|
|
|
129
|
-
/** Docs mcp absent from router when MCP disabled. */
|
|
130
123
|
test("docs mcp absent from router when MCP disabled", async () => {
|
|
131
124
|
const root = docsFixture(false);
|
|
132
125
|
const presentation = cliPresentationRoot(root);
|
|
@@ -139,7 +132,6 @@ test("docs mcp absent from router when MCP disabled", async () => {
|
|
|
139
132
|
expect(result.exitCode).not.toBe(0);
|
|
140
133
|
});
|
|
141
134
|
|
|
142
|
-
/** Presentation includes docs subtree. */
|
|
143
135
|
test("presentation includes docs subtree", () => {
|
|
144
136
|
const presentation = cliPresentationRoot(docsFixture());
|
|
145
137
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
@@ -149,7 +141,6 @@ test("presentation includes docs subtree", () => {
|
|
|
149
141
|
).toBe(true);
|
|
150
142
|
});
|
|
151
143
|
|
|
152
|
-
/** Docs schema prints JSON. */
|
|
153
144
|
test("docs schema prints JSON", async () => {
|
|
154
145
|
const result = await new Cli(docsFixture()).invoke(["docs", "schema"]);
|
|
155
146
|
expect(result.exitCode).toBe(0);
|
|
@@ -158,7 +149,6 @@ test("docs schema prints JSON", async () => {
|
|
|
158
149
|
expect(schema.commands.some((c: { key: string }) => c.key === "run")).toBe(true);
|
|
159
150
|
});
|
|
160
151
|
|
|
161
|
-
/** Docs api prints markdown reference. */
|
|
162
152
|
test("docs api prints markdown reference", async () => {
|
|
163
153
|
const result = await new Cli(docsFixture()).invoke(["docs", "api"]);
|
|
164
154
|
expect(result.exitCode).toBe(0);
|
|
@@ -168,7 +158,6 @@ test("docs api prints markdown reference", async () => {
|
|
|
168
158
|
expect(result.stdout).toContain("myapp docs schema");
|
|
169
159
|
});
|
|
170
160
|
|
|
171
|
-
/** Tests that skipsRequiredAppConfigExit includes docs and config builtins. */
|
|
172
161
|
test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
|
|
173
162
|
const program = {
|
|
174
163
|
...docsFixture(),
|
|
@@ -178,11 +167,10 @@ test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
|
|
|
178
167
|
};
|
|
179
168
|
const caps = resolveCapabilities(program);
|
|
180
169
|
expect(skipsRequiredAppConfigExit(["docs", "api"], caps)).toBe(true);
|
|
181
|
-
expect(skipsRequiredAppConfigExit(["
|
|
170
|
+
expect(skipsRequiredAppConfigExit(["configure", "get"], caps)).toBe(true);
|
|
182
171
|
expect(skipsRequiredAppConfigExit(["run"], caps)).toBe(false);
|
|
183
172
|
});
|
|
184
173
|
|
|
185
|
-
/** Docs skill prints Cursor SKILL.md. */
|
|
186
174
|
test("docs skill prints Cursor SKILL.md", async () => {
|
|
187
175
|
const result = await new Cli(docsFixture()).invoke(["docs", "skill"]);
|
|
188
176
|
expect(result.exitCode).toBe(0);
|
|
@@ -209,7 +197,6 @@ test("docs skill help recommends configure", async () => {
|
|
|
209
197
|
}
|
|
210
198
|
});
|
|
211
199
|
|
|
212
|
-
/** Presentation includes docs schema and skill. */
|
|
213
200
|
test("presentation includes docs schema and skill", () => {
|
|
214
201
|
const presentation = cliPresentationRoot(docsFixture());
|
|
215
202
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
@@ -221,7 +208,6 @@ test("presentation includes docs schema and skill", () => {
|
|
|
221
208
|
}
|
|
222
209
|
});
|
|
223
210
|
|
|
224
|
-
/** Completions offer docs subcommands. */
|
|
225
211
|
test("completions offer docs subcommands", () => {
|
|
226
212
|
const bash = completionBashScript(cliPresentationRoot(docsFixture()));
|
|
227
213
|
expect(bash).toContain("docs) echo");
|
|
@@ -231,7 +217,6 @@ test("completions offer docs subcommands", () => {
|
|
|
231
217
|
expect(bash).toContain("skill) echo");
|
|
232
218
|
});
|
|
233
219
|
|
|
234
|
-
/** Tests that generateMcpGuide includes schema URI and configure sync. */
|
|
235
220
|
test("generateMcpGuide includes schema URI and configure sync", () => {
|
|
236
221
|
const guide = generateMcpGuide(docsFixture(true));
|
|
237
222
|
expect(guide).toContain("myapp://schema");
|
|
@@ -245,7 +230,6 @@ test("generateMcpGuide includes schema URI and configure sync", () => {
|
|
|
245
230
|
expect(guide).toContain("ChatGPT");
|
|
246
231
|
});
|
|
247
232
|
|
|
248
|
-
/** Docs --save writes topic file. */
|
|
249
233
|
test("docs --save writes topic file", async () => {
|
|
250
234
|
const result = await new Cli(docsFixture()).invoke(["docs", "readme", "--save"]);
|
|
251
235
|
expect(result.exitCode).toBe(0);
|
|
@@ -255,7 +239,6 @@ test("docs --save writes topic file", async () => {
|
|
|
255
239
|
expect(text).not.toContain("Generated by");
|
|
256
240
|
});
|
|
257
241
|
|
|
258
|
-
/** Docs api --save prepends generated hint. */
|
|
259
242
|
test("docs api --save prepends generated hint", async () => {
|
|
260
243
|
const result = await new Cli(docsFixture()).invoke(["docs", "api", "--save"]);
|
|
261
244
|
expect(result.exitCode).toBe(0);
|
|
@@ -266,7 +249,6 @@ test("docs api --save prepends generated hint", async () => {
|
|
|
266
249
|
expect(text).toContain("CLI API reference");
|
|
267
250
|
});
|
|
268
251
|
|
|
269
|
-
/** Docs skill --save keeps frontmatter first. */
|
|
270
252
|
test("docs skill --save keeps frontmatter first", async () => {
|
|
271
253
|
const result = await new Cli(docsFixture()).invoke(["docs", "skill", "--save"]);
|
|
272
254
|
expect(result.exitCode).toBe(0);
|
|
@@ -277,7 +259,6 @@ test("docs skill --save keeps frontmatter first", async () => {
|
|
|
277
259
|
expect(text.indexOf(hint)).toBeGreaterThan(text.indexOf("---\n", 4));
|
|
278
260
|
});
|
|
279
261
|
|
|
280
|
-
/** Docs schema --save writes JSON file. */
|
|
281
262
|
test("docs schema --save writes JSON file", async () => {
|
|
282
263
|
const result = await new Cli(docsFixture()).invoke(["docs", "schema", "--save"]);
|
|
283
264
|
expect(result.exitCode).toBe(0);
|
|
@@ -288,7 +269,6 @@ test("docs schema --save writes JSON file", async () => {
|
|
|
288
269
|
expect(schema.key).toBe("myapp");
|
|
289
270
|
});
|
|
290
271
|
|
|
291
|
-
/** Tests that saveDocsTopic returns relative path. */
|
|
292
272
|
test("saveDocsTopic returns relative path", () => {
|
|
293
273
|
const path = saveDocsTopic(docsFixture(), "api");
|
|
294
274
|
expect(path).toBe("docs/api.md");
|
|
@@ -34,17 +34,14 @@ function fixture(opts?: { docs?: boolean; mcp?: boolean }): CliProgram {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/** DefaultDocsTopicResourceUri. */
|
|
38
37
|
test("defaultDocsTopicResourceUri", () => {
|
|
39
38
|
expect(defaultDocsTopicResourceUri("my_app", "readme")).toBe("my_app://docs/readme");
|
|
40
39
|
});
|
|
41
40
|
|
|
42
|
-
/** ResolveDocsTopicResourceUri sanitizes program key. */
|
|
43
41
|
test("resolveDocsTopicResourceUri sanitizes program key", () => {
|
|
44
42
|
expect(resolveDocsTopicResourceUri(fixture(), "readme")).toBe("my_app://docs/readme");
|
|
45
43
|
});
|
|
46
44
|
|
|
47
|
-
/** DocsMcpResources when docs and MCP enabled. */
|
|
48
45
|
test("docsMcpResources when docs and MCP enabled", () => {
|
|
49
46
|
const resources = docsMcpResources(fixture());
|
|
50
47
|
expect(resources.map((r) => r.uri)).toEqual(["my_app://docs/readme", "my_app://docs/arch"]);
|
|
@@ -54,17 +51,14 @@ test("docsMcpResources when docs and MCP enabled", () => {
|
|
|
54
51
|
expect(resources[0]?.load()).toBe("# Readme\n");
|
|
55
52
|
});
|
|
56
53
|
|
|
57
|
-
/** DocsMcpResources empty when docs disabled. */
|
|
58
54
|
test("docsMcpResources empty when docs disabled", () => {
|
|
59
55
|
expect(docsMcpResources(fixture({ docs: false }))).toEqual([]);
|
|
60
56
|
});
|
|
61
57
|
|
|
62
|
-
/** DocsMcpResources empty when MCP disabled. */
|
|
63
58
|
test("docsMcpResources empty when MCP disabled", () => {
|
|
64
59
|
expect(docsMcpResources(fixture({ mcp: false }))).toEqual([]);
|
|
65
60
|
});
|
|
66
61
|
|
|
67
|
-
/** Tests that reservedDocsTopicResourceUris matches docsMcpResources URIs. */
|
|
68
62
|
test("reservedDocsTopicResourceUris matches docsMcpResources URIs", () => {
|
|
69
63
|
const program = fixture();
|
|
70
64
|
expect(reservedDocsTopicResourceUris(program)).toEqual(
|
package/src/formats.test.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
} from "./formats.ts";
|
|
13
13
|
import { CliValueFormat } from "./types.ts";
|
|
14
14
|
|
|
15
|
-
/** ParseDurationMs parses minutes and hours. */
|
|
16
15
|
test("parseDurationMs parses minutes and hours", () => {
|
|
17
16
|
expect(parseDurationMs("30s")).toBe(30_000);
|
|
18
17
|
expect(parseDurationMs("5m")).toBe(5 * 60 * 1000);
|
|
@@ -20,25 +19,21 @@ test("parseDurationMs parses minutes and hours", () => {
|
|
|
20
19
|
expect(parseDurationMs("1d")).toBe(24 * 60 * 60 * 1000);
|
|
21
20
|
});
|
|
22
21
|
|
|
23
|
-
/** ParseCommaList splits and trims. */
|
|
24
22
|
test("parseCommaList splits and trims", () => {
|
|
25
23
|
expect(parseCommaList("a,b")).toEqual(["a", "b"]);
|
|
26
24
|
expect(parseCommaList(" a , b , ")).toEqual(["a", "b"]);
|
|
27
25
|
});
|
|
28
26
|
|
|
29
|
-
/** ParseDate validates calendar dates. */
|
|
30
27
|
test("parseDate validates calendar dates", () => {
|
|
31
28
|
expect(parseDate("2026-06-22")).toBe("2026-06-22");
|
|
32
29
|
expect(() => parseDate("2026-02-30")).toThrow();
|
|
33
30
|
});
|
|
34
31
|
|
|
35
|
-
/** ParseDateTime normalizes to UTC ISO. */
|
|
36
32
|
test("parseDateTime normalizes to UTC ISO", () => {
|
|
37
33
|
expect(parseDateTime("2026-06-22T15:00:00Z")).toBe("2026-06-22T15:00:00.000Z");
|
|
38
34
|
expect(() => parseDateTime("2026-06-22")).toThrow();
|
|
39
35
|
});
|
|
40
36
|
|
|
41
|
-
/** ValidateFormatValue rejects invalid duration. */
|
|
42
37
|
test("validateFormatValue rejects invalid duration", () => {
|
|
43
38
|
expect(() => validateFormatValue("nope", CliValueFormat.Duration)).toThrow();
|
|
44
39
|
});
|
package/src/headless.test.ts
CHANGED
|
@@ -12,14 +12,12 @@ import {
|
|
|
12
12
|
wantsExplicitJson,
|
|
13
13
|
} from "./headless.ts";
|
|
14
14
|
|
|
15
|
-
/** WantsExplicitJson includes MCP invocation. */
|
|
16
15
|
test("wantsExplicitJson includes MCP invocation", () => {
|
|
17
16
|
expect(wantsExplicitJson({ invocation: "cli" }, false)).toBe(false);
|
|
18
17
|
expect(wantsExplicitJson({ invocation: "mcp" }, false)).toBe(true);
|
|
19
18
|
expect(wantsExplicitJson({ invocation: "cli" }, true)).toBe(true);
|
|
20
19
|
});
|
|
21
20
|
|
|
22
|
-
/** Tests that shouldRunHeadless is true for MCP and json. */
|
|
23
21
|
test("shouldRunHeadless is true for MCP and json", () => {
|
|
24
22
|
expect(shouldRunHeadless({ invocation: "mcp" }, false)).toBe(true);
|
|
25
23
|
expect(shouldRunHeadless({ invocation: "cli" }, true)).toBe(true);
|
|
@@ -27,7 +25,6 @@ test("shouldRunHeadless is true for MCP and json", () => {
|
|
|
27
25
|
expect(shouldRunHeadless({ invocation: "cli" }, false, false, false)).toBe(true);
|
|
28
26
|
});
|
|
29
27
|
|
|
30
|
-
/** Tests that shouldRunHeadlessWithPositionals requires positionals in non-tty. */
|
|
31
28
|
test("shouldRunHeadlessWithPositionals requires positionals in non-tty", () => {
|
|
32
29
|
expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, [], false, false)).toBe(
|
|
33
30
|
false,
|
|
@@ -54,7 +51,6 @@ test("shouldRunHeadlessWithYes requires yes in non-tty", () => {
|
|
|
54
51
|
).toBe(true);
|
|
55
52
|
});
|
|
56
53
|
|
|
57
|
-
/** Tests that formatDryRunMessage prefixes dry-run output. */
|
|
58
54
|
test("formatDryRunMessage prefixes dry-run output", () => {
|
|
59
55
|
expect(formatDryRunMessage("hello", false)).toBe("hello");
|
|
60
56
|
expect(formatDryRunMessage("hello", true)).toBe("[DRY RUN] hello");
|
package/src/headless.ts
CHANGED
|
@@ -57,14 +57,15 @@ export function shouldRunHeadlessWithYes(
|
|
|
57
57
|
return opts.yes && opts.hasRequiredArgs;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
* Exits when non-interactive mode is used without `--yes`.
|
|
62
|
-
* @param hint - Command-specific guidance appended to the error
|
|
63
|
-
*/
|
|
60
|
+
/** Exits when non-interactive mode is used without `--yes`. */
|
|
64
61
|
export function requireYesInNonTty(
|
|
62
|
+
/** True when `--yes` was passed on the command line. */
|
|
65
63
|
yes: boolean,
|
|
64
|
+
/** Command-specific guidance appended to the error message. */
|
|
66
65
|
hint: string,
|
|
66
|
+
/** When true, skip the check (dry-run preview). */
|
|
67
67
|
dryRun = false,
|
|
68
|
+
/** Injectable TTY probe for tests. */
|
|
68
69
|
interactive: boolean = isInteractiveTty,
|
|
69
70
|
): void {
|
|
70
71
|
if (dryRun) return;
|