toolcraft 0.0.130 → 0.0.131

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/composition.json CHANGED
@@ -108,7 +108,7 @@
108
108
  },
109
109
  {
110
110
  "name": "toolcraft",
111
- "version": "0.0.130",
111
+ "version": "0.0.131",
112
112
  "license": "MIT"
113
113
  },
114
114
  {
@@ -118,7 +118,7 @@
118
118
  },
119
119
  {
120
120
  "name": "toolcraft-schema",
121
- "version": "0.0.130",
121
+ "version": "0.0.131",
122
122
  "license": "MIT"
123
123
  },
124
124
  {
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { Command as CommanderCommand, CommanderError, InvalidArgumentError, Option } from "commander";
5
5
  import { validate as validateSchema } from "toolcraft-schema";
6
- import { cancel, configureTheme, confirm, createLogger, formatCommandList, formatOptionList, getTheme, helpFormatterPlain, isCancel, note, promptText, renderTable, resetOutputFormatCache, select, text } from "toolcraft-design";
6
+ import { cancel, configureTheme, confirm, createLogger, formatCommandList, formatOptionList, getTheme, helpFormatterPlain, isCancel, note, promptText, renderHelpTokens, renderTable, resetOutputFormatCache, select, text } from "toolcraft-design";
7
7
  import { ApprovalDeclinedError, ToolcraftBugError, UserError, assertCommandRequirements, getCommandSourcePath, hasMcpProxyConfig, resolveCommandSecrets } from "./index.js";
8
8
  import { hasOwnErrorCode } from "./error-codes.js";
9
9
  import { writeErrorReport } from "./error-report.js";
@@ -844,12 +844,37 @@ function appendHelpMetadata(description, metadata) {
844
844
  return description;
845
845
  }
846
846
  if (description.length === 0) {
847
- return `(${metadata.join(", ")})`;
847
+ return metadata.map((entry) => `(${entry})`).join(" ");
848
848
  }
849
- return `${description} (${metadata.join(", ")})`;
849
+ return `${description} ${metadata.map((entry) => `(${entry})`).join(" ")}`;
850
+ }
851
+ function normalizeHelpEchoKey(value) {
852
+ let normalized = "";
853
+ for (const character of value.trim().toLowerCase()) {
854
+ if (character !== " " &&
855
+ character !== "\t" &&
856
+ character !== "\n" &&
857
+ character !== "\r" &&
858
+ character !== "_" &&
859
+ character !== "." &&
860
+ character !== "-") {
861
+ normalized += character;
862
+ }
863
+ }
864
+ return normalized;
865
+ }
866
+ function isEchoHelpDescription(description, name) {
867
+ if (description.length === 0) {
868
+ return false;
869
+ }
870
+ return normalizeHelpEchoKey(description) === normalizeHelpEchoKey(name);
871
+ }
872
+ function suppressEchoHelpDescription(description, name) {
873
+ return isEchoHelpDescription(description, name) ? "" : description;
850
874
  }
851
875
  function formatHelpFieldDescription(field) {
852
- const description = field.description ?? field.displayPath;
876
+ const rawDescription = field.description ?? field.displayPath;
877
+ const description = suppressEchoHelpDescription(rawDescription, field.displayPath);
853
878
  const metadata = [];
854
879
  if (field.schema.kind === "enum" && field.schema.values.length <= 8) {
855
880
  const values = field.schema.values.map((value) => String(value)).join(", ");
@@ -1006,53 +1031,55 @@ function formatDynamicHelpMetadata(field) {
1006
1031
  }
1007
1032
  return metadata;
1008
1033
  }
1034
+ function createHelpOptionRow(flags, description) {
1035
+ return {
1036
+ flags,
1037
+ flagTokens: tokenizeHelpFlags(flags),
1038
+ description
1039
+ };
1040
+ }
1009
1041
  function collectDynamicObjectHelpRows(schema, casing, optionPrefix, displayPrefix, metadata) {
1010
1042
  const rows = [];
1011
1043
  for (const [key, rawChildSchema] of Object.entries(schema.shape)) {
1012
1044
  const childSchema = unwrapOptional(rawChildSchema);
1013
1045
  const optionFlag = `${optionPrefix}.${formatSegment(key, casing)}`;
1014
1046
  const displayPath = `${displayPrefix}.${key}`;
1015
- const description = childSchema.description ?? displayPath;
1047
+ const rawDescription = childSchema.description ?? displayPath;
1048
+ const description = suppressEchoHelpDescription(rawDescription, displayPath);
1016
1049
  if (childSchema.kind === "object") {
1017
1050
  rows.push(...collectDynamicObjectHelpRows(childSchema, casing, optionFlag, displayPath, metadata));
1018
1051
  continue;
1019
1052
  }
1020
1053
  if (childSchema.kind === "record") {
1021
- rows.push({
1022
- flags: `${optionFlag}.<key> <${describeDynamicFieldType({
1023
- ...{
1024
- id: displayPath,
1025
- path: [],
1026
- displayPath,
1027
- optionPath: [],
1028
- optionPathDisplay: `${displayPath}.<key>`,
1029
- optionFlag: `${optionFlag}.<key>`,
1030
- optional: false,
1031
- hasDefault: false,
1032
- defaultValue: undefined,
1033
- requiredWhenActive: false,
1034
- schema: childSchema
1035
- }
1036
- })}>`,
1037
- description: appendHelpMetadata(description, metadata)
1038
- });
1054
+ rows.push(createHelpOptionRow(`${optionFlag}.<key> <${describeDynamicFieldType({
1055
+ ...{
1056
+ id: displayPath,
1057
+ path: [],
1058
+ displayPath,
1059
+ optionPath: [],
1060
+ optionPathDisplay: `${displayPath}.<key>`,
1061
+ optionFlag: `${optionFlag}.<key>`,
1062
+ optional: false,
1063
+ hasDefault: false,
1064
+ defaultValue: undefined,
1065
+ requiredWhenActive: false,
1066
+ schema: childSchema
1067
+ }
1068
+ })}>`, appendHelpMetadata(description, metadata)));
1039
1069
  continue;
1040
1070
  }
1041
1071
  if (childSchema.kind === "array" && unwrapOptional(childSchema.item).kind === "object") {
1042
1072
  rows.push(...collectDynamicObjectHelpRows(unwrapOptional(childSchema.item), casing, `${optionFlag}.<index>`, `${displayPath}.<index>`, metadata));
1043
1073
  continue;
1044
1074
  }
1045
- rows.push({
1046
- flags: childSchema.kind === "boolean"
1047
- ? childSchema.default === true
1048
- ? `--no-${optionFlag.slice(2)}`
1049
- : optionFlag
1050
- : `${optionFlag} <${describeHelpValueToken(childSchema, {
1051
- displayPath,
1052
- optionFlag
1053
- })}>`,
1054
- description: appendHelpMetadata(description, metadata)
1055
- });
1075
+ rows.push(createHelpOptionRow(childSchema.kind === "boolean"
1076
+ ? childSchema.default === true
1077
+ ? `--no-${optionFlag.slice(2)}`
1078
+ : optionFlag
1079
+ : `${optionFlag} <${describeHelpValueToken(childSchema, {
1080
+ displayPath,
1081
+ optionFlag
1082
+ })}>`, appendHelpMetadata(description, metadata)));
1056
1083
  }
1057
1084
  return rows;
1058
1085
  }
@@ -1071,17 +1098,11 @@ function formatDynamicHelpFields(field, casing) {
1071
1098
  }
1072
1099
  }
