@spotpatch/dev-server 0.1.2 → 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.cjs CHANGED
@@ -32,19 +32,25 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DEFAULT_EXCLUDE: () => DEFAULT_EXCLUDE,
34
34
  DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
35
+ applyIntegrationPlan: () => applyIntegrationPlan,
35
36
  createAgentJobManager: () => createAgentJobManager,
37
+ createIntegrationFileChange: () => createIntegrationFileChange,
36
38
  createRuntimeAiConfig: () => createRuntimeAiConfig,
37
39
  createSession: () => createSession,
38
40
  createSourceRegistrationService: () => createSourceRegistrationService,
39
41
  createSourceRegistry: () => createSourceRegistry,
40
42
  createSpotPatchMiddleware: () => createSpotPatchMiddleware,
43
+ discoverProjectValidationCheck: () => discoverProjectValidationCheck,
44
+ integrationPathExists: () => integrationPathExists,
41
45
  isLoopbackHostname: () => isLoopbackHostname,
42
46
  parseSerializedSpotPatchOptions: () => parseSerializedSpotPatchOptions,
47
+ readIntegrationFile: () => readIntegrationFile,
43
48
  readJsonRequestBody: () => readJsonRequestBody,
44
49
  readRuntimeBootstrap: () => readRuntimeBootstrap,
45
50
  resolveCredentialEnvironment: () => resolveCredentialEnvironment,
46
51
  resolveEnvironmentAiConfiguration: () => resolveEnvironmentAiConfiguration,
47
52
  resolveOptions: () => resolveOptions,
53
+ resolveProjectOptions: () => resolveProjectOptions,
48
54
  resolveRuntimeBootstrapOptions: () => resolveRuntimeBootstrapOptions,
49
55
  serializeResolvedSpotPatchOptions: () => serializeResolvedSpotPatchOptions
50
56
  });
@@ -322,7 +328,8 @@ function createAgentJobManager(options) {
322
328
  transition(job, "completed", "No source changes were proposed.");
323
329
  return;
324
330
  }
