@goodandready/dsh-kanban 0.2.21 → 0.2.22

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/lib/board-tool.js CHANGED
@@ -59,7 +59,7 @@ function nameOf(task) {
59
59
  }
60
60
 
61
61
  /** board_move tool definition for `defineTool`. */
62
- export function boardMoveDefinition({ store, config }) {
62
+ export function boardMoveDefinition({ store, config, eventHub }) {
63
63
  return {
64
64
  name: 'board_move',
65
65
  description: 'Move the kanban card of the current task to another column. '
@@ -127,7 +127,7 @@ export function boardMoveDefinition({ store, config }) {
127
127
  /**
128
128
  * board_plan tool definition for `defineTool`.
129
129
  */
130
- export function boardPlanDefinition({ store }) {
130
+ export function boardPlanDefinition({ store, eventHub }) {
131
131
  return {
132
132
  name: 'board_plan',
133
133
  description: 'Publish or update the plan shown on the kanban card of the current task. '
@@ -157,7 +157,10 @@ export function boardPlanDefinition({ store }) {
157
157
  const task = tasks[0]
158
158
  const plan = applyPlan(parsePlan(task.plan), { items, current })
159
159
  if (plan.items.length === 0) return 'A plan needs at least one step.'
160
- for (const one of tasks) store.updateTask(one.id, { plan: serializePlan(plan) })
160
+ for (const one of tasks) {
161
+ store.updateTask(one.id, { plan: serializePlan(plan) })
162
+ eventHub?.broadcast?.('task:change', { taskId: one.id, action: 'updated' })
163
+ }
161
164
  const progress = planProgress(plan)
162
165
  return progress.text === ''
163
166
  ? `Plan of ${progress.total} steps recorded; no step is marked as current.`
@@ -168,7 +171,7 @@ export function boardPlanDefinition({ store }) {
168
171
 
169
172
 
170
173
  /** board_checklist tool definition (#205) */
171
- export function boardChecklistDefinition({ store }) {
174
+ export function boardChecklistDefinition({ store, eventHub }) {
172
175
  return {
173
176
  name: 'board_checklist',
174
177
  description: 'Mark a Definition of Done (DoD) checklist item as completed with verification evidence.',
@@ -429,7 +432,7 @@ export function boardDecomposeDefinition({ store, gitea }) {
429
432
  }
430
433
 
431
434
  /** board_checklist_item tool definition (#248) */
432
- export function boardChecklistItemDefinition({ store }) {
435
+ export function boardChecklistItemDefinition({ store, eventHub }) {
433
436
  return {
434
437
  name: 'board_checklist_item',
435
438
  description: 'Toggle or check a specific DoD checklist item by text or index without replacing the entire list.',
@@ -0,0 +1,582 @@
1
+ // License: CC0 (no rights reserved).
2
+
3
+ // This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
4
+
5
+ export const BASE_62_DIGITS =
6
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // 0-9 + A-Z + a-z
7
+
8
+ export const BASE_52_DIGITS = BASE_62_DIGITS.slice(10); // A-Z + a-z
9
+
10
+ // `intDigits` (the integer-part head alphabet) defaults to `digits`. When
11
+ // `digits` is also omitted it falls back to BASE_52_DIGITS -- A-Z (negative
12
+ // lengths) followed by a-z (positive lengths) -- so the default keys keep the
13
+ // classic "a0", "Zz", ... form.
14
+
15
+ /**
16
+ * Per-alphabet cache mapping each digit's char code to its index, so digit->value
17
+ * lookups are a single typed-array load instead of O(alphabet) String.indexOf.
18
+ * Keys (and therefore alphabets) are required to be single-byte (char code
19
+ * 0-255), so a fixed 256-entry table covers every possible char code.
20
+ * @type {Map<string, Uint8Array>}
21
+ */
22
+ const digitIndexCache = new Map();
23
+
24
+ /**
25
+ * @param {string} digits
26
+ * @return {Uint8Array}
27
+ */
28
+ function getDigitIndex(digits) {
29
+ let m = digitIndexCache.get(digits);
30
+ if (m === undefined) {
31
+ m = new Uint8Array(256);
32
+ for (let i = 0; i < digits.length; i++) {
33
+ m[digits.charCodeAt(i)] = i;
34
+ }
35
+ digitIndexCache.set(digits, m);
36
+ }
37
+ return m;
38
+ }
39
+
40
+ // `a` may be empty string, `b` is null or non-empty string.
41
+ // `a < b` lexicographically if `b` is non-null.
42
+ // no trailing zeros allowed.
43
+ // digits is a string such as '0123456789' for base 10. Digits must be in
44
+ // ascending character code order!
45
+ /**
46
+ * @param {string} a
47
+ * @param {string | null | undefined} b
48
+ * @param {string} digits
49
+ * @param {Uint8Array} lookup
50
+ * @returns {string}
51
+ */
52
+ function midpoint(a, b, digits, lookup) {
53
+ const zero = digits[0];
54
+ if (b != null && a >= b) {
55
+ throw new Error(a + " >= " + b);
56
+ }
57
+ if (a.slice(-1) === zero || (b && b.slice(-1) === zero)) {
58
+ throw new Error("trailing zero");
59
+ }
60
+ if (b) {
61
+ // remove longest common prefix. pad `a` with 0s as we
62
+ // go. note that we don't need to pad `b`, because it can't
63
+ // end before `a` while traversing the common prefix.
64
+ let n = 0;
65
+ while ((a[n] || zero) === b[n]) {
66
+ n++;
67
+ }
68
+ if (n > 0) {
69
+ return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits, lookup);
70
+ }
71
+ }
72
+ // first digits (or lack of digit) are different
73
+ const digitA = a ? /** @type {number} */ (lookup[a.charCodeAt(0)]) : 0;
74
+ const digitB =
75
+ b != null ? /** @type {number} */ (lookup[b.charCodeAt(0)]) : digits.length;
76
+ if (digitB - digitA > 1) {
77
+ const midDigit = Math.round(0.5 * (digitA + digitB));
78
+ return digits[midDigit];
79
+ } else {
80
+ // first digits are consecutive
81
+ if (b && b.length > 1) {
82
+ return b.slice(0, 1);
83
+ } else {
84
+ // `b` is null or has length 1 (a single digit).
85
+ // the first digit of `a` is the previous digit to `b`,
86
+ // or 9 if `b` is null.
87
+ // given, for example, midpoint('49', '5'), return
88
+ // '4' + midpoint('9', null), which will become
89
+ // '4' + '9' + midpoint('', null), which is '495'
90
+ return digits[digitA] + midpoint(a.slice(1), null, digits, lookup);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * @param {string} int
97
+ * @param {string} intDigits
98
+ * @param {Uint8Array} intLookup
99
+ * @return {void}
100
+ */
101
+
102
+ function validateInteger(int, intDigits, intLookup) {
103
+ if (int.length !== getIntegerLength(int[0], intDigits, intLookup)) {
104
+ throw new Error("invalid integer part of order key: " + int);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @param {string} head
110
+ * @param {string} intDigits
111
+ * @param {Uint8Array} intLookup
112
+ * @return {number}
113
+ */
114
+ function getIntegerLength(head, intDigits, intLookup) {
115
+ // intDigits is a single lexicographically ordered (ascending) alphabet: the
116
+ // first half are the negative-length heads and the second half the
117
+ // positive-length heads (the default A-Z/a-z markers are just one such
118
+ // alphabet). The outermost characters mark the longest integer parts, and the
119
+ // two heads straddling the midpoint mark the shortest (length 2).
120
+ const i = intLookup[head.charCodeAt(0)];
121
+ // `intLookup` returns 0 for any char code not in the alphabet, so confirm the
122
+ // char really is at index `i` before trusting it as a head.
123
+ if (intDigits[i] === head) {
124
+ const half = intDigits.length / 2;
125
+ return i < half ? half - i + 1 : i - half + 2;
126
+ }
127
+ throw new Error("invalid order key head: " + head);
128
+ }
129
+
130
+ /**
131
+ * @param {string} key
132
+ * @param {string} intDigits
133
+ * @param {Uint8Array} intLookup
134
+ * @return {string}
135
+ */
136
+
137
+ function getIntegerPart(key, intDigits, intLookup) {
138
+ const integerPartLength = getIntegerLength(key[0], intDigits, intLookup);
139
+ if (integerPartLength > key.length) {
140
+ throw new Error("invalid order key: " + key);
141
+ }
142
+ return key.slice(0, integerPartLength);
143
+ }
144
+
145
+ /**
146
+ * @param {string} key
147
+ * @param {string} digits
148
+ * @param {string} intDigits
149
+ * @param {Uint8Array} intLookup
150
+ * @return {void}
151
+ */
152
+ function validateOrderKey(key, digits, intDigits, intLookup) {
153
+ if (isSmallestInteger(key, digits, intDigits)) {
154
+ throw new Error("invalid order key: " + key);
155
+ }
156
+ // getIntegerPart will throw if the first character is bad,
157
+ // or the key is too short. we'd call it to check these things
158
+ // even if we didn't need the result
159
+ const i = getIntegerPart(key, intDigits, intLookup);
160
+ const f = key.slice(i.length);
161
+ if (f.slice(-1) === digits[0]) {
162
+ throw new Error("invalid order key: " + key);
163
+ }
164
+ }
165
+
166
+ // note that this may return null, as there is a largest integer
167
+ /**
168
+ * @param {string} x
169
+ * @param {string} digits
170
+ * @param {Uint8Array} lookup
171
+ * @param {string} intDigits
172
+ * @param {Uint8Array} intLookup
173
+ * @return {string | null}
174
+ */
175
+ function incrementInteger(x, digits, lookup, intDigits, intLookup) {
176
+ validateInteger(x, intDigits, intLookup);
177
+ const head = x[0];
178
+ const zero = digits[0];
179
+ // Walk the digit run right-to-left, turning maxed-out digits into zeros
180
+ // (`trailing`) until we find one we can bump.
181
+ let trailing = "";
182
+ for (let i = x.length - 1; i >= 1; i--) {
183
+ const d = /** @type {number} */ (lookup[x.charCodeAt(i)]) + 1;
184
+ if (d === digits.length) {
185
+ trailing = zero + trailing;
186
+ } else {
187
+ return head + x.slice(1, i) + digits[d] + trailing;
188
+ }
189
+ }
190
+ // carry out of the whole digit run; `trailing` is now all zeros.
191
+ const headIndex = intLookup[head.charCodeAt(0)];
192
+ if (headIndex === intDigits.length - 1) {
193
+ // already the largest integer
194
+ return null;
195
+ }
196
+ const h = intDigits[headIndex + 1];
197
+ // the head moves one step toward the largest digit; grow or shrink the digit
198
+ // run to match the new head's integer length.
199
+ const lengthDelta =
200
+ getIntegerLength(h, intDigits, intLookup) -
201
+ getIntegerLength(head, intDigits, intLookup);
202
+ return (
203
+ h +
204
+ (lengthDelta > 0
205
+ ? trailing + zero
206
+ : lengthDelta < 0
207
+ ? trailing.slice(1)
208
+ : trailing)
209
+ );
210
+ }
211
+
212
+ // note that this may return null, as there is a smallest integer
213
+ /**
214
+ * @param {string} x
215
+ * @param {string} digits
216
+ * @param {Uint8Array} lookup
217
+ * @param {string} intDigits
218
+ * @param {Uint8Array} intLookup
219
+ * @return {string | null}
220
+ */
221
+
222
+ function decrementInteger(x, digits, lookup, intDigits, intLookup) {
223
+ validateInteger(x, intDigits, intLookup);
224
+ const head = x[0];
225
+ const last = digits[digits.length - 1];
226
+ // Walk the digit run right-to-left, turning underflowed digits into the
227
+ // largest digit (`trailing`) until we find one we can drop.
228
+ let trailing = "";
229
+ for (let i = x.length - 1; i >= 1; i--) {
230
+ const d = /** @type {number} */ (lookup[x.charCodeAt(i)]) - 1;
231
+ if (d === -1) {
232
+ trailing = last + trailing;
233
+ } else {
234
+ return head + x.slice(1, i) + digits[d] + trailing;
235
+ }
236
+ }
237
+ // borrow out of the whole digit run; `trailing` is now all max digits.
238
+ const headIndex = intLookup[head.charCodeAt(0)];
239
+ if (headIndex === 0) {
240
+ // already the smallest integer
241
+ return null;
242
+ }
243
+ const h = intDigits[headIndex - 1];
244
+ // the head moves one step toward the smallest digit; grow or shrink the
245
+ // digit run to match the new head's integer length.
246
+ const lengthDelta =
247
+ getIntegerLength(h, intDigits, intLookup) -
248
+ getIntegerLength(head, intDigits, intLookup);
249
+ return (
250
+ h +
251
+ (lengthDelta > 0
252
+ ? trailing + last
253
+ : lengthDelta < 0
254
+ ? trailing.slice(1)
255
+ : trailing)
256
+ );
257
+ }
258
+
259
+ /**
260
+ * Two-level cache keyed by (intDigits, first digit char code). Nesting the maps
261
+ * lets us look up with the raw `intDigits` string and a primitive char code,
262
+ * avoiding the per-call allocation of a combined string key.
263
+ * @type {Map<string, Map<number, string>>}
264
+ */
265
+ const repeatedKeysCache = new Map();
266
+
267
+ /**
268
+ * @param {string} key
269
+ * @param {string} digits
270
+ * @param {string} intDigits
271
+ */
272
+ function isSmallestInteger(key, digits, intDigits) {
273
+ // The smallest integer is the most-negative head (the first character of
274
+ // intDigits, marking the longest integer part) followed by all-zero digits.
275
+ // Use a cache to avoid constructing the same long string over and over which
276
+ // causes unnecessary GC pressure.
277
+ let byDigit = repeatedKeysCache.get(intDigits);
278
+ if (byDigit === undefined) {
279
+ byDigit = new Map();
280
+ repeatedKeysCache.set(intDigits, byDigit);
281
+ }
282
+ const zeroCode = digits.charCodeAt(0);
283
+ let cached = byDigit.get(zeroCode);
284
+ if (cached === undefined) {
285
+ cached = intDigits[0] + digits[0].repeat(intDigits.length / 2);
286
+ byDigit.set(zeroCode, cached);
287
+ }
288
+ return key === cached;
289
+ }
290
+
291
+ /**
292
+ * Returns true if every character of `s` has a strictly greater character code
293
+ * than the one before it (ascending order, which also rules out duplicates).
294
+ * @param {string} s
295
+ * @return {boolean}
296
+ */
297
+ function isStrictlyAscending(s) {
298
+ for (let i = 1; i < s.length; i++) {
299
+ if (s.charCodeAt(i - 1) >= s.charCodeAt(i)) {
300
+ return false;
301
+ }
302
+ }
303
+ return true;
304
+ }
305
+
306
+ /**
307
+ * Returns true if every character of `s` is single-byte (char code < 256). Keys
308
+ * are required to be single-byte so digit lookups can use a fixed 256-entry
309
+ * table.
310
+ * @param {string} s
311
+ * @return {boolean}
312
+ */
313
+ function isSingleByte(s) {
314
+ for (let i = 0; i < s.length; i++) {
315
+ if (s.charCodeAt(i) > 255) {
316
+ return false;
317
+ }
318
+ }
319
+ return true;
320
+ }
321
+
322
+ /**
323
+ * Alphabets that have already passed `validateDigits`. Validation is pure and
324
+ * its result never changes, so we cache the alphabet to skip re-scanning it on
325
+ * every call. Kept separate from `validatedIntDigits` because the two have
326
+ * different rules (an odd-length string is a valid `digits` but not `intDigits`).
327
+ * @type {Set<string>}
328
+ */
329
+ const validatedDigits = new Set();
330
+
331
+ /**
332
+ * Validates a fractional-digit alphabet: at least two characters, in strictly
333
+ * ascending character-code order.
334
+ * @param {string} digits
335
+ * @return {void}
336
+ */
337
+ function validateDigits(digits) {
338
+ if (validatedDigits.has(digits)) {
339
+ return;
340
+ }
341
+ if (digits.length < 2 || !isStrictlyAscending(digits)) {
342
+ throw new Error(
343
+ "digits must be at least 2 characters in strictly ascending character " +
344
+ "code order: " +
345
+ digits
346
+ );
347
+ }
348
+ if (!isSingleByte(digits)) {
349
+ throw new Error("digits must be single-byte (char code 0-255): " + digits);
350
+ }
351
+ validatedDigits.add(digits);
352
+ }
353
+
354
+ /**
355
+ * Alphabets that have already passed `validateIntDigits`.
356
+ * @type {Set<string>}
357
+ */
358
+ const validatedIntDigits = new Set();
359
+
360
+ /**
361
+ * Validates a head-marker alphabet: an even number of at least two characters
362
+ * (its two halves are the negative- and positive-length heads), in strictly
363
+ * ascending character-code order.
364
+ * @param {string} intDigits
365
+ * @return {void}
366
+ */
367
+ function validateIntDigits(intDigits) {
368
+ if (validatedIntDigits.has(intDigits)) {
369
+ return;
370
+ }
371
+ if (
372
+ intDigits.length < 2 ||
373
+ intDigits.length % 2 !== 0 ||
374
+ !isStrictlyAscending(intDigits)
375
+ ) {
376
+ throw new Error(
377
+ "intDigits must be an even number of at least 2 characters in strictly " +
378
+ "ascending character code order: " +
379
+ intDigits
380
+ );
381
+ }
382
+ if (!isSingleByte(intDigits)) {
383
+ throw new Error(
384
+ "intDigits must be single-byte (char code 0-255): " + intDigits
385
+ );
386
+ }
387
+ validatedIntDigits.add(intDigits);
388
+ }
389
+
390
+ /**
391
+ * Generates an order key that sorts between `a` and `b`.
392
+ *
393
+ * `a` is the lower bound: an order key, or null for the start.
394
+ * `b` is the upper bound: an order key, or null for the end.
395
+ * When both are non-null, they may be passed in either order.
396
+ *
397
+ * `digits` is the alphabet, e.g. '0123456789' for base 10. Its characters
398
+ * must be single-byte (char code 0-255) and in ascending character code order;
399
+ * both are validated. It may otherwise be any alphabet (it does not need to
400
+ * contain 0-9, A-Z or a-z). Because `intDigits` defaults to `digits`, an
401
+ * odd-length `digits` must be paired with an explicit even-length `intDigits`.
402
+ *
403
+ * Note that `digits` only defines the *digit values* of a key. The integer
404
+ * part of every key also begins with a length/magnitude marker (a "head") drawn
405
+ * from the `intDigits` alphabet. The head only ever occupies the first position
406
+ * and is only compared against other heads, which is why `digits` and
407
+ * `intDigits` may overlap (or be identical) and keys still sort correctly.
408
+ *
409
+ * `intDigits` is the head alphabet: a single alphabet in ascending
410
+ * (lexicographical) character order, with even length. Its first half are the
411
+ * negative-length heads and its second half the positive-length heads. The
412
+ * outermost characters mark the longest integer parts and the two characters
413
+ * straddling the midpoint mark the shortest (length 2). The integer part may
414
+ * grow until it reaches the outermost heads, so a shorter alphabet limits how
415
+ * large/small a key's integer part can become.
416
+ *
417
+ * `intDigits` defaults to `digits`, so a base-10 alphabet produces self-headed
418
+ * keys like "50", "600" or "49". When `digits` is also omitted it falls back to
419
+ * BASE_52_DIGITS (A-Z/a-z), giving the classic "a0", "b00", "Z9" form. Note that
420
+ * passing `digits` explicitly (even BASE_62_DIGITS) makes the keys self-headed;
421
+ * only omitting `digits` entirely yields the A-Z/a-z heads.
422
+ *
423
+ * @example
424
+ * // base 10: heads come from `digits` itself. The first half (0-4) are negative
425
+ * // heads, the second half (5-9) positive heads, and 4/5 mark the shortest
426
+ * // (length-2) integer parts.
427
+ * generateKeyBetween(null, null, "0123456789"); // => "50"
428
+ *
429
+ * @param {string | null | undefined} a
430
+ * @param {string | null | undefined} b
431
+ * @param {string=} digits
432
+ * @param {string=} intDigits
433
+ * @return {string}
434
+ */
435
+ export function generateKeyBetween(
436
+ a,
437
+ b,
438
+ digits = undefined,
439
+ intDigits = undefined
440
+ ) {
441
+ if (intDigits !== undefined) {
442
+ validateIntDigits(intDigits);
443
+ } else {
444
+ intDigits = digits ?? BASE_52_DIGITS;
445
+ }
446
+ if (digits !== undefined) {
447
+ validateDigits(digits);
448
+ } else {
449
+ digits = BASE_62_DIGITS;
450
+ }
451
+
452
+ const lookup = getDigitIndex(digits);
453
+ const intLookup = getDigitIndex(intDigits);
454
+ if (a != null) {
455
+ validateOrderKey(a, digits, intDigits, intLookup);
456
+ }
457
+ if (b != null) {
458
+ validateOrderKey(b, digits, intDigits, intLookup);
459
+ }
460
+ if (a != null && b != null) {
461
+ // swap if out of order, so that a < b. this is just a convenience for
462
+ // callers, and doesn't affect the properties of the generated key.
463
+ if (a > b) {
464
+ const temp = a;
465
+ a = b;
466
+ b = temp;
467
+ }
468
+ }
469
+
470
+ if (a == null) {
471
+ if (b == null) {
472
+ // the shortest positive head: the first character of the second half of
473
+ // intDigits ("a" for the default A-Z/a-z markers).
474
+ const head = intDigits[intDigits.length / 2];
475
+ return head + digits[0];
476
+ }
477
+
478
+ const ib = getIntegerPart(b, intDigits, intLookup);
479
+ const fb = b.slice(ib.length);
480
+ if (isSmallestInteger(ib, digits, intDigits)) {
481
+ return ib + midpoint("", fb, digits, lookup);
482
+ }
483
+ if (ib < b) {
484
+ return ib;
485
+ }
486
+ const res = decrementInteger(ib, digits, lookup, intDigits, intLookup);
487
+ if (res == null) {
488
+ throw new Error("cannot decrement any more");
489
+ }
490
+ return res;
491
+ }
492
+
493
+ if (b == null) {
494
+ const ia = getIntegerPart(a, intDigits, intLookup);
495
+ const fa = a.slice(ia.length);
496
+ const i = incrementInteger(ia, digits, lookup, intDigits, intLookup);
497
+ return i == null ? ia + midpoint(fa, null, digits, lookup) : i;
498
+ }
499
+
500
+ const ia = getIntegerPart(a, intDigits, intLookup);
501
+ const fa = a.slice(ia.length);
502
+ const ib = getIntegerPart(b, intDigits, intLookup);
503
+ const fb = b.slice(ib.length);
504
+ if (ia === ib) {
505
+ return ia + midpoint(fa, fb, digits, lookup);
506
+ }
507
+ const i = incrementInteger(ia, digits, lookup, intDigits, intLookup);
508
+ if (i == null) {
509
+ throw new Error("cannot increment any more");
510
+ }
511
+ if (i < b) {
512
+ return i;
513
+ }
514
+ return ia + midpoint(fa, null, digits, lookup);
515
+ }
516
+
517
+ /**
518
+ * same preconditions as generateKeysBetween.
519
+ * n >= 0.
520
+ * Returns an array of n distinct keys in sorted order.
521
+ * If a and b are both null, returns [a0, a1, ...]
522
+ * If one or the other is null, returns consecutive "integer"
523
+ * keys. Otherwise, returns relatively short keys between
524
+ * a and b.
525
+ * @param {string | null | undefined} a
526
+ * @param {string | null | undefined} b
527
+ * @param {number} n
528
+ * @param {string=} digits
529
+ * @param {string=} intDigits
530
+ * @return {string[]}
531
+ */
532
+ export function generateNKeysBetween(
533
+ a,
534
+ b,
535
+ n,
536
+ digits = undefined,
537
+ intDigits = undefined
538
+ ) {
539
+ if (intDigits !== undefined) {
540
+ validateIntDigits(intDigits);
541
+ } else {
542
+ intDigits = digits ?? BASE_52_DIGITS;
543
+ }
544
+ if (digits !== undefined) {
545
+ validateDigits(digits);
546
+ } else {
547
+ digits = BASE_62_DIGITS;
548
+ }
549
+
550
+ if (n === 0) {
551
+ return [];
552
+ }
553
+ if (n === 1) {
554
+ return [generateKeyBetween(a, b, digits, intDigits)];
555
+ }
556
+ if (b == null) {
557
+ let c = generateKeyBetween(a, b, digits, intDigits);
558
+ const result = [c];
559
+ for (let i = 0; i < n - 1; i++) {
560
+ c = generateKeyBetween(c, b, digits, intDigits);
561
+ result.push(c);
562
+ }
563
+ return result;
564
+ }
565
+ if (a == null) {
566
+ let c = generateKeyBetween(a, b, digits, intDigits);
567
+ const result = [c];
568
+ for (let i = 0; i < n - 1; i++) {
569
+ c = generateKeyBetween(a, c, digits, intDigits);
570
+ result.push(c);
571
+ }
572
+ result.reverse();
573
+ return result;
574
+ }
575
+ const mid = Math.floor(n / 2);
576
+ const c = generateKeyBetween(a, b, digits, intDigits);
577
+ return [
578
+ ...generateNKeysBetween(a, c, mid, digits, intDigits),
579
+ c,
580
+ ...generateNKeysBetween(c, b, n - mid - 1, digits, intDigits),
581
+ ];
582
+ }
@@ -8,8 +8,8 @@
8
8
  export function createGitProvider({ gitea, github, getConfig }) {
9
9
  async function resolveActiveProvider(hint) {
10
10
  if (hint && typeof hint === 'object') {
11
- if (hint.provider === 'github') return github
12
- if (hint.provider === 'gitea') return gitea
11
+ if (hint.forge === 'github' || hint.provider === 'github') return github
12
+ if (hint.forge === 'gitea' || hint.provider === 'gitea') return gitea
13
13
  if (typeof hint.issueUrl === 'string' && hint.issueUrl.includes('github.com')) {
14
14
  return github
15
15
  }
@@ -58,6 +58,15 @@ export function createGitProvider({ gitea, github, getConfig }) {
58
58
  return p.me()
59
59
  },
60
60
 
61
+ /** Create repository. */
62
+ async createRepo(options) {
63
+ const p = forTarget(options?.provider) || await resolveActiveProvider(options)
64
+ if (typeof p?.createRepo !== 'function') {
65
+ throw Object.assign(new Error('Repository creation not supported by provider'), { code: 'not-supported' })
66
+ }
67
+ return p.createRepo(options)
68
+ },
69
+
61
70
  /** List issues. */
62
71
  async listIssues(options) {
63
72
  const p = forTarget(options?.provider) || await resolveActiveProvider(options)
package/lib/github.js CHANGED
@@ -188,6 +188,16 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
188
188
  return request('POST', repoPath(owner, repo, `/issues/${n}/comments`), { body })
189
189
  },
190
190
 
191
+ /** Create private, empty repository in organization or user account. */
192
+ async createRepo({ owner, name, description = '' }) {
193
+ const safe = safeSegment(name)
194
+ if (safe === undefined) throw Object.assign(new Error('invalid name'), { code: 'bad-segment' })
195
+ const path = owner ? `/orgs/${safeSegment(owner)}/repos` : '/user/repos'
196
+ return request('POST', path, {
197
+ name: safe, description, private: true, auto_init: false,
198
+ })
199
+ },
200
+
191
201
  /** Create new issue. */
192
202
  async createIssue({ owner, repo, title, body }) {
193
203
  const name = String(title ?? '').trim()