shelfware 0.1.1 → 0.1.3

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.
Files changed (32) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{BZuUK3cr.js → Bzmuhhqm.js} +1 -1
  3. package/.output/public/_nuxt/{6IstVYrV.js → CJmYvhe6.js} +1 -1
  4. package/.output/public/_nuxt/CMk5jOCS.js +1 -0
  5. package/.output/public/_nuxt/DJQqUjJu.js +2 -0
  6. package/.output/public/_nuxt/DfJzPSBi.js +1 -0
  7. package/.output/public/_nuxt/EBA28doP.js +14 -0
  8. package/.output/public/_nuxt/builds/latest.json +1 -1
  9. package/.output/public/_nuxt/builds/meta/87d7ca63-ebd6-4c54-a087-e23ce5bed209.json +1 -0
  10. package/.output/public/_nuxt/entry.CKteCcdN.css +2 -0
  11. package/.output/public/_nuxt/error-404.1qC5mjOI.css +1 -0
  12. package/.output/public/_nuxt/error-500.BcOmuN7K.css +1 -0
  13. package/.output/public/_nuxt/rWsrwf2L.js +36 -0
  14. package/.output/public/_nuxt/{C_VgD9ZI.js → sK9Xse3o.js} +1 -1
  15. package/.output/server/chunks/_/catalog.mjs +33 -8
  16. package/.output/server/chunks/nitro/nitro.mjs +96 -96
  17. package/.output/server/chunks/routes/renderer.mjs +1 -1
  18. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  19. package/.output/server/package.json +1 -1
  20. package/README.md +18 -4
  21. package/bin/launch.d.mts +54 -0
  22. package/bin/launch.mjs +46 -17
  23. package/package.json +7 -3
  24. package/.output/public/_nuxt/9C5WUlLE.js +0 -36
  25. package/.output/public/_nuxt/BXXtFil5.js +0 -1
  26. package/.output/public/_nuxt/BeLyMmd4.js +0 -1
  27. package/.output/public/_nuxt/DB5Vxbte.js +0 -2
  28. package/.output/public/_nuxt/DMg0j7gv.js +0 -14
  29. package/.output/public/_nuxt/builds/meta/e0d2b6a3-3a30-44b7-8c79-a56f66400005.json +0 -1
  30. package/.output/public/_nuxt/entry.b3_DJTEt.css +0 -2
  31. package/.output/public/_nuxt/error-404.CHsn7XF-.css +0 -1
  32. package/.output/public/_nuxt/error-500.Bawx2bLr.css +0 -1
package/bin/launch.mjs CHANGED
@@ -11,6 +11,9 @@ export const DEFAULT_PORT = 3781
11
11
  export const PORT_ATTEMPTS = 20
12
12
  export const HOST = '127.0.0.1'
13
13
 
