pi-profile-switch 0.7.0 → 0.9.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.
@@ -1,86 +1,60 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "title": "pi-profile-switch profile catalog",
4
- "description": "Named profile definitions (global ~/.pi-profile-switch/profiles.json, legacy fallback ~/.pi/agent/profiles.json; or project .pi/profiles.json). Definitions are complete and self-contained: there is no inheritance field, and resource references are names/globs, never copies. The built-in \"default\" profile must not be defined here.",
3
+ "title": "pi-profile-switch profile definition",
4
+ "description": "A named profile definition stored in ~/.pi-profile-switch/profiles/<name>.json or <project>/.pi/profiles/<name>.json. Definitions are complete and self-contained: there is no inheritance field, and resource references are names/globs, never copies.",
5
5
  "type": "object",
6
- "required": [
7
- "schemaVersion",
8
- "profiles"
9
- ],
10
6
  "additionalProperties": false,
11
7
  "properties": {
12
- "schemaVersion": {
13
- "const": 1
8
+ "label": {
9
+ "type": "string",
10
+ "description": "Display label in the selector and /profile list."
14
11
  },
15
- "profiles": {
16
- "type": "object",
17
- "propertyNames": {
18
- "not": {
19
- "const": "default"
20
- }
12
+ "description": {
13
+ "type": "string"
14
+ },
15
+ "skills": {
16
+ "type": "array",
17
+ "items": {
18
+ "type": "string"
21
19
  },
22
- "additionalProperties": {
23
- "$ref": "#/$defs/profile"
24
- }
25
- }
26
- },
27
- "$defs": {
28
- "profile": {
29
- "type": "object",
30
- "additionalProperties": false,
31
- "properties": {
32
- "label": {
33
- "type": "string",
34
- "description": "Display label in the selector and /profile list."
35
- },
36
- "description": {
37
- "type": "string"
38
- },
39
- "skills": {
40
- "type": "array",
41
- "items": {
42
- "type": "string"
43
- },
44
- "description": "Skill names or globs (e.g. \"review\", \"internal-*\") referencing discovered skills."
45
- },
46
- "extensions": {
47
- "type": "array",
48
- "items": {
49
- "type": "string"
50
- },
51
- "description": "Extension names or globs. Package names, source aliases, loose-file stems, glob patterns, or direct file paths, resolved against discovered extensions."
52
- },
53
- "mcps": {
54
- "type": "array",
55
- "items": {
56
- "type": "string"
57
- },
58
- "description": "MCP server names or globs, discovered by pi-mcp-adapter configuration. Connection details stay in adapter-managed config."
59
- },
60
- "tools": {
61
- "type": "array",
62
- "items": {
63
- "type": "string"
64
- },
65
- "description": "Tool names or globs expanded against Pi's live registry (including extension tools)."
66
- },
67
- "defaultProvider": {
68
- "type": "string",
69
- "description": "Startup provider for this profile (e.g. \"anthropic\", \"openai\"). Mirrors Pi's settings.defaultProvider."
70
- },
71
- "defaultModel": {
72
- "type": "string",
73
- "description": "Startup model ID for this profile. Mirrors Pi's settings.defaultModel."
74
- },
75
- "defaultThinkingLevel": {
76
- "type": "string",
77
- "description": "Startup thinking level for this profile. Mirrors Pi's settings.defaultThinkingLevel."
78
- },
79
- "instructions": {
80
- "type": "string",
81
- "description": "Appended to Pi's fully built system prompt on every turn."
82
- }
83
- }
20
+ "description": "Skill names or globs (e.g. \"review\", \"internal-*\") referencing discovered skills."
21
+ },
22
+ "extensions": {
23
+ "type": "array",
24
+ "items": {
25
+ "type": "string"
26
+ },
27
+ "description": "Extension names or globs. Package names, source aliases, loose-file stems, glob patterns, or direct file paths, resolved against discovered extensions."
28
+ },
29
+ "mcps": {
30
+ "type": "array",
31
+ "items": {
32
+ "type": "string"
33
+ },
34
+ "description": "MCP server names or globs, discovered by pi-mcp-adapter configuration. Connection details stay in adapter-managed config."
35
+ },
36
+ "tools": {
37
+ "type": "array",
38
+ "items": {
39
+ "type": "string"
40
+ },
41
+ "description": "Tool names or globs expanded against Pi's live registry (including extension tools)."
42
+ },
43
+ "defaultProvider": {
44
+ "type": "string",
45
+ "description": "Startup provider for this profile (e.g. \"anthropic\", \"openai\"). Mirrors Pi's settings.defaultProvider."
46
+ },
47
+ "defaultModel": {
48
+ "type": "string",
49
+ "description": "Startup model ID for this profile. Mirrors Pi's settings.defaultModel."
50
+ },
51
+ "defaultThinkingLevel": {
52
+ "type": "string",
53
+ "description": "Startup thinking level for this profile. Mirrors Pi's settings.defaultThinkingLevel."
54
+ },
55
+ "instructions": {
56
+ "type": "string",
57
+ "description": "Appended to Pi's fully built system prompt on every turn."
84
58
  }
85
59
  }
86
60
  }
@@ -0,0 +1,167 @@
1
+ ---
2
+ name: profile-config
3
+ description: 指导创建、修改或删除 pi-profile-switch 的 profile。当用户想要创建、修改、配置或删除 profile 时触发。
4
+ ---
5
+
6
+ # profile-config
7
+
8
+ 本 skill 指导 agent 理解用户的模糊需求或明确指令,协助用户创建、修改或删除 `pi-profile-switch` 的 profile 文件。
9
+
10
+ > **声明**:本 skill 文件由 `pi-profile-switch` package 安装分发并在随包升级时自动覆写,请勿手动修改此文件。
11
+
12
+ ---
13
+
14
+ ## 1. 核心概念与约束
15
+
16
+ ### 1.1 Profile 与 Catalog
17
+ - **Profile**:命名的能力定义,引用 skills、extensions、MCP servers 与 tools,可选声明 model、thinking level 与 instructions。
18
+ - **Catalog**:保存 profile 定义的 `profiles/` 目录。每个 profile 对应目录下的一个独立 JSON 文件:`<name>.json`。
19
+ - **default profile**:由 Pi 提供,不可删除、不可编辑的 profile,加载 Pi 可发现的全部资源。**严禁**在任何 profiles 目录下创建 `default.json`。
20
+
21
+ ### 1.2 名字字符集规范
22
+ Profile 名字必须完全匹配正则:
23
+ ```regex
24
+ ^[A-Za-z0-9][A-Za-z0-9._-]*$
25
+ ```
26
+ - 必须以英文字母或数字开头。
27
+ - 只允许英文字母、数字、点(`.`)、下划线(`_`)和连字符(`-`)。
28
+ - 不允许包含空格、中文或特殊符号。
29
+
30
+ ### 1.3 作用域(Source scope)与存储落点
31
+ Profile 文件存放在两个位置之一:
32
+
33
+ | 作用域 | 路径 | 说明 |
34
+ | --- | --- | --- |
35
+ | **全局(global)** | `$PI_PROFILE_SWITCH_DIR/profiles/<name>.json`<br>(缺省为 `~/.pi-profile-switch/profiles/<name>.json`) | 对所有项目通用。若环境变量 `PI_PROFILE_SWITCH_DIR` 存在且非空,则以其下的 `profiles/` 目录为准。 |
36
+ | **项目(project)** | `<projectDir>/.pi/profiles/<name>.json` | 仅在当前项目生效,且仅当项目已受信任时可用。 |
37
+
38
+ - **覆盖规则**:项目 scope 的同名 profile 会完整替换(replace)全局条目,**不会**与全局配置合并字段。
39
+ - **项目信任门禁**:若当前项目未受信任,项目 scope 的 profile 无法解析,向项目 scope 写入也会失败。写入项目 scope 前若项目未受信任,必须提示用户在会话中执行 `/trust` 并重启 Pi。
40
+
41
+ ---
42
+
43
+ ## 2. Profile 文件格式与字段定义
44
+
45
+ 文件内容必须为格式化 JSON,**顶层即为裸定义对象**,严禁包裹 `schemaVersion`、`profiles` 或其他外层信封字段。
46
+
47
+ 全部字段均为可选(optional)。未声明的字段保持原生 Pi 行为或当前状态,不产生任何副作用。
48
+
49
+ ### 字段详细语义
50
+
51
+ | 字段 | 类型 | 语义与约束 |
52
+ | --- | --- | --- |
53
+ | `label` | `string` | 人类可读的显示名称(如 `"Code Review"`)。 |
54
+ | `description` | `string` | Profile 的简要描述(如 `"Read-only review profile"`)。 |
55
+ | `skills` | `string[]` | 引用的 skill 名称或 glob 列表。未声明时不收窄可用 skills。 |
56
+ | `extensions` | `string[]` | 引用的 extension 标识或 glob 列表。未声明时不收窄可用 extensions。 |
57
+ | `mcps` | `string[]` | 引用的 MCP server 名称或 glob 列表。未声明时不依赖 `pi-mcp-adapter`。 |
58
+ | `tools` | `string[]` | 白名单工具名称或 glob 列表。未声明时不收窄工具,保持 Pi 原生工具集合。 |
59
+ | `defaultProvider` | `string` | 默认模型提供商(如 `"anthropic"`、`"openai"`)。与 `defaultModel` 必须同时声明才生效。 |
60
+ | `defaultModel` | `string` | 默认模型名称(如 `"claude-sonnet-4-5"`)。与 `defaultProvider` 必须同时声明才生效。 |
61
+ | `defaultThinkingLevel` | `string` | 默认思考等级,可选值:`"off"`、`"minimal"`、`"low"`、`"medium"`、`"high"`、`"xhigh"`、`"max"`。仅在模型声明成立时生效。 |
62
+ | `instructions` | `string` | 激活该 profile 时追加到系统提示词的指令文本。 |
63
+
64
+ ### 示例格式
65
+ ```json
66
+ {
67
+ "label": "Review Mode",
68
+ "description": "Read-only code review with linting and analysis tools",
69
+ "skills": [
70
+ "profile-config"
71
+ ],
72
+ "extensions": [],
73
+ "mcps": [],
74
+ "tools": [
75
+ "read",
76
+ "grep",
77
+ "find",
78
+ "ls"
79
+ ],
80
+ "defaultProvider": "anthropic",
81
+ "defaultModel": "claude-sonnet-4-5",
82
+ "defaultThinkingLevel": "high",
83
+ "instructions": "Focus on code quality, security, and edge cases. Do not edit files."
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 3. 可引用资源发现指引
90
+
91
+ 当帮助用户配置 profile 时,可检查或参考以下位置发现用户当前已有的可用资源:
92
+
93
+ 1. **Skills**:
94
+ - 发现位置:`<agentDir>/skills/` 以及全局 `~/.agents/skills/`。
95
+ - 引用身份:Pi 的 skill 名称(即 skill 目录下的 `SKILL.md` frontmatter 中声明的 `name`,或目录名)。
96
+ - 支持 glob(如 `"git-*"`)。
97
+ 2. **Extensions**:
98
+ - 发现位置:`<agentDir>/settings.json` 中声明的已安装 packages、`<agentDir>/extensions/` 下的散装文件(`.ts` 或 `.js`)。
99
+ - 引用形式:
100
+ - 已安装包的包名或 source 别名。
101
+ - 多入口包的入口 ID:`<包名>:<相对路径>`。
102
+ - 散装文件 ID:相对扩展目录的路径去掉 `.ts`/`.js`(如 `sub/index.ts` 引用为 `sub`)。
103
+ - 绝对路径或 `~/` 路径。
104
+ - glob 匹配。
105
+ 3. **MCP Servers**:
106
+ - 发现位置:`pi-mcp-adapter` 识别的标准配置位置——全局侧 `~/.config/mcp/mcp.json`、`~/.agents/mcp.json`、`~/.agents/mcp/mcp.json`、`<agentDir>/mcp.json`;受信任项目另有 `<projectDir>/.mcp.json` 与 `<projectDir>/.pi/mcp.json`。
107
+ - 引用身份:上述配置文件中 `mcpServers` 对象下的 server 键名。
108
+ - 声明了 `mcps` 的 profile 需要同时确保 `pi-mcp-adapter` extension 处于可用状态。
109
+ 4. **Tools**:
110
+ - 引用身份:Pi 实时工具注册表中的工具名。
111
+ - 包括内建工具(`read`、`write`、`edit`、`bash` 等)、extension 贡献的工具、以及 MCP server 暴露的工具(代理工具 `mcp__<server>` 与直接工具 `<server>_<tool>`)。
112
+ - 支持 glob(如 `"mcp__*"`、`"github_*"`)。
113
+ 5. **项目级资源的收窄边界(重要)**:
114
+ - Profile 的资源选择(`skills`、`extensions`)**仅作用于用户级资源**(真实 agentDir 与 `~/.agents/skills`)。
115
+ - 项目级资源(如项目 `.pi/skills`、项目 `.pi/extensions`、上级 `.agents/skills`)的可见性由 Pi 项目信任判定决定:在受信任项目中,它们在**任何** profile 下都始终可见;在未受信任项目中均不可见。
116
+ - 因此,项目级资源的可见性**不随 profile 收窄**,无需也不指导在 profile 中声明项目级资源。
117
+
118
+ ---
119
+
120
+ ## 4. 创作时的默认注入规则
121
+
122
+ 当为用户创建或生成声明了 `skills` 的新 profile 时,必须遵守以下约定:
123
+
124
+ 1. **默认注入 `"profile-config"`**:
125
+ - 若生成的 profile 声明了 `skills` 数组,默认在 `skills` 列表中包含 `"profile-config"`,以保证切入该 profile 后用户仍可继续通过本 skill 配置 profile。
126
+ - **例外 1**:用户明确要求不包含 `"profile-config"` 时除外。
127
+ - **例外 2**:若 `skills` 列表中已包含具有覆盖性的 glob(例如 `"*"`),则无需重复显式添加 `"profile-config"`。
128
+ 2. **未声明 `skills` 时不动作**:
129
+ - 若 profile 未声明 `skills` 字段,表示不收窄 skills,全部 skill(包括 `profile-config`)天然可用,因此绝对不要主动添加 `skills` 字段。
130
+
131
+ ---
132
+
133
+ ## 5. 交互与执行流程
134
+
135
+ ### 5.1 创建(Create)
136
+ 1. **需求澄清**:根据用户自然语言描述(如「帮我配一个用于安全审计的只读 profile」),明确:
137
+ - 目标名称(校验符合 `^[A-Za-z0-9][A-Za-z0-9._-]*$`,且非 `default`)。
138
+ - 目标 scope(全局还是项目级)。
139
+ - 需要收窄的工具、skills、extensions、MCP servers 或特定模型设定。
140
+ 2. **资源与环境核对**:根据上述规则构造合法 JSON 定义,应用创作时默认注入规则。
141
+ 3. **写入文件**:
142
+ - 全局路径:`$PI_PROFILE_SWITCH_DIR/profiles/<name>.json`(默认 `~/.pi-profile-switch/profiles/<name>.json`)。
143
+ - 项目路径:`<projectDir>/.pi/profiles/<name>.json`。
144
+ - 确保目录存在,写入格式化的 JSON。
145
+ 4. **提示用户生效**:告知用户可通过 `/profile reload` 或 `/profile use <name>` 立即使用新 profile。
146
+
147
+ ### 5.2 修改(Edit)
148
+ 1. 读取目标 profile 文件已有内容。
149
+ 2. 根据用户要求调整对应字段,保持其余字段完整。
150
+ 3. 校验并写回格式化 JSON。
151
+ 4. 提示用户执行 `/profile reload`。
152
+
153
+ ### 5.3 删除(Delete)
154
+ 1. 确认要删除的 profile 存在于指定 scope。
155
+ 2. 严禁尝试删除 `default` profile。
156
+ 3. 删除对应的 `<name>.json` 文件。若当前正在使用该 profile,提醒用户先切换到其他 profile(如 `/profile use default`)。
157
+
158
+ ---
159
+
160
+ ## 6. 边界与退化处理
161
+
162
+ 1. **只读 / 无 `write` 工具环境**:
163
+ - 如果当前会话处于收窄工具的 profile 中(例如没有 `write` 工具的只读模式):
164
+ - 退化为在回复中输出完整的格式化 JSON 内容与建议保存的文件绝对路径,建议用户手动保存或切换至具备文件写入能力的 profile(如 `/profile use default`)后再行保存。
165
+ 2. **未受信任项目**:
166
+ - 如果需要写入项目 scope(`<projectDir>/.pi/profiles/`),而当前项目尚未受信任:
167
+ - 必须向用户说明项目未受信任无法生效,提示用户执行 `/trust` 并重启 Pi,或改将 profile 保存至全局 scope。
@@ -1,109 +1,86 @@
1
1
  /**
2
- * ProfileCatalogStore: the WRITE side of a profile catalog file
3
- * (ticket 09), kept separate from the read-only ProfileCatalog.
2
+ * ProfileCatalogStore: the WRITE side of profile catalog files,
3
+ * storing each profile in its own `<name>.json` file.
4
4
  *
5
5
  * Invariants:
6
- * - Whole-file overwrites (pretty-printed, schemaVersion envelope);
7
- * wizard saves never block on concurrent edits — re-read at write time,
8
- * same-name conflicts resolve last-write-wins.
9
- * - Definitions are complete and self-contained: no inheritance fields
10
- * (`extends`, merge, array append) exist or are accepted.
6
+ * - Single-file operations (pretty-printed JSON without schemaVersion envelope).
7
+ * - Writes land in `<dir>/<name>.json` via a temporary file + rename.
8
+ * - Names must match `PROFILE_NAME_PATTERN` and cannot be "default".
9
+ * - Definitions are complete and self-contained: no inheritance fields exist.
11
10
  * - Definitions are re-parsed through the catalog's own
12
- * `parseProfileDefinition`, so anything written is loadable;
13
- * "default" is built in and can never be written.
14
- * - Writes land in exactly one scope file (global or project).
11
+ * `parseProfileDefinition`, so anything written is loadable.
12
+ * - Writes land in exactly one scope directory (global or project).
15
13
  */
16
14
 
17
- import { mkdir, writeFile } from "node:fs/promises";
15
+ import { mkdir, rename, rm, unlink, writeFile } from "node:fs/promises";
18
16
  import path from "node:path";
19
17
 
20
- import { isRecord, readJsonFile } from "./json-file.ts";
21
18
  import {
22
19
  CatalogError,
23
20
  DEFAULT_PROFILE_NAME,
21
+ loadCatalogDirectory,
24
22
  parseProfileDefinition,
25
- PROFILE_SCHEMA_VERSION,
23
+ PROFILE_NAME_PATTERN,
26
24
  type ProfileDefinition,
27
25
  } from "./profile-catalog.ts";
28
26
 
29
27
  export class ProfileCatalogStore {
30
- readonly #filePath: string;
31
- readonly #fallbackPath?: string;
28
+ readonly #dirPath: string;
32
29
 
33
- constructor(catalogPath: string, fallbackPath?: string) {
34
- this.#filePath = catalogPath;
35
- this.#fallbackPath = fallbackPath;
30
+ constructor(catalogDir: string) {
31
+ this.#dirPath = catalogDir;
36
32
  }
37
33
 
38
- /** Validated definitions: missing file → empty; malformed → CatalogError
34
+ /** Validated definitions: missing directory → empty; malformed → CatalogError
39
35
  * (catalog errors never pass silently, even on the write path). */
40
36
  async readDefinitions(): Promise<Map<string, ProfileDefinition>> {
41
- let result = await readJsonFile(this.#filePath);
42
- if (!result.ok && result.reason === "missing" && this.#fallbackPath) {
43
- result = await readJsonFile(this.#fallbackPath);
44
- }
45
- if (!result.ok) {
46
- if (result.reason === "missing") return new Map();
47
- throw new CatalogError(`invalid JSON in ${this.#filePath}`);
48
- }
49
- if (!isRecord(result.value)) {
50
- throw new CatalogError(`${this.#filePath}: catalog must be an object`);
51
- }
52
- if (result.value.schemaVersion !== PROFILE_SCHEMA_VERSION) {
53
- throw new CatalogError(
54
- `${this.#filePath}: unsupported schemaVersion ${JSON.stringify(result.value.schemaVersion)} (expected ${PROFILE_SCHEMA_VERSION})`,
55
- );
56
- }
57
- if (!isRecord(result.value.profiles)) {
58
- throw new CatalogError(`${this.#filePath}: "profiles" must be an object mapping names to definitions`);
59
- }
60
- const definitions = new Map<string, ProfileDefinition>();
61
- for (const [name, raw] of Object.entries(result.value.profiles)) {
62
- if (name === DEFAULT_PROFILE_NAME) {
63
- throw new CatalogError(
64
- `${this.#filePath}: "${DEFAULT_PROFILE_NAME}" is built in and must not be defined in the catalog`,
65
- );
66
- }
67
- definitions.set(name, parseProfileDefinition(name, raw));
68
- }
69
- return definitions;
70
- }
71
-
72
- /** Overwrites the file with the given definitions (last write wins). */
73
- async writeDefinitions(definitions: ReadonlyMap<string, ProfileDefinition>): Promise<void> {
74
- const profiles: Record<string, unknown> = {};
75
- for (const [name, definition] of [...definitions.entries()].sort(([a], [b]) => a.localeCompare(b))) {
76
- // Round-trip through the parser: only declared, validated fields
77
- // are written back (self-contained; no unknown keys survive).
78
- profiles[name] = parseProfileDefinition(name, definition);
79
- }
80
- await mkdir(path.dirname(this.#filePath), { recursive: true });
81
- await writeFile(
82
- this.#filePath,
83
- `${JSON.stringify({ schemaVersion: PROFILE_SCHEMA_VERSION, profiles }, null, 2)}\n`,
84
- );
37
+ return loadCatalogDirectory(this.#dirPath);
85
38
  }
86
39
 
87
- /** Inserts or replaces one complete definition. */
40
+ /** Inserts or replaces one complete definition (via atomic rename). */
88
41
  async upsert(name: string, definition: ProfileDefinition): Promise<void> {
89
- if (name.trim().length === 0) {
90
- throw new CatalogError(`profile name must be non-empty`);
42
+ if (!PROFILE_NAME_PATTERN.test(name)) {
43
+ throw new CatalogError(`invalid profile name "${name}" (must match ${PROFILE_NAME_PATTERN})`);
91
44
  }
92
45
  if (name === DEFAULT_PROFILE_NAME) {
93
46
  throw new CatalogError(`"${DEFAULT_PROFILE_NAME}" is built in and must not be defined in the catalog`);
94
47
  }
95
- const definitions = await this.readDefinitions();
96
- // Validate before mutating: the wizard's definition must parse.
97
- definitions.set(name, parseProfileDefinition(name, definition));
98
- await this.writeDefinitions(definitions);
48
+
49
+ // Validate before mutating: the definition must parse.
50
+ const validated = parseProfileDefinition(name, definition);
51
+
52
+ await mkdir(this.#dirPath, { recursive: true });
53
+ const targetPath = path.join(this.#dirPath, `${name}.json`);
54
+ const tempPath = path.join(
55
+ this.#dirPath,
56
+ `.${name}.json.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`,
57
+ );
58
+
59
+ try {
60
+ await writeFile(tempPath, `${JSON.stringify(validated, null, 2)}\n`);
61
+ await rename(tempPath, targetPath);
62
+ } catch (error) {
63
+ await rm(tempPath, { force: true });
64
+ throw error;
65
+ }
99
66
  }
100
67
 
101
68
  /** Removes one profile; unknown names are a loud error, not a no-op. */
102
69
  async remove(name: string): Promise<void> {
103
- const definitions = await this.readDefinitions();
104
- if (!definitions.delete(name)) {
105
- throw new CatalogError(`profile "${name}" not found in ${this.#filePath}`);
70
+ if (!PROFILE_NAME_PATTERN.test(name)) {
71
+ throw new CatalogError(`invalid profile name "${name}" (must match ${PROFILE_NAME_PATTERN})`);
72
+ }
73
+ if (name === DEFAULT_PROFILE_NAME) {
74
+ throw new CatalogError(`"${DEFAULT_PROFILE_NAME}" is built in and cannot be deleted`);
75
+ }
76
+ const targetPath = path.join(this.#dirPath, `${name}.json`);
77
+ try {
78
+ await unlink(targetPath);
79
+ } catch (error) {
80
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
81
+ throw new CatalogError(`profile "${name}" not found in ${this.#dirPath}`);
82
+ }
83
+ throw error;
106
84
  }
107
- await this.writeDefinitions(definitions);
108
85
  }
109
86
  }