argsbarg 4.1.0 → 5.0.1
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 +91 -85
- package/docs/README.md +8 -8
- package/docs/ai-skills.md +9 -9
- package/docs/bundled-docs.md +5 -5
- package/docs/cli-program.md +13 -11
- package/docs/config-schema.md +36 -9
- package/docs/configure.md +177 -0
- package/docs/developing.md +7 -7
- package/docs/distribution-homebrew.md +104 -0
- package/docs/mcp.md +11 -12
- package/docs/output-schema.md +1 -1
- package/examples/full-example/Formula/.gitkeep +0 -0
- package/examples/full-example/README.md +98 -0
- package/examples/full-example/biome.json +22 -0
- package/examples/{consumer-app → full-example}/bun.lock +2 -0
- package/examples/full-example/justfile +134 -0
- package/examples/{consumer-app → full-example}/package.json +10 -3
- package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
- package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
- package/examples/full-example/scripts/create-identity.ts +11 -0
- package/examples/full-example/scripts/formula-shared.ts +73 -0
- package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
- package/examples/full-example/scripts/print-identity.ts +27 -0
- package/examples/full-example/src/commands/echo/command.ts +21 -0
- package/examples/full-example/src/commands/status/command.test.ts +10 -0
- package/examples/full-example/src/commands/status/command.ts +36 -0
- package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
- package/examples/full-example/src/index.ts +10 -0
- package/examples/full-example/src/program.ts +57 -0
- package/examples/{consumer-app → full-example}/src/types.ts +1 -1
- package/examples/nested.ts +1 -3
- package/index.d.ts +49 -81
- package/package.json +2 -2
- package/src/builtins/builtins.test.ts +84 -64
- package/src/builtins/completion-group.ts +17 -17
- package/src/builtins/configure-copy.ts +86 -0
- package/src/builtins/configure.ts +70 -0
- package/src/builtins/dispatch.ts +13 -9
- package/src/builtins/index.ts +1 -1
- package/src/builtins/mcp.ts +2 -2
- package/src/builtins/registry.ts +6 -4
- package/src/capabilities.ts +22 -15
- package/src/cli-tool/cli-smoke.test.ts +29 -0
- package/src/cli-tool/create.test.ts +141 -0
- package/src/cli-tool/create.ts +402 -0
- package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +16 -15
- package/src/cli-tool/main.ts +8 -0
- package/src/cli-tool/post-create.ts +111 -0
- package/src/cli-tool/program.ts +97 -0
- package/src/cli-tool/prompt.ts +28 -0
- package/src/cli-tool/run-create.ts +138 -0
- package/src/cli.ts +0 -2
- package/src/config/bootstrap.ts +27 -18
- package/src/config/file.test.ts +1 -1
- package/src/config/resolve.test.ts +167 -0
- package/src/config/resolve.ts +52 -8
- package/src/configure/configure.test.ts +148 -0
- package/src/configure/index.ts +284 -0
- package/src/configure/prompt.ts +40 -0
- package/src/docs/api-guide.test.ts +4 -5
- package/src/docs/builtin.ts +3 -5
- package/src/docs/docs.test.ts +6 -5
- package/src/docs/mcp-guide.ts +11 -12
- package/src/index.ts +5 -12
- package/src/install/binary-placement.test.ts +101 -0
- package/src/install/binary-placement.ts +47 -0
- package/src/install/install-validate.test.ts +5 -5
- package/src/install/normalize-uninstall.ts +11 -0
- package/src/install/normalize.ts +4 -19
- package/src/install/opts.ts +17 -0
- package/src/install/paths.ts +0 -22
- package/src/install/plan.ts +14 -6
- package/src/install/shell.ts +0 -14
- package/src/install/status.test.ts +6 -6
- package/src/install/status.ts +0 -6
- package/src/install/target-effective.ts +8 -10
- package/src/install/target-scope.ts +26 -36
- package/src/install/target-types.ts +0 -16
- package/src/install/targets/app.ts +19 -28
- package/src/install/targets/configure.ts +6 -2
- package/src/install/targets/index.ts +0 -3
- package/src/install/targets.test.ts +26 -44
- package/src/invoke.test.ts +1 -1
- package/src/mcp/env.test.ts +92 -0
- package/src/mcp/env.ts +15 -14
- package/src/mcp/tools.ts +1 -1
- package/src/mcp.integration.test.ts +4 -4
- package/src/parse.test.ts +13 -14
- package/src/prompt.ts +10 -0
- package/src/schema.ts +1 -1
- package/src/skill/hint.ts +2 -2
- package/src/types.ts +48 -22
- package/src/validate.ts +22 -28
- package/docs/install.md +0 -290
- package/docs/templates/cursor/rules/cli-program.mdc +0 -31
- package/examples/config-app/main.ts +0 -20
- package/examples/config-app/program.ts +0 -78
- package/examples/config-app/schema.ts +0 -37
- package/examples/config-app/types.ts +0 -19
- package/examples/consumer-app/README.md +0 -56
- package/examples/consumer-app/src/main.ts +0 -15
- package/examples/consumer-app/src/program.ts +0 -108
- package/src/builtins/install.ts +0 -136
- package/src/install/app.ts +0 -94
- package/src/install/bootstrap.ts +0 -22
- package/src/install/completions.ts +0 -56
- package/src/install/index.ts +0 -415
- package/src/install/install.test.ts +0 -333
- package/src/install/targets/completions.ts +0 -133
- package/src/install/update.test.ts +0 -123
- package/src/install/update.ts +0 -54
- /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
- /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Interactive prompts for `argsbarg create`. */
|
|
2
|
+
|
|
3
|
+
import { readPromptLine as readStdinLine } from "../prompt.ts";
|
|
4
|
+
|
|
5
|
+
export function readPromptLine(prompt: string): string {
|
|
6
|
+
process.stderr.write(prompt);
|
|
7
|
+
return readStdinLine().trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function promptConfirm(message: string): boolean {
|
|
11
|
+
const ans = readPromptLine(`${message} [y/N]: `);
|
|
12
|
+
return ans === "y" || ans === "Y";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function promptOptional(label: string, current?: string): string | undefined {
|
|
16
|
+
const suffix = current ? ` [${current}]` : "";
|
|
17
|
+
const ans = readPromptLine(`${label}${suffix}: `);
|
|
18
|
+
if (ans.length === 0) return current;
|
|
19
|
+
return ans;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function promptRequired(label: string, current?: string): string {
|
|
23
|
+
while (true) {
|
|
24
|
+
const value = promptOptional(label, current);
|
|
25
|
+
if (value && value.length > 0) return value;
|
|
26
|
+
process.stderr.write(" (required)\n");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** `argsbarg create` command orchestration. */
|
|
2
|
+
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { relative, resolve } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
applyCreate,
|
|
7
|
+
type CreateOptions,
|
|
8
|
+
diffCreateDetails,
|
|
9
|
+
parseCreateArgv,
|
|
10
|
+
printCreateDiffs,
|
|
11
|
+
renderCreateTree,
|
|
12
|
+
resolveCreateOptions,
|
|
13
|
+
} from "./create.ts";
|
|
14
|
+
import { printPostCreatePlan, runPostCreate } from "./post-create.ts";
|
|
15
|
+
import { promptConfirm, promptOptional, promptRequired } from "./prompt.ts";
|
|
16
|
+
|
|
17
|
+
function isInteractiveTty(): boolean {
|
|
18
|
+
return Boolean(process.stdin.isTTY);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectInteractiveOptions(
|
|
22
|
+
partial: Partial<CreateOptions>,
|
|
23
|
+
dir: string,
|
|
24
|
+
): { opts: CreateOptions; baseDir: string } {
|
|
25
|
+
process.stderr.write("Argsbarg create — bootstrap a new CLI from full-example\n\n");
|
|
26
|
+
const targetDir = promptOptional("Target directory", dir) ?? dir;
|
|
27
|
+
const baseDir = resolve(process.cwd(), targetDir);
|
|
28
|
+
const key = promptRequired("CLI key (binary name)", partial.key);
|
|
29
|
+
const releaseRepo = promptRequired("GitHub release repo (org/repo)", partial.releaseRepo);
|
|
30
|
+
|
|
31
|
+
const opts = resolveCreateOptions(
|
|
32
|
+
{
|
|
33
|
+
...partial,
|
|
34
|
+
key,
|
|
35
|
+
releaseRepo,
|
|
36
|
+
force: partial.force ?? false,
|
|
37
|
+
},
|
|
38
|
+
baseDir,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
process.stderr.write(`\nTarget: ${baseDir}\n`);
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
`Key: ${opts.key} Class: ${opts.className} Tap: ${opts.tap} Release: ${opts.releaseRepo}\n`,
|
|
44
|
+
);
|
|
45
|
+
process.stderr.write(`Homepage: ${opts.homepage}\n\n`);
|
|
46
|
+
const tree = renderCreateTree(opts);
|
|
47
|
+
process.stderr.write(`Files (${tree.size}):\n`);
|
|
48
|
+
for (const rel of [...tree.keys()].sort()) {
|
|
49
|
+
process.stderr.write(` ${rel}\n`);
|
|
50
|
+
}
|
|
51
|
+
process.stderr.write("\n");
|
|
52
|
+
printPostCreatePlan();
|
|
53
|
+
process.stderr.write("\n");
|
|
54
|
+
|
|
55
|
+
if (!promptConfirm("Proceed")) {
|
|
56
|
+
throw new Error("Aborted.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { opts, baseDir };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function runCreate(input: Partial<CreateOptions> & { dir?: string }): Promise<number> {
|
|
63
|
+
try {
|
|
64
|
+
const baseDir = resolve(process.cwd(), input.dir ?? ".");
|
|
65
|
+
const partial = { ...input };
|
|
66
|
+
delete (partial as { dir?: string }).dir;
|
|
67
|
+
|
|
68
|
+
if (partial.check || partial.diff) {
|
|
69
|
+
const drifts = diffCreateDetails(baseDir, partial);
|
|
70
|
+
if (drifts.length > 0) {
|
|
71
|
+
process.stderr.write(`Create drift in ${baseDir}:\n`);
|
|
72
|
+
for (const d of drifts) process.stderr.write(` ${d.rel}\n`);
|
|
73
|
+
if (partial.diff) printCreateDiffs(drifts, baseDir);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write(`Create OK: ${baseDir}\n`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let opts: CreateOptions;
|
|
81
|
+
if (!partial.yes && isInteractiveTty()) {
|
|
82
|
+
const collected = collectInteractiveOptions(partial, input.dir ?? ".");
|
|
83
|
+
opts = collected.opts;
|
|
84
|
+
return runCreateApply(collected.baseDir, opts, partial.dryRun ?? false);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!partial.yes && !isInteractiveTty()) {
|
|
88
|
+
throw new Error("Refusing to proceed without --yes (stdin is not a TTY).");
|
|
89
|
+
}
|
|
90
|
+
if (!partial.key) {
|
|
91
|
+
throw new Error("--key is required in non-interactive mode.");
|
|
92
|
+
}
|
|
93
|
+
if (!partial.releaseRepo) {
|
|
94
|
+
throw new Error("--release-repo is required in non-interactive mode.");
|
|
95
|
+
}
|
|
96
|
+
opts = resolveCreateOptions(partial, baseDir);
|
|
97
|
+
return runCreateApply(baseDir, opts, partial.dryRun ?? false);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function runCreateApply(
|
|
105
|
+
baseDir: string,
|
|
106
|
+
opts: CreateOptions,
|
|
107
|
+
dryRun: boolean,
|
|
108
|
+
): Promise<number> {
|
|
109
|
+
if (dryRun) {
|
|
110
|
+
const written = applyCreate(baseDir, { ...opts, dryRun: true, check: false });
|
|
111
|
+
process.stdout.write(`Would write ${written.length} file(s) under ${baseDir}\n`);
|
|
112
|
+
for (const w of written) process.stdout.write(` ${relative(baseDir, w) || w}\n`);
|
|
113
|
+
printPostCreatePlan();
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
mkdirSync(baseDir, { recursive: true });
|
|
118
|
+
const written = applyCreate(baseDir, {
|
|
119
|
+
...opts,
|
|
120
|
+
dryRun: false,
|
|
121
|
+
check: false,
|
|
122
|
+
force: opts.force,
|
|
123
|
+
});
|
|
124
|
+
process.stdout.write(`Created ${written.length} file(s) under ${baseDir}\n`);
|
|
125
|
+
for (const w of written) {
|
|
126
|
+
process.stdout.write(` ${relative(baseDir, w) || w}\n`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await runPostCreate(baseDir, false);
|
|
130
|
+
process.stdout.write("Done.\n");
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Parse argv and run create (for tests and direct script invocation). */
|
|
135
|
+
export async function runCreateCommand(rest: string[]): Promise<number> {
|
|
136
|
+
const { dir, opts } = parseCreateArgv(rest);
|
|
137
|
+
return runCreate({ ...opts, dir });
|
|
138
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { type AnyAppConfigSnapshot, createAppConfigSnapshot } from "./config/con
|
|
|
20
20
|
import { effectiveJsonSchema } from "./config/schema.ts";
|
|
21
21
|
import { CliContext } from "./context.ts";
|
|
22
22
|
import { cliHelpRender } from "./help.ts";
|
|
23
|
-
import { maybeBootstrapInstallArgv } from "./install/bootstrap.ts";
|
|
24
23
|
import { bootstrapMcpEnv } from "./mcp/env.ts";
|
|
25
24
|
import { mcpServeStdioLoop } from "./mcp/server.ts";
|
|
26
25
|
import { ParseKind, type ParseResult, parse, postParseValidate } from "./parse.ts";
|
|
@@ -95,7 +94,6 @@ export class Cli {
|
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
async run(argv: string[] = process.argv.slice(2)): Promise<never> {
|
|
98
|
-
argv = maybeBootstrapInstallArgv(argv, this.program);
|
|
99
97
|
assertBuiltinAllowed(argv, this.caps);
|
|
100
98
|
|
|
101
99
|
const prep = this.prepareDispatch(argv);
|
package/src/config/bootstrap.ts
CHANGED
|
@@ -4,6 +4,7 @@ settings are present before the CLI or MCP server handles a request.
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readSync } from "node:fs";
|
|
7
|
+
import { readPromptLine as readStdinLine } from "../prompt.ts";
|
|
7
8
|
import type { CliAppConfigEntry, CliProgram } from "../types.ts";
|
|
8
9
|
import {
|
|
9
10
|
configEntryRequired,
|
|
@@ -124,9 +125,7 @@ function readPromptLine(mask: boolean): string {
|
|
|
124
125
|
if (mask) {
|
|
125
126
|
return readSensitiveLine();
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
-
const n = readSync(0, buf, { length: 4096 });
|
|
129
|
-
return buf.toString("utf8", 0, n).replace(/\r?\n$/, "");
|
|
128
|
+
return readStdinLine();
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
/** Whether this setting already has a non-empty value in the user's shell environment. */
|
|
@@ -149,7 +148,7 @@ function promptConfigKey(
|
|
|
149
148
|
configure: boolean,
|
|
150
149
|
jsonSchemaRequired: Set<string> | undefined,
|
|
151
150
|
hostEnv: Record<string, string | undefined>,
|
|
152
|
-
): { value: unknown } {
|
|
151
|
+
): { value: unknown; userTyped: boolean } {
|
|
153
152
|
const baseTitle = entry.title ?? defaultConfigEntryTitle(key);
|
|
154
153
|
const titleWithEnv = entry.env ? `${baseTitle} (${entry.env})` : baseTitle;
|
|
155
154
|
const required = configEntryRequired(key, entry, jsonSchemaRequired);
|
|
@@ -169,18 +168,20 @@ function promptConfigKey(
|
|
|
169
168
|
}
|
|
170
169
|
const input = readPromptLine(sensitive);
|
|
171
170
|
if (input.length === 0 && hasCurrent) {
|
|
172
|
-
return { value: current };
|
|
171
|
+
return { value: current, userTyped: false };
|
|
173
172
|
}
|
|
174
173
|
if (input.length > 0) {
|
|
175
|
-
return { value: input };
|
|
174
|
+
return { value: input, userTyped: true };
|
|
176
175
|
}
|
|
177
|
-
return { value: undefined };
|
|
176
|
+
return { value: undefined, userTyped: false };
|
|
178
177
|
}
|
|
179
178
|
|
|
180
|
-
/** Options for the interactive
|
|
179
|
+
/** Options for the interactive configure wizard (app config target). */
|
|
181
180
|
export interface RunInstallConfigureOpts {
|
|
182
181
|
/** Where the wizard was started from (standalone command vs right after install). */
|
|
183
182
|
context?: "standalone" | "after-install";
|
|
183
|
+
/** When false, omit the "Configuration Setup" banner (e.g. inside interactive `configure`). */
|
|
184
|
+
showHeading?: boolean;
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
/** Print the "Configuration Setup" heading before the configure prompts. */
|
|
@@ -218,7 +219,7 @@ function promptMissingRequired(program: CliProgram): Record<string, unknown> {
|
|
|
218
219
|
writeConfigureSetupHeading();
|
|
219
220
|
headingWritten = true;
|
|
220
221
|
}
|
|
221
|
-
const { value } = promptConfigKey(key, entry, undefined, false, fromSchema, hostEnv);
|
|
222
|
+
const { value, userTyped } = promptConfigKey(key, entry, undefined, false, fromSchema, hostEnv);
|
|
222
223
|
if (value !== undefined && String(value).length > 0) {
|
|
223
224
|
updates[key] = value;
|
|
224
225
|
}
|
|
@@ -226,13 +227,13 @@ function promptMissingRequired(program: CliProgram): Record<string, unknown> {
|
|
|
226
227
|
return updates;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
/** Run the full interactive
|
|
230
|
-
export function
|
|
230
|
+
/** Run the full interactive app config wizard (`configure`). */
|
|
231
|
+
export function runConfigure(
|
|
231
232
|
program: CliProgram,
|
|
232
|
-
|
|
233
|
+
opts: RunInstallConfigureOpts = {},
|
|
233
234
|
): { path: string; changed: boolean } {
|
|
234
235
|
if (!program.appConfig) {
|
|
235
|
-
throw new Error("
|
|
236
|
+
throw new Error("configure requires program.appConfig on the program root.");
|
|
236
237
|
}
|
|
237
238
|
if (!process.stdin.isTTY) {
|
|
238
239
|
const { resolved } = bootstrapAppConfig(program, { validateFile: false });
|
|
@@ -240,7 +241,7 @@ export function runInstallConfigure(
|
|
|
240
241
|
if (missing.length > 0) {
|
|
241
242
|
process.stderr.write(`${formatMissingConfigMessage(program, missing)}\n`);
|
|
242
243
|
} else {
|
|
243
|
-
process.stderr.write("
|
|
244
|
+
process.stderr.write("configure requires an interactive terminal.\n");
|
|
244
245
|
}
|
|
245
246
|
process.exit(1);
|
|
246
247
|
}
|
|
@@ -254,15 +255,23 @@ export function runInstallConfigure(
|
|
|
254
255
|
const next: Record<string, unknown> = { ...existing };
|
|
255
256
|
let changed = false;
|
|
256
257
|
|
|
257
|
-
if (shouldShowConfigureSetupHeading(program)) {
|
|
258
|
+
if (opts.showHeading !== false && shouldShowConfigureSetupHeading(program)) {
|
|
258
259
|
writeConfigureSetupHeading();
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
for (const [key, entry] of Object.entries(program.appConfig.entries)) {
|
|
262
263
|
const before = next[key];
|
|
263
264
|
const current = resolved[key];
|
|
264
|
-
const { value } = promptConfigKey(key, entry, current, true, fromSchema, hostEnv);
|
|
265
|
+
const { value, userTyped } = promptConfigKey(key, entry, current, true, fromSchema, hostEnv);
|
|
265
266
|
if (value !== undefined && String(value).length > 0) {
|
|
267
|
+
const storedInFile =
|
|
268
|
+
key in existing &&
|
|
269
|
+
existing[key] !== undefined &&
|
|
270
|
+
existing[key] !== null &&
|
|
271
|
+
String(existing[key]).length > 0;
|
|
272
|
+
if (!userTyped && !storedInFile) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
266
275
|
if (JSON.stringify(value) !== JSON.stringify(before)) {
|
|
267
276
|
changed = true;
|
|
268
277
|
}
|
|
@@ -279,7 +288,7 @@ export function runInstallConfigure(
|
|
|
279
288
|
return { path, changed: false };
|
|
280
289
|
}
|
|
281
290
|
|
|
282
|
-
/** Summary for `
|
|
291
|
+
/** Summary for `configure --status`: config path, whether the file exists, and which required settings are set (never their values). */
|
|
283
292
|
export function appConfigStatus(program: CliProgram):
|
|
284
293
|
| {
|
|
285
294
|
path: string;
|
|
@@ -336,7 +345,7 @@ export function ensureAppConfig(
|
|
|
336
345
|
|
|
337
346
|
if (opts.interactive && process.stdin.isTTY) {
|
|
338
347
|
if (opts.configure) {
|
|
339
|
-
|
|
348
|
+
runConfigure(program, { context: "standalone" });
|
|
340
349
|
fileData = readAppConfigFileRaw(resolveAppConfigPath(program));
|
|
341
350
|
resolved = resolveAppConfig(program, fileData, hostEnv);
|
|
342
351
|
exportConfigToEnv(program, resolved, hostEnv);
|
package/src/config/file.test.ts
CHANGED
|
@@ -85,7 +85,7 @@ describe("config/file", () => {
|
|
|
85
85
|
expect(missing).toContain("apiToken");
|
|
86
86
|
expect(missing).not.toContain("port");
|
|
87
87
|
const msg = formatMissingConfigMessage(program, missing);
|
|
88
|
-
expect(msg).toContain("
|
|
88
|
+
expect(msg).toContain("configure");
|
|
89
89
|
} finally {
|
|
90
90
|
if (prev !== undefined) process.env.API_TOKEN = prev;
|
|
91
91
|
}
|
|
@@ -109,4 +109,171 @@ describe("config/resolve", () => {
|
|
|
109
109
|
else process.env.API_TOKEN = prev;
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
test("resolve callback supplies value when env and file are absent", () => {
|
|
114
|
+
const resolveProgram: CliProgram = {
|
|
115
|
+
...program,
|
|
116
|
+
appConfig: {
|
|
117
|
+
...program.appConfig!,
|
|
118
|
+
entries: {
|
|
119
|
+
...program.appConfig!.entries,
|
|
120
|
+
apiToken: {
|
|
121
|
+
description: "Token.",
|
|
122
|
+
env: "API_TOKEN",
|
|
123
|
+
resolve: () => "from-resolve",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
const prev = process.env.API_TOKEN;
|
|
129
|
+
delete process.env.API_TOKEN;
|
|
130
|
+
try {
|
|
131
|
+
const resolved = resolveAppConfig(resolveProgram, {});
|
|
132
|
+
expect(resolved.apiToken).toBe("from-resolve");
|
|
133
|
+
} finally {
|
|
134
|
+
if (prev !== undefined) process.env.API_TOKEN = prev;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("env overrides resolve callback", () => {
|
|
139
|
+
const resolveProgram: CliProgram = {
|
|
140
|
+
...program,
|
|
141
|
+
appConfig: {
|
|
142
|
+
...program.appConfig!,
|
|
143
|
+
entries: {
|
|
144
|
+
...program.appConfig!.entries,
|
|
145
|
+
apiToken: {
|
|
146
|
+
description: "Token.",
|
|
147
|
+
env: "API_TOKEN",
|
|
148
|
+
resolve: () => "from-resolve",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const prev = process.env.API_TOKEN;
|
|
154
|
+
process.env.API_TOKEN = "from-env";
|
|
155
|
+
try {
|
|
156
|
+
const resolved = resolveAppConfig(resolveProgram, {});
|
|
157
|
+
expect(resolved.apiToken).toBe("from-env");
|
|
158
|
+
} finally {
|
|
159
|
+
if (prev === undefined) delete process.env.API_TOKEN;
|
|
160
|
+
else process.env.API_TOKEN = prev;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("file overrides resolve callback", () => {
|
|
165
|
+
const resolveProgram: CliProgram = {
|
|
166
|
+
...program,
|
|
167
|
+
appConfig: {
|
|
168
|
+
...program.appConfig!,
|
|
169
|
+
entries: {
|
|
170
|
+
...program.appConfig!.entries,
|
|
171
|
+
apiToken: {
|
|
172
|
+
description: "Token.",
|
|
173
|
+
env: "API_TOKEN",
|
|
174
|
+
resolve: () => "from-resolve",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const prev = process.env.API_TOKEN;
|
|
180
|
+
delete process.env.API_TOKEN;
|
|
181
|
+
try {
|
|
182
|
+
const resolved = resolveAppConfig(resolveProgram, { apiToken: "from-file" });
|
|
183
|
+
expect(resolved.apiToken).toBe("from-file");
|
|
184
|
+
} finally {
|
|
185
|
+
if (prev !== undefined) process.env.API_TOKEN = prev;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("falls back to env when resolve returns undefined", () => {
|
|
190
|
+
const resolveProgram: CliProgram = {
|
|
191
|
+
...program,
|
|
192
|
+
appConfig: {
|
|
193
|
+
...program.appConfig!,
|
|
194
|
+
entries: {
|
|
195
|
+
...program.appConfig!.entries,
|
|
196
|
+
apiToken: {
|
|
197
|
+
description: "Token.",
|
|
198
|
+
env: "API_TOKEN",
|
|
199
|
+
resolve: () => undefined,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const hostEnv = { API_TOKEN: "from-env-fallback" };
|
|
205
|
+
const prev = process.env.API_TOKEN;
|
|
206
|
+
delete process.env.API_TOKEN;
|
|
207
|
+
try {
|
|
208
|
+
const resolved = resolveAppConfig(resolveProgram, {}, hostEnv);
|
|
209
|
+
expect(resolved.apiToken).toBe("from-env-fallback");
|
|
210
|
+
} finally {
|
|
211
|
+
if (prev !== undefined) process.env.API_TOKEN = prev;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("resolve callback is skipped when env is set", () => {
|
|
216
|
+
let resolveCalled = false;
|
|
217
|
+
const resolveProgram: CliProgram = {
|
|
218
|
+
...program,
|
|
219
|
+
appConfig: {
|
|
220
|
+
...program.appConfig!,
|
|
221
|
+
entries: {
|
|
222
|
+
...program.appConfig!.entries,
|
|
223
|
+
apiToken: {
|
|
224
|
+
description: "Token.",
|
|
225
|
+
env: "API_TOKEN",
|
|
226
|
+
resolve: () => {
|
|
227
|
+
resolveCalled = true;
|
|
228
|
+
return "from-resolve";
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
const prev = process.env.API_TOKEN;
|
|
235
|
+
process.env.API_TOKEN = "from-env";
|
|
236
|
+
try {
|
|
237
|
+
const resolved = resolveAppConfig(resolveProgram, {});
|
|
238
|
+
expect(resolved.apiToken).toBe("from-env");
|
|
239
|
+
expect(resolveCalled).toBe(false);
|
|
240
|
+
} finally {
|
|
241
|
+
if (prev === undefined) delete process.env.API_TOKEN;
|
|
242
|
+
else process.env.API_TOKEN = prev;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("async resolve is ignored with stderr warning", () => {
|
|
247
|
+
const resolveProgram: CliProgram = {
|
|
248
|
+
...program,
|
|
249
|
+
appConfig: {
|
|
250
|
+
...program.appConfig!,
|
|
251
|
+
entries: {
|
|
252
|
+
...program.appConfig!.entries,
|
|
253
|
+
apiToken: {
|
|
254
|
+
description: "Token.",
|
|
255
|
+
env: "API_TOKEN",
|
|
256
|
+
resolve: async () => "from-async-resolve",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
const prev = process.env.API_TOKEN;
|
|
262
|
+
delete process.env.API_TOKEN;
|
|
263
|
+
const prevStderr = process.stderr.write;
|
|
264
|
+
const stderrLines: string[] = [];
|
|
265
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
266
|
+
stderrLines.push(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
|
|
267
|
+
return true;
|
|
268
|
+
}) as typeof process.stderr.write;
|
|
269
|
+
try {
|
|
270
|
+
const resolved = resolveAppConfig(resolveProgram, {}, { API_TOKEN: undefined });
|
|
271
|
+
expect(resolved.apiToken).toBeUndefined();
|
|
272
|
+
expect(stderrLines.join("")).toContain("returned a Promise");
|
|
273
|
+
} finally {
|
|
274
|
+
process.stderr.write = prevStderr;
|
|
275
|
+
if (prev === undefined) delete process.env.API_TOKEN;
|
|
276
|
+
else process.env.API_TOKEN = prev;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
112
279
|
});
|
package/src/config/resolve.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Resolve program.appConfig values: defaults, file, env override, export to process.env.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { CliAppConfigEntry, CliProgram } from "../types.ts";
|
|
5
|
+
import type { CliAppConfigEntry, CliAppConfigResolveContext, CliProgram } from "../types.ts";
|
|
6
6
|
import { configEntryRequired, jsonSchemaRequiredKeys } from "./entry.ts";
|
|
7
7
|
import type { AppConfigFileData } from "./file.ts";
|
|
8
8
|
import { displayAppConfigPath } from "./file.ts";
|
|
@@ -93,6 +93,38 @@ export function resolveAppConfig(
|
|
|
93
93
|
return out;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function tryEnvOverride(
|
|
97
|
+
program: CliProgram,
|
|
98
|
+
key: string,
|
|
99
|
+
entry: CliAppConfigEntry,
|
|
100
|
+
hostEnv?: Record<string, string | undefined>,
|
|
101
|
+
): unknown {
|
|
102
|
+
if (!entry.env) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const fromEnv = envOverrideValue(entry.env, hostEnv);
|
|
106
|
+
if (fromEnv === undefined) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return coerceEnvValue(program, key, fromEnv);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildResolveContext(
|
|
113
|
+
program: CliProgram,
|
|
114
|
+
key: string,
|
|
115
|
+
entry: CliAppConfigEntry,
|
|
116
|
+
fileData: AppConfigFileData,
|
|
117
|
+
hostEnv?: Record<string, string | undefined>,
|
|
118
|
+
): CliAppConfigResolveContext {
|
|
119
|
+
return {
|
|
120
|
+
key,
|
|
121
|
+
entry,
|
|
122
|
+
program,
|
|
123
|
+
fileValue: fileData[key],
|
|
124
|
+
envValue: entry.env ? envOverrideValue(entry.env, hostEnv) : undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
96
128
|
function resolveConfigKey(
|
|
97
129
|
program: CliProgram,
|
|
98
130
|
key: string,
|
|
@@ -100,15 +132,27 @@ function resolveConfigKey(
|
|
|
100
132
|
fileData: AppConfigFileData,
|
|
101
133
|
hostEnv?: Record<string, string | undefined>,
|
|
102
134
|
): unknown {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return coerceEnvValue(program, key, fromEnv);
|
|
107
|
-
}
|
|
135
|
+
const fromEnv = tryEnvOverride(program, key, entry, hostEnv);
|
|
136
|
+
if (fromEnv !== undefined) {
|
|
137
|
+
return fromEnv;
|
|
108
138
|
}
|
|
109
139
|
if (key in fileData && isPresent(fileData[key])) {
|
|
110
140
|
return fileData[key];
|
|
111
141
|
}
|
|
142
|
+
if (entry.resolve) {
|
|
143
|
+
const fromResolve = entry.resolve(buildResolveContext(program, key, entry, fileData, hostEnv));
|
|
144
|
+
if (fromResolve != null && typeof (fromResolve as Promise<unknown>).then === "function") {
|
|
145
|
+
process.stderr.write(
|
|
146
|
+
`[argsbarg] config "${key}": resolve() returned a Promise; use a synchronous resolver (e.g. Bun.spawnSync with piped stdout).\n`,
|
|
147
|
+
);
|
|
148
|
+
} else if (isPresent(fromResolve)) {
|
|
149
|
+
return fromResolve;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const fromEnvFallback = tryEnvOverride(program, key, entry, hostEnv);
|
|
153
|
+
if (fromEnvFallback !== undefined) {
|
|
154
|
+
return fromEnvFallback;
|
|
155
|
+
}
|
|
112
156
|
const def = schemaDefaultForKey(program, key);
|
|
113
157
|
if (def !== undefined) {
|
|
114
158
|
return def;
|
|
@@ -182,7 +226,7 @@ export function formatMissingConfigMessage(program: CliProgram, keys: string[]):
|
|
|
182
226
|
const path = displayAppConfigPath(program);
|
|
183
227
|
return [
|
|
184
228
|
`Missing required configuration: ${list}`,
|
|
185
|
-
`Configure interactively: ${program.key}
|
|
229
|
+
`Configure interactively: ${program.key} configure`,
|
|
186
230
|
`Or set via: ${program.key} config set <key> <value>`,
|
|
187
231
|
`Config file: ${path}`,
|
|
188
232
|
`See: ${program.key} docs mcp`,
|
|
@@ -195,7 +239,7 @@ export function formatMcpMissingConfigMessage(program: CliProgram, keys: string[
|
|
|
195
239
|
const path = displayAppConfigPath(program);
|
|
196
240
|
return [
|
|
197
241
|
`Missing required configuration: ${list}`,
|
|
198
|
-
`Configure: ${program.key}
|
|
242
|
+
`Configure: ${program.key} configure`,
|
|
199
243
|
`Or set via: ${program.key} config set`,
|
|
200
244
|
`Config file: ${path}`,
|
|
201
245
|
].join("\n");
|