@lesliechan721/aw 0.1.0-beta.34 → 0.1.0-beta.36

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.
Files changed (102) hide show
  1. package/dist/scripts/test-plugin-scripts.js +1 -1
  2. package/dist/src/commands/aw-capabilities.d.ts +1 -1
  3. package/dist/src/commands/aw-capabilities.js +1 -0
  4. package/dist/src/commands/aw-capabilities.js.map +1 -1
  5. package/dist/src/commands/operation-plan.d.ts +19 -0
  6. package/dist/src/commands/operation-plan.js.map +1 -1
  7. package/dist/src/commands/rebuild-command.d.ts +1 -0
  8. package/dist/src/commands/rebuild-command.js +890 -8
  9. package/dist/src/commands/rebuild-command.js.map +1 -1
  10. package/dist/src/commands/rebuild-integration.d.ts +12 -0
  11. package/dist/src/commands/rebuild-integration.js +88 -2
  12. package/dist/src/commands/rebuild-integration.js.map +1 -1
  13. package/dist/src/commands/status-v6.js +109 -19
  14. package/dist/src/commands/status-v6.js.map +1 -1
  15. package/dist/src/commands/wizard.d.ts +5 -1
  16. package/dist/src/commands/wizard.js +4 -2
  17. package/dist/src/commands/wizard.js.map +1 -1
  18. package/dist/src/commands/write-commands.js +3 -0
  19. package/dist/src/commands/write-commands.js.map +1 -1
  20. package/dist/src/index.js +57 -4
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/portable/materializer.js +19 -3
  23. package/dist/src/portable/materializer.js.map +1 -1
  24. package/dist/src/review/review-profile.js +19 -2
  25. package/dist/src/review/review-profile.js.map +1 -1
  26. package/dist/src/tui/components.d.ts +39 -0
  27. package/dist/src/tui/components.js +51 -18
  28. package/dist/src/tui/components.js.map +1 -1
  29. package/dist/src/tui/config-array-draft.d.ts +29 -0
  30. package/dist/src/tui/config-array-draft.js +48 -0
  31. package/dist/src/tui/config-array-draft.js.map +1 -0
  32. package/dist/src/tui/config-value-presentation.d.ts +1 -0
  33. package/dist/src/tui/config-value-presentation.js +17 -1
  34. package/dist/src/tui/config-value-presentation.js.map +1 -1
  35. package/dist/src/tui/console-app-v4.js +1742 -1368
  36. package/dist/src/tui/console-app-v4.js.map +1 -1
  37. package/dist/src/tui/content-item-card.d.ts +13 -0
  38. package/dist/src/tui/content-item-card.js +51 -0
  39. package/dist/src/tui/content-item-card.js.map +1 -0
  40. package/dist/src/tui/controller-runtime.js +45 -4
  41. package/dist/src/tui/controller-runtime.js.map +1 -1
  42. package/dist/src/tui/controller.d.ts +18 -0
  43. package/dist/src/tui/controller.js +168 -12
  44. package/dist/src/tui/controller.js.map +1 -1
  45. package/dist/src/tui/dependency-graph-view.d.ts +39 -0
  46. package/dist/src/tui/dependency-graph-view.js +121 -0
  47. package/dist/src/tui/dependency-graph-view.js.map +1 -0
  48. package/dist/src/tui/dependency-graph.js +16 -19
  49. package/dist/src/tui/dependency-graph.js.map +1 -1
  50. package/dist/src/tui/entity-cards.d.ts +55 -0
  51. package/dist/src/tui/entity-cards.js +220 -0
  52. package/dist/src/tui/entity-cards.js.map +1 -0
  53. package/dist/src/tui/input-display.js +10 -1
  54. package/dist/src/tui/input-display.js.map +1 -1
  55. package/dist/src/tui/model.d.ts +11 -0
  56. package/dist/src/tui/model.js +20 -4
  57. package/dist/src/tui/model.js.map +1 -1
  58. package/dist/src/tui/operation-diagnostic-screens.d.ts +2 -18
  59. package/dist/src/tui/operation-diagnostic-screens.js +16 -69
  60. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -1
  61. package/dist/src/tui/operation-review-planner.js +44 -3
  62. package/dist/src/tui/operation-review-planner.js.map +1 -1
  63. package/dist/src/tui/plugin-content.d.ts +8 -0
  64. package/dist/src/tui/plugin-content.js +409 -65
  65. package/dist/src/tui/plugin-content.js.map +1 -1
  66. package/dist/src/tui/plugin-detail.d.ts +52 -0
  67. package/dist/src/tui/plugin-detail.js +165 -0
  68. package/dist/src/tui/plugin-detail.js.map +1 -0
  69. package/dist/src/tui/project-inventory.d.ts +68 -0
  70. package/dist/src/tui/project-inventory.js +203 -0
  71. package/dist/src/tui/project-inventory.js.map +1 -0
  72. package/dist/src/tui/query-header.d.ts +30 -0
  73. package/dist/src/tui/query-header.js +40 -0
  74. package/dist/src/tui/query-header.js.map +1 -0
  75. package/dist/src/tui/reader.d.ts +37 -0
  76. package/dist/src/tui/reader.js +47 -0
  77. package/dist/src/tui/reader.js.map +1 -0
  78. package/dist/src/tui/router.d.ts +26 -3
  79. package/dist/src/tui/router.js.map +1 -1
  80. package/dist/src/tui/screen-registry.d.ts +9 -1
  81. package/dist/src/tui/screen-registry.js +1 -1
  82. package/dist/src/tui/screen-registry.js.map +1 -1
  83. package/dist/src/tui/source-detail-card.d.ts +29 -0
  84. package/dist/src/tui/source-detail-card.js +45 -0
  85. package/dist/src/tui/source-detail-card.js.map +1 -0
  86. package/dist/src/tui/theme.d.ts +12 -0
  87. package/dist/src/tui/theme.js +12 -0
  88. package/dist/src/tui/theme.js.map +1 -1
  89. package/dist/src/tui/wizard.js +26 -4
  90. package/dist/src/tui/wizard.js.map +1 -1
  91. package/dist/src/types.d.ts +2 -0
  92. package/dist/src/types.js.map +1 -1
  93. package/dist/src/utils/catalog-source-references.js +2 -2
  94. package/dist/src/utils/local-marketplace.d.ts +1 -0
  95. package/dist/src/utils/local-marketplace.js +22 -0
  96. package/dist/src/utils/local-marketplace.js.map +1 -1
  97. package/dist/src/utils/plugin-config-write.js +3 -1
  98. package/dist/src/utils/plugin-config-write.js.map +1 -1
  99. package/dist/src/utils/plugin-config.d.ts +5 -5
  100. package/dist/src/utils/plugin-config.js +47 -23
  101. package/dist/src/utils/plugin-config.js.map +1 -1
  102. package/package.json +4 -4
