servyx-cli 0.1.8 → 0.1.9
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 +1 -1
- package/package.json +1 -1
- package/src/commands/deploy.js +35 -42
- package/src/lib/deploy-stream.js +0 -99
package/README.md
CHANGED
|
@@ -72,4 +72,4 @@ Environment variables:
|
|
|
72
72
|
3. Live dotfiles from `public` are copied into staging so they are not removed on deploy (`.env` is skipped when using `--with-env`)
|
|
73
73
|
4. cli-service calls server-service to queue a CLI deployment (`with_env` tells the agent to sync uploaded env files)
|
|
74
74
|
5. The agent syncs staging → public, preserving `.htaccess` and (by default) `.env` on the server
|
|
75
|
-
6. The CLI
|
|
75
|
+
6. The CLI polls until the deployment succeeds or fails
|
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { getDeployTarget, getDeployment, startDeployment } from '../lib/api.js';
|
|
3
3
|
import { getToken } from '../lib/config.js';
|
|
4
|
-
import { watchDeploymentStream } from '../lib/deploy-stream.js';
|
|
5
4
|
import { loadProjectConfig, loadServyxIgnore } from '../lib/project.js';
|
|
6
5
|
import { rsyncDeploy, sshPreflight, DEFAULT_RSYNC_EXCLUDES, preserveServerDotfiles, ensureRemoteDir, verifyRemoteStagingDir } from '../lib/rsync.js';
|
|
7
6
|
|
|
8
7
|
const TERMINAL_STATUSES = new Set(['success', 'failure', 'cancelled']);
|
|
9
|
-
const
|
|
8
|
+
const MIN_POLL_MS = 3000;
|
|
9
|
+
const MAX_POLL_MS = 10000;
|
|
10
10
|
|
|
11
11
|
function sleep(ms) {
|
|
12
12
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -161,57 +161,50 @@ export async function runDeploy(options = {}) {
|
|
|
161
161
|
|
|
162
162
|
console.log(pc.dim(`Deployment ID: ${deploymentId}`));
|
|
163
163
|
|
|
164
|
+
let pollIntervalMs = MIN_POLL_MS;
|
|
164
165
|
let lastSnapshot = null;
|
|
165
|
-
let current = deployment;
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
while (true) {
|
|
168
|
+
let statusResult;
|
|
169
|
+
try {
|
|
170
|
+
statusResult = await getDeployment(deploymentId);
|
|
171
|
+
pollIntervalMs = Math.min(MAX_POLL_MS, pollIntervalMs + 500);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (err.status === 429 || err.code === 'RATE_LIMITED') {
|
|
174
|
+
const retryMs = (err.retryAfterSeconds || Math.ceil(pollIntervalMs / 1000)) * 1000;
|
|
175
|
+
pollIntervalMs = Math.min(MAX_POLL_MS * 2, Math.max(retryMs, pollIntervalMs * 2));
|
|
176
|
+
console.log(pc.yellow(`Rate limited — retrying in ${Math.ceil(pollIntervalMs / 1000)}s...`));
|
|
177
|
+
await sleep(pollIntervalMs);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const current = statusResult?.data?.deployment;
|
|
184
|
+
if (!current) {
|
|
185
|
+
console.error(pc.red('Deployment not found while polling.'));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
const snapshot = deploymentSnapshot(current);
|
|
170
190
|
if (snapshot !== lastSnapshot) {
|
|
171
191
|
console.log(formatDeploymentLine(current));
|
|
172
192
|
lastSnapshot = snapshot;
|
|
173
193
|
}
|
|
174
|
-
};
|
|
175
194
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (streamError.code !== 'MQTT_UNAVAILABLE' && streamError.status !== 404) {
|
|
182
|
-
console.log(pc.yellow(`Live deploy stream unavailable (${streamError.message}). Falling back to slow status checks...`));
|
|
183
|
-
} else if (streamError.code === 'MQTT_UNAVAILABLE') {
|
|
184
|
-
console.log(pc.yellow('MQTT deploy stream unavailable. Falling back to slow status checks...'));
|
|
185
|
-
} else {
|
|
186
|
-
throw streamError;
|
|
187
|
-
}
|
|
195
|
+
if (TERMINAL_STATUSES.has(current.status)) {
|
|
196
|
+
if (current.status === 'success') {
|
|
197
|
+
console.log(pc.green('Deployment succeeded.'));
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
188
200
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!current) {
|
|
193
|
-
console.error(pc.red('Deployment not found while checking status.'));
|
|
194
|
-
process.exit(1);
|
|
201
|
+
console.error(pc.red(`Deployment ${current.status}.`));
|
|
202
|
+
if (current.error_message) {
|
|
203
|
+
console.error(pc.red(current.error_message));
|
|
195
204
|
}
|
|
196
|
-
|
|
197
|
-
if (TERMINAL_STATUSES.has(current.status)) break;
|
|
198
|
-
await sleep(FALLBACK_POLL_MS);
|
|
205
|
+
process.exit(1);
|
|
199
206
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (!current) {
|
|
203
|
-
console.error(pc.red('Deployment status unavailable.'));
|
|
204
|
-
process.exit(1);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (current.status === 'success') {
|
|
208
|
-
console.log(pc.green('Deployment succeeded.'));
|
|
209
|
-
process.exit(0);
|
|
210
|
-
}
|
|
211
207
|
|
|
212
|
-
|
|
213
|
-
if (current.error_message) {
|
|
214
|
-
console.error(pc.red(current.error_message));
|
|
208
|
+
await sleep(pollIntervalMs);
|
|
215
209
|
}
|
|
216
|
-
process.exit(1);
|
|
217
210
|
}
|
package/src/lib/deploy-stream.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { getApiBaseUrl, getToken } from './config.js';
|
|
2
|
-
|
|
3
|
-
const TERMINAL_STATUSES = new Set(['success', 'failure', 'cancelled']);
|
|
4
|
-
|
|
5
|
-
function parseSseChunk(buffer, onEvent) {
|
|
6
|
-
const blocks = buffer.split('\n\n');
|
|
7
|
-
const remainder = blocks.pop() || '';
|
|
8
|
-
|
|
9
|
-
for (const block of blocks) {
|
|
10
|
-
if (!block.trim()) continue;
|
|
11
|
-
|
|
12
|
-
let event = 'message';
|
|
13
|
-
let data = '';
|
|
14
|
-
|
|
15
|
-
for (const line of block.split('\n')) {
|
|
16
|
-
if (line.startsWith('event:')) {
|
|
17
|
-
event = line.slice(6).trim();
|
|
18
|
-
} else if (line.startsWith('data:')) {
|
|
19
|
-
data += line.slice(5).trim();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!data) continue;
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
onEvent(event, JSON.parse(data));
|
|
27
|
-
} catch {
|
|
28
|
-
// Ignore malformed SSE frames
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return remainder;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Watch deployment progress over a single long-lived SSE stream backed by MQTT.
|
|
37
|
-
*/
|
|
38
|
-
export async function watchDeploymentStream(deploymentId, { onUpdate, signal } = {}) {
|
|
39
|
-
const token = getToken();
|
|
40
|
-
if (!token) {
|
|
41
|
-
throw new Error('Not logged in');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const url = `${getApiBaseUrl()}/api/v1/cli/deployments/${deploymentId}/stream`;
|
|
45
|
-
const response = await fetch(url, {
|
|
46
|
-
headers: {
|
|
47
|
-
Authorization: `Bearer ${token}`,
|
|
48
|
-
Accept: 'text/event-stream',
|
|
49
|
-
},
|
|
50
|
-
signal,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
if (!response.ok) {
|
|
54
|
-
const err = new Error(`Deploy stream failed (${response.status})`);
|
|
55
|
-
err.status = response.status;
|
|
56
|
-
throw err;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!response.body) {
|
|
60
|
-
throw new Error('Deploy stream response has no body');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const reader = response.body.getReader();
|
|
64
|
-
const decoder = new TextDecoder();
|
|
65
|
-
let buffer = '';
|
|
66
|
-
let latestDeployment = null;
|
|
67
|
-
let streamError = null;
|
|
68
|
-
|
|
69
|
-
while (true) {
|
|
70
|
-
const { done, value } = await reader.read();
|
|
71
|
-
if (done) break;
|
|
72
|
-
|
|
73
|
-
buffer += decoder.decode(value, { stream: true });
|
|
74
|
-
buffer = parseSseChunk(buffer, (event, payload) => {
|
|
75
|
-
if (event === 'snapshot' || event === 'progress' || event === 'done') {
|
|
76
|
-
latestDeployment = payload?.deployment || latestDeployment;
|
|
77
|
-
if (latestDeployment) {
|
|
78
|
-
onUpdate?.(latestDeployment, { event, payload });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (event === 'error') {
|
|
83
|
-
const err = new Error(payload?.message || 'Deploy stream error');
|
|
84
|
-
err.code = payload?.code;
|
|
85
|
-
streamError = err;
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (streamError) {
|
|
90
|
-
throw streamError;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return latestDeployment;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function isTerminalDeployment(deployment) {
|
|
98
|
-
return TERMINAL_STATUSES.has(deployment?.status);
|
|
99
|
-
}
|