@phpsandbox/sdk 0.0.57 → 0.0.59

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,49 @@
1
+ import { Client } from '../src/index.js';
2
+
3
+ const token = process.env.PHPSANDBOX_TOKEN;
4
+ if (!token) {
5
+ throw new Error('Missing PHPSANDBOX_TOKEN');
6
+ }
7
+
8
+ const notebookId = process.env.PHPSANDBOX_NOTEBOOK_ID;
9
+ if (!notebookId) {
10
+ throw new Error('Missing PHPSANDBOX_NOTEBOOK_ID');
11
+ }
12
+
13
+ const slug = process.env.PHPSANDBOX_DEPLOY_SLUG ?? `sdk-cloudflare-${Date.now().toString(36)}`;
14
+ const apiUrl = process.env.PHPSANDBOX_API_URL;
15
+ const client = new Client(token, apiUrl);
16
+ const notebook = await client.notebook.get(notebookId);
17
+
18
+ try {
19
+ const deployment = await notebook.deployment();
20
+ if (deployment && deployment.data.targetProvider !== 'okraCloudflareContainer') {
21
+ throw new Error(`Notebook is already published to ${deployment.data.targetProvider}. Delete that deployment before changing providers.`);
22
+ }
23
+
24
+ const run = deployment
25
+ ? await deployment.publish()
26
+ : await notebook.publish({
27
+ slug,
28
+ targetProvider: 'okraCloudflareContainer',
29
+ options: {
30
+ size: 'small',
31
+ sleepAfter: '30m',
32
+ },
33
+ });
34
+
35
+ for await (const event of run.events()) {
36
+ if (event.type === 'phase') {
37
+ console.log('phase:', event.name);
38
+ } else if (event.type === 'log') {
39
+ process.stdout.write(event.content);
40
+ } else if (event.type === 'result') {
41
+ console.log('result:', event);
42
+ }
43
+ }
44
+
45
+ const current = await run.deployment();
46
+ console.log('deployment:', current.data.id, current.data.status, current.data.url);
47
+ } finally {
48
+ notebook.dispose();
49
+ }
@@ -0,0 +1,111 @@
1
+ import { Client, type LaravelCloudRegion } from '../src/index.js';
2
+
3
+ const token = process.env.PHPSANDBOX_TOKEN;
4
+ if (!token) {
5
+ throw new Error('Missing PHPSANDBOX_TOKEN');
6
+ }
7
+
8
+ const notebookId = process.env.PHPSANDBOX_NOTEBOOK_ID;
9
+ if (!notebookId) {
10
+ throw new Error('Missing PHPSANDBOX_NOTEBOOK_ID');
11
+ }
12
+
13
+ const apiKeySecret = 'LARAVEL_CLOUD_API_KEY';
14
+ const githubTokenSecret = 'GITHUB_TOKEN';
15
+ const slug = process.env.PHPSANDBOX_DEPLOY_SLUG ?? `sdk-laravel-cloud-${Date.now().toString(36)}`;
16
+ const region: LaravelCloudRegion = 'eu-central-1';
17
+ const apiUrl = process.env.PHPSANDBOX_API_URL ?? 'https://api.phpsandbox.io/v1';
18
+
19
+ if (new URL(apiUrl).hostname.endsWith('.test') && process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
20
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
21
+ }
22
+
23
+ const client = new Client(token, apiUrl);
24
+ const notebook = await client.notebook.get(notebookId);
25
+
26
+ try {
27
+ const deployment = await notebook.deployment();
28
+ if (deployment && deployment.data.targetProvider !== 'okraLaravelCloud') {
29
+ throw new Error(`Notebook is already published to ${deployment.data.targetProvider}. Delete that deployment before changing providers.`);
30
+ }
31
+
32
+ const run = deployment ? await deployment.publish() : await publishFirstLaravelCloudDeployment();
33
+
34
+ for await (const event of run.events()) {
35
+ if (event.type === 'phase') {
36
+ console.log('phase:', event.name);
37
+ } else if (event.type === 'log') {
38
+ process.stdout.write(`${event.content}\n`);
39
+ } else if (event.type === 'result') {
40
+ console.log('result:', event);
41
+ }
42
+ }
43
+
44
+ const current = await run.deployment();
45
+ console.log('deployment:', current.data.id, current.data.status, current.data.url);
46
+ } finally {
47
+ notebook.dispose();
48
+ }
49
+
50
+ async function publishFirstLaravelCloudDeployment() {
51
+ const repository = process.env.PHPSANDBOX_LARAVEL_CLOUD_REPOSITORY;
52
+ if (!repository) {
53
+ throw new Error('Missing PHPSANDBOX_LARAVEL_CLOUD_REPOSITORY');
54
+ }
55
+
56
+ const githubToken = process.env.PHPSANDBOX_GITHUB_TOKEN;
57
+ if (!githubToken) {
58
+ throw new Error('Missing PHPSANDBOX_GITHUB_TOKEN');
59
+ }
60
+
61
+ const gitAuthorName = process.env.PHPSANDBOX_GIT_AUTHOR_NAME;
62
+ if (!gitAuthorName) {
63
+ throw new Error('Missing PHPSANDBOX_GIT_AUTHOR_NAME');
64
+ }
65
+
66
+ const gitAuthorEmail = process.env.PHPSANDBOX_GIT_AUTHOR_EMAIL;
67
+ if (!gitAuthorEmail) {
68
+ throw new Error('Missing PHPSANDBOX_GIT_AUTHOR_EMAIL');
69
+ }
70
+
71
+ const apiKey = process.env.PHPSANDBOX_LARAVEL_CLOUD_API_KEY;
72
+ if (!apiKey) {
73
+ throw new Error('Missing PHPSANDBOX_LARAVEL_CLOUD_API_KEY');
74
+ }
75
+
76
+ await notebook.secrets.set(githubTokenSecret, {
77
+ value: githubToken,
78
+ type: 'credential',
79
+ });
80
+
81
+ const source = await notebook.git.sync({
82
+ provider: 'github',
83
+ repository,
84
+ author: { name: gitAuthorName, email: gitAuthorEmail },
85
+ branch: 'main',
86
+ credential: {
87
+ secret: githubTokenSecret,
88
+ },
89
+ setDefault: true,
90
+ });
91
+
92
+ console.log('source:', source.repository, source.branch, source.lastCommitSha);
93
+
94
+ const secret = await notebook.secrets.set(apiKeySecret, {
95
+ value: apiKey,
96
+ type: 'credential',
97
+ });
98
+
99
+ console.log('secret:', secret.name);
100
+
101
+ return notebook.publish({
102
+ slug,
103
+ targetProvider: 'okraLaravelCloud',
104
+ options: {
105
+ apiKey: {
106
+ secret: apiKeySecret,
107
+ },
108
+ region,
109
+ },
110
+ });
111
+ }
@@ -0,0 +1,211 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import { existsSync, mkdtempSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Client, type ServerInstance } from '../src/index.js';
7
+
8
+ const token = process.env.PHPSANDBOX_TOKEN;
9
+ if (!token) {
10
+ throw new Error('Missing PHPSANDBOX_TOKEN');
11
+ }
12
+
13
+ const notebookId = process.env.PHPSANDBOX_NOTEBOOK_ID;
14
+ if (!notebookId) {
15
+ throw new Error('Missing PHPSANDBOX_NOTEBOOK_ID');
16
+ }
17
+
18
+ const slug = process.env.PHPSANDBOX_DEPLOY_SLUG ?? `sdk-ssh-${Date.now().toString(36)}`;
19
+ const apiUrl = process.env.PHPSANDBOX_API_URL ?? 'https://api.phpsandbox.io/v1';
20
+ const isLocal = new URL(apiUrl).hostname.endsWith('.test');
21
+
22
+ if (isLocal && process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
23
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
24
+ }
25
+
26
+ const client = new Client(token, apiUrl);
27
+ const notebook = await client.notebook.get(notebookId);
28
+ let agent: ChildProcessWithoutNullStreams | null = null;
29
+
30
+ try {
31
+ const deployment = await notebook.deployment();
32
+ if (deployment && deployment.data.targetProvider !== 'okraSshAgent') {
33
+ throw new Error(`Notebook is already published to ${deployment.data.targetProvider}. Delete that deployment before changing providers.`);
34
+ }
35
+
36
+ const run = deployment
37
+ ? await deployment.publish()
38
+ : await notebook.publish({
39
+ slug,
40
+ targetProvider: 'okraSshAgent',
41
+ options: { serverId: (await resolveServer()).data.id },
42
+ });
43
+
44
+ for await (const event of run.events()) {
45
+ if (event.type === 'phase') {
46
+ console.log('phase:', event.name);
47
+ } else if (event.type === 'log') {
48
+ process.stdout.write(event.content);
49
+ } else if (event.type === 'result') {
50
+ console.log('result:', event);
51
+ }
52
+ }
53
+
54
+ const current = await run.deployment();
55
+ console.log('deployment:', current.data.id, current.data.status, current.data.url);
56
+
57
+ if (current.data.url) {
58
+ const response = await fetch(current.data.url);
59
+ console.log('http:', response.status, response.statusText, await response.text());
60
+ if (!response.ok) {
61
+ throw new Error(`Deployment URL returned ${response.status} ${response.statusText}`);
62
+ }
63
+ }
64
+ } finally {
65
+ notebook.dispose();
66
+ if (agent) {
67
+ stopLocalAgent(agent);
68
+ }
69
+ }
70
+
71
+ async function resolveServer(): Promise<ServerInstance> {
72
+ const serverId = process.env.PHPSANDBOX_DEPLOY_SERVER_ID;
73
+ if (serverId) {
74
+ const server = await client.servers.get(serverId);
75
+ console.log('server:', server.data.id, server.data.status);
76
+ return server;
77
+ }
78
+
79
+ const server = await client.servers.create({
80
+ name: `SDK SSH ${slug}`,
81
+ host: process.env.PHPSANDBOX_DEPLOY_SERVER_HOST ?? 'localhost',
82
+ ssh: {
83
+ user: process.env.USER ?? 'local',
84
+ port: Number(process.env.PHPSANDBOX_DEPLOY_SERVER_PORT ?? 22),
85
+ },
86
+ });
87
+
88
+ console.log('server:', server.data.id, server.data.status);
89
+
90
+ if (!isLocal) {
91
+ throw new Error('Created server registration. Install the agent on the target server, then rerun with PHPSANDBOX_DEPLOY_SERVER_ID.');
92
+ }
93
+
94
+ if (!server.data.token) {
95
+ throw new Error('Created server registration without an agent token.');
96
+ }
97
+
98
+ agent = await startLocalAgent(server.data.id, server.data.token);
99
+ const connected = await server.waitReady({ timeout: 120_000, interval: 1_000 });
100
+ console.log('server:', connected.data.id, connected.data.status);
101
+
102
+ return connected;
103
+ }
104
+
105
+ async function startLocalAgent(serverId: string, serverToken: string): Promise<ChildProcessWithoutNullStreams> {
106
+ const dir = mkdtempSync(join(tmpdir(), 'phpsandbox-sdk-ssh-agent-'));
107
+ const configPath = join(dir, 'agent.yaml');
108
+ const stateDir = join(dir, 'state');
109
+ const controlPlane = process.env.PHPSANDBOX_AGENT_CONTROL_PLANE ?? 'wss://okra-edge.ciroue.test/_okra/agent/connect';
110
+
111
+ writeFileSync(configPath, [
112
+ `server_id: "${serverId}"`,
113
+ `token: "${serverToken}"`,
114
+ `control_plane: "${controlPlane}"`,
115
+ `state_dir: "${stateDir}"`,
116
+ '',
117
+ ].join('\n'));
118
+
119
+ const rookDir = process.env.PHPSANDBOX_ROOK_DIR
120
+ ?? fileURLToPath(new URL('../../rook/', import.meta.url));
121
+ if (!existsSync(join(rookDir, 'cmd/rook/main.go'))) {
122
+ throw new Error('Cannot find local Rook repo. Set PHPSANDBOX_ROOK_DIR to the rook directory.');
123
+ }
124
+
125
+ const child = spawn('go', ['run', './cmd/rook', '--config', configPath], {
126
+ cwd: rookDir,
127
+ detached: true,
128
+ env: {
129
+ ...process.env,
130
+ SSL_CERT_FILE: localCaCertPath(),
131
+ },
132
+ });
133
+
134
+ process.once('exit', () => stopLocalAgent(child));
135
+
136
+ await waitForAgent(child);
137
+ console.log('agent:', 'connected');
138
+
139
+ return child;
140
+ }
141
+
142
+ function stopLocalAgent(child: ChildProcessWithoutNullStreams): void {
143
+ if (child.exitCode !== null || child.signalCode !== null) {
144
+ return;
145
+ }
146
+
147
+ if (child.pid && child.pid > 0) {
148
+ try {
149
+ process.kill(-child.pid, 'SIGTERM');
150
+ return;
151
+ } catch {
152
+ // Fall back to killing the direct child when process group signaling is unavailable.
153
+ }
154
+ }
155
+
156
+ child.kill('SIGTERM');
157
+ }
158
+
159
+ function waitForAgent(child: ChildProcessWithoutNullStreams): Promise<void> {
160
+ return new Promise((resolve, reject) => {
161
+ let settled = false;
162
+ let output = '';
163
+ const timeout = setTimeout(() => {
164
+ finish(new Error(`Timed out waiting for Rook to connect. ${output.trim()}`));
165
+ }, 120_000);
166
+
167
+ const finish = (error?: Error) => {
168
+ if (settled) {
169
+ return;
170
+ }
171
+ settled = true;
172
+ clearTimeout(timeout);
173
+ child.stdout.off('data', onStdout);
174
+ child.stderr.off('data', onStderr);
175
+ child.off('exit', onExit);
176
+ if (error) {
177
+ reject(error);
178
+ } else {
179
+ resolve();
180
+ }
181
+ };
182
+
183
+ const append = (chunk: Buffer) => {
184
+ output = `${output}${chunk.toString()}`.slice(-4000);
185
+ };
186
+
187
+ const onStdout = (chunk: Buffer) => {
188
+ append(chunk);
189
+ if (chunk.toString().includes('connected, waiting for commands')) {
190
+ finish();
191
+ }
192
+ };
193
+
194
+ const onStderr = (chunk: Buffer) => append(chunk);
195
+ const onExit = (code: number | null) => {
196
+ finish(new Error(`Rook exited before connecting with code ${code}. ${output.trim()}`));
197
+ };
198
+
199
+ child.stdout.on('data', onStdout);
200
+ child.stderr.on('data', onStderr);
201
+ child.once('exit', onExit);
202
+ });
203
+ }
204
+
205
+ function localCaCertPath(): string {
206
+ const configured = process.env.PHPSANDBOX_LOCAL_CA_CERT
207
+ ?? process.env.NODE_EXTRA_CA_CERTS
208
+ ?? `${process.env.HOME}/.config/valet/CA/LaravelValetCASelfSigned.pem`;
209
+
210
+ return configured.replace(/^"(.*)"$/, '$1');
211
+ }
package/examples/get.ts CHANGED
@@ -5,7 +5,7 @@ if (!token) {
5
5
  throw new Error('Missing PHPSANDBOX_TOKEN');
6
6
  }
