@pushto/cli 0.0.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.
- package/.turbo/turbo-build.log +4 -0
- package/bin/pushto.js +2 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +46 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +87 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +21 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +48 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api.d.ts +2 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +17 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/config.d.ts +14 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +19 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/resolve-slug.d.ts +7 -0
- package/dist/lib/resolve-slug.d.ts.map +1 -0
- package/dist/lib/resolve-slug.js +24 -0
- package/dist/lib/resolve-slug.js.map +1 -0
- package/package.json +28 -0
- package/src/commands/deploy.ts +125 -0
- package/src/commands/doctor.ts +160 -0
- package/src/commands/domain.ts +54 -0
- package/src/commands/env.ts +128 -0
- package/src/commands/init.ts +65 -0
- package/src/commands/login.ts +97 -0
- package/src/commands/logs.ts +24 -0
- package/src/commands/open.ts +27 -0
- package/src/commands/rollback.ts +43 -0
- package/src/commands/status.ts +58 -0
- package/src/index.ts +88 -0
- package/src/lib/api.ts +20 -0
- package/src/lib/config.ts +30 -0
- package/src/lib/resolve-slug.ts +23 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import archiver from 'archiver';
|
|
7
|
+
import { getToken } from '../lib/config';
|
|
8
|
+
import { api } from '../lib/api';
|
|
9
|
+
import { resolveSlug } from '../lib/resolve-slug';
|
|
10
|
+
|
|
11
|
+
const IGNORE = [
|
|
12
|
+
'node_modules',
|
|
13
|
+
'.git',
|
|
14
|
+
'.next',
|
|
15
|
+
'.vercel',
|
|
16
|
+
'.turbo',
|
|
17
|
+
'dist',
|
|
18
|
+
'.pushto',
|
|
19
|
+
'.env',
|
|
20
|
+
'.env.local',
|
|
21
|
+
'.DS_Store',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const getGitInfo = (): { hash: string | null; message: string | null; branch: string } => {
|
|
25
|
+
try {
|
|
26
|
+
const hash = execSync('git rev-parse --short HEAD', { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
27
|
+
const message = execSync('git log -1 --pretty=%s', { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
28
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
29
|
+
return { hash, message, branch };
|
|
30
|
+
} catch {
|
|
31
|
+
return { hash: null, message: null, branch: 'main' };
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const zipDirectory = (dir: string): Promise<Buffer> => {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
38
|
+
const chunks: Buffer[] = [];
|
|
39
|
+
|
|
40
|
+
archive.on('data', (chunk) => chunks.push(chunk));
|
|
41
|
+
archive.on('end', () => resolve(Buffer.concat(chunks)));
|
|
42
|
+
archive.on('error', reject);
|
|
43
|
+
|
|
44
|
+
// Add files, respecting ignores
|
|
45
|
+
const addDir = (dirPath: string, prefix: string) => {
|
|
46
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (IGNORE.includes(entry.name)) continue;
|
|
49
|
+
if (entry.name.startsWith('.') && entry.name !== '.htaccess') continue;
|
|
50
|
+
|
|
51
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
52
|
+
const archivePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
53
|
+
|
|
54
|
+
if (entry.isFile()) {
|
|
55
|
+
archive.file(fullPath, { name: archivePath });
|
|
56
|
+
} else if (entry.isDirectory()) {
|
|
57
|
+
addDir(fullPath, archivePath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
addDir(dir, '');
|
|
63
|
+
archive.finalize();
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const deploy = async (): Promise<void> => {
|
|
68
|
+
const token = getToken();
|
|
69
|
+
if (!token) {
|
|
70
|
+
console.log(chalk.red('> not logged in.'));
|
|
71
|
+
console.log(chalk.dim(' run ') + chalk.cyan('pushto login') + chalk.dim(' first.'));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const slug = resolveSlug();
|
|
76
|
+
if (!slug) {
|
|
77
|
+
console.log(chalk.red('> no project here.'));
|
|
78
|
+
console.log(chalk.dim(' run ') + chalk.cyan('pushto init <name>') + chalk.dim(' to get started.'));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const spinner = ora('zipping project...').start();
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const git = getGitInfo();
|
|
86
|
+
|
|
87
|
+
// Zip the current directory
|
|
88
|
+
const zipBuffer = await zipDirectory(process.cwd());
|
|
89
|
+
const sizeMB = (zipBuffer.length / 1024 / 1024).toFixed(1);
|
|
90
|
+
spinner.text = `uploading ${sizeMB}MB...`;
|
|
91
|
+
|
|
92
|
+
// Upload to the deploy endpoint
|
|
93
|
+
const res = await api(`/cli/projects/${slug}/upload`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/zip',
|
|
97
|
+
},
|
|
98
|
+
body: zipBuffer,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const data = (await res.json()) as { error?: string };
|
|
103
|
+
spinner.fail(chalk.red(`> ${data.error ?? 'deploy failed.'}`));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const data = (await res.json()) as {
|
|
108
|
+
deployment: { version: number; fileCount: number; buildDuration: number };
|
|
109
|
+
url: string;
|
|
110
|
+
message: string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
spinner.succeed(chalk.green(`> ${data.message}`));
|
|
114
|
+
if (git.hash) {
|
|
115
|
+
console.log(chalk.dim(` commit: ${git.hash} — ${git.message ?? ''}`));
|
|
116
|
+
}
|
|
117
|
+
console.log(chalk.dim(' files: ') + chalk.cyan(String(data.deployment.fileCount)));
|
|
118
|
+
console.log(chalk.dim(' time: ') + chalk.cyan(`${data.deployment.buildDuration}s`));
|
|
119
|
+
console.log(chalk.dim(' url: ') + chalk.cyan(data.url));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const msg = err instanceof Error ? err.message : 'unknown error';
|
|
122
|
+
spinner.fail(chalk.red(`> deploy failed: ${msg}`));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getToken } from '../lib/config';
|
|
5
|
+
import { resolveSlug } from '../lib/resolve-slug';
|
|
6
|
+
import { api } from '../lib/api';
|
|
7
|
+
|
|
8
|
+
interface Check {
|
|
9
|
+
name: string;
|
|
10
|
+
status: 'pass' | 'warn' | 'fail';
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const doctor = async (): Promise<void> => {
|
|
15
|
+
console.log(chalk.green('> pushto doctor\n'));
|
|
16
|
+
|
|
17
|
+
const checks: Check[] = [];
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
|
|
20
|
+
// 1. Auth check
|
|
21
|
+
const token = getToken();
|
|
22
|
+
checks.push(token
|
|
23
|
+
? { name: 'auth', status: 'pass', message: 'logged in' }
|
|
24
|
+
: { name: 'auth', status: 'fail', message: 'not logged in. run pushto login' },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// 2. Project linked
|
|
28
|
+
const slug = resolveSlug();
|
|
29
|
+
checks.push(slug
|
|
30
|
+
? { name: 'project', status: 'pass', message: `linked to ${slug}` }
|
|
31
|
+
: { name: 'project', status: 'fail', message: 'no .pushto file. run pushto init <name>' },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// 3. Package.json exists
|
|
35
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
36
|
+
const hasPkg = fs.existsSync(pkgPath);
|
|
37
|
+
checks.push(hasPkg
|
|
38
|
+
? { name: 'package.json', status: 'pass', message: 'found' }
|
|
39
|
+
: { name: 'package.json', status: 'warn', message: 'not found. static site? that works too.' },
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// 4. Framework detection
|
|
43
|
+
if (hasPkg) {
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
46
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
47
|
+
const framework =
|
|
48
|
+
deps['next'] ? 'Next.js' :
|
|
49
|
+
deps['react'] ? 'React' :
|
|
50
|
+
deps['vue'] ? 'Vue' :
|
|
51
|
+
deps['svelte'] ? 'Svelte' :
|
|
52
|
+
deps['@sveltejs/kit'] ? 'SvelteKit' :
|
|
53
|
+
deps['astro'] ? 'Astro' :
|
|
54
|
+
deps['nuxt'] ? 'Nuxt' :
|
|
55
|
+
deps['express'] ? 'Express' :
|
|
56
|
+
deps['fastify'] ? 'Fastify' :
|
|
57
|
+
deps['hono'] ? 'Hono' :
|
|
58
|
+
null;
|
|
59
|
+
|
|
60
|
+
checks.push(framework
|
|
61
|
+
? { name: 'framework', status: 'pass', message: `detected ${framework}` }
|
|
62
|
+
: { name: 'framework', status: 'warn', message: 'no known framework detected. nixpacks will figure it out.' },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// 5. Build script
|
|
66
|
+
const hasBuild = pkg.scripts?.build;
|
|
67
|
+
checks.push(hasBuild
|
|
68
|
+
? { name: 'build script', status: 'pass', message: `"${hasBuild}"` }
|
|
69
|
+
: { name: 'build script', status: 'warn', message: 'no build script. might be fine for static sites.' },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// 6. Node version
|
|
73
|
+
const nodeVersion = pkg.engines?.node;
|
|
74
|
+
if (nodeVersion) {
|
|
75
|
+
checks.push({ name: 'node version', status: 'pass', message: nodeVersion });
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
checks.push({ name: 'package.json', status: 'warn', message: 'could not parse package.json' });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 7. .gitignore check
|
|
83
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
84
|
+
if (fs.existsSync(gitignorePath)) {
|
|
85
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
86
|
+
if (!gitignore.includes('node_modules')) {
|
|
87
|
+
checks.push({ name: '.gitignore', status: 'warn', message: 'node_modules not in .gitignore. deploy will be slow.' });
|
|
88
|
+
}
|
|
89
|
+
if (!gitignore.includes('.env')) {
|
|
90
|
+
checks.push({ name: '.gitignore', status: 'warn', message: '.env not in .gitignore. secrets might leak.' });
|
|
91
|
+
}
|
|
92
|
+
if (!gitignore.includes('.pushto')) {
|
|
93
|
+
checks.push({ name: '.gitignore', status: 'warn', message: '.pushto not in .gitignore. add it.' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 8. Env vars check (if project linked and authed)
|
|
98
|
+
if (token && slug) {
|
|
99
|
+
try {
|
|
100
|
+
const res = await api(`/cli/projects/${slug}`);
|
|
101
|
+
if (res.ok) {
|
|
102
|
+
checks.push({ name: 'api', status: 'pass', message: 'pushto.host reachable' });
|
|
103
|
+
} else {
|
|
104
|
+
checks.push({ name: 'api', status: 'fail', message: 'project not found on pushto.host' });
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
checks.push({ name: 'api', status: 'fail', message: 'cannot reach pushto.host' });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 9. Large files check
|
|
112
|
+
const largeFiles: string[] = [];
|
|
113
|
+
const checkDir = (dir: string, depth = 0) => {
|
|
114
|
+
if (depth > 3) return;
|
|
115
|
+
try {
|
|
116
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.next') continue;
|
|
119
|
+
const full = path.join(dir, entry.name);
|
|
120
|
+
if (entry.isFile()) {
|
|
121
|
+
const stat = fs.statSync(full);
|
|
122
|
+
if (stat.size > 10 * 1024 * 1024) {
|
|
123
|
+
largeFiles.push(`${entry.name} (${Math.round(stat.size / 1024 / 1024)}MB)`);
|
|
124
|
+
}
|
|
125
|
+
} else if (entry.isDirectory()) {
|
|
126
|
+
checkDir(full, depth + 1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch { /* permission error, skip */ }
|
|
130
|
+
};
|
|
131
|
+
checkDir(cwd);
|
|
132
|
+
|
|
133
|
+
if (largeFiles.length > 0) {
|
|
134
|
+
checks.push({ name: 'large files', status: 'warn', message: `found: ${largeFiles.join(', ')}. will slow deploy.` });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Print results
|
|
138
|
+
console.log();
|
|
139
|
+
for (const check of checks) {
|
|
140
|
+
const icon = check.status === 'pass' ? chalk.green('✓')
|
|
141
|
+
: check.status === 'warn' ? chalk.yellow('⚠')
|
|
142
|
+
: chalk.red('✗');
|
|
143
|
+
const color = check.status === 'pass' ? chalk.dim
|
|
144
|
+
: check.status === 'warn' ? chalk.yellow
|
|
145
|
+
: chalk.red;
|
|
146
|
+
console.log(` ${icon} ${chalk.dim(check.name.padEnd(16))} ${color(check.message)}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const fails = checks.filter((c) => c.status === 'fail');
|
|
150
|
+
const warns = checks.filter((c) => c.status === 'warn');
|
|
151
|
+
|
|
152
|
+
console.log();
|
|
153
|
+
if (fails.length > 0) {
|
|
154
|
+
console.log(chalk.red(` ${fails.length} issue${fails.length > 1 ? 's' : ''} to fix before deploying.`));
|
|
155
|
+
} else if (warns.length > 0) {
|
|
156
|
+
console.log(chalk.yellow(` ${warns.length} warning${warns.length > 1 ? 's' : ''}. probably fine. deploy when ready.`));
|
|
157
|
+
} else {
|
|
158
|
+
console.log(chalk.green(' all good. run pushto to deploy.'));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getToken } from '../lib/config';
|
|
3
|
+
import { resolveSlug } from '../lib/resolve-slug';
|
|
4
|
+
|
|
5
|
+
export const domain = async (action: string, domainName?: string): Promise<void> => {
|
|
6
|
+
const token = getToken();
|
|
7
|
+
if (!token) {
|
|
8
|
+
console.log(chalk.red('> not logged in.'));
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const slug = resolveSlug();
|
|
13
|
+
if (!slug) {
|
|
14
|
+
console.log(chalk.red('> no project here. run pushto init first.'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (action === 'add') {
|
|
19
|
+
if (!domainName) {
|
|
20
|
+
console.log(chalk.red('> usage: pushto domain add yourdomain.com'));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(chalk.green(`> setting up ${domainName} for ${slug}\n`));
|
|
25
|
+
console.log(chalk.dim(' add this DNS record at your domain provider:\n'));
|
|
26
|
+
console.log(` ${chalk.cyan('Type')} ${chalk.cyan('Name')} ${chalk.cyan('Value')}`);
|
|
27
|
+
console.log(` CNAME @ ${slug}.pushto.host`);
|
|
28
|
+
console.log();
|
|
29
|
+
console.log(chalk.dim(' or if your provider doesn\'t support CNAME on root:\n'));
|
|
30
|
+
console.log(` A @ 76.76.21.21`);
|
|
31
|
+
console.log();
|
|
32
|
+
|
|
33
|
+
// TODO: when Cloudflare DNS API is wired, this will:
|
|
34
|
+
// 1. Register the domain in our system
|
|
35
|
+
// 2. Provision SSL via Let's Encrypt
|
|
36
|
+
// 3. Poll DNS until verified
|
|
37
|
+
// 4. Update the project's customDomain field
|
|
38
|
+
|
|
39
|
+
console.log(chalk.yellow(' auto-verification coming soon.'));
|
|
40
|
+
console.log(chalk.dim(' for now, add the domain in your dashboard too:'));
|
|
41
|
+
console.log(chalk.cyan(` https://pushto.host/dashboard/projects/${slug}/settings`));
|
|
42
|
+
|
|
43
|
+
} else if (action === 'remove' || action === 'rm') {
|
|
44
|
+
console.log(chalk.green(`> removing custom domain from ${slug}`));
|
|
45
|
+
console.log(chalk.yellow(' domain management coming soon via CLI.'));
|
|
46
|
+
console.log(chalk.dim(' use the dashboard for now:'));
|
|
47
|
+
console.log(chalk.cyan(` https://pushto.host/dashboard/projects/${slug}/settings`));
|
|
48
|
+
|
|
49
|
+
} else {
|
|
50
|
+
console.log(chalk.dim(' usage:'));
|
|
51
|
+
console.log(chalk.cyan(' pushto domain add yourdomain.com'));
|
|
52
|
+
console.log(chalk.cyan(' pushto domain remove'));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getToken } from '../lib/config';
|
|
5
|
+
import { resolveSlug } from '../lib/resolve-slug';
|
|
6
|
+
import { api } from '../lib/api';
|
|
7
|
+
|
|
8
|
+
export const env = async (action: string, args: string[]): Promise<void> => {
|
|
9
|
+
const token = getToken();
|
|
10
|
+
if (!token) {
|
|
11
|
+
console.log(chalk.red('> not logged in.'));
|
|
12
|
+
console.log(chalk.dim(' run ') + chalk.cyan('pushto login') + chalk.dim(' first.'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const slug = resolveSlug();
|
|
17
|
+
if (!slug) {
|
|
18
|
+
console.log(chalk.red('> no project here. run pushto init first.'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (action === 'set') {
|
|
23
|
+
// pushto env set KEY=value
|
|
24
|
+
const pair = args[0];
|
|
25
|
+
if (!pair || !pair.includes('=')) {
|
|
26
|
+
console.log(chalk.red('> usage: pushto env set KEY=value'));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const eqIdx = pair.indexOf('=');
|
|
31
|
+
const key = pair.slice(0, eqIdx);
|
|
32
|
+
const value = pair.slice(eqIdx + 1);
|
|
33
|
+
|
|
34
|
+
if (!key) {
|
|
35
|
+
console.log(chalk.red('> key is empty.'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Call the tRPC mutation via REST-like endpoint
|
|
40
|
+
// We'll use a simple CLI API endpoint for this
|
|
41
|
+
const res = await api(`/cli/projects/${slug}/env`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body: JSON.stringify({ key, value }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const data = (await res.json()) as { error?: string };
|
|
48
|
+
console.log(chalk.red(`> ${data.error ?? 'failed to set env var.'}`));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(chalk.green(`> ${key} set.`));
|
|
53
|
+
|
|
54
|
+
} else if (action === 'list' || action === 'ls') {
|
|
55
|
+
// pushto env list
|
|
56
|
+
const res = await api(`/cli/projects/${slug}/env`);
|
|
57
|
+
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const data = (await res.json()) as { error?: string };
|
|
60
|
+
console.log(chalk.red(`> ${data.error ?? 'failed to list env vars.'}`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const vars = (await res.json()) as { key: string; value: string }[];
|
|
65
|
+
|
|
66
|
+
if (vars.length === 0) {
|
|
67
|
+
console.log(chalk.dim('> no env vars set.'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(chalk.green(`> ${slug} env vars\n`));
|
|
72
|
+
for (const v of vars) {
|
|
73
|
+
console.log(` ${chalk.cyan(v.key)}=${chalk.dim('••••••••')}`);
|
|
74
|
+
}
|
|
75
|
+
console.log(chalk.dim(`\n ${vars.length} variable${vars.length > 1 ? 's' : ''}`));
|
|
76
|
+
|
|
77
|
+
} else if (action === 'pull') {
|
|
78
|
+
// pushto env pull — download to .env
|
|
79
|
+
const res = await api(`/cli/projects/${slug}/env`);
|
|
80
|
+
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const data = (await res.json()) as { error?: string };
|
|
83
|
+
console.log(chalk.red(`> ${data.error ?? 'failed to pull env vars.'}`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const vars = (await res.json()) as { key: string; value: string }[];
|
|
88
|
+
|
|
89
|
+
if (vars.length === 0) {
|
|
90
|
+
console.log(chalk.dim('> no env vars to pull.'));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const envContent = vars.map((v) => `${v.key}=${v.value}`).join('\n') + '\n';
|
|
95
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
96
|
+
|
|
97
|
+
fs.writeFileSync(envPath, envContent);
|
|
98
|
+
console.log(chalk.green(`> pulled ${vars.length} var${vars.length > 1 ? 's' : ''} to .env`));
|
|
99
|
+
console.log(chalk.dim(' make sure .env is in your .gitignore.'));
|
|
100
|
+
|
|
101
|
+
} else if (action === 'rm' || action === 'remove' || action === 'delete') {
|
|
102
|
+
const key = args[0];
|
|
103
|
+
if (!key) {
|
|
104
|
+
console.log(chalk.red('> usage: pushto env rm KEY'));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const res = await api(`/cli/projects/${slug}/env`, {
|
|
109
|
+
method: 'DELETE',
|
|
110
|
+
body: JSON.stringify({ key }),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
const data = (await res.json()) as { error?: string };
|
|
115
|
+
console.log(chalk.red(`> ${data.error ?? 'failed to delete env var.'}`));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(chalk.green(`> ${key} removed.`));
|
|
120
|
+
|
|
121
|
+
} else {
|
|
122
|
+
console.log(chalk.dim(' usage:'));
|
|
123
|
+
console.log(chalk.cyan(' pushto env set KEY=value'));
|
|
124
|
+
console.log(chalk.cyan(' pushto env list'));
|
|
125
|
+
console.log(chalk.cyan(' pushto env pull'));
|
|
126
|
+
console.log(chalk.cyan(' pushto env rm KEY'));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { getToken, setProjectSlug } from '../lib/config';
|
|
6
|
+
import { api } from '../lib/api';
|
|
7
|
+
|
|
8
|
+
export const init = async (name: string): Promise<void> => {
|
|
9
|
+
const token = getToken();
|
|
10
|
+
if (!token) {
|
|
11
|
+
console.log(chalk.red('> not logged in.'));
|
|
12
|
+
console.log(chalk.dim(' run ') + chalk.cyan('pushto login') + chalk.dim(' first.'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const slug = name
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
19
|
+
.replace(/-+/g, '-')
|
|
20
|
+
.replace(/^-|-$/g, '');
|
|
21
|
+
|
|
22
|
+
if (slug.length < 3) {
|
|
23
|
+
console.log(chalk.red('> name too short. needs at least 3 characters.'));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const spinner = ora('creating project...').start();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const res = await api('/cli/projects', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
body: JSON.stringify({ name, slug }),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const data = (await res.json()) as {
|
|
36
|
+
error?: string;
|
|
37
|
+
project?: { id: string };
|
|
38
|
+
message?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
spinner.fail(chalk.red(`> ${data.error ?? 'something went wrong.'}`));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Write .pushto file in current directory
|
|
47
|
+
const pushtoConfig = { slug, projectId: data.project?.id };
|
|
48
|
+
fs.writeFileSync(
|
|
49
|
+
path.join(process.cwd(), '.pushto'),
|
|
50
|
+
JSON.stringify(pushtoConfig, null, 2) + '\n',
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Also store globally so bare `pushto` works from this dir
|
|
54
|
+
setProjectSlug(slug);
|
|
55
|
+
|
|
56
|
+
spinner.succeed(chalk.green(`> ${slug} is ready.`));
|
|
57
|
+
console.log(chalk.dim(` ${data.message ?? 'project created.'}`));
|
|
58
|
+
console.log();
|
|
59
|
+
console.log(chalk.dim(' deploy anytime with just: ') + chalk.cyan('pushto'));
|
|
60
|
+
console.log(chalk.dim(' your url will be: ') + chalk.cyan(`${slug}.pushto.host`));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
spinner.fail(chalk.red('> could not reach pushto.host. check your internet.'));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { setToken, config } from '../lib/config';
|
|
4
|
+
import { api } from '../lib/api';
|
|
5
|
+
|
|
6
|
+
export const login = async (): Promise<void> => {
|
|
7
|
+
const baseUrl = config.get('apiUrl');
|
|
8
|
+
|
|
9
|
+
// Start a local server to receive the callback
|
|
10
|
+
const server = http.createServer((req, res) => {
|
|
11
|
+
// Handle CORS preflight
|
|
12
|
+
if (req.method === 'OPTIONS') {
|
|
13
|
+
res.writeHead(200, {
|
|
14
|
+
'Access-Control-Allow-Origin': baseUrl,
|
|
15
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
16
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
17
|
+
});
|
|
18
|
+
res.end();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (req.method === 'POST' && req.url === '/callback') {
|
|
23
|
+
let body = '';
|
|
24
|
+
req.on('data', (chunk) => (body += chunk));
|
|
25
|
+
req.on('end', async () => {
|
|
26
|
+
res.writeHead(200, {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'Access-Control-Allow-Origin': baseUrl,
|
|
29
|
+
});
|
|
30
|
+
res.end(JSON.stringify({ ok: true }));
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { access_token } = JSON.parse(body);
|
|
34
|
+
if (!access_token) {
|
|
35
|
+
console.log(chalk.red('\n> auth failed. no token received.'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setToken(access_token);
|
|
40
|
+
|
|
41
|
+
// Verify the token
|
|
42
|
+
const meRes = await api('/cli/me');
|
|
43
|
+
if (meRes.ok) {
|
|
44
|
+
const me = (await meRes.json()) as { email: string; tier: string };
|
|
45
|
+
console.log(chalk.green(`\n> logged in as ${me.email}`));
|
|
46
|
+
console.log(chalk.dim(` tier: ${me.tier.toLowerCase()}`));
|
|
47
|
+
} else {
|
|
48
|
+
console.log(chalk.green('\n> logged in.'));
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
console.log(chalk.green('\n> logged in.'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
server.close();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
res.writeHead(404);
|
|
61
|
+
res.end();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Listen on a random available port
|
|
65
|
+
server.listen(0, () => {
|
|
66
|
+
const port = (server.address() as { port: number }).port;
|
|
67
|
+
const authUrl = `${baseUrl}/cli/auth?port=${port}`;
|
|
68
|
+
|
|
69
|
+
console.log(chalk.dim('> opening browser to log you in...'));
|
|
70
|
+
console.log(chalk.dim(` if it doesn't open, go to: `) + chalk.cyan(authUrl));
|
|
71
|
+
|
|
72
|
+
// Open browser
|
|
73
|
+
const open = (url: string) => {
|
|
74
|
+
const { execSync } = require('node:child_process');
|
|
75
|
+
const cmd =
|
|
76
|
+
process.platform === 'darwin'
|
|
77
|
+
? `open "${url}"`
|
|
78
|
+
: process.platform === 'win32'
|
|
79
|
+
? `start "${url}"`
|
|
80
|
+
: `xdg-open "${url}"`;
|
|
81
|
+
try {
|
|
82
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
83
|
+
} catch {
|
|
84
|
+
// User will see the URL in the console
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
open(authUrl);
|
|
89
|
+
|
|
90
|
+
// Timeout after 2 minutes
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
console.log(chalk.red('\n> login timed out. try again.'));
|
|
93
|
+
server.close();
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}, 120_000);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getToken } from '../lib/config';
|
|
3
|
+
import { resolveSlug } from '../lib/resolve-slug';
|
|
4
|
+
|
|
5
|
+
export const logs = async (): Promise<void> => {
|
|
6
|
+
const token = getToken();
|
|
7
|
+
if (!token) {
|
|
8
|
+
console.log(chalk.red('> not logged in.'));
|
|
9
|
+
console.log(chalk.dim(' run ') + chalk.cyan('pushto login') + chalk.dim(' first.'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const slug = resolveSlug();
|
|
14
|
+
if (!slug) {
|
|
15
|
+
console.log(chalk.red('> no project here. run pushto init first.'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(chalk.dim(`> streaming logs for ${slug}...`));
|
|
20
|
+
console.log(chalk.dim(' (ctrl+c to stop)\n'));
|
|
21
|
+
|
|
22
|
+
// TODO: Phase 3 — SSE connection to log stream API
|
|
23
|
+
console.log(chalk.yellow('> log streaming coming soon. deploy first, then we\'ll show you everything.'));
|
|
24
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { resolveSlug } from '../lib/resolve-slug';
|
|
4
|
+
|
|
5
|
+
export const open = async (): Promise<void> => {
|
|
6
|
+
const slug = resolveSlug();
|
|
7
|
+
if (!slug) {
|
|
8
|
+
console.log(chalk.red('> no project here. run pushto init first.'));
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const url = `https://${slug}.pushto.host`;
|
|
13
|
+
console.log(chalk.green(`> opening ${url}`));
|
|
14
|
+
|
|
15
|
+
const cmd =
|
|
16
|
+
process.platform === 'darwin'
|
|
17
|
+
? `open "${url}"`
|
|
18
|
+
: process.platform === 'win32'
|
|
19
|
+
? `start "${url}"`
|
|
20
|
+
: `xdg-open "${url}"`;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
24
|
+
} catch {
|
|
25
|
+
console.log(chalk.dim(` couldn't open browser. go to: `) + chalk.cyan(url));
|
|
26
|
+
}
|
|
27
|
+
};
|