Skip to content

Commit 3c34c5a

Browse files
committed
fix(update): preserve scoped package roots
1 parent 8c1c3ea commit 3c34c5a

4 files changed

Lines changed: 51 additions & 5 deletions

File tree

src/cli/update-cli/update-command.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3584,6 +3584,7 @@ async function updateCommandInternal(opts: UpdateCommandOptions): Promise<void>
35843584
let packageInstallEnv: NodeJS.ProcessEnv | undefined;
35853585
let packageInstallCwd: string | undefined;
35863586
let packageInstallTarget: ResolvedGlobalInstallTarget | undefined;
3587+
let installedPackageName = DEFAULT_PACKAGE_NAME;
35873588
let packageAlreadyCurrent = false;
35883589
let managedServiceRootRedirect: ManagedServiceRootRedirect | null = null;
35893590
// Resolved independently of the root redirect so it covers the common case
@@ -3641,6 +3642,7 @@ async function updateCommandInternal(opts: UpdateCommandOptions): Promise<void>
36413642
packageInstallEnv = await createGlobalInstallEnv();
36423643
packageInstallCwd = tryResolveInvocationCwd();
36433644
if (updateInstallKind === "package") {
3645+
installedPackageName = (await readPackageName(root)) ?? DEFAULT_PACKAGE_NAME;
36443646
const manager = await resolveGlobalManager({
36453647
root,
36463648
installKind,
@@ -3653,18 +3655,17 @@ async function updateCommandInternal(opts: UpdateCommandOptions): Promise<void>
36533655
pkgRoot: root,
36543656
honorPackageRoot:
36553657
managedServiceRootRedirect !== null || managedServiceNodeRunner !== undefined,
3658+
packageName: installedPackageName,
36563659
});
36573660
}
36583661
const npmMetadataCommand =
36593662
packageInstallTarget?.manager === "npm" ? packageInstallTarget.command : undefined;
36603663
currentVersion = switchToPackage ? null : await readPackageVersion(root);
36613664
if (channel === "extended-stable") {
3662-
const packageName =
3663-
(await readPackageName(packageInstallTarget?.packageRoot ?? root)) ?? DEFAULT_PACKAGE_NAME;
36643665
const extendedStable = await resolveExtendedStablePackage({
36653666
installKind: updateInstallKind,
36663667
timeoutMs,
3667-
packageName,
3668+
packageName: installedPackageName,
36683669
});
36693670
if (extendedStable.status === "failed") {
36703671
await reportPreMutationUpdateFailure({

src/infra/update-global.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,28 @@ describe("update global helpers", () => {
133133
);
134134
});
135135

136+
it("resolves scoped package paths from the package manager global root", async () => {
137+
const globalRoot = path.join("tmp", "npm-root");
138+
const runCommand: CommandRunner = async () => ({
139+
stdout: `${globalRoot}\n`,
140+
stderr: "",
141+
code: 0,
142+
});
143+
144+
await expect(
145+
resolveGlobalInstallTarget({
146+
manager: "npm",
147+
runCommand,
148+
timeoutMs: 1000,
149+
packageName: "@kevins8/openclaw",
150+
}),
151+
).resolves.toMatchObject({
152+
manager: "npm",
153+
globalRoot,
154+
packageRoot: path.join(globalRoot, "@kevins8", "openclaw"),
155+
});
156+
});
157+
136158
it("maps main and explicit install specs for global installs", () => {
137159
expect(resolveGlobalInstallSpec({ packageName: "openclaw", tag: "main" })).toBe(
138160
OPENCLAW_MAIN_PACKAGE_SPEC,

src/infra/update-global.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,22 @@ function inferGlobalRootFromPackageRoot(pkgRoot?: string | null): string | null
599599
return path.basename(globalRoot) === "node_modules" ? globalRoot : null;
600600
}
601601

602+
function resolvePackageRootFromGlobalRoot(params: {
603+
globalRoot: string;
604+
packageName?: string;
605+
}): string {
606+
const packageName = params.packageName?.trim() || PRIMARY_PACKAGE_NAME;
607+
const parts = packageName.split("/");
608+
const hasSafeSegments =
609+
parts.length > 0 &&
610+
parts.length <= 2 &&
611+
parts.every(
612+
(part) => part.length > 0 && part !== "." && part !== ".." && !part.includes("\\"),
613+
) &&
614+
(parts.length === 1 || parts[0]?.startsWith("@"));
615+
return path.join(params.globalRoot, ...(hasSafeSegments ? parts : [PRIMARY_PACKAGE_NAME]));
616+
}
617+
602618
function isDirectNpmNodeModulesRoot(globalRoot: string | null): boolean {
603619
return (
604620
globalRoot !== null &&
@@ -753,6 +769,7 @@ export async function resolveGlobalInstallTarget(params: {
753769
timeoutMs: number;
754770
pkgRoot?: string | null;
755771
honorPackageRoot?: boolean;
772+
packageName?: string;
756773
}): Promise<ResolvedGlobalInstallTarget> {
757774
const honoredPackageRootGlobalRoot = params.honorPackageRoot
758775
? inferGlobalRootFromPackageRoot(params.pkgRoot)
@@ -787,7 +804,12 @@ export async function resolveGlobalInstallTarget(params: {
787804
return {
788805
...command,
789806
globalRoot: targetGlobalRoot,
790-
packageRoot: targetGlobalRoot ? path.join(targetGlobalRoot, PRIMARY_PACKAGE_NAME) : null,
807+
packageRoot: targetGlobalRoot
808+
? resolvePackageRootFromGlobalRoot({
809+
globalRoot: targetGlobalRoot,
810+
packageName: params.packageName,
811+
})
812+
: null,
791813
...(honoredPackageRootGlobalRoot &&
792814
targetGlobalRoot === honoredPackageRootGlobalRoot &&
793815
honoredDirectNpmRoot

src/infra/update-runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1681,13 +1681,14 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
16811681
durationMs: Date.now() - startedAt,
16821682
};
16831683
}
1684+
const packageName = (await readPackageName(pkgRoot)) ?? DEFAULT_PACKAGE_NAME;
16841685
const installTarget = await resolveGlobalInstallTarget({
16851686
manager: globalManager,
16861687
runCommand,
16871688
timeoutMs,
16881689
pkgRoot,
1690+
packageName,
16891691
});
1690-
const packageName = (await readPackageName(pkgRoot)) ?? DEFAULT_PACKAGE_NAME;
16911692
await cleanupGlobalRenameDirs({
16921693
globalRoot: path.dirname(pkgRoot),
16931694
packageName,

0 commit comments

Comments
 (0)