fss-link 1.5.7 → 1.6.0
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 +1 -1
- package/bundle/fss-link.js +4785 -3183
- package/package.json +4 -1
- package/scripts/analyze-session-logs.sh +279 -0
- package/scripts/build.js +55 -0
- package/scripts/build_package.js +37 -0
- package/scripts/build_sandbox.js +195 -0
- package/scripts/build_vscode_companion.js +30 -0
- package/scripts/check-build-status.js +148 -0
- package/scripts/check-publish.js +101 -0
- package/scripts/clean.js +55 -0
- package/scripts/copy_bundle_assets.js +40 -0
- package/scripts/copy_files.js +56 -0
- package/scripts/create_alias.sh +39 -0
- package/scripts/emergency-kill-all-tests.sh +95 -0
- package/scripts/emergency-kill-vitest.sh +95 -0
- package/scripts/extract-session-logs.sh +202 -0
- package/scripts/generate-git-commit-info.js +71 -0
- package/scripts/get-previous-tag.js +213 -0
- package/scripts/get-release-version.js +119 -0
- package/scripts/index-session-logs.sh +173 -0
- package/scripts/install-linux.sh +294 -0
- package/scripts/install-macos.sh +343 -0
- package/scripts/install-windows.ps1 +427 -0
- package/scripts/local_telemetry.js +219 -0
- package/scripts/memory-monitor.sh +165 -0
- package/scripts/postinstall-message.js +31 -0
- package/scripts/prepare-package.js +51 -0
- package/scripts/process-session-log.py +302 -0
- package/scripts/quick-install.sh +195 -0
- package/scripts/sandbox_command.js +126 -0
- package/scripts/start.js +76 -0
- package/scripts/telemetry.js +85 -0
- package/scripts/telemetry_gcp.js +188 -0
- package/scripts/telemetry_utils.js +421 -0
- package/scripts/test-windows-paths.js +51 -0
- package/scripts/tests/get-release-version.test.js +110 -0
- package/scripts/tests/test-setup.ts +12 -0
- package/scripts/tests/vitest.config.ts +20 -0
- package/scripts/version.js +83 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright 2025 Google LLC
|
|
6
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import net from 'net';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import { execSync } from 'child_process';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
|
|
20
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
21
|
+
const projectHash = crypto
|
|
22
|
+
.createHash('sha256')
|
|
23
|
+
.update(projectRoot)
|
|
24
|
+
.digest('hex');
|
|
25
|
+
|
|
26
|
+
// User-level .fss-link directory in home
|
|
27
|
+
const USER_GEMINI_DIR = path.join(os.homedir(), '.fss-link');
|
|
28
|
+
// Project-level .fss-link directory in the workspace
|
|
29
|
+
const WORKSPACE_GEMINI_DIR = path.join(projectRoot, '.fss-link');
|
|
30
|
+
|
|
31
|
+
// Telemetry artifacts are stored in a hashed directory under the user's ~/.fss-link/tmp
|
|
32
|
+
export const OTEL_DIR = path.join(USER_GEMINI_DIR, 'tmp', projectHash, 'otel');
|
|
33
|
+
export const BIN_DIR = path.join(OTEL_DIR, 'bin');
|
|
34
|
+
|
|
35
|
+
// Workspace settings remain in the project's .fss-link directory
|
|
36
|
+
export const WORKSPACE_SETTINGS_FILE = path.join(
|
|
37
|
+
WORKSPACE_GEMINI_DIR,
|
|
38
|
+
'settings.json',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export function getJson(url) {
|
|
42
|
+
const tmpFile = path.join(
|
|
43
|
+
os.tmpdir(),
|
|
44
|
+
`fss-link-releases-${Date.now()}.json`,
|
|
45
|
+
);
|
|
46
|
+
try {
|
|
47
|
+
execSync(
|
|
48
|
+
`curl -sL -H "User-Agent: fss-link-dev-script" -o "${tmpFile}" "${url}"`,
|
|
49
|
+
{ stdio: 'pipe' },
|
|
50
|
+
);
|
|
51
|
+
const content = fs.readFileSync(tmpFile, 'utf-8');
|
|
52
|
+
return JSON.parse(content);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(`Failed to fetch or parse JSON from ${url}`);
|
|
55
|
+
throw e;
|
|
56
|
+
} finally {
|
|
57
|
+
if (fs.existsSync(tmpFile)) {
|
|
58
|
+
fs.unlinkSync(tmpFile);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function downloadFile(url, dest) {
|
|
64
|
+
try {
|
|
65
|
+
execSync(`curl -fL -sS -o "${dest}" "${url}"`, {
|
|
66
|
+
stdio: 'pipe',
|
|
67
|
+
});
|
|
68
|
+
return dest;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(`Failed to download file from ${url}`);
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function findFile(startPath, filter) {
|
|
76
|
+
if (!fs.existsSync(startPath)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const files = fs.readdirSync(startPath);
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const filename = path.join(startPath, file);
|
|
82
|
+
const stat = fs.lstatSync(filename);
|
|
83
|
+
if (stat.isDirectory()) {
|
|
84
|
+
const result = findFile(filename, filter);
|
|
85
|
+
if (result) return result;
|
|
86
|
+
} else if (filter(file)) {
|
|
87
|
+
return filename;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function fileExists(filePath) {
|
|
94
|
+
return fs.existsSync(filePath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function readJsonFile(filePath) {
|
|
98
|
+
if (!fileExists(filePath)) {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(content);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error(`Error parsing JSON from ${filePath}: ${e.message}`);
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function writeJsonFile(filePath, data) {
|
|
111
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function moveBinary(source, destination) {
|
|
115
|
+
try {
|
|
116
|
+
fs.renameSync(source, destination);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code !== 'EXDEV') {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
// Handle a cross-device error: copy-to-temp-then-rename.
|
|
122
|
+
const destDir = path.dirname(destination);
|
|
123
|
+
const destFile = path.basename(destination);
|
|
124
|
+
const tempDest = path.join(destDir, `${destFile}.tmp`);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
fs.copyFileSync(source, tempDest);
|
|
128
|
+
fs.renameSync(tempDest, destination);
|
|
129
|
+
} catch (moveError) {
|
|
130
|
+
// If copy or rename fails, clean up the intermediate temp file.
|
|
131
|
+
if (fs.existsSync(tempDest)) {
|
|
132
|
+
fs.unlinkSync(tempDest);
|
|
133
|
+
}
|
|
134
|
+
throw moveError;
|
|
135
|
+
}
|
|
136
|
+
fs.unlinkSync(source);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function waitForPort(port, timeout = 10000) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const startTime = Date.now();
|
|
143
|
+
const tryConnect = () => {
|
|
144
|
+
const socket = new net.Socket();
|
|
145
|
+
socket.once('connect', () => {
|
|
146
|
+
socket.end();
|
|
147
|
+
resolve();
|
|
148
|
+
});
|
|
149
|
+
socket.once('error', (_) => {
|
|
150
|
+
if (Date.now() - startTime > timeout) {
|
|
151
|
+
reject(new Error(`Timeout waiting for port ${port} to open.`));
|
|
152
|
+
} else {
|
|
153
|
+
setTimeout(tryConnect, 500);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
socket.connect(port, 'localhost');
|
|
157
|
+
};
|
|
158
|
+
tryConnect();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function ensureBinary(
|
|
163
|
+
executableName,
|
|
164
|
+
repo,
|
|
165
|
+
assetNameCallback,
|
|
166
|
+
binaryNameInArchive,
|
|
167
|
+
isJaeger = false,
|
|
168
|
+
) {
|
|
169
|
+
const executablePath = path.join(BIN_DIR, executableName);
|
|
170
|
+
if (fileExists(executablePath)) {
|
|
171
|
+
console.log(`✅ ${executableName} already exists at ${executablePath}`);
|
|
172
|
+
return executablePath;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(`🔍 ${executableName} not found. Downloading from ${repo}...`);
|
|
176
|
+
|
|
177
|
+
const platform = process.platform === 'win32' ? 'windows' : process.platform;
|
|
178
|
+
const arch = process.arch === 'x64' ? 'amd64' : process.arch;
|
|
179
|
+
const ext = platform === 'windows' ? 'zip' : 'tar.gz';
|
|
180
|
+
|
|
181
|
+
if (isJaeger && platform === 'windows' && arch === 'arm64') {
|
|
182
|
+
console.warn(
|
|
183
|
+
`⚠️ Jaeger does not have a release for Windows on ARM64. Skipping.`,
|
|
184
|
+
);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let release;
|
|
189
|
+
let asset;
|
|
190
|
+
|
|
191
|
+
if (isJaeger) {
|
|
192
|
+
console.log(`🔍 Finding latest Jaeger v2+ asset...`);
|
|
193
|
+
const releases = getJson(`https://api.github.com/repos/${repo}/releases`);
|
|
194
|
+
const sortedReleases = releases
|
|
195
|
+
.filter((r) => !r.prerelease && r.tag_name.startsWith('v'))
|
|
196
|
+
.sort((a, b) => {
|
|
197
|
+
const aVersion = a.tag_name.substring(1).split('.').map(Number);
|
|
198
|
+
const bVersion = b.tag_name.substring(1).split('.').map(Number);
|
|
199
|
+
for (let i = 0; i < Math.max(aVersion.length, bVersion.length); i++) {
|
|
200
|
+
if ((aVersion[i] || 0) > (bVersion[i] || 0)) return -1;
|
|
201
|
+
if ((aVersion[i] || 0) < (bVersion[i] || 0)) return 1;
|
|
202
|
+
}
|
|
203
|
+
return 0;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
for (const r of sortedReleases) {
|
|
207
|
+
const expectedSuffix =
|
|
208
|
+
platform === 'windows'
|
|
209
|
+
? `-${platform}-${arch}.zip`
|
|
210
|
+
: `-${platform}-${arch}.tar.gz`;
|
|
211
|
+
const foundAsset = r.assets.find(
|
|
212
|
+
(a) =>
|
|
213
|
+
a.name.startsWith('jaeger-2.') && a.name.endsWith(expectedSuffix),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (foundAsset) {
|
|
217
|
+
release = r;
|
|
218
|
+
asset = foundAsset;
|
|
219
|
+
console.log(
|
|
220
|
+
`⬇️ Found ${asset.name} in release ${r.tag_name}, downloading...`,
|
|
221
|
+
);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!asset) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Could not find a suitable Jaeger v2 asset for platform ${platform}/${arch}.`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
release = getJson(`https://api.github.com/repos/${repo}/releases/latest`);
|
|
232
|
+
const version = release.tag_name.startsWith('v')
|
|
233
|
+
? release.tag_name.substring(1)
|
|
234
|
+
: release.tag_name;
|
|
235
|
+
const assetName = assetNameCallback(version, platform, arch, ext);
|
|
236
|
+
asset = release.assets.find((a) => a.name === assetName);
|
|
237
|
+
if (!asset) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Could not find a suitable asset for ${repo} (version ${version}) on platform ${platform}/${arch}. Searched for: ${assetName}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const downloadUrl = asset.browser_download_url;
|
|
245
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fss-link-telemetry-'));
|
|
246
|
+
const archivePath = path.join(tmpDir, asset.name);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
console.log(`⬇️ Downloading ${asset.name}...`);
|
|
250
|
+
downloadFile(downloadUrl, archivePath);
|
|
251
|
+
console.log(`📦 Extracting ${asset.name}...`);
|
|
252
|
+
|
|
253
|
+
const actualExt = asset.name.endsWith('.zip') ? 'zip' : 'tar.gz';
|
|
254
|
+
|
|
255
|
+
if (actualExt === 'zip') {
|
|
256
|
+
execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`, { stdio: 'pipe' });
|
|
257
|
+
} else {
|
|
258
|
+
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, { stdio: 'pipe' });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const nameToFind = binaryNameInArchive || executableName;
|
|
262
|
+
const foundBinaryPath = findFile(tmpDir, (file) => {
|
|
263
|
+
if (platform === 'windows') {
|
|
264
|
+
return file === `${nameToFind}.exe`;
|
|
265
|
+
}
|
|
266
|
+
return file === nameToFind;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (!foundBinaryPath) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Could not find binary "${nameToFind}" in extracted archive at ${tmpDir}. Contents: ${fs.readdirSync(tmpDir).join(', ')}`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
moveBinary(foundBinaryPath, executablePath);
|
|
276
|
+
|
|
277
|
+
if (platform !== 'windows') {
|
|
278
|
+
fs.chmodSync(executablePath, '755');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(`✅ ${executableName} installed at ${executablePath}`);
|
|
282
|
+
return executablePath;
|
|
283
|
+
} finally {
|
|
284
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
285
|
+
if (fs.existsSync(archivePath)) {
|
|
286
|
+
fs.unlinkSync(archivePath);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function manageTelemetrySettings(
|
|
292
|
+
enable,
|
|
293
|
+
oTelEndpoint = 'http://localhost:4317',
|
|
294
|
+
target = 'local',
|
|
295
|
+
originalSandboxSettingToRestore,
|
|
296
|
+
) {
|
|
297
|
+
const workspaceSettings = readJsonFile(WORKSPACE_SETTINGS_FILE);
|
|
298
|
+
const currentSandboxSetting = workspaceSettings.sandbox;
|
|
299
|
+
let settingsModified = false;
|
|
300
|
+
|
|
301
|
+
if (typeof workspaceSettings.telemetry !== 'object') {
|
|
302
|
+
workspaceSettings.telemetry = {};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (enable) {
|
|
306
|
+
if (workspaceSettings.telemetry.enabled !== true) {
|
|
307
|
+
workspaceSettings.telemetry.enabled = true;
|
|
308
|
+
settingsModified = true;
|
|
309
|
+
console.log('⚙️ Enabled telemetry in workspace settings.');
|
|
310
|
+
}
|
|
311
|
+
if (workspaceSettings.sandbox !== false) {
|
|
312
|
+
workspaceSettings.sandbox = false;
|
|
313
|
+
settingsModified = true;
|
|
314
|
+
console.log('✅ Disabled sandbox mode for telemetry.');
|
|
315
|
+
}
|
|
316
|
+
if (workspaceSettings.telemetry.otlpEndpoint !== oTelEndpoint) {
|
|
317
|
+
workspaceSettings.telemetry.otlpEndpoint = oTelEndpoint;
|
|
318
|
+
settingsModified = true;
|
|
319
|
+
console.log(`🔧 Set telemetry OTLP endpoint to ${oTelEndpoint}.`);
|
|
320
|
+
}
|
|
321
|
+
if (workspaceSettings.telemetry.target !== target) {
|
|
322
|
+
workspaceSettings.telemetry.target = target;
|
|
323
|
+
settingsModified = true;
|
|
324
|
+
console.log(`🎯 Set telemetry target to ${target}.`);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
if (workspaceSettings.telemetry.enabled === true) {
|
|
328
|
+
delete workspaceSettings.telemetry.enabled;
|
|
329
|
+
settingsModified = true;
|
|
330
|
+
console.log('⚙️ Disabled telemetry in workspace settings.');
|
|
331
|
+
}
|
|
332
|
+
if (workspaceSettings.telemetry.otlpEndpoint) {
|
|
333
|
+
delete workspaceSettings.telemetry.otlpEndpoint;
|
|
334
|
+
settingsModified = true;
|
|
335
|
+
console.log('🔧 Cleared telemetry OTLP endpoint.');
|
|
336
|
+
}
|
|
337
|
+
if (workspaceSettings.telemetry.target) {
|
|
338
|
+
delete workspaceSettings.telemetry.target;
|
|
339
|
+
settingsModified = true;
|
|
340
|
+
console.log('🎯 Cleared telemetry target.');
|
|
341
|
+
}
|
|
342
|
+
if (Object.keys(workspaceSettings.telemetry).length === 0) {
|
|
343
|
+
delete workspaceSettings.telemetry;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (
|
|
347
|
+
originalSandboxSettingToRestore !== undefined &&
|
|
348
|
+
workspaceSettings.sandbox !== originalSandboxSettingToRestore
|
|
349
|
+
) {
|
|
350
|
+
workspaceSettings.sandbox = originalSandboxSettingToRestore;
|
|
351
|
+
settingsModified = true;
|
|
352
|
+
console.log('✅ Restored original sandbox setting.');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (settingsModified) {
|
|
357
|
+
writeJsonFile(WORKSPACE_SETTINGS_FILE, workspaceSettings);
|
|
358
|
+
console.log('✅ Workspace settings updated.');
|
|
359
|
+
} else {
|
|
360
|
+
console.log(
|
|
361
|
+
enable
|
|
362
|
+
? '✅ Workspace settings are already configured for telemetry.'
|
|
363
|
+
: '✅ Workspace settings already reflect telemetry disabled.',
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return currentSandboxSetting;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function registerCleanup(
|
|
370
|
+
getProcesses,
|
|
371
|
+
getLogFileDescriptors,
|
|
372
|
+
originalSandboxSetting,
|
|
373
|
+
) {
|
|
374
|
+
let cleanedUp = false;
|
|
375
|
+
const cleanup = () => {
|
|
376
|
+
if (cleanedUp) return;
|
|
377
|
+
cleanedUp = true;
|
|
378
|
+
|
|
379
|
+
console.log('\n👋 Shutting down...');
|
|
380
|
+
|
|
381
|
+
manageTelemetrySettings(false, null, originalSandboxSetting);
|
|
382
|
+
|
|
383
|
+
const processes = getProcesses ? getProcesses() : [];
|
|
384
|
+
processes.forEach((proc) => {
|
|
385
|
+
if (proc && proc.pid) {
|
|
386
|
+
const name = path.basename(proc.spawnfile);
|
|
387
|
+
try {
|
|
388
|
+
console.log(`🛑 Stopping ${name} (PID: ${proc.pid})...`);
|
|
389
|
+
process.kill(proc.pid, 'SIGTERM');
|
|
390
|
+
console.log(`✅ ${name} stopped.`);
|
|
391
|
+
} catch (e) {
|
|
392
|
+
if (e.code !== 'ESRCH') {
|
|
393
|
+
console.error(`Error stopping ${name}: ${e.message}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const logFileDescriptors = getLogFileDescriptors
|
|
400
|
+
? getLogFileDescriptors()
|
|
401
|
+
: [];
|
|
402
|
+
logFileDescriptors.forEach((fd) => {
|
|
403
|
+
if (fd) {
|
|
404
|
+
try {
|
|
405
|
+
fs.closeSync(fd);
|
|
406
|
+
} catch (_) {
|
|
407
|
+
/* no-op */
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
process.on('exit', cleanup);
|
|
414
|
+
process.on('SIGINT', () => process.exit(0));
|
|
415
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
416
|
+
process.on('uncaughtException', (err) => {
|
|
417
|
+
console.error('Uncaught Exception:', err);
|
|
418
|
+
cleanup();
|
|
419
|
+
process.exit(1);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
// Test how paths are normalized
|
|
11
|
+
function testPathNormalization() {
|
|
12
|
+
// Use platform-agnostic path construction instead of hardcoded paths
|
|
13
|
+
const testPath = path.join('test', 'project', 'src', 'file.md');
|
|
14
|
+
const absoluteTestPath = path.resolve('test', 'project', 'src', 'file.md');
|
|
15
|
+
|
|
16
|
+
console.log('Testing path normalization:');
|
|
17
|
+
console.log('Relative path:', testPath);
|
|
18
|
+
console.log('Absolute path:', absoluteTestPath);
|
|
19
|
+
|
|
20
|
+
// Test path.join with different segments
|
|
21
|
+
const joinedPath = path.join('test', 'project', 'src', 'file.md');
|
|
22
|
+
console.log('Joined path:', joinedPath);
|
|
23
|
+
|
|
24
|
+
// Test path.normalize
|
|
25
|
+
console.log('Normalized relative path:', path.normalize(testPath));
|
|
26
|
+
console.log('Normalized absolute path:', path.normalize(absoluteTestPath));
|
|
27
|
+
|
|
28
|
+
// Test how the test would see these paths
|
|
29
|
+
const testContent = `--- File: ${absoluteTestPath} ---\nContent\n--- End of File: ${absoluteTestPath} ---`;
|
|
30
|
+
console.log('\nTest content with platform-agnostic paths:');
|
|
31
|
+
console.log(testContent);
|
|
32
|
+
|
|
33
|
+
// Try to match with different patterns
|
|
34
|
+
const marker = `--- File: ${absoluteTestPath} ---`;
|
|
35
|
+
console.log('\nTrying to match:', marker);
|
|
36
|
+
console.log('Direct match:', testContent.includes(marker));
|
|
37
|
+
|
|
38
|
+
// Test with normalized path in marker
|
|
39
|
+
const normalizedMarker = `--- File: ${path.normalize(absoluteTestPath)} ---`;
|
|
40
|
+
console.log(
|
|
41
|
+
'Normalized marker match:',
|
|
42
|
+
testContent.includes(normalizedMarker),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Test path resolution
|
|
46
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
47
|
+
console.log('\nCurrent file path:', __filename);
|
|
48
|
+
console.log('Directory name:', path.dirname(__filename));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
testPathNormalization();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { getReleaseVersion } from '../get-release-version';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
|
|
12
|
+
vi.mock('child_process', () => ({
|
|
13
|
+
execSync: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('fs', async (importOriginal) => {
|
|
17
|
+
const mod = await importOriginal();
|
|
18
|
+
return {
|
|
19
|
+
...mod,
|
|
20
|
+
default: {
|
|
21
|
+
...mod.default,
|
|
22
|
+
readFileSync: vi.fn(),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('getReleaseVersion', () => {
|
|
28
|
+
const originalEnv = { ...process.env };
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.resetModules();
|
|
32
|
+
process.env = { ...originalEnv };
|
|
33
|
+
vi.useFakeTimers();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
process.env = originalEnv;
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
vi.useRealTimers();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should calculate nightly version when IS_NIGHTLY is true', () => {
|
|
43
|
+
process.env.IS_NIGHTLY = 'true';
|
|
44
|
+
vi.mocked(fs.default.readFileSync).mockReturnValue(
|
|
45
|
+
JSON.stringify({ version: '0.1.0' }),
|
|
46
|
+
);
|
|
47
|
+
// Mock git tag command to return empty (no existing nightly tags)
|
|
48
|
+
vi.mocked(execSync).mockReturnValue('');
|
|
49
|
+
const { releaseTag, releaseVersion, npmTag } = getReleaseVersion();
|
|
50
|
+
expect(releaseTag).toBe('v0.1.1-nightly.0');
|
|
51
|
+
expect(releaseVersion).toBe('0.1.1-nightly.0');
|
|
52
|
+
expect(npmTag).toBe('nightly');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should use manual version when provided', () => {
|
|
56
|
+
process.env.MANUAL_VERSION = '1.2.3';
|
|
57
|
+
const { releaseTag, releaseVersion, npmTag } = getReleaseVersion();
|
|
58
|
+
expect(releaseTag).toBe('v1.2.3');
|
|
59
|
+
expect(releaseVersion).toBe('1.2.3');
|
|
60
|
+
expect(npmTag).toBe('latest');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should prepend v to manual version if missing', () => {
|
|
64
|
+
process.env.MANUAL_VERSION = '1.2.3';
|
|
65
|
+
const { releaseTag } = getReleaseVersion();
|
|
66
|
+
expect(releaseTag).toBe('v1.2.3');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle pre-release versions correctly', () => {
|
|
70
|
+
process.env.MANUAL_VERSION = 'v1.2.3-beta.1';
|
|
71
|
+
const { releaseTag, releaseVersion, npmTag } = getReleaseVersion();
|
|
72
|
+
expect(releaseTag).toBe('v1.2.3-beta.1');
|
|
73
|
+
expect(releaseVersion).toBe('1.2.3-beta.1');
|
|
74
|
+
expect(npmTag).toBe('beta');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should throw an error for invalid version format', () => {
|
|
78
|
+
process.env.MANUAL_VERSION = '1.2';
|
|
79
|
+
expect(() => getReleaseVersion()).toThrow(
|
|
80
|
+
'Error: Version must be in the format vX.Y.Z or vX.Y.Z-prerelease',
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should throw an error if no version is provided for non-nightly release', () => {
|
|
85
|
+
expect(() => getReleaseVersion()).toThrow(
|
|
86
|
+
'Error: No version specified and this is not a nightly release.',
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should throw an error for versions with build metadata', () => {
|
|
91
|
+
process.env.MANUAL_VERSION = 'v1.2.3+build456';
|
|
92
|
+
expect(() => getReleaseVersion()).toThrow(
|
|
93
|
+
'Error: Versions with build metadata (+) are not supported for releases.',
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('get-release-version script', () => {
|
|
99
|
+
it('should print version JSON to stdout when executed directly', () => {
|
|
100
|
+
const expectedJson = {
|
|
101
|
+
releaseTag: 'v0.1.1-nightly.0',
|
|
102
|
+
releaseVersion: '0.1.1-nightly.0',
|
|
103
|
+
npmTag: 'nightly',
|
|
104
|
+
};
|
|
105
|
+
execSync.mockReturnValue(JSON.stringify(expectedJson));
|
|
106
|
+
|
|
107
|
+
const result = execSync('node scripts/get-release-version.js').toString();
|
|
108
|
+
expect(JSON.parse(result)).toEqual(expectedJson);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { defineConfig } from 'vitest/config';
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
test: {
|
|
11
|
+
globals: true,
|
|
12
|
+
environment: 'node',
|
|
13
|
+
include: ['scripts/tests/**/*.test.js'],
|
|
14
|
+
setupFiles: ['scripts/tests/test-setup.ts'],
|
|
15
|
+
coverage: {
|
|
16
|
+
provider: 'v8',
|
|
17
|
+
reporter: ['text', 'lcov'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
|
|
11
|
+
// A script to handle versioning and ensure all related changes are in a single, atomic commit.
|
|
12
|
+
|
|
13
|
+
function run(command) {
|
|
14
|
+
console.log(`> ${command}`);
|
|
15
|
+
execSync(command, { stdio: 'inherit' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readJson(filePath) {
|
|
19
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeJson(filePath, data) {
|
|
23
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 1. Get the version from the command line arguments.
|
|
27
|
+
const versionType = process.argv[2];
|
|
28
|
+
if (!versionType) {
|
|
29
|
+
console.error('Error: No version specified.');
|
|
30
|
+
console.error(
|
|
31
|
+
'Usage: npm run version <version> (e.g., 1.2.3 or patch|minor|major|prerelease)',
|
|
32
|
+
);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Bump the version in the root and all workspace package.json files.
|
|
37
|
+
run(`npm version ${versionType} --no-git-tag-version --allow-same-version`);
|
|
38
|
+
|
|
39
|
+
// 3. Get all workspaces and filter out the one we don't want to version.
|
|
40
|
+
const workspacesToExclude = [];
|
|
41
|
+
const lsOutput = JSON.parse(
|
|
42
|
+
execSync('npm ls --workspaces --json --depth=0').toString(),
|
|
43
|
+
);
|
|
44
|
+
const allWorkspaces = Object.keys(lsOutput.dependencies || {});
|
|
45
|
+
const workspacesToVersion = allWorkspaces.filter(
|
|
46
|
+
(wsName) => !workspacesToExclude.includes(wsName),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
for (const workspaceName of workspacesToVersion) {
|
|
50
|
+
run(
|
|
51
|
+
`npm version ${versionType} --workspace ${workspaceName} --no-git-tag-version --allow-same-version`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 4. Get the new version number from the root package.json
|
|
56
|
+
const rootPackageJsonPath = resolve(process.cwd(), 'package.json');
|
|
57
|
+
const newVersion = readJson(rootPackageJsonPath).version;
|
|
58
|
+
|
|
59
|
+
// 6. Update the sandboxImageUri in the root package.json
|
|
60
|
+
const rootPackageJson = readJson(rootPackageJsonPath);
|
|
61
|
+
if (rootPackageJson.config?.sandboxImageUri) {
|
|
62
|
+
rootPackageJson.config.sandboxImageUri =
|
|
63
|
+
rootPackageJson.config.sandboxImageUri.replace(/:.*$/, `:${newVersion}`);
|
|
64
|
+
console.log(`Updated sandboxImageUri in root to use version ${newVersion}`);
|
|
65
|
+
writeJson(rootPackageJsonPath, rootPackageJson);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 7. Update the sandboxImageUri in the cli package.json
|
|
69
|
+
const cliPackageJsonPath = resolve(process.cwd(), 'packages/cli/package.json');
|
|
70
|
+
const cliPackageJson = readJson(cliPackageJsonPath);
|
|
71
|
+
if (cliPackageJson.config?.sandboxImageUri) {
|
|
72
|
+
cliPackageJson.config.sandboxImageUri =
|
|
73
|
+
cliPackageJson.config.sandboxImageUri.replace(/:.*$/, `:${newVersion}`);
|
|
74
|
+
console.log(
|
|
75
|
+
`Updated sandboxImageUri in cli package to use version ${newVersion}`,
|
|
76
|
+
);
|
|
77
|
+
writeJson(cliPackageJsonPath, cliPackageJson);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 8. Run `npm install` to update package-lock.json.
|
|
81
|
+
run('npm install');
|
|
82
|
+
|
|
83
|
+
console.log(`Successfully bumped versions to v${newVersion}.`);
|