argsbarg 6.1.6 → 6.1.8

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.
Files changed (139) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/docs/README.md +1 -1
  3. package/docs/cli-program.md +3 -3
  4. package/docs/config-schema.md +12 -9
  5. package/docs/decisions.md +1 -1
  6. package/docs/json-schema-subset.md +38 -12
  7. package/docs/output-schema.md +1 -1
  8. package/package.json +3 -2
  9. package/src/builtins/builtins.test.ts +4 -4
  10. package/src/builtins/completion-bash.ts +1 -1
  11. package/src/builtins/completion-fish.ts +1 -1
  12. package/src/builtins/completion-group.ts +3 -3
  13. package/src/builtins/completion-zsh.ts +1 -1
  14. package/src/builtins/config.test.ts +3 -3
  15. package/src/builtins/config.ts +9 -9
  16. package/src/builtins/configure-copy.ts +2 -2
  17. package/src/builtins/configure.ts +3 -3
  18. package/src/builtins/dispatch.ts +10 -10
  19. package/src/builtins/export.ts +4 -4
  20. package/src/builtins/http.ts +5 -5
  21. package/src/builtins/mcp.ts +3 -3
  22. package/src/builtins/presentation.ts +5 -5
  23. package/src/builtins/registry.ts +3 -3
  24. package/src/builtins/scopes.ts +2 -2
  25. package/src/builtins/version.ts +1 -1
  26. package/src/cli-tool/full-example-capabilities.test.ts +2 -2
  27. package/src/cli-tool/main.ts +1 -1
  28. package/src/cli-tool/program.ts +1 -1
  29. package/src/cli-tool/prompt.ts +1 -1
  30. package/src/config/bindings.test.ts +1 -1
  31. package/src/config/bindings.ts +1 -1
  32. package/src/config/bootstrap.test.ts +1 -1
  33. package/src/config/bootstrap.ts +3 -3
  34. package/src/config/context.test.ts +1 -1
  35. package/src/config/context.ts +1 -1
  36. package/src/config/entry.ts +1 -1
  37. package/src/config/file.test.ts +1 -1
  38. package/src/config/file.ts +3 -3
  39. package/src/config/manifest.ts +1 -1
  40. package/src/config/resolve.test.ts +1 -1
  41. package/src/config/resolve.ts +1 -1
  42. package/src/config/schema.ts +1 -1
  43. package/src/config/validate.test.ts +45 -1
  44. package/src/config/validate.ts +138 -203
  45. package/src/configure/artifacts/binary-placement.test.ts +1 -1
  46. package/src/configure/artifacts/binary-placement.ts +1 -1
  47. package/src/configure/artifacts/gh-release-update.ts +1 -1
  48. package/src/configure/artifacts/install-validate.test.ts +3 -3
  49. package/src/configure/artifacts/mcp-config.ts +1 -1
  50. package/src/configure/artifacts/mcp-opencode.test.ts +1 -1
  51. package/src/configure/artifacts/mcp-opencode.ts +1 -1
  52. package/src/configure/artifacts/paths.ts +5 -5
  53. package/src/configure/artifacts/plan.ts +1 -1
  54. package/src/configure/artifacts/status.test.ts +1 -1
  55. package/src/configure/artifacts/status.ts +2 -2
  56. package/src/configure/artifacts/target-base.ts +1 -1
  57. package/src/configure/artifacts/target-detect.ts +1 -1
  58. package/src/configure/artifacts/target-effective.ts +3 -3
  59. package/src/configure/artifacts/target-mcp-cli.ts +1 -1
  60. package/src/configure/artifacts/target-mcp-json.ts +1 -1
  61. package/src/configure/artifacts/target-plan-build.ts +2 -2
  62. package/src/configure/artifacts/target-scope.ts +3 -3
  63. package/src/configure/artifacts/target-skill.ts +1 -1
  64. package/src/configure/artifacts/target-types.ts +2 -2
  65. package/src/configure/artifacts/targets/app.ts +5 -5
  66. package/src/configure/artifacts/targets/chatgpt-mcp.ts +2 -2
  67. package/src/configure/artifacts/targets/claude-code-mcp.ts +2 -2
  68. package/src/configure/artifacts/targets/claude-desktop-mcp.ts +2 -2
  69. package/src/configure/artifacts/targets/claude-skill.ts +2 -2
  70. package/src/configure/artifacts/targets/codex-mcp.ts +2 -2
  71. package/src/configure/artifacts/targets/codex-skill.ts +2 -2
  72. package/src/configure/artifacts/targets/configure.ts +5 -5
  73. package/src/configure/artifacts/targets/cursor-mcp.ts +2 -2
  74. package/src/configure/artifacts/targets/cursor-skill.ts +2 -2
  75. package/src/configure/artifacts/targets/index.ts +1 -1
  76. package/src/configure/artifacts/targets/openclaw-mcp.ts +2 -2
  77. package/src/configure/artifacts/targets/openclaw-skill.ts +3 -3
  78. package/src/configure/artifacts/targets/opencode-mcp.ts +5 -5
  79. package/src/configure/artifacts/targets/opencode-skill.ts +3 -3
  80. package/src/configure/artifacts/targets.test.ts +1 -1
  81. package/src/configure/artifacts/uninstall.ts +1 -1
  82. package/src/configure/configure.test.ts +9 -9
  83. package/src/configure/index.ts +5 -5
  84. package/src/configure/prompt.ts +1 -1
  85. package/src/core/context.ts +3 -3
  86. package/src/core/json-leaf.test.ts +1 -1
  87. package/src/core/leaf-inputs.test.ts +1 -1
  88. package/src/core/leaf-inputs.ts +2 -2
  89. package/src/core/parse.test.ts +9 -9
  90. package/src/core/parse.ts +2 -2
  91. package/src/core/schema.ts +3 -3
  92. package/src/core/types.ts +1 -1
  93. package/src/core/validate.ts +6 -6
  94. package/src/docs/builtin.ts +3 -3
  95. package/src/docs/cli-guide.test.ts +3 -3
  96. package/src/docs/cli-guide.ts +5 -5
  97. package/src/docs/docs.test.ts +8 -8
  98. package/src/docs/http-guide.ts +7 -7
  99. package/src/docs/mcp-guide.ts +7 -7
  100. package/src/docs/mcp-resources.test.ts +1 -1
  101. package/src/docs/mcp-resources.ts +1 -1
  102. package/src/docs/resolve.ts +4 -4
  103. package/src/docs/save.ts +2 -2
  104. package/src/exports/cli.ts +9 -9
  105. package/src/exports/headless.ts +2 -2
  106. package/src/exports/http.ts +2 -2
  107. package/src/exports/mcp.ts +2 -2
  108. package/src/headless/routing.ts +2 -2
  109. package/src/headless/tool-call.ts +11 -11
  110. package/src/hooks/run.ts +6 -6
  111. package/src/http/openapi.ts +3 -3
  112. package/src/http/paths.ts +1 -1
  113. package/src/http/readiness.ts +3 -3
  114. package/src/http/result.ts +1 -1
  115. package/src/http/routes.ts +4 -4
  116. package/src/http/server.ts +6 -6
  117. package/src/log/emitter.ts +1 -1
  118. package/src/mcp/bundle.ts +2 -2
  119. package/src/mcp/claude.test.ts +1 -1
  120. package/src/mcp/claude.ts +4 -4
  121. package/src/mcp/hidden-mcpb.test.ts +5 -5
  122. package/src/mcp/result.ts +2 -2
  123. package/src/mcp/server.ts +3 -3
  124. package/src/mcp/tools.ts +7 -7
  125. package/src/runtime/capabilities.ts +2 -2
  126. package/src/runtime/cli-errors.ts +3 -3
  127. package/src/runtime/cli.ts +22 -22
  128. package/src/runtime/exposure.ts +2 -2
  129. package/src/runtime/invoke.test.ts +6 -6
  130. package/src/server/context.ts +3 -3
  131. package/src/server/overrides.ts +3 -3
  132. package/src/skill/generate.ts +5 -5
  133. package/src/skill/hint.ts +1 -1
  134. package/src/skill/install.ts +2 -2
  135. package/src/skill/naming.ts +1 -1
  136. package/src/test/fixtures.ts +2 -2
  137. package/src/test/integration/config.test.ts +3 -3
  138. package/src/test/integration/http.test.ts +10 -10
  139. package/src/test/integration/mcp.test.ts +6 -6