14
+ /** Spec §10.3: the env var the server reads the session token from. */
15
+ export const TOKEN_ENV = 'NUXT_PUBLIC_SHELFWARE_TOKEN'
16
+
14
17
  export const HELP = `shelfware — a local catalog of the AI-agent skills on this machine
15
18
 
16
19
  Usage: shelfware [--port <n>] [--no-open] [--help]
@@ -28,22 +31,27 @@ export function makeToken() {
28
31
 
29
32
  /**
30
33
  * 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.
34
+ * Decimal digits without a leading zero: `Number()` would otherwise accept `0x10`,
35
+ * `1e3`, `' 4000 '` and `065535`, none of which the help text (`--port <n>`) promises.
33
36
  */
34
37
  function parsePort(raw) {
35
- if (!/^\d+$/.test(raw)) throw new Error(`Invalid port: ${JSON.stringify(raw)}`)
38
+ if (!/^[1-9]\d*$/.test(raw)) throw new Error(`Invalid port: ${JSON.stringify(raw)}`)
36
39
  const port = Number(raw)
37
- if (port < 1 || port > 65535) throw new Error(`Invalid port: ${JSON.stringify(raw)}`)
40
+ if (port > 65535) throw new Error(`Invalid port: ${JSON.stringify(raw)}`)
38
41
  return port
39
42
  }
40
43
 
41
44
  export function parseArgs(argv = [], env = {}) {
42
45
  let port = null
43
46
  let open = env.SHELFWARE_NO_OPEN !== '1'
47
+ let optionsEnded = false
44
48
  for (let i = 0; i < argv.length; i += 1) {
45
49
  const arg = argv[i]
46
- if (arg === '--no-open') {
50
+ if (optionsEnded || !arg.startsWith('-')) {
51
+ throw new Error(`Unexpected argument: ${arg}`)
52
+ } else if (arg === '--') {
53
+ optionsEnded = true
54
+ } else if (arg === '--no-open') {
47
55
  open = false
48
56
  } else if (arg === '--port') {
49
57
  if (i + 1 >= argv.length) throw new Error('--port needs a value')
@@ -57,19 +65,32 @@ export function parseArgs(argv = [], env = {}) {
57
65
  throw new Error(`Unknown option: ${arg}`)
58
66
  }
59
67
  }
60
- if (port === null && env.PORT) port = parsePort(env.PORT)
68
+ // Unset or empty means unset; anything else must be a real port.
69
+ if (port === null && env.PORT !== undefined && env.PORT !== '') port = parsePort(env.PORT)
61
70
  return { help: false, port, open }
62
71
  }
63
72
 
64
- export function isPortFree(port, host = HOST) {
73
+ /** Why `port` cannot be bound, in words a user can act on. */
74
+ export function portReason(code) {
75
+ if (code === 'EADDRINUSE') return 'is already in use'
76
+ if (code === 'EACCES') return 'needs elevated privileges (EACCES)'
77
+ return `cannot be bound (${code})`
78
+ }
79
+
80
+ /** Resolves null when `port` can be bound on `host`, otherwise the reason (see `portReason`). */
81
+ export function checkPort(port, host = HOST) {
65
82
  return new Promise((resolve) => {
66
83
  const server = net.createServer()
67
84
  server.unref()
68
- server.once('error', () => resolve(false))
69
- server.listen({ port, host }, () => server.close(() => resolve(true)))
85
+ server.once('error', err => resolve(portReason(err.code ?? 'UNKNOWN')))
86
+ server.listen({ port, host }, () => server.close(() => resolve(null)))
70
87
  })
71
88
  }
72
89
 
90
+ export async function isPortFree(port, host = HOST) {
91
+ return (await checkPort(port, host)) === null
92
+ }
93
+
73
94
  export async function pickPort(start = DEFAULT_PORT, { attempts = PORT_ATTEMPTS, host = HOST } = {}) {
74
95
  for (let port = start; port < start + attempts; port += 1) {
75
96
  if (await isPortFree(port, host)) return port
@@ -132,7 +153,7 @@ export function openBrowser(url, { platform = process.platform, spawn = spawnPro
132
153
  const args = platform === 'win32' ? ['/c', 'start', '', url] : [url]
133
154
  // The opener execs the browser; strip the session token so it never sits in the
134
155
  // browser's environment (the served page is the only place it belongs).
135
- const { NUXT_PUBLIC_SHELFWARE_TOKEN: _token, ...childEnv } = env
156
+ const { [TOKEN_ENV]: _token, ...childEnv } = env
136
157
  // A missing browser opener (e.g. no xdg-open on a headless box) must never take
137
158
  // the running server down with it: spawn can throw synchronously (rare) or emit
138
159
  // an async 'error' (the common ENOENT case), and an unhandled 'error' on an
@@ -151,6 +172,11 @@ export function serverEntry(binDir = path.dirname(fileURLToPath(import.meta.url)
151
172
  return path.resolve(binDir, '..', '.output', 'server', 'index.mjs')
152
173
  }
153
174
 
175
+ /** One line for the user whatever was thrown: Error, string, or nothing at all. */
176
+ export function describeError(err) {
177
+ return err instanceof Error ? err.message : String(err)
178
+ }
179
+
154
180
  /**
155
181
  * Orchestration with injectable I/O so it can be unit-tested end to end:
156
182
  * flags → port → env → import the prebuilt server → wait for health → open.
@@ -168,7 +194,7 @@ export async function launch({
168
194
  try {
169
195
  args = parseArgs(argv, env)
170
196
  } catch (err) {
171
- log(`shelfware: ${err.message}`)
197
+ log(`shelfware: ${describeError(err)}`)
172
198
  log(HELP)
173
199
  exit(1)
174
200
  return null
@@ -182,10 +208,13 @@ export async function launch({
182
208
  exit(1)
183
209
  return null
184
210
  }
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
211
+ if (args.port !== null) {
212
+ const problem = await checkPort(args.port)
213
+ if (problem) {
214
+ log(`shelfware: port ${args.port} ${problem} on ${HOST}; pick another (--port or PORT)`)
215
+ exit(1)
216
+ return null
217
+ }
189
218
  }
190
219
  // Both of these can fail for reasons the user can act on — every port in the
191
220
  // range is taken, or the server throws at import (a Nitro crash, or an
@@ -194,13 +223,13 @@ export async function launch({
194
223
  let port
195
224
  try {
196
225
  port = args.port ?? await pickPort(DEFAULT_PORT)
197
- env.NUXT_PUBLIC_SHELFWARE_TOKEN = makeToken()
226
+ env[TOKEN_ENV] = makeToken()
198
227
  env.NITRO_HOST = HOST
199
228
  env.NITRO_PORT = String(port)
200
229
  env.NODE_ENV = 'production'
201
230
  await importServer()
202
231
  } catch (err) {
203
- log(`shelfware: ${err.message}`)
232
+ log(`shelfware: ${describeError(err)}`)
204
233
  exit(1)
205
234
  return null
206
235
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelfware",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "shelfware finds the shelfware in your skill drawers.",
5
5
  "keywords": [
6
6
  "skills",
@@ -37,7 +37,7 @@
37
37
  "dev": "nuxt dev",
38
38
  "build": "nuxt build",
39
39
  "prepare": "nuxt prepare",
40
- "typecheck": "nuxt typecheck",
40
+ "typecheck": "nuxt typecheck && tsc -p tsconfig.unit.json",
41
41
  "test": "vitest run",
42
42
  "test:unit": "vitest run --project unit --passWithNoTests",
43
43
  "test:nuxt": "vitest run --project nuxt --passWithNoTests",
@@ -51,6 +51,7 @@
51
51
  "@nuxt/test-utils": "^4.2.0",
52
52
  "@nuxt/ui": "^4.11.0",
53
53
  "@types/markdown-it": "^14.2.0",
54
+ "@types/node": "^20.19.43",
54
55
  "@vue/test-utils": "^2.5.0",
55
56
  "@vueuse/core": "^14.4.0",
56
57
  "happy-dom": "^20.14.0",
@@ -63,6 +64,9 @@
63
64
  "yaml": "^2.9.0"
64
65
  },
65
66
  "pnpm": {
66
- "onlyBuiltDependencies": ["esbuild", "vue-demi"]
67
+ "onlyBuiltDependencies": [
68
+ "esbuild",
69
+ "vue-demi"
70
+ ]
67
71
  }
68
72
  }