@spotpatch/next 0.5.0 → 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/cli.js CHANGED
@@ -3,11 +3,13 @@
3
3
  // src/cli.ts
4
4
  import { createRequire as createRequire2 } from "module";
5
5
  import path4 from "path";
6
+ import { runSpotPatchBridgeCli } from "@spotpatch/bridge";
6
7
 
7
8
  // src/cli-owner.ts
8
9
  import { spawn } from "child_process";
9
10
  import { randomBytes } from "crypto";
10
11
  import { serializeResolvedSpotPatchOptions } from "@spotpatch/dev-server";
12
+ import { SPOTPATCH_API_BASE as SPOTPATCH_API_BASE2 } from "@spotpatch/shared";
11
13
 
12
14
  // src/cli-args.ts
13
15
  import { isLoopbackHostname } from "@spotpatch/dev-server";
@@ -282,13 +284,18 @@ import {
282
284
  } from "http";
283
285
  import {
284
286
  createAgentJobManager,
287
+ createExternalHandoffService,
285
288
  createRuntimeAiConfig,
286
289
  createRuntimeDataFlowConfig,
287
290
  createSession,
288
291
  createSourceRegistrationService,
289
292
  createSourceRegistry,
290
- createSpotPatchMiddleware
293
+ createSpotPatchMiddleware,
294
+ resolveManagedExecutionValidation
291
295
  } from "@spotpatch/dev-server";
