@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,536 +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
|
-
|
|
6
|
-
export type CursorCloudLocalStateUnknownReason =
|
|
7
|
-
| { code: "bare_repo" }
|
|
8
|
-
| { code: "repository_detection_failed" }
|
|
9
|
-
| { code: "status_failed" }
|
|
10
|
-
| { code: "index_failed" }
|
|
11
|
-
| { code: "hidden_index_state" }
|
|
12
|
-
| { code: "history_probe_failed" }
|
|
13
|
-
| { code: "history_overrides" }
|
|
14
|
-
| { code: "head_unavailable" }
|
|
15
|
-
| { code: "unverified_target" }
|
|
16
|
-
| { code: "target_probe_failed" }
|
|
17
|
-
| { code: "comparison_failed" };
|
|
18
|
-
|
|
19
|
-
export type CursorCloudLocalStateUnknownReasons = [
|
|
20
|
-
CursorCloudLocalStateUnknownReason,
|
|
21
|
-
...CursorCloudLocalStateUnknownReason[],
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
export type CursorCloudLocalState =
|
|
25
|
-
| { insideGitRepo: false }
|
|
26
|
-
| { insideGitRepo: true; dirty: boolean; comparison: "contains_head" | "unpushed" }
|
|
27
|
-
| {
|
|
28
|
-
insideGitRepo: true | "unknown";
|
|
29
|
-
dirty: boolean | "unknown";
|
|
30
|
-
comparison: "unknown";
|
|
31
|
-
reasons: CursorCloudLocalStateUnknownReasons;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type CursorCloudStartingRef =
|
|
35
|
-
| { kind: "absent" }
|
|
36
|
-
| { kind: "branch" | "commit"; value: string }
|
|
37
|
-
| { kind: "unsupported" };
|
|
38
|
-
|
|
39
|
-
export type CursorCloudGitRunner = (cwd: string, args: string[]) => string | undefined;
|
|
40
|
-
|
|
41
|
-
type ComparisonRefResult =
|
|
42
|
-
| { kind: "found"; ref: string }
|
|
43
|
-
| { kind: "unverified" }
|
|
44
|
-
| { kind: "failed" };
|
|
45
|
-
|
|
46
|
-
type RemoteResult =
|
|
47
|
-
| { kind: "found"; remote: string }
|
|
48
|
-
| { kind: "unverified" }
|
|
49
|
-
| { kind: "failed" };
|
|
50
|
-
|
|
51
|
-
function isValidCursorCloudBranchName(branch: string): boolean {
|
|
52
|
-
if (!branch || branch === "HEAD") return false;
|
|
53
|
-
if (branch.startsWith("-") || branch.endsWith(".") || branch.endsWith("/") || branch.includes("..") || branch.includes("@{")) return false;
|
|
54
|
-
if (/[\x00-\x20\x7f~^:?*\[\\]/.test(branch)) return false;
|
|
55
|
-
const components = branch.split("/");
|
|
56
|
-
return components.every((component) => component.length > 0 && !component.startsWith(".") && !component.endsWith(".lock"));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function normalizeCursorCloudStartingRef(value: string | undefined): CursorCloudStartingRef {
|
|
60
|
-
const ref = value?.trim();
|
|
61
|
-
if (!ref) return { kind: "absent" };
|
|
62
|
-
if (ref.startsWith("refs/heads/")) {
|
|
63
|
-
const branch = ref.slice("refs/heads/".length);
|
|
64
|
-
return isValidCursorCloudBranchName(branch) ? { kind: "branch", value: branch } : { kind: "unsupported" };
|
|
65
|
-
}
|
|
66
|
-
if (ref.startsWith("refs/")) return { kind: "unsupported" };
|
|
67
|
-
if (/^[0-9a-fA-F]{40}$/.test(ref)) return { kind: "commit", value: ref };
|
|
68
|
-
return isValidCursorCloudBranchName(ref) ? { kind: "branch", value: ref } : { kind: "unsupported" };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function buildCursorCloudGitEnvironment(source: NodeJS.ProcessEnv, isolateConfig: boolean): NodeJS.ProcessEnv {
|
|
72
|
-
const env: NodeJS.ProcessEnv = {};
|
|
73
|
-
for (const [name, value] of Object.entries(source)) {
|
|
74
|
-
if (name.toUpperCase().startsWith("GIT_")) continue;
|
|
75
|
-
env[name] = value;
|
|
76
|
-
}
|
|
77
|
-
if (isolateConfig) {
|
|
78
|
-
const configNullDevice = process.platform === "win32" ? "NUL" : devNull;
|
|
79
|
-
env.GIT_CONFIG_GLOBAL = configNullDevice;
|
|
80
|
-
env.GIT_CONFIG_SYSTEM = configNullDevice;
|
|
81
|
-
env.GIT_CONFIG_NOSYSTEM = "1";
|
|
82
|
-
}
|
|
83
|
-
env.GIT_NO_REPLACE_OBJECTS = "1";
|
|
84
|
-
return env;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function sanitizeCursorCloudGitEnvironment(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
88
|
-
return buildCursorCloudGitEnvironment(source, true);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function runGitWithEnvironment(cwd: string, args: string[], env: NodeJS.ProcessEnv): string | undefined {
|
|
92
|
-
try {
|
|
93
|
-
const output = execFileSync("git", args, {
|
|
94
|
-
cwd,
|
|
95
|
-
env,
|
|
96
|
-
encoding: "utf8",
|
|
97
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
98
|
-
timeout: 30_000,
|
|
99
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
100
|
-
});
|
|
101
|
-
return output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
102
|
-
} catch {
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function runCursorCloudGit(cwd: string, args: string[]): string | undefined {
|
|
108
|
-
return runGitWithEnvironment(cwd, args, sanitizeCursorCloudGitEnvironment());
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function runCursorCloudGitWithUserConfig(cwd: string, args: string[]): string | undefined {
|
|
112
|
-
return runGitWithEnvironment(cwd, args, buildCursorCloudGitEnvironment(process.env, false));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function hasPlausibleGitMetadata(cwd: string): boolean {
|
|
116
|
-
let current = cwd;
|
|
117
|
-
while (true) {
|
|
118
|
-
if (existsSync(join(current, ".git"))) return true;
|
|
119
|
-
if (["HEAD", "config", "objects", "refs"].every((name) => existsSync(join(current, name)))) return true;
|
|
120
|
-
const parent = dirname(current);
|
|
121
|
-
if (parent === current) return false;
|
|
122
|
-
current = parent;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const HTTPS_REPOSITORY_PROTOCOLS = new Set(["https:"]);
|
|
127
|
-
const LOCAL_REPOSITORY_PROTOCOLS = new Set(["https:", "ssh:"]);
|
|
128
|
-
|
|
129
|
-
function parseRepositoryTransportUrl(value: string, protocols: ReadonlySet<string>): URL | undefined {
|
|
130
|
-
if (value !== value.trim() || /[\s\\\x00-\x1f\x7f]/.test(value)) return undefined;
|
|
131
|
-
const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(value);
|
|
132
|
-
if (!scheme?.[1]) return undefined;
|
|
133
|
-
const protocol = `${scheme[1].toLowerCase()}:`;
|
|
134
|
-
if (!protocols.has(protocol)) return undefined;
|
|
135
|
-
const authorityAndPath = value.slice(scheme[0].length);
|
|
136
|
-
const pathSeparator = authorityAndPath.indexOf("/");
|
|
137
|
-
if (pathSeparator <= 0) return undefined;
|
|
138
|
-
const authority = authorityAndPath.slice(0, pathSeparator);
|
|
139
|
-
const rawPath = authorityAndPath.slice(pathSeparator);
|
|
140
|
-
if (protocol === "https:" && authority.includes("@")) return undefined;
|
|
141
|
-
if (protocol === "ssh:" && authority.includes("@")) {
|
|
142
|
-
const userinfo = authority.slice(0, authority.lastIndexOf("@"));
|
|
143
|
-
if (!userinfo || userinfo.includes(":")) return undefined;
|
|
144
|
-
}
|
|
145
|
-
if (!rawPath.replace(/\/+$/, "")) return undefined;
|
|
146
|
-
for (const part of rawPath.split("/")) {
|
|
147
|
-
let decoded: string;
|
|
148
|
-
try {
|
|
149
|
-
decoded = decodeURIComponent(part);
|
|
150
|
-
} catch {
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
if (decoded === "." || decoded === ".." || /[\/\\\s\x00-\x1f\x7f]/.test(decoded)) return undefined;
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
const url = new URL(value);
|
|
157
|
-
return url.search || url.hash || url.password || !url.hostname || url.pathname === "/" ? undefined : url;
|
|
158
|
-
} catch {
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function parseCursorCloudRepositoryUrl(value: string | undefined): string | undefined {
|
|
164
|
-
if (!value || value !== value.trim()) return undefined;
|
|
165
|
-
const url = parseRepositoryTransportUrl(value, HTTPS_REPOSITORY_PROTOCOLS);
|
|
166
|
-
return url && !url.username ? value : undefined;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function normalizeRepositoryIdentity(value: string): string | undefined {
|
|
170
|
-
if (value !== value.trim() || /[\s\\\x00-\x1f\x7f]/.test(value)) return undefined;
|
|
171
|
-
const url = parseRepositoryTransportUrl(value, LOCAL_REPOSITORY_PROTOCOLS);
|
|
172
|
-
let transport: string;
|
|
173
|
-
let host: string;
|
|
174
|
-
let pathname: string;
|
|
175
|
-
if (url) {
|
|
176
|
-
transport = url.protocol;
|
|
177
|
-
host = url.host.toLowerCase();
|
|
178
|
-
pathname = url.pathname;
|
|
179
|
-
} else {
|
|
180
|
-
if (value.includes("://") || /^[A-Za-z]:/.test(value)) return undefined;
|
|
181
|
-
const match = /^(?:[^@/:]+@)?([^@/:]+):([^?#]+)$/.exec(value);
|
|
182
|
-
if (!match?.[1] || !match[2]) return undefined;
|
|
183
|
-
transport = "scp:";
|
|
184
|
-
host = match[1].toLowerCase();
|
|
185
|
-
pathname = `/${match[2]}`;
|
|
186
|
-
}
|
|
187
|
-
pathname = pathname.replace(/\/+$/, "");
|
|
188
|
-
return host === "github.com"
|
|
189
|
-
? `${host}${pathname.replace(/\.git$/, "").toLowerCase()}`
|
|
190
|
-
: `${transport}//${host}${pathname}`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function verifiedGitOutput(
|
|
194
|
-
cwd: string,
|
|
195
|
-
args: string[],
|
|
196
|
-
runGit: CursorCloudGitRunner,
|
|
197
|
-
verifyGit: CursorCloudGitRunner,
|
|
198
|
-
): string | undefined {
|
|
199
|
-
const output = runGit(cwd, args);
|
|
200
|
-
if (output === undefined || verifyGit === runGit) return output;
|
|
201
|
-
return verifyGit(cwd, args) === output ? output : undefined;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
interface GitConfigRecord {
|
|
205
|
-
key: string;
|
|
206
|
-
value: string;
|
|
207
|
-
raw: string;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function parseNullGitConfigRecords(output: string): GitConfigRecord[] | undefined {
|
|
211
|
-
const records = output.split("\0");
|
|
212
|
-
if (records.pop() !== "" || records.some((record) => !record)) return undefined;
|
|
213
|
-
const parsed: GitConfigRecord[] = [];
|
|
214
|
-
for (const raw of records) {
|
|
215
|
-
const separator = raw.indexOf("\n");
|
|
216
|
-
if (separator <= 0) return undefined;
|
|
217
|
-
parsed.push({ key: raw.slice(0, separator), value: raw.slice(separator + 1), raw });
|
|
218
|
-
}
|
|
219
|
-
return parsed;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
interface ConfiguredRemoteUrls {
|
|
223
|
-
fetch: string[];
|
|
224
|
-
push: string[];
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function readConfiguredRemoteUrls(
|
|
228
|
-
cwd: string,
|
|
229
|
-
remote: string,
|
|
230
|
-
runGit: CursorCloudGitRunner,
|
|
231
|
-
verifyGit: CursorCloudGitRunner,
|
|
232
|
-
): ConfiguredRemoteUrls | null | undefined {
|
|
233
|
-
const escapedRemote = remote.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
234
|
-
const output = verifiedGitOutput(
|
|
235
|
-
cwd,
|
|
236
|
-
["config", "--null", "--get-regexp", `^remote\\.${escapedRemote}\\.(url|pushurl)$`],
|
|
237
|
-
runGit,
|
|
238
|
-
verifyGit,
|
|
239
|
-
);
|
|
240
|
-
if (output === undefined) return undefined;
|
|
241
|
-
const records = parseNullGitConfigRecords(output);
|
|
242
|
-
if (!records?.length) return undefined;
|
|
243
|
-
const fetch = records.filter(({ key }) => key.toLowerCase().endsWith(".url")).map(({ value }) => value);
|
|
244
|
-
const push = records.filter(({ key }) => key.toLowerCase().endsWith(".pushurl")).map(({ value }) => value);
|
|
245
|
-
return fetch.length > 0 && [...fetch, ...push].every((url) => normalizeRepositoryIdentity(url))
|
|
246
|
-
? { fetch, push }
|
|
247
|
-
: null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
interface UrlRewriteRule {
|
|
251
|
-
kind: "fetch" | "push";
|
|
252
|
-
prefix: string;
|
|
253
|
-
raw: string;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function readUrlRewriteRules(cwd: string, runGit: CursorCloudGitRunner): UrlRewriteRule[] | undefined {
|
|
257
|
-
const output = runGit(cwd, ["config", "--null", "--list"]);
|
|
258
|
-
if (output === undefined) return undefined;
|
|
259
|
-
const records = parseNullGitConfigRecords(output);
|
|
260
|
-
if (!records) return undefined;
|
|
261
|
-
const rules: UrlRewriteRule[] = [];
|
|
262
|
-
for (const { key, value, raw } of records) {
|
|
263
|
-
const normalizedKey = key.toLowerCase();
|
|
264
|
-
if (!normalizedKey.startsWith("url.")) continue;
|
|
265
|
-
if (normalizedKey.endsWith(".insteadof")) rules.push({ kind: "fetch", prefix: value, raw });
|
|
266
|
-
else if (normalizedKey.endsWith(".pushinsteadof")) rules.push({ kind: "push", prefix: value, raw });
|
|
267
|
-
}
|
|
268
|
-
return rules;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function selectedUrlRewriteRules(rules: UrlRewriteRule[], kind: UrlRewriteRule["kind"], url: string): string[] {
|
|
272
|
-
const matches = rules.filter((rule) => rule.kind === kind && url.startsWith(rule.prefix));
|
|
273
|
-
const longest = matches.reduce((length, rule) => Math.max(length, rule.prefix.length), 0);
|
|
274
|
-
return matches.filter((rule) => rule.prefix.length === longest).map(({ raw }) => raw).sort();
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function urlRewriteSignature(rules: UrlRewriteRule[], urls: ConfiguredRemoteUrls): string {
|
|
278
|
-
const selected: string[] = [];
|
|
279
|
-
for (const url of urls.fetch) selected.push("fetch", ...selectedUrlRewriteRules(rules, "fetch", url));
|
|
280
|
-
if (urls.push.length > 0) {
|
|
281
|
-
for (const url of urls.push) selected.push("push", ...selectedUrlRewriteRules(rules, "fetch", url));
|
|
282
|
-
} else {
|
|
283
|
-
for (const url of urls.fetch) {
|
|
284
|
-
const pushRules = selectedUrlRewriteRules(rules, "push", url);
|
|
285
|
-
selected.push("push", ...(pushRules.length > 0 ? pushRules : selectedUrlRewriteRules(rules, "fetch", url)));
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return selected.join("\0");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function urlRewriteConfigAgrees(
|
|
292
|
-
cwd: string,
|
|
293
|
-
urls: ConfiguredRemoteUrls,
|
|
294
|
-
runGit: CursorCloudGitRunner,
|
|
295
|
-
verifyGit: CursorCloudGitRunner,
|
|
296
|
-
): boolean {
|
|
297
|
-
if (verifyGit === runGit) return true;
|
|
298
|
-
const primary = readUrlRewriteRules(cwd, runGit);
|
|
299
|
-
const ordinary = readUrlRewriteRules(cwd, verifyGit);
|
|
300
|
-
return primary !== undefined && ordinary !== undefined
|
|
301
|
-
&& urlRewriteSignature(primary, urls) === urlRewriteSignature(ordinary, urls);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function listRemotes(cwd: string, runGit: CursorCloudGitRunner, verifyGit: CursorCloudGitRunner): string[] | undefined {
|
|
305
|
-
const output = verifiedGitOutput(cwd, ["remote"], runGit, verifyGit);
|
|
306
|
-
return output === undefined ? undefined : output.split("\n").filter(Boolean);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function resolveRemoteRepository(
|
|
310
|
-
cwd: string,
|
|
311
|
-
remote: string,
|
|
312
|
-
runGit: CursorCloudGitRunner,
|
|
313
|
-
verifyGit: CursorCloudGitRunner,
|
|
314
|
-
): string | null | undefined {
|
|
315
|
-
const configuredUrls = readConfiguredRemoteUrls(cwd, remote, runGit, verifyGit);
|
|
316
|
-
if (configuredUrls === undefined) return undefined;
|
|
317
|
-
if (configuredUrls === null) return null;
|
|
318
|
-
if (!urlRewriteConfigAgrees(cwd, configuredUrls, runGit, verifyGit)) return undefined;
|
|
319
|
-
const fetchUrls = verifiedGitOutput(cwd, ["remote", "get-url", "--all", remote], runGit, verifyGit);
|
|
320
|
-
const pushUrls = verifiedGitOutput(cwd, ["remote", "get-url", "--push", "--all", remote], runGit, verifyGit);
|
|
321
|
-
if (fetchUrls === undefined || pushUrls === undefined) return undefined;
|
|
322
|
-
const records = `${fetchUrls}\n${pushUrls}`.split("\n");
|
|
323
|
-
if (records.some((record) => !record)) return null;
|
|
324
|
-
const urls = records.map(normalizeRepositoryIdentity);
|
|
325
|
-
const repository = urls[0];
|
|
326
|
-
return repository && urls.every((url) => url === repository) ? repository : null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function findMatchingRemote(
|
|
330
|
-
cwd: string,
|
|
331
|
-
repo: string,
|
|
332
|
-
runGit: CursorCloudGitRunner,
|
|
333
|
-
verifyGit: CursorCloudGitRunner,
|
|
334
|
-
): RemoteResult {
|
|
335
|
-
const target = normalizeRepositoryIdentity(repo);
|
|
336
|
-
if (!target) return { kind: "unverified" };
|
|
337
|
-
const remotes = listRemotes(cwd, runGit, verifyGit);
|
|
338
|
-
if (!remotes) return { kind: "failed" };
|
|
339
|
-
const matches: string[] = [];
|
|
340
|
-
for (const remote of remotes) {
|
|
341
|
-
const repository = resolveRemoteRepository(cwd, remote, runGit, verifyGit);
|
|
342
|
-
if (repository === undefined) return { kind: "failed" };
|
|
343
|
-
if (repository === target) matches.push(remote);
|
|
344
|
-
}
|
|
345
|
-
return matches.length === 1 ? { kind: "found", remote: matches[0]! } : { kind: "unverified" };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function matchRefPattern(pattern: string, ref: string): string | undefined {
|
|
349
|
-
const wildcard = pattern.indexOf("*");
|
|
350
|
-
if (wildcard === -1) return pattern === ref ? "" : undefined;
|
|
351
|
-
if (pattern.indexOf("*", wildcard + 1) !== -1) return undefined;
|
|
352
|
-
const prefix = pattern.slice(0, wildcard);
|
|
353
|
-
const suffix = pattern.slice(wildcard + 1);
|
|
354
|
-
if (ref.length < prefix.length + suffix.length || !ref.startsWith(prefix) || !ref.endsWith(suffix)) return undefined;
|
|
355
|
-
return ref.slice(prefix.length, ref.length - suffix.length);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function isValidFetchRefspecShape(refspec: string): boolean {
|
|
359
|
-
if (refspec.startsWith("^")) {
|
|
360
|
-
const source = refspec.slice(1);
|
|
361
|
-
return source.startsWith("refs/") && !source.includes(":") && (source.match(/\*/g)?.length ?? 0) <= 1;
|
|
362
|
-
}
|
|
363
|
-
const positive = refspec.startsWith("+") ? refspec.slice(1) : refspec;
|
|
364
|
-
if (positive.startsWith("^")) return false;
|
|
365
|
-
const separator = positive.indexOf(":");
|
|
366
|
-
const source = separator === -1 ? positive : positive.slice(0, separator);
|
|
367
|
-
const destination = separator === -1 ? undefined : positive.slice(separator + 1);
|
|
368
|
-
if (
|
|
369
|
-
!source ||
|
|
370
|
-
(separator !== -1 && positive.indexOf(":", separator + 1) !== -1) ||
|
|
371
|
-
(!source.startsWith("refs/") && !/^[0-9a-fA-F]{40}$/.test(source)) ||
|
|
372
|
-
(destination !== undefined && destination !== "" && !destination.startsWith("refs/"))
|
|
373
|
-
) return false;
|
|
374
|
-
const sourceWildcards = source.match(/\*/g)?.length ?? 0;
|
|
375
|
-
const destinationWildcards = destination?.match(/\*/g)?.length ?? 0;
|
|
376
|
-
return sourceWildcards <= 1 && sourceWildcards === destinationWildcards;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function parseFetchRefspecs(
|
|
380
|
-
cwd: string,
|
|
381
|
-
runGit: CursorCloudGitRunner,
|
|
382
|
-
verifyGit: CursorCloudGitRunner,
|
|
383
|
-
): Map<string, string[]> | undefined {
|
|
384
|
-
const output = verifiedGitOutput(cwd, ["config", "--get-regexp", "^remote\\..*\\.fetch$"], runGit, verifyGit);
|
|
385
|
-
if (output === undefined) return undefined;
|
|
386
|
-
const remotes = new Map<string, string[]>();
|
|
387
|
-
for (const line of output.split("\n")) {
|
|
388
|
-
const match = /^remote\.(.+)\.fetch\s+(.+)$/.exec(line);
|
|
389
|
-
if (!match?.[1] || !match[2] || !isValidFetchRefspecShape(match[2])) return undefined;
|
|
390
|
-
remotes.set(match[1], [...(remotes.get(match[1]) ?? []), match[2]]);
|
|
391
|
-
}
|
|
392
|
-
return remotes;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function sourceForDestination(refspec: string, destination: string): string | undefined {
|
|
396
|
-
const positive = refspec.startsWith("+") ? refspec.slice(1) : refspec;
|
|
397
|
-
if (positive.startsWith("^")) return undefined;
|
|
398
|
-
const separator = positive.indexOf(":");
|
|
399
|
-
if (separator === -1) return undefined;
|
|
400
|
-
const sourcePattern = positive.slice(0, separator);
|
|
401
|
-
const matched = matchRefPattern(positive.slice(separator + 1), destination);
|
|
402
|
-
if (matched === undefined) return undefined;
|
|
403
|
-
return sourcePattern.includes("*") ? sourcePattern.replace("*", matched) : matched === "" ? sourcePattern : undefined;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function remoteTracksBranch(
|
|
407
|
-
cwd: string,
|
|
408
|
-
remote: string,
|
|
409
|
-
branch: string,
|
|
410
|
-
runGit: CursorCloudGitRunner,
|
|
411
|
-
verifyGit: CursorCloudGitRunner,
|
|
412
|
-
): boolean {
|
|
413
|
-
const allRefspecs = parseFetchRefspecs(cwd, runGit, verifyGit);
|
|
414
|
-
if (!allRefspecs) return false;
|
|
415
|
-
const source = `refs/heads/${branch}`;
|
|
416
|
-
const destination = `refs/remotes/${remote}/${branch}`;
|
|
417
|
-
const selectedRefspecs = allRefspecs.get(remote) ?? [];
|
|
418
|
-
if (selectedRefspecs.some((refspec) => refspec.startsWith("^") && matchRefPattern(refspec.slice(1), source) !== undefined)) return false;
|
|
419
|
-
const writers = new Set<string>();
|
|
420
|
-
for (const [writerRemote, refspecs] of allRefspecs) {
|
|
421
|
-
for (const refspec of refspecs) {
|
|
422
|
-
const writerSource = sourceForDestination(refspec, destination);
|
|
423
|
-
if (writerSource) writers.add(`${writerRemote}\0${writerSource}`);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return writers.size === 1 && writers.has(`${remote}\0${source}`);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function resolveRemoteTrackingRef(
|
|
430
|
-
cwd: string,
|
|
431
|
-
remote: string,
|
|
432
|
-
branch: string,
|
|
433
|
-
runGit: CursorCloudGitRunner,
|
|
434
|
-
verifyGit: CursorCloudGitRunner,
|
|
435
|
-
): ComparisonRefResult {
|
|
436
|
-
if (!remoteTracksBranch(cwd, remote, branch, runGit, verifyGit)) return { kind: "unverified" };
|
|
437
|
-
const ref = `refs/remotes/${remote}/${branch}`;
|
|
438
|
-
return runGit(cwd, ["for-each-ref", "--format=%(objecttype)%09%(symref)", ref]) === "commit\t"
|
|
439
|
-
? { kind: "found", ref }
|
|
440
|
-
: { kind: "unverified" };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function hasHistoryOverrides(cwd: string, runGit: CursorCloudGitRunner): boolean | undefined {
|
|
444
|
-
const replacements = runGit(cwd, ["for-each-ref", "--format=%(refname)", "refs/replace"]);
|
|
445
|
-
const graftsPath = runGit(cwd, ["rev-parse", "--git-path", "info/grafts"]);
|
|
446
|
-
if (replacements === undefined || graftsPath === undefined) return undefined;
|
|
447
|
-
return replacements.length > 0 || existsSync(resolve(cwd, graftsPath));
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function resolveComparisonRef(
|
|
451
|
-
cwd: string,
|
|
452
|
-
target: { repo?: string; branch?: string },
|
|
453
|
-
runGit: CursorCloudGitRunner,
|
|
454
|
-
verifyGit: CursorCloudGitRunner,
|
|
455
|
-
): ComparisonRefResult {
|
|
456
|
-
if (!target.repo) {
|
|
457
|
-
const upstream = verifiedGitOutput(
|
|
458
|
-
cwd,
|
|
459
|
-
["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
|
|
460
|
-
runGit,
|
|
461
|
-
verifyGit,
|
|
462
|
-
);
|
|
463
|
-
if (!upstream) return { kind: "unverified" };
|
|
464
|
-
const remotes = listRemotes(cwd, runGit, verifyGit);
|
|
465
|
-
if (!remotes) return { kind: "failed" };
|
|
466
|
-
const matches = remotes.filter((remote) => upstream.startsWith(`${remote}/`));
|
|
467
|
-
if (matches.length !== 1) return { kind: "unverified" };
|
|
468
|
-
const remote = matches[0]!;
|
|
469
|
-
const repository = resolveRemoteRepository(cwd, remote, runGit, verifyGit);
|
|
470
|
-
if (repository === undefined) return { kind: "failed" };
|
|
471
|
-
if (!repository) return { kind: "unverified" };
|
|
472
|
-
return resolveRemoteTrackingRef(cwd, remote, upstream.slice(remote.length + 1), runGit, verifyGit);
|
|
473
|
-
}
|
|
474
|
-
const ref = normalizeCursorCloudStartingRef(target.branch);
|
|
475
|
-
if (ref.kind !== "branch") return { kind: "unverified" };
|
|
476
|
-
const remote = findMatchingRemote(cwd, target.repo, runGit, verifyGit);
|
|
477
|
-
return remote.kind === "found"
|
|
478
|
-
? resolveRemoteTrackingRef(cwd, remote.remote, ref.value, runGit, verifyGit)
|
|
479
|
-
: remote;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export function inspectCursorCloudLocalState(
|
|
483
|
-
cwd: string,
|
|
484
|
-
target: { repo?: string; branch?: string } = {},
|
|
485
|
-
runGit: CursorCloudGitRunner = runCursorCloudGit,
|
|
486
|
-
): CursorCloudLocalState {
|
|
487
|
-
const verifyGit = runGit === runCursorCloudGit ? runCursorCloudGitWithUserConfig : runGit;
|
|
488
|
-
const insideWorkTree = runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
489
|
-
if (insideWorkTree !== "true") {
|
|
490
|
-
const bareRepository = runGit(cwd, ["rev-parse", "--is-bare-repository"]);
|
|
491
|
-
if (bareRepository === "true") {
|
|
492
|
-
return { insideGitRepo: true, dirty: "unknown", comparison: "unknown", reasons: [{ code: "bare_repo" }] };
|
|
493
|
-
}
|
|
494
|
-
const gitAlive = runGit(cwd, ["--version"]) !== undefined;
|
|
495
|
-
if (gitAlive && !hasPlausibleGitMetadata(cwd)) return { insideGitRepo: false };
|
|
496
|
-
return {
|
|
497
|
-
insideGitRepo: "unknown",
|
|
498
|
-
dirty: "unknown",
|
|
499
|
-
comparison: "unknown",
|
|
500
|
-
reasons: [{ code: "repository_detection_failed" }],
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
const status = runGit(cwd, [
|
|
504
|
-
...(process.platform === "win32" ? [] : ["-c", "core.fileMode=true"]),
|
|
505
|
-
"-c", "core.fsmonitor=false", "--no-optional-locks", "status", "--porcelain=v1", "--untracked-files=normal", "--ignore-submodules=none",
|
|
506
|
-
]);
|
|
507
|
-
const indexFlags = runGit(cwd, ["ls-files", "-v", "--", ":/"]);
|
|
508
|
-
const dirty = status === undefined ? "unknown" : status.length > 0;
|
|
509
|
-
const reasons: CursorCloudLocalStateUnknownReason[] = [];
|
|
510
|
-
if (status === undefined) reasons.push({ code: "status_failed" });
|
|
511
|
-
if (indexFlags === undefined) reasons.push({ code: "index_failed" });
|
|
512
|
-
else if (indexFlags.split("\n").some((line) => /^[a-zS] /.test(line))) reasons.push({ code: "hidden_index_state" });
|
|
513
|
-
const historyOverrides = hasHistoryOverrides(cwd, runGit);
|
|
514
|
-
if (historyOverrides === undefined) reasons.push({ code: "history_probe_failed" });
|
|
515
|
-
else if (historyOverrides) reasons.push({ code: "history_overrides" });
|
|
516
|
-
const hasHead = runGit(cwd, ["rev-parse", "--verify", "HEAD"]) !== undefined;
|
|
517
|
-
if (!hasHead) reasons.push({ code: "head_unavailable" });
|
|
518
|
-
const comparisonRef = resolveComparisonRef(cwd, target, runGit, verifyGit);
|
|
519
|
-
if (comparisonRef.kind === "failed") reasons.push({ code: "target_probe_failed" });
|
|
520
|
-
else if (comparisonRef.kind === "unverified") reasons.push({ code: "unverified_target" });
|
|
521
|
-
const ahead = hasHead && comparisonRef.kind === "found" ? runGit(cwd, ["rev-list", "--count", `${comparisonRef.ref}..HEAD`]) : undefined;
|
|
522
|
-
const aheadCount = ahead !== undefined && /^\d+$/.test(ahead) ? Number(ahead) : Number.NaN;
|
|
523
|
-
if (hasHead && comparisonRef.kind === "found" && !Number.isFinite(aheadCount)) reasons.push({ code: "comparison_failed" });
|
|
524
|
-
const [firstReason, ...remainingReasons] = reasons;
|
|
525
|
-
if (firstReason) {
|
|
526
|
-
return {
|
|
527
|
-
insideGitRepo: true,
|
|
528
|
-
dirty,
|
|
529
|
-
comparison: "unknown",
|
|
530
|
-
reasons: [firstReason, ...remainingReasons],
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
return { insideGitRepo: true, dirty: dirty as boolean, comparison: aheadCount > 0 ? "unpushed" : "contains_head" };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
export const __testUtils = { sourceForDestination };
|