@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.cjs +416 -61
- 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 +414 -55
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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(
|
|
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
|
|
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 =
|
|
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
|
|
360
|
-
|
|
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
|
|
1275
|
+
import path3 from "path";
|
|
924
1276
|
|
|
925
1277
|
// src/registry/source-id.ts
|
|
926
|
-
import { randomBytes as
|
|
1278
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
927
1279
|
var SOURCE_ID_BYTES = 8;
|
|
928
|
-
var createRandomSourceId = () =>
|
|
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
|
|
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
|
|
986
|
-
import
|
|
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
|
|
994
|
-
import
|
|
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
|
|
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
|
-
|
|
1250
|
-
|
|
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 =
|
|
1261
|
-
const outside = relative.startsWith(`..${
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1673
|
+
const extension = path5.extname(sourcePath).toLowerCase();
|
|
1322
1674
|
return extractCodeContext({
|
|
1323
1675
|
source,
|
|
1324
1676
|
sourcePath,
|
|
1325
|
-
relativePath: toDisplayPath(await
|
|
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 =
|
|
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(
|
|
1533
|
-
if (
|
|
1885
|
+
function matchAgentRequestPath(path8) {
|
|
1886
|
+
if (path8 === SPOTPATCH_ENDPOINTS.agentCapability) {
|
|
1534
1887
|
return Object.freeze({ kind: "capability" });
|
|
1535
1888
|
}
|
|
1536
|
-
if (
|
|
1889
|
+
if (path8 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
|
|
1537
1890
|
return Object.freeze({ kind: "workspace-health" });
|
|
1538
1891
|
}
|
|
1539
|
-
if (
|
|
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 (!
|
|
1896
|
+
if (!path8.startsWith(prefix)) {
|
|
1544
1897
|
return void 0;
|
|
1545
1898
|
}
|
|
1546
|
-
const segments =
|
|
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
|
|
2075
|
-
const agentRoute = matchAgentRequestPath(
|
|
2076
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
2131
|
-
import
|
|
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 =
|
|
2156
|
-
return relative === "" || !relative.startsWith(`..${
|
|
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
|
|
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 (!
|
|
2534
|
+
if (!path7.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2182
2535
|
return void 0;
|
|
2183
2536
|
}
|
|
2184
2537
|
try {
|
|
2185
|
-
const sourceStat = await
|
|
2538
|
+
const sourceStat = await lstat3(requestedPath);
|
|
2186
2539
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2187
2540
|
return void 0;
|
|
2188
2541
|
}
|
|
2189
|
-
const resolvedPath = await
|
|
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
|
|
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
|
|
2600
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2248
2601
|
function createSession() {
|
|
2249
2602
|
return Object.freeze({
|
|
2250
|
-
id:
|
|
2251
|
-
token:
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 && !
|
|
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
|
};
|