1073
1100
  return [
1074
- {
1075
- flags: `${field.optionFlag} <${describeDynamicFieldType(field)}>`,
1076
- description: appendHelpMetadata(field.description ?? field.optionPathDisplay, metadata)
1077
- }
1101
+ createHelpOptionRow(`${field.optionFlag} <${describeDynamicFieldType(field)}>`, appendHelpMetadata(suppressEchoHelpDescription(field.description ?? field.optionPathDisplay, field.optionPathDisplay), metadata))
1078
1102
  ];
1079
1103
  }
1080
1104
  function formatSecretRows(secrets) {
1081
- return Object.values(secrets).map((secret) => ({
1082
- flags: secret.env,
1083
- description: formatSecretDescription(secret)
1084
- }));
1105
+ return Object.values(secrets).map((secret) => createHelpOptionRow(secret.env, formatSecretDescription(secret)));
1085
1106
  }
1086
1107
  function formatSecretDescription(secret) {
1087
1108
  if (secret.description !== undefined && secret.description.length > 0) {
@@ -1110,47 +1131,135 @@ function formatExampleCommand(breadcrumb, rootUsageName, params) {
1110
1131
  function formatExampleRows(examples, breadcrumb, rootUsageName) {
1111
1132
  return examples.map((example) => `${example.title}\n ${formatExampleCommand(breadcrumb, rootUsageName, example.params)}`);
1112
1133
  }
1113
- function wrapOptionalCommandParameterToken(token, optional) {
1114
- return optional ? `[${token}]` : token;
1134
+ const MAX_INLINE_OPTIONAL_PARAMETER_TOKENS = 8;
1135
+ function tokenizeHelpFlags(flags) {
1136
+ const tokens = [];
1137
+ let index = 0;
1138
+ while (index < flags.length) {
1139
+ if (flags[index] === " ") {
1140
+ let end = index + 1;
1141
+ while (end < flags.length && flags[end] === " ") {
1142
+ end += 1;
1143
+ }
1144
+ tokens.push({ text: flags.slice(index, end), role: "literal" });
1145
+ index = end;
1146
+ continue;
1147
+ }
1148
+ if (flags[index] === "[" || flags[index] === "]") {
1149
+ tokens.push({ text: flags[index], role: "dim" });
1150
+ index += 1;
1151
+ continue;
1152
+ }
1153
+ if (flags[index] === "<") {
1154
+ const close = flags.indexOf(">", index);
1155
+ if (close === -1) {
1156
+ tokens.push({ text: flags.slice(index), role: "literal" });
1157
+ break;
1158
+ }
1159
+ tokens.push({ text: flags.slice(index, close + 1), role: "argument" });
1160
+ index = close + 1;
1161
+ continue;
1162
+ }
1163
+ if (flags.startsWith("--", index) || (flags[index] === "-" && flags[index + 1] !== undefined && flags[index + 1] !== "-")) {
1164
+ let end = index + 1;
1165
+ while (end < flags.length && flags[end] !== " " && flags[end] !== "[" && flags[end] !== "]" && flags[end] !== "<") {
1166
+ end += 1;
1167
+ }
1168
+ tokens.push({ text: flags.slice(index, end), role: "option" });
1169
+ index = end;
1170
+ continue;
1171
+ }
1172
+ // Enum literals (a|b) or bare words after a flag.
1173
+ let end = index + 1;
1174
+ while (end < flags.length && flags[end] !== " " && flags[end] !== "[" && flags[end] !== "]" && flags[end] !== "<") {
1175
+ end += 1;
1176
+ }
1177
+ const piece = flags.slice(index, end);
1178
+ tokens.push({
1179
+ text: piece,
1180
+ role: piece.includes("|") ? "literal" : piece.startsWith("+") ? "dim" : "literal"
1181
+ });
1182
+ index = end;
1183
+ }
1184
+ return tokens;
1185
+ }
1186
+ function wrapOptionalParameterTokens(tokens, optional) {
1187
+ if (!optional) {
1188
+ return tokens;
1189
+ }
1190
+ return [{ text: "[", role: "dim" }, ...tokens, { text: "]", role: "dim" }];
1191
+ }
1192
+ function createCommandParameterToken(text, optional) {
1193
+ return {
1194
+ text: optional ? `[${text}]` : text,
1195
+ optional,
1196
+ tokens: wrapOptionalParameterTokens(tokenizeHelpFlags(text), optional)
1197
+ };
1115
1198
  }
1116
1199
  function formatCommandDynamicParameterTokens(field, casing) {
1117
1200
  const optional = field.optional || field.hasDefault;
1118
- return formatDynamicHelpFields(field, casing).map((row) => wrapOptionalCommandParameterToken(row.flags, optional));
1201
+ return formatDynamicHelpFields(field, casing).map((row) => createCommandParameterToken(row.flags, optional));
1119
1202
  }
1120
1203
  function formatCommandParameterTokens(command, casing, globalLongOptionFlags) {
1121
1204
  const collected = collectFields(command.params, casing, globalLongOptionFlags);
1122
1205
  const fields = assignPositionals(collected.fields, command.positional);
1123
1206
  return fields
1124
1207
  .filter((field) => field.global !== true)
1125
- .map((field) => wrapOptionalCommandParameterToken(formatCommandParameterFieldFlags(field, globalLongOptionFlags), field.positionalIndex === undefined && (field.optional || field.hasDefault)))
1208
+ .map((field) => createCommandParameterToken(formatCommandParameterFieldFlags(field, globalLongOptionFlags), field.positionalIndex === undefined && (field.optional || field.hasDefault)))
1126
1209
  .concat(collected.dynamicFields.flatMap((field) => formatCommandDynamicParameterTokens(field, casing)));
1127
1210
  }
1128
- function formatCommandRowName(node, casing, globalLongOptionFlags) {
1211
+ function collapseOptionalParameterTokens(parameterTokens) {
1212
+ const optionalCount = parameterTokens.filter((token) => token.optional).length;
1213
+ if (optionalCount <= MAX_INLINE_OPTIONAL_PARAMETER_TOKENS) {
1214
+ return parameterTokens;
1215
+ }
1216
+ const required = parameterTokens.filter((token) => !token.optional);
1217
+ const collapsedText = `+${optionalCount} options`;
1218
+ return [
1219
+ ...required,
1220
+ {
1221
+ text: `[${collapsedText}]`,
1222
+ optional: true,
1223
+ tokens: [
1224
+ { text: "[", role: "dim" },
1225
+ { text: collapsedText, role: "dim" },
1226
+ { text: "]", role: "dim" }
1227
+ ]
1228
+ }
1229
+ ];
1230
+ }
1231
+ function formatCommandRowNameTokens(node, casing, globalLongOptionFlags) {
1129
1232
  const baseName = node.aliases.length === 0 ? node.name : `${node.name} (${node.aliases.join(", ")})`;
1233
+ const nameTokens = [{ text: baseName, role: "command" }];
1130
1234
  const parameterTokens = node.kind === "command"
1131
- ? formatCommandParameterTokens(node, casing, globalLongOptionFlags)
1235
+ ? collapseOptionalParameterTokens(formatCommandParameterTokens(node, casing, globalLongOptionFlags))
1132
1236
  : [];
1133
- const name = parameterTokens.length === 0 ? baseName : `${baseName} ${parameterTokens.join(" ")}`;
1134
- return name;
1237
+ for (const token of parameterTokens) {
1238
+ nameTokens.push({ text: " ", role: "literal" }, ...token.tokens);
1239
+ }
1240
+ const name = parameterTokens.length === 0
1241
+ ? baseName
1242
+ : `${baseName} ${parameterTokens.map((token) => token.text).join(" ")}`;
1243
+ return { name, nameTokens };
1135
1244
  }
1136
1245
  function formatCommandRows(group, scope, casing, globalLongOptionFlags, help) {
1137
- if (help === "concise") {
1138
- return getHelpChildren(group, scope).map((child) => ({
1139
- name: formatCommandRowName(child, casing, globalLongOptionFlags),
1140
- description: child.description ?? "",
1246
+ const toRow = (child, depth) => {
1247
+ const { name, nameTokens } = formatCommandRowNameTokens(child, casing, globalLongOptionFlags);
1248
+ return {
1249
+ name,
1250
+ nameTokens,
1251
+ description: suppressEchoHelpDescription(child.description ?? "", child.name),
1141
1252
  kind: child.kind,
1142
- depth: 0
1143
- }));
1253
+ depth
1254
+ };
1255
+ };
1256
+ if (help === "concise") {
1257
+ return getHelpChildren(group, scope).map((child) => toRow(child, 0));
1144
1258
  }
1145
1259
  const rows = [];
1146
1260
  const visit = (node, depth) => {
1147
1261
  for (const child of getHelpChildren(node, scope)) {
1148
- rows.push({
1149
- name: formatCommandRowName(child, casing, globalLongOptionFlags),
1150
- description: child.description ?? "",
1151
- kind: child.kind,
1152
- depth
1153
- });
1262
+ rows.push(toRow(child, depth));
1154
1263
  if (child.kind === "group") {
1155
1264
  visit(child, depth + 1);
1156
1265
  }
@@ -1197,10 +1306,7 @@ function collectSchemaGlobalFieldRows(group, scope, casing, globalLongOptionFlag
1197
1306
  if (seen.has(dedupeKey)) {
1198
1307
  continue;
1199
1308
  }
1200
- seen.set(dedupeKey, {
1201
- flags: formatHelpFieldFlags(field, globalLongOptionFlags),
1202
- description: formatHelpFieldDescription(field)
1203
- });
1309
+ seen.set(dedupeKey, createHelpOptionRow(formatHelpFieldFlags(field, globalLongOptionFlags), formatHelpFieldDescription(field)));
1204
1310
  }
1205
1311
  return;
1206
1312
  }
@@ -1224,6 +1330,23 @@ function formatHelpOptionList(rows) {
1224
1330
  ? helpFormatterPlain.formatOptionList(rows)
1225
1331
  : formatOptionList(rows);
1226
1332
  }
1333
+ function sortLeafHelpOptionFields(fields) {
1334
+ const positionals = [];
1335
+ const required = [];
1336
+ const optional = [];
1337
+ for (const field of fields) {
1338
+ if (field.positionalIndex !== undefined) {
1339
+ positionals.push(field);
1340
+ continue;
1341
+ }
1342
+ if (!field.optional && !field.hasDefault) {
1343
+ required.push(field);
1344
+ continue;
1345
+ }
1346
+ optional.push(field);
1347
+ }
1348
+ return [...positionals, ...required, ...optional];
1349
+ }
1227
1350
  function buildUsageLine(breadcrumb, rootUsageName, suffix) {
1228
1351
  const visibleBreadcrumb = breadcrumb.filter((segment) => segment.length > 0);
1229
1352
  const usageBreadcrumb = breadcrumb[0] === "" ? [rootUsageName, ...visibleBreadcrumb] : visibleBreadcrumb;
@@ -1235,11 +1358,31 @@ function formatGroupUsageSuffix(group, scope, casing, globalLongOptionFlags) {
1235
1358
  if (group.default !== undefined &&
1236
1359
  group.default.hidden === true &&
1237
1360
  group.default.scope.includes(scope)) {
1238
- const parameterTokens = formatCommandParameterTokens(group.default, casing, globalLongOptionFlags);
1239
- return ["[command]", "[OPTIONS]", ...parameterTokens].join(" ");
1361
+ const parameterTokens = collapseOptionalParameterTokens(formatCommandParameterTokens(group.default, casing, globalLongOptionFlags));
1362
+ return ["[command]", "[OPTIONS]", ...parameterTokens.map((token) => token.text)].join(" ");
1240
1363
  }
1241
1364
  return "[command] [OPTIONS]";
1242
1365
  }
1366
+ function formatHelpDrillDownFooter(rootUsageName) {
1367
+ return text.muted(`Run ${rootUsageName} <command> --help for full options.`);
1368
+ }
1369
+ function formatStyledUsageLine(usageLine) {
1370
+ const parts = usageLine.split(" ").filter((part) => part.length > 0);
1371
+ if (parts.length === 0) {
1372
+ return text.usageCommand(usageLine);
1373
+ }
1374
+ const commandEnd = parts.findIndex((part) => part.startsWith("-") || part.startsWith("[") || part.startsWith("<"));
1375
+ const commandParts = commandEnd === -1 ? parts : parts.slice(0, commandEnd);
1376
+ const argParts = commandEnd === -1 ? [] : parts.slice(commandEnd);
1377
+ const command = commandParts.join(" ");
1378
+ if (argParts.length === 0) {
1379
+ return text.usageCommand(command);
1380
+ }
1381
+ return `${text.usageCommand(command)} ${renderHelpTokens(argParts.flatMap((part, index) => [
1382
+ ...(index === 0 ? [] : [{ text: " ", role: "literal" }]),
1383
+ ...tokenizeHelpFlags(part)
1384
+ ]))}`;
1385
+ }
1243
1386
  function renderGroupHelp(group, breadcrumb, scope, casing, globalOptions, rootUsageName, isRoot) {
1244
1387
  const sections = [];
1245
1388
  const globalLongOptionFlags = getGlobalLongOptionFlags(globalOptions.presetsEnabled, globalOptions.showVersion, globalOptions.controls);
@@ -1257,6 +1400,9 @@ function renderGroupHelp(group, breadcrumb, scope, casing, globalOptions, rootUs
1257
1400
  sections.push(builtInLine);
1258
1401
  }
1259
1402
  }
1403
+ if (commandRows.length > 0) {
1404
+ sections.push(formatHelpDrillDownFooter(rootUsageName));
1405
+ }
1260
1406
  return renderHelpDocument({
1261
1407
  breadcrumb,
1262
1408
  rootUsageName,
@@ -1271,12 +1417,8 @@ function renderLeafHelp(command, breadcrumb, casing, globalOptions, rootUsageNam
1271
1417
  const globalLongOptionFlags = getGlobalLongOptionFlags(globalOptions.presetsEnabled, globalOptions.showVersion, globalOptions.controls);
1272
1418
  const collected = collectFields(command.params, casing, globalLongOptionFlags);
1273
1419
  const fields = assignPositionals(collected.fields, command.positional);
1274
- const optionRows = fields
1275
- .filter((field) => field.global !== true)
1276
- .map((field) => ({
1277
- flags: formatHelpFieldFlags(field, globalLongOptionFlags),
1278
- description: formatHelpFieldDescription(field)
1279
- }))
1420
+ const optionRows = sortLeafHelpOptionFields(fields.filter((field) => field.global !== true))
1421
+ .map((field) => createHelpOptionRow(formatHelpFieldFlags(field, globalLongOptionFlags), formatHelpFieldDescription(field)))
1280
1422
  .concat(collected.dynamicFields.flatMap((field) => formatDynamicHelpFields(field, casing)));
1281
1423
  if (optionRows.length > 0) {
1282
1424
  sections.push(`${text.sectionHeader("Options")}\n${formatHelpOptionList(optionRows)}`);
@@ -1394,7 +1536,7 @@ function renderHelpDocument(input) {
1394
1536
  if (remainingDescription.length > 0) {
1395
1537
  lines.push(remainingDescription, "");
1396
1538
  }
1397
- lines.push(`Usage: ${text.usageCommand(input.usageLine)}`, "");
1539
+ lines.push(`Usage: ${formatStyledUsageLine(input.usageLine)}`, "");
1398
1540
  if (input.requiresAuth) {
1399
1541
  lines.push("Requires: authentication");
1400
1542
  }
@@ -108,7 +108,7 @@
108
108
  },
109
109
  {
110
110
  "name": "toolcraft",
111
- "version": "0.0.130",
111
+ "version": "0.0.131",
112
112
  "license": "MIT"
113
113
  },
114
114
  {
@@ -118,7 +118,7 @@
118
118
  },
119
119
  {
120
120
  "name": "toolcraft-schema",
121
- "version": "0.0.130",
121
+ "version": "0.0.131",
122
122
  "license": "MIT"
123
123
  },
124
124
  {
@@ -1,2 +1,2 @@
1
- export { helpFormatter } from "toolcraft-design/help-formatter";
2
- export type { CommandInfo, FormatColumnsOptions, OptionInfo } from "toolcraft-design/help-formatter";
1
+ export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList, styleHelpToken, joinHelpTokens, renderHelpTokens } from "toolcraft-design/help-formatter";
2
+ export type { CommandInfo, FormatColumnsOptions, OptionInfo, HelpToken, HelpTokenRole } from "toolcraft-design/help-formatter";
@@ -1 +1 @@
1
- export { helpFormatter } from "toolcraft-design/help-formatter";
1
+ export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList, styleHelpToken, joinHelpTokens, renderHelpTokens } from "toolcraft-design/help-formatter";
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.130",
11
+ "version": "0.0.131",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,3 +1,4 @@
1
+ import { joinHelpTokens } from "./help-formatter.js";
1
2
  export function stripAnsi(value) {
2
3
  let output = "";
3
4
  for (let index = 0; index < value.length; index += 1) {
@@ -54,26 +55,49 @@ function splitWords(value) {
54
55
  }
55
56
  return words;
56
57
  }
57
- function wrapWords(value, width) {
58
- const words = splitWords(value);
58
+ function leadingWhitespace(value) {
59
+ let index = 0;
60
+ while (index < value.length && isWhitespace(value[index])) {
61
+ index += 1;
62
+ }
63
+ return { prefix: value.slice(0, index), rest: value.slice(index) };
64
+ }
65
+ function takePrefix(value, width) {
66
+ return { prefix: value.slice(0, width), rest: value.slice(width) };
67
+ }
68
+ function wrapWords(value, width, continuationWidth = width) {
69
+ // Preserve leading whitespace only on the first wrapped line so hang-indented
70
+ // left cells (command depth prefixes) do not re-indent every continuation.
71
+ const { prefix, rest } = leadingWhitespace(value);
72
+ const firstContentWidth = Math.max(1, width - prefix.length);
73
+ const words = splitWords(rest);
59
74
  if (words.length === 0) {
60
- return [""];
75
+ return [prefix];
61
76
  }
62
77
  const lines = [];
63
78
  let line = "";
79
+ let isFirstLine = true;
64
80
  for (const word of words) {
65
- if (!line) {
66
- line = word;
67
- continue;
68
- }
69
- if (line.length + 1 + word.length <= width) {
81
+ const limit = isFirstLine ? firstContentWidth : continuationWidth;
82
+ if (line && line.length + 1 + word.length <= limit) {
70
83
  line += ` ${word}`;
71
84
  continue;
72
85
  }
73
- lines.push(line);
74
- line = word;
86
+ if (line) {
87
+ lines.push(isFirstLine ? `${prefix}${line}` : line);
88
+ isFirstLine = false;
89
+ line = "";
90
+ }
91
+ let remaining = word;
92
+ while (remaining.length > (isFirstLine ? firstContentWidth : continuationWidth)) {
93
+ const chunk = takePrefix(remaining, isFirstLine ? firstContentWidth : continuationWidth);
94
+ lines.push(isFirstLine ? `${prefix}${chunk.prefix}` : chunk.prefix);
95
+ isFirstLine = false;
96
+ remaining = chunk.rest;
97
+ }
98
+ line = remaining;
75
99
  }
76
- lines.push(line);
100
+ lines.push(isFirstLine ? `${prefix}${line}` : line);
77
101
  return lines;
78
102
  }
79
103
  export function formatColumns(opts) {
@@ -91,33 +115,43 @@ export function formatColumns(opts) {
91
115
  const indent = opts.indent ?? 2;
92
116
  const maxLeftContentWidth = Math.max(...rows.map((row) => row.left.length));
93
117
  const leftWidth = clamp(maxLeftContentWidth + gap, minLeftWidth, maxLeftWidth);
94
- const rightWidth = Math.max(20, totalWidth - leftWidth - indent);
118
+ const rightWidth = Math.max(1, totalWidth - leftWidth - indent);
119
+ const leftWrapWidth = Math.max(1, totalWidth - indent);
95
120
  const firstIndent = " ".repeat(indent);
96
121
  const continuationIndent = " ".repeat(indent + leftWidth);
97
122
  return rows
98
123
  .flatMap((row) => {
124
+ let leftLeadingWidth = 0;
125
+ while (leftLeadingWidth < row.left.length && isWhitespace(row.left[leftLeadingWidth])) {
126
+ leftLeadingWidth += 1;
127
+ }
128
+ // Continuations hang under the left cell start (including depth prefix) by +2.
129
+ const leftHangIndent = " ".repeat(indent + leftLeadingWidth + 2);
130
+ const leftLines = wrapWords(row.left, leftWrapWidth, Math.max(1, totalWidth - leftHangIndent.length));
99
131
  if (row.right.length === 0) {
100
- return [`${firstIndent}${row.left}`];
132
+ return leftLines.map((line, index) => index === 0 ? `${firstIndent}${line}` : `${leftHangIndent}${line}`);
101
133
  }
102
134
  const rightLines = wrapWords(row.right, rightWidth);
103
- if (row.left.length >= leftWidth) {
104
- return [
105
- `${firstIndent}${row.left}`,
106
- ...rightLines.map((line) => `${continuationIndent}${line}`)
107
- ];
135
+ const leftFitsInColumn = row.left.length < leftWidth;
136
+ if (leftFitsInColumn && leftLines.length === 1) {
137
+ const firstLine = `${firstIndent}${padEndVisible(leftLines[0] ?? "", leftWidth)}${rightLines[0]}`;
138
+ const continuationLines = rightLines
139
+ .slice(1)
140
+ .map((line) => `${continuationIndent}${line}`);
141
+ return [firstLine, ...continuationLines];
108
142
  }
109
- const firstLine = `${firstIndent}${padEndVisible(row.left, leftWidth)}${rightLines[0]}`;
110
- const continuationLines = rightLines
111
- .slice(1)
112
- .map((line) => `${continuationIndent}${line}`);
113
- return [firstLine, ...continuationLines];
143
+ const renderedLeft = leftLines.map((line, index) => index === 0 ? `${firstIndent}${line}` : `${leftHangIndent}${line}`);
144
+ const renderedRight = rightLines.map((line) => `${continuationIndent}${line}`);
145
+ return [...renderedLeft, ...renderedRight];
114
146
  })
115
147
  .join("\n");
116
148
  }
117
149
  export function formatCommandList(commands) {
118
150
  return formatColumns({
119
151
  rows: commands.map((cmd) => ({
120
- left: `${" ".repeat((cmd.depth ?? 0) * 2)}${cmd.name}`,
152
+ left: `${" ".repeat((cmd.depth ?? 0) * 2)}${cmd.nameTokens !== undefined && cmd.nameTokens.length > 0
153
+ ? joinHelpTokens(cmd.nameTokens)
154
+ : cmd.name}`,
121
155
  right: cmd.description
122
156
  }))
123
157
  });
@@ -125,7 +159,9 @@ export function formatCommandList(commands) {
125
159
  export function formatOptionList(options) {
126
160
  return formatColumns({
127
161
  rows: options.map((opt) => ({
128
- left: opt.flags,
162
+ left: opt.flagTokens !== undefined && opt.flagTokens.length > 0
163
+ ? joinHelpTokens(opt.flagTokens)
164
+ : opt.flags,
129
165
  right: opt.description
130
166
  }))
131
167
  });
@@ -1,11 +1,20 @@
1
+ export type HelpTokenRole = "command" | "argument" | "option" | "literal" | "dim";
2
+ export interface HelpToken {
3
+ text: string;
4
+ role: HelpTokenRole;
5
+ }
1
6
  export interface CommandInfo {
2
7
  name: string;
8
+ /** Structured tokens for TTY/markdown styling. Plain `name` is used when absent. */
9
+ nameTokens?: HelpToken[];
3
10
  description: string;
4
11
  /** Nesting depth relative to the help target. Depth 0 is a direct child. */
5
12
  depth?: number;
6
13
  }
7
14
  export interface OptionInfo {
8
15
  flags: string;
16
+ /** Structured tokens for TTY/markdown styling. Plain `flags` is used when absent. */
17
+ flagTokens?: HelpToken[];
9
18
  description: string;
10
19
  }
11
20
  export interface FormatColumnsOptions {
@@ -20,6 +29,9 @@ export interface FormatColumnsOptions {
20
29
  indent?: number;
21
30
  }
22
31
  export declare function formatColumns(opts: FormatColumnsOptions): string;
32
+ export declare function styleHelpToken(token: HelpToken): string;
33
+ export declare function joinHelpTokens(tokens: HelpToken[]): string;
34
+ export declare function renderHelpTokens(tokens: HelpToken[]): string;
23
35
  export declare function formatCommand(name: string, description: string): string;
24
36
  export declare function formatUsage(command: string, args?: string): string;
25
37
  export declare function formatOption(flags: string, description: string): string;
@@ -32,4 +44,7 @@ export declare const helpFormatter: {
32
44
  readonly formatOption: typeof formatOption;
33
45
  readonly formatCommandList: typeof formatCommandList;
34
46
  readonly formatOptionList: typeof formatOptionList;
47
+ readonly styleHelpToken: typeof styleHelpToken;
48
+ readonly joinHelpTokens: typeof joinHelpTokens;
49
+ readonly renderHelpTokens: typeof renderHelpTokens;
35
50
  };
@@ -1,3 +1,5 @@
1
+ import { resolveOutputFormat } from "../internal/output-format.js";
2
+ import { typography } from "../tokens/typography.js";
1
3
  import { text } from "./text.js";
2
4
  const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
3
5
  function normalizeInline(value) {
@@ -103,26 +105,67 @@ function splitWords(value) {
103
105
  }
104
106
  return words;
105
107
  }
106
- function wrapWords(value, width) {
107
- const words = splitWords(value);
108
+ function leadingWhitespaceWidth(value) {
109
+ let index = 0;
110
+ while (index < value.length && isWhitespace(value[index])) {
111
+ index += 1;
112
+ }
113
+ return { prefix: value.slice(0, index), rest: value.slice(index) };
114
+ }
115
+ function takeVisiblePrefix(value, width) {
116
+ let visible = 0;
117
+ let index = 0;
118
+ while (index < value.length) {
119
+ const controlEnd = readControlSequence(value, index);
120
+ if (controlEnd !== undefined) {
121
+ index = controlEnd;
122
+ continue;
123
+ }
124
+ const segment = graphemeSegmenter.segment(value.slice(index))[Symbol.iterator]().next().value;
125
+ const cluster = segment?.segment ?? value[index] ?? "";
126
+ const nextWidth = clusterWidth(cluster);
127
+ if (visible > 0 && visible + nextWidth > width) {
128
+ break;
129
+ }
130
+ visible += nextWidth;
131
+ index += cluster.length || 1;
132
+ }
133
+ return { prefix: value.slice(0, index), rest: value.slice(index) };
134
+ }
135
+ function wrapWords(value, width, continuationWidth = width) {
136
+ // Preserve leading whitespace only on the first wrapped line so hang-indented
137
+ // left cells (command depth prefixes) do not re-indent every continuation.
138
+ const { prefix, rest } = leadingWhitespaceWidth(value);
139
+ const prefixWidth = visibleWidth(prefix);
140
+ const firstContentWidth = Math.max(1, width - prefixWidth);
141
+ const words = splitWords(rest);
108
142
  if (words.length === 0) {
109
- return [""];
143
+ return [prefix];
110
144
  }
111
145
  const lines = [];
112
146
  let line = "";
147
+ let isFirstLine = true;
113
148
  for (const word of words) {
114
- if (!line) {
115
- line = word;
116
- continue;
117
- }
118
- if (visibleWidth(line) + 1 + visibleWidth(word) <= width) {
149
+ const limit = isFirstLine ? firstContentWidth : continuationWidth;
150
+ if (line && visibleWidth(line) + 1 + visibleWidth(word) <= limit) {
119
151
  line += ` ${word}`;
120
152
  continue;
121
153
  }
122
- lines.push(line);
123
- line = word;
154
+ if (line) {
155
+ lines.push(isFirstLine ? `${prefix}${line}` : line);
156
+ isFirstLine = false;
157
+ line = "";
158
+ }
159
+ let remaining = word;
160
+ while (visibleWidth(remaining) > (isFirstLine ? firstContentWidth : continuationWidth)) {
161
+ const chunk = takeVisiblePrefix(remaining, isFirstLine ? firstContentWidth : continuationWidth);
162
+ lines.push(isFirstLine ? `${prefix}${chunk.prefix}` : chunk.prefix);
163
+ isFirstLine = false;
164
+ remaining = chunk.rest;
165
+ }
166
+ line = remaining;
124
167
  }
125
- lines.push(line);
168
+ lines.push(isFirstLine ? `${prefix}${line}` : line);
126
169
  return lines;
127
170
  }
128
171
  function validateLayoutValue(value, name) {
@@ -150,29 +193,74 @@ export function formatColumns(opts) {
150
193
  validateLayoutValue(indent, "indent");
151
194
  const maxLeftContentWidth = Math.max(...rows.map((row) => visibleWidth(row.left)));
152
195
  const leftWidth = clamp(maxLeftContentWidth + gap, minLeftWidth, maxLeftWidth);
153
- const rightWidth = Math.max(20, totalWidth - leftWidth - indent);
196
+ const rightWidth = Math.max(1, totalWidth - leftWidth - indent);
197
+ const leftWrapWidth = Math.max(1, totalWidth - indent);
154
198
  const firstIndent = " ".repeat(indent);
155
199
  const continuationIndent = " ".repeat(indent + leftWidth);
156
200
  return rows
157
201
  .flatMap((row) => {
202
+ const leftLeading = leadingWhitespaceWidth(row.left).prefix;
203
+ // Continuations hang under the left cell start (including depth prefix) by +2.
204
+ const leftHangIndent = " ".repeat(indent + visibleWidth(leftLeading) + 2);
205
+ const leftLines = wrapWords(row.left, leftWrapWidth, Math.max(1, totalWidth - visibleWidth(leftHangIndent)));
158
206
  if (row.right.length === 0) {
159
- return [`${firstIndent}${row.left}`];
207
+ return leftLines.map((line, index) => index === 0 ? `${firstIndent}${line}` : `${leftHangIndent}${line}`);
160
208
  }
161
209
  const rightLines = wrapWords(row.right, rightWidth);
162
- if (visibleWidth(row.left) >= leftWidth) {
163
- return [
164
- `${firstIndent}${row.left}`,
165
- ...rightLines.map((line) => `${continuationIndent}${line}`)
166
- ];
210
+ const leftFitsInColumn = visibleWidth(row.left) < leftWidth;
211
+ if (leftFitsInColumn && leftLines.length === 1) {
212
+ const firstLine = `${firstIndent}${padEndVisible(leftLines[0] ?? "", leftWidth)}${rightLines[0]}`;
213
+ const continuationLines = rightLines
214
+ .slice(1)
215
+ .map((line) => `${continuationIndent}${line}`);
216
+ return [firstLine, ...continuationLines];
167
217
  }
168
- const firstLine = `${firstIndent}${padEndVisible(row.left, leftWidth)}${rightLines[0]}`;
169
- const continuationLines = rightLines
170
- .slice(1)
171
- .map((line) => `${continuationIndent}${line}`);
172
- return [firstLine, ...continuationLines];
218
+ const renderedLeft = leftLines.map((line, index) => index === 0 ? `${firstIndent}${line}` : `${leftHangIndent}${line}`);
219
+ const renderedRight = rightLines.map((line) => `${continuationIndent}${line}`);
220
+ return [...renderedLeft, ...renderedRight];
173
221
  })
174
222
  .join("\n");
175
223
  }
224
+ export function styleHelpToken(token) {
225
+ switch (token.role) {
226
+ case "command":
227
+ return text.command(token.text);
228
+ case "argument":
229
+ return styleArgumentToken(token.text);
230
+ case "option":
231
+ return text.option(token.text);
232
+ case "dim":
233
+ return styleDim(token.text);
234
+ case "literal":
235
+ return token.text;
236
+ }
237
+ }
238
+ function styleArgumentToken(content) {
239
+ // Token text already includes angle brackets. text.argument re-wraps in markdown,
240
+ // so strip first there; terminal/json keep the full `<value>` form.
241
+ const format = resolveOutputFormat();
242
+ if (format === "markdown" && content.startsWith("<") && content.endsWith(">")) {
243
+ return text.argument(content.slice(1, -1));
244
+ }
245
+ if (format === "json") {
246
+ return content;
247
+ }
248
+ return text.argument(content);
249
+ }
250
+ function styleDim(content) {
251
+ // Structural brackets stay unstyled in markdown/json; italicizing them as muted is wrong.
252
+ const format = resolveOutputFormat();
253
+ if (format === "json" || format === "markdown") {
254
+ return content;
255
+ }
256
+ return typography.dim(content);
257
+ }
258
+ export function joinHelpTokens(tokens) {
259
+ return tokens.map((token) => token.text).join("");
260
+ }
261
+ export function renderHelpTokens(tokens) {
262
+ return tokens.map((token) => styleHelpToken(token)).join("");
263
+ }
176
264
  export function formatCommand(name, description) {
177
265
  return formatColumns({
178
266
  rows: [{ left: text.command(name), right: description }]
@@ -189,16 +277,24 @@ export function formatOption(flags, description) {
189
277
  }
190
278
  export function formatCommandList(commands) {
191
279
  return formatColumns({
192
- rows: commands.map((cmd) => ({
193
- left: `${" ".repeat((cmd.depth ?? 0) * 2)}${text.command(cmd.name)}`,
194
- right: cmd.description
195
- }))
280
+ rows: commands.map((cmd) => {
281
+ const depthPrefix = " ".repeat((cmd.depth ?? 0) * 2);
282
+ const styledName = cmd.nameTokens !== undefined && cmd.nameTokens.length > 0
283
+ ? renderHelpTokens(cmd.nameTokens)
284
+ : text.command(cmd.name);
285
+ return {
286
+ left: `${depthPrefix}${styledName}`,
287
+ right: cmd.description
288
+ };
289
+ })
196
290
  });
197
291
  }
198
292
  export function formatOptionList(options) {
199
293
  return formatColumns({
200
294
  rows: options.map((opt) => ({
201
- left: text.option(opt.flags),
295
+ left: opt.flagTokens !== undefined && opt.flagTokens.length > 0
296
+ ? renderHelpTokens(opt.flagTokens)
297
+ : text.option(opt.flags),
202
298
  right: opt.description
203
299
  }))
204
300
  });
@@ -209,5 +305,8 @@ export const helpFormatter = {
209
305
  formatUsage,
210
306
  formatOption,
211
307
  formatCommandList,
212
- formatOptionList
308
+ formatOptionList,
309
+ styleHelpToken,
310
+ joinHelpTokens,
311
+ renderHelpTokens
213
312
  };
@@ -4,8 +4,8 @@ export type { Color } from "./color.js";
4
4
  export { symbols } from "./symbols.js";
5
5
  export { createLogger, logger } from "./logger.js";
6
6
  export type { LoggerOutput } from "./logger.js";
7
- export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList } from "./help-formatter.js";
8
- export type { CommandInfo, OptionInfo, FormatColumnsOptions } from "./help-formatter.js";
7
+ export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList, styleHelpToken, joinHelpTokens, renderHelpTokens } from "./help-formatter.js";
8
+ export type { CommandInfo, OptionInfo, FormatColumnsOptions, HelpToken, HelpTokenRole } from "./help-formatter.js";
9
9
  export { formatCommandNotFound } from "./command-errors.js";
10
10
  export { formatCommandNotFoundPanel } from "./command-errors.js";
11
11
  export { renderTable } from "./table.js";
@@ -2,7 +2,7 @@ export { text } from "./text.js";
2
2
  export { color } from "./color.js";
3
3
  export { symbols } from "./symbols.js";
4
4
  export { createLogger, logger } from "./logger.js";
5
- export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList } from "./help-formatter.js";
5
+ export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList, styleHelpToken, joinHelpTokens, renderHelpTokens } from "./help-formatter.js";
6
6
  export { formatCommandNotFound } from "./command-errors.js";
7
7
  export { formatCommandNotFoundPanel } from "./command-errors.js";
8
8
  export { renderTable } from "./table.js";
@@ -1,2 +1,2 @@
1
- export { helpFormatter } from "./components/help-formatter.js";
2
- export type { CommandInfo, FormatColumnsOptions, OptionInfo } from "./components/help-formatter.js";
1
+ export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList, styleHelpToken, joinHelpTokens, renderHelpTokens } from "./components/help-formatter.js";
2
+ export type { CommandInfo, FormatColumnsOptions, OptionInfo, HelpToken, HelpTokenRole } from "./components/help-formatter.js";
@@ -1 +1 @@
1
- export { helpFormatter } from "./components/help-formatter.js";
1
+ export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList, styleHelpToken, joinHelpTokens, renderHelpTokens } from "./components/help-formatter.js";
@@ -12,9 +12,9 @@ export type { Color } from "./components/color.js";
12
12
  export { symbols } from "./components/symbols.js";
13
13
  export { createLogger, logger } from "./components/logger.js";
14
14
  export type { LoggerOutput } from "./components/logger.js";
15
- export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList } from "./components/help-formatter.js";
15
+ export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList, styleHelpToken, joinHelpTokens, renderHelpTokens } from "./components/help-formatter.js";
16
16
  export * as helpFormatterPlain from "./components/help-formatter-plain.js";
17
- export type { CommandInfo, OptionInfo, FormatColumnsOptions } from "./components/help-formatter.js";
17
+ export type { CommandInfo, OptionInfo, FormatColumnsOptions, HelpToken, HelpTokenRole } from "./components/help-formatter.js";
18
18
  export { formatCommandNotFound } from "./components/command-errors.js";
19
19
  export { formatCommandNotFoundPanel } from "./components/command-errors.js";
20
20
  export { renderTable } from "./components/table.js";
@@ -10,7 +10,7 @@ export { text } from "./components/text.js";
10
10
  export { color } from "./components/color.js";
11
11
  export { symbols } from "./components/symbols.js";
12
12
  export { createLogger, logger } from "./components/logger.js";
13
- export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList } from "./components/help-formatter.js";
13
+ export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList, styleHelpToken, joinHelpTokens, renderHelpTokens } from "./components/help-formatter.js";
14
14
  export * as helpFormatterPlain from "./components/help-formatter-plain.js";
15
15
  export { formatCommandNotFound } from "./components/command-errors.js";
16
16
  export { formatCommandNotFoundPanel } from "./components/command-errors.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.130",
3
+ "version": "0.0.131",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.130",
3
+ "version": "0.0.131",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -153,7 +153,7 @@
153
153
  "yaml"
154
154
  ],
155
155
  "optionalDependencies": {
156
- "toolcraft-schema": "0.0.130",
156
+ "toolcraft-schema": "0.0.131",
157
157
  "toolcraft-design": "*",
158
158
  "@poe-code/frontmatter": "*",
159
159
  "@poe-code/agent-mcp-config": "*",