@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.
Files changed (105) hide show
  1. package/bin/cursor-pool.mjs +9 -0
  2. package/bin/cursor-pool.ts +169 -0
  3. package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
  4. package/node_modules/@cursor-pool/extension/package.json +64 -0
  5. package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
  6. package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
  7. package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
  8. package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
  9. package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
  10. package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
  11. package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
  12. package/node_modules/@cursor-pool/patcher/package.json +17 -0
  13. package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
  14. package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
  15. package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
  16. package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
  17. package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
  18. package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
  19. package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
  20. package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
  21. package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
  22. package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
  23. package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
  24. package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
  25. package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
  26. package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
  27. package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
  28. package/node_modules/@cursor-pool/service/package.json +17 -0
  29. package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
  30. package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
  31. package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
  32. package/node_modules/@cursor-pool/service/src/health.ts +10 -0
  33. package/node_modules/@cursor-pool/service/src/index.ts +29 -0
  34. package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
  35. package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
  36. package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
  37. package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
  38. package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
  39. package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
  40. package/node_modules/@cursor-pool/service/src/server.ts +939 -0
  41. package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
  42. package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
  43. package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
  44. package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
  45. package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
  46. package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
  47. package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
  48. package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
  49. package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
  50. package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
  51. package/node_modules/@cursor-pool/shared/package.json +17 -0
  52. package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
  53. package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
  54. package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
  55. package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
  56. package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
  57. package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
  58. package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
  59. package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
  60. package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
  61. package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
  62. package/package.json +28 -0
  63. package/src/adHocResign.ts +65 -0
  64. package/src/autostart.ts +240 -0
  65. package/src/compat.ts +282 -0
  66. package/src/confirm.ts +76 -0
  67. package/src/cursor.ts +94 -0
  68. package/src/diagnostics.ts +558 -0
  69. package/src/environment.ts +18 -0
  70. package/src/extensionBundle.ts +111 -0
  71. package/src/extensionLink.ts +168 -0
  72. package/src/index.ts +23 -0
  73. package/src/install.ts +614 -0
  74. package/src/installRecord.ts +105 -0
  75. package/src/launch.ts +182 -0
  76. package/src/patchSet.ts +182 -0
  77. package/src/platform.ts +132 -0
  78. package/src/repair.ts +383 -0
  79. package/src/restore.ts +153 -0
  80. package/src/serviceCommands.ts +79 -0
  81. package/src/serviceProcess.ts +188 -0
  82. package/src/status.ts +241 -0
  83. package/src/target.ts +37 -0
  84. package/src/trial.ts +133 -0
  85. package/src/uninstall.ts +213 -0
  86. package/test/autostart.test.ts +151 -0
  87. package/test/compat.test.ts +192 -0
  88. package/test/confirm.test.ts +114 -0
  89. package/test/cursor-pool-bin.test.ts +658 -0
  90. package/test/cursor.test.ts +20 -0
  91. package/test/diagnostics.test.ts +709 -0
  92. package/test/e2e-install.test.ts +773 -0
  93. package/test/extensionBundle.test.ts +161 -0
  94. package/test/extensionLink.test.ts +209 -0
  95. package/test/install.test.ts +862 -0
  96. package/test/installRecord.test.ts +107 -0
  97. package/test/launch.test.ts +138 -0
  98. package/test/platform.test.ts +226 -0
  99. package/test/repair.test.ts +575 -0
  100. package/test/restore.test.ts +211 -0
  101. package/test/serviceCommands.test.ts +135 -0
  102. package/test/serviceProcess.test.ts +280 -0
  103. package/test/status.test.ts +615 -0
  104. package/test/target.test.ts +49 -0
  105. package/test/trial.test.ts +146 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@cursor-pool/shared",
