issy 0.4.0 → 0.5.2
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/README.md +0 -52
- 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
|
|