@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
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join, normalize } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { patchCursorAgentExec } from '../../patcher/src/patchCursorAgentExec';
|
|
8
|
+
import { patchCursorAlwaysLocal } from '../../patcher/src/patchCursorAlwaysLocal';
|
|
9
|
+
import { patchCursorWorkbenchAuthGate } from '../../patcher/src/patchCursorWorkbenchAuthGate';
|
|
10
|
+
import {
|
|
11
|
+
CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR,
|
|
12
|
+
CURSOR_POOL_PATCH_MARKER,
|
|
13
|
+
} from '../../patcher/src/marker';
|
|
14
|
+
import { startServer } from '../../service/src/server';
|
|
15
|
+
import { writeRuntimeInfo } from '../../service/src/runtime';
|
|
16
|
+
import type { CompatibilityManifestEntry } from '../../shared/src/manifest';
|
|
17
|
+
import { bundleExtension, getExtensionState } from '../src/extensionBundle';
|
|
18
|
+
import { getLinkedExtensionState, linkedExtensionPathForDir } from '../src/extensionLink';
|
|
19
|
+
import {
|
|
20
|
+
installIdForAppPath,
|
|
21
|
+
readInstallRecord,
|
|
22
|
+
writeInstallRecord,
|
|
23
|
+
type InstallRecord,
|
|
24
|
+
} from '../src/installRecord';
|
|
25
|
+
import { repair } from '../src/repair';
|
|
26
|
+
|
|
27
|
+
const targetRelativePath =
|
|
28
|
+
'Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js';
|
|
29
|
+
const alwaysLocalRelativePath =
|
|
30
|
+
'Contents/Resources/app/extensions/cursor-always-local/dist/main.js';
|
|
31
|
+
const workbenchRelativePath = 'Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js';
|
|
32
|
+
const composerAuthGateAnchor =
|
|
33
|
+
'get when(){return p()&&!fzC},get fallback(){return he(DGC,{})}';
|
|
34
|
+
const composerSubmitAuthGateAnchor =
|
|
35
|
+
'if(!p()){e.cursorAuthenticationService.login(),e.commandService.executeCommand(wV,"general");return}';
|
|
36
|
+
const agentLoopRunAnchor = 'await this.agentClientService.run(te,H,$e,Ne,Ce,T,ze,me,we,[],ct)';
|
|
37
|
+
const buildFlagsLocalModeAnchor = 'localMode:!1';
|
|
38
|
+
const localProviderConfigAnchor =
|
|
39
|
+
'async getLocalAgentProviderConfig(e){const t="[AgentClientService][getLocalAgentProviderConfig]",i=L0.localMode?await this.shellEnvironmentService.getShellEnv():{},r=e?.credentials,s=r?.case==="apiKeyCredentials"?r.value:void 0,o=this.reactiveStorageService.applicationUserPersistentStorage,a=o.useOpenAIKey===!0?this.cursorAuthenticationService.openAIKey()??void 0:void 0,u=xgS({apiKeyCandidates:[{value:s?.apiKey,source:"modelDetails.apiKeyCredentials.apiKey"},{value:a,source:"storage.openAIKey"}],baseUrlCandidates:[{value:s?.baseUrl,source:"modelDetails.apiKeyCredentials.baseUrl"},{value:o.openAIBaseUrl,source:"storage.openAIBaseUrl"}]});return{baseUrl:u.baseUrl,apiKey:u.apiKey}}createDefaultLocalModel(e){return "default"}';
|
|
40
|
+
const agentClientRunLocalModeAnchor =
|
|
41
|
+
'if(L0.localMode){try{g.onNetworkPhaseStart?.()}catch(b){this.logService.warn("[AgentClientService] onNetworkPhaseStart callback failed in local mode",b)}return this.runLocalAgentInExtensionHost(e,t,i,r,s,a,d,h,g)}return this.client.run(e,t,i,r,s,o,a,u,d,h,g)';
|
|
42
|
+
|
|
43
|
+
function workbenchFixture() {
|
|
44
|
+
return [
|
|
45
|
+
`function composer(){return he(Mt,{${composerAuthGateAnchor},get children(){return "controls"}})}`,
|
|
46
|
+
`async function submit(){${composerSubmitAuthGateAnchor};return "submitted";}`,
|
|
47
|
+
`const flags={${buildFlagsLocalModeAnchor}}`,
|
|
48
|
+
localProviderConfigAnchor,
|
|
49
|
+
`async function runAgentLoop(){${agentLoopRunAnchor}}`,
|
|
50
|
+
`async function agentClientRun(){const g={};${agentClientRunLocalModeAnchor}}`,
|
|
51
|
+
].join(';');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function patchFullSet(appPath: string, backupDir: string) {
|
|
55
|
+
await patchCursorAgentExec(appPath, { backupDir });
|
|
56
|
+
await patchCursorAlwaysLocal(appPath, { backupDir });
|
|
57
|
+
await patchCursorWorkbenchAuthGate(appPath, { backupDir });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function createFixtureApp() {
|
|
61
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-cli-repair-'));
|
|
62
|
+
const appPath = join(tempDir, 'Cursor.app');
|
|
63
|
+
const targetPath = join(appPath, targetRelativePath);
|
|
64
|
+
const targetContent = `function main() { return "agent"; }\n${CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR}\nmain();\n`;
|
|
65
|
+
await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-agent-exec/dist'), {
|
|
66
|
+
recursive: true,
|
|
67
|
+
});
|
|
68
|
+
await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-always-local/dist'), {
|
|
69
|
+
recursive: true,
|
|
70
|
+
});
|
|
71
|
+
await mkdir(join(appPath, 'Contents/Resources/app/out/vs/workbench'), {
|
|
72
|
+
recursive: true,
|
|
73
|
+
});
|
|
74
|
+
await writeFile(
|
|
75
|
+
join(appPath, 'Contents/Resources/app/product.json'),
|
|
76
|
+
JSON.stringify({
|
|
77
|
+
version: '3.5.38',
|
|
78
|
+
commit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
79
|
+
}),
|
|
80
|
+
'utf8',
|
|
81
|
+
);
|
|
82
|
+
await writeFile(targetPath, targetContent, 'utf8');
|
|
83
|
+
await writeFile(join(appPath, alwaysLocalRelativePath), 'function alwaysLocal(){}\n', 'utf8');
|
|
84
|
+
await writeFile(join(appPath, workbenchRelativePath), workbenchFixture(), 'utf8');
|
|
85
|
+
|
|
86
|
+
const compatEntry: CompatibilityManifestEntry = {
|
|
87
|
+
platform: process.platform,
|
|
88
|
+
arch: process.arch,
|
|
89
|
+
cursorVersion: '3.5.38',
|
|
90
|
+
cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
91
|
+
supportStatus: 'supported',
|
|
92
|
+
targetRelativePath,
|
|
93
|
+
expectedSha256: createHash('sha256').update(targetContent).digest('hex'),
|
|
94
|
+
structureSignature: 'fixture',
|
|
95
|
+
patchStrategy: 'cursor-agent-exec-snippet',
|
|
96
|
+
verifyMarker: 'cursor-pool',
|
|
97
|
+
restoreStrategy: 'external-backup',
|
|
98
|
+
minCliVersion: '0.0.0',
|
|
99
|
+
minExtensionVersion: '0.0.0',
|
|
100
|
+
minServiceVersion: '0.0.0',
|
|
101
|
+
requiresWritableAppBundle: true,
|
|
102
|
+
requiresAdHocResign: false,
|
|
103
|
+
userMessage: 'fixture supported',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
appPath,
|
|
108
|
+
tempDir,
|
|
109
|
+
targetPath,
|
|
110
|
+
targetContent,
|
|
111
|
+
runtimeFile: join(tempDir, 'runtime.json'),
|
|
112
|
+
backupDir: join(tempDir, 'backups'),
|
|
113
|
+
installRecordFile: join(tempDir, 'install.json'),
|
|
114
|
+
extensionInstallPath: join(tempDir, 'extensions/cursor-pool-status'),
|
|
115
|
+
cursorExtensionsDir: join(tempDir, 'Extensions'),
|
|
116
|
+
compatEntry,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createInstallRecordFixture(
|
|
121
|
+
fixture: Awaited<ReturnType<typeof createFixtureApp>>,
|
|
122
|
+
overrides: Partial<InstallRecord> = {},
|
|
123
|
+
): InstallRecord {
|
|
124
|
+
const now = '2026-05-30T00:00:00.000Z';
|
|
125
|
+
return {
|
|
126
|
+
installId: installIdForAppPath(fixture.appPath),
|
|
127
|
+
mode: 'real',
|
|
128
|
+
appPath: fixture.appPath,
|
|
129
|
+
cursorVersion: '3.5.38',
|
|
130
|
+
cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
131
|
+
targetRelativePath,
|
|
132
|
+
originalSha256: fixture.compatEntry.expectedSha256,
|
|
133
|
+
compatSupportStatus: 'supported',
|
|
134
|
+
runtimeFile: fixture.runtimeFile,
|
|
135
|
+
backupDir: fixture.backupDir,
|
|
136
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
137
|
+
extensionLinkedPath: linkedExtensionPathForDir(fixture.cursorExtensionsDir),
|
|
138
|
+
extensionState: 'linked',
|
|
139
|
+
cliVersion: '0.0.0',
|
|
140
|
+
extensionVersion: '0.0.0',
|
|
141
|
+
serviceVersion: '0.0.0',
|
|
142
|
+
lastOperation: 'install',
|
|
143
|
+
lastOperationStatus: 'ok',
|
|
144
|
+
createdAt: now,
|
|
145
|
+
updatedAt: now,
|
|
146
|
+
...overrides,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resignCompatEntry(entry: CompatibilityManifestEntry): CompatibilityManifestEntry {
|
|
151
|
+
return {
|
|
152
|
+
...entry,
|
|
153
|
+
requiresAdHocResign: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function seedInstallRecord(fixture: Awaited<ReturnType<typeof createFixtureApp>>) {
|
|
158
|
+
const record = createInstallRecordFixture(fixture);
|
|
159
|
+
await writeInstallRecord(record, { installRecordFile: fixture.installRecordFile });
|
|
160
|
+
return record;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
test('repair restarts stopped service and updates install record', async () => {
|
|
164
|
+
const fixture = await createFixtureApp();
|
|
165
|
+
let starts = 0;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
169
|
+
await bundleExtension({ installPath: fixture.extensionInstallPath });
|
|
170
|
+
const record = await seedInstallRecord(fixture);
|
|
171
|
+
await writeRuntimeInfo(
|
|
172
|
+
{ host: '127.0.0.1', port: 45123, runtimeId: 'stale-runtime-fixture' },
|
|
173
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const output = await repair({
|
|
177
|
+
realAppPath: fixture.appPath,
|
|
178
|
+
yes: true,
|
|
179
|
+
runtimeFile: fixture.runtimeFile,
|
|
180
|
+
backupDir: fixture.backupDir,
|
|
181
|
+
installRecordFile: fixture.installRecordFile,
|
|
182
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
183
|
+
compatEntries: [fixture.compatEntry],
|
|
184
|
+
startDetachedService: async () => {
|
|
185
|
+
starts += 1;
|
|
186
|
+
await writeRuntimeInfo(
|
|
187
|
+
{ host: '127.0.0.1', port: 45200, runtimeId: 'runtime-repaired' },
|
|
188
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
189
|
+
);
|
|
190
|
+
return { host: '127.0.0.1', port: 45200, runtimeId: 'runtime-repaired' };
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
assert.equal(starts, 1);
|
|
195
|
+
assert.match(output, /service: repaired/);
|
|
196
|
+
assert.match(output, /repair: ok/);
|
|
197
|
+
const repairedRecord = await readInstallRecord({
|
|
198
|
+
installRecordFile: fixture.installRecordFile,
|
|
199
|
+
});
|
|
200
|
+
assert.ok(repairedRecord);
|
|
201
|
+
assert.equal(repairedRecord.lastOperation, 'repair');
|
|
202
|
+
assert.equal(repairedRecord.lastOperationStatus, 'ok');
|
|
203
|
+
assert.equal(repairedRecord.createdAt, record.createdAt);
|
|
204
|
+
assert.notEqual(repairedRecord.updatedAt, record.updatedAt);
|
|
205
|
+
} finally {
|
|
206
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('repair restores missing extension bundle', async () => {
|
|
211
|
+
const fixture = await createFixtureApp();
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
215
|
+
await seedInstallRecord(fixture);
|
|
216
|
+
|
|
217
|
+
const output = await repair({
|
|
218
|
+
realAppPath: fixture.appPath,
|
|
219
|
+
yes: true,
|
|
220
|
+
runtimeFile: fixture.runtimeFile,
|
|
221
|
+
backupDir: fixture.backupDir,
|
|
222
|
+
installRecordFile: fixture.installRecordFile,
|
|
223
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
224
|
+
cursorExtensionsDir: fixture.cursorExtensionsDir,
|
|
225
|
+
compatEntries: [fixture.compatEntry],
|
|
226
|
+
startDetachedService: async () => {
|
|
227
|
+
await writeRuntimeInfo(
|
|
228
|
+
{ host: '127.0.0.1', port: 45201, runtimeId: 'runtime-repaired' },
|
|
229
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
230
|
+
);
|
|
231
|
+
return { host: '127.0.0.1', port: 45201, runtimeId: 'runtime-repaired' };
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
assert.match(output, /extension: repaired/);
|
|
236
|
+
assert.match(output, /linked-extension: repaired/);
|
|
237
|
+
assert.equal(await getExtensionState(fixture.extensionInstallPath), 'bundled');
|
|
238
|
+
assert.equal(
|
|
239
|
+
await getLinkedExtensionState(linkedExtensionPathForDir(fixture.cursorExtensionsDir)),
|
|
240
|
+
'linked',
|
|
241
|
+
);
|
|
242
|
+
} finally {
|
|
243
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('repair preserves linked extension state from install record without explicit extensions dir', async () => {
|
|
248
|
+
const fixture = await createFixtureApp();
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
252
|
+
await bundleExtension({ installPath: fixture.extensionInstallPath });
|
|
253
|
+
await seedInstallRecord(fixture);
|
|
254
|
+
await mkdir(join(linkedExtensionPathForDir(fixture.cursorExtensionsDir), 'dist'), { recursive: true });
|
|
255
|
+
await writeFile(
|
|
256
|
+
join(linkedExtensionPathForDir(fixture.cursorExtensionsDir), 'package.json'),
|
|
257
|
+
JSON.stringify({ publisher: 'cursor-pool', name: 'extension', version: '0.0.0' }),
|
|
258
|
+
'utf8',
|
|
259
|
+
);
|
|
260
|
+
await writeFile(
|
|
261
|
+
join(linkedExtensionPathForDir(fixture.cursorExtensionsDir), 'dist/extension.js'),
|
|
262
|
+
'module.exports = {};\n',
|
|
263
|
+
'utf8',
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const output = await repair({
|
|
267
|
+
realAppPath: fixture.appPath,
|
|
268
|
+
yes: true,
|
|
269
|
+
runtimeFile: fixture.runtimeFile,
|
|
270
|
+
backupDir: fixture.backupDir,
|
|
271
|
+
installRecordFile: fixture.installRecordFile,
|
|
272
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
273
|
+
compatEntries: [fixture.compatEntry],
|
|
274
|
+
startDetachedService: async () => {
|
|
275
|
+
await writeRuntimeInfo(
|
|
276
|
+
{ host: '127.0.0.1', port: 45206, runtimeId: 'runtime-repaired' },
|
|
277
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
278
|
+
);
|
|
279
|
+
return { host: '127.0.0.1', port: 45206, runtimeId: 'runtime-repaired' };
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const repairedRecord = await readInstallRecord({
|
|
284
|
+
installRecordFile: fixture.installRecordFile,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
assert.match(output, /linked-extension: linked/);
|
|
288
|
+
assert.equal(repairedRecord?.extensionState, 'linked');
|
|
289
|
+
} finally {
|
|
290
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('repair reapplies missing patch when compat is supported', async () => {
|
|
295
|
+
const fixture = await createFixtureApp();
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await bundleExtension({ installPath: fixture.extensionInstallPath });
|
|
299
|
+
await seedInstallRecord(fixture);
|
|
300
|
+
|
|
301
|
+
const output = await repair({
|
|
302
|
+
realAppPath: fixture.appPath,
|
|
303
|
+
yes: true,
|
|
304
|
+
runtimeFile: fixture.runtimeFile,
|
|
305
|
+
backupDir: fixture.backupDir,
|
|
306
|
+
installRecordFile: fixture.installRecordFile,
|
|
307
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
308
|
+
compatEntries: [fixture.compatEntry],
|
|
309
|
+
startDetachedService: async () => {
|
|
310
|
+
await writeRuntimeInfo(
|
|
311
|
+
{ host: '127.0.0.1', port: 45202, runtimeId: 'runtime-repaired' },
|
|
312
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
313
|
+
);
|
|
314
|
+
return { host: '127.0.0.1', port: 45202, runtimeId: 'runtime-repaired' };
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
assert.match(output, /patch: repaired/);
|
|
319
|
+
assert.match(await readFile(fixture.targetPath, 'utf8'), new RegExp(CURSOR_POOL_PATCH_MARKER));
|
|
320
|
+
} finally {
|
|
321
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('repair ad-hoc resigns macOS Cursor when reapplying a patch requires resigning', async () => {
|
|
326
|
+
const fixture = await createFixtureApp();
|
|
327
|
+
const resignCalls: string[] = [];
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
await bundleExtension({ installPath: fixture.extensionInstallPath });
|
|
331
|
+
await seedInstallRecord(fixture);
|
|
332
|
+
|
|
333
|
+
const output = await repair({
|
|
334
|
+
realAppPath: fixture.appPath,
|
|
335
|
+
yes: true,
|
|
336
|
+
platform: 'darwin',
|
|
337
|
+
runtimeFile: fixture.runtimeFile,
|
|
338
|
+
backupDir: fixture.backupDir,
|
|
339
|
+
installRecordFile: fixture.installRecordFile,
|
|
340
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
341
|
+
compatEntries: [resignCompatEntry(fixture.compatEntry)],
|
|
342
|
+
startDetachedService: async () => {
|
|
343
|
+
await writeRuntimeInfo(
|
|
344
|
+
{ host: '127.0.0.1', port: 45205, runtimeId: 'runtime-repaired' },
|
|
345
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
346
|
+
);
|
|
347
|
+
return { host: '127.0.0.1', port: 45205, runtimeId: 'runtime-repaired' };
|
|
348
|
+
},
|
|
349
|
+
adHocResignApp: async (appPath) => {
|
|
350
|
+
resignCalls.push(appPath);
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
assert.deepEqual(resignCalls, [fixture.appPath]);
|
|
355
|
+
assert.match(output, /patch: repaired/);
|
|
356
|
+
assert.match(output, /resign: ad-hoc/);
|
|
357
|
+
} finally {
|
|
358
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('repair blocks changed patch target without marker', async () => {
|
|
363
|
+
const fixture = await createFixtureApp();
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
await bundleExtension({ installPath: fixture.extensionInstallPath });
|
|
367
|
+
const record = await seedInstallRecord(fixture);
|
|
368
|
+
await writeFile(fixture.targetPath, `${fixture.targetContent}\n// user change\n`, 'utf8');
|
|
369
|
+
|
|
370
|
+
await assert.rejects(
|
|
371
|
+
repair({
|
|
372
|
+
realAppPath: fixture.appPath,
|
|
373
|
+
yes: true,
|
|
374
|
+
runtimeFile: fixture.runtimeFile,
|
|
375
|
+
backupDir: fixture.backupDir,
|
|
376
|
+
installRecordFile: fixture.installRecordFile,
|
|
377
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
378
|
+
compatEntries: [fixture.compatEntry],
|
|
379
|
+
startDetachedService: async () => {
|
|
380
|
+
throw new Error('must not start service after patch safety failure');
|
|
381
|
+
},
|
|
382
|
+
}),
|
|
383
|
+
/patch: changed/,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
assert.deepEqual(await readInstallRecord({ installRecordFile: fixture.installRecordFile }), record);
|
|
387
|
+
} finally {
|
|
388
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('repair blocks unsupported compat before repairing extension even when patch is applied', async () => {
|
|
393
|
+
const fixture = await createFixtureApp();
|
|
394
|
+
const blockedCompatEntry: CompatibilityManifestEntry = {
|
|
395
|
+
...fixture.compatEntry,
|
|
396
|
+
supportStatus: 'blocked',
|
|
397
|
+
userMessage: 'fixture blocked',
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
402
|
+
const record = await seedInstallRecord(fixture);
|
|
403
|
+
|
|
404
|
+
await assert.rejects(
|
|
405
|
+
repair({
|
|
406
|
+
realAppPath: fixture.appPath,
|
|
407
|
+
yes: true,
|
|
408
|
+
runtimeFile: fixture.runtimeFile,
|
|
409
|
+
backupDir: fixture.backupDir,
|
|
410
|
+
installRecordFile: fixture.installRecordFile,
|
|
411
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
412
|
+
compatEntries: [blockedCompatEntry],
|
|
413
|
+
startDetachedService: async () => {
|
|
414
|
+
throw new Error('must not start service for blocked compat');
|
|
415
|
+
},
|
|
416
|
+
}),
|
|
417
|
+
/compat: blocked/,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
assert.equal(await getExtensionState(fixture.extensionInstallPath), 'missing');
|
|
421
|
+
assert.deepEqual(await readInstallRecord({ installRecordFile: fixture.installRecordFile }), record);
|
|
422
|
+
} finally {
|
|
423
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('repair blocks unknown compat before repairing extension even when patch is applied', async () => {
|
|
428
|
+
const fixture = await createFixtureApp();
|
|
429
|
+
const unknownCompatEntry: CompatibilityManifestEntry = {
|
|
430
|
+
...fixture.compatEntry,
|
|
431
|
+
supportStatus: 'unknown',
|
|
432
|
+
userMessage: 'fixture unknown',
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
437
|
+
const record = await seedInstallRecord(fixture);
|
|
438
|
+
|
|
439
|
+
await assert.rejects(
|
|
440
|
+
repair({
|
|
441
|
+
realAppPath: fixture.appPath,
|
|
442
|
+
yes: true,
|
|
443
|
+
runtimeFile: fixture.runtimeFile,
|
|
444
|
+
backupDir: fixture.backupDir,
|
|
445
|
+
installRecordFile: fixture.installRecordFile,
|
|
446
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
447
|
+
compatEntries: [unknownCompatEntry],
|
|
448
|
+
startDetachedService: async () => {
|
|
449
|
+
throw new Error('must not start service for unknown compat');
|
|
450
|
+
},
|
|
451
|
+
}),
|
|
452
|
+
/compat: unknown/,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
assert.equal(await getExtensionState(fixture.extensionInstallPath), 'missing');
|
|
456
|
+
assert.deepEqual(await readInstallRecord({ installRecordFile: fixture.installRecordFile }), record);
|
|
457
|
+
} finally {
|
|
458
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('repair rejects mismatched install record before mutation', async () => {
|
|
463
|
+
const fixture = await createFixtureApp();
|
|
464
|
+
const otherFixture = await createFixtureApp();
|
|
465
|
+
let starts = 0;
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
469
|
+
const patchedBytes = await readFile(fixture.targetPath, 'utf8');
|
|
470
|
+
const wrongRecord = createInstallRecordFixture(otherFixture);
|
|
471
|
+
await writeInstallRecord(wrongRecord, { installRecordFile: fixture.installRecordFile });
|
|
472
|
+
|
|
473
|
+
await assert.rejects(
|
|
474
|
+
repair({
|
|
475
|
+
realAppPath: fixture.appPath,
|
|
476
|
+
yes: true,
|
|
477
|
+
runtimeFile: fixture.runtimeFile,
|
|
478
|
+
backupDir: fixture.backupDir,
|
|
479
|
+
installRecordFile: fixture.installRecordFile,
|
|
480
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
481
|
+
compatEntries: [fixture.compatEntry],
|
|
482
|
+
startDetachedService: async () => {
|
|
483
|
+
starts += 1;
|
|
484
|
+
throw new Error('must not start service for mismatched record');
|
|
485
|
+
},
|
|
486
|
+
}),
|
|
487
|
+
new RegExp(`Install record does not match Cursor app: ${normalize(fixture.appPath)}`),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
assert.equal(starts, 0);
|
|
491
|
+
assert.equal(await readFile(fixture.targetPath, 'utf8'), patchedBytes);
|
|
492
|
+
assert.deepEqual(
|
|
493
|
+
await readInstallRecord({ installRecordFile: fixture.installRecordFile }),
|
|
494
|
+
wrongRecord,
|
|
495
|
+
);
|
|
496
|
+
} finally {
|
|
497
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
498
|
+
await rm(otherFixture.tempDir, { recursive: true, force: true });
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('repair does not stop an already healthy service after repair', async () => {
|
|
503
|
+
const fixture = await createFixtureApp();
|
|
504
|
+
const service = await startServer({ runtimeFile: fixture.runtimeFile });
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
508
|
+
await bundleExtension({ installPath: fixture.extensionInstallPath });
|
|
509
|
+
await seedInstallRecord(fixture);
|
|
510
|
+
|
|
511
|
+
const output = await repair({
|
|
512
|
+
realAppPath: fixture.appPath,
|
|
513
|
+
yes: true,
|
|
514
|
+
runtimeFile: fixture.runtimeFile,
|
|
515
|
+
backupDir: fixture.backupDir,
|
|
516
|
+
installRecordFile: fixture.installRecordFile,
|
|
517
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
518
|
+
compatEntries: [fixture.compatEntry],
|
|
519
|
+
stopServiceAfterRepair: true,
|
|
520
|
+
startDetachedService: async () => {
|
|
521
|
+
throw new Error('must not start service when existing service is healthy');
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const health = await fetch(`http://127.0.0.1:${service.port}/health`);
|
|
526
|
+
const body = (await health.json()) as { ok?: unknown; runtimeId?: unknown };
|
|
527
|
+
|
|
528
|
+
assert.match(output, /service: running/);
|
|
529
|
+
assert.equal(health.ok, true);
|
|
530
|
+
assert.equal(body.ok, true);
|
|
531
|
+
assert.equal(body.runtimeId, service.runtimeId);
|
|
532
|
+
} finally {
|
|
533
|
+
await service.stop();
|
|
534
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('repair does not stop service when runtime file changes after restart', async () => {
|
|
539
|
+
const fixture = await createFixtureApp();
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
543
|
+
await bundleExtension({ installPath: fixture.extensionInstallPath });
|
|
544
|
+
await seedInstallRecord(fixture);
|
|
545
|
+
|
|
546
|
+
const output = await repair({
|
|
547
|
+
realAppPath: fixture.appPath,
|
|
548
|
+
yes: true,
|
|
549
|
+
runtimeFile: fixture.runtimeFile,
|
|
550
|
+
backupDir: fixture.backupDir,
|
|
551
|
+
installRecordFile: fixture.installRecordFile,
|
|
552
|
+
extensionInstallPath: fixture.extensionInstallPath,
|
|
553
|
+
compatEntries: [fixture.compatEntry],
|
|
554
|
+
stopServiceAfterRepair: true,
|
|
555
|
+
startDetachedService: async () => {
|
|
556
|
+
await writeRuntimeInfo(
|
|
557
|
+
{ host: '127.0.0.1', port: 45203, runtimeId: 'runtime-started-by-repair' },
|
|
558
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
559
|
+
);
|
|
560
|
+
await writeRuntimeInfo(
|
|
561
|
+
{ host: '127.0.0.1', port: 45204, runtimeId: 'runtime-written-by-other-owner' },
|
|
562
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
563
|
+
);
|
|
564
|
+
return { host: '127.0.0.1', port: 45203, runtimeId: 'runtime-started-by-repair' };
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const runtimeBytes = await readFile(fixture.runtimeFile, 'utf8');
|
|
569
|
+
|
|
570
|
+
assert.match(output, /service: repaired/);
|
|
571
|
+
assert.match(runtimeBytes, /runtime-written-by-other-owner/);
|
|
572
|
+
} finally {
|
|
573
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
574
|
+
}
|
|
575
|
+
});
|