@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.
- package/README.md +15 -6
- package/index.js +157 -19
- 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`),
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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,
|
|
3
|
-
// install it
|
|
4
|
-
//
|
|
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
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
81
|
-
Your published site is served at http://localhost
|
|
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
|
|
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' : ''}`)
|