pulumi-package-docs 0.8.0 → 0.9.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.
Files changed (62) hide show
  1. package/README.md +98 -4
  2. package/bin/args.js +62 -3
  3. package/bin/args.test.js +60 -0
  4. package/bin/cli-bun.js +55 -23
  5. package/bin/cli.js +74 -28
  6. package/bin/cli.test.js +18 -0
  7. package/bin/static.js +196 -0
  8. package/bin/static.test.js +176 -0
  9. package/dist/client/assets/MemberDetail-Du4HYTda.js +1 -0
  10. package/dist/client/assets/{token-oQmZfAP3.js → examples-zkHFEaMt.js} +1 -1
  11. package/dist/client/assets/index-g58gbZKl.js +12 -0
  12. package/dist/client/assets/matchContext-BfDTxnb0.js +2 -0
  13. package/dist/client/assets/providers._name-DztqmroK.js +1 -0
  14. package/dist/client/assets/providers._name.functions._token-Dds2hD5R.js +1 -0
  15. package/dist/client/assets/providers._name.functions._token-DkZe4R_v.js +1 -0
  16. package/dist/client/assets/providers._name.index-B6cK3ZCZ.js +1 -0
  17. package/dist/client/assets/providers._name.index-CvFTUtAs.js +1 -0
  18. package/dist/client/assets/providers._name.resources._token-Bf7TPYcB.js +1 -0
  19. package/dist/client/assets/providers._name.resources._token-CIjv9yb8.js +1 -0
  20. package/dist/client/assets/routes-Dz3Kksbm.js +1 -0
  21. package/dist/mcp/stdio.js +449 -0
  22. package/dist/server/assets/{MemberDetail-C5sAHNZW.js → MemberDetail-CdHUSmQD.js} +2 -1
  23. package/dist/server/assets/{_tanstack-start-manifest_v-B0J-S-em.js → _tanstack-start-manifest_v-Drm3Ru6D.js} +15 -15
  24. package/dist/server/assets/api-D49ZDhMh.js +27 -0
  25. package/dist/server/assets/{providers._name.functions._token-Gkw4zt3M.js → providers._name.functions._token-BvkwmQ-L.js} +3 -2
  26. package/dist/server/assets/{providers._name.index-CEXkl-76.js → providers._name.index-DBkfEjsy.js} +3 -2
  27. package/dist/server/assets/{providers._name.resources._token-R_i2MmRl.js → providers._name.resources._token-Sl31Qlvd.js} +3 -2
  28. package/dist/server/assets/{schema.server-B-VvU1qb.js → providers.server-B1ZHZ5jE.js} +48 -1
  29. package/dist/server/assets/{router-1INob9fh.js → router-_zs8P-B2.js} +24 -87
  30. package/dist/server/assets/{routes-4jiXnS1p.js → routes-BUxagg9r.js} +1 -1
  31. package/dist/server/assets/token-DBEFeZIN.js +92 -0
  32. package/dist/server/server.js +987 -375
  33. package/examples/github-pages.yml +56 -0
  34. package/examples/gitlab-ci.yml +22 -0
  35. package/package.json +7 -3
  36. package/src/components/ThemeToggle.tsx +37 -0
  37. package/src/lib/pulumi/api.ts +18 -69
  38. package/src/lib/pulumi/mcp.server.test.ts +112 -0
  39. package/src/lib/pulumi/mcp.server.ts +11 -6
  40. package/src/lib/pulumi/providers.server.ts +80 -0
  41. package/src/lib/pulumi/stdio.server.ts +16 -0
  42. package/src/lib/pulumi/token.test.ts +44 -1
  43. package/src/lib/pulumi/token.ts +28 -0
  44. package/src/lib/static-docs.ts +15 -0
  45. package/src/router.tsx +3 -0
  46. package/src/routes/__root.tsx +6 -3
  47. package/src/routes/providers.$name.functions.$token.tsx +7 -4
  48. package/src/routes/providers.$name.index.tsx +2 -2
  49. package/src/routes/providers.$name.resources.$token.tsx +7 -4
  50. package/vite.config.ts +41 -1
  51. package/dist/client/assets/MemberDetail-BS9DbqEt.js +0 -1
  52. package/dist/client/assets/index-2FzxAhzN.js +0 -13
  53. package/dist/client/assets/providers._name-FmJEVkdV.js +0 -1
  54. package/dist/client/assets/providers._name.functions._token-BzjmNHea.js +0 -1
  55. package/dist/client/assets/providers._name.functions._token-D768U6I-.js +0 -1
  56. package/dist/client/assets/providers._name.index-C42yQ95z.js +0 -1
  57. package/dist/client/assets/providers._name.index-Dm0YiZWP.js +0 -1
  58. package/dist/client/assets/providers._name.resources._token-CIYPoqyA.js +0 -1
  59. package/dist/client/assets/providers._name.resources._token-D0OLvgo_.js +0 -1
  60. package/dist/client/assets/routes-DKNWlgPZ.js +0 -1
  61. package/dist/client/assets/useSelector-BIt42tGI.js +0 -1
  62. package/dist/server/assets/api-C55bS4R6.js +0 -64