296
+ import {
297
+ createExternalAgentSupervisor
298
+ } from "@spotpatch/bridge";
292
299
  import {
293
300
  SPOTPATCH_API_BASE,
294
301
  SPOTPATCH_ENDPOINTS,
@@ -298,7 +305,7 @@ import {
298
305
  // package.json
299
306
  var package_default = {
300
307
  name: "@spotpatch/next",
301
- version: "0.5.0",
308
+ version: "0.7.0",
302
309
  description: "Development-only Next.js adapter and CLI for SpotPatch.",
303
310
  license: "MIT",
304
311
  repository: {
@@ -391,6 +398,7 @@ var package_default = {
391
398
  "verify:loader": "node scripts/verify-loader.cjs"
392
399
  },
393
400
  dependencies: {
401
+ "@spotpatch/bridge": "workspace:^",
394
402
  "@spotpatch/compiler": "workspace:^",
395
403
  "@spotpatch/dev-server": "workspace:^",
396
404
  "@spotpatch/runtime": "workspace:^",
@@ -513,38 +521,102 @@ function validateCredentialEnvironment(options, credentials) {
513
521
  throw new TypeError("The SpotPatch credential environment is inconsistent.");
514
522
  }
515
523
  }
516
- async function selfCheckSidecar(sidecarOrigin, publicOrigin, expectedConfig) {
517
- const response = await fetch(new URL(SPOTPATCH_ENDPOINTS.bootstrap, sidecarOrigin), {
518
- method: "POST",
519
- headers: {
520
- "Content-Type": "application/json",
521
- Origin: publicOrigin,
522
- "Sec-Fetch-Site": "same-origin"
523
- },
524
- body: "{}",
525
- signal: AbortSignal.timeout(SIDECAR_SELF_CHECK_TIMEOUT_MS)
526
- });
524
+ function diagnosticUrl(value, fallbackOrigin) {
525
+ try {
526
+ const url = new URL(value, fallbackOrigin);
527
+ return `${url.origin}${url.pathname}`;
528
+ } catch {
529
+ return fallbackOrigin;
530
+ }
531
+ }
532
+ async function checkBootstrapRoute(routeOrigin, expectedOrigin, expectedConfig) {
533
+ const requestUrl = new URL(SPOTPATCH_ENDPOINTS.bootstrap, routeOrigin);
534
+ let response;
535
+ try {
536
+ response = await fetch(requestUrl, {
537
+ method: "POST",
538
+ headers: {
539
+ "Content-Type": "application/json",
540
+ Origin: expectedOrigin,
541
+ "Sec-Fetch-Site": "same-origin"
542
+ },
543
+ body: "{}",
544
+ redirect: "manual",
545
+ signal: AbortSignal.timeout(SIDECAR_SELF_CHECK_TIMEOUT_MS)
546
+ });
547
+ } catch {
548
+ return Object.freeze({
549
+ finalUrl: diagnosticUrl(requestUrl.href, routeOrigin),
550
+ kind: "unreachable",
551
+ ok: false
552
+ });
553
+ }
554
+ const redirectLocation = response.headers.get("location");
555
+ const finalUrl = diagnosticUrl(
556
+ redirectLocation === null ? response.url || requestUrl.href : new URL(redirectLocation, response.url || requestUrl.href).href,
557
+ routeOrigin
558
+ );
527
559
  const declaredLength = Number(response.headers.get("content-length"));
528
560
  if (Number.isFinite(declaredLength) && declaredLength > SIDECAR_SELF_CHECK_LIMIT_BYTES) {
529
561
  await response.body?.cancel();
530
- throw new Error("SpotPatch Sidecar self-check failed.");
562
+ return Object.freeze({
563
+ finalUrl,
564
+ kind: "response-invalid",
565
+ ok: false,
566
+ status: response.status
567
+ });
531
568
  }
532
569
  const text = await response.text();
533
- if (!response.ok || !response.headers.get("cache-control")?.toLowerCase().includes("no-store") || Buffer.byteLength(text, "utf8") > SIDECAR_SELF_CHECK_LIMIT_BYTES) {
534
- throw new Error("SpotPatch Sidecar self-check failed.");
570
+ if (!response.ok) {
571
+ return Object.freeze({
572
+ finalUrl,
573
+ kind: "http",
574
+ ok: false,
575
+ status: response.status
576
+ });
577
+ }
578
+ if (!response.headers.get("cache-control")?.toLowerCase().includes("no-store") || Buffer.byteLength(text, "utf8") > SIDECAR_SELF_CHECK_LIMIT_BYTES) {
579
+ return Object.freeze({
580
+ finalUrl,
581
+ kind: "response-invalid",
582
+ ok: false,
583
+ status: response.status
584
+ });
535
585
  }
536
586
  let value;
537
587
  try {
538
588
  value = JSON.parse(text);
539
- } catch (error) {
540
- throw new Error("SpotPatch Sidecar self-check failed.", { cause: error });
589
+ } catch {
590
+ return Object.freeze({
591
+ finalUrl,
592
+ kind: "response-invalid",
593
+ ok: false,
594
+ status: response.status
595
+ });
541
596
  }
542
597
  if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).length !== 2 || !("ok" in value) || value.ok !== true || !("data" in value)) {
543
- throw new Error("SpotPatch Sidecar self-check failed.");
598
+ return Object.freeze({
599
+ finalUrl,
600
+ kind: "response-invalid",
601
+ ok: false,
602
+ status: response.status
603
+ });
544
604
  }
545
605
  const parsed = runtimeConfigSchema.safeParse(value.data);
546
606
  const parsedExpected = runtimeConfigSchema.safeParse(expectedConfig);
547
607
  if (!parsed.success || !parsedExpected.success || JSON.stringify(parsed.data) !== JSON.stringify(parsedExpected.data)) {
608
+ return Object.freeze({
609
+ finalUrl,
610
+ kind: "response-invalid",
611
+ ok: false,
612
+ status: response.status
613
+ });
614
+ }
615
+ return Object.freeze({ ok: true });
616
+ }
617
+ async function selfCheckSidecar(sidecarOrigin, publicOrigin, expectedConfig) {
618
+ const result = await checkBootstrapRoute(sidecarOrigin, publicOrigin, expectedConfig);
619
+ if (!result.ok) {
548
620
  throw new Error("SpotPatch Sidecar self-check failed.");
549
621
  }
550
622
  }
@@ -564,6 +636,10 @@ async function createNextSidecar(sidecarOptions) {
564
636
  let closed = false;
565
637
  let registry;
566
638
  let agentManager;
639
+ let externalHandoffService;
640
+ let externalAgentSupervisor;
641
+ let spotPatchMiddleware;
642
+ let publicRouteCheck;
567
643
  let handler = (_request, response) => {
568
644
  writeUnavailable(response, 503);
569
645
  };
@@ -626,6 +702,7 @@ async function createNextSidecar(sidecarOptions) {
626
702
  bundler: input.bundler,
627
703
  debug: input.options.debug,
628
704
  editor: input.options.editor,
705
+ externalAgent: input.options.externalAgent,
629
706
  framework: "next",
630
707
  frameworkVersion: input.nextVersion,
631
708
  locale: input.options.locale,
@@ -642,8 +719,49 @@ async function createNextSidecar(sidecarOptions) {
642
719
  environment: input.credentials,
643
720
  root: input.projectRoot
644
721
  });
722
+ const handoffService = input.options.externalAgent.enabled ? createExternalHandoffService({
723
+ framework: "next",
724
+ root: input.appRoot,
725
+ sessionId: session.id
726
+ }) : void 0;
727
+ let handoffReady = false;
728
+ if (handoffService !== void 0) {
729
+ try {
730
+ await handoffService.start();
731
+ handoffReady = true;
732
+ } catch {
733
+ process.stderr.write(
734
+ "[spotpatch:next] External Agent handoff is unavailable; core tools remain active.\n"
735
+ );
736
+ }
737
+ }
738
+ externalHandoffService = handoffService;
739
+ let supervisor;
740
+ if (handoffReady) {
741
+ try {
742
+ const validation = await resolveManagedExecutionValidation({
743
+ ai: input.options.ai,
744
+ appRoot: input.appRoot
745
+ });
746
+ supervisor = await createExternalAgentSupervisor({
747
+ bridgeAdapter: "next",
748
+ checks: validation.checks,
749
+ limits: validation.limits,
750
+ root: input.appRoot,
751
+ sessionId: session.id,
752
+ projectLabel: input.appRoot.split(/[\\/]/u).at(-1) ?? "project"
753
+ });
754
+ } catch {
755
+ process.stderr.write(
756
+ "[spotpatch:next] Managed Agent control is unavailable; Inbox remains active.\n"
757
+ );
758
+ }
759
+ }
760
+ externalAgentSupervisor = supervisor;
645
761
  const middleware = createSpotPatchMiddleware({
646
762
  ...manager === void 0 ? {} : { agentManager: manager },
763
+ ...handoffService === void 0 ? {} : { externalHandoffService: handoffService },
764
+ ...supervisor === void 0 ? {} : { externalAgentControl: supervisor },
647
765
  bootstrap: {
648
766
  expectedOrigin: input.publicOrigin,
649
767
  runtimeConfig
@@ -668,6 +786,7 @@ async function createNextSidecar(sidecarOptions) {
668
786
  });
669
787
  registry = sourceRegistry;
670
788
  agentManager = manager;
789
+ spotPatchMiddleware = middleware;
671
790
  handler = (request, response) => {
672
791
  if (requestPath(request.url) === NEXT_INTERNAL_REGISTRATION_PATH) {
673
792
  registration.handler(request, response);
@@ -678,8 +797,26 @@ async function createNextSidecar(sidecarOptions) {
678
797
  });
679
798
  };
680
799
  await selfCheckSidecar(sidecarOrigin, input.publicOrigin, runtimeConfig);
800
+ publicRouteCheck = Object.freeze({
801
+ expectedConfig: runtimeConfig,
802
+ publicOrigin: input.publicOrigin
803
+ });
681
804
  active = true;
682
805
  },
806
+ async checkPublicRoute() {
807
+ if (closed || !active || publicRouteCheck === void 0) {
808
+ return Object.freeze({
809
+ finalUrl: sidecarOrigin,
810
+ kind: "unreachable",
811
+ ok: false
812
+ });
813
+ }
814
+ return await checkBootstrapRoute(
815
+ publicRouteCheck.publicOrigin,
816
+ publicRouteCheck.publicOrigin,
817
+ publicRouteCheck.expectedConfig
818
+ );
819
+ },
683
820
  async close() {
684
821
  if (closed) {
685
822
  return;
@@ -689,6 +826,12 @@ async function createNextSidecar(sidecarOptions) {
689
826
  writeUnavailable(response, 503);
690
827
  };
691
828
  registry?.clear();
829
+ spotPatchMiddleware?.dispose();
830
+ spotPatchMiddleware = void 0;
831
+ await externalAgentSupervisor?.dispose();
832
+ externalAgentSupervisor = void 0;
833
+ await externalHandoffService?.close();
834
+ externalHandoffService = void 0;
692
835
  await agentManager?.close();
693
836
  server.closeIdleConnections();
694
837
  const closing = closeServer(server);
@@ -701,6 +844,8 @@ async function createNextSidecar(sidecarOptions) {
701
844
  // src/cli-owner.ts
702
845
  var CONFIGURATION_STARTUP_TIMEOUT_MS = 6e4;
703
846
  var FORCED_TERMINATION_TIMEOUT_MS = 5e3;
847
+ var PUBLIC_ROUTE_CANARY_RETRY_MS = 250;
848
+ var PUBLIC_ROUTE_CANARY_TIMEOUT_MS = 6e4;
704
849
  var MAX_CONFIGURATION_REQUESTS = 32;
705
850
  var ID_PATTERN2 = /^[A-Za-z0-9_-]{16,128}$/u;
706
851
  function isRecord2(value) {
@@ -759,6 +904,51 @@ function waitForChild(child) {
759
904
  });
760
905
  });
761
906
  }
907
+ function waitForDelay(milliseconds, signal) {
908
+ return new Promise((resolve, reject) => {
909
+ if (signal.aborted) {
910
+ reject(
911
+ signal.reason instanceof Error ? signal.reason : new Error("SpotPatch public-route wait was aborted.")
912
+ );
913
+ return;
914
+ }
915
+ const timer = setTimeout(resolve, milliseconds);
916
+ timer.unref();
917
+ signal.addEventListener(
918
+ "abort",
919
+ () => {
920
+ clearTimeout(timer);
921
+ reject(
922
+ signal.reason instanceof Error ? signal.reason : new Error("SpotPatch public-route wait was aborted.")
923
+ );
924
+ },
925
+ { once: true }
926
+ );
927
+ });
928
+ }
929
+ function publicRouteCanaryError(result) {
930
+ if (result.ok) {
931
+ throw new TypeError("A successful public route canary has no error.");
932
+ }
933
+ const status = result.status === void 0 ? "unavailable" : String(result.status);
934
+ return new Error(
935
+ `SpotPatch public-route canary failed at ${result.finalUrl} (stage=${result.kind}, status=${status}). Check Proxy/Middleware matcher exclusions for ${SPOTPATCH_API_BASE2}.`
936
+ );
937
+ }
938
+ async function waitForPublicRoute(sidecar, signal) {
939
+ const deadline = Date.now() + PUBLIC_ROUTE_CANARY_TIMEOUT_MS;
940
+ while (!signal.aborted) {
941
+ const result = await sidecar.checkPublicRoute();
942
+ if (result.ok) {
943
+ return;
944
+ }
945
+ if (result.kind !== "unreachable" || Date.now() >= deadline) {
946
+ throw publicRouteCanaryError(result);
947
+ }
948
+ await waitForDelay(PUBLIC_ROUTE_CANARY_RETRY_MS, signal);
949
+ }
950
+ throw signal.reason;
951
+ }
762
952
  async function closeSidecar(sidecar) {
763
953
  let timer;
764
954
  try {
@@ -797,6 +987,8 @@ async function runNextDevelopment(arguments_) {
797
987
  let configured = false;
798
988
  let failureCode;
799
989
  let configurationQueue = Promise.resolve();
990
+ const publicRouteCanaryAbort = new AbortController();
991
+ let publicRouteCanary;
800
992
  const handleConfiguration = async (value) => {
801
993
  const correlation = readCorrelation(value, launchNonce);
802
994
  let message;
@@ -858,10 +1050,30 @@ async function runNextDevelopment(arguments_) {
858
1050
  if (!configured) {
859
1051
  configured = true;
860
1052
  clearTimeout(startupTimer);
861
- process.stdout.write(
862
- `[spotpatch:next] ready for Next.js ${project.nextVersion} (${dev.bundler}) at ${dev.publicOrigin}
1053
+ if (message.options.enabled) {
1054
+ publicRouteCanary = waitForPublicRoute(
1055
+ sidecar,
1056
+ publicRouteCanaryAbort.signal
1057
+ ).then(
1058
+ () => {
1059
+ process.stdout.write(
1060
+ `[spotpatch:next] ready for Next.js ${project.nextVersion} (${dev.bundler}) at ${dev.publicOrigin}
863
1061
  `
864
- );
1062
+ );
1063
+ },
1064
+ (error) => {
1065
+ if (publicRouteCanaryAbort.signal.aborted) {
1066
+ return;
1067
+ }
1068
+ failureCode = "PUBLIC_ROUTE_CANARY_FAILED";
1069
+ process.stderr.write(
1070
+ `[spotpatch:next] ${error instanceof Error ? error.message : "SpotPatch public-route canary failed."}
1071
+ `
1072
+ );
1073
+ lifecycle.child?.kill("SIGTERM");
1074
+ }
1075
+ );
1076
+ }
865
1077
  }
866
1078
  return createAck(message, { ok: true });
867
1079
  };
@@ -938,6 +1150,7 @@ async function runNextDevelopment(arguments_) {
938
1150
  process.once("SIGINT", onSigint);
939
1151
  process.once("SIGTERM", onSigterm);
940
1152
  const result = await waitForChild(child);
1153
+ publicRouteCanaryAbort.abort();
941
1154
  clearTimeout(startupTimer);
942
1155
  process.off("SIGINT", onSigint);
943
1156
  process.off("SIGTERM", onSigterm);
@@ -945,6 +1158,7 @@ async function runNextDevelopment(arguments_) {
945
1158
  clearTimeout(forceTimer);
946
1159
  }
947
1160
  await configurationQueue;
1161
+ await publicRouteCanary;
948
1162
  try {
949
1163
  await closeSidecar(sidecar);
950
1164
  } catch {
@@ -974,7 +1188,7 @@ import {
974
1188
  integrationPathExists,
975
1189
  readIntegrationFile
976
1190
  } from "@spotpatch/dev-server";
977
- import { DEFAULT_AGENT_LIMITS } from "@spotpatch/shared";
1191
+ import { DEFAULT_AGENT_LIMITS, SPOTPATCH_API_BASE as SPOTPATCH_API_BASE3 } from "@spotpatch/shared";
978
1192
  import { MagicString } from "magic-string";
979
1193
  import {
980
1194
  parseSync,
@@ -1000,6 +1214,8 @@ var INSTRUMENTATION_EXTENSIONS = Object.freeze([
1000
1214
  ".cts",
1001
1215
  ".cjs"
1002
1216
  ]);
1217
+ var PROXY_FILE_BASE_NAMES = Object.freeze(["proxy", "middleware"]);
1218
+ var SPOTPATCH_MATCHER_EXCLUSION = "__spotpatch(?:/|$)";
1003
1219
  var SIMPLE_SCRIPT_ARGUMENT_PATTERN = /^[A-Za-z0-9._:/=@%+,-]+$/u;
1004
1220
  function isParserErrorSeverity(value) {
1005
1221
  return value === "Error";
@@ -1007,6 +1223,10 @@ function isParserErrorSeverity(value) {
1007
1223
  function isRecord3(value) {
1008
1224
  return typeof value === "object" && value !== null && !Array.isArray(value);
1009
1225
  }
1226
+ function staticStringLiteral(value) {
1227
+ if (!isRecord3(value) || value.type !== "Literal") return void 0;
1228
+ return typeof value.value === "string" ? value.value : void 0;
1229
+ }
1010
1230
  function parseModule(absolutePath, source) {
1011
1231
  const result = parseSync(absolutePath, source, {
1012
1232
  sourceType: "module",
@@ -1039,9 +1259,7 @@ function importInsertionOffset(program) {
1039
1259
  function insertStaticImport(magicString, program, statement) {
1040
1260
  const offset = importInsertionOffset(program);
1041
1261
  if (offset === 0) {
1042
- magicString.prepend(`${statement}
1043
-
1044
- `);
1262
+ magicString.prepend(`${statement}${program.body.length === 0 ? "\n" : "\n\n"}`);
1045
1263
  return;
1046
1264
  }
1047
1265
  magicString.appendRight(offset, `
@@ -1127,21 +1345,102 @@ function staticPropertyName(property) {
1127
1345
  if (property.key.type === "Identifier") return property.key.name;
1128
1346
  return property.key.type === "Literal" && typeof property.key.value === "string" ? property.key.value : void 0;
1129
1347
  }
1130
- function assertSupportedDataFlowOption(factory) {
1131
- const argument = factory.arguments[0];
1132
- if (argument?.type !== "ObjectExpression") return;
1133
- const properties = argument.properties.filter(
1134
- (property2) => property2.type === "Property" && staticPropertyName(property2) === "dataFlow"
1348
+ function unwrapExpression(expression) {
1349
+ let current = expression;
1350
+ while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") {
1351
+ current = current.expression;
1352
+ }
1353
+ return current;
1354
+ }
1355
+ function findObjectProperty(object, name) {
1356
+ const matches = object.properties.filter(
1357
+ (property) => property.type === "Property" && staticPropertyName(property) === name
1135
1358
  );
1136
- if (properties.length > 1) {
1137
- throw new Error("SpotPatch init found duplicate dataFlow options.");
1359
+ if (matches.length > 1) {
1360
+ throw new Error(`SpotPatch init found duplicate ${name} options.`);
1138
1361
  }
1139
- const property = properties[0];
1140
- if (property !== void 0 && !(property.value.type === "Literal" && property.value.value === false)) {
1362
+ return matches[0];
1363
+ }
1364
+ function lineIndentAt(source, offset) {
1365
+ const lineStart = source.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
1366
+ return /^[\t ]*/u.exec(source.slice(lineStart, offset))?.[0] ?? "";
1367
+ }
1368
+ function childIndent(source, object) {
1369
+ const first = object.properties[0];
1370
+ return first === void 0 ? `${lineIndentAt(source, object.start)} ` : lineIndentAt(source, first.start);
1371
+ }
1372
+ function initializedNextOptions(trustedFastModeAvailable) {
1373
+ return `{ externalAgent: true${trustedFastModeAvailable ? ", trustedFastMode: true" : ""} }`;
1374
+ }
1375
+ function enableNextOptions(magicString, source, factory, trustedFastModeAvailable) {
1376
+ const argument = factory.arguments[0];
1377
+ if (factory.arguments.length === 0) {
1378
+ magicString.appendLeft(
1379
+ factory.end - 1,
1380
+ initializedNextOptions(trustedFastModeAvailable)
1381
+ );
1382
+ return;
1383
+ }
1384
+ if (factory.arguments.length !== 1 || argument === void 0 || argument.type === "SpreadElement") {
1385
+ throw new Error("SpotPatch init cannot safely update withSpotPatch options.");
1386
+ }
1387
+ const options = unwrapExpression(argument);
1388
+ if (options.type !== "ObjectExpression") {
1389
+ throw new Error("SpotPatch init requires withSpotPatch options to be an object.");
1390
+ }
1391
+ if (options.properties.some((property) => property.type === "SpreadElement")) {
1392
+ throw new Error(
1393
+ "SpotPatch init cannot prove externalAgent through spread withSpotPatch options."
1394
+ );
1395
+ }
1396
+ const dataFlowProperty = findObjectProperty(options, "dataFlow");
1397
+ const dataFlowValue = dataFlowProperty === void 0 ? void 0 : unwrapExpression(dataFlowProperty.value);
1398
+ if (dataFlowValue !== void 0 && !(dataFlowValue.type === "Literal" && dataFlowValue.value === false)) {
1141
1399
  throw new Error(
1142
1400
  "SpotPatch Next does not support component dataFlow yet; remove dataFlow or set it to false."
1143
1401
  );
1144
1402
  }
1403
+ const missing = [];
1404
+ const externalAgentProperty = findObjectProperty(options, "externalAgent");
1405
+ if (externalAgentProperty === void 0) {
1406
+ missing.push("externalAgent: true");
1407
+ } else {
1408
+ const value = unwrapExpression(externalAgentProperty.value);
1409
+ if (value.type !== "Literal" || typeof value.value !== "boolean") {
1410
+ throw new Error("SpotPatch init requires externalAgent to be a boolean literal.");
1411
+ }
1412
+ if (!value.value) {
1413
+ magicString.overwrite(value.start, value.end, "true");
1414
+ }
1415
+ }
1416
+ const trustedFastModeProperty = findObjectProperty(options, "trustedFastMode");
1417
+ if (trustedFastModeAvailable && trustedFastModeProperty === void 0) {
1418
+ missing.push("trustedFastMode: true");
1419
+ } else if (trustedFastModeProperty !== void 0) {
1420
+ const value = unwrapExpression(trustedFastModeProperty.value);
1421
+ if (value.type !== "Literal" || typeof value.value !== "boolean") {
1422
+ throw new Error(
1423
+ "SpotPatch init requires trustedFastMode to be a boolean literal."
1424
+ );
1425
+ }
1426
+ if (trustedFastModeAvailable && !value.value) {
1427
+ magicString.overwrite(value.start, value.end, "true");
1428
+ }
1429
+ }
1430
+ if (missing.length === 0) {
1431
+ return;
1432
+ }
1433
+ const indent = childIndent(source, options);
1434
+ if (options.properties.length === 0) {
1435
+ magicString.appendLeft(options.end - 1, ` ${missing.join(", ")} `);
1436
+ } else {
1437
+ magicString.appendLeft(
1438
+ options.properties[0]?.start ?? options.end - 1,
1439
+ `${missing.join(`,
1440
+ ${indent}`)},
1441
+ ${indent}`
1442
+ );
1443
+ }
1145
1444
  }
1146
1445
  function transformNextConfig(absolutePath, source, trustedFastModeAvailable) {
1147
1446
  if (absolutePath.endsWith(".cjs") || absolutePath.endsWith(".cts")) {
@@ -1161,16 +1460,8 @@ function transformNextConfig(absolutePath, source, trustedFastModeAvailable) {
1161
1460
  const wrapperName = existingWrapperName ?? chooseWrapperName(program);
1162
1461
  const existingFactory = existingWrapperName === void 0 ? void 0 : wrappedFactoryCall(defaultExport.declaration, existingWrapperName);
1163
1462
  if (existingFactory !== void 0) {
1164
- assertSupportedDataFlowOption(existingFactory);
1165
- if (trustedFastModeAvailable && existingFactory.arguments.length === 0) {
1166
- magicString.overwrite(
1167
- existingFactory.start,
1168
- existingFactory.end,
1169
- `${wrapperName}({ trustedFastMode: true })`
1170
- );
1171
- return magicString.toString();
1172
- }
1173
- return source;
1463
+ enableNextOptions(magicString, source, existingFactory, trustedFastModeAvailable);
1464
+ return magicString.toString();
1174
1465
  }
1175
1466
  if (existingWrapperName === void 0) {
1176
1467
  const specifier = wrapperName === "withSpotPatch" ? "withSpotPatch" : `withSpotPatch as ${wrapperName}`;
@@ -1187,7 +1478,7 @@ function transformNextConfig(absolutePath, source, trustedFastModeAvailable) {
1187
1478
  magicString.overwrite(
1188
1479
  defaultExport.declaration.start,
1189
1480
  defaultExport.declaration.end,
1190
- `${wrapperName}(${trustedFastModeAvailable ? "{ trustedFastMode: true }" : ""})(${expression})`
1481
+ `${wrapperName}(${initializedNextOptions(trustedFastModeAvailable)})(${expression})`
1191
1482
  );
1192
1483
  return magicString.toString();
1193
1484
  }
@@ -1212,6 +1503,230 @@ function transformInstrumentationClient(absolutePath, source) {
1212
1503
  );
1213
1504
  return magicString.toString();
1214
1505
  }
1506
+ function findVariableInitializer(program, name) {
1507
+ for (const statement of program.body) {
1508
+ if (statement.type !== "VariableDeclaration") {
1509
+ continue;
1510
+ }
1511
+ for (const declaration of statement.declarations) {
1512
+ if (declaration.id.type === "Identifier" && declaration.id.name === name && declaration.init !== null) {
1513
+ return declaration.init;
1514
+ }
1515
+ }
1516
+ }
1517
+ return void 0;
1518
+ }
1519
+ function resolveStaticNextConfigObject(program) {
1520
+ const exported = findDefaultExport(program).declaration;
1521
+ if (exported.type === "FunctionDeclaration" || exported.type === "TSDeclareFunction" || exported.type === "ClassDeclaration" || exported.type === "TSInterfaceDeclaration") {
1522
+ return void 0;
1523
+ }
1524
+ let expression = unwrapExpression(exported);
1525
+ if (expression.type === "Identifier") {
1526
+ const initializer = findVariableInitializer(program, expression.name);
1527
+ expression = initializer === void 0 ? expression : unwrapExpression(initializer);
1528
+ }
1529
+ if (expression.type === "CallExpression") {
1530
+ const factory = unwrapExpression(expression.callee);
1531
+ const hostArgument = expression.arguments[0];
1532
+ if (factory.type === "CallExpression" && hostArgument !== void 0 && hostArgument.type !== "SpreadElement") {
1533
+ expression = unwrapExpression(hostArgument);
1534
+ if (expression.type === "Identifier") {
1535
+ const initializer = findVariableInitializer(program, expression.name);
1536
+ expression = initializer === void 0 ? expression : unwrapExpression(initializer);
1537
+ }
1538
+ }
1539
+ }
1540
+ return expression.type === "ObjectExpression" ? expression : void 0;
1541
+ }
1542
+ function proxyFileExtensions(program) {
1543
+ const extensions = new Set(
1544
+ INSTRUMENTATION_EXTENSIONS.map((extension) => extension.slice(1))
1545
+ );
1546
+ const config = resolveStaticNextConfigObject(program);
1547
+ const pageExtensions = config === void 0 ? void 0 : findObjectProperty(config, "pageExtensions");
1548
+ if (pageExtensions === void 0) {
1549
+ return Object.freeze([...extensions]);
1550
+ }
1551
+ const value = unwrapExpression(pageExtensions.value);
1552
+ if (value.type !== "ArrayExpression") {
1553
+ throw new Error(
1554
+ "SPOTPATCH_PROXY_MATCHER_UNSAFE: pageExtensions must be a static string array so SpotPatch can inspect Proxy/Middleware files."
1555
+ );
1556
+ }
1557
+ for (const element of value.elements) {
1558
+ const extension = staticStringLiteral(element);
1559
+ if (typeof extension !== "string" || extension.length === 0 || extension.includes("/") || extension.includes("\\")) {
1560
+ throw new Error(
1561
+ "SPOTPATCH_PROXY_MATCHER_UNSAFE: pageExtensions must contain only static filename extensions."
1562
+ );
1563
+ }
1564
+ extensions.add(extension.replace(/^\.+/u, ""));
1565
+ }
1566
+ return Object.freeze([...extensions]);
1567
+ }
1568
+ function rewriteMatcherSource(value) {
1569
+ if (value.includes(SPOTPATCH_MATCHER_EXCLUSION)) {
1570
+ return value;
1571
+ }
1572
+ if (value === SPOTPATCH_API_BASE3 || value.startsWith(`${SPOTPATCH_API_BASE3}/`)) {
1573
+ throw new Error(
1574
+ `SPOTPATCH_PROXY_MATCHER_UNSAFE: Proxy/Middleware explicitly claims ${SPOTPATCH_API_BASE3}.`
1575
+ );
1576
+ }
1577
+ if (value.startsWith("/((?!")) {
1578
+ return `/((?!${SPOTPATCH_MATCHER_EXCLUSION}|${value.slice(5)}`;
1579
+ }
1580
+ if (value === "/:path*" || value === "/:path(.*)" || value === "/(.*)" || value === "/:slug*") {
1581
+ return `/((?!${SPOTPATCH_MATCHER_EXCLUSION}).*)`;
1582
+ }
1583
+ if (value.startsWith("/:") || value.startsWith("/(")) {
1584
+ throw new Error(
1585
+ `SPOTPATCH_PROXY_MATCHER_UNSAFE: cannot safely exclude ${SPOTPATCH_API_BASE3} from matcher ${JSON.stringify(value)}.`
1586
+ );
1587
+ }
1588
+ return value;
1589
+ }
1590
+ function rewriteMatcherLiteral(magicString, expression) {
1591
+ const value = unwrapExpression(expression);
1592
+ if (value.type !== "Literal" || typeof value.value !== "string") {
1593
+ throw new Error(
1594
+ "SPOTPATCH_PROXY_MATCHER_UNSAFE: matcher entries must use static string sources."
1595
+ );
1596
+ }
1597
+ const rewritten = rewriteMatcherSource(value.value);
1598
+ if (rewritten !== value.value) {
1599
+ magicString.overwrite(value.start, value.end, JSON.stringify(rewritten));
1600
+ }
1601
+ }
1602
+ function rewriteMatcherEntry(magicString, expression) {
1603
+ const value = unwrapExpression(expression);
1604
+ if (value.type !== "ObjectExpression") {
1605
+ rewriteMatcherLiteral(magicString, value);
1606
+ return;
1607
+ }
1608
+ if (value.properties.some((property) => property.type === "SpreadElement")) {
1609
+ throw new Error(
1610
+ "SPOTPATCH_PROXY_MATCHER_UNSAFE: matcher objects cannot contain spreads."
1611
+ );
1612
+ }
1613
+ const source = findObjectProperty(value, "source");
1614
+ if (source === void 0) {
1615
+ throw new Error(
1616
+ "SPOTPATCH_PROXY_MATCHER_UNSAFE: matcher objects require a static source."
1617
+ );
1618
+ }
1619
+ rewriteMatcherLiteral(magicString, source.value);
1620
+ }
1621
+ function transformProxyModule(absolutePath, source) {
1622
+ if (absolutePath.endsWith(".cjs") || absolutePath.endsWith(".cts")) {
1623
+ throw new Error(
1624
+ "SPOTPATCH_PROXY_MATCHER_UNSAFE: CommonJS Proxy/Middleware requires a manual __spotpatch matcher exclusion."
1625
+ );
1626
+ }
1627
+ const { program } = parseModule(absolutePath, source);
1628
+ const configDeclarations = [];
1629
+ let unsupportedConfigExport = false;
1630
+ for (const statement of program.body) {
1631
+ if (statement.type !== "ExportNamedDeclaration") {
1632
+ continue;
1633
+ }
1634
+ const declaration = statement.declaration;
1635
+ if (declaration?.type === "VariableDeclaration") {
1636
+ for (const declarator of declaration.declarations) {
1637
+ if (declarator.id.type !== "Identifier" || declarator.id.name !== "config") {
1638
+ continue;
1639
+ }
1640
+ if (declarator.init === null) {
1641
+ unsupportedConfigExport = true;
1642
+ continue;
1643
+ }
1644
+ const value2 = unwrapExpression(declarator.init);
1645
+ if (value2.type === "ObjectExpression") {
1646
+ configDeclarations.push(value2);
1647
+ } else {
1648
+ unsupportedConfigExport = true;
1649
+ }
1650
+ }
1651
+ }
1652
+ if (statement.specifiers.some(
1653
+ (specifier) => specifier.exported.type === "Identifier" && specifier.exported.name === "config"
1654
+ )) {
1655
+ unsupportedConfigExport = true;
1656
+ }
1657
+ }
1658
+ if (unsupportedConfigExport || configDeclarations.length > 1) {
1659
+ throw new Error(
1660
+ "SPOTPATCH_PROXY_MATCHER_UNSAFE: export const config must be one static object."
1661
+ );
1662
+ }
1663
+ const magicString = new MagicString(source);
1664
+ const config = configDeclarations[0];
1665
+ if (config === void 0) {
1666
+ if (collectIdentifierNames(program).has("config")) {
1667
+ throw new Error(
1668
+ "SPOTPATCH_PROXY_MATCHER_UNSAFE: a local config binding prevents a safe matcher export."
1669
+ );
1670
+ }
1671
+ magicString.append(
1672
+ `${source.endsWith("\n") || source.length === 0 ? "" : "\n"}
1673
+ export const config = { matcher: [${JSON.stringify(`/((?!${SPOTPATCH_MATCHER_EXCLUSION}).*)`)}] };
1674
+ `
1675
+ );
1676
+ return magicString.toString();
1677
+ }
1678
+ const matcher = findObjectProperty(config, "matcher");
1679
+ if (matcher === void 0) {
1680
+ const indent = childIndent(source, config);
1681
+ const property = `matcher: [${JSON.stringify(`/((?!${SPOTPATCH_MATCHER_EXCLUSION}).*)`)}]`;
1682
+ if (config.properties.length === 0) {
1683
+ magicString.appendLeft(config.end - 1, `
1684
+ ${indent}${property},
1685
+ `);
1686
+ } else {
1687
+ magicString.appendLeft(
1688
+ config.properties[0]?.start ?? config.end - 1,
1689
+ `${property},
1690
+ ${indent}`
1691
+ );
1692
+ }
1693
+ return magicString.toString();
1694
+ }
1695
+ const value = unwrapExpression(matcher.value);
1696
+ if (value.type === "ArrayExpression") {
1697
+ for (const element of value.elements) {
1698
+ if (element === null || element.type === "SpreadElement") {
1699
+ throw new Error(
1700
+ "SPOTPATCH_PROXY_MATCHER_UNSAFE: matcher arrays cannot contain holes or spreads."
1701
+ );
1702
+ }
1703
+ rewriteMatcherEntry(magicString, element);
1704
+ }
1705
+ } else {
1706
+ rewriteMatcherEntry(magicString, value);
1707
+ }
1708
+ return magicString.toString();
1709
+ }
1710
+ async function findProxyModule(appRoot, configPath, configSource) {
1711
+ const { program } = parseModule(configPath, configSource);
1712
+ const extensions = proxyFileExtensions(program);
1713
+ const candidates = (await Promise.all(
1714
+ [appRoot, path3.join(appRoot, "src")].flatMap(
1715
+ (directory) => PROXY_FILE_BASE_NAMES.flatMap(
1716
+ (baseName) => extensions.map(async (extension) => {
1717
+ const candidate = path3.join(directory, `${baseName}.${extension}`);
1718
+ return await integrationPathExists(candidate) ? candidate : void 0;
1719
+ })
1720
+ )
1721
+ )
1722
+ )).filter((value) => value !== void 0);
1723
+ if (candidates.length > 1) {
1724
+ throw new Error(
1725
+ `SPOTPATCH_PROXY_MATCHER_UNSAFE: found multiple Proxy/Middleware files (${candidates.map((candidate) => path3.relative(appRoot, candidate)).join(", ")}).`
1726
+ );
1727
+ }
1728
+ return candidates[0];
1729
+ }
1215
1730
  function parsePackageManifest(source) {
1216
1731
  let value;
1217
1732
  try {
@@ -1342,7 +1857,9 @@ async function planNextIntegration(directory = process.cwd()) {
1342
1857
  ]);
1343
1858
  const trustedFastModeAvailable = discoveredCheck !== void 0;
1344
1859
  const instrumentationPath = await resolveInstrumentationPath(appRoot, configPath);
1860
+ const proxyPath = await findProxyModule(appRoot, configPath, configSource);
1345
1861
  const instrumentationSource = await integrationPathExists(instrumentationPath) ? await readIntegrationFile(instrumentationPath) : void 0;
1862
+ const proxySource = proxyPath === void 0 ? void 0 : await readIntegrationFile(proxyPath);
1346
1863
  const changes = [
1347
1864
  createIntegrationFileChange(
1348
1865
  appRoot,
@@ -1361,7 +1878,15 @@ async function planNextIntegration(directory = process.cwd()) {
1361
1878
  packagePath,
1362
1879
  transformPackageJson(packageSource),
1363
1880
  packageSource
1364
- )
1881
+ ),
1882
+ ...proxyPath === void 0 || proxySource === void 0 ? [] : [
1883
+ createIntegrationFileChange(
1884
+ appRoot,
1885
+ proxyPath,
1886
+ transformProxyModule(proxyPath, proxySource),
1887
+ proxySource
1888
+ )
1889
+ ]
1365
1890
  ].filter((change) => change !== void 0);
1366
1891
  return Object.freeze({
1367
1892
  appRoot,
@@ -1399,7 +1924,7 @@ async function checkNextIntegration(directory = process.cwd()) {
1399
1924
  // src/cli.ts
1400
1925
  function writeUsage() {
1401
1926
  process.stderr.write(
1402
- "Usage: spotpatch-next <dev|init|check>\n dev [next dev options] Start the local Next.js development server.\n init Preview and apply safe integration changes.\n check Verify the integration without writing files.\n"
1927
+ "Usage: spotpatch-next <dev|init|check|connect|bridge>\n dev [next dev options] Start the local Next.js development server.\n init Preview and apply safe integration changes.\n check Verify the integration without writing files.\n connect codex Start the zero-setup Codex Agent connector.\n bridge Run external-Agent MCP, CLI, or setup commands.\n"
1403
1928
  );
1404
1929
  }
1405
1930
  function verifyAdapterExports(appRoot) {
@@ -1491,6 +2016,12 @@ async function main(arguments_) {
1491
2016
  if (command === "check") {
1492
2017
  return runCheck(rest);
1493
2018
  }
2019
+ if (command === "bridge") {
2020
+ return runSpotPatchBridgeCli(rest, { adapter: "next" });
2021
+ }
2022
+ if (command === "connect") {
2023
+ return runSpotPatchBridgeCli(arguments_, { adapter: "next" });
2024
+ }
1494
2025
  writeUsage();
1495
2026
  return 1;
1496
2027
  }