| 148 | } |
| 149 | |
| 150 | async function resolveSkillsField( |
| 151 | pluginRoot: string, |
| 152 | raw: unknown, |
| 153 | diagnostics: PluginDiagnostic[], |
| 154 | ): Promise<readonly string[]> { |
| 155 | if (raw === undefined) return []; |
| 156 | const entries: string[] = []; |
| 157 | if (typeof raw === 'string') { |
| 158 | entries.push(raw); |
| 159 | } else if (Array.isArray(raw) && raw.every((entry) => typeof entry === 'string')) { |
| 160 | entries.push(...raw); |
| 161 | } else { |
| 162 | diagnostics.push({ severity: 'error', message: '"skills" must be a string or string[]' }); |
| 163 | return []; |
| 164 | } |
| 165 | |
| 166 | const resolved: string[] = []; |
| 167 | for (const entry of entries) { |
| 168 | if (!entry.startsWith('./')) { |
| 169 | diagnostics.push({ |
| 170 | severity: 'error', |
| 171 | message: `"skills" path must start with "./" (got "${entry}")`, |
| 172 | }); |
| 173 | continue; |
| 174 | } |
| 175 | const absolute = path.resolve(pluginRoot, entry); |
| 176 | let real: string; |
| 177 | try { |
| 178 | real = await realpath(absolute); |
| 179 | } catch { |
| 180 | real = absolute; |
| 181 | } |
| 182 | const rootReal = await realpath(pluginRoot).catch(() => pluginRoot); |
| 183 | if (!isWithin(real, rootReal)) { |
| 184 | diagnostics.push({ |
| 185 | severity: 'error', |
| 186 | message: `"skills" path resolves outside the plugin (${entry})`, |
| 187 | }); |
| 188 | continue; |
| 189 | } |
| 190 | if (!(await isDir(real))) { |
| 191 | diagnostics.push({ |
| 192 | severity: 'warn', |
| 193 | message: `"skills" path is not a directory (${entry})`, |
| 194 | }); |
| 195 | continue; |
| 196 | } |
| 197 | resolved.push(real); |
| 198 | } |
| 199 | return resolved; |
| 200 | } |
| 201 | |
| 202 | async function resolvePluginPathField(input: { |
| 203 | readonly pluginRoot: string; |