@oh-my-pi/pi-coding-agent 14.5.7 → 14.5.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.
- package/CHANGELOG.md +21 -0
- package/package.json +7 -7
- package/src/config/model-registry.ts +23 -1
- package/src/config/settings-schema.ts +23 -0
- package/src/modes/components/settings-defs.ts +10 -0
- package/src/modes/controllers/event-controller.ts +14 -9
- package/src/modes/controllers/input-controller.ts +6 -0
- package/src/modes/interactive-mode.ts +15 -1
- package/src/modes/types.ts +1 -0
- package/src/prompts/tools/run-command.md +16 -0
- package/src/tools/bash.ts +149 -115
- package/src/tools/index.ts +11 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/run-command/index.ts +80 -0
- package/src/tools/run-command/render.ts +18 -0
- package/src/tools/run-command/runner.ts +198 -0
- package/src/tools/run-command/runners/cargo.ts +131 -0
- package/src/tools/run-command/runners/index.ts +8 -0
- package/src/tools/run-command/runners/just.ts +73 -0
- package/src/tools/run-command/runners/make.ts +101 -0
- package/src/tools/run-command/runners/pkg.ts +195 -0
- package/src/tools/run-command/runners/task.ts +72 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { $which, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import type { DetectedRunner, RunnerTask, TaskRunner } from "../runner";
|
|
5
|
+
|
|
6
|
+
interface PackageJsonInfo {
|
|
7
|
+
name?: string;
|
|
8
|
+
scripts: string[];
|
|
9
|
+
workspaces: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PackageCommandInfo {
|
|
13
|
+
rootCommandPrefix: string;
|
|
14
|
+
workspaceCommandPrefix: (relativeDir: string) => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function shellQuote(value: string): string {
|
|
22
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function isFile(filePath: string): Promise<boolean> {
|
|
26
|
+
try {
|
|
27
|
+
const stat = await fs.stat(filePath);
|
|
28
|
+
return stat.isFile();
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (isEnoent(err)) return false;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function resolvePackageRunner(cwd: string): Promise<PackageCommandInfo> {
|
|
36
|
+
if ((await isFile(path.join(cwd, "bun.lock"))) || (await isFile(path.join(cwd, "bun.lockb")))) {
|
|
37
|
+
return {
|
|
38
|
+
rootCommandPrefix: "bun run",
|
|
39
|
+
workspaceCommandPrefix: relativeDir => `bun --cwd ${shellQuote(relativeDir)} run`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (await isFile(path.join(cwd, "pnpm-lock.yaml"))) {
|
|
43
|
+
return {
|
|
44
|
+
rootCommandPrefix: "pnpm run",
|
|
45
|
+
workspaceCommandPrefix: relativeDir => `pnpm --dir ${shellQuote(relativeDir)} run`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (await isFile(path.join(cwd, "yarn.lock"))) {
|
|
49
|
+
return {
|
|
50
|
+
rootCommandPrefix: "yarn",
|
|
51
|
+
workspaceCommandPrefix: relativeDir => `yarn --cwd ${shellQuote(relativeDir)}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if ((await isFile(path.join(cwd, "package-lock.json"))) || (await isFile(path.join(cwd, "npm-shrinkwrap.json")))) {
|
|
55
|
+
return {
|
|
56
|
+
rootCommandPrefix: "npm run",
|
|
57
|
+
workspaceCommandPrefix: relativeDir => `npm --prefix ${shellQuote(relativeDir)} run`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if ($which("bun")) {
|
|
61
|
+
return {
|
|
62
|
+
rootCommandPrefix: "bun run",
|
|
63
|
+
workspaceCommandPrefix: relativeDir => `bun --cwd ${shellQuote(relativeDir)} run`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
rootCommandPrefix: "npm run",
|
|
68
|
+
workspaceCommandPrefix: relativeDir => `npm --prefix ${shellQuote(relativeDir)} run`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseWorkspacePatterns(pkg: Record<string, unknown>): string[] {
|
|
73
|
+
const { workspaces } = pkg;
|
|
74
|
+
if (Array.isArray(workspaces)) return workspaces.filter((entry): entry is string => typeof entry === "string");
|
|
75
|
+
if (isRecord(workspaces) && Array.isArray(workspaces.packages)) {
|
|
76
|
+
return workspaces.packages.filter((entry): entry is string => typeof entry === "string");
|
|
77
|
+
}
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeWorkspacePattern(pattern: string): string {
|
|
82
|
+
const negated = pattern.startsWith("!");
|
|
83
|
+
const body = negated ? pattern.slice(1) : pattern;
|
|
84
|
+
const normalizedBody = body.endsWith("package.json") ? body : `${body.replace(/\/+$/u, "")}/package.json`;
|
|
85
|
+
return negated ? `!${normalizedBody}` : normalizedBody;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readPackageJson(filePath: string): Promise<PackageJsonInfo | null> {
|
|
89
|
+
try {
|
|
90
|
+
const pkg = (await Bun.file(filePath).json()) as unknown;
|
|
91
|
+
if (!isRecord(pkg)) return null;
|
|
92
|
+
const scripts = isRecord(pkg.scripts)
|
|
93
|
+
? Object.entries(pkg.scripts)
|
|
94
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[0].length > 0)
|
|
95
|
+
.map(([name]) => name)
|
|
96
|
+
: [];
|
|
97
|
+
const name = typeof pkg.name === "string" && pkg.name.length > 0 ? pkg.name : undefined;
|
|
98
|
+
return { name, scripts, workspaces: parseWorkspacePatterns(pkg) };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (!isEnoent(err)) {
|
|
101
|
+
logger.debug("package.json script detection failed", {
|
|
102
|
+
error: err instanceof Error ? err.message : String(err),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function findWorkspacePackageJsons(cwd: string, patterns: string[]): Promise<string[]> {
|
|
110
|
+
const includePatterns = patterns.filter(pattern => !pattern.startsWith("!")).map(normalizeWorkspacePattern);
|
|
111
|
+
const excludePatterns = patterns.filter(pattern => pattern.startsWith("!")).map(normalizeWorkspacePattern);
|
|
112
|
+
const excluded = new Set<string>();
|
|
113
|
+
for (const pattern of excludePatterns) {
|
|
114
|
+
for await (const entry of new Bun.Glob(pattern.slice(1)).scan({ cwd, onlyFiles: true })) {
|
|
115
|
+
excluded.add(path.normalize(String(entry)));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const files = new Set<string>();
|
|
119
|
+
for (const pattern of includePatterns) {
|
|
120
|
+
for await (const entry of new Bun.Glob(pattern).scan({ cwd, onlyFiles: true })) {
|
|
121
|
+
const normalized = path.normalize(String(entry));
|
|
122
|
+
if (normalized !== "package.json" && !excluded.has(normalized)) files.add(normalized);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return [...files].sort((left, right) => left.localeCompare(right));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function packageTaskName(packageName: string | undefined, packageDir: string, scriptName: string): string {
|
|
129
|
+
return `${packageName ?? packageDir}/${scriptName}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function tasksForPackage(options: {
|
|
133
|
+
pkg: PackageJsonInfo;
|
|
134
|
+
packageDir: string;
|
|
135
|
+
commandPrefix: string;
|
|
136
|
+
namespaced: boolean;
|
|
137
|
+
}): RunnerTask[] {
|
|
138
|
+
return options.pkg.scripts.map(scriptName => ({
|
|
139
|
+
name: options.namespaced ? packageTaskName(options.pkg.name, options.packageDir, scriptName) : scriptName,
|
|
140
|
+
doc: options.namespaced ? options.packageDir : undefined,
|
|
141
|
+
parameters: [],
|
|
142
|
+
commandPrefix: options.commandPrefix,
|
|
143
|
+
commandName: shellQuote(scriptName),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function readPackageTasks(cwd: string, commandInfo: PackageCommandInfo): Promise<RunnerTask[] | null> {
|
|
148
|
+
const rootPkg = await readPackageJson(path.join(cwd, "package.json"));
|
|
149
|
+
if (!rootPkg) return null;
|
|
150
|
+
const workspacePackageJsons = await findWorkspacePackageJsons(cwd, rootPkg.workspaces);
|
|
151
|
+
const tasks: RunnerTask[] = [];
|
|
152
|
+
|
|
153
|
+
if (rootPkg.scripts.length > 0) {
|
|
154
|
+
tasks.push(
|
|
155
|
+
...tasksForPackage({
|
|
156
|
+
pkg: rootPkg,
|
|
157
|
+
packageDir: ".",
|
|
158
|
+
commandPrefix: commandInfo.rootCommandPrefix,
|
|
159
|
+
namespaced: false,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const packageJsonPath of workspacePackageJsons) {
|
|
165
|
+
const pkg = await readPackageJson(path.join(cwd, packageJsonPath));
|
|
166
|
+
if (!pkg || pkg.scripts.length === 0) continue;
|
|
167
|
+
const packageDir = path.dirname(packageJsonPath);
|
|
168
|
+
tasks.push(
|
|
169
|
+
...tasksForPackage({
|
|
170
|
+
pkg,
|
|
171
|
+
packageDir,
|
|
172
|
+
commandPrefix: commandInfo.workspaceCommandPrefix(packageDir),
|
|
173
|
+
namespaced: true,
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return tasks.length > 0 ? tasks : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const pkgRunner: TaskRunner = {
|
|
182
|
+
id: "pkg",
|
|
183
|
+
label: "Pkg",
|
|
184
|
+
async detect(cwd: string): Promise<DetectedRunner | null> {
|
|
185
|
+
try {
|
|
186
|
+
const commandInfo = await resolvePackageRunner(cwd);
|
|
187
|
+
const tasks = await readPackageTasks(cwd, commandInfo);
|
|
188
|
+
if (!tasks || tasks.length === 0) return null;
|
|
189
|
+
return { id: "pkg", label: "Pkg", commandPrefix: commandInfo.rootCommandPrefix, tasks };
|
|
190
|
+
} catch (err) {
|
|
191
|
+
logger.debug("package runner probe failed", { error: err instanceof Error ? err.message : String(err) });
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { $which, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import type { DetectedRunner, RunnerTask, TaskRunner } from "../runner";
|
|
5
|
+
|
|
6
|
+
interface TaskListEntry {
|
|
7
|
+
name?: string;
|
|
8
|
+
desc?: string;
|
|
9
|
+
summary?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TaskListJson {
|
|
13
|
+
tasks?: TaskListEntry[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TASKFILE_NAMES = ["Taskfile.yml", "Taskfile.yaml"] as const;
|
|
17
|
+
|
|
18
|
+
async function hasTaskfile(cwd: string): Promise<boolean> {
|
|
19
|
+
for (const name of TASKFILE_NAMES) {
|
|
20
|
+
try {
|
|
21
|
+
const stat = await fs.stat(path.join(cwd, name));
|
|
22
|
+
if (stat.isFile()) return true;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (!isEnoent(err)) throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function listTaskfileTasks(cwd: string): Promise<RunnerTask[] | null> {
|
|
31
|
+
try {
|
|
32
|
+
const proc = Bun.spawn(["task", "--list-all", "--json"], {
|
|
33
|
+
cwd,
|
|
34
|
+
stdin: "ignore",
|
|
35
|
+
stdout: "pipe",
|
|
36
|
+
stderr: "pipe",
|
|
37
|
+
});
|
|
38
|
+
const [stdout, exit] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
39
|
+
if (exit !== 0) return null;
|
|
40
|
+
const list = JSON.parse(stdout) as TaskListJson;
|
|
41
|
+
const tasks = (list.tasks ?? [])
|
|
42
|
+
.filter(
|
|
43
|
+
(task): task is TaskListEntry & { name: string } => typeof task.name === "string" && task.name.length > 0,
|
|
44
|
+
)
|
|
45
|
+
.map(task => {
|
|
46
|
+
const desc = typeof task.desc === "string" && task.desc.length > 0 ? task.desc : undefined;
|
|
47
|
+
const summary = typeof task.summary === "string" && task.summary.length > 0 ? task.summary : undefined;
|
|
48
|
+
return { name: task.name, doc: desc ?? summary, parameters: [] };
|
|
49
|
+
});
|
|
50
|
+
return tasks.length > 0 ? tasks : null;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
logger.debug("task runner list failed", { error: err instanceof Error ? err.message : String(err) });
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const taskRunner: TaskRunner = {
|
|
58
|
+
id: "task",
|
|
59
|
+
label: "Task",
|
|
60
|
+
async detect(cwd: string): Promise<DetectedRunner | null> {
|
|
61
|
+
try {
|
|
62
|
+
if (!$which("task")) return null;
|
|
63
|
+
if (!(await hasTaskfile(cwd))) return null;
|
|
64
|
+
const tasks = await listTaskfileTasks(cwd);
|
|
65
|
+
if (!tasks || tasks.length === 0) return null;
|
|
66
|
+
return { id: "task", label: "Task", commandPrefix: "task", tasks };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
logger.debug("task runner probe failed", { error: err instanceof Error ? err.message : String(err) });
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|