@useguano/create 0.1.2 → 0.1.4

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 +111 -18
  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,30 +1,81 @@
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 { existsSync } from 'node:fs'
6
17
  import { mkdir, readdir, writeFile } from 'node:fs/promises'
7
18
  import { basename, join, resolve } from 'node:path'
19
+ import { pathToFileURL } from 'node:url'
8
20
  import readline from 'node:readline/promises'
9
21
 
10
22
  const args = process.argv.slice(2)
11
- const install = !args.includes('--no-install')
12
- let dir = args.find((a) => !a.startsWith('--'))
23
+ const flag = (name) => args.includes(name)
24
+ const opt = (name) => {
25
+ const i = args.indexOf(name)
26
+ return i !== -1 ? args[i + 1] : undefined
27
+ }
28
+ const install = !flag('--no-install')
29
+ const interactive = process.stdin.isTTY && process.stdout.isTTY
30
+ let dir = args.find((a, i) => !a.startsWith('--') && args[i - 1] !== '--config')
13
31
  const DEFAULT_DIR = 'my-site'
32
+
33
+ // ---- every question first, so a ^C anywhere here changes nothing ----
34
+ const rl = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null
35
+ const ask = (q) => rl.question(q)
36
+ const yes = async (q, dflt) => {
37
+ const a = (await ask(`${q} [${dflt ? 'Y/n' : 'y/N'}] `)).trim()
38
+ return a ? /^y(es)?$/i.test(a) : dflt
39
+ }
40
+
14
41
  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()
42
+ dir = rl ? (await ask(`Project folder (${DEFAULT_DIR}): `)).trim() || DEFAULT_DIR : DEFAULT_DIR
43
+ }
44
+
45
+ // Connect Claude Desktop? Flags decide non-interactively (CI never asks and
46
+ // never connects); a terminal asks, default yes.
47
+ let connect = false
48
+ let policyFlags = []
49
+ if (flag('--no-connect')) {
50
+ connect = false
51
+ } else if (flag('--connect') || flag('--main') || flag('--publish')) {
52
+ connect = true
53
+ } else if (rl) {
54
+ connect = await yes('Connect Claude Desktop to this site over MCP?', true)
55
+ }
56
+ if (connect) {
57
+ if (flag('--main')) policyFlags.push('--main')
58
+ if (flag('--publish')) policyFlags.push('--publish')
59
+ if (!policyFlags.length && rl && !flag('--connect')) {
60
+ console.log('\nTwo questions about what the agent may do (change them any time in\n/admin → Settings → MCP → Agent permissions):\n')
61
+ if (await yes('Let agents edit the live project (Main)? Off, they work in a draft you apply yourself.', false))
62
+ policyFlags.push('--main')
63
+ if (await yes('Let agents publish the site? Off, they can still preview their work.', false))
64
+ policyFlags.push('--publish')
65
+ console.log('')
66
+ }
18
67
  }
19
68
 
20
69
  const target = resolve(dir)
21
- const name = basename(target)
22
- .toLowerCase()
23
- .replace(/[^a-z0-9-]+/g, '-')
24
- .replace(/^-+|-+$/g, '') || DEFAULT_DIR
70
+ const name =
71
+ basename(target)
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9-]+/g, '-')
74
+ .replace(/^-+|-+$/g, '') || DEFAULT_DIR
25
75
 
26
76
  await mkdir(target, { recursive: true })
27
77
  if ((await readdir(target)).length) {
78
+ rl?.close()
28
79
  console.error(`${target} is not empty — refusing to scaffold over existing files`)
29
80
  process.exit(1)
30
81
  }
@@ -77,12 +128,17 @@ A [Guano](https://www.npmjs.com/package/@useguano/guano) site.
77
128
  npm run dev
78
129
  \`\`\`
79
130
 
80
- Open http://localhost:4174/admin, create the first account, build your site.
81
- Your published site is served at http://localhost:4174/.
131
+ Open http://localhost:4174/admin, create the first account, then build your
132
+ site. Your published site is served at http://localhost:4174/.
133
+
134
+ If you said yes to Claude Desktop when scaffolding, it is already connected:
135
+ the agent token is bound to the admin account you create, and the permissions
136
+ you chose (edit the live project, publish) show on the setup form and stay
137
+ editable in Settings → MCP → Agent permissions.
82
138
 
83
139
  - \`npm run start\` — production server (\`NODE_ENV=production\`)
84
140
  - \`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)
141
+ - \`npm run connect\` — connect Claude Desktop later, or from another machine (quit Claude first; \`--main\` / \`--publish\` grant the agent permissions, or it asks)
86
142
  - everything lives in \`./data\` — **backing up = copying that directory**
87
143
 
88
144
  See the @useguano/guano package README for deploy notes and environment variables.
@@ -104,8 +160,45 @@ if (install) {
104
160
  if (!installed) console.error('\nnpm install failed — run it yourself in the project folder.')
105
161
  }
106
162
 
163
+ // ---- connect Claude Desktop, through the installed package ----
164
+ // GUANO_CREATE_LOCAL_PKG is a dev-only escape hatch: with --no-install there
165
+ // is no node_modules, so a repo checkout names its own packages/guano here.
166
+ let connected = false
167
+ if (connect) {
168
+ const pkgDir = existsSync(join(target, 'node_modules', '@useguano', 'guano'))
169
+ ? join(target, 'node_modules', '@useguano', 'guano')
170
+ : process.env.GUANO_CREATE_LOCAL_PKG
171
+ if (!pkgDir) {
172
+ console.error('\nClaude Desktop not connected: guano is not installed yet — run `npm run connect` inside the project later.')
173
+ } else {
174
+ try {
175
+ // the package resolves its data dir from this env at import time
176
+ const dataDir = join(target, 'data')
177
+ await mkdir(dataDir, { recursive: true, mode: 0o700 })
178
+ process.env.GUANO_DATA_DIR = dataDir
179
+ const { main } = await import(pathToFileURL(join(pkgDir, 'mcp', 'connect.mjs')).href)
180
+ const connectArgs = [...policyFlags, '--offline', '--yes']
181
+ if (opt('--config')) connectArgs.push('--config', resolve(opt('--config')))
182
+ console.log('\nConnecting Claude Desktop…')
183
+ await main(connectArgs, {
184
+ whenClaudeRunning: async () => {
185
+ if (!rl) return 'skip'
186
+ const a = (
187
+ await ask('Claude Desktop is running and overwrites its config on quit. Quit it, then press Enter (or type skip): ')
188
+ ).trim()
189
+ return /^s(kip)?$/i.test(a) ? 'skip' : 'retry'
190
+ },
191
+ })
192
+ connected = true
193
+ } catch (e) {
194
+ console.error(`\n✗ ${e?.message ?? e}\n Claude Desktop not connected — run \`npm run connect\` inside the project later.`)
195
+ }
196
+ }
197
+ }
198
+ rl?.close()
199
+
107
200
  console.log(`
108
201
  Next:
109
- cd ${dir}${installed ? '' : '\n npm install'}
110
- npm run dev → http://localhost:4174/admin
111
- `)
202
+ cd ${dir}${installed || !install ? '' : '\n npm install'}
203
+ npm run dev → http://localhost:4174/admin (create your admin account)
204
+ ${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.2",
3
+ "version": "0.1.4",
4
4
  "license": "AGPL-3.0-only",
5
5
  "publishConfig": {
6
6
  "access": "public"