freestyle-sync 0.1.4 → 0.1.5
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/README.md +28 -2
- package/dist/src/main.js +317 -72
- package/package.json +17 -4
- package/dist/freestyle-sync.config.js +0 -35
- package/dist/main.js +0 -1319
- package/dist/plugins/agent-claude/src/index.js +0 -116
- package/dist/plugins/agent-codex/src/index.js +0 -68
- package/dist/plugins/agent-copilot/src/index.js +0 -529
- package/dist/plugins/auth-aws/src/index.js +0 -29
- package/dist/plugins/auth-azure/src/index.js +0 -29
- package/dist/plugins/auth-context.js +0 -213
- package/dist/plugins/auth-docker/src/index.js +0 -29
- package/dist/plugins/auth-env/src/index.js +0 -35
- package/dist/plugins/auth-gcloud/src/index.js +0 -29
- package/dist/plugins/auth-git/src/index.js +0 -50
- package/dist/plugins/auth-github-cli/src/index.js +0 -39
- package/dist/plugins/auth-npm/src/index.js +0 -38
- package/dist/plugins/auth-ssh/src/index.js +0 -42
- package/dist/plugins/auth-yarn/src/index.js +0 -24
- package/dist/plugins/node-npm/src/index.js +0 -388
- package/dist/plugins/npm-native-deps.js +0 -307
- package/dist/plugins/shell-history/src/index.js +0 -65
- package/dist/plugins/vscode/src/index.js +0 -160
- package/dist/src/pushvm.config.js +0 -36
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { homedir, tmpdir } from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { execFile } from "node:child_process";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
|
-
import { definePlugin } from "../../../src/plugin-api.js";
|
|
8
|
-
const execFileAsync = promisify(execFile);
|
|
9
|
-
export function copilotAgentPlugin() {
|
|
10
|
-
return definePlugin({
|
|
11
|
-
name: "@freestyle-sync/agent-copilot",
|
|
12
|
-
async discoverContextCandidates({ options }) {
|
|
13
|
-
if (!options.includeAgentContext)
|
|
14
|
-
return [];
|
|
15
|
-
const candidates = [];
|
|
16
|
-
for (const codeRoot of codeUserRoots(homedir())) {
|
|
17
|
-
await addCopilotCandidates(candidates, codeRoot, options.projectRoot, options.includeAllCopilotWorkspaces);
|
|
18
|
-
}
|
|
19
|
-
return dedupeCandidates(candidates);
|
|
20
|
-
},
|
|
21
|
-
async afterProjectSync({ vm, utils }) {
|
|
22
|
-
await installAgentCli(vm, utils.checkedExec, "copilot", "@github/copilot");
|
|
23
|
-
},
|
|
24
|
-
async afterSync({ vm, vmId, options, contextCandidates, utils }) {
|
|
25
|
-
const result = await syncLocalCopilotForRemoteWorkspaces(vm, vmId, options.remoteProjectDir, contextCandidates, utils);
|
|
26
|
-
if (result.status === "copied") {
|
|
27
|
-
return [`Copilot sidebar state: copied to ${result.count} VS Code Remote workspace${result.count === 1 ? "" : "s"}`];
|
|
28
|
-
}
|
|
29
|
-
if (result.status === "pending")
|
|
30
|
-
return [`Copilot sidebar state: ${result.reason}`];
|
|
31
|
-
return [];
|
|
32
|
-
},
|
|
33
|
-
async beforeOpenRemoteEditor({ vm, vmId, scheme, remoteWorkspaceUri, contextCandidates, utils }) {
|
|
34
|
-
if (!isSupportedEditorScheme(scheme))
|
|
35
|
-
return [];
|
|
36
|
-
const result = await preseedLocalCopilotForRemoteUri(vm, vmId, contextCandidates, scheme, remoteWorkspaceUri, utils);
|
|
37
|
-
if (result.status === "copied")
|
|
38
|
-
return [`Prepared Copilot sidebar state for ${scheme === "cursor" ? "Cursor" : "VS Code"}.`];
|
|
39
|
-
return [];
|
|
40
|
-
},
|
|
41
|
-
async afterOpenRemoteEditor({ vm, vmId, scheme, remoteWorkspaceUri, contextCandidates, utils }) {
|
|
42
|
-
if (!isSupportedEditorScheme(scheme))
|
|
43
|
-
return [];
|
|
44
|
-
const result = await syncLocalCopilotForRemoteUri(vm, vmId, contextCandidates, remoteWorkspaceUri, utils);
|
|
45
|
-
if (result.status === "copied")
|
|
46
|
-
return [`Copied Copilot sidebar state to opened ${scheme === "cursor" ? "Cursor" : "VS Code"} workspace.`];
|
|
47
|
-
if (result.status === "pending")
|
|
48
|
-
return ["Copilot sidebar state: opened the editor, but the workspace storage bucket was not created yet. Reload the window after it finishes connecting, then rerun vmpush if needed."];
|
|
49
|
-
return [];
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
async function installAgentCli(vm, checkedExec, binary, packageName) {
|
|
54
|
-
await checkedExec(vm, `
|
|
55
|
-
set -e
|
|
56
|
-
export HOME="\${HOME:-/root}"
|
|
57
|
-
export NVM_DIR=/root/.nvm
|
|
58
|
-
if [ -s "$NVM_DIR/nvm.sh" ]; then
|
|
59
|
-
. "$NVM_DIR/nvm.sh"
|
|
60
|
-
nvm use --silent default >/dev/null 2>&1 || true
|
|
61
|
-
fi
|
|
62
|
-
binary_path=$(command -v ${binary} || true)
|
|
63
|
-
if [ -z "$binary_path" ] && command -v npm >/dev/null 2>&1; then
|
|
64
|
-
npm_prefix=$(npm prefix -g 2>/dev/null || true)
|
|
65
|
-
if [ -n "$npm_prefix" ] && [ -x "$npm_prefix/bin/${binary}" ]; then
|
|
66
|
-
binary_path="$npm_prefix/bin/${binary}"
|
|
67
|
-
fi
|
|
68
|
-
fi
|
|
69
|
-
if [ -z "$binary_path" ]; then
|
|
70
|
-
if ! command -v npm >/dev/null 2>&1; then
|
|
71
|
-
echo "npm is required to install ${binary}" >&2
|
|
72
|
-
exit 1
|
|
73
|
-
fi
|
|
74
|
-
npm install -g ${packageName}@latest
|
|
75
|
-
binary_path=$(command -v ${binary} || true)
|
|
76
|
-
if [ -z "$binary_path" ]; then
|
|
77
|
-
npm_prefix=$(npm prefix -g 2>/dev/null || true)
|
|
78
|
-
if [ -n "$npm_prefix" ] && [ -x "$npm_prefix/bin/${binary}" ]; then
|
|
79
|
-
binary_path="$npm_prefix/bin/${binary}"
|
|
80
|
-
fi
|
|
81
|
-
fi
|
|
82
|
-
fi
|
|
83
|
-
if [ -z "$binary_path" ]; then
|
|
84
|
-
echo "${binary} was not found after installing ${packageName}" >&2
|
|
85
|
-
exit 1
|
|
86
|
-
fi
|
|
87
|
-
mkdir -p /usr/local/bin
|
|
88
|
-
if [ "$binary_path" != "/usr/local/bin/${binary}" ]; then
|
|
89
|
-
ln -sf "$binary_path" /usr/local/bin/${binary}
|
|
90
|
-
fi
|
|
91
|
-
test -x /usr/local/bin/${binary}
|
|
92
|
-
`, 600000);
|
|
93
|
-
}
|
|
94
|
-
async function addCopilotCandidates(candidates, codeUserRoot, projectRoot, includeAllWorkspaces) {
|
|
95
|
-
if (!(await exists(codeUserRoot)))
|
|
96
|
-
return;
|
|
97
|
-
const remoteUserRoot = remoteCodeRoot(codeUserRoot);
|
|
98
|
-
const globalStorage = path.join(codeUserRoot, "globalStorage", "github.copilot-chat");
|
|
99
|
-
if (await exists(globalStorage)) {
|
|
100
|
-
addCandidate(candidates, globalStorage, `${remoteUserRoot}/globalStorage/github.copilot-chat`, "Copilot global chat state", "context:copilot-global");
|
|
101
|
-
}
|
|
102
|
-
const workspaceStorage = path.join(codeUserRoot, "workspaceStorage");
|
|
103
|
-
const workspaceDirs = await listDirectories(workspaceStorage);
|
|
104
|
-
for (const workspaceDir of workspaceDirs) {
|
|
105
|
-
const copilotRoot = path.join(workspaceDir, "GitHub.copilot-chat");
|
|
106
|
-
const isCurrentWorkspace = await isWorkspaceStorageForProject(workspaceDir, projectRoot);
|
|
107
|
-
if (await exists(copilotRoot) && (isCurrentWorkspace || includeAllWorkspaces)) {
|
|
108
|
-
const workspaceId = path.basename(workspaceDir);
|
|
109
|
-
const key = isCurrentWorkspace ? "context:copilot-current-workspace" : "context:copilot-all-workspaces";
|
|
110
|
-
const label = isCurrentWorkspace ? "Copilot current workspace conversations" : "Copilot other workspace conversations";
|
|
111
|
-
addCandidate(candidates, copilotRoot, `${remoteUserRoot}/workspaceStorage/${workspaceId}/GitHub.copilot-chat`, label, key);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async function syncLocalCopilotForRemoteWorkspaces(vm, vmId, remoteProjectDir, contextCandidates, utils) {
|
|
116
|
-
const sourceCopilotDir = selectedCurrentCopilotSource(contextCandidates);
|
|
117
|
-
if (!sourceCopilotDir || !(await exists(sourceCopilotDir))) {
|
|
118
|
-
return { status: "skipped", count: 0, reason: "Copilot current workspace state is not selected." };
|
|
119
|
-
}
|
|
120
|
-
const destinations = await findRemoteWorkspaceCopilotDestinations(vmId, remoteProjectDir);
|
|
121
|
-
if (destinations.length === 0) {
|
|
122
|
-
return {
|
|
123
|
-
status: "pending",
|
|
124
|
-
count: 0,
|
|
125
|
-
reason: "open this VM folder in VS Code once, then rerun vmpush to copy sidebar history.",
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
await copyCopilotState(sourceCopilotDir, destinations);
|
|
129
|
-
await copyCopilotWorkspaceStateKeys(sourceCopilotDir, destinations);
|
|
130
|
-
await copyCopilotStateToRemoteServer(vm, vmId, sourceCopilotDir, destinations, utils);
|
|
131
|
-
return { status: "copied", count: destinations.length };
|
|
132
|
-
}
|
|
133
|
-
async function syncLocalCopilotForRemoteUri(vm, vmId, contextCandidates, remoteWorkspaceUri, utils) {
|
|
134
|
-
const sourceCopilotDir = selectedCurrentCopilotSource(contextCandidates);
|
|
135
|
-
if (!sourceCopilotDir || !(await exists(sourceCopilotDir)))
|
|
136
|
-
return { status: "skipped", count: 0 };
|
|
137
|
-
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
138
|
-
const destinations = [];
|
|
139
|
-
for (const codeRoot of codeUserRoots(homedir())) {
|
|
140
|
-
const workspaceStorage = path.join(codeRoot, "workspaceStorage");
|
|
141
|
-
const workspaceDirs = await listDirectories(workspaceStorage);
|
|
142
|
-
for (const workspaceDir of workspaceDirs) {
|
|
143
|
-
if (await isWorkspaceStorageForRemoteUri(workspaceDir, remoteWorkspaceUri)) {
|
|
144
|
-
destinations.push(path.join(workspaceDir, "GitHub.copilot-chat"));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (destinations.length > 0) {
|
|
149
|
-
await copyCopilotState(sourceCopilotDir, destinations);
|
|
150
|
-
await copyCopilotWorkspaceStateKeys(sourceCopilotDir, destinations);
|
|
151
|
-
await copyCopilotStateToRemoteServer(vm, vmId, sourceCopilotDir, destinations, utils);
|
|
152
|
-
return { status: "copied", count: destinations.length };
|
|
153
|
-
}
|
|
154
|
-
await utils.delay(500);
|
|
155
|
-
}
|
|
156
|
-
return { status: "pending", count: 0 };
|
|
157
|
-
}
|
|
158
|
-
async function preseedLocalCopilotForRemoteUri(vm, vmId, contextCandidates, scheme, remoteWorkspaceUri, utils) {
|
|
159
|
-
const sourceCopilotDir = selectedCurrentCopilotSource(contextCandidates);
|
|
160
|
-
if (!sourceCopilotDir || !(await exists(sourceCopilotDir)))
|
|
161
|
-
return { status: "skipped", count: 0 };
|
|
162
|
-
const workspaceId = utils.md5(remoteWorkspaceUri);
|
|
163
|
-
const destinations = [];
|
|
164
|
-
for (const codeRoot of codeUserRootsForScheme(homedir(), scheme)) {
|
|
165
|
-
const workspaceDir = path.join(codeRoot, "workspaceStorage", workspaceId);
|
|
166
|
-
await mkdir(workspaceDir, { recursive: true });
|
|
167
|
-
await writeFile(path.join(workspaceDir, "workspace.json"), `${JSON.stringify({ folder: remoteWorkspaceUri }, null, 2)}\n`, "utf8");
|
|
168
|
-
await ensureWorkspaceStateDb(path.join(workspaceDir, "state.vscdb"));
|
|
169
|
-
destinations.push(path.join(workspaceDir, "GitHub.copilot-chat"));
|
|
170
|
-
}
|
|
171
|
-
await copyCopilotState(sourceCopilotDir, destinations);
|
|
172
|
-
await copyCopilotWorkspaceStateKeys(sourceCopilotDir, destinations);
|
|
173
|
-
await copyCopilotStateToRemoteServer(vm, vmId, sourceCopilotDir, destinations, utils);
|
|
174
|
-
return { status: "copied", count: destinations.length };
|
|
175
|
-
}
|
|
176
|
-
function selectedCurrentCopilotSource(contextCandidates) {
|
|
177
|
-
return contextCandidates.find((candidate) => candidate.preferenceKey === "context:copilot-current-workspace")?.source;
|
|
178
|
-
}
|
|
179
|
-
async function findRemoteWorkspaceCopilotDestinations(vmId, remoteProjectDir) {
|
|
180
|
-
const destinations = [];
|
|
181
|
-
for (const codeRoot of codeUserRoots(homedir())) {
|
|
182
|
-
const workspaceStorage = path.join(codeRoot, "workspaceStorage");
|
|
183
|
-
const workspaceDirs = await listDirectories(workspaceStorage);
|
|
184
|
-
for (const workspaceDir of workspaceDirs) {
|
|
185
|
-
if (await isRemoteWorkspaceStorageForVmProject(workspaceDir, vmId, remoteProjectDir)) {
|
|
186
|
-
destinations.push(path.join(workspaceDir, "GitHub.copilot-chat"));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return destinations;
|
|
191
|
-
}
|
|
192
|
-
async function copyCopilotState(sourceCopilotDir, destinations) {
|
|
193
|
-
for (const destination of destinations) {
|
|
194
|
-
await rm(destination, { recursive: true, force: true });
|
|
195
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
196
|
-
await cp(sourceCopilotDir, destination, { recursive: true, force: true });
|
|
197
|
-
await sanitizeCopilotTranscripts(destination);
|
|
198
|
-
await copyCoreChatState(sourceCopilotDir, destination);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
async function copyCoreChatState(sourceCopilotDir, destinationCopilotDir) {
|
|
202
|
-
const sourceWorkspaceDir = path.dirname(sourceCopilotDir);
|
|
203
|
-
const destinationWorkspaceDir = path.dirname(destinationCopilotDir);
|
|
204
|
-
const sessionIds = await copiedCopilotSessionIds(destinationCopilotDir);
|
|
205
|
-
for (const sessionId of sessionIds) {
|
|
206
|
-
const sourceChatSession = path.join(sourceWorkspaceDir, "chatSessions", `${sessionId}.jsonl`);
|
|
207
|
-
if (await exists(sourceChatSession)) {
|
|
208
|
-
const destinationChatSession = path.join(destinationWorkspaceDir, "chatSessions", `${sessionId}.jsonl`);
|
|
209
|
-
await mkdir(path.dirname(destinationChatSession), { recursive: true });
|
|
210
|
-
await cp(sourceChatSession, destinationChatSession, { force: true });
|
|
211
|
-
await trimTranscriptToCompletedTurn(destinationChatSession);
|
|
212
|
-
}
|
|
213
|
-
const sourceEditingSession = path.join(sourceWorkspaceDir, "chatEditingSessions", sessionId);
|
|
214
|
-
if (await exists(sourceEditingSession)) {
|
|
215
|
-
const destinationEditingSession = path.join(destinationWorkspaceDir, "chatEditingSessions", sessionId);
|
|
216
|
-
await rm(destinationEditingSession, { recursive: true, force: true });
|
|
217
|
-
await mkdir(path.dirname(destinationEditingSession), { recursive: true });
|
|
218
|
-
await cp(sourceEditingSession, destinationEditingSession, { recursive: true, force: true });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
async function copiedCopilotSessionIds(copilotDir) {
|
|
223
|
-
const transcripts = await listFiles(path.join(copilotDir, "transcripts"));
|
|
224
|
-
return transcripts.filter((transcript) => transcript.endsWith(".jsonl")).map((transcript) => path.basename(transcript, ".jsonl"));
|
|
225
|
-
}
|
|
226
|
-
async function sanitizeCopilotTranscripts(copilotDir) {
|
|
227
|
-
const entries = await listFiles(path.join(copilotDir, "transcripts"));
|
|
228
|
-
for (const transcript of entries.filter((entry) => entry.endsWith(".jsonl"))) {
|
|
229
|
-
await trimTranscriptToCompletedTurn(transcript);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
async function trimTranscriptToCompletedTurn(transcriptPath) {
|
|
233
|
-
const content = await readFile(transcriptPath, "utf8").catch(() => undefined);
|
|
234
|
-
if (!content)
|
|
235
|
-
return;
|
|
236
|
-
const lines = content.trimEnd().split("\n");
|
|
237
|
-
let lastCompletedLine = -1;
|
|
238
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
239
|
-
try {
|
|
240
|
-
const event = JSON.parse(lines[index]);
|
|
241
|
-
if (event.type === "assistant.turn_end")
|
|
242
|
-
lastCompletedLine = index;
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (lastCompletedLine >= 0 && lastCompletedLine < lines.length - 1) {
|
|
249
|
-
await writeFile(transcriptPath, `${lines.slice(0, lastCompletedLine + 1).join("\n")}\n`, "utf8");
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
async function copyCopilotWorkspaceStateKeys(sourceCopilotDir, destinations) {
|
|
253
|
-
const sourceStateDb = path.join(path.dirname(sourceCopilotDir), "state.vscdb");
|
|
254
|
-
if (!(await exists(sourceStateDb)))
|
|
255
|
-
return;
|
|
256
|
-
for (const destination of destinations) {
|
|
257
|
-
const destinationStateDb = path.join(path.dirname(destination), "state.vscdb");
|
|
258
|
-
if (!(await exists(destinationStateDb)))
|
|
259
|
-
continue;
|
|
260
|
-
await mergeWorkspaceStateKeys(sourceStateDb, destinationStateDb).catch((error) => {
|
|
261
|
-
console.warn(`Copilot sidebar state database merge skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
262
|
-
});
|
|
263
|
-
await normalizeCopiedChatIndex(destination).catch((error) => {
|
|
264
|
-
console.warn(`Copilot sidebar index normalization skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
async function mergeWorkspaceStateKeys(sourceStateDb, destinationStateDb) {
|
|
269
|
-
await execFileAsync("sqlite3", [
|
|
270
|
-
destinationStateDb,
|
|
271
|
-
`PRAGMA busy_timeout=5000;
|
|
272
|
-
ATTACH DATABASE ${sqliteLiteral(sourceStateDb)} AS source_state;
|
|
273
|
-
INSERT OR REPLACE INTO ItemTable(key, value)
|
|
274
|
-
SELECT key, value FROM source_state.ItemTable
|
|
275
|
-
WHERE key IN (
|
|
276
|
-
'GitHub.copilot-chat',
|
|
277
|
-
'chat.ChatSessionStore.index',
|
|
278
|
-
'memento/interactive-session-view-copilot',
|
|
279
|
-
'workbench.panel.chat',
|
|
280
|
-
'workbench.panel.chat.numberOfVisibleViews'
|
|
281
|
-
) OR key LIKE 'chat.%';
|
|
282
|
-
DETACH DATABASE source_state;`,
|
|
283
|
-
]);
|
|
284
|
-
}
|
|
285
|
-
async function normalizeCopiedChatIndex(copilotDir) {
|
|
286
|
-
const stateDb = path.join(path.dirname(copilotDir), "state.vscdb");
|
|
287
|
-
if (!(await exists(stateDb)))
|
|
288
|
-
return;
|
|
289
|
-
const { stdout } = await execFileAsync("sqlite3", [stateDb, "SELECT value FROM ItemTable WHERE key='chat.ChatSessionStore.index';"]);
|
|
290
|
-
if (!stdout.trim())
|
|
291
|
-
return;
|
|
292
|
-
const index = JSON.parse(stdout);
|
|
293
|
-
if (!index.entries)
|
|
294
|
-
return;
|
|
295
|
-
for (const sessionId of Object.keys(index.entries)) {
|
|
296
|
-
const transcriptPath = path.join(copilotDir, "transcripts", `${sessionId}.jsonl`);
|
|
297
|
-
const lastCompletedTimestamp = await lastCompletedTurnTimestamp(transcriptPath);
|
|
298
|
-
if (!lastCompletedTimestamp) {
|
|
299
|
-
delete index.entries[sessionId];
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
const entry = index.entries[sessionId];
|
|
303
|
-
entry.lastResponseState = 1;
|
|
304
|
-
entry.hasPendingEdits = false;
|
|
305
|
-
entry.lastMessageDate = lastCompletedTimestamp;
|
|
306
|
-
const timing = typeof entry.timing === "object" && entry.timing !== null ? entry.timing : {};
|
|
307
|
-
timing.lastRequestEnded = lastCompletedTimestamp;
|
|
308
|
-
entry.timing = timing;
|
|
309
|
-
}
|
|
310
|
-
await execFileAsync("sqlite3", [
|
|
311
|
-
stateDb,
|
|
312
|
-
`INSERT OR REPLACE INTO ItemTable(key, value) VALUES ('chat.ChatSessionStore.index', ${sqliteLiteral(JSON.stringify(index))});`,
|
|
313
|
-
]);
|
|
314
|
-
}
|
|
315
|
-
async function lastCompletedTurnTimestamp(transcriptPath) {
|
|
316
|
-
const content = await readFile(transcriptPath, "utf8").catch(() => undefined);
|
|
317
|
-
if (!content)
|
|
318
|
-
return undefined;
|
|
319
|
-
const lines = content.trimEnd().split("\n");
|
|
320
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
321
|
-
try {
|
|
322
|
-
const event = JSON.parse(lines[index]);
|
|
323
|
-
if (event.type === "assistant.turn_end" && event.timestamp)
|
|
324
|
-
return Date.parse(event.timestamp);
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
return undefined;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return undefined;
|
|
331
|
-
}
|
|
332
|
-
async function copyCopilotStateToRemoteServer(vm, vmId, sourceCopilotDir, localDestinations, utils) {
|
|
333
|
-
const workspaceIds = localDestinations.map((destination) => path.basename(path.dirname(destination)));
|
|
334
|
-
if (workspaceIds.length === 0)
|
|
335
|
-
return;
|
|
336
|
-
const archive = await createRemoteServerWorkspaceStateArchive(sourceCopilotDir, localDestinations, utils);
|
|
337
|
-
try {
|
|
338
|
-
await utils.uploadArchiveInChunks(vm, vmId, archive, "/tmp/vmpush-copilot-chat.tgz", "copilot-chat");
|
|
339
|
-
const baseDir = "/root/.vscode-server/data/User/workspaceStorage";
|
|
340
|
-
const commands = [`mkdir -p ${utils.shellQuote(baseDir)}`];
|
|
341
|
-
for (const workspaceId of workspaceIds) {
|
|
342
|
-
commands.push(`mkdir -p ${utils.shellQuote(`${baseDir}/${workspaceId}`)} && rm -rf ${utils.shellQuote(`${baseDir}/${workspaceId}/GitHub.copilot-chat`)}`);
|
|
343
|
-
}
|
|
344
|
-
commands.push(`tar --no-same-owner --no-same-permissions -xzf /tmp/vmpush-copilot-chat.tgz -C ${utils.shellQuote(baseDir)}`);
|
|
345
|
-
commands.push("rm -f /tmp/vmpush-copilot-chat.tgz");
|
|
346
|
-
await utils.checkedExec(vm, commands.join("\n"));
|
|
347
|
-
}
|
|
348
|
-
finally {
|
|
349
|
-
await rm(path.dirname(archive), { recursive: true, force: true });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
async function createRemoteServerWorkspaceStateArchive(sourceCopilotDir, localDestinations, utils) {
|
|
353
|
-
const tempDir = await mkdtemp(path.join(tmpdir(), "vmpush-copilot-"));
|
|
354
|
-
const stagingDir = path.join(tempDir, "staging");
|
|
355
|
-
const archivePath = path.join(tempDir, "copilot-chat.tgz");
|
|
356
|
-
await mkdir(stagingDir, { recursive: true });
|
|
357
|
-
for (const destination of localDestinations) {
|
|
358
|
-
const workspaceId = path.basename(path.dirname(destination));
|
|
359
|
-
const workspaceDir = path.join(stagingDir, workspaceId);
|
|
360
|
-
await mkdir(workspaceDir, { recursive: true });
|
|
361
|
-
await cp(sourceCopilotDir, path.join(workspaceDir, "GitHub.copilot-chat"), { recursive: true, force: true });
|
|
362
|
-
await sanitizeCopilotTranscripts(path.join(workspaceDir, "GitHub.copilot-chat"));
|
|
363
|
-
await copyCoreChatState(sourceCopilotDir, path.join(workspaceDir, "GitHub.copilot-chat"));
|
|
364
|
-
const stateDb = path.join(path.dirname(destination), "state.vscdb");
|
|
365
|
-
if (await exists(stateDb))
|
|
366
|
-
await cp(stateDb, path.join(workspaceDir, "state.vscdb"), { force: true });
|
|
367
|
-
}
|
|
368
|
-
await utils.createTar(["--no-xattrs", "-czf", archivePath, "-C", stagingDir, "."]);
|
|
369
|
-
return archivePath;
|
|
370
|
-
}
|
|
371
|
-
async function isRemoteWorkspaceStorageForVmProject(workspaceDir, vmId, remoteProjectDir) {
|
|
372
|
-
try {
|
|
373
|
-
const raw = await readFile(path.join(workspaceDir, "workspace.json"), "utf8");
|
|
374
|
-
const parsed = JSON.parse(raw);
|
|
375
|
-
const workspaceUri = parsed.folder ?? parsed.workspace;
|
|
376
|
-
if (!workspaceUri?.startsWith("vscode-remote://"))
|
|
377
|
-
return false;
|
|
378
|
-
const decodedUri = decodeURIComponent(workspaceUri);
|
|
379
|
-
return decodedUri.includes(vmId) && remoteWorkspacePath(decodedUri) === remoteProjectDir;
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
async function isWorkspaceStorageForRemoteUri(workspaceDir, remoteWorkspaceUri) {
|
|
386
|
-
try {
|
|
387
|
-
const raw = await readFile(path.join(workspaceDir, "workspace.json"), "utf8");
|
|
388
|
-
const parsed = JSON.parse(raw);
|
|
389
|
-
const workspaceUri = parsed.folder ?? parsed.workspace;
|
|
390
|
-
return workspaceUri === remoteWorkspaceUri;
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
async function isWorkspaceStorageForProject(workspaceDir, projectRoot) {
|
|
397
|
-
try {
|
|
398
|
-
const raw = await readFile(path.join(workspaceDir, "workspace.json"), "utf8");
|
|
399
|
-
const parsed = JSON.parse(raw);
|
|
400
|
-
const workspacePath = workspaceJsonPath(parsed.folder ?? parsed.workspace);
|
|
401
|
-
return workspacePath !== undefined && isSameOrParentPath(workspacePath, projectRoot);
|
|
402
|
-
}
|
|
403
|
-
catch {
|
|
404
|
-
return false;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
function workspaceJsonPath(uri) {
|
|
408
|
-
if (!uri)
|
|
409
|
-
return undefined;
|
|
410
|
-
try {
|
|
411
|
-
if (uri.startsWith("file://"))
|
|
412
|
-
return path.resolve(fileURLToPath(uri));
|
|
413
|
-
return path.resolve(uri);
|
|
414
|
-
}
|
|
415
|
-
catch {
|
|
416
|
-
return undefined;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
function isSameOrParentPath(workspacePath, projectRoot) {
|
|
420
|
-
const relative = path.relative(workspacePath, projectRoot);
|
|
421
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
422
|
-
}
|
|
423
|
-
function remoteWorkspacePath(decodedRemoteUri) {
|
|
424
|
-
const hostEnd = decodedRemoteUri.indexOf("/", "vscode-remote://".length);
|
|
425
|
-
if (hostEnd === -1)
|
|
426
|
-
return "/";
|
|
427
|
-
return normalizeRemotePath(decodedRemoteUri.slice(hostEnd));
|
|
428
|
-
}
|
|
429
|
-
function normalizeRemotePath(value) {
|
|
430
|
-
const normalized = path.posix.normalize(value.replace(/\\/g, "/"));
|
|
431
|
-
if (!normalized.startsWith("/"))
|
|
432
|
-
return "/";
|
|
433
|
-
return normalized;
|
|
434
|
-
}
|
|
435
|
-
async function ensureWorkspaceStateDb(stateDb) {
|
|
436
|
-
await execFileAsync("sqlite3", [stateDb, "CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB);"]);
|
|
437
|
-
}
|
|
438
|
-
function codeUserRootsForScheme(home, scheme) {
|
|
439
|
-
switch (scheme) {
|
|
440
|
-
case "cursor":
|
|
441
|
-
return [
|
|
442
|
-
path.join(home, "Library", "Application Support", "Cursor", "User"),
|
|
443
|
-
path.join(home, ".config", "Cursor", "User"),
|
|
444
|
-
];
|
|
445
|
-
case "vscode":
|
|
446
|
-
return [
|
|
447
|
-
path.join(home, "Library", "Application Support", "Code", "User"),
|
|
448
|
-
path.join(home, ".config", "Code", "User"),
|
|
449
|
-
];
|
|
450
|
-
default:
|
|
451
|
-
return [];
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
function isSupportedEditorScheme(scheme) {
|
|
455
|
-
return scheme === "vscode" || scheme === "cursor";
|
|
456
|
-
}
|
|
457
|
-
function codeUserRoots(home) {
|
|
458
|
-
return [
|
|
459
|
-
path.join(home, "Library", "Application Support", "Code", "User"),
|
|
460
|
-
path.join(home, "Library", "Application Support", "Code - Insiders", "User"),
|
|
461
|
-
path.join(home, "Library", "Application Support", "Cursor", "User"),
|
|
462
|
-
path.join(home, ".config", "Code", "User"),
|
|
463
|
-
path.join(home, ".config", "Code - Insiders", "User"),
|
|
464
|
-
path.join(home, ".config", "Cursor", "User"),
|
|
465
|
-
];
|
|
466
|
-
}
|
|
467
|
-
function remoteCodeRoot(localCodeUserRoot) {
|
|
468
|
-
if (localCodeUserRoot.includes("Cursor"))
|
|
469
|
-
return "~/.config/Cursor/User";
|
|
470
|
-
if (localCodeUserRoot.includes("Code - Insiders"))
|
|
471
|
-
return "~/.config/Code - Insiders/User";
|
|
472
|
-
return "~/.config/Code/User";
|
|
473
|
-
}
|
|
474
|
-
async function listDirectories(directory) {
|
|
475
|
-
try {
|
|
476
|
-
const dirents = await import("node:fs/promises").then((fs) => fs.readdir(directory, { withFileTypes: true }));
|
|
477
|
-
return dirents.filter((dirent) => dirent.isDirectory()).map((dirent) => path.join(directory, dirent.name));
|
|
478
|
-
}
|
|
479
|
-
catch {
|
|
480
|
-
return [];
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
async function listFiles(directory) {
|
|
484
|
-
const result = [];
|
|
485
|
-
async function visit(current) {
|
|
486
|
-
const dirents = await import("node:fs/promises").then((fs) => fs.readdir(current, { withFileTypes: true })).catch(() => []);
|
|
487
|
-
for (const dirent of dirents) {
|
|
488
|
-
const child = path.join(current, dirent.name);
|
|
489
|
-
if (dirent.isDirectory())
|
|
490
|
-
await visit(child);
|
|
491
|
-
else if (dirent.isFile())
|
|
492
|
-
result.push(child);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
await visit(directory);
|
|
496
|
-
return result;
|
|
497
|
-
}
|
|
498
|
-
function addCandidate(candidates, source, remoteRoot, label, preferenceKey) {
|
|
499
|
-
candidates.push({ source, remoteRoot: expandRemoteHome(remoteRoot), label, sensitive: true, preferenceKey, promptLabel: label });
|
|
500
|
-
}
|
|
501
|
-
function dedupeCandidates(candidates) {
|
|
502
|
-
const seen = new Set();
|
|
503
|
-
return candidates.filter((candidate) => {
|
|
504
|
-
const key = `${candidate.source}\0${candidate.remoteRoot}`;
|
|
505
|
-
if (seen.has(key))
|
|
506
|
-
return false;
|
|
507
|
-
seen.add(key);
|
|
508
|
-
return true;
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
function expandRemoteHome(value) {
|
|
512
|
-
if (value === "~")
|
|
513
|
-
return "/root";
|
|
514
|
-
if (value.startsWith("~/"))
|
|
515
|
-
return `/root/${value.slice(2)}`;
|
|
516
|
-
return value;
|
|
517
|
-
}
|
|
518
|
-
function sqliteLiteral(value) {
|
|
519
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
520
|
-
}
|
|
521
|
-
async function exists(filePath) {
|
|
522
|
-
try {
|
|
523
|
-
await stat(filePath);
|
|
524
|
-
return true;
|
|
525
|
-
}
|
|
526
|
-
catch {
|
|
527
|
-
return false;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { definePlugin } from "../../../src/plugin-api.js";
|
|
5
|
-
export function awsAuthPlugin() {
|
|
6
|
-
return definePlugin({
|
|
7
|
-
name: "@freestyle-sync/auth-aws",
|
|
8
|
-
async discoverContextCandidates({ options }) {
|
|
9
|
-
if (!options.includeAuth)
|
|
10
|
-
return [];
|
|
11
|
-
const item = { source: path.join(homedir(), ".aws"), remoteRoot: "/root/.aws", label: "AWS credentials", sensitive: true, preferenceKey: "context:aws", promptLabel: "AWS credentials" };
|
|
12
|
-
return await exists(item.source) ? [item] : [];
|
|
13
|
-
},
|
|
14
|
-
async afterContextSync({ vm, changedRemotePaths, utils }) {
|
|
15
|
-
if (!changedRemotePaths.some((remotePath) => remotePath === "/root/.aws" || remotePath.startsWith("/root/.aws/")))
|
|
16
|
-
return;
|
|
17
|
-
await utils.checkedExec(vm, "chown -R root:root /root/.aws 2>/dev/null || true; chmod 700 /root/.aws 2>/dev/null || true");
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
async function exists(filePath) {
|
|
22
|
-
try {
|
|
23
|
-
await stat(filePath);
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { definePlugin } from "../../../src/plugin-api.js";
|
|
5
|
-
export function azureAuthPlugin() {
|
|
6
|
-
return definePlugin({
|
|
7
|
-
name: "@freestyle-sync/auth-azure",
|
|
8
|
-
async discoverContextCandidates({ options }) {
|
|
9
|
-
if (!options.includeAuth)
|
|
10
|
-
return [];
|
|
11
|
-
const item = { source: path.join(homedir(), ".azure"), remoteRoot: "/root/.azure", label: "Azure credentials", sensitive: true, preferenceKey: "context:azure", promptLabel: "Azure credentials" };
|
|
12
|
-
return await exists(item.source) ? [item] : [];
|
|
13
|
-
},
|
|
14
|
-
async afterContextSync({ vm, changedRemotePaths, utils }) {
|
|
15
|
-
if (!changedRemotePaths.some((remotePath) => remotePath === "/root/.azure" || remotePath.startsWith("/root/.azure/")))
|
|
16
|
-
return;
|
|
17
|
-
await utils.checkedExec(vm, "chown -R root:root /root/.azure 2>/dev/null || true; chmod 700 /root/.azure 2>/dev/null || true");
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
async function exists(filePath) {
|
|
22
|
-
try {
|
|
23
|
-
await stat(filePath);
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|