@sapiom/sandbox-preview 0.1.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @sapiom/sandbox-preview
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 41e9ecd: New package: the client flow for deploying a web-app preview to a Sapiom sandbox and getting a live URL.
8
+
9
+ Reads the project's `sapiom.json` (`type: "sandbox"` resource), provisions the sandbox if needed, ships the code — either a local directory upload or a Sapiom git repository — and calls the server-side deploy op to build, start, and expose a public preview URL. Returns `{ name, url, status, logs }`; a non-`deployed` status carries the build/start log tail so a crash-on-boot is visible to fix and retry.
10
+
11
+ Includes a zod-validated `sapiom.json` schema carrying a config version, so a malformed or hand-edited file fails with an actionable message rather than a confusing downstream error. Exposes `configureSandbox` (validate typed input and write the resource) and `checkSandboxes` (validate a project's sandbox resources without deploying) for building tooling on top.
12
+
13
+ - Updated dependencies [41e9ecd]
14
+ - @sapiom/tools@0.17.2
@@ -0,0 +1,12 @@
1
+ import { type SandboxConfigBody } from './schema.js';
2
+ import type { SandboxConfig } from './types.js';
3
+ export declare const CONFIG_FILE = "sapiom.json";
4
+ export declare function readSandboxes(dir: string): Record<string, SandboxConfig>;
5
+ export declare function getSandbox(dir: string, name?: string): SandboxConfig;
6
+ export declare function configureSandbox(dir: string, name: string, body: SandboxConfigBody): SandboxConfig;
7
+ export declare const writeSandbox: (dir: string, cfg: SandboxConfig) => SandboxConfig;
8
+ export declare function checkSandboxes(dir: string): {
9
+ ok: boolean;
10
+ sandboxes: string[];
11
+ issues: string[];
12
+ };
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.writeSandbox = exports.CONFIG_FILE = void 0;
7
+ exports.readSandboxes = readSandboxes;
8
+ exports.getSandbox = getSandbox;
9
+ exports.configureSandbox = configureSandbox;
10
+ exports.checkSandboxes = checkSandboxes;
11
+ const node_fs_1 = require("node:fs");
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const errors_js_1 = require("./errors.js");
14
+ const schema_js_1 = require("./schema.js");
15
+ exports.CONFIG_FILE = 'sapiom.json';
16
+ function readFile(dir) {
17
+ const file = node_path_1.default.join(dir, exports.CONFIG_FILE);
18
+ if (!(0, node_fs_1.existsSync)(file))
19
+ return null;
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
23
+ }
24
+ catch {
25
+ throw new errors_js_1.PreviewOperationError({ code: 'BAD_CONFIG', message: `${exports.CONFIG_FILE} is not valid JSON.` });
26
+ }
27
+ if (typeof parsed.version === 'number' && parsed.version > schema_js_1.CONFIG_VERSION) {
28
+ throw new errors_js_1.PreviewOperationError({
29
+ code: 'UNSUPPORTED_CONFIG_VERSION',
30
+ message: `${exports.CONFIG_FILE} is version ${parsed.version}, but this tool supports up to ${schema_js_1.CONFIG_VERSION}.`,
31
+ hint: 'Upgrade the Sapiom SDK/CLI.',
32
+ });
33
+ }
34
+ return parsed;
35
+ }
36
+ function parseSandboxEntry(name, entry) {
37
+ const result = schema_js_1.storedSandboxSchema.safeParse(entry);
38
+ if (!result.success) {
39
+ const detail = result.error.issues
40
+ .map((i) => `${['resources', name, ...i.path].join('.')}: ${i.message}`)
41
+ .join('; ');
42
+ throw new errors_js_1.PreviewOperationError({
43
+ code: 'INVALID_SANDBOX',
44
+ message: `Invalid sandbox resource "${name}" in ${exports.CONFIG_FILE}.`,
45
+ hint: detail,
46
+ });
47
+ }
48
+ const { type: _type, ...body } = result.data;
49
+ return { name, ...body };
50
+ }
51
+ function readSandboxes(dir) {
52
+ const cfg = readFile(dir);
53
+ const resources = cfg?.resources ?? {};
54
+ const out = {};
55
+ for (const [name, entry] of Object.entries(resources)) {
56
+ if (entry?.type !== 'sandbox')
57
+ continue;
58
+ out[name] = parseSandboxEntry(name, entry);
59
+ }
60
+ return out;
61
+ }
62
+ function getSandbox(dir, name) {
63
+ const sandboxes = readSandboxes(dir);
64
+ const names = Object.keys(sandboxes);
65
+ if (name) {
66
+ const found = sandboxes[name];
67
+ if (!found) {
68
+ throw new errors_js_1.PreviewOperationError({
69
+ code: 'NO_SANDBOX',
70
+ message: `No sandbox named '${name}' in ${exports.CONFIG_FILE}.`,
71
+ hint: names.length ? `Known sandboxes: ${names.join(', ')}.` : undefined,
72
+ });
73
+ }
74
+ return found;
75
+ }
76
+ if (names.length === 0) {
77
+ throw new errors_js_1.PreviewOperationError({
78
+ code: 'NO_SANDBOX',
79
+ message: `No sandbox resources defined in ${exports.CONFIG_FILE}.`,
80
+ hint: 'Add one with sapiom_dev_sandbox_configure (or a "type":"sandbox" resource).',
81
+ });
82
+ }
83
+ if (names.length > 1) {
84
+ throw new errors_js_1.PreviewOperationError({
85
+ code: 'AMBIGUOUS_SANDBOX',
86
+ message: `Multiple sandboxes defined (${names.join(', ')}); specify which one.`,
87
+ hint: 'Pass a name, e.g. `sapiom sandbox preview <name>`.',
88
+ });
89
+ }
90
+ return sandboxes[names[0]];
91
+ }
92
+ function configureSandbox(dir, name, body) {
93
+ const parsed = schema_js_1.storedSandboxSchema.safeParse({ type: 'sandbox', ...body });
94
+ if (!parsed.success) {
95
+ const detail = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
96
+ throw new errors_js_1.PreviewOperationError({
97
+ code: 'INVALID_SANDBOX',
98
+ message: `Invalid sandbox configuration for "${name}".`,
99
+ hint: detail,
100
+ });
101
+ }
102
+ const existing = readFile(dir) ?? {};
103
+ const next = {
104
+ version: schema_js_1.CONFIG_VERSION,
105
+ ...existing,
106
+ resources: { ...(existing.resources ?? {}), [name]: parsed.data },
107
+ };
108
+ next.version = schema_js_1.CONFIG_VERSION;
109
+ (0, node_fs_1.writeFileSync)(node_path_1.default.join(dir, exports.CONFIG_FILE), JSON.stringify(next, null, 2) + '\n');
110
+ return { name, ...body };
111
+ }
112
+ const writeSandbox = (dir, cfg) => {
113
+ const { name, ...body } = cfg;
114
+ return configureSandbox(dir, name, body);
115
+ };
116
+ exports.writeSandbox = writeSandbox;
117
+ function checkSandboxes(dir) {
118
+ const issues = [];
119
+ const cfg = readFile(dir);
120
+ const resources = cfg?.resources ?? {};
121
+ const found = [];
122
+ for (const [name, entry] of Object.entries(resources)) {
123
+ if (entry?.type !== 'sandbox')
124
+ continue;
125
+ found.push(name);
126
+ const r = schema_js_1.storedSandboxSchema.safeParse(entry);
127
+ if (!r.success) {
128
+ for (const i of r.error.issues)
129
+ issues.push(`resources.${name}.${i.path.join('.')}: ${i.message}`);
130
+ }
131
+ }
132
+ return { ok: issues.length === 0, sandboxes: found, issues };
133
+ }
@@ -0,0 +1,15 @@
1
+ export interface StructuredError {
2
+ code: string;
3
+ message: string;
4
+ step?: string;
5
+ hint?: string;
6
+ docsUrl?: string;
7
+ }
8
+ export declare class PreviewOperationError extends Error {
9
+ readonly code: string;
10
+ readonly step?: string;
11
+ readonly hint?: string;
12
+ readonly docsUrl?: string;
13
+ constructor(err: StructuredError);
14
+ toStructured(): StructuredError;
15
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PreviewOperationError = void 0;
4
+ class PreviewOperationError extends Error {
5
+ constructor(err) {
6
+ super(err.message);
7
+ this.name = 'PreviewOperationError';
8
+ this.code = err.code;
9
+ this.step = err.step;
10
+ this.hint = err.hint;
11
+ this.docsUrl = err.docsUrl;
12
+ }
13
+ toStructured() {
14
+ return {
15
+ code: this.code,
16
+ message: this.message,
17
+ ...(this.step ? { step: this.step } : {}),
18
+ ...(this.hint ? { hint: this.hint } : {}),
19
+ ...(this.docsUrl ? { docsUrl: this.docsUrl } : {}),
20
+ };
21
+ }
22
+ }
23
+ exports.PreviewOperationError = PreviewOperationError;
@@ -0,0 +1 @@
1
+ export declare function pushLocalDir(sourceDir: string, cloneUrl: string, token: string, branch?: string): void;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.pushLocalDir = pushLocalDir;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_fs_1 = require("node:fs");
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const errors_js_1 = require("./errors.js");
12
+ function pushLocalDir(sourceDir, cloneUrl, token, branch = 'main') {
13
+ const gitDir = (0, node_fs_1.mkdtempSync)(node_path_1.default.join((0, node_os_1.tmpdir)(), 'sbxprev-git-'));
14
+ const git = (args) => {
15
+ try {
16
+ (0, node_child_process_1.execFileSync)('git', ['--git-dir', gitDir, '--work-tree', sourceDir, ...args], { stdio: 'pipe' });
17
+ }
18
+ catch (err) {
19
+ const stderr = err.stderr?.toString().trim();
20
+ throw new errors_js_1.PreviewOperationError({
21
+ code: 'GIT_PUSH_FAILED',
22
+ message: `git ${args[0]} failed while pushing to the Sapiom repo.`,
23
+ hint: stderr || (err instanceof Error ? err.message : String(err)),
24
+ });
25
+ }
26
+ };
27
+ try {
28
+ git(['init', '-q', '-b', branch]);
29
+ (0, node_fs_1.mkdirSync)(node_path_1.default.join(gitDir, 'info'), { recursive: true });
30
+ (0, node_fs_1.writeFileSync)(node_path_1.default.join(gitDir, 'info', 'exclude'), 'node_modules/\n.git/\n');
31
+ git(['add', '-A']);
32
+ git(['-c', 'user.email=preview@sapiom.ai', '-c', 'user.name=sapiom-preview', 'commit', '-q', '-m', 'preview']);
33
+ git(['-c', `http.extraHeader=x-sapiom-git-token: ${token}`, 'push', '--force', cloneUrl, `HEAD:${branch}`]);
34
+ }
35
+ finally {
36
+ (0, node_fs_1.rmSync)(gitDir, { recursive: true, force: true });
37
+ }
38
+ }
@@ -0,0 +1,8 @@
1
+ export { previewSandbox } from './preview.js';
2
+ export type { PreviewSandboxOptions } from './preview.js';
3
+ export { getSandbox, readSandboxes, writeSandbox, configureSandbox, checkSandboxes, CONFIG_FILE } from './config.js';
4
+ export { sandboxConfigBodySchema, storedSandboxSchema, CONFIG_VERSION } from './schema.js';
5
+ export type { SandboxConfigBody } from './schema.js';
6
+ export { PreviewOperationError } from './errors.js';
7
+ export type { StructuredError } from './errors.js';
8
+ export type { SandboxConfig, SandboxSourceSpec, PreviewResult } from './types.js';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PreviewOperationError = exports.CONFIG_VERSION = exports.storedSandboxSchema = exports.sandboxConfigBodySchema = exports.CONFIG_FILE = exports.checkSandboxes = exports.configureSandbox = exports.writeSandbox = exports.readSandboxes = exports.getSandbox = exports.previewSandbox = void 0;
4
+ var preview_js_1 = require("./preview.js");
5
+ Object.defineProperty(exports, "previewSandbox", { enumerable: true, get: function () { return preview_js_1.previewSandbox; } });
6
+ var config_js_1 = require("./config.js");
7
+ Object.defineProperty(exports, "getSandbox", { enumerable: true, get: function () { return config_js_1.getSandbox; } });
8
+ Object.defineProperty(exports, "readSandboxes", { enumerable: true, get: function () { return config_js_1.readSandboxes; } });
9
+ Object.defineProperty(exports, "writeSandbox", { enumerable: true, get: function () { return config_js_1.writeSandbox; } });
10
+ Object.defineProperty(exports, "configureSandbox", { enumerable: true, get: function () { return config_js_1.configureSandbox; } });
11
+ Object.defineProperty(exports, "checkSandboxes", { enumerable: true, get: function () { return config_js_1.checkSandboxes; } });
12
+ Object.defineProperty(exports, "CONFIG_FILE", { enumerable: true, get: function () { return config_js_1.CONFIG_FILE; } });
13
+ var schema_js_1 = require("./schema.js");
14
+ Object.defineProperty(exports, "sandboxConfigBodySchema", { enumerable: true, get: function () { return schema_js_1.sandboxConfigBodySchema; } });
15
+ Object.defineProperty(exports, "storedSandboxSchema", { enumerable: true, get: function () { return schema_js_1.storedSandboxSchema; } });
16
+ Object.defineProperty(exports, "CONFIG_VERSION", { enumerable: true, get: function () { return schema_js_1.CONFIG_VERSION; } });
17
+ var errors_js_1 = require("./errors.js");
18
+ Object.defineProperty(exports, "PreviewOperationError", { enumerable: true, get: function () { return errors_js_1.PreviewOperationError; } });
@@ -0,0 +1,9 @@
1
+ import type { PreviewResult } from './types.js';
2
+ export interface PreviewSandboxOptions {
3
+ dir: string;
4
+ name?: string;
5
+ apiKey?: string;
6
+ servicesBaseUrl?: string;
7
+ log?: (message: string) => void;
8
+ }
9
+ export declare function previewSandbox(opts: PreviewSandboxOptions): Promise<PreviewResult>;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.previewSandbox = previewSandbox;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const tools_1 = require("@sapiom/tools");
9
+ const config_js_1 = require("./config.js");
10
+ const errors_js_1 = require("./errors.js");
11
+ const git_push_js_1 = require("./git-push.js");
12
+ const RUNNING_TIMEOUT_MS = 120000;
13
+ const RUNNING_POLL_MS = 2000;
14
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
+ async function previewSandbox(opts) {
16
+ const cfg = (0, config_js_1.getSandbox)(opts.dir, opts.name);
17
+ const log = opts.log ?? (() => { });
18
+ const sapiom = (0, tools_1.createClient)({ apiKey: opts.apiKey });
19
+ const baseUrl = opts.servicesBaseUrl;
20
+ const sub = cfg.source.path ?? '.';
21
+ const sourceDir = node_path_1.default.resolve(opts.dir, sub);
22
+ let deploySource;
23
+ if (cfg.source.kind === 'git') {
24
+ const slug = cfg.source.slug;
25
+ log(`ensuring repo "${slug}" …`);
26
+ const repo = await sapiom.repositories.get(slug).catch(() => null);
27
+ const cloneUrl = repo?.cloneUrl ?? (await sapiom.repositories.create(slug)).cloneUrl;
28
+ log(`pushing ${sub} → repo "${slug}" …`);
29
+ (0, git_push_js_1.pushLocalDir)(sourceDir, cloneUrl, opts.apiKey ?? '');
30
+ deploySource = { kind: 'git', repo: slug };
31
+ }
32
+ log(`ensuring sandbox "${cfg.name}" …`);
33
+ await ensureRunning(sapiom, cfg, baseUrl, log);
34
+ const box = sapiom.sandboxes.attach(cfg.name, baseUrl ? { baseUrl } : {});
35
+ if (cfg.source.kind === 'upload') {
36
+ log(`uploading ${sub} …`);
37
+ await box.uploadDir(sourceDir);
38
+ }
39
+ log('deploying …');
40
+ const res = await box.deployPreview({
41
+ ...(deploySource ? { source: deploySource } : {}),
42
+ build: cfg.build,
43
+ start: cfg.start,
44
+ port: cfg.port,
45
+ env: cfg.env,
46
+ });
47
+ return { name: cfg.name, url: res.url, status: res.status, logs: res.logs };
48
+ }
49
+ async function ensureRunning(sapiom, cfg, baseUrl, log) {
50
+ const getOpts = baseUrl ? { baseUrl } : {};
51
+ const existing = await sapiom.sandboxes.get(cfg.name, getOpts).catch(() => null);
52
+ if (!existing) {
53
+ log(`creating sandbox "${cfg.name}" …`);
54
+ const createArgs = {
55
+ name: cfg.name,
56
+ port: cfg.port,
57
+ ...(cfg.tier ? { tier: cfg.tier } : {}),
58
+ ...(cfg.ttl ? { ttl: cfg.ttl } : {}),
59
+ ...(baseUrl ? { baseUrl } : {}),
60
+ };
61
+ await sapiom.sandboxes.create(createArgs);
62
+ }
63
+ const deadline = Date.now() + RUNNING_TIMEOUT_MS;
64
+ for (;;) {
65
+ const info = await sapiom.sandboxes.get(cfg.name, getOpts);
66
+ if (info.status === 'running')
67
+ return;
68
+ if (info.status === 'failed') {
69
+ throw new errors_js_1.PreviewOperationError({
70
+ code: 'SANDBOX_FAILED',
71
+ message: `Sandbox '${cfg.name}' failed to start.`,
72
+ hint: info.error,
73
+ });
74
+ }
75
+ if (Date.now() > deadline) {
76
+ throw new errors_js_1.PreviewOperationError({
77
+ code: 'SANDBOX_NOT_READY',
78
+ message: `Sandbox '${cfg.name}' did not reach 'running' in time (status: ${info.status}).`,
79
+ });
80
+ }
81
+ await sleep(RUNNING_POLL_MS);
82
+ }
83
+ }
@@ -0,0 +1,127 @@
1
+ import { z } from 'zod';
2
+ export declare const CONFIG_VERSION = 1;
3
+ export declare const sandboxConfigBodySchema: z.ZodObject<{
4
+ source: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
5
+ kind: z.ZodLiteral<"upload">;
6
+ path: z.ZodOptional<z.ZodString>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ kind: "upload";
9
+ path?: string | undefined;
10
+ }, {
11
+ kind: "upload";
12
+ path?: string | undefined;
13
+ }>, z.ZodObject<{
14
+ kind: z.ZodLiteral<"git">;
15
+ slug: z.ZodString;
16
+ path: z.ZodOptional<z.ZodString>;
17
+ }, "strip", z.ZodTypeAny, {
18
+ kind: "git";
19
+ slug: string;
20
+ path?: string | undefined;
21
+ }, {
22
+ kind: "git";
23
+ slug: string;
24
+ path?: string | undefined;
25
+ }>]>;
26
+ build: z.ZodOptional<z.ZodString>;
27
+ start: z.ZodString;
28
+ port: z.ZodNumber;
29
+ tier: z.ZodOptional<z.ZodEnum<["xs", "s", "m", "l", "xl"]>>;
30
+ ttl: z.ZodOptional<z.ZodString>;
31
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ source: {
34
+ kind: "upload";
35
+ path?: string | undefined;
36
+ } | {
37
+ kind: "git";
38
+ slug: string;
39
+ path?: string | undefined;
40
+ };
41
+ start: string;
42
+ port: number;
43
+ build?: string | undefined;
44
+ tier?: "xs" | "s" | "m" | "l" | "xl" | undefined;
45
+ ttl?: string | undefined;
46
+ env?: Record<string, string> | undefined;
47
+ }, {
48
+ source: {
49
+ kind: "upload";
50
+ path?: string | undefined;
51
+ } | {
52
+ kind: "git";
53
+ slug: string;
54
+ path?: string | undefined;
55
+ };
56
+ start: string;
57
+ port: number;
58
+ build?: string | undefined;
59
+ tier?: "xs" | "s" | "m" | "l" | "xl" | undefined;
60
+ ttl?: string | undefined;
61
+ env?: Record<string, string> | undefined;
62
+ }>;
63
+ export type SandboxConfigBody = z.infer<typeof sandboxConfigBodySchema>;
64
+ export declare const storedSandboxSchema: z.ZodObject<{
65
+ source: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
66
+ kind: z.ZodLiteral<"upload">;
67
+ path: z.ZodOptional<z.ZodString>;
68
+ }, "strip", z.ZodTypeAny, {
69
+ kind: "upload";
70
+ path?: string | undefined;
71
+ }, {
72
+ kind: "upload";
73
+ path?: string | undefined;
74
+ }>, z.ZodObject<{
75
+ kind: z.ZodLiteral<"git">;
76
+ slug: z.ZodString;
77
+ path: z.ZodOptional<z.ZodString>;
78
+ }, "strip", z.ZodTypeAny, {
79
+ kind: "git";
80
+ slug: string;
81
+ path?: string | undefined;
82
+ }, {
83
+ kind: "git";
84
+ slug: string;
85
+ path?: string | undefined;
86
+ }>]>;
87
+ build: z.ZodOptional<z.ZodString>;
88
+ start: z.ZodString;
89
+ port: z.ZodNumber;
90
+ tier: z.ZodOptional<z.ZodEnum<["xs", "s", "m", "l", "xl"]>>;
91
+ ttl: z.ZodOptional<z.ZodString>;
92
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
93
+ } & {
94
+ type: z.ZodLiteral<"sandbox">;
95
+ }, "strip", z.ZodTypeAny, {
96
+ type: "sandbox";
97
+ source: {
98
+ kind: "upload";
99
+ path?: string | undefined;
100
+ } | {
101
+ kind: "git";
102
+ slug: string;
103
+ path?: string | undefined;
104
+ };
105
+ start: string;
106
+ port: number;
107
+ build?: string | undefined;
108
+ tier?: "xs" | "s" | "m" | "l" | "xl" | undefined;
109
+ ttl?: string | undefined;
110
+ env?: Record<string, string> | undefined;
111
+ }, {
112
+ type: "sandbox";
113
+ source: {
114
+ kind: "upload";
115
+ path?: string | undefined;
116
+ } | {
117
+ kind: "git";
118
+ slug: string;
119
+ path?: string | undefined;
120
+ };
121
+ start: string;
122
+ port: number;
123
+ build?: string | undefined;
124
+ tier?: "xs" | "s" | "m" | "l" | "xl" | undefined;
125
+ ttl?: string | undefined;
126
+ env?: Record<string, string> | undefined;
127
+ }>;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.storedSandboxSchema = exports.sandboxConfigBodySchema = exports.CONFIG_VERSION = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.CONFIG_VERSION = 1;
6
+ const sourceSchema = zod_1.z.discriminatedUnion('kind', [
7
+ zod_1.z.object({ kind: zod_1.z.literal('upload'), path: zod_1.z.string().optional() }),
8
+ zod_1.z.object({ kind: zod_1.z.literal('git'), slug: zod_1.z.string().min(1), path: zod_1.z.string().optional() }),
9
+ ]);
10
+ exports.sandboxConfigBodySchema = zod_1.z.object({
11
+ source: sourceSchema,
12
+ build: zod_1.z.string().optional(),
13
+ start: zod_1.z.string().min(1, 'start (the server command) is required'),
14
+ port: zod_1.z.number().int().min(1).max(65535),
15
+ tier: zod_1.z.enum(['xs', 's', 'm', 'l', 'xl']).optional(),
16
+ ttl: zod_1.z.string().optional(),
17
+ env: zod_1.z.record(zod_1.z.string()).optional(),
18
+ });
19
+ exports.storedSandboxSchema = exports.sandboxConfigBodySchema.extend({ type: zod_1.z.literal('sandbox') });
@@ -0,0 +1,11 @@
1
+ import type { SandboxConfigBody } from './schema.js';
2
+ export type SandboxSourceSpec = SandboxConfigBody['source'];
3
+ export type SandboxConfig = {
4
+ name: string;
5
+ } & SandboxConfigBody;
6
+ export interface PreviewResult {
7
+ name: string;
8
+ url: string | null;
9
+ status: string;
10
+ logs: string;
11
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,12 @@
1
+ import { type SandboxConfigBody } from './schema.js';
2
+ import type { SandboxConfig } from './types.js';
3
+ export declare const CONFIG_FILE = "sapiom.json";
4
+ export declare function readSandboxes(dir: string): Record<string, SandboxConfig>;
5
+ export declare function getSandbox(dir: string, name?: string): SandboxConfig;
6
+ export declare function configureSandbox(dir: string, name: string, body: SandboxConfigBody): SandboxConfig;
7
+ export declare const writeSandbox: (dir: string, cfg: SandboxConfig) => SandboxConfig;
8
+ export declare function checkSandboxes(dir: string): {
9
+ ok: boolean;
10
+ sandboxes: string[];
11
+ issues: string[];
12
+ };
@@ -0,0 +1,122 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PreviewOperationError } from './errors.js';
4
+ import { CONFIG_VERSION, storedSandboxSchema } from './schema.js';
5
+ export const CONFIG_FILE = 'sapiom.json';
6
+ function readFile(dir) {
7
+ const file = path.join(dir, CONFIG_FILE);
8
+ if (!existsSync(file))
9
+ return null;
10
+ let parsed;
11
+ try {
12
+ parsed = JSON.parse(readFileSync(file, 'utf8'));
13
+ }
14
+ catch {
15
+ throw new PreviewOperationError({ code: 'BAD_CONFIG', message: `${CONFIG_FILE} is not valid JSON.` });
16
+ }
17
+ if (typeof parsed.version === 'number' && parsed.version > CONFIG_VERSION) {
18
+ throw new PreviewOperationError({
19
+ code: 'UNSUPPORTED_CONFIG_VERSION',
20
+ message: `${CONFIG_FILE} is version ${parsed.version}, but this tool supports up to ${CONFIG_VERSION}.`,
21
+ hint: 'Upgrade the Sapiom SDK/CLI.',
22
+ });
23
+ }
24
+ return parsed;
25
+ }
26
+ function parseSandboxEntry(name, entry) {
27
+ const result = storedSandboxSchema.safeParse(entry);
28
+ if (!result.success) {
29
+ const detail = result.error.issues
30
+ .map((i) => `${['resources', name, ...i.path].join('.')}: ${i.message}`)
31
+ .join('; ');
32
+ throw new PreviewOperationError({
33
+ code: 'INVALID_SANDBOX',
34
+ message: `Invalid sandbox resource "${name}" in ${CONFIG_FILE}.`,
35
+ hint: detail,
36
+ });
37
+ }
38
+ const { type: _type, ...body } = result.data;
39
+ return { name, ...body };
40
+ }
41
+ export function readSandboxes(dir) {
42
+ const cfg = readFile(dir);
43
+ const resources = cfg?.resources ?? {};
44
+ const out = {};
45
+ for (const [name, entry] of Object.entries(resources)) {
46
+ if (entry?.type !== 'sandbox')
47
+ continue;
48
+ out[name] = parseSandboxEntry(name, entry);
49
+ }
50
+ return out;
51
+ }
52
+ export function getSandbox(dir, name) {
53
+ const sandboxes = readSandboxes(dir);
54
+ const names = Object.keys(sandboxes);
55
+ if (name) {
56
+ const found = sandboxes[name];
57
+ if (!found) {
58
+ throw new PreviewOperationError({
59
+ code: 'NO_SANDBOX',
60
+ message: `No sandbox named '${name}' in ${CONFIG_FILE}.`,
61
+ hint: names.length ? `Known sandboxes: ${names.join(', ')}.` : undefined,
62
+ });
63
+ }
64
+ return found;
65
+ }
66
+ if (names.length === 0) {
67
+ throw new PreviewOperationError({
68
+ code: 'NO_SANDBOX',
69
+ message: `No sandbox resources defined in ${CONFIG_FILE}.`,
70
+ hint: 'Add one with sapiom_dev_sandbox_configure (or a "type":"sandbox" resource).',
71
+ });
72
+ }
73
+ if (names.length > 1) {
74
+ throw new PreviewOperationError({
75
+ code: 'AMBIGUOUS_SANDBOX',
76
+ message: `Multiple sandboxes defined (${names.join(', ')}); specify which one.`,
77
+ hint: 'Pass a name, e.g. `sapiom sandbox preview <name>`.',
78
+ });
79
+ }
80
+ return sandboxes[names[0]];
81
+ }
82
+ export function configureSandbox(dir, name, body) {
83
+ const parsed = storedSandboxSchema.safeParse({ type: 'sandbox', ...body });
84
+ if (!parsed.success) {
85
+ const detail = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
86
+ throw new PreviewOperationError({
87
+ code: 'INVALID_SANDBOX',
88
+ message: `Invalid sandbox configuration for "${name}".`,
89
+ hint: detail,
90
+ });
91
+ }
92
+ const existing = readFile(dir) ?? {};
93
+ const next = {
94
+ version: CONFIG_VERSION,
95
+ ...existing,
96
+ resources: { ...(existing.resources ?? {}), [name]: parsed.data },
97
+ };
98
+ next.version = CONFIG_VERSION;
99
+ writeFileSync(path.join(dir, CONFIG_FILE), JSON.stringify(next, null, 2) + '\n');
100
+ return { name, ...body };
101
+ }
102
+ export const writeSandbox = (dir, cfg) => {
103
+ const { name, ...body } = cfg;
104
+ return configureSandbox(dir, name, body);
105
+ };
106
+ export function checkSandboxes(dir) {
107
+ const issues = [];
108
+ const cfg = readFile(dir);
109
+ const resources = cfg?.resources ?? {};
110
+ const found = [];
111
+ for (const [name, entry] of Object.entries(resources)) {
112
+ if (entry?.type !== 'sandbox')
113
+ continue;
114
+ found.push(name);
115
+ const r = storedSandboxSchema.safeParse(entry);
116
+ if (!r.success) {
117
+ for (const i of r.error.issues)
118
+ issues.push(`resources.${name}.${i.path.join('.')}: ${i.message}`);
119
+ }
120
+ }
121
+ return { ok: issues.length === 0, sandboxes: found, issues };
122
+ }