325
- if (options.ai.execution.applyMode === "auto" && preparedChange.autoApplyEligible) {
331
+ const shouldApplyDirectly = job.applyMode === "auto" && preparedChange.autoApplyEligible || job.applyMode === "trusted-auto" && job.trustedFastModeConsent;
332
+ if (shouldApplyDirectly) {
326
333
  try {
327
334
  await applyChange(job, preparedChange);
328
335
  } catch {
@@ -397,6 +404,13 @@ function createAgentJobManager(options) {
397
404
  if (closed) {
398
405
  throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AI_DISABLED);
399
406
  }
407
+ const configuredApplyMode = options.ai.execution.applyMode;
408
+ const requestedApplyMode = request.applyMode ?? (request.trustedFastModeConsent === true ? "trusted-auto" : configuredApplyMode);
409
+ const applyModeAllowed = configuredApplyMode === "trusted-auto" ? requestedApplyMode === "review" || requestedApplyMode === "trusted-auto" : requestedApplyMode === configuredApplyMode;
410
+ const trustedConsentMatches = requestedApplyMode === "trusted-auto" === (request.trustedFastModeConsent === true);
411
+ if (!applyModeAllowed || !trustedConsentMatches) {
412
+ throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.INVALID_REQUEST);
413
+ }
400
414
  if (hasActiveJob()) {
401
415
  throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AGENT_BUSY);
402
416
  }
@@ -415,6 +429,7 @@ function createAgentJobManager(options) {
415
429
  const timestamp = dependencies.now();
416
430
  const job = {
417
431
  annotation: request.annotation,
432
+ applyMode: requestedApplyMode,
418
433
  controller: new AbortController(),
419
434
  createdAt: timestamp,
420
435
  credential: selection.credential,
@@ -430,6 +445,7 @@ function createAgentJobManager(options) {
430
445
  runPromise: void 0,
431
446
  sequence: 0,
432
447
  status: "queued",
448
+ trustedFastModeConsent: request.trustedFastModeConsent === true,
433
449
  updatedAt: timestamp,
434
450
  workingTreeMode: request.workingTreeMode
435
451
  };
@@ -585,6 +601,171 @@ function resolveEnvironmentAiConfiguration(environment) {
585
601
  });
586
602
  }
587
603
 
604
+ // src/integration/file-plan.ts
605
+ var import_node_crypto2 = require("crypto");
606
+ var import_promises = require("fs/promises");
607
+ var import_node_path = __toESM(require("path"), 1);
608
+ function isMissingPathError(error) {
609
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
610
+ }
611
+ function isPathWithin(root, target) {
612
+ const relative = import_node_path.default.relative(root, target);
613
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
614
+ }
615
+ function relativePathWithin(root, target) {
616
+ const relative = import_node_path.default.relative(root, target);
617
+ if (relative.length === 0 || !isPathWithin(root, target)) {
618
+ throw new Error("SpotPatch init refuses to modify a path outside the app root.");
619
+ }
620
+ return relative.split(import_node_path.default.sep).join("/");
621
+ }
622
+ async function integrationPathExists(absolutePath) {
623
+ try {
624
+ await (0, import_promises.access)(absolutePath);
625
+ return true;
626
+ } catch {
627
+ return false;
628
+ }
629
+ }
630
+ async function readIntegrationFile(absolutePath) {
631
+ const metadata = await (0, import_promises.lstat)(absolutePath);
632
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
633
+ throw new Error(
634
+ `SpotPatch refuses to modify the non-regular file ${import_node_path.default.basename(absolutePath)}.`
635
+ );
636
+ }
637
+ return (0, import_promises.readFile)(absolutePath, "utf8");
638
+ }
639
+ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previousContent) {
640
+ if (previousContent === nextContent) {
641
+ return void 0;
642
+ }
643
+ const root = import_node_path.default.resolve(appRoot);
644
+ const target = import_node_path.default.resolve(absolutePath);
645
+ return Object.freeze({
646
+ absolutePath: target,
647
+ nextContent,
648
+ ...previousContent === void 0 ? {} : { previousContent },
649
+ relativePath: relativePathWithin(root, target)
650
+ });
651
+ }
652
+ function temporaryPath(absolutePath, label) {
653
+ return import_node_path.default.join(
654
+ import_node_path.default.dirname(absolutePath),
655
+ `.${import_node_path.default.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${(0, import_node_crypto2.randomBytes)(8).toString("hex")}`
656
+ );
657
+ }
658
+ async function writeAtomic(absolutePath, content, mode) {
659
+ await (0, import_promises.mkdir)(import_node_path.default.dirname(absolutePath), { recursive: true });
660
+ const stagedPath = temporaryPath(absolutePath, "stage");
661
+ try {
662
+ await (0, import_promises.writeFile)(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
663
+ await (0, import_promises.rename)(stagedPath, absolutePath);
664
+ } catch (error) {
665
+ await (0, import_promises.unlink)(stagedPath).catch(() => void 0);
666
+ throw error;
667
+ }
668
+ }
669
+ async function rollbackChange(change) {
670
+ const currentContent = await readIntegrationFile(change.absolutePath);
671
+ if (currentContent !== change.nextContent) {
672
+ throw new Error(
673
+ `SpotPatch init cannot restore ${change.relativePath} because it changed during initialization.`
674
+ );
675
+ }
676
+ if (change.previousContent === void 0) {
677
+ await (0, import_promises.unlink)(change.absolutePath);
678
+ return;
679
+ }
680
+ const mode = (await (0, import_promises.stat)(change.absolutePath)).mode & 511;
681
+ await writeAtomic(change.absolutePath, change.previousContent, mode);
682
+ }
683
+ async function assertSafeTarget(appRoot, realAppRoot, change) {
684
+ const target = import_node_path.default.resolve(change.absolutePath);
685
+ const relativePath = relativePathWithin(appRoot, target);
686
+ if (target !== change.absolutePath || relativePath !== change.relativePath || import_node_path.default.dirname(target) === target) {
687
+ throw new Error("SpotPatch init received an invalid integration file plan.");
688
+ }
689
+ let targetMetadata;
690
+ try {
691
+ targetMetadata = await (0, import_promises.lstat)(target);
692
+ } catch (error) {
693
+ if (!isMissingPathError(error)) {
694
+ throw error;
695
+ }
696
+ }
697
+ if (targetMetadata?.isSymbolicLink()) {
698
+ throw new Error(
699
+ `SpotPatch refuses to modify the symbolic link ${change.relativePath}.`
700
+ );
701
+ }
702
+ const containmentAnchor = await (0, import_promises.realpath)(
703
+ targetMetadata === void 0 ? import_node_path.default.dirname(target) : target
704
+ );
705
+ if (!isPathWithin(realAppRoot, containmentAnchor)) {
706
+ throw new Error("SpotPatch init refuses to modify a path outside the app root.");
707
+ }
708
+ }
709
+ async function assertCurrentBaseline(change) {
710
+ if (change.previousContent === void 0) {
711
+ try {
712
+ await (0, import_promises.lstat)(change.absolutePath);
713
+ } catch (error) {
714
+ if (isMissingPathError(error)) {
715
+ return;
716
+ }
717
+ throw error;
718
+ }
719
+ throw new Error(
720
+ `SpotPatch init cannot create ${change.relativePath} because it now exists.`
721
+ );
722
+ }
723
+ const currentContent = await readIntegrationFile(change.absolutePath);
724
+ if (currentContent !== change.previousContent) {
725
+ throw new Error(
726
+ `SpotPatch init cannot update ${change.relativePath} because it changed after the preview.`
727
+ );
728
+ }
729
+ }
730
+ async function applyIntegrationPlan(plan) {
731
+ if (plan.changes.length === 0) {
732
+ return;
733
+ }
734
+ const appRoot = import_node_path.default.resolve(plan.appRoot);
735
+ const realAppRoot = await (0, import_promises.realpath)(appRoot);
736
+ const targets = /* @__PURE__ */ new Set();
737
+ for (const change of plan.changes) {
738
+ if (targets.has(change.absolutePath)) {
739
+ throw new Error("SpotPatch init received duplicate integration file changes.");
740
+ }
741
+ targets.add(change.absolutePath);
742
+ await assertSafeTarget(appRoot, realAppRoot, change);
743
+ await assertCurrentBaseline(change);
744
+ }
745
+ const applied = [];
746
+ try {
747
+ for (const change of plan.changes) {
748
+ await assertCurrentBaseline(change);
749
+ const mode = change.previousContent === void 0 ? 384 : (await (0, import_promises.stat)(change.absolutePath)).mode & 511;
750
+ await writeAtomic(change.absolutePath, change.nextContent, mode);
751
+ applied.push(change);
752
+ }
753
+ } catch (error) {
754
+ const rollbackResults = await Promise.allSettled(
755
+ applied.reverse().map(rollbackChange)
756
+ );
757
+ if (rollbackResults.some((result) => result.status === "rejected")) {
758
+ throw new Error(
759
+ "SpotPatch init failed and could not completely restore the previous files.",
760
+ { cause: error }
761
+ );
762
+ }
763
+ throw new Error("SpotPatch init failed; all written files were restored.", {
764
+ cause: error
765
+ });
766
+ }
767
+ }
768
+
588
769
  // src/options.ts
589
770
  var import_shared2 = require("@spotpatch/shared");
590
771
  var import_zod = require("zod");
@@ -662,7 +843,7 @@ var aiOptionsSchema = import_zod.z.strictObject({
662
843
  defaultProvider: import_zod.z.string(),
663
844
  execution: import_zod.z.strictObject({
664
845
  isolation: import_zod.z.literal("git-worktree").optional(),
665
- applyMode: import_zod.z.enum(["review", "auto"]).optional(),
846
+ applyMode: import_zod.z.enum(import_shared2.AGENT_APPLY_MODES).optional(),
666
847
  checks: import_zod.z.record(import_zod.z.string(), agentCheckSchema).optional(),
667
848
  limits: agentLimitsSchema
668
849
  }).optional()
@@ -857,8 +1038,8 @@ function resolveAiOptions(options) {
857
1038
  const limits = resolveLimits(validated.execution?.limits);
858
1039
  const checks = resolveChecks(validated.execution?.checks, limits.checkTimeoutMs);
859
1040
  const applyMode = validated.execution?.applyMode ?? "review";
860
- if (applyMode === "auto" && !Object.values(checks).some((check) => check.required)) {
861
- throw new RangeError("SpotPatch AI auto mode requires a required check.");
1041
+ if (applyMode !== "review" && !Object.values(checks).some((check) => check.required)) {
1042
+ throw new RangeError(`SpotPatch AI ${applyMode} mode requires a required check.`);
862
1043
  }
863
1044
  const providers = resolveProviders(validated.providers);
864
1045
  if (!(validated.defaultProvider in providers)) {
@@ -910,6 +1091,9 @@ function assertPositiveBudget(budget) {
910
1091
  }
911
1092
  }
912
1093
  function resolveOptions(options = {}, environmentAi) {
1094
+ if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
1095
+ throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
1096
+ }
913
1097
  const budget = Object.freeze({
914
1098
  ...DEFAULT_OPTIONS.budget,
915
1099
  ...options.budget
@@ -949,17 +1133,179 @@ function resolveOptions(options = {}, environmentAi) {
949
1133
  return Object.freeze(resolved);
950
1134
  }
951
1135
 
1136
+ // src/project-validation.ts
1137
+ var import_node_child_process = require("child_process");
1138
+ var import_promises2 = require("fs/promises");
1139
+ var import_node_module = require("module");
1140
+ var import_node_path2 = __toESM(require("path"), 1);
1141
+ var import_node_util = require("util");
1142
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
1143
+ var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
1144
+ var TYPESCRIPT_CHECK_LABEL = "TypeScript";
1145
+ function isRecord(value) {
1146
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1147
+ }
1148
+ async function isRegularFile(absolutePath) {
1149
+ try {
1150
+ const metadata = await (0, import_promises2.lstat)(absolutePath);
1151
+ return metadata.isFile() && !metadata.isSymbolicLink();
1152
+ } catch {
1153
+ return false;
1154
+ }
1155
+ }
1156
+ async function readManifest(appRoot) {
1157
+ const manifestPath = import_node_path2.default.join(appRoot, "package.json");
1158
+ if (!await isRegularFile(manifestPath)) {
1159
+ return void 0;
1160
+ }
1161
+ try {
1162
+ const value = JSON.parse(await (0, import_promises2.readFile)(manifestPath, "utf8"));
1163
+ return isRecord(value) ? value : void 0;
1164
+ } catch {
1165
+ return void 0;
1166
+ }
1167
+ }
1168
+ function declaresTypeScript(manifest) {
1169
+ return [
1170
+ manifest.dependencies,
1171
+ manifest.devDependencies,
1172
+ manifest.peerDependencies
1173
+ ].some(
1174
+ (dependencies) => isRecord(dependencies) && typeof dependencies.typescript === "string"
1175
+ );
1176
+ }
1177
+ async function findGitRoot(appRoot) {
1178
+ try {
1179
+ const result = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
1180
+ cwd: appRoot,
1181
+ encoding: "utf8",
1182
+ timeout: 5e3,
1183
+ windowsHide: true
1184
+ });
1185
+ const root = await (0, import_promises2.realpath)(result.stdout.trim());
1186
+ const relative = import_node_path2.default.relative(root, appRoot);
1187
+ if (relative === "" || !relative.startsWith(`..${import_node_path2.default.sep}`) && relative !== ".." && !import_node_path2.default.isAbsolute(relative)) {
1188
+ return root;
1189
+ }
1190
+ } catch {
1191
+ return void 0;
1192
+ }
1193
+ return void 0;
1194
+ }
1195
+ async function resolveTypeScriptCli(appRoot) {
1196
+ const resolveFromApplication = (0, import_node_module.createRequire)(import_node_path2.default.join(appRoot, "package.json"));
1197
+ try {
1198
+ const packagePath = resolveFromApplication.resolve("typescript/package.json");
1199
+ const cliPath = import_node_path2.default.join(import_node_path2.default.dirname(packagePath), "bin", "tsc");
1200
+ await (0, import_promises2.access)(cliPath);
1201
+ return await (0, import_promises2.realpath)(cliPath);
1202
+ } catch {
1203
+ return void 0;
1204
+ }
1205
+ }
1206
+ function portableRelativePath(from, to) {
1207
+ return import_node_path2.default.relative(from, to).split(import_node_path2.default.sep).join("/");
1208
+ }
1209
+ async function discoverProjectValidationCheck(options) {
1210
+ const appRoot = await (0, import_promises2.realpath)(options.appRoot);
1211
+ const tsconfigPath = import_node_path2.default.join(appRoot, "tsconfig.json");
1212
+ const [manifest, projectRoot, hasTsconfig] = await Promise.all([
1213
+ readManifest(appRoot),
1214
+ findGitRoot(appRoot),
1215
+ isRegularFile(tsconfigPath)
1216
+ ]);
1217
+ if (manifest === void 0 || projectRoot === void 0 || !hasTsconfig || !declaresTypeScript(manifest)) {
1218
+ return void 0;
1219
+ }
1220
+ const cliPath = await resolveTypeScriptCli(appRoot);
1221
+ if (cliPath === void 0) {
1222
+ return void 0;
1223
+ }
1224
+ const projectPath = portableRelativePath(projectRoot, tsconfigPath);
1225
+ if (projectPath.length === 0 || projectPath.startsWith("../")) {
1226
+ return void 0;
1227
+ }
1228
+ return Object.freeze({
1229
+ id: TYPESCRIPT_CHECK_ID,
1230
+ label: TYPESCRIPT_CHECK_LABEL,
1231
+ command: process.execPath,
1232
+ args: Object.freeze([
1233
+ cliPath,
1234
+ "--noEmit",
1235
+ "--pretty",
1236
+ "false",
1237
+ "--project",
1238
+ projectPath
1239
+ ]),
1240
+ required: true,
1241
+ timeoutMs: options.timeoutMs
1242
+ });
1243
+ }
1244
+
1245
+ // src/project-options.ts
1246
+ function hasRequiredCheck(ai) {
1247
+ return Object.values(ai.execution.checks).some((check) => check.required);
1248
+ }
1249
+ function availableCheckId(checks, preferred) {
1250
+ if (checks[preferred] === void 0) {
1251
+ return preferred;
1252
+ }
1253
+ let suffix = 2;
1254
+ while (checks[`${preferred}-${String(suffix)}`] !== void 0) {
1255
+ suffix += 1;
1256
+ }
1257
+ return `${preferred}-${String(suffix)}`;
1258
+ }
1259
+ async function resolveProjectOptions(input) {
1260
+ const userOptions = input.options ?? {};
1261
+ const resolved = resolveOptions(userOptions, input.environmentAi);
1262
+ if (!userOptions.trustedFastMode || resolved.ai === false) {
1263
+ return resolved;
1264
+ }
1265
+ if (resolved.ai.execution.applyMode === "auto") {
1266
+ throw new RangeError(
1267
+ "SpotPatch trustedFastMode cannot be combined with applyMode auto."
1268
+ );
1269
+ }
1270
+ let checks = resolved.ai.execution.checks;
1271
+ if (!hasRequiredCheck(resolved.ai)) {
1272
+ const discovered = await discoverProjectValidationCheck({
1273
+ appRoot: input.appRoot,
1274
+ timeoutMs: resolved.ai.execution.limits.checkTimeoutMs
1275
+ });
1276
+ if (discovered === void 0) {
1277
+ throw new RangeError(
1278
+ "SpotPatch trustedFastMode requires a configured required check or a local TypeScript project with tsconfig.json."
1279
+ );
1280
+ }
1281
+ const id = availableCheckId(checks, discovered.id);
1282
+ checks = Object.freeze({
1283
+ ...checks,
1284
+ [id]: Object.freeze({ ...discovered, id })
1285
+ });
1286
+ }
1287
+ const ai = Object.freeze({
1288
+ ...resolved.ai,
1289
+ execution: Object.freeze({
1290
+ ...resolved.ai.execution,
1291
+ applyMode: "trusted-auto",
1292
+ checks
1293
+ })
1294
+ });
1295
+ return Object.freeze({ ...resolved, ai });
1296
+ }
1297
+
952
1298
  // src/registry/source-registry.ts
953
- var import_node_path = __toESM(require("path"), 1);
1299
+ var import_node_path3 = __toESM(require("path"), 1);
954
1300
 
955
1301
  // src/registry/source-id.ts
956
- var import_node_crypto2 = require("crypto");
1302
+ var import_node_crypto3 = require("crypto");
957
1303
  var SOURCE_ID_BYTES = 8;
958
- var createRandomSourceId = () => (0, import_node_crypto2.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
1304
+ var createRandomSourceId = () => (0, import_node_crypto3.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
959
1305
 
960
1306
  // src/registry/source-registry.ts
961
1307
  function normalizeAbsolutePath(absolutePath) {
962
- return import_node_path.default.normalize(import_node_path.default.resolve(absolutePath));
1308
+ return import_node_path3.default.normalize(import_node_path3.default.resolve(absolutePath));
963
1309
  }
964
1310
  function createSourceRegistry(options = {}) {
965
1311
  const createId = options.createId ?? createRandomSourceId;
@@ -997,13 +1343,13 @@ var import_shared10 = require("@spotpatch/shared");
997
1343
  var import_shared7 = require("@spotpatch/shared");
998
1344
 
999
1345
  // src/server/agent-request.ts
1000
- var import_promises3 = require("fs/promises");
1001
- var import_node_path4 = __toESM(require("path"), 1);
1346
+ var import_promises5 = require("fs/promises");
1347
+ var import_node_path6 = __toESM(require("path"), 1);
1002
1348
  var import_shared5 = require("@spotpatch/shared");
1003
1349
 
1004
1350
  // src/server/source-context.ts
1005
- var import_promises2 = require("fs/promises");
1006
- var import_node_path3 = __toESM(require("path"), 1);
1351
+ var import_promises4 = require("fs/promises");
1352
+ var import_node_path5 = __toESM(require("path"), 1);
1007
1353
  var import_shared4 = require("@spotpatch/shared");
1008
1354
 
1009
1355
  // src/server/extract-code-context.ts
@@ -1233,8 +1579,8 @@ function extractCodeContext(options) {
1233
1579
  }
1234
1580
 
1235
1581
  // src/server/source-file.ts
1236
- var import_promises = require("fs/promises");
1237
- var import_node_path2 = __toESM(require("path"), 1);
1582
+ var import_promises3 = require("fs/promises");
1583
+ var import_node_path4 = __toESM(require("path"), 1);
1238
1584
  var import_shared3 = require("@spotpatch/shared");
1239
1585
 
1240
1586
  // src/server/constants.ts
@@ -1252,8 +1598,8 @@ async function assertInsideRoot(root, candidate) {
1252
1598
  let realCandidate;
1253
1599
  try {
1254
1600
  [realRoot, realCandidate] = await Promise.all([
1255
- (0, import_promises.realpath)(root),
1256
- (0, import_promises.realpath)(candidate)
1601
+ (0, import_promises3.realpath)(root),
1602
+ (0, import_promises3.realpath)(candidate)
1257
1603
  ]);
1258
1604
  } catch (error) {
1259
1605
  if (isMissingFileError(error)) {
@@ -1263,8 +1609,8 @@ async function assertInsideRoot(root, candidate) {
1263
1609
  }
1264
1610
  throw error;
1265
1611
  }
1266
- const relative = import_node_path2.default.relative(realRoot, realCandidate);
1267
- const outside = relative.startsWith(`..${import_node_path2.default.sep}`) || relative === ".." || import_node_path2.default.isAbsolute(relative);
1612
+ const relative = import_node_path4.default.relative(realRoot, realCandidate);
1613
+ const outside = relative.startsWith(`..${import_node_path4.default.sep}`) || relative === ".." || import_node_path4.default.isAbsolute(relative);
1268
1614
  if (outside) {
1269
1615
  throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_OUTSIDE_ROOT);
1270
1616
  }
@@ -1276,12 +1622,12 @@ async function resolveSourceFile(options) {
1276
1622
  throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
1277
1623
  }
1278
1624
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
1279
- if (!ALLOWED_EXTENSIONS.has(import_node_path2.default.extname(sourcePath).toLowerCase())) {
1625
+ if (!ALLOWED_EXTENSIONS.has(import_node_path4.default.extname(sourcePath).toLowerCase())) {
1280
1626
  throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
1281
1627
  }
1282
1628
  let sourceStat;
1283
1629
  try {
1284
- sourceStat = await (0, import_promises.stat)(sourcePath);
1630
+ sourceStat = await (0, import_promises3.stat)(sourcePath);
1285
1631
  } catch (error) {
1286
1632
  if (isMissingFileError(error)) {
1287
1633
  throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
@@ -1301,7 +1647,7 @@ async function resolveSourceFile(options) {
1301
1647
 
1302
1648
  // src/server/source-context.ts
1303
1649
  function toDisplayPath(root, sourcePath) {
1304
- return import_node_path3.default.relative(root, sourcePath).split(import_node_path3.default.sep).join("/");
1650
+ return import_node_path5.default.relative(root, sourcePath).split(import_node_path5.default.sep).join("/");
1305
1651
  }
1306
1652
  async function readSourceContext(options) {
1307
1653
  const sourcePath = await resolveSourceFile({
@@ -1311,7 +1657,7 @@ async function readSourceContext(options) {
1311
1657
  });
1312
1658
  let source;
1313
1659
  try {
1314
- source = await (0, import_promises2.readFile)(sourcePath, "utf8");
1660
+ source = await (0, import_promises4.readFile)(sourcePath, "utf8");
1315
1661
  } catch (error) {
1316
1662
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1317
1663
  throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
@@ -1324,11 +1670,11 @@ async function readSourceContext(options) {
1324
1670
  if (options.request.line > lines.length) {
1325
1671
  throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
1326
1672
  }
1327
- const extension = import_node_path3.default.extname(sourcePath).toLowerCase();
1673
+ const extension = import_node_path5.default.extname(sourcePath).toLowerCase();
1328
1674
  return extractCodeContext({
1329
1675
  source,
1330
1676
  sourcePath,
1331
- relativePath: toDisplayPath(await (0, import_promises2.realpath)(options.root), sourcePath),
1677
+ relativePath: toDisplayPath(await (0, import_promises4.realpath)(options.root), sourcePath),
1332
1678
  language: extension === ".tsx" ? "tsx" : "jsx",
1333
1679
  line: options.request.line,
1334
1680
  column: options.request.column,
@@ -1364,7 +1710,7 @@ async function authorizeSourceRef(source, registry, root) {
1364
1710
  registry,
1365
1711
  root
1366
1712
  });
1367
- const relativePath = import_node_path4.default.relative(await (0, import_promises3.realpath)(root), sourcePath).split(import_node_path4.default.sep).join("/");
1713
+ const relativePath = import_node_path6.default.relative(await (0, import_promises5.realpath)(root), sourcePath).split(import_node_path6.default.sep).join("/");
1368
1714
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
1369
1715
  throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
1370
1716
  }
@@ -1469,9 +1815,11 @@ async function authorizeAgentJobRequest(input) {
1469
1815
  });
1470
1816
  return Object.freeze({
1471
1817
  annotation,
1818
+ ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
1472
1819
  providerProfileId: input.request.providerProfileId,
1473
1820
  modelProfileId: input.request.modelProfileId,
1474
1821
  providerDataConsent: true,
1822
+ ...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
1475
1823
  workingTreeMode: input.request.workingTreeMode
1476
1824
  });
1477
1825
  }
@@ -1534,21 +1882,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1534
1882
  "reverted",
1535
1883
  "failed"
1536
1884
  ]);
1537
- function matchAgentRequestPath(path6) {
1538
- if (path6 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
1885
+ function matchAgentRequestPath(path8) {
1886
+ if (path8 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
1539
1887
  return Object.freeze({ kind: "capability" });
1540
1888
  }
1541
- if (path6 === import_shared7.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
1889
+ if (path8 === import_shared7.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
1542
1890
  return Object.freeze({ kind: "workspace-health" });
1543
1891
  }
1544
- if (path6 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
1892
+ if (path8 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
1545
1893
  return Object.freeze({ kind: "create-job" });
1546
1894
  }
1547
1895
  const prefix = `${import_shared7.SPOTPATCH_ENDPOINTS.agentJobs}/`;
1548
- if (!path6.startsWith(prefix)) {
1896
+ if (!path8.startsWith(prefix)) {
1549
1897
  return void 0;
1550
1898
  }
1551
- const segments = path6.slice(prefix.length).split("/");
1899
+ const segments = path8.slice(prefix.length).split("/");
1552
1900
  const jobId = segments[0];
1553
1901
  const action = segments[1];
1554
1902
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -1718,7 +2066,7 @@ async function handleAgentRequest(request, response, options, route, writeSucces
1718
2066
  }
1719
2067
 
1720
2068
  // src/server/editor.ts
1721
- var import_node_child_process = require("child_process");
2069
+ var import_node_child_process2 = require("child_process");
1722
2070
  var import_launch_editor = __toESM(require("launch-editor"), 1);
1723
2071
  var EDITOR_STARTUP_GRACE_MS = 300;
1724
2072
  function normalizedEditorEnvironment(environment) {
@@ -1745,7 +2093,7 @@ function editorCommand(editor) {
1745
2093
  var DEFAULT_DEPENDENCIES2 = Object.freeze({
1746
2094
  environment: process.env,
1747
2095
  fallbackLauncher: import_launch_editor.default,
1748
- processSpawner: (command, arguments_, options) => (0, import_node_child_process.spawn)(command, [...arguments_], options),
2096
+ processSpawner: (command, arguments_, options) => (0, import_node_child_process2.spawn)(command, [...arguments_], options),
1749
2097
  startupGraceMs: EDITOR_STARTUP_GRACE_MS
1750
2098
  });
1751
2099
  function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
@@ -1803,7 +2151,7 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
1803
2151
  var launchConfiguredEditor = createEditorLauncher();
1804
2152
 
1805
2153
  // src/server/request-security.ts
1806
- var import_node_crypto3 = require("crypto");
2154
+ var import_node_crypto4 = require("crypto");
1807
2155
  var import_node_net = require("net");
1808
2156
  var import_shared8 = require("@spotpatch/shared");
1809
2157
  function getSingleHeader(request, name) {
@@ -1816,7 +2164,7 @@ function tokensMatch(actual, expected) {
1816
2164
  }
1817
2165
  const actualBytes = Buffer.from(actual);
1818
2166
  const expectedBytes = Buffer.from(expected);
1819
- return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto3.timingSafeEqual)(actualBytes, expectedBytes);
2167
+ return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto4.timingSafeEqual)(actualBytes, expectedBytes);
1820
2168
  }
1821
2169
  function isLoopbackHostname(hostname) {
1822
2170
  const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
@@ -2071,14 +2419,14 @@ async function handleOpenEditor(request, options) {
2071
2419
  function createSpotPatchMiddleware(options) {
2072
2420
  const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
2073
2421
  return (request, response, next) => {
2074
- const path6 = requestPath(request);
2075
- const agentRoute = matchAgentRequestPath(path6);
2076
- if (path6 !== import_shared10.SPOTPATCH_ENDPOINTS.sourceContext && path6 !== import_shared10.SPOTPATCH_ENDPOINTS.openEditor && agentRoute === void 0 && !path6.startsWith(`${import_shared10.SPOTPATCH_API_BASE}/`)) {
2422
+ const path8 = requestPath(request);
2423
+ const agentRoute = matchAgentRequestPath(path8);
2424
+ if (path8 !== import_shared10.SPOTPATCH_ENDPOINTS.sourceContext && path8 !== import_shared10.SPOTPATCH_ENDPOINTS.openEditor && agentRoute === void 0 && !path8.startsWith(`${import_shared10.SPOTPATCH_API_BASE}/`)) {
2077
2425
  next();
2078
2426
  return;
2079
2427
  }
2080
2428
  const handle = async () => {
2081
- if (path6 === import_shared10.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
2429
+ if (path8 === import_shared10.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
2082
2430
  const data = await readRuntimeBootstrap(
2083
2431
  request,
2084
2432
  bootstrap
@@ -2090,7 +2438,7 @@ function createSpotPatchMiddleware(options) {
2090
2438
  allowLan: options.options.allowLan,
2091
2439
  sessionToken: options.session.token
2092
2440
  });
2093
- if (path6 === import_shared10.SPOTPATCH_ENDPOINTS.sourceContext) {
2441
+ if (path8 === import_shared10.SPOTPATCH_ENDPOINTS.sourceContext) {
2094
2442
  if (request.method !== "POST") {
2095
2443
  throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
2096
2444
  }
@@ -2098,7 +2446,7 @@ function createSpotPatchMiddleware(options) {
2098
2446
  writeJson(response, 200, { ok: true, data });
2099
2447
  return;
2100
2448
  }
2101
- if (path6 === import_shared10.SPOTPATCH_ENDPOINTS.openEditor) {
2449
+ if (path8 === import_shared10.SPOTPATCH_ENDPOINTS.openEditor) {
2102
2450
  if (request.method !== "POST") {
2103
2451
  throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
2104
2452
  }
@@ -2126,9 +2474,9 @@ function createSpotPatchMiddleware(options) {
2126
2474
  }
2127
2475
 
2128
2476
  // src/server/source-registration.ts
2129
- var import_node_crypto4 = require("crypto");
2130
- var import_promises4 = require("fs/promises");
2131
- var import_node_path5 = __toESM(require("path"), 1);
2477
+ var import_node_crypto5 = require("crypto");
2478
+ var import_promises6 = require("fs/promises");
2479
+ var import_node_path7 = __toESM(require("path"), 1);
2132
2480
  var import_compiler = require("@spotpatch/compiler");
2133
2481
  var import_zod2 = require("zod");
2134
2482
  var REGISTRATION_BODY_LIMIT_BYTES = 4096;
@@ -2149,14 +2497,14 @@ function identitiesMatch(actual, expected) {
2149
2497
  }
2150
2498
  const actualBytes = Buffer.from(actual);
2151
2499
  const expectedBytes = Buffer.from(expected);
2152
- return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto4.timingSafeEqual)(actualBytes, expectedBytes);
2500
+ return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto5.timingSafeEqual)(actualBytes, expectedBytes);
2153
2501
  }
2154
2502
  function isWithinRoot(root, candidate) {
2155
- const relative = import_node_path5.default.relative(root, candidate);
2156
- return relative === "" || !relative.startsWith(`..${import_node_path5.default.sep}`) && relative !== ".." && !import_node_path5.default.isAbsolute(relative);
2503
+ const relative = import_node_path7.default.relative(root, candidate);
2504
+ return relative === "" || !relative.startsWith(`..${import_node_path7.default.sep}`) && relative !== ".." && !import_node_path7.default.isAbsolute(relative);
2157
2505
  }
2158
2506
  function hasForbiddenSegment(root, candidate) {
2159
- return import_node_path5.default.relative(root, candidate).split(import_node_path5.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2507
+ return import_node_path7.default.relative(root, candidate).split(import_node_path7.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2160
2508
  }
2161
2509
  function writeJson2(response, statusCode, payload) {
2162
2510
  const body = JSON.stringify(payload);
@@ -2178,15 +2526,15 @@ function requestComesFromLoopbackWorker(request) {
2178
2526
  }
2179
2527
  }
2180
2528
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
2181
- if (!import_node_path5.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2529
+ if (!import_node_path7.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2182
2530
  return void 0;
2183
2531
  }
2184
2532
  try {
2185
- const sourceStat = await (0, import_promises4.lstat)(requestedPath);
2533
+ const sourceStat = await (0, import_promises6.lstat)(requestedPath);
2186
2534
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
2187
2535
  return void 0;
2188
2536
  }
2189
- const resolvedPath = await (0, import_promises4.realpath)(requestedPath);
2537
+ const resolvedPath = await (0, import_promises6.realpath)(requestedPath);
2190
2538
  if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
2191
2539
  return void 0;
2192
2540
  }
@@ -2199,7 +2547,7 @@ async function createSourceRegistrationService(input) {
2199
2547
  if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
2200
2548
  throw new TypeError("The source registration identity is invalid.");
2201
2549
  }
2202
- const root = await (0, import_promises4.realpath)(input.root);
2550
+ const root = await (0, import_promises6.realpath)(input.root);
2203
2551
  const sourceFilter = (0, import_compiler.createSourceFilter)(root, input.options);
2204
2552
  const handler = (request, response) => {
2205
2553
  const handle = async () => {
@@ -2244,11 +2592,11 @@ async function createSourceRegistrationService(input) {
2244
2592
  }
2245
2593
 
2246
2594
  // src/session/session.ts
2247
- var import_node_crypto5 = require("crypto");
2595
+ var import_node_crypto6 = require("crypto");
2248
2596
  function createSession() {
2249
2597
  return Object.freeze({
2250
- id: (0, import_node_crypto5.randomBytes)(16).toString("base64url"),
2251
- token: (0, import_node_crypto5.randomBytes)(16).toString("base64url")
2598
+ id: (0, import_node_crypto6.randomBytes)(16).toString("base64url"),
2599
+ token: (0, import_node_crypto6.randomBytes)(16).toString("base64url")
2252
2600
  });
2253
2601
  }
2254
2602
 
@@ -2276,7 +2624,7 @@ var BUDGET_KEYS = Object.freeze([
2276
2624
  "maxComponentDepth"
2277
2625
  ]);
2278
2626
  var REGEXP_FLAGS_PATTERN = /^(?!.*(.).*\1)[dgimsuvy]*$/u;
2279
- function isRecord(value) {
2627
+ function isRecord2(value) {
2280
2628
  return typeof value === "object" && value !== null && !Array.isArray(value);
2281
2629
  }
2282
2630
  function hasExactKeys(value, keys) {
@@ -2361,7 +2709,7 @@ function parseFilterList(value) {
2361
2709
  }
2362
2710
  return Object.freeze(
2363
2711
  value.map((entry) => {
2364
- if (!isRecord(entry)) {
2712
+ if (!isRecord2(entry)) {
2365
2713
  throw new TypeError("The SpotPatch filter transport is invalid.");
2366
2714
  }
2367
2715
  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 +2727,7 @@ function parseFilterList(value) {
2379
2727
  );
2380
2728
  }
2381
2729
  function parseBudget(value) {
2382
- if (!isRecord(value) || !hasExactKeys(value, BUDGET_KEYS)) {
2730
+ if (!isRecord2(value) || !hasExactKeys(value, BUDGET_KEYS)) {
2383
2731
  throw new TypeError("The SpotPatch budget transport is invalid.");
2384
2732
  }
2385
2733
  const budget = Object.fromEntries(
@@ -2388,10 +2736,10 @@ function parseBudget(value) {
2388
2736
  return Object.freeze(budget);
2389
2737
  }
2390
2738
  function parseSerializedSpotPatchOptions(value) {
2391
- if (!isRecord(value) || !hasExactKeys(value, OPTION_KEYS)) {
2739
+ if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
2392
2740
  throw new TypeError("The SpotPatch options transport is invalid.");
2393
2741
  }
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)) {
2742
+ 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
2743
  throw new TypeError("The SpotPatch options transport is invalid.");
2396
2744
  }
2397
2745
  try {
@@ -2419,19 +2767,25 @@ function parseSerializedSpotPatchOptions(value) {
2419
2767
  0 && (module.exports = {
2420
2768
  DEFAULT_EXCLUDE,
2421
2769
  DEFAULT_OPTIONS,
2770
+ applyIntegrationPlan,
2422
2771
  createAgentJobManager,
2772
+ createIntegrationFileChange,
2423
2773
  createRuntimeAiConfig,
2424
2774
  createSession,
2425
2775
  createSourceRegistrationService,
2426
2776
  createSourceRegistry,
2427
2777
  createSpotPatchMiddleware,
2778
+ discoverProjectValidationCheck,
2779
+ integrationPathExists,
2428
2780
  isLoopbackHostname,
2429
2781
  parseSerializedSpotPatchOptions,
2782
+ readIntegrationFile,
2430
2783
  readJsonRequestBody,
2431
2784
  readRuntimeBootstrap,
2432
2785
  resolveCredentialEnvironment,
2433
2786
  resolveEnvironmentAiConfiguration,
2434
2787
  resolveOptions,
2788
+ resolveProjectOptions,
2435
2789
  resolveRuntimeBootstrapOptions,
2436
2790
  serializeResolvedSpotPatchOptions
2437
2791
  });