@@ -1,9 +1,21 @@
1
1
  import path from 'node:path';
2
2
  import { open } from 'node:fs/promises';
3
+ import { stripVTControlCharacters } from 'node:util';
3
4
  import fs from 'fs-extra';
5
+ import YAML from 'yaml';
4
6
  import { parseSkillFrontmatter } from '../utils/skill-validation.js';
7
+ import { resolveSupportedPlatforms } from '../utils/platform-support.js';
5
8
  const MAX_FRONTMATTER_BYTES = 16 * 1024;
6
9
  const TOOL_ORDER = ['Codex', 'Claude Code', 'Agent Skills'];
10
+ function safeSummary(description, short) {
11
+ const safeDescription = stripVTControlCharacters(description)
12
+ .replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu, ' ')
13
+ .replace(/\s+/gu, ' ').trim();
14
+ const sentence = short ? safeDescription
15
+ : safeDescription.match(/^.*?(?:[。!?]|[.!?](?=\s|$)|$)/u)?.[0] ?? safeDescription;
16
+ const points = [...sentence];
17
+ return points.length > 120 ? `${points.slice(0, 119).join('')}…` : sentence || null;
18
+ }
7
19
  function sortTools(tools) {
8
20
  return [...new Set(tools)].sort((left, right) => TOOL_ORDER.indexOf(left) - TOOL_ORDER.indexOf(right));
9
21
  }
@@ -37,35 +49,164 @@ async function regularPathWithin(root, candidate, kind) {
37
49
  const real = await fs.realpath(lexical).catch(() => undefined);
38
50
  return real && within(root, real) ? real : null;
39
51
  }
