@arcote.tech/arc-cli 0.6.1 → 0.7.0

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/dist/index.js CHANGED
@@ -9744,9 +9744,9 @@ var require_verify_stream = __commonJS((exports, module) => {
9744
9744
  }
9745
9745
  function jwsVerify(jwsSig, algorithm, secretOrKey) {
9746
9746
  if (!algorithm) {
9747
- var err3 = new Error("Missing algorithm parameter for jws.verify");
9748
- err3.code = "MISSING_ALGORITHM";
9749
- throw err3;
9747
+ var err2 = new Error("Missing algorithm parameter for jws.verify");
9748
+ err2.code = "MISSING_ALGORITHM";
9749
+ throw err2;
9750
9750
  }
9751
9751
  jwsSig = toString(jwsSig);
9752
9752
  var signature = signatureFromJWS(jwsSig);
@@ -11934,9 +11934,9 @@ var require_verify = __commonJS((exports, module) => {
11934
11934
  if (callback) {
11935
11935
  done = callback;
11936
11936
  } else {
11937
- done = function(err3, data) {
11938
- if (err3)
11939
- throw err3;
11937
+ done = function(err2, data) {
11938
+ if (err2)
11939
+ throw err2;
11940
11940
  return data;
11941
11941
  };
11942
11942
  }
@@ -11963,8 +11963,8 @@ var require_verify = __commonJS((exports, module) => {
11963
11963
  let decodedToken;
11964
11964
  try {
11965
11965
  decodedToken = decode(jwtString, { complete: true });
11966
- } catch (err3) {
11967
- return done(err3);
11966
+ } catch (err2) {
11967
+ return done(err2);
11968
11968
  }
11969
11969
  if (!decodedToken) {
11970
11970
  return done(new JsonWebTokenError("invalid token"));
@@ -11981,9 +11981,9 @@ var require_verify = __commonJS((exports, module) => {
11981
11981
  return secretCallback(null, secretOrPublicKey);
11982
11982
  };
11983
11983
  }
11984
- return getSecret(header, function(err3, secretOrPublicKey2) {
11985
- if (err3) {
11986
- return done(new JsonWebTokenError("error in secret or public key callback: " + err3.message));
11984
+ return getSecret(header, function(err2, secretOrPublicKey2) {
11985
+ if (err2) {
11986
+ return done(new JsonWebTokenError("error in secret or public key callback: " + err2.message));
11987
11987
  }
11988
11988
  const hasSignature = parts[2].trim() !== "";
11989
11989
  if (!hasSignature && secretOrPublicKey2) {
@@ -12619,11 +12619,11 @@ var require_sign = __commonJS((exports, module) => {
12619
12619
  typ: isObjectPayload ? "JWT" : undefined,
12620
12620
  kid: options.keyid
12621
12621
  }, options.header);
12622
- function failure(err3) {
12622
+ function failure(err2) {
12623
12623
  if (callback) {
12624
- return callback(err3);
12624
+ return callback(err2);
12625
12625
  }
12626
- throw err3;
12626
+ throw err2;
12627
12627
  }
12628
12628
  if (!secretOrPrivateKey && options.algorithm !== "none") {
12629
12629
  return failure(new Error("secretOrPrivateKey must have a value"));
@@ -12695,8 +12695,8 @@ var require_sign = __commonJS((exports, module) => {
12695
12695
  if (typeof options.notBefore !== "undefined") {
12696
12696
  try {
12697
12697
  payload.nbf = timespan(options.notBefore, timestamp2);
12698
- } catch (err3) {
12699
- return failure(err3);
12698
+ } catch (err2) {
12699
+ return failure(err2);
12700
12700
  }
12701
12701
  if (typeof payload.nbf === "undefined") {
12702
12702
  return failure(new Error('"notBefore" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
@@ -12705,8 +12705,8 @@ var require_sign = __commonJS((exports, module) => {
12705
12705
  if (typeof options.expiresIn !== "undefined" && typeof payload === "object") {
12706
12706
  try {
12707
12707
  payload.exp = timespan(options.expiresIn, timestamp2);
12708
- } catch (err3) {
12709
- return failure(err3);
12708
+ } catch (err2) {
12709
+ return failure(err2);
12710
12710
  }
12711
12711
  if (typeof payload.exp === "undefined") {
12712
12712
  return failure(new Error('"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
@@ -13764,8 +13764,8 @@ class EventWire {
13764
13764
  try {
13765
13765
  const message = JSON.parse(event.data);
13766
13766
  this.handleMessage(message);
13767
- } catch (err3) {
13768
- console.error("EventWire: Failed to parse message", err3);
13767
+ } catch (err2) {
13768
+ console.error("EventWire: Failed to parse message", err2);
13769
13769
  }
13770
13770
  };
13771
13771
  this.ws.onclose = (event) => {
@@ -13774,17 +13774,17 @@ class EventWire {
13774
13774
  console.log("EventWire disconnected");
13775
13775
  this.scheduleReconnect();
13776
13776
  };
13777
- this.ws.onerror = (err3) => {
13778
- console.error("EventWire error:", err3);
13777
+ this.ws.onerror = (err2) => {
13778
+ console.error("EventWire error:", err2);
13779
13779
  if (this.state === "connecting") {
13780
13780
  this.state = "disconnected";
13781
13781
  this.ws = null;
13782
13782
  }
13783
13783
  };
13784
- } catch (err3) {
13784
+ } catch (err2) {
13785
13785
  this.state = "disconnected";
13786
13786
  this.ws = null;
13787
- console.error("EventWire: Failed to connect", err3);
13787
+ console.error("EventWire: Failed to connect", err2);
13788
13788
  this.scheduleReconnect();
13789
13789
  }
13790
13790
  }
@@ -18135,7 +18135,7 @@ var {
18135
18135
  // ../../node_modules/.bun/find-up@7.0.0/node_modules/find-up/index.js
18136
18136
  import path2 from "path";
18137
18137
 
18138
- // ../../node_modules/.bun/find-up@7.0.0/node_modules/find-up/node_modules/locate-path/index.js
18138
+ // ../../node_modules/.bun/locate-path@7.2.0/node_modules/locate-path/index.js
18139
18139
  import process2 from "process";
18140
18140
  import path from "path";
18141
18141
  import fs, { promises as fsPromises } from "fs";
@@ -21686,7 +21686,7 @@ var emitWarning = (msg, type, code, fn) => {
21686
21686
  var AC = globalThis.AbortController;
21687
21687
  var AS = globalThis.AbortSignal;
21688
21688
  if (typeof AC === "undefined") {
21689
- AS = class AbortSignal2 {
21689
+ AS = class AbortSignal {
21690
21690
  onabort;
21691
21691
  _onabort = [];
21692
21692
  reason;
@@ -26129,110 +26129,9 @@ ${colors2.yellow}Type declaration errors:${colors2.reset}`);
26129
26129
  }
26130
26130
  }
26131
26131
 
26132
- // src/commands/build-shell.ts
26133
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, rmSync } from "fs";
26134
- import { join as join3 } from "path";
26135
- var REACT_ENTRIES = [
26136
- ["react", `export * from "react";
26137
- import * as React from "react";
26138
- export default React;`],
26139
- ["react-dom", `export * from "react-dom";
26140
- import * as ReactDOM from "react-dom";
26141
- export default ReactDOM;`],
26142
- ["jsx-runtime", `export * from "react/jsx-runtime";`],
26143
- ["jsx-dev-runtime", `export * from "react/jsx-dev-runtime";`],
26144
- ["react-dom-client", `export { createRoot, hydrateRoot } from "react-dom/client";`]
26145
- ];
26146
- var SHELL_BASE_EXTERNAL = [
26147
- "react",
26148
- "react-dom",
26149
- "react/jsx-runtime",
26150
- "react/jsx-dev-runtime",
26151
- "react-dom/client"
26152
- ];
26153
- async function buildShell(opts) {
26154
- const outDir = opts.out;
26155
- const fromDir = opts.from;
26156
- if (!existsSync3(fromDir)) {
26157
- console.error(`[_build-shell] --from not found: ${fromDir}`);
26158
- process.exit(1);
26159
- }
26160
- mkdirSync3(outDir, { recursive: true });
26161
- const tmpDir = join3(outDir, "_tmp");
26162
- mkdirSync3(tmpDir, { recursive: true });
26163
- try {
26164
- const arcPkgs = discoverArcPackages(fromDir);
26165
- if (arcPkgs.length === 0) {
26166
- console.warn("[_build-shell] no @arcote.tech/* packages discovered");
26167
- }
26168
- console.log(`[_build-shell] building shell for react + ${arcPkgs.length} @arcote.tech/* package(s)`);
26169
- await buildReactShell(outDir, tmpDir, fromDir);
26170
- for (const pkg of arcPkgs) {
26171
- await buildArcEntry(pkg, arcPkgs, outDir, tmpDir, fromDir);
26172
- }
26173
- console.log(`[_build-shell] done \u2192 ${outDir}`);
26174
- } finally {
26175
- rmSync(tmpDir, { recursive: true, force: true });
26176
- }
26177
- }
26178
- function discoverArcPackages(fromDir) {
26179
- const arcDir = join3(fromDir, "@arcote.tech");
26180
- if (!existsSync3(arcDir))
26181
- return [];
26182
- return readdirSync2(arcDir).filter((name) => existsSync3(join3(arcDir, name, "package.json"))).map((name) => `@arcote.tech/${name}`);
26183
- }
26184
- async function buildReactShell(outDir, tmpDir, fromDir) {
26185
- const eps = [];
26186
- for (const [name, code] of REACT_ENTRIES) {
26187
- const f = join3(tmpDir, `${name}.ts`);
26188
- await Bun.write(f, code);
26189
- eps.push(f);
26190
- }
26191
- const r = await Bun.build({
26192
- entrypoints: eps,
26193
- outdir: outDir,
26194
- splitting: true,
26195
- format: "esm",
26196
- target: "browser",
26197
- naming: "[name].[ext]",
26198
- root: fromDir
26199
- });
26200
- if (!r.success) {
26201
- for (const l of r.logs)
26202
- console.error(l);
26203
- throw new Error("React shell build failed");
26204
- }
26205
- }
26206
- async function buildArcEntry(pkg, allArcPkgs, outDir, tmpDir, fromDir) {
26207
- const shortName = pkg.replace("@arcote.tech/", "");
26208
- const otherExternals = allArcPkgs.filter((p) => p !== pkg);
26209
- const f = join3(tmpDir, `${shortName}.ts`);
26210
- await Bun.write(f, `export * from "${pkg}";
26211
- `);
26212
- const r = await Bun.build({
26213
- entrypoints: [f],
26214
- outdir: outDir,
26215
- format: "esm",
26216
- target: "browser",
26217
- naming: "[name].[ext]",
26218
- root: fromDir,
26219
- external: [...SHELL_BASE_EXTERNAL, ...otherExternals],
26220
- define: {
26221
- ONLY_SERVER: "false",
26222
- ONLY_BROWSER: "true",
26223
- ONLY_CLIENT: "true"
26224
- }
26225
- });
26226
- if (!r.success) {
26227
- for (const l of r.logs)
26228
- console.error(l);
26229
- throw new Error(`Shell build failed for ${pkg}`);
26230
- }
26231
- }
26232
-
26233
26132
  // src/commands/dev.ts
26234
26133
  var import_chokidar = __toESM(require_chokidar(), 1);
26235
- import { dirname as dirname3, join as join4, relative } from "path";
26134
+ import { dirname as dirname3, join as join3, relative } from "path";
26236
26135
  function getContextForFile(filePath, contexts, configDir) {
26237
26136
  const relativePath = relative(configDir, filePath);
26238
26137
  for (const context of contexts) {
@@ -26349,7 +26248,7 @@ ${colors3.yellow}Type declaration errors:${colors3.reset}`);
26349
26248
  } else {
26350
26249
  log("Initial build complete \u2713", "green");
26351
26250
  }
26352
- const watcher = import_chokidar.default.watch([join4(configDir, "**/*.ts"), join4(configDir, "**/*.tsx")], {
26251
+ const watcher = import_chokidar.default.watch([join3(configDir, "**/*.ts"), join3(configDir, "**/*.tsx")], {
26353
26252
  ignored: [
26354
26253
  "**/node_modules/**",
26355
26254
  `**/${config.outDir}/**`,
@@ -26446,24 +26345,24 @@ ${colors3.yellow}Type declaration errors:${colors3.reset}`);
26446
26345
  }
26447
26346
 
26448
26347
  // src/platform/shared.ts
26449
- import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync11, readdirSync as readdirSync6, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "fs";
26450
- import { dirname as dirname6, join as join12 } from "path";
26348
+ import { copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync10, readdirSync as readdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "fs";
26349
+ import { dirname as dirname7, join as join11 } from "path";
26451
26350
 
26452
26351
  // src/builder/module-builder.ts
26453
26352
  import { execSync } from "child_process";
26454
26353
  import {
26455
- existsSync as existsSync8,
26456
- mkdirSync as mkdirSync7,
26457
- readFileSync as readFileSync8,
26458
- readdirSync as readdirSync5,
26459
- rmSync as rmSync2,
26460
- writeFileSync as writeFileSync7
26354
+ existsSync as existsSync7,
26355
+ mkdirSync as mkdirSync6,
26356
+ readFileSync as readFileSync7,
26357
+ readdirSync as readdirSync4,
26358
+ rmSync,
26359
+ writeFileSync as writeFileSync6
26461
26360
  } from "fs";
26462
- import { basename as basename2, dirname as dirname5, join as join9, relative as relative3 } from "path";
26361
+ import { basename as basename2, dirname as dirname5, join as join8, relative as relative3 } from "path";
26463
26362
 
26464
26363
  // src/i18n/index.ts
26465
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
26466
- import { dirname as dirname4, join as join6 } from "path";
26364
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
26365
+ import { dirname as dirname4, join as join5 } from "path";
26467
26366
 
26468
26367
  // src/i18n/catalog.ts
26469
26368
  function hashMsgid(msgid) {
@@ -26634,10 +26533,10 @@ function quoteString(s) {
26634
26533
  }
26635
26534
 
26636
26535
  // src/i18n/compile.ts
26637
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
26638
- import { join as join5 } from "path";
26536
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync3 } from "fs";
26537
+ import { join as join4 } from "path";
26639
26538
  function compileCatalog(poPath) {
26640
- const content = readFileSync4(poPath, "utf-8");
26539
+ const content = readFileSync3(poPath, "utf-8");
26641
26540
  const entries = parsePo(content);
26642
26541
  const result = {};
26643
26542
  for (const entry of entries) {
@@ -26652,13 +26551,13 @@ function compileCatalog(poPath) {
26652
26551
  return sorted;
26653
26552
  }
26654
26553
  function compileAllCatalogs(localesDir, outDir) {
26655
- mkdirSync4(outDir, { recursive: true });
26656
- for (const file of readdirSync3(localesDir)) {
26554
+ mkdirSync3(outDir, { recursive: true });
26555
+ for (const file of readdirSync2(localesDir)) {
26657
26556
  if (!file.endsWith(".po"))
26658
26557
  continue;
26659
26558
  const locale = file.replace(".po", "");
26660
- const compiled = compileCatalog(join5(localesDir, file));
26661
- writeFileSync4(join5(outDir, `${locale}.json`), JSON.stringify(compiled));
26559
+ const compiled = compileCatalog(join4(localesDir, file));
26560
+ writeFileSync3(join4(outDir, `${locale}.json`), JSON.stringify(compiled));
26662
26561
  }
26663
26562
  }
26664
26563
 
@@ -26702,10 +26601,10 @@ function i18nExtractPlugin(collector, rootDir) {
26702
26601
 
26703
26602
  // src/i18n/index.ts
26704
26603
  function readTranslationsConfig(rootDir) {
26705
- const pkgPath = join6(rootDir, "package.json");
26706
- if (!existsSync5(pkgPath))
26604
+ const pkgPath = join5(rootDir, "package.json");
26605
+ if (!existsSync4(pkgPath))
26707
26606
  return null;
26708
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
26607
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
26709
26608
  const config = pkg.arc?.translations;
26710
26609
  if (!config?.locales?.length)
26711
26610
  return null;
@@ -26718,34 +26617,34 @@ async function finalizeTranslations(rootDir, outDir, collector) {
26718
26617
  const config = readTranslationsConfig(rootDir);
26719
26618
  if (!config || collector.size === 0)
26720
26619
  return;
26721
- const localesJsonDir = join6(outDir, "locales");
26722
- mkdirSync5(localesJsonDir, { recursive: true });
26620
+ const localesJsonDir = join5(outDir, "locales");
26621
+ mkdirSync4(localesJsonDir, { recursive: true });
26723
26622
  console.log(` Extracted ${collector.size} translatable string(s) for ${config.locales.length} locale(s)`);
26724
26623
  for (const locale of config.locales) {
26725
- const poPath = join6(rootDir, "locales", `${locale}.po`);
26726
- mkdirSync5(dirname4(poPath), { recursive: true });
26727
- const existing = existsSync5(poPath) ? parsePo(readFileSync5(poPath, "utf-8")) : [];
26624
+ const poPath = join5(rootDir, "locales", `${locale}.po`);
26625
+ mkdirSync4(dirname4(poPath), { recursive: true });
26626
+ const existing = existsSync4(poPath) ? parsePo(readFileSync4(poPath, "utf-8")) : [];
26728
26627
  const merged = mergeCatalog(existing, collector);
26729
- writeFileSync5(poPath, writePo(merged));
26628
+ writeFileSync4(poPath, writePo(merged));
26730
26629
  const compiled = compileCatalog(poPath);
26731
- writeFileSync5(join6(localesJsonDir, `${locale}.json`), JSON.stringify(compiled));
26630
+ writeFileSync4(join5(localesJsonDir, `${locale}.json`), JSON.stringify(compiled));
26732
26631
  }
26733
26632
  }
26734
26633
 
26735
26634
  // src/builder/build-cache.ts
26736
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
26737
- import { join as join7 } from "path";
26738
- var CACHE_VERSION = 1;
26635
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
26636
+ import { join as join6 } from "path";
26637
+ var CACHE_VERSION = 2;
26739
26638
  var CACHE_FILE = ".build-cache.json";
26740
26639
  function emptyCache() {
26741
26640
  return { version: CACHE_VERSION, units: {} };
26742
26641
  }
26743
26642
  function loadBuildCache(arcDir) {
26744
- const path4 = join7(arcDir, CACHE_FILE);
26745
- if (!existsSync6(path4))
26643
+ const path4 = join6(arcDir, CACHE_FILE);
26644
+ if (!existsSync5(path4))
26746
26645
  return emptyCache();
26747
26646
  try {
26748
- const raw = JSON.parse(readFileSync6(path4, "utf-8"));
26647
+ const raw = JSON.parse(readFileSync5(path4, "utf-8"));
26749
26648
  if (raw?.version !== CACHE_VERSION || typeof raw.units !== "object") {
26750
26649
  return emptyCache();
26751
26650
  }
@@ -26755,15 +26654,15 @@ function loadBuildCache(arcDir) {
26755
26654
  }
26756
26655
  }
26757
26656
  function saveBuildCache(arcDir, cache) {
26758
- mkdirSync6(arcDir, { recursive: true });
26759
- writeFileSync6(join7(arcDir, CACHE_FILE), JSON.stringify(cache, null, 2));
26657
+ mkdirSync5(arcDir, { recursive: true });
26658
+ writeFileSync5(join6(arcDir, CACHE_FILE), JSON.stringify(cache, null, 2));
26760
26659
  }
26761
26660
  function isCacheHit(cache, unitId, inputHash, requiredOutputs = []) {
26762
26661
  const entry = cache.units[unitId];
26763
26662
  if (!entry || entry.inputHash !== inputHash)
26764
26663
  return false;
26765
26664
  for (const out of requiredOutputs) {
26766
- if (!existsSync6(out))
26665
+ if (!existsSync5(out))
26767
26666
  return false;
26768
26667
  }
26769
26668
  return true;
@@ -26776,9 +26675,33 @@ function updateCache(cache, unitId, inputHash, output = {}) {
26776
26675
  };
26777
26676
  }
26778
26677
 
26678
+ // src/builder/framework-peers.ts
26679
+ var CORE_PEERS = [
26680
+ "@arcote.tech/arc",
26681
+ "@arcote.tech/arc-ds",
26682
+ "@arcote.tech/arc-react",
26683
+ "@arcote.tech/platform"
26684
+ ];
26685
+ var BROWSER_FRAGMENT_PEERS = [
26686
+ "@arcote.tech/arc-auth",
26687
+ "@arcote.tech/arc-workspace",
26688
+ "@arcote.tech/arc-utils",
26689
+ "@arcote.tech/arc-chat"
26690
+ ];
26691
+ var ARC_PEERS = [...CORE_PEERS, ...BROWSER_FRAGMENT_PEERS];
26692
+ var REACT_PEERS = ["react", "react-dom"];
26693
+ var FRAMEWORK_PEERS = [...ARC_PEERS, ...REACT_PEERS];
26694
+ var SHELL_EXTERNALS = [
26695
+ ...FRAMEWORK_PEERS,
26696
+ "react/jsx-runtime",
26697
+ "react/jsx-dev-runtime"
26698
+ ];
26699
+ var FRAMEWORK_PEER_SET = new Set(FRAMEWORK_PEERS);
26700
+ var SHELL_EXTERNAL_SET = new Set(SHELL_EXTERNALS);
26701
+
26779
26702
  // src/builder/hash.ts
26780
- import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, statSync } from "fs";
26781
- import { join as join8, relative as relative2, sep as sep2 } from "path";
26703
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync } from "fs";
26704
+ import { join as join7, relative as relative2, sep as sep2 } from "path";
26782
26705
  function sha256Hex(bytes) {
26783
26706
  const hasher = new Bun.CryptoHasher("sha256");
26784
26707
  hasher.update(bytes);
@@ -26788,21 +26711,21 @@ function sha256OfFiles(paths) {
26788
26711
  const hasher = new Bun.CryptoHasher("sha256");
26789
26712
  const sorted = [...paths].sort();
26790
26713
  for (const p of sorted) {
26791
- if (!existsSync7(p))
26714
+ if (!existsSync6(p))
26792
26715
  continue;
26793
- hasher.update(readFileSync7(p));
26716
+ hasher.update(readFileSync6(p));
26794
26717
  hasher.update("\x00");
26795
26718
  }
26796
26719
  return hasher.digest("hex");
26797
26720
  }
26798
26721
  function sha256OfDir(dir, filter2) {
26799
- if (!existsSync7(dir))
26722
+ if (!existsSync6(dir))
26800
26723
  return sha256Hex("");
26801
26724
  const hasher = new Bun.CryptoHasher("sha256");
26802
26725
  const entries = [];
26803
26726
  function walk(absDir) {
26804
- for (const entry of readdirSync4(absDir, { withFileTypes: true })) {
26805
- const abs = join8(absDir, entry.name);
26727
+ for (const entry of readdirSync3(absDir, { withFileTypes: true })) {
26728
+ const abs = join7(absDir, entry.name);
26806
26729
  const rel = relative2(dir, abs).split(sep2).join("/");
26807
26730
  if (filter2 && !filter2(rel))
26808
26731
  continue;
@@ -26817,7 +26740,7 @@ function sha256OfDir(dir, filter2) {
26817
26740
  for (const { rel, abs } of entries) {
26818
26741
  hasher.update(rel);
26819
26742
  hasher.update("\x00");
26820
- hasher.update(readFileSync7(abs));
26743
+ hasher.update(readFileSync6(abs));
26821
26744
  hasher.update("\x00");
26822
26745
  }
26823
26746
  return hasher.digest("hex");
@@ -26836,17 +26759,17 @@ function stableStringify(value) {
26836
26759
  return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
26837
26760
  }
26838
26761
  function readInstalledVersion(rootDir, pkgName) {
26839
- const pkgJson = join8(rootDir, "node_modules", pkgName, "package.json");
26840
- if (!existsSync7(pkgJson))
26762
+ const pkgJson = join7(rootDir, "node_modules", pkgName, "package.json");
26763
+ if (!existsSync6(pkgJson))
26841
26764
  return null;
26842
26765
  try {
26843
- return JSON.parse(readFileSync7(pkgJson, "utf-8")).version ?? null;
26766
+ return JSON.parse(readFileSync6(pkgJson, "utf-8")).version ?? null;
26844
26767
  } catch {
26845
26768
  return null;
26846
26769
  }
26847
26770
  }
26848
26771
  function mtimeOf(path4) {
26849
- if (!existsSync7(path4))
26772
+ if (!existsSync6(path4))
26850
26773
  return 0;
26851
26774
  try {
26852
26775
  return statSync(path4).mtimeMs;
@@ -26876,58 +26799,64 @@ async function pAll(tasks, concurrency = DEFAULT_CONCURRENCY) {
26876
26799
  }
26877
26800
 
26878
26801
  // src/builder/module-builder.ts
26802
+ function jsxDevShimPlugin() {
26803
+ return {
26804
+ name: "jsx-dev-runtime-shim",
26805
+ setup(build2) {
26806
+ build2.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({
26807
+ path: "react-jsx-dev-runtime-shim",
26808
+ namespace: "jsx-dev-shim"
26809
+ }));
26810
+ build2.onLoad({ filter: /.*/, namespace: "jsx-dev-shim" }, () => ({
26811
+ contents: `import { jsx, jsxs, Fragment } from "react/jsx-runtime";
26812
+ export const jsxDEV = jsx;
26813
+ export const jsxsDEV = jsxs;
26814
+ export { Fragment };
26815
+ `,
26816
+ loader: "ts"
26817
+ }));
26818
+ }
26819
+ };
26820
+ }
26879
26821
  var CONTEXT_CLIENTS = [
26880
26822
  { name: "server", target: "bun", defines: { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT: "false" } },
26881
26823
  { name: "browser", target: "browser", defines: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" } }
26882
26824
  ];
26883
- var SHELL_EXTERNALS = [
26884
- "react",
26885
- "react-dom",
26886
- "react/jsx-runtime",
26887
- "react/jsx-dev-runtime",
26888
- "@arcote.tech/arc",
26889
- "@arcote.tech/arc-ds",
26890
- "@arcote.tech/arc-react",
26891
- "@arcote.tech/arc-auth",
26892
- "@arcote.tech/arc-utils",
26893
- "@arcote.tech/arc-workspace",
26894
- "@arcote.tech/platform"
26895
- ];
26896
26825
  function discoverPackages(rootDir) {
26897
- const rootPkg = JSON.parse(readFileSync8(join9(rootDir, "package.json"), "utf-8"));
26826
+ const rootPkg = JSON.parse(readFileSync7(join8(rootDir, "package.json"), "utf-8"));
26898
26827
  const workspaceGlobs = rootPkg.workspaces ?? [];
26899
26828
  const results = [];
26900
26829
  for (const glob2 of workspaceGlobs) {
26901
26830
  const base2 = glob2.replace("/*", "");
26902
- const baseDir = join9(rootDir, base2);
26903
- if (!existsSync8(baseDir))
26831
+ const baseDir = join8(rootDir, base2);
26832
+ if (!existsSync7(baseDir))
26904
26833
  continue;
26905
26834
  let entries;
26906
26835
  try {
26907
- entries = readdirSync5(baseDir);
26836
+ entries = readdirSync4(baseDir);
26908
26837
  } catch {
26909
26838
  continue;
26910
26839
  }
26911
26840
  for (const entry of entries) {
26912
- const pkgPath = join9(baseDir, entry, "package.json");
26913
- if (!existsSync8(pkgPath))
26841
+ const pkgPath = join8(baseDir, entry, "package.json");
26842
+ if (!existsSync7(pkgPath))
26914
26843
  continue;
26915
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
26844
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
26916
26845
  if (pkg.name?.startsWith("@arcote.tech/"))
26917
26846
  continue;
26918
- const pkgDir = join9(baseDir, entry);
26847
+ const pkgDir = join8(baseDir, entry);
26919
26848
  const candidates = [
26920
- join9(pkgDir, "src", "index.ts"),
26921
- join9(pkgDir, "src", "index.tsx"),
26922
- join9(pkgDir, "index.ts"),
26923
- join9(pkgDir, "index.tsx")
26849
+ join8(pkgDir, "src", "index.ts"),
26850
+ join8(pkgDir, "src", "index.tsx"),
26851
+ join8(pkgDir, "index.ts"),
26852
+ join8(pkgDir, "index.tsx")
26924
26853
  ];
26925
- const entrypoint = candidates.find((c) => existsSync8(c)) ?? null;
26854
+ const entrypoint = candidates.find((c) => existsSync7(c)) ?? null;
26926
26855
  if (!entrypoint)
26927
26856
  continue;
26928
26857
  results.push({
26929
26858
  name: pkg.name,
26930
- path: join9(baseDir, entry),
26859
+ path: join8(baseDir, entry),
26931
26860
  entrypoint,
26932
26861
  packageJson: pkg
26933
26862
  });
@@ -26956,7 +26885,7 @@ var sourceFilter = (rel) => {
26956
26885
  return true;
26957
26886
  };
26958
26887
  function pkgSourceHash(pkg) {
26959
- return sha256OfDir(join9(pkg.path, "src"), sourceFilter);
26888
+ return sha256OfDir(join8(pkg.path, "src"), sourceFilter);
26960
26889
  }
26961
26890
  function depVersionsHash(rootDir, pkg) {
26962
26891
  const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
@@ -26969,7 +26898,7 @@ function depVersionsHash(rootDir, pkg) {
26969
26898
  }
26970
26899
  async function buildContextClient(pkg, rootDir, client, cache, noCache) {
26971
26900
  const unitId = `context-pkg:${pkg.name}:${client.name}`;
26972
- const outDir = join9(pkg.path, "dist", client.name);
26901
+ const outDir = join8(pkg.path, "dist", client.name);
26973
26902
  const inputHash = sha256OfJson({
26974
26903
  src: pkgSourceHash(pkg),
26975
26904
  pkg: pkg.packageJson,
@@ -26978,21 +26907,23 @@ async function buildContextClient(pkg, rootDir, client, cache, noCache) {
26978
26907
  target: client.target,
26979
26908
  defines: client.defines
26980
26909
  });
26981
- if (!noCache && isCacheHit(cache, unitId, inputHash, [join9(outDir, "main", "index.js")])) {
26910
+ if (!noCache && isCacheHit(cache, unitId, inputHash, [join8(outDir, "main", "index.js")])) {
26982
26911
  console.log(` \u2713 cached: ${pkg.name} (${client.name})`);
26983
26912
  return { pkgName: pkg.name, client: client.name, declarationErrors: [], cached: true };
26984
26913
  }
26985
26914
  console.log(` building: ${pkg.name} (${client.name})`);
26986
26915
  const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
26987
- const deps = Object.keys(pkg.packageJson.dependencies ?? {});
26988
- const externals = [...peerDeps, ...deps];
26916
+ const allDeps = pkg.packageJson.dependencies ?? {};
26917
+ const npmDeps = Object.entries(allDeps).filter(([, spec]) => !spec.startsWith("workspace:")).map(([name]) => name);
26918
+ const externals = [...peerDeps, ...npmDeps];
26989
26919
  const result = await Bun.build({
26990
26920
  entrypoints: [pkg.entrypoint],
26991
- outdir: join9(outDir, "main"),
26921
+ outdir: join8(outDir, "main"),
26992
26922
  target: client.target,
26993
26923
  format: "esm",
26994
26924
  naming: "index.[ext]",
26995
26925
  external: externals,
26926
+ plugins: [jsxDevShimPlugin()],
26996
26927
  define: client.defines
26997
26928
  });
26998
26929
  if (!result.success) {
@@ -27025,115 +26956,142 @@ async function buildContextPackages(rootDir, packages, cache, noCache) {
27025
26956
  }
27026
26957
  return { declarationErrors };
27027
26958
  }
27028
- async function buildModulesBundle(rootDir, outDir, packages, cache, noCache) {
27029
- mkdirSync7(outDir, { recursive: true });
27030
- const unitId = "modules-bundle";
27031
- const pkgHashes = packages.map((p) => ({
27032
- name: p.name,
27033
- safeName: basename2(p.path),
27034
- srcHash: pkgSourceHash(p)
26959
+ async function buildModulesByChunks(rootDir, outDir, plan, cache, noCache) {
26960
+ mkdirSync6(outDir, { recursive: true });
26961
+ const i18nCollector = new Map;
26962
+ const aggregateModules = [];
26963
+ let allCached = true;
26964
+ for (const chunk of plan.chunks) {
26965
+ const members = plan.groups.get(chunk) ?? [];
26966
+ if (members.length === 0)
26967
+ continue;
26968
+ const chunkOutDir = join8(outDir, chunk);
26969
+ mkdirSync6(chunkOutDir, { recursive: true });
26970
+ const result = await buildChunkGroup(rootDir, chunkOutDir, chunk, members, cache, noCache, i18nCollector);
26971
+ aggregateModules.push(...result.modules);
26972
+ if (!result.cached)
26973
+ allCached = false;
26974
+ }
26975
+ await finalizeTranslations(rootDir, join8(outDir, ".."), i18nCollector);
26976
+ return { modules: aggregateModules, cached: allCached };
26977
+ }
26978
+ async function buildChunkGroup(rootDir, chunkOutDir, chunk, members, cache, noCache, i18nCollector) {
26979
+ const unitId = `modules-chunk:${chunk}`;
26980
+ const pkgHashes = members.map((m) => ({
26981
+ name: m.pkg.name,
26982
+ safeName: m.safeName,
26983
+ moduleName: m.moduleName,
26984
+ srcHash: pkgSourceHash(m.pkg)
27035
26985
  }));
27036
26986
  const inputHash = sha256OfJson({
26987
+ chunk,
27037
26988
  pkgHashes,
27038
26989
  externals: SHELL_EXTERNALS,
27039
26990
  define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" }
27040
26991
  });
27041
26992
  if (!noCache && isCacheHit(cache, unitId, inputHash)) {
27042
26993
  const existing = cache.units[unitId]?.outputHashes ?? {};
27043
- const modules = [];
27044
- for (const { safeName, name } of pkgHashes) {
27045
- const file = `${safeName}.js`;
27046
- const filePath = join9(outDir, file);
27047
- if (!existsSync8(filePath)) {
27048
- console.log(` rebuilding modules-bundle: output ${file} missing`);
27049
- return await actuallyBuild();
27050
- }
27051
- modules.push({ file, name, hash: existing[safeName] ?? sha256Hex(readFileSync8(filePath)) });
27052
- }
27053
- console.log(` \u2713 cached: modules-bundle (${modules.length} module(s))`);
27054
- return { modules, cached: true };
27055
- }
27056
- return await actuallyBuild();
27057
- async function actuallyBuild() {
27058
- console.log(` building: modules-bundle (${packages.length} package(s))`);
27059
- const tmpDir = join9(outDir, "_entries");
27060
- mkdirSync7(tmpDir, { recursive: true });
27061
- const entrypoints = [];
27062
- const fileToName = new Map;
27063
- for (const pkg of packages) {
27064
- const safeName = basename2(pkg.path);
27065
- const moduleName = pkg.name.includes("/") ? pkg.name.split("/").pop() : pkg.name;
27066
- fileToName.set(safeName, moduleName);
27067
- const wrapperFile = join9(tmpDir, `${safeName}.ts`);
27068
- writeFileSync7(wrapperFile, `export * from "${pkg.name}";
27069
- `);
27070
- entrypoints.push(wrapperFile);
27071
- }
27072
- const i18nCollector = new Map;
27073
- const arcExternalPlugin = {
27074
- name: "arc-external",
27075
- setup(build2) {
27076
- build2.onResolve({ filter: /^@arcote\.tech\// }, (args) => {
27077
- return { path: args.path, external: true };
27078
- });
27079
- }
27080
- };
27081
- const result = await Bun.build({
27082
- entrypoints,
27083
- outdir: outDir,
27084
- splitting: true,
27085
- format: "esm",
27086
- target: "browser",
27087
- external: SHELL_EXTERNALS,
27088
- plugins: [arcExternalPlugin, i18nExtractPlugin(i18nCollector, rootDir)],
27089
- naming: "[name].[ext]",
27090
- define: {
27091
- ONLY_SERVER: "false",
27092
- ONLY_BROWSER: "true",
27093
- ONLY_CLIENT: "true"
26994
+ const modules2 = [];
26995
+ let missing = false;
26996
+ for (const h of pkgHashes) {
26997
+ const file = `${h.safeName}.js`;
26998
+ const filePath = join8(chunkOutDir, file);
26999
+ if (!existsSync7(filePath)) {
27000
+ missing = true;
27001
+ break;
27094
27002
  }
27095
- });
27096
- if (!result.success) {
27097
- console.error("Modules bundle build failed:");
27098
- for (const log2 of result.logs)
27099
- console.error(log2);
27100
- throw new Error("Module build failed");
27101
- }
27102
- await finalizeTranslations(rootDir, join9(outDir, ".."), i18nCollector);
27103
- rmSync2(tmpDir, { recursive: true, force: true });
27104
- const outputHashes = {};
27105
- const modules = result.outputs.filter((o) => o.kind === "entry-point").map((o) => {
27106
- const file = basename2(o.path);
27107
- const safeName = file.replace(/\.js$/, "");
27108
- const bytes = readFileSync8(o.path);
27109
- const hash = sha256Hex(bytes);
27110
- outputHashes[safeName] = hash;
27111
- return {
27003
+ modules2.push({
27112
27004
  file,
27113
- name: fileToName.get(safeName) ?? safeName,
27114
- hash
27115
- };
27116
- });
27117
- updateCache(cache, unitId, inputHash, { outputHashes });
27118
- return { modules, cached: false };
27005
+ name: h.moduleName,
27006
+ chunk,
27007
+ hash: existing[h.safeName] ?? sha256Hex(readFileSync7(filePath))
27008
+ });
27009
+ }
27010
+ if (!missing) {
27011
+ console.log(` \u2713 cached: ${unitId} (${modules2.length} module(s))`);
27012
+ return { modules: modules2, cached: true };
27013
+ }
27014
+ console.log(` rebuilding ${unitId}: output file missing`);
27015
+ }
27016
+ console.log(` building: ${unitId} (${members.length} module(s))`);
27017
+ const tmpDir = join8(chunkOutDir, "_entries");
27018
+ mkdirSync6(tmpDir, { recursive: true });
27019
+ const entrypoints = [];
27020
+ const fileToModuleName = new Map;
27021
+ for (const m of members) {
27022
+ fileToModuleName.set(m.safeName, m.moduleName);
27023
+ const wrapperFile = join8(tmpDir, `${m.safeName}.ts`);
27024
+ writeFileSync6(wrapperFile, `export * from "${m.pkg.name}";
27025
+ `);
27026
+ entrypoints.push(wrapperFile);
27027
+ }
27028
+ const arcExternalPlugin = {
27029
+ name: "arc-external",
27030
+ setup(build2) {
27031
+ build2.onResolve({ filter: /^@arcote\.tech\// }, (args) => {
27032
+ return { path: args.path, external: true };
27033
+ });
27034
+ }
27035
+ };
27036
+ const result = await Bun.build({
27037
+ entrypoints,
27038
+ outdir: chunkOutDir,
27039
+ splitting: true,
27040
+ format: "esm",
27041
+ target: "browser",
27042
+ external: [...SHELL_EXTERNALS],
27043
+ plugins: [
27044
+ arcExternalPlugin,
27045
+ jsxDevShimPlugin(),
27046
+ i18nExtractPlugin(i18nCollector, rootDir)
27047
+ ],
27048
+ naming: "[name].[ext]",
27049
+ define: {
27050
+ ONLY_SERVER: "false",
27051
+ ONLY_BROWSER: "true",
27052
+ ONLY_CLIENT: "true"
27053
+ }
27054
+ });
27055
+ if (!result.success) {
27056
+ console.error(`Chunk "${chunk}" build failed:`);
27057
+ for (const log2 of result.logs)
27058
+ console.error(log2);
27059
+ throw new Error(`Module chunk build failed: ${chunk}`);
27119
27060
  }
27061
+ rmSync(tmpDir, { recursive: true, force: true });
27062
+ const outputHashes = {};
27063
+ const modules = result.outputs.filter((o) => o.kind === "entry-point").map((o) => {
27064
+ const file = basename2(o.path);
27065
+ const safeName = file.replace(/\.js$/, "");
27066
+ const bytes = readFileSync7(o.path);
27067
+ const hash = sha256Hex(bytes);
27068
+ outputHashes[safeName] = hash;
27069
+ return {
27070
+ file,
27071
+ name: fileToModuleName.get(safeName) ?? safeName,
27072
+ chunk,
27073
+ hash
27074
+ };
27075
+ });
27076
+ updateCache(cache, unitId, inputHash, { outputHashes });
27077
+ return { modules, cached: false };
27120
27078
  }
27121
27079
  async function buildTranslations(rootDir, arcDir, cache, noCache) {
27122
- const localesDir = join9(rootDir, "locales");
27123
- if (!existsSync8(localesDir))
27080
+ const localesDir = join8(rootDir, "locales");
27081
+ if (!existsSync7(localesDir))
27124
27082
  return;
27125
27083
  const unitId = "translations";
27126
- const poFiles = readdirSync5(localesDir).filter((f) => f.endsWith(".po")).map((f) => join9(localesDir, f));
27084
+ const poFiles = readdirSync4(localesDir).filter((f) => f.endsWith(".po")).map((f) => join8(localesDir, f));
27127
27085
  if (poFiles.length === 0)
27128
27086
  return;
27129
27087
  const inputHash = sha256OfFiles(poFiles);
27130
- if (!noCache && isCacheHit(cache, unitId, inputHash, [join9(arcDir, "locales")])) {
27088
+ if (!noCache && isCacheHit(cache, unitId, inputHash, [join8(arcDir, "locales")])) {
27131
27089
  console.log(` \u2713 cached: translations`);
27132
27090
  return;
27133
27091
  }
27134
27092
  console.log(` building: translations (${poFiles.length} catalog(s))`);
27135
- compileAllCatalogs(localesDir, join9(arcDir, "locales"));
27136
- const jsonFiles = readdirSync5(join9(arcDir, "locales")).filter((f) => f.endsWith(".json")).map((f) => join9(arcDir, "locales", f));
27093
+ compileAllCatalogs(localesDir, join8(arcDir, "locales"));
27094
+ const jsonFiles = readdirSync4(join8(arcDir, "locales")).filter((f) => f.endsWith(".json")).map((f) => join8(arcDir, "locales", f));
27137
27095
  const outputHash = sha256OfFiles(jsonFiles);
27138
27096
  updateCache(cache, unitId, inputHash, { outputHash });
27139
27097
  }
@@ -27197,10 +27155,10 @@ var TAILWIND_INPUT_TEMPLATE = (rootRel) => `@import "tailwindcss";
27197
27155
  }
27198
27156
  `;
27199
27157
  async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache) {
27200
- mkdirSync7(arcDir, { recursive: true });
27201
- const inputCss = join9(arcDir, "_input.css");
27202
- const outputCss = join9(arcDir, "styles.css");
27203
- const themeOutput = join9(arcDir, "theme.css");
27158
+ mkdirSync6(arcDir, { recursive: true });
27159
+ const inputCss = join8(arcDir, "_input.css");
27160
+ const outputCss = join8(arcDir, "styles.css");
27161
+ const themeOutput = join8(arcDir, "theme.css");
27204
27162
  const rootRel = relative3(arcDir, rootDir).replace(/\\/g, "/");
27205
27163
  const inputCssContent = TAILWIND_INPUT_TEMPLATE(rootRel);
27206
27164
  const tsxFilter = (rel) => {
@@ -27212,15 +27170,15 @@ async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache)
27212
27170
  };
27213
27171
  const wsHashes = {};
27214
27172
  for (const p of packages) {
27215
- wsHashes[p.name] = sha256OfDir(join9(p.path, "src"), tsxFilter);
27173
+ wsHashes[p.name] = sha256OfDir(join8(p.path, "src"), tsxFilter);
27216
27174
  }
27217
- const platformSrc = join9(rootDir, "node_modules", "@arcote.tech", "platform", "src");
27218
- const arcDsSrc = join9(rootDir, "node_modules", "@arcote.tech", "arc-ds", "src");
27175
+ const platformSrc = join8(rootDir, "node_modules", "@arcote.tech", "platform", "src");
27176
+ const arcDsSrc = join8(rootDir, "node_modules", "@arcote.tech", "arc-ds", "src");
27219
27177
  const frameworkHashes = {
27220
27178
  platform: sha256OfDir(platformSrc, tsxFilter),
27221
27179
  arcDs: sha256OfDir(arcDsSrc, tsxFilter)
27222
27180
  };
27223
- const themeContent = themePath && existsSync8(join9(rootDir, themePath)) ? readFileSync8(join9(rootDir, themePath)) : null;
27181
+ const themeContent = themePath && existsSync7(join8(rootDir, themePath)) ? readFileSync7(join8(rootDir, themePath)) : null;
27224
27182
  const themeHash = themeContent ? sha256Hex(themeContent) : null;
27225
27183
  const unitId = "styles";
27226
27184
  const inputHash = sha256OfJson({
@@ -27237,140 +27195,57 @@ async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache)
27237
27195
  return;
27238
27196
  }
27239
27197
  console.log(` building: styles`);
27240
- writeFileSync7(inputCss, inputCssContent);
27198
+ writeFileSync6(inputCss, inputCssContent);
27241
27199
  execSync(`bunx @tailwindcss/cli -i ${inputCss} -o ${outputCss} --minify`, {
27242
27200
  cwd: rootDir,
27243
27201
  stdio: "inherit"
27244
27202
  });
27245
27203
  if (themePath && themeContent) {
27246
- writeFileSync7(themeOutput, themeContent);
27204
+ writeFileSync6(themeOutput, themeContent);
27247
27205
  }
27248
27206
  const outFiles = [outputCss];
27249
- if (themePath && existsSync8(themeOutput))
27207
+ if (themePath && existsSync7(themeOutput))
27250
27208
  outFiles.push(themeOutput);
27251
27209
  const outputHash = sha256OfFiles(outFiles);
27252
27210
  updateCache(cache, unitId, inputHash, { outputHash });
27253
27211
  }
27254
27212
 
27255
- // src/builder/dependency-collector.ts
27256
- import { createHash } from "crypto";
27257
- import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
27258
- import { basename as basename3, join as join10 } from "path";
27259
- var FRAMEWORK_PEERS = [
27260
- "@arcote.tech/arc",
27261
- "@arcote.tech/arc-ds",
27262
- "@arcote.tech/arc-react",
27263
- "@arcote.tech/platform",
27264
- "react",
27265
- "react-dom"
27266
- ];
27267
- function collectFrameworkDeps(arcDir, rootDir, packages) {
27268
- mkdirSync8(arcDir, { recursive: true });
27269
- const versions = resolveFrameworkVersions(rootDir, packages);
27270
- const manifest = {
27271
- name: "arc-platform-framework",
27272
- private: true,
27273
- type: "module",
27274
- dependencies: versions
27275
- };
27276
- const manifestPath = join10(arcDir, "package.json");
27277
- writeFileSync8(manifestPath, JSON.stringify(manifest, null, 2) + `
27278
- `);
27279
- const rootLock = join10(rootDir, "bun.lock");
27280
- const targetLock = join10(arcDir, "bun.lock");
27281
- if (existsSync9(rootLock)) {
27282
- copyFileSync(rootLock, targetLock);
27283
- }
27284
- const hash = sha256OfFiles2([manifestPath, targetLock]);
27285
- writeFileSync8(join10(arcDir, ".deps-hash"), hash + `
27286
- `);
27287
- return { hash, manifestPath };
27288
- }
27289
- function collectModuleDeps(arcDir, pkg) {
27290
- const safeName = basename3(pkg.path);
27291
- const moduleDir = join10(arcDir, "modules", safeName);
27292
- mkdirSync8(moduleDir, { recursive: true });
27293
- const pkgDeps = pkg.packageJson.dependencies ?? {};
27294
- const filtered = {};
27295
- const frameworkSet = new Set(FRAMEWORK_PEERS);
27296
- for (const [name, spec] of Object.entries(pkgDeps)) {
27297
- if (frameworkSet.has(name))
27298
- continue;
27299
- if (spec.startsWith("workspace:"))
27300
- continue;
27301
- filtered[name] = spec;
27302
- }
27303
- const manifest = {
27304
- name: `arc-module-${safeName}`,
27305
- private: true,
27306
- type: "module",
27307
- dependencies: filtered
27308
- };
27309
- const manifestPath = join10(moduleDir, "package.json");
27310
- writeFileSync8(manifestPath, JSON.stringify(manifest, null, 2) + `
27311
- `);
27312
- const hash = sha256OfFiles2([manifestPath]);
27313
- writeFileSync8(join10(moduleDir, ".deps-hash"), hash + `
27314
- `);
27315
- return { hash, manifestPath };
27316
- }
27317
- function resolveFrameworkVersions(rootDir, packages) {
27318
- const rootPkg = JSON.parse(readFileSync9(join10(rootDir, "package.json"), "utf-8"));
27319
- const rootDeps = rootPkg.dependencies ?? {};
27320
- const out = {};
27321
- for (const name of FRAMEWORK_PEERS) {
27322
- const rootSpec = rootDeps[name];
27323
- if (rootSpec) {
27324
- out[name] = rootSpec;
27325
- continue;
27326
- }
27327
- let found;
27328
- for (const pkg of packages) {
27329
- const spec = (pkg.packageJson.dependencies ?? {})[name];
27330
- if (spec) {
27331
- found = spec;
27332
- break;
27333
- }
27334
- }
27335
- out[name] = found ?? "*";
27336
- }
27337
- return out;
27338
- }
27339
- function sha256OfFiles2(paths) {
27340
- const hash = createHash("sha256");
27341
- for (const p of paths.sort()) {
27342
- if (!existsSync9(p))
27343
- continue;
27344
- hash.update(p);
27345
- hash.update("\x00");
27346
- hash.update(readFileSync9(p));
27347
- hash.update("\x00");
27348
- }
27349
- return hash.digest("hex");
27350
- }
27351
-
27352
27213
  // src/builder/access-extractor.ts
27353
27214
  var {spawn: spawn2 } = globalThis.Bun;
27354
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "fs";
27355
- import { tmpdir } from "os";
27356
- import { basename as basename4, join as join11 } from "path";
27357
- async function extractAccessMap(arcDir, packages) {
27358
- const serverBundles = packages.filter((p) => isContextPackage(p.packageJson)).map((p) => {
27359
- const v06Path = join11(arcDir, "modules", basename4(p.path), "server.js");
27360
- const legacyPath = join11(p.path, "dist", "server", "main", "index.js");
27361
- return { name: p.name, path: v06Path, fallback: legacyPath };
27362
- });
27363
- const outPath = join11(arcDir, "access.json");
27364
- mkdirSync9(arcDir, { recursive: true });
27365
- const workerPath = join11(tmpdir(), `arc-access-extractor-${Date.now()}.mjs`);
27366
- writeFileSync9(workerPath, WORKER_SOURCE);
27215
+ import {
27216
+ existsSync as existsSync8,
27217
+ mkdirSync as mkdirSync7,
27218
+ readFileSync as readFileSync8,
27219
+ realpathSync as realpathSync2,
27220
+ unlinkSync as unlinkSync2,
27221
+ writeFileSync as writeFileSync7
27222
+ } from "fs";
27223
+ import { dirname as dirname6, join as join9 } from "path";
27224
+ import { fileURLToPath as fileURLToPath5 } from "url";
27225
+ function locatePlatformServerEntry() {
27226
+ const here = fileURLToPath5(import.meta.url);
27227
+ const arcRoot = realpathSync2(dirname6(dirname6(dirname6(dirname6(here)))));
27228
+ return join9(arcRoot, "packages", "platform", "src", "index.server.ts");
27229
+ }
27230
+ async function extractAccessMap(rootDir, packages) {
27231
+ const serverBundles = packages.filter((p) => isContextPackage(p.packageJson)).map((p) => ({
27232
+ name: p.name,
27233
+ path: join9(p.path, "dist", "server", "main", "index.js")
27234
+ })).filter((b) => existsSync8(b.path));
27235
+ const workerDir = join9(rootDir, ".arc", ".tmp");
27236
+ mkdirSync7(workerDir, { recursive: true });
27237
+ const workerPath = join9(workerDir, `access-extractor-${Date.now()}.mjs`);
27238
+ const outPath = join9(workerDir, `access-${Date.now()}.json`);
27239
+ writeFileSync7(workerPath, WORKER_SOURCE);
27367
27240
  try {
27368
27241
  const proc2 = spawn2({
27369
27242
  cmd: ["bun", "run", workerPath],
27243
+ cwd: rootDir,
27370
27244
  env: {
27371
27245
  ...process.env,
27372
27246
  ARC_ACCESS_BUNDLES: JSON.stringify(serverBundles),
27373
- ARC_ACCESS_OUT: outPath
27247
+ ARC_ACCESS_OUT: outPath,
27248
+ ARC_PLATFORM_ENTRY: locatePlatformServerEntry()
27374
27249
  },
27375
27250
  stdout: "pipe",
27376
27251
  stderr: "inherit"
@@ -27379,17 +27254,20 @@ async function extractAccessMap(arcDir, packages) {
27379
27254
  if (exit !== 0) {
27380
27255
  throw new Error(`access-extractor subprocess exited with ${exit}`);
27381
27256
  }
27382
- return JSON.parse(readFileSync10(outPath, "utf-8"));
27257
+ return JSON.parse(readFileSync8(outPath, "utf-8"));
27383
27258
  } finally {
27384
27259
  try {
27385
27260
  unlinkSync2(workerPath);
27386
27261
  } catch {}
27262
+ try {
27263
+ unlinkSync2(outPath);
27264
+ } catch {}
27387
27265
  }
27388
27266
  }
27389
27267
  var WORKER_SOURCE = `
27390
- import { existsSync } from "node:fs";
27391
-
27392
27268
  globalThis.ONLY_SERVER = true;
27269
+ globalThis.ONLY_BROWSER = false;
27270
+ globalThis.ONLY_CLIENT = false;
27393
27271
 
27394
27272
  const bundles = JSON.parse(process.env.ARC_ACCESS_BUNDLES || "[]");
27395
27273
  const out = process.env.ARC_ACCESS_OUT;
@@ -27398,20 +27276,16 @@ if (!out) {
27398
27276
  process.exit(2);
27399
27277
  }
27400
27278
 
27401
- const platform = await import("@arcote.tech/platform");
27279
+ // Direct file-path import \u2014 bypasses node_modules resolution entirely.
27280
+ // Avoids quirks with bun-link snapshots holding stale package.json exports.
27281
+ const platform = await import(process.env.ARC_PLATFORM_ENTRY);
27402
27282
 
27403
- for (const { name, path, fallback } of bundles) {
27404
- const target = existsSync(path) ? path : fallback;
27405
- if (!target || !existsSync(target)) {
27406
- // No server bundle on either path \u2014 module has no protected access rules
27407
- // to discover. Skip silently rather than logging a misleading error.
27408
- continue;
27409
- }
27283
+ for (const { name, path } of bundles) {
27410
27284
  try {
27411
- await import(target);
27285
+ await import(path);
27412
27286
  } catch (e) {
27413
- console.error("[access-extractor-worker] failed to import", name, "from", target, e);
27414
- // Continue \u2014 partial access map is better than total failure.
27287
+ console.error("[access-extractor-worker] failed to import", name, ":", e.message);
27288
+ // Partial map is better than total failure.
27415
27289
  }
27416
27290
  }
27417
27291
 
@@ -27429,6 +27303,123 @@ const { writeFileSync } = await import("node:fs");
27429
27303
  writeFileSync(out, JSON.stringify(result, null, 2) + "\\n");
27430
27304
  `.trim();
27431
27305
 
27306
+ // src/builder/chunk-planner.ts
27307
+ import { basename as basename3 } from "path";
27308
+ var PUBLIC_CHUNK = "public";
27309
+ function planChunks(packages, accessMap) {
27310
+ const assignments = new Map;
27311
+ const groups = new Map;
27312
+ for (const pkg of packages) {
27313
+ const moduleName = moduleNameOf(pkg.name);
27314
+ const access = accessMap[moduleName];
27315
+ const chunk = resolveChunk(moduleName, access);
27316
+ const entry = {
27317
+ pkg,
27318
+ chunk,
27319
+ moduleName,
27320
+ safeName: basename3(pkg.path)
27321
+ };
27322
+ assignments.set(moduleName, entry);
27323
+ const bucket = groups.get(chunk);
27324
+ if (bucket) {
27325
+ bucket.push(entry);
27326
+ } else {
27327
+ groups.set(chunk, [entry]);
27328
+ }
27329
+ }
27330
+ const chunks = [...groups.keys()].sort((a, b) => {
27331
+ if (a === PUBLIC_CHUNK)
27332
+ return -1;
27333
+ if (b === PUBLIC_CHUNK)
27334
+ return 1;
27335
+ return a.localeCompare(b);
27336
+ });
27337
+ return { assignments, groups, chunks };
27338
+ }
27339
+ function moduleNameOf(pkgName) {
27340
+ return pkgName.includes("/") ? pkgName.split("/").pop() : pkgName;
27341
+ }
27342
+ function resolveChunk(moduleName, access) {
27343
+ if (!access || access.rules.length === 0)
27344
+ return PUBLIC_CHUNK;
27345
+ const tokenNames = new Set(access.rules.map((r) => r.token.name));
27346
+ if (tokenNames.size === 1) {
27347
+ return [...tokenNames][0];
27348
+ }
27349
+ const list = [...tokenNames].sort().join(", ");
27350
+ throw new Error(`Module "${moduleName}" has access rules for multiple tokens [${list}]. ` + `Multi-token modules are not supported \u2014 split the module or unify on a single token type.`);
27351
+ }
27352
+
27353
+ // src/builder/dependency-collector.ts
27354
+ import { createHash } from "crypto";
27355
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
27356
+ import { basename as basename4, join as join10 } from "path";
27357
+ function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = []) {
27358
+ mkdirSync8(arcDir, { recursive: true });
27359
+ const versions = resolveFrameworkVersions(rootDir, packages);
27360
+ for (const { name, version } of sharedDeps) {
27361
+ versions[name] = version;
27362
+ }
27363
+ const manifest = {
27364
+ name: "arc-platform-framework",
27365
+ private: true,
27366
+ type: "module",
27367
+ dependencies: versions
27368
+ };
27369
+ const manifestPath = join10(arcDir, "package.json");
27370
+ writeFileSync8(manifestPath, JSON.stringify(manifest, null, 2) + `
27371
+ `);
27372
+ const hash = sha256Hex2(readFileSync9(manifestPath));
27373
+ writeFileSync8(join10(arcDir, ".deps-hash"), hash + `
27374
+ `);
27375
+ return { hash, manifestPath };
27376
+ }
27377
+ function sha256Hex2(bytes) {
27378
+ return createHash("sha256").update(bytes).digest("hex");
27379
+ }
27380
+ function resolveFrameworkVersions(rootDir, packages) {
27381
+ const rootPkg = JSON.parse(readFileSync9(join10(rootDir, "package.json"), "utf-8"));
27382
+ const rootDeps = rootPkg.dependencies ?? {};
27383
+ const rootDevDeps = rootPkg.devDependencies ?? {};
27384
+ const required = new Set(FRAMEWORK_PEERS);
27385
+ for (const pkg of packages) {
27386
+ const peers = pkg.packageJson.peerDependencies ?? {};
27387
+ const deps = pkg.packageJson.dependencies ?? {};
27388
+ for (const name of Object.keys(peers)) {
27389
+ if (name.startsWith("@arcote.tech/"))
27390
+ required.add(name);
27391
+ }
27392
+ for (const [name, spec] of Object.entries(deps)) {
27393
+ if (spec.startsWith("workspace:"))
27394
+ continue;
27395
+ if (name.startsWith("@arcote.tech/") || !isFrameworkExternal(name))
27396
+ continue;
27397
+ required.add(name);
27398
+ }
27399
+ }
27400
+ const out = {};
27401
+ for (const name of required) {
27402
+ const rootSpec = rootDeps[name] ?? rootDevDeps[name];
27403
+ if (rootSpec) {
27404
+ out[name] = rootSpec;
27405
+ continue;
27406
+ }
27407
+ let found;
27408
+ for (const pkg of packages) {
27409
+ const spec = (pkg.packageJson.dependencies ?? {})[name] ?? (pkg.packageJson.peerDependencies ?? {})[name];
27410
+ if (spec) {
27411
+ found = spec;
27412
+ break;
27413
+ }
27414
+ }
27415
+ out[name] = found ?? "*";
27416
+ }
27417
+ return out;
27418
+ }
27419
+ function isFrameworkExternal(_name) {
27420
+ return true;
27421
+ }
27422
+
27432
27423
  // src/platform/shared.ts
27433
27424
  var C = {
27434
27425
  reset: "\x1B[0m",
@@ -27445,23 +27436,23 @@ function resolveWorkspace() {
27445
27436
  err("No package.json found");
27446
27437
  process.exit(1);
27447
27438
  }
27448
- const rootDir = dirname6(packageJsonPath);
27449
- const rootPkg = JSON.parse(readFileSync11(packageJsonPath, "utf-8"));
27439
+ const rootDir = dirname7(packageJsonPath);
27440
+ const rootPkg = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
27450
27441
  const appName = rootPkg.name ?? "Arc App";
27451
- const arcDir = join12(rootDir, ".arc", "platform");
27442
+ const arcDir = join11(rootDir, ".arc", "platform");
27452
27443
  log2("Scanning workspaces...");
27453
27444
  const packages = discoverPackages(rootDir);
27454
- ok(`Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`);
27455
- if (packages.length === 0 && process.env.ARC_DEPLOY_API !== "1") {
27456
- err("No workspace packages found.");
27457
- process.exit(1);
27445
+ if (packages.length > 0) {
27446
+ ok(`Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`);
27447
+ } else {
27448
+ log2("No workspace packages found \u2014 assuming image runtime mode.");
27458
27449
  }
27459
27450
  let manifest;
27460
27451
  for (const name of ["manifest.json", "manifest.webmanifest"]) {
27461
- const manifestPath = join12(rootDir, name);
27452
+ const manifestPath = join11(rootDir, name);
27462
27453
  if (existsSync10(manifestPath)) {
27463
27454
  try {
27464
- const data = JSON.parse(readFileSync11(manifestPath, "utf-8"));
27455
+ const data = JSON.parse(readFileSync10(manifestPath, "utf-8"));
27465
27456
  const icons = data.icons;
27466
27457
  manifest = {
27467
27458
  path: manifestPath,
@@ -27480,10 +27471,10 @@ function resolveWorkspace() {
27480
27471
  rootPkg,
27481
27472
  appName,
27482
27473
  arcDir,
27483
- modulesDir: join12(arcDir, "modules"),
27484
- shellDir: join12(arcDir, "shell"),
27485
- assetsDir: join12(arcDir, "assets"),
27486
- publicDir: join12(rootDir, "public"),
27474
+ modulesDir: join11(arcDir, "modules"),
27475
+ shellDir: join11(arcDir, "shell"),
27476
+ assetsDir: join11(arcDir, "assets"),
27477
+ publicDir: join11(rootDir, "public"),
27487
27478
  packages,
27488
27479
  manifest
27489
27480
  };
@@ -27493,29 +27484,28 @@ async function buildAll(ws, opts = {}) {
27493
27484
  const noCache = opts.noCache ?? false;
27494
27485
  const themePath = ws.rootPkg.arc?.theme;
27495
27486
  log2(`Building (concurrency parallel${noCache ? ", no-cache" : ""})...`);
27496
- const [, modulesResult] = await Promise.all([
27497
- buildContextPackages(ws.rootDir, ws.packages, cache, noCache),
27498
- buildModulesBundle(ws.rootDir, ws.modulesDir, ws.packages, cache, noCache),
27499
- buildShell2(ws, cache, noCache),
27487
+ await buildContextPackages(ws.rootDir, ws.packages, cache, noCache);
27488
+ copyContextServerBundles(ws);
27489
+ const accessMap = await extractAccessMap(ws.rootDir, ws.packages);
27490
+ mkdirSync9(ws.arcDir, { recursive: true });
27491
+ writeFileSync9(join11(ws.arcDir, "access.json"), JSON.stringify(accessMap, null, 2) + `
27492
+ `);
27493
+ const plan = planChunks(ws.packages, accessMap);
27494
+ ok(`Chunks: ${plan.chunks.map((c) => `${c}(${plan.groups.get(c)?.length ?? 0})`).join(", ")}`);
27495
+ const [modulesResult] = await Promise.all([
27496
+ buildModulesByChunks(ws.rootDir, ws.modulesDir, plan, cache, noCache),
27497
+ buildShell(ws, cache, noCache),
27500
27498
  buildStyles(ws.rootDir, ws.arcDir, ws.packages, themePath, cache, noCache),
27501
27499
  copyBrowserAssets(ws, cache, noCache),
27502
27500
  buildTranslations(ws.rootDir, ws.arcDir, cache, noCache)
27503
27501
  ]);
27504
- saveBuildCache(ws.arcDir, cache);
27505
27502
  collectFrameworkDeps(ws.arcDir, ws.rootDir, ws.packages);
27506
- for (const pkg of ws.packages) {
27507
- collectModuleDeps(ws.arcDir, pkg);
27508
- }
27509
- try {
27510
- await extractAccessMap(ws.arcDir, ws.packages);
27511
- } catch (e) {
27512
- err(`access-extractor failed: ${e.message}`);
27513
- }
27514
- const finalManifest = assembleManifest(ws, modulesResult.modules, cache);
27515
- writeFileSync10(join12(ws.modulesDir, "manifest.json"), JSON.stringify(finalManifest, null, 2));
27503
+ saveBuildCache(ws.arcDir, cache);
27504
+ const finalManifest = assembleManifest(ws, modulesResult.modules, plan.chunks, cache);
27505
+ writeFileSync9(join11(ws.modulesDir, "manifest.json"), JSON.stringify(finalManifest, null, 2));
27516
27506
  return finalManifest;
27517
27507
  }
27518
- function assembleManifest(ws, modules, cache) {
27508
+ function assembleManifest(ws, modules, chunks, cache) {
27519
27509
  const shellEntries = {};
27520
27510
  for (const [unitId, entry] of Object.entries(cache.units)) {
27521
27511
  if (unitId.startsWith("shell:") && entry.outputHash) {
@@ -27526,30 +27516,47 @@ function assembleManifest(ws, modules, cache) {
27526
27516
  const stylesHash = cache.units["styles"]?.outputHash ?? "";
27527
27517
  return {
27528
27518
  modules,
27519
+ chunks,
27529
27520
  shellHash,
27530
27521
  stylesHash,
27531
27522
  buildTime: new Date().toISOString()
27532
27523
  };
27533
27524
  }
27525
+ function copyContextServerBundles(ws) {
27526
+ const outDir = join11(ws.arcDir, "server");
27527
+ mkdirSync9(outDir, { recursive: true });
27528
+ for (const pkg of ws.packages) {
27529
+ if (!isContextPackage(pkg.packageJson))
27530
+ continue;
27531
+ const src = join11(pkg.path, "dist", "server", "main", "index.js");
27532
+ if (!existsSync10(src)) {
27533
+ err(`Server bundle missing for ${pkg.name}: ${src}`);
27534
+ continue;
27535
+ }
27536
+ const safeName = pkg.path.split("/").pop();
27537
+ const dst = join11(outDir, `${safeName}.js`);
27538
+ copyFileSync(src, dst);
27539
+ }
27540
+ }
27534
27541
  function resolveAssetSource(from, pkgDir, rootDir) {
27535
27542
  if (from.startsWith("./") || from.startsWith("../")) {
27536
- const resolved = join12(pkgDir, from);
27543
+ const resolved = join11(pkgDir, from);
27537
27544
  return existsSync10(resolved) ? resolved : null;
27538
27545
  }
27539
27546
  const candidates = [
27540
- join12(rootDir, "node_modules", from),
27541
- join12(pkgDir, "node_modules", from)
27547
+ join11(rootDir, "node_modules", from),
27548
+ join11(pkgDir, "node_modules", from)
27542
27549
  ];
27543
27550
  for (const c of candidates) {
27544
27551
  if (existsSync10(c))
27545
27552
  return c;
27546
27553
  }
27547
- const bunCacheDir = join12(rootDir, "node_modules", ".bun");
27554
+ const bunCacheDir = join11(rootDir, "node_modules", ".bun");
27548
27555
  if (existsSync10(bunCacheDir)) {
27549
- for (const entry of readdirSync6(bunCacheDir, { withFileTypes: true })) {
27556
+ for (const entry of readdirSync5(bunCacheDir, { withFileTypes: true })) {
27550
27557
  if (!entry.isDirectory())
27551
27558
  continue;
27552
- const candidate = join12(bunCacheDir, entry.name, "node_modules", from);
27559
+ const candidate = join11(bunCacheDir, entry.name, "node_modules", from);
27553
27560
  if (existsSync10(candidate))
27554
27561
  return candidate;
27555
27562
  }
@@ -27557,11 +27564,11 @@ function resolveAssetSource(from, pkgDir, rootDir) {
27557
27564
  return null;
27558
27565
  }
27559
27566
  function readBrowserAssets(pkgDir) {
27560
- const pkgJsonPath = join12(pkgDir, "package.json");
27567
+ const pkgJsonPath = join11(pkgDir, "package.json");
27561
27568
  if (!existsSync10(pkgJsonPath))
27562
27569
  return [];
27563
27570
  try {
27564
- const pkg = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
27571
+ const pkg = JSON.parse(readFileSync10(pkgJsonPath, "utf-8"));
27565
27572
  const assets = pkg.arc?.browserAssets;
27566
27573
  if (!Array.isArray(assets))
27567
27574
  return [];
@@ -27571,14 +27578,14 @@ function readBrowserAssets(pkgDir) {
27571
27578
  }
27572
27579
  }
27573
27580
  function discoverBrowserAssets(ws) {
27574
- const arcDir = join12(ws.rootDir, "node_modules", "@arcote.tech");
27581
+ const arcDir = join11(ws.rootDir, "node_modules", "@arcote.tech");
27575
27582
  if (!existsSync10(arcDir))
27576
27583
  return [];
27577
27584
  const out = [];
27578
- for (const entry of readdirSync6(arcDir, { withFileTypes: true })) {
27585
+ for (const entry of readdirSync5(arcDir, { withFileTypes: true })) {
27579
27586
  if (!entry.isDirectory() && !entry.isSymbolicLink())
27580
27587
  continue;
27581
- const pkgDir = join12(arcDir, entry.name);
27588
+ const pkgDir = join11(arcDir, entry.name);
27582
27589
  const assets = readBrowserAssets(pkgDir);
27583
27590
  for (const asset of assets) {
27584
27591
  const src = resolveAssetSource(asset.from, pkgDir, ws.rootDir);
@@ -27592,7 +27599,7 @@ function discoverBrowserAssets(ws) {
27592
27599
  return out;
27593
27600
  }
27594
27601
  async function copyBrowserAssets(ws, cache, noCache) {
27595
- mkdirSync10(ws.assetsDir, { recursive: true });
27602
+ mkdirSync9(ws.assetsDir, { recursive: true });
27596
27603
  const assets = discoverBrowserAssets(ws);
27597
27604
  if (assets.length === 0)
27598
27605
  return;
@@ -27603,7 +27610,7 @@ async function copyBrowserAssets(ws, cache, noCache) {
27603
27610
  to: a.to,
27604
27611
  mtime: mtimeOf(a.src)
27605
27612
  })));
27606
- const requiredOutputs = assets.map((a) => join12(ws.assetsDir, a.to));
27613
+ const requiredOutputs = assets.map((a) => join11(ws.assetsDir, a.to));
27607
27614
  if (!noCache && isCacheHit(cache, unitId, inputHash, requiredOutputs)) {
27608
27615
  console.log(` \u2713 cached: browser-assets (${assets.length})`);
27609
27616
  return;
@@ -27611,10 +27618,10 @@ async function copyBrowserAssets(ws, cache, noCache) {
27611
27618
  console.log(` building: browser-assets (${assets.length})`);
27612
27619
  const outputHashes = {};
27613
27620
  for (const asset of assets) {
27614
- const dest = join12(ws.assetsDir, asset.to);
27615
- mkdirSync10(dirname6(dest), { recursive: true });
27616
- copyFileSync2(asset.src, dest);
27617
- outputHashes[asset.to] = sha256Hex(readFileSync11(dest));
27621
+ const dest = join11(ws.assetsDir, asset.to);
27622
+ mkdirSync9(dirname7(dest), { recursive: true });
27623
+ copyFileSync(asset.src, dest);
27624
+ outputHashes[asset.to] = sha256Hex(readFileSync10(dest));
27618
27625
  }
27619
27626
  updateCache(cache, unitId, inputHash, { outputHashes });
27620
27627
  }
@@ -27635,7 +27642,7 @@ function collectArcPeerDeps(packages) {
27635
27642
  return [short, pkg];
27636
27643
  });
27637
27644
  }
27638
- var REACT_ENTRIES2 = [
27645
+ var REACT_ENTRIES = [
27639
27646
  [
27640
27647
  "react",
27641
27648
  `import React from "react";
@@ -27665,8 +27672,8 @@ export const { createPortal, flushSync } = ReactDOM;`
27665
27672
  `export { createRoot, hydrateRoot } from "react-dom/client";`
27666
27673
  ]
27667
27674
  ];
27668
- var REACT_OUTPUT_FILES = REACT_ENTRIES2.map(([n]) => `${n}.js`);
27669
- var SHELL_BASE_EXTERNAL2 = [
27675
+ var REACT_OUTPUT_FILES = REACT_ENTRIES.map(([n]) => `${n}.js`);
27676
+ var SHELL_BASE_EXTERNAL = [
27670
27677
  "react",
27671
27678
  "react-dom",
27672
27679
  "react/jsx-runtime",
@@ -27683,27 +27690,27 @@ var sourceFilter2 = (rel) => {
27683
27690
  return true;
27684
27691
  };
27685
27692
  function arcPkgSrcHash(rootDir, pkg) {
27686
- const srcDir = join12(rootDir, "node_modules", pkg, "src");
27693
+ const srcDir = join11(rootDir, "node_modules", pkg, "src");
27687
27694
  if (existsSync10(srcDir))
27688
27695
  return sha256OfDir(srcDir, sourceFilter2);
27689
- return sha256OfDir(join12(rootDir, "node_modules", pkg), sourceFilter2);
27696
+ return sha256OfDir(join11(rootDir, "node_modules", pkg), sourceFilter2);
27690
27697
  }
27691
27698
  async function buildShellReact(shellDir, tmpDir, rootDir, cache, noCache) {
27692
27699
  const unitId = "shell:react";
27693
27700
  const inputHash = sha256OfJson({
27694
27701
  react: readInstalledVersion(rootDir, "react"),
27695
27702
  "react-dom": readInstalledVersion(rootDir, "react-dom"),
27696
- entries: REACT_ENTRIES2.map(([k, v]) => [k, v])
27703
+ entries: REACT_ENTRIES.map(([k, v]) => [k, v])
27697
27704
  });
27698
- const requiredOutputs = REACT_OUTPUT_FILES.map((f) => join12(shellDir, f));
27705
+ const requiredOutputs = REACT_OUTPUT_FILES.map((f) => join11(shellDir, f));
27699
27706
  if (!noCache && isCacheHit(cache, unitId, inputHash, requiredOutputs)) {
27700
27707
  console.log(` \u2713 cached: shell:react`);
27701
27708
  return;
27702
27709
  }
27703
27710
  console.log(` building: shell:react`);
27704
27711
  const reactEps = [];
27705
- for (const [name, code] of REACT_ENTRIES2) {
27706
- const f = join12(tmpDir, `${name}.ts`);
27712
+ for (const [name, code] of REACT_ENTRIES) {
27713
+ const f = join11(tmpDir, `${name}.ts`);
27707
27714
  await Bun.write(f, code);
27708
27715
  reactEps.push(f);
27709
27716
  }
@@ -27730,16 +27737,16 @@ async function buildShellArcEntry(shortName, pkg, allArcPkgs, shellDir, tmpDir,
27730
27737
  pkg,
27731
27738
  version: readInstalledVersion(rootDir, pkg),
27732
27739
  src: arcPkgSrcHash(rootDir, pkg),
27733
- base: SHELL_BASE_EXTERNAL2,
27740
+ base: SHELL_BASE_EXTERNAL,
27734
27741
  others: [...otherExternals].sort()
27735
27742
  });
27736
- const outputFile = join12(shellDir, `${shortName}.js`);
27743
+ const outputFile = join11(shellDir, `${shortName}.js`);
27737
27744
  if (!noCache && isCacheHit(cache, unitId, inputHash, [outputFile])) {
27738
27745
  console.log(` \u2713 cached: ${unitId}`);
27739
27746
  return;
27740
27747
  }
27741
27748
  console.log(` building: ${unitId}`);
27742
- const f = join12(tmpDir, `${shortName}.ts`);
27749
+ const f = join11(tmpDir, `${shortName}.ts`);
27743
27750
  await Bun.write(f, `export * from "${pkg}";
27744
27751
  `);
27745
27752
  const r = await Bun.build({
@@ -27748,7 +27755,7 @@ async function buildShellArcEntry(shortName, pkg, allArcPkgs, shellDir, tmpDir,
27748
27755
  format: "esm",
27749
27756
  target: "browser",
27750
27757
  naming: "[name].[ext]",
27751
- external: [...SHELL_BASE_EXTERNAL2, ...otherExternals],
27758
+ external: [...SHELL_BASE_EXTERNAL, ...otherExternals],
27752
27759
  define: {
27753
27760
  ONLY_SERVER: "false",
27754
27761
  ONLY_BROWSER: "true",
@@ -27763,10 +27770,10 @@ async function buildShellArcEntry(shortName, pkg, allArcPkgs, shellDir, tmpDir,
27763
27770
  const outputHash = sha256OfFiles([outputFile]);
27764
27771
  updateCache(cache, unitId, inputHash, { outputHash });
27765
27772
  }
27766
- async function buildShell2(ws, cache, noCache) {
27767
- mkdirSync10(ws.shellDir, { recursive: true });
27768
- const tmpDir = join12(ws.shellDir, "_tmp");
27769
- mkdirSync10(tmpDir, { recursive: true });
27773
+ async function buildShell(ws, cache, noCache) {
27774
+ mkdirSync9(ws.shellDir, { recursive: true });
27775
+ const tmpDir = join11(ws.shellDir, "_tmp");
27776
+ mkdirSync9(tmpDir, { recursive: true });
27770
27777
  const arcEntries = collectArcPeerDeps(ws.packages);
27771
27778
  const allArcPkgs = arcEntries.map(([, pkg]) => pkg);
27772
27779
  const tasks = [
@@ -27774,36 +27781,43 @@ async function buildShell2(ws, cache, noCache) {
27774
27781
  ...arcEntries.map(([short, pkg]) => () => buildShellArcEntry(short, pkg, allArcPkgs, ws.shellDir, tmpDir, ws.rootDir, cache, noCache))
27775
27782
  ];
27776
27783
  await pAll(tasks);
27777
- rmSync3(tmpDir, { recursive: true, force: true });
27784
+ rmSync2(tmpDir, { recursive: true, force: true });
27778
27785
  }
27779
- async function loadServerContext(packages) {
27780
- const ctxPackages = packages.filter((p) => isContextPackage(p.packageJson));
27781
- if (ctxPackages.length === 0)
27782
- return { context: null, moduleAccess: new Map };
27786
+ async function loadServerContext(ws) {
27783
27787
  globalThis.ONLY_SERVER = true;
27784
27788
  globalThis.ONLY_BROWSER = false;
27785
27789
  globalThis.ONLY_CLIENT = false;
27786
- const platformDir = join12(process.cwd(), "node_modules", "@arcote.tech", "platform");
27787
- const platformPkg = JSON.parse(readFileSync11(join12(platformDir, "package.json"), "utf-8"));
27788
- const platformEntry = join12(platformDir, platformPkg.main ?? "src/index.ts");
27790
+ const platformDir = join11(process.cwd(), "node_modules", "@arcote.tech", "platform");
27791
+ const platformPkg = JSON.parse(readFileSync10(join11(platformDir, "package.json"), "utf-8"));
27792
+ const platformEntry = join11(platformDir, platformPkg.main ?? "src/index.ts");
27789
27793
  await import(platformEntry);
27790
- for (const ctx of ctxPackages) {
27791
- const serverDist = join12(ctx.path, "dist", "server", "main", "index.js");
27792
- if (!existsSync10(serverDist)) {
27793
- err(`Context server dist not found: ${serverDist}`);
27794
- continue;
27794
+ const serverDir = join11(ws.arcDir, "server");
27795
+ const bundles = existsSync10(serverDir) ? readdirSync5(serverDir).filter((f) => f.endsWith(".js")) : [];
27796
+ if (bundles.length > 0) {
27797
+ for (const file of bundles) {
27798
+ const bundlePath = join11(serverDir, file);
27799
+ try {
27800
+ await import(bundlePath);
27801
+ } catch (e) {
27802
+ err(`Failed to load server bundle ${file}: ${e}`);
27803
+ }
27795
27804
  }
27796
- try {
27797
- await import(serverDist);
27798
- } catch (e) {
27799
- err(`Failed to load server context from ${ctx.name}: ${e}`);
27805
+ } else if (ws.packages.length > 0) {
27806
+ const ctxPackages = ws.packages.filter((p) => isContextPackage(p.packageJson));
27807
+ for (const ctx of ctxPackages) {
27808
+ const serverDist = join11(ctx.path, "dist", "server", "main", "index.js");
27809
+ if (!existsSync10(serverDist)) {
27810
+ err(`Context server dist not found: ${serverDist}`);
27811
+ continue;
27812
+ }
27813
+ try {
27814
+ await import(serverDist);
27815
+ } catch (e) {
27816
+ err(`Failed to load server context from ${ctx.name}: ${e}`);
27817
+ }
27800
27818
  }
27801
- }
27802
- const nonCtxPackages = packages.filter((p) => !isContextPackage(p.packageJson));
27803
- for (const pkg of nonCtxPackages) {
27804
- try {
27805
- await import(pkg.entrypoint);
27806
- } catch {}
27819
+ } else {
27820
+ return { context: null, moduleAccess: new Map };
27807
27821
  }
27808
27822
  const { getContext, getAllModuleAccess } = await import(platformEntry);
27809
27823
  return {
@@ -27820,19 +27834,21 @@ async function platformBuild(opts = {}) {
27820
27834
  }
27821
27835
 
27822
27836
  // src/commands/platform-deploy.ts
27823
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
27824
- import { dirname as dirname7, join as join18 } from "path";
27837
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
27838
+ import { dirname as dirname9, join as join17 } from "path";
27839
+ import { fileURLToPath as fileURLToPath7 } from "url";
27825
27840
 
27826
27841
  // src/deploy/bootstrap.ts
27827
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync14 } from "fs";
27828
- import { tmpdir as tmpdir3 } from "os";
27829
- import { join as join16 } from "path";
27842
+ var {spawn: spawn4 } = globalThis.Bun;
27843
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync13 } from "fs";
27844
+ import { tmpdir as tmpdir2 } from "os";
27845
+ import { join as join15 } from "path";
27830
27846
 
27831
27847
  // src/deploy/ansible.ts
27832
27848
  import { spawn as nodeSpawn } from "child_process";
27833
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
27834
- import { tmpdir as tmpdir2 } from "os";
27835
- import { join as join13 } from "path";
27849
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
27850
+ import { tmpdir } from "os";
27851
+ import { join as join12 } from "path";
27836
27852
 
27837
27853
  // src/deploy/assets.ts
27838
27854
  var TERRAFORM_MAIN_TF = `terraform {
@@ -28089,18 +28105,18 @@ var ASSETS = {
28089
28105
  }
28090
28106
  };
28091
28107
  async function materializeAssets(targetDir, files) {
28092
- const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync11 } = await import("fs");
28093
- const { join: join13 } = await import("path");
28094
- mkdirSync11(targetDir, { recursive: true });
28108
+ const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync10 } = await import("fs");
28109
+ const { join: join12 } = await import("path");
28110
+ mkdirSync10(targetDir, { recursive: true });
28095
28111
  for (const [name, content] of Object.entries(files)) {
28096
- writeFileSync11(join13(targetDir, name), content);
28112
+ writeFileSync10(join12(targetDir, name), content);
28097
28113
  }
28098
28114
  }
28099
28115
 
28100
28116
  // src/deploy/ansible.ts
28101
28117
  async function runAnsible(inputs) {
28102
- const workDir = join13(tmpdir2(), "arc-deploy", `ansible-${Date.now()}`);
28103
- mkdirSync11(workDir, { recursive: true });
28118
+ const workDir = join12(tmpdir(), "arc-deploy", `ansible-${Date.now()}`);
28119
+ mkdirSync10(workDir, { recursive: true });
28104
28120
  await materializeAssets(workDir, ASSETS.ansible);
28105
28121
  const user = inputs.asRoot ? "root" : inputs.target.user;
28106
28122
  const port = inputs.ansible?.sshPort ?? inputs.target.port;
@@ -28114,7 +28130,7 @@ async function runAnsible(inputs) {
28114
28130
  ""
28115
28131
  ].join(`
28116
28132
  `);
28117
- writeFileSync11(join13(workDir, "inventory.ini"), inventory);
28133
+ writeFileSync10(join12(workDir, "inventory.ini"), inventory);
28118
28134
  const extraVarsJson = JSON.stringify({
28119
28135
  username: inputs.target.user,
28120
28136
  ssh_port: port,
@@ -28153,22 +28169,17 @@ function generateCaddyfile(cfg) {
28153
28169
  lines.push("");
28154
28170
  for (const [name, env2] of Object.entries(cfg.envs)) {
28155
28171
  lines.push(`${env2.domain} {${tlsDirective}`);
28156
- lines.push(" @deploy path /api/deploy /api/deploy/*");
28157
- lines.push(" respond @deploy 404");
28158
- lines.push("");
28159
28172
  lines.push(` reverse_proxy arc-${name}:5005`);
28160
28173
  lines.push("}");
28161
28174
  lines.push("");
28162
28175
  }
28163
- lines.push("# Loopback-only management listener (SSH tunnel access).");
28164
- lines.push("http://127.0.0.1:2019 {");
28165
- lines.push(" bind 127.0.0.1");
28166
- for (const [name] of Object.entries(cfg.envs)) {
28167
- lines.push(` handle_path /env/${name}/* {`);
28168
- lines.push(` reverse_proxy arc-${name}:5005`);
28169
- lines.push(` }`);
28170
- }
28171
- lines.push(" respond 404");
28176
+ lines.push(`${cfg.registry.domain} {${tlsDirective}`);
28177
+ lines.push(" reverse_proxy registry:5000 {");
28178
+ lines.push(" header_up Host {host}");
28179
+ lines.push(" }");
28180
+ lines.push(" request_body {");
28181
+ lines.push(" max_size 5GB");
28182
+ lines.push(" }");
28172
28183
  lines.push("}");
28173
28184
  return lines.join(`
28174
28185
  `) + `
@@ -28176,8 +28187,7 @@ function generateCaddyfile(cfg) {
28176
28187
  }
28177
28188
 
28178
28189
  // src/deploy/compose.ts
28179
- var RESERVED_ENV = new Set(["PORT", "ARC_DEPLOY_API", "ARC_CLI_VERSION"]);
28180
- function generateCompose({ cfg, cliVersion }) {
28190
+ function generateCompose({ cfg }) {
28181
28191
  const lines = [];
28182
28192
  lines.push("# Generated by `arc platform deploy` \u2014 do not edit by hand.");
28183
28193
  lines.push("");
@@ -28188,7 +28198,6 @@ function generateCompose({ cfg, cliVersion }) {
28188
28198
  lines.push(" ports:");
28189
28199
  lines.push(' - "80:80"');
28190
28200
  lines.push(' - "443:443"');
28191
- lines.push(' - "127.0.0.1:2019:2019"');
28192
28201
  lines.push(" volumes:");
28193
28202
  lines.push(" - ./Caddyfile:/etc/caddy/Caddyfile:ro");
28194
28203
  lines.push(" - caddy_data:/data");
@@ -28196,25 +28205,39 @@ function generateCompose({ cfg, cliVersion }) {
28196
28205
  lines.push(" networks:");
28197
28206
  lines.push(" - arc-net");
28198
28207
  lines.push("");
28208
+ lines.push(" registry:");
28209
+ lines.push(" image: registry:2");
28210
+ lines.push(" restart: unless-stopped");
28211
+ lines.push(" volumes:");
28212
+ lines.push(" - registry_data:/var/lib/registry");
28213
+ lines.push(" - ./registry-auth/htpasswd:/auth/htpasswd:ro");
28214
+ lines.push(" environment:");
28215
+ lines.push(" REGISTRY_AUTH: htpasswd");
28216
+ lines.push(' REGISTRY_AUTH_HTPASSWD_REALM: "Arc Registry"');
28217
+ lines.push(" REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd");
28218
+ lines.push(' REGISTRY_HTTP_HOST: "https://' + cfg.registry.domain + '"');
28219
+ lines.push(" networks:");
28220
+ lines.push(" - arc-net");
28221
+ lines.push(" expose:");
28222
+ lines.push(' - "5000"');
28223
+ lines.push("");
28199
28224
  for (const [name, env2] of Object.entries(cfg.envs)) {
28225
+ const upperName = name.toUpperCase().replace(/-/g, "_");
28200
28226
  lines.push(` arc-${name}:`);
28201
- lines.push(" image: pkrasinski/arc-runtime:1");
28227
+ lines.push(` image: \${ARC_IMAGE_${upperName}:?Run \\\`arc platform deploy ${name}\\\` to publish an image first}`);
28228
+ lines.push(` container_name: arc-${name}`);
28202
28229
  lines.push(" restart: unless-stopped");
28203
28230
  lines.push(" volumes:");
28204
- lines.push(` - arc-platform-${name}:/app/.arc/platform`);
28205
28231
  lines.push(` - arc-data-${name}:/app/.arc/data`);
28206
- lines.push(" - arc-cli-cache:/app/.arc/cli");
28207
- lines.push(" - arc-bun-cache:/root/.bun/install/cache");
28208
28232
  lines.push(" environment:");
28209
28233
  lines.push(" PORT: 5005");
28210
- lines.push(' ARC_DEPLOY_API: "1"');
28211
- lines.push(` ARC_CLI_VERSION: ${JSON.stringify(cliVersion)}`);
28212
28234
  const userEnv = env2.envVars ?? {};
28213
28235
  if (!("NODE_ENV" in userEnv)) {
28214
28236
  lines.push(" NODE_ENV: production");
28215
28237
  }
28238
+ const reserved = new Set(["PORT"]);
28216
28239
  for (const [k, v] of Object.entries(userEnv)) {
28217
- if (RESERVED_ENV.has(k))
28240
+ if (reserved.has(k))
28218
28241
  continue;
28219
28242
  lines.push(` ${k}: ${JSON.stringify(v)}`);
28220
28243
  }
@@ -28230,10 +28253,8 @@ function generateCompose({ cfg, cliVersion }) {
28230
28253
  lines.push("volumes:");
28231
28254
  lines.push(" caddy_data:");
28232
28255
  lines.push(" caddy_config:");
28233
- lines.push(" arc-cli-cache:");
28234
- lines.push(" arc-bun-cache:");
28256
+ lines.push(" registry_data:");
28235
28257
  for (const [name] of Object.entries(cfg.envs)) {
28236
- lines.push(` arc-platform-${name}:`);
28237
28258
  lines.push(` arc-data-${name}:`);
28238
28259
  }
28239
28260
  return lines.join(`
@@ -28241,16 +28262,29 @@ function generateCompose({ cfg, cliVersion }) {
28241
28262
  `;
28242
28263
  }
28243
28264
 
28265
+ // src/deploy/htpasswd.ts
28266
+ async function generateHtpasswd(user, password) {
28267
+ if (!user || !password) {
28268
+ throw new Error("htpasswd: user and password must both be non-empty");
28269
+ }
28270
+ const hash = await Bun.password.hash(password, {
28271
+ algorithm: "bcrypt",
28272
+ cost: 10
28273
+ });
28274
+ return `${user}:${hash}
28275
+ `;
28276
+ }
28277
+
28244
28278
  // src/deploy/terraform.ts
28245
28279
  import { spawn as nodeSpawn2 } from "child_process";
28246
28280
  import { createHash as createHash2 } from "crypto";
28247
- import { existsSync as existsSync11, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
28281
+ import { existsSync as existsSync11, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
28248
28282
  import { homedir } from "os";
28249
- import { join as join14 } from "path";
28283
+ import { join as join13 } from "path";
28250
28284
  async function runTerraform(inputs) {
28251
28285
  const wsHash = createHash2("sha256").update(inputs.workspaceDir).digest("hex").slice(0, 16);
28252
- const workDir = join14(homedir(), ".arc-deploy", wsHash, "tf");
28253
- mkdirSync12(workDir, { recursive: true });
28286
+ const workDir = join13(homedir(), ".arc-deploy", wsHash, "tf");
28287
+ mkdirSync11(workDir, { recursive: true });
28254
28288
  await materializeAssets(workDir, ASSETS.terraform);
28255
28289
  const sshPubKey = inputs.tf.sshPublicKey ?? expandHome("~/.ssh/id_ed25519.pub");
28256
28290
  if (!existsSync11(expandHome(sshPubKey))) {
@@ -28266,7 +28300,7 @@ async function runTerraform(inputs) {
28266
28300
  ].join(`
28267
28301
  `) + `
28268
28302
  `;
28269
- writeFileSync12(join14(workDir, "terraform.tfvars"), tfvars);
28303
+ writeFileSync11(join13(workDir, "terraform.tfvars"), tfvars);
28270
28304
  await runTf(workDir, ["init", "-input=false", "-no-color"]);
28271
28305
  await runTf(workDir, [
28272
28306
  "apply",
@@ -28324,11 +28358,11 @@ function expandHome(p) {
28324
28358
  }
28325
28359
 
28326
28360
  // src/deploy/config.ts
28327
- import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync13 } from "fs";
28328
- import { join as join15 } from "path";
28361
+ import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync12 } from "fs";
28362
+ import { join as join14 } from "path";
28329
28363
  var DEPLOY_CONFIG_FILE = "deploy.arc.json";
28330
28364
  function deployConfigPath(rootDir) {
28331
- return join15(rootDir, DEPLOY_CONFIG_FILE);
28365
+ return join14(rootDir, DEPLOY_CONFIG_FILE);
28332
28366
  }
28333
28367
  function deployConfigExists(rootDir) {
28334
28368
  return existsSync12(deployConfigPath(rootDir));
@@ -28338,7 +28372,7 @@ function loadDeployConfig(rootDir) {
28338
28372
  if (!existsSync12(path4)) {
28339
28373
  throw new Error(`Missing ${DEPLOY_CONFIG_FILE} at ${path4}`);
28340
28374
  }
28341
- const raw = readFileSync12(path4, "utf-8");
28375
+ const raw = readFileSync11(path4, "utf-8");
28342
28376
  let parsed;
28343
28377
  try {
28344
28378
  parsed = JSON.parse(raw);
@@ -28350,9 +28384,9 @@ function loadDeployConfig(rootDir) {
28350
28384
  }
28351
28385
  function saveDeployConfig(rootDir, cfg) {
28352
28386
  const path4 = deployConfigPath(rootDir);
28353
- const raw = existsSync12(path4) ? JSON.parse(readFileSync12(path4, "utf-8")) : {};
28387
+ const raw = existsSync12(path4) ? JSON.parse(readFileSync11(path4, "utf-8")) : {};
28354
28388
  raw.target = { ...raw.target, ...cfg.target };
28355
- writeFileSync13(path4, JSON.stringify(raw, null, 2) + `
28389
+ writeFileSync12(path4, JSON.stringify(raw, null, 2) + `
28356
28390
  `);
28357
28391
  }
28358
28392
  var VAR_REGEX = /\$\{([A-Z0-9_]+)\}|\$([A-Z0-9_]+)/g;
@@ -28378,6 +28412,15 @@ function validateDeployConfig(input) {
28378
28412
  const target = requireObject(input, "target");
28379
28413
  const envs = requireObject(input, "envs");
28380
28414
  const caddy = requireObject(input, "caddy");
28415
+ const registry = requireObject(input, "registry");
28416
+ const registryDomain = requireString(registry, "registry.domain");
28417
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(registryDomain)) {
28418
+ throw new Error(`deploy.arc.json: registry.domain "${registryDomain}" doesn't look like a domain`);
28419
+ }
28420
+ const passwordEnv = requireString(registry, "registry.passwordEnv");
28421
+ if (!/^[A-Z][A-Z0-9_]*$/.test(passwordEnv)) {
28422
+ throw new Error(`deploy.arc.json: registry.passwordEnv "${passwordEnv}" must be an UPPER_SNAKE_CASE env var name`);
28423
+ }
28381
28424
  const validated = {
28382
28425
  target: {
28383
28426
  host: requireString(target, "target.host"),
@@ -28389,6 +28432,11 @@ function validateDeployConfig(input) {
28389
28432
  envs: {},
28390
28433
  caddy: {
28391
28434
  email: requireString(caddy, "caddy.email")
28435
+ },
28436
+ registry: {
28437
+ domain: registryDomain,
28438
+ username: optionalString(registry, "registry.username") ?? "deploy",
28439
+ passwordEnv
28392
28440
  }
28393
28441
  };
28394
28442
  const envKeys = Object.keys(envs);
@@ -28510,19 +28558,17 @@ async function streamToString(stream2) {
28510
28558
  return new Response(stream2).text();
28511
28559
  }
28512
28560
  function baseSshArgs(target) {
28513
- const key = target.sshKey ?? `${process.env.HOME}/.ssh/id_ed25519`;
28514
- return [
28561
+ const args = [
28515
28562
  "-o",
28516
28563
  "BatchMode=yes",
28517
28564
  "-o",
28518
28565
  "StrictHostKeyChecking=accept-new",
28519
- "-o",
28520
- "IdentitiesOnly=yes",
28521
- "-i",
28522
- key,
28523
28566
  "-p",
28524
28567
  String(target.port)
28525
28568
  ];
28569
+ if (target.sshKey)
28570
+ args.push("-i", target.sshKey);
28571
+ return args;
28526
28572
  }
28527
28573
  async function sshExec(target, cmd, opts = {}) {
28528
28574
  const args = [
@@ -28576,19 +28622,16 @@ async function waitForSsh(target, opts = {}) {
28576
28622
  throw new Error(`Timed out waiting for SSH on ${target.user}@${target.host}`);
28577
28623
  }
28578
28624
  async function scpUpload(target, localPath, remotePath) {
28579
- const key = target.sshKey ?? `${process.env.HOME}/.ssh/id_ed25519`;
28580
28625
  const args = [
28581
28626
  "-o",
28582
28627
  "BatchMode=yes",
28583
28628
  "-o",
28584
28629
  "StrictHostKeyChecking=accept-new",
28585
- "-o",
28586
- "IdentitiesOnly=yes",
28587
- "-i",
28588
- key,
28589
28630
  "-P",
28590
28631
  String(target.port)
28591
28632
  ];
28633
+ if (target.sshKey)
28634
+ args.push("-i", target.sshKey);
28592
28635
  args.push(localPath, `${target.user}@${target.host}:${remotePath}`);
28593
28636
  const proc2 = spawn3({ cmd: ["scp", ...args], stderr: "pipe" });
28594
28637
  const [stderr, exitCode] = await Promise.all([
@@ -28599,52 +28642,6 @@ async function scpUpload(target, localPath, remotePath) {
28599
28642
  throw new Error(`scp failed (${exitCode}): ${stderr}`);
28600
28643
  }
28601
28644
  }
28602
- async function openTunnel(target, localPort, remoteHost, remotePort) {
28603
- const args = [
28604
- ...baseSshArgs(target),
28605
- "-N",
28606
- "-L",
28607
- `${localPort}:${remoteHost}:${remotePort}`,
28608
- `${target.user}@${target.host}`
28609
- ];
28610
- const proc2 = spawn3({
28611
- cmd: ["ssh", ...args],
28612
- stdin: "ignore",
28613
- stdout: "pipe",
28614
- stderr: "pipe"
28615
- });
28616
- const deadline = Date.now() + 1e4;
28617
- let lastErr;
28618
- while (Date.now() < deadline) {
28619
- if (proc2.exitCode !== null) {
28620
- const stderr = await streamToString(proc2.stderr);
28621
- throw new Error(`ssh tunnel exited early: ${stderr}`);
28622
- }
28623
- try {
28624
- const probe = await Bun.connect({
28625
- hostname: "127.0.0.1",
28626
- port: localPort,
28627
- socket: { data() {}, open() {}, close() {}, error() {} }
28628
- });
28629
- probe.end();
28630
- return {
28631
- localPort,
28632
- close() {
28633
- try {
28634
- proc2.kill();
28635
- } catch {}
28636
- }
28637
- };
28638
- } catch (e) {
28639
- lastErr = e;
28640
- await Bun.sleep(200);
28641
- }
28642
- }
28643
- try {
28644
- proc2.kill();
28645
- } catch {}
28646
- throw new Error(`Failed to establish SSH tunnel on localhost:${localPort}: ${String(lastErr)}`);
28647
- }
28648
28645
 
28649
28646
  // src/deploy/remote-state.ts
28650
28647
  var STATE_MARKER_PATH = "/opt/arc/.arc-state.json";
@@ -28727,205 +28724,373 @@ async function bootstrap(inputs) {
28727
28724
  });
28728
28725
  ok("Host bootstrapped");
28729
28726
  }
28730
- if (state.kind !== "ready") {
28731
- await upStack(inputs);
28732
- ok("Docker stack up");
28727
+ if (state.kind !== "ready") {
28728
+ await upStack(inputs);
28729
+ ok("Docker stack up");
28730
+ }
28731
+ await writeStateMarker(cfg.target, {
28732
+ cliVersion: inputs.cliVersion,
28733
+ configHash: inputs.configHash,
28734
+ updatedAt: new Date().toISOString()
28735
+ });
28736
+ }
28737
+ async function upStack(inputs) {
28738
+ const { cfg } = inputs;
28739
+ const workDir = join15(tmpdir2(), "arc-deploy", `stack-${Date.now()}`);
28740
+ mkdirSync12(workDir, { recursive: true });
28741
+ await assertRegistryDnsResolves(cfg);
28742
+ const password = process.env[cfg.registry.passwordEnv];
28743
+ if (!password) {
28744
+ throw new Error(`Registry password env var ${cfg.registry.passwordEnv} is not set. ` + `Set it (e.g. \`export ${cfg.registry.passwordEnv}=...\`) before bootstrap.`);
28745
+ }
28746
+ const htpasswdLine = await generateHtpasswd(cfg.registry.username, password);
28747
+ writeFileSync13(join15(workDir, "htpasswd"), htpasswdLine);
28748
+ writeFileSync13(join15(workDir, "Caddyfile"), generateCaddyfile(cfg));
28749
+ writeFileSync13(join15(workDir, "docker-compose.yml"), generateCompose({ cfg }));
28750
+ await assertExec(cfg.target, `sudo mkdir -p ${cfg.target.remoteDir} && sudo chown ${cfg.target.user}:${cfg.target.user} ${cfg.target.remoteDir}`);
28751
+ for (const name of Object.keys(cfg.envs)) {
28752
+ await assertExec(cfg.target, `mkdir -p ${cfg.target.remoteDir}/${name}`);
28753
+ }
28754
+ await assertExec(cfg.target, `mkdir -p ${cfg.target.remoteDir}/registry-auth`);
28755
+ await scpUpload(cfg.target, join15(workDir, "Caddyfile"), `${cfg.target.remoteDir}/Caddyfile`);
28756
+ await scpUpload(cfg.target, join15(workDir, "docker-compose.yml"), `${cfg.target.remoteDir}/docker-compose.yml`);
28757
+ await scpUpload(cfg.target, join15(workDir, "htpasswd"), `${cfg.target.remoteDir}/registry-auth/htpasswd`);
28758
+ await assertExec(cfg.target, `touch ${cfg.target.remoteDir}/.env`);
28759
+ await assertExec(cfg.target, `cd ${cfg.target.remoteDir} && docker compose pull --ignore-pull-failures caddy registry && docker compose up -d caddy registry`);
28760
+ await sshDockerLogin(cfg);
28761
+ const knownEnvs = await listConfiguredEnvs(cfg);
28762
+ if (knownEnvs.length > 0) {
28763
+ await assertExec(cfg.target, `cd ${cfg.target.remoteDir} && docker compose up -d ${knownEnvs.map((e) => `arc-${e}`).join(" ")}`);
28764
+ }
28765
+ }
28766
+ async function sshDockerLogin(cfg) {
28767
+ const password = process.env[cfg.registry.passwordEnv];
28768
+ if (!password) {
28769
+ throw new Error(`Registry password env var ${cfg.registry.passwordEnv} is not set on the deploy host (CLI machine).`);
28770
+ }
28771
+ const cmd = `echo "$ARC_REGISTRY_PASSWORD_FORWARDED" | docker login ${cfg.registry.domain} -u ${cfg.registry.username} --password-stdin`;
28772
+ const proc2 = spawn4({
28773
+ cmd: [
28774
+ "ssh",
28775
+ ...baseSshArgs(cfg.target),
28776
+ `${cfg.target.user}@${cfg.target.host}`,
28777
+ "--",
28778
+ `ARC_REGISTRY_PASSWORD_FORWARDED='${password.replace(/'/g, "'\\''")}' bash -c ${JSON.stringify(cmd)}`
28779
+ ],
28780
+ stdout: "pipe",
28781
+ stderr: "pipe"
28782
+ });
28783
+ const exit = await proc2.exited;
28784
+ if (exit !== 0) {
28785
+ const stderr = await new Response(proc2.stderr).text();
28786
+ throw new Error(`Server-side docker login failed (exit ${exit}): ${stderr.trim()}`);
28787
+ }
28788
+ }
28789
+ async function listConfiguredEnvs(cfg) {
28790
+ const res = await sshExec(cfg.target, `cat ${cfg.target.remoteDir}/.env 2>/dev/null || true`, { quiet: true });
28791
+ const set = new Set;
28792
+ for (const line of res.stdout.split(`
28793
+ `)) {
28794
+ const m = line.match(/^ARC_IMAGE_([A-Z0-9_]+)=/);
28795
+ if (!m)
28796
+ continue;
28797
+ const lowerName = m[1].toLowerCase().replace(/_/g, "-");
28798
+ if (lowerName in cfg.envs)
28799
+ set.add(lowerName);
28733
28800
  }
28734
- await writeStateMarker(cfg.target, {
28735
- cliVersion: inputs.cliVersion,
28736
- configHash: inputs.configHash,
28737
- updatedAt: new Date().toISOString()
28801
+ return [...set];
28802
+ }
28803
+ async function assertRegistryDnsResolves(cfg) {
28804
+ const proc2 = spawn4({
28805
+ cmd: ["dig", "+short", "+time=3", "+tries=1", cfg.registry.domain],
28806
+ stdout: "pipe",
28807
+ stderr: "ignore"
28738
28808
  });
28809
+ const exit = await proc2.exited;
28810
+ if (exit !== 0) {
28811
+ err(`\`dig\` is not available \u2014 skipping DNS pre-flight for ${cfg.registry.domain}.`);
28812
+ return;
28813
+ }
28814
+ const resolved = (await new Response(proc2.stdout).text()).split(`
28815
+ `).map((s) => s.trim()).filter(Boolean);
28816
+ if (resolved.length === 0) {
28817
+ throw new Error(`Registry DNS not configured: ${cfg.registry.domain} doesn't resolve. ` + `Add an A record pointing to ${cfg.target.host} and re-run deploy.`);
28818
+ }
28819
+ if (!resolved.includes(cfg.target.host)) {
28820
+ throw new Error(`Registry DNS mismatch: ${cfg.registry.domain} resolves to [${resolved.join(", ")}], ` + `but target host is ${cfg.target.host}. Update the A record before continuing.`);
28821
+ }
28822
+ }
28823
+
28824
+ // src/deploy/deploy-env.ts
28825
+ var HEALTH_RETRIES = 10;
28826
+ var HEALTH_DELAY_MS = 1000;
28827
+ async function updateEnvDeployment(opts) {
28828
+ const { target, cfg, env: env2, fullRef } = opts;
28829
+ const upperEnv = env2.toUpperCase().replace(/-/g, "_");
28830
+ const envVarName = `ARC_IMAGE_${upperEnv}`;
28831
+ const envPath = `${cfg.target.remoteDir}/.env`;
28832
+ const escapedRef = fullRef.replace(/"/g, "\\\"");
28833
+ const updateScript = [
28834
+ `touch ${envPath}`,
28835
+ `awk -v line="${envVarName}=${escapedRef}" -v key="${envVarName}=" '`,
28836
+ ` BEGIN { replaced=0 } `,
28837
+ ` $0 ~ "^"key { print line; replaced=1; next } `,
28838
+ ` { print } `,
28839
+ ` END { if (!replaced) print line } `,
28840
+ `' ${envPath} > ${envPath}.tmp && mv ${envPath}.tmp ${envPath}`
28841
+ ].join("");
28842
+ await assertExec(target, updateScript);
28843
+ await assertExec(target, `cd ${cfg.target.remoteDir} && docker compose pull arc-${env2}`);
28844
+ await assertExec(target, `cd ${cfg.target.remoteDir} && docker compose up -d arc-${env2}`);
28845
+ const retain = opts.retainImages ?? 3;
28846
+ const imageBaseName = imageBaseFromRef(fullRef);
28847
+ if (imageBaseName) {
28848
+ const pruneScript = [
28849
+ `docker images "${imageBaseName}" --format "{{.Repository}}:{{.Tag}} {{.CreatedAt}}" `,
28850
+ `| grep -v ":latest " `,
28851
+ `| sort -k2,3 -r `,
28852
+ `| tail -n +${retain + 1} `,
28853
+ `| awk '{print $1}' `,
28854
+ `| xargs -r docker rmi 2>/dev/null || true`
28855
+ ].join("");
28856
+ await sshExec(target, pruneScript, { quiet: true });
28857
+ }
28858
+ const ok2 = await healthCheck(target, env2);
28859
+ return { env: env2, fullRef, redeployed: ok2 };
28860
+ }
28861
+ function imageBaseFromRef(fullRef) {
28862
+ const colonIdx = fullRef.lastIndexOf(":");
28863
+ return colonIdx > 0 ? fullRef.slice(0, colonIdx) : null;
28864
+ }
28865
+ async function healthCheck(target, env2) {
28866
+ for (let i = 0;i < HEALTH_RETRIES; i++) {
28867
+ const res = await sshExec(target, `docker exec arc-${env2} wget -qO- http://localhost:5005/health 2>&1 || echo HEALTHCHECK_FAILED`, { quiet: true });
28868
+ if (res.exitCode === 0 && !res.stdout.includes("HEALTHCHECK_FAILED")) {
28869
+ return true;
28870
+ }
28871
+ await sleep(HEALTH_DELAY_MS);
28872
+ }
28873
+ return false;
28739
28874
  }
28740
- async function upStack(inputs) {
28741
- const { cfg } = inputs;
28742
- const workDir = join16(tmpdir3(), "arc-deploy", `stack-${Date.now()}`);
28743
- mkdirSync13(workDir, { recursive: true });
28744
- writeFileSync14(join16(workDir, "Caddyfile"), generateCaddyfile(cfg));
28745
- writeFileSync14(join16(workDir, "docker-compose.yml"), generateCompose({ cfg, cliVersion: inputs.cliVersion }));
28746
- await assertExec(cfg.target, `sudo mkdir -p ${cfg.target.remoteDir} && sudo chown ${cfg.target.user}:${cfg.target.user} ${cfg.target.remoteDir}`);
28747
- for (const name of Object.keys(cfg.envs)) {
28748
- await assertExec(cfg.target, `mkdir -p ${cfg.target.remoteDir}/${name}`);
28875
+ function sleep(ms) {
28876
+ return new Promise((r) => setTimeout(r, ms));
28877
+ }
28878
+
28879
+ // src/deploy/image.ts
28880
+ var {spawn: spawn5 } = globalThis.Bun;
28881
+ import { createHash as createHash3 } from "crypto";
28882
+ import {
28883
+ copyFileSync as copyFileSync2,
28884
+ existsSync as existsSync13,
28885
+ mkdirSync as mkdirSync13,
28886
+ readFileSync as readFileSync12,
28887
+ realpathSync as realpathSync3,
28888
+ writeFileSync as writeFileSync14
28889
+ } from "fs";
28890
+ import { tmpdir as tmpdir3 } from "os";
28891
+ import { dirname as dirname8, join as join16 } from "path";
28892
+ import { fileURLToPath as fileURLToPath6 } from "url";
28893
+
28894
+ // src/deploy/image-template.ts
28895
+ function generateDockerfile(inputs) {
28896
+ const conditionalCopies = [];
28897
+ if (inputs.hasPublicDir) {
28898
+ conditionalCopies.push("COPY public/ /app/public/");
28899
+ }
28900
+ if (inputs.hasManifest && inputs.manifestPath) {
28901
+ conditionalCopies.push(`COPY ${inputs.manifestPath} /app/${inputs.manifestPath}`);
28902
+ }
28903
+ if (inputs.hasLocales) {
28904
+ conditionalCopies.push("COPY locales/ /app/locales/");
28749
28905
  }
28750
- await scpUpload(cfg.target, join16(workDir, "Caddyfile"), `${cfg.target.remoteDir}/Caddyfile`);
28751
- await scpUpload(cfg.target, join16(workDir, "docker-compose.yml"), `${cfg.target.remoteDir}/docker-compose.yml`);
28752
- await assertExec(cfg.target, `cd ${cfg.target.remoteDir} && docker compose pull --ignore-pull-failures && docker compose up -d`);
28906
+ return [
28907
+ "# Generated by `arc platform deploy` \u2014 do not edit by hand.",
28908
+ "FROM oven/bun:1-alpine",
28909
+ "",
28910
+ "RUN apk add --no-cache tini ca-certificates",
28911
+ "",
28912
+ "WORKDIR /app",
28913
+ "",
28914
+ "# Layer 1 \u2014 framework peers as /app/package.json + install into /app/node_modules.",
28915
+ "# resolveWorkspace() walks up looking for the first package.json (finds /app/),",
28916
+ "# loadServerContext resolves @arcote.tech/platform from /app/node_modules.",
28917
+ "# Cached, invalidates only on .arc/platform/package.json change.",
28918
+ "COPY .arc/platform/package.json /app/package.json",
28919
+ "RUN cd /app && bun install --production --no-save",
28920
+ "",
28921
+ "# Layer 2 \u2014 user artifacts (chunks, shell, styles, server bundles, host.js).",
28922
+ "# host.js IS the arc-cli bundle \u2014 `arc platform deploy` copied it here from",
28923
+ "# whatever arc-cli was on the user's PATH. No npm dependency at runtime.",
28924
+ "COPY .arc/platform/ /app/.arc/platform/",
28925
+ ...conditionalCopies,
28926
+ "",
28927
+ "ENV PORT=5005",
28928
+ "ENV NODE_ENV=production",
28929
+ "EXPOSE 5005",
28930
+ "",
28931
+ 'ENTRYPOINT ["tini", "--"]',
28932
+ 'CMD ["bun", "run", "/app/.arc/platform/host.js", "platform", "start"]',
28933
+ ""
28934
+ ].join(`
28935
+ `);
28753
28936
  }
28754
28937
 
28755
- // src/deploy/remote-sync.ts
28756
- import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
28757
- import { basename as basename5, join as join17 } from "path";
28758
- function diffManifests(local, remote) {
28759
- const remoteByName = new Map(remote.modules.map((m) => [m.name, m]));
28760
- const changedModules = local.modules.filter((m) => remoteByName.get(m.name)?.hash !== m.hash);
28761
- return {
28762
- changedModules: [...changedModules],
28763
- stylesChanged: local.stylesHash !== remote.stylesHash
28764
- };
28765
- }
28766
- async function syncEnv(inputs) {
28767
- const { cfg, env: env2, ws } = inputs;
28768
- const envConfig = cfg.envs[env2];
28769
- if (!envConfig)
28770
- throw new Error(`Unknown env: ${env2}`);
28771
- const localManifestPath = join17(ws.modulesDir, "manifest.json");
28772
- if (!existsSync13(localManifestPath)) {
28773
- throw new Error(`Local build missing at ${localManifestPath}. Run arc platform build first.`);
28774
- }
28775
- const localManifest = JSON.parse(readFileSync13(localManifestPath, "utf-8"));
28776
- const pkgByName = new Map(ws.packages.map((p) => [p.name.includes("/") ? p.name.split("/").pop() : p.name, p]));
28777
- let tunnel = await openTunnel(cfg.target, 15500 + hashEnvToOffset(env2), "127.0.0.1", 2019);
28778
- let restarts = 0;
28779
- let frameworkChanged = false;
28780
- try {
28781
- const base2 = () => `http://127.0.0.1:${tunnel.localPort}/env/${env2}`;
28782
- const localFrameworkHash = readDepsHash(join17(ws.arcDir, ".deps-hash"));
28783
- const remoteFwRes = await fetch(`${base2()}/api/deploy/framework`);
28784
- const remoteFw = remoteFwRes.ok ? await remoteFwRes.json() : { depsHash: null };
28785
- if (localFrameworkHash && localFrameworkHash !== remoteFw.depsHash) {
28786
- console.log("[arc] Pushing framework deps...");
28787
- const form = new FormData;
28788
- form.append("package.json", new Blob([readFileSync13(join17(ws.arcDir, "package.json"))]), "package.json");
28789
- const lockPath = join17(ws.arcDir, "bun.lock");
28790
- if (existsSync13(lockPath)) {
28791
- form.append("bun.lock", new Blob([readFileSync13(lockPath)]), "bun.lock");
28792
- }
28793
- const res = await fetch(`${base2()}/api/deploy/framework`, {
28794
- method: "POST",
28795
- body: form
28796
- });
28797
- if (!res.ok) {
28798
- throw new Error(`framework push failed: ${res.status} ${await res.text()}`);
28799
- }
28800
- frameworkChanged = true;
28801
- const result = await res.json();
28802
- if (result.needsRestart) {
28803
- tunnel = await restartAndReopen(cfg, env2, tunnel);
28804
- restarts += 1;
28805
- }
28806
- }
28807
- const remoteManifestRes = await fetch(`${base2()}/api/deploy/manifest`);
28808
- const remoteManifest = remoteManifestRes.ok ? await remoteManifestRes.json() : {
28809
- modules: [],
28810
- shellHash: "",
28811
- stylesHash: "",
28812
- buildTime: ""
28813
- };
28814
- const diff = diffManifests(localManifest, remoteManifest);
28815
- for (const mod of diff.changedModules) {
28816
- const safeName = sanitizeName(mod.name);
28817
- const moduleDir = join17(ws.modulesDir, safeName);
28818
- const browserPath = join17(moduleDir, "browser.js");
28819
- const serverPath = join17(moduleDir, "server.js");
28820
- const pkgPath = join17(moduleDir, "package.json");
28821
- const accessPath = join17(moduleDir, "access.json");
28822
- const browserActual = existsSync13(browserPath) ? browserPath : join17(ws.modulesDir, `${safeName}.js`);
28823
- if (!existsSync13(browserActual)) {
28824
- throw new Error(`Missing browser bundle for module ${mod.name}`);
28825
- }
28826
- const form = new FormData;
28827
- form.append("browser.js", new Blob([readFileSync13(browserActual)]), "browser.js");
28828
- const pkg = pkgByName.get(safeName);
28829
- if (pkg && isContextPackage(pkg.packageJson) && existsSync13(serverPath)) {
28830
- form.append("server.js", new Blob([readFileSync13(serverPath)]), "server.js");
28831
- }
28832
- if (existsSync13(pkgPath)) {
28833
- form.append("package.json", new Blob([readFileSync13(pkgPath)]), "package.json");
28834
- }
28835
- if (existsSync13(accessPath)) {
28836
- form.append("access.json", new Blob([readFileSync13(accessPath)]), "access.json");
28837
- }
28838
- console.log(`[arc] Pushing module ${safeName}...`);
28839
- const res = await fetch(`${base2()}/api/deploy/modules/${safeName}`, {
28840
- method: "POST",
28841
- body: form
28842
- });
28843
- if (!res.ok) {
28844
- throw new Error(`module ${safeName} push failed: ${res.status} ${await res.text()}`);
28845
- }
28846
- }
28847
- if (diff.stylesChanged) {
28848
- console.log("[arc] Pushing styles...");
28849
- const form = new FormData;
28850
- for (const name of ["styles.css", "theme.css"]) {
28851
- const p = join17(ws.arcDir, name);
28852
- if (existsSync13(p)) {
28853
- form.append(name, new Blob([readFileSync13(p)]), name);
28938
+ // src/deploy/image.ts
28939
+ async function buildImage(ws, opts) {
28940
+ await ensureDocker();
28941
+ const manifestPath = join16(ws.modulesDir, "manifest.json");
28942
+ if (!existsSync13(manifestPath)) {
28943
+ throw new Error(`No build manifest at ${manifestPath}. Run \`arc platform build\` first or omit --skip-build.`);
28944
+ }
28945
+ embedCliBundle(ws);
28946
+ const contentHash = computeContentHash(manifestPath);
28947
+ const imageTag = `${opts.imageName}:${contentHash}`;
28948
+ const fullRef = opts.registryDomain ? `${opts.registryDomain}/${imageTag}` : imageTag;
28949
+ const dockerfileInputs = collectDockerfileInputs(ws);
28950
+ const dockerfile = generateDockerfile(dockerfileInputs);
28951
+ const buildContextDir = ws.rootDir;
28952
+ const dockerfileDir = join16(tmpdir3(), `arc-image-${Date.now()}`);
28953
+ mkdirSync13(dockerfileDir, { recursive: true });
28954
+ const dockerfilePath = join16(dockerfileDir, "Dockerfile");
28955
+ writeFileSync14(dockerfilePath, dockerfile);
28956
+ const buildArgs = [
28957
+ "build",
28958
+ "-f",
28959
+ dockerfilePath,
28960
+ "-t",
28961
+ fullRef,
28962
+ "-t",
28963
+ `${opts.imageName}:latest`,
28964
+ buildContextDir
28965
+ ];
28966
+ const proc2 = spawn5({
28967
+ cmd: ["docker", ...buildArgs],
28968
+ stdout: "inherit",
28969
+ stderr: "inherit"
28970
+ });
28971
+ const exit = await proc2.exited;
28972
+ if (exit !== 0) {
28973
+ throw new Error(`docker build failed (exit ${exit})`);
28974
+ }
28975
+ return { imageTag, fullRef, contentHash };
28976
+ }
28977
+ function embedCliBundle(ws) {
28978
+ const source = locateCliBundle();
28979
+ const target = join16(ws.arcDir, "host.js");
28980
+ copyFileSync2(source, target);
28981
+ }
28982
+ function locateCliBundle() {
28983
+ const here = fileURLToPath6(import.meta.url);
28984
+ let cur = dirname8(here);
28985
+ while (cur !== "/" && cur !== "") {
28986
+ const candidate = join16(cur, "package.json");
28987
+ if (existsSync13(candidate)) {
28988
+ try {
28989
+ const pkg = JSON.parse(readFileSync12(candidate, "utf-8"));
28990
+ if (pkg.name === "@arcote.tech/arc-cli") {
28991
+ const distIndex = join16(realpathSync3(cur), "dist", "index.js");
28992
+ if (!existsSync13(distIndex)) {
28993
+ throw new Error(`arc-cli bundle missing at ${distIndex}. Run \`bun run build\` in packages/cli/.`);
28994
+ }
28995
+ return distIndex;
28854
28996
  }
28997
+ } catch (e) {
28998
+ if (e instanceof Error && e.message.startsWith("arc-cli bundle"))
28999
+ throw e;
28855
29000
  }
28856
- const res = await fetch(`${base2()}/api/deploy/styles`, {
28857
- method: "POST",
28858
- body: form
28859
- });
28860
- if (!res.ok) {
28861
- throw new Error(`styles push failed: ${res.status} ${await res.text()}`);
28862
- }
28863
- }
28864
- const commitRes = await fetch(`${base2()}/api/deploy/manifest`, {
28865
- method: "POST",
28866
- headers: { "Content-Type": "application/json" },
28867
- body: JSON.stringify(localManifest)
28868
- });
28869
- if (!commitRes.ok) {
28870
- throw new Error(`manifest commit failed: ${commitRes.status} ${await commitRes.text()}`);
28871
29001
  }
28872
- const commit = await commitRes.json();
28873
- if (commit.needsRestart) {
28874
- tunnel = await restartAndReopen(cfg, env2, tunnel);
28875
- restarts += 1;
28876
- }
28877
- return {
28878
- env: env2,
28879
- frameworkChanged,
28880
- changedModules: diff.changedModules.map((m) => m.name),
28881
- stylesChanged: diff.stylesChanged,
28882
- restarts
28883
- };
28884
- } finally {
28885
- tunnel.close();
29002
+ const parent = dirname8(cur);
29003
+ if (parent === cur)
29004
+ break;
29005
+ cur = parent;
28886
29006
  }
29007
+ throw new Error("Could not locate @arcote.tech/arc-cli package.json walking up from " + here);
28887
29008
  }
28888
- function readDepsHash(path4) {
28889
- if (!existsSync13(path4))
28890
- return null;
28891
- return readFileSync13(path4, "utf-8").trim() || null;
28892
- }
28893
- function sanitizeName(name) {
28894
- return basename5(name);
28895
- }
28896
- async function restartAndReopen(cfg, env2, oldTunnel) {
28897
- console.log(`[arc] Restarting arc-${env2}...`);
28898
- oldTunnel.close();
28899
- await assertExec(cfg.target, `docker restart arc-${env2}`);
28900
- const tunnel = await openTunnel(cfg.target, 15500 + hashEnvToOffset(env2), "127.0.0.1", 2019);
28901
- await waitForHealthy(`http://127.0.0.1:${tunnel.localPort}/env/${env2}`, 60000);
28902
- return tunnel;
28903
- }
28904
- async function waitForHealthy(baseUrl, timeoutMs) {
28905
- const deadline = Date.now() + timeoutMs;
28906
- let lastErr;
28907
- while (Date.now() < deadline) {
28908
- try {
28909
- const res = await fetch(`${baseUrl}/api/deploy/health`, {
28910
- signal: AbortSignal.timeout(2000)
28911
- });
28912
- if (res.ok)
28913
- return;
28914
- lastErr = `status ${res.status}`;
28915
- } catch (e) {
28916
- lastErr = e;
29009
+ async function ensureDocker() {
29010
+ try {
29011
+ const proc2 = spawn5({
29012
+ cmd: ["docker", "--version"],
29013
+ stdout: "pipe",
29014
+ stderr: "pipe"
29015
+ });
29016
+ const exit = await proc2.exited;
29017
+ if (exit !== 0)
29018
+ throw new Error("docker --version exited non-zero");
29019
+ } catch {
29020
+ throw new Error("Docker is not available on PATH. Install Docker Desktop (or docker engine + buildx) before running deploy.");
29021
+ }
29022
+ }
29023
+ function computeContentHash(manifestPath) {
29024
+ const raw = JSON.parse(readFileSync12(manifestPath, "utf-8"));
29025
+ delete raw.buildTime;
29026
+ const canonical = JSON.stringify(raw);
29027
+ return createHash3("sha256").update(canonical).digest("hex").slice(0, 12);
29028
+ }
29029
+ function collectDockerfileInputs(ws) {
29030
+ const hasPublicDir = existsSync13(join16(ws.rootDir, "public"));
29031
+ const hasLocales = existsSync13(join16(ws.rootDir, "locales"));
29032
+ let manifestPath;
29033
+ for (const name of ["manifest.webmanifest", "manifest.json"]) {
29034
+ if (existsSync13(join16(ws.rootDir, name))) {
29035
+ manifestPath = name;
29036
+ break;
28917
29037
  }
28918
- await new Promise((r) => setTimeout(r, 1000));
28919
29038
  }
28920
- throw new Error(`Health check timeout: ${String(lastErr)}`);
29039
+ return {
29040
+ hasPublicDir,
29041
+ hasManifest: !!manifestPath,
29042
+ manifestPath,
29043
+ hasLocales
29044
+ };
29045
+ }
29046
+ function sanitizeImageName(name) {
29047
+ const cleaned = name.toLowerCase().replace(/^@/, "").replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
29048
+ return cleaned || "arc-app";
29049
+ }
29050
+
29051
+ // src/deploy/registry.ts
29052
+ var {spawn: spawn6 } = globalThis.Bun;
29053
+ async function dockerLogin(registry) {
29054
+ const password = process.env[registry.passwordEnv];
29055
+ if (!password) {
29056
+ throw new Error(`Registry password env var ${registry.passwordEnv} is not set. ` + `Set it in your shell before running deploy.`);
29057
+ }
29058
+ const proc2 = spawn6({
29059
+ cmd: [
29060
+ "docker",
29061
+ "login",
29062
+ registry.domain,
29063
+ "-u",
29064
+ registry.username,
29065
+ "--password-stdin"
29066
+ ],
29067
+ stdin: "pipe",
29068
+ stdout: "pipe",
29069
+ stderr: "pipe"
29070
+ });
29071
+ proc2.stdin.write(password);
29072
+ await proc2.stdin.end();
29073
+ const exit = await proc2.exited;
29074
+ if (exit !== 0) {
29075
+ const stderr = await new Response(proc2.stderr).text();
29076
+ throw new Error(`docker login ${registry.domain} failed (exit ${exit}): ${stderr.trim()}`);
29077
+ }
28921
29078
  }
28922
- function hashEnvToOffset(env2) {
28923
- let h = 0;
28924
- for (const ch of env2)
28925
- h = h * 31 + ch.charCodeAt(0) >>> 0;
28926
- return h % 100;
29079
+ async function dockerPush(fullRef) {
29080
+ const proc2 = spawn6({
29081
+ cmd: ["docker", "push", fullRef],
29082
+ stdout: "inherit",
29083
+ stderr: "inherit"
29084
+ });
29085
+ const exit = await proc2.exited;
29086
+ if (exit !== 0) {
29087
+ throw new Error(`docker push ${fullRef} failed (exit ${exit})`);
29088
+ }
28927
29089
  }
28928
29090
 
29091
+ // ../../node_modules/.bun/@clack+prompts@0.9.1/node_modules/@clack/prompts/dist/index.mjs
29092
+ import { stripVTControlCharacters as T2 } from "util";
29093
+
28929
29094
  // ../../node_modules/.bun/@clack+core@0.4.1/node_modules/@clack/core/dist/index.mjs
28930
29095
  var import_sisteransi = __toESM(require_src(), 1);
28931
29096
  import { stdin as $, stdout as j } from "process";
@@ -29449,6 +29614,21 @@ ${import_picocolors2.default.cyan(m2)}
29449
29614
  }
29450
29615
  } }).prompt();
29451
29616
  };
29617
+ var ye = (s = "", n = "") => {
29618
+ const t = `
29619
+ ${s}
29620
+ `.split(`
29621
+ `), i = T2(n).length, r2 = Math.max(t.reduce((o, l2) => {
29622
+ const $2 = T2(l2);
29623
+ return $2.length > o ? $2.length : o;
29624
+ }, 0), i) + 2, c2 = t.map((o) => `${import_picocolors2.default.gray(a)} ${import_picocolors2.default.dim(o)}${" ".repeat(r2 - T2(o).length)}${import_picocolors2.default.gray(a)}`).join(`
29625
+ `);
29626
+ process.stdout.write(`${import_picocolors2.default.gray(a)}
29627
+ ${import_picocolors2.default.green(S2)} ${import_picocolors2.default.reset(n)} ${import_picocolors2.default.gray(N2.repeat(Math.max(r2 - i - 1, 1)) + re)}
29628
+ ${c2}
29629
+ ${import_picocolors2.default.gray(ie + N2.repeat(r2 + 2) + ne)}
29630
+ `);
29631
+ };
29452
29632
  var ve = (s = "") => {
29453
29633
  process.stdout.write(`${import_picocolors2.default.gray(m2)} ${import_picocolors2.default.red(s)}
29454
29634
 
@@ -29590,6 +29770,45 @@ async function runSurvey() {
29590
29770
  });
29591
29771
  if (BD(email))
29592
29772
  cancel();
29773
+ ye(`The host runs a private Docker registry behind Caddy.
29774
+ Create an A record for the registry domain pointing to your host before deploy.`, "Registry");
29775
+ const registryDomain = await ue({
29776
+ message: "Registry domain (full FQDN)",
29777
+ placeholder: "registry.example.com",
29778
+ validate: (v2) => /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(v2) ? undefined : "Expected a domain"
29779
+ });
29780
+ if (BD(registryDomain))
29781
+ cancel();
29782
+ const registryUser = await ue({
29783
+ message: "Registry username",
29784
+ initialValue: "deploy",
29785
+ validate: (v2) => /^[a-z_][a-z0-9_-]*$/.test(v2) ? undefined : "Invalid username"
29786
+ });
29787
+ if (BD(registryUser))
29788
+ cancel();
29789
+ const registryPasswordEnv = await ue({
29790
+ message: "Env var holding the registry password",
29791
+ initialValue: "ARC_REGISTRY_PASSWORD",
29792
+ validate: (v2) => /^[A-Z][A-Z0-9_]*$/.test(v2) ? undefined : "Must be UPPER_SNAKE_CASE"
29793
+ });
29794
+ if (BD(registryPasswordEnv))
29795
+ cancel();
29796
+ const generatePassword = await me({
29797
+ message: "Generate a random password now?",
29798
+ initialValue: true
29799
+ });
29800
+ if (BD(generatePassword))
29801
+ cancel();
29802
+ if (generatePassword) {
29803
+ const random = generateRandomPassword(32);
29804
+ ye(`Save this password \u2014 you'll need it on every deploy.
29805
+
29806
+ export ${registryPasswordEnv}=${random}
29807
+
29808
+ This prompt is the only time it's shown.`, "Registry password");
29809
+ } else {
29810
+ ye(`Set ${registryPasswordEnv} in your shell before running deploy.`, "Registry password");
29811
+ }
29593
29812
  fe("Configuration ready \u2014 writing deploy.arc.json");
29594
29813
  return {
29595
29814
  target: {
@@ -29600,9 +29819,19 @@ async function runSurvey() {
29600
29819
  },
29601
29820
  envs,
29602
29821
  caddy: { email },
29822
+ registry: {
29823
+ domain: registryDomain,
29824
+ username: registryUser,
29825
+ passwordEnv: registryPasswordEnv
29826
+ },
29603
29827
  provision
29604
29828
  };
29605
29829
  }
29830
+ function generateRandomPassword(bytes) {
29831
+ const buf = new Uint8Array(bytes);
29832
+ crypto.getRandomValues(buf);
29833
+ return btoa(String.fromCharCode(...buf)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
29834
+ }
29606
29835
  function cancel() {
29607
29836
  ve("Cancelled.");
29608
29837
  process.exit(0);
@@ -29623,15 +29852,43 @@ async function platformDeploy(envArg, options = {}) {
29623
29852
  err(`Unknown env "${envArg}". Known: ${Object.keys(cfg.envs).join(", ")}`);
29624
29853
  process.exit(1);
29625
29854
  })() : Object.keys(cfg.envs);
29626
- const manifestPath = join18(ws.modulesDir, "manifest.json");
29627
- const needBuild = options.rebuild || !existsSync14(manifestPath);
29628
- if (needBuild && !options.skipBuild) {
29629
- log2("Building platform...");
29630
- await buildAll(ws, { noCache: options.rebuild });
29631
- ok("Build complete");
29632
- } else if (!existsSync14(manifestPath)) {
29633
- err("No build found and --skip-build was set.");
29634
- process.exit(1);
29855
+ const manifestPath = join17(ws.modulesDir, "manifest.json");
29856
+ if (!options.imageTag) {
29857
+ const needBuild = options.rebuild || !existsSync14(manifestPath);
29858
+ if (needBuild && !options.skipBuild) {
29859
+ log2("Building platform...");
29860
+ await buildAll(ws, { noCache: options.rebuild });
29861
+ ok("Build complete");
29862
+ } else if (!existsSync14(manifestPath)) {
29863
+ err("No build found and --skip-build was set.");
29864
+ process.exit(1);
29865
+ }
29866
+ }
29867
+ const imageName = sanitizeImageName(ws.rootPkg.name ?? ws.appName);
29868
+ let fullRef;
29869
+ let contentHash;
29870
+ if (options.imageTag) {
29871
+ contentHash = options.imageTag.includes(":") ? options.imageTag.split(":").pop() : options.imageTag;
29872
+ fullRef = `${cfg.registry.domain}/${imageName}:${contentHash}`;
29873
+ log2(`Pinning to existing image ${fullRef} (skipping build + push)`);
29874
+ } else {
29875
+ log2(`Building Docker image ${imageName}...`);
29876
+ const result = await buildImage(ws, {
29877
+ imageName,
29878
+ registryDomain: cfg.registry.domain
29879
+ });
29880
+ fullRef = result.fullRef;
29881
+ contentHash = result.contentHash;
29882
+ ok(`Image built: ${fullRef}`);
29883
+ if (options.buildOnly) {
29884
+ log2(`contentHash: ${contentHash}`);
29885
+ return;
29886
+ }
29887
+ log2(`Logging in to ${cfg.registry.domain}...`);
29888
+ await dockerLogin(cfg.registry);
29889
+ log2(`Pushing ${fullRef}...`);
29890
+ await dockerPush(fullRef);
29891
+ ok("Image pushed");
29635
29892
  }
29636
29893
  log2("Inspecting remote server...");
29637
29894
  const state = await detectRemoteState(cfg);
@@ -29648,58 +29905,53 @@ async function platformDeploy(envArg, options = {}) {
29648
29905
  });
29649
29906
  }
29650
29907
  for (const env2 of targetEnvs) {
29651
- log2(`Syncing env "${env2}"...`);
29652
- const outcome = await syncEnv({
29908
+ log2(`Updating env "${env2}"...`);
29909
+ const outcome = await updateEnvDeployment({
29910
+ target: cfg.target,
29653
29911
  cfg,
29654
29912
  env: env2,
29655
- ws,
29656
- projectDir: ws.rootDir
29913
+ fullRef
29657
29914
  });
29658
- if (outcome.changedModules.length === 0 && !outcome.shellChanged && !outcome.stylesChanged) {
29659
- ok(`${env2}: already up to date`);
29915
+ if (outcome.redeployed) {
29916
+ ok(`${env2}: live at ${fullRef}`);
29660
29917
  } else {
29661
- const parts = [];
29662
- if (outcome.changedModules.length > 0) {
29663
- parts.push(`${outcome.changedModules.length} module(s): ${outcome.changedModules.join(", ")}`);
29664
- }
29665
- if (outcome.shellChanged)
29666
- parts.push("shell");
29667
- if (outcome.stylesChanged)
29668
- parts.push("styles");
29669
- ok(`${env2}: updated ${parts.join(", ")}`);
29918
+ err(`${env2}: deployed but health check did not pass within retries \u2014 check \`docker logs arc-${env2}\``);
29670
29919
  }
29671
29920
  }
29672
29921
  }
29673
29922
  function readCliVersion() {
29674
- const candidates = [];
29675
- const entry = process.argv[1];
29676
- if (entry) {
29677
- candidates.push(join18(dirname7(entry), "..", "package.json"));
29678
- }
29679
29923
  try {
29680
- candidates.push(join18(import.meta.dir, "..", "..", "package.json"));
29681
- } catch {}
29682
- for (const path4 of candidates) {
29683
- try {
29684
- const pkg = JSON.parse(readFileSync14(path4, "utf-8"));
29685
- if (pkg.name === "@arcote.tech/arc-cli" && pkg.version) {
29686
- return pkg.version;
29687
- }
29688
- } catch {}
29924
+ let cur = dirname9(fileURLToPath7(import.meta.url));
29925
+ const root = dirname9(cur).startsWith("/") ? "/" : ".";
29926
+ while (cur !== root && cur !== "") {
29927
+ const candidate = join17(cur, "package.json");
29928
+ if (existsSync14(candidate)) {
29929
+ const pkg = JSON.parse(readFileSync13(candidate, "utf-8"));
29930
+ if (pkg.name === "@arcote.tech/arc-cli") {
29931
+ return pkg.version ?? "unknown";
29932
+ }
29933
+ }
29934
+ const parent = dirname9(cur);
29935
+ if (parent === cur)
29936
+ break;
29937
+ cur = parent;
29938
+ }
29939
+ return "unknown";
29940
+ } catch {
29941
+ return "unknown";
29689
29942
  }
29690
- return "unknown";
29691
29943
  }
29692
29944
  async function hashDeployConfig(rootDir) {
29693
- const p2 = join18(rootDir, "deploy.arc.json");
29694
- const content = readFileSync14(p2);
29945
+ const p2 = join17(rootDir, "deploy.arc.json");
29946
+ const content = readFileSync13(p2);
29695
29947
  const hasher = new Bun.CryptoHasher("sha256");
29696
29948
  hasher.update(content);
29697
29949
  return hasher.digest("hex").slice(0, 16);
29698
29950
  }
29699
29951
 
29700
- // src/commands/platform-dev.ts
29701
- import { existsSync as existsSync17, watch } from "fs";
29702
- import { join as join21 } from "path";
29952
+ // src/platform/startup.ts
29953
+ import { existsSync as existsSync16, readFileSync as readFileSync14, watch } from "fs";
29954
+ import { join as join19 } from "path";
29703
29955
 
29704
29956
  // ../host/src/create-server.ts
29705
29957
  var import_jsonwebtoken = __toESM(require_jsonwebtoken(), 1);
@@ -30159,8 +30411,8 @@ var d2 = class {
30159
30411
  throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part (" + this[e2].length + ")");
30160
30412
  if (c2 > w2)
30161
30413
  throw new TypeError("CronPattern: From value is larger than to value: '" + t + "'");
30162
- for (let T2 = c2;T2 <= w2; T2 += C2)
30163
- this.setPart(e2, T2, i[1] || s);
30414
+ for (let T3 = c2;T3 <= w2; T3 += C2)
30415
+ this.setPart(e2, T3, i[1] || s);
30164
30416
  }
30165
30417
  extractNth(t, e2) {
30166
30418
  let r2 = t, s;
@@ -30945,8 +31197,8 @@ function querySubscriptionHandler() {
30945
31197
  subscriptionId,
30946
31198
  data: data ?? null
30947
31199
  });
30948
- } catch (err3) {
30949
- console.error(`[Arc] Query subscription error:`, err3);
31200
+ } catch (err2) {
31201
+ console.error(`[Arc] Query subscription error:`, err2);
30950
31202
  }
30951
31203
  };
30952
31204
  sendData();
@@ -31149,231 +31401,8 @@ async function createArcServer(config) {
31149
31401
  };
31150
31402
  }
31151
31403
  // src/platform/server.ts
31152
- import { existsSync as existsSync16, mkdirSync as mkdirSync15 } from "fs";
31153
- import { join as join20 } from "path";
31154
-
31155
- // src/platform/deploy-api.ts
31156
- var {spawn: spawn4 } = globalThis.Bun;
31157
- import {
31158
- cpSync,
31159
- existsSync as existsSync15,
31160
- mkdirSync as mkdirSync14,
31161
- readFileSync as readFileSync15,
31162
- rmSync as rmSync4,
31163
- writeFileSync as writeFileSync15
31164
- } from "fs";
31165
- import { dirname as dirname8, join as join19, normalize as normalize2, resolve } from "path";
31166
- function createDeployApiHandler(opts) {
31167
- return async (req, url, ctx) => {
31168
- const p3 = url.pathname;
31169
- if (!p3.startsWith("/api/deploy/"))
31170
- return null;
31171
- const cors = ctx.corsHeaders;
31172
- if (p3 === "/api/deploy/health" && req.method === "GET") {
31173
- return Response.json({ ok: true, modules: opts.getManifest().modules.length }, { headers: cors });
31174
- }
31175
- if (p3 === "/api/deploy/framework" && req.method === "GET") {
31176
- const hashPath = join19(opts.ws.arcDir, ".deps-hash");
31177
- const depsHash = existsSync15(hashPath) ? readFileSync15(hashPath, "utf-8").trim() : null;
31178
- return Response.json({ depsHash }, { headers: cors });
31179
- }
31180
- if (p3 === "/api/deploy/manifest" && req.method === "GET") {
31181
- return Response.json(opts.getManifest(), { headers: cors });
31182
- }
31183
- if (p3 === "/api/deploy/framework" && req.method === "POST") {
31184
- const form = await req.formData();
31185
- const pkgFile = form.get("package.json");
31186
- const lockFile = form.get("bun.lock");
31187
- if (!isFile(pkgFile) || !isFile(lockFile)) {
31188
- return badRequest("framework requires package.json + bun.lock", cors);
31189
- }
31190
- mkdirSync14(opts.ws.arcDir, { recursive: true });
31191
- writeFileSync15(join19(opts.ws.arcDir, "package.json"), Buffer.from(await pkgFile.arrayBuffer()));
31192
- writeFileSync15(join19(opts.ws.arcDir, "bun.lock"), Buffer.from(await lockFile.arrayBuffer()));
31193
- const start = Date.now();
31194
- const installOk = await runBun(["install", "--production", "--frozen-lockfile"], opts.ws.arcDir);
31195
- if (!installOk) {
31196
- return Response.json({ error: "bun install failed" }, { status: 500, headers: cors });
31197
- }
31198
- const cliBin = process.argv[1] ?? "arc";
31199
- const shellOk = await runBun([
31200
- "run",
31201
- cliBin,
31202
- "_build-shell",
31203
- "--out",
31204
- opts.ws.shellDir,
31205
- "--from",
31206
- join19(opts.ws.arcDir, "node_modules")
31207
- ], opts.ws.arcDir);
31208
- if (!shellOk) {
31209
- return Response.json({ error: "shell build failed" }, { status: 500, headers: cors });
31210
- }
31211
- const newHash = sha256Hex(Buffer.concat([
31212
- readFileSync15(join19(opts.ws.arcDir, "package.json")),
31213
- readFileSync15(join19(opts.ws.arcDir, "bun.lock"))
31214
- ]));
31215
- writeFileSync15(join19(opts.ws.arcDir, ".deps-hash"), newHash + `
31216
- `);
31217
- return Response.json({
31218
- changed: true,
31219
- tookMs: Date.now() - start,
31220
- needsRestart: true
31221
- }, { headers: cors });
31222
- }
31223
- const moduleMatch = p3.match(/^\/api\/deploy\/modules\/([^/]+)$/);
31224
- if (moduleMatch && req.method === "POST") {
31225
- const safeName = sanitizeName2(moduleMatch[1]);
31226
- if (!safeName)
31227
- return badRequest("invalid module name", cors);
31228
- const form = await req.formData();
31229
- const browser = form.get("browser.js");
31230
- if (!isFile(browser)) {
31231
- return badRequest("modules/<name> requires browser.js", cors);
31232
- }
31233
- const stagingDir = join19(opts.ws.arcDir, ".staging", "modules", safeName);
31234
- mkdirSync14(stagingDir, { recursive: true });
31235
- await writeField(stagingDir, "browser.js", browser);
31236
- const server = form.get("server.js");
31237
- if (isFile(server)) {
31238
- await writeField(stagingDir, "server.js", server);
31239
- }
31240
- const pkg = form.get("package.json");
31241
- if (isFile(pkg)) {
31242
- await writeField(stagingDir, "package.json", pkg);
31243
- }
31244
- const access = form.get("access.json");
31245
- if (isFile(access)) {
31246
- await writeField(stagingDir, "access.json", access);
31247
- }
31248
- const stagingPkgPath = join19(stagingDir, "package.json");
31249
- const livePkgPath = join19(opts.ws.arcDir, "modules", safeName, "package.json");
31250
- const stagingPkgBytes = existsSync15(stagingPkgPath) ? readFileSync15(stagingPkgPath) : Buffer.from("");
31251
- const livePkgBytes = existsSync15(livePkgPath) ? readFileSync15(livePkgPath) : Buffer.from("");
31252
- const depsChanged = sha256Hex(stagingPkgBytes) !== sha256Hex(livePkgBytes);
31253
- writeFileSync15(join19(stagingDir, ".deps-hash"), sha256Hex(stagingPkgBytes) + `
31254
- `);
31255
- if (depsChanged && existsSync15(stagingPkgPath)) {
31256
- const ok2 = await runBun(["install", "--production"], stagingDir);
31257
- if (!ok2) {
31258
- rmSync4(stagingDir, { recursive: true, force: true });
31259
- return Response.json({ error: `bun install failed for module ${safeName}` }, { status: 500, headers: cors });
31260
- }
31261
- } else if (!depsChanged) {
31262
- const liveNodeModules = join19(opts.ws.arcDir, "modules", safeName, "node_modules");
31263
- if (existsSync15(liveNodeModules)) {
31264
- cpSync(liveNodeModules, join19(stagingDir, "node_modules"), {
31265
- recursive: true
31266
- });
31267
- }
31268
- }
31269
- return Response.json({
31270
- ok: true,
31271
- name: safeName,
31272
- depsChanged,
31273
- serverIncluded: isFile(server)
31274
- }, { headers: cors });
31275
- }
31276
- if (p3 === "/api/deploy/styles" && req.method === "POST") {
31277
- const form = await req.formData();
31278
- const stagingDir = join19(opts.ws.arcDir, ".staging");
31279
- mkdirSync14(stagingDir, { recursive: true });
31280
- const written = [];
31281
- for (const name of ["styles.css", "theme.css"]) {
31282
- const f2 = form.get(name);
31283
- if (isFile(f2)) {
31284
- await writeField(stagingDir, name, f2);
31285
- written.push(name);
31286
- }
31287
- }
31288
- return Response.json({ ok: true, written }, { headers: cors });
31289
- }
31290
- if (p3 === "/api/deploy/manifest" && req.method === "POST") {
31291
- const body = await req.json();
31292
- if (!validateManifest(body)) {
31293
- return badRequest("invalid manifest body", cors);
31294
- }
31295
- const stagingRoot = join19(opts.ws.arcDir, ".staging");
31296
- const stagingModules = join19(stagingRoot, "modules");
31297
- let serverSideChanged = false;
31298
- if (existsSync15(stagingModules)) {
31299
- const stagedNames = readDir(stagingModules);
31300
- for (const name of stagedNames) {
31301
- const src2 = join19(stagingModules, name);
31302
- const dst = join19(opts.ws.arcDir, "modules", name);
31303
- if (existsSync15(join19(src2, "server.js")))
31304
- serverSideChanged = true;
31305
- if (existsSync15(dst)) {
31306
- rmSync4(dst, { recursive: true, force: true });
31307
- }
31308
- mkdirSync14(dirname8(dst), { recursive: true });
31309
- cpSync(src2, dst, { recursive: true });
31310
- }
31311
- }
31312
- for (const name of ["styles.css", "theme.css"]) {
31313
- const src2 = join19(stagingRoot, name);
31314
- if (existsSync15(src2)) {
31315
- cpSync(src2, join19(opts.ws.arcDir, name));
31316
- }
31317
- }
31318
- writeFileSync15(join19(opts.ws.modulesDir, "manifest.json"), JSON.stringify(body, null, 2));
31319
- opts.setManifest(body);
31320
- rmSync4(stagingRoot, { recursive: true, force: true });
31321
- if (!serverSideChanged) {
31322
- opts.notifyReload(body);
31323
- }
31324
- return Response.json({
31325
- ok: true,
31326
- moduleCount: body.modules.length,
31327
- needsRestart: serverSideChanged
31328
- }, { headers: cors });
31329
- }
31330
- return new Response("Not Found", { status: 404, headers: cors });
31331
- };
31332
- }
31333
- function badRequest(msg, cors) {
31334
- return Response.json({ error: msg }, { status: 400, headers: cors });
31335
- }
31336
- function isFile(v3) {
31337
- return typeof v3 === "object" && v3 !== null && typeof v3.arrayBuffer === "function" && typeof v3.name === "string";
31338
- }
31339
- async function writeField(targetDir, name, file) {
31340
- const safe = normalize2(name);
31341
- const safeRoot = resolve(targetDir);
31342
- const full = resolve(safeRoot, safe);
31343
- if (!full.startsWith(safeRoot + "/") && full !== safeRoot) {
31344
- throw new Error(`Path traversal rejected: ${name}`);
31345
- }
31346
- mkdirSync14(dirname8(full), { recursive: true });
31347
- writeFileSync15(full, Buffer.from(await file.arrayBuffer()));
31348
- }
31349
- function sanitizeName2(name) {
31350
- if (!/^[a-zA-Z0-9_-]+$/.test(name))
31351
- return null;
31352
- return name;
31353
- }
31354
- function readDir(dir) {
31355
- if (!existsSync15(dir))
31356
- return [];
31357
- return __require("fs").readdirSync(dir);
31358
- }
31359
- async function runBun(args, cwd) {
31360
- const proc2 = spawn4({
31361
- cmd: ["bun", ...args],
31362
- cwd,
31363
- stdout: "inherit",
31364
- stderr: "inherit"
31365
- });
31366
- const exit = await proc2.exited;
31367
- return exit === 0;
31368
- }
31369
- function validateManifest(m4) {
31370
- if (!m4 || typeof m4 !== "object")
31371
- return false;
31372
- const cast = m4;
31373
- return Array.isArray(cast.modules) && typeof cast.shellHash === "string" && typeof cast.stylesHash === "string" && typeof cast.buildTime === "string";
31374
- }
31375
-
31376
- // src/platform/server.ts
31404
+ import { existsSync as existsSync15, mkdirSync as mkdirSync14 } from "fs";
31405
+ import { join as join18 } from "path";
31377
31406
  function generateShellHtml(appName, manifest, arcEntries) {
31378
31407
  const arcImports = {};
31379
31408
  if (arcEntries) {
@@ -31437,7 +31466,7 @@ function getMime(path4) {
31437
31466
  return MIME[ext2] ?? "application/octet-stream";
31438
31467
  }
31439
31468
  function serveFile(filePath, headers = {}) {
31440
- if (!existsSync16(filePath))
31469
+ if (!existsSync15(filePath))
31441
31470
  return new Response("Not Found", { status: 404 });
31442
31471
  return new Response(Bun.file(filePath), {
31443
31472
  headers: { "Content-Type": getMime(filePath), ...headers }
@@ -31445,20 +31474,20 @@ function serveFile(filePath, headers = {}) {
31445
31474
  }
31446
31475
  var MODULE_SIG_SECRET = process.env.ARC_MODULE_SECRET ?? crypto.randomUUID();
31447
31476
  var MODULE_SIG_TTL = 3600;
31448
- function signModuleUrl(filename) {
31477
+ function signChunkUrl(chunk, file) {
31449
31478
  const exp = Math.floor(Date.now() / 1000) + MODULE_SIG_TTL;
31450
31479
  const hasher = new Bun.CryptoHasher("sha256");
31451
- hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
31480
+ hasher.update(`${chunk}/${file}:${exp}:${MODULE_SIG_SECRET}`);
31452
31481
  const sig = hasher.digest("hex").slice(0, 16);
31453
- return `/modules/${filename}?sig=${sig}&exp=${exp}`;
31482
+ return `/modules/${chunk}/${file}?sig=${sig}&exp=${exp}`;
31454
31483
  }
31455
- function verifyModuleSignature(filename, sig, exp) {
31484
+ function verifyChunkSignature(chunk, file, sig, exp) {
31456
31485
  if (!sig || !exp)
31457
31486
  return false;
31458
31487
  if (Number(exp) < Date.now() / 1000)
31459
31488
  return false;
31460
31489
  const hasher = new Bun.CryptoHasher("sha256");
31461
- hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
31490
+ hasher.update(`${chunk}/${file}:${exp}:${MODULE_SIG_SECRET}`);
31462
31491
  return hasher.digest("hex").slice(0, 16) === sig;
31463
31492
  }
31464
31493
  function decodeTokenPayload(jwt2) {
@@ -31493,70 +31522,90 @@ function parseArcTokensHeader(header) {
31493
31522
  return payloads;
31494
31523
  }
31495
31524
  async function filterManifestForTokens(manifest, moduleAccessMap, tokenPayloads) {
31525
+ const allowedChunks = new Set(["public"]);
31526
+ for (const t of tokenPayloads) {
31527
+ if (t?.tokenType)
31528
+ allowedChunks.add(t.tokenType);
31529
+ }
31496
31530
  const filtered = [];
31497
31531
  for (const mod of manifest.modules) {
31498
- const access = moduleAccessMap.get(mod.name);
31499
- if (!access) {
31532
+ if (!allowedChunks.has(mod.chunk))
31533
+ continue;
31534
+ if (mod.chunk === "public") {
31500
31535
  filtered.push(mod);
31501
31536
  continue;
31502
31537
  }
31503
- if (tokenPayloads.length === 0)
31504
- continue;
31505
- let granted = false;
31506
- for (const rule of access.rules) {
31507
- const matching = tokenPayloads.find((t) => t.tokenType === rule.token.name);
31508
- if (matching) {
31538
+ const access = moduleAccessMap.get(mod.name);
31539
+ let granted = true;
31540
+ if (access && access.rules.length > 0) {
31541
+ granted = false;
31542
+ for (const rule of access.rules) {
31543
+ if (rule.token.name !== mod.chunk)
31544
+ continue;
31545
+ const matching = tokenPayloads.find((t) => t.tokenType === rule.token.name);
31546
+ if (!matching)
31547
+ continue;
31509
31548
  granted = rule.check ? await rule.check(matching) : true;
31510
31549
  if (granted)
31511
31550
  break;
31512
31551
  }
31513
31552
  }
31514
31553
  if (granted) {
31515
- filtered.push({ ...mod, url: signModuleUrl(mod.file) });
31554
+ filtered.push({ ...mod, url: signChunkUrl(mod.chunk, mod.file) });
31516
31555
  }
31517
31556
  }
31518
31557
  return {
31519
31558
  modules: filtered,
31559
+ chunks: manifest.chunks,
31520
31560
  shellHash: manifest.shellHash,
31521
31561
  stylesHash: manifest.stylesHash,
31522
31562
  buildTime: manifest.buildTime
31523
31563
  };
31524
31564
  }
31525
- function staticFilesHandler(ws, devMode, moduleAccessMap) {
31565
+ var CHUNK_NAME_RE = /^[A-Za-z0-9_-]+$/;
31566
+ var MODULE_FILE_RE = /^[A-Za-z0-9_.-]+\.js$/;
31567
+ function staticFilesHandler(ws, devMode) {
31526
31568
  return (_req, url, ctx) => {
31527
31569
  const path4 = url.pathname;
31528
31570
  if (path4.startsWith("/shell/"))
31529
- return serveFile(join20(ws.shellDir, path4.slice(7)), ctx.corsHeaders);
31571
+ return serveFile(join18(ws.shellDir, path4.slice(7)), ctx.corsHeaders);
31530
31572
  if (path4.startsWith("/modules/")) {
31531
- const fileWithParams = path4.slice(9);
31532
- const filename = fileWithParams.split("?")[0];
31533
- const moduleName = filename.replace(/\.js$/, "");
31534
- if (moduleAccessMap.has(moduleName)) {
31573
+ const rest = path4.slice(9);
31574
+ const slash = rest.indexOf("/");
31575
+ if (slash <= 0) {
31576
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
31577
+ }
31578
+ const chunk = rest.slice(0, slash);
31579
+ const file = rest.slice(slash + 1);
31580
+ if (!CHUNK_NAME_RE.test(chunk) || !MODULE_FILE_RE.test(file)) {
31581
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
31582
+ }
31583
+ if (chunk !== "public") {
31535
31584
  const sig = url.searchParams.get("sig");
31536
31585
  const exp = url.searchParams.get("exp");
31537
- if (!verifyModuleSignature(filename, sig, exp)) {
31586
+ if (!verifyChunkSignature(chunk, file, sig, exp)) {
31538
31587
  return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
31539
31588
  }
31540
31589
  }
31541
- return serveFile(join20(ws.modulesDir, filename), {
31590
+ return serveFile(join18(ws.modulesDir, chunk, file), {
31542
31591
  ...ctx.corsHeaders,
31543
31592
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
31544
31593
  });
31545
31594
  }
31546
31595
  if (path4.startsWith("/locales/"))
31547
- return serveFile(join20(ws.arcDir, path4.slice(1)), ctx.corsHeaders);
31596
+ return serveFile(join18(ws.arcDir, path4.slice(1)), ctx.corsHeaders);
31548
31597
  if (path4.startsWith("/assets/"))
31549
- return serveFile(join20(ws.assetsDir, path4.slice(8)), ctx.corsHeaders);
31598
+ return serveFile(join18(ws.assetsDir, path4.slice(8)), ctx.corsHeaders);
31550
31599
  if (path4 === "/styles.css")
31551
- return serveFile(join20(ws.arcDir, "styles.css"), ctx.corsHeaders);
31600
+ return serveFile(join18(ws.arcDir, "styles.css"), ctx.corsHeaders);
31552
31601
  if (path4 === "/theme.css")
31553
- return serveFile(join20(ws.arcDir, "theme.css"), ctx.corsHeaders);
31602
+ return serveFile(join18(ws.arcDir, "theme.css"), ctx.corsHeaders);
31554
31603
  if ((path4 === "/manifest.json" || path4 === "/manifest.webmanifest") && ws.manifest) {
31555
31604
  return serveFile(ws.manifest.path, ctx.corsHeaders);
31556
31605
  }
31557
31606
  if (path4.lastIndexOf(".") > path4.lastIndexOf("/")) {
31558
- const publicFile = join20(ws.publicDir, path4.slice(1));
31559
- if (existsSync16(publicFile))
31607
+ const publicFile = join18(ws.publicDir, path4.slice(1));
31608
+ if (existsSync15(publicFile))
31560
31609
  return serveFile(publicFile, ctx.corsHeaders);
31561
31610
  }
31562
31611
  return null;
@@ -31642,13 +31691,6 @@ async function startPlatformServer(opts) {
31642
31691
  }
31643
31692
  }
31644
31693
  };
31645
- const deployApiEnabled = opts.deployApi ?? process.env.ARC_DEPLOY_API === "1";
31646
- const deployApiHandler = deployApiEnabled ? createDeployApiHandler({
31647
- ws,
31648
- getManifest,
31649
- setManifest,
31650
- notifyReload
31651
- }) : null;
31652
31694
  if (!context) {
31653
31695
  const cors = {
31654
31696
  "Access-Control-Allow-Origin": "*",
@@ -31670,10 +31712,9 @@ async function startPlatformServer(opts) {
31670
31712
  corsHeaders: cors
31671
31713
  };
31672
31714
  const handlers = [
31673
- ...deployApiHandler ? [deployApiHandler] : [],
31674
31715
  apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
31675
31716
  devReloadHandler(sseClients),
31676
- staticFilesHandler(ws, !!devMode, moduleAccessMap),
31717
+ staticFilesHandler(ws, !!devMode),
31677
31718
  spaFallbackHandler(shellHtml)
31678
31719
  ];
31679
31720
  for (const handler of handlers) {
@@ -31694,19 +31735,18 @@ async function startPlatformServer(opts) {
31694
31735
  };
31695
31736
  }
31696
31737
  const { createBunSQLiteAdapterFactory: createBunSQLiteAdapterFactory2 } = await Promise.resolve().then(() => (init_dist(), exports_dist2));
31697
- const dbPath = opts.dbPath || join20(ws.arcDir, "data", "arc.db");
31738
+ const dbPath = opts.dbPath || join18(ws.arcDir, "data", "arc.db");
31698
31739
  const dbDir = dbPath.substring(0, dbPath.lastIndexOf("/"));
31699
31740
  if (dbDir)
31700
- mkdirSync15(dbDir, { recursive: true });
31741
+ mkdirSync14(dbDir, { recursive: true });
31701
31742
  const arcServer = await createArcServer({
31702
31743
  context,
31703
31744
  dbAdapterFactory: createBunSQLiteAdapterFactory2(dbPath),
31704
31745
  port,
31705
31746
  httpHandlers: [
31706
- ...deployApiHandler ? [deployApiHandler] : [],
31707
31747
  apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
31708
31748
  devReloadHandler(sseClients),
31709
- staticFilesHandler(ws, !!devMode, moduleAccessMap),
31749
+ staticFilesHandler(ws, !!devMode),
31710
31750
  spaFallbackHandler(shellHtml)
31711
31751
  ],
31712
31752
  onWsClose: (clientId) => cleanupClientSubs(clientId)
@@ -31721,13 +31761,24 @@ async function startPlatformServer(opts) {
31721
31761
  };
31722
31762
  }
31723
31763
 
31724
- // src/commands/platform-dev.ts
31725
- async function platformDev(opts = {}) {
31726
- const ws = resolveWorkspace();
31727
- const port = 5005;
31728
- let manifest = await buildAll(ws, { noCache: opts.noCache });
31764
+ // src/platform/startup.ts
31765
+ async function startPlatform(opts) {
31766
+ const { ws, devMode } = opts;
31767
+ const port = opts.port ?? parseInt(process.env.PORT || "5005", 10);
31768
+ const dbPath = opts.dbPath ?? join19(ws.rootDir, ".arc", "data", devMode ? "dev.db" : "prod.db");
31769
+ let manifest;
31770
+ if (devMode) {
31771
+ manifest = await buildAll(ws);
31772
+ } else {
31773
+ const manifestPath = join19(ws.modulesDir, "manifest.json");
31774
+ if (!existsSync16(manifestPath)) {
31775
+ err("No build found. Run `arc platform build` first.");
31776
+ process.exit(1);
31777
+ }
31778
+ manifest = JSON.parse(readFileSync14(manifestPath, "utf-8"));
31779
+ }
31729
31780
  log2("Loading server context...");
31730
- const { context, moduleAccess } = await loadServerContext(ws.packages);
31781
+ const { context, moduleAccess } = await loadServerContext(ws);
31731
31782
  if (context) {
31732
31783
  ok("Context loaded");
31733
31784
  } else {
@@ -31740,13 +31791,25 @@ async function platformDev(opts = {}) {
31740
31791
  manifest,
31741
31792
  context,
31742
31793
  moduleAccess,
31743
- dbPath: join21(ws.rootDir, ".arc", "data", "dev.db"),
31744
- devMode: true,
31794
+ dbPath,
31795
+ devMode,
31745
31796
  arcEntries
31746
31797
  });
31747
31798
  ok(`Server on http://localhost:${port}`);
31748
- if (platform3.contextHandler)
31799
+ if (platform3.contextHandler) {
31749
31800
  ok("Commands, queries, WebSocket \u2014 all on same port");
31801
+ }
31802
+ if (devMode) {
31803
+ attachDevWatcher(ws, platform3);
31804
+ }
31805
+ const cleanup = () => {
31806
+ platform3.stop();
31807
+ process.exit(0);
31808
+ };
31809
+ process.on("SIGTERM", cleanup);
31810
+ process.on("SIGINT", cleanup);
31811
+ }
31812
+ function attachDevWatcher(ws, platform3) {
31750
31813
  log2("Watching for changes...");
31751
31814
  let rebuildTimer = null;
31752
31815
  let isRebuilding = false;
@@ -31759,10 +31822,10 @@ async function platformDev(opts = {}) {
31759
31822
  isRebuilding = true;
31760
31823
  log2("Rebuilding...");
31761
31824
  try {
31762
- manifest = await buildAll(ws);
31763
- platform3.setManifest(manifest);
31764
- platform3.notifyReload(manifest);
31765
- ok(`Rebuilt \u2014 ${manifest.modules.length} module(s)`);
31825
+ const next = await buildAll(ws);
31826
+ platform3.setManifest(next);
31827
+ platform3.notifyReload(next);
31828
+ ok(`Rebuilt \u2014 ${next.modules.length} module(s)`);
31766
31829
  } catch (e2) {
31767
31830
  console.error(`Rebuild failed: ${e2}`);
31768
31831
  } finally {
@@ -31771,8 +31834,8 @@ async function platformDev(opts = {}) {
31771
31834
  }, 300);
31772
31835
  };
31773
31836
  for (const pkg of ws.packages) {
31774
- const srcDir = join21(pkg.path, "src");
31775
- if (!existsSync17(srcDir))
31837
+ const srcDir = join19(pkg.path, "src");
31838
+ if (!existsSync16(srcDir))
31776
31839
  continue;
31777
31840
  watch(srcDir, { recursive: true }, (_event, filename) => {
31778
31841
  if (!filename || filename.includes(".arc") || filename.endsWith(".d.ts") || filename.includes("node_modules") || filename.includes("dist"))
@@ -31782,91 +31845,26 @@ async function platformDev(opts = {}) {
31782
31845
  triggerRebuild();
31783
31846
  });
31784
31847
  }
31785
- const localesDir = join21(ws.rootDir, "locales");
31786
- if (existsSync17(localesDir)) {
31848
+ const localesDir = join19(ws.rootDir, "locales");
31849
+ if (existsSync16(localesDir)) {
31787
31850
  watch(localesDir, { recursive: false }, (_event, filename) => {
31788
31851
  if (!filename?.endsWith(".po"))
31789
31852
  return;
31790
31853
  triggerRebuild();
31791
31854
  });
31792
31855
  }
31793
- const cleanup = () => {
31794
- platform3.stop();
31795
- process.exit(0);
31796
- };
31797
- process.on("SIGTERM", cleanup);
31798
- process.on("SIGINT", cleanup);
31856
+ }
31857
+
31858
+ // src/commands/platform-dev.ts
31859
+ async function platformDev(opts = {}) {
31860
+ const ws = resolveWorkspace();
31861
+ await startPlatform({ ws, devMode: true });
31799
31862
  }
31800
31863
 
31801
31864
  // src/commands/platform-start.ts
31802
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
31803
- import { join as join22 } from "path";
31804
31865
  async function platformStart() {
31805
31866
  const ws = resolveWorkspace();
31806
- const port = parseInt(process.env.PORT || "5005", 10);
31807
- const deployApi = process.env.ARC_DEPLOY_API === "1";
31808
- const manifestPath = join22(ws.modulesDir, "manifest.json");
31809
- if (!existsSync18(manifestPath)) {
31810
- if (!deployApi) {
31811
- err("No build found. Run `arc platform build` first.");
31812
- process.exit(1);
31813
- }
31814
- log2("Pre-deploy mode \u2014 no manifest yet, awaiting first /api/deploy/*");
31815
- const emptyManifest = {
31816
- modules: [],
31817
- shellHash: "",
31818
- stylesHash: "",
31819
- buildTime: new Date().toISOString()
31820
- };
31821
- const platform4 = await startPlatformServer({
31822
- ws,
31823
- port,
31824
- manifest: emptyManifest,
31825
- context: null,
31826
- moduleAccess: new Map,
31827
- dbPath: join22(ws.rootDir, ".arc", "data", "prod.db"),
31828
- devMode: false,
31829
- deployApi: true,
31830
- arcEntries: []
31831
- });
31832
- ok(`Pre-deploy server on http://localhost:${port}`);
31833
- registerSignalCleanup(platform4);
31834
- return;
31835
- }
31836
- const manifest = JSON.parse(readFileSync16(manifestPath, "utf-8"));
31837
- log2("Loading server context...");
31838
- const { context, moduleAccess } = await loadServerContext(ws.packages);
31839
- if (context) {
31840
- ok("Context loaded");
31841
- } else {
31842
- log2("No context \u2014 server endpoints skipped");
31843
- }
31844
- const arcEntries = collectArcPeerDeps(ws.packages);
31845
- if (deployApi)
31846
- ok("Deploy API enabled (/api/deploy/*)");
31847
- const platform3 = await startPlatformServer({
31848
- ws,
31849
- port,
31850
- manifest,
31851
- context,
31852
- moduleAccess,
31853
- dbPath: join22(ws.rootDir, ".arc", "data", "prod.db"),
31854
- devMode: false,
31855
- deployApi,
31856
- arcEntries
31857
- });
31858
- ok(`Server on http://localhost:${port}`);
31859
- if (platform3.contextHandler)
31860
- ok("Commands, queries, WebSocket \u2014 all on same port");
31861
- registerSignalCleanup(platform3);
31862
- }
31863
- function registerSignalCleanup(platform3) {
31864
- const cleanup = () => {
31865
- platform3.stop();
31866
- process.exit(0);
31867
- };
31868
- process.on("SIGTERM", cleanup);
31869
- process.on("SIGINT", cleanup);
31867
+ await startPlatform({ ws, devMode: false });
31870
31868
  }
31871
31869
 
31872
31870
  // src/index.ts
@@ -31878,8 +31876,7 @@ var platform3 = program2.command("platform").description("Platform commands \u20
31878
31876
  platform3.command("dev").description("Start platform in dev mode (Bun server + Vite HMR)").option("--no-cache", "Force full rebuild on startup").action((opts) => platformDev({ noCache: opts.cache === false }));
31879
31877
  platform3.command("build").description("Build platform for production").option("--no-cache", "Force full rebuild").action((opts) => platformBuild({ noCache: opts.cache === false }));
31880
31878
  platform3.command("start").description("Start platform in production mode (requires prior build)").action(platformStart);
31881
- platform3.command("deploy [env]").description("Deploy platform to a remote server (reads deploy.arc.json, surveys if missing)").option("--skip-build", "Skip local build step").option("--rebuild", "Force rebuild before deploy").action((env2, opts) => platformDeploy(env2, opts));
31882
- program2.command("_build-shell", { hidden: true }).description("Build framework shell bundles from a node_modules dir").requiredOption("--out <dir>", "Output directory for shell .js bundles").requiredOption("--from <dir>", "node_modules directory to discover packages in").action((opts) => buildShell(opts));
31879
+ platform3.command("deploy [env]").description("Deploy platform to a remote server (reads deploy.arc.json, surveys if missing)").option("--skip-build", "Skip local build step").option("--rebuild", "Force rebuild before deploy").option("--build-only", "Build the Docker image locally, then exit (no remote push)").option("--image-tag <hash>", "Roll back / pin to an existing image tag instead of building a new one").action((env2, opts) => platformDeploy(env2, opts));
31883
31880
  program2.parse(process.argv);
31884
31881
  if (process.argv.length === 2) {
31885
31882
  program2.help();