chore(rebrand): remove one-time rename scaffolding, fix X links and install-page Node version (#2270)

The npm rename has shipped: @twigpine/openclaude@0.32.0 and the one final
@gitlawb/openclaude@0.32.0 are both published, and the old package is
deprecated. The pieces that existed only to get that final old-name release
out can go.

Release workflow
- Remove the legacy-scope-gate job, the legacy steps in install-verify and
  the publish-npm-legacy-scope job. The job graph is back to
  release-please, install-verify, publish-npm and docker, so a release no
  longer depends on a registry lookup for the old package.
- Keep what proved itself in the 0.32.0 release: skipping a version that is
  already published, publishing the verified dist/ with --ignore-scripts,
  and the provenance and trusted-publisher line in the run summary.

App code
- Remove src/utils/packageRename.ts, the package-renamed startup notice, and
  the early exits in `openclaude update` and `/update`. They only fire in a
  build published under the old name, and no such build will be made again.
- Remove the tests that covered them, including the release guard test that
  held the legacy wiring together.

Docs and website
- X links point at x.com/twigpine (README badge and community link, the
  website community list and llms.txt).
- The website install page said Node.js 20 or newer; the package requires 22.
- The upgrade notes for people coming from @gitlawb/openclaude stay.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
This commit is contained in:
Kevin Codex
2026-10-12 12:39:24 +08:00
committed by GitHub
co-authored by OpenClaude
parent 88baafe316
commit 79ced71282
16 changed files with 13 additions and 549 deletions
+2 -197
View File
@@ -29,56 +29,13 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: node
# The @gitlawb -> @twigpine package rename owes the old name exactly one
# final release (see publish-npm-legacy-scope below). This decides whether
# that is still owed, so it can never be published twice: it is owed only
# while the old name's latest version is still the last pre-rename one, or is
# this very release (a re-run after its publish already went through).
legacy-scope-gate:
name: Check whether the final @gitlawb release is still owed
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
runs-on: ubuntu-latest
permissions: {}
outputs:
owed: ${{ steps.gate.outputs.owed }}
steps:
- name: Compare the old package's latest version
id: gate
env:
EXPECTED_VERSION: ${{ needs.release-please.outputs.version }}
LAST_PRE_RENAME_VERSION: 0.31.0
run: |
set -euo pipefail
latest=""
for attempt in 1 2 3; do
latest="$(npm view @gitlawb/openclaude@latest version 2>/dev/null || true)"
if [ -n "$latest" ]; then
break
fi
sleep 10
done
if [ -z "$latest" ]; then
echo "Could not read @gitlawb/openclaude@latest from the registry" >&2
exit 1
fi
if [ "$latest" = "$LAST_PRE_RENAME_VERSION" ] || [ "$latest" = "$EXPECTED_VERSION" ]; then
owed=true
else
owed=false
fi
echo "@gitlawb/openclaude@latest is ${latest}; final legacy release owed: ${owed}"
echo "owed=${owed}" >> "$GITHUB_OUTPUT"
# Zero-warning install gate: pack the release tag and install it globally in
# a sandbox (cold + upgrade scenarios) on the supported Node floor (22 →
# npm 10) and current (24 → npm 11), whose warning output differs. Publish
# is blocked unless the real `npm install -g` experience is clean.
install-verify:
name: Verify clean npm install (Node ${{ matrix.node-version }})
needs: [release-please, legacy-scope-gate]
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
runs-on: ubuntu-latest
permissions:
@@ -113,24 +70,6 @@ jobs:
- name: Verify zero-warning global install
run: bun run install:verify
# The final @gitlawb release is this same tree with only the package name
# changed. Verify it here too, before anything is published: once the new
# name is out it cannot be taken back, and in the first release this is
# the only real upgrade test (nothing is published under the new name yet,
# so its own upgrade scenario is skipped).
- name: Rebuild under the legacy package name
if: ${{ needs.legacy-scope-gate.outputs.owed == 'true' }}
run: |
npm pkg set name=@gitlawb/openclaude
bun run build
- name: Verify the legacy build installs clean and announces the move
if: ${{ needs.legacy-scope-gate.outputs.owed == 'true' }}
run: |
set -euo pipefail
bun run install:verify
node dist/cli.mjs update | grep -F "OpenClaude has moved to @twigpine/openclaude"
publish-npm:
name: Publish to npm
needs: [release-please, install-verify]
@@ -183,10 +122,7 @@ jobs:
echo "NODE_AUTH_TOKEN=" >> "$GITHUB_ENV"
# Tokenless: npm authenticates this job through trusted publishing (OIDC)
# and attaches provenance from the same identity. That only works for a
# package that already exists with a trusted publisher attached, so
# @twigpine/openclaude was created once by hand as an empty 0.0.0
# placeholder before its first release from here.
# and attaches provenance from the same identity.
#
# --ignore-scripts publishes the dist/ that was built and verified above
# instead of rebuilding it in `prepack`. Skipping a version that is
@@ -249,137 +185,6 @@ jobs:
bun scripts/npm-publish-report.ts @twigpine/openclaude "${EXPECTED_VERSION}"
} | tee -a "$GITHUB_STEP_SUMMARY"
# The one final release under the old @gitlawb name, for the @gitlawb ->
# @twigpine package rename. Shipped builds do not auto-update, so people
# update by re-running the install command — and anyone still typing the old
# name (habit, shell history, an old tutorial) lands on whatever is latest
# there. This publishes the same release once under that name, so what they
# land on tells them at every launch that the package moved and how to switch
# (src/utils/packageRename.ts). The build bakes in whatever name package.json
# carries, so renaming before the build is all it takes.
#
# legacy-scope-gate keeps this to a single release: once the old name's
# latest is no longer the pre-rename version, this job never runs again.
# After it ships, `npm deprecate` the old package (that warning is what
# people see when they run the old install command), then delete this job,
# the gate, and the legacy steps in install-verify.
publish-npm-legacy-scope:
name: Publish final release to npm (legacy @gitlawb scope)
needs: [release-please, legacy-scope-gate, publish-npm]
if: ${{ needs.release-please.outputs.release_created == 'true' && needs.legacy-scope-gate.outputs.owed == 'true' }}
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
id-token: write
steps:
- name: Checkout release tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.release-please.outputs.tag_name }}
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
registry-url: https://registry.npmjs.org
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Rename package to the legacy scope
run: npm pkg set name=@gitlawb/openclaude
# Unit tests already ran against this exact source in publish-npm; only
# the package name differs here, so rebuild and re-verify the artifact.
- name: Smoke test
run: bun run smoke
# The upgrade scenario is the path people on the old name actually take:
# the previously published @gitlawb version, then this one over it.
- name: Verify zero-warning global install
run: bun run install:verify
# Proves the old name really got baked in: only then does the build say
# the package moved, instead of behaving like a normal release.
- name: Verify the build announces the move
run: |
set -euo pipefail
node dist/cli.mjs update | grep -F "OpenClaude has moved to @twigpine/openclaude"
- name: Clear token auth for trusted publishing
run: |
unset NODE_AUTH_TOKEN
echo "NODE_AUTH_TOKEN=" >> "$GITHUB_ENV"
# Same tokenless publish and re-run rules as publish-npm. The old
# package's trusted publisher was set up before the GitHub org was renamed
# from Gitlawb to Twigpine, so it needs an entry for Twigpine/openclaude
# for this to authenticate.
- name: Publish to npm
run: |
set -euo pipefail
version="$(node -p "require('./package.json').version")"
if [ "$(npm view "@gitlawb/openclaude@${version}" version 2>/dev/null || true)" = "${version}" ]; then
echo "@gitlawb/openclaude@${version} is already on the registry; not publishing again."
exit 0
fi
npm publish --access public --provenance --ignore-scripts
- name: Verify npm latest dist-tag
env:
EXPECTED_VERSION: ${{ needs.release-please.outputs.version }}
run: |
set -euo pipefail
if [ -z "${EXPECTED_VERSION}" ]; then
echo "release-please did not provide an expected version" >&2
exit 1
fi
for attempt in $(seq 1 30); do
published_version="$(npm view @gitlawb/openclaude version 2>/dev/null || true)"
latest_tag="$(npm view @gitlawb/openclaude dist-tags.latest 2>/dev/null || true)"
latest_version="$(npm view @gitlawb/openclaude@latest version 2>/dev/null || true)"
echo "Attempt ${attempt}: version=${published_version:-<empty>} latest=${latest_tag:-<empty>} @latest=${latest_version:-<empty>} expected=${EXPECTED_VERSION}"
if [ "$published_version" = "$EXPECTED_VERSION" ] && \
[ "$latest_tag" = "$EXPECTED_VERSION" ] && \
[ "$latest_version" = "$EXPECTED_VERSION" ]; then
echo "npm latest verified for @gitlawb/openclaude@${EXPECTED_VERSION}"
exit 0
fi
sleep 10
done
echo "npm latest dist-tag did not resolve to ${EXPECTED_VERSION}" >&2
echo "Observed package version: ${published_version:-<empty>}" >&2
echo "Observed dist-tags.latest: ${latest_tag:-<empty>}" >&2
echo "Observed @latest version: ${latest_version:-<empty>}" >&2
exit 1
- name: Release summary
env:
EXPECTED_VERSION: ${{ needs.release-please.outputs.version }}
run: |
{
echo "## Final legacy-scope release ${{ needs.release-please.outputs.tag_name }}"
echo
echo "- npm (legacy scope): https://www.npmjs.com/package/@gitlawb/openclaude"
bun scripts/npm-publish-report.ts @gitlawb/openclaude "${EXPECTED_VERSION}"
echo "- Next: run \`npm deprecate @gitlawb/openclaude\` with the switch commands, then delete this job and its gate"
} | tee -a "$GITHUB_STEP_SUMMARY"
docker:
name: Build & Push Docker Image
needs: release-please
+2 -2
View File
@@ -17,7 +17,7 @@ Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex OAuth, Codex, Ollama, A
[![npm downloads](https://img.shields.io/npm/dm/@twigpine/openclaude)](https://www.npmjs.com/package/@twigpine/openclaude)
[![Discussions](https://img.shields.io/badge/discussions-open-7c3aed)](https://github.com/Twigpine/openclaude/discussions)
[![Discord](https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white)](https://discord.gg/k68zFR6AcB)
[![X](https://img.shields.io/badge/X-@gitlawb-000000?logo=x&logoColor=white)](https://x.com/gitlawb)
[![X](https://img.shields.io/badge/X-@twigpine-000000?logo=x&logoColor=white)](https://x.com/twigpine)
[![Security Policy](https://img.shields.io/badge/security-policy-0f766e)](SECURITY.md)
[![License](https://img.shields.io/badge/license-MIT-2563eb)](LICENSE)
@@ -534,7 +534,7 @@ If you believe you found a security issue, see [SECURITY.md](SECURITY.md).
- Use [GitHub Discussions](https://github.com/Twigpine/openclaude/discussions) for Q&A, ideas, and community conversation
- Use [GitHub Issues](https://github.com/Twigpine/openclaude/issues) for confirmed bugs and actionable feature work
- Join the [Discord](https://discord.gg/k68zFR6AcB) to chat with the community in real time
- Follow [@gitlawb on X](https://x.com/gitlawb) for updates and announcements
- Follow [@twigpine on X](https://x.com/twigpine) for updates and announcements
## Contributing
+3 -3
View File
@@ -18,9 +18,9 @@ import { canonicalStub, collectBundleStubs } from './stubMarkerGuard.js'
const nodeRequire = createRequire(import.meta.url)
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
const version = pkg.version
// Baked into the update hints, the doctor checks and the renamed-package
// notice, so it has to be the name the tarball is actually published under —
// derive it instead of repeating it.
// Baked into the update hints and the doctor checks, so it has to be the
// name the tarball is actually published under — derive it instead of
// repeating it.
const packageName = pkg.name
const reactPackageDir = dirname(nodeRequire.resolve('react/package.json'))
const reactReconcilerPackageDir = dirname(
@@ -1,54 +0,0 @@
import { expect, test } from 'bun:test'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import {
CURRENT_PACKAGE_NAME,
LEGACY_PACKAGE_NAME,
} from '../src/utils/packageRename.js'
// The @gitlawb -> @twigpine rename is wired across files that nothing else
// ties together. A mismatch between them only surfaces at release time, after
// the tag and GitHub Release already exist, so pin the contract here.
const ROOT = join(import.meta.dir, '..')
const read = (path: string): string => readFileSync(join(ROOT, path), 'utf8')
test('package.json publishes under the current package name', () => {
expect(JSON.parse(read('package.json')).name).toBe(CURRENT_PACKAGE_NAME)
})
// The final legacy-scope release is this same tree rebuilt after
// `npm pkg set name=…`. It only announces the move if the build bakes in the
// manifest name; a hardcoded name would ship it as a silent normal release.
test('the build bakes in the manifest name, not a hardcoded scope', () => {
const defines = read('scripts/build.ts').match(/'MACRO\.PACKAGE_URL':[^\n]*/g) ?? []
expect(defines.length).toBeGreaterThan(0)
for (const define of defines) {
expect(define).toContain('JSON.stringify(packageName)')
}
})
test('the release workflow publishes and verifies the current package name', () => {
expect(read('.github/workflows/release.yml')).toContain(
`npm view ${CURRENT_PACKAGE_NAME}@latest version`,
)
})
// Only meaningful while the one-time legacy job still exists; once it has
// shipped and been deleted there is nothing left to hold together, and this
// test should be deleted with it rather than the job restored.
test('while the final legacy-scope job exists, it is gated to a single release', () => {
const workflow = read('.github/workflows/release.yml')
if (!workflow.includes('publish-npm-legacy-scope:')) return
expect(workflow).toContain(`npm pkg set name=${LEGACY_PACKAGE_NAME}`)
expect(workflow).toContain(`npm view ${LEGACY_PACKAGE_NAME}@latest version`)
// The gate is what makes it a hard stop: without it the job would publish
// to the old name again on every later release.
expect(workflow).toContain('legacy-scope-gate:')
expect(workflow).toMatch(
/publish-npm-legacy-scope:[\s\S]*?if: \$\{\{[^\n]*needs\.legacy-scope-gate\.outputs\.owed == 'true'/,
)
})
+2 -4
View File
@@ -46,10 +46,8 @@ import {
validateRuntimeDependencyContract,
} from './externalsValidation.js'
// Read from the manifest instead of repeated here: the release workflow also
// verifies and publishes this tree once under the legacy @gitlawb scope (the
// publish-npm-legacy-scope job in release.yml), and every check below has to
// look for whichever name the tarball was packed with.
// Read from the manifest instead of repeated here, so every check below looks
// for the name the tarball was actually packed with.
const PACKAGE_NAME: string = JSON.parse(
readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf8'),
).name
-36
View File
@@ -3,7 +3,6 @@ import { withMockMacro } from 'src/test/mockMacro.js'
import {
getGlobalUpdateFailureHint,
writePackageManagerUpdateGuidance,
writePackageRenamedNote,
} from './update.js'
describe('getGlobalUpdateFailureHint', () => {
@@ -60,38 +59,3 @@ describe('writePackageManagerUpdateGuidance', () => {
},
)
})
describe('writePackageRenamedNote', () => {
function capture(packageUrl: string): { output: string; wrote: boolean } {
let output = ''
const wrote = withMockMacro({ PACKAGE_URL: packageUrl }, () =>
writePackageRenamedNote({
write: value => {
output += value
},
bold: value => value,
yellow: value => value,
}),
)
return { output, wrote }
}
test('tells legacy-scope installs where the package moved', () => {
const { output, wrote } = capture('@gitlawb/openclaude')
// update() stops after the note when this is true.
expect(wrote).toBe(true)
expect(output).toContain('OpenClaude has moved to @twigpine/openclaude.')
// Uninstall first: installing the new package over the old one fails.
expect(output.indexOf('npm uninstall -g @gitlawb/openclaude')).toBeGreaterThan(-1)
expect(output.indexOf('npm uninstall -g @gitlawb/openclaude')).toBeLessThan(
output.indexOf('npm install -g @twigpine/openclaude@latest'),
)
// The npm commands are wrong for other global package managers; say so.
expect(output).toContain('bun, pnpm or yarn')
})
test('writes nothing and lets the update proceed for the current package', () => {
expect(capture('@twigpine/openclaude')).toEqual({ output: '', wrote: false })
})
})
-44
View File
@@ -32,13 +32,6 @@ import {
getPackageManagerUpdateGuidance,
type PackageManagerUpdateGuidance,
} from 'src/utils/packageManagerUpdateGuidance.js'
import {
isLegacyPackageBuild,
PACKAGE_RENAMED_DETAIL,
PACKAGE_RENAMED_HEADLINE,
PACKAGE_SWITCH_COMMANDS,
PACKAGE_SWITCH_OTHER_MANAGERS,
} from 'src/utils/packageRename.js'
import { writeToStdout } from 'src/utils/process.js'
import { gte } from 'src/utils/semver.js'
import { shouldRemoveInstalledSymlinkForNpmUpdate } from 'src/utils/autoUpdaterRouting.js'
@@ -56,35 +49,6 @@ export function getGlobalUpdateFailureHint(
: `Or update manually with:\n npm install -g ${MACRO.PACKAGE_URL}@latest\n`
}
/**
* The final build published under the pre-rename package name has nothing to
* update to, so say where the package went. Returns whether it wrote the note
* (false, and no output, for every other build).
*/
export function writePackageRenamedNote(
deps: {
isLegacyBuild?: boolean
write?: (value: string) => void
bold?: (value: string) => string
yellow?: (value: string) => string
} = {},
): boolean {
if (!(deps.isLegacyBuild ?? isLegacyPackageBuild())) {
return false
}
const write = deps.write ?? writeToStdout
const bold = deps.bold ?? chalk.bold
const yellow = deps.yellow ?? chalk.yellow
write(yellow(`${PACKAGE_RENAMED_HEADLINE}\n`))
write(`${PACKAGE_RENAMED_DETAIL}\n`)
for (const command of PACKAGE_SWITCH_COMMANDS) {
write(bold(` ${command}`) + '\n')
}
write(`${PACKAGE_SWITCH_OTHER_MANAGERS}\n`)
return true
}
export async function writePackageManagerUpdateGuidance(
manager: PackageManager,
channel: ReleaseChannel,
@@ -122,14 +86,6 @@ export async function writePackageManagerUpdateGuidance(
}
export async function update() {
// The final build published under the pre-rename package name has nothing to
// update to. Say where the package went and stop, rather than run a check
// that can only fail or point back at the old name.
if (writePackageRenamedNote()) {
await gracefulShutdown(0)
return
}
// Block updates for third-party providers using upstream Anthropic builds.
// The update mechanism downloads from the first-party distribution bucket,
// which would silently replace the OpenClaude build with the upstream
-23
View File
@@ -66,26 +66,3 @@ describe('removeStaleNativeLauncherForNpmUpdate', () => {
expect(removed).toBe(1)
})
})
describe('/update on the final legacy-scope build', () => {
// There is nothing to update to under the old name; reporting a successful
// no-op update would hide that the package moved.
test('reports where the package moved instead of running an update', async () => {
const results: string[] = []
const rendered = await withMockMacro(
{ PACKAGE_URL: '@gitlawb/openclaude' },
async () => {
const { call } = await importFreshUpdateCommand()
return call((result: string) => results.push(result), {}, '')
},
)
expect(rendered).toBeNull()
expect(results).toHaveLength(1)
expect(results[0]).toContain('OpenClaude has moved to @twigpine/openclaude.')
expect(results[0]).toContain('npm uninstall -g @gitlawb/openclaude')
expect(results[0]).toContain('npm install -g @twigpine/openclaude@latest')
})
})
-11
View File
@@ -22,10 +22,6 @@ import {
} from '../../utils/nativeInstaller/index.js'
import type { PackageManager } from '../../utils/nativeInstaller/packageManagers.js'
import { getPackageManagerUpdateGuidance } from '../../utils/packageManagerUpdateGuidance.js'
import {
getPackageRenamedMessage,
isLegacyPackageBuild,
} from '../../utils/packageRename.js'
import { shouldRemoveInstalledSymlinkForNpmUpdate } from '../../utils/autoUpdaterRouting.js'
import { resolveUpdateStrategy } from '../../utils/updateStrategy.js'
@@ -362,13 +358,6 @@ export async function call(
_context: unknown,
args: string,
): Promise<React.ReactNode> {
// The final build published under the pre-rename package name has nothing to
// update to; say where the package went instead of reporting a no-op update.
if (isLegacyPackageBuild()) {
onDone(getPackageRenamedMessage())
return null
}
const tokens = (args ?? '').trim().split(/\s+/).filter(Boolean)
const force = tokens.includes('--force')
const nonFlag = tokens.filter(token => !token.startsWith('--'))
-55
View File
@@ -1,55 +0,0 @@
import { describe, expect, test } from 'bun:test'
import { withMockMacro } from 'src/test/mockMacro.js'
import {
CURRENT_PACKAGE_NAME,
getPackageRenamedMessage,
isLegacyPackageBuild,
isLegacyPackageName,
LEGACY_PACKAGE_NAME,
PACKAGE_SWITCH_COMMANDS,
} from './packageRename.js'
describe('isLegacyPackageName', () => {
test('matches only the pre-rename package name', () => {
expect(isLegacyPackageName(LEGACY_PACKAGE_NAME)).toBe(true)
expect(isLegacyPackageName(CURRENT_PACKAGE_NAME)).toBe(false)
expect(isLegacyPackageName('@example/custom-cli')).toBe(false)
expect(isLegacyPackageName(undefined)).toBe(false)
})
})
describe('isLegacyPackageBuild', () => {
test('follows the package name baked into the build', () => {
withMockMacro({ PACKAGE_URL: LEGACY_PACKAGE_NAME }, () => {
expect(isLegacyPackageBuild()).toBe(true)
})
withMockMacro({ PACKAGE_URL: CURRENT_PACKAGE_NAME }, () => {
expect(isLegacyPackageBuild()).toBe(false)
})
})
})
describe('PACKAGE_SWITCH_COMMANDS', () => {
// Both packages own the `openclaude` bin. Installing the new one first fails
// with EEXIST, and forcing it lets the later uninstall delete the shared
// bin, so the uninstall has to come first.
test('uninstalls the old package before installing the new one', () => {
expect(PACKAGE_SWITCH_COMMANDS).toEqual([
`npm uninstall -g ${LEGACY_PACKAGE_NAME}`,
`npm install -g ${CURRENT_PACKAGE_NAME}@latest`,
])
})
})
describe('getPackageRenamedMessage', () => {
test('carries the headline, both commands in order, and the other-managers hint', () => {
const message = getPackageRenamedMessage()
const [uninstall, install] = PACKAGE_SWITCH_COMMANDS
expect(message).toContain(`OpenClaude has moved to ${CURRENT_PACKAGE_NAME}.`)
expect(message.indexOf(uninstall!)).toBeGreaterThan(-1)
expect(message.indexOf(uninstall!)).toBeLessThan(message.indexOf(install!))
expect(message).toContain('bun, pnpm or yarn')
})
})
-51
View File
@@ -1,51 +0,0 @@
/**
* The npm package moved from the `@gitlawb` scope to `@twigpine`.
*
* Shipped builds have no working auto-updater (scripts/no-telemetry-plugin.ts
* stubs it out), so people update by re-running the install command by hand —
* and anyone doing that from habit, shell history or an old tutorial still
* types the old name. `npm deprecate` on the old package warns them at that
* moment; the single final release published under the old name (the
* `publish-npm-legacy-scope` job in `.github/workflows/release.yml`) is what
* they land on, and it keeps telling them where the package went. Nothing is
* published to the old scope after that.
*
* There is deliberately no in-place migration. Both packages own the
* `openclaude` bin, so npm refuses to install one over the other (EEXIST), and
* forcing it lets a later uninstall of the old package delete the shared bin.
* The only safe order is "uninstall old, then install new", run by the user.
*/
export const LEGACY_PACKAGE_NAME = '@gitlawb/openclaude'
export const CURRENT_PACKAGE_NAME = '@twigpine/openclaude'
export const PACKAGE_RENAMED_HEADLINE = `OpenClaude has moved to ${CURRENT_PACKAGE_NAME}.`
export const PACKAGE_RENAMED_DETAIL = `${LEGACY_PACKAGE_NAME} is no longer updated. To keep getting updates, quit OpenClaude and run:`
/** Order matters: installing the new package first fails with EEXIST. */
export const PACKAGE_SWITCH_COMMANDS: readonly string[] = [
`npm uninstall -g ${LEGACY_PACKAGE_NAME}`,
`npm install -g ${CURRENT_PACKAGE_NAME}@latest`,
]
/** The commands above only fit an npm global install. */
export const PACKAGE_SWITCH_OTHER_MANAGERS =
'Installed with bun, pnpm or yarn? Use its global remove and add commands instead.'
/** The whole note as plain text, for surfaces that take a single string. */
export function getPackageRenamedMessage(): string {
return [
PACKAGE_RENAMED_HEADLINE,
PACKAGE_RENAMED_DETAIL,
...PACKAGE_SWITCH_COMMANDS.map(command => ` ${command}`),
PACKAGE_SWITCH_OTHER_MANAGERS,
].join('\n')
}
export function isLegacyPackageName(packageName: string | undefined): boolean {
return packageName === LEGACY_PACKAGE_NAME
}
/** True when this build was published under the pre-rename package name. */
export function isLegacyPackageBuild(): boolean {
return isLegacyPackageName(MACRO.PACKAGE_URL)
}
@@ -1,49 +0,0 @@
import { describe, expect, test } from 'bun:test'
import { withMockMacro } from 'src/test/mockMacro.js'
import {
CURRENT_PACKAGE_NAME,
LEGACY_PACKAGE_NAME,
PACKAGE_SWITCH_COMMANDS,
} from './packageRename.js'
import type { StatusNoticeContext } from './statusNoticeDefinitions.js'
import { statusNoticeDefinitions } from './statusNoticeDefinitions.js'
import { renderToString } from './staticRender.js'
// The final build published under the pre-rename package name is the only
// build that should tell users the package moved.
const context: StatusNoticeContext = {
config: {} as StatusNoticeContext['config'],
memoryFiles: [],
}
function packageRenamedNotice() {
const notice = statusNoticeDefinitions.find(n => n.id === 'package-renamed')
expect(notice).toBeDefined()
return notice!
}
describe('package renamed notice', () => {
test('is active only for the legacy-scope build', () => {
withMockMacro({ PACKAGE_URL: LEGACY_PACKAGE_NAME }, () => {
expect(packageRenamedNotice().isActive(context)).toBe(true)
})
withMockMacro({ PACKAGE_URL: CURRENT_PACKAGE_NAME }, () => {
expect(packageRenamedNotice().isActive(context)).toBe(false)
})
})
test('shows where the package moved and the switch commands in order', async () => {
const output = await renderToString(
packageRenamedNotice().render(context),
100,
)
expect(output).toContain(`OpenClaude has moved to ${CURRENT_PACKAGE_NAME}.`)
const [uninstall, install] = PACKAGE_SWITCH_COMMANDS
expect(output).toContain(uninstall!)
expect(output).toContain(install!)
expect(output.indexOf(uninstall!)).toBeLessThan(output.indexOf(install!))
expect(output).toContain('bun, pnpm or yarn')
})
})
+1 -17
View File
@@ -17,7 +17,6 @@ import { isDangerousPermissionMode, type PermissionMode } from './permissions/Pe
import { modelSupportsAutoMode } from './betas.js';
import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from './model/providers.js';
import { logForDebugging } from './debug.js';
import { isLegacyPackageBuild, PACKAGE_RENAMED_DETAIL, PACKAGE_RENAMED_HEADLINE, PACKAGE_SWITCH_COMMANDS, PACKAGE_SWITCH_OTHER_MANAGERS } from './packageRename.js';
// Types
export type StatusNoticeType = 'warning' | 'info';
@@ -334,23 +333,8 @@ const dangerouslySkipPermissionsNotice: StatusNoticeDefinition = {
},
};
// Only the final build published under the pre-rename package name shows this
// (see packageRename.ts). Nothing newer is ever published under that name, so
// startup is where its users keep being told the package moved.
const packageRenamedNotice: StatusNoticeDefinition = {
id: 'package-renamed',
type: 'warning',
isActive: () => isLegacyPackageBuild(),
render: () => <WarningNoticeRow>
<Text color="warning">{PACKAGE_RENAMED_HEADLINE}</Text>
<Text dimColor>{PACKAGE_RENAMED_DETAIL}</Text>
{PACKAGE_SWITCH_COMMANDS.map(command => <Text key={command}>{` ${command}`}</Text>)}
<Text dimColor>{PACKAGE_SWITCH_OTHER_MANAGERS}</Text>
</WarningNoticeRow>,
};
// All notice definitions
export const statusNoticeDefinitions: StatusNoticeDefinition[] = [largeMemoryFilesNotice, largeAgentDescriptionsNotice, localModelContextLoadNotice, claudeAiSubscriberExternalTokenNotice, apiKeyConflictNotice, bothAuthMethodsNotice, jetbrainsPluginNotice, thirdPartyPermissiveModeNotice, dangerouslySkipPermissionsNotice, packageRenamedNotice];
export const statusNoticeDefinitions: StatusNoticeDefinition[] = [largeMemoryFilesNotice, largeAgentDescriptionsNotice, localModelContextLoadNotice, claudeAiSubscriberExternalTokenNotice, apiKeyConflictNotice, bothAuthMethodsNotice, jetbrainsPluginNotice, thirdPartyPermissiveModeNotice, dangerouslySkipPermissionsNotice];
// Helper functions for external use
export function getActiveNotices(context: StatusNoticeContext): StatusNoticeDefinition[] {
+1 -1
View File
@@ -35,4 +35,4 @@ OpenClaude originated from the Claude Code codebase and has since been substanti
- [Discord](https://discord.gg/k68zFR6AcB)
- [GitHub Discussions](https://github.com/Twigpine/openclaude/discussions)
- [X / Twitter](https://x.com/gitlawb)
- [X / Twitter](https://x.com/twigpine)
+1 -1
View File
@@ -57,7 +57,7 @@ export const partners: Partner[] = [
export const community = [
{ name: 'github', url: 'https://github.com/Twigpine/openclaude', label: 'star the repo' },
{ name: 'discord', url: 'https://discord.gg/k68zFR6AcB', label: 'join the discord' },
{ name: 'x', url: 'https://x.com/gitlawb', label: 'follow @gitlawb' },
{ name: 'x', url: 'https://x.com/twigpine', label: 'follow @twigpine' },
{ name: 'discussions', url: 'https://github.com/Twigpine/openclaude/discussions', label: 'open a discussion' },
{ name: 'trendshift', url: 'https://trendshift.io/repositories/25807', label: 'featured on trendshift' },
]
+1 -1
View File
@@ -22,7 +22,7 @@ const toc = [
>
<h2 id="requirements">requirements</h2>
<ul>
<li><strong>Node.js 20 or newer</strong> — grab it from <a href="https://nodejs.org/" rel="noopener">nodejs.org</a> and confirm with <code>node --version</code>.</li>
<li><strong>Node.js 22 or newer</strong> — grab it from <a href="https://nodejs.org/" rel="noopener">nodejs.org</a> and confirm with <code>node --version</code>.</li>
<li>A terminal: Terminal/iTerm on macOS, any shell on Linux, PowerShell or Windows Terminal on Windows.</li>
<li><code>ripgrep</code> is bundled; if startup reports <code>ripgrep not found</code>, install it system-wide and confirm <code>rg --version</code> works.</li>
</ul>