runwork 0.25.0 → 0.25.1
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/bundled-types/core-routes.d.ts +25 -0
- package/dist/index.js +321 -142
- package/package.json +1 -1
|
@@ -24,6 +24,31 @@ export interface ClientErrorReport {
|
|
|
24
24
|
level?: 'error' | 'warning' | 'info';
|
|
25
25
|
category?: string;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Decide whether to send the permissive `Permissions-Policy` header for an app
|
|
29
|
+
* that is being embedded in an iframe.
|
|
30
|
+
*
|
|
31
|
+
* A document's own Permissions-Policy can only restrict, never grant: the grant
|
|
32
|
+
* comes from the embedding page's policy plus the iframe `allow` attribute. This
|
|
33
|
+
* header's job is therefore to stop the app self-restricting the features the
|
|
34
|
+
* platform preview delegates to it, and this predicate decides who is trusted
|
|
35
|
+
* enough to be handed that.
|
|
36
|
+
*
|
|
37
|
+
* Hostnames are parsed and compared exactly, never substring-matched against the
|
|
38
|
+
* raw header. The difference is not cosmetic: `origin.endsWith('runwork.ai')`
|
|
39
|
+
* also matches `https://notrunwork.ai`, and `origin.includes(domain)` matches
|
|
40
|
+
* `https://evil-acme.com.attacker.net` against a tenant's `acme.com`, which would
|
|
41
|
+
* hand an attacker's page camera, microphone, geolocation and screen capture over
|
|
42
|
+
* an embedded app.
|
|
43
|
+
*
|
|
44
|
+
* @param origin The `Origin` header, or the `Referer` as a fallback. Origin is a
|
|
45
|
+
* bare origin and Referer is a full URL; both parse the same way.
|
|
46
|
+
* Empty means direct (non-embedded) access.
|
|
47
|
+
* @param allowedOrigins Comma-separated exact hostnames from `ALLOWED_ORIGINS`,
|
|
48
|
+
* injected by the platform as the workspace's active custom
|
|
49
|
+
* domains. Matched exactly; subdomains are deliberately excluded.
|
|
50
|
+
*/
|
|
51
|
+
export declare function shouldGrantIframePermissions(origin: string, allowedOrigins?: string): boolean;
|
|
27
52
|
/**
|
|
28
53
|
* Mount all core platform routes on the Hono app
|
|
29
54
|
* Called from index.ts before user-defined routes
|
package/dist/index.js
CHANGED
|
@@ -1380,8 +1380,154 @@ var init_identity = __esm(() => {
|
|
|
1380
1380
|
init_subprocess();
|
|
1381
1381
|
});
|
|
1382
1382
|
|
|
1383
|
+
// src/utils/ignore-matcher.ts
|
|
1384
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1385
|
+
import { basename, join as join4 } from "path";
|
|
1386
|
+
function defaultIgnoreSets() {
|
|
1387
|
+
return {
|
|
1388
|
+
dirs: new Set(DEFAULT_DIR_NAMES),
|
|
1389
|
+
files: new Set(DEFAULT_FILE_NAMES),
|
|
1390
|
+
extensions: new Set(DEFAULT_EXTENSIONS)
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function parseGitignoreContent(content) {
|
|
1394
|
+
const dirs = new Set;
|
|
1395
|
+
const files = new Set;
|
|
1396
|
+
for (const rawLine of content.split(`
|
|
1397
|
+
`)) {
|
|
1398
|
+
const line = rawLine.trim();
|
|
1399
|
+
if (!line || line.startsWith("#"))
|
|
1400
|
+
continue;
|
|
1401
|
+
if (line.startsWith("!"))
|
|
1402
|
+
continue;
|
|
1403
|
+
if (GLOB_CHARS.test(line))
|
|
1404
|
+
continue;
|
|
1405
|
+
let pattern = line;
|
|
1406
|
+
const isDirOnly = pattern.endsWith("/");
|
|
1407
|
+
if (isDirOnly)
|
|
1408
|
+
pattern = pattern.slice(0, -1);
|
|
1409
|
+
if (pattern.startsWith("/"))
|
|
1410
|
+
pattern = pattern.slice(1);
|
|
1411
|
+
if (!pattern)
|
|
1412
|
+
continue;
|
|
1413
|
+
if (pattern.includes("/"))
|
|
1414
|
+
continue;
|
|
1415
|
+
if (isDirOnly) {
|
|
1416
|
+
dirs.add(pattern);
|
|
1417
|
+
} else {
|
|
1418
|
+
dirs.add(pattern);
|
|
1419
|
+
files.add(pattern);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return { dirs, files };
|
|
1423
|
+
}
|
|
1424
|
+
function loadGitignoreFromDir(dir) {
|
|
1425
|
+
const path = join4(dir, ".gitignore");
|
|
1426
|
+
if (!existsSync4(path))
|
|
1427
|
+
return { dirs: new Set, files: new Set };
|
|
1428
|
+
try {
|
|
1429
|
+
return parseGitignoreContent(readFileSync4(path, "utf-8"));
|
|
1430
|
+
} catch {
|
|
1431
|
+
return { dirs: new Set, files: new Set };
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
function buildIgnoreSets(dir) {
|
|
1435
|
+
const sets = defaultIgnoreSets();
|
|
1436
|
+
const parsed = loadGitignoreFromDir(dir);
|
|
1437
|
+
for (const name of parsed.dirs)
|
|
1438
|
+
sets.dirs.add(name);
|
|
1439
|
+
for (const name of parsed.files)
|
|
1440
|
+
sets.files.add(name);
|
|
1441
|
+
return sets;
|
|
1442
|
+
}
|
|
1443
|
+
function isPathIgnored(filePath, sets) {
|
|
1444
|
+
const name = basename(filePath);
|
|
1445
|
+
if (sets.dirs.has(name))
|
|
1446
|
+
return true;
|
|
1447
|
+
if (sets.files.has(name))
|
|
1448
|
+
return true;
|
|
1449
|
+
const dotIndex = name.lastIndexOf(".");
|
|
1450
|
+
if (dotIndex >= 0) {
|
|
1451
|
+
const ext = name.slice(dotIndex);
|
|
1452
|
+
if (sets.extensions.has(ext))
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1457
|
+
function isRelPathIgnored(relPath, sets) {
|
|
1458
|
+
const segments = relPath.split("/");
|
|
1459
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
1460
|
+
if (sets.dirs.has(segments[i]))
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
return isPathIgnored(relPath, sets);
|
|
1464
|
+
}
|
|
1465
|
+
var ALWAYS_IGNORED_DIRS, DEFAULT_DIR_NAMES, DEFAULT_FILE_NAMES, DEFAULT_EXTENSIONS, GLOB_CHARS;
|
|
1466
|
+
var init_ignore_matcher = __esm(() => {
|
|
1467
|
+
ALWAYS_IGNORED_DIRS = [
|
|
1468
|
+
".git",
|
|
1469
|
+
"node_modules",
|
|
1470
|
+
".runwork",
|
|
1471
|
+
".bun-cache",
|
|
1472
|
+
".npm-cache",
|
|
1473
|
+
".pnpm-store",
|
|
1474
|
+
".turbo",
|
|
1475
|
+
".vite",
|
|
1476
|
+
".cache",
|
|
1477
|
+
"coverage",
|
|
1478
|
+
"dist"
|
|
1479
|
+
];
|
|
1480
|
+
DEFAULT_DIR_NAMES = ALWAYS_IGNORED_DIRS;
|
|
1481
|
+
DEFAULT_FILE_NAMES = [
|
|
1482
|
+
".dev.vars",
|
|
1483
|
+
".env"
|
|
1484
|
+
];
|
|
1485
|
+
DEFAULT_EXTENSIONS = [
|
|
1486
|
+
".log"
|
|
1487
|
+
];
|
|
1488
|
+
GLOB_CHARS = /[*?\[\]]/;
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
// src/git/repo-config.ts
|
|
1492
|
+
function hardenRepoForRestrictedFs(cwd) {
|
|
1493
|
+
for (const [key, value] of HARDENING) {
|
|
1494
|
+
try {
|
|
1495
|
+
execFileSync("git", ["config", key, value], { cwd, stdio: "pipe" });
|
|
1496
|
+
} catch {}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function buildInitialGitignore() {
|
|
1500
|
+
const dirs = ALWAYS_IGNORED_DIRS.filter((dir) => dir !== ".git").map((dir) => `${dir}/`);
|
|
1501
|
+
return [
|
|
1502
|
+
"# Dependencies, caches and build output",
|
|
1503
|
+
...dirs,
|
|
1504
|
+
"",
|
|
1505
|
+
"# Local secrets. Never commit these.",
|
|
1506
|
+
".dev.vars",
|
|
1507
|
+
".dev.vars*",
|
|
1508
|
+
".env",
|
|
1509
|
+
".env.*",
|
|
1510
|
+
"",
|
|
1511
|
+
"# Logs and tool state",
|
|
1512
|
+
"*.log",
|
|
1513
|
+
"*.tsbuildinfo",
|
|
1514
|
+
".eslintcache",
|
|
1515
|
+
""
|
|
1516
|
+
].join(`
|
|
1517
|
+
`);
|
|
1518
|
+
}
|
|
1519
|
+
var HARDENING;
|
|
1520
|
+
var init_repo_config = __esm(() => {
|
|
1521
|
+
init_subprocess();
|
|
1522
|
+
init_ignore_matcher();
|
|
1523
|
+
HARDENING = [
|
|
1524
|
+
["gc.auto", "0"],
|
|
1525
|
+
["maintenance.auto", "false"]
|
|
1526
|
+
];
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1383
1529
|
// src/git/preflight.ts
|
|
1384
|
-
import { existsSync as
|
|
1530
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1385
1531
|
import { win32 as winPath } from "path";
|
|
1386
1532
|
import { homedir as homedir3 } from "os";
|
|
1387
1533
|
function tryRun(bin) {
|
|
@@ -1403,9 +1549,9 @@ function whereGit() {
|
|
|
1403
1549
|
const out = buf.toString("utf-8");
|
|
1404
1550
|
const lines = out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1405
1551
|
const exe = lines.find((line) => /\.exe$/i.test(line));
|
|
1406
|
-
if (exe &&
|
|
1552
|
+
if (exe && existsSync5(exe))
|
|
1407
1553
|
return exe;
|
|
1408
|
-
const fallback = lines.find((line) =>
|
|
1554
|
+
const fallback = lines.find((line) => existsSync5(line));
|
|
1409
1555
|
return fallback ?? null;
|
|
1410
1556
|
} catch {
|
|
1411
1557
|
return null;
|
|
@@ -1430,7 +1576,7 @@ function registryGit() {
|
|
|
1430
1576
|
continue;
|
|
1431
1577
|
const installRoot = match[1].trim();
|
|
1432
1578
|
const gitExe = winPath.join(installRoot, "cmd", "git.exe");
|
|
1433
|
-
if (
|
|
1579
|
+
if (existsSync5(gitExe))
|
|
1434
1580
|
return gitExe;
|
|
1435
1581
|
} catch {}
|
|
1436
1582
|
}
|
|
@@ -1498,7 +1644,7 @@ function probeGit() {
|
|
|
1498
1644
|
}
|
|
1499
1645
|
}
|
|
1500
1646
|
for (const candidate of canonicalGitCandidates()) {
|
|
1501
|
-
if (!
|
|
1647
|
+
if (!existsSync5(candidate))
|
|
1502
1648
|
continue;
|
|
1503
1649
|
const verify = tryRun(candidate);
|
|
1504
1650
|
if (verify.ok) {
|
|
@@ -1636,106 +1782,6 @@ async function resolveApp(client, nameOrId, workspaceId) {
|
|
|
1636
1782
|
process.exit(1);
|
|
1637
1783
|
}
|
|
1638
1784
|
|
|
1639
|
-
// src/utils/ignore-matcher.ts
|
|
1640
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1641
|
-
import { basename, join as join4 } from "path";
|
|
1642
|
-
function defaultIgnoreSets() {
|
|
1643
|
-
return {
|
|
1644
|
-
dirs: new Set(DEFAULT_DIR_NAMES),
|
|
1645
|
-
files: new Set(DEFAULT_FILE_NAMES),
|
|
1646
|
-
extensions: new Set(DEFAULT_EXTENSIONS)
|
|
1647
|
-
};
|
|
1648
|
-
}
|
|
1649
|
-
function parseGitignoreContent(content) {
|
|
1650
|
-
const dirs = new Set;
|
|
1651
|
-
const files = new Set;
|
|
1652
|
-
for (const rawLine of content.split(`
|
|
1653
|
-
`)) {
|
|
1654
|
-
const line = rawLine.trim();
|
|
1655
|
-
if (!line || line.startsWith("#"))
|
|
1656
|
-
continue;
|
|
1657
|
-
if (line.startsWith("!"))
|
|
1658
|
-
continue;
|
|
1659
|
-
if (GLOB_CHARS.test(line))
|
|
1660
|
-
continue;
|
|
1661
|
-
let pattern = line;
|
|
1662
|
-
const isDirOnly = pattern.endsWith("/");
|
|
1663
|
-
if (isDirOnly)
|
|
1664
|
-
pattern = pattern.slice(0, -1);
|
|
1665
|
-
if (pattern.startsWith("/"))
|
|
1666
|
-
pattern = pattern.slice(1);
|
|
1667
|
-
if (!pattern)
|
|
1668
|
-
continue;
|
|
1669
|
-
if (pattern.includes("/"))
|
|
1670
|
-
continue;
|
|
1671
|
-
if (isDirOnly) {
|
|
1672
|
-
dirs.add(pattern);
|
|
1673
|
-
} else {
|
|
1674
|
-
dirs.add(pattern);
|
|
1675
|
-
files.add(pattern);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
return { dirs, files };
|
|
1679
|
-
}
|
|
1680
|
-
function loadGitignoreFromDir(dir) {
|
|
1681
|
-
const path = join4(dir, ".gitignore");
|
|
1682
|
-
if (!existsSync5(path))
|
|
1683
|
-
return { dirs: new Set, files: new Set };
|
|
1684
|
-
try {
|
|
1685
|
-
return parseGitignoreContent(readFileSync4(path, "utf-8"));
|
|
1686
|
-
} catch {
|
|
1687
|
-
return { dirs: new Set, files: new Set };
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
function buildIgnoreSets(dir) {
|
|
1691
|
-
const sets = defaultIgnoreSets();
|
|
1692
|
-
const parsed = loadGitignoreFromDir(dir);
|
|
1693
|
-
for (const name of parsed.dirs)
|
|
1694
|
-
sets.dirs.add(name);
|
|
1695
|
-
for (const name of parsed.files)
|
|
1696
|
-
sets.files.add(name);
|
|
1697
|
-
return sets;
|
|
1698
|
-
}
|
|
1699
|
-
function isPathIgnored(filePath, sets) {
|
|
1700
|
-
const name = basename(filePath);
|
|
1701
|
-
if (sets.dirs.has(name))
|
|
1702
|
-
return true;
|
|
1703
|
-
if (sets.files.has(name))
|
|
1704
|
-
return true;
|
|
1705
|
-
const dotIndex = name.lastIndexOf(".");
|
|
1706
|
-
if (dotIndex >= 0) {
|
|
1707
|
-
const ext = name.slice(dotIndex);
|
|
1708
|
-
if (sets.extensions.has(ext))
|
|
1709
|
-
return true;
|
|
1710
|
-
}
|
|
1711
|
-
return false;
|
|
1712
|
-
}
|
|
1713
|
-
function isRelPathIgnored(relPath, sets) {
|
|
1714
|
-
const segments = relPath.split("/");
|
|
1715
|
-
for (let i = 0;i < segments.length - 1; i++) {
|
|
1716
|
-
if (sets.dirs.has(segments[i]))
|
|
1717
|
-
return true;
|
|
1718
|
-
}
|
|
1719
|
-
return isPathIgnored(relPath, sets);
|
|
1720
|
-
}
|
|
1721
|
-
var DEFAULT_DIR_NAMES, DEFAULT_FILE_NAMES, DEFAULT_EXTENSIONS, GLOB_CHARS;
|
|
1722
|
-
var init_ignore_matcher = __esm(() => {
|
|
1723
|
-
DEFAULT_DIR_NAMES = [
|
|
1724
|
-
".git",
|
|
1725
|
-
"node_modules",
|
|
1726
|
-
".runwork",
|
|
1727
|
-
".bun-cache"
|
|
1728
|
-
];
|
|
1729
|
-
DEFAULT_FILE_NAMES = [
|
|
1730
|
-
".dev.vars",
|
|
1731
|
-
".env"
|
|
1732
|
-
];
|
|
1733
|
-
DEFAULT_EXTENSIONS = [
|
|
1734
|
-
".log"
|
|
1735
|
-
];
|
|
1736
|
-
GLOB_CHARS = /[*?\[\]]/;
|
|
1737
|
-
});
|
|
1738
|
-
|
|
1739
1785
|
// src/template/manifest.ts
|
|
1740
1786
|
import { createHash } from "crypto";
|
|
1741
1787
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, readdirSync, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -1782,17 +1828,39 @@ async function saveManifest(dir, manifest) {
|
|
|
1782
1828
|
}
|
|
1783
1829
|
writeFileSync2(join5(manifestDir, "template-manifest.json"), JSON.stringify(manifest, null, 2));
|
|
1784
1830
|
}
|
|
1831
|
+
function loadManifestSync(dir) {
|
|
1832
|
+
const manifestPath = join5(dir, ".runwork", "template-manifest.json");
|
|
1833
|
+
if (!existsSync6(manifestPath))
|
|
1834
|
+
return null;
|
|
1835
|
+
try {
|
|
1836
|
+
return normalizeManifest(JSON.parse(readFileSync5(manifestPath, "utf-8")));
|
|
1837
|
+
} catch {
|
|
1838
|
+
return null;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function isPristineTemplateFile(dir, relPath, manifest) {
|
|
1842
|
+
const expectedHash = manifest?.files[relPath];
|
|
1843
|
+
if (!expectedHash)
|
|
1844
|
+
return false;
|
|
1845
|
+
try {
|
|
1846
|
+
return sha256(readFileSync5(join5(dir, relPath))) === expectedHash;
|
|
1847
|
+
} catch {
|
|
1848
|
+
return false;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function normalizeManifest(manifest) {
|
|
1852
|
+
const files = {};
|
|
1853
|
+
for (const [relPath, hash] of Object.entries(manifest.files)) {
|
|
1854
|
+
files[relPath.split("\\").join("/")] = hash;
|
|
1855
|
+
}
|
|
1856
|
+
return { ...manifest, files };
|
|
1857
|
+
}
|
|
1785
1858
|
async function loadManifest(dir) {
|
|
1786
1859
|
const manifestPath = join5(dir, ".runwork", "template-manifest.json");
|
|
1787
1860
|
if (!existsSync6(manifestPath))
|
|
1788
1861
|
return null;
|
|
1789
1862
|
try {
|
|
1790
|
-
|
|
1791
|
-
const files = {};
|
|
1792
|
-
for (const [relPath, hash] of Object.entries(manifest.files)) {
|
|
1793
|
-
files[relPath.split("\\").join("/")] = hash;
|
|
1794
|
-
}
|
|
1795
|
-
return { ...manifest, files };
|
|
1863
|
+
return normalizeManifest(JSON.parse(readFileSync5(manifestPath, "utf-8")));
|
|
1796
1864
|
} catch {
|
|
1797
1865
|
return null;
|
|
1798
1866
|
}
|
|
@@ -2251,10 +2319,7 @@ async function execInit(client, appName, workspace, options = {}, creds) {
|
|
|
2251
2319
|
execFileSync("git", ["remote", "set-url", "runwork", remoteUrl], { cwd: dir, stdio: "pipe" });
|
|
2252
2320
|
}
|
|
2253
2321
|
if (!existsSync7(join8(dir, ".gitignore"))) {
|
|
2254
|
-
writeFileSync4(join8(dir, ".gitignore"),
|
|
2255
|
-
.runwork/
|
|
2256
|
-
.dev.vars
|
|
2257
|
-
`);
|
|
2322
|
+
writeFileSync4(join8(dir, ".gitignore"), buildInitialGitignore());
|
|
2258
2323
|
}
|
|
2259
2324
|
execFileSync("git", ["add", "--", ".runwork.json", ".gitignore"], { cwd: dir, stdio: "pipe" });
|
|
2260
2325
|
try {
|
|
@@ -2335,6 +2400,7 @@ var init_init = __esm(() => {
|
|
|
2335
2400
|
init_store();
|
|
2336
2401
|
init_client();
|
|
2337
2402
|
init_identity();
|
|
2403
|
+
init_repo_config();
|
|
2338
2404
|
init_preflight();
|
|
2339
2405
|
init_prompt();
|
|
2340
2406
|
init_manifest();
|
|
@@ -2396,23 +2462,6 @@ var init_remote = __esm(() => {
|
|
|
2396
2462
|
init_subprocess();
|
|
2397
2463
|
});
|
|
2398
2464
|
|
|
2399
|
-
// src/git/repo-config.ts
|
|
2400
|
-
function hardenRepoForRestrictedFs(cwd) {
|
|
2401
|
-
for (const [key, value] of HARDENING) {
|
|
2402
|
-
try {
|
|
2403
|
-
execFileSync("git", ["config", key, value], { cwd, stdio: "pipe" });
|
|
2404
|
-
} catch {}
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
var HARDENING;
|
|
2408
|
-
var init_repo_config = __esm(() => {
|
|
2409
|
-
init_subprocess();
|
|
2410
|
-
HARDENING = [
|
|
2411
|
-
["gc.auto", "0"],
|
|
2412
|
-
["maintenance.auto", "false"]
|
|
2413
|
-
];
|
|
2414
|
-
});
|
|
2415
|
-
|
|
2416
2465
|
// src/git/classify-sync-error.ts
|
|
2417
2466
|
function classifySyncError(raw) {
|
|
2418
2467
|
if (!raw)
|
|
@@ -2420,6 +2469,9 @@ function classifySyncError(raw) {
|
|
|
2420
2469
|
const s = raw.toLowerCase();
|
|
2421
2470
|
if (raw === STASH_CONFLICT)
|
|
2422
2471
|
return "conflict";
|
|
2472
|
+
if (s.includes("[remote rejected]") || s.includes("[rejected]") || s.includes("pre-receive hook declined") || s.includes("push declined")) {
|
|
2473
|
+
return "rejected";
|
|
2474
|
+
}
|
|
2423
2475
|
if (s.includes("does not appear to be a git repository") || s.includes("'runwork' does not appear") || s.includes("no such remote") || s.includes("remote") && s.includes("not found")) {
|
|
2424
2476
|
return "no-remote";
|
|
2425
2477
|
}
|
|
@@ -2467,6 +2519,19 @@ function diagnoseSyncError(raw) {
|
|
|
2467
2519
|
"Confirm outbound access to runwork.ai, then run `runwork dev` again"
|
|
2468
2520
|
]
|
|
2469
2521
|
};
|
|
2522
|
+
case "rejected":
|
|
2523
|
+
return {
|
|
2524
|
+
reason,
|
|
2525
|
+
message: "the server refused the push",
|
|
2526
|
+
diagnosis: `The runwork remote rejected this push, so nothing was updated on the server. It said:
|
|
2527
|
+
${(raw ?? "").trim()}`,
|
|
2528
|
+
suggestions: [
|
|
2529
|
+
'Read the "remote:" lines above: they name the exact reason and the fix',
|
|
2530
|
+
"If an oversized file was rejected, remove it from the commit and add it to .gitignore",
|
|
2531
|
+
"If the history is reported as damaged, contact support to rebuild it",
|
|
2532
|
+
"Retrying without changing anything will fail the same way"
|
|
2533
|
+
]
|
|
2534
|
+
};
|
|
2470
2535
|
case "conflict":
|
|
2471
2536
|
return {
|
|
2472
2537
|
reason,
|
|
@@ -2942,26 +3007,61 @@ function hasTrackedChanges(cwd) {
|
|
|
2942
3007
|
}
|
|
2943
3008
|
}
|
|
2944
3009
|
function removeConflictingUntrackedFiles(cwd) {
|
|
3010
|
+
const kept = [];
|
|
2945
3011
|
try {
|
|
2946
3012
|
const remoteFiles = execFileSync("git", ["ls-tree", "-r", "--name-only", "runwork/main"], {
|
|
2947
3013
|
cwd,
|
|
2948
3014
|
encoding: "utf-8"
|
|
2949
3015
|
}).trim().split(`
|
|
2950
3016
|
`);
|
|
2951
|
-
const untrackedOutput = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
3017
|
+
const untrackedOutput = execFileSync("git", ["-c", "core.quotePath=false", "ls-files", "--others", "--exclude-standard"], {
|
|
2952
3018
|
cwd,
|
|
2953
3019
|
encoding: "utf-8"
|
|
2954
3020
|
}).trim();
|
|
2955
3021
|
const untracked = new Set(untrackedOutput.split(`
|
|
2956
3022
|
`).filter(Boolean));
|
|
3023
|
+
const manifest = loadManifestSync(cwd);
|
|
2957
3024
|
for (const file of remoteFiles) {
|
|
2958
|
-
if (untracked.has(file))
|
|
3025
|
+
if (!untracked.has(file))
|
|
3026
|
+
continue;
|
|
3027
|
+
if (matchesRemote(cwd, file) || isPristineTemplateFile(cwd, file, manifest)) {
|
|
2959
3028
|
try {
|
|
2960
3029
|
unlinkSync2(join11(cwd, file));
|
|
2961
3030
|
} catch {}
|
|
3031
|
+
} else {
|
|
3032
|
+
kept.push(file);
|
|
2962
3033
|
}
|
|
2963
3034
|
}
|
|
2964
3035
|
} catch {}
|
|
3036
|
+
return { kept };
|
|
3037
|
+
}
|
|
3038
|
+
function matchesRemote(cwd, file) {
|
|
3039
|
+
try {
|
|
3040
|
+
const remoteOid = execFileSync("git", ["rev-parse", `runwork/main:${file}`], {
|
|
3041
|
+
cwd,
|
|
3042
|
+
encoding: "utf-8",
|
|
3043
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3044
|
+
}).trim();
|
|
3045
|
+
const localOid = execFileSync("git", ["hash-object", "--", file], {
|
|
3046
|
+
cwd,
|
|
3047
|
+
encoding: "utf-8",
|
|
3048
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3049
|
+
}).trim();
|
|
3050
|
+
return Boolean(remoteOid) && remoteOid === localOid;
|
|
3051
|
+
} catch {
|
|
3052
|
+
return false;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
function conflictedPaths(cwd) {
|
|
3056
|
+
try {
|
|
3057
|
+
return execFileSync("git", ["-c", "core.quotePath=false", "diff", "--name-only", "--diff-filter=U"], {
|
|
3058
|
+
cwd,
|
|
3059
|
+
encoding: "utf-8"
|
|
3060
|
+
}).trim().split(`
|
|
3061
|
+
`).filter(Boolean);
|
|
3062
|
+
} catch {
|
|
3063
|
+
return [];
|
|
3064
|
+
}
|
|
2965
3065
|
}
|
|
2966
3066
|
function extractGitError(err) {
|
|
2967
3067
|
if (err && typeof err === "object") {
|
|
@@ -2996,9 +3096,11 @@ function syncWithRemote(cwd) {
|
|
|
2996
3096
|
}
|
|
2997
3097
|
let status = "synced";
|
|
2998
3098
|
let syncError;
|
|
3099
|
+
let keptUntracked = [];
|
|
3100
|
+
let remoteOverwrote = [];
|
|
2999
3101
|
try {
|
|
3000
3102
|
execFileSync("git", ["fetch", "runwork", "main"], { cwd, stdio: "pipe" });
|
|
3001
|
-
removeConflictingUntrackedFiles(cwd);
|
|
3103
|
+
keptUntracked = removeConflictingUntrackedFiles(cwd).kept;
|
|
3002
3104
|
try {
|
|
3003
3105
|
execFileSync("git", ["rebase", "runwork/main"], { cwd, stdio: "pipe" });
|
|
3004
3106
|
} catch (rebaseErr) {
|
|
@@ -3009,6 +3111,7 @@ function syncWithRemote(cwd) {
|
|
|
3009
3111
|
execFileSync("git", ["merge", "runwork/main", "--allow-unrelated-histories", "--no-edit"], { cwd, stdio: "pipe" });
|
|
3010
3112
|
status = "merged";
|
|
3011
3113
|
} catch {
|
|
3114
|
+
remoteOverwrote = conflictedPaths(cwd);
|
|
3012
3115
|
try {
|
|
3013
3116
|
execFileSync("git", ["merge", "--abort"], { cwd, stdio: "pipe" });
|
|
3014
3117
|
} catch {}
|
|
@@ -3019,6 +3122,7 @@ function syncWithRemote(cwd) {
|
|
|
3019
3122
|
try {
|
|
3020
3123
|
execFileSync("git", ["merge", "--abort"], { cwd, stdio: "pipe" });
|
|
3021
3124
|
} catch {}
|
|
3125
|
+
remoteOverwrote = [];
|
|
3022
3126
|
status = "sync-failed";
|
|
3023
3127
|
syncError = extractGitError(mergeErr);
|
|
3024
3128
|
}
|
|
@@ -3032,21 +3136,25 @@ function syncWithRemote(cwd) {
|
|
|
3032
3136
|
try {
|
|
3033
3137
|
execFileSync("git", ["stash", "pop"], { cwd, stdio: "pipe" });
|
|
3034
3138
|
} catch {
|
|
3035
|
-
return { status, pushed: false, error: "stash-conflict" };
|
|
3139
|
+
return { status, pushed: false, error: "stash-conflict", keptUntracked, remoteOverwrote };
|
|
3036
3140
|
}
|
|
3037
3141
|
}
|
|
3038
3142
|
if (status === "sync-failed") {
|
|
3039
|
-
return { status, pushed: false, error: syncError };
|
|
3143
|
+
return { status, pushed: false, error: syncError, keptUntracked, remoteOverwrote };
|
|
3040
3144
|
}
|
|
3041
3145
|
let pushed = false;
|
|
3146
|
+
let pushError;
|
|
3042
3147
|
try {
|
|
3043
3148
|
execFileSync("git", ["push", "runwork", "HEAD:main"], { cwd, stdio: "pipe" });
|
|
3044
3149
|
pushed = true;
|
|
3045
|
-
} catch {
|
|
3046
|
-
|
|
3150
|
+
} catch (pushErr) {
|
|
3151
|
+
pushError = extractGitError(pushErr);
|
|
3152
|
+
}
|
|
3153
|
+
return { status, pushed, pushError, keptUntracked, remoteOverwrote };
|
|
3047
3154
|
}
|
|
3048
3155
|
var init_sync = __esm(() => {
|
|
3049
3156
|
init_subprocess();
|
|
3157
|
+
init_manifest();
|
|
3050
3158
|
});
|
|
3051
3159
|
|
|
3052
3160
|
// src/git/critical-files.ts
|
|
@@ -4982,6 +5090,31 @@ export interface ClientErrorReport {
|
|
|
4982
5090
|
level?: 'error' | 'warning' | 'info';
|
|
4983
5091
|
category?: string;
|
|
4984
5092
|
}
|
|
5093
|
+
/**
|
|
5094
|
+
* Decide whether to send the permissive \`Permissions-Policy\` header for an app
|
|
5095
|
+
* that is being embedded in an iframe.
|
|
5096
|
+
*
|
|
5097
|
+
* A document's own Permissions-Policy can only restrict, never grant: the grant
|
|
5098
|
+
* comes from the embedding page's policy plus the iframe \`allow\` attribute. This
|
|
5099
|
+
* header's job is therefore to stop the app self-restricting the features the
|
|
5100
|
+
* platform preview delegates to it, and this predicate decides who is trusted
|
|
5101
|
+
* enough to be handed that.
|
|
5102
|
+
*
|
|
5103
|
+
* Hostnames are parsed and compared exactly, never substring-matched against the
|
|
5104
|
+
* raw header. The difference is not cosmetic: \`origin.endsWith('runwork.ai')\`
|
|
5105
|
+
* also matches \`https://notrunwork.ai\`, and \`origin.includes(domain)\` matches
|
|
5106
|
+
* \`https://evil-acme.com.attacker.net\` against a tenant's \`acme.com\`, which would
|
|
5107
|
+
* hand an attacker's page camera, microphone, geolocation and screen capture over
|
|
5108
|
+
* an embedded app.
|
|
5109
|
+
*
|
|
5110
|
+
* @param origin The \`Origin\` header, or the \`Referer\` as a fallback. Origin is a
|
|
5111
|
+
* bare origin and Referer is a full URL; both parse the same way.
|
|
5112
|
+
* Empty means direct (non-embedded) access.
|
|
5113
|
+
* @param allowedOrigins Comma-separated exact hostnames from \`ALLOWED_ORIGINS\`,
|
|
5114
|
+
* injected by the platform as the workspace's active custom
|
|
5115
|
+
* domains. Matched exactly; subdomains are deliberately excluded.
|
|
5116
|
+
*/
|
|
5117
|
+
export declare function shouldGrantIframePermissions(origin: string, allowedOrigins?: string): boolean;
|
|
4985
5118
|
/**
|
|
4986
5119
|
* Mount all core platform routes on the Hono app
|
|
4987
5120
|
* Called from index.ts before user-defined routes
|
|
@@ -7840,7 +7973,7 @@ function createKeyboardListener() {
|
|
|
7840
7973
|
}
|
|
7841
7974
|
|
|
7842
7975
|
// src/generated/version.ts
|
|
7843
|
-
var VERSION = "0.25.
|
|
7976
|
+
var VERSION = "0.25.1";
|
|
7844
7977
|
|
|
7845
7978
|
// src/commands/dev.ts
|
|
7846
7979
|
var exports_dev = {};
|
|
@@ -8068,6 +8201,36 @@ async function execDev(options) {
|
|
|
8068
8201
|
}
|
|
8069
8202
|
}
|
|
8070
8203
|
}
|
|
8204
|
+
if (syncResult.remoteOverwrote && syncResult.remoteOverwrote.length > 0) {
|
|
8205
|
+
if (useJson) {
|
|
8206
|
+
jsonLine({ event: "sync_remote_overwrote", files: syncResult.remoteOverwrote, timestamp: ts() });
|
|
8207
|
+
} else {
|
|
8208
|
+
console.warn(yellow(` Sync resolved conflicts in the remote's favour; local changes were replaced in: ${syncResult.remoteOverwrote.join(", ")}`));
|
|
8209
|
+
console.warn(dim(" Recover them with `git reflog` / `git diff ORIG_HEAD` if that was wrong."));
|
|
8210
|
+
}
|
|
8211
|
+
}
|
|
8212
|
+
if (syncResult.keptUntracked && syncResult.keptUntracked.length > 0) {
|
|
8213
|
+
if (useJson) {
|
|
8214
|
+
jsonLine({ event: "sync_kept_untracked", files: syncResult.keptUntracked, timestamp: ts() });
|
|
8215
|
+
} else {
|
|
8216
|
+
console.warn(yellow(` Kept your untracked file(s) over the remote's copy: ${syncResult.keptUntracked.join(", ")}`));
|
|
8217
|
+
console.warn(dim(" Commit or remove them so the sync can reconcile that path."));
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
if (!syncResult.pushed && syncResult.pushError) {
|
|
8221
|
+
const diag = diagnoseSyncError(syncResult.pushError);
|
|
8222
|
+
if (useJson) {
|
|
8223
|
+
jsonLine({
|
|
8224
|
+
event: "error",
|
|
8225
|
+
phase: "push",
|
|
8226
|
+
timestamp: ts(),
|
|
8227
|
+
error: { reason: diag.reason, message: diag.message, diagnosis: diag.diagnosis, suggestions: diag.suggestions }
|
|
8228
|
+
});
|
|
8229
|
+
} else {
|
|
8230
|
+
console.warn(yellow(` Push failed: ${diag.message}`));
|
|
8231
|
+
console.warn(dim(` ${diag.diagnosis}`));
|
|
8232
|
+
}
|
|
8233
|
+
}
|
|
8071
8234
|
if (useJson) {
|
|
8072
8235
|
jsonLine({ event: "startup", phase: "sync", status: syncResult.status, pushed: syncResult.pushed, timestamp: ts() });
|
|
8073
8236
|
if (syncResult.status === "sync-failed") {
|
|
@@ -17964,12 +18127,28 @@ var deployCommand = new Command5("deploy").description("Deploy the current app t
|
|
|
17964
18127
|
console.error(` - ${s}`);
|
|
17965
18128
|
process.exit(1);
|
|
17966
18129
|
}
|
|
18130
|
+
if (syncResult.remoteOverwrote && syncResult.remoteOverwrote.length > 0) {
|
|
18131
|
+
const files = syncResult.remoteOverwrote.join(", ");
|
|
18132
|
+
if (useJson) {
|
|
18133
|
+
jsonLine({ event: "sync_remote_overwrote", files: syncResult.remoteOverwrote, timestamp: new Date().toISOString() });
|
|
18134
|
+
} else {
|
|
18135
|
+
console.warn(`Sync resolved conflicts in the remote's favour; local changes were replaced in: ${files}`);
|
|
18136
|
+
console.warn(" Recover them with `git reflog` / `git diff ORIG_HEAD` before deploying again if that was wrong.");
|
|
18137
|
+
}
|
|
18138
|
+
}
|
|
18139
|
+
if (syncResult.keptUntracked && syncResult.keptUntracked.length > 0 && !useJson) {
|
|
18140
|
+
console.warn(`Kept your untracked file(s) over the remote's copy: ${syncResult.keptUntracked.join(", ")}`);
|
|
18141
|
+
}
|
|
17967
18142
|
if (!syncResult.pushed) {
|
|
18143
|
+
const diag = diagnoseSyncError(syncResult.pushError ?? syncResult.error);
|
|
17968
18144
|
if (useJson) {
|
|
17969
|
-
jsonOut(buildErrorResponse("deploy",
|
|
18145
|
+
jsonOut(buildErrorResponse("deploy", `Failed to push before deployment: ${diag.message}`, diag.diagnosis, [...diag.suggestions, "Then retry runwork deploy"]));
|
|
17970
18146
|
process.exit(1);
|
|
17971
18147
|
}
|
|
17972
|
-
console.error(
|
|
18148
|
+
console.error(`Push failed: ${diag.message}`);
|
|
18149
|
+
console.error(diag.diagnosis);
|
|
18150
|
+
for (const suggestion of diag.suggestions)
|
|
18151
|
+
console.error(` - ${suggestion}`);
|
|
17973
18152
|
process.exit(1);
|
|
17974
18153
|
}
|
|
17975
18154
|
if (!useJson)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runwork",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Runwork, Inc. <info@runwork.ai> (https://www.runwork.ai)",
|