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 CHANGED
@@ -1,313 +1,36 @@
1
1
  #!/usr/bin/env node
2
- import { dirname } from 'node:path'
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 args = process.argv.slice(2)
22
-
23
- // Check for updates (non-blocking, cached, shows on next run)
24
- const here = resolve(fileURLToPath(import.meta.url), '..')
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 = resolve(fileURLToPath(import.meta.url))
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: { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow' }
40
- })
41
- }
42
-
43
- const cliCommands = new Set([
44
- 'list',
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
- // Start the server
313
- await import('@miketromba/issy-app')
35
+ process.env.ISSY_PKG_VERSION = pkg.version
36
+ await import('../dist/main.js')
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env bun
2
- // @bun
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 "\u26AA";
2097
+ return "";
2047
2098
  }
2048
2099
  }
2049
2100
  function typeSymbol(type) {
2050
- return type === "bug" ? "\uD83D\uDC1B" : "\u2728";
2101
+ return type === "bug" ? "\uD83D\uDC1B" : "";
2051
2102
  }
2052
2103
  function formatIssueRow(issue) {
2053
2104
  const status = issue.frontmatter.status === "open" ? "OPEN " : "CLOSED";