@cursorpool-dev/cli 0.5.6
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/bin/cursor-pool.mjs +9 -0
- package/bin/cursor-pool.ts +169 -0
- package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
- package/node_modules/@cursor-pool/extension/package.json +64 -0
- package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
- package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
- package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
- package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
- package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
- package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
- package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
- package/node_modules/@cursor-pool/patcher/package.json +17 -0
- package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
- package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
- package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
- package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
- package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
- package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
- package/node_modules/@cursor-pool/service/package.json +17 -0
- package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
- package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
- package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
- package/node_modules/@cursor-pool/service/src/health.ts +10 -0
- package/node_modules/@cursor-pool/service/src/index.ts +29 -0
- package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
- package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
- package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
- package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
- package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
- package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
- package/node_modules/@cursor-pool/service/src/server.ts +939 -0
- package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
- package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
- package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
- package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
- package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
- package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
- package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
- package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
- package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
- package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
- package/node_modules/@cursor-pool/shared/package.json +17 -0
- package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
- package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
- package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
- package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
- package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
- package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
- package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
- package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
- package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
- package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
- package/package.json +28 -0
- package/src/adHocResign.ts +65 -0
- package/src/autostart.ts +240 -0
- package/src/compat.ts +282 -0
- package/src/confirm.ts +76 -0
- package/src/cursor.ts +94 -0
- package/src/diagnostics.ts +558 -0
- package/src/environment.ts +18 -0
- package/src/extensionBundle.ts +111 -0
- package/src/extensionLink.ts +168 -0
- package/src/index.ts +23 -0
- package/src/install.ts +614 -0
- package/src/installRecord.ts +105 -0
- package/src/launch.ts +182 -0
- package/src/patchSet.ts +182 -0
- package/src/platform.ts +132 -0
- package/src/repair.ts +383 -0
- package/src/restore.ts +153 -0
- package/src/serviceCommands.ts +79 -0
- package/src/serviceProcess.ts +188 -0
- package/src/status.ts +241 -0
- package/src/target.ts +37 -0
- package/src/trial.ts +133 -0
- package/src/uninstall.ts +213 -0
- package/test/autostart.test.ts +151 -0
- package/test/compat.test.ts +192 -0
- package/test/confirm.test.ts +114 -0
- package/test/cursor-pool-bin.test.ts +658 -0
- package/test/cursor.test.ts +20 -0
- package/test/diagnostics.test.ts +709 -0
- package/test/e2e-install.test.ts +773 -0
- package/test/extensionBundle.test.ts +161 -0
- package/test/extensionLink.test.ts +209 -0
- package/test/install.test.ts +862 -0
- package/test/installRecord.test.ts +107 -0
- package/test/launch.test.ts +138 -0
- package/test/platform.test.ts +226 -0
- package/test/repair.test.ts +575 -0
- package/test/restore.test.ts +211 -0
- package/test/serviceCommands.test.ts +135 -0
- package/test/serviceProcess.test.ts +280 -0
- package/test/status.test.ts +615 -0
- package/test/target.test.ts +49 -0
- package/test/trial.test.ts +146 -0
package/src/compat.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import type {
|
|
5
|
+
CompatibilityManifestEntry,
|
|
6
|
+
CompatibilityManifestEnvelope,
|
|
7
|
+
} from '@cursor-pool/shared/manifest';
|
|
8
|
+
import { CURSOR_AGENT_EXEC_RELATIVE_PATH } from '@cursor-pool/patcher';
|
|
9
|
+
import type { CliEnvironment } from './environment';
|
|
10
|
+
import type { CursorInfo } from './cursor';
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_COMPAT_ENTRIES: CompatibilityManifestEntry[] = [
|
|
13
|
+
{
|
|
14
|
+
platform: 'darwin',
|
|
15
|
+
arch: 'arm64',
|
|
16
|
+
cursorVersion: '3.5.38',
|
|
17
|
+
cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
18
|
+
supportStatus: 'supported',
|
|
19
|
+
targetRelativePath: CURSOR_AGENT_EXEC_RELATIVE_PATH,
|
|
20
|
+
expectedSha256: 'cb18f0237278884a39e2ce2b8664255e12689ad0803c20096c38e86c36acc51f',
|
|
21
|
+
structureSignature: 'ep1-verified-current',
|
|
22
|
+
patchStrategy: 'cursor-agent-exec-snippet',
|
|
23
|
+
verifyMarker: 'cursor-pool',
|
|
24
|
+
restoreStrategy: 'external-backup',
|
|
25
|
+
minCliVersion: '0.5.6',
|
|
26
|
+
minExtensionVersion: '0.5.6',
|
|
27
|
+
minServiceVersion: '0.5.6',
|
|
28
|
+
requiresWritableAppBundle: true,
|
|
29
|
+
requiresAdHocResign: true,
|
|
30
|
+
userMessage: 'Cursor 3.5.38 is supported for MVP-0.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
platform: 'darwin',
|
|
34
|
+
arch: 'x64',
|
|
35
|
+
cursorVersion: '3.5.38',
|
|
36
|
+
cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
37
|
+
supportStatus: 'supported',
|
|
38
|
+
targetRelativePath: CURSOR_AGENT_EXEC_RELATIVE_PATH,
|
|
39
|
+
expectedSha256: 'cb18f0237278884a39e2ce2b8664255e12689ad0803c20096c38e86c36acc51f',
|
|
40
|
+
structureSignature: 'ep1-verified-current',
|
|
41
|
+
patchStrategy: 'cursor-agent-exec-snippet',
|
|
42
|
+
verifyMarker: 'cursor-pool',
|
|
43
|
+
restoreStrategy: 'external-backup',
|
|
44
|
+
minCliVersion: '0.5.6',
|
|
45
|
+
minExtensionVersion: '0.5.6',
|
|
46
|
+
minServiceVersion: '0.5.6',
|
|
47
|
+
requiresWritableAppBundle: true,
|
|
48
|
+
requiresAdHocResign: true,
|
|
49
|
+
userMessage: 'Cursor 3.5.38 is supported for MVP-0 under Rosetta-launched installers.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
platform: 'darwin',
|
|
53
|
+
arch: 'arm64',
|
|
54
|
+
cursorVersion: '3.6.21',
|
|
55
|
+
cursorCommit: 'e7a7e93f4d75f8272503ecf33cedbaae10114a10',
|
|
56
|
+
supportStatus: 'supported',
|
|
57
|
+
targetRelativePath: CURSOR_AGENT_EXEC_RELATIVE_PATH,
|
|
58
|
+
expectedSha256: '222512631b78fddcdca3fa76c0dd458a7a86751dde19c998ceb31b3fe1905ebf',
|
|
59
|
+
structureSignature: 'ep1-verified-cursor-3-6-21',
|
|
60
|
+
patchStrategy: 'cursor-agent-exec-snippet',
|
|
61
|
+
verifyMarker: 'cursor-pool',
|
|
62
|
+
restoreStrategy: 'external-backup',
|
|
63
|
+
minCliVersion: '0.5.6',
|
|
64
|
+
minExtensionVersion: '0.5.6',
|
|
65
|
+
minServiceVersion: '0.5.6',
|
|
66
|
+
requiresWritableAppBundle: true,
|
|
67
|
+
requiresAdHocResign: true,
|
|
68
|
+
userMessage: 'Cursor 3.6.21 is supported for MVP-0.',
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export type ResolveCompatOptions = {
|
|
73
|
+
entries?: CompatibilityManifestEntry[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type CompatManifestFetchResponse = {
|
|
77
|
+
ok: boolean;
|
|
78
|
+
json: () => Promise<unknown>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type LoadCompatEntriesOptions = {
|
|
82
|
+
compatEntries?: CompatibilityManifestEntry[];
|
|
83
|
+
apiBaseUrl?: string;
|
|
84
|
+
compatManifestUrl?: string;
|
|
85
|
+
fetchManifest?: (url: string) => Promise<CompatManifestFetchResponse>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type RemoteCompatibilityManifestRule = CompatibilityManifestEntry & {
|
|
89
|
+
revision?: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const DEV_COMPAT_SIGNATURE_KEY = 'cursor-pool-compatibility-dev-signing-key';
|
|
93
|
+
const DEV_COMPAT_SIGNATURE_ALGORITHM = 'hmac-sha256-dev';
|
|
94
|
+
const DEV_COMPAT_SIGNATURE_KEY_ID = 'dev-compatibility-key';
|
|
95
|
+
|
|
96
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
97
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isCompatibilityStatus(value: unknown): value is CompatibilityManifestEntry['supportStatus'] {
|
|
101
|
+
return (
|
|
102
|
+
value === 'supported' ||
|
|
103
|
+
value === 'canary' ||
|
|
104
|
+
value === 'warning' ||
|
|
105
|
+
value === 'blocked' ||
|
|
106
|
+
value === 'unknown'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function asString(value: unknown, field: string) {
|
|
111
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
112
|
+
throw new Error(`compat manifest ${field} invalid`);
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function asBoolean(value: unknown, field: string) {
|
|
118
|
+
if (typeof value !== 'boolean') {
|
|
119
|
+
throw new Error(`compat manifest ${field} invalid`);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeRule(value: unknown): RemoteCompatibilityManifestRule {
|
|
125
|
+
if (!isRecord(value)) {
|
|
126
|
+
throw new Error('compat manifest rule invalid');
|
|
127
|
+
}
|
|
128
|
+
const supportStatus = value.supportStatus;
|
|
129
|
+
if (!isCompatibilityStatus(supportStatus)) {
|
|
130
|
+
throw new Error('compat manifest supportStatus invalid');
|
|
131
|
+
}
|
|
132
|
+
const revision = value.revision;
|
|
133
|
+
if (
|
|
134
|
+
revision !== undefined &&
|
|
135
|
+
(!Number.isInteger(revision) || typeof revision !== 'number' || revision < 1)
|
|
136
|
+
) {
|
|
137
|
+
throw new Error('compat manifest revision invalid');
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
platform: asString(value.platform, 'platform'),
|
|
141
|
+
arch: asString(value.arch, 'arch'),
|
|
142
|
+
cursorVersion: asString(value.cursorVersion, 'cursorVersion'),
|
|
143
|
+
cursorCommit: asString(value.cursorCommit, 'cursorCommit'),
|
|
144
|
+
supportStatus,
|
|
145
|
+
targetRelativePath: asString(value.targetRelativePath, 'targetRelativePath'),
|
|
146
|
+
expectedSha256: asString(value.expectedSha256, 'expectedSha256'),
|
|
147
|
+
structureSignature: asString(value.structureSignature, 'structureSignature'),
|
|
148
|
+
patchStrategy: asString(value.patchStrategy, 'patchStrategy'),
|
|
149
|
+
verifyMarker: asString(value.verifyMarker, 'verifyMarker'),
|
|
150
|
+
restoreStrategy: asString(value.restoreStrategy, 'restoreStrategy'),
|
|
151
|
+
minCliVersion: asString(value.minCliVersion, 'minCliVersion'),
|
|
152
|
+
minExtensionVersion: asString(value.minExtensionVersion, 'minExtensionVersion'),
|
|
153
|
+
minServiceVersion: asString(value.minServiceVersion, 'minServiceVersion'),
|
|
154
|
+
requiresWritableAppBundle: asBoolean(value.requiresWritableAppBundle, 'requiresWritableAppBundle'),
|
|
155
|
+
requiresAdHocResign: asBoolean(value.requiresAdHocResign, 'requiresAdHocResign'),
|
|
156
|
+
userMessage: asString(value.userMessage, 'userMessage'),
|
|
157
|
+
...(revision === undefined ? {} : { revision }),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function canonicalRuleSegment(rule: RemoteCompatibilityManifestRule) {
|
|
162
|
+
return [
|
|
163
|
+
rule.platform,
|
|
164
|
+
rule.arch,
|
|
165
|
+
rule.cursorVersion,
|
|
166
|
+
rule.cursorCommit,
|
|
167
|
+
rule.supportStatus,
|
|
168
|
+
String(rule.revision ?? 1),
|
|
169
|
+
].join(':');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function safeEqualHex(left: string, right: string) {
|
|
173
|
+
const leftBuffer = Buffer.from(left, 'hex');
|
|
174
|
+
const rightBuffer = Buffer.from(right, 'hex');
|
|
175
|
+
if (leftBuffer.length === 0 || leftBuffer.length !== rightBuffer.length) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildCompatManifestSignature(
|
|
182
|
+
version: number,
|
|
183
|
+
rules: RemoteCompatibilityManifestRule[],
|
|
184
|
+
) {
|
|
185
|
+
const canonical = [String(version), ...[...rules].sort((a, b) => canonicalRuleSegment(a).localeCompare(canonicalRuleSegment(b))).map(canonicalRuleSegment)].join('|');
|
|
186
|
+
return createHmac('sha256', DEV_COMPAT_SIGNATURE_KEY).update(canonical).digest('hex');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function verifyCompatManifestEnvelope(
|
|
190
|
+
envelope: unknown,
|
|
191
|
+
): CompatibilityManifestEntry[] {
|
|
192
|
+
if (!isRecord(envelope)) {
|
|
193
|
+
throw new Error('compat manifest envelope invalid');
|
|
194
|
+
}
|
|
195
|
+
const version = envelope.version;
|
|
196
|
+
if (!Number.isInteger(version) || typeof version !== 'number' || version < 1) {
|
|
197
|
+
throw new Error('compat manifest version invalid');
|
|
198
|
+
}
|
|
199
|
+
if (envelope.signatureAlgorithm !== DEV_COMPAT_SIGNATURE_ALGORITHM) {
|
|
200
|
+
throw new Error('compat manifest signature algorithm unsupported');
|
|
201
|
+
}
|
|
202
|
+
if (envelope.signatureKeyId !== DEV_COMPAT_SIGNATURE_KEY_ID) {
|
|
203
|
+
throw new Error('compat manifest signature key unsupported');
|
|
204
|
+
}
|
|
205
|
+
if (typeof envelope.signature !== 'string') {
|
|
206
|
+
throw new Error('compat manifest signature invalid');
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(envelope.rules) || envelope.rules.length === 0) {
|
|
209
|
+
throw new Error('compat manifest rules invalid');
|
|
210
|
+
}
|
|
211
|
+
const rules = envelope.rules.map(normalizeRule);
|
|
212
|
+
const expected = buildCompatManifestSignature(version, rules);
|
|
213
|
+
if (!safeEqualHex(envelope.signature, expected)) {
|
|
214
|
+
throw new Error('compat manifest signature invalid');
|
|
215
|
+
}
|
|
216
|
+
return rules.map(({ revision, ...rule }) => rule);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function compatManifestUrlFromApiBaseUrl(apiBaseUrl: string) {
|
|
220
|
+
return `${apiBaseUrl.replace(/\/+$/, '')}/api/client/compatibility/manifest`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function loadCompatEntries(
|
|
224
|
+
options: LoadCompatEntriesOptions = {},
|
|
225
|
+
): Promise<CompatibilityManifestEntry[]> {
|
|
226
|
+
if (options.compatEntries) {
|
|
227
|
+
return options.compatEntries;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const manifestUrl =
|
|
231
|
+
options.compatManifestUrl ??
|
|
232
|
+
(options.apiBaseUrl ? compatManifestUrlFromApiBaseUrl(options.apiBaseUrl) : undefined);
|
|
233
|
+
if (!manifestUrl) {
|
|
234
|
+
return DEFAULT_COMPAT_ENTRIES;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (manifestUrl.startsWith('file://')) {
|
|
238
|
+
try {
|
|
239
|
+
const body = await readFile(fileURLToPath(manifestUrl), 'utf8');
|
|
240
|
+
return verifyCompatManifestEnvelope(JSON.parse(body));
|
|
241
|
+
} catch {
|
|
242
|
+
return DEFAULT_COMPAT_ENTRIES;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const fetchManifest = options.fetchManifest ?? fetch;
|
|
247
|
+
try {
|
|
248
|
+
const response = await fetchManifest(manifestUrl);
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
return DEFAULT_COMPAT_ENTRIES;
|
|
251
|
+
}
|
|
252
|
+
return verifyCompatManifestEnvelope(await response.json());
|
|
253
|
+
} catch {
|
|
254
|
+
return DEFAULT_COMPAT_ENTRIES;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function resolveCompatEntry(
|
|
259
|
+
cursor: CursorInfo,
|
|
260
|
+
environment: CliEnvironment,
|
|
261
|
+
options: ResolveCompatOptions = {},
|
|
262
|
+
) {
|
|
263
|
+
const entries = options.entries ?? DEFAULT_COMPAT_ENTRIES;
|
|
264
|
+
const entry = entries.find(
|
|
265
|
+
(candidate) =>
|
|
266
|
+
candidate.platform === environment.platform &&
|
|
267
|
+
candidate.arch === environment.arch &&
|
|
268
|
+
candidate.cursorVersion === cursor.version &&
|
|
269
|
+
candidate.cursorCommit === cursor.commit,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (!entry) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`No compatibility entry for Cursor ${cursor.version} ${cursor.commit} on ${environment.platform}/${environment.arch}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (entry.supportStatus === 'blocked') {
|
|
278
|
+
throw new Error(`Cursor ${cursor.version} is blocked: ${entry.userMessage}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return entry;
|
|
282
|
+
}
|
package/src/confirm.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
|
|
4
|
+
export type RealOperation = 'install' | 'repair' | 'restore' | 'uninstall';
|
|
5
|
+
|
|
6
|
+
export type RealOperationSummary = {
|
|
7
|
+
operation: RealOperation;
|
|
8
|
+
appPath: string;
|
|
9
|
+
targetPath: string;
|
|
10
|
+
backupPath?: string;
|
|
11
|
+
runtimeFile?: string;
|
|
12
|
+
extensionInstallPath?: string;
|
|
13
|
+
extensionLinkedPath?: string;
|
|
14
|
+
restorePath?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ConfirmRealOperationOptions = {
|
|
18
|
+
summary: RealOperationSummary;
|
|
19
|
+
yes?: boolean;
|
|
20
|
+
isInteractive?: boolean;
|
|
21
|
+
askConfirmation?: (prompt: string) => boolean | Promise<boolean>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function formatRealOperationSummary(summary: RealOperationSummary) {
|
|
25
|
+
const lines = [
|
|
26
|
+
`operation: ${summary.operation}`,
|
|
27
|
+
`app: ${summary.appPath}`,
|
|
28
|
+
`target: ${summary.targetPath}`,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (summary.backupPath) {
|
|
32
|
+
lines.push(`backup: ${summary.backupPath}`);
|
|
33
|
+
}
|
|
34
|
+
if (summary.runtimeFile) {
|
|
35
|
+
lines.push(`runtime: ${summary.runtimeFile}`);
|
|
36
|
+
}
|
|
37
|
+
if (summary.extensionInstallPath) {
|
|
38
|
+
lines.push(`extension-install: ${summary.extensionInstallPath}`);
|
|
39
|
+
}
|
|
40
|
+
if (summary.extensionLinkedPath) {
|
|
41
|
+
lines.push(`extension-linked: ${summary.extensionLinkedPath}`);
|
|
42
|
+
}
|
|
43
|
+
if (summary.restorePath) {
|
|
44
|
+
lines.push(`restore: ${summary.restorePath}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function askWithReadline(prompt: string) {
|
|
51
|
+
const rl = createInterface({ input, output });
|
|
52
|
+
try {
|
|
53
|
+
const answer = await rl.question(prompt);
|
|
54
|
+
return ['y', 'yes'].includes(answer.trim().toLowerCase());
|
|
55
|
+
} finally {
|
|
56
|
+
rl.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function confirmRealOperation(options: ConfirmRealOperationOptions) {
|
|
61
|
+
const { operation } = options.summary;
|
|
62
|
+
if (options.yes) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isInteractive = options.isInteractive ?? Boolean(input.isTTY && output.isTTY);
|
|
67
|
+
if (!isInteractive) {
|
|
68
|
+
throw new Error(`Pass --yes to confirm ${operation} for real Cursor`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const prompt = `${formatRealOperationSummary(options.summary)}\nProceed? [y/N] `;
|
|
72
|
+
const confirmed = await (options.askConfirmation ?? askWithReadline)(prompt);
|
|
73
|
+
if (!confirmed) {
|
|
74
|
+
throw new Error(`${operation} cancelled`);
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/cursor.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, win32 } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_MACOS_CURSOR_APP_PATH = '/Applications/Cursor.app';
|
|
5
|
+
|
|
6
|
+
export type CursorInfo = {
|
|
7
|
+
appPath: string;
|
|
8
|
+
version: string;
|
|
9
|
+
commit: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type FindCursorOptions = {
|
|
13
|
+
appPath?: string;
|
|
14
|
+
productRelativePath?: string;
|
|
15
|
+
platform?: NodeJS.Platform;
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type CursorProductJson = {
|
|
20
|
+
version?: unknown;
|
|
21
|
+
commit?: unknown;
|
|
22
|
+
commitHash?: unknown;
|
|
23
|
+
cursorCommit?: unknown;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function defaultCursorAppPath(options: Pick<FindCursorOptions, 'platform' | 'env'> = {}) {
|
|
27
|
+
const platform = options.platform ?? process.platform;
|
|
28
|
+
if (platform === 'darwin') {
|
|
29
|
+
return DEFAULT_MACOS_CURSOR_APP_PATH;
|
|
30
|
+
}
|
|
31
|
+
if (platform === 'win32') {
|
|
32
|
+
const localAppData = options.env?.LOCALAPPDATA ?? process.env.LOCALAPPDATA;
|
|
33
|
+
if (!localAppData) {
|
|
34
|
+
throw new Error('LOCALAPPDATA is required to auto-detect Cursor on Windows');
|
|
35
|
+
}
|
|
36
|
+
return win32.join(localAppData, 'Programs', 'Cursor');
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Cursor app auto-detection is not supported on ${platform}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readString(value: unknown) {
|
|
42
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function findCursor(options: FindCursorOptions = {}): Promise<CursorInfo> {
|
|
46
|
+
const appPath = options.appPath ?? defaultCursorAppPath(options);
|
|
47
|
+
const productJsonPath = await resolveProductJsonPath(appPath, options);
|
|
48
|
+
const product = JSON.parse(await readFile(productJsonPath, 'utf8')) as CursorProductJson;
|
|
49
|
+
const version = readString(product.version);
|
|
50
|
+
const commit =
|
|
51
|
+
readString(product.commit) ??
|
|
52
|
+
readString(product.cursorCommit) ??
|
|
53
|
+
readString(product.commitHash);
|
|
54
|
+
|
|
55
|
+
if (!version) {
|
|
56
|
+
throw new Error(`Cursor product.json is missing version: ${productJsonPath}`);
|
|
57
|
+
}
|
|
58
|
+
if (!commit) {
|
|
59
|
+
throw new Error(`Cursor product.json is missing commit: ${productJsonPath}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
appPath,
|
|
64
|
+
version,
|
|
65
|
+
commit,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function resolveProductJsonPath(appPath: string, options: FindCursorOptions) {
|
|
70
|
+
if (options.productRelativePath) {
|
|
71
|
+
return join(appPath, options.productRelativePath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const candidates = [
|
|
75
|
+
'Contents/Resources/app/product.json',
|
|
76
|
+
'usr/share/cursor/resources/app/product.json',
|
|
77
|
+
'resources/app/product.json',
|
|
78
|
+
'opt/Cursor/resources/app/product.json',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
const path = join(appPath, candidate);
|
|
83
|
+
try {
|
|
84
|
+
await readFile(path, 'utf8');
|
|
85
|
+
return path;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return join(appPath, candidates[0] ?? 'Contents/Resources/app/product.json');
|
|
94
|
+
}
|