@zigai/pi-mode 0.3.1 → 0.4.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 +26 -5
- package/config.schema.json +29 -0
- package/package.json +3 -2
- package/src/constants.ts +2 -0
- package/src/index.ts +3 -0
- package/src/mode-state.ts +128 -25
- package/src/settings.ts +157 -41
- package/src/status.ts +116 -0
- package/src/storage.ts +82 -3
package/README.md
CHANGED
|
@@ -20,11 +20,32 @@ pi install npm:@zigai/pi-mode
|
|
|
20
20
|
- Can show the current mode in the prompt editor border when enabled.
|
|
21
21
|
- Colors the prompt editor border from the active mode, with an opt-in setting for thinking-derived border colors.
|
|
22
22
|
|
|
23
|
-
Modes can store a provider, model, thinking level, and optional color.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
Modes can store a provider, model, thinking level, and optional color. By default, Pi Mode hides Pi's transient `Thinking level: …` status message because the active thinking level is already visible in the footer.
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Configure global settings at `~/.pi/agent/pi-mode/config.json`.
|
|
28
|
+
|
|
29
|
+
| Option | Type | Default | Description |
|
|
30
|
+
| ----------------------------- | --------- | ----------- | -------------------------------------------------------------------------- |
|
|
31
|
+
| `version` | `number` | `1` | Config format version. |
|
|
32
|
+
| `currentMode` | `string` | `"default"` | Mode selected at startup. |
|
|
33
|
+
| `modeShowName` | `boolean` | `false` | Shows the current mode name in the prompt editor border. |
|
|
34
|
+
| `modeUseThinkingBorderColors` | `boolean` | `false` | Uses thinking-derived border colors when a mode has no explicit color. |
|
|
35
|
+
| `modeShowThinkingLevelStatus` | `boolean` | `false` | Shows Pi's transient thinking-level status message. |
|
|
36
|
+
| `modes` | `object` | `{}` | Named modes with optional `provider`, `modelId`, `thinkingLevel`, `color`. |
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"$schema": "./config.schema.json",
|
|
41
|
+
"version": 1,
|
|
42
|
+
"currentMode": "default",
|
|
43
|
+
"modeShowName": false,
|
|
44
|
+
"modeUseThinkingBorderColors": false,
|
|
45
|
+
"modeShowThinkingLevelStatus": false,
|
|
46
|
+
"modes": {}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
28
49
|
|
|
29
50
|
## License
|
|
30
51
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/zigai/pi-tweaks/main/packages/pi-mode/config.schema.json",
|
|
4
|
+
"title": "Pi mode config",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": { "type": "string" },
|
|
9
|
+
"version": { "type": "number", "default": 1 },
|
|
10
|
+
"currentMode": { "type": "string", "default": "default" },
|
|
11
|
+
"modeShowName": { "type": "boolean", "default": false },
|
|
12
|
+
"modeUseThinkingBorderColors": { "type": "boolean", "default": false },
|
|
13
|
+
"modeShowThinkingLevelStatus": { "type": "boolean", "default": false },
|
|
14
|
+
"modes": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"provider": { "type": "string" },
|
|
21
|
+
"modelId": { "type": "string" },
|
|
22
|
+
"thinkingLevel": {},
|
|
23
|
+
"color": { "type": "string" }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"default": {}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zigai/pi-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi package that adds prompt modes for model and thinking-level switching.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mode",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"src",
|
|
26
|
-
"README.md"
|
|
26
|
+
"README.md",
|
|
27
|
+
"*.json"
|
|
27
28
|
],
|
|
28
29
|
"type": "module",
|
|
29
30
|
"publishConfig": {
|
package/src/constants.ts
CHANGED
|
@@ -9,6 +9,8 @@ export const MODE_UI_SHOW_NAME_ON = "Show mode name: on";
|
|
|
9
9
|
export const MODE_UI_SHOW_NAME_OFF = "Show mode name: off";
|
|
10
10
|
export const MODE_UI_THINKING_COLORS_ON = "Thinking border colors: on";
|
|
11
11
|
export const MODE_UI_THINKING_COLORS_OFF = "Thinking border colors: off";
|
|
12
|
+
export const MODE_UI_THINKING_STATUS_ON = "Thinking level status: on";
|
|
13
|
+
export const MODE_UI_THINKING_STATUS_OFF = "Thinking level status: off";
|
|
12
14
|
export const MODE_UI_BACK = "Back";
|
|
13
15
|
|
|
14
16
|
export const ALL_THINKING_LEVELS: ThinkingLevel[] = [
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,11 @@ import {
|
|
|
8
8
|
selectModeUI,
|
|
9
9
|
} from "./mode-state.ts";
|
|
10
10
|
import { setSettingsContext } from "./settings.ts";
|
|
11
|
+
import { applyThinkingLevelStatusPatch } from "./status.ts";
|
|
11
12
|
|
|
12
13
|
export default function (pi: ExtensionAPI) {
|
|
14
|
+
void applyThinkingLevelStatusPatch();
|
|
15
|
+
|
|
13
16
|
pi.registerCommand("mode", {
|
|
14
17
|
description: "Select prompt mode",
|
|
15
18
|
handler: async (args, ctx) => {
|
package/src/mode-state.ts
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
MODE_UI_SHOW_NAME_ON,
|
|
21
21
|
MODE_UI_THINKING_COLORS_OFF,
|
|
22
22
|
MODE_UI_THINKING_COLORS_ON,
|
|
23
|
+
MODE_UI_THINKING_STATUS_OFF,
|
|
24
|
+
MODE_UI_THINKING_STATUS_ON,
|
|
23
25
|
THINKING_UNSET_LABEL,
|
|
24
26
|
} from "./constants.ts";
|
|
25
27
|
import {
|
|
@@ -28,12 +30,15 @@ import {
|
|
|
28
30
|
getGlobalModesPath,
|
|
29
31
|
getMtimeMs,
|
|
30
32
|
getProjectModesPath,
|
|
33
|
+
scaffoldGlobalModesConfig,
|
|
31
34
|
withFileLock,
|
|
32
35
|
} from "./storage.ts";
|
|
33
36
|
import {
|
|
34
37
|
setShowModeName,
|
|
38
|
+
setShowThinkingLevelStatus,
|
|
35
39
|
setUseThinkingBorderColors,
|
|
36
40
|
shouldShowModeName,
|
|
41
|
+
shouldShowThinkingLevelStatus,
|
|
37
42
|
shouldUseThinkingBorderColors,
|
|
38
43
|
} from "./settings.ts";
|
|
39
44
|
import type { ModeRuntime, ModesFile, ModesPatch, ModeSpec, ModeSpecPatch } from "./types.ts";
|
|
@@ -43,19 +48,29 @@ type ScopedModelItem = {
|
|
|
43
48
|
thinkingLevel?: string;
|
|
44
49
|
};
|
|
45
50
|
|
|
46
|
-
const ModeSpecJsonSchema = Type.Object(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
const ModeSpecJsonSchema = Type.Object(
|
|
52
|
+
{
|
|
53
|
+
provider: Type.Optional(Type.String()),
|
|
54
|
+
modelId: Type.Optional(Type.String()),
|
|
55
|
+
thinkingLevel: Type.Optional(Type.Unknown()),
|
|
56
|
+
color: Type.Optional(Type.String()),
|
|
57
|
+
},
|
|
58
|
+
{ additionalProperties: false },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const ModesFileJsonSchema = Type.Object(
|
|
62
|
+
{
|
|
63
|
+
$schema: Type.Optional(Type.String()),
|
|
64
|
+
version: Type.Optional(Type.Number()),
|
|
65
|
+
currentMode: Type.Optional(Type.String()),
|
|
66
|
+
modeShowName: Type.Optional(Type.Boolean()),
|
|
67
|
+
modeUseThinkingBorderColors: Type.Optional(Type.Boolean()),
|
|
68
|
+
modeShowThinkingLevelStatus: Type.Optional(Type.Boolean()),
|
|
69
|
+
modes: Type.Optional(Type.Record(Type.String(), ModeSpecJsonSchema)),
|
|
70
|
+
},
|
|
71
|
+
{ additionalProperties: false },
|
|
72
|
+
);
|
|
73
|
+
const ConfigObjectSchema = ModesFileJsonSchema;
|
|
59
74
|
|
|
60
75
|
type ModeSpecJson = Static<typeof ModeSpecJsonSchema>;
|
|
61
76
|
type ModesFileJson = Static<typeof ModesFileJsonSchema>;
|
|
@@ -69,7 +84,11 @@ function formatSchemaPath(instancePath: string): string {
|
|
|
69
84
|
.join(".");
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
function parseSchema
|
|
87
|
+
function parseSchema<Schema extends TSchema>(
|
|
88
|
+
schema: Schema,
|
|
89
|
+
value: unknown,
|
|
90
|
+
label: string,
|
|
91
|
+
): Static<Schema> {
|
|
73
92
|
const errors = [...Value.Errors(schema, value)];
|
|
74
93
|
if (errors.length > 0) {
|
|
75
94
|
const messages = errors
|
|
@@ -82,15 +101,39 @@ function parseSchema(schema: TSchema, value: unknown, label: string): unknown {
|
|
|
82
101
|
throw new Error(`${label} is invalid: ${messages.join("; ")}${suffix}`);
|
|
83
102
|
}
|
|
84
103
|
const parsed: unknown = Value.Parse(schema, value);
|
|
85
|
-
|
|
104
|
+
// SAFETY: Value.Errors returned no schema violations, so Value.Parse returns
|
|
105
|
+
// the TypeBox static type represented by the same schema.
|
|
106
|
+
// oxlint-disable-next-line typescript/no-unsafe-return -- SAFETY: TypeBox exposes parsed schema output through a conditional static type that oxlint treats as any here.
|
|
107
|
+
return parsed as Static<Schema>;
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
function parseModesFileJson(value: unknown): ModesFileJson {
|
|
89
|
-
return parseSchema(ModesFileJsonSchema, value, "
|
|
111
|
+
return parseSchema(ModesFileJsonSchema, value, "pi-mode config.json");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseConfigObject(value: unknown, filePath: string): Record<string, unknown> {
|
|
115
|
+
return Object.fromEntries(Object.entries(parseSchema(ConfigObjectSchema, value, filePath)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cloneModeSpec(spec: ModeSpec): ModeSpec {
|
|
119
|
+
const cloned: ModeSpec = {};
|
|
120
|
+
if (spec.provider !== undefined) cloned.provider = spec.provider;
|
|
121
|
+
if (spec.modelId !== undefined) cloned.modelId = spec.modelId;
|
|
122
|
+
if (spec.thinkingLevel !== undefined) cloned.thinkingLevel = spec.thinkingLevel;
|
|
123
|
+
if (spec.color !== undefined) cloned.color = spec.color;
|
|
124
|
+
return cloned;
|
|
90
125
|
}
|
|
91
126
|
|
|
92
127
|
function cloneModesFile(file: ModesFile): ModesFile {
|
|
93
|
-
|
|
128
|
+
const modes: Record<string, ModeSpec> = {};
|
|
129
|
+
for (const [name, spec] of Object.entries(file.modes)) {
|
|
130
|
+
modes[name] = cloneModeSpec(spec);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
version: file.version,
|
|
134
|
+
currentMode: file.currentMode,
|
|
135
|
+
modes,
|
|
136
|
+
};
|
|
94
137
|
}
|
|
95
138
|
|
|
96
139
|
function modeSpec(modes: Record<string, ModeSpec>, name: string): ModeSpec | undefined {
|
|
@@ -256,7 +299,10 @@ function ensureDefaultModeEntries(file: ModesFile, ctx: ExtensionContext, pi: Ex
|
|
|
256
299
|
for (const name of DEFAULT_MODE_ORDER) {
|
|
257
300
|
if (modeSpec(file.modes, name) === undefined) {
|
|
258
301
|
const defaults = createDefaultModes(ctx, pi);
|
|
259
|
-
|
|
302
|
+
const defaultSpec = modeSpec(defaults.modes, name);
|
|
303
|
+
if (defaultSpec !== undefined) {
|
|
304
|
+
file.modes[name] = cloneModeSpec(defaultSpec);
|
|
305
|
+
}
|
|
260
306
|
}
|
|
261
307
|
}
|
|
262
308
|
|
|
@@ -286,9 +332,14 @@ async function loadModesFile(
|
|
|
286
332
|
pi: ExtensionAPI,
|
|
287
333
|
options?: { throwOnInvalid?: boolean },
|
|
288
334
|
): Promise<ModesFile> {
|
|
335
|
+
if (filePath === getGlobalModesPath()) {
|
|
336
|
+
await scaffoldGlobalModesConfig();
|
|
337
|
+
}
|
|
338
|
+
|
|
289
339
|
try {
|
|
290
340
|
const raw = await fs.readFile(filePath, "utf8");
|
|
291
|
-
const
|
|
341
|
+
const parsedJson: unknown = JSON.parse(raw);
|
|
342
|
+
const parsed = parseModesFileJson(parsedJson);
|
|
292
343
|
const currentMode = parsed.currentMode ?? "default";
|
|
293
344
|
const modesRaw = parsed.modes ?? {};
|
|
294
345
|
|
|
@@ -311,8 +362,23 @@ async function loadModesFile(
|
|
|
311
362
|
}
|
|
312
363
|
}
|
|
313
364
|
|
|
365
|
+
async function readConfigObject(filePath: string): Promise<Record<string, unknown>> {
|
|
366
|
+
try {
|
|
367
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
368
|
+
const parsedJson: unknown = JSON.parse(raw);
|
|
369
|
+
return parseConfigObject(parsedJson, filePath);
|
|
370
|
+
} catch (error: unknown) {
|
|
371
|
+
if (getLoadErrorCode(error) === "ENOENT") return {};
|
|
372
|
+
throwLoadError(filePath, error);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
314
376
|
async function saveModesFile(filePath: string, data: ModesFile): Promise<void> {
|
|
315
|
-
await
|
|
377
|
+
const config = await readConfigObject(filePath);
|
|
378
|
+
config.version = data.version;
|
|
379
|
+
config.currentMode = data.currentMode;
|
|
380
|
+
config.modes = data.modes;
|
|
381
|
+
await atomicWriteUtf8(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
316
382
|
}
|
|
317
383
|
|
|
318
384
|
function orderedModeNames(modes: Record<string, ModeSpec>): string[] {
|
|
@@ -323,9 +389,19 @@ function formatModeLabel(mode: string): string {
|
|
|
323
389
|
return mode;
|
|
324
390
|
}
|
|
325
391
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
392
|
+
type ProjectTrustContext = ExtensionContext & {
|
|
393
|
+
isProjectTrusted?: () => boolean;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
function isProjectTrusted(ctx: ExtensionContext): boolean {
|
|
397
|
+
return (ctx as ProjectTrustContext).isProjectTrusted?.() ?? true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function resolveModesPath(ctx: ExtensionContext): Promise<string> {
|
|
401
|
+
if (isProjectTrusted(ctx)) {
|
|
402
|
+
const projectPath = getProjectModesPath(ctx.cwd);
|
|
403
|
+
if (await fileExists(projectPath)) return projectPath;
|
|
404
|
+
}
|
|
329
405
|
return getGlobalModesPath();
|
|
330
406
|
}
|
|
331
407
|
|
|
@@ -434,7 +510,7 @@ export function getModeBorderColor(
|
|
|
434
510
|
}
|
|
435
511
|
|
|
436
512
|
async function ensureRuntime(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
437
|
-
const filePath = await resolveModesPath(ctx
|
|
513
|
+
const filePath = await resolveModesPath(ctx);
|
|
438
514
|
|
|
439
515
|
const mtimeMs = await getMtimeMs(filePath);
|
|
440
516
|
const filePathChanged = runtime.filePath !== filePath;
|
|
@@ -871,11 +947,16 @@ async function configureModesUI(pi: ExtensionAPI, ctx: ExtensionContext): Promis
|
|
|
871
947
|
if (shouldUseThinkingBorderColors()) {
|
|
872
948
|
thinkingColorsChoice = MODE_UI_THINKING_COLORS_ON;
|
|
873
949
|
}
|
|
950
|
+
let thinkingStatusChoice = MODE_UI_THINKING_STATUS_OFF;
|
|
951
|
+
if (shouldShowThinkingLevelStatus()) {
|
|
952
|
+
thinkingStatusChoice = MODE_UI_THINKING_STATUS_ON;
|
|
953
|
+
}
|
|
874
954
|
const choice = await ctx.ui.select("Configure modes", [
|
|
875
955
|
...names,
|
|
876
956
|
MODE_UI_ADD,
|
|
877
957
|
showModeNameChoice,
|
|
878
958
|
thinkingColorsChoice,
|
|
959
|
+
thinkingStatusChoice,
|
|
879
960
|
MODE_UI_BACK,
|
|
880
961
|
]);
|
|
881
962
|
if (choice === undefined || choice.length === 0 || choice === MODE_UI_BACK) return;
|
|
@@ -925,6 +1006,25 @@ async function configureModesUI(pi: ExtensionAPI, ctx: ExtensionContext): Promis
|
|
|
925
1006
|
continue;
|
|
926
1007
|
}
|
|
927
1008
|
|
|
1009
|
+
if (choice === MODE_UI_THINKING_STATUS_ON || choice === MODE_UI_THINKING_STATUS_OFF) {
|
|
1010
|
+
const next = !shouldShowThinkingLevelStatus();
|
|
1011
|
+
try {
|
|
1012
|
+
setShowThinkingLevelStatus(next);
|
|
1013
|
+
} catch (error: unknown) {
|
|
1014
|
+
ctx.ui.notify(
|
|
1015
|
+
`Thinking level status was not saved: ${errorMessage(error)}`,
|
|
1016
|
+
"error",
|
|
1017
|
+
);
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
let displayState = "disabled";
|
|
1021
|
+
if (next) {
|
|
1022
|
+
displayState = "enabled";
|
|
1023
|
+
}
|
|
1024
|
+
ctx.ui.notify(`Thinking level status ${displayState}`, "info");
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
928
1028
|
await editModeUI(pi, ctx, choice);
|
|
929
1029
|
}
|
|
930
1030
|
}
|
|
@@ -992,8 +1092,11 @@ export async function cycleMode(
|
|
|
992
1092
|
if (runtime.currentMode === CUSTOM_MODE_NAME) {
|
|
993
1093
|
baseMode = runtime.lastRealMode;
|
|
994
1094
|
}
|
|
1095
|
+
const fallbackMode = names[0];
|
|
1096
|
+
if (fallbackMode === undefined) return;
|
|
1097
|
+
|
|
995
1098
|
const index = Math.max(0, names.indexOf(baseMode));
|
|
996
|
-
const next = names[(index + direction + names.length) % names.length] ??
|
|
1099
|
+
const next = names[(index + direction + names.length) % names.length] ?? fallbackMode;
|
|
997
1100
|
await applyMode(pi, ctx, next);
|
|
998
1101
|
}
|
|
999
1102
|
|
package/src/settings.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CONFIG_DIR_NAME,
|
|
2
3
|
getAgentDir,
|
|
3
|
-
SettingsManager,
|
|
4
4
|
type ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import {
|
|
7
7
|
closeSync,
|
|
8
|
-
existsSync,
|
|
9
8
|
mkdirSync,
|
|
10
9
|
openSync,
|
|
11
10
|
readFileSync,
|
|
@@ -20,10 +19,35 @@ import { Value } from "typebox/value";
|
|
|
20
19
|
|
|
21
20
|
export const SHOW_MODE_NAME_SETTINGS_KEY = "modeShowName";
|
|
22
21
|
export const USE_THINKING_BORDER_COLORS_SETTINGS_KEY = "modeUseThinkingBorderColors";
|
|
22
|
+
export const SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY = "modeShowThinkingLevelStatus";
|
|
23
23
|
|
|
24
24
|
const SETTINGS_LOCK_TIMEOUT_MS = 5_000;
|
|
25
25
|
const STALE_SETTINGS_LOCK_MS = 30_000;
|
|
26
|
-
const
|
|
26
|
+
const EXTENSION_ID = "pi-mode";
|
|
27
|
+
const CONFIG_FILE = "config.json";
|
|
28
|
+
const SCHEMA_FILE = "config.schema.json";
|
|
29
|
+
const ModeSpecJsonSchema = Type.Object(
|
|
30
|
+
{
|
|
31
|
+
provider: Type.Optional(Type.String()),
|
|
32
|
+
modelId: Type.Optional(Type.String()),
|
|
33
|
+
thinkingLevel: Type.Optional(Type.Unknown()),
|
|
34
|
+
color: Type.Optional(Type.String()),
|
|
35
|
+
},
|
|
36
|
+
{ additionalProperties: false },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const SettingsObjectSchema = Type.Object(
|
|
40
|
+
{
|
|
41
|
+
$schema: Type.Optional(Type.String()),
|
|
42
|
+
version: Type.Optional(Type.Number()),
|
|
43
|
+
currentMode: Type.Optional(Type.String()),
|
|
44
|
+
[SHOW_MODE_NAME_SETTINGS_KEY]: Type.Optional(Type.Boolean()),
|
|
45
|
+
[USE_THINKING_BORDER_COLORS_SETTINGS_KEY]: Type.Optional(Type.Boolean()),
|
|
46
|
+
[SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY]: Type.Optional(Type.Boolean()),
|
|
47
|
+
modes: Type.Optional(Type.Record(Type.String(), ModeSpecJsonSchema)),
|
|
48
|
+
},
|
|
49
|
+
{ additionalProperties: false },
|
|
50
|
+
);
|
|
27
51
|
const BooleanSettingSchema = Type.Boolean();
|
|
28
52
|
|
|
29
53
|
type SettingsReadContext = {
|
|
@@ -31,12 +55,22 @@ type SettingsReadContext = {
|
|
|
31
55
|
projectTrusted: boolean;
|
|
32
56
|
};
|
|
33
57
|
|
|
58
|
+
const DEFAULT_MODE_CONFIG_FILE = {
|
|
59
|
+
$schema: `./${SCHEMA_FILE}`,
|
|
60
|
+
version: 1,
|
|
61
|
+
currentMode: "default",
|
|
62
|
+
[SHOW_MODE_NAME_SETTINGS_KEY]: false,
|
|
63
|
+
[USE_THINKING_BORDER_COLORS_SETTINGS_KEY]: false,
|
|
64
|
+
[SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY]: false,
|
|
65
|
+
modes: {},
|
|
66
|
+
};
|
|
67
|
+
|
|
34
68
|
let settingsReadContext: SettingsReadContext | undefined;
|
|
35
69
|
let cachedSettings:
|
|
36
70
|
| {
|
|
37
|
-
mtimeKey: string;
|
|
38
71
|
showModeName: boolean;
|
|
39
72
|
useThinkingBorderColors: boolean;
|
|
73
|
+
showThinkingLevelStatus: boolean;
|
|
40
74
|
}
|
|
41
75
|
| undefined;
|
|
42
76
|
|
|
@@ -63,14 +97,76 @@ export function setSettingsContext(ctx: ExtensionContext): void {
|
|
|
63
97
|
}
|
|
64
98
|
|
|
65
99
|
function getSettingsPath(): string {
|
|
66
|
-
return join(getAgentDir(),
|
|
100
|
+
return join(getAgentDir(), EXTENSION_ID, CONFIG_FILE);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getSchemaPath(configPath: string): string {
|
|
104
|
+
return join(dirname(configPath), SCHEMA_FILE);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writeIfMissing(filePath: string, content: string): void {
|
|
108
|
+
try {
|
|
109
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
110
|
+
writeFileSync(filePath, content, { encoding: "utf8", flag: "wx" });
|
|
111
|
+
} catch (error: unknown) {
|
|
112
|
+
if (error instanceof Error && (error as NodeJS.ErrnoException).code === "EEXIST") return;
|
|
113
|
+
if (error instanceof Error) throw error;
|
|
114
|
+
throw new Error(String(error));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function refreshSchemaFile(filePath: string, content: string): void {
|
|
119
|
+
let temporaryPath: string | undefined;
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
122
|
+
try {
|
|
123
|
+
if (readFileSync(filePath, "utf8") === content) return;
|
|
124
|
+
} catch (error: unknown) {
|
|
125
|
+
if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const nextTemporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
131
|
+
writeFileSync(nextTemporaryPath, content, { encoding: "utf8", flag: "wx" });
|
|
132
|
+
temporaryPath = nextTemporaryPath;
|
|
133
|
+
renameSync(temporaryPath, filePath);
|
|
134
|
+
temporaryPath = undefined;
|
|
135
|
+
} catch (error: unknown) {
|
|
136
|
+
if (temporaryPath !== undefined) {
|
|
137
|
+
try {
|
|
138
|
+
unlinkSync(temporaryPath);
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore cleanup failure while reporting the original scaffold failure.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (error instanceof Error) throw error;
|
|
144
|
+
throw new Error(String(error));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readBundledSchema(): string | undefined {
|
|
149
|
+
try {
|
|
150
|
+
return readFileSync(new URL("../config.schema.json", import.meta.url), "utf8");
|
|
151
|
+
} catch {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function scaffoldGlobalConfig(): void {
|
|
157
|
+
const globalConfigPath = getSettingsPath();
|
|
158
|
+
const schema = readBundledSchema();
|
|
159
|
+
if (schema !== undefined) {
|
|
160
|
+
refreshSchemaFile(getSchemaPath(globalConfigPath), schema);
|
|
161
|
+
}
|
|
162
|
+
writeIfMissing(globalConfigPath, `${JSON.stringify(DEFAULT_MODE_CONFIG_FILE, null, 2)}\n`);
|
|
67
163
|
}
|
|
68
164
|
|
|
69
165
|
function getProjectSettingsPath(): string | undefined {
|
|
70
166
|
if (settingsReadContext === undefined || !settingsReadContext.projectTrusted) {
|
|
71
167
|
return undefined;
|
|
72
168
|
}
|
|
73
|
-
return join(settingsReadContext.cwd,
|
|
169
|
+
return join(settingsReadContext.cwd, CONFIG_DIR_NAME, EXTENSION_ID, CONFIG_FILE);
|
|
74
170
|
}
|
|
75
171
|
|
|
76
172
|
function getErrorCode(error: unknown): string | undefined {
|
|
@@ -185,20 +281,6 @@ function atomicWriteUtf8Sync(filePath: string, content: string): void {
|
|
|
185
281
|
}
|
|
186
282
|
}
|
|
187
283
|
|
|
188
|
-
function getFileMtimeMs(filePath: string | undefined): number | null {
|
|
189
|
-
if (filePath === undefined) return null;
|
|
190
|
-
try {
|
|
191
|
-
if (!existsSync(filePath)) return null;
|
|
192
|
-
return statSync(filePath).mtimeMs;
|
|
193
|
-
} catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function getSettingsMtimeKey(): string {
|
|
199
|
-
return `${getFileMtimeMs(getSettingsPath())}:${getFileMtimeMs(getProjectSettingsPath())}`;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
284
|
function formatSchemaPath(instancePath: string): string {
|
|
203
285
|
if (instancePath.length === 0) return "root";
|
|
204
286
|
return instancePath
|
|
@@ -222,27 +304,35 @@ function parseSettingsObject(value: unknown, settingsPath: string): Record<strin
|
|
|
222
304
|
`${settingsPath} must contain a JSON object: ${messages.join("; ")}${suffix}`,
|
|
223
305
|
);
|
|
224
306
|
}
|
|
225
|
-
return
|
|
307
|
+
return Object.fromEntries(Object.entries(Value.Parse(SettingsObjectSchema, value)));
|
|
226
308
|
}
|
|
227
309
|
|
|
228
|
-
function readSettingsObject(
|
|
229
|
-
|
|
310
|
+
function readSettingsObject(
|
|
311
|
+
settingsPath: string,
|
|
312
|
+
options?: { throwOnInvalid?: boolean },
|
|
313
|
+
): Record<string, unknown> {
|
|
314
|
+
if (settingsPath === getSettingsPath()) {
|
|
315
|
+
scaffoldGlobalConfig();
|
|
316
|
+
}
|
|
317
|
+
|
|
230
318
|
try {
|
|
231
319
|
const raw = readFileSync(settingsPath, "utf8");
|
|
232
|
-
|
|
320
|
+
const parsedJson: unknown = JSON.parse(raw);
|
|
321
|
+
return parseSettingsObject(parsedJson, settingsPath);
|
|
233
322
|
} catch (error: unknown) {
|
|
234
323
|
if (getErrorCode(error) === "ENOENT") return {};
|
|
235
324
|
if (options?.throwOnInvalid === true) throwError(error);
|
|
236
|
-
// Ignore malformed
|
|
325
|
+
// Ignore malformed config files while reading and fall back to defaults.
|
|
237
326
|
}
|
|
238
327
|
|
|
239
328
|
return {};
|
|
240
329
|
}
|
|
241
330
|
|
|
242
331
|
function updateSettingsObject(update: (settings: Record<string, unknown>) => void): void {
|
|
332
|
+
scaffoldGlobalConfig();
|
|
243
333
|
const settingsPath = getSettingsPath();
|
|
244
334
|
withSettingsLock(settingsPath, () => {
|
|
245
|
-
const settings = readSettingsObject({ throwOnInvalid: true });
|
|
335
|
+
const settings = readSettingsObject(settingsPath, { throwOnInvalid: true });
|
|
246
336
|
update(settings);
|
|
247
337
|
atomicWriteUtf8Sync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
248
338
|
});
|
|
@@ -261,39 +351,53 @@ function applyBooleanSetting(
|
|
|
261
351
|
function readModeSettings(): {
|
|
262
352
|
showModeName: boolean;
|
|
263
353
|
useThinkingBorderColors: boolean;
|
|
354
|
+
showThinkingLevelStatus: boolean;
|
|
264
355
|
} {
|
|
265
|
-
|
|
266
|
-
if (cachedSettings?.mtimeKey === mtimeKey) {
|
|
356
|
+
if (cachedSettings !== undefined) {
|
|
267
357
|
return cachedSettings;
|
|
268
358
|
}
|
|
269
359
|
|
|
270
|
-
const context = settingsReadContext ?? { cwd: process.cwd(), projectTrusted: false };
|
|
271
|
-
const manager = SettingsManager.create(context.cwd, getAgentDir(), {
|
|
272
|
-
projectTrusted: context.projectTrusted,
|
|
273
|
-
});
|
|
274
360
|
let showModeName = false;
|
|
275
361
|
let useThinkingBorderColors = false;
|
|
362
|
+
let showThinkingLevelStatus = false;
|
|
276
363
|
|
|
277
|
-
const globalSettings =
|
|
364
|
+
const globalSettings = readSettingsObject(getSettingsPath());
|
|
278
365
|
showModeName = applyBooleanSetting(globalSettings, SHOW_MODE_NAME_SETTINGS_KEY, showModeName);
|
|
279
366
|
useThinkingBorderColors = applyBooleanSetting(
|
|
280
367
|
globalSettings,
|
|
281
368
|
USE_THINKING_BORDER_COLORS_SETTINGS_KEY,
|
|
282
369
|
useThinkingBorderColors,
|
|
283
370
|
);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
projectSettings,
|
|
289
|
-
USE_THINKING_BORDER_COLORS_SETTINGS_KEY,
|
|
290
|
-
useThinkingBorderColors,
|
|
371
|
+
showThinkingLevelStatus = applyBooleanSetting(
|
|
372
|
+
globalSettings,
|
|
373
|
+
SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY,
|
|
374
|
+
showThinkingLevelStatus,
|
|
291
375
|
);
|
|
292
376
|
|
|
377
|
+
const projectSettingsPath = getProjectSettingsPath();
|
|
378
|
+
if (projectSettingsPath !== undefined) {
|
|
379
|
+
const projectSettings = readSettingsObject(projectSettingsPath);
|
|
380
|
+
showModeName = applyBooleanSetting(
|
|
381
|
+
projectSettings,
|
|
382
|
+
SHOW_MODE_NAME_SETTINGS_KEY,
|
|
383
|
+
showModeName,
|
|
384
|
+
);
|
|
385
|
+
useThinkingBorderColors = applyBooleanSetting(
|
|
386
|
+
projectSettings,
|
|
387
|
+
USE_THINKING_BORDER_COLORS_SETTINGS_KEY,
|
|
388
|
+
useThinkingBorderColors,
|
|
389
|
+
);
|
|
390
|
+
showThinkingLevelStatus = applyBooleanSetting(
|
|
391
|
+
projectSettings,
|
|
392
|
+
SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY,
|
|
393
|
+
showThinkingLevelStatus,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
293
397
|
cachedSettings = {
|
|
294
|
-
mtimeKey,
|
|
295
398
|
showModeName,
|
|
296
399
|
useThinkingBorderColors,
|
|
400
|
+
showThinkingLevelStatus,
|
|
297
401
|
};
|
|
298
402
|
return cachedSettings;
|
|
299
403
|
}
|
|
@@ -306,6 +410,10 @@ export function shouldUseThinkingBorderColors(): boolean {
|
|
|
306
410
|
return readModeSettings().useThinkingBorderColors;
|
|
307
411
|
}
|
|
308
412
|
|
|
413
|
+
export function shouldShowThinkingLevelStatus(): boolean {
|
|
414
|
+
return readModeSettings().showThinkingLevelStatus;
|
|
415
|
+
}
|
|
416
|
+
|
|
309
417
|
export function setShowModeName(show: boolean): void {
|
|
310
418
|
updateSettingsObject((settings) => {
|
|
311
419
|
settings[SHOW_MODE_NAME_SETTINGS_KEY] = show;
|
|
@@ -321,3 +429,11 @@ export function setUseThinkingBorderColors(useThinkingBorderColors: boolean): vo
|
|
|
321
429
|
|
|
322
430
|
cachedSettings = undefined;
|
|
323
431
|
}
|
|
432
|
+
|
|
433
|
+
export function setShowThinkingLevelStatus(showThinkingLevelStatus: boolean): void {
|
|
434
|
+
updateSettingsObject((settings) => {
|
|
435
|
+
settings[SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY] = showThinkingLevelStatus;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
cachedSettings = undefined;
|
|
439
|
+
}
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { shouldShowThinkingLevelStatus } from "./settings.ts";
|
|
4
|
+
|
|
5
|
+
const STATUS_PATCH_MARKER = Symbol.for("zigai.pi-mode.thinking-status-patch");
|
|
6
|
+
const STATUS_PATCH_STATE = Symbol.for("zigai.pi-mode.thinking-status-state");
|
|
7
|
+
const THINKING_LEVEL_STATUS_PREFIX = "Thinking level: ";
|
|
8
|
+
|
|
9
|
+
type InteractiveModePrototype = {
|
|
10
|
+
showStatus(message: string): void;
|
|
11
|
+
[STATUS_PATCH_MARKER]?: true;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type StatusPatchState = {
|
|
15
|
+
shouldShowThinkingLevelStatus: () => boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type InteractiveModeClass = {
|
|
19
|
+
prototype: InteractiveModePrototype;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type InteractiveModeModule = {
|
|
23
|
+
InteractiveMode?: InteractiveModeClass;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ShowStatus = (this: InteractiveModePrototype, message: string) => void;
|
|
27
|
+
|
|
28
|
+
type ThinkingLevelStatusPatchOptions = {
|
|
29
|
+
readonly loadInteractiveModeModule?: () => Promise<InteractiveModeModule | undefined>;
|
|
30
|
+
readonly shouldShowThinkingLevelStatus?: () => boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function isShowStatus(value: unknown): value is ShowStatus {
|
|
34
|
+
return typeof value === "function";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getStatusPatchState(): StatusPatchState {
|
|
38
|
+
const existing = Reflect.get(globalThis, STATUS_PATCH_STATE);
|
|
39
|
+
if (typeof existing === "object" && existing !== null) {
|
|
40
|
+
const reader = Reflect.get(existing, "shouldShowThinkingLevelStatus");
|
|
41
|
+
if (typeof reader === "function") {
|
|
42
|
+
return existing as StatusPatchState;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const patchState: StatusPatchState = {
|
|
47
|
+
shouldShowThinkingLevelStatus: () => true,
|
|
48
|
+
};
|
|
49
|
+
Reflect.set(globalThis, STATUS_PATCH_STATE, patchState);
|
|
50
|
+
return patchState;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function warnStatusPatchUnavailable(error?: unknown): void {
|
|
54
|
+
let suffix = "";
|
|
55
|
+
if (error instanceof Error && error.message.length > 0) {
|
|
56
|
+
suffix = `: ${error.message}`;
|
|
57
|
+
}
|
|
58
|
+
console.warn(
|
|
59
|
+
`[pi-mode] thinking-level status patch unavailable; Pi internals may have changed${suffix}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loadInteractiveModeModule(): Promise<InteractiveModeModule | undefined> {
|
|
64
|
+
try {
|
|
65
|
+
const codingAgentEntry = fileURLToPath(
|
|
66
|
+
import.meta.resolve("@earendil-works/pi-coding-agent"),
|
|
67
|
+
);
|
|
68
|
+
const distDir = dirname(codingAgentEntry);
|
|
69
|
+
const interactiveModePath = pathToFileURL(
|
|
70
|
+
join(distDir, "modes/interactive/interactive-mode.js"),
|
|
71
|
+
).href;
|
|
72
|
+
return (await import(interactiveModePath)) as InteractiveModeModule;
|
|
73
|
+
} catch (error: unknown) {
|
|
74
|
+
warnStatusPatchUnavailable(error);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function applyThinkingLevelStatusPatch(
|
|
80
|
+
options: ThinkingLevelStatusPatchOptions = {},
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const patchState = getStatusPatchState();
|
|
83
|
+
patchState.shouldShowThinkingLevelStatus =
|
|
84
|
+
options.shouldShowThinkingLevelStatus ?? shouldShowThinkingLevelStatus;
|
|
85
|
+
|
|
86
|
+
const loadModule = options.loadInteractiveModeModule ?? loadInteractiveModeModule;
|
|
87
|
+
const module = await loadModule();
|
|
88
|
+
const prototype = module?.InteractiveMode?.prototype;
|
|
89
|
+
if (prototype === undefined) return;
|
|
90
|
+
if (prototype[STATUS_PATCH_MARKER] === true) return;
|
|
91
|
+
if (typeof prototype.showStatus !== "function") {
|
|
92
|
+
warnStatusPatchUnavailable(new Error("InteractiveMode.showStatus is not a function"));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const showStatusDescriptor = Object.getOwnPropertyDescriptor(prototype, "showStatus");
|
|
97
|
+
const showStatus: unknown = showStatusDescriptor?.value;
|
|
98
|
+
if (!isShowStatus(showStatus)) {
|
|
99
|
+
warnStatusPatchUnavailable(new Error("InteractiveMode.showStatus descriptor is invalid"));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
prototype.showStatus = function patchedShowStatus(
|
|
104
|
+
this: InteractiveModePrototype,
|
|
105
|
+
message: string,
|
|
106
|
+
): void {
|
|
107
|
+
if (
|
|
108
|
+
message.startsWith(THINKING_LEVEL_STATUS_PREFIX) &&
|
|
109
|
+
patchState.shouldShowThinkingLevelStatus() === false
|
|
110
|
+
) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
showStatus.call(this, message);
|
|
114
|
+
};
|
|
115
|
+
prototype[STATUS_PATCH_MARKER] = true;
|
|
116
|
+
}
|
package/src/storage.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
@@ -6,12 +6,26 @@ export function getGlobalAgentDir(): string {
|
|
|
6
6
|
return getAgentDir();
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
const EXTENSION_ID = "pi-mode";
|
|
10
|
+
const CONFIG_FILE = "config.json";
|
|
11
|
+
const SCHEMA_FILE = "config.schema.json";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_MODES_CONFIG_FILE = {
|
|
14
|
+
$schema: `./${SCHEMA_FILE}`,
|
|
15
|
+
version: 1,
|
|
16
|
+
currentMode: "default",
|
|
17
|
+
modeShowName: false,
|
|
18
|
+
modeUseThinkingBorderColors: false,
|
|
19
|
+
modeShowThinkingLevelStatus: false,
|
|
20
|
+
modes: {},
|
|
21
|
+
};
|
|
22
|
+
|
|
9
23
|
export function getGlobalModesPath(): string {
|
|
10
|
-
return path.join(getGlobalAgentDir(),
|
|
24
|
+
return path.join(getGlobalAgentDir(), EXTENSION_ID, CONFIG_FILE);
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
export function getProjectModesPath(cwd: string): string {
|
|
14
|
-
return path.join(cwd,
|
|
28
|
+
return path.join(cwd, CONFIG_DIR_NAME, EXTENSION_ID, CONFIG_FILE);
|
|
15
29
|
}
|
|
16
30
|
|
|
17
31
|
export async function fileExists(filePath: string): Promise<boolean> {
|
|
@@ -27,6 +41,71 @@ export async function ensureDirForFile(filePath: string): Promise<void> {
|
|
|
27
41
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
28
42
|
}
|
|
29
43
|
|
|
44
|
+
function getSchemaPath(configPath: string): string {
|
|
45
|
+
return path.join(path.dirname(configPath), SCHEMA_FILE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function writeIfMissing(filePath: string, content: string): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
await ensureDirForFile(filePath);
|
|
51
|
+
await fs.writeFile(filePath, content, { encoding: "utf8", flag: "wx" });
|
|
52
|
+
} catch (error: unknown) {
|
|
53
|
+
if (error instanceof Error && (error as NodeJS.ErrnoException).code === "EEXIST") return;
|
|
54
|
+
if (error instanceof Error) throw error;
|
|
55
|
+
throw new Error(String(error));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function refreshSchemaFile(filePath: string, content: string): Promise<void> {
|
|
60
|
+
let temporaryPath: string | undefined;
|
|
61
|
+
try {
|
|
62
|
+
await ensureDirForFile(filePath);
|
|
63
|
+
try {
|
|
64
|
+
if ((await fs.readFile(filePath, "utf8")) === content) return;
|
|
65
|
+
} catch (error: unknown) {
|
|
66
|
+
if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const nextTemporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
72
|
+
await fs.writeFile(nextTemporaryPath, content, { encoding: "utf8", flag: "wx" });
|
|
73
|
+
temporaryPath = nextTemporaryPath;
|
|
74
|
+
await fs.rename(temporaryPath, filePath);
|
|
75
|
+
temporaryPath = undefined;
|
|
76
|
+
} catch (error: unknown) {
|
|
77
|
+
if (temporaryPath !== undefined) {
|
|
78
|
+
try {
|
|
79
|
+
await fs.unlink(temporaryPath);
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore cleanup failure while reporting the original scaffold failure.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (error instanceof Error) throw error;
|
|
85
|
+
throw new Error(String(error));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readBundledSchema(): Promise<string | undefined> {
|
|
90
|
+
try {
|
|
91
|
+
return await fs.readFile(new URL("../config.schema.json", import.meta.url), "utf8");
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function scaffoldGlobalModesConfig(): Promise<void> {
|
|
98
|
+
const globalConfigPath = getGlobalModesPath();
|
|
99
|
+
const schema = await readBundledSchema();
|
|
100
|
+
if (schema !== undefined) {
|
|
101
|
+
await refreshSchemaFile(getSchemaPath(globalConfigPath), schema);
|
|
102
|
+
}
|
|
103
|
+
await writeIfMissing(
|
|
104
|
+
globalConfigPath,
|
|
105
|
+
`${JSON.stringify(DEFAULT_MODES_CONFIG_FILE, null, 2)}\n`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
30
109
|
export async function getMtimeMs(filePath: string): Promise<number | null> {
|
|
31
110
|
try {
|
|
32
111
|
const stat = await fs.stat(filePath);
|