@somewhere-tech/cli 0.30.1 → 0.30.2

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@somewhere-tech/cli",
3
- "version": "0.30.1",
3
+ "version": "0.30.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@somewhere-tech/cli",
9
- "version": "0.30.1",
9
+ "version": "0.30.2",
10
10
  "bundleDependencies": [
11
11
  "@modelcontextprotocol/sdk",
12
12
  "chokidar",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@somewhere-tech/cli",
3
- "version": "0.30.1",
3
+ "version": "0.30.2",
4
4
  "description": "CLI for somewhere.tech — auth, projects, deploy, pull, promote, db, logs, env, MCP bridge.",
5
5
  "keywords": [
6
6
  "somewhere",
@@ -1,8 +1,8 @@
1
1
  {
2
- "commit": "1bf981dd",
2
+ "commit": "526d2b4d",
3
3
  "files": {
4
- "sw-init.mjs": "352ebc523b769736f0ea1da0fa87d259553a35d472e0b779d862e4c85252071c",
5
- "platform-context.mjs": "fe4dd3bd4fc3619e80d0da8d21df1e7146c416c154d8251d196bf32c007677a5",
6
- "browser-probes.mjs": "3793cd63cb29e4f2fce3691f6cf41fe3855ef85454e7b55cd55cf086986fc26d"
4
+ "sw-init.mjs": "919ee7188bb6e878bfb082e953154e5991360d935535729f7856d795cf59884f",
5
+ "platform-context.mjs": "8709d7bf8860ab8ac8e8e3a84bbca85261e39405d222958fa7c8afd39b467383",
6
+ "browser-probes.mjs": "cc8856fc970287a698ae4c5ecfff434a0aad67fe61d353bc87c9630952f67ce3"
7
7
  }
8
8
  }
@@ -1,4 +1,4 @@
1
- // VENDORED from worker/src/utils/browser-test.ts (DOM_OUTLINE_SCRIPT) @ 1bf981dd
1
+ // VENDORED from worker/src/utils/browser-test.ts (DOM_OUTLINE_SCRIPT) @ 526d2b4d
2
2
  // — the exact runtime deployed functions run against. Do not edit by hand;
3
3
  // re-sync with: node scripts/extract-runtime.mjs <monorepo>
4
4
  export const MAX_OUTLINE_NODES = 200;
@@ -1,5 +1,5 @@
1
1
  {
2
- "commit": "1bf981dd",
2
+ "commit": "526d2b4d",
3
3
  "esbuild": "0.24.0",
4
4
  "toolchain": {
5
5
  "base": {
@@ -57,7 +57,7 @@
57
57
  }
58
58
  },
59
59
  "files": {
60
- "compile-core.cjs": "06a7c02dacc5fc41ab602ec60eac14d988307b8a7019c62b49be72fafe664f65",
60
+ "compile-core.cjs": "4117fcbf26ac4e06b1ab3492802a4097dd250f3ea1ec9ef5214327defd3663fc",
61
61
  "graph-contract.cjs": "aaeebc34f486ee5f639444de5757accfc283de641e631908ccd8d8c688bfb5c0",
62
62
  "typed-functions.cjs": "5c0b499015dcdf755ca72dab04a80afce0891e782f835547592faec802c63be6"
63
63
  }
@@ -425,6 +425,139 @@ function cleanRange(range) {
425
425
  return String(range || '').replace(/^[\^~>=<v\s]+/, '').split(/\s/)[0] || 'latest';
426
426
  }
427
427
 
428
+ /* ─── Lockfile-derived install pins (tsk_f79d71ce) ────────────────────────────
429
+ * THE INSTALL SPEC IS OWNED HERE, by the core, so `somewhere dev` and
430
+ * `somewhere deploy` derive it from the same code and cannot disagree.
431
+ *
432
+ * The bug this closes: deploy floor-pinned the declared range (`^7.9.5` → 7.9.5)
433
+ * while the developer's own machine had 7.18.3 installed from their lockfile, so
434
+ * the local and deployed entry chunks were built against different library code
435
+ * — measured on the 0.30.0 journey scaffold, where react-router was the only
436
+ * package left in the diff. "Same app, same data, same build" was not true.
437
+ *
438
+ * THE RULE, both directions:
439
+ * - LOCKFILE PRESENT (npm-shrinkwrap.json, else package-lock.json — npm's own
440
+ * precedence): a dependency installs at the version the lockfile pins, which
441
+ * is exactly what the developer has locally. The pin is only honoured when it
442
+ * PROVABLY SATISFIES the range declared in package.json, so a lockfile can
443
+ * only ever move the build WITHIN what package.json already allows; an
444
+ * out-of-sync lockfile falls back to the floor-pin rather than installing
445
+ * something package.json contradicts.
446
+ * - NO LOCKFILE: unchanged, and this is the documented rule — third-party deps
447
+ * FLOOR-PIN the declared range (`^7.9.5` → 7.9.5), first-party
448
+ * `@somewhere-tech/*` pass the range through verbatim so npm resolves the
449
+ * latest satisfying version. A project with no lockfile resolves today
450
+ * exactly as it did before this change (rule 9).
451
+ *
452
+ * WHY `@somewhere-tech/*` IGNORES THE LOCKFILE TOO: pfb_f607d6b27453. Our own
453
+ * scaffold + `npm install` writes a lockfile within seconds of `somewhere init`,
454
+ * so honouring it for first-party packages would re-freeze every scaffold on the
455
+ * SDK version it was born with and published SDK fixes would never reach new
456
+ * deploys — the exact harm the range passthrough exists to prevent. Right after
457
+ * an install the locked version IS the latest in range, so the two agree; they
458
+ * only part as the lockfile ages, and that is the case where the platform's own
459
+ * package must move.
460
+ *
461
+ * BOUNDARY, stated so nobody reads more into this than it does: this pins the
462
+ * dependencies the core decides to install — the ones declared in the project's
463
+ * package.json. Their TRANSITIVE dependencies are still resolved by npm from the
464
+ * parents' ranges. Pinning the whole locked closure was considered and rejected
465
+ * for now: an explicit exact spec for every transitive can trip ERESOLVE peer
466
+ * conflicts and platform-specific optional binaries, which would turn a build
467
+ * that works today into a refused one (rule 9 — tighten incrementally). If a
468
+ * transitive drift is ever measured, that is the next step, not a redesign.
469
+ *
470
+ * Prefer-baked resolution is deliberately NOT changed: which packages come from
471
+ * the baked image, and the React-19 symlink hot path, still key off the DECLARED
472
+ * RANGE. Making the baked decision exact-match would cold-install React on every
473
+ * lockfile project (tsk_2553662f's 600s timeout) and would re-open the very
474
+ * divergence tsk_0312cf17 closed by pinning the local loop to the image's own
475
+ * React set. Aligning the local loop with the rest of the baked set is the CLI's
476
+ * half of this, not the compiler's.
477
+ */
478
+
479
+ /** npm's own lockfile precedence: a shrinkwrap wins over a package-lock. */
480
+ const LOCKFILE_NAMES = ['npm-shrinkwrap.json', 'package-lock.json'];
481
+
482
+ /**
483
+ * Versions an npm lockfile pins for the project's TOP-LEVEL packages.
484
+ *
485
+ * Only `node_modules/<name>` entries count: a nested `node_modules/a/node_modules/b`
486
+ * path is a transitive's private copy, not the version the project itself
487
+ * resolves. Link entries (workspaces, `file:` deps) carry no registry version and
488
+ * are skipped. Handles lockfileVersion 2/3 (`packages`) and 1 (`dependencies`).
489
+ * Malformed input yields an empty map — never a throw; a lockfile we cannot read
490
+ * degrades to today's floor-pin.
491
+ */
492
+ function lockfileVersions(text) {
493
+ const out = new Map();
494
+ if (typeof text !== 'string' || !text.trim()) return out;
495
+ let lock;
496
+ try { lock = JSON.parse(text); } catch { return out; }
497
+ if (!lock || typeof lock !== 'object') return out;
498
+ const packages = lock.packages && typeof lock.packages === 'object' ? lock.packages : null;
499
+ if (packages) {
500
+ for (const [entryPath, entry] of Object.entries(packages)) {
501
+ if (!entryPath.startsWith('node_modules/')) continue;
502
+ const name = entryPath.slice('node_modules/'.length);
503
+ if (name.includes('/node_modules/')) continue;
504
+ if (!entry || typeof entry !== 'object' || entry.link) continue;
505
+ if (typeof entry.version !== 'string' || !entry.version) continue;
506
+ out.set(name, entry.version);
507
+ }
508
+ }
509
+ const v1 = lock.dependencies && typeof lock.dependencies === 'object' ? lock.dependencies : null;
510
+ if (v1) {
511
+ for (const [name, entry] of Object.entries(v1)) {
512
+ if (out.has(name)) continue;
513
+ if (!entry || typeof entry !== 'object') continue;
514
+ if (typeof entry.version !== 'string' || !entry.version) continue;
515
+ if (/^(?:file|link|git\+|https?):/.test(entry.version)) continue;
516
+ out.set(name, entry.version);
517
+ }
518
+ }
519
+ return out;
520
+ }
521
+
522
+ /**
523
+ * The one derivation of what to hand `npm install` for a single dependency.
524
+ * PURE — `satisfies(version, range)` is passed in (semver is host-loaded) and
525
+ * must return TRUE only when it can prove the version is inside the range;
526
+ * anything it cannot prove falls back to the floor-pin, i.e. today's behavior.
527
+ *
528
+ * Exported so the fixture can pin the rule in BOTH directions without a build.
529
+ */
530
+ function resolveInstallSpec({ name, range, lockedVersion, satisfies }) {
531
+ // First-party platform packages: declared range verbatim, lockfile ignored.
532
+ // See the block comment above (pfb_f607d6b27453).
533
+ if (name.startsWith('@somewhere-tech/')) {
534
+ return `${name}@${String(range || 'latest').trim() || 'latest'}`;
535
+ }
536
+ if (lockedVersion && typeof satisfies === 'function') {
537
+ let inRange = false;
538
+ try { inRange = satisfies(lockedVersion, range) === true; } catch { inRange = false; }
539
+ if (inRange) return `${name}@${lockedVersion}`;
540
+ }
541
+ return `${name}@${cleanRange(range)}`;
542
+ }
543
+
544
+ /**
545
+ * The project's locked versions, read from the materialized build root. Both
546
+ * hosts land here: the container materializes every uploaded file (the lockfile
547
+ * included) into the build root, and the CLI's build root is the project itself.
548
+ * A missing or unreadable lockfile is not an error — it is the documented
549
+ * no-lockfile path.
550
+ */
551
+ function readLockedVersions(root) {
552
+ for (const name of LOCKFILE_NAMES) {
553
+ let text;
554
+ try { text = fs.readFileSync(path.join(root, name), 'utf8'); } catch { continue; }
555
+ const versions = lockfileVersions(text);
556
+ if (versions.size) return versions;
557
+ }
558
+ return new Map();
559
+ }
560
+
428
561
  /**
429
562
  * Is `name` resolvable the way ESBUILD will resolve it — i.e. as a direct child
430
563
  * package of one of the exact search dirs (the per-build install + the baked
@@ -851,6 +984,20 @@ function createCompileCore(host) {
851
984
  async function ensureDeps(root, pkg, warnings, ctx, scopedProxyReady = true) {
852
985
  const deps = (pkg && pkg.dependencies) || {};
853
986
  const buildRoot = path.join(root, 'node_modules');
987
+ // The project's own lockfile, materialized alongside package.json. It IS
988
+ // uploaded on every deploy and was, until now, ignored — which is what made
989
+ // the deployed build resolve a different version of an app dependency than
990
+ // the developer's machine (tsk_f79d71ce). Missing/unreadable → empty map →
991
+ // the floor-pin path, unchanged.
992
+ const locked = readLockedVersions(root);
993
+ const semverForLock = getSemver();
994
+ const satisfiesLocked = semverForLock
995
+ ? (version, range) => {
996
+ const wanted = (typeof range === 'string' && range.trim()) ? range.trim() : '*';
997
+ if (semverForLock.validRange(wanted) === null) return false;
998
+ return semverForLock.satisfies(version, wanted, { includePrerelease: true });
999
+ }
1000
+ : null;
854
1001
  const missing = [];
855
1002
  // react/react-dom whose pin the baked 18 misses but the baked 19 satisfies:
856
1003
  // collect them to symlink from the isolated react19 set instead of installing.
@@ -864,16 +1011,13 @@ function createCompileCore(host) {
864
1011
  if (isResolvable(name, IMAGE_NODE_MODULES) && bakedSatisfies(name, range)) continue;
865
1012
  const r19 = react19BakedDir(name, range);
866
1013
  if (r19) { react19Links.push({ name, dir: r19 }); continue; }
867
- // First-party platform packages (@somewhere-tech/*) install the DECLARED
868
- // RANGE verbatim so npm resolves the latest satisfying version from the
869
- // live registry (pfb_f607d6b27453): cleanRange's floor-pinning froze
870
- // every ^-range scaffold on the SDK version it was scaffolded with, so
871
- // published SDK fixes never reached new deploys. Third-party deps keep
872
- // the floor-pin that determinism choice is unchanged here.
873
- const spec = name.startsWith('@somewhere-tech/')
874
- ? `${name}@${String(range || 'latest').trim() || 'latest'}`
875
- : `${name}@${cleanRange(range)}`;
876
- missing.push(spec);
1014
+ // WHAT version to install is resolveInstallSpec's single decision — the
1015
+ // project's lockfile when it pins one inside the declared range, the
1016
+ // floor-pin otherwise, the declared range verbatim for first-party
1017
+ // @somewhere-tech/* packages. See the block comment on resolveInstallSpec.
1018
+ missing.push(resolveInstallSpec({
1019
+ name, range, lockedVersion: locked.get(name), satisfies: satisfiesLocked,
1020
+ }));
877
1021
  }
878
1022
  // Symlink the baked React-19 packages into the build root so esbuild resolves
879
1023
  // them from root/node_modules (which it consults before the baked nodePaths) —
@@ -888,7 +1032,9 @@ function createCompileCore(host) {
888
1032
  if (!fs.existsSync(link)) fs.symlinkSync(dir, link, 'dir');
889
1033
  } catch (e) {
890
1034
  warnings.push(`react19 link skipped for ${name}; installing per-build: ${e && e.message ? e.message : String(e)}`);
891
- missing.push(`${name}@${cleanRange(deps[name])}`);
1035
+ missing.push(resolveInstallSpec({
1036
+ name, range: deps[name], lockedVersion: locked.get(name), satisfies: satisfiesLocked,
1037
+ }));
892
1038
  }
893
1039
  }
894
1040
  }
@@ -1774,6 +1920,12 @@ function isBuildConfigFile(filePath) {
1774
1920
  module.exports = {
1775
1921
  createCompileCore,
1776
1922
  COMPILER_CONTRACT,
1923
+ // Install-spec derivation (tsk_f79d71ce) — pure, exported for the fixture
1924
+ // that pins the rule in both directions (lockfile present / absent).
1925
+ LOCKFILE_NAMES,
1926
+ lockfileVersions,
1927
+ resolveInstallSpec,
1928
+ cleanRange,
1777
1929
  NODE_POLYFILLS,
1778
1930
  ASSET_LOADERS,
1779
1931
  KNOWN_BAD_VERSIONS,
@@ -1,4 +1,4 @@
1
- // VENDORED from worker/src/runtime/context.ts (PLATFORM_CONTEXT_JS) @ 1bf981dd
1
+ // VENDORED from worker/src/runtime/context.ts (PLATFORM_CONTEXT_JS) @ 526d2b4d
2
2
  // — the exact runtime deployed functions run against. Do not edit by hand;
3
3
  // re-sync with: node scripts/extract-runtime.mjs <monorepo>
4
4
  // ── Primordial freeze (tsk_327fe8a4, opinionated-not-greenfield) ───────────
@@ -2702,6 +2702,19 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2702
2702
  'sw.db.' + verb + ' on "' + table + '": the value for "' + col.n + '" must be ' + __sw_structExpectedType(col.t) +
2703
2703
  ' (got ' + got + '). The column types are declared in db/schema.ts.');
2704
2704
  }
2705
+ // The exact form for a whole number wider than a JavaScript number:
2706
+ // plain digits, optionally signed, exactly as a read returns them.
2707
+ const __SW_STRUCT_WHOLE_NUMBER_RE = /^-?\d+$/;
2708
+ // Compare that text against the 64-bit range without a numeric
2709
+ // conversion, which is the very thing that would lose it.
2710
+ function __sw_wholeNumberTextInRange(s) {
2711
+ const negative = s.charCodeAt(0) === 45;
2712
+ let digits = negative ? s.slice(1) : s;
2713
+ while (digits.length > 1 && digits.charCodeAt(0) === 48) digits = digits.slice(1);
2714
+ const limit = negative ? '9223372036854775808' : '9223372036854775807';
2715
+ if (digits.length !== limit.length) return digits.length < limit.length;
2716
+ return digits <= limit;
2717
+ }
2705
2718
  // Type-check (and for json: serialize) ONE written value against its
2706
2719
  // declared column. Returns the value to bind.
2707
2720
  function __sw_structCheckWriteValue(verb, table, col, v) {
@@ -2729,6 +2742,25 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2729
2742
  }
2730
2743
  if (typeof v === 'object') __sw_structThrowTypeMismatch(verb, table, col, __sw_structGotKind(v));
2731
2744
  if (col.t === 'integer') {
2745
+ // A whole number wider than 9007199254740991 cannot travel as a
2746
+ // JavaScript number at all — the language rounds it before this
2747
+ // function is reached — so the exact form for those is the same
2748
+ // decimal STRING a read returns (tsk_d741c40b). Accepting it here is
2749
+ // what makes read -> write back lossless on a managed table; the
2750
+ // database converts it on the way in because the column is an
2751
+ // integer. Text that is not a whole number keeps the type error it
2752
+ // has always had.
2753
+ if (typeof v === 'string') {
2754
+ if (!__sw_reMatch(__SW_STRUCT_WHOLE_NUMBER_RE, v)) {
2755
+ __sw_structThrowTypeMismatch(verb, table, col, 'text that is not a whole number');
2756
+ }
2757
+ if (!__sw_wholeNumberTextInRange(v)) {
2758
+ __sw_structThrowSchema('DATABASE_VALUE_TOO_LARGE',
2759
+ 'sw.db.' + verb + ' on "' + table + '": the value for "' + col.n + '" is outside the range of whole numbers this database stores ' +
2760
+ '(-9223372036854775808 to 9223372036854775807). Keep the value inside that range, or store it as text.');
2761
+ }
2762
+ return v;
2763
+ }
2732
2764
  if (typeof v !== 'number' || v !== v || !Number.isInteger(v)) __sw_structThrowTypeMismatch(verb, table, col, typeof v === 'number' ? 'a number with a fractional part' : __sw_structGotKind(v));
2733
2765
  return v;
2734
2766
  }
@@ -1,4 +1,4 @@
1
- // VENDORED from worker/src/utils/function-bundle.ts (SW_INIT_JS) @ 1bf981dd
1
+ // VENDORED from worker/src/utils/function-bundle.ts (SW_INIT_JS) @ 526d2b4d
2
2
  // — the exact runtime deployed functions run against. Do not edit by hand;
3
3
  // re-sync with: node scripts/extract-runtime.mjs <monorepo>
4
4
  // Generated by somewhere.tech deploy pipeline. Sets up the global