52
+ async function unconfirmedPathCode(root, candidate) {
53
+ const lexical = path.resolve(candidate);
54
+ if (!within(root, lexical))
55
+ return 'unsafePath';
56
+ let current = root;
57
+ for (const segment of path.relative(root, lexical).split(path.sep).filter(Boolean)) {
58
+ current = path.join(current, segment);
59
+ let stats;
60
+ try {
61
+ stats = await fs.lstat(current);
62
+ }
63
+ catch (error) {
64
+ return error.code === 'ENOENT'
65
+ ? 'incompleteList' : 'unreadable';
66
+ }
67
+ if (stats.isSymbolicLink() || (!stats.isDirectory() && current !== lexical))
68
+ return 'unsafePath';
69
+ }
70
+ return 'unsafePath';
71
+ }
40
72
  async function readFrontmatterDescription(root, skillFile) {
41
73
  const safeFile = await regularPathWithin(root, skillFile, 'file');
42
74
  if (!safeFile)
43
75
  return { exists: false, description: null };
44
- const handle = await open(safeFile, 'r');
76
+ let handle;
77
+ try {
78
+ handle = await open(safeFile, 'r');
79
+ }
80
+ catch {
81
+ return { exists: true, description: null, issue: {
82
+ code: 'unreadable', message: '简介文件无法读取。请检查目录内容后重试。',
83
+ } };
84
+ }
45
85
  try {
46
86
  const chunks = [];
47
87
  let total = 0;
48
88
  let frontmatterEnd = -1;
89
+ let reachedEof = false;
49
90
  while (total < MAX_FRONTMATTER_BYTES && frontmatterEnd === -1) {
50
91
  const buffer = Buffer.alloc(Math.min(512, MAX_FRONTMATTER_BYTES - total));
51
92
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, total);
52
- if (bytesRead === 0)
93
+ if (bytesRead === 0) {
94
+ reachedEof = true;
53
95
  break;
96
+ }
54
97
  chunks.push(buffer.subarray(0, bytesRead));
55
98
  total += bytesRead;
56
99
  frontmatterEnd = Buffer.concat(chunks).toString('utf8').replace(/\r\n/gu, '\n').indexOf('\n---\n', 4);
57
100
  }
58
- if (frontmatterEnd === -1)
59
- return { exists: true, description: null };
60
101
  const normalized = Buffer.concat(chunks).toString('utf8').replace(/\r\n/gu, '\n');
61
- const header = normalized.slice(0, frontmatterEnd + '\n---\n'.length);
102
+ const closingAtEnd = frontmatterEnd === -1 && normalized.endsWith('\n---')
103
+ && (reachedEof || (await handle.stat()).size === total);
104
+ if (frontmatterEnd === -1 && !closingAtEnd)
105
+ return { exists: true, description: null, issue: {
106
+ code: 'invalidMetadata', message: '简介元信息无效或超过读取预算。请检查内容文件。',
107
+ } };
108
+ const header = closingAtEnd ? normalized : normalized.slice(0, frontmatterEnd + '\n---\n'.length);
62
109
  const parsed = parseSkillFrontmatter(header, 'SKILL.md');
63
- const description = parsed.frontmatter?.description;
110
+ if (parsed.errors.length > 0)
111
+ return { exists: true, description: null, issue: {
112
+ code: 'invalidMetadata', message: '简介元信息无法解析。请检查内容文件。',
113
+ } };
114
+ const metadata = parsed.frontmatter?.metadata;
115
+ const short = isRecord(metadata) ? metadata['short-description'] : undefined;
116
+ if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
117
+ return { exists: true, description: null, issue: {
118
+ code: 'invalidMetadata', message: '简短用途元信息无效。请检查内容文件。',
119
+ } };
120
+ }
121
+ const description = typeof short === 'string' && short.trim()
122
+ ? short : parsed.frontmatter?.description;
123
+ if (description !== undefined && typeof description !== 'string') {
124
+ return { exists: true, description: null, issue: {
125
+ code: 'invalidMetadata', message: '简介元信息类型无效。请检查内容文件。',
126
+ } };
127
+ }
64
128
  return {
65
129
  exists: true,
66
- description: typeof description === 'string' && description.trim() ? description.trim() : null,
130
+ description: typeof description === 'string' ? safeSummary(description, Boolean(short)) : null,
67
131
  };
68
132
  }
