@deepraxis/sdk 0.1.1-dev.f0ef747 → 0.1.1-dev.fcdebee
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/METHODS.md +354 -1
- package/README.md +1 -1
- package/dist/automation.d.ts +61 -0
- package/dist/core/client.d.ts +7 -1
- package/dist/core/client.js +3 -0
- package/dist/core/transport.d.ts +9 -0
- package/dist/core/transport.js +13 -2
- package/dist/generated/index.d.ts +2 -2
- package/dist/generated/index.js +1 -1
- package/dist/generated/sdk.gen.d.ts +862 -55
- package/dist/generated/sdk.gen.js +2778 -513
- package/dist/generated/types.gen.d.ts +41252 -28117
- package/dist/helpers/application-build.d.ts +10 -0
- package/dist/helpers/application-build.js +98 -0
- package/dist/helpers/application-publish.d.ts +8 -0
- package/dist/helpers/application-publish.js +26 -9
- package/dist/helpers/browser-decisions.d.ts +2 -0
- package/dist/helpers/browser-decisions.js +14 -1
- package/dist/helpers/file-upload.d.ts +2 -2
- package/dist/helpers/sandbox-templates.d.ts +121 -0
- package/dist/helpers/sandbox-templates.js +60 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/operations.generated.d.ts +3559 -170
- package/dist/operations.generated.js +4576 -338
- package/dist/resources.generated.d.ts +490 -40
- package/dist/resources.generated.js +215 -1
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PublishInput } from './application-publish.js';
|
|
2
|
+
/** A vite/vinext build read into memory: paths are relative to dist/server and dist/client. */
|
|
3
|
+
export interface BuildOutput {
|
|
4
|
+
serverFiles: Record<string, Uint8Array>;
|
|
5
|
+
clientFiles: Record<string, Uint8Array>;
|
|
6
|
+
/** Parsed dist/server/wrangler.json. */
|
|
7
|
+
wranglerJson: unknown;
|
|
8
|
+
}
|
|
9
|
+
/** Pure mapping from a vite/vinext build (dist/server + dist/client + wrangler.json) to `praxis.application.publish` input. No fs, no network. */
|
|
10
|
+
export declare function publishInputFromBuild(build: BuildOutput, name: string): PublishInput;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { PraxisError } from "../core/errors.js";
|
|
2
|
+
// wrangler.json keys that declare a binding the platform does not accept (it answers UNSUPPORTED).
|
|
3
|
+
const UNSUPPORTED_BINDING_KEYS = ['services', 'queues', 'durable_objects', 'ai', 'analytics_engine_datasets', 'vectorize', 'hyperdrive',
|
|
4
|
+
'browser', 'send_email', 'mtls_certificates', 'dispatch_namespaces', 'unsafe', 'version_metadata', 'images', 'workflows', 'pipelines',
|
|
5
|
+
'secrets_store_secrets', 'ratelimits', 'vpc_services', 'media', 'tail_consumers', 'worker_loaders'];
|
|
6
|
+
// A key declares something only if it holds a value somewhere inside: vite's generated wrangler.json writes
|
|
7
|
+
// empty containers such as `queues: { producers: [], consumers: [] }` and `durable_objects: { bindings: [] }`.
|
|
8
|
+
const isPresent = (value) => {
|
|
9
|
+
if (value === undefined || value === null)
|
|
10
|
+
return false;
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.some(isPresent);
|
|
13
|
+
if (typeof value === 'object')
|
|
14
|
+
return Object.values(value).some(isPresent);
|
|
15
|
+
return true;
|
|
16
|
+
};
|
|
17
|
+
/** Compile one .assetsignore line (gitignore subset: names, `*`, `**`, leading /, trailing /) to a matcher over a relative path. */
|
|
18
|
+
function ignoreMatcher(line) {
|
|
19
|
+
let pattern = line.trim();
|
|
20
|
+
if (!pattern || pattern.startsWith('#') || pattern.startsWith('!'))
|
|
21
|
+
return null;
|
|
22
|
+
const anchored = pattern.startsWith('/') || pattern.slice(0, -1).includes('/');
|
|
23
|
+
pattern = pattern.replace(/^\//, '').replace(/\/$/, '');
|
|
24
|
+
const source = pattern.split('**').map(part => part.split('*').map(piece => piece.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*')).join('.*');
|
|
25
|
+
const regex = new RegExp(`^${source}$`);
|
|
26
|
+
return path => {
|
|
27
|
+
const segments = path.split('/');
|
|
28
|
+
// The path or any of its parent directories matches, as in gitignore.
|
|
29
|
+
for (let end = 1; end <= segments.length; end++) {
|
|
30
|
+
if (anchored ? regex.test(segments.slice(0, end).join('/')) : regex.test(segments[end - 1]))
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function bindingsFrom(wrangler) {
|
|
37
|
+
const bindings = [];
|
|
38
|
+
const fromList = (key, type) => {
|
|
39
|
+
const list = wrangler[key];
|
|
40
|
+
if (!isPresent(list))
|
|
41
|
+
return;
|
|
42
|
+
if (!Array.isArray(list))
|
|
43
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: `wrangler.json ${key} must be an array` });
|
|
44
|
+
for (const entry of list) {
|
|
45
|
+
if (typeof entry?.binding !== 'string')
|
|
46
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: `wrangler.json ${key} entry has no binding name` });
|
|
47
|
+
bindings.push({ type, name: entry.binding });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
fromList('d1_databases', 'd1');
|
|
51
|
+
fromList('kv_namespaces', 'kv_namespace');
|
|
52
|
+
fromList('r2_buckets', 'r2_bucket');
|
|
53
|
+
for (const [name, value] of Object.entries((wrangler.vars ?? {}))) {
|
|
54
|
+
bindings.push((typeof value === 'string' ? { type: 'plain_text', name, text: value } : { type: 'json', name, json: value }));
|
|
55
|
+
}
|
|
56
|
+
if (typeof wrangler.assets?.binding === 'string')
|
|
57
|
+
bindings.push({ type: 'assets', name: wrangler.assets.binding });
|
|
58
|
+
return bindings;
|
|
59
|
+
}
|
|
60
|
+
/** Pure mapping from a vite/vinext build (dist/server + dist/client + wrangler.json) to `praxis.application.publish` input. No fs, no network. */
|
|
61
|
+
export function publishInputFromBuild(build, name) {
|
|
62
|
+
const wrangler = build.wranglerJson;
|
|
63
|
+
if (!wrangler || typeof wrangler !== 'object' || Array.isArray(wrangler)) {
|
|
64
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: 'wranglerJson must be the parsed dist/server/wrangler.json object' });
|
|
65
|
+
}
|
|
66
|
+
const offending = UNSUPPORTED_BINDING_KEYS.filter(key => isPresent(wrangler[key]));
|
|
67
|
+
if (offending.length) {
|
|
68
|
+
throw new PraxisError({ code: 'UNSUPPORTED', message: `wrangler.json declares bindings the platform does not support: ${offending.join(', ')}` });
|
|
69
|
+
}
|
|
70
|
+
const modules = Object.entries(build.serverFiles)
|
|
71
|
+
.filter(([path]) => path.endsWith('.js'))
|
|
72
|
+
.map(([path, content]) => ({ name: path, content, content_type: 'application/javascript+module' }));
|
|
73
|
+
if (typeof wrangler.main !== 'string')
|
|
74
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: 'wrangler.json has no main entry' });
|
|
75
|
+
if (!modules.some(module => module.name === wrangler.main)) {
|
|
76
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: `wrangler.json main ${wrangler.main} is not a .js file under dist/server` });
|
|
77
|
+
}
|
|
78
|
+
const ignored = new TextDecoder().decode(build.clientFiles['.assetsignore'] ?? new Uint8Array()).split(/\r?\n/)
|
|
79
|
+
.map(ignoreMatcher).filter((matcher) => matcher !== null);
|
|
80
|
+
const files = {};
|
|
81
|
+
for (const [path, content] of Object.entries(build.clientFiles)) {
|
|
82
|
+
if (path === '.assetsignore' || ignored.some(matches => matches(path)))
|
|
83
|
+
continue;
|
|
84
|
+
files[`/${path}`] = content;
|
|
85
|
+
}
|
|
86
|
+
const { binding: _binding, directory: _directory, ...assetsConfig } = (wrangler.assets ?? {});
|
|
87
|
+
const bindings = bindingsFrom(wrangler);
|
|
88
|
+
return {
|
|
89
|
+
name,
|
|
90
|
+
files,
|
|
91
|
+
modules,
|
|
92
|
+
main_module: wrangler.main,
|
|
93
|
+
...(typeof wrangler.compatibility_date === 'string' ? { compatibility_date: wrangler.compatibility_date } : {}),
|
|
94
|
+
...(Array.isArray(wrangler.compatibility_flags) ? { compatibility_flags: wrangler.compatibility_flags } : {}),
|
|
95
|
+
...(bindings.length ? { bindings } : {}),
|
|
96
|
+
...(Object.keys(assetsConfig).length ? { assets_config: assetsConfig } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -12,6 +12,14 @@ export interface PublishInput {
|
|
|
12
12
|
content: string;
|
|
13
13
|
content_type?: string;
|
|
14
14
|
};
|
|
15
|
+
/** Multi-module Worker (e.g. a vite/vinext build). Names pass through unchanged; relative paths with / are legal. Mutually exclusive with main. */
|
|
16
|
+
modules?: {
|
|
17
|
+
name: string;
|
|
18
|
+
content: string | Uint8Array;
|
|
19
|
+
content_type?: string;
|
|
20
|
+
}[];
|
|
21
|
+
/** Entry module name; defaults to the first module and must name one of modules. */
|
|
22
|
+
main_module?: string;
|
|
15
23
|
/** 运行时兼容日期,默认当天 UTC(YYYY-MM-DD)。 */
|
|
16
24
|
compatibility_date?: string;
|
|
17
25
|
/** 运行时兼容标志,原样传递;平台固定附加 global_fetch_strictly_public。 */
|
|
@@ -21,11 +21,30 @@ export async function publish(client, input) {
|
|
|
21
21
|
const timeout = input.timeout_ms ?? 120_000;
|
|
22
22
|
if (!Number.isFinite(timeout) || timeout < 0)
|
|
23
23
|
throw new RangeError('timeout_ms must be non-negative and finite');
|
|
24
|
-
const
|
|
25
|
-
'text/plain', 'application/wasm', 'application/octet-stream', 'application/x-python']
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const moduleTypes = ['application/javascript+module', 'text/javascript', 'application/javascript', 'application/json',
|
|
25
|
+
'text/plain', 'application/wasm', 'application/octet-stream', 'application/x-python'];
|
|
26
|
+
const moduleType = (type) => {
|
|
27
|
+
const found = moduleTypes.find(candidate => candidate === (type ?? 'application/javascript+module'));
|
|
28
|
+
if (!found)
|
|
29
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: `Unsupported module content_type: ${type}` });
|
|
30
|
+
return found;
|
|
31
|
+
};
|
|
32
|
+
if (input.main && input.modules)
|
|
33
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: 'Pass either main or modules, not both' });
|
|
34
|
+
if (input.main_module !== undefined && !input.modules)
|
|
35
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: 'main_module requires modules; with main the entry is main.name' });
|
|
36
|
+
if (input.modules && input.modules.length === 0)
|
|
37
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: 'modules must not be empty' });
|
|
38
|
+
const entryModules = input.main
|
|
39
|
+
? [{ name: input.main.name, content_type: moduleType(input.main.content_type), content_base64: base64(new TextEncoder().encode(input.main.content)) }]
|
|
40
|
+
: input.modules?.map(module => ({ name: module.name, content_type: moduleType(module.content_type),
|
|
41
|
+
content_base64: base64(typeof module.content === 'string' ? new TextEncoder().encode(module.content) : module.content) }));
|
|
42
|
+
if (new Set(entryModules?.map(module => module.name)).size !== (entryModules?.length ?? 0))
|
|
43
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: 'Duplicate module name' });
|
|
44
|
+
const mainModule = input.main ? input.main.name : input.modules ? (input.main_module ?? input.modules[0].name) : undefined;
|
|
45
|
+
if (input.modules && !input.modules.some(module => module.name === mainModule)) {
|
|
46
|
+
throw new PraxisError({ code: 'INVALID_PARAMS', message: `main_module ${mainModule} is not among modules` });
|
|
47
|
+
}
|
|
29
48
|
const files = Object.entries(input.files);
|
|
30
49
|
if (files.length > 100_000)
|
|
31
50
|
throw new PraxisError({ code: 'LIMIT_EXCEEDED', message: 'At most 100000 asset paths can be published' });
|
|
@@ -80,7 +99,7 @@ export async function publish(client, input) {
|
|
|
80
99
|
const removed = Object.keys(previous).filter(key => !Object.hasOwn(manifest, key));
|
|
81
100
|
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(manifest))));
|
|
82
101
|
const manifestHash = Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
83
|
-
const staticOnly = !['main', 'bindings', 'compatibility_date', 'compatibility_flags', 'assets_config'].some(key => Object.hasOwn(input, key));
|
|
102
|
+
const staticOnly = !['main', 'modules', 'main_module', 'bindings', 'compatibility_date', 'compatibility_flags', 'assets_config'].some(key => Object.hasOwn(input, key));
|
|
84
103
|
const unchanged = staticOnly && !!currentVersion && manifestHash === currentVersion.praxis.assets_manifest?.manifest_hash;
|
|
85
104
|
const hashes = new Set(Object.values(manifest).map(entry => entry.hash));
|
|
86
105
|
const currentResult = { url: worker.subdomain.url, worker_id: worker.id, version_id: currentVersion?.id ?? null,
|
|
@@ -127,9 +146,7 @@ export async function publish(client, input) {
|
|
|
127
146
|
assets: versionAssets,
|
|
128
147
|
...(input.compatibility_flags ? { compatibility_flags: input.compatibility_flags } : {}),
|
|
129
148
|
...(input.bindings ? { bindings: input.bindings } : {}),
|
|
130
|
-
...(
|
|
131
|
-
content_type: contentType,
|
|
132
|
-
content_base64: base64(new TextEncoder().encode(input.main.content)) }] } : {}),
|
|
149
|
+
...(entryModules ? { main_module: mainModule, modules: entryModules } : {}),
|
|
133
150
|
} });
|
|
134
151
|
let deployment = result.deployment;
|
|
135
152
|
if (!deployment)
|
|
@@ -40,3 +40,5 @@ export interface ContextLike {
|
|
|
40
40
|
export declare function pickAgentContext<T extends ContextLike>(contexts: readonly T[], agentId: string): T | undefined;
|
|
41
41
|
/** Context names accept ASCII letters, digits and . _ : - only. */
|
|
42
42
|
export declare const agentContextName: (agentId: string) => string;
|
|
43
|
+
/** Start pages are expendable only on Console hosts, never arbitrary sites' /start pages. */
|
|
44
|
+
export declare function isBlankTab(value: string): boolean;
|
|
@@ -48,7 +48,7 @@ export function decideTabReuse(targetUrl, tabs) {
|
|
|
48
48
|
if (best === undefined || rank < best.rank || rank === best.rank && (tab.active || !best.tab.active))
|
|
49
49
|
best = { tab, rank };
|
|
50
50
|
}
|
|
51
|
-
return best?.tab;
|
|
51
|
+
return best?.tab ?? tabs.find(tab => isBlankTab(tab.url));
|
|
52
52
|
}
|
|
53
53
|
/** The `/browser/v1/connect` WebSocket URL. The token travels in a header, never in this URL. */
|
|
54
54
|
export function buildConnectUrl(parts) {
|
|
@@ -69,3 +69,16 @@ export function pickAgentContext(contexts, agentId) {
|
|
|
69
69
|
}
|
|
70
70
|
/** Context names accept ASCII letters, digits and . _ : - only. */
|
|
71
71
|
export const agentContextName = (agentId) => `agent-${agentId}`.replace(/[^A-Za-z0-9._:-]/g, '-').slice(0, 128);
|
|
72
|
+
/** Start pages are expendable only on Console hosts, never arbitrary sites' /start pages. */
|
|
73
|
+
export function isBlankTab(value) {
|
|
74
|
+
if (value === "" || value === "about:blank")
|
|
75
|
+
return true;
|
|
76
|
+
try {
|
|
77
|
+
const url = new URL(value);
|
|
78
|
+
return (url.protocol === "https:" || url.protocol === "http:") && url.pathname === "/start" &&
|
|
79
|
+
(url.hostname.startsWith("console-") || url.hostname === "console.deepraxis.com");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Praxis } from '../core/client.js';
|
|
2
2
|
import type { OwnFileOpenAiFile } from '../generated/types.gen.js';
|
|
3
3
|
export interface UploadFileOptions {
|
|
4
|
-
/** user_data for conversation attachments
|
|
5
|
-
purpose: 'user_data' | '
|
|
4
|
+
/** user_data for conversation attachments and documents; vision for PNG/JPEG/GIF/WebP. */
|
|
5
|
+
purpose: 'user_data' | 'vision';
|
|
6
6
|
/** Defaults to File.name, or upload for a Blob. */
|
|
7
7
|
filename?: string;
|
|
8
8
|
/** Fixed part size in bytes, 5 MiB–64 MB; defaults to 50 MiB. */
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export interface TemplateBuildLog {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
4
|
+
message: string;
|
|
5
|
+
step?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TemplateBuildReason {
|
|
8
|
+
message: string;
|
|
9
|
+
step?: string;
|
|
10
|
+
logEntries?: TemplateBuildLog[];
|
|
11
|
+
}
|
|
12
|
+
export interface TemplateBuildStatus {
|
|
13
|
+
templateID: string;
|
|
14
|
+
buildID: string;
|
|
15
|
+
status: 'waiting' | 'building' | 'ready' | 'error';
|
|
16
|
+
logEntries: TemplateBuildLog[];
|
|
17
|
+
logs: string[];
|
|
18
|
+
reason?: TemplateBuildReason;
|
|
19
|
+
}
|
|
20
|
+
export type SandboxTemplateBuildInput = {
|
|
21
|
+
/** Organization-local name. Reusing it reserves another build of the same template. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Optional resource counts; defaults to 2 vCPUs and 1024 MiB. */
|
|
24
|
+
cpuCount?: number;
|
|
25
|
+
memoryMB?: number;
|
|
26
|
+
/** Disk size in GiB, a positive integer; default 10. */
|
|
27
|
+
diskGb?: number;
|
|
28
|
+
/** Labels assigned only after a successful build. */
|
|
29
|
+
tags?: string[];
|
|
30
|
+
/** Structured build steps; COPY is rejected. Mutually exclusive with dockerfile. */
|
|
31
|
+
steps?: {
|
|
32
|
+
type: 'RUN' | 'ENV' | 'WORKDIR' | 'USER';
|
|
33
|
+
args: string[];
|
|
34
|
+
}[];
|
|
35
|
+
/** Called sequentially for newly read log entries. */
|
|
36
|
+
onLog?: (entry: TemplateBuildLog) => void | Promise<void>;
|
|
37
|
+
/** Stops requests and polling; the remote build continues. */
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
/** Nonnegative polling delay in milliseconds; default 1000. */
|
|
40
|
+
pollIntervalMs?: number;
|
|
41
|
+
} & ({
|
|
42
|
+
dockerfile: string;
|
|
43
|
+
fromTemplate?: never;
|
|
44
|
+
fromImage?: never;
|
|
45
|
+
steps?: never;
|
|
46
|
+
} | {
|
|
47
|
+
dockerfile?: never;
|
|
48
|
+
fromTemplate: string;
|
|
49
|
+
fromImage?: never;
|
|
50
|
+
} | {
|
|
51
|
+
dockerfile?: never;
|
|
52
|
+
fromTemplate?: never;
|
|
53
|
+
fromImage: string;
|
|
54
|
+
});
|
|
55
|
+
export interface SandboxTemplateBuildResult extends TemplateBuildStatus {
|
|
56
|
+
status: 'ready';
|
|
57
|
+
}
|
|
58
|
+
export declare class SandboxTemplateBuildError extends Error {
|
|
59
|
+
readonly templateID: string;
|
|
60
|
+
readonly buildID: string;
|
|
61
|
+
readonly reason: TemplateBuildReason | undefined;
|
|
62
|
+
readonly logEntries: readonly TemplateBuildLog[];
|
|
63
|
+
readonly name = "SandboxTemplateBuildError";
|
|
64
|
+
constructor(templateID: string, buildID: string, reason: TemplateBuildReason | undefined, logEntries: readonly TemplateBuildLog[]);
|
|
65
|
+
}
|
|
66
|
+
/** Structural port keeps the helper independent of the client that attaches it. */
|
|
67
|
+
export interface SandboxTemplateBuildClient {
|
|
68
|
+
sandbox: {
|
|
69
|
+
templates: {
|
|
70
|
+
create(input: {
|
|
71
|
+
body: {
|
|
72
|
+
name: string;
|
|
73
|
+
cpuCount?: number;
|
|
74
|
+
memoryMB?: number;
|
|
75
|
+
diskGb?: number;
|
|
76
|
+
tags?: string[];
|
|
77
|
+
};
|
|
78
|
+
signal: AbortSignal;
|
|
79
|
+
}): Promise<{
|
|
80
|
+
templateID: string;
|
|
81
|
+
buildID: string;
|
|
82
|
+
}>;
|
|
83
|
+
builds: {
|
|
84
|
+
update(input: {
|
|
85
|
+
path: {
|
|
86
|
+
templateID: string;
|
|
87
|
+
buildID: string;
|
|
88
|
+
};
|
|
89
|
+
body: {
|
|
90
|
+
dockerfile?: string;
|
|
91
|
+
fromTemplate?: string;
|
|
92
|
+
fromImage?: string;
|
|
93
|
+
steps?: {
|
|
94
|
+
type: string;
|
|
95
|
+
args: string[];
|
|
96
|
+
}[];
|
|
97
|
+
};
|
|
98
|
+
signal: AbortSignal;
|
|
99
|
+
}): Promise<unknown>;
|
|
100
|
+
status: {
|
|
101
|
+
list(input: {
|
|
102
|
+
path: {
|
|
103
|
+
templateID: string;
|
|
104
|
+
buildID: string;
|
|
105
|
+
};
|
|
106
|
+
query: {
|
|
107
|
+
logsOffset: number;
|
|
108
|
+
};
|
|
109
|
+
signal: AbortSignal;
|
|
110
|
+
}): Promise<TemplateBuildStatus>;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Build an organization-local template and wait until it is ready, streaming logs in order.
|
|
117
|
+
* Uses create, source submission and status polling. COPY is unavailable; use RUN curl or git clone.
|
|
118
|
+
* fromTemplate accepts official catalog names only. Abort stops the client; it does not cancel the build.
|
|
119
|
+
* Terminal errors include the provider reason and the final twenty bounded log entries.
|
|
120
|
+
*/
|
|
121
|
+
export declare function buildSandboxTemplate(client: SandboxTemplateBuildClient, input: SandboxTemplateBuildInput): Promise<SandboxTemplateBuildResult>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { sleep } from "../core/retry.js";
|
|
2
|
+
export class SandboxTemplateBuildError extends Error {
|
|
3
|
+
templateID;
|
|
4
|
+
buildID;
|
|
5
|
+
reason;
|
|
6
|
+
logEntries;
|
|
7
|
+
name = 'SandboxTemplateBuildError';
|
|
8
|
+
constructor(templateID, buildID, reason, logEntries) {
|
|
9
|
+
super(`Template build failed: ${reason?.message ?? 'Unknown build error'}${logEntries.length ? `\n${logEntries.map(log => log.message).join('\n')}` : ''}`);
|
|
10
|
+
this.templateID = templateID;
|
|
11
|
+
this.buildID = buildID;
|
|
12
|
+
this.reason = reason;
|
|
13
|
+
this.logEntries = logEntries;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Build an organization-local template and wait until it is ready, streaming logs in order.
|
|
17
|
+
* Uses create, source submission and status polling. COPY is unavailable; use RUN curl or git clone.
|
|
18
|
+
* fromTemplate accepts official catalog names only. Abort stops the client; it does not cancel the build.
|
|
19
|
+
* Terminal errors include the provider reason and the final twenty bounded log entries.
|
|
20
|
+
*/
|
|
21
|
+
export async function buildSandboxTemplate(client, input) {
|
|
22
|
+
const signal = input.signal ?? new AbortController().signal;
|
|
23
|
+
signal.throwIfAborted();
|
|
24
|
+
const interval = input.pollIntervalMs ?? 1000;
|
|
25
|
+
if (!Number.isSafeInteger(interval) || interval < 0)
|
|
26
|
+
throw new RangeError('pollIntervalMs must be a nonnegative integer');
|
|
27
|
+
if ([input.dockerfile, input.fromTemplate, input.fromImage].filter(value => value !== undefined).length !== 1
|
|
28
|
+
|| (input.dockerfile !== undefined && input.steps !== undefined))
|
|
29
|
+
throw new TypeError('Supply dockerfile, fromTemplate or fromImage; dockerfile cannot include steps');
|
|
30
|
+
const created = await client.sandbox.templates.create({ body: {
|
|
31
|
+
name: input.name, cpuCount: input.cpuCount, memoryMB: input.memoryMB, diskGb: input.diskGb, tags: input.tags,
|
|
32
|
+
}, signal });
|
|
33
|
+
const path = { templateID: created.templateID, buildID: created.buildID };
|
|
34
|
+
await client.sandbox.templates.builds.update({ path, body: input.dockerfile !== undefined ? { dockerfile: input.dockerfile }
|
|
35
|
+
: { fromTemplate: input.fromTemplate, fromImage: input.fromImage, steps: input.steps }, signal });
|
|
36
|
+
let logsOffset = 0;
|
|
37
|
+
const tail = [];
|
|
38
|
+
let terminal;
|
|
39
|
+
while (true) {
|
|
40
|
+
signal.throwIfAborted();
|
|
41
|
+
const result = await client.sandbox.templates.builds.status.list({ path, query: { logsOffset }, signal });
|
|
42
|
+
for (const entry of result.logEntries) {
|
|
43
|
+
signal.throwIfAborted();
|
|
44
|
+
await input.onLog?.(entry);
|
|
45
|
+
// Bound retained diagnostics even when one provider log message is enormous.
|
|
46
|
+
tail.push({ ...entry, message: entry.message.slice(-4096) });
|
|
47
|
+
if (tail.length > 20)
|
|
48
|
+
tail.shift();
|
|
49
|
+
}
|
|
50
|
+
logsOffset += result.logEntries.length;
|
|
51
|
+
if (!terminal && (result.status === 'ready' || result.status === 'error'))
|
|
52
|
+
terminal = result;
|
|
53
|
+
if (terminal && result.logEntries.length === 0) {
|
|
54
|
+
if (terminal.status === 'error')
|
|
55
|
+
throw new SandboxTemplateBuildError(path.templateID, path.buildID, terminal.reason, tail);
|
|
56
|
+
return { ...terminal, status: 'ready', logEntries: [] };
|
|
57
|
+
}
|
|
58
|
+
await sleep(interval, signal);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,9 +7,13 @@ export { parseSSE } from './core/sse.js';
|
|
|
7
7
|
export type { SSEEvent, SSEOptions } from './core/sse.js';
|
|
8
8
|
export { publish } from './helpers/application-publish.js';
|
|
9
9
|
export type { PublishInput, PublishResult, AssetsConfig } from './helpers/application-publish.js';
|
|
10
|
+
export { publishInputFromBuild } from './helpers/application-build.js';
|
|
11
|
+
export type { BuildOutput } from './helpers/application-build.js';
|
|
10
12
|
export { count } from './helpers/database-count.js';
|
|
11
13
|
export type { CountInput, CountResult } from './helpers/database-count.js';
|
|
12
14
|
export { uploadFile } from './helpers/file-upload.js';
|
|
13
15
|
export type { UploadFileOptions, UploadedFile } from './helpers/file-upload.js';
|
|
14
16
|
export { connectBrowser } from './helpers/browser-connect.js';
|
|
15
17
|
export type { PraxisBrowser, BrowserTab, BrowserConnectOptions } from './helpers/browser-connect.js';
|
|
18
|
+
export { buildSandboxTemplate, SandboxTemplateBuildError } from './helpers/sandbox-templates.js';
|
|
19
|
+
export type { SandboxTemplateBuildInput, SandboxTemplateBuildResult, TemplateBuildLog, TemplateBuildReason } from './helpers/sandbox-templates.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export { PraxisError, PraxisConnectionError, PraxisTimeoutError } from "./core/e
|
|
|
3
3
|
export { Page } from "./core/pagination.js";
|
|
4
4
|
export { parseSSE } from "./core/sse.js";
|
|
5
5
|
export { publish } from "./helpers/application-publish.js";
|
|
6
|
+
export { publishInputFromBuild } from "./helpers/application-build.js";
|
|
6
7
|
export { count } from "./helpers/database-count.js";
|
|
7
8
|
export { uploadFile } from "./helpers/file-upload.js";
|
|
8
9
|
export { connectBrowser } from "./helpers/browser-connect.js";
|
|
10
|
+
export { buildSandboxTemplate, SandboxTemplateBuildError } from "./helpers/sandbox-templates.js";
|