@spotpatch/dev-server 0.2.0 → 0.4.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
@@ -204,7 +204,11 @@ function createAgentJobManager(options) {
204
204
  }
205
205
  };
206
206
  const applyChange = async (job, preparedChange) => {
207
- transition(job, "applying", "Applying validated changes to the project.");
207
+ transition(
208
+ job,
209
+ "applying",
210
+ job.applyMode === "trusted-auto" ? "Applying trusted change directly to the project." : "Applying validated changes to the project."
211
+ );
208
212
  try {
209
213
  await dependencies.applyChange(preparedChange);
210
214
  transition(job, "applied", "Changes were applied to local project files.");
@@ -243,11 +247,15 @@ function createAgentJobManager(options) {
243
247
  );
244
248
  }
245
249
  };
250
+ const execution = Object.freeze({
251
+ ...options.ai.execution,
252
+ applyMode: job.applyMode
253
+ });
246
254
  const preparedChange = await dependencies.executeChange({
247
255
  annotation: job.annotation,
248
256
  callbacks,
249
257
  credential: job.credential,
250
- execution: options.ai.execution,
258
+ execution,
251
259
  jobId: job.id,
252
260
  model: job.model,
253
261
  provider: job.provider,
@@ -280,7 +288,7 @@ function createAgentJobManager(options) {
280
288
  transition(job, "completed", "No source changes were proposed.");
281
289
  return;
282
290
  }
283
- const shouldApplyDirectly = options.ai.execution.applyMode === "auto" && preparedChange.autoApplyEligible || options.ai.execution.applyMode === "trusted-auto" && job.trustedFastModeConsent;
291
+ const shouldApplyDirectly = job.applyMode === "auto" && preparedChange.autoApplyEligible || job.applyMode === "trusted-auto" && job.trustedFastModeConsent;
284
292
  if (shouldApplyDirectly) {
285
293
  try {
286
294
  await applyChange(job, preparedChange);
@@ -356,8 +364,11 @@ function createAgentJobManager(options) {
356
364
  if (closed) {
357
365
  throw new SpotPatchError(ERROR_CODES.AI_DISABLED);
358
366
  }
359
- const trustedFastModeConfigured = options.ai.execution.applyMode === "trusted-auto";
360
- if (trustedFastModeConfigured !== (request.trustedFastModeConsent === true)) {
367
+ const configuredApplyMode = options.ai.execution.applyMode;
368
+ const requestedApplyMode = request.applyMode ?? (request.trustedFastModeConsent === true ? "trusted-auto" : configuredApplyMode);
369
+ const applyModeAllowed = configuredApplyMode === "trusted-auto" ? requestedApplyMode === "review" || requestedApplyMode === "trusted-auto" : requestedApplyMode === configuredApplyMode;
370
+ const trustedConsentMatches = requestedApplyMode === "trusted-auto" === (request.trustedFastModeConsent === true);
371
+ if (!applyModeAllowed || !trustedConsentMatches) {
361
372
  throw new SpotPatchError(ERROR_CODES.INVALID_REQUEST);
362
373
  }
363
374
  if (hasActiveJob()) {
@@ -378,6 +389,7 @@ function createAgentJobManager(options) {
378
389
  const timestamp = dependencies.now();
379
390
  const job = {
380
391
  annotation: request.annotation,
392
+ applyMode: requestedApplyMode,
381
393
  controller: new AbortController(),
382
394
  createdAt: timestamp,
383
395
  credential: selection.credential,
@@ -549,6 +561,181 @@ function resolveEnvironmentAiConfiguration(environment) {
549
561
  });
550
562
  }
551
563
 
564
+ // src/integration/file-plan.ts
565
+ import { randomBytes as randomBytes2 } from "crypto";
566
+ import {
567
+ access,
568
+ lstat,
569
+ mkdir,
570
+ readFile,
571
+ realpath,
572
+ rename,
573
+ stat,
574
+ unlink,
575
+ writeFile
576
+ } from "fs/promises";
577
+ import path from "path";
578
+ function isMissingPathError(error) {
579
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
580
+ }
581
+ function isPathWithin(root, target) {
582
+ const relative = path.relative(root, target);
583
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
584
+ }
585
+ function relativePathWithin(root, target) {
586
+ const relative = path.relative(root, target);
587
+ if (relative.length === 0 || !isPathWithin(root, target)) {
588
+ throw new Error("SpotPatch init refuses to modify a path outside the app root.");
589
+ }
590
+ return relative.split(path.sep).join("/");
591
+ }
592
+ async function integrationPathExists(absolutePath) {
593
+ try {
594
+ await access(absolutePath);
595
+ return true;
596
+ } catch {
597
+ return false;
598
+ }
599
+ }
600
+ async function readIntegrationFile(absolutePath) {
601
+ const metadata = await lstat(absolutePath);
602
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
603
+ throw new Error(
604
+ `SpotPatch refuses to modify the non-regular file ${path.basename(absolutePath)}.`
605
+ );
606
+ }
607
+ return readFile(absolutePath, "utf8");
608
+ }
609
+ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previousContent) {
610
+ if (previousContent === nextContent) {
611
+ return void 0;
612
+ }
613
+ const root = path.resolve(appRoot);
614
+ const target = path.resolve(absolutePath);
615
+ return Object.freeze({
616
+ absolutePath: target,
617
+ nextContent,
618
+ ...previousContent === void 0 ? {} : { previousContent },
619
+ relativePath: relativePathWithin(root, target)
620
+ });
621
+ }
622
+ function temporaryPath(absolutePath, label) {
623
+ return path.join(
624
+ path.dirname(absolutePath),
625
+ `.${path.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes2(8).toString("hex")}`
626
+ );
627
+ }
628
+ async function writeAtomic(absolutePath, content, mode) {
629
+ await mkdir(path.dirname(absolutePath), { recursive: true });
630
+ const stagedPath = temporaryPath(absolutePath, "stage");
631
+ try {
632
+ await writeFile(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
633
+ await rename(stagedPath, absolutePath);
634
+ } catch (error) {
635
+ await unlink(stagedPath).catch(() => void 0);
636
+ throw error;
637
+ }
638
+ }
639
+ async function rollbackChange(change) {
640
+ const currentContent = await readIntegrationFile(change.absolutePath);
641
+ if (currentContent !== change.nextContent) {
642
+ throw new Error(
643
+ `SpotPatch init cannot restore ${change.relativePath} because it changed during initialization.`
644
+ );
645
+ }
646
+ if (change.previousContent === void 0) {
647
+ await unlink(change.absolutePath);
648
+ return;
649
+ }
650
+ const mode = (await stat(change.absolutePath)).mode & 511;
651
+ await writeAtomic(change.absolutePath, change.previousContent, mode);
652
+ }
653
+ async function assertSafeTarget(appRoot, realAppRoot, change) {
654
+ const target = path.resolve(change.absolutePath);
655
+ const relativePath = relativePathWithin(appRoot, target);
656
+ if (target !== change.absolutePath || relativePath !== change.relativePath || path.dirname(target) === target) {
657
+ throw new Error("SpotPatch init received an invalid integration file plan.");
658
+ }
659
+ let targetMetadata;
660
+ try {
661
+ targetMetadata = await lstat(target);
662
+ } catch (error) {
663
+ if (!isMissingPathError(error)) {
664
+ throw error;
665
+ }
666
+ }
667
+ if (targetMetadata?.isSymbolicLink()) {
668
+ throw new Error(
669
+ `SpotPatch refuses to modify the symbolic link ${change.relativePath}.`
670
+ );
671
+ }
672
+ const containmentAnchor = await realpath(
673
+ targetMetadata === void 0 ? path.dirname(target) : target
674
+ );
675
+ if (!isPathWithin(realAppRoot, containmentAnchor)) {
676
+ throw new Error("SpotPatch init refuses to modify a path outside the app root.");
677
+ }
678
+ }
679
+ async function assertCurrentBaseline(change) {
680
+ if (change.previousContent === void 0) {
681
+ try {
682
+ await lstat(change.absolutePath);
683
+ } catch (error) {
684
+ if (isMissingPathError(error)) {
685
+ return;
686
+ }
687
+ throw error;
688
+ }
689
+ throw new Error(
690
+ `SpotPatch init cannot create ${change.relativePath} because it now exists.`
691
+ );
692
+ }
693
+ const currentContent = await readIntegrationFile(change.absolutePath);
694
+ if (currentContent !== change.previousContent) {
695
+ throw new Error(
696
+ `SpotPatch init cannot update ${change.relativePath} because it changed after the preview.`
697
+ );
698
+ }
699
+ }
700
+ async function applyIntegrationPlan(plan) {
701
+ if (plan.changes.length === 0) {
702
+ return;
703
+ }
704
+ const appRoot = path.resolve(plan.appRoot);
705
+ const realAppRoot = await realpath(appRoot);
706
+ const targets = /* @__PURE__ */ new Set();
707
+ for (const change of plan.changes) {
708
+ if (targets.has(change.absolutePath)) {
709
+ throw new Error("SpotPatch init received duplicate integration file changes.");
710
+ }
711
+ targets.add(change.absolutePath);
712
+ await assertSafeTarget(appRoot, realAppRoot, change);
713
+ await assertCurrentBaseline(change);
714
+ }
715
+ const applied = [];
716
+ try {
717
+ for (const change of plan.changes) {
718
+ await assertCurrentBaseline(change);
719
+ const mode = change.previousContent === void 0 ? 384 : (await stat(change.absolutePath)).mode & 511;
720
+ await writeAtomic(change.absolutePath, change.nextContent, mode);
721
+ applied.push(change);
722
+ }
723
+ } catch (error) {
724
+ const rollbackResults = await Promise.allSettled(
725
+ applied.reverse().map(rollbackChange)
726
+ );
727
+ if (rollbackResults.some((result) => result.status === "rejected")) {
728
+ throw new Error(
729
+ "SpotPatch init failed and could not completely restore the previous files.",
730
+ { cause: error }
731
+ );
732
+ }
733
+ throw new Error("SpotPatch init failed; all written files were restored.", {
734
+ cause: error
735
+ });
736
+ }
737
+ }
738
+
552
739
  // src/options.ts
553
740
  import {
554
741
  AGENT_APPLY_MODES,
@@ -880,6 +1067,9 @@ function assertPositiveBudget(budget) {
880
1067
  }
881
1068
  }
882
1069
  function resolveOptions(options = {}, environmentAi) {
1070
+ if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
1071
+ throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
1072
+ }
883
1073
  const budget = Object.freeze({
884
1074
  ...DEFAULT_OPTIONS.budget,
885
1075
  ...options.budget
@@ -919,17 +1109,179 @@ function resolveOptions(options = {}, environmentAi) {
919
1109
  return Object.freeze(resolved);
920
1110
  }
921
1111
 
1112
+ // src/project-validation.ts
1113
+ import { execFile } from "child_process";
1114
+ import { access as access2, lstat as lstat2, readFile as readFile2, realpath as realpath2 } from "fs/promises";
1115
+ import { createRequire } from "module";
1116
+ import path2 from "path";
1117
+ import { promisify } from "util";
1118
+ var execFileAsync = promisify(execFile);
1119
+ var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
1120
+ var TYPESCRIPT_CHECK_LABEL = "TypeScript";
1121
+ function isRecord(value) {
1122
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1123
+ }
1124
+ async function isRegularFile(absolutePath) {
1125
+ try {
1126
+ const metadata = await lstat2(absolutePath);
1127
+ return metadata.isFile() && !metadata.isSymbolicLink();
1128
+ } catch {
1129
+ return false;
1130
+ }
1131
+ }
1132
+ async function readManifest(appRoot) {
1133
+ const manifestPath = path2.join(appRoot, "package.json");
1134
+ if (!await isRegularFile(manifestPath)) {
1135
+ return void 0;
1136
+ }
1137
+ try {
1138
+ const value = JSON.parse(await readFile2(manifestPath, "utf8"));
1139
+ return isRecord(value) ? value : void 0;
1140
+ } catch {
1141
+ return void 0;
1142
+ }
1143
+ }
1144
+ function declaresTypeScript(manifest) {
1145
+ return [
1146
+ manifest.dependencies,
1147
+ manifest.devDependencies,
1148
+ manifest.peerDependencies
1149
+ ].some(
1150
+ (dependencies) => isRecord(dependencies) && typeof dependencies.typescript === "string"
1151
+ );
1152
+ }
1153
+ async function findGitRoot(appRoot) {
1154
+ try {
1155
+ const result = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
1156
+ cwd: appRoot,
1157
+ encoding: "utf8",
1158
+ timeout: 5e3,
1159
+ windowsHide: true
1160
+ });
1161
+ const root = await realpath2(result.stdout.trim());
1162
+ const relative = path2.relative(root, appRoot);
1163
+ if (relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative)) {
1164
+ return root;
1165
+ }
1166
+ } catch {
1167
+ return void 0;
1168
+ }
1169
+ return void 0;
1170
+ }
1171
+ async function resolveTypeScriptCli(appRoot) {
1172
+ const resolveFromApplication = createRequire(path2.join(appRoot, "package.json"));
1173
+ try {
1174
+ const packagePath = resolveFromApplication.resolve("typescript/package.json");
1175
+ const cliPath = path2.join(path2.dirname(packagePath), "bin", "tsc");
1176
+ await access2(cliPath);
1177
+ return await realpath2(cliPath);
1178
+ } catch {
1179
+ return void 0;
1180
+ }
1181
+ }
1182
+ function portableRelativePath(from, to) {
1183
+ return path2.relative(from, to).split(path2.sep).join("/");
1184
+ }
1185
+ async function discoverProjectValidationCheck(options) {
1186
+ const appRoot = await realpath2(options.appRoot);
1187
+ const tsconfigPath = path2.join(appRoot, "tsconfig.json");
1188
+ const [manifest, projectRoot, hasTsconfig] = await Promise.all([
1189
+ readManifest(appRoot),
1190
+ findGitRoot(appRoot),
1191
+ isRegularFile(tsconfigPath)
1192
+ ]);
1193
+ if (manifest === void 0 || projectRoot === void 0 || !hasTsconfig || !declaresTypeScript(manifest)) {
1194
+ return void 0;
1195
+ }
1196
+ const cliPath = await resolveTypeScriptCli(appRoot);
1197
+ if (cliPath === void 0) {
1198
+ return void 0;
1199
+ }
1200
+ const projectPath = portableRelativePath(projectRoot, tsconfigPath);
1201
+ if (projectPath.length === 0 || projectPath.startsWith("../")) {
1202
+ return void 0;
1203
+ }
1204
+ return Object.freeze({
1205
+ id: TYPESCRIPT_CHECK_ID,
1206
+ label: TYPESCRIPT_CHECK_LABEL,
1207
+ command: process.execPath,
1208
+ args: Object.freeze([
1209
+ cliPath,
1210
+ "--noEmit",
1211
+ "--pretty",
1212
+ "false",
1213
+ "--project",
1214
+ projectPath
1215
+ ]),
1216
+ required: true,
1217
+ timeoutMs: options.timeoutMs
1218
+ });
1219
+ }
1220
+
1221
+ // src/project-options.ts
1222
+ function hasRequiredCheck(ai) {
1223
+ return Object.values(ai.execution.checks).some((check) => check.required);
1224
+ }
1225
+ function availableCheckId(checks, preferred) {
1226
+ if (checks[preferred] === void 0) {
1227
+ return preferred;
1228
+ }
1229
+ let suffix = 2;
1230
+ while (checks[`${preferred}-${String(suffix)}`] !== void 0) {
1231
+ suffix += 1;
1232
+ }
1233
+ return `${preferred}-${String(suffix)}`;
1234
+ }
1235
+ async function resolveProjectOptions(input) {
1236
+ const userOptions = input.options ?? {};
1237
+ const resolved = resolveOptions(userOptions, input.environmentAi);
1238
+ if (!userOptions.trustedFastMode || resolved.ai === false) {
1239
+ return resolved;
1240
+ }
1241
+ if (resolved.ai.execution.applyMode === "auto") {
1242
+ throw new RangeError(
1243
+ "SpotPatch trustedFastMode cannot be combined with applyMode auto."
1244
+ );
1245
+ }
1246
+ let checks = resolved.ai.execution.checks;
1247
+ if (!hasRequiredCheck(resolved.ai)) {
1248
+ const discovered = await discoverProjectValidationCheck({
1249
+ appRoot: input.appRoot,
1250
+ timeoutMs: resolved.ai.execution.limits.checkTimeoutMs
1251
+ });
1252
+ if (discovered === void 0) {
1253
+ throw new RangeError(
1254
+ "SpotPatch trustedFastMode requires a configured required check or a local TypeScript project with tsconfig.json."
1255
+ );
1256
+ }
1257
+ const id = availableCheckId(checks, discovered.id);
1258
+ checks = Object.freeze({
1259
+ ...checks,
1260
+ [id]: Object.freeze({ ...discovered, id })
1261
+ });
1262
+ }
1263
+ const ai = Object.freeze({
1264
+ ...resolved.ai,
1265
+ execution: Object.freeze({
1266
+ ...resolved.ai.execution,
1267
+ applyMode: "trusted-auto",
1268
+ checks
1269
+ })
1270
+ });
1271
+ return Object.freeze({ ...resolved, ai });
1272
+ }
1273
+
922
1274
  // src/registry/source-registry.ts
923
- import path from "path";
1275
+ import path3 from "path";
924
1276
 
925
1277
  // src/registry/source-id.ts
926
- import { randomBytes as randomBytes2 } from "crypto";
1278
+ import { randomBytes as randomBytes3 } from "crypto";
927
1279
  var SOURCE_ID_BYTES = 8;
928
- var createRandomSourceId = () => randomBytes2(SOURCE_ID_BYTES).toString("base64url");
1280
+ var createRandomSourceId = () => randomBytes3(SOURCE_ID_BYTES).toString("base64url");
929
1281
 
930
1282
  // src/registry/source-registry.ts
931
1283
  function normalizeAbsolutePath(absolutePath) {
932
- return path.normalize(path.resolve(absolutePath));
1284
+ return path3.normalize(path3.resolve(absolutePath));
933
1285
  }
934
1286
  function createSourceRegistry(options = {}) {
935
1287
  const createId = options.createId ?? createRandomSourceId;
@@ -982,16 +1334,16 @@ import {
982
1334
  } from "@spotpatch/shared";
983
1335
 
984
1336
  // src/server/agent-request.ts
985
- import { realpath as realpath3 } from "fs/promises";
986
- import path4 from "path";
1337
+ import { realpath as realpath5 } from "fs/promises";
1338
+ import path6 from "path";
987
1339
  import {
988
1340
  ERROR_CODES as ERROR_CODES4,
989
1341
  SpotPatchError as SpotPatchError4
990
1342
  } from "@spotpatch/shared";
991
1343
 
992
1344
  // src/server/source-context.ts
993
- import { readFile, realpath as realpath2 } from "fs/promises";
994
- import path3 from "path";
1345
+ import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
1346
+ import path5 from "path";
995
1347
  import {
996
1348
  ERROR_CODES as ERROR_CODES3,
997
1349
  SpotPatchError as SpotPatchError3
@@ -1227,8 +1579,8 @@ function extractCodeContext(options) {
1227
1579
  }
1228
1580
 
1229
1581
  // src/server/source-file.ts
1230
- import { realpath, stat } from "fs/promises";
1231
- import path2 from "path";
1582
+ import { realpath as realpath3, stat as stat2 } from "fs/promises";
1583
+ import path4 from "path";
1232
1584
  import { ERROR_CODES as ERROR_CODES2, SpotPatchError as SpotPatchError2 } from "@spotpatch/shared";
1233
1585
 
1234
1586
  // src/server/constants.ts
@@ -1246,8 +1598,8 @@ async function assertInsideRoot(root, candidate) {
1246
1598
  let realCandidate;
1247
1599
  try {
1248
1600
  [realRoot, realCandidate] = await Promise.all([
1249
- realpath(root),
1250
- realpath(candidate)
1601
+ realpath3(root),
1602
+ realpath3(candidate)
1251
1603
  ]);
1252
1604
  } catch (error) {
1253
1605
  if (isMissingFileError(error)) {
@@ -1257,8 +1609,8 @@ async function assertInsideRoot(root, candidate) {
1257
1609
  }
1258
1610
  throw error;
1259
1611
  }
1260
- const relative = path2.relative(realRoot, realCandidate);
1261
- const outside = relative.startsWith(`..${path2.sep}`) || relative === ".." || path2.isAbsolute(relative);
1612
+ const relative = path4.relative(realRoot, realCandidate);
1613
+ const outside = relative.startsWith(`..${path4.sep}`) || relative === ".." || path4.isAbsolute(relative);
1262
1614
  if (outside) {
1263
1615
  throw new SpotPatchError2(ERROR_CODES2.SOURCE_OUTSIDE_ROOT);
1264
1616
  }
@@ -1270,12 +1622,12 @@ async function resolveSourceFile(options) {
1270
1622
  throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
1271
1623
  }
1272
1624
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
1273
- if (!ALLOWED_EXTENSIONS.has(path2.extname(sourcePath).toLowerCase())) {
1625
+ if (!ALLOWED_EXTENSIONS.has(path4.extname(sourcePath).toLowerCase())) {
1274
1626
  throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
1275
1627
  }
1276
1628
  let sourceStat;
1277
1629
  try {
1278
- sourceStat = await stat(sourcePath);
1630
+ sourceStat = await stat2(sourcePath);
1279
1631
  } catch (error) {
1280
1632
  if (isMissingFileError(error)) {
1281
1633
  throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
@@ -1295,7 +1647,7 @@ async function resolveSourceFile(options) {
1295
1647
 
1296
1648
  // src/server/source-context.ts
1297
1649
  function toDisplayPath(root, sourcePath) {
1298
- return path3.relative(root, sourcePath).split(path3.sep).join("/");
1650
+ return path5.relative(root, sourcePath).split(path5.sep).join("/");
1299
1651
  }
1300
1652
  async function readSourceContext(options) {
1301
1653
  const sourcePath = await resolveSourceFile({
@@ -1305,7 +1657,7 @@ async function readSourceContext(options) {
1305
1657
  });
1306
1658
  let source;
1307
1659
  try {
1308
- source = await readFile(sourcePath, "utf8");
1660
+ source = await readFile3(sourcePath, "utf8");
1309
1661
  } catch (error) {
1310
1662
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1311
1663
  throw new SpotPatchError3(ERROR_CODES3.SOURCE_NOT_FOUND, void 0, {
@@ -1318,11 +1670,11 @@ async function readSourceContext(options) {
1318
1670
  if (options.request.line > lines.length) {
1319
1671
  throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
1320
1672
  }
1321
- const extension = path3.extname(sourcePath).toLowerCase();
1673
+ const extension = path5.extname(sourcePath).toLowerCase();
1322
1674
  return extractCodeContext({
1323
1675
  source,
1324
1676
  sourcePath,
1325
- relativePath: toDisplayPath(await realpath2(options.root), sourcePath),
1677
+ relativePath: toDisplayPath(await realpath4(options.root), sourcePath),
1326
1678
  language: extension === ".tsx" ? "tsx" : "jsx",
1327
1679
  line: options.request.line,
1328
1680
  column: options.request.column,
@@ -1358,7 +1710,7 @@ async function authorizeSourceRef(source, registry, root) {
1358
1710
  registry,
1359
1711
  root
1360
1712
  });
1361
- const relativePath = path4.relative(await realpath3(root), sourcePath).split(path4.sep).join("/");
1713
+ const relativePath = path6.relative(await realpath5(root), sourcePath).split(path6.sep).join("/");
1362
1714
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
1363
1715
  throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
1364
1716
  }
@@ -1463,6 +1815,7 @@ async function authorizeAgentJobRequest(input) {
1463
1815
  });
1464
1816
  return Object.freeze({
1465
1817
  annotation,
1818
+ ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
1466
1819
  providerProfileId: input.request.providerProfileId,
1467
1820
  modelProfileId: input.request.modelProfileId,
1468
1821
  providerDataConsent: true,
@@ -1529,21 +1882,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1529
1882
  "reverted",
1530
1883
  "failed"
1531
1884
  ]);
1532
- function matchAgentRequestPath(path6) {
1533
- if (path6 === SPOTPATCH_ENDPOINTS.agentCapability) {
1885
+ function matchAgentRequestPath(path8) {
1886
+ if (path8 === SPOTPATCH_ENDPOINTS.agentCapability) {
1534
1887
  return Object.freeze({ kind: "capability" });
1535
1888
  }
1536
- if (path6 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
1889
+ if (path8 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
1537
1890
  return Object.freeze({ kind: "workspace-health" });
1538
1891
  }
1539
- if (path6 === SPOTPATCH_ENDPOINTS.agentJobs) {
1892
+ if (path8 === SPOTPATCH_ENDPOINTS.agentJobs) {
1540
1893
  return Object.freeze({ kind: "create-job" });
1541
1894
  }
1542
1895
  const prefix = `${SPOTPATCH_ENDPOINTS.agentJobs}/`;
1543
- if (!path6.startsWith(prefix)) {
1896
+ if (!path8.startsWith(prefix)) {
1544
1897
  return void 0;
1545
1898
  }
1546
- const segments = path6.slice(prefix.length).split("/");
1899
+ const segments = path8.slice(prefix.length).split("/");
1547
1900
  const jobId = segments[0];
1548
1901
  const action = segments[1];
1549
1902
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -2071,14 +2424,14 @@ async function handleOpenEditor(request, options) {
2071
2424
  function createSpotPatchMiddleware(options) {
2072
2425
  const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
2073
2426
  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}/`)) {
2427
+ const path8 = requestPath(request);
2428
+ const agentRoute = matchAgentRequestPath(path8);
2429
+ if (path8 !== SPOTPATCH_ENDPOINTS2.sourceContext && path8 !== SPOTPATCH_ENDPOINTS2.openEditor && agentRoute === void 0 && !path8.startsWith(`${SPOTPATCH_API_BASE}/`)) {
2077
2430
  next();
2078
2431
  return;
2079
2432
  }
2080
2433
  const handle = async () => {
2081
- if (path6 === SPOTPATCH_ENDPOINTS2.bootstrap && bootstrap !== void 0) {
2434
+ if (path8 === SPOTPATCH_ENDPOINTS2.bootstrap && bootstrap !== void 0) {
2082
2435
  const data = await readRuntimeBootstrap(
2083
2436
  request,
2084
2437
  bootstrap
@@ -2090,7 +2443,7 @@ function createSpotPatchMiddleware(options) {
2090
2443
  allowLan: options.options.allowLan,
2091
2444
  sessionToken: options.session.token
2092
2445
  });
2093
- if (path6 === SPOTPATCH_ENDPOINTS2.sourceContext) {
2446
+ if (path8 === SPOTPATCH_ENDPOINTS2.sourceContext) {
2094
2447
  if (request.method !== "POST") {
2095
2448
  throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
2096
2449
  }
@@ -2098,7 +2451,7 @@ function createSpotPatchMiddleware(options) {
2098
2451
  writeJson(response, 200, { ok: true, data });
2099
2452
  return;
2100
2453
  }
2101
- if (path6 === SPOTPATCH_ENDPOINTS2.openEditor) {
2454
+ if (path8 === SPOTPATCH_ENDPOINTS2.openEditor) {
2102
2455
  if (request.method !== "POST") {
2103
2456
  throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
2104
2457
  }
@@ -2127,8 +2480,8 @@ function createSpotPatchMiddleware(options) {
2127
2480
 
2128
2481
  // src/server/source-registration.ts
2129
2482
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2130
- import { lstat, realpath as realpath4 } from "fs/promises";
2131
- import path5 from "path";
2483
+ import { lstat as lstat3, realpath as realpath6 } from "fs/promises";
2484
+ import path7 from "path";
2132
2485
  import { createSourceFilter } from "@spotpatch/compiler";
2133
2486
  import { z as z2 } from "zod";
2134
2487
  var REGISTRATION_BODY_LIMIT_BYTES = 4096;
@@ -2152,11 +2505,11 @@ function identitiesMatch(actual, expected) {
2152
2505
  return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
2153
2506
  }
2154
2507
  function isWithinRoot(root, candidate) {
2155
- const relative = path5.relative(root, candidate);
2156
- return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== ".." && !path5.isAbsolute(relative);
2508
+ const relative = path7.relative(root, candidate);
2509
+ return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
2157
2510
  }
2158
2511
  function hasForbiddenSegment(root, candidate) {
2159
- return path5.relative(root, candidate).split(path5.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2512
+ return path7.relative(root, candidate).split(path7.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2160
2513
  }
2161
2514
  function writeJson2(response, statusCode, payload) {
2162
2515
  const body = JSON.stringify(payload);
@@ -2178,15 +2531,15 @@ function requestComesFromLoopbackWorker(request) {
2178
2531
  }
2179
2532
  }
2180
2533
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
2181
- if (!path5.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2534
+ if (!path7.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2182
2535
  return void 0;
2183
2536
  }
2184
2537
  try {
2185
- const sourceStat = await lstat(requestedPath);
2538
+ const sourceStat = await lstat3(requestedPath);
2186
2539
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
2187
2540
  return void 0;
2188
2541
  }
2189
- const resolvedPath = await realpath4(requestedPath);
2542
+ const resolvedPath = await realpath6(requestedPath);
2190
2543
  if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
2191
2544
  return void 0;
2192
2545
  }
@@ -2199,7 +2552,7 @@ async function createSourceRegistrationService(input) {
2199
2552
  if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
2200
2553
  throw new TypeError("The source registration identity is invalid.");
2201
2554
  }
2202
- const root = await realpath4(input.root);
2555
+ const root = await realpath6(input.root);
2203
2556
  const sourceFilter = createSourceFilter(root, input.options);
2204
2557
  const handler = (request, response) => {
2205
2558
  const handle = async () => {
@@ -2244,11 +2597,11 @@ async function createSourceRegistrationService(input) {
2244
2597
  }
2245
2598
 
2246
2599
  // src/session/session.ts
2247
- import { randomBytes as randomBytes3 } from "crypto";
2600
+ import { randomBytes as randomBytes4 } from "crypto";
2248
2601
  function createSession() {
2249
2602
  return Object.freeze({
2250
- id: randomBytes3(16).toString("base64url"),
2251
- token: randomBytes3(16).toString("base64url")
2603
+ id: randomBytes4(16).toString("base64url"),
2604
+ token: randomBytes4(16).toString("base64url")
2252
2605
  });
2253
2606
  }
2254
2607
 
@@ -2276,7 +2629,7 @@ var BUDGET_KEYS = Object.freeze([
2276
2629
  "maxComponentDepth"
2277
2630
  ]);
2278
2631
  var REGEXP_FLAGS_PATTERN = /^(?!.*(.).*\1)[dgimsuvy]*$/u;
2279
- function isRecord(value) {
2632
+ function isRecord2(value) {
2280
2633
  return typeof value === "object" && value !== null && !Array.isArray(value);
2281
2634
  }
2282
2635
  function hasExactKeys(value, keys) {
@@ -2361,7 +2714,7 @@ function parseFilterList(value) {
2361
2714
  }
2362
2715
  return Object.freeze(
2363
2716
  value.map((entry) => {
2364
- if (!isRecord(entry)) {
2717
+ if (!isRecord2(entry)) {
2365
2718
  throw new TypeError("The SpotPatch filter transport is invalid.");
2366
2719
  }
2367
2720
  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 +2732,7 @@ function parseFilterList(value) {
2379
2732
  );
2380
2733
  }
2381
2734
  function parseBudget(value) {
2382
- if (!isRecord(value) || !hasExactKeys(value, BUDGET_KEYS)) {
2735
+ if (!isRecord2(value) || !hasExactKeys(value, BUDGET_KEYS)) {
2383
2736
  throw new TypeError("The SpotPatch budget transport is invalid.");
2384
2737
  }
2385
2738
  const budget = Object.fromEntries(
@@ -2388,10 +2741,10 @@ function parseBudget(value) {
2388
2741
  return Object.freeze(budget);
2389
2742
  }
2390
2743
  function parseSerializedSpotPatchOptions(value) {
2391
- if (!isRecord(value) || !hasExactKeys(value, OPTION_KEYS)) {
2744
+ if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
2392
2745
  throw new TypeError("The SpotPatch options transport is invalid.");
2393
2746
  }
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)) {
2747
+ 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
2748
  throw new TypeError("The SpotPatch options transport is invalid.");
2396
2749
  }
2397
2750
  try {
@@ -2418,19 +2771,25 @@ function parseSerializedSpotPatchOptions(value) {
2418
2771
  export {
2419
2772
  DEFAULT_EXCLUDE,
2420
2773
  DEFAULT_OPTIONS,
2774
+ applyIntegrationPlan,
2421
2775
  createAgentJobManager,
2776
+ createIntegrationFileChange,
2422
2777
  createRuntimeAiConfig,
2423
2778
  createSession,
2424
2779
  createSourceRegistrationService,
2425
2780
  createSourceRegistry,
2426
2781
  createSpotPatchMiddleware,
2782
+ discoverProjectValidationCheck,
2783
+ integrationPathExists,
2427
2784
  isLoopbackHostname,
2428
2785
  parseSerializedSpotPatchOptions,
2786
+ readIntegrationFile,
2429
2787
  readJsonRequestBody,
2430
2788
  readRuntimeBootstrap,
2431
2789
  resolveCredentialEnvironment,
2432
2790
  resolveEnvironmentAiConfiguration,
2433
2791
  resolveOptions,
2792
+ resolveProjectOptions,
2434
2793
  resolveRuntimeBootstrapOptions,
2435
2794
  serializeResolvedSpotPatchOptions
2436
2795
  };