133
+ catch {
134
+ return { exists: true, description: null, issue: {
135
+ code: 'unreadable', message: '简介文件无法读取。请检查目录内容后重试。',
136
+ } };
137
+ }
138
+ finally {
139
+ await handle.close();
140
+ }
141
+ }
142
+ async function readCodexCommandDescription(root, skillRoot) {
143
+ const metadataFile = path.join(skillRoot, 'agents', 'openai.yaml');
144
+ const safeFile = await regularPathWithin(root, metadataFile, 'file');
145
+ if (!safeFile) {
146
+ const code = await unconfirmedPathCode(root, metadataFile);
147
+ return { description: null, issue: {
148
+ code, message: 'Slash command 简介文件不可安全确认。请检查内容目录。',
149
+ } };
150
+ }
151
+ let handle;
152
+ try {
153
+ handle = await open(safeFile, 'r');
154
+ }
155
+ catch {
156
+ return { description: null, issue: {
157
+ code: 'unreadable', message: 'Slash command 简介文件无法读取。请检查内容目录。',
158
+ } };
159
+ }
160
+ try {
161
+ const size = (await handle.stat()).size;
162
+ if (size > MAX_FRONTMATTER_BYTES)
163
+ return { description: null, issue: {
164
+ code: 'invalidMetadata', message: 'Slash command 简介元信息超过读取预算。请检查内容文件。',
165
+ } };
166
+ const buffer = Buffer.alloc(size);
167
+ let total = 0;
168
+ while (total < size) {
169
+ const { bytesRead } = await handle.read(buffer, total, size - total, total);
170
+ if (bytesRead === 0)
171
+ return { description: null, issue: {
172
+ code: 'unreadable', message: 'Slash command 简介文件无法完整读取。请检查内容目录。',
173
+ } };
174
+ total += bytesRead;
175
+ }
176
+ let metadata;
177
+ try {
178
+ const document = YAML.parseDocument(buffer.toString('utf8'), { prettyErrors: false });
179
+ if (document.errors.length > 0)
180
+ throw new Error('Invalid YAML');
181
+ metadata = document.toJS({ maxAliasCount: 0 });
182
+ }
183
+ catch {
184
+ return { description: null, issue: {
185
+ code: 'invalidMetadata', message: 'Slash command 简介元信息无法解析。请检查内容文件。',
186
+ } };
187
+ }
188
+ if (!isRecord(metadata) || (metadata.interface !== undefined && !isRecord(metadata.interface))) {
189
+ return { description: null, issue: {
190
+ code: 'invalidMetadata', message: 'Slash command 简介元信息无效。请检查内容文件。',
191
+ } };
192
+ }
193
+ const short = isRecord(metadata.interface) ? metadata.interface.short_description : undefined;
194
+ if (short === undefined)
195
+ return { description: null };
196
+ if (typeof short !== 'string' || !short.trim())
197
+ return { description: null, issue: {
198
+ code: 'invalidMetadata', message: 'Slash command 简短用途元信息无效。请检查内容文件。',
199
+ } };
200
+ const description = safeSummary(short, true);
201
+ return description ? { description } : { description: null, issue: {
202
+ code: 'invalidMetadata', message: 'Slash command 简短用途元信息无效。请检查内容文件。',
203
+ } };
204
+ }
205
+ catch {
206
+ return { description: null, issue: {
207
+ code: 'unreadable', message: 'Slash command 简介文件无法读取。请检查内容目录。',
208
+ } };
209
+ }
69
210
  finally {
70
211
  await handle.close();
71
212
  }
@@ -80,38 +221,108 @@ function addItem(items, item) {
80
221
  current.tools = sortTools([...current.tools, ...item.tools]);
81
222
  if (current.description === null && item.description !== null)
82
223
  current.description = item.description;
224
+ if (item.issue) {
225
+ current.descriptionState = 'issue';
226
+ current.issue = item.issue;
227
+ }
228
+ else if (current.descriptionState !== 'issue' && current.description !== null) {
229
+ current.descriptionState = 'available';
230
+ }
231
+ }
232
+ function addCoverageIssue(issues, issue) {
233
+ issues.push(issue);
83
234
  }
84
235
  async function addSkillDirectory(options) {
85
236
  const directory = await regularPathWithin(options.root, path.join(options.root, options.relativePath), 'directory');
86
237
  if (!directory) {
87
- options.notices.push(`${options.tool} Skill 清单不可安全读取。`);
238
+ const code = await unconfirmedPathCode(options.root, path.join(options.root, options.relativePath));
239
+ addCoverageIssue(options.coverageIssues, {
240
+ code, message: `${options.tool} Skill 清单无法确认。请检查目录。`,
241
+ });
242
+ return;
243
+ }
244
+ let entries;
245
+ try {
246
+ entries = await fs.readdir(directory, { withFileTypes: true });
247
+ }
248
+ catch {
249
+ addCoverageIssue(options.coverageIssues, {
250
+ code: 'unreadable', message: `${options.tool} Skill 清单无法读取。请检查目录后重试。`,
251
+ });
88
252
  return;
89
253
  }
90
- const entries = await fs.readdir(directory, { withFileTypes: true });
91
254
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
92
- if (!entry.isDirectory() || entry.isSymbolicLink())
255
+ if (entry.isSymbolicLink()) {
256
+ addCoverageIssue(options.coverageIssues, {
257
+ code: 'unsafePath', message: `${options.tool} Skill 清单含不安全路径。请检查目录。`,
258
+ });
259
+ continue;
260
+ }
261
+ if (!entry.isDirectory())
93
262
  continue;
94
263
  const skillRoot = await regularPathWithin(options.root, path.join(directory, entry.name), 'directory');
95
- if (!skillRoot)
264
+ if (!skillRoot) {
265
+ const code = await unconfirmedPathCode(options.root, path.join(directory, entry.name));
266
+ addCoverageIssue(options.coverageIssues, {
267
+ code, message: `${options.tool} Skill 清单含无法确认的条目。请检查目录。`,
268
+ });
96
269
  continue;
97
- const skill = await readFrontmatterDescription(options.root, path.join(skillRoot, 'SKILL.md'));
98
- if (!skill.exists)
270
+ }
271
+ if (options.tool === 'Codex'
272
+ && options.items.get(`command:${entry.name}`)?.tools.includes('Codex'))
99
273
  continue;
274
+ const skillFile = path.join(skillRoot, 'SKILL.md');
275
+ const hasClaudeCommand = options.items.has(`command:${entry.name}`);
276
+ const hasClaudeSkill = options.checkClaudeCommand
277
+ && options.items.get(`skill:${entry.name}`)?.tools.includes('Claude Code')
278
+ && await regularPathWithin(options.root, path.join(options.root, 'claude', 'skills', entry.name, 'SKILL.md'), 'file');
279
+ const uncertainSource = options.checkClaudeCommand && !hasClaudeCommand && !hasClaudeSkill;
280
+ const skill = options.readDescription === false || (uncertainSource && options.type !== 'command')
281
+ ? { exists: Boolean(await regularPathWithin(options.root, skillFile, 'file')), description: null }
282
+ : await readFrontmatterDescription(options.root, skillFile);
283
+ let issue = skill.issue;
284
+ if (!skill.exists) {
285
+ const code = await unconfirmedPathCode(options.root, skillFile);
286
+ if (code === 'incompleteList') {
287
+ addCoverageIssue(options.coverageIssues, {
288
+ code, message: `${options.tool} Skill 清单有无法确认的条目。请检查目录。`,
289
+ });
290
+ continue;
291
+ }
292
+ issue = { code, message: 'Skill 元信息文件不可安全读取。请检查内容目录后重试。' };
293
+ }
294
+ const type = options.type === 'command' && issue?.code === 'unsafePath'
295
+ ? 'skill' : options.type ?? 'skill';
296
+ if (uncertainSource) {
297
+ addCoverageIssue(options.coverageIssues, {
298
+ code: 'incompleteList',
299
+ message: options.type === 'command'
300
+ ? 'Slash command 的 Claude Code 组件无法确认。请检查内容目录。'
301
+ : 'Codex 内容名称已确认,但缺少可安全确认的 Claude Code 对应项,不能区分普通 Skill 与 Slash command。请检查内容目录。',
302
+ });
303
+ }
304
+ const command = type === 'command' && skill.exists
305
+ ? await readCodexCommandDescription(options.root, skillRoot) : undefined;
306
+ issue ??= command?.issue;
307
+ const description = command?.description ?? skill.description;
100
308
  addItem(options.items, {
101
- id: `skill:${entry.name}`,
309
+ id: `${type}:${entry.name}`,
102
310
  name: entry.name,
103
- type: 'skill',
104
- description: skill.description,
311
+ type,
312
+ description,
105
313
  tools: [options.tool],
314
+ descriptionState: issue ? 'issue' : description ? 'available' : 'absent',
315
+ ...(issue ? { issue } : {}),
106
316
  });
107
- if (skill.description === null)
108
- options.notices.push(`Skill ${entry.name} 没有安全可读的简短用途。`);
109
317
  }
110
318
  }
