image-skill 0.1.7 → 0.1.8

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.
@@ -7,8 +7,13 @@ import { Readable } from "node:stream";
7
7
  import { pipeline } from "node:stream/promises";
8
8
  import os from "node:os";
9
9
 
10
- const VERSION = "0.1.7";
10
+ const VERSION = "0.1.8";
11
11
  const DEFAULT_API_BASE_URL = "https://api.image-skill.com";
12
+ const PROMPTLESS_EDIT_MODEL_IDS = new Set([
13
+ "fal.flux-dev-redux",
14
+ "fal.flux-krea-redux",
15
+ "fal.flux-schnell-redux",
16
+ ]);
12
17
  const DEFAULT_CONFIG_PATH = join(
13
18
  process.env.XDG_CONFIG_HOME ?? join(os.homedir(), ".config"),
14
19
  "image-skill",
@@ -499,7 +504,7 @@ async function credits(argv) {
499
504
  idempotency_key: idempotency.value,
500
505
  },
501
506
  });
502
- return result;
507
+ return withStripeCheckoutCopyFallback(result);
503
508
  }
504
509
  if (subcommand === "fake-purchase") {
505
510
  const args = parseArgs(rest);
@@ -546,13 +551,14 @@ async function credits(argv) {
546
551
  addQueryFlag(query, args, "payment-attempt-id", "payment_attempt_id");
547
552
  addQueryFlag(query, args, "checkout-session-id", "checkout_session_id");
548
553
  addQueryFlag(query, args, "receipt-id", "receipt_id");
549
- return apiRequest({
554
+ const result = await apiRequest({
550
555
  command: "image-skill credits status",
551
556
  method: "GET",
552
557
  apiBaseUrl: apiBase(args),
553
558
  path: `/v1/credit-purchases/status?${query.toString()}`,
554
559
  token: token.token,
555
560
  });
561
+ return withStripeCheckoutCopyFallback(result);
556
562
  }
557
563
  return invalid(
558
564
  "image-skill credits",
@@ -647,10 +653,28 @@ async function create(argv) {
647
653
  if (!token.ok) {
648
654
  return token.result;
649
655
  }
656
+ const referencePlan = parseReferencePlan(args, "image-skill create");
657
+ if (!referencePlan.ok) {
658
+ return referencePlan.result;
659
+ }
650
660
  const modelParameters = jsonObjectFlag(args, "model-parameters-json");
651
661
  if (!modelParameters.ok) {
652
662
  return modelParameters.result;
653
663
  }
664
+ const outputCount = positiveIntegerFlag(args, "output-count", {
665
+ command: "image-skill create",
666
+ });
667
+ if (!outputCount.ok) {
668
+ return outputCount.result;
669
+ }
670
+ const references = await resolveReferences(
671
+ referencePlan.referencePlans,
672
+ args,
673
+ token.token,
674
+ );
675
+ if (!references.ok) {
676
+ return references.result;
677
+ }
654
678
  return apiRequest({
655
679
  command: "image-skill create",
656
680
  method: "POST",
@@ -669,6 +693,12 @@ async function create(argv) {
669
693
  ? {}
670
694
  : { intent: flagString(args, "intent") }),
671
695
  aspect_ratio: flagString(args, "aspect-ratio") ?? "1:1",
696
+ ...(references.references.length === 0
697
+ ? {}
698
+ : { references: references.references }),
699
+ ...(outputCount.value === null
700
+ ? {}
701
+ : { output_count: outputCount.value }),
672
702
  ...(flagNumber(args, "max-estimated-usd-per-image") === null
673
703
  ? {}
674
704
  : {
@@ -713,13 +743,14 @@ async function upload(argv) {
713
743
  async function edit(argv) {
714
744
  const args = parseArgs(argv);
715
745
  const input = flagString(args, "input") ?? args.positionals[0];
746
+ const modelId = flagString(args, "model");
716
747
  if (input === undefined) {
717
748
  return invalid(
718
749
  "image-skill edit",
719
750
  "edit requires --input ASSET_ID_OR_PATH_OR_URL",
720
751
  );
721
752
  }
722
- const prompt = await promptValue(args);
753
+ const prompt = await editPromptValue(args, modelId);
723
754
  if (!prompt.ok) {
724
755
  return prompt.result;
725
756
  }
@@ -727,10 +758,28 @@ async function edit(argv) {
727
758
  if (!token.ok) {
728
759
  return token.result;
729
760
  }
761
+ const referencePlan = parseReferencePlan(args, "image-skill edit");
762
+ if (!referencePlan.ok) {
763
+ return referencePlan.result;
764
+ }
730
765
  const assetId = await resolveInputAssetId(input, args, token.token);
731
766
  if (!assetId.ok) {
732
767
  return assetId.result;
733
768
  }
769
+ const mask = flagString(args, "mask");
770
+ const maskAssetId =
771
+ mask === null ? null : await resolveInputAssetId(mask, args, token.token);
772
+ if (maskAssetId !== null && !maskAssetId.ok) {
773
+ return maskAssetId.result;
774
+ }
775
+ const references = await resolveReferences(
776
+ referencePlan.referencePlans,
777
+ args,
778
+ token.token,
779
+ );
780
+ if (!references.ok) {
781
+ return references.result;
782
+ }
734
783
  const modelParameters = jsonObjectFlag(args, "model-parameters-json");
735
784
  if (!modelParameters.ok) {
736
785
  return modelParameters.result;
@@ -743,13 +792,15 @@ async function edit(argv) {
743
792
  token: token.token,
744
793
  body: {
745
794
  input_asset_id: assetId.assetId,
746
- prompt: prompt.value,
795
+ ...(maskAssetId === null ? {} : { mask_asset_id: maskAssetId.assetId }),
796
+ ...(references.references.length === 0
797
+ ? {}
798
+ : { references: references.references }),
799
+ ...(prompt.value.length === 0 ? {} : { prompt: prompt.value }),
747
800
  ...(flagString(args, "provider") === null
748
801
  ? {}
749
802
  : { provider: flagString(args, "provider") }),
750
- ...(flagString(args, "model") === null
751
- ? {}
752
- : { model: flagString(args, "model") }),
803
+ ...(modelId === null ? {} : { model: modelId }),
753
804
  ...(flagString(args, "intent") === null
754
805
  ? {}
755
806
  : { intent: flagString(args, "intent") }),
@@ -1040,6 +1091,308 @@ async function resolveInputAssetId(input, args, token) {
1040
1091
  return { ok: true, assetId };
1041
1092
  }
1042
1093
 
1094
+ function parseReferencePlan(args, command) {
1095
+ for (const flag of [
1096
+ "element-frontal",
1097
+ "element-reference",
1098
+ "reference-image",
1099
+ ]) {
1100
+ if (
1101
+ args.flags.has(flag) &&
1102
+ args.flags.get(flag)?.some((value) => typeof value !== "string")
1103
+ ) {
1104
+ return {
1105
+ ok: false,
1106
+ result: invalid(command, `--${flag} requires an image`),
1107
+ };
1108
+ }
1109
+ }
1110
+ const referencePlans = [];
1111
+ for (const value of flagStrings(args, "element-frontal")) {
1112
+ const parsed = parseElementReferenceFlag(value, {
1113
+ flag: "--element-frontal",
1114
+ allowReferenceIndex: false,
1115
+ command,
1116
+ });
1117
+ if (!parsed.ok) {
1118
+ return parsed;
1119
+ }
1120
+ referencePlans.push({
1121
+ input: parsed.input,
1122
+ role: "element_frontal",
1123
+ index: parsed.index,
1124
+ referenceIndex: null,
1125
+ referenceTask: null,
1126
+ });
1127
+ }
1128
+ for (const value of flagStrings(args, "element-reference")) {
1129
+ const parsed = parseElementReferenceFlag(value, {
1130
+ flag: "--element-reference",
1131
+ allowReferenceIndex: true,
1132
+ command,
1133
+ });
1134
+ if (!parsed.ok) {
1135
+ return parsed;
1136
+ }
1137
+ referencePlans.push({
1138
+ input: parsed.input,
1139
+ role: "element_reference",
1140
+ index: parsed.index,
1141
+ referenceIndex: parsed.referenceIndex,
1142
+ referenceTask: null,
1143
+ });
1144
+ }
1145
+ for (const value of flagStrings(args, "reference-image")) {
1146
+ const parsed = parseReferenceImageFlag(value, {
1147
+ flag: "--reference-image",
1148
+ command,
1149
+ });
1150
+ if (!parsed.ok) {
1151
+ return parsed;
1152
+ }
1153
+ referencePlans.push({
1154
+ input: parsed.input,
1155
+ role: "reference_image",
1156
+ index: parsed.index,
1157
+ referenceIndex: null,
1158
+ referenceTask: parsed.referenceTask,
1159
+ });
1160
+ }
1161
+ const planValidation = validateElementReferencePlan(referencePlans, command);
1162
+ if (!planValidation.ok) {
1163
+ return planValidation;
1164
+ }
1165
+ return { ok: true, referencePlans };
1166
+ }
1167
+
1168
+ async function resolveReferences(referencePlans, args, token) {
1169
+ const references = [];
1170
+ for (const plan of referencePlans) {
1171
+ const assetId = await resolveInputAssetId(plan.input, args, token);
1172
+ if (!assetId.ok) {
1173
+ return assetId;
1174
+ }
1175
+ if (plan.role === "element_frontal") {
1176
+ references.push({
1177
+ asset_id: assetId.assetId,
1178
+ role: "element_frontal",
1179
+ index: plan.index,
1180
+ });
1181
+ continue;
1182
+ }
1183
+ if (plan.role === "reference_image") {
1184
+ references.push({
1185
+ asset_id: assetId.assetId,
1186
+ role: "reference_image",
1187
+ index: plan.index,
1188
+ ...(plan.referenceTask === null
1189
+ ? {}
1190
+ : { reference_task: plan.referenceTask }),
1191
+ });
1192
+ continue;
1193
+ }
1194
+ references.push({
1195
+ asset_id: assetId.assetId,
1196
+ role: "element_reference",
1197
+ index: plan.index,
1198
+ ...(plan.referenceIndex === null
1199
+ ? {}
1200
+ : { reference_index: plan.referenceIndex }),
1201
+ });
1202
+ }
1203
+ return { ok: true, references };
1204
+ }
1205
+
1206
+ function validateElementReferencePlan(referencePlans, command) {
1207
+ if (referencePlans.length === 0) {
1208
+ return { ok: true };
1209
+ }
1210
+ const frontals = new Set();
1211
+ const referencesByElement = new Map();
1212
+ const elementIndexes = new Set();
1213
+ for (const plan of referencePlans) {
1214
+ if (plan.role === "reference_image") {
1215
+ continue;
1216
+ }
1217
+ elementIndexes.add(plan.index);
1218
+ if (plan.role === "element_frontal") {
1219
+ if (frontals.has(plan.index)) {
1220
+ return {
1221
+ ok: false,
1222
+ result: invalid(
1223
+ command,
1224
+ `only one --element-frontal is allowed for element ${plan.index}`,
1225
+ ),
1226
+ };
1227
+ }
1228
+ frontals.add(plan.index);
1229
+ } else {
1230
+ const count = referencesByElement.get(plan.index) ?? 0;
1231
+ referencesByElement.set(plan.index, count + 1);
1232
+ }
1233
+ }
1234
+
1235
+ const sortedIndexes = [...elementIndexes].sort((left, right) => left - right);
1236
+ for (let expected = 0; expected < sortedIndexes.length; expected += 1) {
1237
+ if (sortedIndexes[expected] !== expected) {
1238
+ return {
1239
+ ok: false,
1240
+ result: invalid(
1241
+ command,
1242
+ "element indexes must be contiguous starting at 0",
1243
+ ),
1244
+ };
1245
+ }
1246
+ }
1247
+ for (const [index, count] of referencesByElement.entries()) {
1248
+ if (!frontals.has(index)) {
1249
+ return {
1250
+ ok: false,
1251
+ result: invalid(
1252
+ command,
1253
+ `--element-reference for element ${index} requires --element-frontal for the same element`,
1254
+ ),
1255
+ };
1256
+ }
1257
+ if (count > 3) {
1258
+ return {
1259
+ ok: false,
1260
+ result: invalid(
1261
+ command,
1262
+ `element ${index} accepts at most 3 --element-reference images`,
1263
+ ),
1264
+ };
1265
+ }
1266
+ }
1267
+ const referenceImageIndexes = new Set();
1268
+ for (const plan of referencePlans) {
1269
+ if (plan.role !== "reference_image") {
1270
+ continue;
1271
+ }
1272
+ if (referenceImageIndexes.has(plan.index)) {
1273
+ return {
1274
+ ok: false,
1275
+ result: invalid(
1276
+ command,
1277
+ `only one --reference-image is allowed for index ${plan.index}`,
1278
+ ),
1279
+ };
1280
+ }
1281
+ referenceImageIndexes.add(plan.index);
1282
+ }
1283
+ const sortedReferenceImageIndexes = [...referenceImageIndexes].sort(
1284
+ (left, right) => left - right,
1285
+ );
1286
+ for (
1287
+ let expected = 0;
1288
+ expected < sortedReferenceImageIndexes.length;
1289
+ expected += 1
1290
+ ) {
1291
+ if (sortedReferenceImageIndexes[expected] !== expected) {
1292
+ return {
1293
+ ok: false,
1294
+ result: invalid(
1295
+ command,
1296
+ "reference image indexes must be contiguous starting at 0",
1297
+ ),
1298
+ };
1299
+ }
1300
+ }
1301
+ return { ok: true };
1302
+ }
1303
+
1304
+ function parseReferenceImageFlag(value, options) {
1305
+ const parsed = parseReferenceImageSuffix(value);
1306
+ if (parsed.input.length === 0) {
1307
+ return {
1308
+ ok: false,
1309
+ result: invalid(options.command, `${options.flag} requires an image`),
1310
+ };
1311
+ }
1312
+ if (parsed.index > 9) {
1313
+ return {
1314
+ ok: false,
1315
+ result: invalid(
1316
+ options.command,
1317
+ `${options.flag} index must be between 0 and 9`,
1318
+ ),
1319
+ };
1320
+ }
1321
+ return { ok: true, ...parsed };
1322
+ }
1323
+
1324
+ function parseReferenceImageSuffix(value) {
1325
+ const atIndex = value.lastIndexOf("@");
1326
+ if (atIndex === -1) {
1327
+ return { input: value, index: 0, referenceTask: null };
1328
+ }
1329
+ const suffix = value.slice(atIndex + 1);
1330
+ if (!/^\d+(?::(?:ip|id|style))?$/.test(suffix)) {
1331
+ return { input: value, index: 0, referenceTask: null };
1332
+ }
1333
+ const [index, referenceTask] = suffix.split(":");
1334
+ return {
1335
+ input: value.slice(0, atIndex),
1336
+ index: Number(index),
1337
+ referenceTask: referenceTask ?? null,
1338
+ };
1339
+ }
1340
+
1341
+ function parseElementReferenceFlag(value, options) {
1342
+ const parsed = parseElementReferenceSuffix(value);
1343
+ if (parsed.input.length === 0) {
1344
+ return {
1345
+ ok: false,
1346
+ result: invalid(options.command, `${options.flag} requires an image`),
1347
+ };
1348
+ }
1349
+ if (!options.allowReferenceIndex && parsed.referenceIndex !== null) {
1350
+ return {
1351
+ ok: false,
1352
+ result: invalid(
1353
+ options.command,
1354
+ `${options.flag} accepts IMAGE[@ELEMENT_INDEX], not a reference index`,
1355
+ ),
1356
+ };
1357
+ }
1358
+ if (parsed.index > 9) {
1359
+ return {
1360
+ ok: false,
1361
+ result: invalid(
1362
+ options.command,
1363
+ `${options.flag} element index must be between 0 and 9`,
1364
+ ),
1365
+ };
1366
+ }
1367
+ if (parsed.referenceIndex !== null && parsed.referenceIndex > 2) {
1368
+ return {
1369
+ ok: false,
1370
+ result: invalid(
1371
+ options.command,
1372
+ `${options.flag} reference index must be between 0 and 2`,
1373
+ ),
1374
+ };
1375
+ }
1376
+ return { ok: true, ...parsed };
1377
+ }
1378
+
1379
+ function parseElementReferenceSuffix(value) {
1380
+ const atIndex = value.lastIndexOf("@");
1381
+ if (atIndex === -1) {
1382
+ return { input: value, index: 0, referenceIndex: null };
1383
+ }
1384
+ const suffix = value.slice(atIndex + 1);
1385
+ if (!/^\d+(?::\d+)?$/.test(suffix)) {
1386
+ return { input: value, index: 0, referenceIndex: null };
1387
+ }
1388
+ const [index, referenceIndex] = suffix.split(":").map((part) => Number(part));
1389
+ return {
1390
+ input: value.slice(0, atIndex),
1391
+ index,
1392
+ referenceIndex: referenceIndex ?? null,
1393
+ };
1394
+ }
1395
+
1043
1396
  async function uploadPayload(input) {
1044
1397
  const isRemote = /^https?:\/\//i.test(input);
1045
1398
  let bytes;
@@ -1168,6 +1521,125 @@ function parseEnvelope(text, command, statusCode) {
1168
1521
  };
1169
1522
  }
1170
1523
 
1524
+ function withStripeCheckoutCopyFallback(result) {
1525
+ const data = result.envelope.data;
1526
+ if (!isRecord(data)) {
1527
+ return result;
1528
+ }
1529
+
1530
+ const updated = stripeCheckoutCopyFallbackData(data);
1531
+ if (updated === data) {
1532
+ return result;
1533
+ }
1534
+
1535
+ return {
1536
+ ...result,
1537
+ envelope: {
1538
+ ...result.envelope,
1539
+ data: updated,
1540
+ },
1541
+ };
1542
+ }
1543
+
1544
+ function stripeCheckoutCopyFallbackData(data) {
1545
+ let changed = false;
1546
+ const updated = { ...data };
1547
+
1548
+ if (addCheckoutCompactUrl(updated)) {
1549
+ changed = true;
1550
+ }
1551
+
1552
+ if (isRecord(updated.next)) {
1553
+ const next = { ...updated.next };
1554
+ let nextChanged = addCheckoutCompactUrl(next);
1555
+ if (
1556
+ typeof updated.checkout_compact_url === "string" &&
1557
+ typeof next.checkout_compact_url !== "string"
1558
+ ) {
1559
+ next.checkout_compact_url = updated.checkout_compact_url;
1560
+ nextChanged = true;
1561
+ }
1562
+ if (nextChanged) {
1563
+ updated.next = next;
1564
+ changed = true;
1565
+ }
1566
+ }
1567
+
1568
+ if (isRecord(updated.payment_attempt)) {
1569
+ const paymentAttempt = { ...updated.payment_attempt };
1570
+ if (addCheckoutCompactUrl(paymentAttempt)) {
1571
+ updated.payment_attempt = paymentAttempt;
1572
+ changed = true;
1573
+ }
1574
+ if (isRecord(updated.next)) {
1575
+ const next = { ...updated.next };
1576
+ if (
1577
+ next.human_action === "open_checkout_url" &&
1578
+ typeof paymentAttempt.checkout_compact_url === "string" &&
1579
+ typeof next.checkout_compact_url !== "string"
1580
+ ) {
1581
+ next.checkout_compact_url = paymentAttempt.checkout_compact_url;
1582
+ updated.next = next;
1583
+ changed = true;
1584
+ }
1585
+ }
1586
+ }
1587
+
1588
+ return changed ? updated : data;
1589
+ }
1590
+
1591
+ function addCheckoutCompactUrl(record) {
1592
+ const raw =
1593
+ typeof record.checkout_url === "string"
1594
+ ? record.checkout_url
1595
+ : typeof record.fallback_checkout_url === "string"
1596
+ ? record.fallback_checkout_url
1597
+ : null;
1598
+ if (raw === null || raw.length === 0) {
1599
+ return false;
1600
+ }
1601
+ const compact = stripeCheckoutCompactUrl(raw);
1602
+ let changed = false;
1603
+ if (record.checkout_compact_url !== compact) {
1604
+ record.checkout_compact_url = compact;
1605
+ changed = true;
1606
+ }
1607
+ if (
1608
+ typeof record.checkout_url === "string" &&
1609
+ record.checkout_url !== compact
1610
+ ) {
1611
+ record.checkout_url = compact;
1612
+ changed = true;
1613
+ }
1614
+ if (
1615
+ typeof record.fallback_checkout_url === "string" &&
1616
+ record.fallback_checkout_url !== compact
1617
+ ) {
1618
+ record.fallback_checkout_url = compact;
1619
+ changed = true;
1620
+ }
1621
+ return changed;
1622
+ }
1623
+
1624
+ function stripeCheckoutCompactUrl(checkoutUrl) {
1625
+ const trimmed = checkoutUrl.trim();
1626
+ if (trimmed.length === 0) {
1627
+ return checkoutUrl;
1628
+ }
1629
+ try {
1630
+ const parsed = new URL(trimmed);
1631
+ parsed.hash = "";
1632
+ return parsed.toString();
1633
+ } catch {
1634
+ const hashIndex = trimmed.indexOf("#");
1635
+ return hashIndex === -1 ? trimmed : trimmed.slice(0, hashIndex);
1636
+ }
1637
+ }
1638
+
1639
+ function isRecord(value) {
1640
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1641
+ }
1642
+
1171
1643
  async function promptValue(args) {
1172
1644
  const prompt = flagString(args, "prompt");
1173
1645
  const promptFile = flagString(args, "prompt-file");
@@ -1192,6 +1664,69 @@ async function promptValue(args) {
1192
1664
  };
1193
1665
  }
1194
1666
 
1667
+ async function editPromptValue(args, modelId) {
1668
+ if (args.flags.has("prompt") && flagString(args, "prompt") === null) {
1669
+ return {
1670
+ ok: false,
1671
+ result: invalid("image-skill edit", "--prompt requires a value"),
1672
+ };
1673
+ }
1674
+ if (
1675
+ args.flags.has("prompt-file") &&
1676
+ flagString(args, "prompt-file") === null
1677
+ ) {
1678
+ return {
1679
+ ok: false,
1680
+ result: invalid("image-skill edit", "--prompt-file requires a value"),
1681
+ };
1682
+ }
1683
+ const prompt = flagString(args, "prompt");
1684
+ const promptFile = flagString(args, "prompt-file");
1685
+ if (prompt !== null && promptFile !== null) {
1686
+ return {
1687
+ ok: false,
1688
+ result: invalid(
1689
+ "image-skill edit",
1690
+ "provide either --prompt or --prompt-file, not both",
1691
+ ),
1692
+ };
1693
+ }
1694
+ const isPromptlessModel =
1695
+ modelId !== null && PROMPTLESS_EDIT_MODEL_IDS.has(modelId);
1696
+ let value = null;
1697
+ if (prompt !== null) {
1698
+ value = prompt;
1699
+ } else if (promptFile !== null) {
1700
+ value = await readFile(promptFile, "utf8");
1701
+ }
1702
+ const trimmed = value?.trim() ?? "";
1703
+ if (isPromptlessModel) {
1704
+ if (trimmed.length > 0) {
1705
+ return {
1706
+ ok: false,
1707
+ result: invalid(
1708
+ "image-skill edit",
1709
+ `model ${modelId} does not accept --prompt`,
1710
+ ),
1711
+ };
1712
+ }
1713
+ return { ok: true, value: "" };
1714
+ }
1715
+ if (value === null) {
1716
+ return {
1717
+ ok: false,
1718
+ result: invalid("image-skill edit", "edit requires --prompt"),
1719
+ };
1720
+ }
1721
+ if (trimmed.length === 0) {
1722
+ return {
1723
+ ok: false,
1724
+ result: invalid("image-skill edit", "edit prompt cannot be empty"),
1725
+ };
1726
+ }
1727
+ return { ok: true, value: trimmed };
1728
+ }
1729
+
1195
1730
  async function resolveToken(args, options = {}) {
1196
1731
  if (flagBool(args, "token-stdin")) {
1197
1732
  if (process.stdin.isTTY) {
@@ -1348,6 +1883,12 @@ function flagString(args, name) {
1348
1883
  return typeof value === "string" ? value : null;
1349
1884
  }
1350
1885
 
1886
+ function flagStrings(args, name) {
1887
+ return (args.flags.get(name) ?? []).filter(
1888
+ (value) => typeof value === "string",
1889
+ );
1890
+ }
1891
+
1351
1892
  function flagBool(args, name) {
1352
1893
  return args.flags.has(name) && args.flags.get(name)?.at(-1) !== "false";
1353
1894
  }
@@ -1404,6 +1945,21 @@ function flagNumber(args, name) {
1404
1945
  return Number.isFinite(number) ? number : null;
1405
1946
  }
1406
1947
 
1948
+ function positiveIntegerFlag(args, name, input) {
1949
+ const value = flagString(args, name);
1950
+ if (value === null) {
1951
+ return { ok: true, value: null };
1952
+ }
1953
+ const number = Number(value);
1954
+ if (!Number.isInteger(number) || number <= 0) {
1955
+ return {
1956
+ ok: false,
1957
+ result: invalid(input.command, `${name} must be a positive integer`),
1958
+ };
1959
+ }
1960
+ return { ok: true, value: number };
1961
+ }
1962
+
1407
1963
  function jsonObjectFlag(args, name) {
1408
1964
  const raw = flagString(args, name);
1409
1965
  if (raw === null) {