@@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test";
6
6
  import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { dirname, join } from "node:path";
9
- import type { CliProgram } from "~/core/types.ts";
9
+ import type { CliProgram } from "../core/types.ts";
10
10
  import { CONFIG_BINDINGS_KEY } from "./bindings.ts";
11
11
  import { bootstrapAppConfig } from "./bootstrap.ts";
12
12
  import {
@@ -4,9 +4,9 @@ JSON app config file path helpers and strict read/write.
4
4
 
5
5
  import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
- import type { CliProgram } from "~/core/types.ts";
8
- import { sanitizeToolSegment } from "~/mcp/tools.ts";
9
- import { appConfigLibHome, displayHomePath } from "~/paths/host.ts";
7
+ import type { CliProgram } from "../core/types.ts";
8
+ import { sanitizeToolSegment } from "../mcp/tools.ts";
9
+ import { appConfigLibHome, displayHomePath } from "../paths/host.ts";
10
10
  import { isFrameworkConfigKey, validateBindingsShape } from "./bindings.ts";
11
11
  import { effectiveJsonSchema } from "./schema.ts";
12
12
  import { validateConfigDocument, validateConfigDocumentPartial } from "./validate.ts";
@@ -2,7 +2,7 @@
2
2
  MCP bundle / plugin manifest builders for program.appConfig env-mapped entries.
3
3
  */
4
4
 
5
- import type { CliAppConfigEntry, CliProgram } from "~/core/types.ts";
5
+ import type { CliAppConfigEntry, CliProgram } from "../core/types.ts";
6
6
  import {
7
7
  configEntryRequired,
8
8
  configEntrySensitive,
@@ -3,7 +3,7 @@ Tests for config/resolve module behavior.
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
- import type { CliProgram } from "~/core/types.ts";
6
+ import type { CliProgram } from "../core/types.ts";
7
7
  import { captureMappedHostEnv, exportConfigToEnv, resolveAppConfig } from "./resolve.ts";
8
8
 
9
9
  const program: CliProgram = {
@@ -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, CliAppConfigResolveContext, CliProgram } from "~/core/types.ts";
5
+ import type { CliAppConfigEntry, CliAppConfigResolveContext, CliProgram } from "../core/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";
@@ -2,7 +2,7 @@
2
2
  Effective JSON Schema for program.appConfig (block schema or all-string synthesis).
3
3
  */
4
4
 
5
- import type { CliAppConfigEntry, CliProgram } from "~/core/types.ts";
5
+ import type { CliAppConfigEntry, CliProgram } from "../core/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. */
@@ -3,7 +3,7 @@ Tests for config/validate module behavior.
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
- import { parseConfigSetValue, validateConfigDocument } from "./validate.ts";
6
+ import { parseConfigSetValue, resolveSchemaDraft, validateConfigDocument } from "./validate.ts";
7
7
 
8
8
  const rootSchema = {
9
9
  type: "object",
@@ -98,4 +98,48 @@ describe("config/validate", () => {
98
98
  expect(() => parseConfigSetValue("a,b", schema, rootSchema, false)).toThrow(/--json/);
99
99
  expect(parseConfigSetValue('[{"ttl":1}]', schema, rootSchema, false)).toEqual([{ ttl: 1 }]);
100
100
  });
101
+
102
+ test("resolveSchemaDraft maps $schema URIs", () => {
103
+ expect(resolveSchemaDraft({ $schema: "http://json-schema.org/draft-07/schema#" })).toBe("7");
104
+ expect(resolveSchemaDraft({ $schema: "https://json-schema.org/draft/2020-12/schema" })).toBe("2020-12");
105
+ expect(resolveSchemaDraft({ $schema: "https://json-schema.org/draft/2019-09/schema" })).toBe("2019-09");
106
+ expect(resolveSchemaDraft({})).toBe("7");
107
+ });
108
+
109
+ test("validates draft 2020-12 schemas when $schema is set", () => {
110
+ const schema = {
111
+ $schema: "https://json-schema.org/draft/2020-12/schema",
112
+ type: "object",
113
+ additionalProperties: false,
114
+ required: ["name"],
115
+ properties: {
116
+ name: { type: "string", minLength: 1 },
117
+ },
118
+ };
119
+ expect(validateConfigDocument({ name: "ok" }, schema).valid).toBe(true);
120
+ expect(validateConfigDocument({ name: "" }, schema).valid).toBe(false);
121
+ expect(validateConfigDocument({}, schema).valid).toBe(false);
122
+ });
123
+
124
+ test("validates draft 2020-12 $defs refs", () => {
125
+ const schema = {
126
+ $schema: "https://json-schema.org/draft/2020-12/schema",
127
+ type: "object",
128
+ additionalProperties: false,
129
+ required: ["item"],
130
+ properties: {
131
+ item: { $ref: "#/$defs/Item" },
132
+ },
133
+ $defs: {
134
+ Item: {
135
+ type: "object",
136
+ additionalProperties: false,
137
+ required: ["id"],
138
+ properties: { id: { type: "string" } },
139
+ },
140
+ },
141
+ };
142
+ expect(validateConfigDocument({ item: { id: "a" } }, schema).valid).toBe(true);
143
+ expect(validateConfigDocument({ item: { id: 1 } }, schema).valid).toBe(false);
144
+ });
101
145
  });
@@ -1,9 +1,10 @@
1
1
  /*
2
- Draft-07 JSON Schema subset validator for program.appConfig files.
3
- Aligned with common ts-json-schema-generator output (local $ref, objects, scalars).
2
+ JSON Schema validation for program.appConfig and leaf inputSchema (@cfworker/json-schema).
3
+ CLI value coercion for configure set remains here (comma-separated arrays, booleans, etc.).
4
4
  */
5
5
 
6
- import { parseCommaList, parseDate, parseDateTime } from "~/core/formats.ts";
6
+ import { format as jsonSchemaFormats, type Schema, type SchemaDraft, Validator } from "@cfworker/json-schema";
7
+ import { parseCommaList, parseDate, parseDateTime, validateCommaList } from "../core/formats.ts";
7
8
  import { isFrameworkConfigKey } from "./bindings.ts";
8
9
 
9
10
  type JsonSchema = Record<string, unknown>;
@@ -19,248 +20,183 @@ export interface ValidateResult {
19
20
  errors: string[];
20
21
  }
21
22
 
22
- /** Validate `data` against a JSON Schema root. Returns human-readable error messages. */
23
- export function validateConfigDocument(data: unknown, rootSchema: JsonSchema): ValidateResult {
24
- const errors: string[] = [];
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);
33
- return { valid: errors.length === 0, errors };
34
- }
35
-
36
- function validateValue(
37
- value: unknown,
38
- schema: JsonSchema,
39
- root: JsonSchema,
40
- path: string,
41
- errors: string[],
42
- partial: boolean,
43
- ): void {
44
- const resolved = resolveSchema(schema, root);
45
- if (!resolved) {
46
- return;
47
- }
48
-
49
- if ("const" in resolved) {
50
- if (value !== resolved.const) {
51
- errors.push(`${path}: must be ${JSON.stringify(resolved.const)}`);
23
+ if (!jsonSchemaFormats["comma-list"]) {
24
+ jsonSchemaFormats["comma-list"] = (value: string) => {
25
+ try {
26
+ validateCommaList(value);
27
+ return true;
28
+ } catch {
29
+ return false;
52
30
  }
53
- return;
54
- }
31
+ };
32
+ }
55
33
 
56
- if (Array.isArray(resolved.enum)) {
57
- if (!resolved.enum.some((item) => item === value)) {
58
- errors.push(`${path}: must be one of ${JSON.stringify(resolved.enum)}`);
59
- }
60
- return;
34
+ function dataForSchemaValidation(data: unknown): unknown {
35
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
36
+ return data;
61
37
  }
62
-
63
- const anyOf = resolved.anyOf ?? resolved.oneOf;
64
- if (Array.isArray(anyOf)) {
65
- const branchErrors: string[][] = [];
66
- for (const branch of anyOf) {
67
- if (typeof branch !== "object" || branch === null || Array.isArray(branch)) {
68
- continue;
69
- }
70
- const branchErrs: string[] = [];
71
- validateValue(value, branch as JsonSchema, root, path, branchErrs, partial);
72
- if (branchErrs.length === 0) {
73
- return;
74
- }
75
- branchErrors.push(branchErrs);
38
+ const out: Record<string, unknown> = {};
39
+ for (const [key, value] of Object.entries(data)) {
40
+ if (isFrameworkConfigKey(key)) {
41
+ continue;
76
42
  }
77
- errors.push(`${path}: must match one of the allowed types`);
78
- if (branchErrors.length === 1 && branchErrors[0]?.[0]) {
79
- errors.push(branchErrors[0][0]);
80
- }
81
- return;
82
- }
83
-
84
- const types = normalizeTypes(resolved.type);
85
- if (types.length > 0 && !matchesAnyType(value, types)) {
86
- errors.push(`${path}: must be ${types.join(" or ")}`);
87
- return;
43
+ out[key] = value;
88
44
  }
45
+ return out;
46
+ }
89
47
 
90
- if (types.includes("string") || (types.length === 0 && typeof value === "string")) {
91
- validateString(value, resolved, path, errors);
48
+ function schemaWithoutRequired(schema: unknown): JsonSchema {
49
+ if (typeof schema !== "object" || schema === null) {
50
+ return schema as JsonSchema;
92
51
  }
93
- if (types.includes("number") || types.includes("integer")) {
94
- validateNumber(value, resolved, path, errors, types.includes("integer"));
52
+ if (Array.isArray(schema)) {
53
+ return schema.map(schemaWithoutRequired) as unknown as JsonSchema;
95
54
  }
96
- if (types.includes("boolean")) {
97
- if (typeof value !== "boolean") {
98
- errors.push(`${path}: must be boolean`);
55
+ const out: Record<string, unknown> = {};
56
+ for (const [key, value] of Object.entries(schema)) {
57
+ if (key === "required") {
58
+ continue;
99
59
  }
60
+ out[key] = schemaWithoutRequired(value);
100
61
  }
101
- if (types.includes("null") && value !== null) {
102
- errors.push(`${path}: must be null`);
103
- }
104
- if (types.includes("object") || (types.length === 0 && isPlainObject(value))) {
105
- validateObject(value, resolved, root, path, errors, partial);
106
- }
107
- if (types.includes("array") || (types.length === 0 && Array.isArray(value))) {
108
- validateArray(value, resolved, root, path, errors, partial);
109
- }
62
+ return out as JsonSchema;
110
63
  }
111
64
 
112
- function resolveSchema(schema: JsonSchema, root: JsonSchema): JsonSchema | undefined {
113
- const ref = schema.$ref;
114
- if (typeof ref === "string" && ref.startsWith("#/definitions/")) {
115
- const name = decodeURIComponent(ref.slice("#/definitions/".length));
116
- const definitions = root.definitions;
117
- if (typeof definitions !== "object" || definitions === null || Array.isArray(definitions)) {
118
- return schema;
119
- }
120
- const target = (definitions as Record<string, unknown>)[name];
121
- if (typeof target === "object" && target !== null && !Array.isArray(target)) {
122
- return target as JsonSchema;
123
- }
65
+ function formatInstancePath(instanceLocation: string): string {
66
+ if (instanceLocation.length === 0 || instanceLocation === "#") {
67
+ return "$";
124
68
  }
125
- return schema;
126
- }
127
-
128
- function normalizeTypes(type: unknown): string[] {
129
- if (typeof type === "string") {
130
- return [type];
69
+ if (instanceLocation.startsWith("#/")) {
70
+ return instanceLocation.slice(2).replace(/\//g, ".");
131
71
  }
132
- if (Array.isArray(type)) {
133
- return type.filter((t): t is string => typeof t === "string");
134
- }
135
- return [];
72
+ return instanceLocation;
136
73
  }
137
74
 
138
- function matchesAnyType(value: unknown, types: string[]): boolean {
139
- for (const t of types) {
140
- if (t === "null" && value === null) return true;
141
- if (t === "array" && Array.isArray(value)) return true;
142
- if (t === "object" && isPlainObject(value)) return true;
143
- if (t === "integer" && typeof value === "number" && Number.isInteger(value)) return true;
144
- if (t === "number" && typeof value === "number") return true;
145
- if (t === "boolean" && typeof value === "boolean") return true;
146
- if (t === "string" && typeof value === "string") return true;
147
- }
148
- return types.length === 0;
75
+ function formatValidationErrors(errors: { instanceLocation: string; error: string }[]): string[] {
76
+ return errors.map(({ instanceLocation, error }) => {
77
+ const path = formatInstancePath(instanceLocation);
78
+ return `${path}: ${error}`;
79
+ });
149
80
  }
150
81
 
151
- function isPlainObject(value: unknown): value is Record<string, unknown> {
152
- return typeof value === "object" && value !== null && !Array.isArray(value);
82
+ function decodeJsonPointerSegment(segment: string): string {
83
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
153
84
  }
154
85
 
155
- function validateString(value: unknown, schema: JsonSchema, path: string, errors: string[]): void {
156
- if (typeof value !== "string") {
157
- errors.push(`${path}: must be string`);
158
- return;
86
+ /** Map a schema `$schema` URI to the @cfworker/json-schema draft (defaults to Draft-07). */
87
+ export function resolveSchemaDraft(schema: JsonSchema): SchemaDraft {
88
+ const $schema = schema.$schema;
89
+ if (typeof $schema !== "string") {
90
+ return "7";
159
91
  }
160
- if (typeof schema.minLength === "number" && value.length < schema.minLength) {
161
- errors.push(`${path}: string shorter than minLength ${schema.minLength}`);
92
+ const normalized = $schema.toLowerCase();
93
+ if (normalized.includes("2020-12")) {
94
+ return "2020-12";
162
95
  }
163
- if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
164
- errors.push(`${path}: string longer than maxLength ${schema.maxLength}`);
96
+ if (normalized.includes("2019-09")) {
97
+ return "2019-09";
165
98
  }
166
- if (typeof schema.pattern === "string") {
167
- try {
168
- if (!new RegExp(schema.pattern).test(value)) {
169
- errors.push(`${path}: does not match pattern`);
170
- }
171
- } catch {
172
- /* invalid pattern in schema — skip */
99
+ if (normalized.includes("draft-04") || normalized.includes("draft/4")) {
100
+ return "4";
101
+ }
102
+ if (normalized.includes("draft-07") || normalized.includes("draft/7")) {
103
+ return "7";
104
+ }
105
+ return "7";
106
+ }
107
+
108
+ function resolveJsonPointer(root: JsonSchema, ref: string): unknown {
109
+ if (!ref.startsWith("#/")) {
110
+ return undefined;
111
+ }
112
+ const segments = ref
113
+ .slice(2)
114
+ .split("/")
115
+ .filter((segment) => segment.length > 0)
116
+ .map(decodeJsonPointerSegment);
117
+ let current: unknown = root;
118
+ for (const segment of segments) {
119
+ if (typeof current !== "object" || current === null || Array.isArray(current)) {
120
+ return undefined;
173
121
  }
122
+ current = (current as Record<string, unknown>)[segment];
174
123
  }
124
+ return current;
175
125
  }
176
126
 
177
- function validateNumber(value: unknown, schema: JsonSchema, path: string, errors: string[], integer: boolean): void {
178
- if (typeof value !== "number" || Number.isNaN(value)) {
179
- errors.push(`${path}: must be number`);
127
+ function attachRootCompanionSchemas(validator: Validator, root: JsonSchema, active: JsonSchema): void {
128
+ if (active === root) {
180
129
  return;
181
130
  }
182
- if (integer && !Number.isInteger(value)) {
183
- errors.push(`${path}: must be integer`);
131
+ const companion: Schema = {};
132
+ if (typeof root.definitions === "object" && root.definitions !== null && !Array.isArray(root.definitions)) {
133
+ companion.definitions = root.definitions;
184
134
  }
185
- if (typeof schema.minimum === "number" && value < schema.minimum) {
186
- errors.push(`${path}: below minimum ${schema.minimum}`);
135
+ if (typeof root.$defs === "object" && root.$defs !== null && !Array.isArray(root.$defs)) {
136
+ companion.$defs = root.$defs;
187
137
  }
188
- if (typeof schema.maximum === "number" && value > schema.maximum) {
189
- errors.push(`${path}: above maximum ${schema.maximum}`);
138
+ if (Object.keys(companion).length > 0) {
139
+ validator.addSchema(companion);
190
140
  }
191
141
  }
192
142
 
193
- function validateObject(
194
- value: unknown,
143
+ function validatorForSchema(schema: JsonSchema, root: JsonSchema, partial: boolean): Validator {
144
+ const active = partial ? schemaWithoutRequired(schema) : schema;
145
+ const validator = new Validator(active as Schema, resolveSchemaDraft(root), false);
146
+ attachRootCompanionSchemas(validator, root, active);
147
+ return validator;
148
+ }
149
+
150
+ function validateInstance(
151
+ data: unknown,
195
152
  schema: JsonSchema,
196
153
  root: JsonSchema,
197
- path: string,
198
- errors: string[],
199
154
  partial: boolean,
200
- ): void {
201
- if (!isPlainObject(value)) {
202
- errors.push(`${path}: must be object`);
203
- return;
204
- }
205
- const properties = schema.properties;
206
- const propMap =
207
- typeof properties === "object" && properties !== null && !Array.isArray(properties)
208
- ? (properties as Record<string, JsonSchema>)
209
- : undefined;
155
+ stripFrameworkKeys: boolean,
156
+ ): ValidateResult {
157
+ const validator = validatorForSchema(schema, root, partial);
158
+ const payload = stripFrameworkKeys ? dataForSchemaValidation(data) : data;
159
+ const result = validator.validate(payload);
160
+ if (result.valid) {
161
+ return { valid: true, errors: [] };
162
+ }
163
+ return { valid: false, errors: formatValidationErrors(result.errors) };
164
+ }
210
165
 
211
- if (propMap) {
212
- for (const [key, propSchema] of Object.entries(propMap)) {
213
- if (key in value) {
214
- validateValue(value[key], propSchema, root, `${path}.${key}`, errors, partial);
215
- }
216
- }
217
- }
166
+ function validateAgainstSchema(data: unknown, rootSchema: JsonSchema, partial: boolean): ValidateResult {
167
+ return validateInstance(data, rootSchema, rootSchema, partial, true);
168
+ }
218
169
 
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
- }
226
- }
227
- }
228
- }
170
+ /** Validate `data` against a JSON Schema root. Returns human-readable error messages. */
171
+ export function validateConfigDocument(data: unknown, rootSchema: JsonSchema): ValidateResult {
172
+ return validateAgainstSchema(data, rootSchema, false);
173
+ }
229
174
 
230
- if (schema.additionalProperties === false && propMap) {
231
- for (const key of Object.keys(value)) {
232
- if (isFrameworkConfigKey(key)) continue;
233
- if (!(key in propMap)) {
234
- errors.push(`${path}: unknown property '${key}'`);
235
- }
236
- }
237
- }
175
+ /** Validate present keys only — skips `required` checks (partial writes / bootstrap). */
176
+ export function validateConfigDocumentPartial(data: unknown, rootSchema: JsonSchema): ValidateResult {
177
+ return validateAgainstSchema(data, rootSchema, true);
238
178
  }
239
179
 
240
- function validateArray(
241
- value: unknown,
242
- schema: JsonSchema,
243
- root: JsonSchema,
244
- path: string,
245
- errors: string[],
246
- partial: boolean,
247
- ): void {
248
- if (!Array.isArray(value)) {
249
- errors.push(`${path}: must be array`);
250
- return;
180
+ function resolveSchema(schema: JsonSchema, root: JsonSchema): JsonSchema | undefined {
181
+ const ref = schema.$ref;
182
+ if (typeof ref !== "string" || !ref.startsWith("#/")) {
183
+ return schema;
251
184
  }
252
- if (typeof schema.minItems === "number" && value.length < schema.minItems) {
253
- errors.push(`${path}: fewer than minItems ${schema.minItems}`);
185
+ const target = resolveJsonPointer(root, ref);
186
+ if (typeof target === "object" && target !== null && !Array.isArray(target)) {
187
+ return target as JsonSchema;
254
188
  }
255
- if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
256
- errors.push(`${path}: more than maxItems ${schema.maxItems}`);
189
+ return schema;
190
+ }
191
+
192
+ function normalizeTypes(type: unknown): string[] {
193
+ if (typeof type === "string") {
194
+ return [type];
257
195
  }
258
- const items = schema.items;
259
- if (typeof items === "object" && items !== null && !Array.isArray(items)) {
260
- for (let i = 0; i < value.length; i++) {
261
- validateValue(value[i], items as JsonSchema, root, `${path}[${i}]`, errors, partial);
262
- }
196
+ if (Array.isArray(type)) {
197
+ return type.filter((t): t is string => typeof t === "string");
263
198
  }
199
+ return [];
264
200
  }
265
201
 
266
202
  export function validateParsedConfigValue(
@@ -271,10 +207,9 @@ export function validateParsedConfigValue(
271
207
  if (!propertySchema) {
272
208
  return parsed;
273
209
  }
274
- const errors: string[] = [];
275
- validateValue(parsed, propertySchema, rootSchema, "$", errors, false);
276
- if (errors.length > 0) {
277
- throw new Error(errors[0] ?? "Invalid config value");
210
+ const result = validateInstance(parsed, propertySchema, rootSchema, false, false);
211
+ if (!result.valid) {
212
+ throw new Error(result.errors[0] ?? "Invalid config value");
278
213
  }
279
214
  return parsed;
280
215
  }
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
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
- import type { CliProgram } from "~/core/types.ts";
9
+ import type { CliProgram } from "../../core/types.ts";
10
10
  import { isAppInstalled, isExternallyManagedBinary, resolvePathCommand } from "./binary-placement.ts";
11
11
 
12
12
  const program: CliProgram = {
@@ -1,6 +1,6 @@
1
1
  import { accessSync, constants, realpathSync } from "node:fs";
2
2
  import { delimiter, join } from "node:path";
3
- import type { CliProgram } from "~/core/types.ts";
3
+ import type { CliProgram } from "../../core/types.ts";
4
4
 
5
5
  /** Resolves a command name on PATH to its real path, if present. */
6
6
  export function resolvePathCommand(key: string): string | undefined {
@@ -1,7 +1,7 @@
1
1
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- import type { CliUpdateGetLatest } from "~/core/types.ts";
4
+ import type { CliUpdateGetLatest } from "../../core/types.ts";
5
5
 
6
6
  /** Config for {@link ghReleaseUpdateGetLatest}. */
7
7
  export interface GhReleaseUpdateConfig {
@@ -3,9 +3,9 @@ Tests for install/install-validate module behavior.
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
- import type { CliProgram } from "~/core/types.ts";
7
- import { CliSchemaValidationError } from "~/core/types.ts";
8
- import { cliValidateProgram } from "~/core/validate.ts";
6
+ import type { CliProgram } from "../../core/types.ts";
7
+ import { CliSchemaValidationError } from "../../core/types.ts";
8
+ import { cliValidateProgram } from "../../core/validate.ts";
9
9
 
10
10
  const base: CliProgram = {
11
11
  key: "app",
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import type { CliProgram } from "~/core/types.ts";
3
+ import type { CliProgram } from "../../core/types.ts";
4
4
 
5
5
  export interface McpServerEntry {
6
6
  command: string;
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
6
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
- import type { CliProgram } from "~/core/types.ts";
9
+ import type { CliProgram } from "../../core/types.ts";
10
10
  import {
11
11
  detectOpenCodeMcpConfigPath,
12
12
  expectedOpenCodeMcpEntry,
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import type { CliProgram } from "~/core/types.ts";
3
+ import type { CliProgram } from "../../core/types.ts";
4
4
  import { expectedMcpEntry } from "./mcp-config.ts";
5
5
 
6
6
  export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
@@ -1,9 +1,9 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import type { CliProgram } from "~/core/types.ts";
4
- import { mcpServerId, sanitizeToolSegment } from "~/mcp/tools.ts";
5
- import { displayHomePath, userHome, xdgConfigHome } from "~/paths/host.ts";
6
- import { skillDirNameForTarget } from "~/skill/install.ts";
3
+ import type { CliProgram } from "../../core/types.ts";
4
+ import { mcpServerId, sanitizeToolSegment } from "../../mcp/tools.ts";
5
+ import { displayHomePath, userHome, xdgConfigHome } from "../../paths/host.ts";
6
+ import { skillDirNameForTarget } from "../../skill/install.ts";
7
7
  import { resolveOpenclawConfigPath } from "./mcp-openclaw.ts";
8
8
  import { resolveOpenCodeConfigPathForInstall } from "./mcp-opencode.ts";
9
9
 
@@ -24,7 +24,7 @@ export interface InstallPaths {
24
24
  skillDirName: string;
25
25
  }
26
26
 
27
- export { userHome } from "~/paths/host.ts";
27
+ export { userHome } from "../../paths/host.ts";
28
28
 
29
29
  /** Format an absolute path for user-facing install output. */
30
30
  export function displayInstallPath(path: string): string {
@@ -1,4 +1,4 @@
1
- import type { CliProgram } from "~/core/types.ts";
1
+ import type { CliProgram } from "../../core/types.ts";
2
2
  import type { InstallPaths } from "./paths.ts";
3
3
  import { buildInstallPlanFromTargets } from "./target-plan-build.ts";
4
4
  import type { InstallAction, InstallOpts } from "./target-types.ts";
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
6
  import { mkdtempSync, rmSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
- import type { CliProgram } from "~/core/types.ts";
9
+ import type { CliProgram } from "../../core/types.ts";
10
10
  import { resolveInstallPaths } from "./paths.ts";
11
11
  import { printInstallStatus } from "./status.ts";
12
12
  import { resolveInstallTargetPreview } from "./target-scope.ts";