@glash/cli 0.2.2 → 0.2.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.
- package/README.md +3 -1
- package/bin/glash.mjs +15 -2
- package/commands/deploy.mjs +63 -13
- package/commands/pull.mjs +0 -11
- package/commands/update.mjs +11 -0
- package/lib/zip.mjs +0 -1
- package/package.json +2 -3
- package/glash-pull.mjs +0 -132
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ glash deploy
|
|
|
17
17
|
| `glash whoami` | Show the signed-in user |
|
|
18
18
|
| `glash projects` | List your projects |
|
|
19
19
|
| `glash projects:new <name>` | Create a project and link it to the current directory |
|
|
20
|
+
| `glash new <name>` | Alias for `projects:new` |
|
|
21
|
+
| `glash new deploy` | Deploy to a brand new project, ignoring any existing link |
|
|
20
22
|
| `glash projects:rm <slug>` | Delete a project |
|
|
21
23
|
| `glash link <slug>` | Link the current directory to an existing project |
|
|
22
24
|
| `glash open` | Open the linked project's primary domain |
|
|
@@ -31,7 +33,7 @@ glash deploy
|
|
|
31
33
|
| `glash env:rm KEY` | Remove an env var |
|
|
32
34
|
| `glash env:pull [--out .env.local]` | Pull env vars to a local file |
|
|
33
35
|
| `glash env:push [<file>]` | Bulk upload from a `.env` file |
|
|
34
|
-
| `glash
|
|
36
|
+
| `glash update` | Update `@glash/cli` to the latest version |
|
|
35
37
|
|
|
36
38
|
Pass `--project <slug>` to any command to operate on a project other than
|
|
37
39
|
the one linked in the current directory.
|
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.2.
|
|
6
|
+
const VERSION = '0.2.4';
|
|
7
7
|
|
|
8
8
|
const sub = argv[2];
|
|
9
9
|
const rest = argv.slice(3);
|
|
@@ -19,6 +19,14 @@ const commands = {
|
|
|
19
19
|
'projects:rm': () => import('../commands/projects.mjs').then((m) => m.removeProject(parsed)),
|
|
20
20
|
link: () => import('../commands/projects.mjs').then((m) => m.linkProject(parsed)),
|
|
21
21
|
|
|
22
|
+
new: () => {
|
|
23
|
+
if (parsed.positional[0] === 'deploy') {
|
|
24
|
+
const nextParsed = { ...parsed, flags: { ...parsed.flags, new: true } };
|
|
25
|
+
return import('../commands/deploy.mjs').then((m) => m.deploy(nextParsed));
|
|
26
|
+
}
|
|
27
|
+
return import('../commands/projects.mjs').then((m) => m.createProject(parsed));
|
|
28
|
+
},
|
|
29
|
+
|
|
22
30
|
deploy: () => import('../commands/deploy.mjs').then((m) => m.deploy(parsed)),
|
|
23
31
|
deployments: () => import('../commands/deployments.mjs').then((m) => m.listDeployments(parsed)),
|
|
24
32
|
status: () => import('../commands/deployments.mjs').then((m) => m.status(parsed)),
|
|
@@ -34,6 +42,7 @@ const commands = {
|
|
|
34
42
|
|
|
35
43
|
pull: () => import('../commands/pull.mjs').then((m) => m.pull(parsed)),
|
|
36
44
|
open: () => import('../commands/open.mjs').then((m) => m.open(parsed)),
|
|
45
|
+
update: () => import('../commands/update.mjs').then((m) => m.update(parsed)),
|
|
37
46
|
};
|
|
38
47
|
|
|
39
48
|
function usage() {
|
|
@@ -50,12 +59,14 @@ ${color.bold('AUTH')}
|
|
|
50
59
|
${color.bold('PROJECTS')}
|
|
51
60
|
projects List your projects
|
|
52
61
|
projects:new <name> Create a project (auto-links to cwd)
|
|
62
|
+
new <name> Alias for projects:new
|
|
53
63
|
projects:rm <slug> Delete a project (--yes to skip confirm)
|
|
54
64
|
link <slug> Link the current directory to a project
|
|
55
65
|
open Open the project's primary domain in a browser
|
|
56
66
|
|
|
57
67
|
${color.bold('DEPLOY')}
|
|
58
68
|
deploy [--prod] [--detach] Deploy the linked project, syncing .env files first
|
|
69
|
+
new deploy Deploy to a brand new project, ignoring any existing link
|
|
59
70
|
deployments List recent deployments
|
|
60
71
|
status <id> Show deployment pipeline + events
|
|
61
72
|
logs <id> Stream deployment events (alias of status)
|
|
@@ -71,7 +82,9 @@ ${color.bold('ENV VARS')}
|
|
|
71
82
|
|
|
72
83
|
${color.bold('PULL')}
|
|
73
84
|
pull [<slug>] [--out dir] [--no-env] Pull source + .env from a glashDB project
|
|
74
|
-
|
|
85
|
+
|
|
86
|
+
${color.bold('SYSTEM')}
|
|
87
|
+
update Update @glash/cli to the latest version
|
|
75
88
|
|
|
76
89
|
${color.bold('FLAGS')}
|
|
77
90
|
--project <slug|id> Override the linked project for this command
|
package/commands/deploy.mjs
CHANGED
|
@@ -20,19 +20,69 @@ function currentBranch() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
async function resolveProjectId(flags) {
|
|
23
|
-
if (flags.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
if (flags.new) {
|
|
24
|
+
info(color.dim('Initiating new project deployment...'));
|
|
25
|
+
} else if (flags.project) {
|
|
26
|
+
if (/^[0-9a-f-]{36}$/.test(flags.project)) {
|
|
27
|
+
try {
|
|
28
|
+
await apiGet(`/projects/${flags.project}`);
|
|
29
|
+
return flags.project;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err.status === 404 || err.message?.toLowerCase().includes('not found')) {
|
|
32
|
+
warn(`Project "${flags.project}" is no longer available or was deleted.`);
|
|
33
|
+
info('Let\'s set up a new project.');
|
|
34
|
+
} else {
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
const list = await apiGet('/projects');
|
|
40
|
+
const found = list.find((p) => p.slug === flags.project || p.id === flags.project);
|
|
41
|
+
if (!found) {
|
|
42
|
+
warn(`No project matches "${flags.project}".`);
|
|
43
|
+
info('Let\'s set up a new project.');
|
|
44
|
+
} else {
|
|
45
|
+
return found.id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
const link = await readProjectLink();
|
|
50
|
+
if (link?.projectId) {
|
|
51
|
+
const confirm = await prompt(`Deploy to linked project "${color.bold(link.slug || link.projectId)}"? [Y/n/select] `);
|
|
52
|
+
if (confirm.toLowerCase() === 'y' || confirm === '') {
|
|
53
|
+
try {
|
|
54
|
+
await apiGet(`/projects/${link.projectId}`);
|
|
55
|
+
return link.projectId;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err.status === 404 || err.message?.toLowerCase().includes('not found')) {
|
|
58
|
+
warn(`Linked project "${link.slug || link.projectId}" is no longer available or was deleted.`);
|
|
59
|
+
info('Let\'s set up a new project.');
|
|
60
|
+
} else {
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else if (confirm.toLowerCase() === 'n') {
|
|
65
|
+
throw new Error('Deployment aborted.');
|
|
66
|
+
} else {
|
|
67
|
+
// If they chose 'select' / 's', let them pick from existing projects
|
|
68
|
+
const list = await apiGet('/projects');
|
|
69
|
+
if (list.length === 0) {
|
|
70
|
+
info('No existing projects found. Proceeding to create a new one.');
|
|
71
|
+
} else {
|
|
72
|
+
info('\nYour existing projects:');
|
|
73
|
+
list.forEach((p, i) => info(` ${color.dim(i + 1 + '.')} ${color.bold(p.name)} ${color.dim('(' + p.slug + ')')}`));
|
|
74
|
+
const choice = await prompt('\nWhich project do you want to deploy to? (enter number or slug): ');
|
|
75
|
+
const p = list.find((x, i) => x.slug === choice || String(i + 1) === choice);
|
|
76
|
+
if (!p) throw new Error(`Project "${choice}" not found.`);
|
|
77
|
+
await writeProjectLink({ projectId: p.id, slug: p.slug, name: p.name });
|
|
78
|
+
ok(`Linked to ${p.name} (${p.slug})`);
|
|
79
|
+
return p.id;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
33
83
|
}
|
|
34
84
|
|
|
35
|
-
// INTERACTIVE PROJECT SETUP
|
|
85
|
+
// INTERACTIVE PROJECT SETUP
|
|
36
86
|
const dirName = basename(process.cwd());
|
|
37
87
|
const setup = await prompt(`Set up and deploy "${dirName}"? [Y/n] `);
|
|
38
88
|
if (setup.toLowerCase() === 'n') {
|
|
@@ -50,7 +100,7 @@ async function resolveProjectId(flags) {
|
|
|
50
100
|
const choice = await prompt('\nWhich project do you want to link to? (enter number or slug): ');
|
|
51
101
|
const p = list.find((x, i) => x.slug === choice || String(i + 1) === choice);
|
|
52
102
|
if (!p) throw new Error(`Project "${choice}" not found.`);
|
|
53
|
-
await writeProjectLink({ projectId: p.id, slug: p.slug });
|
|
103
|
+
await writeProjectLink({ projectId: p.id, slug: p.slug, name: p.name });
|
|
54
104
|
ok(`Linked to ${p.name} (${p.slug})`);
|
|
55
105
|
return p.id;
|
|
56
106
|
}
|
|
@@ -62,7 +112,7 @@ async function resolveProjectId(flags) {
|
|
|
62
112
|
|
|
63
113
|
info(color.dim(`Creating project "${name}"...`));
|
|
64
114
|
const p = await apiPost('/projects', { name, slug });
|
|
65
|
-
await writeProjectLink({ projectId: p.id, slug: p.slug });
|
|
115
|
+
await writeProjectLink({ projectId: p.id, slug: p.slug, name: p.name });
|
|
66
116
|
ok(`Created and linked to ${p.name} (${p.slug})`);
|
|
67
117
|
return p.id;
|
|
68
118
|
}
|
package/commands/pull.mjs
CHANGED
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
// glash pull <slug> → pulls a specific project by slug
|
|
8
8
|
// glash pull --out ../somewhere → write to a different directory
|
|
9
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
10
|
|
|
14
11
|
import { spawnSync } from 'node:child_process';
|
|
15
12
|
import { fileURLToPath } from 'node:url';
|
|
@@ -25,14 +22,6 @@ export async function pull(parsed) {
|
|
|
25
22
|
const flags = parsed?.flags ?? {};
|
|
26
23
|
const positional = parsed?.positional ?? [];
|
|
27
24
|
|
|
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
25
|
const token = await getToken();
|
|
37
26
|
if (!token) return fail('Not signed in. Run `glash login` first.');
|
|
38
27
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { ok, fail, info, color } from '../lib/ui.mjs';
|
|
3
|
+
|
|
4
|
+
export async function update() {
|
|
5
|
+
info(color.dim('→ updating @glash/cli to latest...'));
|
|
6
|
+
const child = spawnSync('npm', ['install', '-g', '@glash/cli@latest'], { stdio: 'inherit' });
|
|
7
|
+
if (child.status !== 0) {
|
|
8
|
+
return fail(`Update failed with exit code ${child.status ?? 'unknown'}. Please run \`npm install -g @glash/cli@latest\` manually.`);
|
|
9
|
+
}
|
|
10
|
+
ok('Successfully updated @glash/cli to the latest version.');
|
|
11
|
+
}
|
package/lib/zip.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glash/cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "glashDB command-line interface — deploy projects, pull source + env, manage env vars
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "glashDB command-line interface — deploy projects, pull source + env, manage env vars.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://glashdb.com",
|
|
7
7
|
"repository": {
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"bin",
|
|
21
21
|
"lib",
|
|
22
22
|
"commands",
|
|
23
|
-
"glash-pull.mjs",
|
|
24
23
|
"README.md"
|
|
25
24
|
],
|
|
26
25
|
"keywords": [
|
package/glash-pull.mjs
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* glash pull — reference implementation
|
|
4
|
-
*
|
|
5
|
-
* glash pull --from vercel --project <id> [--team <id>] [--token <vercel-token>] [--out .]
|
|
6
|
-
*
|
|
7
|
-
* Pulls the latest production deployment from Vercel into the current
|
|
8
|
-
* working directory:
|
|
9
|
-
* • walks /v6/deployments/<uid>/files
|
|
10
|
-
* • downloads each file via /v7/deployments/<uid>/files/<file-uid>
|
|
11
|
-
* • writes them at the same paths locally
|
|
12
|
-
* • dumps env vars to .env.local
|
|
13
|
-
*
|
|
14
|
-
* Token sources, in order: --token flag, $VERCEL_TOKEN, ~/.glash/credentials.json (vercelToken).
|
|
15
|
-
*
|
|
16
|
-
* No external deps — pure Node 20+. The shipping `@glashdb/cli` wraps this
|
|
17
|
-
* with a nicer UX (interactive prompts, progress bars, `glash login`-aware).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { argv, env, exit, cwd, stdout, stderr } from 'node:process';
|
|
21
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
22
|
-
import { homedir } from 'node:os';
|
|
23
|
-
import { join, dirname } from 'node:path';
|
|
24
|
-
|
|
25
|
-
const VERCEL = 'https://api.vercel.com';
|
|
26
|
-
|
|
27
|
-
function parseArgs(argv) {
|
|
28
|
-
const out = {};
|
|
29
|
-
for (let i = 2; i < argv.length; i++) {
|
|
30
|
-
const a = argv[i];
|
|
31
|
-
if (a.startsWith('--')) {
|
|
32
|
-
const k = a.slice(2);
|
|
33
|
-
const v = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : 'true';
|
|
34
|
-
out[k] = v;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function readToken(flag) {
|
|
41
|
-
if (flag) return flag;
|
|
42
|
-
if (env.VERCEL_TOKEN) return env.VERCEL_TOKEN;
|
|
43
|
-
try {
|
|
44
|
-
const raw = await readFile(join(homedir(), '.glash', 'credentials.json'), 'utf8');
|
|
45
|
-
const j = JSON.parse(raw);
|
|
46
|
-
if (j.vercelToken) return j.vercelToken;
|
|
47
|
-
} catch {}
|
|
48
|
-
stderr.write("error: no Vercel token. Pass --token, set VERCEL_TOKEN, or run `glash login --vercel`.\n");
|
|
49
|
-
exit(2);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function vercel(token, path, teamId) {
|
|
53
|
-
const url = `${VERCEL}${path}${teamId ? (path.includes('?') ? '&' : '?') + 'teamId=' + encodeURIComponent(teamId) : ''}`;
|
|
54
|
-
const r = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
55
|
-
if (!r.ok) throw new Error(`Vercel ${r.status} ${r.statusText} on ${path}`);
|
|
56
|
-
return r.json();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function vercelRaw(token, path, teamId) {
|
|
60
|
-
const url = `${VERCEL}${path}${teamId ? (path.includes('?') ? '&' : '?') + 'teamId=' + encodeURIComponent(teamId) : ''}`;
|
|
61
|
-
const r = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
62
|
-
if (!r.ok) throw new Error(`Vercel ${r.status} on ${path}`);
|
|
63
|
-
return Buffer.from(await r.arrayBuffer());
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function walk(token, deploymentUid, teamId, tree, prefix, out) {
|
|
67
|
-
for (const node of tree) {
|
|
68
|
-
const rel = prefix ? `${prefix}/${node.name}` : node.name;
|
|
69
|
-
if (node.type === 'directory' && node.children) {
|
|
70
|
-
await walk(token, deploymentUid, teamId, node.children, rel, out);
|
|
71
|
-
} else if (node.type === 'file' && node.uid) {
|
|
72
|
-
out.push({ rel, uid: node.uid });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function main() {
|
|
78
|
-
const args = parseArgs(argv);
|
|
79
|
-
if (args.from !== 'vercel') {
|
|
80
|
-
stderr.write("usage: glash pull --from vercel --project <id> [--team <id>] [--out .]\n");
|
|
81
|
-
exit(2);
|
|
82
|
-
}
|
|
83
|
-
if (!args.project) { stderr.write("error: --project <vercel-project-id> required\n"); exit(2); }
|
|
84
|
-
|
|
85
|
-
const token = await readToken(args.token);
|
|
86
|
-
const team = args.team;
|
|
87
|
-
const outDir = args.out || cwd();
|
|
88
|
-
|
|
89
|
-
stdout.write(`→ pulling vercel project ${args.project}${team ? ` (team ${team})` : ''} into ${outDir}\n`);
|
|
90
|
-
|
|
91
|
-
// 1) latest production deployment
|
|
92
|
-
const list = await vercel(token, `/v6/deployments?projectId=${encodeURIComponent(args.project)}&limit=1&target=production&state=READY`, team);
|
|
93
|
-
const dep = list.deployments?.[0];
|
|
94
|
-
if (!dep) { stderr.write("error: no production deployment on Vercel for that project.\n"); exit(1); }
|
|
95
|
-
stdout.write(`✓ deployment ${dep.uid}\n`);
|
|
96
|
-
|
|
97
|
-
// 2) file tree
|
|
98
|
-
const tree = await vercel(token, `/v6/deployments/${dep.uid}/files`, team);
|
|
99
|
-
const files = [];
|
|
100
|
-
await walk(token, dep.uid, team, tree, '', files);
|
|
101
|
-
stdout.write(`✓ ${files.length} files to download\n`);
|
|
102
|
-
|
|
103
|
-
// 3) write each file
|
|
104
|
-
let n = 0;
|
|
105
|
-
for (const f of files) {
|
|
106
|
-
const buf = await vercelRaw(token, `/v7/deployments/${dep.uid}/files/${f.uid}`, team);
|
|
107
|
-
const dest = join(outDir, f.rel);
|
|
108
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
109
|
-
await writeFile(dest, buf);
|
|
110
|
-
n++;
|
|
111
|
-
if (n % 10 === 0 || n === files.length) stdout.write(` ${n}/${files.length}\r`);
|
|
112
|
-
}
|
|
113
|
-
stdout.write(`\n✓ wrote ${n} files\n`);
|
|
114
|
-
|
|
115
|
-
// 4) env vars → .env.local
|
|
116
|
-
try {
|
|
117
|
-
const env = await vercel(token, `/v9/projects/${encodeURIComponent(args.project)}/env?decrypt=true`, team);
|
|
118
|
-
const lines = (env.envs ?? [])
|
|
119
|
-
.filter((e) => !e.target || e.target.includes('production'))
|
|
120
|
-
.map((e) => `${e.key}=${JSON.stringify(e.value ?? '')}`);
|
|
121
|
-
if (lines.length) {
|
|
122
|
-
await writeFile(join(outDir, '.env.local'), lines.join('\n') + '\n');
|
|
123
|
-
stdout.write(`✓ wrote ${lines.length} env vars to .env.local\n`);
|
|
124
|
-
}
|
|
125
|
-
} catch (e) {
|
|
126
|
-
stdout.write(`! skipped env vars: ${e.message}\n`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
stdout.write(`done.\n next: cd ${outDir} && glash deploy\n`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
main().catch((e) => { stderr.write(`error: ${e.message}\n`); exit(1); });
|