@henryqw/pi-open-in 0.2.8 → 1.0.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.
- package/README.md +19 -4
- package/extensions/open.ts +29 -32
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ pi install npm:@henryqw/pi-open-in
|
|
|
22
22
|
|
|
23
23
|
## Config
|
|
24
24
|
|
|
25
|
-
`~/.pi/agent/config/pi-open-in.json`
|
|
25
|
+
`~/.pi/agent/config/pi-open-in/config.json`
|
|
26
26
|
|
|
27
27
|
```json
|
|
28
28
|
{
|
|
@@ -30,10 +30,25 @@ pi install npm:@henryqw/pi-open-in
|
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
-
|
|
33
|
+
- A missing file silently uses the default command, `"code"`.
|
|
34
|
+
- Reads do not create or write the config home.
|
|
34
35
|
- When the file exists, `command` is required. It must be a non-empty string.
|
|
35
36
|
- The command splits on whitespace into an executable and arguments. Tokens cannot contain spaces, and quoting is unsupported. Use a wrapper script for executables in spaced paths.
|
|
36
37
|
- An existing file must be a JSON object with exactly one non-empty string `command` property. Otherwise `/open` fails with a visible error and offers no open URI.
|
|
37
|
-
-
|
|
38
|
+
- Malformed files remain unchanged.
|
|
39
|
+
- Only `/set-open-in` writes the file. Its write is atomic.
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
## Owner API
|
|
42
|
+
|
|
43
|
+
Consumers use the owner API instead of reading this file.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { loadOpenInConfig } from "@henryqw/pi-open-in/open-uri";
|
|
47
|
+
|
|
48
|
+
const { source, value } = loadOpenInConfig();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`source` is `"missing"` or `"file"`. `value.command` is validated.
|
|
52
|
+
Pass an agent directory to `loadOpenInConfig(agentDir)` when needed.
|
|
53
|
+
|
|
54
|
+
This extension owns command validation. `@henryqw/pi-config-store` owns the config home and storage.
|
package/extensions/open.ts
CHANGED
|
@@ -1,41 +1,38 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
1
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
type ExtensionAPI,
|
|
8
|
-
} from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { createConfigStore } from "@henryqw/pi-config-store";
|
|
9
4
|
|
|
10
5
|
const DEFAULT_COMMAND = "code";
|
|
11
|
-
const configPath = () => join(getAgentDir(), "config", "pi-open-in.json");
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Only a missing file falls back to the default command.
|
|
19
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return DEFAULT_COMMAND;
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
let config: unknown;
|
|
23
|
-
try {
|
|
24
|
-
config = JSON.parse(raw);
|
|
25
|
-
} catch {
|
|
26
|
-
throw new Error(`Invalid ${configPath()}: not valid JSON`);
|
|
7
|
+
export type OpenInConfig = { command: string };
|
|
8
|
+
|
|
9
|
+
function parseOpenInConfig(value: unknown): OpenInConfig {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
11
|
+
throw new Error('Invalid open-in config: expected exactly one non-empty string "command" property');
|
|
27
12
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
throw new Error(`Invalid ${configPath()}: expected exactly one non-empty string "command" property`);
|
|
13
|
+
const config = value as Record<string, unknown>;
|
|
14
|
+
if (Object.keys(config).length !== 1 || typeof config.command !== "string" || !config.command.trim()) {
|
|
15
|
+
throw new Error('Invalid open-in config: expected exactly one non-empty string "command" property');
|
|
32
16
|
}
|
|
33
|
-
return config.command.trim();
|
|
17
|
+
return { command: config.command.trim() };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createOpenInConfigStore(agentDir?: string) {
|
|
21
|
+
return createConfigStore<OpenInConfig>({
|
|
22
|
+
extensionId: "pi-open-in",
|
|
23
|
+
agentDir,
|
|
24
|
+
defaults: () => ({ command: DEFAULT_COMMAND }),
|
|
25
|
+
parse: parseOpenInConfig,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadOpenInConfig(agentDir?: string): { source: "file" | "missing"; value: OpenInConfig } {
|
|
30
|
+
return createOpenInConfigStore(agentDir).loadSync();
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
export function configuredOpenUri(path: string): string | undefined {
|
|
37
34
|
try {
|
|
38
|
-
if (
|
|
35
|
+
if (loadOpenInConfig().value.command !== "code") return undefined;
|
|
39
36
|
} catch {
|
|
40
37
|
// Invalid config must not break footer render; just omit the URI.
|
|
41
38
|
return undefined;
|
|
@@ -50,13 +47,15 @@ export function configuredOpenUri(path: string): string | undefined {
|
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
export default function openInExtension(pi: ExtensionAPI): void {
|
|
50
|
+
const configStore = createOpenInConfigStore();
|
|
51
|
+
|
|
53
52
|
// ponytail: static description so it never goes stale after /set-open-in
|
|
54
53
|
// (handler re-reads config per invocation); per-token whitespace splitting,
|
|
55
54
|
// tokens with spaces not supported.
|
|
56
55
|
pi.registerCommand("open", {
|
|
57
56
|
description: "Open the current path with the configured command",
|
|
58
57
|
handler: async (_args, ctx) => {
|
|
59
|
-
const [executable, ...args] =
|
|
58
|
+
const [executable, ...args] = configStore.loadSync().value.command.split(/\s+/);
|
|
60
59
|
const result = await pi.exec(executable, [...args, ctx.cwd]);
|
|
61
60
|
if (result.code !== 0) {
|
|
62
61
|
throw new Error(`Open command failed: ${result.stderr.trim() || `exit code ${result.code}`}`);
|
|
@@ -69,9 +68,7 @@ export default function openInExtension(pi: ExtensionAPI): void {
|
|
|
69
68
|
handler: async (args, ctx) => {
|
|
70
69
|
const command = args.trim();
|
|
71
70
|
if (!command) throw new Error("Usage: /set-open-in <command>");
|
|
72
|
-
|
|
73
|
-
await mkdir(dirname(path), { recursive: true });
|
|
74
|
-
await writeFile(path, `${JSON.stringify({ command }, null, 2)}\n`, "utf8");
|
|
71
|
+
await configStore.save({ command });
|
|
75
72
|
ctx.ui.notify(`Saved open-in command: ${command}`, "info");
|
|
76
73
|
},
|
|
77
74
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-open-in",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Open the current Pi working directory with a configurable command.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -45,5 +45,8 @@
|
|
|
45
45
|
"extensions": [
|
|
46
46
|
"./extensions"
|
|
47
47
|
]
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@henryqw/pi-config-store": "^0.1.0"
|
|
48
51
|
}
|
|
49
52
|
}
|