@watasu/sdk 0.1.6 → 0.1.25

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.
@@ -0,0 +1,624 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ConnectionConfig } from './connectionConfig.js';
4
+ import { InvalidArgumentError, NotFoundError, SandboxError, unsupported } from './errors.js';
5
+ import { ControlClient, withQuery } from './transport.js';
6
+ /** Ready-check command wrapper accepted by template builders. */
7
+ export class ReadyCmd {
8
+ cmd;
9
+ constructor(cmd) {
10
+ this.cmd = cmd;
11
+ }
12
+ /** Return the shell command used as the ready check. */
13
+ getCmd() {
14
+ return this.cmd;
15
+ }
16
+ }
17
+ /** Return a ready check that waits for a TCP port to listen. */
18
+ export function waitForPort(port) {
19
+ return new ReadyCmd(`ss -tuln | grep :${port}`);
20
+ }
21
+ /** Return a ready check that waits for a URL to return an HTTP status code. */
22
+ export function waitForURL(url, statusCode = 200) {
23
+ return new ReadyCmd(`curl -s -o /dev/null -w "%{http_code}" ${url} | grep -q "${statusCode}"`);
24
+ }
25
+ /** Alias for `waitForURL`. */
26
+ export const waitForUrl = waitForURL;
27
+ /** Return a ready check that waits for a process name. */
28
+ export function waitForProcess(processName) {
29
+ return new ReadyCmd(`pgrep ${processName} > /dev/null`);
30
+ }
31
+ /** Return a ready check that waits for a file to exist. */
32
+ export function waitForFile(filename) {
33
+ return new ReadyCmd(`[ -f ${filename} ]`);
34
+ }
35
+ /** Return a ready check that waits for a fixed duration in milliseconds. */
36
+ export function waitForTimeout(timeout) {
37
+ const seconds = Math.max(1, Math.floor(timeout / 1000));
38
+ return new ReadyCmd(`sleep ${seconds}`);
39
+ }
40
+ /** Chainable template builder for Watasu package-spec template builds. */
41
+ export class TemplateBase {
42
+ base;
43
+ packages = {};
44
+ files = [];
45
+ setup = [];
46
+ env = {};
47
+ currentWorkdir;
48
+ currentUser;
49
+ startCmd;
50
+ readyCmd;
51
+ force = false;
52
+ fileContextPath;
53
+ fileIgnorePatterns;
54
+ constructor(options = {}) {
55
+ this.fileContextPath = path.resolve(options.fileContextPath ?? process.cwd());
56
+ this.fileIgnorePatterns = options.fileIgnorePatterns ?? [];
57
+ }
58
+ static async build(template, nameOrOptions, options = {}) {
59
+ const { name, buildOptions } = normalizeBuildArguments(nameOrOptions, options);
60
+ buildOptions.onBuildLogs?.({ timestamp: new Date(), level: 'info', message: 'Build started' });
61
+ const data = await TemplateBase.buildInBackground(template, name, buildOptions);
62
+ await waitForBuildFinish(data, buildOptions);
63
+ buildOptions.onBuildLogs?.({ timestamp: new Date(), level: 'info', message: 'Build finished' });
64
+ return data;
65
+ }
66
+ static async buildInBackground(template, nameOrOptions, options = {}) {
67
+ const { name, buildOptions } = normalizeBuildArguments(nameOrOptions, options);
68
+ const config = new ConnectionConfig(buildOptions);
69
+ const control = new ControlClient(config);
70
+ const payload = {
71
+ name,
72
+ tags: buildOptions.tags,
73
+ cpu_count: buildOptions.cpuCount ?? 2,
74
+ memory_mb: buildOptions.memoryMB ?? 1024,
75
+ skip_cache: buildOptions.skipCache ?? template.force,
76
+ build_spec: template.toBuildSpec(),
77
+ };
78
+ if (buildOptions.team !== undefined)
79
+ payload.team = buildOptions.team;
80
+ const response = await control.post('/templates', { json: payload });
81
+ return buildInfo(record(response.template_build ?? response));
82
+ }
83
+ static async getBuildStatus(data, options = {}) {
84
+ const config = new ConnectionConfig(options);
85
+ const control = new ControlClient(config);
86
+ const payload = await control.get(withQuery(`/templates/${encodeURIComponent(data.templateId)}/builds/${encodeURIComponent(data.buildId)}/status`, {
87
+ logs_offset: options.logsOffset,
88
+ }));
89
+ return templateBuildStatus(record(payload));
90
+ }
91
+ static async exists(name, options) {
92
+ return TemplateBase.aliasExists(name, options);
93
+ }
94
+ static async aliasExists(alias, options = {}) {
95
+ const config = new ConnectionConfig(options);
96
+ const control = new ControlClient(config);
97
+ try {
98
+ await control.get(`/templates/aliases/${encodeURIComponent(alias)}`);
99
+ return true;
100
+ }
101
+ catch (error) {
102
+ if (error instanceof NotFoundError)
103
+ return false;
104
+ throw error;
105
+ }
106
+ }
107
+ static async assignTags(targetName, tags, options = {}) {
108
+ const config = new ConnectionConfig(options);
109
+ const control = new ControlClient(config);
110
+ const response = await control.post('/templates/tags', {
111
+ json: { target: targetName, tags: Array.isArray(tags) ? tags : [tags] },
112
+ });
113
+ return {
114
+ buildId: stringValue(response.build_id) ?? '',
115
+ tags: arrayOfStrings(response.tags),
116
+ };
117
+ }
118
+ static async removeTags(name, tags, options = {}) {
119
+ const config = new ConnectionConfig(options);
120
+ const control = new ControlClient(config);
121
+ await control.delete('/templates/tags', {
122
+ json: { name, tags: Array.isArray(tags) ? tags : [tags] },
123
+ });
124
+ }
125
+ static async getTags(templateId, options = {}) {
126
+ const config = new ConnectionConfig(options);
127
+ const control = new ControlClient(config);
128
+ const response = await control.get(`/templates/${encodeURIComponent(templateId)}/tags`);
129
+ const tags = Array.isArray(response) ? response : [];
130
+ return tags.map((item) => templateTag(record(item)));
131
+ }
132
+ static async toJSON(template) {
133
+ return JSON.stringify(template.toBuildSpec());
134
+ }
135
+ static toDockerfile(template) {
136
+ return template.toDockerfile();
137
+ }
138
+ fromDebianImage(_variant = 'stable') {
139
+ this.base = 'base';
140
+ return this;
141
+ }
142
+ fromUbuntuImage(_variant = 'latest') {
143
+ this.base = 'base';
144
+ return this;
145
+ }
146
+ fromPythonImage(_version = '3') {
147
+ this.base = this.base ?? 'base';
148
+ return this;
149
+ }
150
+ fromNodeImage(_variant = 'lts') {
151
+ this.base = this.base ?? 'base';
152
+ return this;
153
+ }
154
+ fromBunImage(_variant = 'latest') {
155
+ this.base = this.base ?? 'base';
156
+ return this;
157
+ }
158
+ fromBaseImage() {
159
+ this.base = 'base';
160
+ return this;
161
+ }
162
+ fromImage(_baseImage, _credentials) {
163
+ this.base = this.base ?? 'base';
164
+ return this;
165
+ }
166
+ fromAWSRegistry(_image, _credentials) {
167
+ unsupported('Template.fromAWSRegistry');
168
+ }
169
+ fromGCPRegistry(_image, _credentials) {
170
+ unsupported('Template.fromGCPRegistry');
171
+ }
172
+ fromTemplate(template) {
173
+ this.base = template;
174
+ return this;
175
+ }
176
+ fromDockerfile(dockerfileContentOrPath) {
177
+ const candidate = path.isAbsolute(dockerfileContentOrPath)
178
+ ? dockerfileContentOrPath
179
+ : path.resolve(this.fileContextPath, dockerfileContentOrPath);
180
+ const content = fs.existsSync(candidate) && fs.statSync(candidate).isFile()
181
+ ? fs.readFileSync(candidate, 'utf8')
182
+ : dockerfileContentOrPath;
183
+ parseDockerfileIntoTemplate(content, this);
184
+ return this;
185
+ }
186
+ copy(src, dest, options = {}) {
187
+ const sources = Array.isArray(src) ? src : [src];
188
+ for (const source of sources)
189
+ this.addCopySource(source, dest, options, sources.length > 1);
190
+ return this;
191
+ }
192
+ copyItems(items) {
193
+ for (const item of items) {
194
+ this.copy(item.src, item.dest, {
195
+ forceUpload: item.forceUpload,
196
+ user: item.user,
197
+ mode: item.mode,
198
+ resolveSymlinks: item.resolveSymlinks,
199
+ });
200
+ }
201
+ return this;
202
+ }
203
+ remove(path, options = {}) {
204
+ const paths = Array.isArray(path) ? path : [path];
205
+ return this.runCmd(`rm ${options.recursive ? '-r ' : ''}${options.force ? '-f ' : ''}${paths.join(' ')}`, {
206
+ user: options.user,
207
+ });
208
+ }
209
+ rename(src, dest, options = {}) {
210
+ return this.runCmd(`mv ${src} ${dest}${options.force ? ' -f' : ''}`, { user: options.user });
211
+ }
212
+ makeDir(path, options = {}) {
213
+ const paths = Array.isArray(path) ? path : [path];
214
+ const mode = options.mode === undefined ? '' : `-m ${options.mode.toString(8)} `;
215
+ return this.runCmd(`mkdir -p ${mode}${paths.join(' ')}`, { user: options.user });
216
+ }
217
+ makeSymlink(src, dest, options = {}) {
218
+ return this.runCmd(`ln -s ${options.force ? '-f ' : ''}${src} ${dest}`, { user: options.user });
219
+ }
220
+ runCmd(command, options = {}) {
221
+ const commandText = Array.isArray(command) ? command.join(' && ') : command;
222
+ this.setup.push(this.commandWithContext(commandText, options.user));
223
+ return this;
224
+ }
225
+ setWorkdir(workdir) {
226
+ this.currentWorkdir = workdir;
227
+ return this;
228
+ }
229
+ setUser(user) {
230
+ this.currentUser = user;
231
+ return this;
232
+ }
233
+ pipInstall(packages, options = {}) {
234
+ const packageList = packages === undefined ? [] : arrayOfStrings(packages);
235
+ if (packageList.length > 0 && options.g !== false) {
236
+ this.addPackages('pip', packageList);
237
+ }
238
+ else {
239
+ const suffix = packageList.length > 0 ? packageList.join(' ') : '.';
240
+ this.runCmd(`python3 -m pip install ${options.g === false ? '--user ' : ''}${suffix}`);
241
+ }
242
+ return this;
243
+ }
244
+ npmInstall(packages, options = {}) {
245
+ const packageList = packages === undefined ? [] : arrayOfStrings(packages);
246
+ if (packageList.length > 0 && options.g) {
247
+ this.addPackages('npm', packageList);
248
+ }
249
+ else {
250
+ this.runCmd(`npm install ${options.g ? '-g ' : ''}${options.dev ? '--save-dev ' : ''}${packageList.join(' ')}`.trim());
251
+ }
252
+ return this;
253
+ }
254
+ bunInstall(packages, options = {}) {
255
+ const packageList = packages === undefined ? [] : arrayOfStrings(packages);
256
+ this.runCmd(`bun install ${options.g ? '-g ' : ''}${options.dev ? '--dev ' : ''}${packageList.join(' ')}`.trim());
257
+ return this;
258
+ }
259
+ aptInstall(packages, _options = {}) {
260
+ this.addPackages('apt', arrayOfStrings(packages));
261
+ return this;
262
+ }
263
+ addMcpServer(servers) {
264
+ if (this.base !== 'mcp-gateway') {
265
+ throw new SandboxError('MCP servers can only be added to mcp-gateway template');
266
+ }
267
+ return this.runCmd(`mcp-gateway pull ${arrayOfStrings(servers).join(' ')}`, { user: 'root' });
268
+ }
269
+ gitClone(url, path, options = {}) {
270
+ const args = ['git clone'];
271
+ if (options.branch)
272
+ args.push(`--branch ${options.branch}`, '--single-branch');
273
+ if (options.depth)
274
+ args.push(`--depth ${options.depth}`);
275
+ args.push(url);
276
+ if (path)
277
+ args.push(path);
278
+ return this.runCmd(args.join(' '), { user: options.user });
279
+ }
280
+ setStartCmd(startCommand, readyCommand) {
281
+ this.startCmd = startCommand;
282
+ this.readyCmd = readyCommandText(readyCommand);
283
+ return this;
284
+ }
285
+ setReadyCmd(readyCommand) {
286
+ this.readyCmd = readyCommandText(readyCommand);
287
+ return this;
288
+ }
289
+ setEnvs(envs) {
290
+ Object.assign(this.env, envs);
291
+ return this;
292
+ }
293
+ skipCache() {
294
+ this.force = true;
295
+ return this;
296
+ }
297
+ toBuildSpec() {
298
+ const spec = {};
299
+ if (this.base)
300
+ spec.base = this.base;
301
+ if (Object.keys(this.packages).length > 0)
302
+ spec.packages = this.packages;
303
+ if (this.files.length > 0)
304
+ spec.files = this.files;
305
+ if (this.setup.length > 0)
306
+ spec.setup = this.setup;
307
+ if (Object.keys(this.env).length > 0)
308
+ spec.env = this.env;
309
+ if (this.startCmd)
310
+ spec.start_cmd = this.startCmd;
311
+ if (this.readyCmd)
312
+ spec.ready_cmd = this.readyCmd;
313
+ return spec;
314
+ }
315
+ addPackages(manager, packages) {
316
+ this.packages[manager] = [...(this.packages[manager] ?? []), ...packages];
317
+ }
318
+ addCopySource(source, dest, options, multipleSources) {
319
+ const sourcePath = this.resolveContextPath(source);
320
+ const stat = fs.statSync(sourcePath);
321
+ if (stat.isDirectory()) {
322
+ for (const filePath of walkFiles(sourcePath, options.resolveSymlinks ?? true)) {
323
+ const relativePath = toPosixPath(path.relative(sourcePath, filePath));
324
+ if (this.ignored(relativePath) || this.ignored(toPosixPath(path.relative(this.fileContextPath, filePath))))
325
+ continue;
326
+ this.addFileSpec(filePath, posixJoin(dest, relativePath), toPosixPath(path.relative(this.fileContextPath, filePath)), options);
327
+ }
328
+ return;
329
+ }
330
+ if (!stat.isFile()) {
331
+ throw new InvalidArgumentError(`copy source is not a file or directory: ${source}`);
332
+ }
333
+ const destPath = multipleSources || dest.endsWith('/') ? posixJoin(dest, path.basename(source)) : dest;
334
+ this.addFileSpec(sourcePath, destPath, toPosixPath(path.relative(this.fileContextPath, sourcePath)), options);
335
+ }
336
+ addFileSpec(filePath, destPath, sourcePath, options) {
337
+ const file = {
338
+ path: normalizeSandboxPath(destPath),
339
+ source_path: sourcePath,
340
+ content_b64: fs.readFileSync(filePath).toString('base64'),
341
+ };
342
+ if (options.mode !== undefined)
343
+ file.mode = options.mode;
344
+ if (options.user !== undefined)
345
+ file.user = options.user;
346
+ this.files.push(file);
347
+ }
348
+ resolveContextPath(source) {
349
+ if (path.isAbsolute(source)) {
350
+ throw new InvalidArgumentError('copy source must be relative to the template file context');
351
+ }
352
+ const resolved = path.resolve(this.fileContextPath, source);
353
+ const relative = path.relative(this.fileContextPath, resolved);
354
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
355
+ throw new InvalidArgumentError('copy source must stay inside the template file context');
356
+ }
357
+ return resolved;
358
+ }
359
+ ignored(relativePath) {
360
+ return this.fileIgnorePatterns.some((pattern) => matchesIgnorePattern(relativePath, pattern));
361
+ }
362
+ commandWithContext(command, user) {
363
+ const workdir = this.currentWorkdir ? `cd ${shellQuote(this.currentWorkdir)} && ` : '';
364
+ const commandText = `${workdir}${command}`;
365
+ const commandUser = user ?? this.currentUser;
366
+ return commandUser && commandUser !== 'root'
367
+ ? `su -s /bin/bash -c ${shellQuote(commandText)} ${shellQuote(commandUser)}`
368
+ : commandText;
369
+ }
370
+ toDockerfile() {
371
+ const lines = ['FROM base'];
372
+ for (const packageName of this.packages.apt ?? [])
373
+ lines.push(`RUN apt-get update && apt-get install -y ${packageName}`);
374
+ for (const packageName of this.packages.pip ?? [])
375
+ lines.push(`RUN python3 -m pip install ${packageName}`);
376
+ for (const packageName of this.packages.npm ?? [])
377
+ lines.push(`RUN npm install -g ${packageName}`);
378
+ for (const file of this.files)
379
+ lines.push(`COPY ${file.source_path ?? file.path} ${file.path}`);
380
+ for (const command of this.setup)
381
+ lines.push(`RUN ${command}`);
382
+ return `${lines.join('\n')}\n`;
383
+ }
384
+ }
385
+ function readyCommandText(command) {
386
+ return command instanceof ReadyCmd ? command.getCmd() : command;
387
+ }
388
+ function walkFiles(root, resolveSymlinks) {
389
+ const entries = fs.readdirSync(root, { withFileTypes: true });
390
+ const files = [];
391
+ for (const entry of entries) {
392
+ const entryPath = path.join(root, entry.name);
393
+ const stat = resolveSymlinks ? fs.statSync(entryPath) : fs.lstatSync(entryPath);
394
+ if (stat.isDirectory())
395
+ files.push(...walkFiles(entryPath, resolveSymlinks));
396
+ if (stat.isFile())
397
+ files.push(entryPath);
398
+ }
399
+ return files.sort();
400
+ }
401
+ function normalizeSandboxPath(value) {
402
+ return toPosixPath(value).replace(/\/+/g, '/');
403
+ }
404
+ function posixJoin(base, relativePath) {
405
+ const normalizedBase = normalizeSandboxPath(base);
406
+ return normalizeSandboxPath(path.posix.join(normalizedBase, relativePath));
407
+ }
408
+ function toPosixPath(value) {
409
+ return value.split(path.sep).join('/');
410
+ }
411
+ function matchesIgnorePattern(relativePath, pattern) {
412
+ if (!pattern)
413
+ return false;
414
+ const normalizedPattern = toPosixPath(pattern);
415
+ if (normalizedPattern.endsWith('/'))
416
+ return relativePath.startsWith(normalizedPattern);
417
+ if (!normalizedPattern.includes('*'))
418
+ return relativePath === normalizedPattern || relativePath.startsWith(`${normalizedPattern}/`);
419
+ const escaped = normalizedPattern.split('*').map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*');
420
+ return new RegExp(`^${escaped}$`).test(relativePath);
421
+ }
422
+ function parseDockerfileIntoTemplate(dockerfileContentOrPath, template) {
423
+ for (const instruction of dockerfileInstructions(dockerfileContentOrPath)) {
424
+ const keyword = instruction.keyword.toUpperCase();
425
+ const value = instruction.value;
426
+ if (keyword === 'FROM') {
427
+ template.fromImage(value.split(/\s+/)[0] || 'base');
428
+ }
429
+ else if (keyword === 'RUN') {
430
+ template.runCmd(value);
431
+ }
432
+ else if (keyword === 'WORKDIR') {
433
+ template.setWorkdir(value);
434
+ }
435
+ else if (keyword === 'USER') {
436
+ template.setUser(value);
437
+ }
438
+ else if (keyword === 'ENV') {
439
+ template.setEnvs(parseEnvInstruction(value));
440
+ }
441
+ else if (keyword === 'COPY' || keyword === 'ADD') {
442
+ const args = shellWords(value).filter((word) => !word.startsWith('--'));
443
+ if (args.length < 2)
444
+ throw new InvalidArgumentError(`${keyword} requires source and destination`);
445
+ const dest = args[args.length - 1];
446
+ template.copy(args.slice(0, -1), dest);
447
+ }
448
+ else if (keyword === 'CMD' || keyword === 'ENTRYPOINT') {
449
+ template.setStartCmd(value, waitForTimeout(20_000));
450
+ }
451
+ }
452
+ }
453
+ function dockerfileInstructions(content) {
454
+ const logicalLines = [];
455
+ let current = '';
456
+ for (const rawLine of content.split(/\r?\n/)) {
457
+ const line = rawLine.trim();
458
+ if (!line || line.startsWith('#'))
459
+ continue;
460
+ if (line.endsWith('\\')) {
461
+ current += `${line.slice(0, -1)} `;
462
+ continue;
463
+ }
464
+ logicalLines.push(`${current}${line}`);
465
+ current = '';
466
+ }
467
+ if (current.trim())
468
+ logicalLines.push(current.trim());
469
+ return logicalLines.flatMap((line) => {
470
+ const match = line.match(/^([A-Za-z]+)\s+(.*)$/);
471
+ return match ? [{ keyword: match[1], value: match[2].trim() }] : [];
472
+ });
473
+ }
474
+ function parseEnvInstruction(value) {
475
+ const words = shellWords(value);
476
+ if (words.length === 2 && !words[0].includes('='))
477
+ return { [words[0]]: words[1] };
478
+ const env = {};
479
+ for (const word of words) {
480
+ const index = word.indexOf('=');
481
+ if (index > 0)
482
+ env[word.slice(0, index)] = word.slice(index + 1);
483
+ }
484
+ return env;
485
+ }
486
+ function shellWords(value) {
487
+ const words = [];
488
+ let word = '';
489
+ let quote;
490
+ let escaping = false;
491
+ for (const char of value) {
492
+ if (escaping) {
493
+ word += char;
494
+ escaping = false;
495
+ }
496
+ else if (char === '\\') {
497
+ escaping = true;
498
+ }
499
+ else if (quote) {
500
+ if (char === quote)
501
+ quote = undefined;
502
+ else
503
+ word += char;
504
+ }
505
+ else if (char === '"' || char === "'") {
506
+ quote = char;
507
+ }
508
+ else if (/\s/.test(char)) {
509
+ if (word) {
510
+ words.push(word);
511
+ word = '';
512
+ }
513
+ }
514
+ else {
515
+ word += char;
516
+ }
517
+ }
518
+ if (word)
519
+ words.push(word);
520
+ return words;
521
+ }
522
+ export const Template = Object.assign((options) => new TemplateBase(options), {
523
+ build: TemplateBase.build,
524
+ buildInBackground: TemplateBase.buildInBackground,
525
+ getBuildStatus: TemplateBase.getBuildStatus,
526
+ exists: TemplateBase.exists,
527
+ aliasExists: TemplateBase.aliasExists,
528
+ assignTags: TemplateBase.assignTags,
529
+ removeTags: TemplateBase.removeTags,
530
+ getTags: TemplateBase.getTags,
531
+ toJSON: TemplateBase.toJSON,
532
+ toDockerfile: TemplateBase.toDockerfile,
533
+ });
534
+ async function waitForBuildFinish(data, options) {
535
+ let logsOffset = 0;
536
+ let status = 'building';
537
+ while (status === 'building' || status === 'waiting') {
538
+ const buildStatus = await TemplateBase.getBuildStatus(data, { ...options, logsOffset });
539
+ logsOffset += buildStatus.logEntries.length;
540
+ buildStatus.logEntries.forEach((entry) => options.onBuildLogs?.(entry));
541
+ status = buildStatus.status;
542
+ if (status === 'ready')
543
+ return;
544
+ if (status === 'error')
545
+ throw new SandboxError(buildStatus.reason?.message ?? 'Template build failed');
546
+ await new Promise((resolve) => setTimeout(resolve, 200));
547
+ }
548
+ }
549
+ function normalizeBuildArguments(nameOrOptions, options) {
550
+ if (typeof nameOrOptions === 'string')
551
+ return { name: nameOrOptions, buildOptions: options };
552
+ if (!nameOrOptions.alias)
553
+ throw new InvalidArgumentError('name is required');
554
+ return { name: nameOrOptions.alias, buildOptions: nameOrOptions };
555
+ }
556
+ function buildInfo(payload) {
557
+ const templateId = stringValue(payload.template_id ?? payload.templateId);
558
+ const buildId = stringValue(payload.build_id ?? payload.buildId);
559
+ if (!templateId || !buildId)
560
+ throw new SandboxError('template build response did not include identifiers');
561
+ return {
562
+ alias: stringValue(payload.alias) ?? stringValue(payload.name) ?? '',
563
+ name: stringValue(payload.name) ?? stringValue(payload.alias) ?? '',
564
+ tags: arrayOfStrings(payload.tags),
565
+ templateId,
566
+ buildId,
567
+ };
568
+ }
569
+ function templateBuildStatus(payload) {
570
+ return {
571
+ buildID: stringValue(payload.build_id ?? payload.buildID) ?? '',
572
+ templateID: stringValue(payload.template_id ?? payload.templateID) ?? '',
573
+ status: (stringValue(payload.status) ?? 'building'),
574
+ logEntries: Array.isArray(payload.log_entries)
575
+ ? payload.log_entries.map((item) => logEntry(record(item)))
576
+ : [],
577
+ logs: arrayOfStrings(payload.logs),
578
+ reason: payload.reason ? buildStatusReason(record(payload.reason)) : undefined,
579
+ };
580
+ }
581
+ function buildStatusReason(payload) {
582
+ return {
583
+ message: stringValue(payload.message) ?? 'Template build failed',
584
+ step: stringValue(payload.step),
585
+ logEntries: Array.isArray(payload.log_entries)
586
+ ? payload.log_entries.map((item) => logEntry(record(item)))
587
+ : [],
588
+ };
589
+ }
590
+ function logEntry(payload) {
591
+ const timestamp = stringValue(payload.timestamp);
592
+ return {
593
+ timestamp: timestamp ? new Date(timestamp) : undefined,
594
+ level: stringValue(payload.level) ?? 'info',
595
+ message: stringValue(payload.message) ?? '',
596
+ };
597
+ }
598
+ function templateTag(payload) {
599
+ return {
600
+ tag: stringValue(payload.tag) ?? '',
601
+ buildId: stringValue(payload.build_id ?? payload.buildId) ?? '',
602
+ createdAt: new Date(stringValue(payload.created_at ?? payload.createdAt) ?? 0),
603
+ };
604
+ }
605
+ function record(value) {
606
+ return value && typeof value === 'object' ? value : {};
607
+ }
608
+ function stringValue(value) {
609
+ if (typeof value === 'string' && value.length > 0)
610
+ return value;
611
+ if (typeof value === 'number')
612
+ return String(value);
613
+ return undefined;
614
+ }
615
+ function arrayOfStrings(value) {
616
+ if (Array.isArray(value))
617
+ return value.map(String);
618
+ if (typeof value === 'string')
619
+ return [value];
620
+ return [];
621
+ }
622
+ function shellQuote(value) {
623
+ return `'${value.replace(/'/g, `'\\''`)}'`;
624
+ }
@@ -0,0 +1,41 @@
1
+ import { CommandHandle } from './commands.js';
2
+ import { Pty, PtySize } from './pty.js';
3
+ /** Captured terminal output. */
4
+ export declare class TerminalOutput {
5
+ private _data;
6
+ get data(): string;
7
+ addData(data: string): void;
8
+ }
9
+ export type TerminalOpts = {
10
+ onData?: (data: string) => Promise<void> | void;
11
+ onExit?: () => Promise<void> | void;
12
+ size?: PtySize;
13
+ terminalID?: string;
14
+ cmd?: string;
15
+ cwd?: string;
16
+ rootDir?: string;
17
+ envVars?: Record<string, string>;
18
+ timeout?: number;
19
+ };
20
+ /** A running terminal session in a sandbox. */
21
+ export declare class Terminal {
22
+ readonly terminalID: string;
23
+ private readonly handle;
24
+ readonly output: TerminalOutput;
25
+ private readonly onExit?;
26
+ readonly finished: Promise<TerminalOutput>;
27
+ private waitPromise?;
28
+ constructor(terminalID: string, handle: CommandHandle, output: TerminalOutput, onExit?: (() => Promise<void> | void) | undefined);
29
+ get data(): string;
30
+ kill(): Promise<void>;
31
+ wait(): Promise<TerminalOutput>;
32
+ private waitOnce;
33
+ sendData(data: string): Promise<void>;
34
+ resize({ cols, rows }: PtySize): Promise<void>;
35
+ }
36
+ /** Manager for starting terminal sessions. */
37
+ export declare class TerminalManager {
38
+ private readonly pty;
39
+ constructor(pty: Pty);
40
+ start(opts?: TerminalOpts): Promise<Terminal>;
41
+ }