@fitlab-ai/agent-infra 0.11.8 → 0.11.9

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 (45) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
  4. package/dist/lib/agent-clients/next-steps.js +1 -1
  5. package/dist/lib/agent-clients/reconcile.js +3 -3
  6. package/dist/lib/agent-clients/schema.js +18 -10
  7. package/dist/lib/init.js +2 -2
  8. package/dist/lib/internal/agent-client.js +3 -3
  9. package/dist/lib/sandbox/recovery.js +1 -22
  10. package/dist/lib/sandbox/tools.js +2 -0
  11. package/dist/lib/task/finalization.js +90 -93
  12. package/dist/lib/task/verification.js +0 -1
  13. package/dist/lib/update.js +2 -2
  14. package/dist/lifecycle-build-manifest.json +3 -3
  15. package/lib/agent-clients/custom-tool-invocations.ts +129 -0
  16. package/lib/agent-clients/next-steps.ts +3 -3
  17. package/lib/agent-clients/reconcile.ts +3 -3
  18. package/lib/agent-clients/schema.ts +18 -10
  19. package/lib/init.ts +2 -2
  20. package/lib/internal/agent-client.ts +3 -3
  21. package/lib/sandbox/recovery.ts +0 -25
  22. package/lib/sandbox/tool-types.ts +1 -0
  23. package/lib/sandbox/tools.ts +2 -0
  24. package/lib/task/finalization.ts +91 -88
  25. package/lib/task/verification.ts +1 -2
  26. package/lib/update.ts +2 -3
  27. package/package.json +1 -1
  28. package/templates/.agents/README.en.md +2 -50
  29. package/templates/.agents/README.zh-CN.md +2 -50
  30. package/templates/.agents/rules/next-step-output.en.md +1 -1
  31. package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
  32. package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
  33. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  34. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  35. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  36. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  37. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  38. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  39. package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
  40. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
  41. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
  42. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  43. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +2 -268
  44. package/dist/lib/agent-clients/custom-tuis.js +0 -99
  45. package/lib/agent-clients/custom-tuis.ts +0 -149
@@ -225,17 +225,6 @@ const AGENT_CLIENT_MANIFEST = [
225
225
  }
226
226
  }
227
227
  ];
228
- const CUSTOM_TUI_CONTRACT = {
229
- "requiredFields": [
230
- "name",
231
- "dir",
232
- "invoke"
233
- ],
234
- "allowedPlaceholders": [
235
- "skillName",
236
- "projectName"
237
- ]
238
- };
239
228
  // Add a new identifier here only after shipping matching .{platform}. template variants.
240
229
  const KNOWN_PLATFORMS = new Set(['github', 'none']);
241
230
  const KNOWN_LANGUAGES = new Set(['en', 'zh-CN']);
@@ -444,10 +433,6 @@ function trustedBaseline(value) {
444
433
  return typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value) ? value : null;
445
434
  }
446
435
 
