argsbarg 5.1.8 → 5.1.9
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 +18 -1
- package/docs/cli-program.md +3 -1
- package/docs/config-schema.md +5 -3
- package/docs/configure.md +13 -7
- package/docs/distribution-homebrew.md +3 -2
- package/examples/full-example/justfile +2 -5
- package/index.d.ts +10 -1
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +1 -3
- package/src/builtins/completion-bash.ts +6 -27
- package/src/builtins/completion-group.ts +1 -2
- package/src/builtins/completion-simulate-shared.ts +2 -10
- package/src/builtins/completion-zsh.ts +7 -33
- package/src/builtins/config.test.ts +28 -0
- package/src/builtins/config.ts +40 -10
- package/src/builtins/configure-copy.ts +3 -6
- package/src/builtins/dispatch.ts +2 -10
- package/src/builtins/export.ts +1 -3
- package/src/builtins/presentation.ts +2 -7
- package/src/builtins/registry.ts +1 -5
- package/src/cli-tool/create.test.ts +3 -6
- package/src/cli-tool/create.ts +5 -22
- package/src/cli-tool/post-create.ts +1 -3
- package/src/cli-tool/program.ts +1 -2
- package/src/cli-tool/run-create.ts +2 -8
- package/src/cli.ts +6 -27
- package/src/config/bindings.test.ts +72 -0
- package/src/config/bindings.ts +115 -0
- package/src/config/bootstrap.ts +80 -65
- package/src/config/context.test.ts +45 -0
- package/src/config/context.ts +74 -14
- package/src/config/entry.ts +1 -3
- package/src/config/file.test.ts +46 -3
- package/src/config/file.ts +59 -19
- package/src/config/resolve.ts +1 -4
- package/src/config/schema.ts +1 -3
- package/src/config/validate.test.ts +3 -11
- package/src/config/validate.ts +30 -30
- package/src/config.integration.test.ts +1 -5
- package/src/configure/configure.test.ts +19 -5
- package/src/configure/index.ts +17 -15
- package/src/docs/api-guide.ts +1 -6
- package/src/docs/builtin.ts +1 -5
- package/src/docs/docs.test.ts +2 -6
- package/src/docs/mcp-guide.ts +3 -13
- package/src/docs/mcp-resources.test.ts +1 -3
- package/src/docs/mcp-resources.ts +1 -6
- package/src/docs/save.ts +2 -10
- package/src/formats.test.ts +1 -7
- package/src/formats.ts +1 -5
- package/src/headless.test.ts +5 -17
- package/src/help.ts +9 -51
- package/src/hidden-mcpb.test.ts +3 -12
- package/src/hidden.ts +1 -3
- package/src/install/binary-placement.test.ts +2 -6
- package/src/install/binary-placement.ts +1 -4
- package/src/install/gh-release-update.ts +5 -22
- package/src/install/mcp-codex.ts +1 -2
- package/src/install/mcp-config.ts +2 -12
- package/src/install/mcp-opencode.test.ts +1 -3
- package/src/install/mcp-opencode.ts +3 -15
- package/src/install/plan.ts +2 -10
- package/src/install/status.ts +3 -5
- package/src/install/target-base.ts +2 -11
- package/src/install/target-detect.ts +1 -5
- package/src/install/target-effective.ts +3 -13
- package/src/install/target-mcp-cli.ts +4 -26
- package/src/install/target-mcp-json.ts +3 -14
- package/src/install/target-plan-build.ts +1 -5
- package/src/install/target-registry.ts +13 -21
- package/src/install/target-scope.ts +8 -25
- package/src/install/target-types.ts +1 -6
- package/src/install/targets/app.ts +2 -2
- package/src/install/targets/opencode-mcp.ts +1 -6
- package/src/install/targets.test.ts +3 -10
- package/src/install/uninstall.ts +2 -9
- package/src/invoke.test.ts +1 -3
- package/src/mcp/bundle.ts +4 -19
- package/src/mcp/claude.test.ts +3 -3
- package/src/mcp/claude.ts +4 -17
- package/src/mcp/env.ts +1 -3
- package/src/mcp/server.ts +3 -12
- package/src/mcp/tools.ts +2 -11
- package/src/mcp.integration.test.ts +4 -10
- package/src/parse.test.ts +9 -34
- package/src/parse.ts +10 -44
- package/src/schema.ts +2 -10
- package/src/skill/hint.ts +1 -5
- package/src/skill/install.ts +1 -5
- package/src/test-fixtures.ts +1 -3
- package/src/validate.ts +29 -90
package/src/configure/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ Interactive and automated `configure` command orchestration (agent artifacts and
|
|
|
4
4
|
|
|
5
5
|
import { resolveCapabilities } from "../capabilities.ts";
|
|
6
6
|
import { displayAppConfigPath, runConfigure } from "../config/bootstrap.ts";
|
|
7
|
+
import { ensureAppConfigFile } from "../config/file.ts";
|
|
7
8
|
import { resolveInstallPaths } from "../install/paths.ts";
|
|
8
9
|
import { buildInstallPlan, buildUpdatePlan } from "../install/plan.ts";
|
|
9
10
|
import {
|
|
@@ -25,11 +26,7 @@ import type {
|
|
|
25
26
|
TargetPlanContext,
|
|
26
27
|
UninstallAction,
|
|
27
28
|
} from "../install/target-types.ts";
|
|
28
|
-
import {
|
|
29
|
-
buildUninstallPlan,
|
|
30
|
-
skillDirFromUninstallSummary,
|
|
31
|
-
uninstallSkillDir,
|
|
32
|
-
} from "../install/uninstall.ts";
|
|
29
|
+
import { buildUninstallPlan, skillDirFromUninstallSummary, uninstallSkillDir } from "../install/uninstall.ts";
|
|
33
30
|
import { cliSkillInstall, skillTargetFromActionKind } from "../skill/install.ts";
|
|
34
31
|
import type { CliProgram } from "../types.ts";
|
|
35
32
|
import { artifactPromptLabel, promptTargetAction } from "./prompt.ts";
|
|
@@ -218,8 +215,7 @@ async function runInteractiveConfigure(root: CliProgram, opts: ConfigureOpts): P
|
|
|
218
215
|
if (actions.length === 0) continue;
|
|
219
216
|
|
|
220
217
|
const installActions = actions.filter(
|
|
221
|
-
(a): a is InstallAction =>
|
|
222
|
-
"kind" in a && typeof a.kind === "string" && a.kind.includes("-mcp"),
|
|
218
|
+
(a): a is InstallAction => "kind" in a && typeof a.kind === "string" && a.kind.includes("-mcp"),
|
|
223
219
|
);
|
|
224
220
|
if (choice === "install" && resolveCapabilities(root).mcp && installActions.length > 0) {
|
|
225
221
|
runTargetPreflight(root, paths, mutationOpts, installActions);
|
|
@@ -241,6 +237,17 @@ async function runAutomatedConfigure(root: CliProgram, opts: ConfigureOpts): Pro
|
|
|
241
237
|
return [];
|
|
242
238
|
}
|
|
243
239
|
|
|
240
|
+
const changed: string[] = [];
|
|
241
|
+
|
|
242
|
+
if (installOpts.reinstall && !installOpts.uninstall) {
|
|
243
|
+
const bootstrapped = ensureAppConfigFile(root, !!installOpts.dry);
|
|
244
|
+
if (bootstrapped) {
|
|
245
|
+
const display = displayAppConfigPath(root);
|
|
246
|
+
changed.push(display);
|
|
247
|
+
installOut(`Initialized config: ${display}`, installOpts);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
244
251
|
let actions: Array<InstallAction | UninstallAction>;
|
|
245
252
|
if (installOpts.uninstall) {
|
|
246
253
|
actions = buildUninstallPlan(root, paths, installOpts);
|
|
@@ -257,14 +264,11 @@ async function runAutomatedConfigure(root: CliProgram, opts: ConfigureOpts): Pro
|
|
|
257
264
|
runTargetPreflight(root, paths, installOpts, installActions);
|
|
258
265
|
}
|
|
259
266
|
|
|
260
|
-
return executePlan(root, actions, installOpts, true);
|
|
267
|
+
return [...changed, ...executePlan(root, actions, installOpts, true)];
|
|
261
268
|
}
|
|
262
269
|
|
|
263
270
|
/** Main configure command orchestrator. */
|
|
264
|
-
export async function cliConfigure(
|
|
265
|
-
root: CliProgram,
|
|
266
|
-
rawOpts: Record<string, string>,
|
|
267
|
-
): Promise<never> {
|
|
271
|
+
export async function cliConfigure(root: CliProgram, rawOpts: Record<string, string>): Promise<never> {
|
|
268
272
|
const opts = parseConfigureOpts(rawOpts);
|
|
269
273
|
const err = validateConfigureOpts(opts);
|
|
270
274
|
if (err) {
|
|
@@ -276,9 +280,7 @@ export async function cliConfigure(
|
|
|
276
280
|
|
|
277
281
|
let changed: string[] = [];
|
|
278
282
|
try {
|
|
279
|
-
changed = isInteractive
|
|
280
|
-
? await runInteractiveConfigure(root, opts)
|
|
281
|
-
: await runAutomatedConfigure(root, opts);
|
|
283
|
+
changed = isInteractive ? await runInteractiveConfigure(root, opts) : await runAutomatedConfigure(root, opts);
|
|
282
284
|
} catch (mutationErr) {
|
|
283
285
|
installErr(mutationErr instanceof Error ? mutationErr.message : String(mutationErr));
|
|
284
286
|
process.exit(1);
|
package/src/docs/api-guide.ts
CHANGED
|
@@ -90,12 +90,7 @@ function fallbackLine(node: CliSchemaExport): string | null {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/** Renders one command node and recurses into subcommands. */
|
|
93
|
-
function renderCommandNode(
|
|
94
|
-
rootKey: string,
|
|
95
|
-
path: string[],
|
|
96
|
-
node: CliSchemaExport,
|
|
97
|
-
lines: string[],
|
|
98
|
-
): void {
|
|
93
|
+
function renderCommandNode(rootKey: string, path: string[], node: CliSchemaExport, lines: string[]): void {
|
|
99
94
|
const level = Math.min(path.length + 2, 6);
|
|
100
95
|
const heading = "#".repeat(level);
|
|
101
96
|
const cmd = commandPath(rootKey, path);
|
package/src/docs/builtin.ts
CHANGED
|
@@ -25,11 +25,7 @@ const DOCS_SAVE_OPTION: CliOption = {
|
|
|
25
25
|
kind: CliOptionKind.Presence,
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
function runDocsTopic(
|
|
29
|
-
program: CliProgram,
|
|
30
|
-
topic: string,
|
|
31
|
-
ctx: { hasFlag(name: string): boolean },
|
|
32
|
-
): void {
|
|
28
|
+
function runDocsTopic(program: CliProgram, topic: string, ctx: { hasFlag(name: string): boolean }): void {
|
|
33
29
|
if (ctx.hasFlag("save")) {
|
|
34
30
|
process.stdout.write(`${saveDocsTopic(program, topic)}\n`);
|
|
35
31
|
return;
|
package/src/docs/docs.test.ts
CHANGED
|
@@ -136,9 +136,7 @@ test("presentation includes docs subtree", () => {
|
|
|
136
136
|
const presentation = cliPresentationRoot(docsFixture());
|
|
137
137
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
138
138
|
expect(docsNode).toBeDefined();
|
|
139
|
-
expect(
|
|
140
|
-
docsNode && "commands" in docsNode && docsNode.commands.some((c) => c.key === "readme"),
|
|
141
|
-
).toBe(true);
|
|
139
|
+
expect(docsNode && "commands" in docsNode && docsNode.commands.some((c) => c.key === "readme")).toBe(true);
|
|
142
140
|
});
|
|
143
141
|
|
|
144
142
|
test("docs schema prints JSON", async () => {
|
|
@@ -243,9 +241,7 @@ test("docs api --save prepends generated hint", async () => {
|
|
|
243
241
|
const result = await new Cli(docsFixture()).invoke(["docs", "api", "--save"]);
|
|
244
242
|
expect(result.exitCode).toBe(0);
|
|
245
243
|
const text = readFileSync(join(workDir, "docs/api.md"), "utf8");
|
|
246
|
-
expect(text.startsWith("<!-- Generated by myapp docs api --save; do not edit. -->\n\n")).toBe(
|
|
247
|
-
true,
|
|
248
|
-
);
|
|
244
|
+
expect(text.startsWith("<!-- Generated by myapp docs api --save; do not edit. -->\n\n")).toBe(true);
|
|
249
245
|
expect(text).toContain("CLI API reference");
|
|
250
246
|
});
|
|
251
247
|
|
package/src/docs/mcp-guide.ts
CHANGED
|
@@ -2,12 +2,7 @@ import { resolveCapabilities } from "../capabilities.ts";
|
|
|
2
2
|
import { defaultConfigEntryTitle } from "../config/entry.ts";
|
|
3
3
|
import { displayAppConfigPath } from "../config/file.ts";
|
|
4
4
|
import { expectedOpenCodeMcpEntry, OPENCODE_CONFIG_SCHEMA } from "../install/mcp-opencode.ts";
|
|
5
|
-
import {
|
|
6
|
-
collectMcpTools,
|
|
7
|
-
type McpToolDef,
|
|
8
|
-
mcpServerId,
|
|
9
|
-
resolveMcpSchemaUri,
|
|
10
|
-
} from "../mcp/tools.ts";
|
|
5
|
+
import { collectMcpTools, type McpToolDef, mcpServerId, resolveMcpSchemaUri } from "../mcp/tools.ts";
|
|
11
6
|
import { collectOptionDefs } from "../parse.ts";
|
|
12
7
|
import { CliOptionKind, type CliProgram } from "../types.ts";
|
|
13
8
|
import { resolveDocsTopicResourceUri } from "./mcp-resources.ts";
|
|
@@ -115,10 +110,7 @@ export function generateMcpGuide(root: CliProgram): string {
|
|
|
115
110
|
"",
|
|
116
111
|
);
|
|
117
112
|
} else {
|
|
118
|
-
lines.push(
|
|
119
|
-
`The CLI binary \`${root.key}\` must already be on your PATH. Host configs reference it by name.`,
|
|
120
|
-
"",
|
|
121
|
-
);
|
|
113
|
+
lines.push(`The CLI binary \`${root.key}\` must already be on your PATH. Host configs reference it by name.`, "");
|
|
122
114
|
}
|
|
123
115
|
|
|
124
116
|
lines.push(
|
|
@@ -217,9 +209,7 @@ export function generateMcpGuide(root: CliProgram): string {
|
|
|
217
209
|
if (docs) {
|
|
218
210
|
for (const key of docsUserTopicKeys(docs)) {
|
|
219
211
|
const uri = resolveDocsTopicResourceUri(root, key);
|
|
220
|
-
lines.push(
|
|
221
|
-
`| Docs topic \`${key}\` | \`${uri}\` — same markdown as \`${root.key} docs ${key}\` |`,
|
|
222
|
-
);
|
|
212
|
+
lines.push(`| Docs topic \`${key}\` | \`${uri}\` — same markdown as \`${root.key} docs ${key}\` |`);
|
|
223
213
|
}
|
|
224
214
|
}
|
|
225
215
|
}
|
|
@@ -61,7 +61,5 @@ test("docsMcpResources empty when MCP disabled", () => {
|
|
|
61
61
|
|
|
62
62
|
test("reservedDocsTopicResourceUris matches docsMcpResources URIs", () => {
|
|
63
63
|
const program = fixture();
|
|
64
|
-
expect(reservedDocsTopicResourceUris(program)).toEqual(
|
|
65
|
-
docsMcpResources(program).map((r) => r.uri),
|
|
66
|
-
);
|
|
64
|
+
expect(reservedDocsTopicResourceUris(program)).toEqual(docsMcpResources(program).map((r) => r.uri));
|
|
67
65
|
});
|
|
@@ -3,12 +3,7 @@ Auto MCP resources for user docs.topics when docs and MCP are both enabled.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { CliProgram } from "../types.ts";
|
|
6
|
-
import {
|
|
7
|
-
docsEnabled,
|
|
8
|
-
docsTopicContent,
|
|
9
|
-
docsTopicDescription,
|
|
10
|
-
docsUserTopicKeys,
|
|
11
|
-
} from "./resolve.ts";
|
|
6
|
+
import { docsEnabled, docsTopicContent, docsTopicDescription, docsUserTopicKeys } from "./resolve.ts";
|
|
12
7
|
|
|
13
8
|
/** Default URI pattern for a docs topic MCP resource (`<mcpId>://docs/<topicKey>`). */
|
|
14
9
|
export function defaultDocsTopicResourceUri(mcpId: string, topicKey: string): string {
|
package/src/docs/save.ts
CHANGED
|
@@ -21,20 +21,12 @@ export function docsSaveGeneratedHint(program: CliProgram, topic: string): strin
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** Inserts save hint without breaking YAML frontmatter (`docs skill`). */
|
|
24
|
-
export function applySaveGeneratedHint(
|
|
25
|
-
program: CliProgram,
|
|
26
|
-
topic: string,
|
|
27
|
-
content: string,
|
|
28
|
-
): string {
|
|
24
|
+
export function applySaveGeneratedHint(program: CliProgram, topic: string, content: string): string {
|
|
29
25
|
if (!docsTopicIsGeneratedByArgsbarg(topic)) {
|
|
30
26
|
return content;
|
|
31
27
|
}
|
|
32
28
|
const hint = docsSaveGeneratedHint(program, topic);
|
|
33
|
-
return insertGeneratedHint(
|
|
34
|
-
content,
|
|
35
|
-
hint,
|
|
36
|
-
topic === "skill" ? { afterFrontmatter: true } : undefined,
|
|
37
|
-
);
|
|
29
|
+
return insertGeneratedHint(content, hint, topic === "skill" ? { afterFrontmatter: true } : undefined);
|
|
38
30
|
}
|
|
39
31
|
|
|
40
32
|
/** File body for `--save` (hint on argsbarg-generated markdown only). */
|
package/src/formats.test.ts
CHANGED
|
@@ -3,13 +3,7 @@ Tests for formats module behavior.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { expect, test } from "bun:test";
|
|
6
|
-
import {
|
|
7
|
-
parseCommaList,
|
|
8
|
-
parseDate,
|
|
9
|
-
parseDateTime,
|
|
10
|
-
parseDurationMs,
|
|
11
|
-
validateFormatValue,
|
|
12
|
-
} from "./formats.ts";
|
|
6
|
+
import { parseCommaList, parseDate, parseDateTime, parseDurationMs, validateFormatValue } from "./formats.ts";
|
|
13
7
|
import { CliValueFormat } from "./types.ts";
|
|
14
8
|
|
|
15
9
|
test("parseDurationMs parses minutes and hours", () => {
|
package/src/formats.ts
CHANGED
|
@@ -108,11 +108,7 @@ export function formatValidationError(format: CliValueFormat, value: string): st
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/** Validates a string value against format and/or pattern metadata. */
|
|
111
|
-
export function validateFormatValue(
|
|
112
|
-
value: string,
|
|
113
|
-
format?: CliValueFormat,
|
|
114
|
-
pattern?: string,
|
|
115
|
-
): void {
|
|
111
|
+
export function validateFormatValue(value: string, format?: CliValueFormat, pattern?: string): void {
|
|
116
112
|
if (format !== undefined) {
|
|
117
113
|
switch (format) {
|
|
118
114
|
case CliValueFormat.Duration:
|
package/src/headless.test.ts
CHANGED
|
@@ -26,28 +26,16 @@ test("shouldRunHeadless is true for MCP and json", () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
test("shouldRunHeadlessWithPositionals requires positionals in non-tty", () => {
|
|
29
|
-
expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, [], false, false)).toBe(
|
|
30
|
-
|
|
31
|
-
);
|
|
32
|
-
expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, ["a"], false, false)).toBe(
|
|
33
|
-
true,
|
|
34
|
-
);
|
|
29
|
+
expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, [], false, false)).toBe(false);
|
|
30
|
+
expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, ["a"], false, false)).toBe(true);
|
|
35
31
|
});
|
|
36
32
|
|
|
37
33
|
/** Tests that shouldRunHeadlessWithYes requires yes in non-tty. */
|
|
38
34
|
test("shouldRunHeadlessWithYes requires yes in non-tty", () => {
|
|
35
|
+
expect(shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: true, hasRequiredArgs: true }, false)).toBe(true);
|
|
36
|
+
expect(shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: false, hasRequiredArgs: true }, false)).toBe(false);
|
|
39
37
|
expect(
|
|
40
|
-
shouldRunHeadlessWithYes({ invocation: "cli" }, { yes:
|
|
41
|
-
).toBe(true);
|
|
42
|
-
expect(
|
|
43
|
-
shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: false, hasRequiredArgs: true }, false),
|
|
44
|
-
).toBe(false);
|
|
45
|
-
expect(
|
|
46
|
-
shouldRunHeadlessWithYes(
|
|
47
|
-
{ invocation: "cli" },
|
|
48
|
-
{ yes: false, hasRequiredArgs: true, dryRun: true },
|
|
49
|
-
false,
|
|
50
|
-
),
|
|
38
|
+
shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: false, hasRequiredArgs: true, dryRun: true }, false),
|
|
51
39
|
).toBe(true);
|
|
52
40
|
});
|
|
53
41
|
|
package/src/help.ts
CHANGED
|
@@ -252,20 +252,10 @@ function renderTextBox(title: string, lines: string[], hw: number, color: boolea
|
|
|
252
252
|
|
|
253
253
|
for (const line of lines) {
|
|
254
254
|
const padded = padVisible(line, contentWidth);
|
|
255
|
-
out.push(
|
|
256
|
-
(color ? style.gray(kBoxV) : kBoxV) +
|
|
257
|
-
" " +
|
|
258
|
-
padded +
|
|
259
|
-
" " +
|
|
260
|
-
(color ? style.gray(kBoxV) : kBoxV),
|
|
261
|
-
);
|
|
255
|
+
out.push(`${color ? style.gray(kBoxV) : kBoxV} ${padded} ${color ? style.gray(kBoxV) : kBoxV}`);
|
|
262
256
|
}
|
|
263
257
|
|
|
264
|
-
out.push(
|
|
265
|
-
color
|
|
266
|
-
? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR)
|
|
267
|
-
: kBoxBL + repeatBoxH(borderWidth) + kBoxBR,
|
|
268
|
-
);
|
|
258
|
+
out.push(color ? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR) : kBoxBL + repeatBoxH(borderWidth) + kBoxBR);
|
|
269
259
|
|
|
270
260
|
return out;
|
|
271
261
|
}
|
|
@@ -287,11 +277,7 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
|
|
|
287
277
|
const bodyLines: string[] = [];
|
|
288
278
|
for (const row of rows) {
|
|
289
279
|
const wrapped = wrapText(row.description, descWidth);
|
|
290
|
-
const first =
|
|
291
|
-
row.label +
|
|
292
|
-
spaces(labelWidth - visibleWidth(row.label)) +
|
|
293
|
-
" " +
|
|
294
|
-
(color ? style.white(wrapped[0]) : wrapped[0]);
|
|
280
|
+
const first = `${row.label + spaces(labelWidth - visibleWidth(row.label))} ${color ? style.white(wrapped[0]) : wrapped[0]}`;
|
|
295
281
|
bodyLines.push(first);
|
|
296
282
|
for (let idx = 1; idx < wrapped.length; idx++) {
|
|
297
283
|
const pad = color ? style.gray(spaces(labelWidth)) : spaces(labelWidth);
|
|
@@ -324,20 +310,10 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
|
|
|
324
310
|
|
|
325
311
|
for (const line of bodyLines) {
|
|
326
312
|
const padded = padVisible(line, contentWidth);
|
|
327
|
-
out.push(
|
|
328
|
-
(color ? style.gray(kBoxV) : kBoxV) +
|
|
329
|
-
" " +
|
|
330
|
-
padded +
|
|
331
|
-
" " +
|
|
332
|
-
(color ? style.gray(kBoxV) : kBoxV),
|
|
333
|
-
);
|
|
313
|
+
out.push(`${color ? style.gray(kBoxV) : kBoxV} ${padded} ${color ? style.gray(kBoxV) : kBoxV}`);
|
|
334
314
|
}
|
|
335
315
|
|
|
336
|
-
out.push(
|
|
337
|
-
color
|
|
338
|
-
? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR)
|
|
339
|
-
: kBoxBL + repeatBoxH(borderWidth) + kBoxBR,
|
|
340
|
-
);
|
|
316
|
+
out.push(color ? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR) : kBoxBL + repeatBoxH(borderWidth) + kBoxBR);
|
|
341
317
|
|
|
342
318
|
return out;
|
|
343
319
|
}
|
|
@@ -402,13 +378,7 @@ function rowsForSubcommands(cmds: CliNode[]): HelpRow[] {
|
|
|
402
378
|
|
|
403
379
|
// ── Main Help Render ──────────────────────────────────────────────────────────
|
|
404
380
|
|
|
405
|
-
function appendNotesBox(
|
|
406
|
-
lines: string[],
|
|
407
|
-
notes: string | undefined,
|
|
408
|
-
appKey: string,
|
|
409
|
-
hw: number,
|
|
410
|
-
color: boolean,
|
|
411
|
-
): void {
|
|
381
|
+
function appendNotesBox(lines: string[], notes: string | undefined, appKey: string, hw: number, color: boolean): void {
|
|
412
382
|
if ((notes ?? "").length === 0) {
|
|
413
383
|
return;
|
|
414
384
|
}
|
|
@@ -441,21 +411,14 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
|
|
|
441
411
|
).join("\n"),
|
|
442
412
|
);
|
|
443
413
|
|
|
444
|
-
const optBox = renderTableBox(
|
|
445
|
-
"Options",
|
|
446
|
-
rowsForOptions(visibleOptions(schema.options), color),
|
|
447
|
-
hw,
|
|
448
|
-
color,
|
|
449
|
-
);
|
|
414
|
+
const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(schema.options), color), hw, color);
|
|
450
415
|
if (optBox.length > 0) {
|
|
451
416
|
lines.push("");
|
|
452
417
|
lines.push(optBox.join("\n"));
|
|
453
418
|
}
|
|
454
419
|
if ((schema.commands ?? []).length > 0) {
|
|
455
420
|
lines.push("");
|
|
456
|
-
lines.push(
|
|
457
|
-
renderTableBox("Commands", rowsForSubcommands(schema.commands ?? []), hw, color).join("\n"),
|
|
458
|
-
);
|
|
421
|
+
lines.push(renderTableBox("Commands", rowsForSubcommands(schema.commands ?? []), hw, color).join("\n"));
|
|
459
422
|
}
|
|
460
423
|
appendNotesBox(lines, schema.notes, schema.key, hw, color);
|
|
461
424
|
return `${lines.join("\n")}\n\n`;
|
|
@@ -496,12 +459,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
|
|
|
496
459
|
).join("\n"),
|
|
497
460
|
);
|
|
498
461
|
|
|
499
|
-
const optBox = renderTableBox(
|
|
500
|
-
"Options",
|
|
501
|
-
rowsForOptions(visibleOptions(node.options), color),
|
|
502
|
-
hw,
|
|
503
|
-
color,
|
|
504
|
-
);
|
|
462
|
+
const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(node.options), color), hw, color);
|
|
505
463
|
if (optBox.length > 0) {
|
|
506
464
|
lines.push("");
|
|
507
465
|
lines.push(optBox.join("\n"));
|
package/src/hidden-mcpb.test.ts
CHANGED
|
@@ -9,12 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import { exportPresentationBuiltins } from "./builtins/export.ts";
|
|
10
10
|
import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
|
|
11
11
|
import { cliHelpRender } from "./help.ts";
|
|
12
|
-
import {
|
|
13
|
-
defaultMcpBundlePaths,
|
|
14
|
-
generateMcpManifest,
|
|
15
|
-
packMcpBundle,
|
|
16
|
-
runMcpBundle,
|
|
17
|
-
} from "./mcp/bundle.ts";
|
|
12
|
+
import { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle, runMcpBundle } from "./mcp/bundle.ts";
|
|
18
13
|
import { collectMcpTools } from "./mcp/tools.ts";
|
|
19
14
|
import { cliSchemaExport } from "./schema.ts";
|
|
20
15
|
import { CliOptionKind, type CliProgram } from "./types.ts";
|
|
@@ -128,8 +123,7 @@ describe("mcp bundle", () => {
|
|
|
128
123
|
expect(manifest.manifest_version).toBe("0.3");
|
|
129
124
|
expect((manifest.server as { type: string }).type).toBe("binary");
|
|
130
125
|
expect((manifest.server as { entry_point: string }).entry_point).toBe("myapp");
|
|
131
|
-
const mcpConfig = (manifest.server as { mcp_config: { command: string; args: string[] } })
|
|
132
|
-
.mcp_config;
|
|
126
|
+
const mcpConfig = (manifest.server as { mcp_config: { command: string; args: string[] } }).mcp_config;
|
|
133
127
|
expect(mcpConfig.command).toBe("${__dirname}/myapp");
|
|
134
128
|
expect(mcpConfig.args).toEqual(["mcp"]);
|
|
135
129
|
expect((manifest.compatibility as { platforms: string[] }).platforms).toEqual(["darwin"]);
|
|
@@ -185,10 +179,7 @@ describe("mcp bundle", () => {
|
|
|
185
179
|
}
|
|
186
180
|
const lines = stdout.join("").trim().split("\n");
|
|
187
181
|
const norm = (p: string) => realpathSync.native(p);
|
|
188
|
-
expect(lines.map(norm)).toEqual([
|
|
189
|
-
norm(join(dist, "myapp.mcpb")),
|
|
190
|
-
norm(join(dist, "claude-plugin", "myapp.zip")),
|
|
191
|
-
]);
|
|
182
|
+
expect(lines.map(norm)).toEqual([norm(join(dist, "myapp.mcpb")), norm(join(dist, "claude-plugin", "myapp.zip"))]);
|
|
192
183
|
const zip = readFileSync(join(dist, "claude-plugin", "myapp.zip"));
|
|
193
184
|
expect(zip.indexOf(Buffer.from(".mcp.json"))).toBeGreaterThanOrEqual(0);
|
|
194
185
|
} finally {
|
package/src/hidden.ts
CHANGED
|
@@ -18,9 +18,7 @@ export function presentationNode(node: CliNode): CliNode | null {
|
|
|
18
18
|
}
|
|
19
19
|
const options = visibleOptions(node.options);
|
|
20
20
|
if (isCliRouter(node)) {
|
|
21
|
-
const commands = node.commands
|
|
22
|
-
.map((ch) => presentationNode(ch))
|
|
23
|
-
.filter((ch): ch is CliNode => ch !== null);
|
|
21
|
+
const commands = node.commands.map((ch) => presentationNode(ch)).filter((ch): ch is CliNode => ch !== null);
|
|
24
22
|
return { ...node, options, commands };
|
|
25
23
|
}
|
|
26
24
|
return { ...node, options };
|
|
@@ -3,15 +3,11 @@ Tests for install/binary-placement module behavior.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
-
import { chmodSync,
|
|
6
|
+
import { chmodSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import type { CliProgram } from "../types.ts";
|
|
10
|
-
import {
|
|
11
|
-
isAppInstalled,
|
|
12
|
-
isExternallyManagedBinary,
|
|
13
|
-
resolvePathCommand,
|
|
14
|
-
} from "./binary-placement.ts";
|
|
10
|
+
import { isAppInstalled, isExternallyManagedBinary, resolvePathCommand } from "./binary-placement.ts";
|
|
15
11
|
|
|
16
12
|
const program: CliProgram = {
|
|
17
13
|
key: "placementapp",
|
|
@@ -32,10 +32,7 @@ function realpathOrSelf(path: string): string {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** True when PATH resolves to the running executable (e.g. Homebrew Cellar). */
|
|
35
|
-
export function isExternallyManagedBinary(
|
|
36
|
-
key: string,
|
|
37
|
-
execPath: string = process.execPath,
|
|
38
|
-
): boolean {
|
|
35
|
+
export function isExternallyManagedBinary(key: string, execPath: string = process.execPath): boolean {
|
|
39
36
|
const resolved = resolvePathCommand(key);
|
|
40
37
|
if (!resolved) return false;
|
|
41
38
|
return resolved === realpathOrSelf(execPath);
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
chmodSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
mkdtempSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
rmSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
2
|
import { tmpdir } from "node:os";
|
|
11
3
|
import { dirname, join } from "node:path";
|
|
12
4
|
import type { CliUpdateGetLatest } from "../types.ts";
|
|
@@ -150,9 +142,7 @@ export function createGhFetchLatest(
|
|
|
150
142
|
? ` ${config.repoEnvHint}`
|
|
151
143
|
: ` No releases found for ${config.repo}.`
|
|
152
144
|
: "";
|
|
153
|
-
throw new Error(
|
|
154
|
-
`Failed to fetch latest release from ${config.repo}: ${detail || "unknown error"}.${hint}`,
|
|
155
|
-
);
|
|
145
|
+
throw new Error(`Failed to fetch latest release from ${config.repo}: ${detail || "unknown error"}.${hint}`);
|
|
156
146
|
}
|
|
157
147
|
|
|
158
148
|
let parsed: { tagName?: string };
|
|
@@ -172,16 +162,11 @@ export function createGhFetchLatest(
|
|
|
172
162
|
|
|
173
163
|
function ensureGhAvailable(): void {
|
|
174
164
|
if (Bun.which("gh") === null) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
"GitHub CLI (gh) is required. Install from https://cli.github.com/ and run `gh auth login`.",
|
|
177
|
-
);
|
|
165
|
+
throw new Error("GitHub CLI (gh) is required. Install from https://cli.github.com/ and run `gh auth login`.");
|
|
178
166
|
}
|
|
179
167
|
}
|
|
180
168
|
|
|
181
|
-
async function downloadReleaseAsset(
|
|
182
|
-
config: GhReleaseUpdateConfig,
|
|
183
|
-
tempDir: string,
|
|
184
|
-
): Promise<string> {
|
|
169
|
+
async function downloadReleaseAsset(config: GhReleaseUpdateConfig, tempDir: string): Promise<string> {
|
|
185
170
|
const result = await runGh([
|
|
186
171
|
"release",
|
|
187
172
|
"download",
|
|
@@ -206,9 +191,7 @@ async function downloadReleaseAsset(
|
|
|
206
191
|
return downloadedPath;
|
|
207
192
|
}
|
|
208
193
|
|
|
209
|
-
async function runGh(
|
|
210
|
-
args: string[],
|
|
211
|
-
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
194
|
+
async function runGh(args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
212
195
|
const proc = Bun.spawn(["gh", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
213
196
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
214
197
|
proc.exited,
|
package/src/install/mcp-codex.ts
CHANGED
|
@@ -45,8 +45,7 @@ export function readCodexMcpEntry(path: string, name: string): McpServerEntry |
|
|
|
45
45
|
const transport = block.match(/transport\s*=\s*\{[^}]*type\s*=\s*"stdio"[^}]*\}/s)?.[0];
|
|
46
46
|
const search = transport ?? block;
|
|
47
47
|
|
|
48
|
-
const cmd =
|
|
49
|
-
search.match(/command\s*=\s*"([^"]*)"/)?.[1] ?? search.match(/command\s*=\s*'([^']*)'/)?.[1];
|
|
48
|
+
const cmd = search.match(/command\s*=\s*"([^"]*)"/)?.[1] ?? search.match(/command\s*=\s*'([^']*)'/)?.[1];
|
|
50
49
|
if (!cmd) return undefined;
|
|
51
50
|
|
|
52
51
|
const argsMatch = search.match(/args\s*=\s*\[([^\]]*)\]/s);
|
|
@@ -29,12 +29,7 @@ export function readMcpServerEntry(path: string, name: string): McpServerEntry |
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Returns an error message when existing entry conflicts, or null if safe to merge. */
|
|
32
|
-
export function checkMcpConflict(
|
|
33
|
-
path: string,
|
|
34
|
-
name: string,
|
|
35
|
-
expected: McpServerEntry,
|
|
36
|
-
yes: boolean,
|
|
37
|
-
): string | null {
|
|
32
|
+
export function checkMcpConflict(path: string, name: string, expected: McpServerEntry, yes: boolean): string | null {
|
|
38
33
|
const existing = readMcpServerEntry(path, name);
|
|
39
34
|
if (existing && !entriesEqual(existing, expected) && !yes) {
|
|
40
35
|
return (
|
|
@@ -48,12 +43,7 @@ export function checkMcpConflict(
|
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
/** Merges MCP server entry into config file. */
|
|
51
|
-
export function mergeMcpConfig(
|
|
52
|
-
path: string,
|
|
53
|
-
name: string,
|
|
54
|
-
entry: McpServerEntry,
|
|
55
|
-
dry: boolean,
|
|
56
|
-
): void {
|
|
46
|
+
export function mergeMcpConfig(path: string, name: string, entry: McpServerEntry, dry: boolean): void {
|
|
57
47
|
if (dry) return;
|
|
58
48
|
let data: Record<string, unknown> = {};
|
|
59
49
|
if (existsSync(path)) {
|
|
@@ -57,9 +57,7 @@ describe("opencode mcp config", () => {
|
|
|
57
57
|
|
|
58
58
|
test("resolveOpenCodeConfigPathForInstall defaults to config.json", () => {
|
|
59
59
|
mkdirSync(opencodeConfigDir(home), { recursive: true });
|
|
60
|
-
expect(resolveOpenCodeConfigPathForInstall(home)).toBe(
|
|
61
|
-
join(opencodeConfigDir(home), "config.json"),
|
|
62
|
-
);
|
|
60
|
+
expect(resolveOpenCodeConfigPathForInstall(home)).toBe(join(opencodeConfigDir(home), "config.json"));
|
|
63
61
|
});
|
|
64
62
|
|
|
65
63
|
test("mergeOpenCodeMcpConfig writes mcp block", () => {
|
|
@@ -6,11 +6,7 @@ import { expectedMcpEntry } from "./mcp-config.ts";
|
|
|
6
6
|
export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
|
|
7
7
|
|
|
8
8
|
/** Global OpenCode config filenames, in precedence order (matches OpenCode). */
|
|
9
|
-
export const OPENCODE_CONFIG_FILENAMES = [
|
|
10
|
-
"opencode.jsonc",
|
|
11
|
-
"opencode.json",
|
|
12
|
-
"config.json",
|
|
13
|
-
] as const;
|
|
9
|
+
export const OPENCODE_CONFIG_FILENAMES = ["opencode.jsonc", "opencode.json", "config.json"] as const;
|
|
14
10
|
|
|
15
11
|
export interface OpenCodeLocalMcpEntry {
|
|
16
12
|
type: "local";
|
|
@@ -71,10 +67,7 @@ function readConfig(path: string): Record<string, unknown> {
|
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
/** Reads `mcp[name]` when it is a local stdio server. */
|
|
74
|
-
export function readOpenCodeMcpEntry(
|
|
75
|
-
path: string,
|
|
76
|
-
name: string,
|
|
77
|
-
): OpenCodeLocalMcpEntry | undefined {
|
|
70
|
+
export function readOpenCodeMcpEntry(path: string, name: string): OpenCodeLocalMcpEntry | undefined {
|
|
78
71
|
if (!existsSync(path)) return undefined;
|
|
79
72
|
try {
|
|
80
73
|
const data = readConfig(path);
|
|
@@ -122,12 +115,7 @@ export function checkOpenCodeMcpConflict(
|
|
|
122
115
|
}
|
|
123
116
|
|
|
124
117
|
/** Merges a local MCP entry into OpenCode `mcp` config. */
|
|
125
|
-
export function mergeOpenCodeMcpConfig(
|
|
126
|
-
path: string,
|
|
127
|
-
name: string,
|
|
128
|
-
entry: OpenCodeLocalMcpEntry,
|
|
129
|
-
dry: boolean,
|
|
130
|
-
): void {
|
|
118
|
+
export function mergeOpenCodeMcpConfig(path: string, name: string, entry: OpenCodeLocalMcpEntry, dry: boolean): void {
|
|
131
119
|
if (dry) return;
|
|
132
120
|
const data = readConfig(path);
|
|
133
121
|
if (!data.$schema) data.$schema = OPENCODE_CONFIG_SCHEMA;
|
package/src/install/plan.ts
CHANGED
|
@@ -7,20 +7,12 @@ import type { InstallAction, InstallOpts } from "./target-types.ts";
|
|
|
7
7
|
export type { InstallAction, InstallActionKind, InstallOpts } from "./target-types.ts";
|
|
8
8
|
|
|
9
9
|
/** Builds install actions for normal mode (--all / scoped targets). */
|
|
10
|
-
export function buildInstallPlan(
|
|
11
|
-
root: CliProgram,
|
|
12
|
-
paths: InstallPaths,
|
|
13
|
-
opts: InstallOpts,
|
|
14
|
-
): InstallAction[] {
|
|
10
|
+
export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: InstallOpts): InstallAction[] {
|
|
15
11
|
return buildInstallPlanFromTargets(root, paths, opts);
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
/** Builds update/reinstall actions; greenfield fallback when nothing detected. */
|
|
19
|
-
export function buildUpdatePlan(
|
|
20
|
-
root: CliProgram,
|
|
21
|
-
paths: InstallPaths,
|
|
22
|
-
opts: InstallOpts,
|
|
23
|
-
): InstallAction[] {
|
|
15
|
+
export function buildUpdatePlan(root: CliProgram, paths: InstallPaths, opts: InstallOpts): InstallAction[] {
|
|
24
16
|
const refresh = buildInstallPlanFromTargets(root, paths, {
|
|
25
17
|
...opts,
|
|
26
18
|
reinstall: true,
|
package/src/install/status.ts
CHANGED
|
@@ -79,12 +79,10 @@ export function printInstallStatus(root: CliProgram, opts: InstallOpts): void {
|
|
|
79
79
|
|
|
80
80
|
const configStatus = appConfigStatus(root);
|
|
81
81
|
if (configStatus) {
|
|
82
|
-
installOut(
|
|
83
|
-
` app config: ${configStatus.path}${configStatus.exists ? "" : " (missing)"}`,
|
|
84
|
-
opts,
|
|
85
|
-
);
|
|
82
|
+
installOut(` app config: ${configStatus.path}${configStatus.exists ? "" : " (missing)"}`, opts);
|
|
86
83
|
for (const req of configStatus.required) {
|
|
87
|
-
|
|
84
|
+
const bindingHint = req.binding && req.binding !== "missing" && req.set ? ` (${req.binding})` : "";
|
|
85
|
+
installOut(` ${req.key}: ${req.set ? "set" : "missing"}${bindingHint}`, opts);
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
88
|
}
|