@goodandready/dsh-kanban 0.2.20 → 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 +8 -5
- package/lib/config.js +1 -1
- package/lib/events.js +16 -4
- package/lib/fractional-indexing.js +582 -0
- package/lib/git-provider.js +11 -2
- package/lib/gitea.js +8 -1
- package/lib/github.js +22 -6
- package/lib/import.js +11 -10
- package/lib/index.js +98 -16
- package/lib/launcher.js +164 -53
- package/lib/order.js +20 -50
- package/lib/outbound.js +29 -12
- package/lib/progress-dump.js +16 -11
- package/lib/routes.js +47 -25
- package/lib/store.js +165 -6
- package/lib/sync.js +6 -5
- package/lib/transitions.js +8 -4
- package/lib/workflow-bridge.js +61 -31
- package/lib/worktree.js +66 -12
- package/package.json +1 -1
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)
|
|
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.',
|
package/lib/config.js
CHANGED
|
@@ -106,7 +106,7 @@ export const CONFIG_HINTS = {
|
|
|
106
106
|
preventIdleSleep: 'Prevent host sleep while task agent sessions are running.',
|
|
107
107
|
progressDumpEnabled: 'Enable PROGRESSDUMP state checkpoints between agent sessions.',
|
|
108
108
|
permissionGateEnabled: 'Require human confirmation before launching elevated permission tasks.',
|
|
109
|
-
sessionDefaultPermission: 'Default permission tier for task sessions (read-only, workspace-write, full).',
|
|
109
|
+
sessionDefaultPermission: 'Default permission tier for task sessions (read-only, workspace-write, danger-full-access).',
|
|
110
110
|
announceToAgent: 'Post stage change announcement messages directly into task agent session.',
|
|
111
111
|
maxConcurrentSessions: 'Maximum number of simultaneous active agent sessions on the board.',
|
|
112
112
|
workspaceClaimBoundaries: 'Enforce workspace ownership boundaries to prevent cross-workspace task execution.',
|
package/lib/events.js
CHANGED
|
@@ -28,14 +28,26 @@ export class KanbanEventHub {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
handleSseRequest(req, res) {
|
|
32
|
-
|
|
31
|
+
handleSseRequest(req, res, { isTrustedRequest } = {}) {
|
|
32
|
+
if (typeof isTrustedRequest === 'function' && !isTrustedRequest(req)) {
|
|
33
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
34
|
+
res.end('forbidden')
|
|
35
|
+
return () => {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const headers = {
|
|
33
39
|
'Content-Type': 'text/event-stream',
|
|
34
40
|
'Cache-Control': 'no-cache, no-transform',
|
|
35
41
|
'Connection': 'keep-alive',
|
|
36
42
|
'X-Accel-Buffering': 'no',
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
}
|
|
44
|
+
const origin = req?.headers?.origin
|
|
45
|
+
if (origin && origin !== 'null') {
|
|
46
|
+
headers['Access-Control-Allow-Origin'] = origin
|
|
47
|
+
headers['Vary'] = 'Origin'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
res.writeHead(200, headers)
|
|
39
51
|
|
|
40
52
|
res.write(':connected\n\n')
|
|
41
53
|
this.clients.add(res)
|
|
@@ -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
|
+
}
|
package/lib/git-provider.js
CHANGED
|
@@ -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)
|