@guanghechen/commander 4.5.0 → 4.5.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Add built-in host and network validators (ip, domain, host), expose is helpers, and extend coerce
8
+ factories with port and choice support.
9
+
3
10
  ## 4.5.0
4
11
 
5
12
  ### Minor Changes
package/README.md CHANGED
@@ -229,6 +229,41 @@ new Command({ name: 'example', desc: 'Coerce demo' })
229
229
  coerce: Coerce.positiveNumber('--duration'),
230
230
  desc: 'Duration in seconds',
231
231
  })
232
+ .option({
233
+ long: 'port',
234
+ type: 'number',
235
+ args: 'required',
236
+ coerce: Coerce.port('--port'),
237
+ desc: 'Server port',
238
+ })
239
+ .option({
240
+ long: 'domain',
241
+ type: 'string',
242
+ args: 'required',
243
+ coerce: Coerce.domain('--domain'),
244
+ desc: 'Domain name',
245
+ })
246
+ .option({
247
+ long: 'ip',
248
+ type: 'string',
249
+ args: 'required',
250
+ coerce: Coerce.ip('--ip'),
251
+ desc: 'IP address',
252
+ })
253
+ .option({
254
+ long: 'host',
255
+ type: 'string',
256
+ args: 'required',
257
+ coerce: Coerce.host('--host'),
258
+ desc: 'Host (IP or domain)',
259
+ })
260
+ .option({
261
+ long: 'mode',
262
+ type: 'string',
263
+ args: 'required',
264
+ coerce: Coerce.choice('--mode', ['dev', 'test', 'prod'] as const),
265
+ desc: 'Deploy mode',
266
+ })
232
267
  .option({
233
268
  long: 'scale',
234
269
  type: 'number',
@@ -246,6 +281,17 @@ Default error message format:
246
281
 
247
282
  You can still override the message via `Coerce.xxx(name, 'custom error message')`.
248
283
 
284
+ ### Built-in Is Helpers
285
+
286
+ ```typescript
287
+ import { isDomain, isIp, isIpv4, isIpv6 } from '@guanghechen/commander'
288
+
289
+ isIpv4('127.0.0.1') // true
290
+ isIpv6('::1') // true
291
+ isIp('2001:db8::1') // true
292
+ isDomain('example.com') // true
293
+ ```
294
+
249
295
  ### Help Examples
250
296
 
251
297
  ```typescript
@@ -263,11 +309,11 @@ await cli.run({ argv: ['--help'], envs: process.env })
263
309
 
264
310
  `usage` 是相对当前 command path 的片段,help 中会自动补齐前缀,例如 `mycli build --watch`。
265
311
 
266
- `--color` / `--no-color` 仅控制 help 文本的终端着色;
267
- `--log-colorful` / `--no-log-colorful` 控制 `Reporter` 的日志着色。
312
+ `--color` / `--no-color` 仅控制 help 文本的终端着色; `--log-colorful` / `--no-log-colorful` 控制
313
+ `Reporter` 的日志着色。
268
314
 
269
- 当环境变量 `NO_COLOR` 存在时,help 渲染默认视为 `--no-color`;
270
- 显式传入 `--color` 可以覆盖这个默认值。
315
+ 当环境变量 `NO_COLOR` 存在时,help 渲染默认视为 `--no-color`;显式传入 `--color`
316
+ 可以覆盖这个默认值。
271
317
 
272
318
  ## Reference
273
319
 
package/lib/cjs/index.cjs CHANGED
@@ -1186,6 +1186,96 @@ class Command {
1186
1186
  }
1187
1187
  }
1188
1188
 
1189
+ function isIpv4(rawValue) {
1190
+ const parts = rawValue.split('.');
1191
+ if (parts.length !== 4) {
1192
+ return false;
1193
+ }
1194
+ for (const part of parts) {
1195
+ if (part.length < 1 || !/^\d+$/.test(part)) {
1196
+ return false;
1197
+ }
1198
+ if (part.length > 1 && part.startsWith('0')) {
1199
+ return false;
1200
+ }
1201
+ const value = Number(part);
1202
+ if (!Number.isInteger(value) || value < 0 || value > 255) {
1203
+ return false;
1204
+ }
1205
+ }
1206
+ return true;
1207
+ }
1208
+ function countIpv6Segments(part, allowIpv4Tail) {
1209
+ if (!part) {
1210
+ return { count: 0, hasIpv4Tail: false };
1211
+ }
1212
+ const segments = part.split(':');
1213
+ let count = 0;
1214
+ let hasIpv4Tail = false;
1215
+ for (let i = 0; i < segments.length; ++i) {
1216
+ const segment = segments[i];
1217
+ const isLastSegment = i === segments.length - 1;
1218
+ if (!segment) {
1219
+ return null;
1220
+ }
1221
+ if (segment.includes('.')) {
1222
+ if (!allowIpv4Tail || !isLastSegment || hasIpv4Tail || !isIpv4(segment)) {
1223
+ return null;
1224
+ }
1225
+ hasIpv4Tail = true;
1226
+ count += 2;
1227
+ continue;
1228
+ }
1229
+ if (!/^[0-9A-Fa-f]{1,4}$/.test(segment)) {
1230
+ return null;
1231
+ }
1232
+ count += 1;
1233
+ }
1234
+ return { count, hasIpv4Tail };
1235
+ }
1236
+ function isIpv6(rawValue) {
1237
+ if (!rawValue || !/^[0-9A-Fa-f:.]+$/.test(rawValue)) {
1238
+ return false;
1239
+ }
1240
+ const doubleColonCount = rawValue.split('::').length - 1;
1241
+ if (doubleColonCount > 1) {
1242
+ return false;
1243
+ }
1244
+ if (doubleColonCount === 0) {
1245
+ const full = countIpv6Segments(rawValue, true);
1246
+ return full !== null && full.count === 8;
1247
+ }
1248
+ const [left, right] = rawValue.split('::');
1249
+ const leftPart = countIpv6Segments(left, right.length === 0);
1250
+ const rightPart = countIpv6Segments(right, true);
1251
+ if (!leftPart || !rightPart) {
1252
+ return false;
1253
+ }
1254
+ const totalSegments = leftPart.count + rightPart.count;
1255
+ return totalSegments < 8;
1256
+ }
1257
+ function isIp(rawValue) {
1258
+ return isIpv4(rawValue) || isIpv6(rawValue);
1259
+ }
1260
+ function isDomain(rawValue) {
1261
+ if (rawValue.length < 1 || rawValue.length > 253 || rawValue.endsWith('.')) {
1262
+ return false;
1263
+ }
1264
+ const labels = rawValue.split('.');
1265
+ if (labels.length < 2) {
1266
+ return false;
1267
+ }
1268
+ if (labels.some(label => label.length < 1 || label.length > 63)) {
1269
+ return false;
1270
+ }
1271
+ const labelPattern = /^[A-Za-z0-9-]+$/;
1272
+ if (labels.some(label => !labelPattern.test(label) || label.startsWith('-') || label.endsWith('-'))) {
1273
+ return false;
1274
+ }
1275
+ const topLevelLabel = labels[labels.length - 1];
1276
+ return /[A-Za-z]/.test(topLevelLabel);
1277
+ }
1278
+
1189
1279
  class Coerce {
1190
1280
  constructor() { }
1191
1281
  static create(name, expectedType, validator, errorMessage) {
@@ -1197,12 +1287,47 @@ class Coerce {
1197
1287
  return value;
1198
1288
  };
1199
1289
  }
1200
- static number(name, errorMessage) {
1201
- return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
1290
+ static choice(name, values, errorMessage) {
1291
+ return (rawValue) => {
1292
+ if (values.includes(rawValue)) {
1293
+ return rawValue;
1294
+ }
1295
+ throw new Error(errorMessage ?? `${name} is expected as one of [${values.join(', ')}], but got ${rawValue}`);
1296
+ };
1297
+ }
1298
+ static domain(name, errorMessage) {
1299
+ return (rawValue) => {
1300
+ if (isDomain(rawValue)) {
1301
+ return rawValue;
1302
+ }
1303
+ throw new Error(errorMessage ?? `${name} is expected as a valid domain, but got ${rawValue}`);
1304
+ };
1305
+ }
1306
+ static host(name, errorMessage) {
1307
+ return (rawValue) => {
1308
+ if (isIp(rawValue) || isDomain(rawValue)) {
1309
+ return rawValue;
1310
+ }
1311
+ throw new Error(errorMessage ?? `${name} is expected as a valid host (IP or domain), but got ${rawValue}`);
1312
+ };
1202
1313
  }
1203
1314
  static integer(name, errorMessage) {
1204
1315
  return this.create(name, 'an integer', value => Number.isInteger(value), errorMessage);
1205
1316
  }
1317
+ static ip(name, errorMessage) {
1318
+ return (rawValue) => {
1319
+ if (isIp(rawValue)) {
1320
+ return rawValue;
1321
+ }
1322
+ throw new Error(errorMessage ?? `${name} is expected as a valid IP address, but got ${rawValue}`);
1323
+ };
1324
+ }
1325
+ static number(name, errorMessage) {
1326
+ return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
1327
+ }
1328
+ static port(name, errorMessage) {
1329
+ return this.create(name, 'a valid port number (0-65535)', value => Number.isInteger(value) && value >= 0 && value <= 65535, errorMessage);
1330
+ }
1206
1331
  static positiveInteger(name, errorMessage) {
1207
1332
  return this.create(name, 'a positive integer', value => Number.isInteger(value) && value > 0, errorMessage);
1208
1333
  }
@@ -1576,6 +1701,10 @@ exports.CommanderError = CommanderError;
1576
1701
  exports.CompletionCommand = CompletionCommand;
1577
1702
  exports.FishCompletion = FishCompletion;
1578
1703
  exports.PwshCompletion = PwshCompletion;
1704
+ exports.isDomain = isDomain;
1705
+ exports.isIp = isIp;
1706
+ exports.isIpv4 = isIpv4;
1707
+ exports.isIpv6 = isIpv6;
1579
1708
  exports.logColorfulOption = logColorfulOption;
1580
1709
  exports.logDateOption = logDateOption;
1581
1710
  exports.logLevelOption = logLevelOption;
package/lib/esm/index.mjs CHANGED
@@ -1164,6 +1164,96 @@ class Command {
1164
1164
  }
1165
1165
  }
1166
1166
 
1167
+ function isIpv4(rawValue) {
1168
+ const parts = rawValue.split('.');
1169
+ if (parts.length !== 4) {
1170
+ return false;
1171
+ }
1172
+ for (const part of parts) {
1173
+ if (part.length < 1 || !/^\d+$/.test(part)) {
1174
+ return false;
1175
+ }
1176
+ if (part.length > 1 && part.startsWith('0')) {
1177
+ return false;
1178
+ }
1179
+ const value = Number(part);
1180
+ if (!Number.isInteger(value) || value < 0 || value > 255) {
1181
+ return false;
1182
+ }
1183
+ }
1184
+ return true;
1185
+ }
1186
+ function countIpv6Segments(part, allowIpv4Tail) {
1187
+ if (!part) {
1188
+ return { count: 0, hasIpv4Tail: false };
1189
+ }
1190
+ const segments = part.split(':');
1191
+ let count = 0;
1192
+ let hasIpv4Tail = false;
1193
+ for (let i = 0; i < segments.length; ++i) {
1194
+ const segment = segments[i];
1195
+ const isLastSegment = i === segments.length - 1;
1196
+ if (!segment) {
1197
+ return null;
1198
+ }
1199
+ if (segment.includes('.')) {
1200
+ if (!allowIpv4Tail || !isLastSegment || hasIpv4Tail || !isIpv4(segment)) {
1201
+ return null;
1202
+ }
1203
+ hasIpv4Tail = true;
1204
+ count += 2;
1205
+ continue;
1206
+ }
1207
+ if (!/^[0-9A-Fa-f]{1,4}$/.test(segment)) {
1208
+ return null;
1209
+ }
1210
+ count += 1;
1211
+ }
1212
+ return { count, hasIpv4Tail };
1213
+ }
1214
+ function isIpv6(rawValue) {
1215
+ if (!rawValue || !/^[0-9A-Fa-f:.]+$/.test(rawValue)) {
1216
+ return false;
1217
+ }
1218
+ const doubleColonCount = rawValue.split('::').length - 1;
1219
+ if (doubleColonCount > 1) {
1220
+ return false;
1221
+ }
1222
+ if (doubleColonCount === 0) {
1223
+ const full = countIpv6Segments(rawValue, true);
1224
+ return full !== null && full.count === 8;
1225
+ }
1226
+ const [left, right] = rawValue.split('::');
1227
+ const leftPart = countIpv6Segments(left, right.length === 0);
1228
+ const rightPart = countIpv6Segments(right, true);
1229
+ if (!leftPart || !rightPart) {
1230
+ return false;
1231
+ }
1232
+ const totalSegments = leftPart.count + rightPart.count;
1233
+ return totalSegments < 8;
1234
+ }
1235
+ function isIp(rawValue) {
1236
+ return isIpv4(rawValue) || isIpv6(rawValue);
1237
+ }
1238
+ function isDomain(rawValue) {
1239
+ if (rawValue.length < 1 || rawValue.length > 253 || rawValue.endsWith('.')) {
1240
+ return false;
1241
+ }
1242
+ const labels = rawValue.split('.');
1243
+ if (labels.length < 2) {
1244
+ return false;
1245
+ }
1246
+ if (labels.some(label => label.length < 1 || label.length > 63)) {
1247
+ return false;
1248
+ }
1249
+ const labelPattern = /^[A-Za-z0-9-]+$/;
1250
+ if (labels.some(label => !labelPattern.test(label) || label.startsWith('-') || label.endsWith('-'))) {
1251
+ return false;
1252
+ }
1253
+ const topLevelLabel = labels[labels.length - 1];
1254
+ return /[A-Za-z]/.test(topLevelLabel);
1255
+ }
1256
+
1167
1257
  class Coerce {
1168
1258
  constructor() { }
1169
1259
  static create(name, expectedType, validator, errorMessage) {
@@ -1175,12 +1265,47 @@ class Coerce {
1175
1265
  return value;
1176
1266
  };
1177
1267
  }
1178
- static number(name, errorMessage) {
1179
- return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
1268
+ static choice(name, values, errorMessage) {
1269
+ return (rawValue) => {
1270
+ if (values.includes(rawValue)) {
1271
+ return rawValue;
1272
+ }
1273
+ throw new Error(errorMessage ?? `${name} is expected as one of [${values.join(', ')}], but got ${rawValue}`);
1274
+ };
1275
+ }
1276
+ static domain(name, errorMessage) {
1277
+ return (rawValue) => {
1278
+ if (isDomain(rawValue)) {
1279
+ return rawValue;
1280
+ }
1281
+ throw new Error(errorMessage ?? `${name} is expected as a valid domain, but got ${rawValue}`);
1282
+ };
1283
+ }
1284
+ static host(name, errorMessage) {
1285
+ return (rawValue) => {
1286
+ if (isIp(rawValue) || isDomain(rawValue)) {
1287
+ return rawValue;
1288
+ }
1289
+ throw new Error(errorMessage ?? `${name} is expected as a valid host (IP or domain), but got ${rawValue}`);
1290
+ };
1180
1291
  }
1181
1292
  static integer(name, errorMessage) {
1182
1293
  return this.create(name, 'an integer', value => Number.isInteger(value), errorMessage);
1183
1294
  }
1295
+ static ip(name, errorMessage) {
1296
+ return (rawValue) => {
1297
+ if (isIp(rawValue)) {
1298
+ return rawValue;
1299
+ }
1300
+ throw new Error(errorMessage ?? `${name} is expected as a valid IP address, but got ${rawValue}`);
1301
+ };
1302
+ }
1303
+ static number(name, errorMessage) {
1304
+ return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
1305
+ }
1306
+ static port(name, errorMessage) {
1307
+ return this.create(name, 'a valid port number (0-65535)', value => Number.isInteger(value) && value >= 0 && value <= 65535, errorMessage);
1308
+ }
1184
1309
  static positiveInteger(name, errorMessage) {
1185
1310
  return this.create(name, 'a positive integer', value => Number.isInteger(value) && value > 0, errorMessage);
1186
1311
  }
@@ -1547,4 +1672,4 @@ class PwshCompletion {
1547
1672
  }
1548
1673
  }
1549
1674
 
1550
- export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
1675
+ export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, isDomain, isIp, isIpv4, isIpv6, logColorfulOption, logDateOption, logLevelOption, silentOption };
@@ -223,6 +223,56 @@ interface ICommandParseResult {
223
223
  /** Raw argument strings */
224
224
  rawArgs: string[];
225
225
  }
226
+ /** Built-in option resolution result (internal) */
227
+ interface ICommandBuiltinOptionResolved {
228
+ color: boolean;
229
+ logLevel: boolean;
230
+ silent: boolean;
231
+ logDate: boolean;
232
+ logColorful: boolean;
233
+ }
234
+ /** Built-in config resolution result (internal) */
235
+ interface ICommandBuiltinResolved {
236
+ option: ICommandBuiltinOptionResolved;
237
+ command: {
238
+ help: boolean;
239
+ };
240
+ }
241
+ /** Subcommand registry entry (internal) */
242
+ interface ISubcommandEntry<TCommand = ICommand> {
243
+ name: string;
244
+ aliases: string[];
245
+ command: TCommand;
246
+ }
247
+ /** Internal route result */
248
+ interface IInternalRouteResult<TCommand = ICommand> {
249
+ chain: TCommand[];
250
+ remaining: string[];
251
+ }
252
+ /** Help option line (internal) */
253
+ interface IHelpOptionLine {
254
+ sig: string;
255
+ desc: string;
256
+ }
257
+ /** Help command line (internal) */
258
+ interface IHelpCommandLine {
259
+ name: string;
260
+ desc: string;
261
+ }
262
+ /** Help example line (internal) */
263
+ interface IHelpExampleLine {
264
+ title: string;
265
+ usage: string;
266
+ desc: string;
267
+ }
268
+ /** Structured help data for rendering (internal) */
269
+ interface IHelpData {
270
+ desc: string;
271
+ usage: string;
272
+ options: IHelpOptionLine[];
273
+ commands: IHelpCommandLine[];
274
+ examples: IHelpExampleLine[];
275
+ }
226
276
  /** Error kinds for command parsing */
227
277
  type ICommanderErrorKind = 'InvalidOptionFormat' | 'InvalidNegativeOption' | 'NegativeOptionWithValue' | 'NegativeOptionType' | 'UnknownOption' | 'UnknownSubcommand' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'TooManyArguments' | 'ConfigurationError';
228
278
  /** Commander error with structured information */
@@ -316,12 +366,22 @@ declare class Command implements ICommand {
316
366
  declare class Coerce {
317
367
  private constructor();
318
368
  private static create;
319
- static number(name: string, errorMessage?: string): (rawValue: string) => number;
369
+ static choice<TValue extends string>(name: string, values: ReadonlyArray<TValue>, errorMessage?: string): (rawValue: string) => TValue;
370
+ static domain(name: string, errorMessage?: string): (rawValue: string) => string;
371
+ static host(name: string, errorMessage?: string): (rawValue: string) => string;
320
372
  static integer(name: string, errorMessage?: string): (rawValue: string) => number;
373
+ static ip(name: string, errorMessage?: string): (rawValue: string) => string;
374
+ static number(name: string, errorMessage?: string): (rawValue: string) => number;
375
+ static port(name: string, errorMessage?: string): (rawValue: string) => number;
321
376
  static positiveInteger(name: string, errorMessage?: string): (rawValue: string) => number;
322
377
  static positiveNumber(name: string, errorMessage?: string): (rawValue: string) => number;
323
378
  }
324
379
 
380
+ declare function isIpv4(rawValue: string): boolean;
381
+ declare function isIpv6(rawValue: string): boolean;
382
+ declare function isIp(rawValue: string): boolean;
383
+ declare function isDomain(rawValue: string): boolean;
384
+
325
385
  /**
326
386
  * Shell completion generators
327
387
  *
@@ -450,5 +510,5 @@ declare const logColorfulOption: ICommandOptionConfig<boolean>;
450
510
  */
451
511
  declare const silentOption: ICommandOptionConfig<boolean>;
452
512
 
453
- export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
454
- export type { ICommand, ICommandAction, ICommandActionParams, ICommandArgumentConfig, ICommandArgumentKind, ICommandArgumentType, ICommandBuiltinCommandConfig, ICommandBuiltinConfig, ICommandBuiltinOptionConfig, ICommandConfig, ICommandContext, ICommandExample, ICommandOptionArgs, ICommandOptionConfig, ICommandOptionType, ICommandParseResult, ICommandParsedArgs, ICommandParsedOpts, ICommandResolveResult, ICommandRouteResult, ICommandRunParams, ICommandShiftResult, ICommandToken, ICommandTokenType, ICommandTokenizeResult, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, ICompletionShellType };
513
+ export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, isDomain, isIp, isIpv4, isIpv6, logColorfulOption, logDateOption, logLevelOption, silentOption };
514
+ export type { ICommand, ICommandAction, ICommandActionParams, ICommandArgumentConfig, ICommandArgumentKind, ICommandArgumentType, ICommandBuiltinCommandConfig, ICommandBuiltinConfig, ICommandBuiltinOptionConfig, ICommandBuiltinOptionResolved, ICommandBuiltinResolved, ICommandConfig, ICommandContext, ICommandExample, ICommandOptionArgs, ICommandOptionConfig, ICommandOptionType, ICommandParseResult, ICommandParsedArgs, ICommandParsedOpts, ICommandResolveResult, ICommandRouteResult, ICommandRunParams, ICommandShiftResult, ICommandToken, ICommandTokenType, ICommandTokenizeResult, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, ICompletionShellType, IHelpCommandLine, IHelpData, IHelpExampleLine, IHelpOptionLine, IInternalRouteResult, ISubcommandEntry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanghechen/commander",
3
- "version": "4.5.0",
3
+ "version": "4.5.1",
4
4
  "description": "A minimal, type-safe command-line interface builder with fluent API",
5
5
  "author": {
6
6
  "name": "guanghechen",