issy 0.5.2 → 0.5.4
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 +16 -293
- package/dist/cli.js +56 -5
- package/dist/main.js +2220 -0
- package/dist/update-checker.js +3 -0
- package/package.json +4 -4
package/bin/issy
CHANGED
|
@@ -1,313 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
existsSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
renameSync,
|
|
10
|
-
cpSync
|
|
11
|
-
} from 'node:fs'
|
|
12
|
-
import { join, resolve } from 'node:path'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
13
4
|
import { fileURLToPath } from 'node:url'
|
|
14
5
|
import updateNotifier from 'update-notifier'
|
|
15
6
|
import {
|
|
16
7
|
detectPackageManager,
|
|
17
8
|
isGlobalInstall,
|
|
18
|
-
getUpdateCommand
|
|
9
|
+
getUpdateCommand,
|
|
19
10
|
} from '../dist/update-checker.js'
|
|
20
11
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const pkgPath = resolve(here, '..', 'package.json')
|
|
26
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const pkg = JSON.parse(
|
|
14
|
+
readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'),
|
|
15
|
+
)
|
|
27
16
|
|
|
28
17
|
const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 })
|
|
29
|
-
|
|
30
18
|
if (notifier.update) {
|
|
31
|
-
const scriptPath =
|
|
19
|
+
const scriptPath = fileURLToPath(import.meta.url)
|
|
32
20
|
const pm = detectPackageManager(scriptPath)
|
|
33
21
|
const isGlobal = isGlobalInstall(scriptPath)
|
|
34
22
|
const updateCmd = getUpdateCommand(pm, isGlobal)
|
|
35
|
-
|
|
36
23
|
notifier.notify({
|
|
37
24
|
message: `Update available: ${notifier.update.current} → ${notifier.update.latest}\nRun: ${updateCmd}`,
|
|
38
25
|
defer: true,
|
|
39
|
-
boxenOptions: {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'search',
|
|
46
|
-
'read',
|
|
47
|
-
'create',
|
|
48
|
-
'update',
|
|
49
|
-
'close',
|
|
50
|
-
'reopen',
|
|
51
|
-
'next',
|
|
52
|
-
'help',
|
|
53
|
-
'--help',
|
|
54
|
-
'-h'
|
|
55
|
-
])
|
|
56
|
-
|
|
57
|
-
// Handle --version / -v flag
|
|
58
|
-
if (args[0] === '--version' || args[0] === '-v') {
|
|
59
|
-
console.log(`issy v${pkg.version}`)
|
|
60
|
-
process.exit(0)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Find .issy directory by walking up from the given path.
|
|
65
|
-
*/
|
|
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) {
|
|
84
|
-
let current = resolve(fromPath)
|
|
85
|
-
for (let i = 0; i < 20; i++) {
|
|
86
|
-
const candidate = join(current, '.issues')
|
|
87
|
-
if (existsSync(candidate)) {
|
|
88
|
-
return candidate
|
|
89
|
-
}
|
|
90
|
-
const parent = dirname(current)
|
|
91
|
-
if (parent === current) break
|
|
92
|
-
current = parent
|
|
93
|
-
}
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Find the git repository root by walking up from the given path.
|
|
99
|
-
*/
|
|
100
|
-
function findGitRoot(fromPath) {
|
|
101
|
-
let current = resolve(fromPath)
|
|
102
|
-
for (let i = 0; i < 20; i++) {
|
|
103
|
-
const gitDir = join(current, '.git')
|
|
104
|
-
if (existsSync(gitDir)) {
|
|
105
|
-
return current
|
|
106
|
-
}
|
|
107
|
-
const parent = dirname(current)
|
|
108
|
-
if (parent === current) break
|
|
109
|
-
current = parent
|
|
110
|
-
}
|
|
111
|
-
return null
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
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
|
|
120
|
-
*/
|
|
121
|
-
function resolveIssyDir() {
|
|
122
|
-
if (process.env.ISSY_DIR) {
|
|
123
|
-
return resolve(process.env.ISSY_DIR)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const startDir = process.env.ISSY_ROOT || process.cwd()
|
|
127
|
-
const found = findIssyDirUpward(startDir)
|
|
128
|
-
if (found) {
|
|
129
|
-
return found
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const gitRoot = findGitRoot(startDir)
|
|
133
|
-
if (gitRoot) {
|
|
134
|
-
return join(gitRoot, '.issy')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return join(resolve(startDir), '.issy')
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const issyDir = resolveIssyDir()
|
|
141
|
-
const issuesDir = join(issyDir, 'issues')
|
|
142
|
-
|
|
143
|
-
// Set env vars for downstream consumers
|
|
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])
|
|
26
|
+
boxenOptions: {
|
|
27
|
+
padding: 1,
|
|
28
|
+
margin: 1,
|
|
29
|
+
borderStyle: 'round',
|
|
30
|
+
borderColor: 'yellow',
|
|
31
|
+
},
|
|
217
32
|
})
|
|
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
|
-
}
|
|
256
|
-
|
|
257
|
-
if (cliCommands.has(args[0] || '')) {
|
|
258
|
-
const here = resolve(fileURLToPath(import.meta.url), '..')
|
|
259
|
-
const entry = resolve(here, '..', 'dist', 'cli.js')
|
|
260
|
-
process.argv = [process.argv[0], process.argv[1], ...args]
|
|
261
|
-
const cli = await import(entry)
|
|
262
|
-
await cli.ready
|
|
263
|
-
process.exit(0)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const portIdx = args.findIndex(arg => arg === '--port' || arg === '-p')
|
|
267
|
-
if (portIdx >= 0 && args[portIdx + 1]) {
|
|
268
|
-
process.env.ISSUES_PORT = args[portIdx + 1]
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const shouldInitOnly = args.includes('init')
|
|
272
|
-
const shouldSeed = args.includes('--seed')
|
|
273
|
-
|
|
274
|
-
if (shouldInitOnly) {
|
|
275
|
-
if (!existsSync(issuesDir)) {
|
|
276
|
-
mkdirSync(issuesDir, { recursive: true })
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (shouldSeed) {
|
|
280
|
-
const hasIssues =
|
|
281
|
-
existsSync(issuesDir) &&
|
|
282
|
-
readdirSync(issuesDir).some(f => f.endsWith('.md'))
|
|
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
|
-
|
|
291
|
-
const welcome =
|
|
292
|
-
`---\n` +
|
|
293
|
-
`title: Welcome to issy\n` +
|
|
294
|
-
`description: Your first issue in this repo\n` +
|
|
295
|
-
`priority: medium\n` +
|
|
296
|
-
`type: improvement\n` +
|
|
297
|
-
`status: open\n` +
|
|
298
|
-
`order: ${firstOrderKey}\n` +
|
|
299
|
-
`created: ${new Date().toISOString().slice(0, 19)}\n` +
|
|
300
|
-
`---\n\n` +
|
|
301
|
-
`## Details\n\n` +
|
|
302
|
-
`- This issue was created automatically on first run.\n` +
|
|
303
|
-
`- Edit it, close it, or delete it to get started.\n`
|
|
304
|
-
writeFileSync(join(issuesDir, '0001-welcome-to-issy.md'), welcome)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
console.log(`Initialized ${issyDir}`)
|
|
309
|
-
process.exit(0)
|
|
310
33
|
}
|
|
311
34
|
|
|
312
|
-
|
|
313
|
-
await import('
|
|
35
|
+
process.env.ISSY_PKG_VERSION = pkg.version
|
|
36
|
+
await import('../dist/main.js')
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { parseArgs } from "util";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
6
|
// ../core/src/lib/issues.ts
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -183,6 +183,40 @@ function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
|
|
|
183
183
|
}
|
|
184
184
|
return ia + midpoint(fa, null, digits);
|
|
185
185
|
}
|
|
186
|
+
function generateNKeysBetween(a, b, n, digits = BASE_62_DIGITS) {
|
|
187
|
+
if (n === 0) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
if (n === 1) {
|
|
191
|
+
return [generateKeyBetween(a, b, digits)];
|
|
192
|
+
}
|
|
193
|
+
if (b == null) {
|
|
194
|
+
let c2 = generateKeyBetween(a, b, digits);
|
|
195
|
+
const result = [c2];
|
|
196
|
+
for (let i = 0;i < n - 1; i++) {
|
|
197
|
+
c2 = generateKeyBetween(c2, b, digits);
|
|
198
|
+
result.push(c2);
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
if (a == null) {
|
|
203
|
+
let c2 = generateKeyBetween(a, b, digits);
|
|
204
|
+
const result = [c2];
|
|
205
|
+
for (let i = 0;i < n - 1; i++) {
|
|
206
|
+
c2 = generateKeyBetween(a, c2, digits);
|
|
207
|
+
result.push(c2);
|
|
208
|
+
}
|
|
209
|
+
result.reverse();
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
const mid = Math.floor(n / 2);
|
|
213
|
+
const c = generateKeyBetween(a, b, digits);
|
|
214
|
+
return [
|
|
215
|
+
...generateNKeysBetween(a, c, mid, digits),
|
|
216
|
+
c,
|
|
217
|
+
...generateNKeysBetween(c, b, n - mid - 1, digits)
|
|
218
|
+
];
|
|
219
|
+
}
|
|
186
220
|
|
|
187
221
|
// ../core/src/lib/issues.ts
|
|
188
222
|
var issyDir = null;
|
|
@@ -220,6 +254,20 @@ function findIssyDirUpward(fromPath) {
|
|
|
220
254
|
}
|
|
221
255
|
return null;
|
|
222
256
|
}
|
|
257
|
+
function findLegacyIssuesDirUpward(fromPath) {
|
|
258
|
+
let current = resolve(fromPath);
|
|
259
|
+
for (let i = 0;i < 20; i++) {
|
|
260
|
+
const candidate = join(current, ".issues");
|
|
261
|
+
if (existsSync(candidate)) {
|
|
262
|
+
return candidate;
|
|
263
|
+
}
|
|
264
|
+
const parent = dirname(current);
|
|
265
|
+
if (parent === current)
|
|
266
|
+
break;
|
|
267
|
+
current = parent;
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
223
271
|
function findGitRoot(fromPath) {
|
|
224
272
|
let current = resolve(fromPath);
|
|
225
273
|
for (let i = 0;i < 20; i++) {
|
|
@@ -408,6 +456,9 @@ function computeOrderKey(openIssues, options, excludeId) {
|
|
|
408
456
|
const lastOrder = issues[issues.length - 1].frontmatter.order || null;
|
|
409
457
|
return generateKeyBetween(lastOrder, null);
|
|
410
458
|
}
|
|
459
|
+
function generateBatchOrderKeys(count) {
|
|
460
|
+
return generateNKeysBetween(null, null, count);
|
|
461
|
+
}
|
|
411
462
|
async function createIssue(input) {
|
|
412
463
|
await ensureIssuesDir();
|
|
413
464
|
if (!input.title) {
|
|
@@ -2043,11 +2094,11 @@ function prioritySymbol(priority) {
|
|
|
2043
2094
|
case "low":
|
|
2044
2095
|
return "\uD83D\uDFE2";
|
|
2045
2096
|
default:
|
|
2046
|
-
return "
|
|
2097
|
+
return "⚪";
|
|
2047
2098
|
}
|
|
2048
2099
|
}
|
|
2049
2100
|
function typeSymbol(type) {
|
|
2050
|
-
return type === "bug" ? "\uD83D\uDC1B" : "
|
|
2101
|
+
return type === "bug" ? "\uD83D\uDC1B" : "✨";
|
|
2051
2102
|
}
|
|
2052
2103
|
function formatIssueRow(issue) {
|
|
2053
2104
|
const status = issue.frontmatter.status === "open" ? "OPEN " : "CLOSED";
|