@useguano/create 0.1.3 → 0.1.5

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 (3) hide show
  1. package/README.md +15 -6
  2. package/index.js +157 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,15 +6,24 @@ Scaffold a new [Guano](https://www.npmjs.com/package/@useguano/guano) site proje
6
6
  npm create @useguano
7
7
  ```
8
8
 
9
- Asks for a folder name (default `my-site`), creates it with `@useguano/guano`
10
- as a dependency plus `dev`/`start`/`build` scripts, a `.env.example` and a
11
- `.gitignore`, and runs `npm install`. Then:
9
+ Asks for a folder name (default `my-site`), whether to connect **Claude
10
+ Desktop** over MCP (default yes — quit Claude first), and the two agent
11
+ permissions (may it edit the live project, may it publish — both default no).
12
+ Then it creates the folder with `@useguano/guano` as a dependency plus
13
+ `dev`/`start`/`build`/`connect` scripts, a `.env.example` and a `.gitignore`,
14
+ runs `npm install`, and connects Claude. Then:
12
15
 
13
16
  ```sh
14
17
  cd my-site
15
18
  npm run dev
16
19
  ```
17
20
 
18
- Open `http://localhost:4174/admin` and create the first account. Pass the
19
- folder as an argument to skip the prompt (`npm create @useguano my-site`), and
20
- `--no-install` to skip the install step.
21
+ Open `http://localhost:4174/admin` and create the first account — the Claude
22
+ token is bound to it, and the form shows the permissions you chose. Change
23
+ them any time in Settings → MCP → Agent permissions.
24
+
25
+ Flags: pass the folder to skip that prompt (`npm create @useguano my-site`);
26
+ `--no-install` skips the install; `--no-connect` skips Claude, `--connect`
27
+ connects without asking, `--main` / `--publish` grant the permissions
28
+ non-interactively. Without a terminal (CI) nothing is asked and Claude is not
29
+ connected.
package/index.js CHANGED
@@ -1,34 +1,118 @@
1
1
  #!/usr/bin/env node
2
- // @useguano/create: scaffold a project dir that runs Guano via npm scripts, then
3
- // install it. Zero dependencies — prompts with node:readline when no dir is
4
- // given. `npm create @useguano [dir] [--no-install]`.
2
+ // @useguano/create: scaffold a project dir that runs Guano via npm scripts,
3
+ // install it, and connect Claude Desktop — all in this one command, so what
4
+ // is left afterwards is `cd my-site && npm run dev`. Zero dependencies —
5
+ // prompts with node:readline.
6
+ //
7
+ // npm create @useguano [dir] [--no-install] [--no-connect | --connect]
8
+ // [--main] [--publish] [--config <claude config path>]
9
+ //
10
+ // The MCP connect runs through the just-installed package's own
11
+ // `guano connect --offline`: it mints a PENDING API token into <dir>/data,
12
+ // writes the two agent permissions there, and writes Claude Desktop's config.
13
+ // The server binds the token to the first admin at /admin setup, and the
14
+ // setup form shows the permissions instead of asking them again.
5
15
  import { spawnSync } from 'node:child_process'
16
+ import { createServer } from 'node:net'
17
+ import { existsSync } from 'node:fs'
6
18
  import { mkdir, readdir, writeFile } from 'node:fs/promises'
7
19
  import { basename, join, resolve } from 'node:path'
20
+ import { pathToFileURL } from 'node:url'
8
21
  import readline from 'node:readline/promises'
9
22
 
10
23
  const args = process.argv.slice(2)
11
- const install = !args.includes('--no-install')
12
- let dir = args.find((a) => !a.startsWith('--'))
24
+ const flag = (name) => args.includes(name)
25
+ const opt = (name) => {
26
+ const i = args.indexOf(name)
27
+ return i !== -1 ? args[i + 1] : undefined
28
+ }
29
+ const install = !flag('--no-install')
30
+ const interactive = process.stdin.isTTY && process.stdout.isTTY
31
+ let dir = args.find((a, i) => !a.startsWith('--') && args[i - 1] !== '--config')
13
32
  const DEFAULT_DIR = 'my-site'
33
+
34
+ // ---- every question first, so a ^C anywhere here changes nothing ----
35
+ const rl = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null
36
+ const ask = (q) => rl.question(q)
37
+ const yes = async (q, dflt) => {
38
+ const a = (await ask(`${q} [${dflt ? 'Y/n' : 'y/N'}] `)).trim()
39
+ return a ? /^y(es)?$/i.test(a) : dflt
40
+ }
41
+
14
42
  if (!dir) {
15
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
16
- dir = (await rl.question(`Project folder (${DEFAULT_DIR}): `)).trim() || DEFAULT_DIR
17
- rl.close()
43
+ dir = rl ? (await ask(`Project folder (${DEFAULT_DIR}): `)).trim() || DEFAULT_DIR : DEFAULT_DIR
44
+ }
45
+
46
+ // Connect Claude Desktop? Flags decide non-interactively (CI never asks and
47
+ // never connects); a terminal asks, default yes.
48
+ let connect = false
49
+ let policyFlags = []
50
+ if (flag('--no-connect')) {
51
+ connect = false
52
+ } else if (flag('--connect') || flag('--main') || flag('--publish')) {
53
+ connect = true
54
+ } else if (rl) {
55
+ connect = await yes('Connect Claude Desktop to this site over MCP?', true)
56
+ }
57
+ if (connect) {
58
+ if (flag('--main')) policyFlags.push('--main')
59
+ if (flag('--publish')) policyFlags.push('--publish')
60
+ if (!policyFlags.length && rl && !flag('--connect')) {
61
+ console.log('\nTwo questions about what the agent may do (change them any time in\n/admin → Settings → MCP → Agent permissions):\n')
62
+ if (await yes('Let agents edit the live project (Main)? Off, they work in a draft you apply yourself.', false))
63
+ policyFlags.push('--main')
64
+ if (await yes('Let agents publish the site? Off, they can still preview their work.', false))
65
+ policyFlags.push('--publish')
66
+ console.log('')
67
+ }
18
68
  }
19
69
 
20
70
  const target = resolve(dir)
21
- const name = basename(target)
22
- .toLowerCase()
23
- .replace(/[^a-z0-9-]+/g, '-')
24
- .replace(/^-+|-+$/g, '') || DEFAULT_DIR
71
+ const name =
72
+ basename(target)
73
+ .toLowerCase()
74
+ .replace(/[^a-z0-9-]+/g, '-')
75
+ .replace(/^-+|-+$/g, '') || DEFAULT_DIR
25
76
 
26
77
  await mkdir(target, { recursive: true })
27
78
  if ((await readdir(target)).length) {
79
+ rl?.close()
28
80
  console.error(`${target} is not empty — refusing to scaffold over existing files`)
29
81
  process.exit(1)
30
82
  }
31
83
 
84
+ /**
85
+ * A free port for THIS project, written into .env as an explicit PORT.
86
+ *
87
+ * Every project used to default to 4174, so a second one on the same machine
88
+ * lost the race and walked to the next free port — which left its MCP entry,
89
+ * the Vite proxy and any open tab addressing the FIRST project's socket, where
90
+ * its own token is unknown and the 401 reads as "the token is dead". An
91
+ * explicit port is a contract the server honours instead of walking, and
92
+ * putting it in a file the user can read makes "which port is this site on" a
93
+ * question with an answer.
94
+ *
95
+ * 4174 is still preferred, so a first project looks exactly as documented.
96
+ */
97
+ async function pickPort(first = 4174, tries = 40) {
98
+ // A site needs its own port AND the preview port above it, so step by two.
99
+ for (let port = first; port < first + tries * 2; port += 2) {
100
+ if ((await free(port)) && (await free(port + 1))) return port
101
+ }
102
+ return first
103
+ }
104
+
105
+ function free(port) {
106
+ return new Promise((resolve) => {
107
+ const probe = createServer()
108
+ probe.once('error', () => resolve(false))
109
+ probe.once('listening', () => probe.close(() => resolve(true)))
110
+ probe.listen(port, '0.0.0.0')
111
+ })
112
+ }
113
+
114
+ const port = await pickPort()
115
+
32
116
  const files = {
33
117
  'package.json':
34
118
  JSON.stringify(
@@ -56,7 +140,16 @@ const files = {
56
140
  export default {}
57
141
  `,
58
142
 
59
- '.env.example': `# PORT=4174
143
+ // A real .env, not just the example: the port is this project's identity on
144
+ // the machine, and leaving it to the 4174 default is what makes two projects
145
+ // fight over one address.
146
+ '.env': `# This project's own port. Keep it — two Guano sites on one machine
147
+ # must not share one, or whichever starts first answers for both and the
148
+ # other's MCP token is refused with a 401.
149
+ PORT=${port}
150
+ `,
151
+
152
+ '.env.example': `PORT=${port}
60
153
  # GUANO_DATA_DIR=./data # users, media, drafts, the published site — back this up
61
154
  # COOKIE_SECURE=1 # set 0 only for plain-HTTP LAN setups
62
155
  # PUBLISH_TOKEN= # optional: token for CI publishes
@@ -77,12 +170,20 @@ A [Guano](https://www.npmjs.com/package/@useguano/guano) site.
77
170
  npm run dev
78
171
  \`\`\`
79
172
 
80
- Open http://localhost:4174/admin, create the first account, build your site.
81
- Your published site is served at http://localhost:4174/.
173
+ Open http://localhost:${port}/admin, create the first account, then build your
174
+ site. Your published site is served at http://localhost:${port}/.
175
+
176
+ This project runs on port ${port} (set in \`.env\`). Each Guano site on a
177
+ machine needs its own, so two of them never answer for one address.
178
+
179
+ If you said yes to Claude Desktop when scaffolding, it is already connected:
180
+ the agent token is bound to the admin account you create, and the permissions
181
+ you chose (edit the live project, publish) show on the setup form and stay
182
+ editable in Settings → MCP → Agent permissions.
82
183
 
83
184
  - \`npm run start\` — production server (\`NODE_ENV=production\`)
84
185
  - \`npm run build\` — export the site as static files to \`./dist-site\`
85
- - \`npm run connect\` — connect Claude Desktop to this instance over MCP (quit Claude first)
186
+ - \`npm run connect\` — connect Claude Desktop later, or from another machine (quit Claude first; \`--main\` / \`--publish\` grant the agent permissions, or it asks)
86
187
  - everything lives in \`./data\` — **backing up = copying that directory**
87
188
 
88
189
  See the @useguano/guano package README for deploy notes and environment variables.
@@ -104,8 +205,45 @@ if (install) {
104
205
  if (!installed) console.error('\nnpm install failed — run it yourself in the project folder.')
105
206
  }
106
207
 
208
+ // ---- connect Claude Desktop, through the installed package ----
209
+ // GUANO_CREATE_LOCAL_PKG is a dev-only escape hatch: with --no-install there
210
+ // is no node_modules, so a repo checkout names its own packages/guano here.
211
+ let connected = false
212
+ if (connect) {
213
+ const pkgDir = existsSync(join(target, 'node_modules', '@useguano', 'guano'))
214
+ ? join(target, 'node_modules', '@useguano', 'guano')
215
+ : process.env.GUANO_CREATE_LOCAL_PKG
216
+ if (!pkgDir) {
217
+ console.error('\nClaude Desktop not connected: guano is not installed yet — run `npm run connect` inside the project later.')
218
+ } else {
219
+ try {
220
+ // the package resolves its data dir from this env at import time
221
+ const dataDir = join(target, 'data')
222
+ await mkdir(dataDir, { recursive: true, mode: 0o700 })
223
+ process.env.GUANO_DATA_DIR = dataDir
224
+ const { main } = await import(pathToFileURL(join(pkgDir, 'mcp', 'connect.mjs')).href)
225
+ const connectArgs = [...policyFlags, '--offline', '--yes']
226
+ if (opt('--config')) connectArgs.push('--config', resolve(opt('--config')))
227
+ console.log('\nConnecting Claude Desktop…')
228
+ await main(connectArgs, {
229
+ whenClaudeRunning: async () => {
230
+ if (!rl) return 'skip'
231
+ const a = (
232
+ await ask('Claude Desktop is running and overwrites its config on quit. Quit it, then press Enter (or type skip): ')
233
+ ).trim()
234
+ return /^s(kip)?$/i.test(a) ? 'skip' : 'retry'
235
+ },
236
+ })
237
+ connected = true
238
+ } catch (e) {
239
+ console.error(`\n✗ ${e?.message ?? e}\n Claude Desktop not connected — run \`npm run connect\` inside the project later.`)
240
+ }
241
+ }
242
+ }
243
+ rl?.close()
244
+
107
245
  console.log(`
108
246
  Next:
109
- cd ${dir}${installed ? '' : '\n npm install'}
110
- npm run dev → http://localhost:4174/admin
111
- `)
247
+ cd ${dir}${installed || !install ? '' : '\n npm install'}
248
+ npm run dev → http://localhost:4174/admin (create your admin account)
249
+ ${connected ? '\nThen open Claude Desktop — guano is connected; the agent token is bound to the admin you create.\n' : ''}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useguano/create",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "license": "AGPL-3.0-only",
5
5
  "publishConfig": {
6
6
  "access": "public"