@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 +414 -60
- 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 +413 -54
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -280,7 +280,8 @@ function createAgentJobManager(options) {
|
|
|
280
280
|
transition(job, "completed", "No source changes were proposed.");
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
|
-
|
|
283
|
+
const shouldApplyDirectly = job.applyMode === "auto" && preparedChange.autoApplyEligible || job.applyMode === "trusted-auto" && job.trustedFastModeConsent;
|
|
284
|
+
if (shouldApplyDirectly) {
|
|
284
285
|
try {
|
|
285
286
|
await applyChange(job, preparedChange);
|
|
286
287
|
} catch {
|
|
@@ -355,6 +356,13 @@ function createAgentJobManager(options) {
|
|
|
355
356
|
if (closed) {
|
|
356
357
|
throw new SpotPatchError(ERROR_CODES.AI_DISABLED);
|
|
357
358
|
}
|
|
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) {
|
|
364
|
+
throw new SpotPatchError(ERROR_CODES.INVALID_REQUEST);
|
|
365
|
+
}
|
|
358
366
|
if (hasActiveJob()) {
|
|
359
367
|
throw new SpotPatchError(ERROR_CODES.AGENT_BUSY);
|
|
360
368
|
}
|
|
@@ -373,6 +381,7 @@ function createAgentJobManager(options) {
|
|
|
373
381
|
const timestamp = dependencies.now();
|
|
374
382
|
const job = {
|
|
375
383
|
annotation: request.annotation,
|
|
384
|
+
applyMode: requestedApplyMode,
|
|
376
385
|
controller: new AbortController(),
|
|
377
386
|
createdAt: timestamp,
|
|
378
387
|
credential: selection.credential,
|
|
@@ -388,6 +397,7 @@ function createAgentJobManager(options) {
|
|
|
388
397
|
runPromise: void 0,
|
|
389
398
|
sequence: 0,
|
|
390
399
|
status: "queued",
|
|
400
|
+
trustedFastModeConsent: request.trustedFastModeConsent === true,
|
|
391
401
|
updatedAt: timestamp,
|
|
392
402
|
workingTreeMode: request.workingTreeMode
|
|
393
403
|
};
|
|
@@ -543,8 +553,184 @@ function resolveEnvironmentAiConfiguration(environment) {
|
|
|
543
553
|
});
|
|
544
554
|
}
|
|
545
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
|
+
|
|
546
731
|
// src/options.ts
|
|
547
732
|
import {
|
|
733
|
+
AGENT_APPLY_MODES,
|
|
548
734
|
DEFAULT_AGENT_LIMITS,
|
|
549
735
|
MAX_ANNOTATION_TARGETS,
|
|
550
736
|
SPOTPATCH_EDITOR_PREFERENCES,
|
|
@@ -625,7 +811,7 @@ var aiOptionsSchema = z.strictObject({
|
|
|
625
811
|
defaultProvider: z.string(),
|
|
626
812
|
execution: z.strictObject({
|
|
627
813
|
isolation: z.literal("git-worktree").optional(),
|
|
628
|
-
applyMode: z.enum(
|
|
814
|
+
applyMode: z.enum(AGENT_APPLY_MODES).optional(),
|
|
629
815
|
checks: z.record(z.string(), agentCheckSchema).optional(),
|
|
630
816
|
limits: agentLimitsSchema
|
|
631
817
|
}).optional()
|
|
@@ -820,8 +1006,8 @@ function resolveAiOptions(options) {
|
|
|
820
1006
|
const limits = resolveLimits(validated.execution?.limits);
|
|
821
1007
|
const checks = resolveChecks(validated.execution?.checks, limits.checkTimeoutMs);
|
|
822
1008
|
const applyMode = validated.execution?.applyMode ?? "review";
|
|
823
|
-
if (applyMode
|
|
824
|
-
throw new RangeError(
|
|
1009
|
+
if (applyMode !== "review" && !Object.values(checks).some((check) => check.required)) {
|
|
1010
|
+
throw new RangeError(`SpotPatch AI ${applyMode} mode requires a required check.`);
|
|
825
1011
|
}
|
|
826
1012
|
const providers = resolveProviders(validated.providers);
|
|
827
1013
|
if (!(validated.defaultProvider in providers)) {
|
|
@@ -873,6 +1059,9 @@ function assertPositiveBudget(budget) {
|
|
|
873
1059
|
}
|
|
874
1060
|
}
|
|
875
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
|
+
}
|
|
876
1065
|
const budget = Object.freeze({
|
|
877
1066
|
...DEFAULT_OPTIONS.budget,
|
|
878
1067
|
...options.budget
|
|
@@ -912,17 +1101,179 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
912
1101
|
return Object.freeze(resolved);
|
|
913
1102
|
}
|
|
914
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
|
+
|
|
915
1266
|
// src/registry/source-registry.ts
|
|
916
|
-
import
|
|
1267
|
+
import path3 from "path";
|
|
917
1268
|
|
|
918
1269
|
// src/registry/source-id.ts
|
|
919
|
-
import { randomBytes as
|
|
1270
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
920
1271
|
var SOURCE_ID_BYTES = 8;
|
|
921
|
-
var createRandomSourceId = () =>
|
|
1272
|
+
var createRandomSourceId = () => randomBytes3(SOURCE_ID_BYTES).toString("base64url");
|
|
922
1273
|
|
|
923
1274
|
// src/registry/source-registry.ts
|
|
924
1275
|
function normalizeAbsolutePath(absolutePath) {
|
|
925
|
-
return
|
|
1276
|
+
return path3.normalize(path3.resolve(absolutePath));
|
|
926
1277
|
}
|
|
927
1278
|
function createSourceRegistry(options = {}) {
|
|
928
1279
|
const createId = options.createId ?? createRandomSourceId;
|
|
@@ -975,16 +1326,16 @@ import {
|
|
|
975
1326
|
} from "@spotpatch/shared";
|
|
976
1327
|
|
|
977
1328
|
// src/server/agent-request.ts
|
|
978
|
-
import { realpath as
|
|
979
|
-
import
|
|
1329
|
+
import { realpath as realpath5 } from "fs/promises";
|
|
1330
|
+
import path6 from "path";
|
|
980
1331
|
import {
|
|
981
1332
|
ERROR_CODES as ERROR_CODES4,
|
|
982
1333
|
SpotPatchError as SpotPatchError4
|
|
983
1334
|
} from "@spotpatch/shared";
|
|
984
1335
|
|
|
985
1336
|
// src/server/source-context.ts
|
|
986
|
-
import { readFile, realpath as
|
|
987
|
-
import
|
|
1337
|
+
import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
|
|
1338
|
+
import path5 from "path";
|
|
988
1339
|
import {
|
|
989
1340
|
ERROR_CODES as ERROR_CODES3,
|
|
990
1341
|
SpotPatchError as SpotPatchError3
|
|
@@ -1220,8 +1571,8 @@ function extractCodeContext(options) {
|
|
|
1220
1571
|
}
|
|
1221
1572
|
|
|
1222
1573
|
// src/server/source-file.ts
|
|
1223
|
-
import { realpath, stat } from "fs/promises";
|
|
1224
|
-
import
|
|
1574
|
+
import { realpath as realpath3, stat as stat2 } from "fs/promises";
|
|
1575
|
+
import path4 from "path";
|
|
1225
1576
|
import { ERROR_CODES as ERROR_CODES2, SpotPatchError as SpotPatchError2 } from "@spotpatch/shared";
|
|
1226
1577
|
|
|
1227
1578
|
// src/server/constants.ts
|
|
@@ -1239,8 +1590,8 @@ async function assertInsideRoot(root, candidate) {
|
|
|
1239
1590
|
let realCandidate;
|
|
1240
1591
|
try {
|
|
1241
1592
|
[realRoot, realCandidate] = await Promise.all([
|
|
1242
|
-
|
|
1243
|
-
|
|
1593
|
+
realpath3(root),
|
|
1594
|
+
realpath3(candidate)
|
|
1244
1595
|
]);
|
|
1245
1596
|
} catch (error) {
|
|
1246
1597
|
if (isMissingFileError(error)) {
|
|
@@ -1250,8 +1601,8 @@ async function assertInsideRoot(root, candidate) {
|
|
|
1250
1601
|
}
|
|
1251
1602
|
throw error;
|
|
1252
1603
|
}
|
|
1253
|
-
const relative =
|
|
1254
|
-
const outside = relative.startsWith(`..${
|
|
1604
|
+
const relative = path4.relative(realRoot, realCandidate);
|
|
1605
|
+
const outside = relative.startsWith(`..${path4.sep}`) || relative === ".." || path4.isAbsolute(relative);
|
|
1255
1606
|
if (outside) {
|
|
1256
1607
|
throw new SpotPatchError2(ERROR_CODES2.SOURCE_OUTSIDE_ROOT);
|
|
1257
1608
|
}
|
|
@@ -1263,12 +1614,12 @@ async function resolveSourceFile(options) {
|
|
|
1263
1614
|
throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
|
|
1264
1615
|
}
|
|
1265
1616
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
1266
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
1617
|
+
if (!ALLOWED_EXTENSIONS.has(path4.extname(sourcePath).toLowerCase())) {
|
|
1267
1618
|
throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
|
|
1268
1619
|
}
|
|
1269
1620
|
let sourceStat;
|
|
1270
1621
|
try {
|
|
1271
|
-
sourceStat = await
|
|
1622
|
+
sourceStat = await stat2(sourcePath);
|
|
1272
1623
|
} catch (error) {
|
|
1273
1624
|
if (isMissingFileError(error)) {
|
|
1274
1625
|
throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
|
|
@@ -1288,7 +1639,7 @@ async function resolveSourceFile(options) {
|
|
|
1288
1639
|
|
|
1289
1640
|
// src/server/source-context.ts
|
|
1290
1641
|
function toDisplayPath(root, sourcePath) {
|
|
1291
|
-
return
|
|
1642
|
+
return path5.relative(root, sourcePath).split(path5.sep).join("/");
|
|
1292
1643
|
}
|
|
1293
1644
|
async function readSourceContext(options) {
|
|
1294
1645
|
const sourcePath = await resolveSourceFile({
|
|
@@ -1298,7 +1649,7 @@ async function readSourceContext(options) {
|
|
|
1298
1649
|
});
|
|
1299
1650
|
let source;
|
|
1300
1651
|
try {
|
|
1301
|
-
source = await
|
|
1652
|
+
source = await readFile3(sourcePath, "utf8");
|
|
1302
1653
|
} catch (error) {
|
|
1303
1654
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1304
1655
|
throw new SpotPatchError3(ERROR_CODES3.SOURCE_NOT_FOUND, void 0, {
|
|
@@ -1311,11 +1662,11 @@ async function readSourceContext(options) {
|
|
|
1311
1662
|
if (options.request.line > lines.length) {
|
|
1312
1663
|
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
1313
1664
|
}
|
|
1314
|
-
const extension =
|
|
1665
|
+
const extension = path5.extname(sourcePath).toLowerCase();
|
|
1315
1666
|
return extractCodeContext({
|
|
1316
1667
|
source,
|
|
1317
1668
|
sourcePath,
|
|
1318
|
-
relativePath: toDisplayPath(await
|
|
1669
|
+
relativePath: toDisplayPath(await realpath4(options.root), sourcePath),
|
|
1319
1670
|
language: extension === ".tsx" ? "tsx" : "jsx",
|
|
1320
1671
|
line: options.request.line,
|
|
1321
1672
|
column: options.request.column,
|
|
@@ -1351,7 +1702,7 @@ async function authorizeSourceRef(source, registry, root) {
|
|
|
1351
1702
|
registry,
|
|
1352
1703
|
root
|
|
1353
1704
|
});
|
|
1354
|
-
const relativePath =
|
|
1705
|
+
const relativePath = path6.relative(await realpath5(root), sourcePath).split(path6.sep).join("/");
|
|
1355
1706
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
1356
1707
|
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
1357
1708
|
}
|
|
@@ -1456,9 +1807,11 @@ async function authorizeAgentJobRequest(input) {
|
|
|
1456
1807
|
});
|
|
1457
1808
|
return Object.freeze({
|
|
1458
1809
|
annotation,
|
|
1810
|
+
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
1459
1811
|
providerProfileId: input.request.providerProfileId,
|
|
1460
1812
|
modelProfileId: input.request.modelProfileId,
|
|
1461
1813
|
providerDataConsent: true,
|
|
1814
|
+
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
1462
1815
|
workingTreeMode: input.request.workingTreeMode
|
|
1463
1816
|
});
|
|
1464
1817
|
}
|
|
@@ -1521,21 +1874,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
1521
1874
|
"reverted",
|
|
1522
1875
|
"failed"
|
|
1523
1876
|
]);
|
|
1524
|
-
function matchAgentRequestPath(
|
|
1525
|
-
if (
|
|
1877
|
+
function matchAgentRequestPath(path8) {
|
|
1878
|
+
if (path8 === SPOTPATCH_ENDPOINTS.agentCapability) {
|
|
1526
1879
|
return Object.freeze({ kind: "capability" });
|
|
1527
1880
|
}
|
|
1528
|
-
if (
|
|
1881
|
+
if (path8 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
|
|
1529
1882
|
return Object.freeze({ kind: "workspace-health" });
|
|
1530
1883
|
}
|
|
1531
|
-
if (
|
|
1884
|
+
if (path8 === SPOTPATCH_ENDPOINTS.agentJobs) {
|
|
1532
1885
|
return Object.freeze({ kind: "create-job" });
|
|
1533
1886
|
}
|
|
1534
1887
|
const prefix = `${SPOTPATCH_ENDPOINTS.agentJobs}/`;
|
|
1535
|
-
if (!
|
|
1888
|
+
if (!path8.startsWith(prefix)) {
|
|
1536
1889
|
return void 0;
|
|
1537
1890
|
}
|
|
1538
|
-
const segments =
|
|
1891
|
+
const segments = path8.slice(prefix.length).split("/");
|
|
1539
1892
|
const jobId = segments[0];
|
|
1540
1893
|
const action = segments[1];
|
|
1541
1894
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -2063,14 +2416,14 @@ async function handleOpenEditor(request, options) {
|
|
|
2063
2416
|
function createSpotPatchMiddleware(options) {
|
|
2064
2417
|
const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
|
|
2065
2418
|
return (request, response, next) => {
|
|
2066
|
-
const
|
|
2067
|
-
const agentRoute = matchAgentRequestPath(
|
|
2068
|
-
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}/`)) {
|
|
2069
2422
|
next();
|
|
2070
2423
|
return;
|
|
2071
2424
|
}
|
|
2072
2425
|
const handle = async () => {
|
|
2073
|
-
if (
|
|
2426
|
+
if (path8 === SPOTPATCH_ENDPOINTS2.bootstrap && bootstrap !== void 0) {
|
|
2074
2427
|
const data = await readRuntimeBootstrap(
|
|
2075
2428
|
request,
|
|
2076
2429
|
bootstrap
|
|
@@ -2082,7 +2435,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
2082
2435
|
allowLan: options.options.allowLan,
|
|
2083
2436
|
sessionToken: options.session.token
|
|
2084
2437
|
});
|
|
2085
|
-
if (
|
|
2438
|
+
if (path8 === SPOTPATCH_ENDPOINTS2.sourceContext) {
|
|
2086
2439
|
if (request.method !== "POST") {
|
|
2087
2440
|
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
2088
2441
|
}
|
|
@@ -2090,7 +2443,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
2090
2443
|
writeJson(response, 200, { ok: true, data });
|
|
2091
2444
|
return;
|
|
2092
2445
|
}
|
|
2093
|
-
if (
|
|
2446
|
+
if (path8 === SPOTPATCH_ENDPOINTS2.openEditor) {
|
|
2094
2447
|
if (request.method !== "POST") {
|
|
2095
2448
|
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
2096
2449
|
}
|
|
@@ -2119,8 +2472,8 @@ function createSpotPatchMiddleware(options) {
|
|
|
2119
2472
|
|
|
2120
2473
|
// src/server/source-registration.ts
|
|
2121
2474
|
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2122
|
-
import { lstat, realpath as
|
|
2123
|
-
import
|
|
2475
|
+
import { lstat as lstat3, realpath as realpath6 } from "fs/promises";
|
|
2476
|
+
import path7 from "path";
|
|
2124
2477
|
import { createSourceFilter } from "@spotpatch/compiler";
|
|
2125
2478
|
import { z as z2 } from "zod";
|
|
2126
2479
|
var REGISTRATION_BODY_LIMIT_BYTES = 4096;
|
|
@@ -2144,11 +2497,11 @@ function identitiesMatch(actual, expected) {
|
|
|
2144
2497
|
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
|
|
2145
2498
|
}
|
|
2146
2499
|
function isWithinRoot(root, candidate) {
|
|
2147
|
-
const relative =
|
|
2148
|
-
return relative === "" || !relative.startsWith(`..${
|
|
2500
|
+
const relative = path7.relative(root, candidate);
|
|
2501
|
+
return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
|
|
2149
2502
|
}
|
|
2150
2503
|
function hasForbiddenSegment(root, candidate) {
|
|
2151
|
-
return
|
|
2504
|
+
return path7.relative(root, candidate).split(path7.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
2152
2505
|
}
|
|
2153
2506
|
function writeJson2(response, statusCode, payload) {
|
|
2154
2507
|
const body = JSON.stringify(payload);
|
|
@@ -2170,15 +2523,15 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
2170
2523
|
}
|
|
2171
2524
|
}
|
|
2172
2525
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
2173
|
-
if (!
|
|
2526
|
+
if (!path7.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2174
2527
|
return void 0;
|
|
2175
2528
|
}
|
|
2176
2529
|
try {
|
|
2177
|
-
const sourceStat = await
|
|
2530
|
+
const sourceStat = await lstat3(requestedPath);
|
|
2178
2531
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2179
2532
|
return void 0;
|
|
2180
2533
|
}
|
|
2181
|
-
const resolvedPath = await
|
|
2534
|
+
const resolvedPath = await realpath6(requestedPath);
|
|
2182
2535
|
if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
|
|
2183
2536
|
return void 0;
|
|
2184
2537
|
}
|
|
@@ -2191,7 +2544,7 @@ async function createSourceRegistrationService(input) {
|
|
|
2191
2544
|
if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
|
|
2192
2545
|
throw new TypeError("The source registration identity is invalid.");
|
|
2193
2546
|
}
|
|
2194
|
-
const root = await
|
|
2547
|
+
const root = await realpath6(input.root);
|
|
2195
2548
|
const sourceFilter = createSourceFilter(root, input.options);
|
|
2196
2549
|
const handler = (request, response) => {
|
|
2197
2550
|
const handle = async () => {
|
|
@@ -2236,11 +2589,11 @@ async function createSourceRegistrationService(input) {
|
|
|
2236
2589
|
}
|
|
2237
2590
|
|
|
2238
2591
|
// src/session/session.ts
|
|
2239
|
-
import { randomBytes as
|
|
2592
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2240
2593
|
function createSession() {
|
|
2241
2594
|
return Object.freeze({
|
|
2242
|
-
id:
|
|
2243
|
-
token:
|
|
2595
|
+
id: randomBytes4(16).toString("base64url"),
|
|
2596
|
+
token: randomBytes4(16).toString("base64url")
|
|
2244
2597
|
});
|
|
2245
2598
|
}
|
|
2246
2599
|
|
|
@@ -2268,7 +2621,7 @@ var BUDGET_KEYS = Object.freeze([
|
|
|
2268
2621
|
"maxComponentDepth"
|
|
2269
2622
|
]);
|
|
2270
2623
|
var REGEXP_FLAGS_PATTERN = /^(?!.*(.).*\1)[dgimsuvy]*$/u;
|
|
2271
|
-
function
|
|
2624
|
+
function isRecord2(value) {
|
|
2272
2625
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2273
2626
|
}
|
|
2274
2627
|
function hasExactKeys(value, keys) {
|
|
@@ -2353,7 +2706,7 @@ function parseFilterList(value) {
|
|
|
2353
2706
|
}
|
|
2354
2707
|
return Object.freeze(
|
|
2355
2708
|
value.map((entry) => {
|
|
2356
|
-
if (!
|
|
2709
|
+
if (!isRecord2(entry)) {
|
|
2357
2710
|
throw new TypeError("The SpotPatch filter transport is invalid.");
|
|
2358
2711
|
}
|
|
2359
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")) {
|
|
@@ -2371,7 +2724,7 @@ function parseFilterList(value) {
|
|
|
2371
2724
|
);
|
|
2372
2725
|
}
|
|
2373
2726
|
function parseBudget(value) {
|
|
2374
|
-
if (!
|
|
2727
|
+
if (!isRecord2(value) || !hasExactKeys(value, BUDGET_KEYS)) {
|
|
2375
2728
|
throw new TypeError("The SpotPatch budget transport is invalid.");
|
|
2376
2729
|
}
|
|
2377
2730
|
const budget = Object.fromEntries(
|
|
@@ -2380,10 +2733,10 @@ function parseBudget(value) {
|
|
|
2380
2733
|
return Object.freeze(budget);
|
|
2381
2734
|
}
|
|
2382
2735
|
function parseSerializedSpotPatchOptions(value) {
|
|
2383
|
-
if (!
|
|
2736
|
+
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
2384
2737
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
2385
2738
|
}
|
|
2386
|
-
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)) {
|
|
2387
2740
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
2388
2741
|
}
|
|
2389
2742
|
try {
|
|
@@ -2410,19 +2763,25 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
2410
2763
|
export {
|
|
2411
2764
|
DEFAULT_EXCLUDE,
|
|
2412
2765
|
DEFAULT_OPTIONS,
|
|
2766
|
+
applyIntegrationPlan,
|
|
2413
2767
|
createAgentJobManager,
|
|
2768
|
+
createIntegrationFileChange,
|
|
2414
2769
|
createRuntimeAiConfig,
|
|
2415
2770
|
createSession,
|
|
2416
2771
|
createSourceRegistrationService,
|
|
2417
2772
|
createSourceRegistry,
|
|
2418
2773
|
createSpotPatchMiddleware,
|
|
2774
|
+
discoverProjectValidationCheck,
|
|
2775
|
+
integrationPathExists,
|
|
2419
2776
|
isLoopbackHostname,
|
|
2420
2777
|
parseSerializedSpotPatchOptions,
|
|
2778
|
+
readIntegrationFile,
|
|
2421
2779
|
readJsonRequestBody,
|
|
2422
2780
|
readRuntimeBootstrap,
|
|
2423
2781
|
resolveCredentialEnvironment,
|
|
2424
2782
|
resolveEnvironmentAiConfiguration,
|
|
2425
2783
|
resolveOptions,
|
|
2784
|
+
resolveProjectOptions,
|
|
2426
2785
|
resolveRuntimeBootstrapOptions,
|
|
2427
2786
|
serializeResolvedSpotPatchOptions
|
|
2428
2787
|
};
|