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