111
319
  async function addJsonComponents(options) {
112
320
  const file = await regularPathWithin(options.root, path.join(options.root, options.relativePath), 'file');
113
321
  if (!file) {
114
- options.notices.push(`${options.tool} ${options.type} 清单不可安全读取。`);
322
+ const code = await unconfirmedPathCode(options.root, path.join(options.root, options.relativePath));
323
+ addCoverageIssue(options.coverageIssues, {
324
+ code, message: `${options.tool} ${options.type} 清单无法确认。请检查目录。`,
325
+ });
115
326
  return;
116
327
  }
117
328
  let value;
@@ -119,14 +330,19 @@ async function addJsonComponents(options) {
119
330
  value = await fs.readJson(file);
120
331
  }
121
332
  catch {
122
- options.notices.push(`${options.tool} ${options.type} 清单不可安全读取。`);
333
+ addCoverageIssue(options.coverageIssues, {
334
+ code: 'unreadable', message: `${options.tool} ${options.type} 清单无法读取。请检查内容文件。`,
335
+ });
123
336
  return;
124
337
  }
125
338
  const collection = isRecord(value) ? value[options.property] : undefined;
126
- const entries = isRecord(collection) ? Object.keys(collection).sort() : [];
127
- if (entries.length === 0) {
128
- options.notices.push(`${options.tool} ${options.type} 清单没有可确认的组件名称。`);
339
+ if (!isRecord(collection)) {
340
+ addCoverageIssue(options.coverageIssues, {
341
+ code: 'incompleteList', message: `${options.tool} ${options.type} 清单格式无效。请检查内容文件。`,
342
+ });
343
+ return;
129
344
  }
345
+ const entries = Object.keys(collection).sort();
130
346
  for (const name of entries) {
131
347
  addItem(options.items, {
132
348
  id: `${options.type}:${name}`,
@@ -134,35 +350,83 @@ async function addJsonComponents(options) {
134
350
  type: options.type,
135
351
  description: null,
136
352
  tools: [options.tool],
353
+ descriptionState: 'absent',
137
354
  });
138
355
  }
139
356
  }
140
357
  async function addSlashCommands(options) {
141
358
  const directory = await regularPathWithin(options.root, path.join(options.root, 'claude', 'commands'), 'directory');
142
359
  if (!directory) {
143
- options.notices.push('Slash command 清单没有可安全确认的组件名称。');
360
+ const code = await unconfirmedPathCode(options.root, path.join(options.root, 'claude', 'commands'));
361
+ addCoverageIssue(options.coverageIssues, {
362
+ code, message: 'Slash command 清单无法确认。请检查目录。',
363
+ });
364
+ return;
365
+ }
366
+ let entries;
367
+ try {
368
+ entries = await fs.readdir(directory, { withFileTypes: true });
369
+ }
370
+ catch {
371
+ addCoverageIssue(options.coverageIssues, {
372
+ code: 'unreadable', message: 'Slash command 清单无法读取。请检查目录后重试。',
373
+ });
144
374
  return;
145
375
  }
146
- const entries = await fs.readdir(directory, { withFileTypes: true });
147
376
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
148
- if (!entry.isFile() || entry.isSymbolicLink() || path.extname(entry.name) !== '.md')
377
+ if (path.extname(entry.name) !== '.md')
149
378
  continue;
379
+ if (entry.isSymbolicLink() || !entry.isFile()) {
380
+ addCoverageIssue(options.coverageIssues, {
381
+ code: 'unsafePath', message: 'Slash command 清单含无法确认的条目。请检查目录。',
382
+ });
383
+ continue;
384
+ }
385
+ if (!await regularPathWithin(options.root, path.join(directory, entry.name), 'file')) {
386
+ addCoverageIssue(options.coverageIssues, {
387
+ code: 'unsafePath', message: 'Slash command 清单含不安全路径。请检查目录。',
388
+ });
389
+ continue;
390
+ }
150
391
  const name = path.basename(entry.name, '.md');
151
392
  const tools = ['Claude Code'];
152
393
  const codexSkillFile = path.join(options.root, 'codex', 'skills', name, 'SKILL.md');
153
394
  const codexCommand = options.hasCodex
154
395
  && await regularPathWithin(options.root, codexSkillFile, 'file');
396
+ if (options.hasCodex && !codexCommand) {
397
+ const code = await unconfirmedPathCode(options.root, codexSkillFile);
398
+ addCoverageIssue(options.coverageIssues, {
399
+ code, message: 'Slash command 的 Codex 组件无法安全确认。请检查内容目录。',
400
+ });
401
+ }
402
+ if (options.hasCodex && !codexCommand && await fs.lstat(codexSkillFile).catch(() => undefined)) {
403
+ if (await regularPathWithin(options.root, path.dirname(codexSkillFile), 'directory')) {
404
+ addItem(options.items, {
405
+ id: `skill:${name}`,
406
+ name,
407
+ type: 'skill',
408
+ description: null,
409
+ tools: ['Codex'],
410
+ descriptionState: 'issue',
411
+ issue: { code: 'unsafePath', message: 'Skill 元信息文件不可安全读取。请检查内容目录后重试。' },
412
+ });
413
+ }
414
+ }
415
+ const command = await readFrontmatterDescription(options.root, path.join(directory, entry.name));
416
+ const codexDescription = codexCommand
417
+ ? await readCodexCommandDescription(options.root, path.dirname(codexSkillFile)) : undefined;
418
+ const description = codexDescription?.description ?? command.description;
419
+ const issue = codexDescription?.issue ?? command.issue;
155
420
  if (codexCommand)
156
421
  tools.unshift('Codex');
157
- options.items.delete(`skill:${name}`);
158
422
  addItem(options.items, {
159
423
  id: `command:${name}`,
160
424
  name,
161
425
  type: 'command',
162
- description: codexCommand
163
- ? (await readFrontmatterDescription(options.root, codexSkillFile)).description
164
- : null,
426
+ description,
165
427
  tools,
428
+ descriptionState: issue ? 'issue' : description ? 'available' : 'absent',
429
+ ...(issue ? { issue } : {}),
166
430
  });
167
431
  }
168
432
  }
@@ -202,35 +466,105 @@ async function projectedPlatforms(root) {
202
466
  return tools;
203
467
  }
204
468
  export async function projectPluginContentOverview(options) {
205
- const root = await fs.realpath(options.plugin.rootDir);
469
+ let root;
470
+ try {
471
+ root = await fs.realpath(options.plugin.rootDir);
472
+ }
473
+ catch {
474
+ const message = '当前无法安全读取这个插件的目录内容。请检查来源状态后重试。';
475
+ return {
476
+ pluginId: options.pluginId,
477
+ pluginVersion: options.plugin.manifest.version,
478
+ catalogVersion: options.catalogVersion,
479
+ evidence: 'unavailable',
480
+ items: [],
481
+ notices: [message],
482
+ coverage: 'unavailable',
483
+ coverageIssue: { code: 'unreadable', message },
484
+ };
485
+ }
206
486
  const items = new Map();
207
- const notices = [];
487
+ const coverageIssues = [];
208
488
  const platforms = await projectedPlatforms(root);
489
+ const supportedPlatforms = resolveSupportedPlatforms(options.plugin.manifest);
490
+ for (const [platform, tool] of [
491
+ ['codex', 'Codex'], ['claude', 'Claude Code'],
492
+ ]) {
493
+ if (!supportedPlatforms.has(platform) || platforms.includes(tool))
494
+ continue;
495
+ const marker = path.join(root, `.${platform}-plugin`, 'plugin.json');
496
+ const code = await unconfirmedPathCode(root, marker);
497
+ addCoverageIssue(coverageIssues, {
498
+ code, message: `${tool} 平台清单无法安全确认。请检查目录。`,
499
+ });
500
+ }
501
+ const codexOnlyCommands = Boolean(options.plugin.manifest.slashCommands)
502
+ && platforms.includes('Codex') && !platforms.includes('Claude Code');
503
+ const mixedCodexSkills = Boolean(options.plugin.manifest.slashCommands)
504
+ && platforms.includes('Codex')
505
+ && Boolean(options.plugin.manifest.skills || options.plugin.manifest.buildConfig?.fileRules?.codex);
506
+ if (mixedCodexSkills && codexOnlyCommands) {
507
+ addCoverageIssue(coverageIssues, {
508
+ code: 'incompleteList',
509
+ message: 'Codex 内容名称已确认,但当前目录不能安全区分普通 Skill 与 Slash command。请查看来源定义。',
510
+ });
511
+ }
512
+ if (options.plugin.manifest.slashCommands && platforms.includes('Claude Code')) {
513
+ await addSlashCommands({
514
+ root,
515
+ hasCodex: platforms.includes('Codex'),
516
+ items,
517
+ coverageIssues,
518
+ });
519
+ }
209
520
  if (options.plugin.manifest.skills) {
210
- await Promise.all([
211
- ...(platforms.includes('Codex') ? [addSkillDirectory({
212
- root, relativePath: 'codex/skills', tool: 'Codex', items, notices,
213
- })] : []),
214
- ...(platforms.includes('Claude Code') ? [addSkillDirectory({
215
- root, relativePath: 'claude/skills', tool: 'Claude Code', items, notices,
216
- })] : []),
217
- ]);
521
+ if (platforms.includes('Claude Code')) {
522
+ await addSkillDirectory({
523
+ root, relativePath: 'claude/skills', tool: 'Claude Code', items, coverageIssues,
524
+ });
525
+ }
526
+ if (platforms.includes('Codex')) {
527
+ await addSkillDirectory({
528
+ root, relativePath: 'codex/skills', tool: 'Codex', items, coverageIssues,
529
+ ...(codexOnlyCommands ? { readDescription: false } : {}),
530
+ checkClaudeCommand: Boolean(options.plugin.manifest.slashCommands)
531
+ && platforms.includes('Claude Code'),
532
+ });
533
+ }
534
+ }
535
+ if (codexOnlyCommands && !options.plugin.manifest.skills) {
536
+ await addSkillDirectory({
537
+ root, relativePath: 'codex/skills', tool: 'Codex', items, coverageIssues,
538
+ type: mixedCodexSkills ? 'skill' : 'command',
539
+ readDescription: false,
540
+ });
541
+ }
542
+ if (options.plugin.manifest.slashCommands && platforms.includes('Codex')
543
+ && platforms.includes('Claude Code') && !options.plugin.manifest.skills) {
544
+ await addSkillDirectory({
545
+ root, relativePath: 'codex/skills', tool: 'Codex', items, coverageIssues,
546
+ type: mixedCodexSkills ? 'skill' : 'command',
547
+ readDescription: false,
548
+ checkClaudeCommand: true,
549
+ });
218
550
  }
219
551
  if (options.plugin.manifest.distributions?.agentSkills?.skills) {
220
552
  await addSkillDirectory({
221
- root, relativePath: 'agent-skills/skills', tool: 'Agent Skills', items, notices,
553
+ root, relativePath: 'agent-skills/skills', tool: 'Agent Skills', items, coverageIssues,
222
554
  });
223
555
  }
224
556
  if (options.plugin.manifest.mcpServers) {
225
- await Promise.all(platforms.map((tool) => addJsonComponents({
226
- root,
227
- relativePath: `${tool === 'Codex' ? 'codex' : 'claude'}/${options.plugin.manifest.mcpServers.replace(/^\.\//u, '')}`,
228
- property: 'mcpServers',
229
- type: 'mcpServer',
230
- tool,
231
- items,
232
- notices,
233
- })));
557
+ for (const tool of platforms) {
558
+ await addJsonComponents({
559
+ root,
560
+ relativePath: `${tool === 'Codex' ? 'codex' : 'claude'}/${options.plugin.manifest.mcpServers.replace(/^\.\//u, '')}`,
561
+ property: 'mcpServers',
562
+ type: 'mcpServer',
563
+ tool,
564
+ items,
565
+ coverageIssues,
566
+ });
567
+ }
234
568
  }
235
569
  if (options.plugin.manifest.apps && platforms.includes('Codex')) {
236
570
  await addJsonComponents({
@@ -240,15 +574,7 @@ export async function projectPluginContentOverview(options) {
240
574
  type: 'app',
241
575
  tool: 'Codex',
242
576
  items,
243
- notices,
244
- });
245
- }
246
- if (options.plugin.manifest.slashCommands) {
247
- await addSlashCommands({
248
- root,
249
- hasCodex: platforms.includes('Codex'),
250
- items,
251
- notices,
577
+ coverageIssues,
252
578
  });
253
579
  }
254
580
  for (const agent of options.plugin.manifest.extensions?.agents ?? []) {
@@ -258,6 +584,7 @@ export async function projectPluginContentOverview(options) {
258
584
  type: 'agent',
259
585
  description: agent.description.trim() || null,
260
586
  tools: toolsForOverrides(platforms, agent.agentOverrides),
587
+ descriptionState: agent.description.trim() ? 'available' : 'absent',
261
588
  });
262
589
  }
263
590
  for (const instruction of options.plugin.manifest.extensions?.instructions ?? []) {
@@ -267,6 +594,7 @@ export async function projectPluginContentOverview(options) {
267
594
  type: 'instruction',
268
595
  description: null,
269
596
  tools: toolsForOverrides(platforms, instruction.agentOverrides),
597
+ descriptionState: 'absent',
270
598
  });
271
599
  }
272
600
  for (const permission of options.plugin.manifest.extensions?.permissions ?? []) {
@@ -276,6 +604,7 @@ export async function projectPluginContentOverview(options) {
276
604
  type: 'permission',
277
605
  description: null,
278
606
  tools: toolsForOverrides(platforms, permission.agentOverrides),
607
+ descriptionState: 'absent',
279
608
  });
280
609
  }
