Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/remove/cmd.ts
12925 views
1
/*
2
* cmd.ts
3
*
4
* Copyright (C) 2021-2022 Posit Software, PBC
5
*/
6
7
import { Command } from "cliffy/command/mod.ts";
8
import { Checkbox } from "cliffy/prompt/mod.ts";
9
import { initYamlIntelligenceResourcesFromFilesystem } from "../../core/schema/utils.ts";
10
import { createTempContext } from "../../core/temp.ts";
11
12
import { info } from "../../deno_ral/log.ts";
13
import { removeExtension } from "../../extension/remove.ts";
14
import { createExtensionContext } from "../../extension/extension.ts";
15
import { extensionIdString } from "../../extension/extension-shared.ts";
16
import { Extension } from "../../extension/types.ts";
17
import { projectContext } from "../../project/project-context.ts";
18
import {
19
afterConfirm,
20
loadTools,
21
removeTool,
22
selectTool,
23
} from "../../tools/tools-console.ts";
24
import { installableTools } from "../../tools/tools.ts";
25
import { notebookContext } from "../../render/notebook/notebook-context.ts";
26
import { signalCommandFailure } from "../utils.ts";
27
28
export const removeCommand = new Command()
29
.name("remove")
30
.arguments("[target...]")
31
.option(
32
"--no-prompt",
33
"Do not prompt to confirm actions",
34
)
35
.option(
36
"--embed <extensionId>",
37
"Remove this extension from within another extension (used when authoring extensions).",
38
)
39
.option(
40
"--update-path",
41
"Update system path when a tool is installed",
42
{
43
hidden: true,
44
},
45
)
46
.description(
47
"Removes an extension.",
48
)
49
.example(
50
"Remove extension using name",
51
"quarto remove <extension-name>",
52
)
53
.action(
54
async (
55
options: { prompt?: boolean; embed?: string; updatePath?: boolean },
56
...target: string[]
57
) => {
58
await initYamlIntelligenceResourcesFromFilesystem();
59
const temp = createTempContext();
60
const extensionContext = createExtensionContext();
61
62
// -- update path
63
try {
64
const resolved = resolveCompatibleArgs(target || [], "extension");
65
if (resolved.action === "tool") {
66
if (resolved.name) {
67
// Explicitly provided
68
await removeTool(resolved.name, options.prompt, options.updatePath);
69
} else {
70
// Not provided, give the user a list to choose from
71
const allTools = await loadTools();
72
if (allTools.filter((tool) => tool.installed).length === 0) {
73
info("No tools are installed.");
74
signalCommandFailure();
75
} else {
76
// Select which tool should be installed
77
const toolTarget = await selectTool(allTools, "remove");
78
if (toolTarget) {
79
info("");
80
await removeTool(toolTarget);
81
}
82
}
83
}
84
} else {
85
// Not provided, give the user a list to select from
86
const workingDir = Deno.cwd();
87
88
const resolveTargetDir = async () => {
89
if (options.embed) {
90
// We're removing an embedded extension, lookup the extension
91
// and use its path
92
const context = createExtensionContext();
93
const extension = await context.extension(
94
options.embed,
95
workingDir,
96
);
97
if (extension) {
98
return extension?.path;
99
} else {
100
throw new Error(`Unable to find extension '${options.embed}.`);
101
}
102
} else {
103
// Just use the current directory
104
return workingDir;
105
}
106
};
107
const targetDir = await resolveTargetDir();
108
109
// Process extension
110
if (resolved.name) {
111
// explicitly provided
112
const extensions = await extensionContext.find(
113
resolved.name,
114
targetDir,
115
undefined,
116
undefined,
117
undefined,
118
{ builtIn: false },
119
);
120
if (extensions.length > 0) {
121
await removeExtensions(extensions.slice(), options.prompt);
122
} else {
123
info("No matching extension found.");
124
signalCommandFailure();
125
}
126
} else {
127
const nbContext = notebookContext();
128
// Provide the with with a list
129
const project = await projectContext(targetDir, nbContext);
130
const extensions = await extensionContext.extensions(
131
targetDir,
132
project?.config,
133
project?.dir,
134
{ builtIn: false },
135
);
136
137
// Show a list
138
if (extensions.length > 0) {
139
const extensionsToRemove = await selectExtensions(extensions);
140
if (extensionsToRemove.length > 0) {
141
await removeExtensions(extensionsToRemove);
142
}
143
} else {
144
info("No extensions installed.");
145
signalCommandFailure();
146
}
147
}
148
}
149
} finally {
150
temp.cleanup();
151
}
152
},
153
);
154
155
// note that we're using variadic arguments here to preserve backware compatibility.
156
export const resolveCompatibleArgs = (
157
args: string[],
158
defaultAction: "extension" | "tool",
159
): {
160
action: string;
161
name?: string;
162
} => {
163
if (args.length === 1) {
164
// tool
165
// extension
166
// quarto-ext/lightbox
167
const extname = args[0];
168
if (extname === "tool") {
169
return {
170
action: "tool",
171
};
172
} else if (extname === "extension") {
173
return {
174
action: "extension",
175
};
176
} else if (installableTools().includes(extname.toLowerCase())) {
177
return {
178
action: "tool",
179
name: args[0],
180
};
181
} else {
182
return {
183
action: defaultAction,
184
name: args[0],
185
};
186
}
187
} else if (args.length > 1) {
188
// tool chromium
189
// tool tinytex
190
// extension quarto-ext/lightbox
191
const action = args[0];
192
const name = args[1];
193
194
if (action === "tool") {
195
return {
196
action,
197
name,
198
};
199
} else if (action === "extension") {
200
return {
201
action: "extension",
202
name,
203
};
204
} else {
205
return {
206
action: defaultAction,
207
name,
208
};
209
}
210
} else {
211
return {
212
action: defaultAction,
213
};
214
}
215
};
216
217
function removeExtensions(extensions: Extension[], prompt?: boolean) {
218
const removeOneExtension = async (extension: Extension) => {
219
// Exactly one extension
220
return await afterConfirm(
221
`Are you sure you'd like to remove ${extension.title}?`,
222
async () => {
223
await removeExtension(extension);
224
info("Extension removed.");
225
},
226
prompt,
227
);
228
};
229
230
const removeMultipleExtensions = async (extensions: Extension[]) => {
231
return await afterConfirm(
232
`Are you sure you'd like to remove ${extensions.length} ${
233
extensions.length === 1 ? "extension" : "extensions"
234
}?`,
235
async () => {
236
for (const extensionToRemove of extensions) {
237
await removeExtension(extensionToRemove);
238
}
239
info(
240
`${extensions.length} ${
241
extensions.length === 1 ? "extension" : "extensions"
242
} removed.`,
243
);
244
},
245
prompt,
246
);
247
};
248
249
info("");
250
if (extensions.length === 1) {
251
return removeOneExtension(extensions[0]);
252
} else {
253
return removeMultipleExtensions(extensions);
254
}
255
}
256
257
async function selectExtensions(extensions: Extension[]) {
258
const sorted = extensions.sort((ext1, ext2) => {
259
const orgSort = (ext1.id.organization || "").localeCompare(
260
ext2.id.organization || "",
261
);
262
if (orgSort !== 0) {
263
return orgSort;
264
} else {
265
return ext1.title.localeCompare(ext2.title);
266
}
267
});
268
269
const extsToKeep: string[] = (await Checkbox.prompt({
270
message: "Select extension(s) to keep",
271
options: sorted.map((ext) => {
272
return {
273
name: `${ext.title}${
274
ext.id.organization ? " (" + ext.id.organization + ")" : ""
275
}`,
276
value: extensionIdString(ext.id),
277
checked: true,
278
};
279
}),
280
hint:
281
`Use the arrow keys and spacebar to specify extensions you'd like to remove.\n` +
282
" Press Enter to confirm the list of accounts you wish to remain available.",
283
})).map((x) => x.value);
284
285
return extensions.filter((extension) => {
286
return !extsToKeep.includes(extensionIdString(extension.id));
287
});
288
}
289
290