@henryqw/pi-open-in 0.2.7 → 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 CHANGED
@@ -1,11 +1,11 @@
1
1
  # `@henryqw/pi-open-in`
2
2
 
3
- Open the current working directory with a configurable command. Default is `code`.
3
+ Open the current working directory with a configurable editor command. The default is `code`.
4
4
 
5
5
  ## Why
6
6
 
7
- - **Created for**: Replacing manually typed editor launcher commands with one configurable action for the working directory.
8
- - **Advantage**: `/open` works while the agent is busy and supports any simple launcher command, defaulting to `code`.
7
+ - **Created for**: Replace manually typed editor commands with one configurable action for the working directory.
8
+ - **Advantage**: `/open` works while the agent is busy and supports any simple editor command, defaulting to `code`.
9
9
 
10
10
  ## Install
11
11
 
@@ -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,8 +30,25 @@ pi install npm:@henryqw/pi-open-in
30
30
  }
31
31
  ```
32
32
 
33
- | Field | Required | Possible values | Default (missing file) |
34
- | --- | --- | --- | --- |
35
- | `command` | Yes when the file exists | Non-empty string, split on whitespace into executable plus arguments; tokens cannot contain spaces (no quoting) — use a wrapper script for executables in spaced paths | `"code"` |
33
+ - A missing file silently uses the default command, `"code"`.
34
+ - Reads do not create or write the config home.
35
+ - When the file exists, `command` is required. It must be a non-empty string.
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.
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.
38
+ - Malformed files remain unchanged.
39
+ - Only `/set-open-in` writes the file. Its write is atomic.
36
40
 
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 no open URI is offered. The file is never rewritten by this extension except via `/set-open-in`. This package uses no shared config.
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.
@@ -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
- getAgentDir,
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
- function loadCommand(): string {
14
- let raw: string;
15
- try {
16
- raw = readFileSync(configPath(), "utf8");
17
- } catch (error) {
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 isObject = (value: unknown): value is Record<string, unknown> =>
29
- typeof value === "object" && value !== null && !Array.isArray(value);
30
- if (!isObject(config) || Object.keys(config).length !== 1 || typeof config.command !== "string" || !config.command.trim()) {
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 (loadCommand() !== "code") return undefined;
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] = loadCommand().split(/\s+/);
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
- const path = configPath();
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.2.7",
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",
@@ -15,9 +15,9 @@
15
15
  },
16
16
  "license": "MIT",
17
17
  "files": [
18
+ "LICENSE",
18
19
  "extensions",
19
- "README.md",
20
- "LICENSE"
20
+ "README.md"
21
21
  ],
22
22
  "exports": {
23
23
  "./open-uri": "./extensions/open.ts"
@@ -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
  }