Repository navigation
feat(extensions): interactive multi-tab /extensions manager (Installed / Discover / Sources) #4850
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 49 commits
3f7cbda
a63e37a
86c760b
d1f8e98
54c4f9f
d5bf144
d47d32a
99d26a4
73d9b58
cff6ad4
23f5dc1
dde9c59
3f891f9
aed6a93
6dcfb3a
229de3a
2c49f01
6c32c09
59f850c
c83dd43
a5a70df
0eacf78
d8919ed
1abc5f8
a52a5cd
b98100c
bf46992
c308b6e
fcdb7c1
e24709d
c5e49d5
12eb6e1
e275ce2
baeaf69
05eb44f
811e61b
a8b745e
34f8252
b050fe0
1f302ac
7199a17
cd5a11e
3e5f7a5
74a2d0e
e08694f
61a69e2
a7b2df2
b5fa71a
a2d63d7
631e271
158002c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,11 +9,12 @@ import type { CommandModule } from 'yargs'; | |
| import { | ||
| ExtensionManager, | ||
| parseInstallSource, | ||
| type ExtensionScope, | ||
| } from '@qwen-code/qwen-code-core'; | ||
| import { getErrorMessage } from '../../utils/errors.js'; | ||
| import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; | ||
| import { isWorkspaceTrusted } from '../../config/trustedFolders.js'; | ||
| import { loadSettings } from '../../config/settings.js'; | ||
| import { loadSettings, SettingScope } from '../../config/settings.js'; | ||
| import { | ||
| requestConsentOrFail, | ||
| requestConsentNonInteractive, | ||
|
|
@@ -28,6 +29,12 @@ interface InstallArgs { | |
| allowPreRelease?: boolean; | ||
| consent?: boolean; | ||
| registry?: string; | ||
| scope?: string; | ||
| } | ||
|
|
||
| // "workspace" is accepted as an alias of "project" to match enable/disable. | ||
| function normalizeScope(scope: string | undefined): ExtensionScope { | ||
| return scope === 'project' || scope === 'workspace' ? 'project' : 'user'; | ||
| } | ||
|
|
||
| export async function handleInstall(args: InstallArgs) { | ||
|
|
@@ -87,10 +94,51 @@ export async function handleInstall(args: InstallArgs) { | |
| }, | ||
| requestConsent, | ||
| ); | ||
| const scope = normalizeScope(args.scope); | ||
| if (args.scope) { | ||
| extensionManager.setExtensionScope(extension.name, scope); | ||
| // installExtension auto-enables at the user (global) scope. For a | ||
| // project-scoped install, re-scope enablement to this workspace only. | ||
| if (scope === 'project') { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in e08694f for the CLI I scoped this to |
||
| await extensionManager.disableExtension( | ||
| extension.name, | ||
| SettingScope.User, | ||
| ); | ||
| try { | ||
| await extensionManager.enableExtension( | ||
| extension.name, | ||
| SettingScope.Workspace, | ||
| ); | ||
| } catch (enableError) { | ||
| // The User-scope disable already landed. If the Workspace enable | ||
| // fails, the extension would be left disabled everywhere — roll the | ||
| // User enable back so it isn't silently dead, then surface the error. | ||
| try { | ||
| await extensionManager.enableExtension( | ||
| extension.name, | ||
| SettingScope.User, | ||
| ); | ||
| } catch (rollbackError) { | ||
| // Rollback failed too: the extension is now disabled at every | ||
| // scope. Surface this so the user knows recovery also failed, | ||
| // before the original error is reported below. | ||
| writeStderrLine( | ||
| `Warning: failed to roll back the scope change for "${extension.name}"; it may be disabled at all scopes: ${getErrorMessage(rollbackError)}`, | ||
| ); | ||
| } | ||
| throw enableError; | ||
| } | ||
| } | ||
| } | ||
| writeStdoutLine( | ||
| t('Extension "{{name}}" installed successfully and enabled.', { | ||
| name: extension.name, | ||
| }), | ||
| scope === 'project' | ||
| ? t( | ||
| 'Extension "{{name}}" installed successfully and enabled for the current workspace.', | ||
| { name: extension.name }, | ||
| ) | ||
| : t('Extension "{{name}}" installed successfully and enabled.', { | ||
| name: extension.name, | ||
| }), | ||
| ); | ||
| } catch (error) { | ||
| writeStderrLine(getErrorMessage(error)); | ||
|
|
@@ -135,6 +183,13 @@ export const installCommand: CommandModule = { | |
| type: 'boolean', | ||
| default: false, | ||
| }) | ||
| .option('scope', { | ||
| describe: t( | ||
| 'The scope to install the extension in: "user" (global, default) or "project" (current workspace only).', | ||
| ), | ||
| type: 'string', | ||
| choices: ['user', 'project', 'workspace'], | ||
| }) | ||
| .check((argv) => { | ||
| if (!argv.source) { | ||
| throw new Error(t('The source argument must be provided.')); | ||
|
|
@@ -149,6 +204,7 @@ export const installCommand: CommandModule = { | |
| allowPreRelease: argv['pre-release'] as boolean | undefined, | ||
| consent: argv['consent'] as boolean | undefined, | ||
| registry: argv['registry'] as string | undefined, | ||
| scope: argv['scope'] as string | undefined, | ||
| }); | ||
| }, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in
631e271fb. MovedsetExtensionScope(extension.name, scope)to after the enable/disable block. On a Workspace-enable failure the rollback re-enables User scope and the scope preference is never written (defaults to user), so the Installed tab no longer mislabels it as "Project level". Same reorder applied to the two UI install paths.