@emmaneugene/pi-cursor-sdk 0.3.7 → 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/CHANGELOG.md +16 -0
- package/README.md +99 -126
- package/dist/cursor-agents-context-registration.js +1 -3
- package/dist/cursor-agents-context.js +2 -2
- package/dist/cursor-config.js +2 -290
- package/dist/cursor-provider-errors.js +3 -58
- package/dist/cursor-provider-live-run-drain.js +0 -3
- package/dist/cursor-provider-run-finalizer.js +4 -7
- package/dist/cursor-provider-run-outcome.js +1 -1
- package/dist/cursor-provider-turn-finalize.js +3 -55
- package/dist/cursor-provider-turn-prepare.js +8 -144
- package/dist/cursor-provider-turn-runner.js +9 -23
- package/dist/cursor-provider-turn-send.js +8 -35
- package/dist/cursor-runtime-state.js +6 -268
- package/dist/cursor-sdk-billed-usage.js +3 -18
- package/dist/cursor-sdk-platform-package.js +88 -0
- package/dist/cursor-session-agent.js +18 -14
- package/dist/cursor-skill-tool.js +12 -22
- package/dist/cursor-state.js +5 -8
- package/dist/cursor-usage-accounting.js +1 -1
- package/docs/cursor-dogfood-checklist.md +4 -4
- package/docs/cursor-live-smoke-checklist.md +23 -23
- package/docs/cursor-model-ux-spec.md +27 -30
- package/docs/cursor-native-tool-replay.md +4 -4
- package/docs/cursor-native-tool-visual-audit.md +7 -7
- package/docs/cursor-testing-lessons.md +23 -16
- package/docs/cursor-tool-surfaces.md +2 -4
- package/docs/platform-smoke-implementation.md +5 -5
- package/docs/platform-smoke.md +27 -72
- package/package.json +3 -17
- package/platform-smoke.config.mjs +2 -1
- package/scripts/lib/local-resume-smoke-harness.mjs +0 -18
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/local-resume-runner.mjs +1 -1
- package/scripts/platform-smoke/scenarios.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +14 -5
- package/src/cursor-agents-context-registration.ts +0 -5
- package/src/cursor-agents-context.ts +1 -3
- package/src/cursor-config.ts +8 -379
- package/src/cursor-provider-errors.ts +2 -62
- package/src/cursor-provider-live-run-drain.ts +0 -3
- package/src/cursor-provider-run-finalizer.ts +4 -12
- package/src/cursor-provider-run-outcome.ts +0 -3
- package/src/cursor-provider-turn-finalize.ts +3 -61
- package/src/cursor-provider-turn-prepare.ts +8 -165
- package/src/cursor-provider-turn-runner.ts +15 -31
- package/src/cursor-provider-turn-send.ts +7 -38
- package/src/cursor-provider-turn-types.ts +9 -30
- package/src/cursor-runtime-state.ts +6 -345
- package/src/cursor-sdk-billed-usage.ts +3 -24
- package/src/cursor-sdk-platform-package.ts +106 -0
- package/src/cursor-session-agent.ts +16 -12
- package/src/cursor-skill-tool.ts +10 -26
- package/src/cursor-state.ts +5 -10
- package/src/cursor-usage-accounting.ts +1 -3
- package/dist/cursor-cloud-lifecycle.js +0 -650
- package/dist/cursor-cloud-local-state.js +0 -460
- package/dist/cursor-cloud-options.js +0 -145
- package/dist/cursor-cloud-reporting.js +0 -222
- package/dist/cursor-ripgrep-path.js +0 -27
- package/scripts/cloud-runtime-smoke.d.mts +0 -42
- package/scripts/cloud-runtime-smoke.mjs +0 -623
- package/scripts/lib/cloud-smoke-artifacts.d.mts +0 -16
- package/scripts/lib/cloud-smoke-artifacts.mjs +0 -94
- package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +0 -209
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +0 -585
- package/scripts/lib/cloud-smoke-github.d.mts +0 -78
- package/scripts/lib/cloud-smoke-github.mjs +0 -331
- package/scripts/lib/cloud-smoke-pi-runner.d.mts +0 -42
- package/scripts/lib/cloud-smoke-pi-runner.mjs +0 -214
- package/scripts/lib/cloud-smoke-shutdown.d.mts +0 -67
- package/scripts/lib/cloud-smoke-shutdown.mjs +0 -133
- package/shared/cursor-cloud-lifecycle-constants.d.mts +0 -3
- package/shared/cursor-cloud-lifecycle-constants.mjs +0 -7
- package/src/cursor-cloud-lifecycle.ts +0 -733
- package/src/cursor-cloud-local-state.ts +0 -536
- package/src/cursor-cloud-options.ts +0 -182
- package/src/cursor-cloud-reporting.ts +0 -267
- package/src/cursor-ripgrep-path.ts +0 -32
|
@@ -1,460 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { devNull } from "node:os";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
function isValidCursorCloudBranchName(branch) {
|
|
6
|
-
if (!branch || branch === "HEAD")
|
|
7
|
-
return false;
|
|
8
|
-
if (branch.startsWith("-") || branch.endsWith(".") || branch.endsWith("/") || branch.includes("..") || branch.includes("@{"))
|
|
9
|
-
return false;
|
|
10
|
-
if (/[\x00-\x20\x7f~^:?*\[\\]/.test(branch))
|
|
11
|
-
return false;
|
|
12
|
-
const components = branch.split("/");
|
|
13
|
-
return components.every((component) => component.length > 0 && !component.startsWith(".") && !component.endsWith(".lock"));
|
|
14
|
-
}
|
|
15
|
-
export function normalizeCursorCloudStartingRef(value) {
|
|
16
|
-
const ref = value?.trim();
|
|
17
|
-
if (!ref)
|
|
18
|
-
return { kind: "absent" };
|
|
19
|
-
if (ref.startsWith("refs/heads/")) {
|
|
20
|
-
const branch = ref.slice("refs/heads/".length);
|
|
21
|
-
return isValidCursorCloudBranchName(branch) ? { kind: "branch", value: branch } : { kind: "unsupported" };
|
|
22
|
-
}
|
|
23
|
-
if (ref.startsWith("refs/"))
|
|
24
|
-
return { kind: "unsupported" };
|
|
25
|
-
if (/^[0-9a-fA-F]{40}$/.test(ref))
|
|
26
|
-
return { kind: "commit", value: ref };
|
|
27
|
-
return isValidCursorCloudBranchName(ref) ? { kind: "branch", value: ref } : { kind: "unsupported" };
|
|
28
|
-
}
|
|
29
|
-
function buildCursorCloudGitEnvironment(source, isolateConfig) {
|
|
30
|
-
const env = {};
|
|
31
|
-
for (const [name, value] of Object.entries(source)) {
|
|
32
|
-
if (name.toUpperCase().startsWith("GIT_"))
|
|
33
|
-
continue;
|
|
34
|
-
env[name] = value;
|
|
35
|
-
}
|
|
36
|
-
if (isolateConfig) {
|
|
37
|
-
const configNullDevice = process.platform === "win32" ? "NUL" : devNull;
|
|
38
|
-
env.GIT_CONFIG_GLOBAL = configNullDevice;
|
|
39
|
-
env.GIT_CONFIG_SYSTEM = configNullDevice;
|
|
40
|
-
env.GIT_CONFIG_NOSYSTEM = "1";
|
|
41
|
-
}
|
|
42
|
-
env.GIT_NO_REPLACE_OBJECTS = "1";
|
|
43
|
-
return env;
|
|
44
|
-
}
|
|
45
|
-
export function sanitizeCursorCloudGitEnvironment(source = process.env) {
|
|
46
|
-
return buildCursorCloudGitEnvironment(source, true);
|
|
47
|
-
}
|
|
48
|
-
function runGitWithEnvironment(cwd, args, env) {
|
|
49
|
-
try {
|
|
50
|
-
const output = execFileSync("git", args, {
|
|
51
|
-
cwd,
|
|
52
|
-
env,
|
|
53
|
-
encoding: "utf8",
|
|
54
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
55
|
-
timeout: 30_000,
|
|
56
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
57
|
-
});
|
|
58
|
-
return output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
export function runCursorCloudGit(cwd, args) {
|
|
65
|
-
return runGitWithEnvironment(cwd, args, sanitizeCursorCloudGitEnvironment());
|
|
66
|
-
}
|
|
67
|
-
function runCursorCloudGitWithUserConfig(cwd, args) {
|
|
68
|
-
return runGitWithEnvironment(cwd, args, buildCursorCloudGitEnvironment(process.env, false));
|
|
69
|
-
}
|
|
70
|
-
function hasPlausibleGitMetadata(cwd) {
|
|
71
|
-
let current = cwd;
|
|
72
|
-
while (true) {
|
|
73
|
-
if (existsSync(join(current, ".git")))
|
|
74
|
-
return true;
|
|
75
|
-
if (["HEAD", "config", "objects", "refs"].every((name) => existsSync(join(current, name))))
|
|
76
|
-
return true;
|
|
77
|
-
const parent = dirname(current);
|
|
78
|
-
if (parent === current)
|
|
79
|
-
return false;
|
|
80
|
-
current = parent;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const HTTPS_REPOSITORY_PROTOCOLS = new Set(["https:"]);
|
|
84
|
-
const LOCAL_REPOSITORY_PROTOCOLS = new Set(["https:", "ssh:"]);
|
|
85
|
-
function parseRepositoryTransportUrl(value, protocols) {
|
|
86
|
-
if (value !== value.trim() || /[\s\\\x00-\x1f\x7f]/.test(value))
|
|
87
|
-
return undefined;
|
|
88
|
-
const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(value);
|
|
89
|
-
if (!scheme?.[1])
|
|
90
|
-
return undefined;
|
|
91
|
-
const protocol = `${scheme[1].toLowerCase()}:`;
|
|
92
|
-
if (!protocols.has(protocol))
|
|
93
|
-
return undefined;
|
|
94
|
-
const authorityAndPath = value.slice(scheme[0].length);
|
|
95
|
-
const pathSeparator = authorityAndPath.indexOf("/");
|
|
96
|
-
if (pathSeparator <= 0)
|
|
97
|
-
return undefined;
|
|
98
|
-
const authority = authorityAndPath.slice(0, pathSeparator);
|
|
99
|
-
const rawPath = authorityAndPath.slice(pathSeparator);
|
|
100
|
-
if (protocol === "https:" && authority.includes("@"))
|
|
101
|
-
return undefined;
|
|
102
|
-
if (protocol === "ssh:" && authority.includes("@")) {
|
|
103
|
-
const userinfo = authority.slice(0, authority.lastIndexOf("@"));
|
|
104
|
-
if (!userinfo || userinfo.includes(":"))
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
if (!rawPath.replace(/\/+$/, ""))
|
|
108
|
-
return undefined;
|
|
109
|
-
for (const part of rawPath.split("/")) {
|
|
110
|
-
let decoded;
|
|
111
|
-
try {
|
|
112
|
-
decoded = decodeURIComponent(part);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
if (decoded === "." || decoded === ".." || /[\/\\\s\x00-\x1f\x7f]/.test(decoded))
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
const url = new URL(value);
|
|
122
|
-
return url.search || url.hash || url.password || !url.hostname || url.pathname === "/" ? undefined : url;
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
export function parseCursorCloudRepositoryUrl(value) {
|
|
129
|
-
if (!value || value !== value.trim())
|
|
130
|
-
return undefined;
|
|
131
|
-
const url = parseRepositoryTransportUrl(value, HTTPS_REPOSITORY_PROTOCOLS);
|
|
132
|
-
return url && !url.username ? value : undefined;
|
|
133
|
-
}
|
|
134
|
-
function normalizeRepositoryIdentity(value) {
|
|
135
|
-
if (value !== value.trim() || /[\s\\\x00-\x1f\x7f]/.test(value))
|
|
136
|
-
return undefined;
|
|
137
|
-
const url = parseRepositoryTransportUrl(value, LOCAL_REPOSITORY_PROTOCOLS);
|
|
138
|
-
let transport;
|
|
139
|
-
let host;
|
|
140
|
-
let pathname;
|
|
141
|
-
if (url) {
|
|
142
|
-
transport = url.protocol;
|
|
143
|
-
host = url.host.toLowerCase();
|
|
144
|
-
pathname = url.pathname;
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
if (value.includes("://") || /^[A-Za-z]:/.test(value))
|
|
148
|
-
return undefined;
|
|
149
|
-
const match = /^(?:[^@/:]+@)?([^@/:]+):([^?#]+)$/.exec(value);
|
|
150
|
-
if (!match?.[1] || !match[2])
|
|
151
|
-
return undefined;
|
|
152
|
-
transport = "scp:";
|
|
153
|
-
host = match[1].toLowerCase();
|
|
154
|
-
pathname = `/${match[2]}`;
|
|
155
|
-
}
|
|
156
|
-
pathname = pathname.replace(/\/+$/, "");
|
|
157
|
-
return host === "github.com"
|
|
158
|
-
? `${host}${pathname.replace(/\.git$/, "").toLowerCase()}`
|
|
159
|
-
: `${transport}//${host}${pathname}`;
|
|
160
|
-
}
|
|
161
|
-
function verifiedGitOutput(cwd, args, runGit, verifyGit) {
|
|
162
|
-
const output = runGit(cwd, args);
|
|
163
|
-
if (output === undefined || verifyGit === runGit)
|
|
164
|
-
return output;
|
|
165
|
-
return verifyGit(cwd, args) === output ? output : undefined;
|
|
166
|
-
}
|
|
167
|
-
function parseNullGitConfigRecords(output) {
|
|
168
|
-
const records = output.split("\0");
|
|
169
|
-
if (records.pop() !== "" || records.some((record) => !record))
|
|
170
|
-
return undefined;
|
|
171
|
-
const parsed = [];
|
|
172
|
-
for (const raw of records) {
|
|
173
|
-
const separator = raw.indexOf("\n");
|
|
174
|
-
if (separator <= 0)
|
|
175
|
-
return undefined;
|
|
176
|
-
parsed.push({ key: raw.slice(0, separator), value: raw.slice(separator + 1), raw });
|
|
177
|
-
}
|
|
178
|
-
return parsed;
|
|
179
|
-
}
|
|
180
|
-
function readConfiguredRemoteUrls(cwd, remote, runGit, verifyGit) {
|
|
181
|
-
const escapedRemote = remote.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
182
|
-
const output = verifiedGitOutput(cwd, ["config", "--null", "--get-regexp", `^remote\\.${escapedRemote}\\.(url|pushurl)$`], runGit, verifyGit);
|
|
183
|
-
if (output === undefined)
|
|
184
|
-
return undefined;
|
|
185
|
-
const records = parseNullGitConfigRecords(output);
|
|
186
|
-
if (!records?.length)
|
|
187
|
-
return undefined;
|
|
188
|
-
const fetch = records.filter(({ key }) => key.toLowerCase().endsWith(".url")).map(({ value }) => value);
|
|
189
|
-
const push = records.filter(({ key }) => key.toLowerCase().endsWith(".pushurl")).map(({ value }) => value);
|
|
190
|
-
return fetch.length > 0 && [...fetch, ...push].every((url) => normalizeRepositoryIdentity(url))
|
|
191
|
-
? { fetch, push }
|
|
192
|
-
: null;
|
|
193
|
-
}
|
|
194
|
-
function readUrlRewriteRules(cwd, runGit) {
|
|
195
|
-
const output = runGit(cwd, ["config", "--null", "--list"]);
|
|
196
|
-
if (output === undefined)
|
|
197
|
-
return undefined;
|
|
198
|
-
const records = parseNullGitConfigRecords(output);
|
|
199
|
-
if (!records)
|
|
200
|
-
return undefined;
|
|
201
|
-
const rules = [];
|
|
202
|
-
for (const { key, value, raw } of records) {
|
|
203
|
-
const normalizedKey = key.toLowerCase();
|
|
204
|
-
if (!normalizedKey.startsWith("url."))
|
|
205
|
-
continue;
|
|
206
|
-
if (normalizedKey.endsWith(".insteadof"))
|
|
207
|
-
rules.push({ kind: "fetch", prefix: value, raw });
|
|
208
|
-
else if (normalizedKey.endsWith(".pushinsteadof"))
|
|
209
|
-
rules.push({ kind: "push", prefix: value, raw });
|
|
210
|
-
}
|
|
211
|
-
return rules;
|
|
212
|
-
}
|
|
213
|
-
function selectedUrlRewriteRules(rules, kind, url) {
|
|
214
|
-
const matches = rules.filter((rule) => rule.kind === kind && url.startsWith(rule.prefix));
|
|
215
|
-
const longest = matches.reduce((length, rule) => Math.max(length, rule.prefix.length), 0);
|
|
216
|
-
return matches.filter((rule) => rule.prefix.length === longest).map(({ raw }) => raw).sort();
|
|
217
|
-
}
|
|
218
|
-
function urlRewriteSignature(rules, urls) {
|
|
219
|
-
const selected = [];
|
|
220
|
-
for (const url of urls.fetch)
|
|
221
|
-
selected.push("fetch", ...selectedUrlRewriteRules(rules, "fetch", url));
|
|
222
|
-
if (urls.push.length > 0) {
|
|
223
|
-
for (const url of urls.push)
|
|
224
|
-
selected.push("push", ...selectedUrlRewriteRules(rules, "fetch", url));
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
for (const url of urls.fetch) {
|
|
228
|
-
const pushRules = selectedUrlRewriteRules(rules, "push", url);
|
|
229
|
-
selected.push("push", ...(pushRules.length > 0 ? pushRules : selectedUrlRewriteRules(rules, "fetch", url)));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return selected.join("\0");
|
|
233
|
-
}
|
|
234
|
-
function urlRewriteConfigAgrees(cwd, urls, runGit, verifyGit) {
|
|
235
|
-
if (verifyGit === runGit)
|
|
236
|
-
return true;
|
|
237
|
-
const primary = readUrlRewriteRules(cwd, runGit);
|
|
238
|
-
const ordinary = readUrlRewriteRules(cwd, verifyGit);
|
|
239
|
-
return primary !== undefined && ordinary !== undefined
|
|
240
|
-
&& urlRewriteSignature(primary, urls) === urlRewriteSignature(ordinary, urls);
|
|
241
|
-
}
|
|
242
|
-
function listRemotes(cwd, runGit, verifyGit) {
|
|
243
|
-
const output = verifiedGitOutput(cwd, ["remote"], runGit, verifyGit);
|
|
244
|
-
return output === undefined ? undefined : output.split("\n").filter(Boolean);
|
|
245
|
-
}
|
|
246
|
-
function resolveRemoteRepository(cwd, remote, runGit, verifyGit) {
|
|
247
|
-
const configuredUrls = readConfiguredRemoteUrls(cwd, remote, runGit, verifyGit);
|
|
248
|
-
if (configuredUrls === undefined)
|
|
249
|
-
return undefined;
|
|
250
|
-
if (configuredUrls === null)
|
|
251
|
-
return null;
|
|
252
|
-
if (!urlRewriteConfigAgrees(cwd, configuredUrls, runGit, verifyGit))
|
|
253
|
-
return undefined;
|
|
254
|
-
const fetchUrls = verifiedGitOutput(cwd, ["remote", "get-url", "--all", remote], runGit, verifyGit);
|
|
255
|
-
const pushUrls = verifiedGitOutput(cwd, ["remote", "get-url", "--push", "--all", remote], runGit, verifyGit);
|
|
256
|
-
if (fetchUrls === undefined || pushUrls === undefined)
|
|
257
|
-
return undefined;
|
|
258
|
-
const records = `${fetchUrls}\n${pushUrls}`.split("\n");
|
|
259
|
-
if (records.some((record) => !record))
|
|
260
|
-
return null;
|
|
261
|
-
const urls = records.map(normalizeRepositoryIdentity);
|
|
262
|
-
const repository = urls[0];
|
|
263
|
-
return repository && urls.every((url) => url === repository) ? repository : null;
|
|
264
|
-
}
|
|
265
|
-
function findMatchingRemote(cwd, repo, runGit, verifyGit) {
|
|
266
|
-
const target = normalizeRepositoryIdentity(repo);
|
|
267
|
-
if (!target)
|
|
268
|
-
return { kind: "unverified" };
|
|
269
|
-
const remotes = listRemotes(cwd, runGit, verifyGit);
|
|
270
|
-
if (!remotes)
|
|
271
|
-
return { kind: "failed" };
|
|
272
|
-
const matches = [];
|
|
273
|
-
for (const remote of remotes) {
|
|
274
|
-
const repository = resolveRemoteRepository(cwd, remote, runGit, verifyGit);
|
|
275
|
-
if (repository === undefined)
|
|
276
|
-
return { kind: "failed" };
|
|
277
|
-
if (repository === target)
|
|
278
|
-
matches.push(remote);
|
|
279
|
-
}
|
|
280
|
-
return matches.length === 1 ? { kind: "found", remote: matches[0] } : { kind: "unverified" };
|
|
281
|
-
}
|
|
282
|
-
function matchRefPattern(pattern, ref) {
|
|
283
|
-
const wildcard = pattern.indexOf("*");
|
|
284
|
-
if (wildcard === -1)
|
|
285
|
-
return pattern === ref ? "" : undefined;
|
|
286
|
-
if (pattern.indexOf("*", wildcard + 1) !== -1)
|
|
287
|
-
return undefined;
|
|
288
|
-
const prefix = pattern.slice(0, wildcard);
|
|
289
|
-
const suffix = pattern.slice(wildcard + 1);
|
|
290
|
-
if (ref.length < prefix.length + suffix.length || !ref.startsWith(prefix) || !ref.endsWith(suffix))
|
|
291
|
-
return undefined;
|
|
292
|
-
return ref.slice(prefix.length, ref.length - suffix.length);
|
|
293
|
-
}
|
|
294
|
-
function isValidFetchRefspecShape(refspec) {
|
|
295
|
-
if (refspec.startsWith("^")) {
|
|
296
|
-
const source = refspec.slice(1);
|
|
297
|
-
return source.startsWith("refs/") && !source.includes(":") && (source.match(/\*/g)?.length ?? 0) <= 1;
|
|
298
|
-
}
|
|
299
|
-
const positive = refspec.startsWith("+") ? refspec.slice(1) : refspec;
|
|
300
|
-
if (positive.startsWith("^"))
|
|
301
|
-
return false;
|
|
302
|
-
const separator = positive.indexOf(":");
|
|
303
|
-
const source = separator === -1 ? positive : positive.slice(0, separator);
|
|
304
|
-
const destination = separator === -1 ? undefined : positive.slice(separator + 1);
|
|
305
|
-
if (!source ||
|
|
306
|
-
(separator !== -1 && positive.indexOf(":", separator + 1) !== -1) ||
|
|
307
|
-
(!source.startsWith("refs/") && !/^[0-9a-fA-F]{40}$/.test(source)) ||
|
|
308
|
-
(destination !== undefined && destination !== "" && !destination.startsWith("refs/")))
|
|
309
|
-
return false;
|
|
310
|
-
const sourceWildcards = source.match(/\*/g)?.length ?? 0;
|
|
311
|
-
const destinationWildcards = destination?.match(/\*/g)?.length ?? 0;
|
|
312
|
-
return sourceWildcards <= 1 && sourceWildcards === destinationWildcards;
|
|
313
|
-
}
|
|
314
|
-
function parseFetchRefspecs(cwd, runGit, verifyGit) {
|
|
315
|
-
const output = verifiedGitOutput(cwd, ["config", "--get-regexp", "^remote\\..*\\.fetch$"], runGit, verifyGit);
|
|
316
|
-
if (output === undefined)
|
|
317
|
-
return undefined;
|
|
318
|
-
const remotes = new Map();
|
|
319
|
-
for (const line of output.split("\n")) {
|
|
320
|
-
const match = /^remote\.(.+)\.fetch\s+(.+)$/.exec(line);
|
|
321
|
-
if (!match?.[1] || !match[2] || !isValidFetchRefspecShape(match[2]))
|
|
322
|
-
return undefined;
|
|
323
|
-
remotes.set(match[1], [...(remotes.get(match[1]) ?? []), match[2]]);
|
|
324
|
-
}
|
|
325
|
-
return remotes;
|
|
326
|
-
}
|
|
327
|
-
function sourceForDestination(refspec, destination) {
|
|
328
|
-
const positive = refspec.startsWith("+") ? refspec.slice(1) : refspec;
|
|
329
|
-
if (positive.startsWith("^"))
|
|
330
|
-
return undefined;
|
|
331
|
-
const separator = positive.indexOf(":");
|
|
332
|
-
if (separator === -1)
|
|
333
|
-
return undefined;
|
|
334
|
-
const sourcePattern = positive.slice(0, separator);
|
|
335
|
-
const matched = matchRefPattern(positive.slice(separator + 1), destination);
|
|
336
|
-
if (matched === undefined)
|
|
337
|
-
return undefined;
|
|
338
|
-
return sourcePattern.includes("*") ? sourcePattern.replace("*", matched) : matched === "" ? sourcePattern : undefined;
|
|
339
|
-
}
|
|
340
|
-
function remoteTracksBranch(cwd, remote, branch, runGit, verifyGit) {
|
|
341
|
-
const allRefspecs = parseFetchRefspecs(cwd, runGit, verifyGit);
|
|
342
|
-
if (!allRefspecs)
|
|
343
|
-
return false;
|
|
344
|
-
const source = `refs/heads/${branch}`;
|
|
345
|
-
const destination = `refs/remotes/${remote}/${branch}`;
|
|
346
|
-
const selectedRefspecs = allRefspecs.get(remote) ?? [];
|
|
347
|
-
if (selectedRefspecs.some((refspec) => refspec.startsWith("^") && matchRefPattern(refspec.slice(1), source) !== undefined))
|
|
348
|
-
return false;
|
|
349
|
-
const writers = new Set();
|
|
350
|
-
for (const [writerRemote, refspecs] of allRefspecs) {
|
|
351
|
-
for (const refspec of refspecs) {
|
|
352
|
-
const writerSource = sourceForDestination(refspec, destination);
|
|
353
|
-
if (writerSource)
|
|
354
|
-
writers.add(`${writerRemote}\0${writerSource}`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return writers.size === 1 && writers.has(`${remote}\0${source}`);
|
|
358
|
-
}
|
|
359
|
-
function resolveRemoteTrackingRef(cwd, remote, branch, runGit, verifyGit) {
|
|
360
|
-
if (!remoteTracksBranch(cwd, remote, branch, runGit, verifyGit))
|
|
361
|
-
return { kind: "unverified" };
|
|
362
|
-
const ref = `refs/remotes/${remote}/${branch}`;
|
|
363
|
-
return runGit(cwd, ["for-each-ref", "--format=%(objecttype)%09%(symref)", ref]) === "commit\t"
|
|
364
|
-
? { kind: "found", ref }
|
|
365
|
-
: { kind: "unverified" };
|
|
366
|
-
}
|
|
367
|
-
function hasHistoryOverrides(cwd, runGit) {
|
|
368
|
-
const replacements = runGit(cwd, ["for-each-ref", "--format=%(refname)", "refs/replace"]);
|
|
369
|
-
const graftsPath = runGit(cwd, ["rev-parse", "--git-path", "info/grafts"]);
|
|
370
|
-
if (replacements === undefined || graftsPath === undefined)
|
|
371
|
-
return undefined;
|
|
372
|
-
return replacements.length > 0 || existsSync(resolve(cwd, graftsPath));
|
|
373
|
-
}
|
|
374
|
-
function resolveComparisonRef(cwd, target, runGit, verifyGit) {
|
|
375
|
-
if (!target.repo) {
|
|
376
|
-
const upstream = verifiedGitOutput(cwd, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], runGit, verifyGit);
|
|
377
|
-
if (!upstream)
|
|
378
|
-
return { kind: "unverified" };
|
|
379
|
-
const remotes = listRemotes(cwd, runGit, verifyGit);
|
|
380
|
-
if (!remotes)
|
|
381
|
-
return { kind: "failed" };
|
|
382
|
-
const matches = remotes.filter((remote) => upstream.startsWith(`${remote}/`));
|
|
383
|
-
if (matches.length !== 1)
|
|
384
|
-
return { kind: "unverified" };
|
|
385
|
-
const remote = matches[0];
|
|
386
|
-
const repository = resolveRemoteRepository(cwd, remote, runGit, verifyGit);
|
|
387
|
-
if (repository === undefined)
|
|
388
|
-
return { kind: "failed" };
|
|
389
|
-
if (!repository)
|
|
390
|
-
return { kind: "unverified" };
|
|
391
|
-
return resolveRemoteTrackingRef(cwd, remote, upstream.slice(remote.length + 1), runGit, verifyGit);
|
|
392
|
-
}
|
|
393
|
-
const ref = normalizeCursorCloudStartingRef(target.branch);
|
|
394
|
-
if (ref.kind !== "branch")
|
|
395
|
-
return { kind: "unverified" };
|
|
396
|
-
const remote = findMatchingRemote(cwd, target.repo, runGit, verifyGit);
|
|
397
|
-
return remote.kind === "found"
|
|
398
|
-
? resolveRemoteTrackingRef(cwd, remote.remote, ref.value, runGit, verifyGit)
|
|
399
|
-
: remote;
|
|
400
|
-
}
|
|
401
|
-
export function inspectCursorCloudLocalState(cwd, target = {}, runGit = runCursorCloudGit) {
|
|
402
|
-
const verifyGit = runGit === runCursorCloudGit ? runCursorCloudGitWithUserConfig : runGit;
|
|
403
|
-
const insideWorkTree = runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
404
|
-
if (insideWorkTree !== "true") {
|
|
405
|
-
const bareRepository = runGit(cwd, ["rev-parse", "--is-bare-repository"]);
|
|
406
|
-
if (bareRepository === "true") {
|
|
407
|
-
return { insideGitRepo: true, dirty: "unknown", comparison: "unknown", reasons: [{ code: "bare_repo" }] };
|
|
408
|
-
}
|
|
409
|
-
const gitAlive = runGit(cwd, ["--version"]) !== undefined;
|
|
410
|
-
if (gitAlive && !hasPlausibleGitMetadata(cwd))
|
|
411
|
-
return { insideGitRepo: false };
|
|
412
|
-
return {
|
|
413
|
-
insideGitRepo: "unknown",
|
|
414
|
-
dirty: "unknown",
|
|
415
|
-
comparison: "unknown",
|
|
416
|
-
reasons: [{ code: "repository_detection_failed" }],
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
const status = runGit(cwd, [
|
|
420
|
-
...(process.platform === "win32" ? [] : ["-c", "core.fileMode=true"]),
|
|
421
|
-
"-c", "core.fsmonitor=false", "--no-optional-locks", "status", "--porcelain=v1", "--untracked-files=normal", "--ignore-submodules=none",
|
|
422
|
-
]);
|
|
423
|
-
const indexFlags = runGit(cwd, ["ls-files", "-v", "--", ":/"]);
|
|
424
|
-
const dirty = status === undefined ? "unknown" : status.length > 0;
|
|
425
|
-
const reasons = [];
|
|
426
|
-
if (status === undefined)
|
|
427
|
-
reasons.push({ code: "status_failed" });
|
|
428
|
-
if (indexFlags === undefined)
|
|
429
|
-
reasons.push({ code: "index_failed" });
|
|
430
|
-
else if (indexFlags.split("\n").some((line) => /^[a-zS] /.test(line)))
|
|
431
|
-
reasons.push({ code: "hidden_index_state" });
|
|
432
|
-
const historyOverrides = hasHistoryOverrides(cwd, runGit);
|
|
433
|
-
if (historyOverrides === undefined)
|
|
434
|
-
reasons.push({ code: "history_probe_failed" });
|
|
435
|
-
else if (historyOverrides)
|
|
436
|
-
reasons.push({ code: "history_overrides" });
|
|
437
|
-
const hasHead = runGit(cwd, ["rev-parse", "--verify", "HEAD"]) !== undefined;
|
|
438
|
-
if (!hasHead)
|
|
439
|
-
reasons.push({ code: "head_unavailable" });
|
|
440
|
-
const comparisonRef = resolveComparisonRef(cwd, target, runGit, verifyGit);
|
|
441
|
-
if (comparisonRef.kind === "failed")
|
|
442
|
-
reasons.push({ code: "target_probe_failed" });
|
|
443
|
-
else if (comparisonRef.kind === "unverified")
|
|
444
|
-
reasons.push({ code: "unverified_target" });
|
|
445
|
-
const ahead = hasHead && comparisonRef.kind === "found" ? runGit(cwd, ["rev-list", "--count", `${comparisonRef.ref}..HEAD`]) : undefined;
|
|
446
|
-
const aheadCount = ahead !== undefined && /^\d+$/.test(ahead) ? Number(ahead) : Number.NaN;
|
|
447
|
-
if (hasHead && comparisonRef.kind === "found" && !Number.isFinite(aheadCount))
|
|
448
|
-
reasons.push({ code: "comparison_failed" });
|
|
449
|
-
const [firstReason, ...remainingReasons] = reasons;
|
|
450
|
-
if (firstReason) {
|
|
451
|
-
return {
|
|
452
|
-
insideGitRepo: true,
|
|
453
|
-
dirty,
|
|
454
|
-
comparison: "unknown",
|
|
455
|
-
reasons: [firstReason, ...remainingReasons],
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
return { insideGitRepo: true, dirty: dirty, comparison: aheadCount > 0 ? "unpushed" : "contains_head" };
|
|
459
|
-
}
|
|
460
|
-
export const __testUtils = { sourceForDestination };
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { isCursorCloudEnvironmentType } from "./cursor-config.js";
|
|
2
|
-
import { normalizeCursorCloudStartingRef, parseCursorCloudRepositoryUrl, } from "./cursor-cloud-local-state.js";
|
|
3
|
-
const CLOUD_REPO_URL_MESSAGE = "Cursor cloud repository must be an HTTPS repository URL without embedded credentials, query parameters, or fragments.";
|
|
4
|
-
const CLOUD_STARTING_REF_MESSAGE = "Cursor cloud branch/ref must be a valid Git branch name, refs/heads/<branch>, or full commit SHA; invalid branch names and other refs/* are unsupported.";
|
|
5
|
-
const CLOUD_LOCAL_STATE_REASON_LABELS = {
|
|
6
|
-
bare_repo: "the repository is bare",
|
|
7
|
-
repository_detection_failed: "Git could not determine whether the working directory is a repository",
|
|
8
|
-
status_failed: "Git could not determine whether the worktree or index is clean",
|
|
9
|
-
index_failed: "Git index inspection failed",
|
|
10
|
-
hidden_index_state: "the index hides worktree changes",
|
|
11
|
-
history_probe_failed: "Git could not inspect replacement or graft history overrides",
|
|
12
|
-
history_overrides: "replacement refs or graft metadata are present",
|
|
13
|
-
head_unavailable: "local HEAD is unavailable",
|
|
14
|
-
unverified_target: "the cloud target has no locally verified tracking ref",
|
|
15
|
-
target_probe_failed: "Git failed while resolving the cloud target",
|
|
16
|
-
comparison_failed: "the local HEAD comparison failed",
|
|
17
|
-
};
|
|
18
|
-
export function buildCursorCloudAgentOptions(options) {
|
|
19
|
-
const { resolvedConfig } = options;
|
|
20
|
-
const environment = resolvedConfig.cloud.environment.value;
|
|
21
|
-
const environmentType = environment?.type;
|
|
22
|
-
const environmentName = environment?.name;
|
|
23
|
-
const configuredRepo = resolvedConfig.cloud.repo.value;
|
|
24
|
-
const repo = parseCursorCloudRepositoryUrl(configuredRepo);
|
|
25
|
-
if (configuredRepo && !repo)
|
|
26
|
-
throw new Error(CLOUD_REPO_URL_MESSAGE);
|
|
27
|
-
const startingRef = normalizeCursorCloudStartingRef(resolvedConfig.cloud.branch.value);
|
|
28
|
-
if (startingRef.kind === "unsupported")
|
|
29
|
-
throw new Error(CLOUD_STARTING_REF_MESSAGE);
|
|
30
|
-
const cloud = {
|
|
31
|
-
...(isCursorCloudEnvironmentType(environmentType)
|
|
32
|
-
? {
|
|
33
|
-
env: {
|
|
34
|
-
type: environmentType,
|
|
35
|
-
...(environmentName ? { name: environmentName } : {}),
|
|
36
|
-
},
|
|
37
|
-
}
|
|
38
|
-
: {}),
|
|
39
|
-
...(repo
|
|
40
|
-
? {
|
|
41
|
-
repos: [
|
|
42
|
-
{
|
|
43
|
-
url: repo,
|
|
44
|
-
...(startingRef.kind === "branch" || startingRef.kind === "commit" ? { startingRef: startingRef.value } : {}),
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
}
|
|
48
|
-
: {}),
|
|
49
|
-
...(resolvedConfig.cloud.directPush.value ? { workOnCurrentBranch: true } : {}),
|
|
50
|
-
...(resolvedConfig.cloud.autoCreatePR.source !== "builtin"
|
|
51
|
-
? { autoCreatePR: resolvedConfig.cloud.autoCreatePR.value }
|
|
52
|
-
: {}),
|
|
53
|
-
...(resolvedConfig.cloud.skipReviewerRequest.source !== "builtin"
|
|
54
|
-
? { skipReviewerRequest: resolvedConfig.cloud.skipReviewerRequest.value }
|
|
55
|
-
: {}),
|
|
56
|
-
};
|
|
57
|
-
return {
|
|
58
|
-
apiKey: options.apiKey,
|
|
59
|
-
model: options.modelSelection,
|
|
60
|
-
mode: options.agentMode,
|
|
61
|
-
...(options.name ? { name: options.name } : {}),
|
|
62
|
-
cloud,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
export function preflightCursorCloudRuntime(options) {
|
|
66
|
-
const { resolvedConfig } = options;
|
|
67
|
-
const issues = [];
|
|
68
|
-
if (!resolvedConfig.cloud.acknowledged.value) {
|
|
69
|
-
issues.push({
|
|
70
|
-
code: "cloud_ack_required",
|
|
71
|
-
message: "Cursor cloud runtime requires first-use acknowledgement; run /cursor-runtime cloud in an interactive session or pass --cursor-cloud-ack / PI_CURSOR_CLOUD_ACK=1.",
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
if (options.hasPriorContext && resolvedConfig.cloud.contextHandoff.value === "never") {
|
|
75
|
-
issues.push({
|
|
76
|
-
code: "context_handoff_required",
|
|
77
|
-
message: "Cursor cloud runtime needs --cursor-cloud-context=fresh or --cursor-cloud-context=bootstrap for sessions with prior pi context.",
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
if (options.localState.insideGitRepo !== false &&
|
|
81
|
-
(options.localState.dirty !== false || options.localState.comparison !== "contains_head") &&
|
|
82
|
-
!resolvedConfig.cloud.allowLocalState.value) {
|
|
83
|
-
const causes = [];
|
|
84
|
-
if (options.localState.dirty === true)
|
|
85
|
-
causes.push("the worktree or index is dirty");
|
|
86
|
-
if (options.localState.dirty === "unknown")
|
|
87
|
-
causes.push("Git could not determine whether the worktree or index is clean");
|
|
88
|
-
if (options.localState.comparison === "unpushed")
|
|
89
|
-
causes.push("local HEAD contains unpushed commits");
|
|
90
|
-
if (options.localState.comparison === "unknown") {
|
|
91
|
-
causes.push(...options.localState.reasons
|
|
92
|
-
.filter((reason) => reason.code !== "status_failed")
|
|
93
|
-
.map((reason) => CLOUD_LOCAL_STATE_REASON_LABELS[reason.code]));
|
|
94
|
-
}
|
|
95
|
-
issues.push({
|
|
96
|
-
code: "local_state_not_allowed",
|
|
97
|
-
message: `Cursor cloud runtime cannot safely omit local state because ${causes.join("; ")}. Configure an explicit repository branch/ref with current local tracking evidence, or pass --cursor-cloud-allow-local-state only after accepting that risk.`,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
if (resolvedConfig.cloud.envNames.value.length > 0 || resolvedConfig.cloud.envFromFiles.value) {
|
|
101
|
-
issues.push({
|
|
102
|
-
code: "env_forwarding_not_implemented",
|
|
103
|
-
message: "Cursor cloud env forwarding is not implemented; use Cursor-native environment setup such as .cursor/environment.json or dashboard-managed secrets.",
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
if (resolvedConfig.cloud.repo.value && !parseCursorCloudRepositoryUrl(resolvedConfig.cloud.repo.value)) {
|
|
107
|
-
issues.push({
|
|
108
|
-
code: "cloud_repo_invalid",
|
|
109
|
-
message: CLOUD_REPO_URL_MESSAGE,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
if (resolvedConfig.cloud.branch.value && !resolvedConfig.cloud.repo.value) {
|
|
113
|
-
issues.push({
|
|
114
|
-
code: "cloud_branch_repo_required",
|
|
115
|
-
message: "Cursor cloud branch/ref requires --cursor-cloud-repo because the installed Cursor SDK supports startingRef only on cloud.repos entries.",
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
if (normalizeCursorCloudStartingRef(resolvedConfig.cloud.branch.value).kind === "unsupported") {
|
|
119
|
-
issues.push({ code: "cloud_branch_invalid", message: CLOUD_STARTING_REF_MESSAGE });
|
|
120
|
-
}
|
|
121
|
-
const environment = resolvedConfig.cloud.environment.value;
|
|
122
|
-
if (environment?.type && !isCursorCloudEnvironmentType(environment.type)) {
|
|
123
|
-
issues.push({
|
|
124
|
-
code: "cloud_environment_type_invalid",
|
|
125
|
-
message: `Invalid Cursor cloud environment type "${environment.type}"; expected cloud, pool, or machine.`,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
if (environment?.name && !environment.type) {
|
|
129
|
-
issues.push({
|
|
130
|
-
code: "cloud_environment_type_required",
|
|
131
|
-
message: "Cursor cloud environment name requires --cursor-cloud-env-type=cloud|pool|machine or PI_CURSOR_CLOUD_ENV_TYPE.",
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
if (environment?.type === "cloud" && environment.name && resolvedConfig.cloud.repo.value) {
|
|
135
|
-
issues.push({
|
|
136
|
-
code: "cloud_environment_repo_conflict",
|
|
137
|
-
message: "Cursor cloud named environments cannot be combined with --cursor-cloud-repo; omit the repo or use a pool/machine environment.",
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
return { ok: issues.length === 0, issues };
|
|
141
|
-
}
|
|
142
|
-
export function formatCursorCloudPreflightError(result) {
|
|
143
|
-
const details = result.issues.map((issue) => `- ${issue.message}`).join("\n");
|
|
144
|
-
return `Cursor cloud runtime is not ready to start.\n${details}\nUse --cursor-runtime local or /cursor-runtime local to run with the local Cursor SDK agent.`;
|
|
145
|
-
}
|