@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.
Files changed (56) hide show
  1. package/dist/commands/__tests__/mcp-add.test.d.ts +2 -0
  2. package/dist/commands/__tests__/mcp-add.test.d.ts.map +1 -0
  3. package/dist/commands/__tests__/mcp-add.test.js +112 -0
  4. package/dist/commands/__tests__/mcp-add.test.js.map +1 -0
  5. package/dist/commands/__tests__/mcp-serve.test.d.ts +2 -0
  6. package/dist/commands/__tests__/mcp-serve.test.d.ts.map +1 -0
  7. package/dist/commands/__tests__/mcp-serve.test.js +169 -0
  8. package/dist/commands/__tests__/mcp-serve.test.js.map +1 -0
  9. package/dist/commands/deploy.d.ts.map +1 -1
  10. package/dist/commands/deploy.js +63 -101
  11. package/dist/commands/deploy.js.map +1 -1
  12. package/dist/commands/mcp.d.ts +7 -0
  13. package/dist/commands/mcp.d.ts.map +1 -0
  14. package/dist/commands/mcp.js +137 -0
  15. package/dist/commands/mcp.js.map +1 -0
  16. package/dist/index.js +12 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/lib/__tests__/deploy-local.test.d.ts +2 -0
  19. package/dist/lib/__tests__/deploy-local.test.d.ts.map +1 -0
  20. package/dist/lib/__tests__/deploy-local.test.js +103 -0
  21. package/dist/lib/__tests__/deploy-local.test.js.map +1 -0
  22. package/dist/lib/__tests__/mcp-serve-client.test.d.ts +2 -0
  23. package/dist/lib/__tests__/mcp-serve-client.test.d.ts.map +1 -0
  24. package/dist/lib/__tests__/mcp-serve-client.test.js +74 -0
  25. package/dist/lib/__tests__/mcp-serve-client.test.js.map +1 -0
  26. package/dist/lib/__tests__/npm-packaging.test.js +1 -1
  27. package/dist/lib/__tests__/npm-packaging.test.js.map +1 -1
  28. package/dist/lib/mcp-serve/agent-config.d.ts +26 -0
  29. package/dist/lib/mcp-serve/agent-config.d.ts.map +1 -0
  30. package/dist/lib/mcp-serve/agent-config.js +38 -0
  31. package/dist/lib/mcp-serve/agent-config.js.map +1 -0
  32. package/dist/lib/mcp-serve/client.d.ts +22 -0
  33. package/dist/lib/mcp-serve/client.d.ts.map +1 -0
  34. package/dist/lib/mcp-serve/client.js +68 -0
  35. package/dist/lib/mcp-serve/client.js.map +1 -0
  36. package/dist/lib/mcp-serve/deploy-local.d.ts +13 -0
  37. package/dist/lib/mcp-serve/deploy-local.d.ts.map +1 -0
  38. package/dist/lib/mcp-serve/deploy-local.js +99 -0
  39. package/dist/lib/mcp-serve/deploy-local.js.map +1 -0
  40. package/dist/lib/mcp-serve/serve.d.ts +19 -0
  41. package/dist/lib/mcp-serve/serve.d.ts.map +1 -0
  42. package/dist/lib/mcp-serve/serve.js +60 -0
  43. package/dist/lib/mcp-serve/serve.js.map +1 -0
  44. package/dist/lib/mcp-serve/token.d.ts +20 -0
  45. package/dist/lib/mcp-serve/token.d.ts.map +1 -0
  46. package/dist/lib/mcp-serve/token.js +43 -0
  47. package/dist/lib/mcp-serve/token.js.map +1 -0
  48. package/dist/lib/perform-deploy.d.ts +74 -0
  49. package/dist/lib/perform-deploy.d.ts.map +1 -0
  50. package/dist/lib/perform-deploy.js +112 -0
  51. package/dist/lib/perform-deploy.js.map +1 -0
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.d.ts.map +1 -1
  54. package/dist/version.js +3 -3
  55. package/dist/version.js.map +1 -1
  56. package/package.json +2 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mcp-add.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mcp-serve.test.d.ts.map
@@ -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":"AAmBA;;;;;;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,CAiavF,CAAC"}
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"}
@@ -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, deploySignal } from '../lib/api.js';
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 = getGitInfo();
219
- const { buffer: zipBuffer, fileCount: zippedCount } = await zipDirectory(prep.zipDir);
220
- const sizeBytes = zipBuffer.length;
221
- const sizeMB = (sizeBytes / 1024 / 1024).toFixed(1);
222
- // Catch empty uploads before we waste a roundtrip. A user in the wrong
223
- // directory, or with everything gitignored, would otherwise get a
224
- // server-side 400 ~30s later. Tell them up front + point them at the
225
- // doctor command for diagnosis.
226
- if (zippedCount === 0) {
227
- spinner.fail(chalk.red('> no files found in this directory.'));
228
- console.log(chalk.dim(' is your entire project in .gitignore? are you in the wrong directory?'));
229
- console.log(chalk.dim(' run ') + chalk.cyan('pushto doctor') + chalk.dim(' to see what would be uploaded.'));
230
- process.exit(1);
231
- }
232
- // Match the server cap exactly so we fail FAST (locally, instantly)
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
- // Upload to the deploy endpoint (extended timeout for large projects)
246
- const res = await api(`/cli/projects/${slug}/upload`, {
247
- method: 'POST',
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
- const data = await safeJson(res);
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
- try {
287
- const deployRes = await api(`/cli/projects/${slug}/deploy`, {
288
- method: 'POST',
289
- headers: { 'Content-Type': 'application/json' },
290
- body: JSON.stringify({
291
- commitHash: git.hash,
292
- commitMessage: git.message,
293
- branch: git.branch,
294
- builtFramework: prep.kind === 'artifact' ? prep.framework : undefined,
295
- }),
296
- signal: deploySignal(),
297
- });
298
- if (deployRes.ok) {
299
- const deployData = await safeJson(deployRes);
300
- if (deployData.container) {
301
- outcome = 'container';
302
- containerDeployed = true;
303
- finalUrl = deployData.url ?? finalUrl;
304
- const fw = deployData.deployment?.framework ?? 'dynamic';
305
- const dur = deployData.deployment?.buildDuration ?? 0;
306
- containerBuildDuration = deployData.deployment?.buildDuration ?? null;
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
- else if (deployRes.status === 403) {
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 = gateData.error ?? 'dynamic deploys are a paid feature.';
305
+ failureDetail = trig.detail;
325
306
  deploySpinner.fail(chalk.red('> this project needs a server'));
326
- }
327
- else if (deployRes.status === 504) {
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('> still building — taking longer than usual'));
333
- }
334
- else if (deployRes.status === 409) {
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 = errData.error ?? `server returned ${deployRes.status}`;
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.