argsbarg 6.1.2 → 6.1.4
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 +74 -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 +30 -31
- package/examples/full-example/docs/mcp.md +8 -22
- package/examples/full-example/docs/openapi.json +798 -44
- 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 +39 -36
- 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 +290 -0
- package/src/http/readiness.ts +78 -0
- package/src/{api → http}/result.ts +22 -11
- 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/test/integration/http.test.ts +651 -0
- 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/api.integration.test.ts +0 -441
- 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/help.test.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Help rendering and label formatting tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { cliPresentationRoot } from "./builtins/presentation.ts";
|
|
7
|
+
import { type CliOption, CliOptionKind, type CliPositional } from "./core/types.ts";
|
|
8
|
+
import { CLI_NOTES_PROGRAM, cliHelpRender, cliOptionLabel, cliPositionalLabel, cliResolveNotes } from "./help.ts";
|
|
9
|
+
import { testProgram } from "./test/fixtures.ts";
|
|
10
|
+
|
|
11
|
+
describe("cliOptionLabel", () => {
|
|
12
|
+
test.each([
|
|
13
|
+
{
|
|
14
|
+
name: "string option",
|
|
15
|
+
option: { name: "out", description: "Output path.", kind: CliOptionKind.String },
|
|
16
|
+
expected: "--out <string>",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "required enum",
|
|
20
|
+
option: {
|
|
21
|
+
name: "format",
|
|
22
|
+
description: "Format.",
|
|
23
|
+
kind: CliOptionKind.Enum,
|
|
24
|
+
choices: ["pdf", "html"],
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
expected: "--format <pdf|html>",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "short name",
|
|
31
|
+
option: {
|
|
32
|
+
name: "verbose",
|
|
33
|
+
description: "Verbose.",
|
|
34
|
+
kind: CliOptionKind.Presence,
|
|
35
|
+
shortName: "v",
|
|
36
|
+
},
|
|
37
|
+
expected: "--verbose, -v",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "json option",
|
|
41
|
+
option: { name: "body", description: "JSON body.", kind: CliOptionKind.Json },
|
|
42
|
+
expected: "--body <json>",
|
|
43
|
+
},
|
|
44
|
+
])("$name", ({ option, expected }) => {
|
|
45
|
+
expect(cliOptionLabel(option as CliOption, false)).toBe(expected);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("cliPositionalLabel", () => {
|
|
50
|
+
test.each([
|
|
51
|
+
{ positional: { name: "file", description: "File." }, expected: "<file>" },
|
|
52
|
+
{ positional: { name: "file", description: "File.", argMin: 0 }, expected: "[file]" },
|
|
53
|
+
{ positional: { name: "paths", description: "Paths.", argMax: 0 }, expected: "<paths...>" },
|
|
54
|
+
{ positional: { name: "paths", description: "Paths.", argMin: 0, argMax: 0 }, expected: "[paths...]" },
|
|
55
|
+
])("$expected", ({ positional, expected }) => {
|
|
56
|
+
expect(cliPositionalLabel(positional as CliPositional, false)).toBe(expected);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("cliResolveNotes", () => {
|
|
61
|
+
test("replaces program placeholder", () => {
|
|
62
|
+
expect(cliResolveNotes(`Run \`${CLI_NOTES_PROGRAM} docs readme\`.`, "myapp")).toBe("Run `myapp docs readme`.");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("cliHelpRender", () => {
|
|
67
|
+
test("docs help lists schema, cli, and skill subcommands", () => {
|
|
68
|
+
const root = testProgram({
|
|
69
|
+
key: "app",
|
|
70
|
+
version: "1.0.0",
|
|
71
|
+
description: "demo",
|
|
72
|
+
docs: {
|
|
73
|
+
topics: { readme: { text: "# readme\n" } },
|
|
74
|
+
},
|
|
75
|
+
commands: [
|
|
76
|
+
{
|
|
77
|
+
key: "x",
|
|
78
|
+
description: "cmd",
|
|
79
|
+
handler: () => {},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
const help = cliHelpRender(cliPresentationRoot(root), ["docs"], false);
|
|
84
|
+
expect(help).toContain("cli-schema");
|
|
85
|
+
expect(help).toContain("Print the full CLI command tree as JSON.");
|
|
86
|
+
expect(help).toContain("cli");
|
|
87
|
+
expect(help).toContain("markdown");
|
|
88
|
+
expect(help).toContain("skill");
|
|
89
|
+
expect(help).toContain("reference agent SKILL");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("root help omits legacy --schema flag", () => {
|
|
93
|
+
const root = testProgram({
|
|
94
|
+
key: "app",
|
|
95
|
+
version: "1.0.0",
|
|
96
|
+
description: "demo",
|
|
97
|
+
commands: [
|
|
98
|
+
{
|
|
99
|
+
key: "x",
|
|
100
|
+
description: "cmd",
|
|
101
|
+
handler: () => {},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
const help = cliHelpRender(cliPresentationRoot(root), [], false);
|
|
106
|
+
expect(help).not.toContain("--schema");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("root help shows agent docs hint when docs enabled", () => {
|
|
110
|
+
const root = testProgram({
|
|
111
|
+
key: "myapp",
|
|
112
|
+
version: "1.0.0",
|
|
113
|
+
description: "demo",
|
|
114
|
+
docs: {
|
|
115
|
+
topics: { readme: { text: "# readme\n" } },
|
|
116
|
+
},
|
|
117
|
+
commands: [{ key: "run", description: "Run.", handler: () => {} }],
|
|
118
|
+
});
|
|
119
|
+
const help = cliHelpRender(cliPresentationRoot(root), [], false);
|
|
120
|
+
expect(help).toContain("For AI agents: `myapp docs skill`.");
|
|
121
|
+
expect(help).not.toContain("install --skill");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("root help omits agent hint when docs disabled", () => {
|
|
125
|
+
const root = testProgram({
|
|
126
|
+
key: "myapp",
|
|
127
|
+
version: "1.0.0",
|
|
128
|
+
description: "demo",
|
|
129
|
+
docs: { enabled: false },
|
|
130
|
+
commands: [{ key: "run", description: "Run.", handler: () => {} }],
|
|
131
|
+
});
|
|
132
|
+
const help = cliHelpRender(cliPresentationRoot(root), [], false);
|
|
133
|
+
expect(help).not.toContain("Agents:");
|
|
134
|
+
expect(help).not.toContain("docs skill");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("root help includes program notes and agent hint", () => {
|
|
138
|
+
const root = testProgram({
|
|
139
|
+
key: "myapp",
|
|
140
|
+
version: "1.0.0",
|
|
141
|
+
description: "demo",
|
|
142
|
+
notes: "See `{argsbarg:program} docs readme` for the user guide.",
|
|
143
|
+
docs: {
|
|
144
|
+
topics: { readme: { text: "# readme\n" } },
|
|
145
|
+
},
|
|
146
|
+
commands: [{ key: "run", description: "Run.", handler: () => {} }],
|
|
147
|
+
});
|
|
148
|
+
const help = cliHelpRender(cliPresentationRoot(root), [], false);
|
|
149
|
+
expect(help).toContain("See `myapp docs readme` for the user guide.");
|
|
150
|
+
expect(help).toContain("myapp docs skill");
|
|
151
|
+
});
|
|
152
|
+
});
|
package/src/help.ts
CHANGED
|
@@ -7,7 +7,6 @@ It keeps help formatting shared across help and error paths so users see one con
|
|
|
7
7
|
style no matter how help is reached.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { visibleOptions, visibleSubcommands } from "./hidden.ts";
|
|
11
10
|
import {
|
|
12
11
|
type CliNode,
|
|
13
12
|
type CliOption,
|
|
@@ -17,7 +16,8 @@ import {
|
|
|
17
16
|
isCliLeaf,
|
|
18
17
|
isCliRouter,
|
|
19
18
|
isJsonLeaf,
|
|
20
|
-
} from "./types.ts";
|
|
19
|
+
} from "./core/types.ts";
|
|
20
|
+
import { visibleOptions, visibleSubcommands } from "./runtime/exposure.ts";
|
|
21
21
|
|
|
22
22
|
// ── ANSI Style Helpers ────────────────────────────────────────────────────────
|
|
23
23
|
|
|
@@ -197,7 +197,7 @@ export function cliOptionLabel(o: CliOption, color: boolean): string {
|
|
|
197
197
|
return `${style.aquaBold(left)} ${style.greenBright(right)}`;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
/** Placeholder in `notes` for the root program key (resolved in help, schema, and docs
|
|
200
|
+
/** Placeholder in `notes` for the root program key (resolved in help, schema, and docs cli). */
|
|
201
201
|
export const CLI_NOTES_PROGRAM = "{argsbarg:program}";
|
|
202
202
|
|
|
203
203
|
/** Replaces `{argsbarg:program}` in notes/help text with the program key. */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Detects built-in command paths so invoke hooks are skipped for framework commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const BUILTIN_ROOTS = new Set(["completion", "version", "http", "mcp", "configure", "docs"]);
|
|
6
|
+
|
|
7
|
+
/** True when `path` routes to a framework built-in (hooks are skipped). */
|
|
8
|
+
export function isBuiltinInvokePath(path: string[]): boolean {
|
|
9
|
+
const root = path[0];
|
|
10
|
+
if (!root || !BUILTIN_ROOTS.has(root)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (root === "http") {
|
|
14
|
+
return path.length <= 1 || path[1] === "serve";
|
|
15
|
+
}
|
|
16
|
+
if (root === "mcp") {
|
|
17
|
+
return path.length <= 1 || path[1] === "serve" || path[1] === "bundle";
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
package/src/hooks/run.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Safe async hook runner, failure classification, and invoke error pipeline.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CliContext } from "~/core/context.ts";
|
|
6
|
+
import { LeafInputError } from "~/core/leaf-inputs.ts";
|
|
7
|
+
import type {
|
|
8
|
+
ClientErrorOverride,
|
|
9
|
+
ErrorHookContext,
|
|
10
|
+
InvokeFailureKind,
|
|
11
|
+
InvokeHookContext,
|
|
12
|
+
ServerRuntime,
|
|
13
|
+
} from "~/core/types.ts";
|
|
14
|
+
import { firstErrorLine } from "~/http/result.ts";
|
|
15
|
+
import { type LogEmitter, obscureUnexpectedClientMessage } from "~/log/emitter.ts";
|
|
16
|
+
|
|
17
|
+
/** Runs a hook without letting hook throws escape uncaught. */
|
|
18
|
+
export async function runHook<T>(hook: (() => T | Promise<T>) | undefined, label: string): Promise<T | undefined> {
|
|
19
|
+
if (!hook) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return await Promise.resolve(hook());
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26
|
+
throw new Error(`${label} hook failed: ${message}`, { cause: err });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Classifies an invoke failure for status mapping and logging. */
|
|
31
|
+
export function classifyFailureKind(
|
|
32
|
+
err: unknown,
|
|
33
|
+
opts: { parseError?: boolean; help?: boolean; missingConfig?: boolean; notReady?: boolean },
|
|
34
|
+
): InvokeFailureKind {
|
|
35
|
+
if (opts.help) {
|
|
36
|
+
return "help";
|
|
37
|
+
}
|
|
38
|
+
if (opts.missingConfig) {
|
|
39
|
+
return "missing_config";
|
|
40
|
+
}
|
|
41
|
+
if (opts.notReady) {
|
|
42
|
+
return "not_ready";
|
|
43
|
+
}
|
|
44
|
+
if (opts.parseError || err instanceof LeafInputError) {
|
|
45
|
+
return "validation";
|
|
46
|
+
}
|
|
47
|
+
if (err instanceof Error) {
|
|
48
|
+
return "validation";
|
|
49
|
+
}
|
|
50
|
+
return "unexpected";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** HTTP status for a classified failure kind. */
|
|
54
|
+
export function failureKindHttpStatus(kind: InvokeFailureKind): number {
|
|
55
|
+
switch (kind) {
|
|
56
|
+
case "validation":
|
|
57
|
+
case "help":
|
|
58
|
+
return 400;
|
|
59
|
+
case "unknown_route":
|
|
60
|
+
return 404;
|
|
61
|
+
case "missing_config":
|
|
62
|
+
case "not_ready":
|
|
63
|
+
return 503;
|
|
64
|
+
case "unexpected":
|
|
65
|
+
return 500;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Builds {@link InvokeHookContext} from a live {@link CliContext}. */
|
|
70
|
+
export function buildInvokeHookContext(
|
|
71
|
+
ctx: CliContext,
|
|
72
|
+
extras: {
|
|
73
|
+
path: string[];
|
|
74
|
+
runtime?: ServerRuntime;
|
|
75
|
+
http?: InvokeHookContext["http"];
|
|
76
|
+
mcp?: InvokeHookContext["mcp"];
|
|
77
|
+
},
|
|
78
|
+
): InvokeHookContext {
|
|
79
|
+
return {
|
|
80
|
+
invocation: ctx.invocation,
|
|
81
|
+
path: extras.path,
|
|
82
|
+
pathParams: { ...ctx.pathParams },
|
|
83
|
+
opts: ctx.opts,
|
|
84
|
+
locals: ctx.locals,
|
|
85
|
+
runtime: extras.runtime,
|
|
86
|
+
appConfig: ctx.appConfig,
|
|
87
|
+
http: extras.http,
|
|
88
|
+
mcp: extras.mcp,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function defaultClientError(err: unknown, _failureKind: InvokeFailureKind): ClientErrorOverride {
|
|
93
|
+
const message =
|
|
94
|
+
err instanceof Error ? err.message : typeof err === "string" ? err : firstErrorLine(String(err)) || "Error";
|
|
95
|
+
return { message, exitCode: 1 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ErrorPipelineResult {
|
|
99
|
+
failureKind: InvokeFailureKind;
|
|
100
|
+
clientError: ClientErrorOverride;
|
|
101
|
+
errorMsg: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Runs formatError → onError → ECS emit for one invoke failure. */
|
|
105
|
+
export async function runErrorPipeline(
|
|
106
|
+
hookCtx: InvokeHookContext,
|
|
107
|
+
err: unknown,
|
|
108
|
+
failureKind: InvokeFailureKind,
|
|
109
|
+
hooks: import("~/core/types.ts").CliProgramHooks | undefined,
|
|
110
|
+
emitter: LogEmitter | undefined,
|
|
111
|
+
obscureUnexpected: boolean,
|
|
112
|
+
): Promise<ErrorPipelineResult> {
|
|
113
|
+
let clientError = defaultClientError(err, failureKind);
|
|
114
|
+
if (failureKind === "unexpected" && obscureUnexpected) {
|
|
115
|
+
clientError = { message: obscureUnexpectedClientMessage(), exitCode: 1 };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const errorCtx: ErrorHookContext = {
|
|
119
|
+
...hookCtx,
|
|
120
|
+
failureKind,
|
|
121
|
+
error: err,
|
|
122
|
+
clientError: { ...clientError },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const formatted = await runHook(() => hooks?.formatError?.(errorCtx), "formatError");
|
|
126
|
+
if (formatted) {
|
|
127
|
+
clientError = { ...clientError, ...formatted };
|
|
128
|
+
errorCtx.clientError = { ...clientError };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await runHook(() => hooks?.onError?.(errorCtx), "onError");
|
|
132
|
+
|
|
133
|
+
const displayMessage =
|
|
134
|
+
failureKind === "unexpected" && obscureUnexpected ? obscureUnexpectedClientMessage() : clientError.message;
|
|
135
|
+
|
|
136
|
+
emitter?.emitInvokeError(failureKind, err, displayMessage, {
|
|
137
|
+
invocation: hookCtx.invocation,
|
|
138
|
+
path: hookCtx.path.join(" "),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return { failureKind, clientError, errorMsg: displayMessage };
|
|
142
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Hand-built OpenAPI 3.1 document from exposed HTTP REST routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { collectOptionDefs } from "~/core/parse.ts";
|
|
6
|
+
import type { CliHttpMethod, CliNode, CliProgram } from "~/core/types.ts";
|
|
7
|
+
import { CliOptionKind, isCliLeaf, isJsonLeaf } from "~/core/types.ts";
|
|
8
|
+
import { collectHttpRoutes, defaultSuccessStatus } from "./routes.ts";
|
|
9
|
+
import { dereferenceJsonSchema } from "./schema-deref.ts";
|
|
10
|
+
|
|
11
|
+
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
12
|
+
|
|
13
|
+
function defaultErrorSchema(): Record<string, unknown> {
|
|
14
|
+
return {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: { error: { type: "string" } },
|
|
17
|
+
required: ["error"],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function errorResponseSchema(program: CliProgram): Record<string, unknown> {
|
|
22
|
+
const custom = program.httpServer?.errors?.errorSchema;
|
|
23
|
+
return custom ? dereferenceJsonSchema(custom) : defaultErrorSchema();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function errorResponseEntry(program: CliProgram, description: string): Record<string, unknown> {
|
|
27
|
+
return {
|
|
28
|
+
description,
|
|
29
|
+
content: {
|
|
30
|
+
[JSON_CONTENT_TYPE]: {
|
|
31
|
+
schema: errorResponseSchema(program),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildInputSchema(
|
|
38
|
+
program: CliProgram,
|
|
39
|
+
route: ReturnType<typeof collectHttpRoutes>[number],
|
|
40
|
+
): Record<string, unknown> {
|
|
41
|
+
const leaf = route.leaf;
|
|
42
|
+
if (leaf.inputSchema) {
|
|
43
|
+
return leaf.inputSchema;
|
|
44
|
+
}
|
|
45
|
+
const properties: Record<string, unknown> = {};
|
|
46
|
+
const required: string[] = [];
|
|
47
|
+
for (const p of route.paramNames) {
|
|
48
|
+
properties[p] = { type: "string" };
|
|
49
|
+
required.push(p);
|
|
50
|
+
}
|
|
51
|
+
const argv = route.commandPath.filter((k) => !k.startsWith(":"));
|
|
52
|
+
for (const opt of collectOptionDefs(program, argv)) {
|
|
53
|
+
if (opt.kind === CliOptionKind.Json) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
properties[opt.name] = { type: "string", description: opt.description };
|
|
57
|
+
if (opt.required) {
|
|
58
|
+
required.push(opt.name);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const p of leaf.positionals ?? []) {
|
|
62
|
+
properties[p.name] = { type: "string", description: p.description };
|
|
63
|
+
if ((p.argMin ?? 1) >= 1) {
|
|
64
|
+
required.push(p.name);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties,
|
|
70
|
+
...(required.length > 0 ? { required } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Builds success response entries for OpenAPI (status → response object). */
|
|
75
|
+
function buildSuccessResponses(route: ReturnType<typeof collectHttpRoutes>[number]): Record<string, unknown> {
|
|
76
|
+
const contentType = route.leaf.http?.successContentType ?? "application/json";
|
|
77
|
+
const media: Record<string, unknown> = {};
|
|
78
|
+
const method = route.method;
|
|
79
|
+
|
|
80
|
+
if (contentType.includes("application/json")) {
|
|
81
|
+
const outputSchema = route.leaf.outputSchema ?? { type: "object" };
|
|
82
|
+
media[contentType] = {
|
|
83
|
+
schema: dereferenceJsonSchema(outputSchema),
|
|
84
|
+
};
|
|
85
|
+
} else if (contentType.includes("text/html")) {
|
|
86
|
+
media[contentType] = { schema: { type: "string" } };
|
|
87
|
+
} else {
|
|
88
|
+
media[contentType] = { schema: { type: "string", format: "binary" } };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const status = String(route.leaf.http?.successStatus ?? defaultSuccessStatus(method, method !== "DELETE"));
|
|
92
|
+
if (method === "DELETE" && status === "204") {
|
|
93
|
+
return {
|
|
94
|
+
"204": { description: "Successful invocation" },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
[status]: {
|
|
99
|
+
description: "Successful invocation",
|
|
100
|
+
content: media,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function methodLower(method: CliHttpMethod): string {
|
|
106
|
+
return method.toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const HEALTH_TAG = "health";
|
|
110
|
+
|
|
111
|
+
const livenessResponseSchema = {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: { ok: { type: "boolean", const: true } },
|
|
114
|
+
required: ["ok"],
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
117
|
+
const readinessCheckSchema = {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
ok: { type: "boolean" },
|
|
121
|
+
error: { type: "string" },
|
|
122
|
+
missing: { type: "array", items: { type: "string" } },
|
|
123
|
+
},
|
|
124
|
+
required: ["ok"],
|
|
125
|
+
} as const;
|
|
126
|
+
|
|
127
|
+
const readinessResponseSchema = {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
ok: { type: "boolean" },
|
|
131
|
+
checks: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
config_file: readinessCheckSchema,
|
|
135
|
+
config_required: readinessCheckSchema,
|
|
136
|
+
custom: readinessCheckSchema,
|
|
137
|
+
},
|
|
138
|
+
required: ["config_file", "config_required", "custom"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ["ok", "checks"],
|
|
142
|
+
} as const;
|
|
143
|
+
|
|
144
|
+
function jsonResponseEntry(description: string, schema: Record<string, unknown>): Record<string, unknown> {
|
|
145
|
+
return {
|
|
146
|
+
description,
|
|
147
|
+
content: {
|
|
148
|
+
[JSON_CONTENT_TYPE]: { schema },
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function livenessGetOp(operationId: string, summary: string): Record<string, unknown> {
|
|
154
|
+
return {
|
|
155
|
+
tags: [HEALTH_TAG],
|
|
156
|
+
operationId,
|
|
157
|
+
summary,
|
|
158
|
+
responses: {
|
|
159
|
+
"200": jsonResponseEntry("Server is listening", livenessResponseSchema),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Framework health probe paths served alongside `/api/*` routes. */
|
|
165
|
+
function buildHealthPaths(): Record<string, unknown> {
|
|
166
|
+
return {
|
|
167
|
+
"/health": {
|
|
168
|
+
get: livenessGetOp("health", "Liveness probe (alias of /health/live)"),
|
|
169
|
+
},
|
|
170
|
+
"/health/live": {
|
|
171
|
+
get: livenessGetOp("health_live", "Liveness probe"),
|
|
172
|
+
},
|
|
173
|
+
"/health/ready": {
|
|
174
|
+
get: {
|
|
175
|
+
tags: [HEALTH_TAG],
|
|
176
|
+
operationId: "health_ready",
|
|
177
|
+
summary: "Readiness probe",
|
|
178
|
+
description: "Config file, required app config, and optional program.readiness checks.",
|
|
179
|
+
responses: {
|
|
180
|
+
"200": jsonResponseEntry("Ready to serve traffic", readinessResponseSchema),
|
|
181
|
+
"503": jsonResponseEntry("Not ready", readinessResponseSchema),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type HttpRoute = ReturnType<typeof collectHttpRoutes>[number];
|
|
189
|
+
|
|
190
|
+
/** Top-level command key for OpenAPI grouping (first non-`:param` segment). */
|
|
191
|
+
function topLevelCommandKey(route: HttpRoute, program: CliProgram): string {
|
|
192
|
+
const key = route.commandPath.find((k) => !k.startsWith(":"));
|
|
193
|
+
return key ?? program.key;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function findTopLevelCommand(program: CliProgram, key: string): CliNode | undefined {
|
|
197
|
+
if (isCliLeaf(program)) {
|
|
198
|
+
return program.key === key ? program : undefined;
|
|
199
|
+
}
|
|
200
|
+
return program.commands.find((c) => c.key === key);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** OpenAPI tags for user `/api/*` routes, one per top-level command. */
|
|
204
|
+
function collectCommandTags(program: CliProgram, routes: HttpRoute[]): { name: string; description?: string }[] {
|
|
205
|
+
const names = [...new Set(routes.map((route) => topLevelCommandKey(route, program)))].sort();
|
|
206
|
+
return names.map((name) => {
|
|
207
|
+
const node = findTopLevelCommand(program, name);
|
|
208
|
+
return node?.description ? { name, description: node.description } : { name };
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Generates an OpenAPI 3.1 document for the program's HTTP routes. */
|
|
213
|
+
export function generateOpenApi(program: CliProgram): Record<string, unknown> {
|
|
214
|
+
const routes = collectHttpRoutes(program);
|
|
215
|
+
const paths: Record<string, unknown> = program.httpServer?.enabled ? buildHealthPaths() : {};
|
|
216
|
+
const commandTags = collectCommandTags(program, routes);
|
|
217
|
+
|
|
218
|
+
for (const route of routes) {
|
|
219
|
+
const pathKey = route.openApiPath;
|
|
220
|
+
const existing = (paths[pathKey] as Record<string, unknown> | undefined) ?? {};
|
|
221
|
+
const op: Record<string, unknown> = {
|
|
222
|
+
tags: [topLevelCommandKey(route, program)],
|
|
223
|
+
operationId: route.openApiPath.replace(/\//g, "_").replace(/[{}]/g, ""),
|
|
224
|
+
summary: route.leaf.description ?? route.leaf.key,
|
|
225
|
+
responses: {
|
|
226
|
+
...buildSuccessResponses(route),
|
|
227
|
+
"400": errorResponseEntry(program, "Invalid arguments or help requested"),
|
|
228
|
+
"404": errorResponseEntry(program, "Not found"),
|
|
229
|
+
"500": errorResponseEntry(program, "Handler error"),
|
|
230
|
+
"503": errorResponseEntry(program, "Not ready or missing required config"),
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (route.paramNames.length > 0) {
|
|
235
|
+
op.parameters = route.paramNames.map((name) => ({
|
|
236
|
+
name,
|
|
237
|
+
in: "path",
|
|
238
|
+
required: true,
|
|
239
|
+
schema: { type: "string" },
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const method = methodLower(route.method);
|
|
244
|
+
if (method === "get" || method === "delete") {
|
|
245
|
+
op.parameters = [
|
|
246
|
+
...((op.parameters as unknown[]) ?? []),
|
|
247
|
+
...collectOptionDefs(
|
|
248
|
+
program,
|
|
249
|
+
route.commandPath.filter((k) => !k.startsWith(":")),
|
|
250
|
+
).map((opt) => ({
|
|
251
|
+
name: opt.name,
|
|
252
|
+
in: "query",
|
|
253
|
+
required: opt.required ?? false,
|
|
254
|
+
schema: { type: "string" },
|
|
255
|
+
description: opt.description,
|
|
256
|
+
})),
|
|
257
|
+
];
|
|
258
|
+
} else {
|
|
259
|
+
op.requestBody = {
|
|
260
|
+
required: isJsonLeaf(route.leaf),
|
|
261
|
+
content: {
|
|
262
|
+
[JSON_CONTENT_TYPE]: {
|
|
263
|
+
schema: dereferenceJsonSchema(buildInputSchema(program, route)),
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
existing[method] = op;
|
|
270
|
+
paths[pathKey] = existing;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
openapi: "3.1.0",
|
|
275
|
+
info: {
|
|
276
|
+
title: program.key,
|
|
277
|
+
version: program.version,
|
|
278
|
+
description: program.description,
|
|
279
|
+
},
|
|
280
|
+
...(program.httpServer?.enabled
|
|
281
|
+
? { tags: [{ name: HEALTH_TAG, description: "Server health probes" }, ...commandTags] }
|
|
282
|
+
: {}),
|
|
283
|
+
paths,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Pretty-printed OpenAPI JSON (same document as `GET /openapi.json`). */
|
|
288
|
+
export function openApiJson(program: CliProgram): string {
|
|
289
|
+
return `${JSON.stringify(generateOpenApi(program), null, 2)}\n`;
|
|
290
|
+
}
|