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 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 "node:path";
3
- import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
4
- import { join, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
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
- "list",
10
- "search",
11
- "read",
12
- "create",
13
- "update",
14
- "close",
15
- "help",
16
- "--help",
17
- "-h",
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
- let current = resolve(fromPath);
26
- for (let i = 0; i < 20; i++) {
27
- const candidate = join(current, ".issues");
28
- if (existsSync(candidate)) {
29
- return candidate;
30
- }
31
- const parent = dirname(current);
32
- if (parent === current) break;
33
- current = parent;
34
- }
35
- return null;
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
- let current = resolve(fromPath);
44
- for (let i = 0; i < 20; i++) {
45
- const gitDir = join(current, ".git");
46
- if (existsSync(gitDir)) {
47
- return current;
48
- }
49
- const parent = dirname(current);
50
- if (parent === current) break;
51
- current = parent;
52
- }
53
- return null;
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
- // 1. Explicit override
65
- if (process.env.ISSUES_DIR) {
66
- return resolve(process.env.ISSUES_DIR);
67
- }
68
-
69
- // 2. Try to find existing .issues by walking up
70
- const startDir = process.env.ISSUES_ROOT || process.cwd();
71
- const found = findIssuesDirUpward(startDir);
72
- if (found) {
73
- return found;
74
- }
75
-
76
- // 3. If in a git repo, use .issues at the repo root
77
- const gitRoot = findGitRoot(startDir);
78
- if (gitRoot) {
79
- return join(gitRoot, ".issues");
80
- }
81
-
82
- // 4. Fall back to creating in start directory
83
- return join(resolve(startDir), ".issues");
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
- const portIdx = args.findIndex((arg) => arg === "--port" || arg === "-p");
102
- if (portIdx >= 0 && args[portIdx + 1]) {
103
- process.env.ISSUES_PORT = args[portIdx + 1];
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 shouldInitOnly = args.includes("init");
107
- const skipSeed = args.includes("--no-seed");
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
- if (!skipSeed) {
114
- const hasIssues =
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
- console.log(`Initialized ${issuesDir}`);
134
- process.exit(0);
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("@miketromba/issy-app");
190
+ await import('@miketromba/issy-app')
package/dist/cli.js CHANGED
@@ -1778,6 +1778,9 @@ issy CLI
1778
1778
  Usage:
1779
1779
  issy <command> [options]
1780
1780
 
1781
+ Options:
1782
+ --version, -v Show version number
1783
+
1781
1784
  Commands:
1782
1785
  list List all open issues
1783
1786
  --all, -a Include closed issues
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "issy",
3
- "version": "0.1.8",
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.1.8",
38
- "@miketromba/issy-core": "^0.1.8"
37
+ "@miketromba/issy-app": "^0.2.0",
38
+ "@miketromba/issy-core": "^0.2.0"
39
39
  }
40
40
  }