mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-06 12:50:54 -04:00
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:
co-authored by
OpenClaude
parent
88baafe316
commit
79ced71282
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@ Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex OAuth, Codex, Ollama, A
|
||||
[](https://www.npmjs.com/package/@twigpine/openclaude)
|
||||
[](https://github.com/Twigpine/openclaude/discussions)
|
||||
[](https://discord.gg/k68zFR6AcB)
|
||||
[](https://x.com/gitlawb)
|
||||
[](https://x.com/twigpine)
|
||||
[](SECURITY.md)
|
||||
[](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
@@ -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'/,
|
||||
)
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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('--'))
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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
@@ -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)
|
||||
@@ -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' },
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user