@open-cr-agent/cli 0.5.0 → 0.6.0

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/dist/cloud/account-salt.d.ts +6 -0
  2. package/dist/cloud/account-salt.js +33 -0
  3. package/dist/{review → cloud}/account-settings.js +5 -4
  4. package/dist/cloud/browser.d.ts +17 -0
  5. package/dist/cloud/browser.js +37 -0
  6. package/dist/cloud/client.d.ts +93 -0
  7. package/dist/cloud/client.js +221 -0
  8. package/dist/cloud/credentials.d.ts +18 -0
  9. package/dist/cloud/credentials.js +52 -0
  10. package/dist/cloud/deps.d.ts +14 -0
  11. package/dist/cloud/deps.js +34 -0
  12. package/dist/cloud/file-lock.d.ts +14 -0
  13. package/dist/cloud/file-lock.js +77 -0
  14. package/dist/cloud/findings.d.ts +8 -0
  15. package/dist/{review/cloud-findings.js → cloud/findings.js} +10 -32
  16. package/dist/cloud/gateway-token.d.ts +18 -0
  17. package/dist/cloud/gateway-token.js +63 -0
  18. package/dist/cloud/memory.d.ts +8 -0
  19. package/dist/cloud/memory.js +68 -0
  20. package/dist/cloud/private-file.d.ts +8 -0
  21. package/dist/cloud/private-file.js +46 -0
  22. package/dist/{review/cloud-providers.d.ts → cloud/providers.d.ts} +12 -6
  23. package/dist/cloud/providers.js +132 -0
  24. package/dist/cloud/review.d.ts +28 -0
  25. package/dist/cloud/review.js +76 -0
  26. package/dist/cloud/settings.d.ts +22 -0
  27. package/dist/cloud/settings.js +64 -0
  28. package/dist/cloud/upload.d.ts +20 -0
  29. package/dist/{review/cloud-upload.js → cloud/upload.js} +74 -71
  30. package/dist/commands/login.d.ts +5 -0
  31. package/dist/commands/login.js +164 -0
  32. package/dist/commands/memory.d.ts +3 -0
  33. package/dist/{memory.js → commands/memory.js} +21 -20
  34. package/dist/commands/metrics.d.ts +45 -0
  35. package/dist/{metrics.js → commands/metrics.js} +32 -45
  36. package/dist/{plugins-command.d.ts → commands/plugins.d.ts} +3 -9
  37. package/dist/{plugins-command.js → commands/plugins.js} +29 -53
  38. package/dist/{review → commands/review}/args.d.ts +7 -10
  39. package/dist/{review → commands/review}/args.js +11 -16
  40. package/dist/commands/review/deliver.d.ts +7 -0
  41. package/dist/commands/review/deliver.js +56 -0
  42. package/dist/commands/review/deps.d.ts +18 -0
  43. package/dist/commands/review/deps.js +2 -0
  44. package/dist/commands/review/execute-run.d.ts +13 -0
  45. package/dist/commands/review/execute-run.js +97 -0
  46. package/dist/commands/review/exit-code.d.ts +4 -0
  47. package/dist/commands/review/exit-code.js +29 -0
  48. package/dist/commands/review/plan-prices.d.ts +3 -0
  49. package/dist/commands/review/plan-prices.js +18 -0
  50. package/dist/{review → commands/review}/plan-render.js +22 -2
  51. package/dist/commands/review/plan-run.d.ts +5 -0
  52. package/dist/commands/review/plan-run.js +38 -0
  53. package/dist/commands/review/plan-schema.d.ts +97 -0
  54. package/dist/commands/review/plan-schema.js +34 -0
  55. package/dist/{review → commands/review}/progress.d.ts +3 -5
  56. package/dist/{review → commands/review}/progress.js +3 -3
  57. package/dist/{review → commands/review}/provenance.d.ts +4 -2
  58. package/dist/{review → commands/review}/provenance.js +2 -3
  59. package/dist/{review → commands/review}/render.js +1 -1
  60. package/dist/commands/review/resolve-run.d.ts +29 -0
  61. package/dist/commands/review/resolve-run.js +125 -0
  62. package/dist/commands/review/review-options.d.ts +6 -0
  63. package/dist/commands/review/review-options.js +35 -0
  64. package/dist/{review → commands/review}/runtimes.js +1 -1
  65. package/dist/{review → commands/review}/sarif-input.d.ts +0 -1
  66. package/dist/{review → commands/review}/sarif-input.js +5 -5
  67. package/dist/commands/review/sarif.d.ts +96 -0
  68. package/dist/{review → commands/review}/sarif.js +29 -7
  69. package/dist/commands/review/settings-sources.d.ts +17 -0
  70. package/dist/commands/review/settings-sources.js +30 -0
  71. package/dist/commands/review/target.d.ts +38 -0
  72. package/dist/commands/review/target.js +126 -0
  73. package/dist/commands/review.d.ts +7 -0
  74. package/dist/commands/review.js +34 -0
  75. package/dist/config/cli-config.d.ts +41 -0
  76. package/dist/config/cli-config.js +199 -0
  77. package/dist/{review/remote-config.d.ts → config/remote.d.ts} +3 -3
  78. package/dist/{review/remote-config.js → config/remote.js} +5 -23
  79. package/dist/{review/config.d.ts → config/schema.d.ts} +8 -31
  80. package/dist/config/schema.js +138 -0
  81. package/dist/config/settings.d.ts +37 -0
  82. package/dist/config/settings.js +133 -0
  83. package/dist/config/user-dir.d.ts +3 -0
  84. package/dist/config/user-dir.js +10 -0
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.js +10 -1
  87. package/dist/internal.d.ts +6 -0
  88. package/dist/internal.js +7 -0
  89. package/dist/io/exit.d.ts +9 -0
  90. package/dist/io/exit.js +12 -0
  91. package/dist/io/output.d.ts +4 -0
  92. package/dist/io/output.js +2 -0
  93. package/dist/io/usage-error.d.ts +5 -0
  94. package/dist/io/usage-error.js +8 -0
  95. package/dist/main.js +2 -2
  96. package/dist/{review/account-plugins.d.ts → plugins/account.d.ts} +2 -2
  97. package/dist/{review/account-plugins.js → plugins/account.js} +7 -7
  98. package/dist/{review/plugins.d.ts → plugins/load.d.ts} +2 -2
  99. package/dist/{review/plugins.js → plugins/load.js} +6 -5
  100. package/dist/plugins/npm.d.ts +8 -0
  101. package/dist/plugins/npm.js +24 -0
  102. package/dist/{plugin-store.d.ts → plugins/store.d.ts} +1 -1
  103. package/dist/{plugin-store.js → plugins/store.js} +5 -4
  104. package/dist/repository-id.d.ts +12 -0
  105. package/dist/repository-id.js +72 -0
  106. package/dist/run.d.ts +2 -2
  107. package/dist/run.js +15 -12
  108. package/dist/session/store.d.ts +9 -0
  109. package/dist/session/store.js +31 -0
  110. package/package.json +17 -7
  111. package/dist/account-salt.d.ts +0 -7
  112. package/dist/account-salt.js +0 -43
  113. package/dist/cloud.d.ts +0 -34
  114. package/dist/cloud.js +0 -272
  115. package/dist/memory.d.ts +0 -4
  116. package/dist/metrics.d.ts +0 -47
  117. package/dist/review/cloud-findings.d.ts +0 -31
  118. package/dist/review/cloud-memory.d.ts +0 -6
  119. package/dist/review/cloud-memory.js +0 -58
  120. package/dist/review/cloud-providers.js +0 -60
  121. package/dist/review/cloud-review.d.ts +0 -19
  122. package/dist/review/cloud-review.js +0 -51
  123. package/dist/review/cloud-settings.d.ts +0 -16
  124. package/dist/review/cloud-settings.js +0 -118
  125. package/dist/review/cloud-upload.d.ts +0 -60
  126. package/dist/review/command.d.ts +0 -33
  127. package/dist/review/command.js +0 -293
  128. package/dist/review/config.js +0 -304
  129. package/dist/review/sarif.d.ts +0 -4
  130. package/dist/review/settings-sources.d.ts +0 -11
  131. package/dist/review/settings-sources.js +0 -58
  132. package/dist/review/target.d.ts +0 -18
  133. package/dist/review/target.js +0 -210
  134. /package/dist/{review → cloud}/account-settings.d.ts +0 -0
  135. /package/dist/{review → commands/review}/plan-render.d.ts +0 -0
  136. /package/dist/{review → commands/review}/render.d.ts +0 -0
  137. /package/dist/{review → commands/review}/runtimes.d.ts +0 -0
  138. /package/dist/{review → io}/terminal.d.ts +0 -0
  139. /package/dist/{review → io}/terminal.js +0 -0
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { OcraError } from "@open-cr-agent/core";
2
+ import { MODEL_TIERS, OcraError } from "@open-cr-agent/core";
3
3
  import { repoRuleSchema } from "@open-cr-agent/core/internal";