281
610
  for (const hook of options.plugin.manifest.extensions?.hooks ?? []) {
@@ -286,6 +615,7 @@ export async function projectPluginContentOverview(options) {
286
615
  type: 'hook',
287
616
  description: null,
288
617
  tools: toolsForOverrides(platforms, hook.agentOverrides),
618
+ descriptionState: 'absent',
289
619
  });
290
620
  }
291
621
  for (const [platform, hooks] of Object.entries(options.plugin.manifest.extensions?.nativeHooks ?? {})) {
@@ -298,6 +628,7 @@ export async function projectPluginContentOverview(options) {
298
628
  type: 'hook',
299
629
  description: null,
300
630
  tools: [tool],
631
+ descriptionState: 'absent',
301
632
  });
302
633
  }
303
634
  }
@@ -308,8 +639,18 @@ export async function projectPluginContentOverview(options) {
308
639
  continue;
309
640
  const name = target.skillKey ?? target.roleKey ?? target.id;
310
641
  let description = null;
642
+ let issue;
311
643
  if (type === 'skill' && target.platform) {
312
- description = (await readFrontmatterDescription(root, path.join(root, '.agents-plugin', 'artifact-sources', target.platform, target.id, '0', 'SKILL.md'))).description;
644
+ const skill = await readFrontmatterDescription(root, path.join(root, '.agents-plugin', 'artifact-sources', target.platform, target.id, '0', 'SKILL.md'));
645
+ description = skill.description;
646
+ issue = skill.issue;
647
+ if (!skill.exists) {
648
+ const code = await unconfirmedPathCode(root, path.join(root, '.agents-plugin', 'artifact-sources', target.platform, target.id, '0', 'SKILL.md'));
649
+ if (code !== 'incompleteList')
650
+ issue = {
651
+ code, message: '简介文件不可安全读取。请检查内容目录。',
652
+ };
653
+ }
313
654
  }
314
655
  addItem(items, {
315
656
  id: `artifact:${target.id}`,
@@ -317,20 +658,23 @@ export async function projectPluginContentOverview(options) {
317
658
  type,
318
659
  description,
319
660
  tools: [tool],
661
+ descriptionState: issue ? 'issue' : description ? 'available' : 'absent',
662
+ ...(issue ? { issue } : {}),
320
663
  });
321
- if (description === null)
322
- notices.push(`${type} ${name} 没有安全可读的简短用途。`);
323
664
  }
324
665
  const result = [...items.values()].sort((left, right) => left.type.localeCompare(right.type) || left.name.localeCompare(right.name));
325
- if (result.length === 0)
326
- notices.push('此目录版本没有可安全确认的内容清单。');
666
+ const coverage = coverageIssues.length === 0
667
+ ? 'complete' : result.length > 0 ? 'partial' : 'unavailable';
668
+ const notices = [...new Set(coverageIssues.map((issue) => issue.message))];
327
669
  return {
328
670
  pluginId: options.pluginId,
329
671
  pluginVersion: options.plugin.manifest.version,
330
672
  catalogVersion: options.catalogVersion,
331
673
  evidence: 'catalogBundle',
332
674
  items: result,
333
- notices: [...new Set(notices)],
675
+ notices,
676
+ coverage,
677
+ ...(coverageIssues[0] ? { coverageIssue: coverageIssues[0] } : {}),
334
678
  };
335
679
  }
336
680
  //# sourceMappingURL=plugin-content.js.map