7
7
 
8
- const notebookId = 'sfl89';
8
+ const notebookId = process.env.PHPSANDBOX_NOTEBOOK_ID;
9
9
  if (!notebookId) {
10
10
  throw new Error('Missing PHPSANDBOX_NOTEBOOK_ID');
11
11
  }
@@ -15,11 +15,8 @@ const client = new Client(token, apiUrl);
15
15
  const notebook = await client.notebook.get(notebookId);
16
16
 
17
17
  try {
18
+ console.log('notebook:', notebook.data.id, notebook.data.title ?? '');
18
19
  console.log(await notebook.mail.status());
19
- console.log(await notebook.preview.createHandoff({
20
- preview_session_id: 'ps_29c3265107354bb9a70d19470a57b272',
21
- expires_in_seconds: 900,
22
- }));
23
20
  } finally {
24
21
  notebook.dispose();
25
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phpsandbox/sdk",
3
- "version": "0.0.57",
3
+ "version": "0.0.59",
4
4
  "description": "A comprehensive TypeScript SDK for interacting with cloud-based PHP development environments",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "build:browser": "node build.mjs",
29
29
  "clean": "rm -rf dist",
30
30
  "test": "vitest run",
31
- "check:examples": "tsc --noEmit --module NodeNext --moduleResolution NodeNext --target ES2020 --types node --skipLibCheck --allowSyntheticDefaultImports --esModuleInterop examples/basic.ts examples/get.ts examples/git-merge-smoke.ts examples/laravel-load-test.ts examples/notebook-readonly-smoke.ts",
31
+ "check:examples": "tsc --noEmit --module NodeNext --moduleResolution NodeNext --target ES2020 --types node --skipLibCheck --allowSyntheticDefaultImports --esModuleInterop examples/basic.ts examples/deploy-cloudflare-container.ts examples/deploy-laravel-cloud.ts examples/deploy-ssh-agent.ts examples/get.ts examples/git-merge-smoke.ts examples/laravel-load-test.ts examples/notebook-readonly-smoke.ts",
32
32
  "prepublishOnly": "npm test && npm run check:examples && npm run clean && npm run build",
33
33
  "bumpAndPublish": "npm version patch && npm publish --access public"
34
34
  },
@@ -61,7 +61,8 @@
61
61
  "mitt": "^3.0.1",
62
62
  "nanoid": "^5.1.2",
63
63
  "reconnecting-websocket": "^4.4.0",
64
- "ws": "^8.21.0"
64
+ "ws": "^8.21.0",
65
+ "zod": "^4.4.3"
65
66
  },
66
67
  "devDependencies": {
67
68
  "@types/node": "^24.12.2",