@zhijiewang/openharness 2.21.0 → 2.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/harness/api-key-helper.d.ts +32 -0
- package/dist/harness/api-key-helper.js +70 -0
- package/dist/harness/config.d.ts +25 -0
- package/dist/harness/credentials.d.ts +6 -4
- package/dist/harness/credentials.js +15 -4
- package/dist/harness/hooks.d.ts +11 -1
- package/dist/harness/hooks.js +10 -0
- package/dist/lsp/client.d.ts +9 -0
- package/dist/lsp/client.js +50 -0
- package/dist/main.js +176 -63
- package/dist/mcp/elicitation.d.ts +66 -0
- package/dist/mcp/elicitation.js +88 -0
- package/dist/mcp/roots.d.ts +36 -0
- package/dist/mcp/roots.js +56 -0
- package/dist/mcp/transport.js +45 -3
- package/dist/providers/index.d.ts +25 -1
- package/dist/providers/index.js +27 -2
- package/dist/query/index.js +1 -1
- package/dist/query/tools.d.ts +2 -2
- package/dist/query/tools.js +68 -4
- package/dist/query/types.d.ts +10 -0
- package/dist/tools/DiagnosticsTool/index.js +10 -22
- package/dist/utils/install-method.d.ts +42 -0
- package/dist/utils/install-method.js +110 -0
- package/package.json +1 -1
|
@@ -4,11 +4,35 @@
|
|
|
4
4
|
import type { Provider, ProviderConfig } from "./base.js";
|
|
5
5
|
/**
|
|
6
6
|
* Create a provider from a model string like "ollama/llama3" or "gpt-4o".
|
|
7
|
+
*
|
|
8
|
+
* `opts.fallbackModel` (audit B2) is the CLI override path for the existing
|
|
9
|
+
* `fallbackProviders` config — when set, REPLACES the config-file fallbacks
|
|
10
|
+
* with a single entry derived from the model string. Mirrors Claude Code's
|
|
11
|
+
* `--fallback-model <model>` for one-shot CI runs that want a fallback
|
|
12
|
+
* without editing `.oh/config.yaml`. Format matches `modelArg`:
|
|
13
|
+
* `provider/model` or just `model` (provider guessed). When unset, the
|
|
14
|
+
* existing config-file path is unchanged.
|
|
7
15
|
*/
|
|
8
|
-
export declare function createProvider(modelArg?: string, overrides?: Partial<ProviderConfig
|
|
16
|
+
export declare function createProvider(modelArg?: string, overrides?: Partial<ProviderConfig>, opts?: {
|
|
17
|
+
fallbackModel?: string;
|
|
18
|
+
}): Promise<{
|
|
9
19
|
provider: Provider;
|
|
10
20
|
model: string;
|
|
11
21
|
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Parse `--fallback-model <value>` into the same shape as a `fallbackProviders[]`
|
|
24
|
+
* entry. Accepts `provider/model` (explicit) or just `model` (provider guessed
|
|
25
|
+
* via `guessProviderFromModel`, same as the primary modelArg). Exposed for
|
|
26
|
+
* tests.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseFallbackModel(raw: string): {
|
|
31
|
+
provider: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
baseUrl?: string;
|
|
35
|
+
};
|
|
12
36
|
export { createProviderInstance, guessProviderFromModel };
|
|
13
37
|
declare function createProviderInstance(name: string, config: ProviderConfig): Provider;
|
|
14
38
|
declare function guessProviderFromModel(model: string): string;
|
package/dist/providers/index.js
CHANGED
|
@@ -10,8 +10,16 @@ import { OpenAIProvider } from "./openai.js";
|
|
|
10
10
|
import { OpenRouterProvider } from "./openrouter.js";
|
|
11
11
|
/**
|
|
12
12
|
* Create a provider from a model string like "ollama/llama3" or "gpt-4o".
|
|
13
|
+
*
|
|
14
|
+
* `opts.fallbackModel` (audit B2) is the CLI override path for the existing
|
|
15
|
+
* `fallbackProviders` config — when set, REPLACES the config-file fallbacks
|
|
16
|
+
* with a single entry derived from the model string. Mirrors Claude Code's
|
|
17
|
+
* `--fallback-model <model>` for one-shot CI runs that want a fallback
|
|
18
|
+
* without editing `.oh/config.yaml`. Format matches `modelArg`:
|
|
19
|
+
* `provider/model` or just `model` (provider guessed). When unset, the
|
|
20
|
+
* existing config-file path is unchanged.
|
|
13
21
|
*/
|
|
14
|
-
export async function createProvider(modelArg, overrides) {
|
|
22
|
+
export async function createProvider(modelArg, overrides, opts = {}) {
|
|
15
23
|
let providerName = "ollama";
|
|
16
24
|
let model = "llama3";
|
|
17
25
|
if (modelArg) {
|
|
@@ -32,7 +40,9 @@ export async function createProvider(modelArg, overrides) {
|
|
|
32
40
|
...overrides,
|
|
33
41
|
};
|
|
34
42
|
const primary = createProviderInstance(providerName, config);
|
|
35
|
-
const fallbackCfgs =
|
|
43
|
+
const fallbackCfgs = opts.fallbackModel
|
|
44
|
+
? [parseFallbackModel(opts.fallbackModel)]
|
|
45
|
+
: (readOhConfig()?.fallbackProviders ?? []);
|
|
36
46
|
if (fallbackCfgs.length === 0) {
|
|
37
47
|
return { provider: primary, model };
|
|
38
48
|
}
|
|
@@ -48,6 +58,21 @@ export async function createProvider(modelArg, overrides) {
|
|
|
48
58
|
const wrapped = createFallbackProvider(primary, fallbacks);
|
|
49
59
|
return { provider: wrapped, model };
|
|
50
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse `--fallback-model <value>` into the same shape as a `fallbackProviders[]`
|
|
63
|
+
* entry. Accepts `provider/model` (explicit) or just `model` (provider guessed
|
|
64
|
+
* via `guessProviderFromModel`, same as the primary modelArg). Exposed for
|
|
65
|
+
* tests.
|
|
66
|
+
*
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
export function parseFallbackModel(raw) {
|
|
70
|
+
if (raw.includes("/")) {
|
|
71
|
+
const [p, m] = raw.split("/", 2);
|
|
72
|
+
return { provider: p, model: m };
|
|
73
|
+
}
|
|
74
|
+
return { provider: guessProviderFromModel(raw), model: raw };
|
|
75
|
+
}
|
|
51
76
|
export { createProviderInstance, guessProviderFromModel };
|
|
52
77
|
function createProviderInstance(name, config) {
|
|
53
78
|
switch (name) {
|
package/dist/query/index.js
CHANGED
|
@@ -311,7 +311,7 @@ export async function* query(userMessage, config, existingMessages = []) {
|
|
|
311
311
|
// Execute remaining tools not started during streaming
|
|
312
312
|
const remaining = toolCalls.filter((tc) => !executedIds.has(tc.id));
|
|
313
313
|
if (remaining.length > 0) {
|
|
314
|
-
yield* executeToolCalls(remaining, config.tools, toolContext, config.permissionMode, config.askUser, state);
|
|
314
|
+
yield* executeToolCalls(remaining, config.tools, toolContext, config.permissionMode, config.askUser, state, config.permissionPromptTool);
|
|
315
315
|
}
|
|
316
316
|
state.lastTurnHadTools = toolCalls.length > 0;
|
|
317
317
|
state.lastTurnToolCount = toolCalls.length;
|
package/dist/query/tools.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ type Batch = {
|
|
|
11
11
|
calls: ToolCall[];
|
|
12
12
|
};
|
|
13
13
|
export declare function partitionToolCalls(toolCalls: ToolCall[], tools: Tools): Batch[];
|
|
14
|
-
export declare function executeSingleTool(toolCall: ToolCall, tools: Tools, context: ToolContext, permissionMode: PermissionMode, askUser?: AskUserFn): Promise<ToolResult>;
|
|
15
|
-
export declare function executeToolCalls(toolCalls: ToolCall[], tools: Tools, context: ToolContext, permissionMode: PermissionMode, askUser?: AskUserFn, state?: QueryLoopState): AsyncGenerator<StreamEvent, void>;
|
|
14
|
+
export declare function executeSingleTool(toolCall: ToolCall, tools: Tools, context: ToolContext, permissionMode: PermissionMode, askUser?: AskUserFn, permissionPromptTool?: string): Promise<ToolResult>;
|
|
15
|
+
export declare function executeToolCalls(toolCalls: ToolCall[], tools: Tools, context: ToolContext, permissionMode: PermissionMode, askUser?: AskUserFn, state?: QueryLoopState, permissionPromptTool?: string): AsyncGenerator<StreamEvent, void>;
|
|
16
16
|
export {};
|
|
17
17
|
//# sourceMappingURL=tools.d.ts.map
|
package/dist/query/tools.js
CHANGED
|
@@ -8,6 +8,42 @@ import { createToolResultMessage } from "../types/message.js";
|
|
|
8
8
|
import { checkPermission } from "../types/permissions.js";
|
|
9
9
|
const MAX_TOOL_RESULT_CHARS = 100_000;
|
|
10
10
|
const TOOL_TIMEOUT_MS = 120_000;
|
|
11
|
+
/**
|
|
12
|
+
* Invoke the configured `--permission-prompt-tool` (audit B1). The tool is
|
|
13
|
+
* looked up by name in the active tool registry (so MCP tools wired through
|
|
14
|
+
* `loadMcpTools` are reachable). Failure modes — missing tool, exception
|
|
15
|
+
* during call, malformed JSON, unknown `behavior` — collapse into
|
|
16
|
+
* `behavior: "fallthrough"` so the caller can try the next branch
|
|
17
|
+
* (interactive prompt or headless deny). A broken permission tool must
|
|
18
|
+
* not lock the user out.
|
|
19
|
+
*/
|
|
20
|
+
async function callPermissionPromptTool(toolName, tools, context, permissionedToolName, permissionedInput) {
|
|
21
|
+
const promptTool = findToolByName(tools, toolName);
|
|
22
|
+
if (!promptTool)
|
|
23
|
+
return { behavior: "fallthrough" };
|
|
24
|
+
let raw;
|
|
25
|
+
try {
|
|
26
|
+
raw = await promptTool.call({ tool_name: permissionedToolName, input: permissionedInput }, context);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { behavior: "fallthrough" };
|
|
30
|
+
}
|
|
31
|
+
if (raw.isError)
|
|
32
|
+
return { behavior: "fallthrough" };
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(raw.output);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { behavior: "fallthrough" };
|
|
39
|
+
}
|
|
40
|
+
if (parsed.behavior === "allow")
|
|
41
|
+
return { behavior: "allow" };
|
|
42
|
+
if (parsed.behavior === "deny") {
|
|
43
|
+
return parsed.message ? { behavior: "deny", message: parsed.message } : { behavior: "deny" };
|
|
44
|
+
}
|
|
45
|
+
return { behavior: "fallthrough" };
|
|
46
|
+
}
|
|
11
47
|
export function partitionToolCalls(toolCalls, tools) {
|
|
12
48
|
const batches = [];
|
|
13
49
|
let currentConcurrent = [];
|
|
@@ -30,7 +66,7 @@ export function partitionToolCalls(toolCalls, tools) {
|
|
|
30
66
|
}
|
|
31
67
|
return batches;
|
|
32
68
|
}
|
|
33
|
-
export async function executeSingleTool(toolCall, tools, context, permissionMode, askUser) {
|
|
69
|
+
export async function executeSingleTool(toolCall, tools, context, permissionMode, askUser, permissionPromptTool) {
|
|
34
70
|
const tool = findToolByName(tools, toolCall.toolName);
|
|
35
71
|
if (!tool) {
|
|
36
72
|
return { output: `Error: Unknown tool '${toolCall.toolName}'`, isError: true };
|
|
@@ -72,6 +108,34 @@ export async function executeSingleTool(toolCall, tools, context, permissionMode
|
|
|
72
108
|
const reason = hookOutcome.reason ? `: ${hookOutcome.reason}` : "";
|
|
73
109
|
return denyAndEmit("hook", hookOutcome.reason ?? "hook denied", `Permission denied by hook${reason}`);
|
|
74
110
|
}
|
|
111
|
+
else if (permissionPromptTool) {
|
|
112
|
+
// No hook decision → consult the configured MCP permission tool
|
|
113
|
+
// (audit B1). Mirrors Claude Code's --permission-prompt-tool. The
|
|
114
|
+
// tool returns JSON: { "behavior": "allow" | "deny", "message"?: string }.
|
|
115
|
+
// On any failure (tool missing, throws, malformed JSON, unknown
|
|
116
|
+
// behavior) we fall through to askUser / headless deny so a broken
|
|
117
|
+
// permission tool doesn't lock the user out.
|
|
118
|
+
const promptDecision = await callPermissionPromptTool(permissionPromptTool, tools, context, tool.name, parsed.data);
|
|
119
|
+
if (promptDecision.behavior === "allow") {
|
|
120
|
+
// Permission tool granted — proceed.
|
|
121
|
+
}
|
|
122
|
+
else if (promptDecision.behavior === "deny") {
|
|
123
|
+
return denyAndEmit("permission-prompt-tool", promptDecision.message ?? "denied", `Permission denied by ${permissionPromptTool}${promptDecision.message ? `: ${promptDecision.message}` : ""}`);
|
|
124
|
+
}
|
|
125
|
+
else if (askUser) {
|
|
126
|
+
// promptDecision.behavior === "fallthrough" — tool was unavailable
|
|
127
|
+
// or its response was malformed. Try the interactive prompt next.
|
|
128
|
+
const { formatToolArgs } = await import("../utils/tool-summary.js");
|
|
129
|
+
const description = formatToolArgs(tool.name, toolCall.arguments);
|
|
130
|
+
const allowed = await askUser(tool.name, description, tool.riskLevel);
|
|
131
|
+
if (!allowed) {
|
|
132
|
+
return denyAndEmit("user", "user declined", "Permission denied by user.");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
return denyAndEmit("headless", "permission-prompt-tool unavailable and no interactive prompt", `Permission denied: ${permissionPromptTool} did not produce a usable decision and no interactive prompt is available.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
75
139
|
else if (askUser) {
|
|
76
140
|
// "ask" or no decision → interactive prompt when available
|
|
77
141
|
const { formatToolArgs } = await import("../utils/tool-summary.js");
|
|
@@ -209,7 +273,7 @@ export async function executeSingleTool(toolCall, tools, context, permissionMode
|
|
|
209
273
|
return { output: `Tool error: ${errMsg}`, isError: true };
|
|
210
274
|
}
|
|
211
275
|
}
|
|
212
|
-
export async function* executeToolCalls(toolCalls, tools, context, permissionMode, askUser, state) {
|
|
276
|
+
export async function* executeToolCalls(toolCalls, tools, context, permissionMode, askUser, state, permissionPromptTool) {
|
|
213
277
|
const batches = partitionToolCalls(toolCalls, tools);
|
|
214
278
|
const outputChunks = [];
|
|
215
279
|
const onOutputChunk = (callId, chunk) => {
|
|
@@ -218,7 +282,7 @@ export async function* executeToolCalls(toolCalls, tools, context, permissionMod
|
|
|
218
282
|
const allToolNames = toolCalls.map((tc) => tc.toolName);
|
|
219
283
|
for (const batch of batches) {
|
|
220
284
|
if (batch.concurrent) {
|
|
221
|
-
const results = await Promise.all(batch.calls.map((tc) => executeSingleTool(tc, tools, { ...context, callId: tc.id, onOutputChunk }, permissionMode, askUser)));
|
|
285
|
+
const results = await Promise.all(batch.calls.map((tc) => executeSingleTool(tc, tools, { ...context, callId: tc.id, onOutputChunk }, permissionMode, askUser, permissionPromptTool)));
|
|
222
286
|
for (const chunk of outputChunks.splice(0))
|
|
223
287
|
yield chunk;
|
|
224
288
|
for (let i = 0; i < batch.calls.length; i++) {
|
|
@@ -230,7 +294,7 @@ export async function* executeToolCalls(toolCalls, tools, context, permissionMod
|
|
|
230
294
|
}
|
|
231
295
|
else {
|
|
232
296
|
for (const tc of batch.calls) {
|
|
233
|
-
const result = await executeSingleTool(tc, tools, { ...context, callId: tc.id, onOutputChunk }, permissionMode, askUser);
|
|
297
|
+
const result = await executeSingleTool(tc, tools, { ...context, callId: tc.id, onOutputChunk }, permissionMode, askUser, permissionPromptTool);
|
|
234
298
|
for (const chunk of outputChunks.splice(0))
|
|
235
299
|
yield chunk;
|
|
236
300
|
yield { type: "tool_call_end", callId: tc.id, output: result.output, isError: result.isError };
|
package/dist/query/types.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export type QueryConfig = {
|
|
|
22
22
|
gitCommitPerTool?: boolean;
|
|
23
23
|
/** For sub-agent invocations: the agent role name (feeds into the model router). */
|
|
24
24
|
role?: string;
|
|
25
|
+
/**
|
|
26
|
+
* MCP tool name (e.g. `mcp__myperm__check`) consulted when a tool needs
|
|
27
|
+
* approval and no permission hook gave a decision (audit B1). Mirrors
|
|
28
|
+
* Claude Code's `--permission-prompt-tool`. The tool is invoked with
|
|
29
|
+
* `{ tool_name, input }` and is expected to return a JSON string with
|
|
30
|
+
* shape `{ "behavior": "allow" | "deny", "message"?: string }`. Falls
|
|
31
|
+
* through to the interactive `askUser` prompt (or headless deny) when
|
|
32
|
+
* the tool is missing, throws, or returns malformed JSON.
|
|
33
|
+
*/
|
|
34
|
+
permissionPromptTool?: string;
|
|
25
35
|
};
|
|
26
36
|
export type TransitionReason = "next_turn" | "retry_network" | "retry_prompt_too_long" | "retry_max_output_tokens";
|
|
27
37
|
export type QueryLoopState = {
|
|
@@ -44,7 +44,7 @@ async function getClient(filePath, workingDir) {
|
|
|
44
44
|
}
|
|
45
45
|
export const DiagnosticsTool = {
|
|
46
46
|
name: "Diagnostics",
|
|
47
|
-
description: "
|
|
47
|
+
description: "Language Server Protocol (LSP) code intelligence — pick action: diagnostics (errors/warnings), definition (go-to-def), references (find-refs), or hover (type info / docs). Supports TypeScript, JavaScript, Python, Go, Rust.",
|
|
48
48
|
inputSchema,
|
|
49
49
|
riskLevel: "low",
|
|
50
50
|
isReadOnly() {
|
|
@@ -101,22 +101,10 @@ export const DiagnosticsTool = {
|
|
|
101
101
|
return { output: "line and character are required for hover.", isError: true };
|
|
102
102
|
}
|
|
103
103
|
await client.openFile(input.file_path);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
position: { line: input.line, character: input.character },
|
|
109
|
-
});
|
|
110
|
-
if (!result?.contents)
|
|
111
|
-
return { output: "No hover information.", isError: false };
|
|
112
|
-
const content = typeof result.contents === "string"
|
|
113
|
-
? result.contents
|
|
114
|
-
: (result.contents.value ?? JSON.stringify(result.contents));
|
|
115
|
-
return { output: content, isError: false };
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return { output: "Hover not supported by this language server.", isError: false };
|
|
119
|
-
}
|
|
104
|
+
const content = await client.getHover(input.file_path, input.line, input.character);
|
|
105
|
+
if (!content)
|
|
106
|
+
return { output: "No hover information.", isError: false };
|
|
107
|
+
return { output: content, isError: false };
|
|
120
108
|
}
|
|
121
109
|
return { output: `Unknown action: ${input.action}`, isError: true };
|
|
122
110
|
}
|
|
@@ -128,11 +116,11 @@ export const DiagnosticsTool = {
|
|
|
128
116
|
}
|
|
129
117
|
},
|
|
130
118
|
prompt() {
|
|
131
|
-
return `
|
|
132
|
-
- diagnostics:
|
|
133
|
-
- definition:
|
|
134
|
-
- references: Find all references to
|
|
135
|
-
- hover:
|
|
119
|
+
return `LSP code intelligence — diagnostics, go-to-definition, find-references, hover. Supports TypeScript, JavaScript, Python, Go, Rust (a language server must be installed: typescript-language-server / pylsp / gopls / rust-analyzer). Actions:
|
|
120
|
+
- diagnostics: Errors and warnings for the file
|
|
121
|
+
- definition: Resolve the symbol at (line, character) to its declaration
|
|
122
|
+
- references: Find all references to the symbol at (line, character)
|
|
123
|
+
- hover: Type information and documentation for the symbol at (line, character)
|
|
136
124
|
Parameters:
|
|
137
125
|
- file_path (string, required): Absolute path to the file
|
|
138
126
|
- action (string): "diagnostics" | "definition" | "references" | "hover" (default: diagnostics)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect how the running OH CLI was installed (audit B7) so `oh update` can
|
|
3
|
+
* print the appropriate upgrade command. Pure function — `detectInstallMethod`
|
|
4
|
+
* inspects the process's own filesystem path and current working directory;
|
|
5
|
+
* exported for unit testing and reuse.
|
|
6
|
+
*
|
|
7
|
+
* Detection rules, in order:
|
|
8
|
+
* - "local-clone" → `dist/main.js` lives inside a git repo whose root is
|
|
9
|
+
* the package itself (the user is running from a clone).
|
|
10
|
+
* Suggest `git pull && npm install && npm run build`.
|
|
11
|
+
* - "npm-global" → `dist/main.js` lives under a directory containing the
|
|
12
|
+
* segment `node_modules/@zhijiewang/openharness/`. This
|
|
13
|
+
* is the standard npm global install layout. Suggest
|
|
14
|
+
* `npm install -g @zhijiewang/openharness@latest`.
|
|
15
|
+
* - "npx-cache" → `dist/main.js` lives under a path containing
|
|
16
|
+
* `_npx/` (npx caches packages there). npx auto-fetches
|
|
17
|
+
* the latest by default; suggest re-running with
|
|
18
|
+
* `@latest` to bypass cache.
|
|
19
|
+
* - "unknown" → Couldn't classify. Print all three options and let
|
|
20
|
+
* the user choose.
|
|
21
|
+
*/
|
|
22
|
+
export type InstallMethod = "local-clone" | "npm-global" | "npx-cache" | "unknown";
|
|
23
|
+
export interface InstallMethodResult {
|
|
24
|
+
method: InstallMethod;
|
|
25
|
+
/** The detected install root, mostly for diagnostics. */
|
|
26
|
+
root: string;
|
|
27
|
+
/** Multi-line user-facing message describing the upgrade command. */
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Classify the install method given the running script's filesystem path.
|
|
32
|
+
* `mainPath` defaults to `import.meta.url`-derived path in the CLI; tests
|
|
33
|
+
* override it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectInstallMethod(mainPath: string): InstallMethodResult;
|
|
36
|
+
/**
|
|
37
|
+
* Default `mainPath` resolver — walks up from `process.argv[1]` to find the
|
|
38
|
+
* package root. Exported so tests can stub it. Falls back to argv[1] verbatim
|
|
39
|
+
* when nothing matches.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getDefaultMainPath(): string;
|
|
42
|
+
//# sourceMappingURL=install-method.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect how the running OH CLI was installed (audit B7) so `oh update` can
|
|
3
|
+
* print the appropriate upgrade command. Pure function — `detectInstallMethod`
|
|
4
|
+
* inspects the process's own filesystem path and current working directory;
|
|
5
|
+
* exported for unit testing and reuse.
|
|
6
|
+
*
|
|
7
|
+
* Detection rules, in order:
|
|
8
|
+
* - "local-clone" → `dist/main.js` lives inside a git repo whose root is
|
|
9
|
+
* the package itself (the user is running from a clone).
|
|
10
|
+
* Suggest `git pull && npm install && npm run build`.
|
|
11
|
+
* - "npm-global" → `dist/main.js` lives under a directory containing the
|
|
12
|
+
* segment `node_modules/@zhijiewang/openharness/`. This
|
|
13
|
+
* is the standard npm global install layout. Suggest
|
|
14
|
+
* `npm install -g @zhijiewang/openharness@latest`.
|
|
15
|
+
* - "npx-cache" → `dist/main.js` lives under a path containing
|
|
16
|
+
* `_npx/` (npx caches packages there). npx auto-fetches
|
|
17
|
+
* the latest by default; suggest re-running with
|
|
18
|
+
* `@latest` to bypass cache.
|
|
19
|
+
* - "unknown" → Couldn't classify. Print all three options and let
|
|
20
|
+
* the user choose.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
|
+
import { dirname, join, sep } from "node:path";
|
|
24
|
+
/**
|
|
25
|
+
* Classify the install method given the running script's filesystem path.
|
|
26
|
+
* `mainPath` defaults to `import.meta.url`-derived path in the CLI; tests
|
|
27
|
+
* override it.
|
|
28
|
+
*/
|
|
29
|
+
export function detectInstallMethod(mainPath) {
|
|
30
|
+
// Normalize to forward slashes so the substring tests below work on Windows.
|
|
31
|
+
const normalized = mainPath.replace(/\\/g, "/");
|
|
32
|
+
// npx-cache: path contains `/_npx/` (Node's npx puts packages there)
|
|
33
|
+
if (normalized.includes("/_npx/")) {
|
|
34
|
+
return {
|
|
35
|
+
method: "npx-cache",
|
|
36
|
+
root: dirname(mainPath),
|
|
37
|
+
message: [
|
|
38
|
+
"You're running OH via npx (auto-fetched on each invocation).",
|
|
39
|
+
"To force the latest version on the next run, use:",
|
|
40
|
+
"",
|
|
41
|
+
" npx @zhijiewang/openharness@latest",
|
|
42
|
+
"",
|
|
43
|
+
"Or install globally to avoid the npx cache entirely:",
|
|
44
|
+
" npm install -g @zhijiewang/openharness@latest",
|
|
45
|
+
].join("\n"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// local-clone: walk up to find a package.json whose name matches AND a .git dir
|
|
49
|
+
let dir = dirname(mainPath);
|
|
50
|
+
while (dir && dir !== dirname(dir)) {
|
|
51
|
+
const pkgPath = join(dir, "package.json");
|
|
52
|
+
if (existsSync(pkgPath)) {
|
|
53
|
+
const isClone = existsSync(join(dir, ".git"));
|
|
54
|
+
if (isClone) {
|
|
55
|
+
return {
|
|
56
|
+
method: "local-clone",
|
|
57
|
+
root: dir,
|
|
58
|
+
message: [
|
|
59
|
+
`Detected a local clone at: ${dir}`,
|
|
60
|
+
"Pull the latest and rebuild:",
|
|
61
|
+
"",
|
|
62
|
+
` cd ${dir}`,
|
|
63
|
+
" git pull && npm install && npm run build",
|
|
64
|
+
].join("\n"),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// npm-global: the package.json belongs to OH and lives under a global
|
|
68
|
+
// node_modules directory.
|
|
69
|
+
if (normalized.includes("/node_modules/@zhijiewang/openharness/")) {
|
|
70
|
+
return {
|
|
71
|
+
method: "npm-global",
|
|
72
|
+
root: dir,
|
|
73
|
+
message: [
|
|
74
|
+
`Detected a global npm install at: ${dir}`,
|
|
75
|
+
"Upgrade with:",
|
|
76
|
+
"",
|
|
77
|
+
" npm install -g @zhijiewang/openharness@latest",
|
|
78
|
+
].join("\n"),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
dir = dirname(dir);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
method: "unknown",
|
|
87
|
+
root: dirname(mainPath),
|
|
88
|
+
message: [
|
|
89
|
+
"Could not determine how OH was installed. Pick the option that matches your setup:",
|
|
90
|
+
"",
|
|
91
|
+
" Global npm install: npm install -g @zhijiewang/openharness@latest",
|
|
92
|
+
" npx (one-shot): npx @zhijiewang/openharness@latest",
|
|
93
|
+
" Local clone: git pull && npm install && npm run build",
|
|
94
|
+
].join("\n"),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Default `mainPath` resolver — walks up from `process.argv[1]` to find the
|
|
99
|
+
* package root. Exported so tests can stub it. Falls back to argv[1] verbatim
|
|
100
|
+
* when nothing matches.
|
|
101
|
+
*/
|
|
102
|
+
export function getDefaultMainPath() {
|
|
103
|
+
const entry = process.argv[1] ?? "";
|
|
104
|
+
if (!entry)
|
|
105
|
+
return "";
|
|
106
|
+
// If argv[1] points at a `dist/main.js`, that's already the right anchor.
|
|
107
|
+
// Otherwise return as-is and let `detectInstallMethod` figure it out.
|
|
108
|
+
return entry.split(sep).join("/");
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=install-method.js.map
|