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.
|
|
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
|
-
- **
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
48
|
+
// Download rootfs if not present locally, then import
|
|
46
49
|
if (!existsSync(rootfsPath)) {
|
|
47
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|