447
- function normDir(p) {
448
- return norm(p).replace(/^\.\//, '').replace(/\/+$/, '');
449
- }
450
-
451
436
  function isInsideProject(projectRoot, relativePath) {
452
437
  if (typeof relativePath !== 'string' || relativePath.trim() === '' || path.isAbsolute(relativePath)) {
453
438
  return false;
@@ -607,14 +592,6 @@ function isCustomProtected(targetPath, customSkills, customCommandTargets) {
607
592
  )) || customCommandTargets.has(normalized);
608
593
  }
609
594
 
610
- function recordCustomTUISkipped(report, entry) {
611
- report?.custom?.customTUIs?.skipped?.push(entry);
612
- }
613
-
614
- function recordCustomTUISkippedRef(report, entry) {
615
- report?.custom?.customTUIs?.skippedRefs?.push(entry);
616
- }
617
-
618
595
  function expandHome(inputPath) {
619
596
  if (inputPath === '~') return os.homedir();
620
597
  if (inputPath.startsWith('~/')) {
@@ -856,204 +833,6 @@ function generateAgentClientCommand(skill, lang, descriptor) {
856
833
  return `${lines.join('\n')}\n`;
857
834
  }
858
835
 
859
- function validateCustomTUIs(projectRoot, customTUIs, report) {
860
- if (customTUIs === undefined) return [];
861
- if (!Array.isArray(customTUIs)) {
862
- recordCustomTUISkipped(report, {
863
- index: -1,
864
- name: '',
865
- dir: '',
866
- reason: 'customTUIs must be an array'
867
- });
868
- return [];
869
- }
870
- const tools = customTUIs;
871
- return tools
872
- .map((tool, index) => {
873
- if (
874
- typeof tool !== 'object'
875
- || tool === null
876
- || Array.isArray(tool)
877
- ) {
878
- recordCustomTUISkipped(report, {
879
- index,
880
- name: '',
881
- dir: '',
882
- reason: 'invalid custom TUI'
883
- });
884
- return null;
885
- }
886
-
887
- for (const field of CUSTOM_TUI_CONTRACT.requiredFields) {
888
- if (
889
- typeof tool[field] !== 'string'
890
- || tool[field].trim() === ''
891
- || /[\r\n]/.test(tool[field])
892
- ) {
893
- recordCustomTUISkipped(report, {
894
- index,
895
- name: String(tool.name || ''),
896
- dir: String(tool.dir || ''),
897
- reason: `invalid ${field}`
898
- });
899
- return null;
900
- }
901
- }
902
-
903
- if (typeof tool?.dir !== 'string' || tool.dir.trim() === '') {
904
- recordCustomTUISkipped(report, {
905
- index,
906
- name: String(tool?.name || ''),
907
- dir: String(tool?.dir || ''),
908
- reason: 'invalid dir'
909
- });
910
- return null;
911
- }
912
-
913
- if (tool.dir.includes('\\')) {
914
- recordCustomTUISkipped(report, {
915
- index,
916
- name: tool.name,
917
- dir: tool.dir,
918
- reason: 'dir must use POSIX separators'
919
- });
920
- return null;
921
- }
922
-
923
- if (!isInsideProject(projectRoot, tool.dir)) {
924
- recordCustomTUISkipped(report, {
925
- index,
926
- name: String(tool?.name || ''),
927
- dir: tool.dir,
928
- reason: 'dir must be a relative path inside the project root'
929
- });
930
- return null;
931
- }
932
-
933
- const placeholders = [...tool.invoke.matchAll(/\$\{([^}]+)\}/g)]
934
- .map((match) => match[1]);
935
- if (
936
- !placeholders.includes('skillName')
937
- || placeholders.some((placeholder) =>
938
- !CUSTOM_TUI_CONTRACT.allowedPlaceholders.includes(placeholder)
939
- )
940
- || tool.invoke
941
- .replaceAll('${skillName}', '')
942
- .replaceAll('${projectName}', '')
943
- .includes('${')
944
- ) {
945
- recordCustomTUISkipped(report, {
946
- index,
947
- name: tool.name,
948
- dir: tool.dir,
949
- reason: 'invalid invoke placeholders'
950
- });
951
- return null;
952
- }
953
-
954
- return { ...tool, index, dir: normDir(tool.dir) };
955
- })
956
- .filter(Boolean);
957
- }
958
-
959
- function customTUITargetPath(tool, refFile, refSkillName, skillName) {
960
- const targetFile = refFile.includes(refSkillName)
961
- ? refFile.replaceAll(refSkillName, skillName)
962
- : `${skillName}${path.extname(refFile)}`;
963
- return norm(path.join(tool.dir, targetFile));
964
- }
965
-
966
- function findCustomTUIReference(projectRoot, tool, templateSkillNames, report, logSkipped = false) {
967
- const cmdDir = path.join(projectRoot, tool.dir);
968
- if (!fs.existsSync(cmdDir) || !fs.statSync(cmdDir).isDirectory()) {
969
- if (logSkipped) {
970
- recordCustomTUISkipped(report, {
971
- index: tool.index,
972
- name: String(tool.name || ''),
973
- dir: tool.dir,
974
- reason: 'directory not found'
975
- });
976
- }
977
- return null;
978
- }
979
-
980
- const cmdFiles = fs.readdirSync(cmdDir)
981
- .filter((file) => fs.statSync(path.join(cmdDir, file)).isFile())
982
- .sort((left, right) => left.localeCompare(right));
983
- if (cmdFiles.length === 0) {
984
- if (logSkipped) {
985
- recordCustomTUISkipped(report, {
986
- index: tool.index,
987
- name: String(tool.name || ''),
988
- dir: tool.dir,
989
- reason: 'no command files'
990
- });
991
- }
992
- return null;
993
- }
994
-
995
- let sawKnownSkillReference = false;
996
-
997
- for (const file of cmdFiles) {
998
- const content = fs.readFileSync(path.join(cmdDir, file), 'utf8');
999
- const match = content.match(/\.agents\/skills\/([^/]+)\/SKILL\.md/);
1000
- if (!match) continue;
1001
-
1002
- const skillName = match[1];
1003
- if (!templateSkillNames.has(skillName)) continue;
1004
-
1005
- const skillMd = path.join(projectRoot, '.agents/skills', skillName, 'SKILL.md');
1006
- if (!fs.existsSync(skillMd)) continue;
1007
-
1008
- const meta = parseSkillFrontmatter(skillMd);
1009
- if (!meta.description) continue;
1010
-
1011
- sawKnownSkillReference = true;
1012
- if (!content.includes(meta.description)) {
1013
- if (logSkipped) {
1014
- recordCustomTUISkippedRef(report, {
1015
- index: tool.index,
1016
- name: String(tool.name || ''),
1017
- dir: tool.dir,
1018
- file,
1019
- skill: skillName,
1020
- reason: 'description not found in reference command file'
1021
- });
1022
- }
1023
- continue;
1024
- }
1025
-
1026
- return { content, file, skillName, skillDesc: meta.description };
1027
- }
1028
-
1029
- if (logSkipped) {
1030
- recordCustomTUISkipped(report, {
1031
- index: tool.index,
1032
- name: String(tool.name || ''),
1033
- dir: tool.dir,
1034
- reason: sawKnownSkillReference
1035
- ? 'no reference command file with matching description'
1036
- : 'no usable reference command file'
1037
- });
1038
- }
1039
-
1040
- return null;
1041
- }
1042
-
1043
- function buildCustomTUICommandTargets(projectRoot, customSkills, customTUIs, templateSkillNames) {
1044
- const targets = new Set();
1045
- for (const tool of customTUIs) {
1046
- const ref = findCustomTUIReference(projectRoot, tool, templateSkillNames, null, false);
1047
- if (!ref) continue;
1048
-
1049
- for (const skill of customSkills) {
1050
- targets.add(customTUITargetPath(tool, ref.file, ref.skillName, skill.dirName));
1051
- }
1052
- }
1053
-
1054
- return targets;
1055
- }
1056
-
1057
836
  function buildBuiltinCustomCommandTargets(customSkills, enabledTUIs) {
1058
837
  return new Set(AGENT_CLIENT_MANIFEST.flatMap((adapter) => {
1059
838
  if (!enabledTUIs.has(adapter.id) || !adapter.customCommand) return [];
@@ -1063,33 +842,10 @@ function buildBuiltinCustomCommandTargets(customSkills, enabledTUIs) {
1063
842
  }));
1064
843
  }
1065
844
 
1066
- function learnAndGenerateCommands(projectRoot, customSkills, tool, templateSkillNames, report) {
1067
- const ref = findCustomTUIReference(projectRoot, tool, templateSkillNames, report, true);
1068
- if (!ref) return;
1069
-
1070
- for (const skill of customSkills) {
1071
- const descToken = '__AGENT_INFRA_CUSTOM_SKILL_DESCRIPTION__';
1072
- const generated = ref.content
1073
- .replaceAll(ref.skillDesc, descToken)
1074
- .replaceAll(ref.skillName, skill.dirName)
1075
- .replaceAll(descToken, skill.description);
1076
-
1077
- writeIfChanged(
1078
- projectRoot,
1079
- customTUITargetPath(tool, ref.file, ref.skillName, skill.dirName),
1080
- generated,
1081
- report.custom.commands
1082
- );
1083
- }
1084
- }
1085
-
1086
845
  function generateCustomCommands(
1087
- projectRoot,
1088
846
  customSkills,
1089
847
  lang,
1090
848
  report,
1091
- customTUIs,
1092
- templateSkillNames,
1093
849
  enabledTUIs,
1094
850
  managedWriter
1095
851
  ) {
@@ -1107,11 +863,6 @@ function generateCustomCommands(
1107
863
  );
1108
864
  }
1109
865
  }
1110
-
1111
- const tools = Array.isArray(customTUIs) ? customTUIs : [];
1112
- for (const tool of tools) {
1113
- learnAndGenerateCommands(projectRoot, customSkills, tool, templateSkillNames, report);
1114
- }
1115
866
  }
1116
867
 
1117
868
  function matchesAny(rel, patterns) {
@@ -1277,9 +1028,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
1277
1028
  const configPathRel = norm(path.relative(projectRoot, cfgPath));
1278
1029
  let templateRoot = templateRootOverride;
1279
1030
  if (!templateRoot) {
1280
- const packageResolution = resolveAgentInfraPackage({
1281
- startPath: path.join(projectRoot, '.agents', 'scripts', 'lib', 'agent-infra-package.js')
1282
- });
1031
+ const packageResolution = resolveAgentInfraPackage();
1283
1032
  if (packageResolution.templateRoot && fs.existsSync(packageResolution.templateRoot)) {
1284
1033
  templateRoot = packageResolution.templateRoot;
1285
1034
  } else {
@@ -1303,7 +1052,6 @@ function syncTemplates(projectRoot, templateRootOverride) {
1303
1052
  AGENT_CLIENT_IDS.filter((id) => enabledResolution.state[id].enabled)
1304
1053
  );
1305
1054
  materializeAgentClientConfig(cfg, enabledResolution);
1306
- const customTUIsConfig = Array.isArray(cfg.customTUIs) ? cfg.customTUIs : [];
1307
1055
  const vars = { project, org };
1308
1056
  const templateSkillNames = listTemplateSkillNames(templateRoot);
1309
1057
  const protectedCustomSkills = detectCustomSkills(projectRoot, templateSkillNames);
@@ -1390,24 +1138,13 @@ function syncTemplates(projectRoot, templateRootOverride) {
1390
1138
  unchanged: [],
1391
1139
  removed: [],
1392
1140
  sourceErrors: [],
1393
- customTUIs: { skipped: [], skippedRefs: [] },
1394
1141
  commands: { generated: [], updated: [], unchanged: [] }
1395
1142
  },
1396
1143
  ejected: { created: [], skipped: [] },
1397
1144
  merged: { pending: [] },
1398
1145
  configUpdated: false
1399
1146
  };
1400
- const customTUIs = validateCustomTUIs(projectRoot, customTUIsConfig, report);
1401
- const customTUICommandTargets = buildCustomTUICommandTargets(
1402
- projectRoot,
1403
- protectedCustomSkills,
1404
- customTUIs,
1405
- templateSkillNames
1406
- );
1407
- const customCommandTargets = new Set([
1408
- ...buildBuiltinCustomCommandTargets(protectedCustomSkills, enabledTUIs),
1409
- ...customTUICommandTargets
1410
- ]);
1147
+ const customCommandTargets = buildBuiltinCustomCommandTargets(protectedCustomSkills, enabledTUIs);
1411
1148
 
1412
1149
  for (const category of ['managed', 'merged', 'ejected']) {
1413
1150
  const before = currentRegistry[category];
@@ -1896,12 +1633,9 @@ function syncTemplates(projectRoot, templateRootOverride) {
1896
1633
  const customSkills = detectCustomSkills(projectRoot, templateSkillNames);
1897
1634
  report.custom.detected = customSkills.map((skill) => skill.dirName);
1898
1635
  generateCustomCommands(
1899
- projectRoot,
1900
1636
  customSkills,
1901
1637
  lang,
1902
1638
  report,
1903
- customTUIs,
1904
- templateSkillNames,
1905
1639
  enabledTUIs,
1906
1640
  writeProtectedManaged
1907
1641
  );
@@ -1,99 +0,0 @@
1
- import path from 'node:path';
2
- const CUSTOM_TUI_CONTRACT = Object.freeze({
3
- requiredFields: Object.freeze(['name', 'dir', 'invoke']),
4
- allowedPlaceholders: Object.freeze(['skillName', 'projectName'])
5
- });
6
- function isRecord(value) {
7
- return typeof value === 'object' && value !== null && !Array.isArray(value);
8
- }
9
- function isNonEmptySingleLine(value) {
10
- return typeof value === 'string'
11
- && value.trim() !== ''
12
- && !/[\r\n]/.test(value);
13
- }
14
- function normalizeDir(projectRoot, value) {
15
- if (!isNonEmptySingleLine(value)
16
- || value.includes('\\')
17
- || path.isAbsolute(value)
18
- || /^[a-zA-Z]:/.test(value)) {
19
- return null;
20
- }
21
- const normalized = path.posix.normalize(value);
22
- if (normalized === '.'
23
- || normalized === '..'
24
- || normalized.startsWith('../')
25
- || normalized.split('/').includes('..')) {
26
- return null;
27
- }
28
- const relative = path.relative(path.resolve(projectRoot), path.resolve(projectRoot, normalized));
29
- return relative === ''
30
- || path.isAbsolute(relative)
31
- || relative === '..'
32
- || relative.startsWith(`..${path.sep}`)
33
- ? null
34
- : normalized;
35
- }
36
- function hasValidPlaceholders(invocation) {
37
- const placeholders = [
38
- ...invocation.matchAll(/\$\{([^}]+)\}/g)
39
- ].map((match) => match[1]);
40
- return placeholders.includes('skillName')
41
- && placeholders.every((placeholder) => CUSTOM_TUI_CONTRACT.allowedPlaceholders.includes(placeholder))
42
- && !invocation
43
- .replaceAll('${skillName}', '')
44
- .replaceAll('${projectName}', '')
45
- .includes('${');
46
- }
47
- function normalizeCustomTUIs(projectRoot, input) {
48
- const items = [];
49
- const diagnostics = [];
50
- if (!Array.isArray(input)) {
51
- return Object.freeze({
52
- items: Object.freeze([]),
53
- diagnostics: Object.freeze([
54
- Object.freeze({
55
- code: 'INVALID_CUSTOM_TUIS',
56
- path: 'customTUIs'
57
- })
58
- ])
59
- });
60
- }
61
- for (const [index, candidate] of input.entries()) {
62
- const base = `customTUIs[${index}]`;
63
- if (!isRecord(candidate)) {
64
- diagnostics.push({ code: 'INVALID_CUSTOM_TUI', path: base });
65
- continue;
66
- }
67
- if (!isNonEmptySingleLine(candidate.name)) {
68
- diagnostics.push({ code: 'INVALID_CUSTOM_TUI', path: `${base}.name` });
69
- continue;
70
- }
71
- const dir = normalizeDir(projectRoot, candidate.dir);
72
- if (!dir) {
73
- diagnostics.push({ code: 'INVALID_CUSTOM_TUI', path: `${base}.dir` });
74
- continue;
75
- }
76
- if (!isNonEmptySingleLine(candidate.invoke)) {
77
- diagnostics.push({ code: 'INVALID_CUSTOM_TUI', path: `${base}.invoke` });
78
- continue;
79
- }
80
- if (!hasValidPlaceholders(candidate.invoke)) {
81
- diagnostics.push({
82
- code: 'INVALID_CUSTOM_TUI_PLACEHOLDER',
83
- path: `${base}.invoke`
84
- });
85
- continue;
86
- }
87
- items.push(Object.freeze({
88
- name: candidate.name,
89
- dir,
90
- invocation: candidate.invoke
91
- }));
92
- }
93
- return Object.freeze({
94
- items: Object.freeze(items),
95
- diagnostics: Object.freeze(diagnostics.map((diagnostic) => Object.freeze(diagnostic)))
96
- });
97
- }
98
- export { CUSTOM_TUI_CONTRACT, normalizeCustomTUIs };
99
- //# sourceMappingURL=custom-tuis.js.map
@@ -1,149 +0,0 @@
1
- import path from 'node:path';
2
-
3
- type CustomTUI = Readonly<{
4
- name: string;
5
- dir: string;
6
- invocation: string;
7
- }>;
8
-
9
- type CustomTUIDiagnosticCode =
10
- | 'INVALID_CUSTOM_TUIS'
11
- | 'INVALID_CUSTOM_TUI'
12
- | 'INVALID_CUSTOM_TUI_PLACEHOLDER';
13
-
14
- type CustomTUIDiagnostic = Readonly<{
15
- code: CustomTUIDiagnosticCode;
16
- path: string;
17
- }>;
18
-
19
- type NormalizeCustomTUIsResult = Readonly<{
20
- items: readonly CustomTUI[];
21
- diagnostics: readonly CustomTUIDiagnostic[];
22
- }>;
23
-
24
- const CUSTOM_TUI_CONTRACT = Object.freeze({
25
- requiredFields: Object.freeze(['name', 'dir', 'invoke']),
26
- allowedPlaceholders: Object.freeze(['skillName', 'projectName'])
27
- });
28
-
29
- function isRecord(value: unknown): value is Record<string, unknown> {
30
- return typeof value === 'object' && value !== null && !Array.isArray(value);
31
- }
32
-
33
- function isNonEmptySingleLine(value: unknown): value is string {
34
- return typeof value === 'string'
35
- && value.trim() !== ''
36
- && !/[\r\n]/.test(value);
37
- }
38
-
39
- function normalizeDir(projectRoot: string, value: unknown): string | null {
40
- if (
41
- !isNonEmptySingleLine(value)
42
- || value.includes('\\')
43
- || path.isAbsolute(value)
44
- || /^[a-zA-Z]:/.test(value)
45
- ) {
46
- return null;
47
- }
48
- const normalized = path.posix.normalize(value);
49
- if (
50
- normalized === '.'
51
- || normalized === '..'
52
- || normalized.startsWith('../')
53
- || normalized.split('/').includes('..')
54
- ) {
55
- return null;
56
- }
57
- const relative = path.relative(
58
- path.resolve(projectRoot),
59
- path.resolve(projectRoot, normalized)
60
- );
61
- return relative === ''
62
- || path.isAbsolute(relative)
63
- || relative === '..'
64
- || relative.startsWith(`..${path.sep}`)
65
- ? null
66
- : normalized;
67
- }
68
-
69
- function hasValidPlaceholders(invocation: string): boolean {
70
- const placeholders = [
71
- ...invocation.matchAll(/\$\{([^}]+)\}/g)
72
- ].map((match) => match[1]!);
73
- return placeholders.includes('skillName')
74
- && placeholders.every((placeholder) =>
75
- CUSTOM_TUI_CONTRACT.allowedPlaceholders.includes(placeholder)
76
- )
77
- && !invocation
78
- .replaceAll('${skillName}', '')
79
- .replaceAll('${projectName}', '')
80
- .includes('${');
81
- }
82
-
83
- function normalizeCustomTUIs(
84
- projectRoot: string,
85
- input: unknown
86
- ): NormalizeCustomTUIsResult {
87
- const items: CustomTUI[] = [];
88
- const diagnostics: CustomTUIDiagnostic[] = [];
89
-
90
- if (!Array.isArray(input)) {
91
- return Object.freeze({
92
- items: Object.freeze([]),
93
- diagnostics: Object.freeze([
94
- Object.freeze({
95
- code: 'INVALID_CUSTOM_TUIS' as const,
96
- path: 'customTUIs'
97
- })
98
- ])
99
- });
100
- }
101
-
102
- for (const [index, candidate] of input.entries()) {
103
- const base = `customTUIs[${index}]`;
104
- if (!isRecord(candidate)) {
105
- diagnostics.push({ code: 'INVALID_CUSTOM_TUI', path: base });
106
- continue;
107
- }
108
- if (!isNonEmptySingleLine(candidate.name)) {
109
- diagnostics.push({ code: 'INVALID_CUSTOM_TUI', path: `${base}.name` });
110
- continue;
111
- }
112
- const dir = normalizeDir(projectRoot, candidate.dir);
113
- if (!dir) {
114
- diagnostics.push({ code: 'INVALID_CUSTOM_TUI', path: `${base}.dir` });
115
- continue;
116
- }
117
- if (!isNonEmptySingleLine(candidate.invoke)) {
118
- diagnostics.push({ code: 'INVALID_CUSTOM_TUI', path: `${base}.invoke` });
119
- continue;
120
- }
121
- if (!hasValidPlaceholders(candidate.invoke)) {
122
- diagnostics.push({
123
- code: 'INVALID_CUSTOM_TUI_PLACEHOLDER',
124
- path: `${base}.invoke`
125
- });
126
- continue;
127
- }
128
- items.push(Object.freeze({
129
- name: candidate.name,
130
- dir,
131
- invocation: candidate.invoke
132
- }));
133
- }
134
-
135
- return Object.freeze({
136
- items: Object.freeze(items),
137
- diagnostics: Object.freeze(
138
- diagnostics.map((diagnostic) => Object.freeze(diagnostic))
139
- )
140
- });
141
- }
142
-
143
- export { CUSTOM_TUI_CONTRACT, normalizeCustomTUIs };
144
- export type {
145
- CustomTUI,
146
- CustomTUIDiagnostic,
147
- CustomTUIDiagnosticCode,
148
- NormalizeCustomTUIsResult
149
- };