argsbarg 6.1.2 → 6.1.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 +65 -1
- package/README.md +17 -19
- package/bin/argsbarg +10 -0
- package/docs/README.md +4 -3
- package/docs/ai-skills.md +4 -2
- package/docs/bundled-docs.md +50 -25
- package/docs/cli-program.md +52 -10
- package/docs/config-schema.md +10 -11
- package/docs/configure.md +2 -0
- package/docs/decisions.md +40 -0
- package/docs/developing.md +43 -5
- package/docs/http-server.md +171 -0
- package/docs/json-schema-subset.md +51 -0
- package/docs/mcp.md +4 -2
- package/docs/output-schema.md +55 -62
- package/examples/formats.ts +6 -6
- package/examples/full-example/Formula/full-example.rb +35 -0
- package/examples/full-example/README.md +20 -21
- package/examples/full-example/docs/README.md +1 -1
- package/examples/full-example/docs/cli-schema.json +1790 -98
- package/examples/full-example/docs/cli.md +1990 -0
- package/examples/full-example/docs/http.md +28 -29
- package/examples/full-example/docs/mcp.md +8 -22
- package/examples/full-example/docs/openapi.json +783 -50
- package/examples/full-example/docs/skill.md +10 -10
- package/examples/full-example/justfile +11 -1
- package/examples/full-example/src/commands/render-json/__generated__/RenderJsonInputSchema.json +15 -0
- package/examples/full-example/src/commands/render-json/__generated__/index.ts +5 -0
- package/examples/full-example/src/commands/render-json/command.test.ts +46 -0
- package/examples/full-example/src/commands/render-json/command.ts +30 -0
- package/examples/full-example/src/commands/render-json/types.ts +9 -0
- package/examples/full-example/src/commands/status/__generated__/StatusJsonOutputSchema.json +15 -0
- package/examples/full-example/src/commands/status/__generated__/index.ts +2 -2
- package/examples/full-example/src/commands/status/command.ts +5 -13
- package/examples/full-example/src/commands/status/types.ts +1 -14
- package/examples/full-example/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +15 -0
- package/examples/full-example/src/commands/workspaces/__generated__/index.ts +5 -0
- package/examples/full-example/src/commands/workspaces/command.test.ts +58 -0
- package/examples/full-example/src/commands/workspaces/command.ts +94 -0
- package/examples/full-example/src/commands/workspaces/types.ts +6 -0
- package/examples/full-example/src/db/index.test.ts +86 -0
- package/examples/full-example/src/db/index.ts +101 -0
- package/examples/full-example/src/db/migrate.test.ts +35 -0
- package/examples/full-example/src/db/migrate.ts +69 -0
- package/examples/full-example/src/db/migrations/001_workspaces.sql +6 -0
- package/examples/full-example/src/db/tables/workspaces.ts +66 -0
- package/examples/full-example/src/program.ts +11 -36
- package/examples/full-example/src/types/argsbarg.d.ts +11 -0
- package/examples/full-example/src/types/md.d.ts +4 -0
- package/examples/full-example/tsconfig.json +5 -2
- package/examples/mcp-test.ts +1 -2
- package/examples/minimal.ts +1 -7
- package/examples/nested.ts +1 -2
- package/examples/option-required.ts +1 -1
- package/examples/servers.ts +4 -5
- package/index.d.ts +431 -136
- package/package.json +19 -2
- package/src/builtins/builtins.test.ts +7 -7
- package/src/builtins/completion-bash.ts +1 -1
- package/src/builtins/completion-fish.ts +1 -1
- package/src/builtins/completion-group.ts +4 -4
- package/src/builtins/completion-simulate-shared.ts +9 -0
- package/src/builtins/completion-zsh.ts +1 -1
- package/src/builtins/config.test.ts +3 -3
- package/src/builtins/config.ts +9 -9
- package/src/builtins/configure-copy.ts +2 -2
- package/src/builtins/configure.ts +4 -4
- package/src/builtins/dispatch.ts +19 -18
- package/src/builtins/export.ts +7 -5
- package/src/builtins/http.ts +68 -0
- package/src/builtins/mcp.ts +28 -4
- package/src/builtins/presentation.ts +6 -6
- package/src/builtins/registry.ts +6 -6
- package/src/builtins/scopes.ts +2 -2
- package/src/builtins/version.ts +1 -1
- package/src/cli-tool/full-example-capabilities.test.ts +10 -15
- package/src/cli-tool/main.ts +1 -1
- package/src/cli-tool/program.ts +3 -2
- package/src/cli-tool/prompt.ts +1 -1
- package/src/cli-tool/run-schemagen.ts +1 -3
- package/src/cli-tool/schemagen/cleanup.ts +6 -7
- package/src/cli-tool/schemagen/discover-schema-roots.ts +66 -120
- package/src/cli-tool/schemagen/index.ts +2 -2
- package/src/cli-tool/schemagen/names.ts +8 -13
- package/src/cli-tool/schemagen/run.ts +21 -28
- package/src/cli-tool/schemagen/schemagen.test.ts +136 -46
- package/src/config/bindings.test.ts +1 -1
- package/src/config/bindings.ts +1 -1
- package/src/config/bootstrap.test.ts +1 -1
- package/src/config/bootstrap.ts +36 -4
- package/src/config/context.test.ts +1 -1
- package/src/config/context.ts +1 -1
- package/src/config/entry.ts +1 -1
- package/src/config/file.test.ts +1 -1
- package/src/config/file.ts +3 -3
- package/src/config/manifest.ts +1 -1
- package/src/config/resolve.test.ts +1 -1
- package/src/config/resolve.ts +1 -1
- package/src/config/schema.ts +1 -1
- package/src/config/validate.ts +1 -1
- package/src/{install → configure/artifacts}/binary-placement.test.ts +1 -1
- package/src/{install → configure/artifacts}/binary-placement.ts +1 -1
- package/src/{install → configure/artifacts}/gh-release-update.ts +1 -1
- package/src/{install → configure/artifacts}/install-validate.test.ts +3 -3
- package/src/{install → configure/artifacts}/mcp-config.ts +1 -1
- package/src/{install → configure/artifacts}/mcp-opencode.test.ts +1 -1
- package/src/{install → configure/artifacts}/mcp-opencode.ts +1 -1
- package/src/{install → configure/artifacts}/paths.ts +5 -5
- package/src/configure/artifacts/plan.ts +24 -0
- package/src/{install → configure/artifacts}/status.test.ts +1 -1
- package/src/{install → configure/artifacts}/status.ts +2 -2
- package/src/{install → configure/artifacts}/target-base.ts +1 -1
- package/src/{install → configure/artifacts}/target-detect.ts +1 -1
- package/src/{install → configure/artifacts}/target-effective.ts +3 -9
- package/src/{install → configure/artifacts}/target-mcp-cli.ts +1 -1
- package/src/{install → configure/artifacts}/target-mcp-json.ts +1 -1
- package/src/{install → configure/artifacts}/target-plan-build.ts +2 -2
- package/src/{install → configure/artifacts}/target-registry.ts +2 -2
- package/src/{install → configure/artifacts}/target-scope.ts +3 -3
- package/src/{install → configure/artifacts}/target-skill.ts +1 -1
- package/src/{install → configure/artifacts}/target-types.ts +2 -2
- package/src/{install → configure/artifacts}/targets/app.ts +5 -5
- package/src/{install → configure/artifacts}/targets/chatgpt-mcp.ts +2 -2
- package/src/{install → configure/artifacts}/targets/claude-code-mcp.ts +2 -2
- package/src/{install → configure/artifacts}/targets/claude-desktop-mcp.ts +2 -2
- package/src/{install → configure/artifacts}/targets/claude-skill.ts +2 -2
- package/src/{install → configure/artifacts}/targets/codex-mcp.ts +2 -2
- package/src/{install → configure/artifacts}/targets/codex-skill.ts +2 -2
- package/src/{install → configure/artifacts}/targets/configure.ts +5 -5
- package/src/{install → configure/artifacts}/targets/cursor-mcp.ts +2 -2
- package/src/{install → configure/artifacts}/targets/cursor-skill.ts +2 -2
- package/src/{install → configure/artifacts}/targets/index.ts +1 -1
- package/src/{install → configure/artifacts}/targets/openclaw-mcp.ts +2 -2
- package/src/{install → configure/artifacts}/targets/openclaw-skill.ts +3 -3
- package/src/{install → configure/artifacts}/targets/opencode-mcp.ts +5 -5
- package/src/{install → configure/artifacts}/targets/opencode-skill.ts +3 -3
- package/src/{install → configure/artifacts}/targets.test.ts +1 -1
- package/src/{install → configure/artifacts}/uninstall.ts +1 -1
- package/src/configure/configure.test.ts +11 -11
- package/src/configure/index.ts +14 -14
- package/src/configure/prompt.ts +2 -2
- package/src/{context.ts → core/context.ts} +26 -20
- package/src/{json-leaf.test.ts → core/json-leaf.test.ts} +4 -4
- package/src/{leaf-inputs.test.ts → core/leaf-inputs.test.ts} +7 -7
- package/src/{leaf-inputs.ts → core/leaf-inputs.ts} +16 -12
- package/src/{parse.test.ts → core/parse.test.ts} +97 -109
- package/src/{parse.ts → core/parse.ts} +129 -31
- package/src/{schema.ts → core/schema.ts} +25 -13
- package/src/{types.ts → core/types.ts} +225 -35
- package/src/{validate.ts → core/validate.ts} +39 -29
- package/src/docs/builtin.ts +8 -19
- package/src/docs/{api-guide.test.ts → cli-guide.test.ts} +21 -21
- package/src/docs/{api-guide.ts → cli-guide.ts} +45 -16
- package/src/docs/docs.test.ts +76 -41
- package/src/docs/http-guide.ts +37 -34
- package/src/docs/mcp-guide.ts +12 -14
- package/src/docs/mcp-resources.test.ts +2 -3
- package/src/docs/mcp-resources.ts +6 -11
- package/src/docs/resolve.ts +22 -30
- package/src/docs/save.ts +3 -3
- package/src/exports/cli.ts +47 -0
- package/src/exports/headless.ts +13 -0
- package/src/exports/http.ts +6 -0
- package/src/exports/mcp.ts +6 -0
- package/src/{headless.test.ts → headless/routing.test.ts} +3 -3
- package/src/{headless.ts → headless/routing.ts} +3 -3
- package/src/headless/tool-call.ts +114 -46
- package/src/help.test.ts +152 -0
- package/src/help.ts +3 -3
- package/src/hooks/builtin.ts +20 -0
- package/src/hooks/run.ts +142 -0
- package/src/http/openapi.ts +182 -0
- package/src/http/readiness.ts +78 -0
- package/src/{api → http}/result.ts +16 -5
- package/src/http/routes.ts +329 -0
- package/src/http/server.ts +225 -0
- package/src/index.ts +36 -25
- package/src/log/ecs.test.ts +43 -0
- package/src/log/ecs.ts +59 -0
- package/src/log/emitter.ts +166 -0
- package/src/mcp/bundle.ts +2 -2
- package/src/mcp/claude.test.ts +1 -1
- package/src/mcp/claude.ts +4 -4
- package/src/{hidden-mcpb.test.ts → mcp/hidden-mcpb.test.ts} +10 -9
- package/src/mcp/result.ts +2 -2
- package/src/mcp/server.ts +54 -6
- package/src/mcp/tools.ts +9 -20
- package/src/{capabilities.ts → runtime/capabilities.ts} +11 -11
- package/src/{cli-errors.ts → runtime/cli-errors.ts} +4 -4
- package/src/{cli.ts → runtime/cli.ts} +159 -49
- package/src/runtime/exposure.ts +102 -0
- package/src/{invoke.test.ts → runtime/invoke.test.ts} +31 -7
- package/src/server/context.ts +25 -0
- package/src/server/overrides.ts +112 -0
- package/src/skill/generate.ts +8 -8
- package/src/skill/hint.ts +1 -1
- package/src/skill/install.ts +2 -2
- package/src/skill/naming.ts +1 -1
- package/src/{test-fixtures.ts → test/fixtures.ts} +3 -2
- package/src/{config.integration.test.ts → test/integration/config.test.ts} +8 -8
- package/src/{api.integration.test.ts → test/integration/http.test.ts} +170 -67
- package/src/{mcp.integration.test.ts → test/integration/mcp.test.ts} +11 -57
- package/docs/api-server.md +0 -141
- package/examples/full-example/docs/api.md +0 -511
- package/examples/full-example/src/commands/status/__generated__/outputSchema.json +0 -28
- package/examples/full-example/src/config/__generated__/configSchema.json +0 -40
- package/examples/full-example/src/config/__generated__/index.ts +0 -5
- package/examples/full-example/src/config/types.ts +0 -24
- package/src/api/openapi.ts +0 -117
- package/src/api/server.ts +0 -120
- package/src/builtins/api.ts +0 -38
- package/src/hidden.ts +0 -30
- package/src/install/plan.ts +0 -53
- /package/src/{install → configure/artifacts}/detect-installed.ts +0 -0
- /package/src/{install → configure/artifacts}/gh-release-update.test.ts +0 -0
- /package/src/{install → configure/artifacts}/mcp-codex.test.ts +0 -0
- /package/src/{install → configure/artifacts}/mcp-codex.ts +0 -0
- /package/src/{install → configure/artifacts}/mcp-openclaw.test.ts +0 -0
- /package/src/{install → configure/artifacts}/mcp-openclaw.ts +0 -0
- /package/src/{install → configure/artifacts}/normalize-uninstall.ts +0 -0
- /package/src/{install → configure/artifacts}/normalize.ts +0 -0
- /package/src/{install → configure/artifacts}/opts.ts +0 -0
- /package/src/{install → configure/artifacts}/shell.ts +0 -0
- /package/src/{formats.test.ts → core/formats.test.ts} +0 -0
- /package/src/{formats.ts → core/formats.ts} +0 -0
- /package/src/{respond.ts → core/respond.ts} +0 -0
- /package/src/{types.test.ts → core/types.test.ts} +0 -0
- /package/src/{api → http}/schema-deref.test.ts +0 -0
- /package/src/{api → http}/schema-deref.ts +0 -0
package/src/skill/generate.ts
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
This module generates Agent Skills content (SKILL.md + reference.md) from a CLI schema.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { defaultConfigEntryTitle } from "
|
|
6
|
-
import {
|
|
5
|
+
import { defaultConfigEntryTitle } from "~/config/entry.ts";
|
|
6
|
+
import { collectOptionDefs } from "~/core/parse.ts";
|
|
7
|
+
import { CliOptionKind, type CliProgram } from "~/core/types.ts";
|
|
8
|
+
import { generateCliGuide } from "~/docs/cli-guide.ts";
|
|
7
9
|
import {
|
|
8
10
|
collectMcpTools,
|
|
9
11
|
type McpToolDef,
|
|
10
12
|
mcpServerId,
|
|
11
13
|
resolveMcpSchemaUri,
|
|
12
14
|
sanitizeToolSegment,
|
|
13
|
-
} from "
|
|
14
|
-
import { collectOptionDefs } from "../parse.ts";
|
|
15
|
-
import { CliOptionKind, type CliProgram } from "../types.ts";
|
|
15
|
+
} from "~/mcp/tools.ts";
|
|
16
16
|
import type { SkillTarget } from "./naming.ts";
|
|
17
17
|
import { skillDirNameForTarget, skillFrontmatterName } from "./naming.ts";
|
|
18
18
|
|
|
@@ -149,7 +149,7 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
|
|
|
149
149
|
"",
|
|
150
150
|
"## Reference",
|
|
151
151
|
"",
|
|
152
|
-
`For full detail, open \`reference.md\` in this skill directory (same as \`${root.key} docs
|
|
152
|
+
`For full detail, open \`reference.md\` in this skill directory (same as \`${root.key} docs cli\`).`,
|
|
153
153
|
"",
|
|
154
154
|
);
|
|
155
155
|
|
|
@@ -190,9 +190,9 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
|
|
|
190
190
|
return lines.join("\n");
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
/** Builds reference.md with the
|
|
193
|
+
/** Builds reference.md with the compact `docs cli` markdown guide. */
|
|
194
194
|
function buildReferenceMd(root: CliProgram): string {
|
|
195
|
-
return
|
|
195
|
+
return generateCliGuide(root, { compact: true });
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
/** Builds MCP routing SKILL.md for Claude Code plugin zips. */
|
package/src/skill/hint.ts
CHANGED
package/src/skill/install.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import type { CliProgram } from "~/core/types.ts";
|
|
4
|
+
import { userHome } from "~/paths/host.ts";
|
|
5
5
|
import { generateSkillBundle, type SkillTarget } from "./generate.ts";
|
|
6
6
|
import { applySkillInstallHints } from "./hint.ts";
|
|
7
7
|
|
package/src/skill/naming.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Agent skill install targets. */
|
|
2
2
|
export type SkillTarget = "cursor" | "claude" | "codex" | "opencode" | "openclaw";
|
|
3
3
|
|
|
4
|
-
import { sanitizeToolSegment } from "
|
|
4
|
+
import { sanitizeToolSegment } from "~/mcp/tools.ts";
|
|
5
5
|
|
|
6
6
|
/** Kebab-case skill folder + frontmatter name for agents that require hyphen slugs. */
|
|
7
7
|
export function skillSlug(programKey: string): string {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CliProgram } from "
|
|
2
|
-
import { CliFallbackMode, CliOptionKind } from "
|
|
1
|
+
import type { CliProgram } from "~/core/types.ts";
|
|
2
|
+
import { CliFallbackMode, CliOptionKind } from "~/core/types.ts";
|
|
3
3
|
|
|
4
4
|
export function testProgram(prog: Record<string, unknown> & { key: string; description: string }): CliProgram {
|
|
5
5
|
return { version: "0.0.0", ...prog } as CliProgram;
|
|
@@ -166,6 +166,7 @@ export function nestedDocsFallbackFixture(): CliProgram {
|
|
|
166
166
|
return testProgram({
|
|
167
167
|
key: "app",
|
|
168
168
|
description: "",
|
|
169
|
+
docs: { enabled: false },
|
|
169
170
|
commands: [
|
|
170
171
|
{
|
|
171
172
|
key: "docs",
|
|
@@ -6,9 +6,9 @@ import { expect, test } from "bun:test";
|
|
|
6
6
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
|
-
import { bootstrapAppConfig } from "
|
|
10
|
-
import { resolveAppConfigPath } from "
|
|
11
|
-
import { mcpRequest, testProgram } from "
|
|
9
|
+
import { bootstrapAppConfig } from "~/config/bootstrap.ts";
|
|
10
|
+
import { resolveAppConfigPath } from "~/config/file.ts";
|
|
11
|
+
import { mcpRequest, testProgram } from "~/test/fixtures.ts";
|
|
12
12
|
|
|
13
13
|
/** Tests that bootstrapAppConfig prefers host env over config file. */
|
|
14
14
|
test("bootstrapAppConfig prefers host env over config file", () => {
|
|
@@ -124,13 +124,13 @@ test("MCP config file loads and exports vars for tool handlers", async () => {
|
|
|
124
124
|
rmSync(dir, { recursive: true, force: true });
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
/** Cli.run docs
|
|
128
|
-
test("Cli.run docs
|
|
127
|
+
/** Cli.run docs cli skips required appConfig exit. */
|
|
128
|
+
test("Cli.run docs cli skips required appConfig exit", async () => {
|
|
129
129
|
const dir = mkdtempSync(join(tmpdir(), "argsbarg-docs-skip-"));
|
|
130
130
|
const configFile = join(dir, ".local", "lib", "docs_skip_test", "config");
|
|
131
131
|
mkdirSync(dirname(configFile), { recursive: true });
|
|
132
132
|
writeFileSync(configFile, "{}\n");
|
|
133
|
-
const entry = join(import.meta.dir, "index.ts");
|
|
133
|
+
const entry = join(import.meta.dir, "..", "..", "index.ts");
|
|
134
134
|
const mainPath = join(dir, "run-docs.ts");
|
|
135
135
|
writeFileSync(
|
|
136
136
|
mainPath,
|
|
@@ -139,7 +139,7 @@ const program = {
|
|
|
139
139
|
key: "docs-skip-test",
|
|
140
140
|
version: "1.0.0",
|
|
141
141
|
description: "test",
|
|
142
|
-
docs: {
|
|
142
|
+
docs: { topics: { readme: { text: "# readme\\n" } } },
|
|
143
143
|
appConfig: {
|
|
144
144
|
entries: { token: { description: "Token.", env: "DOCS_SKIP_RUN_TOKEN" } },
|
|
145
145
|
},
|
|
@@ -151,7 +151,7 @@ await new Cli(program).run(process.argv.slice(2));
|
|
|
151
151
|
const env = { ...process.env, HOME: dir } as Record<string, string | undefined>;
|
|
152
152
|
delete env.DOCS_SKIP_RUN_TOKEN;
|
|
153
153
|
try {
|
|
154
|
-
const proc = Bun.spawn(["bun", "run", mainPath, "docs", "
|
|
154
|
+
const proc = Bun.spawn(["bun", "run", mainPath, "docs", "cli"], {
|
|
155
155
|
stdout: "pipe",
|
|
156
156
|
stderr: "pipe",
|
|
157
157
|
env,
|
|
@@ -5,18 +5,21 @@ HTTP API integration tests: routes, tool invocation, CORS, OpenAPI, and validati
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { $ } from "bun";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
8
|
+
import { cliValidateProgram } from "~/core/validate.ts";
|
|
9
|
+
import { generateOpenApi } from "~/http/openapi.ts";
|
|
10
|
+
import { API_CORS_HEADERS } from "~/http/result.ts";
|
|
11
|
+
import { handleApiRequest } from "~/http/server.ts";
|
|
12
|
+
import { Cli, CliContext, type CliContext as CliContextType, CliOptionKind, cliErrWithHelp } from "~/index";
|
|
13
|
+
import { LogEmitter } from "~/log/emitter.ts";
|
|
14
|
+
import { createServerRuntime } from "~/server/context.ts";
|
|
15
|
+
import { resolveHttpServeConfig } from "~/server/overrides.ts";
|
|
16
|
+
import { nestedMcpFixture, testProgram } from "~/test/fixtures.ts";
|
|
14
17
|
|
|
15
18
|
/** Program with HTTP API enabled and handlers that return values. */
|
|
16
19
|
function nestedApiFixture() {
|
|
17
20
|
return testProgram({
|
|
18
21
|
...nestedMcpFixture,
|
|
19
|
-
|
|
22
|
+
httpServer: { enabled: true },
|
|
20
23
|
commands: [
|
|
21
24
|
{
|
|
22
25
|
key: "stat",
|
|
@@ -80,7 +83,7 @@ function nestedApiFixture() {
|
|
|
80
83
|
{
|
|
81
84
|
key: "pdf",
|
|
82
85
|
description: "Return a minimal PDF.",
|
|
83
|
-
|
|
86
|
+
http: { successContentType: "application/pdf" },
|
|
84
87
|
handler: (ctx: CliContextType) => {
|
|
85
88
|
ctx.respond({
|
|
86
89
|
body: new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x34]),
|
|
@@ -91,7 +94,7 @@ function nestedApiFixture() {
|
|
|
91
94
|
{
|
|
92
95
|
key: "html",
|
|
93
96
|
description: "Return HTML.",
|
|
94
|
-
|
|
97
|
+
http: { successContentType: "text/html; charset=utf-8" },
|
|
95
98
|
handler: (ctx: CliContextType) => {
|
|
96
99
|
ctx.respond({
|
|
97
100
|
body: "<!DOCTYPE html><html><body>hi</body></html>",
|
|
@@ -109,42 +112,57 @@ function nestedApiFixture() {
|
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
/** Sends one HTTP request through the in-process API handler. */
|
|
112
|
-
async function apiRequest(
|
|
115
|
+
async function apiRequest(
|
|
116
|
+
program: ReturnType<typeof nestedApiFixture>,
|
|
117
|
+
request: Request,
|
|
118
|
+
opts?: { withServer?: boolean },
|
|
119
|
+
) {
|
|
113
120
|
const cli = new Cli(program);
|
|
114
|
-
|
|
121
|
+
if (opts?.withServer) {
|
|
122
|
+
const resolved = resolveHttpServeConfig(program);
|
|
123
|
+
cli.server = {
|
|
124
|
+
runtime: createServerRuntime(program, "http"),
|
|
125
|
+
emitter: new LogEmitter({
|
|
126
|
+
program,
|
|
127
|
+
resolved: { ...resolved.log, access: false },
|
|
128
|
+
}),
|
|
129
|
+
http: resolved,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return handleApiRequest(cli, request, cli.server?.http);
|
|
115
133
|
}
|
|
116
134
|
|
|
117
|
-
describe("
|
|
118
|
-
test("rejects empty
|
|
135
|
+
describe("httpServer validation", () => {
|
|
136
|
+
test("rejects empty httpServer", () => {
|
|
119
137
|
const root = testProgram({
|
|
120
138
|
key: "app",
|
|
121
139
|
description: "",
|
|
122
|
-
|
|
140
|
+
httpServer: {} as { enabled: boolean },
|
|
123
141
|
handler: () => {},
|
|
124
142
|
});
|
|
125
|
-
expect(() => cliValidateProgram(root)).toThrow(/
|
|
143
|
+
expect(() => cliValidateProgram(root)).toThrow(/httpServer requires enabled: true/);
|
|
126
144
|
});
|
|
127
145
|
|
|
128
|
-
test("rejects top-level command name
|
|
146
|
+
test("rejects top-level command name http when httpServer enabled", () => {
|
|
129
147
|
const root = testProgram({
|
|
130
148
|
key: "app",
|
|
131
149
|
description: "",
|
|
132
|
-
|
|
133
|
-
commands: [{ key: "
|
|
150
|
+
httpServer: { enabled: true },
|
|
151
|
+
commands: [{ key: "http", description: "user", handler: () => {} }],
|
|
134
152
|
});
|
|
135
|
-
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name:
|
|
153
|
+
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: http/);
|
|
136
154
|
});
|
|
137
155
|
|
|
138
|
-
test("allows top-level command name
|
|
156
|
+
test("allows top-level command name http without httpServer", () => {
|
|
139
157
|
const root = testProgram({
|
|
140
158
|
key: "app",
|
|
141
159
|
description: "",
|
|
142
|
-
commands: [{ key: "
|
|
160
|
+
commands: [{ key: "http", description: "user", handler: () => {} }],
|
|
143
161
|
});
|
|
144
162
|
expect(() => cliValidateProgram(root)).not.toThrow();
|
|
145
163
|
});
|
|
146
164
|
|
|
147
|
-
test("rejects
|
|
165
|
+
test("rejects httpServer on non-root node", () => {
|
|
148
166
|
const root = {
|
|
149
167
|
key: "app",
|
|
150
168
|
version: "0.0.0",
|
|
@@ -153,12 +171,12 @@ describe("apiServer validation", () => {
|
|
|
153
171
|
{
|
|
154
172
|
key: "x",
|
|
155
173
|
description: "cmd",
|
|
156
|
-
|
|
174
|
+
httpServer: { enabled: true },
|
|
157
175
|
handler: () => {},
|
|
158
176
|
},
|
|
159
177
|
],
|
|
160
|
-
} as unknown as import("
|
|
161
|
-
expect(() => cliValidateProgram(root)).toThrow(/
|
|
178
|
+
} as unknown as import("~/core/types.ts").CliProgram;
|
|
179
|
+
expect(() => cliValidateProgram(root)).toThrow(/httpServer is only supported on the program root/);
|
|
162
180
|
});
|
|
163
181
|
});
|
|
164
182
|
|
|
@@ -166,6 +184,94 @@ describe("HTTP API routes", () => {
|
|
|
166
184
|
const program = nestedApiFixture();
|
|
167
185
|
cliValidateProgram(program);
|
|
168
186
|
|
|
187
|
+
test("GET /health/live returns ok", async () => {
|
|
188
|
+
const res = await apiRequest(program, new Request("http://127.0.0.1/health/live"));
|
|
189
|
+
expect(res.status).toBe(200);
|
|
190
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("GET /health/ready returns ok when healthy", async () => {
|
|
194
|
+
const res = await apiRequest(program, new Request("http://127.0.0.1/health/ready"), { withServer: true });
|
|
195
|
+
expect(res.status).toBe(200);
|
|
196
|
+
const body = (await res.json()) as { ok: boolean; checks: Record<string, { ok: boolean }> };
|
|
197
|
+
expect(body.ok).toBe(true);
|
|
198
|
+
expect(body.checks.config_file.ok).toBe(true);
|
|
199
|
+
expect(body.checks.config_required.ok).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("GET /health/ready returns 503 when custom readiness fails", async () => {
|
|
203
|
+
const failProgram = testProgram({
|
|
204
|
+
key: "app",
|
|
205
|
+
description: "Test",
|
|
206
|
+
version: "1.0.0",
|
|
207
|
+
httpServer: { enabled: true },
|
|
208
|
+
readiness: () => false,
|
|
209
|
+
handler: () => ({ ok: true }),
|
|
210
|
+
});
|
|
211
|
+
cliValidateProgram(failProgram);
|
|
212
|
+
const res = await apiRequest(failProgram, new Request("http://127.0.0.1/health/ready"), { withServer: true });
|
|
213
|
+
expect(res.status).toBe(503);
|
|
214
|
+
const body = (await res.json()) as { ok: boolean; checks: { custom: { ok: boolean } } };
|
|
215
|
+
expect(body.ok).toBe(false);
|
|
216
|
+
expect(body.checks.custom.ok).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("POST /api returns 500 for non-Error throw", async () => {
|
|
220
|
+
const throwProgram = testProgram({
|
|
221
|
+
key: "app",
|
|
222
|
+
description: "Test",
|
|
223
|
+
httpServer: { enabled: true },
|
|
224
|
+
commands: [
|
|
225
|
+
{
|
|
226
|
+
key: "boom",
|
|
227
|
+
description: "Throws non-Error",
|
|
228
|
+
handler: () => {
|
|
229
|
+
throw "unexpected";
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
cliValidateProgram(throwProgram);
|
|
235
|
+
const res = await apiRequest(
|
|
236
|
+
throwProgram,
|
|
237
|
+
new Request("http://127.0.0.1/api/boom", { method: "POST", body: "{}" }),
|
|
238
|
+
);
|
|
239
|
+
expect(res.status).toBe(500);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("POST /api obscures unexpected errors when configured", async () => {
|
|
243
|
+
const throwProgram = testProgram({
|
|
244
|
+
key: "app",
|
|
245
|
+
description: "Test",
|
|
246
|
+
httpServer: { enabled: true, errors: { obscureUnexpected: true } },
|
|
247
|
+
commands: [
|
|
248
|
+
{
|
|
249
|
+
key: "boom",
|
|
250
|
+
description: "Throws non-Error",
|
|
251
|
+
handler: () => {
|
|
252
|
+
throw "secret";
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
cliValidateProgram(throwProgram);
|
|
258
|
+
const cli = new Cli(throwProgram);
|
|
259
|
+
const resolved = resolveHttpServeConfig(throwProgram);
|
|
260
|
+
cli.server = {
|
|
261
|
+
runtime: createServerRuntime(throwProgram, "http"),
|
|
262
|
+
emitter: new LogEmitter({ program: throwProgram, resolved: { ...resolved.log, access: false } }),
|
|
263
|
+
http: resolved,
|
|
264
|
+
};
|
|
265
|
+
const res = await handleApiRequest(
|
|
266
|
+
cli,
|
|
267
|
+
new Request("http://127.0.0.1/api/boom", { method: "POST", body: "{}" }),
|
|
268
|
+
resolved,
|
|
269
|
+
);
|
|
270
|
+
expect(res.status).toBe(500);
|
|
271
|
+
const body = (await res.json()) as { error: string };
|
|
272
|
+
expect(body.error).toBe("An unexpected error occurred.");
|
|
273
|
+
});
|
|
274
|
+
|
|
169
275
|
test("GET /health includes CORS headers", async () => {
|
|
170
276
|
const res = await apiRequest(program, new Request("http://127.0.0.1/health"));
|
|
171
277
|
expect(res.status).toBe(200);
|
|
@@ -174,46 +280,43 @@ describe("HTTP API routes", () => {
|
|
|
174
280
|
});
|
|
175
281
|
|
|
176
282
|
test("OPTIONS returns 204 with CORS headers", async () => {
|
|
177
|
-
const res = await apiRequest(
|
|
178
|
-
program,
|
|
179
|
-
new Request("http://127.0.0.1/tools/stat-owner-lookup", { method: "OPTIONS" }),
|
|
180
|
-
);
|
|
283
|
+
const res = await apiRequest(program, new Request("http://127.0.0.1/api/stat/owner/lookup", { method: "OPTIONS" }));
|
|
181
284
|
expect(res.status).toBe(204);
|
|
182
285
|
expect(res.headers.get("access-control-allow-origin")).toBe("*");
|
|
183
286
|
expect(res.headers.get("access-control-allow-methods")).toContain("POST");
|
|
184
287
|
});
|
|
185
288
|
|
|
186
|
-
test("POST /
|
|
187
|
-
const readme = join(import.meta.dir, "..", "README.md");
|
|
289
|
+
test("POST /api/... returns raw JSON body with 201", async () => {
|
|
290
|
+
const readme = join(import.meta.dir, "..", "..", "..", "README.md");
|
|
188
291
|
const res = await apiRequest(
|
|
189
292
|
program,
|
|
190
|
-
new Request("http://127.0.0.1/
|
|
293
|
+
new Request("http://127.0.0.1/api/stat/owner/lookup", {
|
|
191
294
|
method: "POST",
|
|
192
295
|
headers: { "content-type": "application/json" },
|
|
193
296
|
body: JSON.stringify({ "user-name": "alice", path: readme, json: true }),
|
|
194
297
|
}),
|
|
195
298
|
);
|
|
196
|
-
expect(res.status).toBe(
|
|
299
|
+
expect(res.status).toBe(201);
|
|
197
300
|
expect(res.headers.get("content-type")).toContain("application/json");
|
|
198
301
|
expect(await res.json()).toEqual({ user: "alice", path: readme });
|
|
199
302
|
});
|
|
200
303
|
|
|
201
|
-
test("POST /
|
|
202
|
-
const readme = join(import.meta.dir, "..", "README.md");
|
|
304
|
+
test("POST /api/... returns raw text body with 201", async () => {
|
|
305
|
+
const readme = join(import.meta.dir, "..", "..", "..", "README.md");
|
|
203
306
|
const res = await apiRequest(
|
|
204
307
|
program,
|
|
205
|
-
new Request("http://127.0.0.1/
|
|
308
|
+
new Request("http://127.0.0.1/api/stat/owner/lookup", {
|
|
206
309
|
method: "POST",
|
|
207
310
|
headers: { "content-type": "application/json" },
|
|
208
311
|
body: JSON.stringify({ "user-name": "alice", path: readme }),
|
|
209
312
|
}),
|
|
210
313
|
);
|
|
211
|
-
expect(res.status).toBe(
|
|
314
|
+
expect(res.status).toBe(201);
|
|
212
315
|
const text = await res.text();
|
|
213
316
|
expect(text).toContain("lookup user=alice");
|
|
214
317
|
});
|
|
215
318
|
|
|
216
|
-
test("POST /tools returns
|
|
319
|
+
test("POST /tools returns 404 (legacy path removed)", async () => {
|
|
217
320
|
const res = await apiRequest(
|
|
218
321
|
program,
|
|
219
322
|
new Request("http://127.0.0.1/tools", {
|
|
@@ -222,41 +325,41 @@ describe("HTTP API routes", () => {
|
|
|
222
325
|
body: "{}",
|
|
223
326
|
}),
|
|
224
327
|
);
|
|
225
|
-
expect(res.status).toBe(
|
|
328
|
+
expect(res.status).toBe(404);
|
|
226
329
|
});
|
|
227
330
|
|
|
228
|
-
test("POST /
|
|
331
|
+
test("POST /api/pdf returns PDF bytes with 201", async () => {
|
|
229
332
|
const res = await apiRequest(
|
|
230
333
|
program,
|
|
231
|
-
new Request("http://127.0.0.1/
|
|
334
|
+
new Request("http://127.0.0.1/api/pdf", {
|
|
232
335
|
method: "POST",
|
|
233
336
|
headers: { "content-type": "application/json" },
|
|
234
337
|
body: "{}",
|
|
235
338
|
}),
|
|
236
339
|
);
|
|
237
|
-
expect(res.status).toBe(
|
|
340
|
+
expect(res.status).toBe(201);
|
|
238
341
|
expect(res.headers.get("content-type")).toBe("application/pdf");
|
|
239
342
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
240
343
|
expect(String.fromCharCode(...bytes.slice(0, 4))).toBe("%PDF");
|
|
241
344
|
});
|
|
242
345
|
|
|
243
|
-
test("POST /
|
|
346
|
+
test("POST /api/html returns HTML with 201", async () => {
|
|
244
347
|
const res = await apiRequest(
|
|
245
348
|
program,
|
|
246
|
-
new Request("http://127.0.0.1/
|
|
349
|
+
new Request("http://127.0.0.1/api/html", {
|
|
247
350
|
method: "POST",
|
|
248
351
|
body: "{}",
|
|
249
352
|
}),
|
|
250
353
|
);
|
|
251
|
-
expect(res.status).toBe(
|
|
354
|
+
expect(res.status).toBe(201);
|
|
252
355
|
expect(res.headers.get("content-type")).toContain("text/html");
|
|
253
356
|
expect(await res.text()).toContain("<!DOCTYPE html>");
|
|
254
357
|
});
|
|
255
358
|
|
|
256
|
-
test("POST /
|
|
359
|
+
test("POST /api/silent returns 500 when handler has no response", async () => {
|
|
257
360
|
const res = await apiRequest(
|
|
258
361
|
program,
|
|
259
|
-
new Request("http://127.0.0.1/
|
|
362
|
+
new Request("http://127.0.0.1/api/silent", {
|
|
260
363
|
method: "POST",
|
|
261
364
|
body: "{}",
|
|
262
365
|
}),
|
|
@@ -266,10 +369,10 @@ describe("HTTP API routes", () => {
|
|
|
266
369
|
expect(body.error).toContain("ctx.respond()");
|
|
267
370
|
});
|
|
268
371
|
|
|
269
|
-
test("POST /
|
|
372
|
+
test("POST /api returns 404 for unknown route", async () => {
|
|
270
373
|
const res = await apiRequest(
|
|
271
374
|
program,
|
|
272
|
-
new Request("http://127.0.0.1/
|
|
375
|
+
new Request("http://127.0.0.1/api/missing_tool", {
|
|
273
376
|
method: "POST",
|
|
274
377
|
headers: { "content-type": "application/json" },
|
|
275
378
|
body: "{}",
|
|
@@ -278,10 +381,10 @@ describe("HTTP API routes", () => {
|
|
|
278
381
|
expect(res.status).toBe(404);
|
|
279
382
|
});
|
|
280
383
|
|
|
281
|
-
test("POST /
|
|
384
|
+
test("POST /api returns 400 for bad args", async () => {
|
|
282
385
|
const res = await apiRequest(
|
|
283
386
|
program,
|
|
284
|
-
new Request("http://127.0.0.1/
|
|
387
|
+
new Request("http://127.0.0.1/api/stat/owner/lookup", {
|
|
285
388
|
method: "POST",
|
|
286
389
|
headers: { "content-type": "application/json" },
|
|
287
390
|
body: JSON.stringify({ "user-name": "alice" }),
|
|
@@ -294,11 +397,11 @@ describe("HTTP API routes", () => {
|
|
|
294
397
|
expect(body.error).not.toContain("\u001B[");
|
|
295
398
|
});
|
|
296
399
|
|
|
297
|
-
test("POST /
|
|
400
|
+
test("POST /api/... returns plain JSON validation errors", async () => {
|
|
298
401
|
const failProgram = testProgram({
|
|
299
402
|
key: "app",
|
|
300
403
|
description: "Test app",
|
|
301
|
-
|
|
404
|
+
httpServer: { enabled: true },
|
|
302
405
|
commands: [
|
|
303
406
|
{
|
|
304
407
|
key: "fail",
|
|
@@ -312,7 +415,7 @@ describe("HTTP API routes", () => {
|
|
|
312
415
|
cliValidateProgram(failProgram);
|
|
313
416
|
const res = await apiRequest(
|
|
314
417
|
failProgram,
|
|
315
|
-
new Request("http://127.0.0.1/
|
|
418
|
+
new Request("http://127.0.0.1/api/fail", {
|
|
316
419
|
method: "POST",
|
|
317
420
|
headers: { "content-type": "application/json" },
|
|
318
421
|
body: "{}",
|
|
@@ -323,12 +426,12 @@ describe("HTTP API routes", () => {
|
|
|
323
426
|
expect(body).toEqual({ error: "bad input" });
|
|
324
427
|
});
|
|
325
428
|
|
|
326
|
-
test("GET /openapi.json lists
|
|
429
|
+
test("GET /openapi.json lists REST paths", async () => {
|
|
327
430
|
const res = await apiRequest(program, new Request("http://127.0.0.1/openapi.json"));
|
|
328
431
|
expect(res.status).toBe(200);
|
|
329
432
|
const doc = (await res.json()) as { openapi: string; paths: Record<string, unknown> };
|
|
330
433
|
expect(doc.openapi).toBe("3.1.0");
|
|
331
|
-
expect(doc.paths["/
|
|
434
|
+
expect(doc.paths["/api/stat/owner/lookup"]).toBeDefined();
|
|
332
435
|
});
|
|
333
436
|
|
|
334
437
|
test("GET /openapi-browser returns Scalar HTML", async () => {
|
|
@@ -345,9 +448,9 @@ describe("HTTP API routes", () => {
|
|
|
345
448
|
test("generateOpenApi maps binary content types", () => {
|
|
346
449
|
const program = nestedApiFixture();
|
|
347
450
|
const doc = generateOpenApi(program) as {
|
|
348
|
-
paths: Record<string, { post: { responses: { "
|
|
451
|
+
paths: Record<string, { post: { responses: { "201": { content: Record<string, unknown> } } } }>;
|
|
349
452
|
};
|
|
350
|
-
const pdf = doc.paths["/
|
|
453
|
+
const pdf = doc.paths["/api/pdf"]?.post.responses["201"].content["application/pdf"] as {
|
|
351
454
|
schema: { format: string };
|
|
352
455
|
};
|
|
353
456
|
expect(pdf.schema.format).toBe("binary");
|
|
@@ -357,7 +460,7 @@ test("generateOpenApi dereferences nested inputSchema definitions", () => {
|
|
|
357
460
|
const program = testProgram({
|
|
358
461
|
key: "app",
|
|
359
462
|
description: "Test app",
|
|
360
|
-
|
|
463
|
+
httpServer: { enabled: true },
|
|
361
464
|
commands: [
|
|
362
465
|
{
|
|
363
466
|
key: "render",
|
|
@@ -394,7 +497,7 @@ test("generateOpenApi dereferences nested inputSchema definitions", () => {
|
|
|
394
497
|
}
|
|
395
498
|
>;
|
|
396
499
|
};
|
|
397
|
-
const schema = doc.paths["/
|
|
500
|
+
const schema = doc.paths["/api/render"]?.post.requestBody.content["application/json; charset=utf-8"].schema;
|
|
398
501
|
expect(schema.properties.invoice).toEqual({
|
|
399
502
|
type: "object",
|
|
400
503
|
properties: { id: { type: "string" } },
|
|
@@ -408,7 +511,7 @@ test("ctx.respond throws when called twice", () => {
|
|
|
408
511
|
description: "",
|
|
409
512
|
handler: () => {},
|
|
410
513
|
});
|
|
411
|
-
const context = new CliContext("app", [], [], {}, program, "
|
|
514
|
+
const context = new CliContext("app", [], [], {}, program, "http");
|
|
412
515
|
context.respond({ body: { ok: true } });
|
|
413
516
|
expect(() => context.respond({ body: { ok: true } })).toThrow(/already called/);
|
|
414
517
|
});
|
|
@@ -417,7 +520,7 @@ test("API_CORS_HEADERS are wide open", () => {
|
|
|
417
520
|
expect(API_CORS_HEADERS["access-control-allow-origin"]).toBe("*");
|
|
418
521
|
});
|
|
419
522
|
|
|
420
|
-
test("ctx.invocation is
|
|
523
|
+
test("ctx.invocation is http via Cli.invoke", async () => {
|
|
421
524
|
let seen = "";
|
|
422
525
|
const root = testProgram({
|
|
423
526
|
key: "app",
|
|
@@ -428,14 +531,14 @@ test("ctx.invocation is api via Cli.invoke", async () => {
|
|
|
428
531
|
},
|
|
429
532
|
});
|
|
430
533
|
cliValidateProgram(root);
|
|
431
|
-
const result = await new Cli(root).invoke([], { invocation: "
|
|
534
|
+
const result = await new Cli(root).invoke([], { invocation: "http" });
|
|
432
535
|
expect(result.kind).toBe("ok");
|
|
433
|
-
expect(seen).toBe("
|
|
434
|
-
expect(result.response?.body).toEqual({ invocation: "
|
|
536
|
+
expect(seen).toBe("http");
|
|
537
|
+
expect(result.response?.body).toEqual({ invocation: "http" });
|
|
435
538
|
});
|
|
436
539
|
|
|
437
|
-
test("minimal.ts
|
|
438
|
-
const { stderr, exitCode } = await $`bun run examples/minimal.ts
|
|
540
|
+
test("minimal.ts http without opt-in fails", async () => {
|
|
541
|
+
const { stderr, exitCode } = await $`bun run examples/minimal.ts http`.nothrow().quiet();
|
|
439
542
|
expect(exitCode).toBe(1);
|
|
440
543
|
expect(stderr.toString()).toContain("HTTP API is not available");
|
|
441
544
|
});
|