@glash/cli 0.1.2 → 0.2.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/bin/glash.mjs +6 -3
- package/commands/deploy.mjs +78 -6
- package/commands/deployments.mjs +1 -1
- package/commands/pull.mjs +109 -10
- package/lib/zip.mjs +28 -1
- package/package.json +2 -2
package/bin/glash.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { argv, exit } from 'node:process';
|
|
|
3
3
|
import { parseArgs } from '../lib/args.mjs';
|
|
4
4
|
import { info, fail, color } from '../lib/ui.mjs';
|
|
5
5
|
|
|
6
|
-
const VERSION = '0.1
|
|
6
|
+
const VERSION = '0.2.1';
|
|
7
7
|
|
|
8
8
|
const sub = argv[2];
|
|
9
9
|
const rest = argv.slice(3);
|
|
@@ -55,7 +55,7 @@ ${color.bold('PROJECTS')}
|
|
|
55
55
|
open Open the project's primary domain in a browser
|
|
56
56
|
|
|
57
57
|
${color.bold('DEPLOY')}
|
|
58
|
-
deploy [--prod] [--detach] Deploy the linked project
|
|
58
|
+
deploy [--prod] [--detach] Deploy the linked project, syncing .env files first
|
|
59
59
|
deployments List recent deployments
|
|
60
60
|
status <id> Show deployment pipeline + events
|
|
61
61
|
logs <id> Stream deployment events (alias of status)
|
|
@@ -69,12 +69,15 @@ ${color.bold('ENV VARS')}
|
|
|
69
69
|
env:pull [--out .env.local] Write env vars to a local .env file
|
|
70
70
|
env:push [<file>] Bulk upload from a .env file
|
|
71
71
|
|
|
72
|
-
${color.bold('
|
|
72
|
+
${color.bold('PULL')}
|
|
73
|
+
pull [<slug>] [--out dir] [--no-env] Pull source + .env from a glashDB project
|
|
73
74
|
pull --from vercel --project <id> Pull source + env from a Vercel project
|
|
74
75
|
|
|
75
76
|
${color.bold('FLAGS')}
|
|
76
77
|
--project <slug|id> Override the linked project for this command
|
|
77
78
|
--environment <name> Env-var environment (default: production)
|
|
79
|
+
--env-file <file[,file]> Env file(s) to sync before deploy
|
|
80
|
+
--no-env Skip automatic env sync during deploy
|
|
78
81
|
--help, -h Show this help
|
|
79
82
|
--version, -v Show CLI version
|
|
80
83
|
|
package/commands/deploy.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { execSync } from 'node:child_process';
|
|
|
5
5
|
import { zipDirectory } from '../lib/zip.mjs';
|
|
6
6
|
import { join, basename } from 'node:path';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
|
-
import { readFile, rm } from 'node:fs/promises';
|
|
8
|
+
import { access, readFile, rm } from 'node:fs/promises';
|
|
9
9
|
|
|
10
10
|
function currentCommit() {
|
|
11
11
|
try {
|
|
@@ -76,9 +76,17 @@ export async function deploy({ flags }) {
|
|
|
76
76
|
const target = flags.prod || flags.production ? 'production' : (flags.target || 'production');
|
|
77
77
|
|
|
78
78
|
let dep;
|
|
79
|
+
|
|
80
|
+
if (!flags['no-env']) {
|
|
81
|
+
try {
|
|
82
|
+
await syncEnvBeforeDeploy(projectId, target, flags);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return fail(`Env sync failed: ${e.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
79
87
|
|
|
80
|
-
if (!
|
|
81
|
-
info(color.dim(`→
|
|
88
|
+
if (!flags.git && !flags.repo && !flags.remote) {
|
|
89
|
+
info(color.dim(`→ uploading local source for project ${projectId} (${target})`));
|
|
82
90
|
const zipPath = join(tmpdir(), `glash-deploy-${Date.now()}.zip`);
|
|
83
91
|
try {
|
|
84
92
|
await zipDirectory(process.cwd(), zipPath);
|
|
@@ -88,6 +96,10 @@ export async function deploy({ flags }) {
|
|
|
88
96
|
// We wrap the buffer in a Blob so fetch treats it as a file
|
|
89
97
|
formData.append('file', new Blob([zipBuffer]), 'project.zip');
|
|
90
98
|
|
|
99
|
+
formData.append('commitHash', commitHash || 'local-upload');
|
|
100
|
+
formData.append('gitBranch', gitBranch || '');
|
|
101
|
+
formData.append('target', target);
|
|
102
|
+
|
|
91
103
|
dep = await apiPost('/deployments/upload', formData);
|
|
92
104
|
await rm(zipPath, { force: true }).catch(() => {});
|
|
93
105
|
} catch (e) {
|
|
@@ -95,7 +107,7 @@ export async function deploy({ flags }) {
|
|
|
95
107
|
return fail(`Packaging failed: ${e.message}`);
|
|
96
108
|
}
|
|
97
109
|
} else {
|
|
98
|
-
info(color.dim(`→ deploying project ${projectId}${commitHash ? ` @ ${commitHash.slice(0, 7)}` : ''} (${target})`));
|
|
110
|
+
info(color.dim(`→ deploying connected repo for project ${projectId}${commitHash ? ` @ ${commitHash.slice(0, 7)}` : ''} (${target})`));
|
|
99
111
|
try {
|
|
100
112
|
dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target });
|
|
101
113
|
} catch (e) {
|
|
@@ -127,10 +139,10 @@ export async function deploy({ flags }) {
|
|
|
127
139
|
info(` ${tag} ${color.dim(ev.step.padEnd(8))} ${ev.message ?? ''}`);
|
|
128
140
|
}
|
|
129
141
|
|
|
130
|
-
const status = pipe.
|
|
142
|
+
const status = pipe.deployStatus ?? pipe.buildStatus ?? '';
|
|
131
143
|
if (status && status !== lastStatus) lastStatus = status;
|
|
132
144
|
if (['ready', 'live', 'success'].includes(status)) {
|
|
133
|
-
const url = pipe.
|
|
145
|
+
const url = pipe.hostings?.[0]?.domains?.[0]?.domain;
|
|
134
146
|
ok(`Live${url ? ` → ${color.cyan(`https://${url.replace(/^https?:\/\//, '')}`)}` : ''}`);
|
|
135
147
|
return;
|
|
136
148
|
}
|
|
@@ -142,6 +154,66 @@ export async function deploy({ flags }) {
|
|
|
142
154
|
warn('Timed out waiting for deployment. Check `glash status <id>`.');
|
|
143
155
|
}
|
|
144
156
|
|
|
157
|
+
async function syncEnvBeforeDeploy(projectId, target, flags) {
|
|
158
|
+
const files = await resolveEnvFiles(target, flags);
|
|
159
|
+
if (!files.length) return;
|
|
160
|
+
|
|
161
|
+
const merged = new Map();
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
const text = await readFile(file, 'utf8');
|
|
164
|
+
for (const entry of parseDotenv(text)) merged.set(entry.key, entry.value);
|
|
165
|
+
}
|
|
166
|
+
const vars = [...merged.entries()]
|
|
167
|
+
.filter(([key]) => !key.startsWith('GLASH_'))
|
|
168
|
+
.map(([key, value]) => ({ key, value, environment: target }));
|
|
169
|
+
|
|
170
|
+
if (!vars.length) return;
|
|
171
|
+
await apiPost(`/projects/${projectId}/env-vars/bulk`, { vars });
|
|
172
|
+
info(color.dim(`→ synced ${vars.length} env vars from ${files.join(', ')}`));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function resolveEnvFiles(target, flags) {
|
|
176
|
+
if (flags['env-file']) {
|
|
177
|
+
const files = String(flags['env-file']).split(',').map((f) => f.trim()).filter(Boolean);
|
|
178
|
+
for (const file of files) await access(file);
|
|
179
|
+
return files;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const candidates = [
|
|
183
|
+
'.env',
|
|
184
|
+
'.env.local',
|
|
185
|
+
`.env.${target}`,
|
|
186
|
+
`.env.${target}.local`,
|
|
187
|
+
];
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const file of candidates) {
|
|
190
|
+
try {
|
|
191
|
+
await access(file);
|
|
192
|
+
out.push(file);
|
|
193
|
+
} catch {}
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseDotenv(text) {
|
|
199
|
+
const out = [];
|
|
200
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
201
|
+
let line = raw.trim();
|
|
202
|
+
if (!line || line.startsWith('#')) continue;
|
|
203
|
+
if (line.startsWith('export ')) line = line.slice('export '.length).trim();
|
|
204
|
+
const eq = line.indexOf('=');
|
|
205
|
+
if (eq === -1) continue;
|
|
206
|
+
const key = line.slice(0, eq).trim();
|
|
207
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
208
|
+
let value = line.slice(eq + 1).trim();
|
|
209
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
210
|
+
value = value.slice(1, -1);
|
|
211
|
+
}
|
|
212
|
+
out.push({ key, value });
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
145
217
|
function stepBadge(status) {
|
|
146
218
|
if (status === 'success' || status === 'issued' || status === 'active' || status === 'ready') return color.green('✓');
|
|
147
219
|
if (status === 'error' || status === 'failed') return color.red('✗');
|
package/commands/deployments.mjs
CHANGED
|
@@ -34,7 +34,7 @@ export async function status({ positional, flags }) {
|
|
|
34
34
|
try {
|
|
35
35
|
const pipe = await apiGet(`/deployments/${id}/pipeline`);
|
|
36
36
|
info(color.bold(`Deployment ${id}`));
|
|
37
|
-
info(color.dim(`status: ${pipe.
|
|
37
|
+
info(color.dim(`status: ${pipe.deployStatus ?? pipe.buildStatus ?? '?'}`));
|
|
38
38
|
for (const ev of pipe.events ?? []) {
|
|
39
39
|
info(` ${color.dim((ev.createdAt ?? '').slice(11, 19))} ${ev.step.padEnd(8)} ${ev.status.padEnd(10)} ${ev.message ?? ''}`);
|
|
40
40
|
}
|
package/commands/pull.mjs
CHANGED
|
@@ -1,13 +1,112 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// glash pull
|
|
2
|
+
//
|
|
3
|
+
// Default mode: pulls the linked glashDB project's source + .env to the
|
|
4
|
+
// current directory.
|
|
5
|
+
//
|
|
6
|
+
// glash pull → uses the linked project (.glash/project.json)
|
|
7
|
+
// glash pull <slug> → pulls a specific project by slug
|
|
8
|
+
// glash pull --out ../somewhere → write to a different directory
|
|
9
|
+
// glash pull --no-env → skip writing .env
|
|
10
|
+
//
|
|
11
|
+
// Vercel mode (unchanged): glash pull --from vercel --project <id> [--team <id>]
|
|
12
|
+
// delegates to the existing reference implementation in glash-pull.mjs.
|
|
13
|
+
|
|
3
14
|
import { spawnSync } from 'node:child_process';
|
|
4
15
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { mkdir, writeFile, stat } from 'node:fs/promises';
|
|
18
|
+
import { createWriteStream } from 'node:fs';
|
|
19
|
+
import { Readable } from 'node:stream';
|
|
20
|
+
import { argv, cwd, exit, stdout, stderr } from 'node:process';
|
|
21
|
+
import { API_URL, getToken, readProjectLink } from '../lib/config.mjs';
|
|
22
|
+
import { color, info, fail } from '../lib/ui.mjs';
|
|
23
|
+
|
|
24
|
+
export async function pull(parsed) {
|
|
25
|
+
const flags = parsed?.flags ?? {};
|
|
26
|
+
const positional = parsed?.positional ?? [];
|
|
27
|
+
|
|
28
|
+
// Vercel-import compatibility path.
|
|
29
|
+
if (flags.from === 'vercel') {
|
|
30
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const script = join(here, '..', 'glash-pull.mjs');
|
|
32
|
+
const r = spawnSync(process.execPath, [script, ...argv.slice(3)], { stdio: 'inherit' });
|
|
33
|
+
exit(r.status ?? 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const token = await getToken();
|
|
37
|
+
if (!token) return fail('Not signed in. Run `glash login` first.');
|
|
38
|
+
|
|
39
|
+
// Resolve which project to pull
|
|
40
|
+
let slugOrId = positional[0] ?? flags.project;
|
|
41
|
+
if (!slugOrId) {
|
|
42
|
+
const link = await readProjectLink();
|
|
43
|
+
if (link?.slug) slugOrId = link.slug;
|
|
44
|
+
else if (link?.projectId) slugOrId = link.projectId;
|
|
45
|
+
}
|
|
46
|
+
if (!slugOrId) {
|
|
47
|
+
return fail('No project specified. Pass `glash pull <slug>` or run `glash link <slug>` first.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const outDir = resolve(flags.out ? String(flags.out) : cwd());
|
|
51
|
+
await mkdir(outDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
info(color.dim(`→ pulling ${slugOrId} into ${outDir}`));
|
|
54
|
+
|
|
55
|
+
// 1) source.tar.gz
|
|
56
|
+
await downloadAndExtract(token, slugOrId, outDir);
|
|
57
|
+
|
|
58
|
+
// 2) .env (unless --no-env)
|
|
59
|
+
if (!flags['no-env']) {
|
|
60
|
+
await downloadEnv(token, slugOrId, outDir, !!flags.force);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
stdout.write(color.dim(`\n next: cd ${outDir} && glash deploy\n`));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function downloadAndExtract(token, slugOrId, outDir) {
|
|
67
|
+
const url = `${API_URL}/projects/${encodeURIComponent(slugOrId)}/source.tar.gz`;
|
|
68
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const body = await res.text().catch(() => '');
|
|
71
|
+
throw new Error(`source download failed (${res.status}): ${body.slice(0, 200) || res.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
if (!res.body) throw new Error('empty response from server');
|
|
74
|
+
|
|
75
|
+
// Stream-pipe into `tar -xz -C outDir` via stdin
|
|
76
|
+
const child = spawnSync('tar', ['-xz', '-C', outDir], { input: Buffer.from(await res.arrayBuffer()) });
|
|
77
|
+
if (child.status !== 0) {
|
|
78
|
+
throw new Error(`tar extract failed: ${child.stderr?.toString() || 'unknown'}`);
|
|
79
|
+
}
|
|
80
|
+
info(color.dim(` ✓ source written to ${outDir}`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function downloadEnv(token, slugOrId, outDir, force) {
|
|
84
|
+
const url = `${API_URL}/projects/${encodeURIComponent(slugOrId)}/env-vars/dotenv`;
|
|
85
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
stderr.write(color.dim(` ! skipped .env (${res.status})\n`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const body = await res.text();
|
|
91
|
+
if (!body.trim()) {
|
|
92
|
+
info(color.dim(' · no env vars set on this project'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Preserve any existing .env unless --force
|
|
97
|
+
const envPath = join(outDir, '.env');
|
|
98
|
+
const exists = await stat(envPath).then(() => true).catch(() => false);
|
|
99
|
+
if (exists && !force) {
|
|
100
|
+
const backup = `${envPath}.glash-backup-${Date.now()}`;
|
|
101
|
+
await writeFile(backup, await readFileOrEmpty(envPath));
|
|
102
|
+
info(color.dim(` · existing .env backed up → ${backup}`));
|
|
103
|
+
}
|
|
104
|
+
await writeFile(envPath, body, { mode: 0o600 });
|
|
105
|
+
const lines = body.split('\n').filter(Boolean).length;
|
|
106
|
+
info(color.dim(` ✓ ${lines} env var${lines === 1 ? '' : 's'} written to .env`));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readFileOrEmpty(p) {
|
|
110
|
+
try { const { readFile } = await import('node:fs/promises'); return await readFile(p); }
|
|
111
|
+
catch { return ''; }
|
|
13
112
|
}
|
package/lib/zip.mjs
CHANGED
|
@@ -14,7 +14,34 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
|
|
|
14
14
|
archive.pipe(output);
|
|
15
15
|
|
|
16
16
|
// Default ignores
|
|
17
|
-
const defaultIgnore = [
|
|
17
|
+
const defaultIgnore = [
|
|
18
|
+
'node_modules/**',
|
|
19
|
+
'.git/**',
|
|
20
|
+
'.next/**',
|
|
21
|
+
'.nuxt/**',
|
|
22
|
+
'.output/**',
|
|
23
|
+
'.svelte-kit/**',
|
|
24
|
+
'.vercel/**',
|
|
25
|
+
'.wrangler/**',
|
|
26
|
+
'.netlify/**',
|
|
27
|
+
'dist/**',
|
|
28
|
+
'build/**',
|
|
29
|
+
'out/**',
|
|
30
|
+
'coverage/**',
|
|
31
|
+
'.turbo/**',
|
|
32
|
+
'.cache/**',
|
|
33
|
+
'.parcel-cache/**',
|
|
34
|
+
'.vite/**',
|
|
35
|
+
'.glash/**',
|
|
36
|
+
'*.log',
|
|
37
|
+
'npm-debug.log*',
|
|
38
|
+
'yarn-debug.log*',
|
|
39
|
+
'yarn-error.log*',
|
|
40
|
+
'.env',
|
|
41
|
+
'.env.local',
|
|
42
|
+
'.env.production',
|
|
43
|
+
'.env.development',
|
|
44
|
+
];
|
|
18
45
|
const allIgnore = [...new Set([...defaultIgnore, ...ignore])];
|
|
19
46
|
|
|
20
47
|
archive.glob('**/*', {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glash/cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "glashDB command-line interface — deploy projects, manage env vars,
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "glashDB command-line interface — deploy projects, pull source + env, manage env vars, import from Vercel.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://glashdb.com",
|
|
7
7
|
"repository": {
|