3
+ "version": "0.5.6",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./clientConfig": "./src/clientConfig.ts",
9
+ "./manifest": "./src/manifest.ts",
10
+ "./metadata": "./src/metadata.ts",
11
+ "./paths": "./src/paths.ts",
12
+ "./runtime": "./src/runtime.ts"
13
+ },
14
+ "scripts": {
15
+ "test": "tsx --test test/*.test.ts"
16
+ }
17
+ }
@@ -0,0 +1,49 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+
5
+ export const DEFAULT_CLIENT_CONFIG_FILE = '~/.cursor-pool/client-config.json';
6
+
7
+ export type ClientConfig = {
8
+ apiBaseUrl?: string;
9
+ };
10
+
11
+ export type ClientConfigOptions = {
12
+ configFile?: string;
13
+ };
14
+
15
+ export function resolveClientConfigFile(configFile = DEFAULT_CLIENT_CONFIG_FILE) {
16
+ if (configFile.startsWith('~/')) {
17
+ return join(homedir(), configFile.slice(2));
18
+ }
19
+ return configFile;
20
+ }
21
+
22
+ function normalizeConfig(value: unknown): ClientConfig {
23
+ const record = value as ClientConfig;
24
+ return {
25
+ ...(typeof record?.apiBaseUrl === 'string' && record.apiBaseUrl.trim() !== ''
26
+ ? { apiBaseUrl: record.apiBaseUrl.trim().replace(/\/+$/, '') }
27
+ : {}),
28
+ };
29
+ }
30
+
31
+ export async function readClientConfig(options: ClientConfigOptions = {}): Promise<ClientConfig> {
32
+ try {
33
+ return normalizeConfig(JSON.parse(await readFile(resolveClientConfigFile(options.configFile), 'utf8')));
34
+ } catch (error) {
35
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' || error instanceof SyntaxError) {
36
+ return {};
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ export async function writeClientConfig(
43
+ config: ClientConfig,
44
+ options: ClientConfigOptions = {},
45
+ ) {
46
+ const configFile = resolveClientConfigFile(options.configFile);
47
+ await mkdir(dirname(configFile), { recursive: true });
48
+ await writeFile(configFile, `${JSON.stringify(normalizeConfig(config), null, 2)}\n`, 'utf8');
49
+ }
@@ -0,0 +1,14 @@
1
+ export const workspacePackages = [
2
+ 'packages/shared',
3
+ 'packages/cli',
4
+ 'packages/service',
5
+ 'packages/patcher',
6
+ 'packages/extension',
7
+ ] as const;
8
+ export {
9
+ DEFAULT_CLIENT_CONFIG_FILE,
10
+ readClientConfig,
11
+ resolveClientConfigFile,
12
+ writeClientConfig,
13
+ } from './clientConfig';
14
+ export type { ClientConfig, ClientConfigOptions } from './clientConfig';
@@ -0,0 +1,36 @@
1
+ export type CompatibilityStatus =
2
+ | 'supported'
3
+ | 'canary'
4
+ | 'warning'
5
+ | 'blocked'
6
+ | 'unknown';
7
+
8
+ export interface CompatibilityManifestEntry {
9
+ platform: string;
10
+ arch: string;
11
+ cursorVersion: string;
12
+ cursorCommit: string;
13
+ supportStatus: CompatibilityStatus;
14
+ targetRelativePath: string;
15
+ expectedSha256: string;
16
+ structureSignature: string;
17
+ patchStrategy: string;
18
+ verifyMarker: string;
19
+ restoreStrategy: string;
20
+ minCliVersion: string;
21
+ minExtensionVersion: string;
22
+ minServiceVersion: string;
23
+ requiresWritableAppBundle: boolean;
24
+ requiresAdHocResign: boolean;
25
+ userMessage: string;
26
+ }
27
+
28
+ export type CompatibilityManifestSignatureAlgorithm = 'hmac-sha256-dev';
29
+
30
+ export interface CompatibilityManifestEnvelope {
31
+ version: number;
32
+ signature: string;
33
+ signatureAlgorithm: CompatibilityManifestSignatureAlgorithm;
34
+ signatureKeyId: string;
35
+ rules: CompatibilityManifestEntry[];
36
+ }
@@ -0,0 +1,19 @@
1
+ export const SAFE_METADATA_KEYS = [
2
+ 'requestId',
3
+ 'model',
4
+ 'requestType',
5
+ 'source',
6
+ 'cursorVersion',
7
+ 'clientVersion',
8
+ 'entrypoint',
9
+ ] as const;
10
+
11
+ export function sanitizeRequestMetadata(input: Record<string, unknown>) {
12
+ const output: Record<string, unknown> = {};
13
+ for (const key of SAFE_METADATA_KEYS) {
14
+ if (input[key] !== undefined) {
15
+ output[key] = input[key];
16
+ }
17
+ }
18
+ return output;
19
+ }
@@ -0,0 +1,5 @@
1
+ import path from 'node:path';
2
+
3
+ export function joinCursorAppPath(appPath: string, relativePath: string) {
4
+ return path.join(appPath, relativePath);
5
+ }
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_RUNTIME_DIR = '~/.cursor-pool';
2
+ export const DEFAULT_RUNTIME_FILE = '~/.cursor-pool/runtime.json';
3
+ export const DEFAULT_DIAGNOSTICS_FILE = '~/.cursor-pool/diagnostics.jsonl';
@@ -0,0 +1,56 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import test from 'node:test';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../../..');
8
+ const packageDirs = ['shared', 'cli', 'service', 'patcher', 'extension'];
9
+
10
+ function readJson(path: string) {
11
+ return JSON.parse(readFileSync(join(repoRoot, path), 'utf8'));
12
+ }
13
+
14
+ function assertEntrypointExists(packageDir: string, target: string, label: string) {
15
+ const normalizedTarget = target.replace(/^\.\//, '');
16
+ const entrypointPath = join(repoRoot, 'packages', packageDir, normalizedTarget);
17
+
18
+ assert.equal(existsSync(entrypointPath), true, `${label} target ${target} exists`);
19
+ }
20
+
21
+ test('root workspace exposes the minimal MVP-0 package skeleton', () => {
22
+ const rootPackage = readJson('package.json');
23
+
24
+ assert.equal(rootPackage.name, 'cursor-pool-platform');
25
+ assert.equal(rootPackage.private, true);
26
+ assert.match(rootPackage.scripts?.test ?? '', /tsx --test/);
27
+ assert.ok(rootPackage.devDependencies?.tsx);
28
+ assert.ok(rootPackage.devDependencies?.typescript);
29
+
30
+ const workspace = readFileSync(join(repoRoot, 'pnpm-workspace.yaml'), 'utf8');
31
+ assert.match(workspace, /packages\/\*/);
32
+
33
+ for (const packageDir of packageDirs) {
34
+ const manifestPath = `packages/${packageDir}/package.json`;
35
+ assert.equal(existsSync(join(repoRoot, manifestPath)), true, `${manifestPath} exists`);
36
+
37
+ const manifest = readJson(manifestPath);
38
+ assert.match(manifest.name, /^@cursor-pool\/[a-z-]+$/);
39
+ assert.equal(typeof manifest.version, 'string');
40
+ assert.equal(manifest.type, 'module');
41
+ assert.ok(manifest.main || manifest.exports, `${manifestPath} exposes an entrypoint`);
42
+ if (manifest.main) {
43
+ assertEntrypointExists(packageDir, manifest.main, `${manifestPath} main`);
44
+ }
45
+ if (manifest.exports?.['.']) {
46
+ assertEntrypointExists(packageDir, manifest.exports['.'], `${manifestPath} exports["."]`);
47
+ }
48
+ assert.match(manifest.scripts?.test ?? '', /tsx --test/);
49
+ }
50
+
51
+ const sharedEntryPath = 'packages/shared/src/index.ts';
52
+ assert.equal(existsSync(join(repoRoot, sharedEntryPath)), true, `${sharedEntryPath} exists`);
53
+
54
+ const sharedEntry = readFileSync(join(repoRoot, sharedEntryPath), 'utf8');
55
+ assert.match(sharedEntry, /export\s+(const|function|type|interface|class)\s+\w+/);
56
+ });
@@ -0,0 +1,65 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import type {
4
+ CompatibilityManifestEnvelope,
5
+ CompatibilityManifestEntry,
6
+ CompatibilityStatus,
7
+ } from '../src/manifest';
8
+
9
+ test('compatibility manifest entry captures patch compatibility metadata', () => {
10
+ const supportStatus: CompatibilityStatus = 'supported';
11
+ const entry: CompatibilityManifestEntry = {
12
+ platform: 'darwin',
13
+ arch: 'arm64',
14
+ cursorVersion: '1.0.0',
15
+ cursorCommit: 'abc123',
16
+ supportStatus,
17
+ targetRelativePath: 'Contents/Resources/app/out/main.js',
18
+ expectedSha256: 'sha256',
19
+ structureSignature: 'signature',
20
+ patchStrategy: 'replace-marker',
21
+ verifyMarker: 'cursor-pool',
22
+ restoreStrategy: 'backup',
23
+ minCliVersion: '0.5.6',
24
+ minExtensionVersion: '0.5.6',
25
+ minServiceVersion: '0.5.6',
26
+ requiresWritableAppBundle: true,
27
+ requiresAdHocResign: true,
28
+ userMessage: 'Supported Cursor build.',
29
+ };
30
+
31
+ assert.equal(entry.supportStatus, 'supported');
32
+ assert.equal(entry.targetRelativePath, 'Contents/Resources/app/out/main.js');
33
+ });
34
+
35
+ test('compatibility manifest envelope captures signed rule payload', () => {
36
+ const rule: CompatibilityManifestEntry = {
37
+ platform: 'darwin',
38
+ arch: 'arm64',
39
+ cursorVersion: '1.0.0',
40
+ cursorCommit: 'abc123',
41
+ supportStatus: 'supported',
42
+ targetRelativePath: 'Contents/Resources/app/out/main.js',
43
+ expectedSha256: 'sha256',
44
+ structureSignature: 'signature',
45
+ patchStrategy: 'replace-marker',
46
+ verifyMarker: 'cursor-pool',
47
+ restoreStrategy: 'backup',
48
+ minCliVersion: '0.5.6',
49
+ minExtensionVersion: '0.5.6',
50
+ minServiceVersion: '0.5.6',
51
+ requiresWritableAppBundle: true,
52
+ requiresAdHocResign: false,
53
+ userMessage: 'Supported Cursor build.',
54
+ };
55
+ const envelope: CompatibilityManifestEnvelope = {
56
+ version: 7,
57
+ signature: 'signed',
58
+ signatureAlgorithm: 'hmac-sha256-dev',
59
+ signatureKeyId: 'dev-compatibility-key',
60
+ rules: [rule],
61
+ };
62
+
63
+ assert.equal(envelope.version, 7);
64
+ assert.equal(envelope.rules[0]?.cursorVersion, '1.0.0');
65
+ });
@@ -0,0 +1,25 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { sanitizeRequestMetadata } from '../src/metadata';
4
+
5
+ test('sanitizeRequestMetadata keeps only safe cursor metadata', () => {
6
+ const result = sanitizeRequestMetadata({
7
+ requestId: 'req-1',
8
+ model: 'claude-unknown',
9
+ requestType: 'agent',
10
+ source: 'cursor-agent-exec',
11
+ prompt: 'secret prompt',
12
+ messages: ['hidden'],
13
+ cursorAuthToken: 'token',
14
+ providerSecret: 'secret',
15
+ apiKey: 'key',
16
+ authorization: 'bearer secret',
17
+ });
18
+
19
+ assert.deepEqual(result, {
20
+ requestId: 'req-1',
21
+ model: 'claude-unknown',
22
+ requestType: 'agent',
23
+ source: 'cursor-agent-exec',
24
+ });
25
+ });
@@ -0,0 +1,8 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { DEFAULT_RUNTIME_DIR, DEFAULT_RUNTIME_FILE } from '../src/runtime';
4
+
5
+ test('runtime constants use the cursor pool runtime path convention', () => {
6
+ assert.equal(DEFAULT_RUNTIME_DIR, '~/.cursor-pool');
7
+ assert.equal(DEFAULT_RUNTIME_FILE, '~/.cursor-pool/runtime.json');
8
+ });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@cursorpool-dev/cli",
3
+ "version": "0.5.6",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "bin": {
7
+ "cursor-pool": "./bin/cursor-pool.mjs"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.ts"
11
+ },
12
+ "dependencies": {
13
+ "tsx": "^4.22.3",
14
+ "@cursor-pool/patcher": "0.5.6",
15
+ "@cursor-pool/service": "0.5.6",
16
+ "@cursor-pool/shared": "0.5.6",
17
+ "@cursor-pool/extension": "0.5.6"
18
+ },
19
+ "bundledDependencies": [
20
+ "@cursor-pool/extension",
21
+ "@cursor-pool/patcher",
22
+ "@cursor-pool/service",
23
+ "@cursor-pool/shared"
24
+ ],
25
+ "scripts": {
26
+ "test": "tsx --test test/*.test.ts"
27
+ }
28
+ }
@@ -0,0 +1,65 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export const AD_HOC_RESIGN_ENTITLEMENTS = `<?xml version="1.0" encoding="UTF-8"?>
10
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
11
+ <plist version="1.0">
12
+ <dict>
13
+ <key>com.apple.security.automation.apple-events</key><true/>
14
+ <key>com.apple.security.cs.allow-jit</key><true/>
15
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
16
+ <key>com.apple.security.cs.disable-library-validation</key><true/>
17
+ <key>com.apple.security.device.audio-input</key><true/>
18
+ <key>com.apple.security.device.camera</key><true/>
19
+ </dict>
20
+ </plist>
21
+ `;
22
+
23
+ export type AdHocResignOptions = {
24
+ execFile?: typeof execFileAsync;
25
+ };
26
+
27
+ function errorOutput(error: unknown) {
28
+ if (error instanceof Error) {
29
+ const details = [
30
+ error.message,
31
+ 'stdout' in error && typeof error.stdout === 'string' ? error.stdout : '',
32
+ 'stderr' in error && typeof error.stderr === 'string' ? error.stderr : '',
33
+ ]
34
+ .filter(Boolean)
35
+ .join('\n');
36
+ return details;
37
+ }
38
+ return String(error);
39
+ }
40
+
41
+ export async function adHocResignApp(appPath: string, options: AdHocResignOptions = {}) {
42
+ const run = options.execFile ?? execFileAsync;
43
+ const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-entitlements-'));
44
+ const entitlementsPath = join(tempDir, 'entitlements.plist');
45
+
46
+ try {
47
+ await writeFile(entitlementsPath, AD_HOC_RESIGN_ENTITLEMENTS, 'utf8');
48
+ await run('codesign', [
49
+ '--force',
50
+ '--deep',
51
+ '--options',
52
+ 'runtime',
53
+ '--entitlements',
54
+ entitlementsPath,
55
+ '--sign',
56
+ '-',
57
+ appPath,
58
+ ]);
59
+ await run('codesign', ['--verify', '--deep', '--strict', '--verbose=2', appPath]);
60
+ } catch (error) {
61
+ throw new Error(`Ad-hoc resign failed: ${errorOutput(error)}`, { cause: error });
62
+ } finally {
63
+ await rm(tempDir, { recursive: true, force: true });
64
+ }
65
+ }
@@ -0,0 +1,240 @@
1
+ import { execFile as execFileCallback } from 'node:child_process';
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { homedir, userInfo } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { DEFAULT_CLIENT_CONFIG_FILE, resolveClientConfigFile } from '@cursor-pool/shared/clientConfig';
7
+ import { DEFAULT_RUNTIME_FILE } from '@cursor-pool/shared/runtime';
8
+
9
+ export type AutostartResult = { state: 'installed' | 'removed' | 'skipped'; reason?: string };
10
+ export type ExecFile = (
11
+ command: string,
12
+ args: string[],
13
+ ) => Promise<{ stdout: string; stderr: string }>;
14
+
15
+ export type UserAutostartOptions = {
16
+ platform?: NodeJS.Platform;
17
+ launchAgentFile?: string;
18
+ windowsStartupFile?: string;
19
+ linuxServiceFile?: string;
20
+ cliCommand?: string;
21
+ nodeCommand?: string;
22
+ cliEntry?: string;
23
+ runtimeFile?: string;
24
+ serviceLogFile?: string;
25
+ configFile?: string;
26
+ tsxLoader?: string;
27
+ serviceEntry?: string;
28
+ uid?: number;
29
+ execFile?: ExecFile;
30
+ };
31
+
32
+ const execFileAsync = promisify(execFileCallback) as ExecFile;
33
+ const SERVICE_LABEL = 'com.cursor-pool.service';
34
+ const DEFAULT_SERVICE_LOG_FILE = '~/.cursor-pool/logs/service.log';
35
+
36
+ function resolveTildePath(path: string) {
37
+ if (path.startsWith('~/')) {
38
+ return join(homedir(), path.slice(2));
39
+ }
40
+ return path;
41
+ }
42
+
43
+ function defaultCliCommand() {
44
+ return join(homedir(), '.cursor-pool/client/node_modules/.bin/cursor-pool');
45
+ }
46
+
47
+ function defaultCliEntry() {
48
+ return join(homedir(), '.cursor-pool/client/node_modules/@cursorpool-dev/cli/bin/cursor-pool.mjs');
49
+ }
50
+
51
+ function defaultTsxLoader() {
52
+ return join(homedir(), '.cursor-pool/client/node_modules/tsx/dist/esm/index.mjs');
53
+ }
54
+
55
+ function defaultServiceEntry() {
56
+ return join(
57
+ homedir(),
58
+ '.cursor-pool/client/node_modules/@cursorpool-dev/cli/node_modules/@cursor-pool/service/src/entry.ts',
59
+ );
60
+ }
61
+
62
+ function defaultLaunchAgentFile() {
63
+ return join(homedir(), 'Library/LaunchAgents/com.cursor-pool.service.plist');
64
+ }
65
+
66
+ function defaultWindowsStartupFile() {
67
+ return join(
68
+ process.env.APPDATA ?? join(homedir(), 'AppData/Roaming'),
69
+ 'Microsoft/Windows/Start Menu/Programs/Startup/CursorPoolService.cmd',
70
+ );
71
+ }
72
+
73
+ function defaultLinuxServiceFile() {
74
+ return join(homedir(), '.config/systemd/user/cursor-pool.service');
75
+ }
76
+
77
+ function launchctlDomain(uid = userInfo().uid) {
78
+ return `gui/${uid}`;
79
+ }
80
+
81
+ function xmlEscape(value: string) {
82
+ return value
83
+ .replaceAll('&', '&amp;')
84
+ .replaceAll('<', '&lt;')
85
+ .replaceAll('>', '&gt;')
86
+ .replaceAll('"', '&quot;')
87
+ .replaceAll("'", '&apos;');
88
+ }
89
+
90
+ function launchAgentPlist(options: Required<Pick<UserAutostartOptions,
91
+ 'cliCommand' | 'nodeCommand' | 'cliEntry' | 'runtimeFile' | 'serviceLogFile' | 'configFile' | 'tsxLoader' | 'serviceEntry'
92
+ >>) {
93
+ const args = serviceArgs(options);
94
+ return `<?xml version="1.0" encoding="UTF-8"?>
95
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
96
+ <plist version="1.0">
97
+ <dict>
98
+ <key>Label</key>
99
+ <string>${SERVICE_LABEL}</string>
100
+ <key>ProgramArguments</key>
101
+ <array>
102
+ ${args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n')}
103
+ </array>
104
+ <key>RunAtLoad</key>
105
+ <true/>
106
+ <key>KeepAlive</key>
107
+ <true/>
108
+ <key>StandardOutPath</key>
109
+ <string>${xmlEscape(resolveTildePath(options.serviceLogFile))}</string>
110
+ <key>StandardErrorPath</key>
111
+ <string>${xmlEscape(resolveTildePath(options.serviceLogFile))}</string>
112
+ </dict>
113
+ </plist>
114
+ `;
115
+ }
116
+
117
+ function serviceArgs(options: Required<Pick<UserAutostartOptions,
118
+ 'cliCommand' | 'nodeCommand' | 'cliEntry' | 'runtimeFile' | 'serviceLogFile' | 'configFile' | 'tsxLoader' | 'serviceEntry'
119
+ >>) {
120
+ return [
121
+ options.nodeCommand,
122
+ '--import',
123
+ options.tsxLoader,
124
+ options.serviceEntry,
125
+ '--runtime-file',
126
+ options.runtimeFile,
127
+ '--service-log-file',
128
+ options.serviceLogFile,
129
+ '--config-file',
130
+ options.configFile,
131
+ ];
132
+ }
133
+
134
+ function windowsStartupScript(options: Required<Pick<UserAutostartOptions,
135
+ 'cliCommand' | 'nodeCommand' | 'cliEntry' | 'runtimeFile' | 'serviceLogFile' | 'configFile' | 'tsxLoader' | 'serviceEntry'
136
+ >>) {
137
+ const command = serviceArgs(options)
138
+ .map((arg) => `"${arg.replaceAll('"', '""')}"`)
139
+ .join(' ');
140
+ return `@echo off\r\nstart "" /min ${command}\r\n`;
141
+ }
142
+
143
+ function linuxSystemdService(options: Required<Pick<UserAutostartOptions,
144
+ 'cliCommand' | 'nodeCommand' | 'cliEntry' | 'runtimeFile' | 'serviceLogFile' | 'configFile' | 'tsxLoader' | 'serviceEntry'
145
+ >>) {
146
+ const execStart = serviceArgs(options)
147
+ .map((arg) => arg.includes(' ') ? `"${arg.replaceAll('"', '\\"')}"` : arg)
148
+ .join(' ');
149
+ return `[Unit]
150
+ Description=Cursor Pool local service
151
+
152
+ [Service]
153
+ Type=simple
154
+ ExecStart=${execStart}
155
+ Restart=always
156
+ RestartSec=3
157
+
158
+ [Install]
159
+ WantedBy=default.target
160
+ `;
161
+ }
162
+
163
+ export async function installUserAutostart(
164
+ options: UserAutostartOptions = {},
165
+ ): Promise<AutostartResult> {
166
+ const platform = options.platform ?? process.platform;
167
+ const serviceOptions = {
168
+ cliCommand: options.cliCommand ?? defaultCliCommand(),
169
+ nodeCommand: options.nodeCommand ?? process.execPath,
170
+ cliEntry: options.cliEntry ?? defaultCliEntry(),
171
+ tsxLoader: options.tsxLoader ?? defaultTsxLoader(),
172
+ serviceEntry: options.serviceEntry ?? defaultServiceEntry(),
173
+ runtimeFile: options.runtimeFile ?? DEFAULT_RUNTIME_FILE,
174
+ serviceLogFile: options.serviceLogFile ?? DEFAULT_SERVICE_LOG_FILE,
175
+ configFile: options.configFile ?? DEFAULT_CLIENT_CONFIG_FILE,
176
+ };
177
+
178
+ if (platform === 'win32') {
179
+ const startupFile = options.windowsStartupFile ?? defaultWindowsStartupFile();
180
+ await mkdir(dirname(startupFile), { recursive: true });
181
+ await writeFile(startupFile, windowsStartupScript(serviceOptions), 'utf8');
182
+ return { state: 'installed' };
183
+ }
184
+
185
+ if (platform === 'linux') {
186
+ const serviceFile = options.linuxServiceFile ?? defaultLinuxServiceFile();
187
+ const execFile = options.execFile ?? execFileAsync;
188
+ await mkdir(dirname(serviceFile), { recursive: true });
189
+ await writeFile(serviceFile, linuxSystemdService(serviceOptions), 'utf8');
190
+ await execFile('systemctl', ['--user', 'daemon-reload']).catch(() => ({ stdout: '', stderr: '' }));
191
+ await execFile('systemctl', ['--user', 'enable', '--now', 'cursor-pool.service']).catch(() => ({ stdout: '', stderr: '' }));
192
+ return { state: 'installed' };
193
+ }
194
+
195
+ if (platform !== 'darwin') {
196
+ return { state: 'skipped', reason: 'unsupported-platform' };
197
+ }
198
+
199
+ const launchAgentFile = options.launchAgentFile ?? defaultLaunchAgentFile();
200
+ const execFile = options.execFile ?? execFileAsync;
201
+ const domain = launchctlDomain(options.uid);
202
+ const plist = launchAgentPlist(serviceOptions);
203
+
204
+ await mkdir(dirname(launchAgentFile), { recursive: true });
205
+ await writeFile(launchAgentFile, plist, 'utf8');
206
+ await execFile('launchctl', ['bootstrap', domain, launchAgentFile]).catch(() => ({ stdout: '', stderr: '' }));
207
+ await execFile('launchctl', ['kickstart', '-k', `${domain}/${SERVICE_LABEL}`]);
208
+ return { state: 'installed' };
209
+ }
210
+
211
+ export async function removeUserAutostart(
212
+ options: UserAutostartOptions = {},
213
+ ): Promise<AutostartResult> {
214
+ const platform = options.platform ?? process.platform;
215
+ if (platform === 'win32') {
216
+ await rm(options.windowsStartupFile ?? defaultWindowsStartupFile(), { force: true });
217
+ return { state: 'removed' };
218
+ }
219
+ if (platform === 'linux') {
220
+ const execFile = options.execFile ?? execFileAsync;
221
+ await execFile('systemctl', ['--user', 'disable', '--now', 'cursor-pool.service']).catch(() => ({ stdout: '', stderr: '' }));
222
+ await rm(options.linuxServiceFile ?? defaultLinuxServiceFile(), { force: true });
223
+ await execFile('systemctl', ['--user', 'daemon-reload']).catch(() => ({ stdout: '', stderr: '' }));
224
+ return { state: 'removed' };
225
+ }
226
+ if (platform !== 'darwin') {
227
+ return { state: 'skipped', reason: 'unsupported-platform' };
228
+ }
229
+
230
+ const launchAgentFile = options.launchAgentFile ?? defaultLaunchAgentFile();
231
+ const execFile = options.execFile ?? execFileAsync;
232
+ const domain = launchctlDomain(options.uid);
233
+ await execFile('launchctl', ['bootout', `${domain}/${SERVICE_LABEL}`]).catch(() => ({ stdout: '', stderr: '' }));
234
+ await rm(launchAgentFile, { force: true });
235
+ return { state: 'removed' };
236
+ }
237
+
238
+ export function resolveAutostartConfigFile(configFile = DEFAULT_CLIENT_CONFIG_FILE) {
239
+ return resolveClientConfigFile(configFile);
240
+ }