issy 0.4.0 → 0.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/bin/issy +162 -30
- package/dist/cli.js +621 -74
- package/package.json +3 -3
- package/dist/install-info.js +0 -54
- package/dist/postinstall.js +0 -61
package/bin/issy
CHANGED
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
existsSync,
|
|
5
5
|
mkdirSync,
|
|
6
6
|
readdirSync,
|
|
7
|
+
readFileSync,
|
|
7
8
|
writeFileSync,
|
|
8
|
-
|
|
9
|
+
renameSync,
|
|
10
|
+
cpSync
|
|
9
11
|
} from 'node:fs'
|
|
10
12
|
import { join, resolve } from 'node:path'
|
|
11
13
|
import { fileURLToPath } from 'node:url'
|
|
@@ -23,7 +25,7 @@ const here = resolve(fileURLToPath(import.meta.url), '..')
|
|
|
23
25
|
const pkgPath = resolve(here, '..', 'package.json')
|
|
24
26
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
25
27
|
|
|
26
|
-
const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 })
|
|
28
|
+
const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 })
|
|
27
29
|
|
|
28
30
|
if (notifier.update) {
|
|
29
31
|
const scriptPath = resolve(fileURLToPath(import.meta.url))
|
|
@@ -45,6 +47,8 @@ const cliCommands = new Set([
|
|
|
45
47
|
'create',
|
|
46
48
|
'update',
|
|
47
49
|
'close',
|
|
50
|
+
'reopen',
|
|
51
|
+
'next',
|
|
48
52
|
'help',
|
|
49
53
|
'--help',
|
|
50
54
|
'-h'
|
|
@@ -57,10 +61,26 @@ if (args[0] === '--version' || args[0] === '-v') {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
/**
|
|
60
|
-
* Find .
|
|
61
|
-
* Returns the path if found, or null if not found.
|
|
64
|
+
* Find .issy directory by walking up from the given path.
|
|
62
65
|
*/
|
|
63
|
-
function
|
|
66
|
+
function findIssyDirUpward(fromPath) {
|
|
67
|
+
let current = resolve(fromPath)
|
|
68
|
+
for (let i = 0; i < 20; i++) {
|
|
69
|
+
const candidate = join(current, '.issy')
|
|
70
|
+
if (existsSync(candidate)) {
|
|
71
|
+
return candidate
|
|
72
|
+
}
|
|
73
|
+
const parent = dirname(current)
|
|
74
|
+
if (parent === current) break
|
|
75
|
+
current = parent
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Find legacy .issues directory by walking up (for migration detection).
|
|
82
|
+
*/
|
|
83
|
+
function findLegacyIssuesDir(fromPath) {
|
|
64
84
|
let current = resolve(fromPath)
|
|
65
85
|
for (let i = 0; i < 20; i++) {
|
|
66
86
|
const candidate = join(current, '.issues')
|
|
@@ -76,7 +96,6 @@ function findIssuesDirUpward(fromPath) {
|
|
|
76
96
|
|
|
77
97
|
/**
|
|
78
98
|
* Find the git repository root by walking up from the given path.
|
|
79
|
-
* Returns the directory containing .git, or null if not in a git repo.
|
|
80
99
|
*/
|
|
81
100
|
function findGitRoot(fromPath) {
|
|
82
101
|
let current = resolve(fromPath)
|
|
@@ -93,47 +112,154 @@ function findGitRoot(fromPath) {
|
|
|
93
112
|
}
|
|
94
113
|
|
|
95
114
|
/**
|
|
96
|
-
* Resolve the
|
|
97
|
-
* 1.
|
|
98
|
-
* 2. Walk up from
|
|
99
|
-
* 3. If in a git repo, use .
|
|
100
|
-
* 4. Fall back to
|
|
115
|
+
* Resolve the .issy directory using priority:
|
|
116
|
+
* 1. ISSY_DIR env var (explicit override)
|
|
117
|
+
* 2. Walk up from cwd to find existing .issy
|
|
118
|
+
* 3. If in a git repo, use .issy at the repo root
|
|
119
|
+
* 4. Fall back to cwd/.issy
|
|
101
120
|
*/
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return resolve(process.env.ISSUES_DIR)
|
|
121
|
+
function resolveIssyDir() {
|
|
122
|
+
if (process.env.ISSY_DIR) {
|
|
123
|
+
return resolve(process.env.ISSY_DIR)
|
|
106
124
|
}
|
|
107
125
|
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const found = findIssuesDirUpward(startDir)
|
|
126
|
+
const startDir = process.env.ISSY_ROOT || process.cwd()
|
|
127
|
+
const found = findIssyDirUpward(startDir)
|
|
111
128
|
if (found) {
|
|
112
129
|
return found
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
// 3. If in a git repo, use .issues at the repo root
|
|
116
132
|
const gitRoot = findGitRoot(startDir)
|
|
117
133
|
if (gitRoot) {
|
|
118
|
-
return join(gitRoot, '.
|
|
134
|
+
return join(gitRoot, '.issy')
|
|
119
135
|
}
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
return join(resolve(startDir), '.issues')
|
|
137
|
+
return join(resolve(startDir), '.issy')
|
|
123
138
|
}
|
|
124
139
|
|
|
125
|
-
const
|
|
140
|
+
const issyDir = resolveIssyDir()
|
|
141
|
+
const issuesDir = join(issyDir, 'issues')
|
|
142
|
+
|
|
126
143
|
// Set env vars for downstream consumers
|
|
127
|
-
process.env.
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
process.env.ISSY_DIR = issyDir
|
|
145
|
+
process.env.ISSY_ROOT = dirname(issyDir)
|
|
146
|
+
|
|
147
|
+
// Detect legacy .issues/ directory and warn (unless running migrate)
|
|
148
|
+
const legacyDir = findLegacyIssuesDir(process.env.ISSY_ROOT || process.cwd())
|
|
149
|
+
if (legacyDir && args[0] !== 'migrate' && args[0] !== 'init') {
|
|
150
|
+
// Only warn if .issy doesn't exist yet (haven't migrated)
|
|
151
|
+
if (!existsSync(issyDir)) {
|
|
152
|
+
console.warn(`⚠️ Legacy .issues/ directory detected at ${legacyDir}`)
|
|
153
|
+
console.warn(` Run "issy migrate" to upgrade to the new .issy/ structure.\n`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Handle commands ---
|
|
158
|
+
|
|
159
|
+
if (args[0] === 'migrate') {
|
|
160
|
+
const startDir = process.env.ISSY_ROOT || process.cwd()
|
|
161
|
+
const legacy = findLegacyIssuesDir(startDir)
|
|
162
|
+
|
|
163
|
+
if (!legacy) {
|
|
164
|
+
console.log('No legacy .issues/ directory found. Nothing to migrate.')
|
|
165
|
+
process.exit(0)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (existsSync(issyDir) && existsSync(issuesDir)) {
|
|
169
|
+
console.log('.issy/issues/ already exists. Migration may have already been completed.')
|
|
170
|
+
process.exit(1)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(`Migrating ${legacy} → ${issuesDir}`)
|
|
174
|
+
|
|
175
|
+
// Create .issy/issues/
|
|
176
|
+
mkdirSync(issuesDir, { recursive: true })
|
|
177
|
+
|
|
178
|
+
// Copy issue files
|
|
179
|
+
const files = readdirSync(legacy).filter(f => f.endsWith('.md') && /^\d{4}-/.test(f))
|
|
180
|
+
|
|
181
|
+
// We'll use fractional-indexing to assign initial order keys to open issues.
|
|
182
|
+
// Since we can't import ESM from the bundled core here easily,
|
|
183
|
+
// we assign order keys inline using the same algorithm.
|
|
184
|
+
// Import dynamically from the built core.
|
|
185
|
+
let generateNKeysBetween
|
|
186
|
+
try {
|
|
187
|
+
const fi = await import('fractional-indexing')
|
|
188
|
+
generateNKeysBetween = fi.generateNKeysBetween
|
|
189
|
+
} catch {
|
|
190
|
+
console.error('Failed to load fractional-indexing. Please ensure dependencies are installed.')
|
|
191
|
+
process.exit(1)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Parse frontmatter from each file and sort by ID
|
|
195
|
+
const issueData = files.map(f => {
|
|
196
|
+
const content = readFileSync(join(legacy, f), 'utf-8')
|
|
197
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
198
|
+
const frontmatter = {}
|
|
199
|
+
if (match) {
|
|
200
|
+
for (const line of match[1].split('\n')) {
|
|
201
|
+
const colonIdx = line.indexOf(':')
|
|
202
|
+
if (colonIdx > 0) {
|
|
203
|
+
frontmatter[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { filename: f, content, frontmatter }
|
|
208
|
+
}).sort((a, b) => a.filename.localeCompare(b.filename))
|
|
209
|
+
|
|
210
|
+
// Assign order keys to open issues
|
|
211
|
+
const openIssues = issueData.filter(i => i.frontmatter.status === 'open')
|
|
212
|
+
const orderKeys = generateNKeysBetween(null, null, openIssues.length)
|
|
213
|
+
|
|
214
|
+
const orderMap = new Map()
|
|
215
|
+
openIssues.forEach((issue, idx) => {
|
|
216
|
+
orderMap.set(issue.filename, orderKeys[idx])
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Write migrated files with order keys
|
|
220
|
+
for (const issue of issueData) {
|
|
221
|
+
let content = issue.content
|
|
222
|
+
const orderKey = orderMap.get(issue.filename)
|
|
223
|
+
|
|
224
|
+
if (orderKey) {
|
|
225
|
+
// Insert order field before the status line
|
|
226
|
+
content = content.replace(
|
|
227
|
+
/^(---\n[\s\S]*?)(status: \w+)/m,
|
|
228
|
+
`$1$2\norder: ${orderKey}`
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
writeFileSync(join(issuesDir, issue.filename), content)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Copy any non-issue files (e.g., README)
|
|
236
|
+
const otherFiles = readdirSync(legacy).filter(f => !files.includes(f))
|
|
237
|
+
for (const f of otherFiles) {
|
|
238
|
+
const src = join(legacy, f)
|
|
239
|
+
const dest = join(issuesDir, f)
|
|
240
|
+
try {
|
|
241
|
+
cpSync(src, dest, { recursive: true })
|
|
242
|
+
} catch { /* skip if can't copy */ }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Remove legacy directory
|
|
246
|
+
const { rmSync } = await import('node:fs')
|
|
247
|
+
rmSync(legacy, { recursive: true })
|
|
248
|
+
|
|
249
|
+
console.log(`✅ Migrated ${files.length} issue(s) to ${issuesDir}`)
|
|
250
|
+
if (openIssues.length > 0) {
|
|
251
|
+
console.log(` Assigned roadmap order to ${openIssues.length} open issue(s).`)
|
|
252
|
+
}
|
|
253
|
+
console.log(` Removed ${legacy}`)
|
|
254
|
+
process.exit(0)
|
|
255
|
+
}
|
|
130
256
|
|
|
131
257
|
if (cliCommands.has(args[0] || '')) {
|
|
132
258
|
const here = resolve(fileURLToPath(import.meta.url), '..')
|
|
133
259
|
const entry = resolve(here, '..', 'dist', 'cli.js')
|
|
134
260
|
process.argv = [process.argv[0], process.argv[1], ...args]
|
|
135
261
|
const cli = await import(entry)
|
|
136
|
-
await cli.ready
|
|
262
|
+
await cli.ready
|
|
137
263
|
process.exit(0)
|
|
138
264
|
}
|
|
139
265
|
|
|
@@ -145,18 +271,23 @@ if (portIdx >= 0 && args[portIdx + 1]) {
|
|
|
145
271
|
const shouldInitOnly = args.includes('init')
|
|
146
272
|
const shouldSeed = args.includes('--seed')
|
|
147
273
|
|
|
148
|
-
// Only create .issues directory on explicit 'init' command
|
|
149
274
|
if (shouldInitOnly) {
|
|
150
275
|
if (!existsSync(issuesDir)) {
|
|
151
276
|
mkdirSync(issuesDir, { recursive: true })
|
|
152
277
|
}
|
|
153
278
|
|
|
154
|
-
// Only seed with welcome issue if --seed flag is passed
|
|
155
279
|
if (shouldSeed) {
|
|
156
280
|
const hasIssues =
|
|
157
281
|
existsSync(issuesDir) &&
|
|
158
282
|
readdirSync(issuesDir).some(f => f.endsWith('.md'))
|
|
159
283
|
if (!hasIssues) {
|
|
284
|
+
// First issue gets the initial order key
|
|
285
|
+
let firstOrderKey = 'a0'
|
|
286
|
+
try {
|
|
287
|
+
const fi = await import('fractional-indexing')
|
|
288
|
+
firstOrderKey = fi.generateKeyBetween(null, null)
|
|
289
|
+
} catch { /* use fallback */ }
|
|
290
|
+
|
|
160
291
|
const welcome =
|
|
161
292
|
`---\n` +
|
|
162
293
|
`title: Welcome to issy\n` +
|
|
@@ -164,6 +295,7 @@ if (shouldInitOnly) {
|
|
|
164
295
|
`priority: medium\n` +
|
|
165
296
|
`type: improvement\n` +
|
|
166
297
|
`status: open\n` +
|
|
298
|
+
`order: ${firstOrderKey}\n` +
|
|
167
299
|
`created: ${new Date().toISOString().slice(0, 19)}\n` +
|
|
168
300
|
`---\n\n` +
|
|
169
301
|
`## Details\n\n` +
|
|
@@ -173,7 +305,7 @@ if (shouldInitOnly) {
|
|
|
173
305
|
}
|
|
174
306
|
}
|
|
175
307
|
|
|
176
|
-
console.log(`Initialized ${
|
|
308
|
+
console.log(`Initialized ${issyDir}`)
|
|
177
309
|
process.exit(0)
|
|
178
310
|
}
|
|
179
311
|
|
package/dist/cli.js
CHANGED
|
@@ -7,23 +7,209 @@ import { parseArgs } from "util";
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
9
9
|
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
// ../../node_modules/.bun/fractional-indexing@3.2.0/node_modules/fractional-indexing/src/index.js
|
|
12
|
+
var BASE_62_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
13
|
+
function midpoint(a, b, digits) {
|
|
14
|
+
const zero = digits[0];
|
|
15
|
+
if (b != null && a >= b) {
|
|
16
|
+
throw new Error(a + " >= " + b);
|
|
17
|
+
}
|
|
18
|
+
if (a.slice(-1) === zero || b && b.slice(-1) === zero) {
|
|
19
|
+
throw new Error("trailing zero");
|
|
20
|
+
}
|
|
21
|
+
if (b) {
|
|
22
|
+
let n = 0;
|
|
23
|
+
while ((a[n] || zero) === b[n]) {
|
|
24
|
+
n++;
|
|
25
|
+
}
|
|
26
|
+
if (n > 0) {
|
|
27
|
+
return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const digitA = a ? digits.indexOf(a[0]) : 0;
|
|
31
|
+
const digitB = b != null ? digits.indexOf(b[0]) : digits.length;
|
|
32
|
+
if (digitB - digitA > 1) {
|
|
33
|
+
const midDigit = Math.round(0.5 * (digitA + digitB));
|
|
34
|
+
return digits[midDigit];
|
|
35
|
+
} else {
|
|
36
|
+
if (b && b.length > 1) {
|
|
37
|
+
return b.slice(0, 1);
|
|
38
|
+
} else {
|
|
39
|
+
return digits[digitA] + midpoint(a.slice(1), null, digits);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function validateInteger(int) {
|
|
44
|
+
if (int.length !== getIntegerLength(int[0])) {
|
|
45
|
+
throw new Error("invalid integer part of order key: " + int);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function getIntegerLength(head) {
|
|
49
|
+
if (head >= "a" && head <= "z") {
|
|
50
|
+
return head.charCodeAt(0) - 97 + 2;
|
|
51
|
+
} else if (head >= "A" && head <= "Z") {
|
|
52
|
+
return 90 - head.charCodeAt(0) + 2;
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error("invalid order key head: " + head);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function getIntegerPart(key) {
|
|
58
|
+
const integerPartLength = getIntegerLength(key[0]);
|
|
59
|
+
if (integerPartLength > key.length) {
|
|
60
|
+
throw new Error("invalid order key: " + key);
|
|
61
|
+
}
|
|
62
|
+
return key.slice(0, integerPartLength);
|
|
63
|
+
}
|
|
64
|
+
function validateOrderKey(key, digits) {
|
|
65
|
+
if (key === "A" + digits[0].repeat(26)) {
|
|
66
|
+
throw new Error("invalid order key: " + key);
|
|
67
|
+
}
|
|
68
|
+
const i = getIntegerPart(key);
|
|
69
|
+
const f = key.slice(i.length);
|
|
70
|
+
if (f.slice(-1) === digits[0]) {
|
|
71
|
+
throw new Error("invalid order key: " + key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function incrementInteger(x, digits) {
|
|
75
|
+
validateInteger(x);
|
|
76
|
+
const [head, ...digs] = x.split("");
|
|
77
|
+
let carry = true;
|
|
78
|
+
for (let i = digs.length - 1;carry && i >= 0; i--) {
|
|
79
|
+
const d = digits.indexOf(digs[i]) + 1;
|
|
80
|
+
if (d === digits.length) {
|
|
81
|
+
digs[i] = digits[0];
|
|
82
|
+
} else {
|
|
83
|
+
digs[i] = digits[d];
|
|
84
|
+
carry = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (carry) {
|
|
88
|
+
if (head === "Z") {
|
|
89
|
+
return "a" + digits[0];
|
|
90
|
+
}
|
|
91
|
+
if (head === "z") {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const h = String.fromCharCode(head.charCodeAt(0) + 1);
|
|
95
|
+
if (h > "a") {
|
|
96
|
+
digs.push(digits[0]);
|
|
97
|
+
} else {
|
|
98
|
+
digs.pop();
|
|
99
|
+
}
|
|
100
|
+
return h + digs.join("");
|
|
101
|
+
} else {
|
|
102
|
+
return head + digs.join("");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function decrementInteger(x, digits) {
|
|
106
|
+
validateInteger(x);
|
|
107
|
+
const [head, ...digs] = x.split("");
|
|
108
|
+
let borrow = true;
|
|
109
|
+
for (let i = digs.length - 1;borrow && i >= 0; i--) {
|
|
110
|
+
const d = digits.indexOf(digs[i]) - 1;
|
|
111
|
+
if (d === -1) {
|
|
112
|
+
digs[i] = digits.slice(-1);
|
|
113
|
+
} else {
|
|
114
|
+
digs[i] = digits[d];
|
|
115
|
+
borrow = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (borrow) {
|
|
119
|
+
if (head === "a") {
|
|
120
|
+
return "Z" + digits.slice(-1);
|
|
121
|
+
}
|
|
122
|
+
if (head === "A") {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const h = String.fromCharCode(head.charCodeAt(0) - 1);
|
|
126
|
+
if (h < "Z") {
|
|
127
|
+
digs.push(digits.slice(-1));
|
|
128
|
+
} else {
|
|
129
|
+
digs.pop();
|
|
130
|
+
}
|
|
131
|
+
return h + digs.join("");
|
|
132
|
+
} else {
|
|
133
|
+
return head + digs.join("");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
|
|
137
|
+
if (a != null) {
|
|
138
|
+
validateOrderKey(a, digits);
|
|
139
|
+
}
|
|
140
|
+
if (b != null) {
|
|
141
|
+
validateOrderKey(b, digits);
|
|
142
|
+
}
|
|
143
|
+
if (a != null && b != null && a >= b) {
|
|
144
|
+
throw new Error(a + " >= " + b);
|
|
145
|
+
}
|
|
146
|
+
if (a == null) {
|
|
147
|
+
if (b == null) {
|
|
148
|
+
return "a" + digits[0];
|
|
149
|
+
}
|
|
150
|
+
const ib2 = getIntegerPart(b);
|
|
151
|
+
const fb2 = b.slice(ib2.length);
|
|
152
|
+
if (ib2 === "A" + digits[0].repeat(26)) {
|
|
153
|
+
return ib2 + midpoint("", fb2, digits);
|
|
154
|
+
}
|
|
155
|
+
if (ib2 < b) {
|
|
156
|
+
return ib2;
|
|
157
|
+
}
|
|
158
|
+
const res = decrementInteger(ib2, digits);
|
|
159
|
+
if (res == null) {
|
|
160
|
+
throw new Error("cannot decrement any more");
|
|
161
|
+
}
|
|
162
|
+
return res;
|
|
163
|
+
}
|
|
164
|
+
if (b == null) {
|
|
165
|
+
const ia2 = getIntegerPart(a);
|
|
166
|
+
const fa2 = a.slice(ia2.length);
|
|
167
|
+
const i2 = incrementInteger(ia2, digits);
|
|
168
|
+
return i2 == null ? ia2 + midpoint(fa2, null, digits) : i2;
|
|
169
|
+
}
|
|
170
|
+
const ia = getIntegerPart(a);
|
|
171
|
+
const fa = a.slice(ia.length);
|
|
172
|
+
const ib = getIntegerPart(b);
|
|
173
|
+
const fb = b.slice(ib.length);
|
|
174
|
+
if (ia === ib) {
|
|
175
|
+
return ia + midpoint(fa, fb, digits);
|
|
176
|
+
}
|
|
177
|
+
const i = incrementInteger(ia, digits);
|
|
178
|
+
if (i == null) {
|
|
179
|
+
throw new Error("cannot increment any more");
|
|
180
|
+
}
|
|
181
|
+
if (i < b) {
|
|
182
|
+
return i;
|
|
183
|
+
}
|
|
184
|
+
return ia + midpoint(fa, null, digits);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ../core/src/lib/issues.ts
|
|
188
|
+
var issyDir = null;
|
|
10
189
|
var issuesDir = null;
|
|
11
|
-
function
|
|
12
|
-
|
|
190
|
+
function setIssyDir(dir) {
|
|
191
|
+
issyDir = dir;
|
|
192
|
+
issuesDir = join(dir, "issues");
|
|
193
|
+
}
|
|
194
|
+
function getIssyDir() {
|
|
195
|
+
if (!issyDir) {
|
|
196
|
+
throw new Error("Issy directory not initialized. Call resolveIssyDir() first.");
|
|
197
|
+
}
|
|
198
|
+
return issyDir;
|
|
13
199
|
}
|
|
14
200
|
function getIssuesDir() {
|
|
15
201
|
if (!issuesDir) {
|
|
16
|
-
throw new Error("Issues directory not initialized. Call
|
|
202
|
+
throw new Error("Issues directory not initialized. Call resolveIssyDir() first.");
|
|
17
203
|
}
|
|
18
204
|
return issuesDir;
|
|
19
205
|
}
|
|
20
206
|
async function ensureIssuesDir() {
|
|
21
207
|
await mkdir(getIssuesDir(), { recursive: true });
|
|
22
208
|
}
|
|
23
|
-
function
|
|
209
|
+
function findIssyDirUpward(fromPath) {
|
|
24
210
|
let current = resolve(fromPath);
|
|
25
211
|
for (let i = 0;i < 20; i++) {
|
|
26
|
-
const candidate = join(current, ".
|
|
212
|
+
const candidate = join(current, ".issy");
|
|
27
213
|
if (existsSync(candidate)) {
|
|
28
214
|
return candidate;
|
|
29
215
|
}
|
|
@@ -48,26 +234,26 @@ function findGitRoot(fromPath) {
|
|
|
48
234
|
}
|
|
49
235
|
return null;
|
|
50
236
|
}
|
|
51
|
-
function
|
|
52
|
-
if (process.env.
|
|
53
|
-
const dir = resolve(process.env.
|
|
54
|
-
|
|
237
|
+
function resolveIssyDir() {
|
|
238
|
+
if (process.env.ISSY_DIR) {
|
|
239
|
+
const dir = resolve(process.env.ISSY_DIR);
|
|
240
|
+
setIssyDir(dir);
|
|
55
241
|
return dir;
|
|
56
242
|
}
|
|
57
|
-
const startDir = process.env.
|
|
58
|
-
const found =
|
|
243
|
+
const startDir = process.env.ISSY_ROOT || process.cwd();
|
|
244
|
+
const found = findIssyDirUpward(startDir);
|
|
59
245
|
if (found) {
|
|
60
|
-
|
|
246
|
+
setIssyDir(found);
|
|
61
247
|
return found;
|
|
62
248
|
}
|
|
63
249
|
const gitRoot = findGitRoot(startDir);
|
|
64
250
|
if (gitRoot) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
return
|
|
251
|
+
const gitIssyDir = join(gitRoot, ".issy");
|
|
252
|
+
setIssyDir(gitIssyDir);
|
|
253
|
+
return gitIssyDir;
|
|
68
254
|
}
|
|
69
|
-
const fallback = join(resolve(startDir), ".
|
|
70
|
-
|
|
255
|
+
const fallback = join(resolve(startDir), ".issy");
|
|
256
|
+
setIssyDir(fallback);
|
|
71
257
|
return fallback;
|
|
72
258
|
}
|
|
73
259
|
function parseFrontmatter(content) {
|
|
@@ -101,6 +287,9 @@ function generateFrontmatter(data) {
|
|
|
101
287
|
lines.push(`labels: ${data.labels}`);
|
|
102
288
|
}
|
|
103
289
|
lines.push(`status: ${data.status}`);
|
|
290
|
+
if (data.order) {
|
|
291
|
+
lines.push(`order: ${data.order}`);
|
|
292
|
+
}
|
|
104
293
|
lines.push(`created: ${data.created}`);
|
|
105
294
|
if (data.updated) {
|
|
106
295
|
lines.push(`updated: ${data.updated}`);
|
|
@@ -165,16 +354,60 @@ async function getAllIssues() {
|
|
|
165
354
|
content: body
|
|
166
355
|
});
|
|
167
356
|
}
|
|
168
|
-
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
169
357
|
return issues.sort((a, b) => {
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
358
|
+
const orderA = a.frontmatter.order;
|
|
359
|
+
const orderB = b.frontmatter.order;
|
|
360
|
+
if (orderA && orderB)
|
|
361
|
+
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
362
|
+
if (orderA && !orderB)
|
|
363
|
+
return -1;
|
|
364
|
+
if (!orderA && orderB)
|
|
365
|
+
return 1;
|
|
366
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
176
367
|
});
|
|
177
368
|
}
|
|
369
|
+
async function getOpenIssuesByOrder() {
|
|
370
|
+
const allIssues = await getAllIssues();
|
|
371
|
+
return allIssues.filter((i) => i.frontmatter.status === "open");
|
|
372
|
+
}
|
|
373
|
+
function computeOrderKey(openIssues, options, excludeId) {
|
|
374
|
+
const issues = excludeId ? openIssues.filter((i) => i.id !== excludeId.padStart(4, "0")) : openIssues;
|
|
375
|
+
if (options.first) {
|
|
376
|
+
if (issues.length === 0)
|
|
377
|
+
return generateKeyBetween(null, null);
|
|
378
|
+
const firstOrder = issues[0].frontmatter.order || null;
|
|
379
|
+
return generateKeyBetween(null, firstOrder);
|
|
380
|
+
}
|
|
381
|
+
if (options.last) {
|
|
382
|
+
if (issues.length === 0)
|
|
383
|
+
return generateKeyBetween(null, null);
|
|
384
|
+
const lastOrder2 = issues[issues.length - 1].frontmatter.order || null;
|
|
385
|
+
return generateKeyBetween(lastOrder2, null);
|
|
386
|
+
}
|
|
387
|
+
if (options.after) {
|
|
388
|
+
const targetId = options.after.padStart(4, "0");
|
|
389
|
+
const idx = issues.findIndex((i) => i.id === targetId);
|
|
390
|
+
if (idx === -1)
|
|
391
|
+
throw new Error(`Issue #${options.after} not found among open issues. The --after target must be an open issue.`);
|
|
392
|
+
const afterOrder = issues[idx].frontmatter.order || null;
|
|
393
|
+
const nextOrder = idx + 1 < issues.length ? issues[idx + 1].frontmatter.order || null : null;
|
|
394
|
+
return generateKeyBetween(afterOrder, nextOrder);
|
|
395
|
+
}
|
|
396
|
+
if (options.before) {
|
|
397
|
+
const targetId = options.before.padStart(4, "0");
|
|
398
|
+
const idx = issues.findIndex((i) => i.id === targetId);
|
|
399
|
+
if (idx === -1)
|
|
400
|
+
throw new Error(`Issue #${options.before} not found among open issues. The --before target must be an open issue.`);
|
|
401
|
+
const beforeOrder = issues[idx].frontmatter.order || null;
|
|
402
|
+
const prevOrder = idx > 0 ? issues[idx - 1].frontmatter.order || null : null;
|
|
403
|
+
return generateKeyBetween(prevOrder, beforeOrder);
|
|
404
|
+
}
|
|
405
|
+
if (issues.length === 0) {
|
|
406
|
+
return generateKeyBetween(null, null);
|
|
407
|
+
}
|
|
408
|
+
const lastOrder = issues[issues.length - 1].frontmatter.order || null;
|
|
409
|
+
return generateKeyBetween(lastOrder, null);
|
|
410
|
+
}
|
|
178
411
|
async function createIssue(input) {
|
|
179
412
|
await ensureIssuesDir();
|
|
180
413
|
if (!input.title) {
|
|
@@ -203,6 +436,7 @@ async function createIssue(input) {
|
|
|
203
436
|
type,
|
|
204
437
|
labels: input.labels || undefined,
|
|
205
438
|
status: "open",
|
|
439
|
+
order: input.order || undefined,
|
|
206
440
|
created: formatDate()
|
|
207
441
|
};
|
|
208
442
|
const content = `${generateFrontmatter(frontmatter)}
|
|
@@ -241,6 +475,7 @@ async function updateIssue(id, input) {
|
|
|
241
475
|
labels: input.labels || undefined
|
|
242
476
|
},
|
|
243
477
|
...input.status && { status: input.status },
|
|
478
|
+
...input.order && { order: input.order },
|
|
244
479
|
updated: formatDate()
|
|
245
480
|
};
|
|
246
481
|
const content = `${generateFrontmatter(updatedFrontmatter)}
|
|
@@ -254,6 +489,21 @@ ${issue.content}`;
|
|
|
254
489
|
async function closeIssue(id) {
|
|
255
490
|
return updateIssue(id, { status: "closed" });
|
|
256
491
|
}
|
|
492
|
+
async function reopenIssue(id, order) {
|
|
493
|
+
return updateIssue(id, { status: "open", order });
|
|
494
|
+
}
|
|
495
|
+
async function getOnCloseContent() {
|
|
496
|
+
try {
|
|
497
|
+
const onClosePath = join(getIssyDir(), "on_close.md");
|
|
498
|
+
return await readFile(onClosePath, "utf-8");
|
|
499
|
+
} catch {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function getNextIssue() {
|
|
504
|
+
const openIssues = await getOpenIssuesByOrder();
|
|
505
|
+
return openIssues.length > 0 ? openIssues[0] : null;
|
|
506
|
+
}
|
|
257
507
|
// ../core/src/lib/query-parser.ts
|
|
258
508
|
var SUPPORTED_QUALIFIERS = new Set([
|
|
259
509
|
"is",
|
|
@@ -263,6 +513,59 @@ var SUPPORTED_QUALIFIERS = new Set([
|
|
|
263
513
|
"label",
|
|
264
514
|
"sort"
|
|
265
515
|
]);
|
|
516
|
+
function parseQuery(query) {
|
|
517
|
+
const qualifiers = {};
|
|
518
|
+
const searchTextParts = [];
|
|
519
|
+
if (!query || !query.trim()) {
|
|
520
|
+
return { qualifiers, searchText: "" };
|
|
521
|
+
}
|
|
522
|
+
const tokens = tokenizeQuery(query);
|
|
523
|
+
for (const token of tokens) {
|
|
524
|
+
const colonIndex = token.indexOf(":");
|
|
525
|
+
if (colonIndex > 0 && colonIndex < token.length - 1) {
|
|
526
|
+
const key = token.substring(0, colonIndex);
|
|
527
|
+
const value = token.substring(colonIndex + 1);
|
|
528
|
+
if (SUPPORTED_QUALIFIERS.has(key)) {
|
|
529
|
+
qualifiers[key] = value;
|
|
530
|
+
} else {
|
|
531
|
+
searchTextParts.push(token);
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
searchTextParts.push(token);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
qualifiers,
|
|
539
|
+
searchText: searchTextParts.join(" ").trim()
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function tokenizeQuery(query) {
|
|
543
|
+
const tokens = [];
|
|
544
|
+
let currentToken = "";
|
|
545
|
+
let inQuotes = false;
|
|
546
|
+
let quoteChar = "";
|
|
547
|
+
for (let i = 0;i < query.length; i++) {
|
|
548
|
+
const char = query[i];
|
|
549
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
550
|
+
inQuotes = true;
|
|
551
|
+
quoteChar = char;
|
|
552
|
+
} else if (char === quoteChar && inQuotes) {
|
|
553
|
+
inQuotes = false;
|
|
554
|
+
quoteChar = "";
|
|
555
|
+
} else if (char === " " && !inQuotes) {
|
|
556
|
+
if (currentToken) {
|
|
557
|
+
tokens.push(currentToken);
|
|
558
|
+
currentToken = "";
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
currentToken += char;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (currentToken) {
|
|
565
|
+
tokens.push(currentToken);
|
|
566
|
+
}
|
|
567
|
+
return tokens;
|
|
568
|
+
}
|
|
266
569
|
// ../../node_modules/.bun/fuse.js@7.1.0/node_modules/fuse.js/dist/fuse.mjs
|
|
267
570
|
function isArray(value) {
|
|
268
571
|
return !Array.isArray ? getTag(value) === "[object Array]" : Array.isArray(value);
|
|
@@ -1099,7 +1402,7 @@ var searchers = [
|
|
|
1099
1402
|
var searchersLen = searchers.length;
|
|
1100
1403
|
var SPACE_RE = / +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;
|
|
1101
1404
|
var OR_TOKEN = "|";
|
|
1102
|
-
function
|
|
1405
|
+
function parseQuery2(pattern, options = {}) {
|
|
1103
1406
|
return pattern.split(OR_TOKEN).map((item) => {
|
|
1104
1407
|
let query = item.trim().split(SPACE_RE).filter((item2) => item2 && !!item2.trim());
|
|
1105
1408
|
let results = [];
|
|
@@ -1160,7 +1463,7 @@ class ExtendedSearch {
|
|
|
1160
1463
|
pattern = isCaseSensitive ? pattern : pattern.toLowerCase();
|
|
1161
1464
|
pattern = ignoreDiacritics ? stripDiacritics(pattern) : pattern;
|
|
1162
1465
|
this.pattern = pattern;
|
|
1163
|
-
this.query =
|
|
1466
|
+
this.query = parseQuery2(this.pattern, this.options);
|
|
1164
1467
|
}
|
|
1165
1468
|
static condition(_, options) {
|
|
1166
1469
|
return options.useExtendedSearch;
|
|
@@ -1572,38 +1875,147 @@ var FUSE_OPTIONS = {
|
|
|
1572
1875
|
function createSearchIndex(issues) {
|
|
1573
1876
|
return new Fuse(issues, FUSE_OPTIONS);
|
|
1574
1877
|
}
|
|
1575
|
-
function
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1878
|
+
function sortIssues(issues, sortBy) {
|
|
1879
|
+
const sortOption = sortBy.toLowerCase();
|
|
1880
|
+
if (sortOption === "roadmap") {
|
|
1881
|
+
issues.sort((a, b) => {
|
|
1882
|
+
const orderA = a.frontmatter.order;
|
|
1883
|
+
const orderB = b.frontmatter.order;
|
|
1884
|
+
if (orderA && orderB)
|
|
1885
|
+
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
1886
|
+
if (orderA && !orderB)
|
|
1887
|
+
return -1;
|
|
1888
|
+
if (!orderA && orderB)
|
|
1889
|
+
return 1;
|
|
1890
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
1891
|
+
});
|
|
1892
|
+
} else if (sortOption === "priority") {
|
|
1893
|
+
const priorityOrder = {
|
|
1894
|
+
high: 0,
|
|
1895
|
+
medium: 1,
|
|
1896
|
+
low: 2
|
|
1897
|
+
};
|
|
1898
|
+
issues.sort((a, b) => {
|
|
1899
|
+
const priorityA = priorityOrder[a.frontmatter.priority] ?? 999;
|
|
1900
|
+
const priorityB = priorityOrder[b.frontmatter.priority] ?? 999;
|
|
1901
|
+
if (priorityA !== priorityB)
|
|
1902
|
+
return priorityA - priorityB;
|
|
1903
|
+
return b.id.localeCompare(a.id);
|
|
1904
|
+
});
|
|
1905
|
+
} else if (sortOption === "scope") {
|
|
1906
|
+
const scopeOrder = {
|
|
1907
|
+
small: 0,
|
|
1908
|
+
medium: 1,
|
|
1909
|
+
large: 2
|
|
1910
|
+
};
|
|
1911
|
+
issues.sort((a, b) => {
|
|
1912
|
+
const scopeA = a.frontmatter.scope ? scopeOrder[a.frontmatter.scope] ?? 99 : 99;
|
|
1913
|
+
const scopeB = b.frontmatter.scope ? scopeOrder[b.frontmatter.scope] ?? 99 : 99;
|
|
1914
|
+
if (scopeA !== scopeB)
|
|
1915
|
+
return scopeA - scopeB;
|
|
1916
|
+
return b.id.localeCompare(a.id);
|
|
1917
|
+
});
|
|
1918
|
+
} else if (sortOption === "created") {
|
|
1919
|
+
issues.sort((a, b) => {
|
|
1920
|
+
const dateA = a.frontmatter.created || "";
|
|
1921
|
+
const dateB = b.frontmatter.created || "";
|
|
1922
|
+
if (dateA !== dateB)
|
|
1923
|
+
return dateB.localeCompare(dateA);
|
|
1924
|
+
return b.id.localeCompare(a.id);
|
|
1925
|
+
});
|
|
1926
|
+
} else if (sortOption === "created-asc") {
|
|
1927
|
+
issues.sort((a, b) => {
|
|
1928
|
+
const dateA = a.frontmatter.created || "";
|
|
1929
|
+
const dateB = b.frontmatter.created || "";
|
|
1930
|
+
if (dateA !== dateB)
|
|
1931
|
+
return dateA.localeCompare(dateB);
|
|
1932
|
+
return a.id.localeCompare(b.id);
|
|
1933
|
+
});
|
|
1934
|
+
} else if (sortOption === "updated") {
|
|
1935
|
+
issues.sort((a, b) => {
|
|
1936
|
+
const dateA = a.frontmatter.updated || a.frontmatter.created || "";
|
|
1937
|
+
const dateB = b.frontmatter.updated || b.frontmatter.created || "";
|
|
1938
|
+
if (dateA !== dateB)
|
|
1939
|
+
return dateB.localeCompare(dateA);
|
|
1940
|
+
return b.id.localeCompare(a.id);
|
|
1941
|
+
});
|
|
1942
|
+
} else if (sortOption === "id") {
|
|
1943
|
+
issues.sort((a, b) => b.id.localeCompare(a.id));
|
|
1944
|
+
} else {
|
|
1945
|
+
issues.sort((a, b) => {
|
|
1946
|
+
const orderA = a.frontmatter.order;
|
|
1947
|
+
const orderB = b.frontmatter.order;
|
|
1948
|
+
if (orderA && orderB)
|
|
1949
|
+
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
1950
|
+
if (orderA && !orderB)
|
|
1951
|
+
return -1;
|
|
1952
|
+
if (!orderA && orderB)
|
|
1953
|
+
return 1;
|
|
1954
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
function filterByQuery(issues, query) {
|
|
1959
|
+
const parsed = parseQuery(query);
|
|
1960
|
+
let result = issues.filter((issue) => {
|
|
1961
|
+
if (parsed.qualifiers.is) {
|
|
1962
|
+
const statusValue = parsed.qualifiers.is.toLowerCase();
|
|
1963
|
+
if (statusValue === "open" || statusValue === "closed") {
|
|
1964
|
+
if (issue.frontmatter.status !== statusValue) {
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1579
1968
|
}
|
|
1580
|
-
if (
|
|
1581
|
-
|
|
1969
|
+
if (parsed.qualifiers.priority) {
|
|
1970
|
+
const priorityValue = parsed.qualifiers.priority.toLowerCase();
|
|
1971
|
+
if (priorityValue === "high" || priorityValue === "medium" || priorityValue === "low") {
|
|
1972
|
+
if (issue.frontmatter.priority !== priorityValue) {
|
|
1973
|
+
return false;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (parsed.qualifiers.scope) {
|
|
1978
|
+
const scopeValue = parsed.qualifiers.scope.toLowerCase();
|
|
1979
|
+
if (scopeValue === "small" || scopeValue === "medium" || scopeValue === "large") {
|
|
1980
|
+
if (issue.frontmatter.scope !== scopeValue) {
|
|
1981
|
+
return false;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1582
1984
|
}
|
|
1583
|
-
if (
|
|
1584
|
-
|
|
1985
|
+
if (parsed.qualifiers.type) {
|
|
1986
|
+
const typeValue = parsed.qualifiers.type.toLowerCase();
|
|
1987
|
+
if (typeValue === "bug" || typeValue === "improvement") {
|
|
1988
|
+
if (issue.frontmatter.type !== typeValue) {
|
|
1989
|
+
return false;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1585
1992
|
}
|
|
1586
|
-
if (
|
|
1587
|
-
|
|
1993
|
+
if (parsed.qualifiers.label) {
|
|
1994
|
+
const labelQuery = parsed.qualifiers.label.toLowerCase();
|
|
1995
|
+
const issueLabels = (issue.frontmatter.labels || "").toLowerCase();
|
|
1996
|
+
if (!issueLabels.includes(labelQuery)) {
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1588
1999
|
}
|
|
1589
2000
|
return true;
|
|
1590
2001
|
});
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
2002
|
+
if (!parsed.searchText.trim()) {
|
|
2003
|
+
const sortBy = parsed.qualifiers.sort?.toLowerCase() || "roadmap";
|
|
2004
|
+
sortIssues(result, sortBy);
|
|
2005
|
+
}
|
|
2006
|
+
if (parsed.searchText.trim()) {
|
|
2007
|
+
const searchQuery = parsed.searchText.trim();
|
|
1596
2008
|
const idMatches = [];
|
|
1597
2009
|
const nonIdMatches = [];
|
|
1598
|
-
const normalizedQuery =
|
|
2010
|
+
const normalizedQuery = searchQuery.replace(/^0+/, "");
|
|
1599
2011
|
for (const issue of result) {
|
|
1600
2012
|
const normalizedId = issue.id.replace(/^0+/, "");
|
|
1601
|
-
if (normalizedId.startsWith(normalizedQuery) || issue.id.startsWith(
|
|
2013
|
+
if (normalizedId.startsWith(normalizedQuery) || issue.id.startsWith(searchQuery)) {
|
|
1602
2014
|
idMatches.push(issue);
|
|
1603
2015
|
}
|
|
1604
2016
|
}
|
|
1605
|
-
const fuse = createSearchIndex(
|
|
1606
|
-
const searchResults = fuse.search(
|
|
2017
|
+
const fuse = createSearchIndex(result);
|
|
2018
|
+
const searchResults = fuse.search(searchQuery);
|
|
1607
2019
|
const matchedIds = new Set(searchResults.map((r) => r.item.id));
|
|
1608
2020
|
const idMatchSet = new Set(idMatches.map((i) => i.id));
|
|
1609
2021
|
for (const issue of result) {
|
|
@@ -1621,7 +2033,7 @@ function filterAndSearchIssues(issues, filters) {
|
|
|
1621
2033
|
return result;
|
|
1622
2034
|
}
|
|
1623
2035
|
// src/cli.ts
|
|
1624
|
-
|
|
2036
|
+
resolveIssyDir();
|
|
1625
2037
|
function prioritySymbol(priority) {
|
|
1626
2038
|
switch (priority) {
|
|
1627
2039
|
case "high":
|
|
@@ -1637,15 +2049,46 @@ function prioritySymbol(priority) {
|
|
|
1637
2049
|
function typeSymbol(type) {
|
|
1638
2050
|
return type === "bug" ? "\uD83D\uDC1B" : "\u2728";
|
|
1639
2051
|
}
|
|
2052
|
+
function formatIssueRow(issue) {
|
|
2053
|
+
const status = issue.frontmatter.status === "open" ? "OPEN " : "CLOSED";
|
|
2054
|
+
return ` ${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${status} ${issue.frontmatter.title.slice(0, 45)}`;
|
|
2055
|
+
}
|
|
2056
|
+
async function resolvePosition(opts) {
|
|
2057
|
+
const openIssues = await getOpenIssuesByOrder();
|
|
2058
|
+
const relevantIssues = opts.excludeId ? openIssues.filter((i) => i.id !== opts.excludeId?.padStart(4, "0")) : openIssues;
|
|
2059
|
+
const positionFlags = [opts.before, opts.after, opts.first, opts.last].filter(Boolean).length;
|
|
2060
|
+
if (positionFlags > 1) {
|
|
2061
|
+
throw new Error("Only one of --before, --after, --first, or --last can be specified.");
|
|
2062
|
+
}
|
|
2063
|
+
const hasPosition = opts.before || opts.after || opts.first || opts.last;
|
|
2064
|
+
if (relevantIssues.length > 0 && opts.requireIfOpenIssues && !hasPosition) {
|
|
2065
|
+
const ids = relevantIssues.map((i) => `#${i.id}`).join(", ");
|
|
2066
|
+
throw new Error(`A position flag (--before, --after, --first, or --last) is required when there are open issues. Open issues: ${ids}`);
|
|
2067
|
+
}
|
|
2068
|
+
return computeOrderKey(openIssues, {
|
|
2069
|
+
before: opts.before,
|
|
2070
|
+
after: opts.after,
|
|
2071
|
+
first: opts.first,
|
|
2072
|
+
last: opts.last
|
|
2073
|
+
}, opts.excludeId);
|
|
2074
|
+
}
|
|
1640
2075
|
async function listIssues(options) {
|
|
1641
2076
|
const allIssues = await getAllIssues();
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2077
|
+
const queryParts = [];
|
|
2078
|
+
if (!options.all)
|
|
2079
|
+
queryParts.push("is:open");
|
|
2080
|
+
if (options.priority)
|
|
2081
|
+
queryParts.push(`priority:${options.priority}`);
|
|
2082
|
+
if (options.scope)
|
|
2083
|
+
queryParts.push(`scope:${options.scope}`);
|
|
2084
|
+
if (options.type)
|
|
2085
|
+
queryParts.push(`type:${options.type}`);
|
|
2086
|
+
if (options.sort)
|
|
2087
|
+
queryParts.push(`sort:${options.sort}`);
|
|
2088
|
+
if (options.search)
|
|
2089
|
+
queryParts.push(options.search);
|
|
2090
|
+
const query = queryParts.join(" ") || "is:open";
|
|
2091
|
+
const issues = filterByQuery(allIssues, query);
|
|
1649
2092
|
if (issues.length === 0) {
|
|
1650
2093
|
console.log("No issues found.");
|
|
1651
2094
|
return;
|
|
@@ -1654,8 +2097,7 @@ async function listIssues(options) {
|
|
|
1654
2097
|
ID Pri Type Status Title`);
|
|
1655
2098
|
console.log(` ${"-".repeat(70)}`);
|
|
1656
2099
|
for (const issue of issues) {
|
|
1657
|
-
|
|
1658
|
-
console.log(` ${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${status} ${issue.frontmatter.title.slice(0, 45)}`);
|
|
2100
|
+
console.log(formatIssueRow(issue));
|
|
1659
2101
|
}
|
|
1660
2102
|
console.log(`
|
|
1661
2103
|
Total: ${issues.length} issue(s)
|
|
@@ -1681,6 +2123,9 @@ ${"=".repeat(70)}`);
|
|
|
1681
2123
|
if (issue.frontmatter.labels) {
|
|
1682
2124
|
console.log(` Labels: ${issue.frontmatter.labels}`);
|
|
1683
2125
|
}
|
|
2126
|
+
if (issue.frontmatter.order) {
|
|
2127
|
+
console.log(` Order: ${issue.frontmatter.order}`);
|
|
2128
|
+
}
|
|
1684
2129
|
console.log(` Created: ${issue.frontmatter.created}`);
|
|
1685
2130
|
if (issue.frontmatter.updated) {
|
|
1686
2131
|
console.log(` Updated: ${issue.frontmatter.updated}`);
|
|
@@ -1691,10 +2136,8 @@ ${"=".repeat(70)}`);
|
|
|
1691
2136
|
}
|
|
1692
2137
|
async function searchIssuesCommand(query, options) {
|
|
1693
2138
|
const allIssues = await getAllIssues();
|
|
1694
|
-
const
|
|
1695
|
-
|
|
1696
|
-
search: query
|
|
1697
|
-
});
|
|
2139
|
+
const searchQuery = options.all ? query : `is:open ${query}`;
|
|
2140
|
+
const issues = filterByQuery(allIssues, searchQuery);
|
|
1698
2141
|
if (issues.length === 0) {
|
|
1699
2142
|
console.log(`No issues found matching "${query}".`);
|
|
1700
2143
|
return;
|
|
@@ -1705,8 +2148,7 @@ async function searchIssuesCommand(query, options) {
|
|
|
1705
2148
|
ID Pri Type Status Title`);
|
|
1706
2149
|
console.log(` ${"-".repeat(70)}`);
|
|
1707
2150
|
for (const issue of issues) {
|
|
1708
|
-
|
|
1709
|
-
console.log(` ${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${status} ${issue.frontmatter.title.slice(0, 45)}`);
|
|
2151
|
+
console.log(formatIssueRow(issue));
|
|
1710
2152
|
}
|
|
1711
2153
|
console.log(`
|
|
1712
2154
|
Found: ${issues.length} issue(s)
|
|
@@ -1746,13 +2188,21 @@ Create New Issue`);
|
|
|
1746
2188
|
process.exit(1);
|
|
1747
2189
|
}
|
|
1748
2190
|
try {
|
|
2191
|
+
const order = await resolvePosition({
|
|
2192
|
+
before: options.before,
|
|
2193
|
+
after: options.after,
|
|
2194
|
+
first: options.first,
|
|
2195
|
+
last: options.last,
|
|
2196
|
+
requireIfOpenIssues: true
|
|
2197
|
+
});
|
|
1749
2198
|
const input = {
|
|
1750
2199
|
title: options.title,
|
|
1751
2200
|
description: options.description,
|
|
1752
2201
|
priority: options.priority,
|
|
1753
2202
|
scope: options.scope,
|
|
1754
2203
|
type: options.type,
|
|
1755
|
-
labels: options.labels
|
|
2204
|
+
labels: options.labels,
|
|
2205
|
+
order
|
|
1756
2206
|
};
|
|
1757
2207
|
const issue = await createIssue(input);
|
|
1758
2208
|
console.log(`
|
|
@@ -1764,6 +2214,17 @@ Created issue: ${issue.filename}`);
|
|
|
1764
2214
|
}
|
|
1765
2215
|
async function updateIssueCommand(id, options) {
|
|
1766
2216
|
try {
|
|
2217
|
+
let order;
|
|
2218
|
+
if (options.before || options.after || options.first || options.last) {
|
|
2219
|
+
order = await resolvePosition({
|
|
2220
|
+
before: options.before,
|
|
2221
|
+
after: options.after,
|
|
2222
|
+
first: options.first,
|
|
2223
|
+
last: options.last,
|
|
2224
|
+
requireIfOpenIssues: false,
|
|
2225
|
+
excludeId: id
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
1767
2228
|
const issue = await updateIssue(id, {
|
|
1768
2229
|
title: options.title,
|
|
1769
2230
|
description: options.description,
|
|
@@ -1771,7 +2232,7 @@ async function updateIssueCommand(id, options) {
|
|
|
1771
2232
|
scope: options.scope,
|
|
1772
2233
|
type: options.type,
|
|
1773
2234
|
labels: options.labels,
|
|
1774
|
-
|
|
2235
|
+
order
|
|
1775
2236
|
});
|
|
1776
2237
|
console.log(`Updated issue: ${issue.filename}`);
|
|
1777
2238
|
} catch (e) {
|
|
@@ -1783,11 +2244,49 @@ async function closeIssueCommand(id) {
|
|
|
1783
2244
|
try {
|
|
1784
2245
|
await closeIssue(id);
|
|
1785
2246
|
console.log("Issue closed.");
|
|
2247
|
+
const onCloseContent = await getOnCloseContent();
|
|
2248
|
+
if (onCloseContent) {
|
|
2249
|
+
console.log(`
|
|
2250
|
+
${onCloseContent.trim()}
|
|
2251
|
+
`);
|
|
2252
|
+
}
|
|
1786
2253
|
} catch (e) {
|
|
1787
2254
|
console.error(e instanceof Error ? e.message : "Failed to close issue");
|
|
1788
2255
|
process.exit(1);
|
|
1789
2256
|
}
|
|
1790
2257
|
}
|
|
2258
|
+
async function reopenIssueCommand(id, options) {
|
|
2259
|
+
try {
|
|
2260
|
+
const order = await resolvePosition({
|
|
2261
|
+
before: options.before,
|
|
2262
|
+
after: options.after,
|
|
2263
|
+
first: options.first,
|
|
2264
|
+
last: options.last,
|
|
2265
|
+
requireIfOpenIssues: true,
|
|
2266
|
+
excludeId: id
|
|
2267
|
+
});
|
|
2268
|
+
await reopenIssue(id, order);
|
|
2269
|
+
console.log("Issue reopened.");
|
|
2270
|
+
} catch (e) {
|
|
2271
|
+
console.error(e instanceof Error ? e.message : "Failed to reopen issue");
|
|
2272
|
+
process.exit(1);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
async function nextIssueCommand() {
|
|
2276
|
+
const issue = await getNextIssue();
|
|
2277
|
+
if (!issue) {
|
|
2278
|
+
console.log("No open issues.");
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
console.log(`
|
|
2282
|
+
Next issue:`);
|
|
2283
|
+
console.log(` ${"-".repeat(60)}`);
|
|
2284
|
+
console.log(` #${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${issue.frontmatter.title}`);
|
|
2285
|
+
if (issue.frontmatter.description !== issue.frontmatter.title) {
|
|
2286
|
+
console.log(` ${issue.frontmatter.description}`);
|
|
2287
|
+
}
|
|
2288
|
+
console.log();
|
|
2289
|
+
}
|
|
1791
2290
|
async function main() {
|
|
1792
2291
|
const args = process.argv.slice(2);
|
|
1793
2292
|
const command = args[0];
|
|
@@ -1802,25 +2301,32 @@ Options:
|
|
|
1802
2301
|
--version, -v Show version number
|
|
1803
2302
|
|
|
1804
2303
|
Commands:
|
|
1805
|
-
list List all open issues
|
|
2304
|
+
list List all open issues (roadmap order)
|
|
1806
2305
|
--all, -a Include closed issues
|
|
1807
2306
|
--priority, -p <p> Filter by priority (high, medium, low)
|
|
1808
2307
|
--scope <s> Filter by scope (small, medium, large)
|
|
1809
2308
|
--type, -t <t> Filter by type (bug, improvement)
|
|
1810
2309
|
--search, -s <q> Fuzzy search issues
|
|
2310
|
+
--sort <s> Sort: roadmap (default), priority, created, updated, id
|
|
1811
2311
|
|
|
1812
2312
|
search <query> Fuzzy search issues
|
|
1813
2313
|
--all, -a Include closed issues
|
|
1814
2314
|
|
|
1815
2315
|
read <id> Read a specific issue
|
|
1816
2316
|
|
|
1817
|
-
|
|
2317
|
+
next Show the next issue to work on
|
|
2318
|
+
|
|
2319
|
+
create Create a new issue
|
|
1818
2320
|
--title, -t <t> Issue title
|
|
1819
2321
|
--description, -d <d> Short description
|
|
1820
2322
|
--priority, -p <p> Priority (high, medium, low)
|
|
1821
2323
|
--scope <s> Scope (small, medium, large)
|
|
1822
2324
|
--type <t> Type (bug, improvement)
|
|
1823
2325
|
--labels, -l <l> Comma-separated labels
|
|
2326
|
+
--before <id> Insert before this issue in roadmap
|
|
2327
|
+
--after <id> Insert after this issue in roadmap
|
|
2328
|
+
--first Insert at the beginning of the roadmap
|
|
2329
|
+
--last Insert at the end of the roadmap
|
|
1824
2330
|
|
|
1825
2331
|
update <id> Update an issue
|
|
1826
2332
|
--title, -t <t> New title
|
|
@@ -1829,20 +2335,30 @@ Commands:
|
|
|
1829
2335
|
--scope <s> New scope
|
|
1830
2336
|
--type <t> New type
|
|
1831
2337
|
--labels, -l <l> New labels
|
|
1832
|
-
--
|
|
2338
|
+
--before <id> Move before this issue in roadmap
|
|
2339
|
+
--after <id> Move after this issue in roadmap
|
|
2340
|
+
--first Move to the beginning of the roadmap
|
|
2341
|
+
--last Move to the end of the roadmap
|
|
1833
2342
|
|
|
1834
2343
|
close <id> Close an issue
|
|
1835
2344
|
|
|
2345
|
+
reopen <id> Reopen a closed issue
|
|
2346
|
+
--before <id> Insert before this issue in roadmap
|
|
2347
|
+
--after <id> Insert after this issue in roadmap
|
|
2348
|
+
--first Insert at the beginning of the roadmap
|
|
2349
|
+
--last Insert at the end of the roadmap
|
|
2350
|
+
|
|
1836
2351
|
Examples:
|
|
1837
2352
|
issy list
|
|
1838
2353
|
issy list --priority high --type bug
|
|
1839
|
-
issy
|
|
1840
|
-
issy search "dashboard"
|
|
1841
|
-
issy search "k8s" --all
|
|
2354
|
+
issy next
|
|
1842
2355
|
issy read 0001
|
|
1843
|
-
issy create --title "Fix login bug" --type bug --priority high --
|
|
1844
|
-
issy
|
|
2356
|
+
issy create --title "Fix login bug" --type bug --priority high --after 0002
|
|
2357
|
+
issy create --title "Add dark mode" --last
|
|
2358
|
+
issy create --title "Urgent fix" --first
|
|
2359
|
+
issy update 0001 --priority low --after 0003
|
|
1845
2360
|
issy close 0001
|
|
2361
|
+
issy reopen 0001 --last
|
|
1846
2362
|
`);
|
|
1847
2363
|
return;
|
|
1848
2364
|
}
|
|
@@ -1855,7 +2371,8 @@ Examples:
|
|
|
1855
2371
|
priority: { type: "string", short: "p" },
|
|
1856
2372
|
scope: { type: "string" },
|
|
1857
2373
|
type: { type: "string", short: "t" },
|
|
1858
|
-
search: { type: "string", short: "s" }
|
|
2374
|
+
search: { type: "string", short: "s" },
|
|
2375
|
+
sort: { type: "string" }
|
|
1859
2376
|
},
|
|
1860
2377
|
allowPositionals: true
|
|
1861
2378
|
});
|
|
@@ -1887,6 +2404,10 @@ Examples:
|
|
|
1887
2404
|
await readIssue(id);
|
|
1888
2405
|
break;
|
|
1889
2406
|
}
|
|
2407
|
+
case "next": {
|
|
2408
|
+
await nextIssueCommand();
|
|
2409
|
+
break;
|
|
2410
|
+
}
|
|
1890
2411
|
case "create": {
|
|
1891
2412
|
const { values } = parseArgs({
|
|
1892
2413
|
args: args.slice(1),
|
|
@@ -1896,7 +2417,11 @@ Examples:
|
|
|
1896
2417
|
priority: { type: "string", short: "p" },
|
|
1897
2418
|
scope: { type: "string" },
|
|
1898
2419
|
type: { type: "string" },
|
|
1899
|
-
labels: { type: "string", short: "l" }
|
|
2420
|
+
labels: { type: "string", short: "l" },
|
|
2421
|
+
before: { type: "string" },
|
|
2422
|
+
after: { type: "string" },
|
|
2423
|
+
first: { type: "boolean" },
|
|
2424
|
+
last: { type: "boolean" }
|
|
1900
2425
|
},
|
|
1901
2426
|
allowPositionals: true
|
|
1902
2427
|
});
|
|
@@ -1918,7 +2443,10 @@ Examples:
|
|
|
1918
2443
|
scope: { type: "string" },
|
|
1919
2444
|
type: { type: "string" },
|
|
1920
2445
|
labels: { type: "string", short: "l" },
|
|
1921
|
-
|
|
2446
|
+
before: { type: "string" },
|
|
2447
|
+
after: { type: "string" },
|
|
2448
|
+
first: { type: "boolean" },
|
|
2449
|
+
last: { type: "boolean" }
|
|
1922
2450
|
},
|
|
1923
2451
|
allowPositionals: true
|
|
1924
2452
|
});
|
|
@@ -1934,6 +2462,25 @@ Examples:
|
|
|
1934
2462
|
await closeIssueCommand(id);
|
|
1935
2463
|
break;
|
|
1936
2464
|
}
|
|
2465
|
+
case "reopen": {
|
|
2466
|
+
const id = args[1];
|
|
2467
|
+
if (!id) {
|
|
2468
|
+
console.error("Usage: issy reopen <id>");
|
|
2469
|
+
process.exit(1);
|
|
2470
|
+
}
|
|
2471
|
+
const { values } = parseArgs({
|
|
2472
|
+
args: args.slice(2),
|
|
2473
|
+
options: {
|
|
2474
|
+
before: { type: "string" },
|
|
2475
|
+
after: { type: "string" },
|
|
2476
|
+
first: { type: "boolean" },
|
|
2477
|
+
last: { type: "boolean" }
|
|
2478
|
+
},
|
|
2479
|
+
allowPositionals: true
|
|
2480
|
+
});
|
|
2481
|
+
await reopenIssueCommand(id, values);
|
|
2482
|
+
break;
|
|
2483
|
+
}
|
|
1937
2484
|
default:
|
|
1938
2485
|
console.error(`Unknown command: ${command}`);
|
|
1939
2486
|
console.log('Run "issy help" for usage.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "issy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "AI-native issue tracking. Markdown files in .issues/, managed by your coding assistant.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"lint": "biome check src bin"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@miketromba/issy-app": "
|
|
39
|
-
"@miketromba/issy-core": "
|
|
38
|
+
"@miketromba/issy-app": "workspace:^",
|
|
39
|
+
"@miketromba/issy-core": "workspace:^",
|
|
40
40
|
"update-notifier": "^7.3.1"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/dist/install-info.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// src/install-info.ts
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
var CONFIG_DIR = join(homedir(), ".config", "issy");
|
|
6
|
-
var INFO_FILE = join(CONFIG_DIR, "install-info.json");
|
|
7
|
-
function saveInstallInfo(info) {
|
|
8
|
-
try {
|
|
9
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
10
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
11
|
-
}
|
|
12
|
-
writeFileSync(INFO_FILE, JSON.stringify(info, null, 2));
|
|
13
|
-
} catch {}
|
|
14
|
-
}
|
|
15
|
-
function loadInstallInfo() {
|
|
16
|
-
try {
|
|
17
|
-
if (!existsSync(INFO_FILE)) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
const data = readFileSync(INFO_FILE, "utf-8");
|
|
21
|
-
return JSON.parse(data);
|
|
22
|
-
} catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function detectPackageManagerFromEnv() {
|
|
27
|
-
const userAgent = process.env.npm_config_user_agent || "";
|
|
28
|
-
if (userAgent.includes("bun/"))
|
|
29
|
-
return "bun";
|
|
30
|
-
if (userAgent.includes("pnpm/"))
|
|
31
|
-
return "pnpm";
|
|
32
|
-
if (userAgent.includes("yarn/"))
|
|
33
|
-
return "yarn";
|
|
34
|
-
if (userAgent.includes("npm/"))
|
|
35
|
-
return "npm";
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
function detectIsGlobalFromEnv() {
|
|
39
|
-
if (process.env.npm_config_global === "true")
|
|
40
|
-
return true;
|
|
41
|
-
if (process.env.PNPM_HOME && process.env.npm_config_global !== "false")
|
|
42
|
-
return true;
|
|
43
|
-
const npmConfigPrefix = process.env.npm_config_prefix || "";
|
|
44
|
-
if (npmConfigPrefix.includes("/usr/local") || npmConfigPrefix.includes("/.bun/") || npmConfigPrefix.includes("/.nvm/") || npmConfigPrefix.includes("/pnpm/global")) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
export {
|
|
50
|
-
saveInstallInfo,
|
|
51
|
-
loadInstallInfo,
|
|
52
|
-
detectPackageManagerFromEnv,
|
|
53
|
-
detectIsGlobalFromEnv
|
|
54
|
-
};
|
package/dist/postinstall.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/install-info.ts
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
var CONFIG_DIR = join(homedir(), ".config", "issy");
|
|
8
|
-
var INFO_FILE = join(CONFIG_DIR, "install-info.json");
|
|
9
|
-
function saveInstallInfo(info) {
|
|
10
|
-
try {
|
|
11
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
12
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
13
|
-
}
|
|
14
|
-
writeFileSync(INFO_FILE, JSON.stringify(info, null, 2));
|
|
15
|
-
} catch {}
|
|
16
|
-
}
|
|
17
|
-
function loadInstallInfo() {
|
|
18
|
-
try {
|
|
19
|
-
if (!existsSync(INFO_FILE)) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
const data = readFileSync(INFO_FILE, "utf-8");
|
|
23
|
-
return JSON.parse(data);
|
|
24
|
-
} catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function detectPackageManagerFromEnv() {
|
|
29
|
-
const userAgent = process.env.npm_config_user_agent || "";
|
|
30
|
-
if (userAgent.includes("bun/"))
|
|
31
|
-
return "bun";
|
|
32
|
-
if (userAgent.includes("pnpm/"))
|
|
33
|
-
return "pnpm";
|
|
34
|
-
if (userAgent.includes("yarn/"))
|
|
35
|
-
return "yarn";
|
|
36
|
-
if (userAgent.includes("npm/"))
|
|
37
|
-
return "npm";
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
function detectIsGlobalFromEnv() {
|
|
41
|
-
if (process.env.npm_config_global === "true")
|
|
42
|
-
return true;
|
|
43
|
-
if (process.env.PNPM_HOME && process.env.npm_config_global !== "false")
|
|
44
|
-
return true;
|
|
45
|
-
const npmConfigPrefix = process.env.npm_config_prefix || "";
|
|
46
|
-
if (npmConfigPrefix.includes("/usr/local") || npmConfigPrefix.includes("/.bun/") || npmConfigPrefix.includes("/.nvm/") || npmConfigPrefix.includes("/pnpm/global")) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// src/postinstall.ts
|
|
53
|
-
var pm = detectPackageManagerFromEnv();
|
|
54
|
-
var isGlobal = detectIsGlobalFromEnv();
|
|
55
|
-
if (pm) {
|
|
56
|
-
saveInstallInfo({
|
|
57
|
-
packageManager: pm,
|
|
58
|
-
isGlobal,
|
|
59
|
-
installedAt: new Date().toISOString()
|
|
60
|
-
});
|
|
61
|
-
}
|