@t3x-dev/local 0.1.3 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t3x-dev/local",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "T3X local entry package",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -38,10 +38,10 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@hono/node-server": "^1.15.2",
41
- "@t3x-dev/api": "0.1.3",
42
- "@t3x-dev/cli": "0.1.3",
43
- "@t3x-dev/mcp": "0.1.3",
44
- "@t3x-dev/storage": "0.1.3",
41
+ "@t3x-dev/api": "0.1.5",
42
+ "@t3x-dev/cli": "0.1.5",
43
+ "@t3x-dev/mcp": "0.1.5",
44
+ "@t3x-dev/storage": "0.1.5",
45
45
  "commander": "^13.1.0"
46
46
  },
47
47
  "devDependencies": {
@@ -1,26 +1,26 @@
1
1
  {
2
2
  "manifestVersion": 1,
3
- "packageVersion": "0.1.3",
4
- "fixedVersion": "0.1.3",
5
- "generatedAt": "2026-04-27T03:16:33.886Z",
3
+ "packageVersion": "0.1.5",
4
+ "fixedVersion": "0.1.5",
5
+ "generatedAt": "2026-04-27T06:51:55.647Z",
6
6
  "dependencies": {
7
- "@t3x-dev/yops": "0.1.3",
8
- "@t3x-dev/yschema": "0.1.3",
9
- "@t3x-dev/core": "0.1.3",
10
- "@t3x-dev/storage": "0.1.3",
11
- "@t3x-dev/api": "0.1.3",
12
- "@t3x-dev/api-client": "0.1.3",
13
- "@t3x-dev/cli": "0.1.3",
14
- "@t3x-dev/mcp": "0.1.3",
15
- "@t3x-dev/local": "0.1.3",
7
+ "@t3x-dev/yops": "0.1.5",
8
+ "@t3x-dev/yschema": "0.1.5",
9
+ "@t3x-dev/core": "0.1.5",
10
+ "@t3x-dev/storage": "0.1.5",
11
+ "@t3x-dev/api": "0.1.5",
12
+ "@t3x-dev/api-client": "0.1.5",
13
+ "@t3x-dev/cli": "0.1.5",
14
+ "@t3x-dev/mcp": "0.1.5",
15
+ "@t3x-dev/local": "0.1.5",
16
16
  "web": "0.1.0"
17
17
  },
18
18
  "platforms": {
19
19
  "darwin-arm64": {
20
- "fileName": "t3x-local-runtime-0.1.3-darwin-arm64.tar.gz",
21
- "url": "https://github.com/t3x-dev/t3x-core/releases/download/t3x-local-v0.1.3/t3x-local-runtime-0.1.3-darwin-arm64.tar.gz",
22
- "sha256": "1c4194999a3bbde721ad9bc021f3f5e371f3a8a20c34c55a89de80118c220c3b",
23
- "size": 20376214
20
+ "fileName": "t3x-local-runtime-0.1.5-darwin-arm64.tar.gz",
21
+ "url": "https://github.com/t3x-dev/t3x-core/releases/download/t3x-local-v0.1.5/t3x-local-runtime-0.1.5-darwin-arm64.tar.gz",
22
+ "sha256": "943a2f3847d6e7bfcdb288c434059effe3530e104e32ee15fc0ec1bc509e1e1e",
23
+ "size": 20377018
24
24
  }
25
25
  }
26
26
  }
@@ -2,9 +2,13 @@
2
2
 
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import crypto from 'node:crypto';
5
+ import { createWriteStream } from 'node:fs';
5
6
  import fs from 'node:fs/promises';
6
7
  import os from 'node:os';
7
8
  import path from 'node:path';
9
+ import { Readable } from 'node:stream';
10
+ import { pipeline } from 'node:stream/promises';
11
+ import { setTimeout as sleep } from 'node:timers/promises';
8
12
  import {
9
13
  assertRuntimeLayout,
10
14
  ensureDir,
@@ -36,6 +40,11 @@ const LOCAL_DIRECT_FIXED_DEPENDENCIES = [
36
40
  '@t3x-dev/storage',
37
41
  ];
38
42
  const GITHUB_TOKEN_ENV_NAMES = ['T3X_LOCAL_GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN'];
43
+ const DOWNLOAD_MAX_ATTEMPTS = parsePositiveInt(process.env.T3X_LOCAL_DOWNLOAD_ATTEMPTS, 3);
44
+ const DOWNLOAD_RETRY_DELAY_MS = parsePositiveInt(
45
+ process.env.T3X_LOCAL_DOWNLOAD_RETRY_DELAY_MS,
46
+ 3000
47
+ );
39
48
 
40
49
  if (process.env.T3X_LOCAL_SKIP_DOWNLOAD === '1' || process.env.T3X_LOCAL_SKIP_DOWNLOAD === 'true') {
41
50
  console.log(
@@ -132,20 +141,58 @@ async function downloadArtifact(source, destinationPath) {
132
141
  }
133
142
 
134
143
  if (source.startsWith('http://') || source.startsWith('https://')) {
135
- const response = await fetchRuntimeArtifact(source);
144
+ await downloadHttpArtifact(source, destinationPath);
145
+ return;
146
+ }
147
+
148
+ await fs.copyFile(source, destinationPath);
149
+ }
150
+
151
+ async function downloadHttpArtifact(source, destinationPath) {
152
+ let lastError = null;
153
+
154
+ for (let attempt = 1; attempt <= DOWNLOAD_MAX_ATTEMPTS; attempt += 1) {
155
+ try {
156
+ await fs.rm(destinationPath, { force: true });
136
157
 
137
- if (!response.ok) {
138
- throw new Error(
139
- `[t3x-local:postinstall] Failed to download runtime: HTTP ${response.status}${getDownloadHint(source)}`
158
+ const response = await fetchRuntimeArtifact(source);
159
+ if (!response.ok) {
160
+ throw buildHttpDownloadError(source, response.status);
161
+ }
162
+
163
+ await writeResponseBody(response, destinationPath);
164
+ return;
165
+ } catch (error) {
166
+ lastError = error;
167
+
168
+ if (attempt === DOWNLOAD_MAX_ATTEMPTS || !isRetryableDownloadError(error)) {
169
+ throw error;
170
+ }
171
+
172
+ console.log(
173
+ `[t3x-local:postinstall] Runtime download failed (${getErrorSummary(error)}). Retrying ${attempt + 1}/${DOWNLOAD_MAX_ATTEMPTS}...`
140
174
  );
175
+ await sleep(DOWNLOAD_RETRY_DELAY_MS * attempt);
141
176
  }
177
+ }
142
178
 
143
- const arrayBuffer = await response.arrayBuffer();
144
- await fs.writeFile(destinationPath, new Uint8Array(arrayBuffer));
145
- return;
179
+ throw lastError;
180
+ }
181
+
182
+ async function writeResponseBody(response, destinationPath) {
183
+ if (!response.body) {
184
+ throw new Error('[t3x-local:postinstall] Runtime download response did not contain a body');
146
185
  }
147
186
 
148
- await fs.copyFile(source, destinationPath);
187
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(destinationPath));
188
+ }
189
+
190
+ function buildHttpDownloadError(source, status) {
191
+ const error = new Error(
192
+ `[t3x-local:postinstall] Failed to download runtime: HTTP ${status}${getDownloadHint(source)}`
193
+ );
194
+ error.httpStatus = status;
195
+ return error;
149
196
  }
150
197
 
151
198
  async function fetchRuntimeArtifact(source) {
@@ -287,6 +334,35 @@ function isAuthRetryStatus(status) {
287
334
  return status === 403 || status === 404;
288
335
  }
289
336
 
337
+ function isRetryableDownloadError(error) {
338
+ if (isRetryableHttpStatus(error?.httpStatus)) {
339
+ return true;
340
+ }
341
+
342
+ const code = error?.code ?? error?.cause?.code;
343
+ return (
344
+ code === 'ETIMEDOUT' ||
345
+ code === 'ECONNRESET' ||
346
+ code === 'EPIPE' ||
347
+ code === 'UND_ERR_BODY_TIMEOUT' ||
348
+ code === 'UND_ERR_SOCKET' ||
349
+ error instanceof TypeError
350
+ );
351
+ }
352
+
353
+ function isRetryableHttpStatus(status) {
354
+ return status === 408 || status === 429 || (status >= 500 && status <= 599);
355
+ }
356
+
357
+ function getErrorSummary(error) {
358
+ return error?.cause?.code ?? error?.code ?? error?.message ?? String(error);
359
+ }
360
+
361
+ function parsePositiveInt(value, fallbackValue) {
362
+ const parsed = Number.parseInt(value ?? '', 10);
363
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackValue;
364
+ }
365
+
290
366
  async function verifyArchiveSha(archivePath, expectedSha) {
291
367
  const file = await fs.readFile(archivePath);
292
368
  const actualSha = crypto.createHash('sha256').update(file).digest('hex');