@narumitw/pi-chrome-devtools 0.11.0 → 0.13.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
@@ -164,13 +164,19 @@ Direct subcommands are also available:
164
164
  The selected tool names are saved to:
165
165
 
166
166
  ```text
167
- ${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-chrome-devtools-settings.json
167
+ ${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-chrome-devtools.json
168
168
  ```
169
169
 
170
170
  When the file is missing or invalid, the extension preserves Pi's current active-tool policy
171
171
  instead of enabling tools by itself. A valid saved selection is restored on Pi startup and
172
172
  `/reload`.
173
173
 
174
+ Compatibility: older versions used `pi-chrome-devtools-settings.json`. During the migration
175
+ window, a legacy-only file is automatically migrated to `pi-chrome-devtools.json` with a warning.
176
+ If both files exist, `pi-chrome-devtools.json` wins and the legacy file is ignored. The legacy
177
+ filename is deprecated and planned for removal after at least one minor release cycle, preferably
178
+ 4–8 weeks, or at the next major release.
179
+
174
180
  ## 🧠 Use cases
175
181
 
176
182
  - Debug front-end applications with an AI coding agent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-chrome-devtools",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Pi extension that exposes Chrome DevTools Protocol tools.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { constants } from "node:fs";
4
- import { access, lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
4
+ import { access, link, lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import {
@@ -24,10 +24,8 @@ const MANAGED_BROWSER_PROFILE_PREFIX = "pi-chrome-devtools-profile-";
24
24
  const DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
25
25
  const BROWSER_SHUTDOWN_WAIT_MS = 1_500;
26
26
  const STATUS_KEY = "chrome-devtools";
27
- const SETTINGS_FILE = join(
28
- process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"),
29
- "pi-chrome-devtools-settings.json",
30
- );
27
+ const NEW_SETTINGS_FILE_NAME = "pi-chrome-devtools.json";
28
+ const LEGACY_SETTINGS_FILE_NAME = "pi-chrome-devtools-settings.json";
31
29
  const CHROME_DEVTOOLS_TOOL_NAMES = [
32
30
  "chrome_devtools_list_pages",
33
31
  "chrome_devtools_select_page",
@@ -108,6 +106,7 @@ interface ChromeDevToolsState {
108
106
  launchPromise?: Promise<void>;
109
107
  lastLaunchAttempt?: BrowserLaunchAttempt;
110
108
  shuttingDown: boolean;
109
+ settingsNotice?: string;
111
110
  }
112
111
 
113
112
  interface ManagedBrowser {
@@ -375,8 +374,11 @@ export default function chromeDevtools(pi: ExtensionAPI) {
375
374
 
376
375
  pi.on("session_start", async (_event, ctx) => {
377
376
  state.shuttingDown = false;
377
+ state.settingsNotice = undefined;
378
378
  ctx.ui.setStatus(STATUS_KEY, undefined);
379
379
  const settings = await loadSettings();
380
+ recordSettingsNotice(settings);
381
+ if (settings.notice) ctx.ui.notify(settings.notice, "warning");
380
382
  if (settings.kind === "loaded") {
381
383
  applyChromeDevtoolsTools(pi, settings.settings.tools);
382
384
  return;
@@ -670,7 +672,8 @@ async function buildToolStatusMessage(pi: ExtensionAPI) {
670
672
  return [
671
673
  `Chrome DevTools tools: ${formatRuntimeStatus(summary)}`,
672
674
  `Persisted selection: ${persistedSetting}`,
673
- `Settings file: ${SETTINGS_FILE}`,
675
+ `Settings file: ${settingsFilePath()}`,
676
+ ...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
674
677
  `Other active tools preserved: ${summary.activeNonChromeToolCount}`,
675
678
  `Endpoint: ${devToolsEndpoint()}`,
676
679
  `Endpoint source: ${endpointSourceLabel()}`,
@@ -745,6 +748,7 @@ function formatRuntimeStatus(summary: ToolStatusSummary) {
745
748
 
746
749
  async function persistedSettingLabel() {
747
750
  const settings = await loadSettings();
751
+ recordSettingsNotice(settings);
748
752
  if (settings.kind === "loaded") return formatPersistedSelection(settings.settings.tools);
749
753
  if (settings.kind === "invalid") {
750
754
  return `none; current active-tool policy preserved (invalid settings ignored: ${settings.reason})`;
@@ -771,29 +775,129 @@ async function persistSettings(
771
775
  }
772
776
  }
773
777
 
774
- async function loadSettings(): Promise<
775
- | { kind: "missing" }
776
- | { kind: "invalid"; reason: string }
777
- | { kind: "loaded"; settings: ChromeDevToolsSettings }
778
- > {
778
+ type SettingsLoadResult =
779
+ | { kind: "missing"; notice?: string }
780
+ | { kind: "invalid"; reason: string; notice?: string }
781
+ | { kind: "loaded"; settings: ChromeDevToolsSettings; notice?: string };
782
+
783
+ type SettingsMigrationResult = {
784
+ kind: "migrated" | "failed";
785
+ notice: string;
786
+ };
787
+
788
+ async function loadSettings(): Promise<SettingsLoadResult> {
789
+ const newPath = settingsFilePath();
790
+ const newSettings = await readSettingsFile(newPath);
791
+ if (newSettings.kind !== "missing") {
792
+ return withLegacyIgnoredNotice(newSettings);
793
+ }
794
+
795
+ const legacyPath = legacySettingsFilePath();
796
+ const legacySettings = await readSettingsFile(legacyPath);
797
+ const concurrentlyCreatedSettings = await readSettingsFile(newPath);
798
+ if (concurrentlyCreatedSettings.kind !== "missing") {
799
+ return withLegacyIgnoredNotice(concurrentlyCreatedSettings);
800
+ }
801
+ if (legacySettings.kind === "missing") return { kind: "missing" };
802
+ if (legacySettings.kind === "invalid") return legacySettings;
803
+
804
+ const migration = await migrateLegacySettings(legacyPath, legacySettings.settings);
805
+ if (migration.kind === "failed") {
806
+ const settingsCreatedDuringMigration = await readSettingsFile(newPath);
807
+ if (settingsCreatedDuringMigration.kind !== "missing") {
808
+ return withLegacyIgnoredNotice(settingsCreatedDuringMigration);
809
+ }
810
+ }
811
+
812
+ return { ...legacySettings, notice: migration.notice };
813
+ }
814
+
815
+ async function readSettingsFile(filePath: string): Promise<SettingsLoadResult> {
779
816
  let text: string;
780
817
  try {
781
- text = await readFile(SETTINGS_FILE, "utf8");
818
+ text = await readFile(filePath, "utf8");
782
819
  } catch (error) {
783
820
  if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
784
- return { kind: "invalid", reason: formatError(error) };
821
+ return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
785
822
  }
786
823
 
787
824
  try {
788
825
  const parsed = JSON.parse(text) as unknown;
789
826
  const settings = normalizeChromeDevtoolsSettings(parsed);
790
827
  if (settings) return { kind: "loaded", settings };
791
- return { kind: "invalid", reason: "expected tools to be an array of Chrome DevTools tool names" };
828
+ return {
829
+ kind: "invalid",
830
+ reason: `${filePath}: expected tools to be an array of Chrome DevTools tool names`,
831
+ };
832
+ } catch (error) {
833
+ return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
834
+ }
835
+ }
836
+
837
+ async function withLegacyIgnoredNotice(settings: SettingsLoadResult): Promise<SettingsLoadResult> {
838
+ if (!(await fileExists(legacySettingsFilePath()))) return settings;
839
+ return {
840
+ ...settings,
841
+ notice: `Chrome DevTools legacy settings ignored: ${legacySettingsFilePath()} exists, but ${settingsFilePath()} takes precedence. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`,
842
+ };
843
+ }
844
+
845
+ export async function installSettingsFileExclusively(filePath: string, contents: string) {
846
+ await mkdir(dirname(filePath), { recursive: true });
847
+ const tempFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
848
+ try {
849
+ await writeFile(tempFile, contents, { encoding: "utf8", flag: "wx" });
850
+ await link(tempFile, filePath);
851
+ } finally {
852
+ await rm(tempFile, { force: true }).catch(() => undefined);
853
+ }
854
+ }
855
+
856
+ async function migrateLegacySettings(
857
+ legacyPath: string,
858
+ settings: ChromeDevToolsSettings,
859
+ ): Promise<SettingsMigrationResult> {
860
+ const newPath = settingsFilePath();
861
+ try {
862
+ await installSettingsFileExclusively(
863
+ newPath,
864
+ `${JSON.stringify(settings, null, 2)}\n`,
865
+ );
866
+ } catch (error) {
867
+ return {
868
+ kind: "failed",
869
+ notice: `Chrome DevTools legacy settings migration failed: could not migrate ${legacyPath} to ${newPath}: ${formatError(error)}. The legacy file was used for this session; future saves will write ${NEW_SETTINGS_FILE_NAME}.`,
870
+ };
871
+ }
872
+
873
+ try {
874
+ await rm(legacyPath, { force: true });
792
875
  } catch (error) {
793
- return { kind: "invalid", reason: formatError(error) };
876
+ return {
877
+ kind: "migrated",
878
+ notice: `Chrome DevTools settings migrated from ${legacyPath} to ${newPath}, but the legacy file could not be removed: ${formatError(error)}. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`,
879
+ };
880
+ }
881
+
882
+ return {
883
+ kind: "migrated",
884
+ notice: `Chrome DevTools settings migrated from ${legacyPath} to ${newPath}. ${LEGACY_SETTINGS_FILE_NAME} is deprecated and will be removed in a future major release.`,
885
+ };
886
+ }
887
+
888
+ async function fileExists(filePath: string) {
889
+ try {
890
+ await access(filePath, constants.F_OK);
891
+ return true;
892
+ } catch {
893
+ return false;
794
894
  }
795
895
  }
796
896
 
897
+ function recordSettingsNotice(settings: SettingsLoadResult) {
898
+ if (settings.notice) state.settingsNotice = settings.notice;
899
+ }
900
+
797
901
  export function normalizeChromeDevtoolsSettings(value: unknown): ChromeDevToolsSettings | undefined {
798
902
  if (!value || typeof value !== "object") return undefined;
799
903
  const settings = value as { tools?: unknown; updatedAt?: unknown };
@@ -819,18 +923,30 @@ function orderedUniqueChromeDevtoolsTools(tools: readonly ChromeDevToolsToolName
819
923
  }
820
924
 
821
925
  async function saveSettings(settings: ChromeDevToolsSettings) {
822
- await mkdir(dirname(SETTINGS_FILE), { recursive: true });
823
- const tempFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`;
926
+ const filePath = settingsFilePath();
927
+ await mkdir(dirname(filePath), { recursive: true });
928
+ const tempFile = `${filePath}.${process.pid}.${Date.now()}.tmp`;
824
929
  try {
825
- await writeFile(tempFile, `${JSON.stringify(settings, null, 2)}
826
- `, "utf8");
827
- await rename(tempFile, SETTINGS_FILE);
930
+ await writeFile(tempFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
931
+ await rename(tempFile, filePath);
828
932
  } catch (error) {
829
933
  await rm(tempFile, { force: true }).catch(() => undefined);
830
934
  throw error;
831
935
  }
832
936
  }
833
937
 
938
+ function settingsFilePath() {
939
+ return join(agentDir(), NEW_SETTINGS_FILE_NAME);
940
+ }
941
+
942
+ function legacySettingsFilePath() {
943
+ return join(agentDir(), LEGACY_SETTINGS_FILE_NAME);
944
+ }
945
+
946
+ function agentDir() {
947
+ return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
948
+ }
949
+
834
950
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
835
951
  return error instanceof Error && "code" in error;
836
952
  }