@allwright.dev/core 0.0.33 → 0.0.35

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,2 @@
1
+ export declare function ensureRuntimeReady(serverAddr: string): Promise<void>;
2
+ export declare function shutdownManagedServer(): Promise<void>;
@@ -0,0 +1,240 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawn, spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import grpc from "@grpc/grpc-js";
7
+ import protoLoader from "@grpc/proto-loader";
8
+ const ALLWRIGHT_AUTO_INSTALL_ENV_VAR = "ALLWRIGHT_AUTO_INSTALL";
9
+ const ALLWRIGHT_CLI_PATH_ENV_VAR = "ALLWRIGHT_CLI_PATH";
10
+ const ALLWRIGHT_HOME_ENV_VAR = "ALLWRIGHT_HOME";
11
+ const ALLWRIGHT_REPOSITORY_ENV_VAR = "ALLWRIGHT_REPOSITORY";
12
+ const ALLWRIGHT_VERSION_ENV_VAR = "ALLWRIGHT_VERSION";
13
+ const DEFAULT_RELEASE_REPOSITORY = "allwright-dev/allwright";
14
+ const DEFAULT_RELEASE_VERSION = "0.0.1";
15
+ const STARTUP_TIMEOUT_MS = 20_000;
16
+ const PING_TIMEOUT_MS = 1_000;
17
+ const PROTO_ROOT = fileURLToPath(new URL("../proto/", import.meta.url));
18
+ const ENGINE_PROTO_PATH = fileURLToPath(new URL("../proto/engine/v1/engine.proto", import.meta.url));
19
+ let managedServer = null;
20
+ let managedServerAddr = null;
21
+ export async function ensureRuntimeReady(serverAddr) {
22
+ if (await pingServer(serverAddr)) {
23
+ return;
24
+ }
25
+ if (!isLocalServerAddr(serverAddr)) {
26
+ throw new Error(`allwright could not reach engine server at ${serverAddr}. Automatic startup is only supported for local addresses.`);
27
+ }
28
+ if (managedServer && !managedServer.killed && managedServer.exitCode === null && managedServerAddr === serverAddr) {
29
+ await waitForServer(serverAddr);
30
+ return;
31
+ }
32
+ const cliPath = await ensureCliAvailable();
33
+ ensureWebPlugin(cliPath);
34
+ managedServer = spawn(cliPath, ["serve", "--listen-addr", cliListenAddr(serverAddr)], {
35
+ stdio: "ignore",
36
+ });
37
+ managedServerAddr = serverAddr;
38
+ await waitForServer(serverAddr);
39
+ }
40
+ export async function shutdownManagedServer() {
41
+ if (managedServer && managedServer.exitCode === null && !managedServer.killed) {
42
+ managedServer.kill("SIGTERM");
43
+ }
44
+ managedServer = null;
45
+ managedServerAddr = null;
46
+ }
47
+ async function waitForServer(serverAddr) {
48
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
49
+ while (Date.now() < deadline) {
50
+ if (await pingServer(serverAddr)) {
51
+ return;
52
+ }
53
+ await new Promise((resolve) => setTimeout(resolve, 250));
54
+ }
55
+ await shutdownManagedServer();
56
+ throw new Error(`timed out waiting for allwright server at ${serverAddr} to become ready`);
57
+ }
58
+ async function pingServer(serverAddr) {
59
+ const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
60
+ includeDirs: [PROTO_ROOT],
61
+ keepCase: false,
62
+ longs: String,
63
+ enums: String,
64
+ defaults: true,
65
+ oneofs: true,
66
+ });
67
+ const proto = grpc.loadPackageDefinition(loaded);
68
+ const client = new proto.allwright.engine.v1.EngineService(serverAddr, grpc.credentials.createInsecure());
69
+ return await new Promise((resolve) => {
70
+ client.Ping({}, new grpc.Metadata(), { deadline: new Date(Date.now() + PING_TIMEOUT_MS) }, (error) => {
71
+ client.close();
72
+ resolve(!error);
73
+ });
74
+ });
75
+ }
76
+ async function ensureCliAvailable() {
77
+ const envPath = process.env[ALLWRIGHT_CLI_PATH_ENV_VAR]?.trim();
78
+ if (envPath && isFile(envPath)) {
79
+ return envPath;
80
+ }
81
+ const bundled = path.join(allwrightHome(), "bin", cliFilename());
82
+ if (isFile(bundled)) {
83
+ return bundled;
84
+ }
85
+ const fromPath = resolveFromPath(cliFilename());
86
+ if (fromPath) {
87
+ return fromPath;
88
+ }
89
+ if (!autoInstallEnabled()) {
90
+ throw new Error("allwright CLI was not found. Install it first or set ALLWRIGHT_CLI_PATH.");
91
+ }
92
+ return await installCli();
93
+ }
94
+ async function installCli() {
95
+ const installDir = path.join(allwrightHome(), "bin");
96
+ fs.mkdirSync(installDir, { recursive: true });
97
+ const cliPath = path.join(installDir, cliFilename());
98
+ const versionTag = await resolveReleaseTag();
99
+ const assetName = cliAssetName(versionTag);
100
+ const assetPath = path.join(os.tmpdir(), assetName);
101
+ const response = await fetch(`https://github.com/${releaseRepository()}/releases/download/${versionTag}/${assetName}`, { headers: { "user-agent": `allwright-ts/${DEFAULT_RELEASE_VERSION}` } });
102
+ if (!response.ok) {
103
+ throw new Error(`failed to download allwright CLI asset ${assetName}: ${response.status} ${response.statusText}`);
104
+ }
105
+ fs.writeFileSync(assetPath, Buffer.from(await response.arrayBuffer()));
106
+ extractCliArchive(assetPath, cliPath);
107
+ fs.chmodSync(cliPath, 0o755);
108
+ fs.rmSync(assetPath, { force: true });
109
+ return cliPath;
110
+ }
111
+ function ensureWebPlugin(cliPath) {
112
+ const pluginPath = path.join(allwrightHome(), "plugins", "web", "lib", webPluginFilename());
113
+ if (isFile(pluginPath)) {
114
+ return;
115
+ }
116
+ const version = process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION;
117
+ const result = spawnSync(cliPath, ["plugin", "install", "web", "--version", normalizeReleaseVersion(version)], {
118
+ stdio: "ignore",
119
+ });
120
+ if (result.status !== 0 || !isFile(pluginPath)) {
121
+ throw new Error("allwright attempted to install the `web` plugin automatically, but the install did not complete successfully");
122
+ }
123
+ }
124
+ async function resolveReleaseTag() {
125
+ const version = process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION;
126
+ if (version !== "latest") {
127
+ return normalizeReleaseTag(version);
128
+ }
129
+ const response = await fetch(`https://api.github.com/repos/${releaseRepository()}/releases/latest`, {
130
+ headers: { "user-agent": `allwright-ts/${DEFAULT_RELEASE_VERSION}` },
131
+ });
132
+ if (!response.ok) {
133
+ throw new Error(`failed to resolve latest allwright release: ${response.status} ${response.statusText}`);
134
+ }
135
+ const payload = (await response.json());
136
+ if (!payload.tag_name?.trim()) {
137
+ throw new Error("latest allwright release metadata did not include tag_name");
138
+ }
139
+ return payload.tag_name;
140
+ }
141
+ function extractCliArchive(archivePath, cliPath) {
142
+ if (archivePath.endsWith(".zip")) {
143
+ const result = spawnSync("powershell", [
144
+ "-NoProfile",
145
+ "-Command",
146
+ `Expand-Archive -Path '${archivePath.replaceAll("'", "''")}' -DestinationPath '${path.dirname(cliPath).replaceAll("'", "''")}' -Force`,
147
+ ], { stdio: "ignore" });
148
+ if (result.status !== 0) {
149
+ throw new Error("failed to extract allwright CLI zip archive");
150
+ }
151
+ const extracted = path.join(path.dirname(cliPath), "bin", cliFilename());
152
+ fs.copyFileSync(extracted, cliPath);
153
+ fs.rmSync(path.join(path.dirname(cliPath), "bin"), { recursive: true, force: true });
154
+ return;
155
+ }
156
+ const result = spawnSync("tar", [
157
+ "-xzf",
158
+ archivePath,
159
+ "-C",
160
+ path.dirname(cliPath),
161
+ `bin/${cliFilename()}`,
162
+ ], { stdio: "ignore" });
163
+ if (result.status !== 0) {
164
+ throw new Error("failed to extract allwright CLI tar archive");
165
+ }
166
+ const extracted = path.join(path.dirname(cliPath), "bin", cliFilename());
167
+ fs.copyFileSync(extracted, cliPath);
168
+ fs.rmSync(path.join(path.dirname(cliPath), "bin"), { recursive: true, force: true });
169
+ }
170
+ function cliAssetName(versionTag) {
171
+ const targets = new Map([
172
+ ["darwin/arm64", "aarch64-apple-darwin"],
173
+ ["darwin/x64", "x86_64-apple-darwin"],
174
+ ["linux/arm64", "aarch64-unknown-linux-gnu"],
175
+ ["linux/x64", "x86_64-unknown-linux-gnu"],
176
+ ["win32/arm64", "aarch64-pc-windows-msvc"],
177
+ ["win32/x64", "x86_64-pc-windows-msvc"],
178
+ ]);
179
+ const target = targets.get(`${process.platform}/${process.arch}`);
180
+ if (!target) {
181
+ throw new Error(`automatic allwright CLI install is not supported on ${process.platform}/${process.arch}`);
182
+ }
183
+ const extension = process.platform === "win32" ? "zip" : "tar.gz";
184
+ return `allwright-${versionTag}-${target}.${extension}`;
185
+ }
186
+ function normalizeReleaseTag(version) {
187
+ return version.startsWith("v") ? version : `v${version}`;
188
+ }
189
+ function normalizeReleaseVersion(version) {
190
+ return version.replace(/^v/, "");
191
+ }
192
+ function cliListenAddr(serverAddr) {
193
+ return serverAddr.replace(/^https?:\/\//, "");
194
+ }
195
+ function isLocalServerAddr(serverAddr) {
196
+ const host = cliListenAddr(serverAddr).split(":")[0]?.replace(/^\[|\]$/g, "") ?? "";
197
+ return host === "127.0.0.1" || host === "localhost" || host === "::1";
198
+ }
199
+ function allwrightHome() {
200
+ return process.env[ALLWRIGHT_HOME_ENV_VAR]?.trim() || path.join(os.homedir(), ".allwright");
201
+ }
202
+ function cliFilename() {
203
+ return process.platform === "win32" ? "allwright.exe" : "allwright";
204
+ }
205
+ function webPluginFilename() {
206
+ if (process.platform === "darwin") {
207
+ return "liballwright_surface_web.dylib";
208
+ }
209
+ if (process.platform === "win32") {
210
+ return "allwright_surface_web.dll";
211
+ }
212
+ return "liballwright_surface_web.so";
213
+ }
214
+ function autoInstallEnabled() {
215
+ const raw = process.env[ALLWRIGHT_AUTO_INSTALL_ENV_VAR]?.trim().toLowerCase();
216
+ return raw !== "0" && raw !== "false" && raw !== "no";
217
+ }
218
+ function resolveFromPath(filename) {
219
+ for (const entry of (process.env.PATH ?? "").split(path.delimiter)) {
220
+ if (!entry) {
221
+ continue;
222
+ }
223
+ const candidate = path.join(entry, filename);
224
+ if (isFile(candidate)) {
225
+ return candidate;
226
+ }
227
+ }
228
+ return null;
229
+ }
230
+ function releaseRepository() {
231
+ return process.env[ALLWRIGHT_REPOSITORY_ENV_VAR]?.trim() || DEFAULT_RELEASE_REPOSITORY;
232
+ }
233
+ function isFile(candidate) {
234
+ try {
235
+ return fs.statSync(candidate).isFile();
236
+ }
237
+ catch {
238
+ return false;
239
+ }
240
+ }
package/dist/runtime.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import grpc from "@grpc/grpc-js";
3
3
  import protoLoader from "@grpc/proto-loader";
4
+ import { ensureRuntimeReady, shutdownManagedServer } from "./bootstrap.js";
4
5
  import { EventQueue } from "./types.js";
5
6
  const DEFAULT_SERVER_ADDR = "127.0.0.1:50051";
6
7
  const SERVER_ADDR_ENV_VAR = "ALLWRIGHT_SERVER_ADDR";
@@ -11,6 +12,7 @@ let serverAddrOverride = null;
11
12
  export function setServerAddr(serverAddr) {
12
13
  serverAddrOverride = normalizeServerAddr(serverAddr);
13
14
  runtimePromise = null;
15
+ void shutdownManagedServer();
14
16
  }
15
17
  export async function shutdown() {
16
18
  if (!runtimePromise) {
@@ -19,6 +21,7 @@ export async function shutdown() {
19
21
  const runtime = await runtimePromise;
20
22
  runtime.client.close();
21
23
  runtimePromise = null;
24
+ await shutdownManagedServer();
22
25
  }
23
26
  export async function ping() {
24
27
  const runtime = await getRuntime();
@@ -34,7 +37,7 @@ export async function ping() {
34
37
  }
35
38
  export async function getRuntime() {
36
39
  if (!runtimePromise) {
37
- runtimePromise = Promise.resolve(createRuntime());
40
+ runtimePromise = createRuntime();
38
41
  }
39
42
  return runtimePromise;
40
43
  }
@@ -72,7 +75,9 @@ export function normalizeServerAddr(raw) {
72
75
  export function resolveLaunchBrowserArgs(browserKindOrOptions) {
73
76
  return browserKindOrOptions === undefined || typeof browserKindOrOptions !== "string";
74
77
  }
75
- function createRuntime() {
78
+ async function createRuntime() {
79
+ const serverAddr = configuredServerAddr();
80
+ await ensureRuntimeReady(serverAddr);
76
81
  const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
77
82
  includeDirs: [PROTO_ROOT],
78
83
  keepCase: false,
@@ -83,7 +88,7 @@ function createRuntime() {
83
88
  });
84
89
  const proto = grpc.loadPackageDefinition(loaded);
85
90
  const ClientCtor = proto.allwright.engine.v1.EngineService;
86
- const client = new ClientCtor(configuredServerAddr(), grpc.credentials.createInsecure());
91
+ const client = new ClientCtor(serverAddr, grpc.credentials.createInsecure());
87
92
  return { client };
88
93
  }
89
94
  function configuredServerAddr() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allwright.dev/core",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "High-level TypeScript client for the allwright automation engine.",
5
5
  "license": "MIT",
6
6
  "type": "module",