package/README.md CHANGED
@@ -11,13 +11,27 @@ npx pulumi-package-docs
11
11
  This serves docs for the Pulumi project in your current directory. Options:
12
12
 
13
13
  ```bash
14
- pulumi-package-docs [options]
14
+ pulumi-package-docs [command] [options]
15
15
 
16
+ Commands:
17
+ serve Serve the docs (default)
18
+ static Generate a static site to publish (GitHub/GitLab Pages)
19
+
20
+ Options:
16
21
  -d, --dir <path> Pulumi project directory to inspect (default: current directory)
22
+ -h, --help Show this help message
23
+
24
+ serve options:
17
25
  -p, --port <port> Port to serve on (default: 3000)
18
26
  --host Expose the server on your network
19
27
  --open Open the docs in your default browser once ready
20
- -h, --help Show this help message
28
+ --stdio Run as an MCP server over stdio instead of serving the docs site
29
+
30
+ static options:
31
+ -o, --out <path> Directory to write the site to (default: <dir>/pulumi-package-docs-site)
32
+ -b, --base <path> Base URL path the site is served from, e.g. /my-repo/ for a
33
+ GitHub/GitLab Pages project site (default: /)
34
+ -f, --force Overwrite --out even if it holds files we didn't generate
21
35
  ```
22
36
 
23
37
  Requires the `pulumi` CLI to be installed and on `PATH`; it's used to fetch each provider's schema (`pulumi package get-schema`). If a schema was fetched successfully before, it's cached and reused if a later CLI call fails.
@@ -37,9 +51,81 @@ On macOS or Linux, it's also available via Homebrew:
37
51
  brew install pierskarsenbarg/tap/pulumi-package-docs
