@syncular/cli 0.0.0-108 → 0.0.0-113
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/package.json +12 -15
- package/src/args.ts +0 -113
- package/src/auth-storage.ts +0 -57
- package/src/buildpacks/index.ts +0 -2
- package/src/buildpacks/registry.ts +0 -47
- package/src/buildpacks/types.ts +0 -1
- package/src/command-registry.ts +0 -685
- package/src/commands/auth.ts +0 -426
- package/src/commands/build.ts +0 -210
- package/src/commands/console.ts +0 -252
- package/src/commands/demo.ts +0 -1151
- package/src/commands/doctor.tsx +0 -133
- package/src/commands/migrate.ts +0 -271
- package/src/commands/project.ts +0 -381
- package/src/commands/target.ts +0 -62
- package/src/commands/typegen.ts +0 -404
- package/src/constants.ts +0 -7
- package/src/control-plane-token.ts +0 -46
- package/src/control-plane.ts +0 -64
- package/src/dev-logging.tsx +0 -415
- package/src/extensions/index.ts +0 -1
- package/src/extensions/manifest.ts +0 -37
- package/src/flags.ts +0 -92
- package/src/help.tsx +0 -239
- package/src/index.ts +0 -4
- package/src/interactive.tsx +0 -306
- package/src/main.tsx +0 -268
- package/src/output.tsx +0 -47
- package/src/paths.ts +0 -11
- package/src/spaces-config.ts +0 -2
- package/src/targets/index.ts +0 -13
- package/src/targets/state.ts +0 -99
- package/src/targets/types.ts +0 -8
- package/src/templates/index.ts +0 -2
- package/src/templates/registry.ts +0 -42
- package/src/templates/syncular-types.ts +0 -10
- package/src/types.ts +0 -67
- package/src/update-check.ts +0 -296
package/src/commands/demo.ts
DELETED
|
@@ -1,1151 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
|
-
import { getBuildpackById } from '../buildpacks';
|
|
6
|
-
import { parseBearerToken, resolveControlPlaneBase } from '../control-plane';
|
|
7
|
-
import { resolveControlPlaneToken } from '../control-plane-token';
|
|
8
|
-
import {
|
|
9
|
-
optionalBooleanFlag,
|
|
10
|
-
optionalEnumFlag,
|
|
11
|
-
optionalFlag,
|
|
12
|
-
optionalIntegerFlag,
|
|
13
|
-
} from '../flags';
|
|
14
|
-
import { printError, printInfo } from '../output';
|
|
15
|
-
import { resolveDefaultSyncularConfigPath, resolveRepoRoot } from '../paths';
|
|
16
|
-
import { resolveEffectiveTargetId } from '../targets';
|
|
17
|
-
|
|
18
|
-
interface RuntimeResponse {
|
|
19
|
-
runtime?: {
|
|
20
|
-
workerName?: string;
|
|
21
|
-
hostname?: string;
|
|
22
|
-
baseUrl?: string;
|
|
23
|
-
syncUrl?: string;
|
|
24
|
-
consoleToken?: string;
|
|
25
|
-
databaseProvider?: 'sqlite' | 'neon' | 'postgres';
|
|
26
|
-
};
|
|
27
|
-
error?: string;
|
|
28
|
-
message?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface RuntimeInfo {
|
|
32
|
-
workerName: string;
|
|
33
|
-
hostname: string;
|
|
34
|
-
baseUrl: string;
|
|
35
|
-
syncUrl: string;
|
|
36
|
-
consoleToken: string | null;
|
|
37
|
-
databaseProvider: 'sqlite' | 'neon' | 'postgres';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface DeploymentResponse {
|
|
41
|
-
deployment?: {
|
|
42
|
-
id?: string;
|
|
43
|
-
scriptName?: string;
|
|
44
|
-
status?: string;
|
|
45
|
-
createdAt?: string;
|
|
46
|
-
artifactHash?: string | null;
|
|
47
|
-
rollbackOfDeploymentId?: string | null;
|
|
48
|
-
};
|
|
49
|
-
error?: string;
|
|
50
|
-
message?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface DeploymentsResponse {
|
|
54
|
-
deployments?: Array<{
|
|
55
|
-
id?: string;
|
|
56
|
-
scriptName?: string;
|
|
57
|
-
source?: string;
|
|
58
|
-
status?: string;
|
|
59
|
-
createdAt?: string;
|
|
60
|
-
artifactHash?: string | null;
|
|
61
|
-
rollbackOfDeploymentId?: string | null;
|
|
62
|
-
}>;
|
|
63
|
-
error?: string;
|
|
64
|
-
message?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface DeployArtifactResponse {
|
|
68
|
-
artifact?: {
|
|
69
|
-
id?: string;
|
|
70
|
-
scriptName?: string;
|
|
71
|
-
artifactHash?: string;
|
|
72
|
-
source?: string;
|
|
73
|
-
byteSize?: number;
|
|
74
|
-
createdAt?: string;
|
|
75
|
-
};
|
|
76
|
-
error?: string;
|
|
77
|
-
message?: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface DeployJobResponse {
|
|
81
|
-
job?: {
|
|
82
|
-
id?: string;
|
|
83
|
-
artifactId?: string;
|
|
84
|
-
deploymentId?: string | null;
|
|
85
|
-
scriptName?: string;
|
|
86
|
-
status?:
|
|
87
|
-
| 'queued'
|
|
88
|
-
| 'preparing'
|
|
89
|
-
| 'deploying'
|
|
90
|
-
| 'verifying'
|
|
91
|
-
| 'activating'
|
|
92
|
-
| 'completed'
|
|
93
|
-
| 'failed';
|
|
94
|
-
errorText?: string | null;
|
|
95
|
-
createdAt?: string;
|
|
96
|
-
updatedAt?: string;
|
|
97
|
-
completedAt?: string | null;
|
|
98
|
-
};
|
|
99
|
-
error?: string;
|
|
100
|
-
message?: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
type SpaceDbProvider = 'sqlite' | 'neon' | 'postgres';
|
|
104
|
-
type SpaceRegion = 'auto' | 'enam' | 'wnam' | 'weur' | 'eeur' | 'apac' | 'oc';
|
|
105
|
-
|
|
106
|
-
interface CreateSpaceResponse {
|
|
107
|
-
space?: {
|
|
108
|
-
id?: string;
|
|
109
|
-
name?: string;
|
|
110
|
-
};
|
|
111
|
-
provisionJob?: {
|
|
112
|
-
id?: string;
|
|
113
|
-
status?: string;
|
|
114
|
-
};
|
|
115
|
-
runtime?: {
|
|
116
|
-
workerName?: string;
|
|
117
|
-
hostname?: string;
|
|
118
|
-
baseUrl?: string;
|
|
119
|
-
syncUrl?: string;
|
|
120
|
-
consoleUrl?: string;
|
|
121
|
-
consoleServerUrl?: string;
|
|
122
|
-
consoleToken?: string;
|
|
123
|
-
publishableKey?: string;
|
|
124
|
-
databaseProvider?: SpaceDbProvider;
|
|
125
|
-
databaseName?: string;
|
|
126
|
-
region?: string | null;
|
|
127
|
-
};
|
|
128
|
-
error?: string;
|
|
129
|
-
message?: string;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const DEFAULT_FETCH_TIMEOUT_MS = 15_000;
|
|
133
|
-
const CLI_SOURCE_LABEL = 'syncular-cli';
|
|
134
|
-
const CONTRACT_WORKER_BUILDPACK_ID = 'contract-worker';
|
|
135
|
-
const DEFAULT_VERIFY_CORS_ORIGIN = 'http://127.0.0.1:4320';
|
|
136
|
-
const SPACE_DB_PROVIDERS = ['sqlite', 'neon', 'postgres'] as const;
|
|
137
|
-
const SPACE_REGIONS = [
|
|
138
|
-
'auto',
|
|
139
|
-
'enam',
|
|
140
|
-
'wnam',
|
|
141
|
-
'weur',
|
|
142
|
-
'eeur',
|
|
143
|
-
'apac',
|
|
144
|
-
'oc',
|
|
145
|
-
] as const;
|
|
146
|
-
|
|
147
|
-
const contractWorkerBuildpack = (() => {
|
|
148
|
-
const buildpack = getBuildpackById(CONTRACT_WORKER_BUILDPACK_ID);
|
|
149
|
-
if (!buildpack) {
|
|
150
|
-
throw new Error(
|
|
151
|
-
`Required buildpack "${CONTRACT_WORKER_BUILDPACK_ID}" is not registered.`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
return buildpack;
|
|
155
|
-
})();
|
|
156
|
-
|
|
157
|
-
function logStep(message: string): void {
|
|
158
|
-
printInfo(`[${CLI_SOURCE_LABEL}] ${message}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function fetchWithTimeout(
|
|
162
|
-
url: string,
|
|
163
|
-
init?: RequestInit,
|
|
164
|
-
timeoutMs = DEFAULT_FETCH_TIMEOUT_MS
|
|
165
|
-
): Promise<Response> {
|
|
166
|
-
try {
|
|
167
|
-
return await fetch(url, {
|
|
168
|
-
...init,
|
|
169
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
170
|
-
});
|
|
171
|
-
} catch (error: unknown) {
|
|
172
|
-
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
173
|
-
throw new Error(`Request timed out after ${timeoutMs}ms: ${url}`);
|
|
174
|
-
}
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function requiredFlag(
|
|
180
|
-
flagValues: Map<string, string>,
|
|
181
|
-
flag: string,
|
|
182
|
-
envName?: string
|
|
183
|
-
): string {
|
|
184
|
-
const value =
|
|
185
|
-
flagValues.get(flag)?.trim() ||
|
|
186
|
-
(envName ? process.env[envName]?.trim() : null);
|
|
187
|
-
if (!value) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
`Missing required ${flag}${envName ? ` (or ${envName})` : ''}`
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
return value;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function resolveControlTokenForCommand(args: {
|
|
196
|
-
flagValues: Map<string, string>;
|
|
197
|
-
controlPlaneBase: string;
|
|
198
|
-
}): Promise<string> {
|
|
199
|
-
return (
|
|
200
|
-
(await resolveControlPlaneToken({
|
|
201
|
-
flagValues: args.flagValues,
|
|
202
|
-
controlPlaneBase: args.controlPlaneBase,
|
|
203
|
-
includeStdinToken: false,
|
|
204
|
-
includeStoredToken: true,
|
|
205
|
-
})) || ''
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function buildControlPlaneHeaders(args: {
|
|
210
|
-
actorId: string;
|
|
211
|
-
controlToken: string;
|
|
212
|
-
}): Record<string, string> {
|
|
213
|
-
const token = parseBearerToken(args.controlToken);
|
|
214
|
-
if (token.length > 0) {
|
|
215
|
-
return {
|
|
216
|
-
Authorization: `Bearer ${token}`,
|
|
217
|
-
'Content-Type': 'application/json',
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
return {
|
|
221
|
-
'x-user-id': args.actorId,
|
|
222
|
-
'Content-Type': 'application/json',
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function resolveDefaultSpaceContractPath(): string {
|
|
227
|
-
return resolveDefaultSyncularConfigPath(process.cwd());
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function resolveSyncularVersion(): string {
|
|
231
|
-
try {
|
|
232
|
-
const repoRoot = resolveRepoRoot();
|
|
233
|
-
const syncularPackageJsonPath = resolve(
|
|
234
|
-
repoRoot,
|
|
235
|
-
'..',
|
|
236
|
-
'syncular',
|
|
237
|
-
'package.json'
|
|
238
|
-
);
|
|
239
|
-
const parsed = JSON.parse(
|
|
240
|
-
readFileSync(syncularPackageJsonPath, 'utf8')
|
|
241
|
-
) as {
|
|
242
|
-
version?: string;
|
|
243
|
-
};
|
|
244
|
-
if (typeof parsed.version === 'string' && parsed.version.length > 0) {
|
|
245
|
-
return parsed.version;
|
|
246
|
-
}
|
|
247
|
-
} catch {
|
|
248
|
-
// ignored - fallback below
|
|
249
|
-
}
|
|
250
|
-
return 'unknown';
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function resolveSyncularCliVersion(): string {
|
|
254
|
-
const explicitVersion =
|
|
255
|
-
process.env.SYNCULAR_CLI_NPM_VERSION?.trim() ||
|
|
256
|
-
process.env.npm_package_version?.trim() ||
|
|
257
|
-
'';
|
|
258
|
-
return explicitVersion.length > 0 ? explicitVersion : '0.0.0';
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function resolveGitCommit(): string {
|
|
262
|
-
try {
|
|
263
|
-
const repoRoot = resolveRepoRoot();
|
|
264
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
265
|
-
cwd: repoRoot,
|
|
266
|
-
encoding: 'utf8',
|
|
267
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
268
|
-
}).trim();
|
|
269
|
-
} catch {
|
|
270
|
-
return 'unknown';
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function buildImmutableScriptName(spaceId: string): string {
|
|
275
|
-
const suffix = `${Date.now().toString(36)}-${randomUUID().slice(0, 6)}`;
|
|
276
|
-
return `space-${spaceId}-${suffix}`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async function waitForSuccessfulFetch(args: {
|
|
280
|
-
url: string;
|
|
281
|
-
init?: RequestInit;
|
|
282
|
-
label: string;
|
|
283
|
-
attempts?: number;
|
|
284
|
-
delayMs?: number;
|
|
285
|
-
timeoutMs?: number;
|
|
286
|
-
}): Promise<Response> {
|
|
287
|
-
const attempts = args.attempts ?? 12;
|
|
288
|
-
const delayMs = args.delayMs ?? 1000;
|
|
289
|
-
const timeoutMs = args.timeoutMs ?? 8_000;
|
|
290
|
-
|
|
291
|
-
let lastResponse: Response | null = null;
|
|
292
|
-
let lastError: Error | null = null;
|
|
293
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
294
|
-
try {
|
|
295
|
-
const response = await fetchWithTimeout(args.url, args.init, timeoutMs);
|
|
296
|
-
lastResponse = response;
|
|
297
|
-
if (response.ok) {
|
|
298
|
-
return response;
|
|
299
|
-
}
|
|
300
|
-
} catch (error: unknown) {
|
|
301
|
-
lastError =
|
|
302
|
-
error instanceof Error ? error : new Error('Unknown request error');
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (attempt < attempts) {
|
|
306
|
-
const reason = lastResponse
|
|
307
|
-
? `${lastResponse.status} ${lastResponse.statusText}`
|
|
308
|
-
: (lastError?.message ?? 'unknown error');
|
|
309
|
-
logStep(
|
|
310
|
-
`${args.label} attempt ${attempt}/${attempts} failed (${reason}), retrying in ${delayMs}ms`
|
|
311
|
-
);
|
|
312
|
-
await Bun.sleep(delayMs);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
throw new Error(
|
|
317
|
-
`${args.label} failed (${lastResponse ? `${lastResponse.status} ${lastResponse.statusText}` : (lastError?.message ?? 'unknown error')})`
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function hasCaseInsensitiveToken(value: string | null, token: string): boolean {
|
|
322
|
-
if (!value) {
|
|
323
|
-
return false;
|
|
324
|
-
}
|
|
325
|
-
const normalizedToken = token.trim().toLowerCase();
|
|
326
|
-
return value
|
|
327
|
-
.split(',')
|
|
328
|
-
.map((entry) => entry.trim().toLowerCase())
|
|
329
|
-
.includes(normalizedToken);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function verifyRuntimeSyncCorsPreflight(args: {
|
|
333
|
-
baseUrl: string;
|
|
334
|
-
origin: string;
|
|
335
|
-
}): Promise<void> {
|
|
336
|
-
const targetUrl = `${args.baseUrl.replace(/\/$/, '')}/api/sync`;
|
|
337
|
-
const response = await fetchWithTimeout(
|
|
338
|
-
targetUrl,
|
|
339
|
-
{
|
|
340
|
-
method: 'OPTIONS',
|
|
341
|
-
headers: {
|
|
342
|
-
Origin: args.origin,
|
|
343
|
-
'Access-Control-Request-Method': 'POST',
|
|
344
|
-
'Access-Control-Request-Headers':
|
|
345
|
-
'authorization,content-type,x-syncular-publishable-key',
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
10_000
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
if (!response.ok) {
|
|
352
|
-
let details = `${response.status} ${response.statusText}`;
|
|
353
|
-
try {
|
|
354
|
-
const payload = (await response.clone().json()) as {
|
|
355
|
-
error?: string;
|
|
356
|
-
message?: string;
|
|
357
|
-
};
|
|
358
|
-
details = payload.message || payload.error || details;
|
|
359
|
-
} catch {
|
|
360
|
-
// ignored
|
|
361
|
-
}
|
|
362
|
-
throw new Error(
|
|
363
|
-
`Runtime CORS preflight failed for ${args.origin}: ${details}`
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const allowOrigin = response.headers.get('access-control-allow-origin');
|
|
368
|
-
if (allowOrigin !== '*' && allowOrigin !== args.origin) {
|
|
369
|
-
throw new Error(
|
|
370
|
-
`Runtime CORS origin mismatch for ${args.origin}. Received Access-Control-Allow-Origin=${allowOrigin ?? 'null'}.`
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (
|
|
375
|
-
!hasCaseInsensitiveToken(
|
|
376
|
-
response.headers.get('access-control-allow-methods'),
|
|
377
|
-
'POST'
|
|
378
|
-
)
|
|
379
|
-
) {
|
|
380
|
-
throw new Error('Runtime CORS preflight missing POST in allow-methods.');
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const requiredHeaders = [
|
|
384
|
-
'authorization',
|
|
385
|
-
'content-type',
|
|
386
|
-
'x-syncular-publishable-key',
|
|
387
|
-
];
|
|
388
|
-
for (const requiredHeader of requiredHeaders) {
|
|
389
|
-
if (
|
|
390
|
-
!hasCaseInsensitiveToken(
|
|
391
|
-
response.headers.get('access-control-allow-headers'),
|
|
392
|
-
requiredHeader
|
|
393
|
-
)
|
|
394
|
-
) {
|
|
395
|
-
throw new Error(
|
|
396
|
-
`Runtime CORS preflight missing ${requiredHeader} in allow-headers.`
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
async function getSpaceRuntime(args: {
|
|
403
|
-
controlPlaneBase: string;
|
|
404
|
-
actorId: string;
|
|
405
|
-
controlToken: string;
|
|
406
|
-
spaceId: string;
|
|
407
|
-
}): Promise<RuntimeInfo> {
|
|
408
|
-
const response = await fetchWithTimeout(
|
|
409
|
-
`${args.controlPlaneBase.replace(/\/$/, '')}/spaces/${args.spaceId}/runtime`,
|
|
410
|
-
{
|
|
411
|
-
method: 'GET',
|
|
412
|
-
headers: buildControlPlaneHeaders(args),
|
|
413
|
-
},
|
|
414
|
-
10_000
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
const payload = (await response.json()) as RuntimeResponse;
|
|
418
|
-
if (
|
|
419
|
-
!response.ok ||
|
|
420
|
-
!payload.runtime?.workerName ||
|
|
421
|
-
!payload.runtime.baseUrl
|
|
422
|
-
) {
|
|
423
|
-
throw new Error(
|
|
424
|
-
`Failed to load runtime for ${args.spaceId} (${response.status}): ${JSON.stringify(payload)}`
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const runtime = payload.runtime;
|
|
429
|
-
const workerName = runtime?.workerName;
|
|
430
|
-
const baseUrl = runtime?.baseUrl;
|
|
431
|
-
const hostname = runtime?.hostname;
|
|
432
|
-
const syncUrl = runtime?.syncUrl;
|
|
433
|
-
const consoleToken = runtime?.consoleToken?.trim() || null;
|
|
434
|
-
const databaseProvider =
|
|
435
|
-
runtime?.databaseProvider === 'neon' ||
|
|
436
|
-
runtime?.databaseProvider === 'postgres'
|
|
437
|
-
? runtime.databaseProvider
|
|
438
|
-
: 'sqlite';
|
|
439
|
-
|
|
440
|
-
if (!workerName || !baseUrl || !hostname || !syncUrl) {
|
|
441
|
-
throw new Error(
|
|
442
|
-
`Runtime response for ${args.spaceId} is incomplete: ${JSON.stringify(payload)}`
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return {
|
|
447
|
-
workerName,
|
|
448
|
-
hostname,
|
|
449
|
-
baseUrl,
|
|
450
|
-
syncUrl,
|
|
451
|
-
consoleToken,
|
|
452
|
-
databaseProvider,
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async function createDeployArtifactRecord(args: {
|
|
457
|
-
controlPlaneBase: string;
|
|
458
|
-
actorId: string;
|
|
459
|
-
controlToken: string;
|
|
460
|
-
spaceId: string;
|
|
461
|
-
scriptName: string;
|
|
462
|
-
artifactHash: string;
|
|
463
|
-
source: string;
|
|
464
|
-
sourceLabel: string;
|
|
465
|
-
manifest: Record<string, unknown>;
|
|
466
|
-
}): Promise<string> {
|
|
467
|
-
const response = await fetchWithTimeout(
|
|
468
|
-
`${args.controlPlaneBase.replace(/\/$/, '')}/spaces/${args.spaceId}/deploy-artifacts`,
|
|
469
|
-
{
|
|
470
|
-
method: 'POST',
|
|
471
|
-
headers: buildControlPlaneHeaders(args),
|
|
472
|
-
body: JSON.stringify({
|
|
473
|
-
scriptName: args.scriptName,
|
|
474
|
-
artifactHash: args.artifactHash,
|
|
475
|
-
source: args.source,
|
|
476
|
-
sourceLabel: args.sourceLabel,
|
|
477
|
-
manifest: args.manifest,
|
|
478
|
-
}),
|
|
479
|
-
},
|
|
480
|
-
30_000
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
const payload = (await response.json()) as DeployArtifactResponse;
|
|
484
|
-
if (!response.ok || !payload.artifact?.id) {
|
|
485
|
-
throw new Error(
|
|
486
|
-
`Failed to create deploy artifact (${response.status}): ${JSON.stringify(payload)}`
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return payload.artifact.id;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
async function createDeployJobRecord(args: {
|
|
494
|
-
controlPlaneBase: string;
|
|
495
|
-
actorId: string;
|
|
496
|
-
controlToken: string;
|
|
497
|
-
spaceId: string;
|
|
498
|
-
artifactId: string;
|
|
499
|
-
scriptName: string;
|
|
500
|
-
}): Promise<NonNullable<DeployJobResponse['job']>> {
|
|
501
|
-
const response = await fetchWithTimeout(
|
|
502
|
-
`${args.controlPlaneBase.replace(/\/$/, '')}/spaces/${args.spaceId}/deploy-jobs`,
|
|
503
|
-
{
|
|
504
|
-
method: 'POST',
|
|
505
|
-
headers: buildControlPlaneHeaders(args),
|
|
506
|
-
body: JSON.stringify({
|
|
507
|
-
artifactId: args.artifactId,
|
|
508
|
-
scriptName: args.scriptName,
|
|
509
|
-
}),
|
|
510
|
-
},
|
|
511
|
-
20_000
|
|
512
|
-
);
|
|
513
|
-
|
|
514
|
-
const payload = (await response.json()) as DeployJobResponse;
|
|
515
|
-
if (!response.ok || !payload.job?.id) {
|
|
516
|
-
throw new Error(
|
|
517
|
-
`Failed to create deploy job (${response.status}): ${JSON.stringify(payload)}`
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return payload.job;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
async function getDeployJob(args: {
|
|
525
|
-
controlPlaneBase: string;
|
|
526
|
-
actorId: string;
|
|
527
|
-
controlToken: string;
|
|
528
|
-
spaceId: string;
|
|
529
|
-
jobId: string;
|
|
530
|
-
}): Promise<NonNullable<DeployJobResponse['job']>> {
|
|
531
|
-
const response = await fetchWithTimeout(
|
|
532
|
-
`${args.controlPlaneBase.replace(/\/$/, '')}/spaces/${args.spaceId}/deploy-jobs/${args.jobId}`,
|
|
533
|
-
{
|
|
534
|
-
method: 'GET',
|
|
535
|
-
headers: buildControlPlaneHeaders(args),
|
|
536
|
-
},
|
|
537
|
-
10_000
|
|
538
|
-
);
|
|
539
|
-
|
|
540
|
-
const payload = (await response.json()) as DeployJobResponse;
|
|
541
|
-
if (!response.ok || !payload.job?.id) {
|
|
542
|
-
throw new Error(
|
|
543
|
-
`Failed to load deploy job ${args.jobId} (${response.status}): ${JSON.stringify(payload)}`
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return payload.job;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async function waitForDeployJob(args: {
|
|
551
|
-
controlPlaneBase: string;
|
|
552
|
-
actorId: string;
|
|
553
|
-
controlToken: string;
|
|
554
|
-
spaceId: string;
|
|
555
|
-
jobId: string;
|
|
556
|
-
timeoutMs?: number;
|
|
557
|
-
pollMs?: number;
|
|
558
|
-
}): Promise<NonNullable<DeployJobResponse['job']>> {
|
|
559
|
-
const timeoutMs = args.timeoutMs ?? 180_000;
|
|
560
|
-
const pollMs = args.pollMs ?? 1_000;
|
|
561
|
-
const startedAt = Date.now();
|
|
562
|
-
|
|
563
|
-
let lastStatus: string | undefined;
|
|
564
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
565
|
-
const job = await getDeployJob(args);
|
|
566
|
-
if (job.status && job.status !== lastStatus) {
|
|
567
|
-
logStep(`Deploy job ${job.id} -> ${job.status}`);
|
|
568
|
-
lastStatus = job.status;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (job.status === 'completed' || job.status === 'failed') {
|
|
572
|
-
return job;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
await Bun.sleep(pollMs);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
throw new Error(`Timed out waiting for deploy job ${args.jobId}.`);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
async function listDeploymentRecords(args: {
|
|
582
|
-
controlPlaneBase: string;
|
|
583
|
-
actorId: string;
|
|
584
|
-
controlToken: string;
|
|
585
|
-
spaceId: string;
|
|
586
|
-
limit: number;
|
|
587
|
-
}): Promise<DeploymentsResponse> {
|
|
588
|
-
const url = new URL(
|
|
589
|
-
`${args.controlPlaneBase.replace(/\/$/, '')}/spaces/${args.spaceId}/deployments`
|
|
590
|
-
);
|
|
591
|
-
url.searchParams.set('limit', String(args.limit));
|
|
592
|
-
|
|
593
|
-
const response = await fetchWithTimeout(
|
|
594
|
-
url.toString(),
|
|
595
|
-
{
|
|
596
|
-
method: 'GET',
|
|
597
|
-
headers: buildControlPlaneHeaders(args),
|
|
598
|
-
},
|
|
599
|
-
10_000
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
const payload = (await response.json()) as DeploymentsResponse;
|
|
603
|
-
if (!response.ok) {
|
|
604
|
-
throw new Error(
|
|
605
|
-
`Failed to list deployment records (${response.status}): ${JSON.stringify(payload)}`
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return payload;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
async function rollbackDeploymentRecord(args: {
|
|
613
|
-
controlPlaneBase: string;
|
|
614
|
-
actorId: string;
|
|
615
|
-
controlToken: string;
|
|
616
|
-
spaceId: string;
|
|
617
|
-
deploymentId: string;
|
|
618
|
-
reason?: string;
|
|
619
|
-
}): Promise<DeploymentResponse['deployment']> {
|
|
620
|
-
const response = await fetchWithTimeout(
|
|
621
|
-
`${args.controlPlaneBase.replace(/\/$/, '')}/spaces/${args.spaceId}/deployments/${args.deploymentId}/rollback`,
|
|
622
|
-
{
|
|
623
|
-
method: 'POST',
|
|
624
|
-
headers: buildControlPlaneHeaders(args),
|
|
625
|
-
body: JSON.stringify(args.reason ? { reason: args.reason } : {}),
|
|
626
|
-
},
|
|
627
|
-
20_000
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
const payload = (await response.json()) as DeploymentResponse;
|
|
631
|
-
if (!response.ok) {
|
|
632
|
-
throw new Error(
|
|
633
|
-
`Failed to rollback deployment (${response.status}): ${JSON.stringify(payload)}`
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return payload.deployment;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function redactConsoleUrl(url: string): string {
|
|
641
|
-
try {
|
|
642
|
-
const parsed = new URL(url);
|
|
643
|
-
if (parsed.searchParams.has('token')) {
|
|
644
|
-
parsed.searchParams.set('token', 'REDACTED');
|
|
645
|
-
}
|
|
646
|
-
return parsed.toString();
|
|
647
|
-
} catch {
|
|
648
|
-
return url.replace(/([?&]token=)[^&]+/gi, '$1REDACTED');
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function redactCreateSpaceValue(value: unknown): unknown {
|
|
653
|
-
if (Array.isArray(value)) {
|
|
654
|
-
return value.map((entry) => redactCreateSpaceValue(entry));
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (value && typeof value === 'object') {
|
|
658
|
-
const redacted: Record<string, unknown> = {};
|
|
659
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
660
|
-
if (key === 'consoleToken') {
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
if (key === 'consoleUrl' && typeof nestedValue === 'string') {
|
|
664
|
-
redacted[key] = redactConsoleUrl(nestedValue);
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
redacted[key] = redactCreateSpaceValue(nestedValue);
|
|
668
|
-
}
|
|
669
|
-
return redacted;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return value;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function redactCreateSpaceResponse(payload: CreateSpaceResponse): unknown {
|
|
676
|
-
return redactCreateSpaceValue(payload);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async function parseCreateSpaceResponse(
|
|
680
|
-
response: Response
|
|
681
|
-
): Promise<CreateSpaceResponse | null> {
|
|
682
|
-
try {
|
|
683
|
-
return (await response.json()) as CreateSpaceResponse;
|
|
684
|
-
} catch {
|
|
685
|
-
return null;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
export async function runCreateSpace(
|
|
690
|
-
flagValues: Map<string, string>
|
|
691
|
-
): Promise<number> {
|
|
692
|
-
try {
|
|
693
|
-
const name = requiredFlag(flagValues, '--name');
|
|
694
|
-
const databaseProvider = optionalEnumFlag(
|
|
695
|
-
flagValues,
|
|
696
|
-
'--database-provider',
|
|
697
|
-
SPACE_DB_PROVIDERS,
|
|
698
|
-
'sqlite'
|
|
699
|
-
);
|
|
700
|
-
const region = optionalEnumFlag(
|
|
701
|
-
flagValues,
|
|
702
|
-
'--region',
|
|
703
|
-
SPACE_REGIONS,
|
|
704
|
-
'auto'
|
|
705
|
-
);
|
|
706
|
-
const connectionString = flagValues.get('--connection-string')?.trim();
|
|
707
|
-
if (
|
|
708
|
-
(databaseProvider === 'neon' || databaseProvider === 'postgres') &&
|
|
709
|
-
!connectionString
|
|
710
|
-
) {
|
|
711
|
-
throw new Error(
|
|
712
|
-
'--connection-string is required when --database-provider is neon or postgres.'
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const controlPlaneBase = resolveControlPlaneBase(flagValues);
|
|
717
|
-
const actorId = optionalFlag(
|
|
718
|
-
flagValues,
|
|
719
|
-
'--actor-id',
|
|
720
|
-
process.env.SPACES_TEST_ACTOR_ID?.trim() || CLI_SOURCE_LABEL
|
|
721
|
-
);
|
|
722
|
-
const controlToken = await resolveControlTokenForCommand({
|
|
723
|
-
flagValues,
|
|
724
|
-
controlPlaneBase,
|
|
725
|
-
});
|
|
726
|
-
const jsonOutput = optionalBooleanFlag(flagValues, '--json', false);
|
|
727
|
-
|
|
728
|
-
const requestBody: {
|
|
729
|
-
name: string;
|
|
730
|
-
databaseProvider: SpaceDbProvider;
|
|
731
|
-
region: SpaceRegion;
|
|
732
|
-
connectionString?: string;
|
|
733
|
-
} = {
|
|
734
|
-
name,
|
|
735
|
-
databaseProvider,
|
|
736
|
-
region,
|
|
737
|
-
};
|
|
738
|
-
if (connectionString && connectionString.length > 0) {
|
|
739
|
-
requestBody.connectionString = connectionString;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const response = await fetchWithTimeout(
|
|
743
|
-
`${controlPlaneBase.replace(/\/$/, '')}/spaces`,
|
|
744
|
-
{
|
|
745
|
-
method: 'POST',
|
|
746
|
-
headers: buildControlPlaneHeaders({
|
|
747
|
-
actorId,
|
|
748
|
-
controlToken,
|
|
749
|
-
}),
|
|
750
|
-
body: JSON.stringify(requestBody),
|
|
751
|
-
},
|
|
752
|
-
30_000
|
|
753
|
-
);
|
|
754
|
-
|
|
755
|
-
const payload = await parseCreateSpaceResponse(response);
|
|
756
|
-
if (!response.ok) {
|
|
757
|
-
throw new Error(
|
|
758
|
-
`Failed to create space (${response.status}): ${payload?.message || payload?.error || response.statusText || 'unknown error'}`
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const spaceId = payload?.space?.id;
|
|
763
|
-
const runtimeBaseUrl = payload?.runtime?.baseUrl;
|
|
764
|
-
const runtimeSyncUrl = payload?.runtime?.syncUrl;
|
|
765
|
-
if (!spaceId || !runtimeBaseUrl || !runtimeSyncUrl) {
|
|
766
|
-
throw new Error(
|
|
767
|
-
`Create-space response is incomplete: ${JSON.stringify(payload)}`
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
if (jsonOutput) {
|
|
772
|
-
console.log(JSON.stringify(redactCreateSpaceResponse(payload), null, 2));
|
|
773
|
-
return 0;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
console.log('Space created successfully.');
|
|
777
|
-
console.log(`Space: ${spaceId}`);
|
|
778
|
-
console.log(`Name: ${payload.space?.name ?? name}`);
|
|
779
|
-
console.log(`Runtime URL: ${runtimeBaseUrl}`);
|
|
780
|
-
console.log(`Sync URL: ${runtimeSyncUrl}`);
|
|
781
|
-
if (payload.runtime?.consoleUrl) {
|
|
782
|
-
console.log(
|
|
783
|
-
`Console URL: ${redactConsoleUrl(payload.runtime.consoleUrl)}`
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
console.log(
|
|
787
|
-
`Database provider: ${payload.runtime?.databaseProvider ?? databaseProvider}`
|
|
788
|
-
);
|
|
789
|
-
if (payload.runtime?.region) {
|
|
790
|
-
console.log(`Region: ${payload.runtime.region}`);
|
|
791
|
-
}
|
|
792
|
-
if (payload.provisionJob?.id) {
|
|
793
|
-
console.log(`Provision job: ${payload.provisionJob.id}`);
|
|
794
|
-
}
|
|
795
|
-
return 0;
|
|
796
|
-
} catch (error: unknown) {
|
|
797
|
-
printError(
|
|
798
|
-
error instanceof Error
|
|
799
|
-
? error.message
|
|
800
|
-
: 'Create-space failed unexpectedly.'
|
|
801
|
-
);
|
|
802
|
-
return 1;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
async function runDeployImmutable(
|
|
807
|
-
flagValues: Map<string, string>
|
|
808
|
-
): Promise<number> {
|
|
809
|
-
try {
|
|
810
|
-
const spaceId = requiredFlag(flagValues, '--space');
|
|
811
|
-
const controlPlaneBase = resolveControlPlaneBase(flagValues);
|
|
812
|
-
const actorId = optionalFlag(
|
|
813
|
-
flagValues,
|
|
814
|
-
'--actor-id',
|
|
815
|
-
process.env.SPACES_TEST_ACTOR_ID?.trim() || CLI_SOURCE_LABEL
|
|
816
|
-
);
|
|
817
|
-
const controlToken = await resolveControlTokenForCommand({
|
|
818
|
-
flagValues,
|
|
819
|
-
controlPlaneBase,
|
|
820
|
-
});
|
|
821
|
-
const sourceLabel = optionalFlag(flagValues, '--source', CLI_SOURCE_LABEL);
|
|
822
|
-
const dryRun = optionalBooleanFlag(flagValues, '--dry-run', false);
|
|
823
|
-
const explicitEntry = flagValues.get('--entry')?.trim() || null;
|
|
824
|
-
const configPath = optionalFlag(
|
|
825
|
-
flagValues,
|
|
826
|
-
'--config',
|
|
827
|
-
resolveDefaultSpaceContractPath()
|
|
828
|
-
);
|
|
829
|
-
if (!explicitEntry && !existsSync(configPath)) {
|
|
830
|
-
throw new Error(
|
|
831
|
-
`Syncular config not found: ${configPath}. Provide --config or --entry.`
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const scriptName = optionalFlag(
|
|
836
|
-
flagValues,
|
|
837
|
-
'--script-name',
|
|
838
|
-
buildImmutableScriptName(spaceId)
|
|
839
|
-
);
|
|
840
|
-
|
|
841
|
-
let runtime: RuntimeInfo | null = null;
|
|
842
|
-
if (!dryRun) {
|
|
843
|
-
logStep(`Loading runtime for space ${spaceId}`);
|
|
844
|
-
runtime = await getSpaceRuntime({
|
|
845
|
-
controlPlaneBase,
|
|
846
|
-
actorId,
|
|
847
|
-
controlToken,
|
|
848
|
-
spaceId,
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const bundleLabel = explicitEntry
|
|
853
|
-
? `entry ${explicitEntry}`
|
|
854
|
-
: `config ${configPath}`;
|
|
855
|
-
logStep(`Bundling space contract module from ${bundleLabel}`);
|
|
856
|
-
const migrationMetadata = await contractWorkerBuildpack.inspect({
|
|
857
|
-
configPath,
|
|
858
|
-
explicitEntry,
|
|
859
|
-
});
|
|
860
|
-
const deploymentMetadata = {
|
|
861
|
-
deployedAt: new Date().toISOString(),
|
|
862
|
-
syncularVersion: resolveSyncularVersion(),
|
|
863
|
-
syncularCliVersion: resolveSyncularCliVersion(),
|
|
864
|
-
gitCommit: resolveGitCommit(),
|
|
865
|
-
spaceId,
|
|
866
|
-
workerName: scriptName,
|
|
867
|
-
source: sourceLabel,
|
|
868
|
-
schemaVersion: migrationMetadata.schemaVersion,
|
|
869
|
-
migrationDigest: migrationMetadata.migrationDigest,
|
|
870
|
-
migrations: {
|
|
871
|
-
schemaVersion: migrationMetadata.schemaVersion,
|
|
872
|
-
migrationDigest: migrationMetadata.migrationDigest,
|
|
873
|
-
count: migrationMetadata.migrationCount,
|
|
874
|
-
},
|
|
875
|
-
...(explicitEntry ? { entry: explicitEntry } : { configPath }),
|
|
876
|
-
};
|
|
877
|
-
const { source, artifactHash } = await contractWorkerBuildpack.build({
|
|
878
|
-
configPath,
|
|
879
|
-
explicitEntry,
|
|
880
|
-
commentTag: 'syncular-spaces-deploy',
|
|
881
|
-
metadata: deploymentMetadata,
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
if (dryRun) {
|
|
885
|
-
logStep('Dry run complete (no Cloudflare deployment performed)');
|
|
886
|
-
console.log(`Space: ${spaceId}`);
|
|
887
|
-
console.log(`Worker: ${scriptName}`);
|
|
888
|
-
if (explicitEntry) {
|
|
889
|
-
console.log(`Entry: ${explicitEntry}`);
|
|
890
|
-
} else {
|
|
891
|
-
console.log(`Config: ${configPath}`);
|
|
892
|
-
}
|
|
893
|
-
console.log(`Artifact hash: ${artifactHash}`);
|
|
894
|
-
console.log(`Deploy metadata: ${JSON.stringify(deploymentMetadata)}`);
|
|
895
|
-
return 0;
|
|
896
|
-
}
|
|
897
|
-
logStep('Uploading artifact to control-plane');
|
|
898
|
-
const artifactId = await createDeployArtifactRecord({
|
|
899
|
-
controlPlaneBase,
|
|
900
|
-
actorId,
|
|
901
|
-
controlToken,
|
|
902
|
-
spaceId,
|
|
903
|
-
scriptName,
|
|
904
|
-
artifactHash,
|
|
905
|
-
source,
|
|
906
|
-
sourceLabel,
|
|
907
|
-
manifest: deploymentMetadata,
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
logStep('Creating deploy job in control-plane');
|
|
911
|
-
const job = await createDeployJobRecord({
|
|
912
|
-
controlPlaneBase,
|
|
913
|
-
actorId,
|
|
914
|
-
controlToken,
|
|
915
|
-
spaceId,
|
|
916
|
-
artifactId,
|
|
917
|
-
scriptName,
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
const completedJob = await waitForDeployJob({
|
|
921
|
-
controlPlaneBase,
|
|
922
|
-
actorId,
|
|
923
|
-
controlToken,
|
|
924
|
-
spaceId,
|
|
925
|
-
jobId: job.id!,
|
|
926
|
-
});
|
|
927
|
-
if (completedJob.status !== 'completed') {
|
|
928
|
-
throw new Error(
|
|
929
|
-
`Deploy job ${completedJob.id} failed: ${completedJob.errorText ?? 'unknown error'}`
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
const deploymentId = completedJob.deploymentId ?? null;
|
|
933
|
-
|
|
934
|
-
logStep('Validating health after activation');
|
|
935
|
-
await waitForSuccessfulFetch({
|
|
936
|
-
url: `${runtime!.baseUrl.replace(/\/$/, '')}/api/health`,
|
|
937
|
-
label: 'Post-activation health check',
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
console.log('Runtime deployed successfully.');
|
|
941
|
-
console.log(`Space: ${spaceId}`);
|
|
942
|
-
console.log(`Worker: ${scriptName}`);
|
|
943
|
-
console.log(`Base URL: ${runtime!.baseUrl}`);
|
|
944
|
-
console.log(`Sync URL: ${runtime!.syncUrl}`);
|
|
945
|
-
console.log(`Artifact hash: ${artifactHash}`);
|
|
946
|
-
if (deploymentId) {
|
|
947
|
-
console.log(`Deployment record: ${deploymentId}`);
|
|
948
|
-
}
|
|
949
|
-
console.log(`Deploy metadata: ${JSON.stringify(deploymentMetadata)}`);
|
|
950
|
-
return 0;
|
|
951
|
-
} catch (error: unknown) {
|
|
952
|
-
printError(
|
|
953
|
-
error instanceof Error ? error.message : 'Runtime deploy failed.'
|
|
954
|
-
);
|
|
955
|
-
return 1;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
export async function runDeploy(
|
|
960
|
-
flagValues: Map<string, string>
|
|
961
|
-
): Promise<number> {
|
|
962
|
-
try {
|
|
963
|
-
await resolveEffectiveTargetId({
|
|
964
|
-
cwd: process.cwd(),
|
|
965
|
-
explicitTargetId: flagValues.get('--target')?.trim() || null,
|
|
966
|
-
});
|
|
967
|
-
return runDeployImmutable(flagValues);
|
|
968
|
-
} catch (error: unknown) {
|
|
969
|
-
printError(
|
|
970
|
-
error instanceof Error ? error.message : 'Runtime deploy failed.'
|
|
971
|
-
);
|
|
972
|
-
return 1;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
async function runDeploymentsList(
|
|
977
|
-
flagValues: Map<string, string>
|
|
978
|
-
): Promise<number> {
|
|
979
|
-
try {
|
|
980
|
-
const spaceId = requiredFlag(flagValues, '--space');
|
|
981
|
-
const controlPlaneBase = resolveControlPlaneBase(flagValues);
|
|
982
|
-
const actorId = optionalFlag(
|
|
983
|
-
flagValues,
|
|
984
|
-
'--actor-id',
|
|
985
|
-
process.env.SPACES_TEST_ACTOR_ID?.trim() || CLI_SOURCE_LABEL
|
|
986
|
-
);
|
|
987
|
-
const controlToken = await resolveControlTokenForCommand({
|
|
988
|
-
flagValues,
|
|
989
|
-
controlPlaneBase,
|
|
990
|
-
});
|
|
991
|
-
const limit = optionalIntegerFlag(flagValues, '--limit', 20);
|
|
992
|
-
|
|
993
|
-
const payload = await listDeploymentRecords({
|
|
994
|
-
controlPlaneBase,
|
|
995
|
-
actorId,
|
|
996
|
-
controlToken,
|
|
997
|
-
spaceId,
|
|
998
|
-
limit,
|
|
999
|
-
});
|
|
1000
|
-
const deployments = payload.deployments ?? [];
|
|
1001
|
-
|
|
1002
|
-
if (deployments.length === 0) {
|
|
1003
|
-
console.log(`No deployments found for space ${spaceId}.`);
|
|
1004
|
-
return 0;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
console.log(`Deployments for ${spaceId}:`);
|
|
1008
|
-
for (const deployment of deployments) {
|
|
1009
|
-
const id = deployment.id ?? '<unknown>';
|
|
1010
|
-
const status = deployment.status ?? 'unknown';
|
|
1011
|
-
const scriptName = deployment.scriptName ?? '<unknown-script>';
|
|
1012
|
-
const source = deployment.source ?? 'unknown';
|
|
1013
|
-
const createdAt = deployment.createdAt ?? 'unknown-time';
|
|
1014
|
-
const artifactHash = deployment.artifactHash ?? '-';
|
|
1015
|
-
const rollbackOf = deployment.rollbackOfDeploymentId ?? '-';
|
|
1016
|
-
console.log(
|
|
1017
|
-
`${id} | ${status} | ${scriptName} | source=${source} | created=${createdAt} | hash=${artifactHash} | rollbackOf=${rollbackOf}`
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
return 0;
|
|
1021
|
-
} catch (error: unknown) {
|
|
1022
|
-
printError(
|
|
1023
|
-
error instanceof Error
|
|
1024
|
-
? error.message
|
|
1025
|
-
: 'Failed to list deployments unexpectedly.'
|
|
1026
|
-
);
|
|
1027
|
-
return 1;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
export async function runDeployments(
|
|
1032
|
-
flagValues: Map<string, string>
|
|
1033
|
-
): Promise<number> {
|
|
1034
|
-
return runDeploymentsList(flagValues);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
export async function runRollback(
|
|
1038
|
-
flagValues: Map<string, string>
|
|
1039
|
-
): Promise<number> {
|
|
1040
|
-
try {
|
|
1041
|
-
const spaceId = requiredFlag(flagValues, '--space');
|
|
1042
|
-
const deploymentId = requiredFlag(flagValues, '--to');
|
|
1043
|
-
const controlPlaneBase = resolveControlPlaneBase(flagValues);
|
|
1044
|
-
const actorId = optionalFlag(
|
|
1045
|
-
flagValues,
|
|
1046
|
-
'--actor-id',
|
|
1047
|
-
process.env.SPACES_TEST_ACTOR_ID?.trim() || CLI_SOURCE_LABEL
|
|
1048
|
-
);
|
|
1049
|
-
const controlToken = await resolveControlTokenForCommand({
|
|
1050
|
-
flagValues,
|
|
1051
|
-
controlPlaneBase,
|
|
1052
|
-
});
|
|
1053
|
-
const reason = flagValues.get('--reason')?.trim();
|
|
1054
|
-
|
|
1055
|
-
const rollbackDeployment = await rollbackDeploymentRecord({
|
|
1056
|
-
controlPlaneBase,
|
|
1057
|
-
actorId,
|
|
1058
|
-
controlToken,
|
|
1059
|
-
spaceId,
|
|
1060
|
-
deploymentId,
|
|
1061
|
-
reason: reason && reason.length > 0 ? reason : undefined,
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
console.log('Rollback completed.');
|
|
1065
|
-
console.log(`Space: ${spaceId}`);
|
|
1066
|
-
console.log(`Target deployment: ${deploymentId}`);
|
|
1067
|
-
if (rollbackDeployment?.id) {
|
|
1068
|
-
console.log(`New deployment: ${rollbackDeployment.id}`);
|
|
1069
|
-
}
|
|
1070
|
-
if (rollbackDeployment?.scriptName) {
|
|
1071
|
-
console.log(`Script: ${rollbackDeployment.scriptName}`);
|
|
1072
|
-
}
|
|
1073
|
-
if (rollbackDeployment?.status) {
|
|
1074
|
-
console.log(`Status: ${rollbackDeployment.status}`);
|
|
1075
|
-
}
|
|
1076
|
-
return 0;
|
|
1077
|
-
} catch (error: unknown) {
|
|
1078
|
-
printError(
|
|
1079
|
-
error instanceof Error ? error.message : 'Rollback failed unexpectedly.'
|
|
1080
|
-
);
|
|
1081
|
-
return 1;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
async function runVerifySpaces(
|
|
1086
|
-
flagValues: Map<string, string>
|
|
1087
|
-
): Promise<number> {
|
|
1088
|
-
try {
|
|
1089
|
-
const spaceId = requiredFlag(flagValues, '--space');
|
|
1090
|
-
const controlPlaneBase = resolveControlPlaneBase(flagValues);
|
|
1091
|
-
const actorId = optionalFlag(
|
|
1092
|
-
flagValues,
|
|
1093
|
-
'--actor-id',
|
|
1094
|
-
process.env.SPACES_TEST_ACTOR_ID?.trim() || CLI_SOURCE_LABEL
|
|
1095
|
-
);
|
|
1096
|
-
const controlToken = await resolveControlTokenForCommand({
|
|
1097
|
-
flagValues,
|
|
1098
|
-
controlPlaneBase,
|
|
1099
|
-
});
|
|
1100
|
-
const runtime = await getSpaceRuntime({
|
|
1101
|
-
controlPlaneBase,
|
|
1102
|
-
actorId,
|
|
1103
|
-
controlToken,
|
|
1104
|
-
spaceId,
|
|
1105
|
-
});
|
|
1106
|
-
const baseUrl = optionalFlag(flagValues, '--base-url', runtime.baseUrl);
|
|
1107
|
-
const corsOrigin = optionalFlag(
|
|
1108
|
-
flagValues,
|
|
1109
|
-
'--origin',
|
|
1110
|
-
DEFAULT_VERIFY_CORS_ORIGIN
|
|
1111
|
-
);
|
|
1112
|
-
|
|
1113
|
-
await waitForSuccessfulFetch({
|
|
1114
|
-
url: `${baseUrl.replace(/\/$/, '')}/api/health`,
|
|
1115
|
-
label: 'Runtime health check',
|
|
1116
|
-
});
|
|
1117
|
-
await verifyRuntimeSyncCorsPreflight({
|
|
1118
|
-
baseUrl,
|
|
1119
|
-
origin: corsOrigin,
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
console.log('Runtime verify passed.');
|
|
1123
|
-
console.log(`Space: ${spaceId}`);
|
|
1124
|
-
console.log(`Runtime URL: ${baseUrl}`);
|
|
1125
|
-
console.log(`Sync URL: ${runtime.syncUrl}`);
|
|
1126
|
-
console.log(`CORS origin check: ${corsOrigin}`);
|
|
1127
|
-
return 0;
|
|
1128
|
-
} catch (error: unknown) {
|
|
1129
|
-
printError(
|
|
1130
|
-
error instanceof Error ? error.message : 'Runtime verify failed.'
|
|
1131
|
-
);
|
|
1132
|
-
return 1;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
export async function runVerify(
|
|
1137
|
-
flagValues: Map<string, string>
|
|
1138
|
-
): Promise<number> {
|
|
1139
|
-
try {
|
|
1140
|
-
await resolveEffectiveTargetId({
|
|
1141
|
-
cwd: process.cwd(),
|
|
1142
|
-
explicitTargetId: flagValues.get('--target')?.trim() || null,
|
|
1143
|
-
});
|
|
1144
|
-
return runVerifySpaces(flagValues);
|
|
1145
|
-
} catch (error: unknown) {
|
|
1146
|
-
printError(
|
|
1147
|
-
error instanceof Error ? error.message : 'Runtime verify failed.'
|
|
1148
|
-
);
|
|
1149
|
-
return 1;
|
|
1150
|
-
}
|
|
1151
|
-
}
|