shelfware 0.1.0 → 0.1.1

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "date": "2026-09-06T10:58:50.215Z",
2
+ "date": "2026-09-06T14:46:30.939Z",
3
3
  "preset": "node-server",
4
4
  "framework": {
5
5
  "name": "nuxt",
@@ -1 +1 @@
1
- {"id":"4c307968-4837-497b-a570-d6ee336accc4","timestamp":1788692326920}
1
+ {"id":"e0d2b6a3-3a30-44b7-8c79-a56f66400005","timestamp":1788705987913}
@@ -0,0 +1 @@
1
+ {"id":"e0d2b6a3-3a30-44b7-8c79-a56f66400005","timestamp":1788705987913,"prerendered":[]}
@@ -4056,7 +4056,7 @@ function _expandFromEnv(value) {
4056
4056
  const _inlineRuntimeConfig = {
4057
4057
  "app": {
4058
4058
  "baseURL": "/",
4059
- "buildId": "4c307968-4837-497b-a570-d6ee336accc4",
4059
+ "buildId": "e0d2b6a3-3a30-44b7-8c79-a56f66400005",
4060
4060
  "buildAssetsDir": "/_nuxt/",
4061
4061
  "cdnURL": ""
4062
4062
  },
@@ -4574,107 +4574,107 @@ const assets = {
4574
4574
  "/_nuxt/6IstVYrV.js": {
4575
4575
  "type": "text/javascript; charset=utf-8",
4576
4576
  "etag": "\"e90-QSo0QKAvkvHEus5Dhcj+CyKCqD4\"",
4577
- "mtime": "2026-09-06T10:58:49.045Z",
4577
+ "mtime": "2026-09-06T14:46:29.734Z",
4578
4578
  "size": 3728,
4579
4579
  "path": "../public/_nuxt/6IstVYrV.js"
4580
4580
  },
4581
4581
  "/_nuxt/BDNMzG2s.js": {
4582
4582
  "type": "text/javascript; charset=utf-8",
4583
4583
  "etag": "\"54-MasMfSk/A98C3Gn9uIOxtFxkWNw\"",
4584
- "mtime": "2026-09-06T10:58:49.045Z",
4584
+ "mtime": "2026-09-06T14:46:29.734Z",
4585
4585
  "size": 84,
4586
4586
  "path": "../public/_nuxt/BDNMzG2s.js"
4587
4587
  },
4588
- "/_nuxt/BXXtFil5.js": {
4589
- "type": "text/javascript; charset=utf-8",
4590
- "etag": "\"1bd2-NDdun38lYj0OD2wayMdetDX3ehc\"",
4591
- "mtime": "2026-09-06T10:58:49.045Z",
4592
- "size": 7122,
4593
- "path": "../public/_nuxt/BXXtFil5.js"
4594
- },
4595
- "/_nuxt/BZuUK3cr.js": {
4596
- "type": "text/javascript; charset=utf-8",
4597
- "etag": "\"d4e-rtWL1k144ZrYuBUrIE7FN7q4gDI\"",
4598
- "mtime": "2026-09-06T10:58:49.045Z",
4599
- "size": 3406,
4600
- "path": "../public/_nuxt/BZuUK3cr.js"
4601
- },
4602
4588
  "/_nuxt/BW3xJdEx.js": {
4603
4589
  "type": "text/javascript; charset=utf-8",
4604
4590
  "etag": "\"2da-zDmvCG/tLW/peRiDRHBkm+3Edw4\"",
4605
- "mtime": "2026-09-06T10:58:49.045Z",
4591
+ "mtime": "2026-09-06T14:46:29.735Z",
4606
4592
  "size": 730,
4607
4593
  "path": "../public/_nuxt/BW3xJdEx.js"
4608
4594
  },
4595
+ "/_nuxt/BXXtFil5.js": {
4596
+ "type": "text/javascript; charset=utf-8",
4597
+ "etag": "\"1bd2-NDdun38lYj0OD2wayMdetDX3ehc\"",
4598
+ "mtime": "2026-09-06T14:46:29.735Z",
4599
+ "size": 7122,
4600
+ "path": "../public/_nuxt/BXXtFil5.js"
4601
+ },
4609
4602
  "/_nuxt/BeLyMmd4.js": {
4610
4603
  "type": "text/javascript; charset=utf-8",
4611
4604
  "etag": "\"1c1-HX03BAMzK3TbabTUt+zazZac6jY\"",
4612
- "mtime": "2026-09-06T10:58:49.045Z",
4605
+ "mtime": "2026-09-06T14:46:29.735Z",
4613
4606
  "size": 449,
4614
4607
  "path": "../public/_nuxt/BeLyMmd4.js"
4615
4608
  },
4616
- "/_nuxt/9C5WUlLE.js": {
4609
+ "/_nuxt/BZuUK3cr.js": {
4617
4610
  "type": "text/javascript; charset=utf-8",
4618
- "etag": "\"76057-x5s6sLzHAii+nwAfxpSA3vSHenw\"",
4619
- "mtime": "2026-09-06T10:58:49.046Z",
4620
- "size": 483415,
4621
- "path": "../public/_nuxt/9C5WUlLE.js"
4611
+ "etag": "\"d4e-rtWL1k144ZrYuBUrIE7FN7q4gDI\"",
4612
+ "mtime": "2026-09-06T14:46:29.735Z",
4613
+ "size": 3406,
4614
+ "path": "../public/_nuxt/BZuUK3cr.js"
4622
4615
  },
4623
4616
  "/_nuxt/C_VgD9ZI.js": {
4624
4617
  "type": "text/javascript; charset=utf-8",
4625
4618
  "etag": "\"16e9b-CaBNJIMBZbSuSxfSRmsiBioAcZc\"",
4626
- "mtime": "2026-09-06T10:58:49.045Z",
4619
+ "mtime": "2026-09-06T14:46:29.735Z",
4627
4620
  "size": 93851,
4628
4621
  "path": "../public/_nuxt/C_VgD9ZI.js"
4629
4622
  },
4623
+ "/_nuxt/DB5Vxbte.js": {
4624
+ "type": "text/javascript; charset=utf-8",
4625
+ "etag": "\"9f1f-lZvm375DqcFM+vOcKc2rvEVowVM\"",
4626
+ "mtime": "2026-09-06T14:46:29.735Z",
4627
+ "size": 40735,
4628
+ "path": "../public/_nuxt/DB5Vxbte.js"
4629
+ },
4630
4630
  "/_nuxt/error-404.CHsn7XF-.css": {
4631
4631
  "type": "text/css; charset=utf-8",
4632
4632
  "etag": "\"97d-2Jv1I5SVRWWiHcs7d0fjdFylhP4\"",
4633
- "mtime": "2026-09-06T10:58:49.046Z",
4633
+ "mtime": "2026-09-06T14:46:29.735Z",
4634
4634
  "size": 2429,
4635
4635
  "path": "../public/_nuxt/error-404.CHsn7XF-.css"
4636
4636
  },
4637
4637
  "/_nuxt/error-500.Bawx2bLr.css": {
4638
4638
  "type": "text/css; charset=utf-8",
4639
4639
  "etag": "\"772-1WbEnJKsOodm+94L4M1TNGEMGdA\"",
4640
- "mtime": "2026-09-06T10:58:49.046Z",
4640
+ "mtime": "2026-09-06T14:46:29.735Z",
4641
4641
  "size": 1906,
4642
4642
  "path": "../public/_nuxt/error-500.Bawx2bLr.css"
4643
4643
  },
4644
+ "/_nuxt/9C5WUlLE.js": {
4645
+ "type": "text/javascript; charset=utf-8",
4646
+ "etag": "\"76057-x5s6sLzHAii+nwAfxpSA3vSHenw\"",
4647
+ "mtime": "2026-09-06T14:46:29.735Z",
4648
+ "size": 483415,
4649
+ "path": "../public/_nuxt/9C5WUlLE.js"
4650
+ },
4644
4651
  "/_nuxt/builds/latest.json": {
4645
4652
  "type": "application/json",
4646
- "etag": "\"47-A62hsqMqrttpShO9U5Ab+nBZHXY\"",
4647
- "mtime": "2026-09-06T10:58:49.041Z",
4653
+ "etag": "\"47-gnWfjO3atXpuKHcTeevtRdg7IRg\"",
4654
+ "mtime": "2026-09-06T14:46:29.732Z",
4648
4655
  "size": 71,
4649
4656
  "path": "../public/_nuxt/builds/latest.json"
4650
4657
  },
4651
4658
  "/_nuxt/entry.b3_DJTEt.css": {
4652
4659
  "type": "text/css; charset=utf-8",
4653
4660
  "etag": "\"2fd30-a1fPhKsI3m5s3WkUz/aQvIKj6Ow\"",
4654
- "mtime": "2026-09-06T10:58:49.046Z",
4661
+ "mtime": "2026-09-06T14:46:29.735Z",
4655
4662
  "size": 195888,
4656
4663
  "path": "../public/_nuxt/entry.b3_DJTEt.css"
4657
4664
  },
4665
+ "/_nuxt/builds/meta/e0d2b6a3-3a30-44b7-8c79-a56f66400005.json": {
4666
+ "type": "application/json",
4667
+ "etag": "\"58-/O8b6cy7JVz90KrikBRsiFQaWdI\"",
4668
+ "mtime": "2026-09-06T14:46:29.731Z",
4669
+ "size": 88,
4670
+ "path": "../public/_nuxt/builds/meta/e0d2b6a3-3a30-44b7-8c79-a56f66400005.json"
4671
+ },
4658
4672
  "/_nuxt/DMg0j7gv.js": {
4659
4673
  "type": "text/javascript; charset=utf-8",
4660
4674
  "etag": "\"25529-MrmjBHZKW5MJ6UC+MAR50nTTq7U\"",
4661
- "mtime": "2026-09-06T10:58:49.046Z",
4675
+ "mtime": "2026-09-06T14:46:29.735Z",
4662
4676
  "size": 152873,
4663
4677
  "path": "../public/_nuxt/DMg0j7gv.js"
4664
- },
4665
- "/_nuxt/DB5Vxbte.js": {
4666
- "type": "text/javascript; charset=utf-8",
4667
- "etag": "\"9f1f-lZvm375DqcFM+vOcKc2rvEVowVM\"",
4668
- "mtime": "2026-09-06T10:58:49.045Z",
4669
- "size": 40735,
4670
- "path": "../public/_nuxt/DB5Vxbte.js"
4671
- },
4672
- "/_nuxt/builds/meta/4c307968-4837-497b-a570-d6ee336accc4.json": {
4673
- "type": "application/json",
4674
- "etag": "\"58-vdwrkuxkvo48U14qzidl95rF2gw\"",
4675
- "mtime": "2026-09-06T10:58:49.040Z",
4676
- "size": 88,
4677
- "path": "../public/_nuxt/builds/meta/4c307968-4837-497b-a570-d6ee336accc4.json"
4678
4678
  }
4679
4679
  };
4680
4680
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelfware-prod",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
package/README.md CHANGED
@@ -24,7 +24,7 @@ Skills outside your home directory (project-level `.claude/skills` inside a repo
24
24
 
25
25
  - **`npx shelfware` prints "coming soon".** Your npx cache still holds the `0.0.1` placeholder. Run `npx shelfware@latest` once; npx then caches the real release.
26
26
  - **The browser did not open.** Copy the URL from the `shelfware at http://127.0.0.1:<port>` line, or start with `--no-open` and open it yourself.
27
- - **Port in use.** Pass `--port <n>`; without it shelfware tries 3781 through 3800.
27
+ - **Port in use.** An explicit `--port <n>` that is taken is refused at once with a one-line message; without either the flag or PORT shelfware tries 3781 through 3800. A typo in a flag prints the message and the help text instead of a stack trace.
28
28
  - **The catalog is empty.** Check that your skills live under `$HOME/.<tool>/skills`; other locations are not scanned yet.
29
29
 
30
30
  ## What you get
package/bin/launch.mjs CHANGED
@@ -13,7 +13,7 @@ export const HOST = '127.0.0.1'
13
13
 
14
14
  export const HELP = `shelfware — a local catalog of the AI-agent skills on this machine
15
15
 
16
- Usage: shelfware [--port <n>] [--no-open]
16
+ Usage: shelfware [--port <n>] [--no-open] [--help]
17
17
 
18
18
  --port <n> bind 127.0.0.1:<n> instead of the first free port from ${DEFAULT_PORT}
19
19
  --no-open do not open the browser (also SHELFWARE_NO_OPEN=1)
@@ -26,6 +26,18 @@ export function makeToken() {
26
26
  return `sw_${crypto.randomBytes(32).toString('hex')}`
27
27
  }
28
28
 
29
+ /**
30
+ * The raw text stays in the message so `--port abc` reads back as "abc", not NaN.
31
+ * Digits only: `Number()` would otherwise accept `0x10`, `1e3` and `' 4000 '`,
32
+ * none of which the help text (`--port <n>`) promises.
33
+ */
34
+ function parsePort(raw) {
35
+ if (!/^\d+$/.test(raw)) throw new Error(`Invalid port: ${JSON.stringify(raw)}`)
36
+ const port = Number(raw)
37
+ if (port < 1 || port > 65535) throw new Error(`Invalid port: ${JSON.stringify(raw)}`)
38
+ return port
39
+ }
40
+
29
41
  export function parseArgs(argv = [], env = {}) {
30
42
  let port = null
31
43
  let open = env.SHELFWARE_NO_OPEN !== '1'
@@ -34,18 +46,18 @@ export function parseArgs(argv = [], env = {}) {
34
46
  if (arg === '--no-open') {
35
47
  open = false
36
48
  } else if (arg === '--port') {
37
- port = Number(argv[i + 1])
49
+ if (i + 1 >= argv.length) throw new Error('--port needs a value')
50
+ port = parsePort(argv[i + 1])
38
51
  i += 1
39
52
  } else if (arg.startsWith('--port=')) {
40
- port = Number(arg.slice('--port='.length))
53
+ port = parsePort(arg.slice('--port='.length))
41
54
  } else if (arg === '--help' || arg === '-h') {
42
55
  return { help: true, port: null, open }
56
+ } else {
57
+ throw new Error(`Unknown option: ${arg}`)
43
58
  }
44
59
  }
45
- if (port === null && env.PORT) port = Number(env.PORT)
46
- if (port !== null && (!Number.isInteger(port) || port < 1 || port > 65535)) {
47
- throw new Error(`Invalid port: ${port}`)
48
- }
60
+ if (port === null && env.PORT) port = parsePort(env.PORT)
49
61
  return { help: false, port, open }
50
62
  }
51
63
 
@@ -87,24 +99,46 @@ export function probeHealth(port, host = HOST) {
87
99
  })
88
100
  }
89
101
 
90
- export async function waitForHealth(port, { timeoutMs = 10_000, intervalMs = 100, host = HOST, probe = probeHealth } = {}) {
102
+ export async function waitForHealth(port, { timeoutMs = 10_000, intervalMs = 100, host = HOST, probe = probeHealth, signal } = {}) {
91
103
  const deadline = Date.now() + timeoutMs
92
- while (Date.now() < deadline) {
104
+ while (Date.now() < deadline && !signal?.aborted) {
93
105
  if (await probe(port, host)) return true
94
- await new Promise(resolve => setTimeout(resolve, intervalMs))
106
+ await sleep(intervalMs, signal)
95
107
  }
96
108
  return false
97
109
  }
98
110
 
99
- export function openBrowser(url, { platform = process.platform, spawn = spawnProcess } = {}) {
111
+ /** Resolves after `ms`, or as soon as `signal` aborts and then leaves no timer behind. */
112
+ function sleep(ms, signal) {
113
+ return new Promise((resolve) => {
114
+ if (signal?.aborted) {
115
+ resolve()
116
+ return
117
+ }
118
+ const onAbort = () => {
119
+ clearTimeout(timer)
120
+ resolve()
121
+ }
122
+ const timer = setTimeout(() => {
123
+ signal?.removeEventListener('abort', onAbort)
124
+ resolve()
125
+ }, ms)
126
+ signal?.addEventListener('abort', onAbort, { once: true })
127
+ })
128
+ }
129
+
130
+ export function openBrowser(url, { platform = process.platform, spawn = spawnProcess, env = process.env } = {}) {
100
131
  const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open'
101
132
  const args = platform === 'win32' ? ['/c', 'start', '', url] : [url]
133
+ // The opener execs the browser; strip the session token so it never sits in the
134
+ // browser's environment (the served page is the only place it belongs).
135
+ const { NUXT_PUBLIC_SHELFWARE_TOKEN: _token, ...childEnv } = env
102
136
  // A missing browser opener (e.g. no xdg-open on a headless box) must never take
103
137
  // the running server down with it: spawn can throw synchronously (rare) or emit
104
138
  // an async 'error' (the common ENOENT case), and an unhandled 'error' on an
105
139
  // EventEmitter is fatal. Swallow both.
106
140
  try {
107
- const child = spawn(cmd, args, { stdio: 'ignore', detached: true })
141
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true, env: childEnv })
108
142
  child.on?.('error', () => {})
109
143
  child.unref?.()
110
144
  } catch {
@@ -130,7 +164,15 @@ export async function launch({
130
164
  entry = serverEntry(),
131
165
  spawn = spawnProcess,
132
166
  } = {}) {
133
- const args = parseArgs(argv, env)
167
+ let args
168
+ try {
169
+ args = parseArgs(argv, env)
170
+ } catch (err) {
171
+ log(`shelfware: ${err.message}`)
172
+ log(HELP)
173
+ exit(1)
174
+ return null
175
+ }
134
176
  if (args.help) {
135
177
  log(HELP)
136
178
  return null
@@ -140,12 +182,28 @@ export async function launch({
140
182
  exit(1)
141
183
  return null
142
184
  }
143
- const port = args.port ?? await pickPort(DEFAULT_PORT)
144
- env.NUXT_PUBLIC_SHELFWARE_TOKEN = makeToken()
145
- env.NITRO_HOST = HOST
146
- env.NITRO_PORT = String(port)
147
- env.NODE_ENV = 'production'
148
- await importServer()
185
+ if (args.port !== null && !(await isPortFree(args.port))) {
186
+ log(`shelfware: port ${args.port} is already in use on ${HOST}; pick another with --port`)
187
+ exit(1)
188
+ return null
189
+ }
190
+ // Both of these can fail for reasons the user can act on — every port in the
191
+ // range is taken, or the server throws at import (a Nitro crash, or an
192
+ // EADDRINUSE from losing the race for the auto-picked port). One line, exit 1,
193
+ // never a stack trace.
194
+ let port
195
+ try {
196
+ port = args.port ?? await pickPort(DEFAULT_PORT)
197
+ env.NUXT_PUBLIC_SHELFWARE_TOKEN = makeToken()
198
+ env.NITRO_HOST = HOST
199
+ env.NITRO_PORT = String(port)
200
+ env.NODE_ENV = 'production'
201
+ await importServer()
202
+ } catch (err) {
203
+ log(`shelfware: ${err.message}`)
204
+ exit(1)
205
+ return null
206
+ }
149
207
  if (!(await waitForHealth(port))) {
150
208
  log(`shelfware: the server did not answer on http://${HOST}:${port} within 10 s`)
151
209
  exit(1)
@@ -153,6 +211,6 @@ export async function launch({
153
211
  }
154
212
  const url = `http://${HOST}:${port}`
155
213
  log(`shelfware at ${url}`)
156
- if (args.open) openBrowser(url, { spawn })
214
+ if (args.open) openBrowser(url, { spawn, env })
157
215
  return { port, url }
158
216
  }
package/bin/shelfware.mjs CHANGED
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { launch } from './launch.mjs'
3
3
 
4
- await launch({
5
- importServer: () => import('../.output/server/index.mjs'),
6
- })
4
+ // launch() reports every failure it can foresee as one line and exits 1; this is
5
+ // the last resort, so an unforeseen throw still never reaches the user as a stack.
6
+ try {
7
+ await launch({ importServer: () => import('../.output/server/index.mjs') })
8
+ } catch (err) {
9
+ console.error(`shelfware: ${err instanceof Error ? err.message : String(err)}`)
10
+ process.exit(1)
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelfware",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "shelfware finds the shelfware in your skill drawers.",
5
5
  "keywords": [
6
6
  "skills",
@@ -1 +0,0 @@
1
- {"id":"4c307968-4837-497b-a570-d6ee336accc4","timestamp":1788692326920,"prerendered":[]}