argsbarg 5.1.8 → 5.1.10
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 +29 -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 +17 -3
- package/examples/full-example/justfile +6 -5
- package/examples/full-example/scripts/formula-shared.test.ts +68 -0
- package/examples/full-example/scripts/formula-shared.ts +39 -1
- package/examples/full-example/scripts/release.ts +212 -0
- package/index.d.ts +10 -1
- package/package.json +2 -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/config/file.test.ts
CHANGED
|
@@ -7,13 +7,17 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:
|
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import type { CliProgram } from "../types.ts";
|
|
10
|
+
import { CONFIG_BINDINGS_KEY } from "./bindings.ts";
|
|
10
11
|
import { bootstrapAppConfig } from "./bootstrap.ts";
|
|
11
12
|
import {
|
|
13
|
+
appConfigFileExists,
|
|
14
|
+
ensureAppConfigFile,
|
|
12
15
|
readAppConfigFile,
|
|
13
16
|
resolveAppConfigDir,
|
|
14
17
|
resolveAppConfigPath,
|
|
15
18
|
uninstallAppConfig,
|
|
16
19
|
writeAppConfigFile,
|
|
20
|
+
writeAppConfigFileRaw,
|
|
17
21
|
} from "./file.ts";
|
|
18
22
|
import { buildProgramUserConfig } from "./manifest.ts";
|
|
19
23
|
import { formatMissingConfigMessage, missingRequiredConfig, resolveAppConfig } from "./resolve.ts";
|
|
@@ -58,9 +62,7 @@ describe("config/file", () => {
|
|
|
58
62
|
|
|
59
63
|
test("resolveAppConfigPath uses config.json", () => {
|
|
60
64
|
withHome((home) => {
|
|
61
|
-
expect(resolveAppConfigPath(program)).toBe(
|
|
62
|
-
join(home, ".local", "lib", "myapp", "config.json"),
|
|
63
|
-
);
|
|
65
|
+
expect(resolveAppConfigPath(program)).toBe(join(home, ".local", "lib", "myapp", "config.json"));
|
|
64
66
|
});
|
|
65
67
|
});
|
|
66
68
|
|
|
@@ -126,4 +128,45 @@ describe("config/file", () => {
|
|
|
126
128
|
expect(existsSync(configDir)).toBe(false);
|
|
127
129
|
});
|
|
128
130
|
});
|
|
131
|
+
|
|
132
|
+
test("ensureAppConfigFile creates empty config", () => {
|
|
133
|
+
withHome(() => {
|
|
134
|
+
const created = ensureAppConfigFile(program);
|
|
135
|
+
expect(created).toBe(resolveAppConfigPath(program));
|
|
136
|
+
expect(appConfigFileExists(program)).toBe(true);
|
|
137
|
+
expect(readAppConfigFile(program)).toEqual({});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("ensureAppConfigFile no-op when file exists", () => {
|
|
142
|
+
withHome(() => {
|
|
143
|
+
writeAppConfigFileRaw(program, { apiToken: "x" });
|
|
144
|
+
expect(ensureAppConfigFile(program)).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("read allows _bindings framework key", () => {
|
|
149
|
+
withHome(() => {
|
|
150
|
+
writeAppConfigFileRaw(program, {
|
|
151
|
+
[CONFIG_BINDINGS_KEY]: { apiToken: "env" },
|
|
152
|
+
});
|
|
153
|
+
expect(readAppConfigFile(program)).toEqual({
|
|
154
|
+
[CONFIG_BINDINGS_KEY]: { apiToken: "env" },
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("partial write allows bindings without required keys", () => {
|
|
160
|
+
withHome(() => {
|
|
161
|
+
writeAppConfigFile(program, { [CONFIG_BINDINGS_KEY]: { apiToken: "env" } }, { partial: true });
|
|
162
|
+
expect(readAppConfigFile(program)[CONFIG_BINDINGS_KEY]).toEqual({ apiToken: "env" });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("appConfigFileExists false when only directory exists", () => {
|
|
167
|
+
withHome(() => {
|
|
168
|
+
mkdirSync(resolveAppConfigDir(program), { recursive: true });
|
|
169
|
+
expect(appConfigFileExists(program)).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
129
172
|
});
|
package/src/config/file.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { dirname, join } from "node:path";
|
|
|
7
7
|
import { sanitizeToolSegment } from "../mcp/tools.ts";
|
|
8
8
|
import { appConfigLibHome, displayHomePath } from "../paths/host.ts";
|
|
9
9
|
import type { CliProgram } from "../types.ts";
|
|
10
|
+
import { isFrameworkConfigKey, validateBindingsShape } from "./bindings.ts";
|
|
10
11
|
import { effectiveJsonSchema } from "./schema.ts";
|
|
11
|
-
import { validateConfigDocument } from "./validate.ts";
|
|
12
|
+
import { validateConfigDocument, validateConfigDocumentPartial } from "./validate.ts";
|
|
12
13
|
|
|
13
14
|
export type AppConfigFileData = Record<string, unknown>;
|
|
14
15
|
|
|
@@ -56,15 +57,13 @@ export function readAppConfigFileRaw(path: string): AppConfigFileData {
|
|
|
56
57
|
return parseConfigJson(text, path);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
validateAppConfigData(program, data, path);
|
|
67
|
-
return data;
|
|
60
|
+
function isEmptyConfigDocument(data: AppConfigFileData): boolean {
|
|
61
|
+
return Object.keys(data).every((k) => isFrameworkConfigKey(k));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** True when `config.json` exists on disk. */
|
|
65
|
+
export function appConfigFileExists(program: CliProgram): boolean {
|
|
66
|
+
return existsSync(resolveAppConfigPath(program));
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
/** Validate in-memory config data; throws on failure. */
|
|
@@ -72,15 +71,21 @@ export function validateAppConfigData(
|
|
|
72
71
|
program: CliProgram,
|
|
73
72
|
data: AppConfigFileData,
|
|
74
73
|
pathLabel?: string,
|
|
74
|
+
opts: { partial?: boolean } = {},
|
|
75
75
|
): void {
|
|
76
76
|
const appConfig = program.appConfig;
|
|
77
77
|
if (!appConfig) {
|
|
78
78
|
throw new Error("program.appConfig is not set");
|
|
79
79
|
}
|
|
80
|
+
const where = pathLabel ? displayHomePath(pathLabel) : "config";
|
|
81
|
+
const bindingErrors = validateBindingsShape(data);
|
|
82
|
+
if (bindingErrors.length > 0) {
|
|
83
|
+
throw new Error(`Invalid config in ${where}: ${bindingErrors.join("; ")}`);
|
|
84
|
+
}
|
|
80
85
|
const allowed = new Set(Object.keys(appConfig.entries));
|
|
81
86
|
for (const key of Object.keys(data)) {
|
|
87
|
+
if (isFrameworkConfigKey(key)) continue;
|
|
82
88
|
if (!allowed.has(key)) {
|
|
83
|
-
const where = pathLabel ? displayHomePath(pathLabel) : "config";
|
|
84
89
|
throw new Error(`Unknown config key '${key}' in ${where}`);
|
|
85
90
|
}
|
|
86
91
|
}
|
|
@@ -88,26 +93,61 @@ export function validateAppConfigData(
|
|
|
88
93
|
if (!jsonSchema) {
|
|
89
94
|
return;
|
|
90
95
|
}
|
|
96
|
+
if (opts.partial || isEmptyConfigDocument(data)) {
|
|
97
|
+
const result = validateConfigDocumentPartial(data, jsonSchema);
|
|
98
|
+
if (!result.valid) {
|
|
99
|
+
throw new Error(`Invalid config in ${where}: ${result.errors.join("; ")}`);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
91
103
|
const result = validateConfigDocument(data, jsonSchema);
|
|
92
104
|
if (!result.valid) {
|
|
93
|
-
const where = pathLabel ? displayHomePath(pathLabel) : "config";
|
|
94
105
|
throw new Error(`Invalid config in ${where}: ${result.errors.join("; ")}`);
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
108
|
|
|
98
|
-
/**
|
|
99
|
-
export function
|
|
100
|
-
validateAppConfigData(program, data, displayAppConfigPath(program));
|
|
109
|
+
/** Write JSON to the config path without schema validation (`0o600`). */
|
|
110
|
+
export function writeAppConfigFileRaw(program: CliProgram, data: AppConfigFileData, dry = false): void {
|
|
101
111
|
const path = resolveAppConfigPath(program);
|
|
112
|
+
if (dry) return;
|
|
102
113
|
mkdirSync(dirname(path), { recursive: true });
|
|
103
114
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
/**
|
|
107
|
-
export function
|
|
117
|
+
/** Create `config.json` as `{}` when missing; returns absolute path if created. */
|
|
118
|
+
export function ensureAppConfigFile(program: CliProgram, dry = false): string | null {
|
|
119
|
+
const path = resolveAppConfigPath(program);
|
|
120
|
+
if (existsSync(path)) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
writeAppConfigFileRaw(program, {}, dry);
|
|
124
|
+
return path;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Read and validate config file against program schema. */
|
|
128
|
+
export function readAppConfigFile(program: CliProgram): AppConfigFileData {
|
|
108
129
|
const path = resolveAppConfigPath(program);
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
const data = readAppConfigFileRaw(path);
|
|
131
|
+
if (isEmptyConfigDocument(data)) {
|
|
132
|
+
return data;
|
|
133
|
+
}
|
|
134
|
+
validateAppConfigData(program, data, path);
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Merge-write schema keys to the config file (`0o600`). */
|
|
139
|
+
export function writeAppConfigFile(
|
|
140
|
+
program: CliProgram,
|
|
141
|
+
data: AppConfigFileData,
|
|
142
|
+
opts: { partial?: boolean } = {},
|
|
143
|
+
): void {
|
|
144
|
+
validateAppConfigData(program, data, displayAppConfigPath(program), opts);
|
|
145
|
+
writeAppConfigFileRaw(program, data);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** True when the app config file is present. */
|
|
149
|
+
export function appConfigInstalled(program: CliProgram): boolean {
|
|
150
|
+
return appConfigFileExists(program);
|
|
111
151
|
}
|
|
112
152
|
|
|
113
153
|
/** Removes the app config file and config directory when present. */
|
package/src/config/resolve.ts
CHANGED
|
@@ -35,10 +35,7 @@ export function captureMappedHostEnv(program: CliProgram): Record<string, string
|
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function envOverrideValue(
|
|
39
|
-
envName: string,
|
|
40
|
-
hostEnv?: Record<string, string | undefined>,
|
|
41
|
-
): string | undefined {
|
|
38
|
+
function envOverrideValue(envName: string, hostEnv?: Record<string, string | undefined>): string | undefined {
|
|
42
39
|
const val = hostEnv && envName in hostEnv ? hostEnv[envName] : process.env[envName];
|
|
43
40
|
if (val === undefined || val.length === 0) {
|
|
44
41
|
return undefined;
|
package/src/config/schema.ts
CHANGED
|
@@ -6,9 +6,7 @@ import type { CliAppConfigEntry, CliProgram } from "../types.ts";
|
|
|
6
6
|
import { configEntryRequired, jsonSchemaRequiredKeys } from "./entry.ts";
|
|
7
7
|
|
|
8
8
|
/** Synthesize a draft-07 object schema with string properties from metadata entries. */
|
|
9
|
-
export function synthesizeAllStringSchema(
|
|
10
|
-
schema: Record<string, CliAppConfigEntry>,
|
|
11
|
-
): Record<string, unknown> {
|
|
9
|
+
export function synthesizeAllStringSchema(schema: Record<string, CliAppConfigEntry>): Record<string, unknown> {
|
|
12
10
|
const properties: Record<string, unknown> = {};
|
|
13
11
|
const required: string[] = [];
|
|
14
12
|
for (const [key, entry] of Object.entries(schema)) {
|
|
@@ -24,10 +24,7 @@ const rootSchema = {
|
|
|
24
24
|
/** Tests for config/validate. */
|
|
25
25
|
describe("config/validate", () => {
|
|
26
26
|
test("accepts valid document", () => {
|
|
27
|
-
const result = validateConfigDocument(
|
|
28
|
-
{ apiToken: "x", maxRetries: 3, prefs: { ttl: 3600 } },
|
|
29
|
-
rootSchema,
|
|
30
|
-
);
|
|
27
|
+
const result = validateConfigDocument({ apiToken: "x", maxRetries: 3, prefs: { ttl: 3600 } }, rootSchema);
|
|
31
28
|
expect(result.valid).toBe(true);
|
|
32
29
|
expect(result.errors).toEqual([]);
|
|
33
30
|
});
|
|
@@ -39,10 +36,7 @@ describe("config/validate", () => {
|
|
|
39
36
|
});
|
|
40
37
|
|
|
41
38
|
test("rejects unknown property when additionalProperties is false", () => {
|
|
42
|
-
const result = validateConfigDocument(
|
|
43
|
-
{ apiToken: "x", maxRetries: 1, extra: true },
|
|
44
|
-
rootSchema,
|
|
45
|
-
);
|
|
39
|
+
const result = validateConfigDocument({ apiToken: "x", maxRetries: 1, extra: true }, rootSchema);
|
|
46
40
|
expect(result.valid).toBe(false);
|
|
47
41
|
expect(result.errors.some((e) => e.includes("extra"))).toBe(true);
|
|
48
42
|
});
|
|
@@ -58,9 +52,7 @@ describe("config/validate", () => {
|
|
|
58
52
|
});
|
|
59
53
|
|
|
60
54
|
test("parseConfigSetValue requires --json for objects", () => {
|
|
61
|
-
expect(() => parseConfigSetValue("ttl:1", { type: "object" }, rootSchema, false)).toThrow(
|
|
62
|
-
/--json/,
|
|
63
|
-
);
|
|
55
|
+
expect(() => parseConfigSetValue("ttl:1", { type: "object" }, rootSchema, false)).toThrow(/--json/);
|
|
64
56
|
expect(parseConfigSetValue('{"ttl":1}', { type: "object" }, rootSchema, true)).toEqual({
|
|
65
57
|
ttl: 1,
|
|
66
58
|
});
|
package/src/config/validate.ts
CHANGED
|
@@ -4,6 +4,7 @@ Aligned with common ts-json-schema-generator output (local $ref, objects, scalar
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { parseCommaList, parseDate, parseDateTime } from "../formats.ts";
|
|
7
|
+
import { isFrameworkConfigKey } from "./bindings.ts";
|
|
7
8
|
|
|
8
9
|
type JsonSchema = Record<string, unknown>;
|
|
9
10
|
|
|
@@ -21,7 +22,14 @@ export interface ValidateResult {
|
|
|
21
22
|
/** Validate `data` against a JSON Schema root. Returns human-readable error messages. */
|
|
22
23
|
export function validateConfigDocument(data: unknown, rootSchema: JsonSchema): ValidateResult {
|
|
23
24
|
const errors: string[] = [];
|
|
24
|
-
validateValue(data, rootSchema, rootSchema, "$", errors);
|
|
25
|
+
validateValue(data, rootSchema, rootSchema, "$", errors, false);
|
|
26
|
+
return { valid: errors.length === 0, errors };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Validate present keys only — skips root `required` checks (partial writes / bootstrap). */
|
|
30
|
+
export function validateConfigDocumentPartial(data: unknown, rootSchema: JsonSchema): ValidateResult {
|
|
31
|
+
const errors: string[] = [];
|
|
32
|
+
validateValue(data, rootSchema, rootSchema, "$", errors, true);
|
|
25
33
|
return { valid: errors.length === 0, errors };
|
|
26
34
|
}
|
|
27
35
|
|
|
@@ -31,6 +39,7 @@ function validateValue(
|
|
|
31
39
|
root: JsonSchema,
|
|
32
40
|
path: string,
|
|
33
41
|
errors: string[],
|
|
42
|
+
partial: boolean,
|
|
34
43
|
): void {
|
|
35
44
|
const resolved = resolveSchema(schema, root);
|
|
36
45
|
if (!resolved) {
|
|
@@ -59,7 +68,7 @@ function validateValue(
|
|
|
59
68
|
continue;
|
|
60
69
|
}
|
|
61
70
|
const branchErrs: string[] = [];
|
|
62
|
-
validateValue(value, branch as JsonSchema, root, path, branchErrs);
|
|
71
|
+
validateValue(value, branch as JsonSchema, root, path, branchErrs, partial);
|
|
63
72
|
if (branchErrs.length === 0) {
|
|
64
73
|
return;
|
|
65
74
|
}
|
|
@@ -93,10 +102,10 @@ function validateValue(
|
|
|
93
102
|
errors.push(`${path}: must be null`);
|
|
94
103
|
}
|
|
95
104
|
if (types.includes("object") || (types.length === 0 && isPlainObject(value))) {
|
|
96
|
-
validateObject(value, resolved, root, path, errors);
|
|
105
|
+
validateObject(value, resolved, root, path, errors, partial);
|
|
97
106
|
}
|
|
98
107
|
if (types.includes("array") || (types.length === 0 && Array.isArray(value))) {
|
|
99
|
-
validateArray(value, resolved, root, path, errors);
|
|
108
|
+
validateArray(value, resolved, root, path, errors, partial);
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
|
|
@@ -165,13 +174,7 @@ function validateString(value: unknown, schema: JsonSchema, path: string, errors
|
|
|
165
174
|
}
|
|
166
175
|
}
|
|
167
176
|
|
|
168
|
-
function validateNumber(
|
|
169
|
-
value: unknown,
|
|
170
|
-
schema: JsonSchema,
|
|
171
|
-
path: string,
|
|
172
|
-
errors: string[],
|
|
173
|
-
integer: boolean,
|
|
174
|
-
): void {
|
|
177
|
+
function validateNumber(value: unknown, schema: JsonSchema, path: string, errors: string[], integer: boolean): void {
|
|
175
178
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
176
179
|
errors.push(`${path}: must be number`);
|
|
177
180
|
return;
|
|
@@ -193,6 +196,7 @@ function validateObject(
|
|
|
193
196
|
root: JsonSchema,
|
|
194
197
|
path: string,
|
|
195
198
|
errors: string[],
|
|
199
|
+
partial: boolean,
|
|
196
200
|
): void {
|
|
197
201
|
if (!isPlainObject(value)) {
|
|
198
202
|
errors.push(`${path}: must be object`);
|
|
@@ -207,22 +211,25 @@ function validateObject(
|
|
|
207
211
|
if (propMap) {
|
|
208
212
|
for (const [key, propSchema] of Object.entries(propMap)) {
|
|
209
213
|
if (key in value) {
|
|
210
|
-
validateValue(value[key], propSchema, root, `${path}.${key}`, errors);
|
|
214
|
+
validateValue(value[key], propSchema, root, `${path}.${key}`, errors, partial);
|
|
211
215
|
}
|
|
212
216
|
}
|
|
213
217
|
}
|
|
214
218
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
if (!partial) {
|
|
220
|
+
const required = schema.required;
|
|
221
|
+
if (Array.isArray(required)) {
|
|
222
|
+
for (const key of required) {
|
|
223
|
+
if (typeof key === "string" && !(key in value)) {
|
|
224
|
+
errors.push(`${path}: missing required property '${key}'`);
|
|
225
|
+
}
|
|
220
226
|
}
|
|
221
227
|
}
|
|
222
228
|
}
|
|
223
229
|
|
|
224
230
|
if (schema.additionalProperties === false && propMap) {
|
|
225
231
|
for (const key of Object.keys(value)) {
|
|
232
|
+
if (isFrameworkConfigKey(key)) continue;
|
|
226
233
|
if (!(key in propMap)) {
|
|
227
234
|
errors.push(`${path}: unknown property '${key}'`);
|
|
228
235
|
}
|
|
@@ -236,6 +243,7 @@ function validateArray(
|
|
|
236
243
|
root: JsonSchema,
|
|
237
244
|
path: string,
|
|
238
245
|
errors: string[],
|
|
246
|
+
partial: boolean,
|
|
239
247
|
): void {
|
|
240
248
|
if (!Array.isArray(value)) {
|
|
241
249
|
errors.push(`${path}: must be array`);
|
|
@@ -250,12 +258,12 @@ function validateArray(
|
|
|
250
258
|
const items = schema.items;
|
|
251
259
|
if (typeof items === "object" && items !== null && !Array.isArray(items)) {
|
|
252
260
|
for (let i = 0; i < value.length; i++) {
|
|
253
|
-
validateValue(value[i], items as JsonSchema, root, `${path}[${i}]`, errors);
|
|
261
|
+
validateValue(value[i], items as JsonSchema, root, `${path}[${i}]`, errors, partial);
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
|
|
258
|
-
function validateParsedConfigValue(
|
|
266
|
+
export function validateParsedConfigValue(
|
|
259
267
|
parsed: unknown,
|
|
260
268
|
propertySchema: JsonSchema | undefined,
|
|
261
269
|
rootSchema: JsonSchema,
|
|
@@ -264,18 +272,14 @@ function validateParsedConfigValue(
|
|
|
264
272
|
return parsed;
|
|
265
273
|
}
|
|
266
274
|
const errors: string[] = [];
|
|
267
|
-
validateValue(parsed, propertySchema, rootSchema, "$", errors);
|
|
275
|
+
validateValue(parsed, propertySchema, rootSchema, "$", errors, false);
|
|
268
276
|
if (errors.length > 0) {
|
|
269
277
|
throw new Error(errors[0] ?? "Invalid config value");
|
|
270
278
|
}
|
|
271
279
|
return parsed;
|
|
272
280
|
}
|
|
273
281
|
|
|
274
|
-
function parseJsonLiteral(
|
|
275
|
-
raw: string,
|
|
276
|
-
propertySchema: JsonSchema | undefined,
|
|
277
|
-
rootSchema: JsonSchema,
|
|
278
|
-
): unknown {
|
|
282
|
+
function parseJsonLiteral(raw: string, propertySchema: JsonSchema | undefined, rootSchema: JsonSchema): unknown {
|
|
279
283
|
let parsed: unknown;
|
|
280
284
|
try {
|
|
281
285
|
parsed = JSON.parse(raw) as unknown;
|
|
@@ -346,11 +350,7 @@ function parsePrimitiveArraySegment(segment: string, items: PrimitiveArrayItems)
|
|
|
346
350
|
}
|
|
347
351
|
}
|
|
348
352
|
|
|
349
|
-
function parseHomogeneousPrimitiveArray(
|
|
350
|
-
raw: string,
|
|
351
|
-
arraySchema: JsonSchema,
|
|
352
|
-
rootSchema: JsonSchema,
|
|
353
|
-
): unknown[] {
|
|
353
|
+
function parseHomogeneousPrimitiveArray(raw: string, arraySchema: JsonSchema, rootSchema: JsonSchema): unknown[] {
|
|
354
354
|
const items = homogeneousPrimitiveArrayItems(arraySchema, rootSchema);
|
|
355
355
|
if (!items) {
|
|
356
356
|
throw new Error("Use --json for object or array config values");
|
|
@@ -85,11 +85,7 @@ test("MCP config file loads and exports vars for tool handlers", async () => {
|
|
|
85
85
|
const dir = mkdtempSync(join(tmpdir(), "argsbarg-mcp-"));
|
|
86
86
|
const configFile = join(dir, ".local", "lib", "mcp_test", "config");
|
|
87
87
|
mkdirSync(dirname(configFile), { recursive: true });
|
|
88
|
-
writeFileSync(
|
|
89
|
-
configFile,
|
|
90
|
-
`${JSON.stringify({ argsTestSecret: "file-value" }, null, 2)}\n`,
|
|
91
|
-
"utf8",
|
|
92
|
-
);
|
|
88
|
+
writeFileSync(configFile, `${JSON.stringify({ argsTestSecret: "file-value" }, null, 2)}\n`, "utf8");
|
|
93
89
|
const responses = await mcpRequest(
|
|
94
90
|
[
|
|
95
91
|
{
|
|
@@ -6,11 +6,9 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
6
6
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
validateConfigureOpts,
|
|
13
|
-
} from "../configure/index.ts";
|
|
9
|
+
import { appConfigFileExists } from "../config/file.ts";
|
|
10
|
+
import { appConfigHasEntries, parseConfigureOpts, validateConfigureOpts } from "../configure/index.ts";
|
|
11
|
+
import { Cli } from "../index.ts";
|
|
14
12
|
import { detectInstalledArtifacts } from "../install/detect-installed.ts";
|
|
15
13
|
import { parseInstallOpts } from "../install/opts.ts";
|
|
16
14
|
import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "../install/paths.ts";
|
|
@@ -157,3 +155,19 @@ describe("app config wizard", () => {
|
|
|
157
155
|
expect(appConfigHasEntries(fixture)).toBe(false);
|
|
158
156
|
});
|
|
159
157
|
});
|
|
158
|
+
|
|
159
|
+
describe("configure --sync bootstrap", () => {
|
|
160
|
+
test("creates config.json for apps without appConfig", async () => {
|
|
161
|
+
const program: CliProgram = {
|
|
162
|
+
key: "syncboot",
|
|
163
|
+
version: "0.0.0",
|
|
164
|
+
description: "Sync bootstrap test.",
|
|
165
|
+
handler: () => {},
|
|
166
|
+
configure: { enabled: true },
|
|
167
|
+
};
|
|
168
|
+
expect(appConfigFileExists(program)).toBe(false);
|
|
169
|
+
const result = await new Cli(program).invoke(["configure", "--sync", "--yes"]);
|
|
170
|
+
expect(result.exitCode).toBe(0);
|
|
171
|
+
expect(appConfigFileExists(program)).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
});
|
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 {
|