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.
@@ -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 existsSync4 } from "fs";
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 && existsSync4(exe))
1552
+ if (exe && existsSync5(exe))
1407
1553
  return exe;
1408
- const fallback = lines.find((line) => existsSync4(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 (existsSync4(gitExe))
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 (!existsSync4(candidate))
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
- const manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
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"), `node_modules/
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
- return { status, pushed };
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.0";
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", "Failed to push before deployment", "Local commits were synced but could not be pushed to the remote.", ["Check your network connection", "Run runwork dev to retry syncing", "Then retry runwork 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("Push failed. Check your connection and retry `runwork deploy`.");
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.0",
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)",