@peume/cli 0.0.0-stage → 0.1.0
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 +27 -2
- package/package.json +24 -4
- package/peume.mjs +167 -0
package/README.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @peume/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Put your app online from Claude Code or your terminal. No account, no GitHub. Free on `yourapp.peume.com`.
|
|
4
|
+
|
|
5
|
+
## Claude Code
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
claude mcp add peume -- npx -y @peume/cli mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then ask: **“Deploy this on Peume”**. Claude gets your link, and if the build fails it reads the logs and fixes the code.
|
|
12
|
+
|
|
13
|
+
## Terminal
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @peume/cli deploy # deploy the current folder and follow the build
|
|
17
|
+
npx @peume/cli status # URL, state and last build
|
|
18
|
+
npx @peume/cli logs # runtime logs
|
|
19
|
+
npx @peume/cli env set KEY=value # set variables, then redeploy
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Works with Node, Next.js, React/Vite, Python, plain HTML or a Dockerfile.
|
|
23
|
+
- Your app must listen on the `PORT` environment variable.
|
|
24
|
+
- Variables in `.env` are set on the app, never stored with the code.
|
|
25
|
+
- `node_modules`, `.git` and build caches are not uploaded.
|
|
26
|
+
- A new app stops after 24 h unless you claim it (free) with the link shown after the deploy.
|
|
27
|
+
|
|
28
|
+
https://cloud.peume.com
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peume/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deploy your app on Peume from Claude Code or your terminal. No account, no GitHub.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"peume": "peume.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"peume.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"deploy",
|
|
19
|
+
"hosting",
|
|
20
|
+
"mcp",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"vibe-coding",
|
|
23
|
+
"paas"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://cloud.peume.com"
|
|
26
|
+
}
|
package/peume.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Peume CLI et serveur MCP (même fichier) : envoie le dossier courant sur Peume, suit le build, rend l'adresse de l'app.
|
|
3
|
+
// peume deploy [dossier] peume status peume logs peume env set CLE=valeur… peume mcp (pour Claude Code)
|
|
4
|
+
// Le nom de l'app est gardé dans <dossier>/.peume ; le jeton qui permet de la redéployer reste sur la machine (~/.peume/jetons.json).
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
7
|
+
import { join, resolve, basename } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { createInterface } from 'node:readline';
|
|
10
|
+
|
|
11
|
+
const PEUME = (process.env.PEUME_URL || 'https://cloud.peume.com').replace(/\/$/, '');
|
|
12
|
+
const JETONS = join(homedir(), '.peume', 'jetons.json');
|
|
13
|
+
const EXCLUS = ['node_modules', '.git', '.next', '.nuxt', '.svelte-kit', '.vercel', '.turbo', '.cache', '.DS_Store', '.peume', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '*.log'];
|
|
14
|
+
const VERSION = '0.1.0';
|
|
15
|
+
|
|
16
|
+
const lireJSON = async (f, d) => { try { return JSON.parse(await readFile(f, 'utf8')); } catch { return d; } };
|
|
17
|
+
async function lien(dossier) {
|
|
18
|
+
const app = (await lireJSON(join(dossier, '.peume'), {})).app;
|
|
19
|
+
return { app, jeton: app ? (await lireJSON(JETONS, {}))[app] : undefined };
|
|
20
|
+
}
|
|
21
|
+
async function retenir(dossier, app, jeton) {
|
|
22
|
+
await writeFile(join(dossier, '.peume'), JSON.stringify({ app }, null, 2) + '\n');
|
|
23
|
+
if (jeton) {
|
|
24
|
+
const j = await lireJSON(JETONS, {}); j[app] = jeton;
|
|
25
|
+
await mkdir(join(homedir(), '.peume'), { recursive: true });
|
|
26
|
+
await writeFile(JETONS, JSON.stringify(j, null, 2), { mode: 0o600 });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// dossier → .tar.gz en mémoire (tar est présent sur macOS, Linux et Windows 10+)
|
|
31
|
+
function empaqueter(dossier) {
|
|
32
|
+
return new Promise((ok, ko) => {
|
|
33
|
+
const p = spawn('tar', ['-czf', '-', ...EXCLUS.map(e => `--exclude=${e}`), '-C', dossier, '.'], { env: { ...process.env, COPYFILE_DISABLE: '1' } });
|
|
34
|
+
const d = [], err = [];
|
|
35
|
+
p.stdout.on('data', x => d.push(x)); p.stderr.on('data', x => err.push(x));
|
|
36
|
+
p.on('error', e => ko(new Error(`tar is needed to pack the folder: ${e.message}`)));
|
|
37
|
+
p.on('close', c => c === 0 ? ok(Buffer.concat(d)) : ko(new Error(`Could not pack the folder: ${Buffer.concat(err).toString().slice(0, 300)}`)));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function appel(chemin, { methode = 'GET', corps, entetes = {}, delai = 30000 } = {}) {
|
|
42
|
+
let r;
|
|
43
|
+
try { r = await fetch(`${PEUME}${chemin}`, { method: methode, body: corps, headers: { 'user-agent': `peume-cli/${VERSION}`, ...entetes }, signal: AbortSignal.timeout(delai) }); }
|
|
44
|
+
catch (e) { throw new Error(e.name === 'TimeoutError' ? 'Peume did not answer in time. Try again.' : `Could not reach Peume (${e.message}).`); }
|
|
45
|
+
const j = await r.json().catch(() => ({}));
|
|
46
|
+
if (!r.ok) throw new Error(j.error || `Peume answered ${r.status}.`);
|
|
47
|
+
return j;
|
|
48
|
+
}
|
|
49
|
+
const auth = jeton => (jeton ? { 'x-peume-jeton': jeton } : {});
|
|
50
|
+
|
|
51
|
+
// envoie le dossier puis suit le build (10 min max) ; surLigne reçoit chaque nouvelle ligne de log
|
|
52
|
+
export async function deployer(dossier, { nom, surLigne = () => {}, surEtape = () => {} } = {}) {
|
|
53
|
+
dossier = resolve(dossier);
|
|
54
|
+
const { app, jeton } = await lien(dossier);
|
|
55
|
+
surEtape('Packing the folder…');
|
|
56
|
+
const archive = await empaqueter(dossier);
|
|
57
|
+
if (archive.length > 50e6) throw new Error(`The folder is ${Math.round(archive.length / 1e6)} MB once packed (50 MB max). Move big files out of it.`);
|
|
58
|
+
surEtape(`Uploading ${Math.max(1, Math.round(archive.length / 1e3))} KB…`);
|
|
59
|
+
const r = await appel('/api/deploy', { methode: 'POST', corps: archive, delai: 180000, entetes: {
|
|
60
|
+
'content-type': 'application/gzip', 'x-peume-nom': nom || basename(dossier), ...(app ? { 'x-peume-app': app } : {}), ...auth(jeton) } });
|
|
61
|
+
await retenir(dossier, r.id, r.jeton);
|
|
62
|
+
surEtape(`Building ${r.url}…`);
|
|
63
|
+
let vus = 0, e = r;
|
|
64
|
+
const fin = Date.now() + 10 * 60e3;
|
|
65
|
+
while (Date.now() < fin) {
|
|
66
|
+
await new Promise(t => setTimeout(t, 2000));
|
|
67
|
+
try { e = await appel(`/api/apps/${r.id}`, { entetes: auth(r.jeton || jeton) }); } catch { continue; }
|
|
68
|
+
const logs = e.deploiement?.logs || [];
|
|
69
|
+
for (const l of logs.slice(vus)) surLigne(l);
|
|
70
|
+
vus = logs.length;
|
|
71
|
+
if (['live', 'failed', 'cancelled'].includes(e.deploiement?.statut)) break;
|
|
72
|
+
}
|
|
73
|
+
return e;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const resume = e => {
|
|
77
|
+
const ok = e.deploiement?.statut === 'live';
|
|
78
|
+
const l = [ok ? `Live: ${e.url}` : e.deploiement?.statut === 'failed' ? `Build failed for ${e.url}.` : `Still building: ${e.url} (check again with "npx @peume/cli status").`];
|
|
79
|
+
if (!ok && e.deploiement?.logs?.length) l.push('', 'Last build lines:', ...e.deploiement.logs.slice(-40));
|
|
80
|
+
if (!e.reclamee && e.lienClaim) l.push('', `This app stops in 24 h unless you claim it (free): ${e.lienClaim}`);
|
|
81
|
+
return l.join('\n');
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
async function etat(dossier) {
|
|
85
|
+
const { app, jeton } = await lien(resolve(dossier));
|
|
86
|
+
if (!app) throw new Error('This folder is not deployed yet. Run "npx @peume/cli deploy" first.');
|
|
87
|
+
return { app, jeton, e: await appel(`/api/apps/${app}`, { entetes: auth(jeton) }) };
|
|
88
|
+
}
|
|
89
|
+
async function logs(dossier) {
|
|
90
|
+
const { app, jeton } = await lien(resolve(dossier));
|
|
91
|
+
if (!app) throw new Error('This folder is not deployed yet.');
|
|
92
|
+
return (await appel(`/api/apps/${app}/logs`, { entetes: auth(jeton) })).logs;
|
|
93
|
+
}
|
|
94
|
+
async function variables(dossier, paires) {
|
|
95
|
+
const { app, jeton } = await lien(resolve(dossier));
|
|
96
|
+
if (!app) throw new Error('This folder is not deployed yet.');
|
|
97
|
+
const vars = Object.entries(paires).map(([key, value]) => ({ key, value: String(value) }));
|
|
98
|
+
await appel(`/api/apps/${app}/env`, { methode: 'PUT', corps: JSON.stringify({ vars, fusion: true }), entetes: { 'content-type': 'application/json', ...auth(jeton) } });
|
|
99
|
+
return appel(`/api/apps/${app}/redeploy`, { methode: 'POST', entetes: auth(jeton) });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ——— serveur MCP (stdio) pour Claude Code : claude mcp add peume -- npx -y @peume/cli mcp
|
|
103
|
+
const OUTILS = [
|
|
104
|
+
{ name: 'deploy', description: 'Deploy a project folder on Peume (free, no account needed) and wait for the build. Returns the live URL (name.peume.com), or the build logs if it failed so you can fix the code and deploy again. The app must listen on the PORT environment variable. Variables in .env are set on the app, never uploaded as files.',
|
|
105
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the project folder.' }, name: { type: 'string', description: 'Wanted app name for the first deploy (becomes name.peume.com). Defaults to the folder name.' } }, required: ['path'] } },
|
|
106
|
+
{ name: 'status', description: 'Status of the Peume app deployed from this folder: URL, state and the latest build logs.',
|
|
107
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the project folder.' } }, required: ['path'] } },
|
|
108
|
+
{ name: 'logs', description: 'Runtime logs of the running Peume app deployed from this folder.',
|
|
109
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the project folder.' } }, required: ['path'] } },
|
|
110
|
+
{ name: 'set_env', description: 'Set environment variables on the Peume app deployed from this folder, then redeploy it.',
|
|
111
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path of the project folder.' }, vars: { type: 'object', additionalProperties: { type: 'string' }, description: 'Variables to set, e.g. {"API_KEY": "…"}.' } }, required: ['path', 'vars'] } },
|
|
112
|
+
];
|
|
113
|
+
async function outil(nom, args = {}) {
|
|
114
|
+
if (!args.path) throw new Error('path is required.');
|
|
115
|
+
if (nom === 'deploy') return resume(await deployer(args.path, { nom: args.name }));
|
|
116
|
+
if (nom === 'status') { const { e } = await etat(args.path); return `${e.url} — ${e.statut}${e.deploiement ? ` (last build: ${e.deploiement.statut}, ${e.deploiement.commit})` : ''}\n${resume(e)}`; }
|
|
117
|
+
if (nom === 'logs') return (await logs(args.path)).join('\n') || 'No logs yet.';
|
|
118
|
+
if (nom === 'set_env') { const e = await variables(args.path, args.vars || {}); return `Variables set (${Object.keys(args.vars || {}).join(', ')}). Redeploying ${e.url}.`; }
|
|
119
|
+
throw new Error(`Unknown tool ${nom}.`);
|
|
120
|
+
}
|
|
121
|
+
function mcp() {
|
|
122
|
+
const envoyer = m => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...m }) + '\n');
|
|
123
|
+
createInterface({ input: process.stdin }).on('line', async l => {
|
|
124
|
+
let m; try { m = JSON.parse(l); } catch { return; }
|
|
125
|
+
if (m.id === undefined) return; // notifications
|
|
126
|
+
try {
|
|
127
|
+
if (m.method === 'initialize') return envoyer({ id: m.id, result: { protocolVersion: m.params?.protocolVersion || '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'peume', version: VERSION } } });
|
|
128
|
+
if (m.method === 'ping') return envoyer({ id: m.id, result: {} });
|
|
129
|
+
if (m.method === 'tools/list') return envoyer({ id: m.id, result: { tools: OUTILS } });
|
|
130
|
+
if (m.method === 'tools/call') {
|
|
131
|
+
try { return envoyer({ id: m.id, result: { content: [{ type: 'text', text: await outil(m.params?.name, m.params?.arguments) }] } }); }
|
|
132
|
+
catch (e) { return envoyer({ id: m.id, result: { content: [{ type: 'text', text: e.message }], isError: true } }); }
|
|
133
|
+
}
|
|
134
|
+
envoyer({ id: m.id, error: { code: -32601, message: `Unknown method ${m.method}` } });
|
|
135
|
+
} catch (e) { envoyer({ id: m.id, error: { code: -32603, message: e.message } }); }
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ——— ligne de commande
|
|
140
|
+
const AIDE = `Peume — deploy your app, no account, no GitHub.
|
|
141
|
+
|
|
142
|
+
npx @peume/cli deploy [folder] deploy the folder (default: current) and follow the build
|
|
143
|
+
npx @peume/cli status URL, state and last build
|
|
144
|
+
npx @peume/cli logs runtime logs
|
|
145
|
+
npx @peume/cli env set KEY=value… set variables, then redeploy
|
|
146
|
+
npx @peume/cli mcp MCP server for Claude Code:
|
|
147
|
+
claude mcp add peume -- npx -y @peume/cli mcp
|
|
148
|
+
`;
|
|
149
|
+
const gris = s => process.stdout.isTTY ? `\x1b[2m${s}\x1b[0m` : s;
|
|
150
|
+
async function principal([cmd = 'help', ...args]) {
|
|
151
|
+
if (cmd === 'mcp') return mcp();
|
|
152
|
+
try {
|
|
153
|
+
if (cmd === 'deploy') {
|
|
154
|
+
const e = await deployer(args.find(a => !a.startsWith('-')) || '.', { surEtape: s => console.log(s), surLigne: l => console.log(gris(` ${l}`)) });
|
|
155
|
+
console.log('\n' + resume({ ...e, deploiement: e.deploiement && { ...e.deploiement, logs: e.deploiement.statut === 'live' ? [] : e.deploiement.logs } }));
|
|
156
|
+
process.exitCode = e.deploiement?.statut === 'live' ? 0 : 1;
|
|
157
|
+
} else if (cmd === 'status') console.log(await outil('status', { path: '.' }));
|
|
158
|
+
else if (cmd === 'logs') console.log(await outil('logs', { path: '.' }));
|
|
159
|
+
else if (cmd === 'env' && args[0] === 'set') {
|
|
160
|
+
const vars = Object.fromEntries(args.slice(1).map(a => { const i = a.indexOf('='); return i > 0 ? [a.slice(0, i), a.slice(i + 1)] : null; }).filter(Boolean));
|
|
161
|
+
if (!Object.keys(vars).length) throw new Error('Usage: npx @peume/cli env set KEY=value');
|
|
162
|
+
console.log(await outil('set_env', { path: '.', vars }));
|
|
163
|
+
} else if (cmd === '--version' || cmd === '-v') console.log(VERSION);
|
|
164
|
+
else console.log(AIDE);
|
|
165
|
+
} catch (e) { console.error(e.message); process.exitCode = 1; }
|
|
166
|
+
}
|
|
167
|
+
principal(process.argv.slice(2));
|