mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
618bd6a0df | ||
|
|
c455a09fc3 | ||
|
|
b6be356e40 | ||
|
|
945bf1523d | ||
|
|
a928a9fb76 | ||
|
|
0b2d7e95cc | ||
|
|
556bea1645 | ||
|
|
0302d5592b | ||
|
|
eb34ec0dbf | ||
|
|
59e2c3224e | ||
|
|
3c77ef7b01 |
@@ -0,0 +1,92 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to BrowserSkill will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
Starting from 0.2.0, CLI / Extension / DSH Plugin share the same version number.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.0] - 2026-09-02
|
||||
|
||||
### Added
|
||||
- File transfer: upload and download support across CLI, Extension, and DSH Plugin
|
||||
- File transfer: drag-and-drop upload (`drop-to-upload`)
|
||||
- VOM semantic graph, name enrichment, and hover perception modules
|
||||
- VOM hover probing (opt-in via `observe` parameter)
|
||||
- DSH Plugin: browser tool parity with CLI commands
|
||||
- Edge Add-ons automated publishing in CI
|
||||
- Protocol upgrade reminder when CLI / Extension protocol versions differ
|
||||
- Browser evaluation harness (`evals/browser/`)
|
||||
- Unified release script (`scripts/release.mjs`)
|
||||
|
||||
### Changed
|
||||
- **Version scheme**: all three components now share the same semver
|
||||
- VOM rendering algorithm optimizations
|
||||
- Leaner SKILL.md agent instructions
|
||||
- DSH Plugin: simplified browser commands
|
||||
- Borrow confirmation UX — proactive focus and longer timeout
|
||||
- PiP window now has a close button
|
||||
|
||||
### Fixed
|
||||
- Screenshot media type detection (was hard-coded to `image/png`)
|
||||
- DSH Plugin session lifecycle stability
|
||||
- DSH Plugin Cordis package ID mismatch
|
||||
- Observation thumbnail media type sniffing
|
||||
- Upload/download race conditions and layout bypass issues
|
||||
- VOM repeated name and safety policy issues
|
||||
|
||||
---
|
||||
|
||||
*Previous releases used independent version numbers per component.*
|
||||
|
||||
## CLI 0.1.11 / Extension 0.1.7 / DSH Plugin 0.1.2 — 2026-08-26 ~ 2026-08-29
|
||||
|
||||
### Added
|
||||
- DSH Plugin sidebar integration, session lifecycle, and archive cleanup
|
||||
- Recorder iframe and OOPIF support
|
||||
|
||||
### Changed
|
||||
- VOM functional refactor
|
||||
|
||||
### Fixed
|
||||
- Recorder safety policy and bug fixes
|
||||
|
||||
## CLI 0.1.10 / Extension 0.1.6 — 2026-08-08
|
||||
|
||||
### Added
|
||||
- VOM observation recording and settled-state detection
|
||||
- Record overlay timer
|
||||
|
||||
### Fixed
|
||||
- Browser keepalive disconnect handling
|
||||
|
||||
## CLI 0.1.9 / Extension 0.1.5 — 2026-07-29
|
||||
|
||||
### Added
|
||||
- CLI auto-update mechanism
|
||||
- Trace v3 protocol and recorder
|
||||
|
||||
### Fixed
|
||||
- MV3 keepalive disconnect
|
||||
|
||||
## CLI 0.1.8 / Extension 0.1.4 — 2026-07-22
|
||||
|
||||
### Added
|
||||
- More browser interaction actions
|
||||
|
||||
### Fixed
|
||||
- Windows named-pipe hash-only path issue
|
||||
|
||||
## CLI 0.1.7 / Extension 0.1.3 — 2026-07-07
|
||||
|
||||
Initial public release pair.
|
||||
|
||||
## CLI 0.1.6 — 2026-06-30
|
||||
|
||||
### Fixed
|
||||
- Minor CLI fixes
|
||||
|
||||
## CLI 0.1.5 / Extension 0.1.2 — 2026-06-22
|
||||
|
||||
First tagged releases.
|
||||
Generated
+2
-2
@@ -134,7 +134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bsk"
|
||||
version = "0.1.11"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -169,7 +169,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bsk-protocol"
|
||||
version = "0.1.11"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"semver",
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ members = [
|
||||
default-members = ["crates/bsk-cli"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.11"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
license = "MIT"
|
||||
@@ -43,7 +43,7 @@ rand = "0.8"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
|
||||
# Workspace crates (version required for `cargo publish` of dependents)
|
||||
bsk-protocol = { version = "0.1.9", path = "crates/bsk-protocol" }
|
||||
bsk-protocol = { version = "0.2.0", path = "crates/bsk-protocol" }
|
||||
|
||||
[profile.release]
|
||||
strip = "symbols"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@browser-skill/extension",
|
||||
"version": "0.1.7",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -45,10 +45,32 @@ function portHarness() {
|
||||
|
||||
describe("RecordFrameAgent", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
document.documentElement.removeAttribute(RECORD_DOCUMENT_ATTRIBUTE);
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
it("starts when crypto.randomUUID is unavailable", async () => {
|
||||
const harness = portHarness();
|
||||
const getRandomValues = vi.fn((bytes: Uint8Array) => {
|
||||
bytes.fill(0x11);
|
||||
return bytes;
|
||||
});
|
||||
vi.stubGlobal("crypto", { getRandomValues });
|
||||
vi.stubGlobal("chrome", {
|
||||
runtime: {
|
||||
connect: vi.fn(() => harness.port),
|
||||
sendMessage: vi.fn(),
|
||||
},
|
||||
});
|
||||
const agent = new RecordFrameAgent();
|
||||
|
||||
await expect(
|
||||
agent.start({ type: RECORD_FRAME_START, requestId: "rec-http", startedAtMs: 10 }),
|
||||
).resolves.toEqual({ ok: true });
|
||||
expect(getRandomValues).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps a failed stop retryable and flushes the final dirty fill", async () => {
|
||||
const harness = portHarness();
|
||||
const sendMessage = vi
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createRandomUuid } from "@/lib/random-uuid";
|
||||
import {
|
||||
isAcceptedRecordStepAck,
|
||||
RECORD_STEP,
|
||||
@@ -18,7 +19,7 @@ export class RecordStepDelivery {
|
||||
constructor(
|
||||
requestId: string,
|
||||
send: SendRecordStepMessage = (message) => chrome.runtime.sendMessage(message),
|
||||
producerId: string = crypto.randomUUID(),
|
||||
producerId: string = createRandomUuid(),
|
||||
) {
|
||||
this.#requestId = requestId;
|
||||
this.#producerId = producerId;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createRandomUuid } from "@/lib/random-uuid";
|
||||
import {
|
||||
markRecordingDocument,
|
||||
type RecordingDocumentMarker,
|
||||
@@ -67,7 +68,7 @@ export class RecordFrameAgent {
|
||||
async #start(
|
||||
message: RecordFrameStartMessage,
|
||||
): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
const producerId = crypto.randomUUID();
|
||||
const producerId = createRandomUuid();
|
||||
const root = await waitForDocumentElement();
|
||||
if (this.#disposed) return { ok: false, error: "recording document is disposed" };
|
||||
const marker = markRecordingDocument(producerId, root);
|
||||
|
||||
@@ -122,8 +122,8 @@ describe("App", () => {
|
||||
|
||||
expect(screen.queryByText(/^扩展 v/)).toBeNull();
|
||||
expect(screen.queryByText(/^daemon v/)).toBeNull();
|
||||
expect(screen.getByTitle("扩展版本").textContent).toBe(EXTENSION_VERSION);
|
||||
expect(screen.getByTitle("bsk 版本").textContent).toBe(mockDaemonVersion);
|
||||
expect(screen.getByTitle("扩展版本").textContent).toBe(`Ext ${EXTENSION_VERSION}`);
|
||||
expect(screen.getByTitle("bsk 版本").textContent).toBe(`CLI ${mockDaemonVersion}`);
|
||||
expect(screen.getByText("03c3e47f")).toBeTruthy();
|
||||
|
||||
const copyButton = screen.getByRole("button", { name: "复制实例 ID" });
|
||||
|
||||
@@ -267,9 +267,9 @@ export function App() {
|
||||
data-slot="popup-meta"
|
||||
>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<span title={t("popup.extensionVersionHint")}>{extensionVersion}</span>
|
||||
<span title={t("popup.daemonVersionHint")}>CLI {daemonVersion}</span>
|
||||
<span aria-hidden>/</span>
|
||||
<span title={t("popup.daemonVersionHint")}>{daemonVersion}</span>
|
||||
<span title={t("popup.extensionVersionHint")}>Ext {extensionVersion}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center justify-end gap-1">
|
||||
<span className="shrink-0">{t("popup.instanceTitle")}</span>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRandomUuid } from "../random-uuid";
|
||||
|
||||
describe("createRandomUuid", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("uses crypto.randomUUID when it is available", () => {
|
||||
const randomUUID = vi.fn(() => "00000000-0000-4000-8000-000000000000");
|
||||
vi.stubGlobal("crypto", { randomUUID, getRandomValues: vi.fn() });
|
||||
|
||||
expect(createRandomUuid()).toBe("00000000-0000-4000-8000-000000000000");
|
||||
expect(randomUUID).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("falls back to getRandomValues and sets UUID v4 bits", () => {
|
||||
const bytes = new Uint8Array(16).fill(0xff);
|
||||
const getRandomValues = vi.fn((target: Uint8Array) => {
|
||||
target.set(bytes);
|
||||
return target;
|
||||
});
|
||||
vi.stubGlobal("crypto", { getRandomValues });
|
||||
|
||||
const uuid = createRandomUuid();
|
||||
|
||||
expect(uuid).toBe("ffffffff-ffff-4fff-bfff-ffffffffffff");
|
||||
expect(getRandomValues).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Generate a UUID v4 in browser contexts where randomUUID() is unavailable.
|
||||
*
|
||||
* Crypto.randomUUID() is restricted to secure contexts, while
|
||||
* crypto.getRandomValues() is available to content scripts on ordinary HTTP
|
||||
* origins. BrowserSkill records many intranet pages served over HTTP, so the
|
||||
* fallback is required for recording to work on those pages.
|
||||
*/
|
||||
export function createRandomUuid(): string {
|
||||
const cryptoApi = globalThis.crypto;
|
||||
if (typeof cryptoApi.randomUUID === "function") return cryptoApi.randomUUID();
|
||||
|
||||
const bytes = new Uint8Array(16);
|
||||
cryptoApi.getRandomValues(bytes);
|
||||
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
||||
|
||||
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
return [
|
||||
hex.slice(0, 8),
|
||||
hex.slice(8, 12),
|
||||
hex.slice(12, 16),
|
||||
hex.slice(16, 20),
|
||||
hex.slice(20),
|
||||
].join("-");
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createRandomUuid } from "../random-uuid";
|
||||
import {
|
||||
isRecordFrameQueryMessage,
|
||||
RECORD_FRAME_PORT,
|
||||
@@ -188,7 +189,7 @@ export class RecordFrameCoordinator {
|
||||
const recording = this.#recordings.get(requestId);
|
||||
if (!recording) return true;
|
||||
recording.finishing = true;
|
||||
const commandId = crypto.randomUUID();
|
||||
const commandId = createRandomUuid();
|
||||
const results = await Promise.all(
|
||||
[...recording.agents.values()].map((agent) => this.#stopAgent(agent, requestId, commandId)),
|
||||
);
|
||||
|
||||
@@ -761,6 +761,7 @@ fn stage_windows_replacement(
|
||||
.with_context(|| format!("write {}", paths.script_path.display()))?;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
|
||||
let command = format!("\"{}\"", paths.script_path.display());
|
||||
if let Err(err) = Command::new("cmd.exe")
|
||||
.args(["/D", "/S", "/C"])
|
||||
@@ -768,7 +769,7 @@ fn stage_windows_replacement(
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP)
|
||||
.spawn()
|
||||
{
|
||||
let _ = std::fs::remove_file(&paths.binary_path);
|
||||
@@ -806,7 +807,7 @@ fn windows_replacement_script(target: &Path, staged_binary: &Path, restart_daemo
|
||||
:retry\r\n\
|
||||
move /Y \"{staged_binary}\" \"{target}\" >nul 2>nul\r\n\
|
||||
if errorlevel 1 (\r\n\
|
||||
timeout /t 1 /nobreak >nul\r\n\
|
||||
ping -n 2 127.0.0.1 >nul\r\n\
|
||||
goto retry\r\n\
|
||||
)\r\n\
|
||||
{restart}\
|
||||
@@ -1007,6 +1008,8 @@ mod tests {
|
||||
|
||||
assert!(script.contains(":retry"));
|
||||
assert!(script.contains("move /Y"));
|
||||
assert!(script.contains("ping -n 2 127.0.0.1"));
|
||||
assert!(!script.contains("timeout"));
|
||||
assert!(script.contains(r#"start "" /B "C:\Program Files\bsk.exe" daemon start"#));
|
||||
assert!(script.contains(r#"del /F /Q "%~f0""#));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wxg-prc-cpg/browser-skill-dsh-plugin",
|
||||
"description": "DeepSeek Harness tool plugin that exposes BrowserSkill browser automation (browser_* tools) to the model",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
Executable
+299
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Unified release script for BrowserSkill.
|
||||
*
|
||||
* Bumps CLI, Extension, and DSH Plugin to the same version, updates
|
||||
* CHANGELOG.md, commits, tags, and optionally pushes.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/release.mjs <version> # bump + commit + tag
|
||||
* node scripts/release.mjs <version> --dry-run # preview changes only
|
||||
* node scripts/release.mjs <version> --push # also push to origin
|
||||
* node scripts/release.mjs <version> --push --only ext # push only ext tag
|
||||
* node scripts/release.mjs <version> --push --only cli,dsh # push only cli + dsh tags
|
||||
* node scripts/release.mjs --push-tags --only ext # push tag(s) for an already-committed release
|
||||
*
|
||||
* Options:
|
||||
* --dry-run Show what would change without writing anything
|
||||
* --push After commit+tag, push the branch and all release tags
|
||||
* --push-tags Push release tags only (skip version bump; useful for staged rollout)
|
||||
* --only <list> Comma-separated subset of: cli, ext, dsh (default: all three)
|
||||
* --no-commit Write files but skip git commit and tagging
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const TAG_PREFIX = { cli: "cli-v", ext: "ext-v", dsh: "dsh-plugin-v" };
|
||||
const ALL_COMPONENTS = ["cli", "ext", "dsh"];
|
||||
|
||||
const VERSION_FILES = {
|
||||
cargo: "Cargo.toml",
|
||||
extension: "apps/extension/package.json",
|
||||
dshPlugin: "packages/dsh-plugin-browserskill/package.json",
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI argument parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { values, positionals } = parseArgs({
|
||||
allowPositionals: true,
|
||||
options: {
|
||||
"dry-run": { type: "boolean", default: false },
|
||||
push: { type: "boolean", default: false },
|
||||
"push-tags": { type: "boolean", default: false },
|
||||
"no-commit": { type: "boolean", default: false },
|
||||
only: { type: "string", default: "" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
});
|
||||
|
||||
if (values.help) {
|
||||
console.log(
|
||||
readFileSync(new URL(import.meta.url), "utf8").match(/\/\*\*([\s\S]*?)\*\//)?.[1] ?? "",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const dryRun = values["dry-run"];
|
||||
const pushAfter = values.push;
|
||||
const pushTagsOnly = values["push-tags"];
|
||||
const noCommit = values["no-commit"];
|
||||
const onlyRaw = values.only;
|
||||
const components = onlyRaw ? onlyRaw.split(",").map((s) => s.trim().toLowerCase()) : ALL_COMPONENTS;
|
||||
|
||||
for (const c of components) {
|
||||
if (!ALL_COMPONENTS.includes(c)) {
|
||||
console.error(`Unknown component "${c}". Valid: ${ALL_COMPONENTS.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Push-tags-only mode (no version bump)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (pushTagsOnly) {
|
||||
const currentVersion = readCurrentVersions();
|
||||
const ver = positionals[0] ?? currentVersion.cargo;
|
||||
for (const c of components) {
|
||||
const tag = `${TAG_PREFIX[c]}${ver}`;
|
||||
console.log(` pushing tag ${tag}`);
|
||||
if (!dryRun) {
|
||||
run(`git push origin ${tag}`);
|
||||
}
|
||||
}
|
||||
console.log("\nDone.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normal mode — require version argument
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const newVersion = positionals[0];
|
||||
if (!newVersion) {
|
||||
console.error(
|
||||
"Usage: node scripts/release.mjs <version> [--dry-run] [--push] [--only cli,ext,dsh]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(newVersion)) {
|
||||
console.error(`Invalid semver: "${newVersion}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read current versions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function readCurrentVersions() {
|
||||
const cargo = readFileSync(VERSION_FILES.cargo, "utf8");
|
||||
const cargoVer =
|
||||
cargo.match(/^\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/m)?.[1] ??
|
||||
cargo.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
|
||||
|
||||
const extPkg = JSON.parse(readFileSync(VERSION_FILES.extension, "utf8"));
|
||||
const dshPkg = JSON.parse(readFileSync(VERSION_FILES.dshPlugin, "utf8"));
|
||||
|
||||
return { cargo: cargoVer, extension: extPkg.version, dshPlugin: dshPkg.version };
|
||||
}
|
||||
|
||||
const current = readCurrentVersions();
|
||||
|
||||
console.log("Current versions:");
|
||||
console.log(` CLI/Daemon (Cargo.toml): ${current.cargo}`);
|
||||
console.log(` Extension (package.json): ${current.extension}`);
|
||||
console.log(` DSH Plugin (package.json): ${current.dshPlugin}`);
|
||||
console.log(`\nBumping all → ${newVersion}`);
|
||||
if (dryRun) console.log(" (dry-run mode — no files will be written)\n");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bump Cargo.toml
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function bumpCargo(version) {
|
||||
let content = readFileSync(VERSION_FILES.cargo, "utf8");
|
||||
|
||||
// workspace.package version
|
||||
content = content.replace(
|
||||
/^(\[workspace\.package\]\s*\nversion\s*=\s*")([^"]+)(")/m,
|
||||
`$1${version}$3`,
|
||||
);
|
||||
|
||||
// workspace.dependencies.bsk-protocol version (for cargo publish)
|
||||
content = content.replace(
|
||||
/(bsk-protocol\s*=\s*\{\s*version\s*=\s*")([^"]+)(")/,
|
||||
`$1${version}$3`,
|
||||
);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function bumpPackageJson(filePath, version) {
|
||||
const pkg = JSON.parse(readFileSync(filePath, "utf8"));
|
||||
pkg.version = version;
|
||||
return JSON.stringify(pkg, null, 2) + "\n";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Update CHANGELOG.md
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function updateChangelog(version) {
|
||||
const changelogPath = "CHANGELOG.md";
|
||||
let content;
|
||||
try {
|
||||
content = readFileSync(changelogPath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const escapedVer = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
// First: fill in any date-placeholder line for this version (e.g. "## [0.2.0] - 2026-09-XX")
|
||||
const versionPlaceholder = new RegExp(`^## \\[${escapedVer}\\]\\s*-\\s*\\d{4}-\\d{2}-XX`, "m");
|
||||
if (versionPlaceholder.test(content)) {
|
||||
content = content.replace(versionPlaceholder, `## [${version}] - ${today}`);
|
||||
} else {
|
||||
// Otherwise: insert a new section after [Unreleased]
|
||||
const unreleased = /^## \[Unreleased\]/m;
|
||||
if (unreleased.test(content)) {
|
||||
content = content.replace(unreleased, `## [Unreleased]\n\n## [${version}] - ${today}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { path: changelogPath, content };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Write files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const writes = [
|
||||
{ path: VERSION_FILES.cargo, content: bumpCargo(newVersion), label: "Cargo.toml" },
|
||||
{
|
||||
path: VERSION_FILES.extension,
|
||||
content: bumpPackageJson(VERSION_FILES.extension, newVersion),
|
||||
label: "Extension package.json",
|
||||
},
|
||||
{
|
||||
path: VERSION_FILES.dshPlugin,
|
||||
content: bumpPackageJson(VERSION_FILES.dshPlugin, newVersion),
|
||||
label: "DSH Plugin package.json",
|
||||
},
|
||||
];
|
||||
|
||||
const changelogUpdate = updateChangelog(newVersion);
|
||||
if (changelogUpdate) {
|
||||
writes.push({
|
||||
path: changelogUpdate.path,
|
||||
content: changelogUpdate.content,
|
||||
label: "CHANGELOG.md",
|
||||
});
|
||||
}
|
||||
|
||||
for (const w of writes) {
|
||||
console.log(` ✓ ${w.label}`);
|
||||
if (!dryRun) {
|
||||
writeFileSync(w.path, w.content);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Update Cargo.lock (cargo check triggers it)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (!dryRun) {
|
||||
console.log("\n Updating Cargo.lock...");
|
||||
run("cargo check --workspace 2>&1 || true");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git commit + tag
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (noCommit || dryRun) {
|
||||
if (dryRun) {
|
||||
console.log(`\nWould commit: "chore(release): ${newVersion}"`);
|
||||
console.log("Would create tags:");
|
||||
for (const c of components) {
|
||||
console.log(` ${TAG_PREFIX[c]}${newVersion}`);
|
||||
}
|
||||
}
|
||||
console.log("\nDone.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log("\nCommitting...");
|
||||
run(
|
||||
"git add Cargo.toml Cargo.lock apps/extension/package.json packages/dsh-plugin-browserskill/package.json CHANGELOG.md 2>/dev/null || true",
|
||||
);
|
||||
run(`git commit -m "chore(release): ${newVersion}"`);
|
||||
|
||||
console.log("Tagging...");
|
||||
for (const c of components) {
|
||||
const tag = `${TAG_PREFIX[c]}${newVersion}`;
|
||||
run(`git tag ${tag}`);
|
||||
console.log(` ✓ ${tag}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Push (optional)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (pushAfter) {
|
||||
console.log("\nPushing...");
|
||||
run("git push origin HEAD");
|
||||
for (const c of components) {
|
||||
const tag = `${TAG_PREFIX[c]}${newVersion}`;
|
||||
console.log(` pushing tag ${tag}`);
|
||||
run(`git push origin ${tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nRelease ${newVersion} prepared successfully.`);
|
||||
if (!pushAfter) {
|
||||
console.log("\nNext steps:");
|
||||
console.log(" # Push branch");
|
||||
console.log(" git push origin main");
|
||||
console.log("");
|
||||
console.log(" # Push extension tag first (store review takes time)");
|
||||
console.log(` git push origin ext-v${newVersion}`);
|
||||
console.log("");
|
||||
console.log(" # Then push CLI and DSH plugin tags");
|
||||
console.log(` git push origin cli-v${newVersion}`);
|
||||
console.log(` git push origin dsh-plugin-v${newVersion}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function run(cmd) {
|
||||
execSync(cmd, { stdio: "inherit" });
|
||||
}
|
||||
Reference in New Issue
Block a user