issy 0.1.8 → 0.2.0
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/README.md +6 -0
- package/bin/issy +150 -98
- package/dist/cli.js +3 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@ Opens a local UI at `http://localhost:1554`.
|
|
|
25
25
|
### CLI
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
+
issy init # Create .issues/ directory
|
|
29
|
+
issy init --seed # Create with a welcome issue
|
|
28
30
|
issy list # List open issues
|
|
29
31
|
issy search "auth" # Fuzzy search
|
|
30
32
|
issy read 0001 # View issue
|
|
@@ -34,6 +36,10 @@ issy close 0001 # Close issue
|
|
|
34
36
|
|
|
35
37
|
Run `issy help` for full options.
|
|
36
38
|
|
|
39
|
+
### Monorepo Support
|
|
40
|
+
|
|
41
|
+
issy walks up from the current directory to find an existing `.issues/` folder, so you can run it from any subdirectory.
|
|
42
|
+
|
|
37
43
|
## Configuration
|
|
38
44
|
|
|
39
45
|
| Variable | Description | Default |
|
package/bin/issy
CHANGED
|
@@ -1,38 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { dirname } from
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync } from
|
|
4
|
-
import { join, resolve } from
|
|
5
|
-
import { fileURLToPath } from
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
|
-
const args = process.argv.slice(2)
|
|
7
|
+
const args = process.argv.slice(2)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check for newer version on npm and warn if outdated.
|
|
11
|
+
* Runs async and doesn't block startup.
|
|
12
|
+
*/
|
|
13
|
+
async function checkForUpdates() {
|
|
14
|
+
try {
|
|
15
|
+
const here = resolve(fileURLToPath(import.meta.url), '..')
|
|
16
|
+
const pkgPath = resolve(here, '..', 'package.json')
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
18
|
+
const currentVersion = pkg.version
|
|
19
|
+
|
|
20
|
+
const controller = new AbortController()
|
|
21
|
+
const timeout = setTimeout(() => controller.abort(), 2000)
|
|
22
|
+
|
|
23
|
+
const res = await fetch('https://registry.npmjs.org/issy/latest', {
|
|
24
|
+
signal: controller.signal
|
|
25
|
+
})
|
|
26
|
+
clearTimeout(timeout)
|
|
27
|
+
|
|
28
|
+
if (!res.ok) return
|
|
29
|
+
|
|
30
|
+
const data = await res.json()
|
|
31
|
+
const latestVersion = data.version
|
|
32
|
+
|
|
33
|
+
if (currentVersion !== latestVersion) {
|
|
34
|
+
console.error(`\x1b[33m⚠ You're running issy v${currentVersion}, but v${latestVersion} is available.\x1b[0m`)
|
|
35
|
+
console.error(`\x1b[33m Run: npx issy@latest\x1b[0m\n`)
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Silently ignore - network issues, timeouts, etc.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fire off update check (non-blocking)
|
|
43
|
+
const updateCheck = checkForUpdates()
|
|
8
44
|
const cliCommands = new Set([
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
])
|
|
45
|
+
'list',
|
|
46
|
+
'search',
|
|
47
|
+
'read',
|
|
48
|
+
'create',
|
|
49
|
+
'update',
|
|
50
|
+
'close',
|
|
51
|
+
'help',
|
|
52
|
+
'--help',
|
|
53
|
+
'-h'
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
// Handle --version / -v flag
|
|
57
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
58
|
+
const here = resolve(fileURLToPath(import.meta.url), '..')
|
|
59
|
+
const pkgPath = resolve(here, '..', 'package.json')
|
|
60
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
61
|
+
console.log(`issy v${pkg.version}`)
|
|
62
|
+
await updateCheck
|
|
63
|
+
process.exit(0)
|
|
64
|
+
}
|
|
19
65
|
|
|
20
66
|
/**
|
|
21
67
|
* Find .issues directory by walking up from the given path.
|
|
22
68
|
* Returns the path if found, or null if not found.
|
|
23
69
|
*/
|
|
24
70
|
function findIssuesDirUpward(fromPath) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
71
|
+
let current = resolve(fromPath)
|
|
72
|
+
for (let i = 0; i < 20; i++) {
|
|
73
|
+
const candidate = join(current, '.issues')
|
|
74
|
+
if (existsSync(candidate)) {
|
|
75
|
+
return candidate
|
|
76
|
+
}
|
|
77
|
+
const parent = dirname(current)
|
|
78
|
+
if (parent === current) break
|
|
79
|
+
current = parent
|
|
80
|
+
}
|
|
81
|
+
return null
|
|
36
82
|
}
|
|
37
83
|
|
|
38
84
|
/**
|
|
@@ -40,17 +86,17 @@ function findIssuesDirUpward(fromPath) {
|
|
|
40
86
|
* Returns the directory containing .git, or null if not in a git repo.
|
|
41
87
|
*/
|
|
42
88
|
function findGitRoot(fromPath) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
89
|
+
let current = resolve(fromPath)
|
|
90
|
+
for (let i = 0; i < 20; i++) {
|
|
91
|
+
const gitDir = join(current, '.git')
|
|
92
|
+
if (existsSync(gitDir)) {
|
|
93
|
+
return current
|
|
94
|
+
}
|
|
95
|
+
const parent = dirname(current)
|
|
96
|
+
if (parent === current) break
|
|
97
|
+
current = parent
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
54
100
|
}
|
|
55
101
|
|
|
56
102
|
/**
|
|
@@ -61,78 +107,84 @@ function findGitRoot(fromPath) {
|
|
|
61
107
|
* 4. Fall back to creating .issues in ISSUES_ROOT or cwd
|
|
62
108
|
*/
|
|
63
109
|
function resolveIssuesDir() {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
110
|
+
// 1. Explicit override
|
|
111
|
+
if (process.env.ISSUES_DIR) {
|
|
112
|
+
return resolve(process.env.ISSUES_DIR)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Try to find existing .issues by walking up
|
|
116
|
+
const startDir = process.env.ISSUES_ROOT || process.cwd()
|
|
117
|
+
const found = findIssuesDirUpward(startDir)
|
|
118
|
+
if (found) {
|
|
119
|
+
return found
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. If in a git repo, use .issues at the repo root
|
|
123
|
+
const gitRoot = findGitRoot(startDir)
|
|
124
|
+
if (gitRoot) {
|
|
125
|
+
return join(gitRoot, '.issues')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 4. Fall back to creating in start directory
|
|
129
|
+
return join(resolve(startDir), '.issues')
|
|
84
130
|
}
|
|
85
131
|
|
|
86
|
-
const issuesDir = resolveIssuesDir()
|
|
132
|
+
const issuesDir = resolveIssuesDir()
|
|
87
133
|
// Set env vars for downstream consumers
|
|
88
|
-
process.env.ISSUES_DIR = issuesDir
|
|
134
|
+
process.env.ISSUES_DIR = issuesDir
|
|
89
135
|
// Set ISSUES_ROOT to the parent of .issues for consistency
|
|
90
|
-
process.env.ISSUES_ROOT = dirname(issuesDir)
|
|
91
|
-
|
|
92
|
-
if (cliCommands.has(args[0] || "")) {
|
|
93
|
-
const here = resolve(fileURLToPath(import.meta.url), "..");
|
|
94
|
-
const entry = resolve(here, "..", "dist", "cli.js");
|
|
95
|
-
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
96
|
-
const cli = await import(entry);
|
|
97
|
-
await cli.ready; // Wait for main() to complete before exiting
|
|
98
|
-
process.exit(0);
|
|
99
|
-
}
|
|
136
|
+
process.env.ISSUES_ROOT = dirname(issuesDir)
|
|
100
137
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
138
|
+
if (cliCommands.has(args[0] || '')) {
|
|
139
|
+
const here = resolve(fileURLToPath(import.meta.url), '..')
|
|
140
|
+
const entry = resolve(here, '..', 'dist', 'cli.js')
|
|
141
|
+
process.argv = [process.argv[0], process.argv[1], ...args]
|
|
142
|
+
const cli = await import(entry)
|
|
143
|
+
await cli.ready // Wait for main() to complete before exiting
|
|
144
|
+
await updateCheck // Wait for version check to complete
|
|
145
|
+
process.exit(0)
|
|
104
146
|
}
|
|
105
147
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (!existsSync(issuesDir)) {
|
|
110
|
-
mkdirSync(issuesDir, { recursive: true });
|
|
148
|
+
const portIdx = args.findIndex(arg => arg === '--port' || arg === '-p')
|
|
149
|
+
if (portIdx >= 0 && args[portIdx + 1]) {
|
|
150
|
+
process.env.ISSUES_PORT = args[portIdx + 1]
|
|
111
151
|
}
|
|
112
152
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
existsSync(issuesDir) && readdirSync(issuesDir).some((f) => f.endsWith(".md"));
|
|
116
|
-
if (!hasIssues) {
|
|
117
|
-
const welcome = `---\n` +
|
|
118
|
-
`title: Welcome to issy\n` +
|
|
119
|
-
`description: Your first issue in this repo\n` +
|
|
120
|
-
`priority: medium\n` +
|
|
121
|
-
`type: improvement\n` +
|
|
122
|
-
`status: open\n` +
|
|
123
|
-
`created: ${new Date().toISOString().slice(0, 19)}\n` +
|
|
124
|
-
`---\n\n` +
|
|
125
|
-
`## Details\n\n` +
|
|
126
|
-
`- This issue was created automatically on first run.\n` +
|
|
127
|
-
`- Edit it, close it, or delete it to get started.\n`;
|
|
128
|
-
writeFileSync(join(issuesDir, "0001-welcome-to-issy.md"), welcome);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
153
|
+
const shouldInitOnly = args.includes('init')
|
|
154
|
+
const shouldSeed = args.includes('--seed')
|
|
131
155
|
|
|
156
|
+
// Only create .issues directory on explicit 'init' command
|
|
132
157
|
if (shouldInitOnly) {
|
|
133
|
-
|
|
134
|
-
|
|
158
|
+
if (!existsSync(issuesDir)) {
|
|
159
|
+
mkdirSync(issuesDir, { recursive: true })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Only seed with welcome issue if --seed flag is passed
|
|
163
|
+
if (shouldSeed) {
|
|
164
|
+
const hasIssues =
|
|
165
|
+
existsSync(issuesDir) &&
|
|
166
|
+
readdirSync(issuesDir).some(f => f.endsWith('.md'))
|
|
167
|
+
if (!hasIssues) {
|
|
168
|
+
const welcome =
|
|
169
|
+
`---\n` +
|
|
170
|
+
`title: Welcome to issy\n` +
|
|
171
|
+
`description: Your first issue in this repo\n` +
|
|
172
|
+
`priority: medium\n` +
|
|
173
|
+
`type: improvement\n` +
|
|
174
|
+
`status: open\n` +
|
|
175
|
+
`created: ${new Date().toISOString().slice(0, 19)}\n` +
|
|
176
|
+
`---\n\n` +
|
|
177
|
+
`## Details\n\n` +
|
|
178
|
+
`- This issue was created automatically on first run.\n` +
|
|
179
|
+
`- Edit it, close it, or delete it to get started.\n`
|
|
180
|
+
writeFileSync(join(issuesDir, '0001-welcome-to-issy.md'), welcome)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`Initialized ${issuesDir}`)
|
|
185
|
+
await updateCheck // Wait for version check to complete
|
|
186
|
+
process.exit(0)
|
|
135
187
|
}
|
|
136
188
|
|
|
137
189
|
// Start the server
|
|
138
|
-
await import(
|
|
190
|
+
await import('@miketromba/issy-app')
|
package/dist/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "issy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI-native issue tracking. Markdown files in .issues/, managed by your coding assistant.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"lint": "biome check src bin"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@miketromba/issy-app": "^0.
|
|
38
|
-
"@miketromba/issy-core": "^0.
|
|
37
|
+
"@miketromba/issy-app": "^0.2.0",
|
|
38
|
+
"@miketromba/issy-core": "^0.2.0"
|
|
39
39
|
}
|
|
40
40
|
}
|