38
52
  ```
39
53
 
54
+ ## Static site (GitHub Pages / GitLab Pages)
55
+
56
+ `pulumi-package-docs static` renders the same docs to a folder of plain HTML,
57
+ so a project's provider docs can live on GitHub Pages, GitLab Pages, or any
58
+ other static host instead of only on a developer's machine:
59
+
60
+ ```bash
61
+ npx pulumi-package-docs static # -> ./pulumi-package-docs-site
62
+ npx pulumi-package-docs static --out ./site \
63
+ --base /my-repo/ # for https://<owner>.github.io/my-repo/
64
+ ```
65
+
66
+ Every provider, resource and function gets its own prerendered page, with the
67
+ project's `Pulumi.yaml` deciding what's in the site — the generator crawls its
68
+ own output, so nothing needs listing up front. The pages ship without the
69
+ client bundle: links are ordinary links and each page stands on its own, which
70
+ keeps a large provider's schema out of the pages that don't need it. The
71
+ `pulumi` CLI is needed to generate the site (same as serving it), but not to
72
+ host it.
73
+
74
+ `--base` is the path the site is served from, and it has to match: a
75
+ GitHub/GitLab Pages _project_ site lives under `/<repo>/`, while a user/org
76
+ site (or a custom domain) lives at `/`, where `--base` can be left off. The
77
+ output directory is rebuilt on each run, and a non-empty directory that wasn't
78
+ generated by this tool is left alone unless you pass `--force`.
79
+
80
+ Preview the result with any static file server:
81
+
82
+ ```bash
83
+ npx serve ./site
84
+ ```
85
+
86
+ Ready-to-copy CI config lives in [`examples/`](examples): [GitHub
87
+ Actions](examples/github-pages.yml) and [GitLab
88
+ CI](examples/gitlab-ci.yml). Both install the `pulumi` CLI (the schemas come
89
+ from it), run the generator, and publish the output — note that GitLab Pages
90
+ only publishes a directory called `public`.
91
+
92
+ The standalone binaries can't generate a static site (they carry no build
93
+ toolchain); use `npx pulumi-package-docs static` for that.
94
+
40
95
  ## MCP
41
96
 
42
- The server also exposes an [MCP](https://modelcontextprotocol.io) endpoint at `/mcp` (Streamable HTTP), so a coding agent can look up the same local provider docs without browsing the site. Point an MCP client at `http://localhost:3000/mcp` (e.g. in Claude Code, `claude mcp add --transport http pulumi-package-docs http://localhost:3000/mcp`). Tools:
97
+ The same local provider docs are available to coding agents over [MCP](https://modelcontextprotocol.io), via either transport.
98
+
99
+ **Streamable HTTP**, while the docs site is running: the server exposes an MCP endpoint at `/mcp`, so point an MCP client at `http://localhost:3000/mcp` (e.g. in Claude Code, `claude mcp add --transport http pulumi-package-docs http://localhost:3000/mcp`).
100
+
101
+ **stdio**, for clients that spawn a server process instead of connecting to a URL: run with `--stdio` and no docs site is started at all — the process speaks MCP on stdin/stdout and exits with the client.
102
+
103
+ ```bash
104
+ claude mcp add pulumi-package-docs -- npx -y pulumi-package-docs --stdio --dir /path/to/pulumi/project
105
+ ```
106
+
107
+ Or, in an MCP client's JSON config:
108
+
109
+ ```json
110
+ {
111
+ "mcpServers": {
112
+ "pulumi-package-docs": {
113
+ "command": "npx",
114
+ "args": [
115
+ "-y",
116
+ "pulumi-package-docs",
117
+ "--stdio",
118
+ "--dir",
119
+ "/path/to/pulumi/project"
120
+ ]
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ `--dir` defaults to the process's working directory, so it can be omitted when the client launches the server from the Pulumi project itself. The standalone binary works the same way — use its path in place of `npx -y pulumi-package-docs`.
127
+
128
+ Both transports serve the same tools:
43
129
 
44
130
  - `list_providers` — the local providers declared in `Pulumi.yaml`, with resource/function counts and schema load status.
45
131
  - `list_resources` / `list_functions` — a provider's resource or function tokens, with a one-line summary and deprecation status for each.
@@ -73,7 +159,15 @@ Build the production app with:
73
159
  npm run build
74
160
  ```
75
161
 
76
- `bin/cli.js` is the `npx` entry point: it serves the production build with `vite preview` if one exists (`npm run build` first), otherwise falls back to `vite dev`. `bin/cli-bun.js` is a separate entry point used to build the standalone Bun executables (see above); it serves the built SSR handler directly with `Bun.serve` instead of shelling out to Vite. Build one locally with:
162
+ Build a static site from the same sources (`VITE_STATIC_DOCS=true` switches the
163
+ app into its no-hydration, prerendered mode, and `PULUMI_DOCS_BASE` sets the
164
+ path it's served from):
165
+
166
+ ```bash
167
+ node bin/cli.js static --dir /path/to/pulumi/project --out ./site
168
+ ```
169
+
170
+ `bin/cli.js` is the `npx` entry point: it serves the production build with `vite preview` if one exists (`npm run build` first), otherwise falls back to `vite dev`. With `--stdio` it skips Vite entirely and runs `dist/mcp/stdio.js` in-process, which `npm run build` produces from `vite.mcp.config.ts` (so `--stdio` needs a build — there's no dev-server fallback for it). `bin/cli-bun.js` is a separate entry point used to build the standalone Bun executables (see above); it serves the built SSR handler directly with `Bun.serve` instead of shelling out to Vite. Build one locally with:
77
171
 
78
172
  ```bash
79
173
  npm run build
package/bin/args.js CHANGED
@@ -1,35 +1,94 @@
1
1
  import { resolve } from 'node:path'
2
2
 
3
+ export const COMMANDS = ['serve', 'static']
4
+
5
+ const DEFAULT_OUT_DIR = 'pulumi-package-docs-site'
6
+
3
7
  export function parseArgs(argv) {
4
- const options = { dir: process.cwd(), port: '3000', host: false, open: false }
8
+ const options = {
9
+ command: 'serve',
10
+ dir: process.cwd(),
11
+ port: '3000',
12
+ host: false,
13
+ open: false,
14
+ stdio: false,
15
+ out: null,
16
+ base: '/',
17
+ force: false,
18
+ }
19
+ let commandSeen = false
20
+
5
21
  for (let i = 0; i < argv.length; i++) {
6
22
  const arg = argv[i]
7
23
  if (arg === '--dir' || arg === '-d') {
8
24
  options.dir = resolve(argv[++i])
9
25
  } else if (arg === '--port' || arg === '-p') {
10
26
  options.port = argv[++i]
27
+ } else if (arg === '--stdio') {
28
+ options.stdio = true
29
+ } else if (arg === '--out' || arg === '-o') {
30
+ options.out = resolve(argv[++i])
31
+ } else if (arg === '--base' || arg === '-b') {
32
+ options.base = argv[++i]
33
+ } else if (arg === '--force' || arg === '-f') {
34
+ options.force = true
11
35
  } else if (arg === '--host') {
12
36
  options.host = true
13
37
  } else if (arg === '--open') {
14
38
  options.open = true
15
39
  } else if (arg === '--help' || arg === '-h') {
16
40
  options.help = true
41
+ } else if (!commandSeen && !arg.startsWith('-')) {
42
+ commandSeen = true
43
+ if (!COMMANDS.includes(arg)) {
44
+ options.unknownCommand = arg
45
+ } else {
46
+ options.command = arg
47
+ }
17
48
  }
18
49
  }
50
+
51
+ // Deliberately resolved after the loop: the default depends on --dir, which
52
+ // may be given after the command.
53
+ if (options.out === null) options.out = resolve(options.dir, DEFAULT_OUT_DIR)
54
+
19
55
  return options
20
56
  }
21
57
 
58
+ /**
59
+ * The address the standalone binary's `Bun.serve` listens on. Bun treats a
60
+ * missing hostname as every interface, so loopback has to be explicit.
61
+ */
62
+ export function serveHostname(options) {
63
+ return options.host ? '0.0.0.0' : '127.0.0.1'
64
+ }
65
+
22
66
  export function printHelp() {
23
- console.log(`pulumi-package-docs [options]
67
+ console.log(`pulumi-package-docs [command] [options]
24
68
 
25
69
  Serves local documentation for the parameterized/local Pulumi providers used
26
70
  by a Pulumi project.
27
71
 
72
+ Commands:
73
+ serve Serve the docs (default)
74
+ static Generate a static site to publish (GitHub/GitLab Pages)
75
+
28
76
  Options:
29
77
  -d, --dir <path> Pulumi project directory to inspect (default: current directory)
78
+ -h, --help Show this help message
79
+
80
+ serve options:
30
81
  -p, --port <port> Port to serve on (default: 3000)
31
82
  --host Expose the server on your network
32
83
  --open Open the docs in your default browser once ready
33
- -h, --help Show this help message
84
+ --stdio Run as an MCP server over stdio instead of serving the docs
85
+ site (for MCP clients that spawn a server process). The
86
+ same tools are served over HTTP at /mcp when serving the site.
87
+
88
+ static options:
89
+ -o, --out <path> Directory to write the site to (default: <dir>/${DEFAULT_OUT_DIR})
90
+ -b, --base <path> Base URL path the site is served from, e.g. /my-repo/ for a
91
+ GitHub/GitLab Pages project site (default: /)
92
+ -f, --force Overwrite --out even if it holds files we didn't generate
34
93
  `)
35
94
  }
@@ -0,0 +1,60 @@
1
+ import { resolve } from 'node:path'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { parseArgs, serveHostname } from './args.js'
5
+
6
+ describe('serveHostname', () => {
7
+ it('listens on loopback only unless --host is passed', () => {
8
+ expect(serveHostname(parseArgs([]))).toBe('127.0.0.1')
9
+ })
10
+
11
+ it('listens on every interface with --host', () => {
12
+ expect(serveHostname(parseArgs(['--host']))).toBe('0.0.0.0')
13
+ })
14
+ })
15
+
16
+ describe('parseArgs', () => {
17
+ it('defaults to serving the current directory', () => {
18
+ const options = parseArgs([])
19
+ expect(options.command).toBe('serve')
20
+ expect(options.dir).toBe(process.cwd())
21
+ expect(options.port).toBe('3000')
22
+ })
23
+
24
+ it('reads the static command and its options', () => {
25
+ const options = parseArgs([
26
+ 'static',
27
+ '--dir',
28
+ '/tmp/project',
29
+ '--out',
30
+ '/tmp/site',
31
+ '--base',
32
+ '/my-repo/',
33
+ '--force',
34
+ ])
35
+ expect(options.command).toBe('static')
36
+ expect(options.dir).toBe(resolve('/tmp/project'))
37
+ expect(options.out).toBe(resolve('/tmp/site'))
38
+ expect(options.base).toBe('/my-repo/')
39
+ expect(options.force).toBe(true)
40
+ })
41
+
42
+ it('defaults the output directory to one inside the project directory', () => {
43
+ const options = parseArgs(['static', '--dir', '/tmp/project'])
44
+ expect(options.out).toBe(
45
+ resolve('/tmp/project', 'pulumi-package-docs-site'),
46
+ )
47
+ })
48
+
49
+ it('reports an unknown command rather than treating it as a flag value', () => {
50
+ const options = parseArgs(['publish'])
51
+ expect(options.unknownCommand).toBe('publish')
52
+ expect(options.command).toBe('serve')
53
+ })
54
+
55
+ it("doesn't mistake an option value for the command", () => {
56
+ const options = parseArgs(['--port', '8080', 'static'])
57
+ expect(options.command).toBe('static')
58
+ expect(options.port).toBe('8080')
59
+ })
60
+ })
package/bin/cli-bun.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn } from 'node:child_process'
3
- import { parseArgs, printHelp } from './args.js'
4
- import server from '../dist/server/server.js'
5
- import embeddedAssets from './embedded-assets.generated.js'
3
+ import { parseArgs, printHelp, serveHostname } from './args.js'
6
4
 
7
5
  const options = parseArgs(process.argv.slice(2))
8
6
 
@@ -11,8 +9,36 @@ if (options.help) {
11
9
  process.exit(0)
12
10
  }
13
11
 
12
+ if (options.unknownCommand) {
13
+ console.error(`Unknown command: ${options.unknownCommand}\n`)
14
+ printHelp()
15
+ process.exit(1)
16
+ }
17
+
18
+ if (options.command === 'static') {
19
+ // Generating a site runs a Vite build, which isn't part of this standalone
20
+ // executable (it only carries the prebuilt server and its assets).
21
+ console.error(
22
+ 'The standalone binary cannot generate a static site: it has no build toolchain.\n' +
23
+ 'Run `npx pulumi-package-docs static` instead.',
24
+ )
25
+ process.exit(1)
26
+ }
27
+
14
28
  process.env.PULUMI_LOCAL_DOCS_DIR = options.dir
15
29
 
30
+ if (options.stdio) {
31
+ // stdout is the MCP protocol channel here, so the banner goes to stderr and the
32
+ // docs site (and its embedded client assets) is never loaded — these imports stay
33
+ // dynamic so that stays true, and so `bun build --compile` still bundles both paths.
34
+ const { runStdioMcpServer } =
35
+ await import('../src/lib/pulumi/stdio.server.ts')
36
+ console.error(`Serving provider docs over stdio for: ${options.dir}`)
37
+ await runStdioMcpServer()
38
+ } else {
39
+ await serveDocsSite()
40
+ }
41
+
16
42
  function openBrowser(url) {
17
43
  const command =
18
44
  process.platform === 'darwin'
@@ -30,23 +56,29 @@ function openBrowser(url) {
30
56
  child.unref()
31
57
  }
32
58
 
33
- console.log(`Serving provider docs for: ${options.dir}`)
34
-
35
- const bunServer = Bun.serve({
36
- port: Number(options.port),
37
- hostname: options.host ? '0.0.0.0' : undefined,
38
- async fetch(request) {
39
- const url = new URL(request.url)
40
- const embeddedPath = embeddedAssets[url.pathname]
41
- if (embeddedPath) {
42
- return new Response(Bun.file(embeddedPath))
43
- }
44
- return server.fetch(request)
45
- },
46
- })
47
-
48
- const localUrl = `http://localhost:${bunServer.port}/`
49
- console.log(` ➜ Local: ${localUrl}`)
50
- console.log(` ➜ MCP: ${new URL('mcp', localUrl)}`)
51
-
52
- if (options.open) openBrowser(localUrl)
59
+ async function serveDocsSite() {
60
+ const { default: server } = await import('../dist/server/server.js')
61
+ const { default: embeddedAssets } =
62
+ await import('./embedded-assets.generated.js')
63
+
64
+ console.log(`Serving provider docs for: ${options.dir}`)
65
+
66
+ const bunServer = Bun.serve({
67
+ port: Number(options.port),
68
+ hostname: serveHostname(options),
69
+ async fetch(request) {
70
+ const url = new URL(request.url)
71
+ const embeddedPath = embeddedAssets[url.pathname]
72
+ if (embeddedPath) {
73
+ return new Response(Bun.file(embeddedPath))
74
+ }
75
+ return server.fetch(request)
76
+ },
77
+ })
78
+
79
+ const localUrl = `http://localhost:${bunServer.port}/`
80
+ console.log(` ➜ Local: ${localUrl}`)
81
+ console.log(` ➜ MCP: ${new URL('mcp', localUrl)}`)
82
+
83
+ if (options.open) openBrowser(localUrl)
84
+ }
package/bin/cli.js CHANGED
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
6
  import { spawn } from 'node:child_process'
7
7
  import { parseArgs, printHelp } from './args.js'
8
+ import { buildStaticSite } from './static.js'
8
9
 
9
10
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
10
11
  const require = createRequire(import.meta.url)
@@ -16,35 +17,80 @@ if (options.help) {
16
17
  process.exit(0)
17
18
  }
18
19
 
19
- const vitePkgJson = require.resolve('vite/package.json')
20
- const viteBin = join(dirname(vitePkgJson), 'bin', 'vite.js')
21
-
22
- const hasProductionBuild = existsSync(
23
- join(packageRoot, 'dist', 'server', 'server.js'),
24
- )
25
- const viteArgs = [
26
- hasProductionBuild ? 'preview' : 'dev',
27
- '--port',
28
- options.port,
29
- ]
30
- if (options.host) viteArgs.push('--host')
31
- if (options.open) viteArgs.push('--open')
32
-
33
- if (!hasProductionBuild) {
34
- console.log(
35
- 'No production build found, starting the dev server instead (this is slower).',
36
- )
20
+ if (options.unknownCommand) {
21
+ console.error(`Unknown command: ${options.unknownCommand}\n`)
22
+ printHelp()
23
+ process.exit(1)
37
24
  }
38
- console.log(`Serving provider docs for: ${options.dir}`)
39
25
 
40
- const child = spawn(process.execPath, [viteBin, ...viteArgs], {
41
- cwd: packageRoot,
42
- stdio: 'inherit',
43
- env: { ...process.env, PULUMI_LOCAL_DOCS_DIR: options.dir },
44
- })
26
+ process.env.PULUMI_LOCAL_DOCS_DIR = options.dir
45
27
 
46
- child.on('exit', (code) => process.exit(code ?? 0))
47
- child.on('error', (err) => {
48
- console.error('Failed to start pulumi-package-docs:', err)
28
+ if (options.command === 'static' && options.stdio) {
29
+ console.error('`--stdio` is a serve option; it has no effect on `static`.')
49
30
  process.exit(1)
50
- })
31
+ }
32
+
33
+ if (options.stdio) {
34
+ // stdout is the MCP protocol channel here, so the usual startup banner goes to
35
+ // stderr and the server runs in-process (no vite, no HTTP server).
36
+ const stdioEntry = join(packageRoot, 'dist', 'mcp', 'stdio.js')
37
+ if (!existsSync(stdioEntry)) {
38
+ console.error(
39
+ 'No production build found. Run `npm run build` before using --stdio.',
40
+ )
41
+ process.exit(1)
42
+ }
43
+ console.error(`Serving provider docs over stdio for: ${options.dir}`)
44
+ const { runStdioMcpServer } = await import(stdioEntry)
45
+ await runStdioMcpServer()
46
+ } else {
47
+ const vitePkgJson = require.resolve('vite/package.json')
48
+ const viteBin = join(dirname(vitePkgJson), 'bin', 'vite.js')
49
+
50
+ if (options.command === 'static') {
51
+ try {
52
+ await buildStaticSite({
53
+ packageRoot,
54
+ viteBin,
55
+ dir: options.dir,
56
+ out: options.out,
57
+ base: options.base,
58
+ force: options.force,
59
+ })
60
+ } catch (err) {
61
+ console.error(err instanceof Error ? err.message : err)
62
+ process.exit(1)
63
+ }
64
+ process.exit(0)
65
+ }
66
+
67
+ const hasProductionBuild = existsSync(
68
+ join(packageRoot, 'dist', 'server', 'server.js'),
69
+ )
70
+ const viteArgs = [
71
+ hasProductionBuild ? 'preview' : 'dev',
72
+ '--port',
73
+ options.port,
74
+ ]
75
+ if (options.host) viteArgs.push('--host')
76
+ if (options.open) viteArgs.push('--open')
77
+
78
+ if (!hasProductionBuild) {
79
+ console.log(
80
+ 'No production build found, starting the dev server instead (this is slower).',
81
+ )
82
+ }
83
+ console.log(`Serving provider docs for: ${options.dir}`)
84
+
85
+ const child = spawn(process.execPath, [viteBin, ...viteArgs], {
86
+ cwd: packageRoot,
87
+ stdio: 'inherit',
88
+ env: { ...process.env, PULUMI_LOCAL_DOCS_DIR: options.dir },
89
+ })
90
+
91
+ child.on('exit', (code) => process.exit(code ?? 0))
92
+ child.on('error', (err) => {
93
+ console.error('Failed to start pulumi-package-docs:', err)
94
+ process.exit(1)
95
+ })
96
+ }
@@ -0,0 +1,18 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { describe, expect, it } from 'vitest'
5
+
6
+ const cliPath = join(dirname(fileURLToPath(import.meta.url)), 'cli.js')
7
+
8
+ describe('cli.js', () => {
9
+ it('rejects `static --stdio` instead of silently serving MCP over stdio', () => {
10
+ const result = spawnSync(process.execPath, [cliPath, 'static', '--stdio'], {
11
+ encoding: 'utf8',
12
+ })
13
+
14
+ expect(result.status).toBe(1)
15
+ expect(result.stderr).toContain('--stdio')
16
+ expect(result.stderr).toContain('static')
17
+ })
18
+ })