@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.cjs +406 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +404 -53
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 =
|
|
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
|
|
360
|
-
|
|
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
|
|
1267
|
+
import path3 from "path";
|
|
924
1268
|
|
|
925
1269
|
// src/registry/source-id.ts
|
|
926
|
-
import { randomBytes as
|
|
1270
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
927
1271
|
var SOURCE_ID_BYTES = 8;
|
|
928
|
-
var createRandomSourceId = () =>
|
|
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
|
|
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
|
|
986
|
-
import
|
|
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
|
|
994
|
-
import
|
|
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
|
|
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
|
-
|
|
1250
|
-
|
|
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 =
|
|
1261
|
-
const outside = relative.startsWith(`..${
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1665
|
+
const extension = path5.extname(sourcePath).toLowerCase();
|
|
1322
1666
|
return extractCodeContext({
|
|
1323
1667
|
source,
|
|
1324
1668
|
sourcePath,
|
|
1325
|
-
relativePath: toDisplayPath(await
|
|
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 =
|
|
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(
|
|
1533
|
-
if (
|
|
1877
|
+
function matchAgentRequestPath(path8) {
|
|
1878
|
+
if (path8 === SPOTPATCH_ENDPOINTS.agentCapability) {
|
|
1534
1879
|
return Object.freeze({ kind: "capability" });
|
|
1535
1880
|
}
|
|
1536
|
-
if (
|
|
1881
|
+
if (path8 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
|
|
1537
1882
|
return Object.freeze({ kind: "workspace-health" });
|
|
1538
1883
|
}
|
|
1539
|
-
if (
|
|
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 (!
|
|
1888
|
+
if (!path8.startsWith(prefix)) {
|
|
1544
1889
|
return void 0;
|
|
1545
1890
|
}
|
|
1546
|
-
const segments =
|
|
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
|
|
2075
|
-
const agentRoute = matchAgentRequestPath(
|
|
2076
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
2131
|
-
import
|
|
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 =
|
|
2156
|
-
return relative === "" || !relative.startsWith(`..${
|
|
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
|
|
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 (!
|
|
2526
|
+
if (!path7.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2182
2527
|
return void 0;
|
|
2183
2528
|
}
|
|
2184
2529
|
try {
|
|
2185
|
-
const sourceStat = await
|
|
2530
|
+
const sourceStat = await lstat3(requestedPath);
|
|
2186
2531
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2187
2532
|
return void 0;
|
|
2188
2533
|
}
|
|
2189
|
-
const resolvedPath = await
|
|
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
|
|
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
|
|
2592
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2248
2593
|
function createSession() {
|
|
2249
2594
|
return Object.freeze({
|
|
2250
|
-
id:
|
|
2251
|
-
token:
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 && !
|
|
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
|
};
|