From 8eec5119775a684ba84eefd4ec72223d2d07ce1f Mon Sep 17 00:00:00 2001 From: ernan Date: Thu, 27 Aug 2026 01:51:07 -0300 Subject: [PATCH] feat: add Windows and PowerShell support for OpenUltraCode --- .githooks/pre-commit | 6 +- .github/workflows/release.yml | 4 +- README.md | 6 +- install-dev.ps1 | 125 ++++++++++++++++++++++++++++++ install.ps1 | 142 ++++++++++++++++++++++++++++++++++ package.json | 2 + scripts/validate-assets.ts | 2 + tests/assets.test.ts | 16 +++- tests/state.test.ts | 4 +- 9 files changed, 299 insertions(+), 8 deletions(-) create mode 100644 install-dev.ps1 create mode 100644 install.ps1 diff --git a/.githooks/pre-commit b/.githooks/pre-commit index d427193..61fee00 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,4 +1,8 @@ #!/usr/bin/env sh set -eu -pnpm run lint +if command -v dagger >/dev/null 2>&1; then + pnpm run lint +else + pnpm run eslint +fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 503b8ff..d5c760d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,9 @@ jobs: pnpm-workspace.yaml \ tsconfig.json \ install.sh \ - install-dev.sh + install-dev.sh \ + install.ps1 \ + install-dev.ps1 - name: Publish GitHub release env: diff --git a/README.md b/README.md index 42191e7..2c9ad4b 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ It uses no proxy. It does not replace the selected model, create synthetic model ## Install -1. Install the latest release with `curl -fsSL https://raw.githubusercontent.com/norandom/OpenUltraCode/main/install.sh | sh`. -2. If you already have a checkout and want the development path, run `./install-dev.sh` from the repo root. +1. Install the latest release with `curl -fsSL https://raw.githubusercontent.com/norandom/OpenUltraCode/main/install.sh | sh`. On Windows (PowerShell), run `irm https://raw.githubusercontent.com/norandom/OpenUltraCode/main/install.ps1 | iex`. +2. If you already have a checkout and want the development path, run `./install-dev.sh` (or `.\install-dev.ps1` on Windows) from the repo root. 3. The release installer pulls the latest GitHub release asset, copies the skill, slash commands, and agents into `~/.config/opencode/`, asks which fusion model IDs to set, and registers the plugin path in `~/.config/opencode/opencode.json`. 4. Quit and restart opencode so the global plugin, commands, skills, and agents are loaded. -The restart matters because opencode loads plugins, commands, skills, agents, and config at startup. If `/ultracode` does not appear, confirm you restarted opencode after running `install.sh` or `install-dev.sh`. +The restart matters because opencode loads plugins, commands, skills, agents, and config at startup. If `/ultracode` does not appear, confirm you restarted opencode after running `install.sh`, `install-dev.sh`, `install.ps1`, or `install-dev.ps1`. Release installs do not run Dagger or `pnpm run check` on the user's machine. The GitHub Actions release workflow runs build, ESLint, tests, and asset validation, then publishes `open-ultracode-release.tar.gz` as the GitHub release asset consumed by `install.sh`. diff --git a/install-dev.ps1 b/install-dev.ps1 new file mode 100644 index 0000000..0049e56 --- /dev/null +++ b/install-dev.ps1 @@ -0,0 +1,125 @@ +[CmdletBinding()] +param( + [string]$OpenCodeConfigDir = $env:OPENCODE_CONFIG_DIR +) + +$ErrorActionPreference = "Stop" + +if (-not (Get-Command node -ErrorAction SilentlyContinue)) { + Write-Error "node is required for OpenUltraCode install." + exit 1 +} + +if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) { + if (Get-Command corepack -ErrorAction SilentlyContinue) { + corepack enable + } +} + +if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) { + Write-Error "pnpm is required. Enable it with 'corepack enable' or install pnpm, then rerun .\install-dev.ps1." + exit 1 +} + +if (-not $OpenCodeConfigDir) { + $OpenCodeConfigDir = Join-Path $HOME ".config\opencode" +} + +$repoRoot = $PSScriptRoot +if (-not $repoRoot) { + $repoRoot = (Get-Location).Path +} + +function Get-AgentModel([string]$AgentFile) { + if (-not (Test-Path $AgentFile)) { return $null } + $env:AGENT_FILE = $AgentFile + $model = node -e "const fs = require('node:fs'); const file = process.env.AGENT_FILE; if (!file || !fs.existsSync(file)) process.exit(0); const content = fs.readFileSync(file, 'utf8'); const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1]; const model = frontmatter === undefined ? undefined : /^model:[ \t]*(\S+)[ \t]*$/m.exec(frontmatter)?.[1]; const legacyFusionPlaceholders = new Set(['provider/model-a', 'provider/model-b', 'provider/arbiter-model']); const baseModel = model?.replace(/#[^#]+$/, ''); if (baseModel !== undefined && !legacyFusionPlaceholders.has(baseModel)) { process.stdout.write(baseModel + '#max'); }" + return $model +} + +function Restore-AgentModel([string]$AgentFile, [string]$Model) { + if (-not $Model -or -not (Test-Path $AgentFile)) { return } + $env:AGENT_FILE = $AgentFile + $env:AGENT_MODEL = $Model + node -e "const fs = require('node:fs'); const file = process.env.AGENT_FILE; const model = process.env.AGENT_MODEL; if (!file || !model) throw new Error('missing fusion model restore input'); if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\/[^\s#]+#max$/.test(model)) { throw new Error(model + ' must be provider/model#max'); } const content = fs.readFileSync(file, 'utf8'); const baseModel = model.replace(/#[^#]+$/, ''); let updated = content.replace(/^model:[^\r\n]+$/m, 'model: ' + baseModel); updated = /^variant:/m.test(updated) ? updated.replace(/^variant:.*$/m, 'variant: max') : updated.replace(/^model:.*$/m, (line) => line + '\nvariant: max'); fs.writeFileSync(file, updated);" +} + +function Register-GlobalPlugin { + $configFile = Join-Path $OpenCodeConfigDir "opencode.json" + if (-not (Test-Path $configFile) -and (Test-Path (Join-Path $OpenCodeConfigDir "opencode.jsonc"))) { + $configFile = Join-Path $OpenCodeConfigDir "opencode.jsonc" + } + $pluginPath = (Join-Path $repoRoot ".opencode\plugins\open-ultracode.ts").Replace("\", "/") + + if (-not (Test-Path $OpenCodeConfigDir)) { + New-Item -ItemType Directory -Path $OpenCodeConfigDir -Force | Out-Null + } + + if (-not (Test-Path $configFile)) { + "{\n `"`$schema`": `"https://opencode.ai/config.json`"\n}\n" | Set-Content -Path $configFile -Encoding utf8 + } + + $env:OPENCODE_GLOBAL_CONFIG = $configFile + $env:OPENCODE_PLUGIN_PATH = $pluginPath + + node -e "const fs = require('node:fs'); const configPath = process.env.OPENCODE_GLOBAL_CONFIG; const pluginPath = process.env.OPENCODE_PLUGIN_PATH; if (!configPath || !pluginPath) { throw new Error('missing OpenUltraCode installer environment'); } let config = {}; try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (error) { throw new Error('Could not parse ' + configPath + ': ' + (error instanceof Error ? error.message : String(error))); } if (typeof config !== 'object' || config === null || Array.isArray(config)) { throw new Error(configPath + ' must contain a JSON object'); } config['`$schema'] = config['`$schema'] || 'https://opencode.ai/config.json'; if (config.permission === undefined) { config.permission = { open_ultracode_configure_fusion: 'ask' }; } else if (typeof config.permission === 'object' && config.permission !== null && !Array.isArray(config.permission) && !('open_ultracode_configure_fusion' in config.permission)) { config.permission.open_ultracode_configure_fusion = 'ask'; } const existing = Array.isArray(config.plugin) ? config.plugin : []; config.plugin = existing.filter((entry) => entry !== './.opencode/plugins/open-ultracode.ts'); if (!config.plugin.includes(pluginPath)) { config.plugin.push(pluginPath); } fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');" +} + +function Install-GlobalAssets { + $skillsDir = Join-Path $OpenCodeConfigDir "skills" + $commandsDir = Join-Path $OpenCodeConfigDir "commands" + $agentsDir = Join-Path $OpenCodeConfigDir "agents" + $toolsDir = Join-Path $OpenCodeConfigDir "tools" + $libDir = Join-Path $OpenCodeConfigDir "lib" + + New-Item -ItemType Directory -Path $skillsDir, $commandsDir, $agentsDir, $toolsDir, $libDir -Force | Out-Null + + $panelAFile = Join-Path $agentsDir "ultracode-fusion-panel-a.md" + $panelBFile = Join-Path $agentsDir "ultracode-fusion-panel-b.md" + $arbiterFile = Join-Path $agentsDir "ultracode-fusion-arbiter.md" + + $existingPanelA = Get-AgentModel $panelAFile + $existingPanelB = Get-AgentModel $panelBFile + $existingArbiter = Get-AgentModel $arbiterFile + + $targetSkill = Join-Path $skillsDir "open-ultracode" + if (Test-Path $targetSkill) { + Remove-Item -Path $targetSkill -Recurse -Force + } + Copy-Item -Path (Join-Path $repoRoot ".opencode\skills\open-ultracode") -Destination $targetSkill -Recurse -Force + + Get-ChildItem -Path (Join-Path $repoRoot ".opencode\commands") -Filter "ultracode*.md" | Copy-Item -Destination $commandsDir -Force + Get-ChildItem -Path (Join-Path $repoRoot ".opencode\commands") -Filter "ultrathink*.md" | Copy-Item -Destination $commandsDir -Force + Get-ChildItem -Path (Join-Path $repoRoot ".opencode\agents") -Filter "open-ultracode*.md" | Copy-Item -Destination $agentsDir -Force + Get-ChildItem -Path (Join-Path $repoRoot ".opencode\agents") -Filter "ultracode-fusion*.md" | Copy-Item -Destination $agentsDir -Force + Copy-Item -Path (Join-Path $repoRoot ".opencode\tools\open_ultracode_configure_fusion.ts") -Destination $toolsDir -Force + Copy-Item -Path (Join-Path $repoRoot ".opencode\lib\fusion-config.ts") -Destination $libDir -Force + + Restore-AgentModel $panelAFile $existingPanelA + Restore-AgentModel $panelBFile $existingPanelB + Restore-AgentModel $arbiterFile $existingArbiter + + Register-GlobalPlugin +} + +Push-Location $repoRoot +try { + pnpm install --frozen-lockfile + if (Get-Command dagger -ErrorAction SilentlyContinue) { + pnpm run check + } else { + pnpm run build + pnpm run eslint + pnpm run test + pnpm run validate:assets + } + Install-GlobalAssets + + if ((Get-Command git -ErrorAction SilentlyContinue) -and (git rev-parse --is-inside-work-tree 2>$null)) { + git config core.hooksPath .githooks + } + + Write-Host "OpenUltraCode is installed globally for opencode from this checkout. Restart opencode to load the plugin, commands, skills, and agents." +} finally { + Pop-Location +} diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..678b404 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,142 @@ +[CmdletBinding()] +param( + [string]$OpenCodeConfigDir = $env:OPENCODE_CONFIG_DIR, + [string]$InstallDir = $env:OPENULTRACODE_HOME, + [string]$Repo = "norandom/OpenUltraCode", + [string]$PanelA, + [string]$PanelB, + [string]$Decider, + [switch]$NonInteractive +) + +$ErrorActionPreference = "Stop" + +if (-not (Get-Command node -ErrorAction SilentlyContinue)) { + Write-Error "node is required for OpenUltraCode install." + exit 1 +} + +if (-not $OpenCodeConfigDir) { + $OpenCodeConfigDir = Join-Path $HOME ".config\opencode" +} + +if (-not $InstallDir) { + $InstallDir = Join-Path $HOME ".local\share\open-ultracode" +} + +$DefaultFusionPanelAModel = "opencode/deepseek-v4-pro#max" +$DefaultFusionPanelBModel = "opencode/glm-5.2#max" +$DefaultFusionArbiterModel = "opencode/kimi-k3#max" + +function Get-AgentModel([string]$AgentFile) { + if (-not (Test-Path $AgentFile)) { return $null } + $env:AGENT_FILE = $AgentFile + $model = node -e "const fs = require('node:fs'); const file = process.env.AGENT_FILE; if (!file || !fs.existsSync(file)) process.exit(0); const content = fs.readFileSync(file, 'utf8'); const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1]; const model = frontmatter === undefined ? undefined : /^model:[ \t]*(\S+)[ \t]*$/m.exec(frontmatter)?.[1]; const legacyFusionPlaceholders = new Set(['provider/model-a', 'provider/model-b', 'provider/arbiter-model']); const baseModel = model?.replace(/#[^#]+$/, ''); if (baseModel !== undefined && !legacyFusionPlaceholders.has(baseModel)) { process.stdout.write(baseModel + '#max'); }" + return $model +} + +function Set-AgentModel([string]$AgentFile, [string]$Model) { + if (-not (Test-Path $AgentFile)) { + Write-Error "Missing fusion agent file: $AgentFile" + exit 1 + } + $env:AGENT_FILE = $AgentFile + $env:AGENT_MODEL = $Model + node -e "const fs = require('node:fs'); const file = process.env.AGENT_FILE; const model = process.env.AGENT_MODEL; if (!file || !model) throw new Error('missing agent model replacement input'); if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\/[^\s#]+(?:#max)?$/.test(model)) { throw new Error(model + ' must be provider/model or provider/model#max'); } const content = fs.readFileSync(file, 'utf8'); if (!/^model: .+$/m.test(content)) throw new Error(file + ' does not contain model frontmatter'); const baseModel = model.replace(/#[^#]+$/, ''); let updated = content.replace(/^model: .+$/m, 'model: ' + baseModel); updated = /^variant:/m.test(updated) ? updated.replace(/^variant:.*$/m, 'variant: max') : updated.replace(/^model:.*$/m, (line) => line + '\nvariant: max'); fs.writeFileSync(file, updated);" +} + +function Download-LatestRelease { + $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("open-ultracode-" + [System.Guid]::NewGuid().ToString("N")) + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + $archive = Join-Path $tempDir "open-ultracode-release.tar.gz" + + $releaseUrl = "https://github.com/$Repo/releases/latest/download/open-ultracode-release.tar.gz" + Write-Host "Downloading latest OpenUltraCode release from $releaseUrl..." + try { + Invoke-WebRequest -Uri $releaseUrl -OutFile $archive + } catch { + Write-Error "Could not download the latest OpenUltraCode release asset from $releaseUrl: $_" + exit 1 + } + + if (Test-Path $InstallDir) { + Remove-Item -Path $InstallDir -Recurse -Force + } + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + + if (Get-Command tar -ErrorAction SilentlyContinue) { + tar -xzf $archive -C $InstallDir + } else { + Write-Error "tar is required to extract release archive." + exit 1 + } + + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue +} + +function Register-GlobalPlugin { + $configFile = Join-Path $OpenCodeConfigDir "opencode.json" + if (-not (Test-Path $configFile) -and (Test-Path (Join-Path $OpenCodeConfigDir "opencode.jsonc"))) { + $configFile = Join-Path $OpenCodeConfigDir "opencode.jsonc" + } + $pluginPath = (Join-Path $InstallDir ".opencode\plugins\open-ultracode.ts").Replace("\", "/") + + if (-not (Test-Path $OpenCodeConfigDir)) { + New-Item -ItemType Directory -Path $OpenCodeConfigDir -Force | Out-Null + } + + if (-not (Test-Path $configFile)) { + "{\n `"`$schema`": `"https://opencode.ai/config.json`"\n}\n" | Set-Content -Path $configFile -Encoding utf8 + } + + $env:OPENCODE_GLOBAL_CONFIG = $configFile + $env:OPENCODE_PLUGIN_PATH = $pluginPath + + node -e "const fs = require('node:fs'); const configPath = process.env.OPENCODE_GLOBAL_CONFIG; const pluginPath = process.env.OPENCODE_PLUGIN_PATH; if (!configPath || !pluginPath) { throw new Error('missing OpenUltraCode installer environment'); } let config = {}; try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (error) { throw new Error('Could not parse ' + configPath + ': ' + (error instanceof Error ? error.message : String(error))); } if (typeof config !== 'object' || config === null || Array.isArray(config)) { throw new Error(configPath + ' must contain a JSON object'); } config['`$schema'] = config['`$schema'] || 'https://opencode.ai/config.json'; if (config.permission === undefined) { config.permission = { open_ultracode_configure_fusion: 'ask' }; } else if (typeof config.permission === 'object' && config.permission !== null && !Array.isArray(config.permission) && !('open_ultracode_configure_fusion' in config.permission)) { config.permission.open_ultracode_configure_fusion = 'ask'; } const existing = Array.isArray(config.plugin) ? config.plugin : []; config.plugin = existing.filter((entry) => entry !== './.opencode/plugins/open-ultracode.ts'); if (!config.plugin.includes(pluginPath)) { config.plugin.push(pluginPath); } fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');" +} + +function Install-GlobalAssets { + $skillsDir = Join-Path $OpenCodeConfigDir "skills" + $commandsDir = Join-Path $OpenCodeConfigDir "commands" + $agentsDir = Join-Path $OpenCodeConfigDir "agents" + $toolsDir = Join-Path $OpenCodeConfigDir "tools" + $libDir = Join-Path $OpenCodeConfigDir "lib" + + New-Item -ItemType Directory -Path $skillsDir, $commandsDir, $agentsDir, $toolsDir, $libDir -Force | Out-Null + + $panelAFile = Join-Path $agentsDir "ultracode-fusion-panel-a.md" + $panelBFile = Join-Path $agentsDir "ultracode-fusion-panel-b.md" + $arbiterFile = Join-Path $agentsDir "ultracode-fusion-arbiter.md" + + $existingPanelA = Get-AgentModel $panelAFile + $existingPanelB = Get-AgentModel $panelBFile + $existingArbiter = Get-AgentModel $arbiterFile + + $targetSkill = Join-Path $skillsDir "open-ultracode" + if (Test-Path $targetSkill) { + Remove-Item -Path $targetSkill -Recurse -Force + } + Copy-Item -Path (Join-Path $InstallDir ".opencode\skills\open-ultracode") -Destination $targetSkill -Recurse -Force + + Get-ChildItem -Path (Join-Path $InstallDir ".opencode\commands") -Filter "ultracode*.md" | Copy-Item -Destination $commandsDir -Force + Get-ChildItem -Path (Join-Path $InstallDir ".opencode\commands") -Filter "ultrathink*.md" | Copy-Item -Destination $commandsDir -Force + Get-ChildItem -Path (Join-Path $InstallDir ".opencode\agents") -Filter "open-ultracode*.md" | Copy-Item -Destination $agentsDir -Force + Get-ChildItem -Path (Join-Path $InstallDir ".opencode\agents") -Filter "ultracode-fusion*.md" | Copy-Item -Destination $agentsDir -Force + Copy-Item -Path (Join-Path $InstallDir ".opencode\tools\open_ultracode_configure_fusion.ts") -Destination $toolsDir -Force + Copy-Item -Path (Join-Path $InstallDir ".opencode\lib\fusion-config.ts") -Destination $libDir -Force + + $chosenPanelA = if ($PanelA) { $PanelA } elseif ($existingPanelA) { $existingPanelA } else { $DefaultFusionPanelAModel } + $chosenPanelB = if ($PanelB) { $PanelB } elseif ($existingPanelB) { $existingPanelB } else { $DefaultFusionPanelBModel } + $chosenArbiter = if ($Decider) { $Decider } elseif ($existingArbiter) { $existingArbiter } else { $DefaultFusionArbiterModel } + + Set-AgentModel $panelAFile $chosenPanelA + Set-AgentModel $panelBFile $chosenPanelB + Set-AgentModel $arbiterFile $chosenArbiter + + Register-GlobalPlugin +} + +Download-LatestRelease +Install-GlobalAssets + +Write-Host "OpenUltraCode is installed globally for opencode. Restart opencode to load the plugin, commands, skills, and agents." diff --git a/package.json b/package.json index 5848d93..847838e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "README.md", "install.sh", "install-dev.sh", + "install.ps1", + "install-dev.ps1", "eslint.config.js", "dagger.json", ".dagger", diff --git a/scripts/validate-assets.ts b/scripts/validate-assets.ts index 43d7a74..a0167e5 100644 --- a/scripts/validate-assets.ts +++ b/scripts/validate-assets.ts @@ -19,6 +19,8 @@ const requiredScaffoldPaths = [ "tsconfig.json", "install.sh", "install-dev.sh", + "install.ps1", + "install-dev.ps1", ".github/workflows/release.yml", "src", ".opencode/plugins", diff --git a/tests/assets.test.ts b/tests/assets.test.ts index 4340290..777d8f8 100644 --- a/tests/assets.test.ts +++ b/tests/assets.test.ts @@ -24,6 +24,8 @@ const requiredScaffoldPaths = [ "scripts/validate-assets.ts", "install.sh", "install-dev.sh", + "install.ps1", + "install-dev.ps1", ".github/workflows/release.yml", "pnpm-workspace.yaml" ] as const @@ -48,6 +50,8 @@ describe("OpenUltraCode scaffold", () => { const pnpmWorkspace = readFileSync(join(projectRoot, "pnpm-workspace.yaml"), "utf8") const installer = readFileSync(join(projectRoot, "install.sh"), "utf8") const devInstaller = readFileSync(join(projectRoot, "install-dev.sh"), "utf8") + const psInstaller = readFileSync(join(projectRoot, "install.ps1"), "utf8") + const psDevInstaller = readFileSync(join(projectRoot, "install-dev.ps1"), "utf8") const releaseWorkflow = readFileSync(join(projectRoot, ".github/workflows/release.yml"), "utf8") assert.equal(config.$schema, "https://opencode.ai/config.json") @@ -69,6 +73,11 @@ describe("OpenUltraCode scaffold", () => { assert.match(installer, /DEFAULT_FUSION_ARBITER_MODEL="opencode\/kimi-k3#max"/) assert.match(installer, /legacyFusionPlaceholders/) assert.match(devInstaller, /legacyFusionPlaceholders/) + assert.match(psInstaller, /DefaultFusionPanelAModel = "opencode\/deepseek-v4-pro#max"/) + assert.match(psInstaller, /DefaultFusionPanelBModel = "opencode\/glm-5\.2#max"/) + assert.match(psInstaller, /DefaultFusionArbiterModel = "opencode\/kimi-k3#max"/) + assert.match(psInstaller, /legacyFusionPlaceholders/) + assert.match(psDevInstaller, /legacyFusionPlaceholders/) assert.match(installer, /read_current_model\(\)/) assert.match(installer, /select_from_menu\(\)/) assert.match(installer, /releases\/latest\/download\/open-ultracode-release\.tar\.gz/) @@ -83,6 +92,7 @@ describe("OpenUltraCode scaffold", () => { assert.match(devInstaller, /pnpm install --frozen-lockfile/) assert.match(devInstaller, /pnpm run check/) assert.match(devInstaller, /git config core\.hooksPath \.githooks/) + assert.match(psDevInstaller, /pnpm install --frozen-lockfile/) assert.match(releaseWorkflow, /open-ultracode-release\.tar\.gz/) assert.match(releaseWorkflow, /gh release create/) assert.match(releaseWorkflow, /--prerelease/) @@ -95,6 +105,8 @@ describe("OpenUltraCode scaffold", () => { "README.md", "install.sh", "install-dev.sh", + "install.ps1", + "install-dev.ps1", "eslint.config.js", "dagger.json", ".dagger", @@ -210,7 +222,7 @@ function createAssetFixture(): string { lint: "dagger call lint --source .", check: "pnpm run build && pnpm run lint && pnpm run test && pnpm run validate:assets" }, - files: [".opencode", "src", "docs", "scripts/validate-assets.ts", "README.md", "install.sh", "install-dev.sh", "eslint.config.js", "dagger.json", ".dagger", ".githooks", "opencode.json", "pnpm-workspace.yaml"] + files: [".opencode", "src", "docs", "scripts/validate-assets.ts", "README.md", "install.sh", "install-dev.sh", "install.ps1", "install-dev.ps1", "eslint.config.js", "dagger.json", ".dagger", ".githooks", "opencode.json", "pnpm-workspace.yaml"] })) writeFileSync( join(root, "opencode.json"), @@ -225,6 +237,8 @@ function createAssetFixture(): string { writeFileSync(join(root, "README.md"), "# OpenUltraCode\n") writeFileSync(join(root, "install.sh"), "#!/usr/bin/env sh\n") writeFileSync(join(root, "install-dev.sh"), "#!/usr/bin/env sh\n") + writeFileSync(join(root, "install.ps1"), "# PowerShell installer\n") + writeFileSync(join(root, "install-dev.ps1"), "# PowerShell dev installer\n") writeFileSync(join(root, ".github/workflows/release.yml"), "name: Release\n") writeFileSync(join(root, "pnpm-workspace.yaml"), "allowBuilds:\n esbuild: true\nminimumReleaseAge: 4320\n") writeFileSync(join(root, "scripts/validate-assets.ts"), "") diff --git a/tests/state.test.ts b/tests/state.test.ts index 0582fdf..1af1072 100644 --- a/tests/state.test.ts +++ b/tests/state.test.ts @@ -111,7 +111,7 @@ describe("workflow state store", () => { const outsideRoot = join(parent, "outside") await mkdir(join(projectRoot, ".opencode", "open-ultracode"), { recursive: true }) await mkdir(outsideRoot) - await symlink(outsideRoot, join(projectRoot, ".opencode", "open-ultracode", "state"), "dir") + await symlink(outsideRoot, join(projectRoot, ".opencode", "open-ultracode", "state"), process.platform === "win32" ? "junction" : "dir") const store = createWorkflowStateStore(projectRoot, { state: { directory: stateDirectory } }) await assert.rejects(() => store.update(createState()), /state.directory must not contain symlinks/) @@ -127,7 +127,7 @@ describe("workflow state store", () => { await mkdir(join(projectRoot, ".opencode", "open-ultracode"), { recursive: true }) await mkdir(outsideRoot) await writeFile(join(outsideRoot, "workflow-state.json"), "not json", "utf8") - await symlink(outsideRoot, join(projectRoot, ".opencode", "open-ultracode", "state"), "dir") + await symlink(outsideRoot, join(projectRoot, ".opencode", "open-ultracode", "state"), process.platform === "win32" ? "junction" : "dir") const store = createWorkflowStateStore(projectRoot, { state: { directory: stateDirectory } }) await assert.rejects(() => store.load(), /state.directory must not contain symlinks/)