@pushto/cli 0.4.10 → 0.5.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/dist/commands/__tests__/mcp-add.test.d.ts +2 -0
- package/dist/commands/__tests__/mcp-add.test.d.ts.map +1 -0
- package/dist/commands/__tests__/mcp-add.test.js +112 -0
- package/dist/commands/__tests__/mcp-add.test.js.map +1 -0
- package/dist/commands/__tests__/mcp-serve.test.d.ts +2 -0
- package/dist/commands/__tests__/mcp-serve.test.d.ts.map +1 -0
- package/dist/commands/__tests__/mcp-serve.test.js +169 -0
- package/dist/commands/__tests__/mcp-serve.test.js.map +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +63 -101
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/mcp.d.ts +7 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +137 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/__tests__/deploy-local.test.d.ts +2 -0
- package/dist/lib/__tests__/deploy-local.test.d.ts.map +1 -0
- package/dist/lib/__tests__/deploy-local.test.js +103 -0
- package/dist/lib/__tests__/deploy-local.test.js.map +1 -0
- package/dist/lib/__tests__/mcp-serve-client.test.d.ts +2 -0
- package/dist/lib/__tests__/mcp-serve-client.test.d.ts.map +1 -0
- package/dist/lib/__tests__/mcp-serve-client.test.js +74 -0
- package/dist/lib/__tests__/mcp-serve-client.test.js.map +1 -0
- package/dist/lib/__tests__/npm-packaging.test.js +1 -1
- package/dist/lib/__tests__/npm-packaging.test.js.map +1 -1
- package/dist/lib/mcp-serve/agent-config.d.ts +26 -0
- package/dist/lib/mcp-serve/agent-config.d.ts.map +1 -0
- package/dist/lib/mcp-serve/agent-config.js +38 -0
- package/dist/lib/mcp-serve/agent-config.js.map +1 -0
- package/dist/lib/mcp-serve/client.d.ts +22 -0
- package/dist/lib/mcp-serve/client.d.ts.map +1 -0
- package/dist/lib/mcp-serve/client.js +68 -0
- package/dist/lib/mcp-serve/client.js.map +1 -0
- package/dist/lib/mcp-serve/deploy-local.d.ts +13 -0
- package/dist/lib/mcp-serve/deploy-local.d.ts.map +1 -0
- package/dist/lib/mcp-serve/deploy-local.js +99 -0
- package/dist/lib/mcp-serve/deploy-local.js.map +1 -0
- package/dist/lib/mcp-serve/serve.d.ts +19 -0
- package/dist/lib/mcp-serve/serve.d.ts.map +1 -0
- package/dist/lib/mcp-serve/serve.js +60 -0
- package/dist/lib/mcp-serve/serve.js.map +1 -0
- package/dist/lib/mcp-serve/token.d.ts +20 -0
- package/dist/lib/mcp-serve/token.d.ts.map +1 -0
- package/dist/lib/mcp-serve/token.js +43 -0
- package/dist/lib/mcp-serve/token.js.map +1 -0
- package/dist/lib/perform-deploy.d.ts +74 -0
- package/dist/lib/perform-deploy.d.ts.map +1 -0
- package/dist/lib/perform-deploy.js +112 -0
- package/dist/lib/perform-deploy.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +3 -3
- package/dist/version.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-add.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/mcp-add.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('node:fs', () => ({
|
|
3
|
+
default: { existsSync: vi.fn(), readFileSync: vi.fn(), writeFileSync: vi.fn() },
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('node:child_process', () => ({ execFile: vi.fn() }));
|
|
6
|
+
vi.mock('../../lib/config.js', () => ({ getToken: vi.fn(() => 'tok') }));
|
|
7
|
+
vi.mock('../../lib/interactive.js', () => ({ canPrompt: vi.fn() }));
|
|
8
|
+
vi.mock('../../lib/ask.js', () => ({ ask: vi.fn() }));
|
|
9
|
+
vi.mock('../../lib/mcp-serve/serve.js', () => ({ runMcpServe: vi.fn() }));
|
|
10
|
+
vi.mock('../../lib/mcp-serve/client.js', () => ({ McpApiClient: vi.fn() }));
|
|
11
|
+
vi.mock('../../lib/mcp-serve/deploy-local.js', () => ({ deployLocalTool: {} }));
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import { execFile } from 'node:child_process';
|
|
14
|
+
import { mcp } from '../mcp.js';
|
|
15
|
+
import { canPrompt } from '../../lib/interactive.js';
|
|
16
|
+
import { ask } from '../../lib/ask.js';
|
|
17
|
+
import { mergeCursorConfig, genericSnippet } from '../../lib/mcp-serve/agent-config.js';
|
|
18
|
+
const execCalls = [];
|
|
19
|
+
let claudeOnPath = false;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
execCalls.length = 0;
|
|
23
|
+
claudeOnPath = false;
|
|
24
|
+
vi.mocked(execFile).mockImplementation(((cmd, args, cb) => {
|
|
25
|
+
const done = (typeof cb === 'function' ? cb : undefined);
|
|
26
|
+
execCalls.push([cmd, ...args]);
|
|
27
|
+
if (cmd === 'which') {
|
|
28
|
+
if (claudeOnPath)
|
|
29
|
+
done?.(null, { stdout: '/usr/local/bin/claude', stderr: '' });
|
|
30
|
+
else
|
|
31
|
+
done?.(new Error('not found'), { stdout: '', stderr: '' });
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
done?.(null, { stdout: '', stderr: '' });
|
|
35
|
+
return {};
|
|
36
|
+
}));
|
|
37
|
+
vi.mocked(canPrompt).mockReturnValue(true);
|
|
38
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
39
|
+
});
|
|
40
|
+
const captureStdout = () => {
|
|
41
|
+
const spy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
42
|
+
return () => spy.mock.calls.map((c) => String(c[0])).join('');
|
|
43
|
+
};
|
|
44
|
+
describe('mergeCursorConfig', () => {
|
|
45
|
+
it('merges into an existing config preserving other servers', () => {
|
|
46
|
+
const out = mergeCursorConfig(JSON.stringify({ mcpServers: { other: { command: 'x' } } }));
|
|
47
|
+
expect(out.ok).toBe(true);
|
|
48
|
+
if (out.ok) {
|
|
49
|
+
const parsed = JSON.parse(out.merged);
|
|
50
|
+
expect(Object.keys(parsed.mcpServers).sort()).toEqual(['other', 'pushto']);
|
|
51
|
+
expect(out.already).toBe(false);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
it('reports already when the exact entry exists', () => {
|
|
55
|
+
const out = mergeCursorConfig(JSON.stringify({ mcpServers: { pushto: { command: 'pushto', args: ['mcp', 'serve'] } } }));
|
|
56
|
+
expect(out.ok && out.already).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it('refuses to clobber unparseable json', () => {
|
|
59
|
+
const out = mergeCursorConfig('{not json');
|
|
60
|
+
expect(out.ok).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it('starts from empty for a missing file', () => {
|
|
63
|
+
const out = mergeCursorConfig(null);
|
|
64
|
+
expect(out.ok).toBe(true);
|
|
65
|
+
if (out.ok)
|
|
66
|
+
expect(JSON.parse(out.merged)).toEqual(JSON.parse(genericSnippet()));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('pushto mcp add', () => {
|
|
70
|
+
it('runs the claude one-liner on consent', async () => {
|
|
71
|
+
claudeOnPath = true;
|
|
72
|
+
vi.mocked(ask).mockResolvedValue('y');
|
|
73
|
+
await mcp('add', [], {});
|
|
74
|
+
expect(execCalls).toContainEqual([
|
|
75
|
+
'claude', 'mcp', 'add', 'pushto', '--', 'pushto', 'mcp', 'serve',
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
it('prints snippets instead when consent is declined', async () => {
|
|
79
|
+
claudeOnPath = true;
|
|
80
|
+
vi.mocked(ask).mockResolvedValue('n');
|
|
81
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
82
|
+
await mcp('add', [], {});
|
|
83
|
+
expect(execCalls.some((c) => c[0] === 'claude')).toBe(false);
|
|
84
|
+
expect(logSpy.mock.calls.flat().join('\n')).toContain('claude mcp add pushto');
|
|
85
|
+
});
|
|
86
|
+
it('writes the merged cursor config on --yes without prompting', async () => {
|
|
87
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
88
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ mcpServers: {} }));
|
|
89
|
+
await mcp('add', [], { yes: true });
|
|
90
|
+
expect(ask).not.toHaveBeenCalled();
|
|
91
|
+
const written = vi.mocked(fs.writeFileSync).mock.calls[0]?.[1];
|
|
92
|
+
expect(JSON.parse(written).mcpServers.pushto).toEqual({ command: 'pushto', args: ['mcp', 'serve'] });
|
|
93
|
+
});
|
|
94
|
+
it('--json never prompts and emits the machine shape', async () => {
|
|
95
|
+
vi.mocked(canPrompt).mockReturnValue(false);
|
|
96
|
+
const read = captureStdout();
|
|
97
|
+
await mcp('add', [], { json: true });
|
|
98
|
+
expect(ask).not.toHaveBeenCalled();
|
|
99
|
+
const payload = JSON.parse(read());
|
|
100
|
+
expect(payload.detected).toBe('none');
|
|
101
|
+
expect(payload.configured).toBe(false);
|
|
102
|
+
expect(payload.snippet).toContain('pushto');
|
|
103
|
+
});
|
|
104
|
+
it('never overwrites an unparseable cursor config', async () => {
|
|
105
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
106
|
+
vi.mocked(fs.readFileSync).mockReturnValue('{broken');
|
|
107
|
+
vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
108
|
+
await mcp('add', [], { yes: true });
|
|
109
|
+
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=mcp-add.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-add.test.js","sourceRoot":"","sources":["../../../src/commands/__tests__/mcp-add.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-serve.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/mcp-serve.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { spawn, execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
10
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const CLI_ROOT = path.resolve(HERE, '../../..');
|
|
14
|
+
const ENTRY = path.join(CLI_ROOT, 'dist', 'cli.js');
|
|
15
|
+
/**
|
|
16
|
+
* The serve command against a stub API, driven by the REAL sdk client over
|
|
17
|
+
* stdio — the same wire a claude/cursor session uses. HOME is redirected per
|
|
18
|
+
* run (conf reads the login token under HOME; see headless-smoke.test.ts for
|
|
19
|
+
* the incident this guards).
|
|
20
|
+
*/
|
|
21
|
+
const ENVELOPE = { summary: 'demo is live. it serves at demo.pushto.host.', data: { slug: 'demo' } };
|
|
22
|
+
let apiServer;
|
|
23
|
+
let apiUrl = '';
|
|
24
|
+
let mintCount = 0;
|
|
25
|
+
const makeHome = () => {
|
|
26
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'pushto-mcp-home-'));
|
|
27
|
+
// conf resolves its dir via env-paths, which is ~/.config on linux but
|
|
28
|
+
// ~/Library/Preferences on macOS (XDG is ignored there). Seed both so the
|
|
29
|
+
// spawned server reads the stub apiUrl on either platform — miss it and
|
|
30
|
+
// the default apiUrl points the test at production.
|
|
31
|
+
for (const confDir of [
|
|
32
|
+
path.join(home, '.config', 'pushto-nodejs'),
|
|
33
|
+
path.join(home, 'Library', 'Preferences', 'pushto-nodejs'),
|
|
34
|
+
]) {
|
|
35
|
+
fs.mkdirSync(confDir, { recursive: true });
|
|
36
|
+
fs.writeFileSync(path.join(confDir, 'config.json'), JSON.stringify({ apiUrl }));
|
|
37
|
+
}
|
|
38
|
+
return home;
|
|
39
|
+
};
|
|
40
|
+
const serveEnv = (home) => ({
|
|
41
|
+
...process.env,
|
|
42
|
+
HOME: home,
|
|
43
|
+
USERPROFILE: home,
|
|
44
|
+
XDG_CONFIG_HOME: path.join(home, '.config'),
|
|
45
|
+
PUSHTO_TOKEN: 'stub-cli-session',
|
|
46
|
+
CI: '1',
|
|
47
|
+
});
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
await execFileAsync('npx', ['tsc'], { cwd: CLI_ROOT });
|
|
50
|
+
apiServer = http.createServer((req, res) => {
|
|
51
|
+
const url = req.url ?? '';
|
|
52
|
+
if (req.method === 'POST' && url === '/api/cli/mcp-token') {
|
|
53
|
+
mintCount += 1;
|
|
54
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
55
|
+
res.end(JSON.stringify({ token: 'stub-mcp-token', expiresAt: new Date(Date.now() + 604_800_000).toISOString() }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (req.method === 'GET' && url === '/api/mcp') {
|
|
59
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
60
|
+
res.end(JSON.stringify({
|
|
61
|
+
tools: [
|
|
62
|
+
{
|
|
63
|
+
name: 'pushto.project.describe',
|
|
64
|
+
description: "read one project's current state.",
|
|
65
|
+
inputSchema: { type: 'object', properties: { slug: { type: 'string' } }, required: ['slug'] },
|
|
66
|
+
available: true,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'pushto.deploy.status',
|
|
70
|
+
description: 'poll one deployment.',
|
|
71
|
+
inputSchema: { type: 'object' },
|
|
72
|
+
available: true,
|
|
73
|
+
},
|
|
74
|
+
{ name: 'pushto.broken.tool', description: 'stub', inputSchema: null, available: false, unavailableReason: 'nope' },
|
|
75
|
+
],
|
|
76
|
+
}));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (req.method === 'POST' && url === '/api/mcp') {
|
|
80
|
+
let body = '';
|
|
81
|
+
req.on('data', (d) => { body += String(d); });
|
|
82
|
+
req.on('end', () => {
|
|
83
|
+
const parsed = JSON.parse(body);
|
|
84
|
+
if (parsed.tool === 'pushto.project.describe') {
|
|
85
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
86
|
+
res.end(JSON.stringify({ result: ENVELOPE }));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
90
|
+
res.end(JSON.stringify({ error: `unknown tool: ${parsed.tool}` }));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
res.writeHead(404).end();
|
|
96
|
+
});
|
|
97
|
+
await new Promise((resolve) => apiServer.listen(0, '127.0.0.1', resolve));
|
|
98
|
+
apiUrl = `http://127.0.0.1:${apiServer.address().port}`;
|
|
99
|
+
}, 120_000);
|
|
100
|
+
afterAll(async () => {
|
|
101
|
+
await new Promise((resolve) => apiServer.close(() => resolve()));
|
|
102
|
+
});
|
|
103
|
+
describe('pushto mcp serve', () => {
|
|
104
|
+
it('serves the remote catalog to a real sdk client and proxies calls', async () => {
|
|
105
|
+
const home = makeHome();
|
|
106
|
+
const transport = new StdioClientTransport({
|
|
107
|
+
command: process.execPath,
|
|
108
|
+
args: [ENTRY, 'mcp', 'serve'],
|
|
109
|
+
env: serveEnv(home),
|
|
110
|
+
});
|
|
111
|
+
const client = new Client({ name: 'test-agent', version: '0.0.0' });
|
|
112
|
+
await client.connect(transport);
|
|
113
|
+
const { tools } = await client.listTools();
|
|
114
|
+
const names = tools.map((t) => t.name);
|
|
115
|
+
expect(names).toContain('pushto.project.describe');
|
|
116
|
+
expect(names).toContain('pushto.deploy.status');
|
|
117
|
+
// unavailable tools are filtered out of the served catalog
|
|
118
|
+
expect(names).not.toContain('pushto.broken.tool');
|
|
119
|
+
const result = await client.callTool({ name: 'pushto.project.describe', arguments: { slug: 'demo' } });
|
|
120
|
+
const content = result.content;
|
|
121
|
+
expect(JSON.parse(content[0].text)).toEqual(ENVELOPE);
|
|
122
|
+
expect(result.isError ?? false).toBe(false);
|
|
123
|
+
const failed = await client.callTool({ name: 'pushto.nope', arguments: {} });
|
|
124
|
+
expect(failed.isError).toBe(true);
|
|
125
|
+
const failedContent = failed.content;
|
|
126
|
+
const errPayload = JSON.parse(failedContent[0].text);
|
|
127
|
+
expect(errPayload.code).toBe('SERVER_ERROR');
|
|
128
|
+
expect(errPayload.human).toBe('unknown tool: pushto.nope');
|
|
129
|
+
expect(mintCount).toBeGreaterThanOrEqual(1);
|
|
130
|
+
await client.close();
|
|
131
|
+
}, 30_000);
|
|
132
|
+
it('keeps stdout pure protocol — every line parses as json-rpc', async () => {
|
|
133
|
+
const home = makeHome();
|
|
134
|
+
const child = spawn(process.execPath, [ENTRY, 'mcp', 'serve'], {
|
|
135
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
136
|
+
env: serveEnv(home),
|
|
137
|
+
});
|
|
138
|
+
let stdout = '';
|
|
139
|
+
let stderr = '';
|
|
140
|
+
child.stdout.on('data', (d) => { stdout += String(d); });
|
|
141
|
+
child.stderr.on('data', (d) => { stderr += String(d); });
|
|
142
|
+
child.stdin.write('{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}\n' +
|
|
143
|
+
'{"jsonrpc":"2.0","method":"notifications/initialized"}\n' +
|
|
144
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n');
|
|
145
|
+
await new Promise((r) => setTimeout(r, 2_500));
|
|
146
|
+
child.kill('SIGKILL');
|
|
147
|
+
const lines = stdout.split('\n').filter((l) => l.trim().length > 0);
|
|
148
|
+
expect(lines.length).toBeGreaterThanOrEqual(2);
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const frame = JSON.parse(line);
|
|
151
|
+
expect(frame.jsonrpc).toBe('2.0');
|
|
152
|
+
}
|
|
153
|
+
// the boot notice goes to stderr, never stdout
|
|
154
|
+
expect(stderr).toContain('pushto mcp server up');
|
|
155
|
+
}, 30_000);
|
|
156
|
+
it('exits band 10 with a login hint when no session exists', async () => {
|
|
157
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'pushto-mcp-nologin-'));
|
|
158
|
+
const child = spawn(process.execPath, [ENTRY, 'mcp', 'serve'], {
|
|
159
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
160
|
+
env: { ...serveEnv(home), PUSHTO_TOKEN: '' },
|
|
161
|
+
});
|
|
162
|
+
let stderr = '';
|
|
163
|
+
child.stderr.on('data', (d) => { stderr += String(d); });
|
|
164
|
+
const code = await new Promise((resolve) => child.on('close', resolve));
|
|
165
|
+
expect(code).toBe(10);
|
|
166
|
+
expect(stderr).toContain('pushto login');
|
|
167
|
+
}, 30_000);
|
|
168
|
+
});
|
|
169
|
+
//# sourceMappingURL=mcp-serve.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-serve.test.js","sourceRoot":"","sources":["../../../src/commands/__tests__/mcp-serve.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAwBA;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAU,MAAM;IAC9C,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACnC,SAAS,EAAE,OAAO,CAAC;IACnB,GAAG,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5C,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAKxB,CAAC;AAEF,MAAM,MAAM,YAAY,GACpB;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,iBAAiB,GAAG,YAAY,CAAA;CAAE,CAAC;AAEjE;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM;IACvC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,GAAG,EAAE,OAAO,CAAC;CACd,KAAG,YAQH,CAAC;AAqDF,eAAO,MAAM,MAAM,GAAU,OAAM;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,KAAG,OAAO,CAAC,IAAI,CAiXvF,CAAC"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -3,14 +3,14 @@ import ora from 'ora';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { getToken } from '../lib/config.js';
|
|
6
|
-
import { api
|
|
6
|
+
import { api } from '../lib/api.js';
|
|
7
7
|
import { readLocalSlug } from '../lib/resolve-slug.js';
|
|
8
8
|
import { slugifyProjectName, MIN_SLUG_LENGTH } from '../lib/project-name.js';
|
|
9
9
|
import { createAndLinkProject } from '../lib/create-project.js';
|
|
10
10
|
import { exitCodeFor } from '../lib/errors.js';
|
|
11
11
|
import { safeJson } from '../lib/safe-json.js';
|
|
12
|
-
import { getGitInfo, zipDirectory } from '../lib/zip-project.js';
|
|
13
12
|
import { SERVER_UPLOAD_CAP_BYTES, fmtMB } from '../lib/upload-limits.js';
|
|
13
|
+
import { describeGit, zipAndCheck, uploadArtifact, triggerDynamicDeploy, } from '../lib/perform-deploy.js';
|
|
14
14
|
import { parseEnvFile } from '../lib/env-parse.js';
|
|
15
15
|
import { prepareArtifact } from '../lib/prepare-artifact.js';
|
|
16
16
|
import { canPrompt } from '../lib/interactive.js';
|
|
@@ -215,48 +215,41 @@ export const deploy = async (opts = {}) => {
|
|
|
215
215
|
}
|
|
216
216
|
const spinner = ora('zipping project...').start();
|
|
217
217
|
try {
|
|
218
|
-
const git =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// instead of slow (after a 30-second upload that the server then 413s).
|
|
234
|
-
// Pre-fix the local cap was 500MB but the server cap is 50MB — uploads
|
|
235
|
-
// between 50MB and 500MB went up the wire and got rejected after the
|
|
236
|
-
// long upload finished, with a confusing "upload too large" error
|
|
237
|
-
// long after the user had walked away thinking it was working.
|
|
238
|
-
if (sizeBytes > SERVER_UPLOAD_CAP_BYTES) {
|
|
239
|
-
spinner.fail(chalk.red(`> deploy is ${fmtMB(sizeBytes)} — server cap is ${fmtMB(SERVER_UPLOAD_CAP_BYTES)}.`));
|
|
240
|
-
console.log(chalk.dim(' add large files (videos, datasets, build artifacts) to .gitignore or .pushtoignore.'));
|
|
241
|
-
console.log(chalk.dim(' run ') + chalk.cyan('pushto doctor') + chalk.dim(' to see the biggest files.'));
|
|
218
|
+
const git = describeGit();
|
|
219
|
+
// Mechanics live in lib/perform-deploy.ts (shared with pushto.deploy.local
|
|
220
|
+
// in the mcp server); this command owns every line of presentation.
|
|
221
|
+
const zipped = await zipAndCheck(prep.zipDir);
|
|
222
|
+
if (!zipped.ok) {
|
|
223
|
+
if (zipped.error.code === 'NO_FILES') {
|
|
224
|
+
spinner.fail(chalk.red('> no files found in this directory.'));
|
|
225
|
+
console.log(chalk.dim(' is your entire project in .gitignore? are you in the wrong directory?'));
|
|
226
|
+
console.log(chalk.dim(' run ') + chalk.cyan('pushto doctor') + chalk.dim(' to see what would be uploaded.'));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
spinner.fail(chalk.red(`> deploy is ${fmtMB(zipped.sizeBytes)} — server cap is ${fmtMB(SERVER_UPLOAD_CAP_BYTES)}.`));
|
|
230
|
+
console.log(chalk.dim(' add large files (videos, datasets, build artifacts) to .gitignore or .pushtoignore.'));
|
|
231
|
+
console.log(chalk.dim(' run ') + chalk.cyan('pushto doctor') + chalk.dim(' to see the biggest files.'));
|
|
232
|
+
}
|
|
242
233
|
process.exit(1);
|
|
243
234
|
}
|
|
235
|
+
const sizeMB = (zipped.sizeBytes / 1024 / 1024).toFixed(1);
|
|
244
236
|
spinner.text = `uploading ${sizeMB}MB...`;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
headers: {
|
|
249
|
-
'Content-Type': 'application/zip',
|
|
250
|
-
},
|
|
251
|
-
body: new Uint8Array(zipBuffer),
|
|
252
|
-
signal: deploySignal(),
|
|
253
|
-
});
|
|
254
|
-
if (!res.ok) {
|
|
255
|
-
const data = await safeJson(res);
|
|
256
|
-
spinner.fail(chalk.red(`> ${data.error ?? 'deploy failed.'}`));
|
|
237
|
+
const uploaded = await uploadArtifact(slug, zipped.buffer);
|
|
238
|
+
if (!uploaded.ok) {
|
|
239
|
+
spinner.fail(chalk.red(`> ${uploaded.error.human}`));
|
|
257
240
|
process.exit(1);
|
|
258
241
|
}
|
|
259
|
-
|
|
242
|
+
// Keep the historical local shape so the outcome machine below reads
|
|
243
|
+
// exactly as it always has.
|
|
244
|
+
const data = {
|
|
245
|
+
deployment: {
|
|
246
|
+
version: uploaded.deployment.version,
|
|
247
|
+
fileCount: uploaded.deployment.fileCount,
|
|
248
|
+
buildDuration: uploaded.deployment.buildDuration,
|
|
249
|
+
},
|
|
250
|
+
url: uploaded.deployment.url,
|
|
251
|
+
container: uploaded.deployment.container ?? undefined,
|
|
252
|
+
};
|
|
260
253
|
// Don't trust the server's `data.message` here — it currently says
|
|
261
254
|
// "v1 live" even though for container apps, the build hasn't run yet.
|
|
262
255
|
// What we know for sure at this point is: the bundle uploaded.
|
|
@@ -283,75 +276,44 @@ export const deploy = async (opts = {}) => {
|
|
|
283
276
|
// Try dynamic deploy — server will return 403 if it's a free tier
|
|
284
277
|
// user (static-only) or 200 with no container if it's a static site.
|
|
285
278
|
const deploySpinner = ora('detecting framework...').start();
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
deploySpinner.succeed(chalk.green(`> deployed as ${fw} (${dur}s)`));
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
outcome = 'static';
|
|
311
|
-
const label = prep.kind === 'artifact' ? `${prep.framework} build` : 'static site';
|
|
312
|
-
deploySpinner.succeed(chalk.green(`> ${label} live`));
|
|
313
|
-
if (deployData.message && deployData.message.includes('container was stopped')) {
|
|
314
|
-
console.log(chalk.yellow(` ${deployData.message}`));
|
|
315
|
-
}
|
|
279
|
+
// Branch semantics live in triggerDynamicDeploy (403 = free-tier gate
|
|
280
|
+
// on a genuinely dynamic project; 504/409 = build continues without
|
|
281
|
+
// us). This switch owns only the words and colors.
|
|
282
|
+
const trig = await triggerDynamicDeploy(slug, git, prep.kind === 'artifact' ? prep.framework : undefined);
|
|
283
|
+
switch (trig.kind) {
|
|
284
|
+
case 'container': {
|
|
285
|
+
outcome = 'container';
|
|
286
|
+
containerDeployed = true;
|
|
287
|
+
finalUrl = trig.url ?? finalUrl;
|
|
288
|
+
const fw = trig.framework ?? 'dynamic';
|
|
289
|
+
const dur = trig.buildDuration ?? 0;
|
|
290
|
+
containerBuildDuration = trig.buildDuration;
|
|
291
|
+
deploySpinner.succeed(chalk.green(`> deployed as ${fw} (${dur}s)`));
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case 'static': {
|
|
295
|
+
outcome = 'static';
|
|
296
|
+
const label = prep.kind === 'artifact' ? `${prep.framework} build` : 'static site';
|
|
297
|
+
deploySpinner.succeed(chalk.green(`> ${label} live`));
|
|
298
|
+
if (trig.message && trig.message.includes('container was stopped')) {
|
|
299
|
+
console.log(chalk.yellow(` ${trig.message}`));
|
|
316
300
|
}
|
|
301
|
+
break;
|
|
317
302
|
}
|
|
318
|
-
|
|
319
|
-
// After the server's detect-before-gate reorder, 403 means one
|
|
320
|
-
// thing: genuinely dynamic project, free tier. Printing static
|
|
321
|
-
// success here was a lie (raw server source served as files).
|
|
322
|
-
const gateData = await safeJson(deployRes);
|
|
303
|
+
case 'gated':
|
|
323
304
|
outcome = 'failed';
|
|
324
|
-
failureDetail =
|
|
305
|
+
failureDetail = trig.detail;
|
|
325
306
|
deploySpinner.fail(chalk.red('> this project needs a server'));
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// Server gave up waiting after 10 minutes, but the deploy-agent
|
|
329
|
-
// is still processing the build. Show a non-failure message and
|
|
330
|
-
// exit 0 — the user can poll the dashboard for completion.
|
|
307
|
+
break;
|
|
308
|
+
case 'still-building':
|
|
331
309
|
outcome = 'still-building';
|
|
332
|
-
deploySpinner.warn(chalk.yellow('
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// Server refused to start a second concurrent build for the same
|
|
336
|
-
// version (another `pushto deploy` is already mid-build). Treat
|
|
337
|
-
// as a non-failure — the previous build will land any moment.
|
|
338
|
-
// Server includes `stillBuilding: true` for parity with 504.
|
|
339
|
-
const conflictData = await safeJson(deployRes);
|
|
340
|
-
outcome = 'still-building';
|
|
341
|
-
deploySpinner.warn(chalk.yellow(`> ${conflictData.error ?? 'a build for this version is already in progress'}`));
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
// Real failure (build failed, container crashed, etc.)
|
|
345
|
-
const errData = await safeJson(deployRes);
|
|
310
|
+
deploySpinner.warn(chalk.yellow(`> ${trig.detail ?? 'still building — taking longer than usual'}`));
|
|
311
|
+
break;
|
|
312
|
+
case 'failed':
|
|
346
313
|
outcome = 'failed';
|
|
347
|
-
failureDetail =
|
|
314
|
+
failureDetail = trig.detail;
|
|
348
315
|
deploySpinner.fail(chalk.red(`> deploy failed`));
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
catch (err) {
|
|
352
|
-
outcome = 'failed';
|
|
353
|
-
failureDetail = err instanceof Error ? err.message : 'network error during deploy';
|
|
354
|
-
deploySpinner.fail(chalk.red(`> deploy failed`));
|
|
316
|
+
break;
|
|
355
317
|
}
|
|
356
318
|
}
|
|
357
319
|
// Surface failures honestly — no success box for a broken deploy.
|