4
4
  import { z } from "zod";
5
5
  const MAX_BYTES = 256 * 1024;
@@ -7,11 +7,11 @@ const TIMEOUT_MS = 10_000;
7
7
  // What a shared configuration may set: settings, rules and model providers
8
8
  // (a company gateway, for example), never plugins or a runtime, because it
9
9
  // is fetched from outside the repository.
10
- export const remoteConfigSchema = z
10
+ const remoteConfigSchema = z
11
11
  .object({
12
12
  $schema: z.string().optional(),
13
- models: z.record(z.enum(["top", "standard", "light"]), z.unknown()).optional(),
14
- effort: z.partialRecord(z.enum(["top", "standard", "light"]), z.unknown()).optional(),
13
+ models: z.record(z.enum(MODEL_TIERS), z.unknown()).optional(),
14
+ effort: z.partialRecord(z.enum(MODEL_TIERS), z.unknown()).optional(),
15
15
  concurrency: z.unknown().optional(),
16
16
  taskTimeoutMinutes: z.unknown().optional(),
17
17
  runTimeoutMinutes: z.unknown().optional(),
@@ -81,22 +81,4 @@ async function readLimited(response, limit, name) {
81
81
  }
82
82
  return Buffer.concat(chunks).toString("utf8");
83
83
  }
84
- // The repository's own values win; lists and maps are combined.
85
- export function mergeConfig(remote, local) {
86
- const { rules: _rules, ...settings } = remote;
87
- const merged = { ...settings, ...local };
88
- for (const key of ["models", "effort", "reviewers", "roles", "github", "providers"]) {
89
- const a = settings[key];
90
- const b = local[key];
91
- if (a !== undefined || b !== undefined)
92
- merged[key] = { ...(a ?? {}), ...(b ?? {}) };
93
- }
94
- for (const key of ["include", "exclude"]) {
95
- const a = settings[key] ?? [];
96
- const b = local[key] ?? [];
97
- if (a.length + b.length > 0)
98
- merged[key] = [...a, ...b];
99
- }
100
- return merged;
101
- }
102
- //# sourceMappingURL=remote-config.js.map
84
+ //# sourceMappingURL=remote.js.map
@@ -1,6 +1,11 @@
1
- import { type CustomProvider, type ModelChains, OcraError, type ReviewerOverrides, type RoleSettings, type SourcedRule, type TierEfforts } from "@open-cr-agent/core";
2
1
  import { z } from "zod";
3
- export declare const CONFIG_PATH = ".ocra/config.json";
2
+ export declare const effort: z.ZodEnum<{
3
+ high: "high";
4
+ low: "low";
5
+ medium: "medium";
6
+ minimal: "minimal";
7
+ none: "none";
8
+ }>;
4
9
  export declare const DEFAULT_RUNTIME = "opencode";
5
10
  export declare const configSchema: z.ZodObject<{
6
11
  $schema: z.ZodOptional<z.ZodString>;
@@ -103,32 +108,4 @@ export declare const configSchema: z.ZodObject<{
103
108
  }, z.core.$strict>;
104
109
  export declare const CONFIG_SCHEMA_ID = "https://ocra.majincheng.com/schema/config.v1.json";
105
110
  export declare function configJsonSchema(): Record<string, unknown>;
106
- export type CliConfig = Omit<z.infer<typeof configSchema>, "models" | "effort" | "providers" | "runtime"> & {
107
- runtime: string;
108
- runtimeSet: boolean;
109
- models: ModelChains;
110
- effort: TierEfforts;
111
- providers: Record<string, CustomProvider>;
112
- rules: SourcedRule[];
113
- accountSettings?: {
114
- version: string | null;
115
- };
116
- };
117
- export interface LoadOptions {
118
- repository: boolean;
119
- read?: (path: string) => Promise<string | undefined>;
120
- file?: string;
121
- fetch?: typeof fetch;
122
- warn?: (message: string) => void;
123
- }
124
- export declare class ConfigError extends OcraError {
125
- constructor(message: string, options?: {
126
- cause?: unknown;
127
- });
128
- }
129
- export declare function loadConfig(root: string, env: Readonly<Record<string, string | undefined>>, options?: LoadOptions): Promise<CliConfig>;
130
- export declare function agentChains(config: {
131
- reviewers: ReviewerOverrides;
132
- roles: RoleSettings;
133
- }): Record<string, readonly string[]>;
134
- //# sourceMappingURL=config.d.ts.map
111
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,138 @@
1
+ import { EFFORT_LEVELS } from "@open-cr-agent/core";
2
+ import { AGENT_ROLES, MAX_TIMER_MS, RISK_TIERS } from "@open-cr-agent/core/internal";
3
+ import { z } from "zod";
4
+ // What .ocra/config.json may say. Every layer of settings (settings.ts) is
5
+ // checked against it.
6
+ // How much a model reasons before answering (ADR-0025); unset leaves the
7
+ // provider's default.
8
+ export const effort = z.enum(EFFORT_LEVELS);
9
+ // The longest timeout a timer keeps, about 24.8 days; a longer one would fire at once.
10
+ const timeoutMinutes = z
11
+ .number()
12
+ .positive()
13
+ .max(Math.floor(MAX_TIMER_MS / 60_000))
14
+ .optional();
15
+ const modelChain = z
16
+ .union([z.string().min(1), z.array(z.string().min(1)).min(1)])
17
+ .transform((value) => (typeof value === "string" ? [value] : value));
18
+ // Where review code is sent: over https, or plain http only to this machine.
19
+ // OpenCode replaces {env:NAME} and {file:path} anywhere in its configuration,
20
+ // so an address with braces could carry a variable or a file (the checkout's
21
+ // .git/config holds its token) to the endpoint.
22
+ const endpoint = z
23
+ .url()
24
+ .refine((value) => {
25
+ const url = new URL(value);
26
+ if (url.protocol === "https:")
27
+ return true;
28
+ const host = url.hostname.replace(/^\[|\]$/g, "");
29
+ return (url.protocol === "http:" &&
30
+ (host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host)));
31
+ }, "must be an https URL, or http on this machine (localhost, 127.0.0.1, ::1)")
32
+ .refine((value) => !/[{}]/.test(value), "must not contain { or }");
33
+ // The platforms' and clouds' credentials never go to a model endpoint; the
34
+ // same prefixes are never passed to the runtime by prefix (server-env.ts).
35
+ const PLATFORM_TOKENS = /^(GITHUB_|GH_|GITLAB_|CI_|ACTIONS_|RUNNER_|AWS_|AZURE_|NPM_|SSH_|OCRA_)/;
36
+ const price = z.number().min(0).max(10_000);
37
+ const providerSchema = z
38
+ .object({
39
+ type: z.literal("openai-compatible"),
40
+ baseUrl: endpoint,
41
+ apiKeyEnv: z
42
+ .string()
43
+ .regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be an environment variable name")
44
+ .refine((name) => !PLATFORM_TOKENS.test(name), "must not name a platform or cloud credential")
45
+ .optional(),
46
+ // Prices in US dollars per million tokens; 0 means unpriced.
47
+ models: z
48
+ .record(z.string().regex(/^[\w./:@-]{1,200}$/), z.object({ input: price, output: price, cachedInput: price.optional() }).strict())
49
+ .refine((models) => Object.keys(models).length > 0, "must list at least one model"),
50
+ // How the endpoint takes a reasoning effort: "openai" sends
51
+ // reasoning_effort (the default), "openrouter" sends reasoning.effort.
52
+ effort: z.enum(["openai", "openrouter"]).optional(),
53
+ })
54
+ .strict();
55
+ // The runtimes ocra ships; a plugin may register another name, so the
56
+ // schema lists these for editors and still accepts any other string.
57
+ const BUILTIN_RUNTIMES = ["opencode", "direct"];
58
+ export const DEFAULT_RUNTIME = "opencode";
59
+ export const configSchema = z
60
+ .object({
61
+ $schema: z.string().optional(),
62
+ models: z
63
+ .object({ top: modelChain, standard: modelChain, light: modelChain })
64
+ .partial()
65
+ .strict()
66
+ .default({}),
67
+ effort: z
68
+ .object({ top: effort, standard: effort, light: effort })
69
+ .partial()
70
+ .strict()
71
+ .default({}),
72
+ concurrency: z.number().int().min(1).max(32).optional(),
73
+ taskTimeoutMinutes: timeoutMinutes,
74
+ runTimeoutMinutes: timeoutMinutes,
75
+ verify: z.boolean().optional(),
76
+ judge: z.boolean().optional(),
77
+ maxCostUsd: z.number().positive().optional(),
78
+ maxTasks: z.number().int().min(1).max(1_000).optional(),
79
+ // Unset: each provider's default. A seed makes sampling repeatable only
80
+ // where the provider supports it.
81
+ sampling: z
82
+ .object({
83
+ temperature: z.number().min(0).max(2),
84
+ seed: z
85
+ .number()
86
+ .int()
87
+ .min(0)
88
+ .max(2 ** 31 - 1),
89
+ })
90
+ .partial()
91
+ .strict()
92
+ .optional(),
93
+ github: z
94
+ .object({ requestChanges: z.boolean(), botLogin: z.string().min(1) })
95
+ .partial()
96
+ .strict()
97
+ .default({}),
98
+ include: z.array(z.string().min(1)).default([]),
99
+ exclude: z.array(z.string().min(1)).default([]),
100
+ // Unset means "opencode"; loadConfig fills it in and records whether the
101
+ // file chose, so account defaults (ADR-0025) apply only where it did not.
102
+ runtime: z.enum(BUILTIN_RUNTIMES).or(z.string().min(1)).optional(),
103
+ plugins: z.array(z.string().min(1)).default([]),
104
+ reviewers: z
105
+ .record(z.string(), z
106
+ .object({
107
+ enabled: z.boolean(),
108
+ minTier: z.enum(RISK_TIERS),
109
+ // The reviewer's own chain, for its review tasks and plan call.
110
+ models: modelChain,
111
+ effort,
112
+ })
113
+ .partial()
114
+ .strict())
115
+ .default({}),
116
+ roles: z
117
+ .partialRecord(z.enum(AGENT_ROLES), z.object({ models: modelChain, effort }).partial().strict())
118
+ .default({}),
119
+ pluginSettings: z.record(z.string(), z.unknown()).default({}),
120
+ providers: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,39}$/), providerSchema).default({}),
121
+ extends: z.string().min(1).optional(),
122
+ })
123
+ .strict();
124
+ export const CONFIG_SCHEMA_ID = "https://ocra.majincheng.com/schema/config.v1.json";
125
+ // JSON Schema (draft 2020-12) of .ocra/config.json, for editors: what a
126
+ // file may say, so defaults are optional and model chains take either form.
127
+ export function configJsonSchema() {
128
+ const generated = z.toJSONSchema(configSchema, { target: "draft-2020-12", io: "input" });
129
+ const { $schema, ...rest } = generated;
130
+ return {
131
+ $schema,
132
+ $id: CONFIG_SCHEMA_ID,
133
+ title: "ocra configuration",
134
+ description: "The .ocra/config.json file of a repository, or a file passed with --config. Unknown keys are rejected.",
135
+ ...rest,
136
+ };
137
+ }
138
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,37 @@
1
+ import { type SourcedRule } from "@open-cr-agent/core";
2
+ import { z } from "zod";
3
+ import { configSchema } from "./schema.js";
4
+ type FileSettings = z.output<typeof configSchema>;
5
+ export type Settings = FileSettings & {
6
+ rules: SourcedRule[];
7
+ ultra: boolean;
8
+ };
9
+ type SettingSource = "shared" | "file" | "env" | "account" | "flag";
10
+ export type LayerSettings = {
11
+ [K in keyof Settings]?: Settings[K];
12
+ };
13
+ export interface SettingsLayer {
14
+ source: SettingSource;
15
+ settings: LayerSettings;
16
+ under?: boolean;
17
+ }
18
+ export type SettingSources = Record<string, SettingSource[]>;
19
+ export declare const layerSchema: z.ZodType<Omit<LayerSettings, "rules" | "ultra">>;
20
+ /** The layers, earliest first, over ocra's defaults, and where each setting came from. */
21
+ export declare function resolveSettings(layers: readonly SettingsLayer[]): {
22
+ settings: Settings;
23
+ sources: SettingSources;
24
+ };
25
+ export interface ListedSetting {
26
+ key: string;
27
+ value?: unknown;
28
+ sources: SettingSource[];
29
+ }
30
+ /**
31
+ * The settings --plan lists, in its order, with their sources. A setting
32
+ * only a command-line flag set is the flag's, not a setting, and is left
33
+ * out.
34
+ */
35
+ export declare function listSettings(settings: Settings, sources: SettingSources): ListedSetting[];
36
+ export {};
37
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1,133 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { MODEL_TIERS, OcraError } from "@open-cr-agent/core";
3
+ import { z } from "zod";
4
+ import { configSchema } from "./schema.js";
5
+ const ALL = ["shared", "file", "env", "account"];
6
+ const NOT_ENV = ["shared", "file", "account"];
7
+ const ALWAYS = { list: "always" };
8
+ // How each setting merges, which layers may set it, and how --plan lists
9
+ // it, in --plan's order. A shared configuration is fetched from outside the
10
+ // repository, so it never names plugins or a runtime; the account never
11
+ // moves code or keys (providers, extends) or decides whose comments are
12
+ // trusted (github). The layers' parsers refuse these with a warning; here
13
+ // they are a broken invariant.
14
+ const MERGE = {
15
+ runtime: { merge: "replace", from: ["file", "account"], plan: ALWAYS },
16
+ models: { merge: "entries", from: ALL, plan: { list: "always", entries: MODEL_TIERS } },
17
+ effort: { merge: "entries", from: ALL, plan: { list: "always", entries: MODEL_TIERS } },
18
+ reviewers: { merge: "entries", from: NOT_ENV, plan: { list: "set" } },
19
+ roles: { merge: "entries", from: NOT_ENV, plan: { list: "set" } },
20
+ concurrency: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
21
+ taskTimeoutMinutes: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
22
+ runTimeoutMinutes: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
23
+ maxCostUsd: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
24
+ maxTasks: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
25
+ verify: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
26
+ judge: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
27
+ sampling: { merge: "replace", from: ["file", "account"], plan: ALWAYS },
28
+ include: { merge: "concat", from: NOT_ENV, plan: ALWAYS },
29
+ exclude: { merge: "concat", from: NOT_ENV, plan: ALWAYS },
30
+ rules: {
31
+ merge: "concat",
32
+ from: ["shared", "account"],
33
+ // Where each rule applies and whose it is; the rule's text can be long.
34
+ plan: {
35
+ list: "always",
36
+ show: (rules) => rules.map(({ path, source }) => ({ path, source })),
37
+ },
38
+ },
39
+ ultra: { merge: "replace", from: ["account", "flag"], plan: { list: "set" } },
40
+ $schema: { merge: "replace", from: ["shared", "file"] },
41
+ github: { merge: "entries", from: ["shared", "file"] },
42
+ plugins: { merge: "replace", from: ["file"] },
43
+ pluginSettings: { merge: "replace", from: ["file"] },
44
+ providers: { merge: "entries", from: ["shared", "file"] },
45
+ extends: { merge: "replace", from: ["file"] },
46
+ };
47
+ // The configuration file's schema without its defaults, so a layer holds
48
+ // only what it sets; unknown keys are refused as in the file.
49
+ export const layerSchema = z
50
+ .object(Object.fromEntries(Object.entries(configSchema.shape).map(([key, schema]) => [
51
+ key,
52
+ (schema instanceof z.ZodDefault ? schema.unwrap() : schema).optional(),
53
+ ])))
54
+ .strict();
55
+ /** The layers, earliest first, over ocra's defaults, and where each setting came from. */
56
+ export function resolveSettings(layers) {
57
+ const merged = { ...configSchema.parse({}), rules: [], ultra: false };
58
+ const sources = {};
59
+ const record = (name, source) => {
60
+ const from = sources[name] ?? [];
61
+ if (!from.includes(source))
62
+ sources[name] = [...from, source];
63
+ };
64
+ for (const { source, settings, under = false } of layers) {
65
+ for (const [key, value] of Object.entries(settings)) {
66
+ if (value === undefined)
67
+ continue;
68
+ const rule = MERGE[key];
69
+ if (!rule.from.includes(source)) {
70
+ throw new OcraError("INTERNAL", `the ${source} settings may not set ${key}`);
71
+ }
72
+ if (rule.merge === "replace") {
73
+ if (under && sources[key])
74
+ continue;
75
+ merged[key] = value;
76
+ sources[key] = [source];
77
+ }
78
+ else if (rule.merge === "entries") {
79
+ const entries = { ...merged[key] };
80
+ for (const [entry, setting] of Object.entries(value)) {
81
+ const name = `${key}.${entry}`;
82
+ if (setting === undefined || (under && sources[name]))
83
+ continue;
84
+ entries[entry] = setting;
85
+ sources[name] = [source];
86
+ }
87
+ merged[key] = entries;
88
+ }
89
+ else {
90
+ const current = merged[key];
91
+ const added = under
92
+ ? value.filter((v) => !current.some((c) => isDeepStrictEqual(c, v)))
93
+ : value;
94
+ if (added.length === 0)
95
+ continue;
96
+ merged[key] = [...current, ...added];
97
+ record(key, source);
98
+ }
99
+ }
100
+ }
101
+ return { settings: merged, sources };
102
+ }
103
+ /**
104
+ * The settings --plan lists, in its order, with their sources. A setting
105
+ * only a command-line flag set is the flag's, not a setting, and is left
106
+ * out.
107
+ */
108
+ export function listSettings(settings, sources) {
109
+ const listed = [];
110
+ for (const [key, rule] of Object.entries(MERGE)) {
111
+ const plan = rule.plan;
112
+ if (!plan)
113
+ continue;
114
+ const value = settings[key];
115
+ const add = (name, shown, always) => {
116
+ const from = (sources[name] ?? []).filter((source) => source !== "flag");
117
+ if (from.length === 0 && !always)
118
+ return;
119
+ listed.push(from.length === 0 ? { key: name, sources: [] } : { key: name, value: shown, sources: from });
120
+ };
121
+ if (rule.merge === "entries") {
122
+ const map = value;
123
+ for (const entry of plan.entries ?? Object.keys(map)) {
124
+ add(`${key}.${entry}`, map[entry], plan.list === "always");
125
+ }
126
+ }
127
+ else {
128
+ add(key, plan.show ? plan.show(value) : value, plan.list === "always");
129
+ }
130
+ }
131
+ return listed;
132
+ }
133
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1,3 @@
1
+ /** ~/.config/ocra ($XDG_CONFIG_HOME/ocra), or %APPDATA%\ocra on Windows: this machine's own. */
2
+ export declare function ocraConfigDir(env: Readonly<Record<string, string | undefined>>): string;
3
+ //# sourceMappingURL=user-dir.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ /** ~/.config/ocra ($XDG_CONFIG_HOME/ocra), or %APPDATA%\ocra on Windows: this machine's own. */
4
+ export function ocraConfigDir(env) {
5
+ const base = process.platform === "win32"
6
+ ? (env.APPDATA ?? join(homedir(), "AppData", "Roaming"))
7
+ : (env.XDG_CONFIG_HOME ?? join(homedir(), ".config"));
8
+ return join(base, "ocra");
9
+ }
10
+ //# sourceMappingURL=user-dir.js.map
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { run } from "./run.js";
1
+ export declare function run(argv: readonly string[]): Promise<number>;
2
2
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,2 +1,11 @@
1
- export { run } from "./run.js";
1
+ // The public API of @open-cr-agent/cli, a contract under the 0.x rule
2
+ // (manual: Embedding, Stability); etc/cli.api.md records it. What the
3
+ // other workspace packages need beyond this comes from
4
+ // "@open-cr-agent/cli/internal", which is not a contract.
5
+ import { defaultDeps, run as runWith } from "./run.js";
6
+ // The `ocra` command with its arguments (no node or script path), writing
7
+ // to stdout and stderr; resolves to the exit code.
8
+ export function run(argv) {
9
+ return runWith([...argv], process.stdout, process.stderr, defaultDeps());
10
+ }
2
11
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ export type { ReviewDeps } from "./commands/review/deps.js";
2
+ export { type CliPlanOutput, cliPlanOutputSchema, planJsonSchema, } from "./commands/review/plan-schema.js";
3
+ export { configJsonSchema } from "./config/schema.js";
4
+ export type { Output } from "./io/output.js";
5
+ export { run } from "./run.js";
6
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1,7 @@
1
+ // "@open-cr-agent/cli/internal": what ocra's own packages share beyond the
2
+ // public API (index.ts), such as the command with its dependencies
3
+ // injected. Not a contract: any release may change or remove any of it.
4
+ export { cliPlanOutputSchema, planJsonSchema, } from "./commands/review/plan-schema.js";
5
+ export { configJsonSchema } from "./config/schema.js";
6
+ export { run } from "./run.js";
7
+ //# sourceMappingURL=internal.js.map
@@ -0,0 +1,9 @@
1
+ export declare const EXIT: {
2
+ readonly ok: 0;
3
+ readonly blocking: 1;
4
+ readonly notSignedIn: 1;
5
+ readonly error: 2;
6
+ readonly incomplete: 3;
7
+ readonly interrupted: 130;
8
+ };
9
+ //# sourceMappingURL=exit.d.ts.map
@@ -0,0 +1,12 @@
1
+ // Exit codes are a contract (docs/manual, "Exit codes"): every command
2
+ // returns one of these, never a bare number.
3
+ export const EXIT = {
4
+ ok: 0,
5
+ blocking: 1,
6
+ // ocra login, logout and whoami: no usable ocra Cloud session.
7
+ notSignedIn: 1,
8
+ error: 2,
9
+ incomplete: 3,
10
+ interrupted: 130,
11
+ };
12
+ //# sourceMappingURL=exit.js.map
@@ -0,0 +1,4 @@
1
+ export interface Output {
2
+ write(chunk: string): unknown;
3
+ }
4
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1,5 @@
1
+ import { OcraError } from "@open-cr-agent/core";
2
+ export declare class UsageError extends OcraError {
3
+ constructor(message: string);
4
+ }
5
+ //# sourceMappingURL=usage-error.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { OcraError } from "@open-cr-agent/core";
2
+ export class UsageError extends OcraError {
3
+ constructor(message) {
4
+ super("INPUT_USAGE", message);
5
+ this.name = "UsageError";
6
+ }
7
+ }
8
+ //# sourceMappingURL=usage-error.js.map
package/dist/main.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { run } from "./run.js";
3
- process.exitCode = await run(process.argv.slice(2), process.stdout, process.stderr);
2
+ import { run } from "./index.js";
3
+ process.exitCode = await run(process.argv.slice(2));
4
4
  //# sourceMappingURL=main.js.map
