@narumitw/pi-chrome-devtools 0.11.0 → 0.12.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 +7 -1
- package/package.json +1 -1
- package/src/chrome-devtools.ts +136 -20
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
|
|
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
package/src/chrome-devtools.ts
CHANGED
|
@@ -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
|
|
28
|
-
|
|
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: ${
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
823
|
-
|
|
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
|
-
|
|
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
|
}
|