mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 21:13:44 +00:00
Apply PR #40845: feat(app): redesign non-modal settings
This commit is contained in:
commit
cd44746a5e
109 changed files with 6872 additions and 4666 deletions
|
|
@ -0,0 +1,12 @@
|
|||
id: no-drizzle-column-name
|
||||
snapshots:
|
||||
? |
|
||||
const table = sqliteTable("session", {
|
||||
projectID: text("project_id").notNull(),
|
||||
createdAt: integer("time_created").notNull(),
|
||||
})
|
||||
: labels:
|
||||
- source: text("project_id")
|
||||
style: primary
|
||||
start: 52
|
||||
end: 70
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
id: no-effect-die-string
|
||||
snapshots:
|
||||
Effect.die("boom"):
|
||||
labels:
|
||||
- source: Effect.die("boom")
|
||||
style: primary
|
||||
start: 0
|
||||
end: 18
|
||||
- source: '"boom"'
|
||||
style: secondary
|
||||
start: 11
|
||||
end: 17
|
||||
- source: ("boom")
|
||||
style: secondary
|
||||
start: 10
|
||||
end: 18
|
||||
Effect.die(`boom ${value}`):
|
||||
labels:
|
||||
- source: Effect.die(`boom ${value}`)
|
||||
style: primary
|
||||
start: 0
|
||||
end: 27
|
||||
- source: '`boom ${value}`'
|
||||
style: secondary
|
||||
start: 11
|
||||
end: 26
|
||||
- source: (`boom ${value}`)
|
||||
style: secondary
|
||||
start: 10
|
||||
end: 27
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
id: no-import-alias
|
||||
snapshots:
|
||||
import { baz, foo as bar } from "./foo":
|
||||
labels:
|
||||
- source: foo as bar
|
||||
style: primary
|
||||
start: 14
|
||||
end: 24
|
||||
- source: bar
|
||||
style: secondary
|
||||
start: 21
|
||||
end: 24
|
||||
import { foo as bar } from "./foo":
|
||||
labels:
|
||||
- source: foo as bar
|
||||
style: primary
|
||||
start: 9
|
||||
end: 19
|
||||
- source: bar
|
||||
style: secondary
|
||||
start: 16
|
||||
end: 19
|
||||
import { foo as bar, baz } from "./foo":
|
||||
labels:
|
||||
- source: foo as bar
|
||||
style: primary
|
||||
start: 9
|
||||
end: 19
|
||||
- source: bar
|
||||
style: secondary
|
||||
start: 16
|
||||
end: 19
|
||||
import { type Foo as Bar, baz } from "./foo":
|
||||
labels:
|
||||
- source: type Foo as Bar
|
||||
style: primary
|
||||
start: 9
|
||||
end: 24
|
||||
- source: Bar
|
||||
style: secondary
|
||||
start: 21
|
||||
end: 24
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
id: no-json-parse-cast
|
||||
snapshots:
|
||||
const value = JSON.parse(input) as Record<string, unknown>:
|
||||
labels:
|
||||
- source: JSON.parse(input) as Record<string, unknown>
|
||||
style: primary
|
||||
start: 14
|
||||
end: 58
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
id: no-nested-effect-service-yield
|
||||
snapshots:
|
||||
? |
|
||||
Effect.gen(function* () {
|
||||
yield* (yield* Foo.Service).client.run()
|
||||
})
|
||||
: labels:
|
||||
- source: (yield* Foo.Service).client.run()
|
||||
style: primary
|
||||
start: 35
|
||||
end: 68
|
||||
? |
|
||||
Effect.gen(function* () {
|
||||
yield* (yield* Foo.Service).run()
|
||||
})
|
||||
: labels:
|
||||
- source: (yield* Foo.Service).run()
|
||||
style: primary
|
||||
start: 35
|
||||
end: 61
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
id: no-star-import
|
||||
snapshots:
|
||||
import * as Foo from "./foo":
|
||||
labels:
|
||||
- source: '* as Foo'
|
||||
style: primary
|
||||
start: 7
|
||||
end: 15
|
||||
- source: import * as Foo from "./foo"
|
||||
style: secondary
|
||||
start: 0
|
||||
end: 28
|
||||
import type * as Foo from "./foo":
|
||||
labels:
|
||||
- source: '* as Foo'
|
||||
style: primary
|
||||
start: 12
|
||||
end: 20
|
||||
- source: import type * as Foo from "./foo"
|
||||
style: secondary
|
||||
start: 0
|
||||
end: 33
|
||||
14
script/ast-grep/rule-tests/no-drizzle-column-name-test.yml
Normal file
14
script/ast-grep/rule-tests/no-drizzle-column-name-test.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
id: no-drizzle-column-name
|
||||
valid:
|
||||
- |
|
||||
const table = sqliteTable("session", {
|
||||
project_id: text().notNull(),
|
||||
time_created: integer().notNull(),
|
||||
payload: text({ mode: "json" }),
|
||||
})
|
||||
invalid:
|
||||
- |
|
||||
const table = sqliteTable("session", {
|
||||
projectID: text("project_id").notNull(),
|
||||
createdAt: integer("time_created").notNull(),
|
||||
})
|
||||
7
script/ast-grep/rule-tests/no-effect-die-string-test.yml
Normal file
7
script/ast-grep/rule-tests/no-effect-die-string-test.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
id: no-effect-die-string
|
||||
valid:
|
||||
- Effect.die(new Error("boom"))
|
||||
- Effect.fail("boom")
|
||||
invalid:
|
||||
- Effect.die("boom")
|
||||
- Effect.die(`boom ${value}`)
|
||||
13
script/ast-grep/rule-tests/no-import-alias-test.yml
Normal file
13
script/ast-grep/rule-tests/no-import-alias-test.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
id: no-import-alias
|
||||
valid:
|
||||
- import { foo } from "./foo"
|
||||
- import type { Foo } from "./foo"
|
||||
- import foo from "./foo"
|
||||
- export { foo as bar } from "./foo"
|
||||
- import type { Plugin as EffectPlugin } from "./foo"
|
||||
- import type { Foo as Bar, Baz } from "./foo"
|
||||
invalid:
|
||||
- import { foo as bar } from "./foo"
|
||||
- import { baz, foo as bar } from "./foo"
|
||||
- import { foo as bar, baz } from "./foo"
|
||||
- import { type Foo as Bar, baz } from "./foo"
|
||||
6
script/ast-grep/rule-tests/no-json-parse-cast-test.yml
Normal file
6
script/ast-grep/rule-tests/no-json-parse-cast-test.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
id: no-json-parse-cast
|
||||
valid:
|
||||
- const value = JSON.parse(input)
|
||||
- const value = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(input)
|
||||
invalid:
|
||||
- const value = JSON.parse(input) as Record<string, unknown>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
id: no-nested-effect-service-yield
|
||||
valid:
|
||||
- |
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Foo.Service
|
||||
yield* service.run()
|
||||
})
|
||||
- |
|
||||
Effect.gen(function* () {
|
||||
const db = (yield* Database.Service).db
|
||||
yield* db.run()
|
||||
})
|
||||
invalid:
|
||||
- |
|
||||
Effect.gen(function* () {
|
||||
yield* (yield* Foo.Service).run()
|
||||
})
|
||||
- |
|
||||
Effect.gen(function* () {
|
||||
yield* (yield* Foo.Service).client.run()
|
||||
})
|
||||
8
script/ast-grep/rule-tests/no-star-import-test.yml
Normal file
8
script/ast-grep/rule-tests/no-star-import-test.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
id: no-star-import
|
||||
valid:
|
||||
- import { Foo } from "./foo"
|
||||
- import Foo from "./foo"
|
||||
- export * as Foo from "./foo"
|
||||
invalid:
|
||||
- import * as Foo from "./foo"
|
||||
- import type * as Foo from "./foo"
|
||||
22
script/ast-grep/rules/no-drizzle-column-name.yml
Normal file
22
script/ast-grep/rules/no-drizzle-column-name.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
id: no-drizzle-column-name
|
||||
language: TypeScript
|
||||
message: Use snake_case object keys instead of explicit drizzle column names.
|
||||
severity: error
|
||||
files:
|
||||
- packages/core/src/**/sql.ts
|
||||
- packages/core/src/**/*.sql.ts
|
||||
rule:
|
||||
any:
|
||||
- pattern: text($NAME)
|
||||
- pattern: text($NAME, $$$ARGS)
|
||||
- pattern: integer($NAME)
|
||||
- pattern: integer($NAME, $$$ARGS)
|
||||
- pattern: blob($NAME)
|
||||
- pattern: blob($NAME, $$$ARGS)
|
||||
- pattern: real($NAME)
|
||||
- pattern: real($NAME, $$$ARGS)
|
||||
- pattern: numeric($NAME)
|
||||
- pattern: numeric($NAME, $$$ARGS)
|
||||
constraints:
|
||||
NAME:
|
||||
kind: string
|
||||
22
script/ast-grep/rules/no-effect-die-string.yml
Normal file
22
script/ast-grep/rules/no-effect-die-string.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
id: no-effect-die-string
|
||||
language: TypeScript
|
||||
message: die with `new Error(...)`.
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- all:
|
||||
- pattern: Effect.die($MESSAGE)
|
||||
- has:
|
||||
field: arguments
|
||||
all:
|
||||
- kind: arguments
|
||||
- has:
|
||||
kind: string
|
||||
- all:
|
||||
- pattern: Effect.die($MESSAGE)
|
||||
- has:
|
||||
field: arguments
|
||||
all:
|
||||
- kind: arguments
|
||||
- has:
|
||||
kind: template_string
|
||||
14
script/ast-grep/rules/no-import-alias.yml
Normal file
14
script/ast-grep/rules/no-import-alias.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
id: no-import-alias
|
||||
language: TypeScript
|
||||
message: Do not alias value imports. For type name collisions, alias inside a dedicated `import type` statement.
|
||||
severity: error
|
||||
rule:
|
||||
all:
|
||||
- kind: import_specifier
|
||||
- has:
|
||||
field: alias
|
||||
kind: identifier
|
||||
- not:
|
||||
inside:
|
||||
pattern: import type { $$$SPECS } from "$MOD"
|
||||
stopBy: end
|
||||
6
script/ast-grep/rules/no-json-parse-cast.yml
Normal file
6
script/ast-grep/rules/no-json-parse-cast.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
id: no-json-parse-cast
|
||||
language: TypeScript
|
||||
message: Prefer Effect Schema JSON decoding over JSON.parse casts.
|
||||
severity: error
|
||||
rule:
|
||||
pattern: JSON.parse($INPUT) as $TYPE
|
||||
11
script/ast-grep/rules/no-nested-effect-service-yield.yml
Normal file
11
script/ast-grep/rules/no-nested-effect-service-yield.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
id: no-nested-effect-service-yield
|
||||
language: TypeScript
|
||||
message: Bind Effect services before calling methods instead of nesting service yields.
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern: (yield* $SERVICE).$METHOD($$$ARGS)
|
||||
- pattern: (yield* $SERVICE).$PROPERTY.$METHOD($$$ARGS)
|
||||
constraints:
|
||||
SERVICE:
|
||||
regex: \.Service$
|
||||
10
script/ast-grep/rules/no-star-import.yml
Normal file
10
script/ast-grep/rules/no-star-import.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
id: no-star-import
|
||||
language: TypeScript
|
||||
message: Do not use star imports.
|
||||
severity: error
|
||||
rule:
|
||||
all:
|
||||
- kind: namespace_import
|
||||
- inside:
|
||||
kind: import_statement
|
||||
stopBy: end
|
||||
4
script/ast-grep/sgconfig.yml
Normal file
4
script/ast-grep/sgconfig.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ruleDirs:
|
||||
- rules
|
||||
testConfigs:
|
||||
- testDir: rule-tests
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
import { $ } from "bun"
|
||||
|
||||
await $`bun ./packages/sdk/js/script/build.ts`
|
||||
await $`bun run generate`.cwd("packages/protocol")
|
||||
|
||||
await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode")
|
||||
await $`bun run generate`.cwd("packages/www")
|
||||
|
||||
await $`./script/format.ts`
|
||||
|
|
|
|||
66
script/profile-typecheck-packages.ts
Normal file
66
script/profile-typecheck-packages.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import path from "path"
|
||||
|
||||
const root = path.resolve(import.meta.dir, "..")
|
||||
const proc = Bun.spawn(
|
||||
[
|
||||
"bun",
|
||||
"turbo",
|
||||
"typecheck",
|
||||
"--concurrency=1",
|
||||
"--force",
|
||||
"--continue=always",
|
||||
"--summarize",
|
||||
"--output-logs=errors-only",
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
)
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited])
|
||||
const output = stdout + stderr
|
||||
if (exitCode !== 0) {
|
||||
process.stdout.write(stdout)
|
||||
process.stderr.write(stderr)
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
const summary = output.match(/Summary:\s+(.+\.json)/)?.[1]?.trim()
|
||||
if (!summary) {
|
||||
process.stdout.write(stdout)
|
||||
process.stderr.write(stderr)
|
||||
throw new Error("Turbo did not report a run summary")
|
||||
}
|
||||
|
||||
const report = (await Bun.file(summary).json()) as {
|
||||
tasks: Array<{
|
||||
taskId: string
|
||||
execution: { startTime: number; endTime: number; exitCode: number } | null
|
||||
}>
|
||||
}
|
||||
const tasks = report.tasks
|
||||
.flatMap((task) =>
|
||||
task.execution
|
||||
? [
|
||||
{
|
||||
task: task.taskId.replace(/#typecheck$/, ""),
|
||||
durationMs: task.execution.endTime - task.execution.startTime,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
)
|
||||
.sort((a, b) => b.durationMs - a.durationMs)
|
||||
const total = tasks.reduce((duration, task) => duration + task.durationMs, 0)
|
||||
const width = Math.max(...tasks.map((task) => task.task.length), "Package".length)
|
||||
|
||||
console.log(`Package${" ".repeat(width - "Package".length)} Time Share`)
|
||||
tasks.forEach((task) => {
|
||||
const duration = `${(task.durationMs / 1000).toFixed(2)}s`.padStart(7)
|
||||
const share = `${((task.durationMs / total) * 100).toFixed(1)}%`.padStart(6)
|
||||
console.log(`${task.task.padEnd(width)} ${duration} ${share}`)
|
||||
})
|
||||
console.log(`\nTotal serial task time: ${(total / 1000).toFixed(2)}s`)
|
||||
console.log(`Turbo summary: ${path.relative(root, summary)}`)
|
||||
172
script/profile-typecheck.ts
Normal file
172
script/profile-typecheck.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import { mkdir } from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
if (process.platform !== "darwin") throw new Error("System typecheck profiling currently supports macOS only")
|
||||
|
||||
const root = path.resolve(import.meta.dir, "..")
|
||||
const startedAt = new Date()
|
||||
const args = Bun.argv.slice(2)
|
||||
const command = [
|
||||
"bun",
|
||||
"turbo",
|
||||
"typecheck",
|
||||
...(args.some((arg) => arg.startsWith("--concurrency")) ? [] : ["--concurrency=3"]),
|
||||
...args,
|
||||
]
|
||||
const before = systemSnapshot()
|
||||
const proc = Bun.spawn(command, {
|
||||
cwd: root,
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
const samples = [processTreeSnapshot(proc.pid, startedAt)]
|
||||
const timer = setInterval(() => samples.push(processTreeSnapshot(proc.pid, startedAt)), 200)
|
||||
const exitCode = await proc.exited
|
||||
clearInterval(timer)
|
||||
samples.push(processTreeSnapshot(proc.pid, startedAt))
|
||||
|
||||
const finishedAt = new Date()
|
||||
const after = systemSnapshot()
|
||||
const active = samples.filter((sample) => sample.processes > 0)
|
||||
const report = {
|
||||
command,
|
||||
cwd: root,
|
||||
startedAt: startedAt.toISOString(),
|
||||
finishedAt: finishedAt.toISOString(),
|
||||
durationSeconds: (finishedAt.getTime() - startedAt.getTime()) / 1000,
|
||||
exitCode,
|
||||
summary: {
|
||||
peakCpuPercent: Math.max(0, ...active.map((sample) => sample.cpuPercent)),
|
||||
averageCpuPercent: average(active.map((sample) => sample.cpuPercent)),
|
||||
peakAggregateRssMB: Math.max(0, ...active.map((sample) => sample.aggregateRssMB)),
|
||||
peakProcesses: Math.max(0, ...active.map((sample) => sample.processes)),
|
||||
peakTsgoRelatedProcesses: Math.max(0, ...active.map((sample) => sample.tsgoRelatedProcesses)),
|
||||
swapDeltaMB: after.swapUsedMB - before.swapUsedMB,
|
||||
compressedMemoryDeltaMB: after.compressedMemoryMB - before.compressedMemoryMB,
|
||||
pageoutDelta: after.pageouts - before.pageouts,
|
||||
},
|
||||
system: { before, after },
|
||||
samples,
|
||||
}
|
||||
|
||||
const directory = path.join(root, ".typecheck-profiles")
|
||||
const file = path.join(directory, `${startedAt.toISOString().replaceAll(":", "-")}.json`)
|
||||
await mkdir(directory, { recursive: true })
|
||||
await Bun.write(file, JSON.stringify(report, null, 2) + "\n")
|
||||
|
||||
console.log(`
|
||||
Typecheck profile
|
||||
Duration: ${report.durationSeconds.toFixed(1)}s
|
||||
Average CPU: ${report.summary.averageCpuPercent.toFixed(0)}%
|
||||
Peak CPU: ${report.summary.peakCpuPercent.toFixed(0)}%
|
||||
Aggregate RSS: ${report.summary.peakAggregateRssMB.toFixed(0)} MB
|
||||
Peak processes: ${report.summary.peakProcesses} (${report.summary.peakTsgoRelatedProcesses} tsgo-related)
|
||||
Swap delta: ${signed(report.summary.swapDeltaMB)} MB
|
||||
Compressed: ${signed(report.summary.compressedMemoryDeltaMB)} MB
|
||||
Pageouts: ${signed(report.summary.pageoutDelta)}
|
||||
Report: ${path.relative(root, file)}
|
||||
`)
|
||||
|
||||
process.exit(exitCode)
|
||||
|
||||
function processTreeSnapshot(rootPID: number, startedAt: Date) {
|
||||
const processes = processList()
|
||||
const pids = new Set([rootPID])
|
||||
const pending = [rootPID]
|
||||
while (pending.length > 0) {
|
||||
const parent = pending.shift()
|
||||
processes
|
||||
.filter((process) => process.ppid === parent && !pids.has(process.pid))
|
||||
.forEach((process) => {
|
||||
pids.add(process.pid)
|
||||
pending.push(process.pid)
|
||||
})
|
||||
}
|
||||
const tree = processes.filter((process) => pids.has(process.pid))
|
||||
return {
|
||||
elapsedSeconds: (Date.now() - startedAt.getTime()) / 1000,
|
||||
processes: tree.length,
|
||||
tsgoRelatedProcesses: tree.filter((process) => /\btsgo\b/.test(process.command)).length,
|
||||
cpuPercent: sum(tree.map((process) => process.cpuPercent)),
|
||||
aggregateRssMB: sum(tree.map((process) => process.rssKB)) / 1024,
|
||||
}
|
||||
}
|
||||
|
||||
function systemSnapshot() {
|
||||
const vm = text(["vm_stat"])
|
||||
const pageSize = Number(vm.match(/page size of (\d+) bytes/)?.[1] ?? 4096)
|
||||
const fields = Object.fromEntries(
|
||||
vm
|
||||
.split("\n")
|
||||
.map((line) => line.match(/^([^:]+):\s+(\d+)\.?$/))
|
||||
.filter((match): match is RegExpMatchArray => match !== null)
|
||||
.map((match) => [match[1], Number(match[2])]),
|
||||
)
|
||||
const swap = text(["sysctl", "-n", "vm.swapusage"])
|
||||
return {
|
||||
loadAverage: text(["sysctl", "-n", "vm.loadavg"]).trim(),
|
||||
thermalState: text(["pmset", "-g", "therm"]).trim(),
|
||||
swapUsedMB: Number(swap.match(/used = ([\d.]+)M/)?.[1] ?? 0),
|
||||
freeMemoryMB: ((fields["Pages free"] ?? 0) * pageSize) / 1024 / 1024,
|
||||
compressedMemoryMB: ((fields["Pages occupied by compressor"] ?? 0) * pageSize) / 1024 / 1024,
|
||||
pageouts: fields.Pageouts ?? 0,
|
||||
relevantProcesses: processList()
|
||||
.filter((process) =>
|
||||
/opencode|tsgo|tsserver|vtsls|eslintServer|tailwindcss-language-server/.test(process.command),
|
||||
)
|
||||
.sort((a, b) => b.rssKB - a.rssKB)
|
||||
.map(processSummary),
|
||||
topCpuProcesses: processList()
|
||||
.sort((a, b) => b.cpuPercent - a.cpuPercent)
|
||||
.slice(0, 15)
|
||||
.map(processSummary),
|
||||
topMemoryProcesses: processList()
|
||||
.sort((a, b) => b.rssKB - a.rssKB)
|
||||
.slice(0, 15)
|
||||
.map(processSummary),
|
||||
}
|
||||
}
|
||||
|
||||
function processSummary(process: ReturnType<typeof processList>[number]) {
|
||||
return {
|
||||
pid: process.pid,
|
||||
ppid: process.ppid,
|
||||
rssMB: process.rssKB / 1024,
|
||||
cpuPercent: process.cpuPercent,
|
||||
command: process.command,
|
||||
}
|
||||
}
|
||||
|
||||
function processList() {
|
||||
return text(["ps", "-axo", "pid=,ppid=,rss=,%cpu=,command="])
|
||||
.split("\n")
|
||||
.map((line) => line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+([\d.]+)\s+(.*)$/))
|
||||
.filter((match): match is RegExpMatchArray => match !== null)
|
||||
.map((match) => ({
|
||||
pid: Number(match[1]),
|
||||
ppid: Number(match[2]),
|
||||
rssKB: Number(match[3]),
|
||||
cpuPercent: Number(match[4]),
|
||||
command: match[5],
|
||||
}))
|
||||
}
|
||||
|
||||
function text(command: string[]) {
|
||||
return Bun.spawnSync(command).stdout.toString()
|
||||
}
|
||||
|
||||
function sum(values: number[]) {
|
||||
return values.reduce((total, value) => total + value, 0)
|
||||
}
|
||||
|
||||
function average(values: number[]) {
|
||||
if (values.length === 0) return 0
|
||||
return sum(values) / values.length
|
||||
}
|
||||
|
||||
function signed(value: number) {
|
||||
return `${value >= 0 ? "+" : ""}${value.toFixed(0)}`
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { Script } from "@opencode-ai/script"
|
||||
import { $ } from "bun"
|
||||
import { fileURLToPath } from "url"
|
||||
import { UpdateArtifact } from "./update-artifact"
|
||||
|
||||
console.log("=== publishing ===\n")
|
||||
|
||||
|
|
@ -25,7 +26,6 @@ async function prepareReleaseFiles() {
|
|||
}
|
||||
|
||||
await $`bun install`
|
||||
await $`./packages/sdk/js/script/build.ts`
|
||||
}
|
||||
|
||||
if (Script.release && !Script.preview) {
|
||||
|
|
@ -35,15 +35,27 @@ if (Script.release && !Script.preview) {
|
|||
|
||||
await prepareReleaseFiles()
|
||||
|
||||
console.log("\n=== schema ===\n")
|
||||
await $`bun ./packages/schema/script/publish.ts`
|
||||
|
||||
console.log("\n=== theme ===\n")
|
||||
await $`bun ./packages/theme/script/publish.ts`
|
||||
|
||||
console.log("\n=== ai ===\n")
|
||||
await $`bun ./packages/ai/script/publish.ts`
|
||||
|
||||
console.log("\n=== util ===\n")
|
||||
await $`bun ./packages/util/script/publish.ts`
|
||||
|
||||
console.log("\n=== protocol ===\n")
|
||||
await $`bun ./packages/protocol/script/publish.ts`
|
||||
|
||||
console.log("\n=== client ===\n")
|
||||
await $`bun ./packages/client/script/publish.ts`
|
||||
|
||||
console.log("\n=== cli ===\n")
|
||||
await $`bun ./packages/opencode/script/publish.ts`
|
||||
|
||||
console.log("\n=== preview cli ===\n")
|
||||
await $`bun ./packages/cli/script/publish.ts`
|
||||
|
||||
console.log("\n=== sdk ===\n")
|
||||
await $`bun ./packages/sdk/js/script/publish.ts`
|
||||
|
||||
console.log("\n=== plugin ===\n")
|
||||
await $`bun ./packages/plugin/script/publish.ts`
|
||||
|
||||
|
|
@ -70,4 +82,13 @@ if (Script.release && !Script.preview) {
|
|||
|
||||
if (Script.release) {
|
||||
await $`gh release edit ${tag} --draft=false --repo ${process.env.GH_REPO}`
|
||||
const repo = process.env.GH_REPO
|
||||
if (!repo) throw new Error("GH_REPO is required")
|
||||
await UpdateArtifact.publish({
|
||||
channel: Script.channel,
|
||||
name: "desktop",
|
||||
distribution: "github",
|
||||
version: Script.version,
|
||||
metadata: await UpdateArtifact.desktopMetadata(Script.version, repo),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ async function commits(from: string, to: string) {
|
|||
}
|
||||
|
||||
const log =
|
||||
await $`git log ${base}..${head} --format=%H -- packages/opencode packages/sdk packages/plugin packages/desktop packages/app sdks/vscode packages/extensions github`.text()
|
||||
await $`git log ${base}..${head} --format=%H -- packages/opencode packages/plugin packages/desktop packages/app sdks/vscode packages/extensions github`.text()
|
||||
|
||||
const list: Commit[] = []
|
||||
for (const hash of log.split("\n").filter(Boolean)) {
|
||||
|
|
@ -136,7 +136,7 @@ async function commits(from: string, to: string) {
|
|||
else if (file.startsWith("packages/opencode/")) areas.add("core")
|
||||
else if (file.startsWith("packages/desktop/src-tauri/")) areas.add("tauri")
|
||||
else if (file.startsWith("packages/desktop/") || file.startsWith("packages/app/")) areas.add("app")
|
||||
else if (file.startsWith("packages/sdk/") || file.startsWith("packages/plugin/")) areas.add("sdk")
|
||||
else if (file.startsWith("packages/plugin/")) areas.add("sdk")
|
||||
else if (file.startsWith("sdks/vscode/") || file.startsWith("github/")) areas.add("extensions/vscode")
|
||||
}
|
||||
|
||||
|
|
|
|||
104
script/update-artifact.ts
Normal file
104
script/update-artifact.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
type Artifact = {
|
||||
channel: string
|
||||
name: string
|
||||
distribution: string
|
||||
version: string
|
||||
metadata: Record<string, unknown>
|
||||
}
|
||||
|
||||
type DesktopFile = {
|
||||
url: string
|
||||
sha512: string
|
||||
size: number
|
||||
blockMapSize?: number
|
||||
}
|
||||
|
||||
export namespace UpdateArtifact {
|
||||
export async function publish(artifact: Artifact) {
|
||||
if (process.env.GITHUB_ACTIONS !== "true") {
|
||||
console.log("skipped update artifact publication outside GitHub Actions")
|
||||
return
|
||||
}
|
||||
const requestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL
|
||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
||||
if (!requestURL || !requestToken) throw new Error("GitHub Actions OIDC is unavailable")
|
||||
|
||||
const url = new URL(requestURL)
|
||||
url.searchParams.set("audience", "https://update.opencode.ai")
|
||||
const tokenResponse = await fetch(url, { headers: { Authorization: `Bearer ${requestToken}` } })
|
||||
if (!tokenResponse.ok) throw new Error(`Failed to request GitHub OIDC token: ${tokenResponse.status}`)
|
||||
const token: unknown = await tokenResponse.json()
|
||||
if (!isRecord(token) || typeof token.value !== "string") throw new Error("GitHub OIDC response did not include a token")
|
||||
|
||||
const response = await fetch("https://update.opencode.ai/api/publish", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.value}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(artifact),
|
||||
})
|
||||
if (response.ok) return
|
||||
throw new Error(`Failed to publish update artifact: ${response.status} ${await response.text()}`)
|
||||
}
|
||||
|
||||
export async function desktopMetadata(version: string, repo: string) {
|
||||
const directory = process.env.RUNNER_TEMP ?? "/tmp"
|
||||
const entries = await Promise.all(
|
||||
[
|
||||
["desktop.yml", "latest.yml"],
|
||||
["desktop-mac.yml", "latest-mac.yml"],
|
||||
["desktop-linux.yml", "latest-linux.yml"],
|
||||
["desktop-linux-arm64.yml", "latest-linux-arm64.yml"],
|
||||
].map(async ([name, source]) => {
|
||||
const file = Bun.file(`${directory}/${source}`)
|
||||
if (!(await file.exists())) return
|
||||
return [name, parseDesktop(await file.text(), version, repo)] as const
|
||||
}),
|
||||
)
|
||||
const manifests = Object.fromEntries(entries.filter((entry) => entry !== undefined))
|
||||
if (!Object.keys(manifests).length) throw new Error("No desktop update metadata found")
|
||||
return { manifests }
|
||||
}
|
||||
}
|
||||
|
||||
function parseDesktop(content: string, version: string, repo: string) {
|
||||
const lines = content.split("\n")
|
||||
const found = lines.find((line) => line.startsWith("version:"))?.slice("version:".length).trim()
|
||||
if (found !== version) throw new Error(`Desktop metadata version mismatch: expected ${version}, got ${found}`)
|
||||
const releaseDate = lines
|
||||
.find((line) => line.startsWith("releaseDate:"))
|
||||
?.slice("releaseDate:".length)
|
||||
.trim()
|
||||
.replace(/^['"]|['"]$/g, "")
|
||||
if (!releaseDate) throw new Error("Desktop metadata did not include a release date")
|
||||
|
||||
const files: DesktopFile[] = []
|
||||
lines.forEach((line) => {
|
||||
const value = line.trim()
|
||||
if (value.startsWith("- url:")) {
|
||||
const name = value.slice("- url:".length).trim()
|
||||
files.push({
|
||||
url: name.startsWith("http")
|
||||
? name
|
||||
: `https://github.com/${repo}/releases/download/v${version}/${encodeURIComponent(name)}`,
|
||||
sha512: "",
|
||||
size: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
const current = files.at(-1)
|
||||
if (!current) return
|
||||
if (value.startsWith("sha512:")) current.sha512 = value.slice("sha512:".length).trim()
|
||||
if (value.startsWith("size:")) current.size = Number(value.slice("size:".length).trim())
|
||||
if (value.startsWith("blockMapSize:")) current.blockMapSize = Number(value.slice("blockMapSize:".length).trim())
|
||||
})
|
||||
if (!files.length || files.some((file) => !file.sha512 || !file.size)) {
|
||||
throw new Error("Desktop metadata contained an incomplete file")
|
||||
}
|
||||
return { files, releaseDate }
|
||||
}
|
||||
|
||||
function isRecord(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input !== null && !Array.isArray(input)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue