freestyle-sync 0.1.2 → 0.1.4

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 (45) hide show
  1. package/README.md +2 -1
  2. package/{freestyle-sync.config.ts → dist/freestyle-sync.config.js} +2 -3
  3. package/dist/main.js +1319 -0
  4. package/{plugins/agent-claude/src/index.ts → dist/plugins/agent-claude/src/index.js} +32 -29
  5. package/{plugins/agent-codex/src/index.ts → dist/plugins/agent-codex/src/index.js} +13 -14
  6. package/{plugins/agent-copilot/src/index.ts → dist/plugins/agent-copilot/src/index.js} +151 -164
  7. package/{plugins/auth-aws/src/index.ts → dist/plugins/auth-aws/src/index.js} +14 -8
  8. package/{plugins/auth-azure/src/index.ts → dist/plugins/auth-azure/src/index.js} +14 -8
  9. package/dist/plugins/auth-context.js +213 -0
  10. package/{plugins/auth-docker/src/index.ts → dist/plugins/auth-docker/src/index.js} +14 -8
  11. package/{plugins/auth-env/src/index.ts → dist/plugins/auth-env/src/index.js} +11 -11
  12. package/{plugins/auth-gcloud/src/index.ts → dist/plugins/auth-gcloud/src/index.js} +14 -8
  13. package/{plugins/auth-git/src/index.ts → dist/plugins/auth-git/src/index.js} +24 -17
  14. package/{plugins/auth-github-cli/src/index.ts → dist/plugins/auth-github-cli/src/index.js} +20 -14
  15. package/{plugins/auth-npm/src/index.ts → dist/plugins/auth-npm/src/index.js} +19 -13
  16. package/{plugins/auth-ssh/src/index.ts → dist/plugins/auth-ssh/src/index.js} +19 -13
  17. package/dist/plugins/auth-yarn/src/index.js +24 -0
  18. package/{plugins/node-npm/src/index.ts → dist/plugins/node-npm/src/index.js} +6 -8
  19. package/dist/plugins/npm-native-deps.js +307 -0
  20. package/{plugins/shell-history/src/index.ts → dist/plugins/shell-history/src/index.js} +13 -12
  21. package/{plugins/vscode/src/index.ts → dist/plugins/vscode/src/index.js} +38 -40
  22. package/{src/main.ts → dist/src/main.js} +406 -463
  23. package/dist/src/plugin-api.js +6 -0
  24. package/dist/src/pushvm.config.js +36 -0
  25. package/package.json +8 -4
  26. package/PUBLISHING.md +0 -3
  27. package/plugins/agent-claude/package.json +0 -8
  28. package/plugins/agent-codex/package.json +0 -8
  29. package/plugins/agent-copilot/package.json +0 -8
  30. package/plugins/auth-aws/package.json +0 -8
  31. package/plugins/auth-azure/package.json +0 -8
  32. package/plugins/auth-docker/package.json +0 -8
  33. package/plugins/auth-env/package.json +0 -8
  34. package/plugins/auth-gcloud/package.json +0 -8
  35. package/plugins/auth-git/package.json +0 -8
  36. package/plugins/auth-github-cli/package.json +0 -8
  37. package/plugins/auth-npm/package.json +0 -8
  38. package/plugins/auth-ssh/package.json +0 -8
  39. package/plugins/auth-yarn/package.json +0 -8
  40. package/plugins/auth-yarn/src/index.ts +0 -19
  41. package/plugins/node-npm/package.json +0 -8
  42. package/plugins/shell-history/package.json +0 -8
  43. package/plugins/vscode/package.json +0 -8
  44. package/src/plugin-api.ts +0 -107
  45. package/tsconfig.json +0 -18
@@ -1,9 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { definePlugin } from "../../../src/plugin-api.ts";
4
-
3
+ import { definePlugin } from "../../../src/plugin-api.js";
5
4
  const execFileAsync = promisify(execFile);
