Skip to content

Commit e67ce56

Browse files
authored
Merge pull request #12484 from quarto-dev/bugfix/11788
`quarto add`, `quarto remove`: return non-zero status code on failure
2 parents fcb7761 + 20993be commit e67ce56

File tree

6 files changed

+91
-56
lines changed

6 files changed

+91
-56
lines changed

news/changelog-1.7.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ All changes included in 1.7:
164164
- ([#11441](https://github.com/quarto-dev/quarto-cli/issues/11441)): Don't add newlines around shortcodes during processing.
165165
- ([#11606](https://github.com/quarto-dev/quarto-cli/discussions/11606)): Added a new `QUARTO_DOCUMENT_FILE` env var available to computation engine to the name of the file currently being rendered.
166166
- ([#11643](https://github.com/quarto-dev/quarto-cli/issues/11643)): Improve highlighting of nested code block inside markdown code block, i.e. using ` ```{{python}} ` or ` ```python ` inside ` ````markdown` fenced code block.
167+
- ([#11788](https://github.com/quarto-dev/quarto-cli/issues/11788)): `quarto add` and `quarto remove` will return non-zero code when they fail.
167168
- ([#11803](https://github.com/quarto-dev/quarto-cli/pull/11803)): Added a new CLI command `quarto call`. First users of this interface are the new `quarto call engine julia ...` subcommands.
168169
- ([#11951](https://github.com/quarto-dev/quarto-cli/issues/11951)): Raw LaTeX table without `tbl-` prefix label for using Quarto crossref are now correctly passed through unmodified.
169170
- ([#11967](https://github.com/quarto-dev/quarto-cli/issues/11967)): Produce a better error message when YAML metadata with `!expr` tags are used outside of `knitr` code cells.

src/command/add/cmd.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createTempContext } from "../../core/temp.ts";
99
import { installExtension } from "../../extension/install.ts";
1010

1111
import { info } from "../../deno_ral/log.ts";
12+
import { signalCommandFailure } from "../utils.ts";
1213

1314
export const addCommand = new Command()
1415
.name("add")
@@ -44,16 +45,19 @@ export const addCommand = new Command()
4445
await initYamlIntelligenceResourcesFromFilesystem();
4546
const temp = createTempContext();
4647
try {
47-
// Install an extension
48-
if (extension) {
49-
await installExtension(
50-
extension,
51-
temp,
52-
options.prompt !== false,
53-
options.embed,
54-
);
55-
} else {
48+
if (!extension) {
5649
info("Please provide an extension name, url, or path.");
50+
signalCommandFailure();
51+
}
52+
// Install an extension
53+
const result = await installExtension(
54+
extension,
55+
temp,
56+
options.prompt !== false,
57+
options.embed,
58+
);
59+
if (!result) {
60+
signalCommandFailure();
5761
}
5862
} finally {
5963
temp.cleanup();

src/command/remove/cmd.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
selectTool,
2323
} from "../../tools/tools-console.ts";
2424
import { notebookContext } from "../../render/notebook/notebook-context.ts";
25+
import { signalCommandFailure } from "../utils.ts";
2526

2627
export const removeCommand = new Command()
2728
.name("remove")
@@ -69,6 +70,7 @@ export const removeCommand = new Command()
6970
const allTools = await loadTools();
7071
if (allTools.filter((tool) => tool.installed).length === 0) {
7172
info("No tools are installed.");
73+
signalCommandFailure();
7274
} else {
7375
// Select which tool should be installed
7476
const toolTarget = await selectTool(allTools, "remove");
@@ -118,6 +120,7 @@ export const removeCommand = new Command()
118120
await removeExtensions(extensions.slice(), options.prompt);
119121
} else {
120122
info("No matching extension found.");
123+
signalCommandFailure();
121124
}
122125
} else {
123126
const nbContext = notebookContext();
@@ -138,6 +141,7 @@ export const removeCommand = new Command()
138141
}
139142
} else {
140143
info("No extensions installed.");
144+
signalCommandFailure();
141145
}
142146
}
143147
}

src/command/utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* utils.ts
3+
*
4+
* Copyright (C) 2025 Posit Software, PBC
5+
*/
6+
7+
let someCommandFailed = false;
8+
9+
// we do this the roundabout way because there doesn't seem to be any clean way
10+
// for cliffy commands to return values? Likely a skill issue on my part
11+
export const signalCommandFailure = () => {
12+
someCommandFailed = true;
13+
};
14+
15+
export const commandFailed = () => {
16+
return someCommandFailed;
17+
};

src/extension/install.ts

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export async function installExtension(
3434
temp: TempContext,
3535
allowPrompt: boolean,
3636
embed?: string,
37-
) {
37+
): Promise<boolean> {
3838
// Is this local or remote?
3939
const source = await extensionSource(target);
4040

@@ -43,68 +43,70 @@ export async function installExtension(
4343
info(
4444
`Extension not found in local or remote sources`,
4545
);
46-
return;
46+
return false;
4747
}
4848

4949
// Does the user trust the extension?
5050
const trusted = await isTrusted(source, allowPrompt);
5151
if (!trusted) {
5252
// Not trusted, cancel
5353
cancelInstallation();
54-
} else {
55-
// Compute the installation directory
56-
const currentDir = Deno.cwd();
57-
const installDir = await determineInstallDir(
58-
currentDir,
59-
allowPrompt,
60-
embed,
61-
);
54+
return false;
55+
}
6256

63-
// Stage the extension locally
64-
const extensionDir = await stageExtension(source, temp.createDir());
57+
// Compute the installation directory
58+
const currentDir = Deno.cwd();
59+
const installDir = await determineInstallDir(
60+
currentDir,
61+
allowPrompt,
62+
embed,
63+
);
6564

66-
// Validate the extension in in the staging dir
67-
const stagedExtensions = await validateExtension(extensionDir);
65+
// Stage the extension locally
66+
const extensionDir = await stageExtension(source, temp.createDir());
6867

69-
// Confirm that the user would like to take this action
70-
const confirmed = await confirmInstallation(
71-
stagedExtensions,
72-
installDir,
73-
{ allowPrompt },
74-
);
68+
// Validate the extension in in the staging dir
69+
const stagedExtensions = await validateExtension(extensionDir);
7570

76-
if (confirmed) {
77-
// Complete the installation
78-
await completeInstallation(extensionDir, installDir);
71+
// Confirm that the user would like to take this action
72+
const confirmed = await confirmInstallation(
73+
stagedExtensions,
74+
installDir,
75+
{ allowPrompt },
76+
);
7977

80-
await withSpinner(
81-
{ message: "Extension installation complete" },
82-
() => {
83-
return Promise.resolve();
84-
},
85-
);
78+
if (!confirmed) {
79+
// Not confirmed, cancel the installation
80+
cancelInstallation();
81+
}
8682

87-
if (source.learnMoreUrl) {
88-
info("");
89-
if (allowPrompt) {
90-
const open = await Confirm.prompt({
91-
message: "View documentation using default browser?",
92-
default: true,
93-
});
94-
if (open) {
95-
await openUrl(source.learnMoreUrl);
96-
}
97-
} else {
98-
info(
99-
`\nLearn more about this extension at:\n${source.learnMoreUrl}\n`,
100-
);
101-
}
83+
// Complete the installation
84+
await completeInstallation(extensionDir, installDir);
85+
86+
await withSpinner(
87+
{ message: "Extension installation complete" },
88+
() => {
89+
return Promise.resolve();
90+
},
91+
);
92+
93+
if (source.learnMoreUrl) {
94+
info("");
95+
if (allowPrompt) {
96+
const open = await Confirm.prompt({
97+
message: "View documentation using default browser?",
98+
default: true,
99+
});
100+
if (open) {
101+
await openUrl(source.learnMoreUrl);
102102
}
103103
} else {
104-
// Not confirmed, cancel the installation
105-
cancelInstallation();
104+
info(
105+
`\nLearn more about this extension at:\n${source.learnMoreUrl}\n`,
106+
);
106107
}
107108
}
109+
return true;
108110
}
109111

110112
// Cancels the installation, providing user feedback that the installation is canceled

src/quarto.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { typstBinaryPath } from "./core/typst.ts";
3636
import { exitWithCleanup, onCleanup } from "./core/cleanup.ts";
3737

3838
import { runScript } from "./command/run/run.ts";
39+
import { commandFailed } from "./command/utils.ts";
3940

4041
// ensures run handlers are registered
4142
import "./core/run/register.ts";
@@ -51,7 +52,6 @@ import "./format/imports.ts";
5152

5253
import { kCliffyImplicitCwd } from "./config/constants.ts";
5354
import { mainRunner } from "./core/main.ts";
54-
import { engineCommand } from "./execute/engine.ts";
5555

5656
const checkVersionRequirement = () => {
5757
const versionReq = Deno.env.get("QUARTO_VERSION_REQUIREMENT");
@@ -196,6 +196,9 @@ export async function quarto(
196196
Deno.env.set(key, value);
197197
}
198198
}
199+
if (commandFailed()) {
200+
exitWithCleanup(1);
201+
}
199202
} catch (e) {
200203
if (e instanceof CommandError) {
201204
logError(e, false);
@@ -221,5 +224,9 @@ if (import.meta.main) {
221224
cmd = appendLogOptions(cmd);
222225
return appendProfileArg(cmd);
223226
});
227+
228+
if (commandFailed()) {
229+
exitWithCleanup(1);
230+
}
224231
});
225232
}

0 commit comments

Comments
 (0)