@@ -10,7 +10,7 @@ export declare function parseAccountPlugins(body: unknown, warn: (message: strin
10
10
  * never with --no-repo-config); a package the configuration already lists
11
11
  * loads as the configuration's, and a plugin whose name is taken is skipped.
12
12
  */
13
- export declare function accountPlugins(account: AccountPlugins, options: {
13
+ export declare function loadAccountPlugins(account: AccountPlugins, options: {
14
14
  allowed: boolean;
15
15
  configured: readonly string[];
16
16
  taken: readonly OcraPlugin[];
@@ -20,4 +20,4 @@ export declare function accountPlugins(account: AccountPlugins, options: {
20
20
  plugins: OcraPlugin[];
21
21
  settings: Record<string, unknown>;
22
22
  }>;
23
- //# sourceMappingURL=account-plugins.d.ts.map
23
+ //# sourceMappingURL=account.d.ts.map
@@ -1,5 +1,5 @@
1
- import { isPackageName } from "../plugin-store.js";
2
- import { loadAccountPlugins } from "./plugins.js";
1
+ import { loadAllowedPlugins } from "./load.js";
2
+ import { isPackageName } from "./store.js";
3
3
  const MAX_PLUGINS = 50;
4
4
  export function parseAccountPlugins(body, warn) {
5
5
  const settings = record(record(body).settings);
@@ -22,7 +22,7 @@ export function parseAccountPlugins(body, warn) {
22
22
  * never with --no-repo-config); a package the configuration already lists
23
23
  * loads as the configuration's, and a plugin whose name is taken is skipped.
24
24
  */
25
- export async function accountPlugins(account, options) {
25
+ export async function loadAccountPlugins(account, options) {
26
26
  const names = account.plugins.filter((name) => !options.configured.includes(name));
27
27
  if (names.length === 0)
28
28
  return { plugins: [], settings: {} };
@@ -33,15 +33,15 @@ export async function accountPlugins(account, options) {
33
33
  const taken = new Set(options.taken.map((p) => p.name));
34
34
  const plugins = [];
35
35
  const settings = {};
36
- for (const { name, plugin } of await loadAccountPlugins(names, options.dir, options.warn)) {
36
+ for (const { name, plugin } of await loadAllowedPlugins(names, options.dir, options.warn)) {
37
37
  if (taken.has(plugin.name)) {
38
38
  options.warn(`skipping plugin ${name} from your ocra Cloud settings: a plugin named "${plugin.name}" is already loaded`);
39
39
  continue;
40
40
  }
41
41
  taken.add(plugin.name);
42
42
  plugins.push(plugin);
43
- if (Object.hasOwn(account.pluginSettings, plugin.name)) {
44
- settings[plugin.name] = account.pluginSettings[plugin.name];
43
+ if (Object.hasOwn(account.pluginSettings, name)) {
44
+ settings[plugin.name] = account.pluginSettings[name];
45
45
  }
46
46
  }
47
47
  return { plugins, settings };
@@ -51,4 +51,4 @@ function record(value) {
51
51
  ? value
52
52
  : {};
53
53
  }
54
- //# sourceMappingURL=account-plugins.js.map
54
+ //# sourceMappingURL=account.js.map
@@ -6,8 +6,8 @@ export declare function loadExternalPlugins(specifiers: readonly string[], root:
6
6
  * when it was allowed. Anything else is skipped with a warning; an account
7
7
  * plugin never fails the review.
8
8
  */
9
- export declare function loadAccountPlugins(names: readonly string[], dir: string, warn: (message: string) => void): Promise<{
9
+ export declare function loadAllowedPlugins(names: readonly string[], dir: string, warn: (message: string) => void): Promise<{
10
10
  name: string;
11
11
  plugin: OcraPlugin;
12
12
  }[]>;
13
- //# sourceMappingURL=plugins.d.ts.map
13
+ //# sourceMappingURL=load.d.ts.map
@@ -2,8 +2,9 @@ import { realpath } from "node:fs/promises";
2
2
  import { createRequire } from "node:module";
3
3
  import { isAbsolute, join, resolve, sep } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { installedEntry, readAllowed } from "../plugin-store.js";
6
- import { ConfigError } from "./config.js";
5
+ import { errorMessage } from "@open-cr-agent/core";
6
+ import { ConfigError } from "../config/cli-config.js";
7
+ import { installedEntry, readAllowed } from "./store.js";
7
8
  // Plugins listed in the repository config execute code, so they are resolved
8
9
  // only from the repository's own dependencies or relative paths inside it.
9
10
  export async function loadExternalPlugins(specifiers, root) {
@@ -37,7 +38,7 @@ function isPlugin(value) {
37
38
  * when it was allowed. Anything else is skipped with a warning; an account
38
39
  * plugin never fails the review.
39
40
  */
40
- export async function loadAccountPlugins(names, dir, warn) {
41
+ export async function loadAllowedPlugins(names, dir, warn) {
41
42
  const allowed = new Map((await readAllowed(dir)).map((p) => [p.name, p]));
42
43
  const missing = names.filter((name) => !allowed.has(name));
43
44
  if (missing.length > 0) {
@@ -60,7 +61,7 @@ export async function loadAccountPlugins(names, dir, warn) {
60
61
  loaded.push({ name, plugin });
61
62
  }
62
63
  catch (error) {
63
- warn(`skipping plugin ${name} from your ocra Cloud settings: ${error.message}`);
64
+ warn(`skipping plugin ${name} from your ocra Cloud settings: ${errorMessage(error)}`);
64
65
  }
65
66
  }
66
67
  return loaded;
@@ -74,4 +75,4 @@ async function allowedEntry(dir, name) {
74
75
  throw new Error(`it resolves outside ${dir}`);
75
76
  return entry;
76
77
  }
77
- //# sourceMappingURL=plugins.js.map
78
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Runs npm with these arguments in `cwd`, never through a shell; resolves
3
+ * with its standard output. Every call runs in the plugin directory, so the
4
+ * npm configuration of whatever project ocra was started in does not apply.
5
+ */
6
+ export type NpmRunner = (args: readonly string[], cwd: string) => Promise<string>;
7
+ export declare function defaultNpm(env?: NodeJS.ProcessEnv): NpmRunner;
8
+ //# sourceMappingURL=npm.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { execFile } from "node:child_process";
2
+ import { dirname, join } from "node:path";
3
+ import { ConfigError } from "../config/cli-config.js";
4
+ import { forTerminal } from "../io/terminal.js";
5
+ // npm on Windows is npm.cmd, which execFile cannot run without a shell; run
6
+ // the npm that ships with this Node directly instead.
7
+ export function defaultNpm(env = process.env) {
8
+ const [file, prefix] = process.platform === "win32"
9
+ ? [
10
+ process.execPath,
11
+ [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")],
12
+ ]
13
+ : ["npm", []];
14
+ return (args, cwd) => new Promise((resolve, reject) => {
15
+ execFile(file, [...prefix, ...args], { cwd, env, timeout: 5 * 60_000, maxBuffer: 16 * 1024 * 1024, shell: false }, (error, stdout, stderr) => {
16
+ if (error) {
17
+ reject(new ConfigError(`npm ${args[0]} failed: ${forTerminal(String(stderr).trim().split("\n").slice(-3).join(" ") || error.message)}`));
18
+ }
19
+ else
20
+ resolve(String(stdout));
21
+ });
22
+ });
23
+ }
24
+ //# sourceMappingURL=npm.js.map