shortcutxl 0.2.21 → 0.2.22

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## [0.2.21]
3
+ ## [0.2.22]
4
4
 
5
5
  - **Tool extensions** — Build custom tools that the agent can call.
6
6
  - **Sandbox hardening** — The sandbox now blocks more ways code could escape isolation. When you update ShortcutXL, the sandbox automatically updates itself too.
7
- - **Smaller package** — Removed ~290MB of accidentally bundled Python packages (cv2, pandas, numpy, etc.).
7
+ - **Faster install** — Sandbox is now downloaded on demand instead of bundled.
8
8
 
9
9
  ## [0.2.19]
10
10
 
package/dist/config.js CHANGED
@@ -288,9 +288,19 @@ export function getSessionsDir() {
288
288
  export function getWslDir() {
289
289
  return join(getAgentDir(), 'wsl');
290
290
  }
291
- /** Get path to the shipped Alpine rootfs tarball (bundled in the npm package) */
291
+ /** Get path to the Alpine rootfs tarball. Prefers user-local (downloaded), falls back to package dir (local dev). */
292
292
  export function getAlpineRootfsPath() {
293
- return join(getPackageDir(), 'wsl', 'alpine-minirootfs.tar.gz');
293
+ const userLocal = join(getWslDir(), 'alpine-minirootfs.tar.gz');
294
+ if (existsSync(userLocal))
295
+ return userLocal;
296
+ const shipped = join(getPackageDir(), 'wsl', 'alpine-minirootfs.tar.gz');
297
+ if (existsSync(shipped))
298
+ return shipped;
299
+ return userLocal; // canonical location — will be downloaded
300
+ }
301
+ /** GitHub Release download URL for the Alpine rootfs tarball (hosted on public repo). */
302
+ export function getAlpineRootfsDownloadUrl(version) {
303
+ return `https://github.com/lyfegame/shortcutxl-releases/releases/download/v${version}/alpine-minirootfs.tar.gz`;
294
304
  }
295
305
  /** Get path to debug log file */
296
306
  export function getDebugLogPath() {
@@ -6,8 +6,11 @@
6
6
  * this package has no opinion about where they live.
7
7
  */
8
8
  import { spawn, spawnSync } from 'child_process';
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
10
- import { join } from 'path';
9
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
10
+ import { dirname, join } from 'path';
11
+ import { Readable } from 'stream';
12
+ import { finished } from 'stream/promises';
13
+ import { getAlpineRootfsDownloadUrl } from '../../../config.js';
11
14
  import { log } from '../../../utils/log.js';
12
15
  import { runSandboxSmokeTestAsync } from './smoke.js';
13
16
  import { clearDetectionCache, isDistroInstalled, isWsl2Available } from './wsl-detect.js';
@@ -42,9 +45,15 @@ export async function ensureWslSandbox(options) {
42
45
  }
43
46
  // Fall through to re-import
44
47
  }
45
- // Import rootfs
48
+ // Download rootfs if not present locally, then import
46
49
  if (!existsSync(rootfsPath)) {
47
- return { installed: false, error: `Rootfs not found at ${rootfsPath}` };
50
+ if (!version) {
51
+ return { installed: false, error: `Rootfs not found at ${rootfsPath}` };
52
+ }
53
+ const dlError = await downloadRootfs(rootfsPath, version);
54
+ if (dlError) {
55
+ return { installed: false, error: dlError };
56
+ }
48
57
  }
49
58
  const installDir = join(wslDir, distro);
50
59
  if (!existsSync(installDir)) {
@@ -94,6 +103,54 @@ export function enableWsl2() {
94
103
  }
95
104
  return { success: false, needsReboot: false, error: output || 'Failed to enable WSL2' };
96
105
  }
106
+ /** Download the Alpine rootfs tarball from GitHub Releases. Returns null on success, error string on failure. */
107
+ async function downloadRootfs(rootfsPath, version) {
108
+ if (process.env.SHORTCUT_OFFLINE === '1') {
109
+ return `Rootfs not found at ${rootfsPath} (offline mode — cannot download)`;
110
+ }
111
+ const url = getAlpineRootfsDownloadUrl(version);
112
+ const dir = dirname(rootfsPath);
113
+ mkdirSync(dir, { recursive: true });
114
+ const tmpPath = rootfsPath + '.downloading';
115
+ log.info('sandbox', `Downloading Alpine rootfs from ${url}`);
116
+ try {
117
+ const response = await fetch(url, { signal: AbortSignal.timeout(120_000) });
118
+ if (!response.ok) {
119
+ return `Failed to download rootfs: HTTP ${response.status} from ${url}`;
120
+ }
121
+ if (!response.body) {
122
+ return 'Failed to download rootfs: no response body';
123
+ }
124
+ const totalBytes = Number(response.headers.get('content-length') || 0);
125
+ let receivedBytes = 0;
126
+ let lastLoggedPct = -10;
127
+ const progress = new TransformStream({
128
+ transform(chunk, controller) {
129
+ receivedBytes += chunk.byteLength;
130
+ if (totalBytes > 0) {
131
+ const pct = Math.floor((receivedBytes / totalBytes) * 100);
132
+ if (pct - lastLoggedPct >= 10) {
133
+ lastLoggedPct = pct;
134
+ log.info('sandbox', `Download progress: ${pct}%`);
135
+ }
136
+ }
137
+ controller.enqueue(chunk);
138
+ }
139
+ });
140
+ const fileStream = createWriteStream(tmpPath);
141
+ await finished(Readable.fromWeb(response.body.pipeThrough(progress)).pipe(fileStream));
142
+ renameSync(tmpPath, rootfsPath);
143
+ log.info('sandbox', 'Alpine rootfs downloaded successfully');
144
+ return null;
145
+ }
146
+ catch (err) {
147
+ try {
148
+ unlinkSync(tmpPath);
149
+ }
150
+ catch { /* ignore */ }
151
+ return `Failed to download rootfs: ${err instanceof Error ? err.message : String(err)}`;
152
+ }
153
+ }
97
154
  function isRootfsStale(stampFile, currentVersion) {
98
155
  try {
99
156
  return !existsSync(stampFile) || readFileSync(stampFile, 'utf-8').trim() !== currentVersion;
@@ -7,6 +7,7 @@
7
7
  import { enableWsl2, ensureWslSandbox, isDistroInstalled, isWsl2Available } from './lib/index.js';
8
8
  import chalk from 'chalk';
9
9
  import { execSync } from 'node:child_process';
10
+ import { existsSync } from 'node:fs';
10
11
  import { getAlpineRootfsPath, getSettingsPath, getWslDir, VERSION } from '../../config.js';
11
12
  import { SANDBOX_DISTRO } from './index.js';
12
13
  const WSL_INSTALL_HINT = '\n\n To fix this:\n' +
@@ -37,7 +38,10 @@ export async function handleSandboxCommand(showStatus, select, input, settingsMa
37
38
  return true;
38
39
  }
39
40
  // Step 3: Not installed → set up
40
- showStatus('Setting up sandbox (importing Alpine Linux + bubblewrap)...');
41
+ const rootfsExists = existsSync(getAlpineRootfsPath());
42
+ showStatus(rootfsExists
43
+ ? 'Setting up sandbox (importing Alpine Linux + bubblewrap)...'
44
+ : 'Downloading sandbox (~80 MB) and setting up...');
41
45
  const result = await ensureWslSandbox({
42
46
  distro: SANDBOX_DISTRO,
43
47
  wslDir: getWslDir(),
package/dist/main.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { supportsXhigh } from '@mariozechner/pi-ai';
8
8
  import chalk from 'chalk';
9
9
  import { spawn } from 'child_process';
10
+ import { existsSync } from 'fs';
10
11
  import { createInterface } from 'readline';
11
12
  import { parseArgs, printHelp } from './cli/args.js';
12
13
  import { selectConfig } from './cli/config-selector.js';
@@ -37,7 +38,6 @@ import { EXCEL_HTTP_URL, SHORTCUT_LLM_PROXY_URL } from './custom/constants.js';
37
38
  import { fetchWorkbookSummary, formatSummaryForLlm, parseWorkbookNames } from './custom/context/workbook-summary.js';
38
39
  import { fetchCreditBalance } from './custom/credits/shortcut-credits.js';
39
40
  import { startCronStatusPolling } from './custom/cron/status-line.js';
40
- import { log } from './utils/log.js';
41
41
  import { getTraceHooks } from './custom/dev/index.js';
42
42
  import { runPreflight } from './custom/preflight.js';
43
43
  import { SHORTCUT_PROVIDER_ID } from './custom/providers/provider-ids.js';
@@ -428,20 +428,30 @@ export async function main(args) {
428
428
  let sandboxAvailable = isWsl2Available() && isDistroInstalled(SANDBOX_DISTRO);
429
429
  // Auto-update sandbox rootfs if the package version changed.
430
430
  if (sandboxMode === 'enabled' && sandboxAvailable) {
431
+ const sandboxFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
432
+ let sandboxFrame = 0;
433
+ const needsDownload = !existsSync(getAlpineRootfsPath());
434
+ const spinnerMsg = needsDownload ? 'Downloading sandbox (~80 MB)...' : 'Checking sandbox...';
435
+ const sandboxSpinner = setInterval(() => {
436
+ process.stderr.write(`\r${sandboxFrames[sandboxFrame++ % sandboxFrames.length]} ${spinnerMsg}`);
437
+ }, 80);
431
438
  try {
432
- log.info('sandbox', 'Checking sandbox rootfs version...');
433
439
  const result = await ensureWslSandbox({
434
440
  distro: SANDBOX_DISTRO,
435
441
  wslDir: getWslDir(),
436
442
  rootfsPath: getAlpineRootfsPath(),
437
443
  version: VERSION
438
444
  });
445
+ clearInterval(sandboxSpinner);
446
+ process.stderr.write('\r\x1b[K');
439
447
  if (!result.installed) {
440
448
  sandboxAvailable = false;
441
449
  startupWarnings.push(`Sandbox rootfs update failed: ${result.error}`);
442
450
  }
443
451
  }
444
452
  catch (err) {
453
+ clearInterval(sandboxSpinner);
454
+ process.stderr.write('\r\x1b[K');
445
455
  sandboxAvailable = false;
446
456
  startupWarnings.push(`Sandbox check failed: ${err instanceof Error ? err.message : String(err)}`);
447
457
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shortcutxl",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -14,7 +14,6 @@
14
14
  "skills/",
15
15
  "agent-docs/",
16
16
  "xll/",
17
- "wsl/",
18
17
  "CHANGELOG.md"
19
18
  ],
20
19
  "shortcutConfig": {
@@ -27,7 +26,7 @@
27
26
  "scripts": {
28
27
  "build": "tsc -b && npm run copy-assets",
29
28
  "vendor-alpine": "powershell -File ../build-scripts/vendor-alpine.ps1",
30
- "prepublishOnly": "npm run vendor-alpine && npm run build",
29
+ "prepublishOnly": "npm run build",
31
30
  "sync-modules": "node -e \"require('fs').cpSync('../modules/shortcut_xl','xll/modules/shortcut_xl',{recursive:true,filter:s=>!s.includes('__pycache__')})\"",
32
31
  "copy-assets": "npm run sync-modules && cp -r src/tui dist/tui && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && cp src/core/export-html/*.{html,css,js} dist/core/export-html/ && cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && cp ../../os/docs/generated/api-reference.json agent-docs/api-reference.json",
33
32
  "test-install": "npm uninstall -g shortcutxl & npm run build && npm pack && node -e \"const g=require('glob').globSync('shortcutxl-*.tgz')[0];require('child_process').execSync('npm install -g '+g,{stdio:'inherit'});require('fs').unlinkSync(g)\"",
@@ -42,7 +41,7 @@
42
41
  "format:check": "prettier --check 'src/**/*.{ts,tsx,js,json}'",
43
42
  "test": "vitest --run",
44
43
  "test:e2e": "vitest --run --config vitest.e2e.config.ts",
45
- "release": "node -e \"const v=require('./package.json').version;const t='shortcutxl/v'+v;require('child_process').execSync('git tag '+t,{stdio:'inherit'});require('child_process').execSync('git push origin '+t,{stdio:'inherit'});console.log('Tagged and pushed '+t)\"",
44
+ "release": "node scripts/release.js",
46
45
  "test:watch": "vitest",
47
46
  "knip": "knip",
48
47
  "lint:python": "cd .. && uvx ruff check modules/ tests/",
Binary file