@spotpatch/dev-server 0.2.0 → 0.3.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
@@ -280,7 +280,7 @@ function createAgentJobManager(options) {
280
280
  transition(job, "completed", "No source changes were proposed.");
281
281
  return;
282
282
  }
283
- const shouldApplyDirectly = options.ai.execution.applyMode === "auto" && preparedChange.autoApplyEligible || options.ai.execution.applyMode === "trusted-auto" && job.trustedFastModeConsent;
283
+ const shouldApplyDirectly = job.applyMode === "auto" && preparedChange.autoApplyEligible || job.applyMode === "trusted-auto" && job.trustedFastModeConsent;
284
284
  if (shouldApplyDirectly) {
285
285
  try {
286
286
  await applyChange(job, preparedChange);
@@ -356,8 +356,11 @@ function createAgentJobManager(options) {
356
356
  if (closed) {
357
357
  throw new SpotPatchError(ERROR_CODES.AI_DISABLED);
358
358
  }
359
- const trustedFastModeConfigured = options.ai.execution.applyMode === "trusted-auto";
360
- if (trustedFastModeConfigured !== (request.trustedFastModeConsent === true)) {
359
+ const configuredApplyMode = options.ai.execution.applyMode;
360
+ const requestedApplyMode = request.applyMode ?? (request.trustedFastModeConsent === true ? "trusted-auto" : configuredApplyMode);
361
+ const applyModeAllowed = configuredApplyMode === "trusted-auto" ? requestedApplyMode === "review" || requestedApplyMode === "trusted-auto" : requestedApplyMode === configuredApplyMode;
362
+ const trustedConsentMatches = requestedApplyMode === "trusted-auto" === (request.trustedFastModeConsent === true);
363
+ if (!applyModeAllowed || !trustedConsentMatches) {
361
364
  throw new SpotPatchError(ERROR_CODES.INVALID_REQUEST);
362
365
  }
363
366
  if (hasActiveJob()) {
@@ -378,6 +381,7 @@ function createAgentJobManager(options) {
378
381
  const timestamp = dependencies.now();
379
382
  const job = {
380
383
  annotation: request.annotation,
384
+ applyMode: requestedApplyMode,
381
385
  controller: new AbortController(),
382
386
  createdAt: timestamp,
383
387
  credential: selection.credential,
@@ -549,6 +553,181 @@ function resolveEnvironmentAiConfiguration(environment) {
549
553
  });
550
554
  }
551
555
 
556
+ // src/integration/file-plan.ts
557
+ import { randomBytes as randomBytes2 } from "crypto";
558
+ import {
559
+ access,
560
+ lstat,
561
+ mkdir,
562
+ readFile,
563
+ realpath,
564
+ rename,
565
+ stat,
566
+ unlink,
567
+ writeFile
568
+ } from "fs/promises";
569
+ import path from "path";
570
+ function isMissingPathError(error) {
571
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
572
+ }
573
+ function isPathWithin(root, target) {
574
+ const relative = path.relative(root, target);
575
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
576
+ }
577
+ function relativePathWithin(root, target) {
578
+ const relative = path.relative(root, target);
579
+ if (relative.length === 0 || !isPathWithin(root, target)) {
580
+ throw new Error("SpotPatch init refuses to modify a path outside the app root.");
581
+ }
582
+ return relative.split(path.sep).join("/");
583
+ }
584
+ async function integrationPathExists(absolutePath) {
585
+ try {
586
+ await access(absolutePath);
587
+ return true;
588
+ } catch {
589
+ return false;
590
+ }
591
+ }
592
+ async function readIntegrationFile(absolutePath) {
593
+ const metadata = await lstat(absolutePath);
594
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
595
+ throw new Error(
596
+ `SpotPatch refuses to modify the non-regular file ${path.basename(absolutePath)}.`
597
+ );
598
+ }
599
+ return readFile(absolutePath, "utf8");
600
+ }
601
+ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previousContent) {
602
+ if (previousContent === nextContent) {
603
+ return void 0;
604
+ }
605
+ const root = path.resolve(appRoot);
606
+ const target = path.resolve(absolutePath);
607
+ return Object.freeze({
608
+ absolutePath: target,
609
+ nextContent,
610
+ ...previousContent === void 0 ? {} : { previousContent },
611
+ relativePath: relativePathWithin(root, target)
612
+ });
613
+ }
614
+ function temporaryPath(absolutePath, label) {
615
+ return path.join(
616
+ path.dirname(absolutePath),
617
+ `.${path.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes2(8).toString("hex")}`
618
+ );
619
+ }
620
+ async function writeAtomic(absolutePath, content, mode) {
621
+ await mkdir(path.dirname(absolutePath), { recursive: true });
622
+ const stagedPath = temporaryPath(absolutePath, "stage");
623
+ try {
624
+ await writeFile(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
625
+ await rename(stagedPath, absolutePath);
626
+ } catch (error) {
627
+ await unlink(stagedPath).catch(() => void 0);
628
+ throw error;
629
+ }
630
+ }
631
+ async function rollbackChange(change) {
632
+ const currentContent = await readIntegrationFile(change.absolutePath);
633
+ if (currentContent !== change.nextContent) {
634
+ throw new Error(
635
+ `SpotPatch init cannot restore ${change.relativePath} because it changed during initialization.`
636
+ );
637
+ }
638
+ if (change.previousContent === void 0) {
639
+ await unlink(change.absolutePath);
640
+ return;
641
+ }
642
+ const mode = (await stat(change.absolutePath)).mode & 511;
643
+ await writeAtomic(change.absolutePath, change.previousContent, mode);
644
+ }
645
+ async function assertSafeTarget(appRoot, realAppRoot, change) {
646
+ const target = path.resolve(change.absolutePath);
647
+ const relativePath = relativePathWithin(appRoot, target);
648
+ if (target !== change.absolutePath || relativePath !== change.relativePath || path.dirname(target) === target) {
649
+ throw new Error("SpotPatch init received an invalid integration file plan.");
650
+ }
651
+ let targetMetadata;
652
+ try {
653
+ targetMetadata = await lstat(target);
654
+ } catch (error) {
655
+ if (!isMissingPathError(error)) {
656
+ throw error;
657
+ }
658
+ }
659
+ if (targetMetadata?.isSymbolicLink()) {
660
+ throw new Error(
661
+ `SpotPatch refuses to modify the symbolic link ${change.relativePath}.`
662
+ );
663
+ }
664
+ const containmentAnchor = await realpath(
665
+ targetMetadata === void 0 ? path.dirname(target) : target
666
+ );
667
+ if (!isPathWithin(realAppRoot, containmentAnchor)) {
668
+ throw new Error("SpotPatch init refuses to modify a path outside the app root.");
669
+ }
670
+ }
671
+ async function assertCurrentBaseline(change) {
672
+ if (change.previousContent === void 0) {
673
+ try {
674
+ await lstat(change.absolutePath);
675
+ } catch (error) {
676
+ if (isMissingPathError(error)) {
677
+ return;
678
+ }
679
+ throw error;
680
+ }
681
+ throw new Error(
682
+ `SpotPatch init cannot create ${change.relativePath} because it now exists.`
683
+ );
684
+ }
685
+ const currentContent = await readIntegrationFile(change.absolutePath);
686
+ if (currentContent !== change.previousContent) {
687
+ throw new Error(
688
+ `SpotPatch init cannot update ${change.relativePath} because it changed after the preview.`
689
+ );
690
+ }
691
+ }
692
+ async function applyIntegrationPlan(plan) {
693
+ if (plan.changes.length === 0) {
694
+ return;
695
+ }
696
+ const appRoot = path.resolve(plan.appRoot);
697
+ const realAppRoot = await realpath(appRoot);
698
+ const targets = /* @__PURE__ */ new Set();
699
+ for (const change of plan.changes) {
700
+ if (targets.has(change.absolutePath)) {
701
+ throw new Error("SpotPatch init received duplicate integration file changes.");
702
+ }
703
+ targets.add(change.absolutePath);
704
+ await assertSafeTarget(appRoot, realAppRoot, change);
705
+ await assertCurrentBaseline(change);
706
+ }
707
+ const applied = [];
708
+ try {
709
+ for (const change of plan.changes) {
710
+ await assertCurrentBaseline(change);
711
+ const mode = change.previousContent === void 0 ? 384 : (await stat(change.absolutePath)).mode & 511;
712
+ await writeAtomic(change.absolutePath, change.nextContent, mode);
713
+ applied.push(change);
714
+ }
715
+ } catch (error) {
716
+ const rollbackResults = await Promise.allSettled(
717
+ applied.reverse().map(rollbackChange)
718
+ );
719
+ if (rollbackResults.some((result) => result.status === "rejected")) {
720
+ throw new Error(
721
+ "SpotPatch init failed and could not completely restore the previous files.",
722
+ { cause: error }
723
+ );
724
+ }
725
+ throw new Error("SpotPatch init failed; all written files were restored.", {
726
+ cause: error
727
+ });
728
+ }
729
+ }
730
+
552
731
  // src/options.ts
553
732
  import {
554
733
  AGENT_APPLY_MODES,
@@ -880,6 +1059,9 @@ function assertPositiveBudget(budget) {
880
1059
  }
881
1060
  }
882
1061
  function resolveOptions(options = {}, environmentAi) {
1062
+ if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
1063
+ throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
1064
+ }
883
1065
  const budget = Object.freeze({
884
1066
  ...DEFAULT_OPTIONS.budget,
885
1067
  ...options.budget
@@ -919,17 +1101,179 @@ function resolveOptions(options = {}, environmentAi) {
919
1101
  return Object.freeze(resolved);
920
1102
  }
921
1103
 
1104
+ // src/project-validation.ts
1105
+ import { execFile } from "child_process";
1106
+ import { access as access2, lstat as lstat2, readFile as readFile2, realpath as realpath2 } from "fs/promises";
1107
+ import { createRequire } from "module";
1108
+ import path2 from "path";
1109
+ import { promisify } from "util";
1110
+ var execFileAsync = promisify(execFile);
1111
+ var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
1112
+ var TYPESCRIPT_CHECK_LABEL = "TypeScript";
1113
+ function isRecord(value) {
1114
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1115
+ }
1116
+ async function isRegularFile(absolutePath) {
1117
+ try {
1118
+ const metadata = await lstat2(absolutePath);
1119
+ return metadata.isFile() && !metadata.isSymbolicLink();
1120
+ } catch {
1121
+ return false;
1122
+ }
1123
+ }
1124
+ async function readManifest(appRoot) {
1125
+ const manifestPath = path2.join(appRoot, "package.json");
1126
+ if (!await isRegularFile(manifestPath)) {
1127
+ return void 0;
1128
+ }
1129
+ try {
1130
+ const value = JSON.parse(await readFile2(manifestPath, "utf8"));
1131
+ return isRecord(value) ? value : void 0;
1132
+ } catch {
1133
+ return void 0;
1134
+ }
1135
+ }
1136
+ function declaresTypeScript(manifest) {
1137
+ return [
1138
+ manifest.dependencies,
1139
+ manifest.devDependencies,
1140
+ manifest.peerDependencies
1141
+ ].some(
1142
+ (dependencies) => isRecord(dependencies) && typeof dependencies.typescript === "string"
1143
+ );
1144
+ }
1145
+ async function findGitRoot(appRoot) {
1146
+ try {
1147
+ const result = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
1148
+ cwd: appRoot,
1149
+ encoding: "utf8",
1150
+ timeout: 5e3,
1151
+ windowsHide: true
1152
+ });
1153
+ const root = await realpath2(result.stdout.trim());
1154
+ const relative = path2.relative(root, appRoot);
1155
+ if (relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative)) {
1156
+ return root;
1157
+ }
1158
+ } catch {
1159
+ return void 0;
1160
+ }
1161
+ return void 0;
1162
+ }
1163
+ async function resolveTypeScriptCli(appRoot) {
1164
+ const resolveFromApplication = createRequire(path2.join(appRoot, "package.json"));
1165
+ try {
1166
+ const packagePath = resolveFromApplication.resolve("typescript/package.json");
1167
+ const cliPath = path2.join(path2.dirname(packagePath), "bin", "tsc");
1168
+ await access2(cliPath);
1169
+ return await realpath2(cliPath);
1170
+ } catch {
1171
+ return void 0;
1172
+ }
1173
+ }
1174
+ function portableRelativePath(from, to) {
1175
+ return path2.relative(from, to).split(path2.sep).join("/");
1176
+ }
1177
+ async function discoverProjectValidationCheck(options) {
1178
+ const appRoot = await realpath2(options.appRoot);
1179
+ const tsconfigPath = path2.join(appRoot, "tsconfig.json");
1180
+ const [manifest, projectRoot, hasTsconfig] = await Promise.all([
1181
+ readManifest(appRoot),
1182
+ findGitRoot(appRoot),
1183
+ isRegularFile(tsconfigPath)
1184
+ ]);
1185
+ if (manifest === void 0 || projectRoot === void 0 || !hasTsconfig || !declaresTypeScript(manifest)) {
1186
+ return void 0;
1187
+ }
1188
+ const cliPath = await resolveTypeScriptCli(appRoot);
1189
+ if (cliPath === void 0) {
1190
+ return void 0;
1191
+ }
1192
+ const projectPath = portableRelativePath(projectRoot, tsconfigPath);
1193
+ if (projectPath.length === 0 || projectPath.startsWith("../")) {
1194
+ return void 0;
1195
+ }
1196
+ return Object.freeze({
1197
+ id: TYPESCRIPT_CHECK_ID,
1198
+ label: TYPESCRIPT_CHECK_LABEL,
1199
+ command: process.execPath,
1200
+ args: Object.freeze([
1201
+ cliPath,
1202
+ "--noEmit",
1203
+ "--pretty",
1204
+ "false",
1205
+ "--project",
1206
+ projectPath
1207
+ ]),
1208
+ required: true,
1209
+ timeoutMs: options.timeoutMs
1210
+ });
1211
+ }
1212
+
1213
+ // src/project-options.ts
1214
+ function hasRequiredCheck(ai) {
1215
+ return Object.values(ai.execution.checks).some((check) => check.required);
1216
+ }
1217
+ function availableCheckId(checks, preferred) {
1218
+ if (checks[preferred] === void 0) {
1219
+ return preferred;
1220
+ }
1221
+ let suffix = 2;
1222
+ while (checks[`${preferred}-${String(suffix)}`] !== void 0) {
1223
+ suffix += 1;
1224
+ }
1225
+ return `${preferred}-${String(suffix)}`;
1226
+ }
1227
+ async function resolveProjectOptions(input) {
1228
+ const userOptions = input.options ?? {};
1229
+ const resolved = resolveOptions(userOptions, input.environmentAi);
1230
+ if (!userOptions.trustedFastMode || resolved.ai === false) {
1231
+ return resolved;
1232
+ }
1233
+ if (resolved.ai.execution.applyMode === "auto") {
1234
+ throw new RangeError(
1235
+ "SpotPatch trustedFastMode cannot be combined with applyMode auto."
1236
+ );
1237
+ }
1238
+ let checks = resolved.ai.execution.checks;
1239
+ if (!hasRequiredCheck(resolved.ai)) {
1240
+ const discovered = await discoverProjectValidationCheck({
1241
+ appRoot: input.appRoot,
1242
+ timeoutMs: resolved.ai.execution.limits.checkTimeoutMs
1243
+ });
1244
+ if (discovered === void 0) {
1245
+ throw new RangeError(
1246
+ "SpotPatch trustedFastMode requires a configured required check or a local TypeScript project with tsconfig.json."
1247
+ );
1248
+ }
1249
+ const id = availableCheckId(checks, discovered.id);
1250
+ checks = Object.freeze({
1251
+ ...checks,
1252
+ [id]: Object.freeze({ ...discovered, id })
1253
+ });
1254
+ }
1255
+ const ai = Object.freeze({
1256
+ ...resolved.ai,
1257
+ execution: Object.freeze({
1258
+ ...resolved.ai.execution,
1259
+ applyMode: "trusted-auto",
1260
+ checks
1261
+ })
1262
+ });
1263
+ return Object.freeze({ ...resolved, ai });
1264
+ }
1265
+
922
1266
  // src/registry/source-registry.ts
923
- import path from "path";
1267
+ import path3 from "path";
924
1268
 
925
1269
  // src/registry/source-id.ts
926
- import { randomBytes as randomBytes2 } from "crypto";
1270
+ import { randomBytes as randomBytes3 } from "crypto";
927
1271
  var SOURCE_ID_BYTES = 8;
928
- var createRandomSourceId = () => randomBytes2(SOURCE_ID_BYTES).toString("base64url");
1272
+ var createRandomSourceId = () => randomBytes3(SOURCE_ID_BYTES).toString("base64url");
929
1273
 
930
1274
  // src/registry/source-registry.ts
931
1275
  function normalizeAbsolutePath(absolutePath) {
932
- return path.normalize(path.resolve(absolutePath));
1276
+ return path3.normalize(path3.resolve(absolutePath));
933
1277
  }
934
1278
  function createSourceRegistry(options = {}) {
935
1279
  const createId = options.createId ?? createRandomSourceId;
@@ -982,16 +1326,16 @@ import {
982
1326
  } from "@spotpatch/shared";
983
1327
 
984
1328
  // src/server/agent-request.ts
985
- import { realpath as realpath3 } from "fs/promises";
986
- import path4 from "path";
1329
+ import { realpath as realpath5 } from "fs/promises";
1330
+ import path6 from "path";
987
1331
  import {
988
1332
  ERROR_CODES as ERROR_CODES4,
989
1333
  SpotPatchError as SpotPatchError4
990
1334
  } from "@spotpatch/shared";
991
1335
 
992
1336
  // src/server/source-context.ts
993
- import { readFile, realpath as realpath2 } from "fs/promises";
994
- import path3 from "path";
1337
+ import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
1338
+ import path5 from "path";
995
1339
  import {
996
1340
  ERROR_CODES as ERROR_CODES3,
997
1341
  SpotPatchError as SpotPatchError3
@@ -1227,8 +1571,8 @@ function extractCodeContext(options) {
1227
1571
  }
1228
1572
 
1229
1573
  // src/server/source-file.ts
1230
- import { realpath, stat } from "fs/promises";
1231
- import path2 from "path";
1574
+ import { realpath as realpath3, stat as stat2 } from "fs/promises";
1575
+ import path4 from "path";
1232
1576
  import { ERROR_CODES as ERROR_CODES2, SpotPatchError as SpotPatchError2 } from "@spotpatch/shared";
1233
1577
 
1234
1578
  // src/server/constants.ts
@@ -1246,8 +1590,8 @@ async function assertInsideRoot(root, candidate) {
1246
1590
  let realCandidate;
1247
1591
  try {
1248
1592
  [realRoot, realCandidate] = await Promise.all([
1249
- realpath(root),
1250
- realpath(candidate)
1593
+ realpath3(root),
1594
+ realpath3(candidate)
1251
1595
  ]);
1252
1596
  } catch (error) {
1253
1597
  if (isMissingFileError(error)) {
@@ -1257,8 +1601,8 @@ async function assertInsideRoot(root, candidate) {
1257
1601
  }
1258
1602
  throw error;
1259
1603
  }
1260
- const relative = path2.relative(realRoot, realCandidate);
1261
- const outside = relative.startsWith(`..${path2.sep}`) || relative === ".." || path2.isAbsolute(relative);
1604
+ const relative = path4.relative(realRoot, realCandidate);
1605
+ const outside = relative.startsWith(`..${path4.sep}`) || relative === ".." || path4.isAbsolute(relative);
1262
1606
  if (outside) {
1263
1607
  throw new SpotPatchError2(ERROR_CODES2.SOURCE_OUTSIDE_ROOT);
1264
1608
  }
@@ -1270,12 +1614,12 @@ async function resolveSourceFile(options) {
1270
1614
  throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
1271
1615
  }
1272
1616
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
1273
- if (!ALLOWED_EXTENSIONS.has(path2.extname(sourcePath).toLowerCase())) {
1617
+ if (!ALLOWED_EXTENSIONS.has(path4.extname(sourcePath).toLowerCase())) {
1274
1618
  throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
1275
1619
  }
1276
1620
  let sourceStat;
1277
1621
  try {
1278
- sourceStat = await stat(sourcePath);
1622
+ sourceStat = await stat2(sourcePath);
1279
1623
  } catch (error) {
1280
1624
  if (isMissingFileError(error)) {
1281
1625
  throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
@@ -1295,7 +1639,7 @@ async function resolveSourceFile(options) {
1295
1639
 
1296
1640
  // src/server/source-context.ts
1297
1641
  function toDisplayPath(root, sourcePath) {
1298
- return path3.relative(root, sourcePath).split(path3.sep).join("/");
1642
+ return path5.relative(root, sourcePath).split(path5.sep).join("/");
1299
1643
  }
1300
1644
  async function readSourceContext(options) {
1301
1645
  const sourcePath = await resolveSourceFile({
@@ -1305,7 +1649,7 @@ async function readSourceContext(options) {
1305
1649
  });
1306
1650
  let source;
1307
1651
  try {
1308
- source = await readFile(sourcePath, "utf8");
1652
+ source = await readFile3(sourcePath, "utf8");
1309
1653
  } catch (error) {
1310
1654
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1311
1655
  throw new SpotPatchError3(ERROR_CODES3.SOURCE_NOT_FOUND, void 0, {
@@ -1318,11 +1662,11 @@ async function readSourceContext(options) {
1318
1662
  if (options.request.line > lines.length) {
1319
1663
  throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
1320
1664
  }
1321
- const extension = path3.extname(sourcePath).toLowerCase();
1665
+ const extension = path5.extname(sourcePath).toLowerCase();
1322
1666
  return extractCodeContext({
1323
1667
  source,
1324
1668
  sourcePath,
1325
- relativePath: toDisplayPath(await realpath2(options.root), sourcePath),
1669
+ relativePath: toDisplayPath(await realpath4(options.root), sourcePath),
1326
1670
  language: extension === ".tsx" ? "tsx" : "jsx",
1327
1671
  line: options.request.line,
1328
1672
  column: options.request.column,
@@ -1358,7 +1702,7 @@ async function authorizeSourceRef(source, registry, root) {
1358
1702
  registry,
1359
1703
  root
1360
1704
  });
1361
- const relativePath = path4.relative(await realpath3(root), sourcePath).split(path4.sep).join("/");
1705
+ const relativePath = path6.relative(await realpath5(root), sourcePath).split(path6.sep).join("/");
1362
1706
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
1363
1707
  throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
1364
1708
  }
@@ -1463,6 +1807,7 @@ async function authorizeAgentJobRequest(input) {
1463
1807
  });
1464
1808
  return Object.freeze({
1465
1809
  annotation,
1810
+ ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
1466
1811
  providerProfileId: input.request.providerProfileId,
1467
1812
  modelProfileId: input.request.modelProfileId,
1468
1813
  providerDataConsent: true,
@@ -1529,21 +1874,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1529
1874
  "reverted",
1530
1875
  "failed"
1531
1876
  ]);
1532
- function matchAgentRequestPath(path6) {
1533
- if (path6 === SPOTPATCH_ENDPOINTS.agentCapability) {
1877
+ function matchAgentRequestPath(path8) {
1878
+ if (path8 === SPOTPATCH_ENDPOINTS.agentCapability) {
1534
1879
  return Object.freeze({ kind: "capability" });
1535
1880
  }
1536
- if (path6 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
1881
+ if (path8 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
1537
1882
  return Object.freeze({ kind: "workspace-health" });
1538
1883
  }
1539
- if (path6 === SPOTPATCH_ENDPOINTS.agentJobs) {
1884
+ if (path8 === SPOTPATCH_ENDPOINTS.agentJobs) {
1540
1885
  return Object.freeze({ kind: "create-job" });
1541
1886
  }
1542
1887
  const prefix = `${SPOTPATCH_ENDPOINTS.agentJobs}/`;
1543
- if (!path6.startsWith(prefix)) {
1888
+ if (!path8.startsWith(prefix)) {
1544
1889
  return void 0;
1545
1890
  }
1546
- const segments = path6.slice(prefix.length).split("/");
1891
+ const segments = path8.slice(prefix.length).split("/");
1547
1892
  const jobId = segments[0];
1548
1893
  const action = segments[1];
1549
1894
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -2071,14 +2416,14 @@ async function handleOpenEditor(request, options) {
2071
2416
  function createSpotPatchMiddleware(options) {
2072
2417
  const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
2073
2418
  return (request, response, next) => {
2074
- const path6 = requestPath(request);
2075
- const agentRoute = matchAgentRequestPath(path6);
2076
- if (path6 !== SPOTPATCH_ENDPOINTS2.sourceContext && path6 !== SPOTPATCH_ENDPOINTS2.openEditor && agentRoute === void 0 && !path6.startsWith(`${SPOTPATCH_API_BASE}/`)) {
2419
+ const path8 = requestPath(request);
2420
+ const agentRoute = matchAgentRequestPath(path8);
2421
+ if (path8 !== SPOTPATCH_ENDPOINTS2.sourceContext && path8 !== SPOTPATCH_ENDPOINTS2.openEditor && agentRoute === void 0 && !path8.startsWith(`${SPOTPATCH_API_BASE}/`)) {
2077
2422
  next();
2078
2423
  return;
2079
2424
  }
2080
2425
  const handle = async () => {
2081
- if (path6 === SPOTPATCH_ENDPOINTS2.bootstrap && bootstrap !== void 0) {
2426
+ if (path8 === SPOTPATCH_ENDPOINTS2.bootstrap && bootstrap !== void 0) {
2082
2427
  const data = await readRuntimeBootstrap(
2083
2428
  request,
2084
2429
  bootstrap
@@ -2090,7 +2435,7 @@ function createSpotPatchMiddleware(options) {
2090
2435
  allowLan: options.options.allowLan,
2091
2436
  sessionToken: options.session.token
2092
2437
  });
2093
- if (path6 === SPOTPATCH_ENDPOINTS2.sourceContext) {
2438
+ if (path8 === SPOTPATCH_ENDPOINTS2.sourceContext) {
2094
2439
  if (request.method !== "POST") {
2095
2440
  throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
2096
2441
  }
@@ -2098,7 +2443,7 @@ function createSpotPatchMiddleware(options) {
2098
2443
  writeJson(response, 200, { ok: true, data });
2099
2444
  return;
2100
2445
  }
2101
- if (path6 === SPOTPATCH_ENDPOINTS2.openEditor) {
2446
+ if (path8 === SPOTPATCH_ENDPOINTS2.openEditor) {
2102
2447
  if (request.method !== "POST") {
2103
2448
  throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
2104
2449
  }
@@ -2127,8 +2472,8 @@ function createSpotPatchMiddleware(options) {
2127
2472
 
2128
2473
  // src/server/source-registration.ts
2129
2474
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2130
- import { lstat, realpath as realpath4 } from "fs/promises";
2131
- import path5 from "path";
2475
+ import { lstat as lstat3, realpath as realpath6 } from "fs/promises";
2476
+ import path7 from "path";
2132
2477
  import { createSourceFilter } from "@spotpatch/compiler";
2133
2478
  import { z as z2 } from "zod";
2134
2479
  var REGISTRATION_BODY_LIMIT_BYTES = 4096;
@@ -2152,11 +2497,11 @@ function identitiesMatch(actual, expected) {
2152
2497
  return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
2153
2498
  }
2154
2499
  function isWithinRoot(root, candidate) {
2155
- const relative = path5.relative(root, candidate);
2156
- return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== ".." && !path5.isAbsolute(relative);
2500
+ const relative = path7.relative(root, candidate);
2501
+ return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
2157
2502
  }
2158
2503
  function hasForbiddenSegment(root, candidate) {
2159
- return path5.relative(root, candidate).split(path5.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2504
+ return path7.relative(root, candidate).split(path7.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2160
2505
  }
2161
2506
  function writeJson2(response, statusCode, payload) {
2162
2507
  const body = JSON.stringify(payload);
@@ -2178,15 +2523,15 @@ function requestComesFromLoopbackWorker(request) {
2178
2523
  }
2179
2524
  }
2180
2525
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
2181
- if (!path5.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2526
+ if (!path7.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2182
2527
  return void 0;
2183
2528
  }
2184
2529
  try {
2185
- const sourceStat = await lstat(requestedPath);
2530
+ const sourceStat = await lstat3(requestedPath);
2186
2531
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
2187
2532
  return void 0;
2188
2533
  }
2189
- const resolvedPath = await realpath4(requestedPath);
2534
+ const resolvedPath = await realpath6(requestedPath);
2190
2535
  if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
2191
2536
  return void 0;
2192
2537
  }
@@ -2199,7 +2544,7 @@ async function createSourceRegistrationService(input) {
2199
2544
  if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
2200
2545
  throw new TypeError("The source registration identity is invalid.");
2201
2546
  }
2202
- const root = await realpath4(input.root);
2547
+ const root = await realpath6(input.root);
2203
2548
  const sourceFilter = createSourceFilter(root, input.options);
2204
2549
  const handler = (request, response) => {
2205
2550
  const handle = async () => {
@@ -2244,11 +2589,11 @@ async function createSourceRegistrationService(input) {
2244
2589
  }
2245
2590
 
2246
2591
  // src/session/session.ts
2247
- import { randomBytes as randomBytes3 } from "crypto";
2592
+ import { randomBytes as randomBytes4 } from "crypto";
2248
2593
  function createSession() {
2249
2594
  return Object.freeze({
2250
- id: randomBytes3(16).toString("base64url"),
2251
- token: randomBytes3(16).toString("base64url")
2595
+ id: randomBytes4(16).toString("base64url"),
2596
+ token: randomBytes4(16).toString("base64url")
2252
2597
  });
2253
2598
  }
2254
2599
 
@@ -2276,7 +2621,7 @@ var BUDGET_KEYS = Object.freeze([
2276
2621
  "maxComponentDepth"
2277
2622
  ]);
2278
2623
  var REGEXP_FLAGS_PATTERN = /^(?!.*(.).*\1)[dgimsuvy]*$/u;
2279
- function isRecord(value) {
2624
+ function isRecord2(value) {
2280
2625
  return typeof value === "object" && value !== null && !Array.isArray(value);
2281
2626
  }
2282
2627
  function hasExactKeys(value, keys) {
@@ -2361,7 +2706,7 @@ function parseFilterList(value) {
2361
2706
  }
2362
2707
  return Object.freeze(
2363
2708
  value.map((entry) => {
2364
- if (!isRecord(entry)) {
2709
+ if (!isRecord2(entry)) {
2365
2710
  throw new TypeError("The SpotPatch filter transport is invalid.");
2366
2711
  }
2367
2712
  if (entry.kind === "string" && hasExactKeys(entry, ["kind", "value"]) && typeof entry.value === "string" && entry.value.length > 0 && entry.value.length <= 1024 && !entry.value.includes("\0")) {
@@ -2379,7 +2724,7 @@ function parseFilterList(value) {
2379
2724
  );
2380
2725
  }
2381
2726
  function parseBudget(value) {
2382
- if (!isRecord(value) || !hasExactKeys(value, BUDGET_KEYS)) {
2727
+ if (!isRecord2(value) || !hasExactKeys(value, BUDGET_KEYS)) {
2383
2728
  throw new TypeError("The SpotPatch budget transport is invalid.");
2384
2729
  }
2385
2730
  const budget = Object.fromEntries(
@@ -2388,10 +2733,10 @@ function parseBudget(value) {
2388
2733
  return Object.freeze(budget);
2389
2734
  }
2390
2735
  function parseSerializedSpotPatchOptions(value) {
2391
- if (!isRecord(value) || !hasExactKeys(value, OPTION_KEYS)) {
2736
+ if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
2392
2737
  throw new TypeError("The SpotPatch options transport is invalid.");
2393
2738
  }
2394
- if (typeof value.enabled !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord(value.ai)) {
2739
+ if (typeof value.enabled !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord2(value.ai)) {
2395
2740
  throw new TypeError("The SpotPatch options transport is invalid.");
2396
2741
  }
2397
2742
  try {
@@ -2418,19 +2763,25 @@ function parseSerializedSpotPatchOptions(value) {
2418
2763
  export {
2419
2764
  DEFAULT_EXCLUDE,
2420
2765
  DEFAULT_OPTIONS,
2766
+ applyIntegrationPlan,
2421
2767
  createAgentJobManager,
2768
+ createIntegrationFileChange,
2422
2769
  createRuntimeAiConfig,
2423
2770
  createSession,
2424
2771
  createSourceRegistrationService,
2425
2772
  createSourceRegistry,
2426
2773
  createSpotPatchMiddleware,
2774
+ discoverProjectValidationCheck,
2775
+ integrationPathExists,
2427
2776
  isLoopbackHostname,
2428
2777
  parseSerializedSpotPatchOptions,
2778
+ readIntegrationFile,
2429
2779
  readJsonRequestBody,
2430
2780
  readRuntimeBootstrap,
2431
2781
  resolveCredentialEnvironment,
2432
2782
  resolveEnvironmentAiConfiguration,
2433
2783
  resolveOptions,
2784
+ resolveProjectOptions,
2434
2785
  resolveRuntimeBootstrapOptions,
2435
2786
  serializeResolvedSpotPatchOptions
2436
2787
  };