Skip to content

Commit 5cea25b

Browse files
committed
Merge remote-tracking branch 'origin/main' into feature/publish-ghpages
2 parents 6c4fb15 + 3f1b245 commit 5cea25b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1119
-5673
lines changed

src/command/command.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { inspectCommand } from "./inspect/cmd.ts";
2121
import { buildJsCommand } from "./build-js/cmd.ts";
2222
import { installCommand } from "./install/cmd.ts";
2323
import { publishCommand } from "./publish/cmd.ts";
24+
import { removeCommand } from "./remove/cmd.ts";
2425

2526
// deno-lint-ignore no-explicit-any
2627
export function commands(): Command<any>[] {
@@ -33,6 +34,7 @@ export function commands(): Command<any>[] {
3334
pandocCommand,
3435
runCommand,
3536
installCommand,
37+
removeCommand,
3638
publishCommand,
3739
capabilitiesCommand,
3840
inspectCommand,

src/command/install/cmd.ts

Lines changed: 103 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,124 @@
55
*
66
*/
77
import { Command } from "cliffy/command/mod.ts";
8+
import { Select } from "cliffy/prompt/select.ts";
89
import { initYamlIntelligenceResourcesFromFilesystem } from "../../core/schema/utils.ts";
910
import { createTempContext } from "../../core/temp.ts";
1011
import { installExtension } from "../../extension/install.ts";
12+
import {
13+
allTools,
14+
installableTool,
15+
installableTools,
16+
installTool,
17+
toolSummary,
18+
} from "../tools/tools.ts";
19+
20+
import { info } from "log/mod.ts";
21+
import { withSpinner } from "../../core/console.ts";
22+
import { InstallableTool } from "../tools/types.ts";
1123

1224
export const installCommand = new Command()
13-
.hidden() // TODO: unhide when ready
25+
.hidden()
1426
.name("install")
1527
.arguments("[target:string]")
28+
.arguments("<type:string> [target:string]")
29+
.option(
30+
"--no-prompt",
31+
"Do not prompt to confirm actions during installation",
32+
)
1633
.description(
17-
"Installs a Quarto Extension into the current directory or Project directory.",
34+
"Installs an extension or global dependency.",
1835
)
1936
.example(
20-
"Install extension from file",
21-
"quarto install /Users/catmemes/tools/my-extension.tar.gz",
37+
"Install extension from Github",
38+
"quarto install extension <gh-organization>/<gh-repo>",
2239
)
2340
.example(
24-
"Install extension from folder",
25-
"quarto install /Users/catmemes/tools/my-extension/",
41+
"Install extension from file",
42+
"quarto install extension tools/my-extension.tar.gz",
2643
)
2744
.example(
2845
"Install extension from url",
29-
"quarto install https://github.com/quarto-dev/quarto-extensions/releases/download/latest/my-extension.tar.gz",
46+
"quarto install extension <url>",
47+
)
48+
.example(
49+
"Install TinyTeX",
50+
"quarto install tool tinytex",
51+
)
52+
.example(
53+
"Install Chromium",
54+
"quarto install tool chromium",
3055
)
31-
.action(async (_options: unknown, target?: string) => {
32-
await initYamlIntelligenceResourcesFromFilesystem();
33-
const temp = createTempContext();
34-
try {
35-
if (target) {
36-
await installExtension(target, temp);
56+
.action(
57+
async (options: { prompt?: boolean }, type: string, target?: string) => {
58+
await initYamlIntelligenceResourcesFromFilesystem();
59+
const temp = createTempContext();
60+
try {
61+
if (type.toLowerCase() === "extension") {
62+
// Install an extension
63+
if (target) {
64+
await installExtension(target, temp, options.prompt !== false);
65+
} else {
66+
info("Please provide an extension name, url, or path.");
67+
}
68+
} else if (type.toLowerCase() === "tool") {
69+
// Install a tool
70+
if (target) {
71+
// Use the tool name
72+
await installTool(target);
73+
} else {
74+
// Present a list of tools
75+
const toolsToInstall = await notInstalledTools();
76+
if (toolsToInstall.length === 0) {
77+
info("All tools already installed.");
78+
const summaries = [];
79+
for (const tool of installableTools()) {
80+
const summary = await toolSummary(tool);
81+
summaries.push(summary);
82+
}
83+
} else {
84+
const toolsWithSummary = [];
85+
for (const tool of toolsToInstall) {
86+
const summary = await toolSummary(tool.name);
87+
toolsWithSummary.push({ tool, summary });
88+
}
89+
90+
const toolTarget: string = await Select.prompt({
91+
message: "Select a tool to install",
92+
options: toolsWithSummary.map((toolWithSummary) => {
93+
return {
94+
name: `${toolWithSummary.tool.name}${
95+
toolWithSummary.summary?.latestRelease.version
96+
? " (" +
97+
toolWithSummary.summary?.latestRelease.version + ")"
98+
: ""
99+
}`,
100+
value: toolWithSummary.tool.name,
101+
};
102+
}),
103+
});
104+
if (toolTarget) {
105+
await installTool(toolTarget);
106+
}
107+
}
108+
}
109+
} else {
110+
// This is an unrecognized type option
111+
info(
112+
`Unrecognized option '${type}' - please choose 'tool' or 'extension'.`,
113+
);
114+
}
115+
} finally {
116+
temp.cleanup();
37117
}
38-
} finally {
39-
temp.cleanup();
40-
}
118+
},
119+
);
120+
121+
async function notInstalledTools() {
122+
const toolsToInstall: InstallableTool[] = [];
123+
await withSpinner({ message: "Inspecting tools" }, async () => {
124+
const all = await allTools();
125+
toolsToInstall.push(...all.notInstalled);
41126
});
127+
return toolsToInstall;
128+
}

src/command/remove/cmd.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* cmd.ts
3+
*
4+
* Copyright (C) 2021 by RStudio, PBC
5+
*
6+
*/
7+
import { Command } from "cliffy/command/mod.ts";
8+
import { Checkbox, Confirm, Select } from "cliffy/prompt/mod.ts";
9+
import { Table } from "cliffy/table/mod.ts";
10+
import { initYamlIntelligenceResourcesFromFilesystem } from "../../core/schema/utils.ts";
11+
import { createTempContext } from "../../core/temp.ts";
12+
import { allTools, uninstallTool } from "../tools/tools.ts";
13+
14+
import { info } from "log/mod.ts";
15+
import { removeExtension } from "../../extension/remove.ts";
16+
import { withSpinner } from "../../core/console.ts";
17+
import { InstallableTool } from "../tools/types.ts";
18+
import { createExtensionContext } from "../../extension/extension.ts";
19+
import {
20+
Extension,
21+
extensionIdString,
22+
} from "../../extension/extension-shared.ts";
23+
import { projectContext } from "../../project/project-context.ts";
24+
25+
export const removeCommand = new Command()
26+
.hidden()
27+
.name("remove")
28+
.arguments("[target:string]")
29+
.arguments("<type:string> [target:string]")
30+
.option(
31+
"--no-prompt",
32+
"Do not prompt to confirm actions during installation",
33+
)
34+
.description(
35+
"Removes an extension or global dependency.",
36+
)
37+
.example(
38+
"Remove extension using name",
39+
"quarto remove extension <extension-name>",
40+
)
41+
.example(
42+
"Remove TinyTeX",
43+
"quarto remove tool tinytex",
44+
)
45+
.example(
46+
"Remove Chromium",
47+
"quarto remove tool chromium",
48+
)
49+
.action(
50+
async (_options: { prompt?: boolean }, type: string, target?: string) => {
51+
await initYamlIntelligenceResourcesFromFilesystem();
52+
const temp = createTempContext();
53+
const extensionContext = createExtensionContext();
54+
55+
try {
56+
if (type.toLowerCase() === "extension") {
57+
// Not provided, give the user a list to select from
58+
const targetDir = Deno.cwd();
59+
60+
// Process extension
61+
if (target) {
62+
// explicitly provided
63+
const extensions = extensionContext.find(target, targetDir);
64+
if (extensions.length > 0) {
65+
await removeExtensions(extensions.slice());
66+
} else {
67+
info("No matching extension found.");
68+
}
69+
} else {
70+
// Provide the with with a list
71+
const project = await projectContext(targetDir);
72+
const extensions = extensionContext.extensions(targetDir, project);
73+
74+
// Show a list
75+
if (extensions.length > 0) {
76+
const extensionsToRemove = await selectExtensions(extensions);
77+
await removeExtensions(extensionsToRemove);
78+
} else {
79+
info("No extensions installed.");
80+
}
81+
}
82+
} else if (type.toLowerCase() === "tool") {
83+
// Process tool
84+
if (target) {
85+
// Explicitly provided
86+
await confirmAction(
87+
`Are you sure you'd like to remove ${target}?`,
88+
() => {
89+
return uninstallTool(target);
90+
},
91+
);
92+
} else {
93+
// Not provided, give the user a list to choose from
94+
const tools = await installedTools();
95+
if (tools.length === 0) {
96+
info("No tools are installed.");
97+
} else {
98+
// Select which tool should be installed
99+
const toolTarget = await selectTool(tools);
100+
if (toolTarget) {
101+
await uninstallTool(toolTarget);
102+
}
103+
}
104+
}
105+
} else {
106+
// This is an unrecognized type option
107+
info(
108+
`Unrecognized option '${type}' - please choose 'tool' or 'extension'.`,
109+
);
110+
}
111+
} finally {
112+
temp.cleanup();
113+
}
114+
},
115+
);
116+
117+
function removeExtensions(extensions: Extension[]) {
118+
const removeOneExtension = async (extension: Extension) => {
119+
// Exactly one extension
120+
return await confirmAction(
121+
`Are you sure you'd like to remove ${extension.title}?`,
122+
() => {
123+
return removeExtension(extension);
124+
},
125+
);
126+
};
127+
128+
const removeMultipleExtensions = async (extensions: Extension[]) => {
129+
return await confirmAction(
130+
`Are you sure you'd like to remove ${extensions.length} extensions?`,
131+
async () => {
132+
for (const extensionToRemove of extensions) {
133+
await removeExtension(extensionToRemove);
134+
}
135+
},
136+
);
137+
};
138+
139+
if (extensions.length === 1) {
140+
return removeOneExtension(extensions[1]);
141+
} else {
142+
return removeMultipleExtensions(extensions);
143+
}
144+
}
145+
146+
async function confirmAction(message: string, fn: () => Promise<void>) {
147+
const confirmed: boolean = await Confirm.prompt(message);
148+
if (confirmed) {
149+
return fn();
150+
}
151+
}
152+
153+
async function selectTool(tools: InstallableTool[]) {
154+
const toolInfos = [];
155+
for (const tool of tools) {
156+
const version = await tool.installedVersion();
157+
toolInfos.push({
158+
tool,
159+
version,
160+
});
161+
}
162+
163+
const toolTarget: string = await Select.prompt({
164+
message: "Select a tool to remove",
165+
options: toolInfos.map((toolInfo) => {
166+
return {
167+
name: `${toolInfo.tool.name} ${
168+
toolInfo.version ? " (" + toolInfo.version + ")" : ""
169+
}`,
170+
value: toolInfo.tool.name,
171+
};
172+
}),
173+
});
174+
return toolTarget;
175+
}
176+
177+
async function selectExtensions(extensions: Extension[]) {
178+
const sorted = extensions.sort((ext1, ext2) => {
179+
const orgSort = (ext1.id.organization || "").localeCompare(
180+
ext2.id.organization || "",
181+
);
182+
if (orgSort !== 0) {
183+
return orgSort;
184+
} else {
185+
return ext1.title.localeCompare(ext2.title);
186+
}
187+
});
188+
189+
const extsToRemove: string[] = await Checkbox.prompt({
190+
message: "Select extension(s) to remove",
191+
options: sorted.map((ext) => {
192+
return {
193+
name: `${ext.title}${
194+
ext.id.organization ? " (" + ext.id.organization + ")" : ""
195+
}`,
196+
value: extensionIdString(ext.id),
197+
};
198+
}),
199+
});
200+
201+
return extensions.filter((extension) => {
202+
return extsToRemove.includes(extensionIdString(extension.id));
203+
});
204+
}
205+
206+
async function installedTools() {
207+
const tools: InstallableTool[] = [];
208+
await withSpinner({ message: "Inspecting tools" }, async () => {
209+
const all = await allTools();
210+
tools.push(...all.installed);
211+
});
212+
return tools;
213+
}

0 commit comments

Comments
 (0)