6
-
7
5
  export function nodeNpmPlugin() {
8
6
  return definePlugin({
9
7
  name: "@freestyle-sync/node-npm",
@@ -370,21 +368,21 @@ cp "$fingerprint_file" "$cache_file"
370
368
  if (result.statusCode && result.statusCode !== 0) {
371
369
  throw new Error(result.stderr ?? result.stdout ?? `Node/npm fixup failed with status ${result.statusCode}`);
372
370
  }
373
- if (result.stdout?.trim()) console.log(result.stdout.trim());
371
+ if (result.stdout?.trim())
372
+ console.log(result.stdout.trim());
374
373
  },
375
374
  });
376
375
  }
377
-
378
376
  async function localNpmVersion() {
379
377
  try {
380
378
  const { stdout } = await execFileAsync("npm", ["--version"]);
381
379
  const version = stdout.trim();
382
380
  return version.length > 0 ? version : undefined;
383
- } catch {
381
+ }
382
+ catch {
384
383
  return undefined;
385
384
  }
386
385
  }
387
-
388
- function shellQuote(value: string): string {
386
+ function shellQuote(value) {
389
387
  return `'${value.replace(/'/g, `'"'"'`)}'`;
390
388
  }
@@ -0,0 +1,307 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ export const npmNativeDepsPlugin = {
5
+ name: "npm-native-deps",
6
+ async afterProjectSync({ vm, options }) {
7
+ const nodeVersion = process.versions.node;
8
+ const npmVersion = await localNpmVersion();
9
+ const script = `
10
+ set -eu
11
+ cd ${shellQuote(options.remoteProjectDir)}
12
+ if [ ! -f package.json ]; then
13
+ exit 0
14
+ fi
15
+ desired_node=${shellQuote(nodeVersion)}
16
+ desired_npm=${shellQuote(npmVersion ?? "")}
17
+ nvm_version=v0.40.3
18
+ export NVM_DIR=/root/.nvm
19
+ runtime_cache=.vmpush/node-runtime-cache.json
20
+
21
+ install_nvm() {
22
+ if [ -s "$NVM_DIR/nvm.sh" ]; then
23
+ return 0
24
+ fi
25
+ mkdir -p "$NVM_DIR"
26
+ if command -v git >/dev/null 2>&1; then
27
+ rm -rf "$NVM_DIR"
28
+ git clone --depth 1 --branch "$nvm_version" https://github.com/nvm-sh/nvm.git "$NVM_DIR"
29
+ elif command -v curl >/dev/null 2>&1; then
30
+ curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/$nvm_version/install.sh" | PROFILE=/dev/null bash
31
+ elif command -v wget >/dev/null 2>&1; then
32
+ wget -qO- "https://raw.githubusercontent.com/nvm-sh/nvm/$nvm_version/install.sh" | PROFILE=/dev/null bash
33
+ else
34
+ echo "git, curl, or wget is required to install nvm" >&2
35
+ return 1
36
+ fi
37
+ }
38
+
39
+ install_nvm
40
+ . "$NVM_DIR/nvm.sh"
41
+ installed_node=$(nvm version "$desired_node" || true)
42
+ if [ "$installed_node" = "N/A" ]; then
43
+ echo "Installing Node $desired_node with nvm"
44
+ nvm install "$desired_node"
45
+ fi
46
+ nvm alias default "$desired_node" >/dev/null
47
+ nvm use --silent "$desired_node"
48
+ actual_node=$(node -p 'process.versions.node')
49
+ if [ "$actual_node" != "$desired_node" ]; then
50
+ echo "Expected Node $desired_node but activated $actual_node" >&2
51
+ exit 1
52
+ fi
53
+ actual_npm=$(npm --version 2>/dev/null || true)
54
+ if [ -n "$desired_npm" ] && [ "$actual_npm" != "$desired_npm" ]; then
55
+ echo "Installing npm $desired_npm for Node $desired_node"
56
+ npm install -g "npm@$desired_npm"
57
+ actual_npm=$(npm --version)
58
+ fi
59
+ mkdir -p .vmpush
60
+ printf '{"node":"%s","npm":"%s","nvm":"%s"}\n' "$actual_node" "$actual_npm" "$nvm_version" > "$runtime_cache"
61
+ grep -qxF 'export NVM_DIR=/root/.nvm' /root/.profile || printf '\n%s\n' 'export NVM_DIR=/root/.nvm' >> /root/.profile
62
+ grep -qxF '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use --silent default >/dev/null 2>&1' /root/.profile || printf '%s\n' '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use --silent default >/dev/null 2>&1' >> /root/.profile
63
+
64
+ if [ ! -d node_modules ]; then
65
+ exit 0
66
+ fi
67
+ package_list=$(mktemp /tmp/vmpush-native-packages.XXXXXX)
68
+ fingerprint_file=$(mktemp /tmp/vmpush-native-fingerprints.XXXXXX)
69
+ scanner_file=$(mktemp /tmp/vmpush-native-scanner.XXXXXX.js)
70
+ cache_file=.vmpush/npm-native-deps-cache.json
71
+ trap 'rm -f "$package_list" "$fingerprint_file" "$scanner_file"' EXIT
72
+ cat > "$scanner_file" <<'NODE'
73
+ const fs = require('node:fs');
74
+ const path = require('node:path');
75
+ const crypto = require('node:crypto');
76
+
77
+ const cachePath = path.resolve(process.argv[2]);
78
+ const fingerprintPath = process.argv[3];
79
+ const mode = process.argv[4];
80
+ const nodeModules = path.resolve('node_modules');
81
+ const packages = new Set();
82
+ const packageInfos = [];
83
+ const incompatiblePlatformPackages = new Set();
84
+ const nativeFilesByPackage = new Map();
85
+ const platformPackagesByWrapper = new Map();
86
+
87
+ function readPackageJson(packageDir) {
88
+ try {
89
+ return JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ }
94
+
95
+ function addPackage(packageDir) {
96
+ const packageJson = readPackageJson(packageDir);
97
+ if (typeof packageJson?.name !== 'string' || packageJson.name.length === 0) return;
98
+ packageInfos.push({ dir: packageDir, json: packageJson, name: packageJson.name });
99
+ }
100
+
101
+ function listAllowsCurrent(value, current) {
102
+ if (value === undefined) return true;
103
+ const list = Array.isArray(value) ? value : [value];
104
+ const positives = list.filter((item) => typeof item === 'string' && !item.startsWith('!'));
105
+ const negatives = list.filter((item) => typeof item === 'string' && item.startsWith('!')).map((item) => item.slice(1));
106
+ if (negatives.includes(current)) return false;
107
+ return positives.length === 0 || positives.includes(current);
108
+ }
109
+
110
+ function packageAllowsCurrentPlatform(packageJson) {
111
+ return listAllowsCurrent(packageJson.os, process.platform) && listAllowsCurrent(packageJson.cpu, process.arch);
112
+ }
113
+
114
+ function dependencyNames(packageJson) {
115
+ return Object.keys(Object.assign({}, packageJson.dependencies, packageJson.optionalDependencies));
116
+ }
117
+
118
+ function addMapSet(map, key, value) {
119
+ const existing = map.get(key);
120
+ if (existing) existing.add(value);
121
+ else map.set(key, new Set([value]));
122
+ }
123
+
124
+ function relativeToProject(filePath) {
125
+ return path.relative(process.cwd(), filePath).split(path.sep).join('/');
126
+ }
127
+
128
+ function collectPackageFiles(packageDir, files) {
129
+ let entries;
130
+ try {
131
+ entries = fs.readdirSync(packageDir, { withFileTypes: true });
132
+ } catch {
133
+ return;
134
+ }
135
+
136
+ for (const entry of entries) {
137
+ const child = path.join(packageDir, entry.name);
138
+ if (entry.isDirectory()) collectPackageFiles(child, files);
139
+ else if (entry.isFile()) files.push(child);
140
+ }
141
+ }
142
+
143
+ function packageFingerprint(packageName) {
144
+ const info = packageInfos.find((candidate) => candidate.name === packageName);
145
+ if (!info) return undefined;
146
+ const files = [path.join(info.dir, 'package.json')];
147
+ for (const nativeFile of nativeFilesByPackage.get(packageName) || []) files.push(nativeFile);
148
+ for (const platformPackageName of platformPackagesByWrapper.get(packageName) || []) {
149
+ const platformInfo = packageInfos.find((candidate) => candidate.name === platformPackageName);
150
+ if (!platformInfo) continue;
151
+ files.push(path.join(platformInfo.dir, 'package.json'));
152
+ collectPackageFiles(platformInfo.dir, files);
153
+ }
154
+
155
+ const hash = crypto.createHash('sha256');
156
+ hash.update(process.platform);
157
+ hash.update('\0');
158
+ hash.update(process.arch);
159
+ hash.update('\0');
160
+ hash.update(process.versions.modules || '');
161
+ hash.update('\0');
162
+ for (const filePath of Array.from(new Set(files)).sort()) {
163
+ try {
164
+ const stats = fs.statSync(filePath);
165
+ if (!stats.isFile()) continue;
166
+ hash.update(relativeToProject(filePath));
167
+ hash.update('\0');
168
+ hash.update(String(stats.size));
169
+ hash.update('\0');
170
+ hash.update(String(stats.mtimeMs));
171
+ hash.update('\0');
172
+ hash.update(String(stats.mode));
173
+ hash.update('\0');
174
+ } catch {
175
+ // Ignore files that disappeared while scanning.
176
+ }
177
+ }
178
+ return hash.digest('hex');
179
+ }
180
+
181
+ function nearestPackageDir(start) {
182
+ let current = start;
183
+ while (current.startsWith(nodeModules)) {
184
+ if (fs.existsSync(path.join(current, 'package.json'))) return current;
185
+ const parent = path.dirname(current);
186
+ if (parent === current) break;
187
+ current = parent;
188
+ }
189
+ return undefined;
190
+ }
191
+
192
+ function addNativePackage(nativeFile) {
193
+ const packageDir = nearestPackageDir(path.dirname(nativeFile));
194
+ if (!packageDir) return;
195
+ const packageJson = readPackageJson(packageDir);
196
+ if (typeof packageJson?.name !== 'string' || packageJson.name.length === 0) return;
197
+ packages.add(packageJson.name);
198
+ addMapSet(nativeFilesByPackage, packageJson.name, nativeFile);
199
+ }
200
+
201
+ function walk(directory) {
202
+ let entries;
203
+ try {
204
+ entries = fs.readdirSync(directory, { withFileTypes: true });
205
+ } catch {
206
+ return;
207
+ }
208
+
209
+ for (const entry of entries) {
210
+ const child = path.join(directory, entry.name);
211
+ if (entry.isDirectory()) {
212
+ if (entry.name !== 'node_modules' && fs.existsSync(path.join(child, 'package.json'))) addPackage(child);
213
+ walk(child);
214
+ } else if (entry.isFile() && entry.name.endsWith('.node')) {
215
+ addNativePackage(child);
216
+ }
217
+ }
218
+ }
219
+
220
+ function cacheEntryMatches(entry, fingerprint) {
221
+ if (entry === fingerprint) return true;
222
+ if (entry && typeof entry === 'object' && (entry.input === fingerprint || entry.fixed === fingerprint)) return true;
223
+ return false;
224
+ }
225
+
226
+ walk(nodeModules);
227
+ for (const info of packageInfos) {
228
+ if ((info.json.os !== undefined || info.json.cpu !== undefined) && !packageAllowsCurrentPlatform(info.json)) {
229
+ incompatiblePlatformPackages.add(info.name);
230
+ }
231
+ }
232
+ for (const info of packageInfos) {
233
+ const platformDependencies = dependencyNames(info.json).filter((dependency) => incompatiblePlatformPackages.has(dependency));
234
+ if (platformDependencies.length === 0) continue;
235
+ packages.add(info.name);
236
+ for (const dependency of platformDependencies) addMapSet(platformPackagesByWrapper, info.name, dependency);
237
+ }
238
+
239
+ let previous = {};
240
+ try {
241
+ previous = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
242
+ } catch {
243
+ previous = {};
244
+ }
245
+ const next = Object.assign({}, previous);
246
+ const changedPackages = [];
247
+ for (const packageName of Array.from(packages).sort()) {
248
+ const fingerprint = packageFingerprint(packageName);
249
+ if (!fingerprint) continue;
250
+ const previousEntry = previous[packageName];
251
+ if (mode !== 'record' && !cacheEntryMatches(previousEntry, fingerprint)) changedPackages.push(packageName);
252
+ const previousObject = previousEntry && typeof previousEntry === 'object' ? previousEntry : {};
253
+ next[packageName] = mode === 'record'
254
+ ? Object.assign({}, previousObject, { fixed: fingerprint })
255
+ : Object.assign({}, previousObject, { input: fingerprint });
256
+ }
257
+ fs.mkdirSync(path.dirname(fingerprintPath), { recursive: true });
258
+ fs.writeFileSync(fingerprintPath, JSON.stringify(next, null, 2) + '\n');
259
+ if (mode !== 'record') {
260
+ for (const packageName of changedPackages) console.log(packageName);
261
+ }
262
+ NODE
263
+ node "$scanner_file" "$cache_file" "$fingerprint_file" detect > "$package_list"
264
+ if [ ! -s "$package_list" ]; then
265
+ mkdir -p .vmpush
266
+ cp "$fingerprint_file" "$cache_file"
267
+ exit 0
268
+ fi
269
+ echo "Rebuilding native Node dependencies: $(tr '\n' ' ' < "$package_list")"
270
+ if [ -f pnpm-lock.yaml ] && command -v corepack >/dev/null 2>&1; then
271
+ corepack enable >/dev/null 2>&1 || true
272
+ xargs pnpm rebuild < "$package_list"
273
+ elif [ -f yarn.lock ] && command -v corepack >/dev/null 2>&1; then
274
+ corepack enable >/dev/null 2>&1 || true
275
+ if yarn rebuild --help >/dev/null 2>&1; then
276
+ xargs yarn rebuild < "$package_list"
277
+ else
278
+ echo "Yarn does not support targeted rebuild here; skipping broad reinstall"
279
+ fi
280
+ elif command -v npm >/dev/null 2>&1; then
281
+ xargs npm rebuild < "$package_list"
282
+ fi
283
+ node "$scanner_file" "$cache_file" "$fingerprint_file" record > /dev/null
284
+ mkdir -p .vmpush
285
+ cp "$fingerprint_file" "$cache_file"
286
+ `;
287
+ const result = await vm.exec({ command: script, timeoutMs: 20 * 60 * 1000 });
288
+ if (result.statusCode && result.statusCode !== 0) {
289
+ throw new Error(result.stderr ?? result.stdout ?? `npm native dependency fixup failed with status ${result.statusCode}`);
290
+ }
291
+ if (result.stdout?.trim())
292
+ console.log(result.stdout.trim());
293
+ },
294
+ };
295
+ async function localNpmVersion() {
296
+ try {
297
+ const { stdout } = await execFileAsync("npm", ["--version"]);
298
+ const version = stdout.trim();
299
+ return version.length > 0 ? version : undefined;
300
+ }
301
+ catch {
302
+ return undefined;
303
+ }
304
+ }
305
+ function shellQuote(value) {
306
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
307
+ }
@@ -1,13 +1,12 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
- import { definePlugin, type ContextCandidate, type PushvmPluginUtils, type RemoteVm } from "../../../src/plugin-api.ts";
5
-
4
+ import { definePlugin } from "../../../src/plugin-api.js";
6
5
  export function shellHistoryPlugin() {
7
6
  return definePlugin({
8
7
  name: "@freestyle-sync/shell-history",
9
8
  async discoverContextCandidates() {
10
- const item: ContextCandidate = {
9
+ const item = {
11
10
  source: path.join(homedir(), ".zsh_history"),
12
11
  remoteRoot: "/root/.zsh_history",
13
12
  label: "zsh history",
@@ -18,7 +17,8 @@ export function shellHistoryPlugin() {
18
17
  return await exists(item.source) ? [item] : [];
19
18
  },
20
19
  async afterContextSync({ vm, changedRemotePaths, utils }) {
21
- if (!changedRemotePaths.includes("/root/.zsh_history")) return;
20
+ if (!changedRemotePaths.includes("/root/.zsh_history"))
21
+ return;
22
22
  await seedRemoteShellHistory(vm, utils.checkedExec);
23
23
  },
24
24
  async afterSync({ vm, utils }) {
@@ -26,11 +26,7 @@ export function shellHistoryPlugin() {
26
26
  },
27
27
  });
28
28
  }
29
-
30
- async function seedRemoteShellHistory(
31
- vm: RemoteVm,
32
- checkedExec: PushvmPluginUtils["checkedExec"],
33
- ) {
29
+ async function seedRemoteShellHistory(vm, checkedExec) {
34
30
  await checkedExec(vm, `
35
31
  set -eu
36
32
  if [ ! -f /root/.zsh_history ]; then
@@ -58,7 +54,12 @@ chown root:root /root/.zsh_history /root/.bash_history 2>/dev/null || true
58
54
  chmod 600 /root/.zsh_history /root/.bash_history 2>/dev/null || true
59
55
  `);
60
56
  }
61
-
62
- async function exists(filePath: string) {
63
- try { await stat(filePath); return true; } catch { return false; }
57
+ async function exists(filePath) {
58
+ try {
59
+ await stat(filePath);
60
+ return true;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
64
65
  }
@@ -1,10 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { freestyle } from "freestyle";
4
- import { definePlugin, type ConnectHookContext, type RemoteVm } from "../../../src/plugin-api.ts";
5
-
4
+ import { definePlugin } from "../../../src/plugin-api.js";
6
5
  const execFileAsync = promisify(execFile);
7
-
8
6
  export function vscodePlugin() {
9
7
  return definePlugin({
10
8
  name: "@freestyle-sync/vscode",
@@ -15,21 +13,17 @@ export function vscodePlugin() {
15
13
  },
16
14
  async connect(context) {
17
15
  const editorScheme = await detectEditorScheme();
18
- if (!editorScheme) return false;
16
+ if (!editorScheme)
17
+ return false;
19
18
  await openRemoteEditor(context, editorScheme);
20
19
  return true;
21
20
  },
22
21
  });
23
22
  }
24
-
25
- async function warmVsCodeServerCache(
26
- vm: RemoteVm,
27
- shellQuote: (value: string) => string,
28
- checkedExec: (vm: RemoteVm, command: string, timeoutMs?: number) => Promise<{ stdout?: string | null }>,
29
- ) {
23
+ async function warmVsCodeServerCache(vm, shellQuote, checkedExec) {
30
24
  const commit = await localVsCodeCommit();
31
- if (!commit) return;
32
-
25
+ if (!commit)
26
+ return;
33
27
  const script = `
34
28
  set -eu
35
29
  commit=${shellQuote(commit)}
@@ -75,22 +69,21 @@ if [ ! -x "$cli_server_dir/bin/code-server" ]; then
75
69
  fi
76
70
  echo "Cached VS Code Server $commit for linux-$arch"
77
71
  `;
78
-
79
72
  const result = await checkedExec(vm, script);
80
- if (result.stdout?.trim()) console.log(result.stdout.trim());
73
+ if (result.stdout?.trim())
74
+ console.log(result.stdout.trim());
81
75
  }
82
-
83
76
  async function localVsCodeCommit() {
84
77
  try {
85
78
  const { stdout } = await execFileAsync("code", ["--version"]);
86
79
  const commit = stdout.split(/\r?\n/)[1]?.trim();
87
80
  return /^[0-9a-f]{40}$/i.test(commit ?? "") ? commit : undefined;
88
- } catch {
81
+ }
82
+ catch {
89
83
  return undefined;
90
84
  }
91
85
  }
92
-
93
- async function openRemoteEditor(context: ConnectHookContext, scheme: "vscode" | "cursor") {
86
+ async function openRemoteEditor(context, scheme) {
94
87
  const { vmId, options } = context;
95
88
  console.log(`Opening ${scheme === "cursor" ? "Cursor" : "VS Code"} Remote SSH window for VM ${vmId}...`);
96
89
  const { identity, identityId } = await freestyle.identities.create();
@@ -100,63 +93,68 @@ async function openRemoteEditor(context: ConnectHookContext, scheme: "vscode" |
100
93
  const uri = `${scheme}://vscode-remote/ssh-remote+${vmId},${token}@${vmId}.vm-ssh.freestyle.sh${encodeRemotePath(options.remoteProjectDir)}?windowId=_blank`;
101
94
  try {
102
95
  const preOpenMessages = await context.runBeforeOpenRemoteEditor({ scheme, remoteWorkspaceUri });
103
- for (const message of preOpenMessages) console.log(message);
96
+ for (const message of preOpenMessages)
97
+ console.log(message);
104
98
  await openUri(uri);
105
- } catch (error) {
99
+ }
100
+ catch (error) {
106
101
  await identity.tokens.revoke({ tokenId }).catch(() => undefined);
107
102
  await freestyle.identities.delete({ identityId }).catch(() => undefined);
108
103
  throw error;
109
104
  }
110
105
  await context.utils.delay(5000);
111
106
  const postOpenMessages = await context.runAfterOpenRemoteEditor({ scheme, remoteWorkspaceUri });
112
- for (const message of postOpenMessages) console.log(message);
107
+ for (const message of postOpenMessages)
108
+ console.log(message);
113
109
  console.log(`Opened ${scheme === "cursor" ? "Cursor" : "VS Code"}. SSH identity ${identityId} remains active for the editor session.`);
114
110
  }
115
-
116
- async function detectEditorScheme(): Promise<"vscode" | "cursor" | undefined> {
111
+ async function detectEditorScheme() {
117
112
  const envText = Object.entries(process.env)
118
113
  .filter(([key]) => key.startsWith("VSCODE_") || key.startsWith("CURSOR") || key === "TERM_PROGRAM")
119
114
  .map(([key, value]) => `${key}=${value ?? ""}`)
120
115
  .join("\n")
121
116
  .toLowerCase();
122
-
123
- if (envText.includes("cursor")) return "cursor";
124
- if (!envText.includes("term_program=vscode") && !envText.includes("vscode_")) return undefined;
125
-
117
+ if (envText.includes("cursor"))
118
+ return "cursor";
119
+ if (!envText.includes("term_program=vscode") && !envText.includes("vscode_"))
120
+ return undefined;
126
121
  const parentCommands = await processCommandChain(process.ppid);
127
- if (parentCommands.some((command) => command.toLowerCase().includes("cursor"))) return "cursor";
122
+ if (parentCommands.some((command) => command.toLowerCase().includes("cursor")))
123
+ return "cursor";
128
124
  return "vscode";
129
125
  }
130
-
131
- async function processCommandChain(startPid: number) {
132
- const commands: string[] = [];
126
+ async function processCommandChain(startPid) {
127
+ const commands = [];
133
128
  let pid = startPid;
134
129
  for (let depth = 0; depth < 20 && pid > 1; depth += 1) {
135
130
  try {
136
131
  const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "ppid=", "-o", "comm="]);
137
132
  const line = stdout.trim();
138
- if (!line) break;
133
+ if (!line)
134
+ break;
139
135
  const match = line.match(/^(\d+)\s+(.+)$/);
140
- if (!match) break;
136
+ if (!match)
137
+ break;
141
138
  pid = Number(match[1]);
142
139
  commands.push(match[2]);
143
- } catch {
140
+ }
141
+ catch {
144
142
  break;
145
143
  }
146
144
  }
147
145
  return commands;
148
146
  }
149
-
150
- function encodeRemotePath(remoteProjectDir: string) {
147
+ function encodeRemotePath(remoteProjectDir) {
151
148
  return remoteProjectDir.split("/").map((segment) => encodeURIComponent(segment)).join("/");
152
149
  }
153
-
154
- async function openUri(uri: string) {
150
+ async function openUri(uri) {
155
151
  if (process.platform === "darwin") {
156
152
  await execFileAsync("open", [uri]);
157
- } else if (process.platform === "win32") {
153
+ }
154
+ else if (process.platform === "win32") {
158
155
  await execFileAsync("cmd", ["/c", "start", "", uri]);
159
- } else {
156
+ }
157
+ else {
160
158
  await execFileAsync("xdg-open", [uri]);
161
159
  }
162
160
  }