import { existsSync } from "../../deno_ral/fs.ts";
import { Command } from "cliffy/command/mod.ts";
import { Select } from "cliffy/prompt/select.ts";
import { prompt } from "cliffy/prompt/mod.ts";
import { findProvider } from "../../publish/provider.ts";
import { AccountToken, PublishProvider } from "../../publish/provider-types.ts";
import { publishProviders } from "../../publish/provider.ts";
import { initYamlIntelligenceResourcesFromFilesystem } from "../../core/schema/utils.ts";
import {
initState,
setInitializer,
} from "../../core/lib/yaml-validation/state.ts";
import { projectContext } from "../../project/project-context.ts";
import {
projectIsManuscript,
projectIsWebsite,
} from "../../project/project-shared.ts";
import { PublishCommandOptions } from "./options.ts";
import { resolveDeployment } from "./deployment.ts";
import { AccountPrompt, manageAccounts, resolveAccount } from "./account.ts";
import { PublishOptions, PublishRecord } from "../../publish/types.ts";
import { isInteractiveTerminal, isServerSession } from "../../core/platform.ts";
import { runningInCI } from "../../core/ci-info.ts";
import { ProjectContext } from "../../project/types.ts";
import { openUrl } from "../../core/shell.ts";
import { publishDocument, publishSite } from "../../publish/publish.ts";
import { handleUnauthorized } from "../../publish/account.ts";
import { notebookContext } from "../../render/notebook/notebook-context.ts";
import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts";
export const publishCommand =
new Command<any>()
.name("publish")
.description(
"Publish a document or project to a provider.\n\nAvailable providers include:\n\n" +
" - Quarto Pub (quarto-pub)\n" +
" - GitHub Pages (gh-pages)\n" +
" - Posit Connect (connect)\n" +
" - Netlify (netlify)\n" +
" - Confluence (confluence)\n" +
" - Hugging Face Spaces (huggingface)\n\n" +
"Accounts are configured interactively during publishing.\n" +
"Manage/remove accounts with: quarto publish accounts",
)
.arguments("[provider] [path]")
.option(
"--id <id:string>",
"Identifier of content to publish",
)
.option(
"--server <server:string>",
"Server to publish to",
)
.option(
"--token <token:string>",
"Access token for publising provider",
)
.option(
"--no-render",
"Do not render before publishing.",
)
.option(
"--no-prompt",
"Do not prompt to confirm publishing destination",
)
.option(
"--no-browser",
"Do not open a browser to the site after publishing",
)
.example(
"Publish project (prompt for provider)",
"quarto publish",
)
.example(
"Publish document (prompt for provider)",
"quarto publish document.qmd",
)
.example(
"Publish project to Hugging Face Spaces",
"quarto publish huggingface",
)
.example(
"Publish project to Netlify",
"quarto publish netlify",
)
.example(
"Publish with explicit target",
"quarto publish netlify --id DA36416-F950-4647-815C-01A24233E294",
)
.example(
"Publish project to GitHub Pages",
"quarto publish gh-pages",
)
.example(
"Publish project to Posit Connect",
"quarto publish connect",
)
.example(
"Publish with explicit credentials",
"quarto publish connect --server example.com --token 01A24233E294",
)
.example(
"Publish without confirmation prompt",
"quarto publish --no-prompt",
)
.example(
"Publish without rendering",
"quarto publish --no-render",
)
.example(
"Publish without opening browser",
"quarto publish --no-browser",
)
.example(
"Manage/remove publishing accounts",
"quarto publish accounts",
)
.action(
async (
options: PublishCommandOptions,
provider?: string,
path?: string,
) => {
if (provider && !path && existsSync(provider)) {
path = provider;
provider = undefined;
}
if (provider === "accounts") {
await manageAccounts();
} else {
let providerInterface: PublishProvider | undefined;
if (provider) {
providerInterface = findProvider(provider);
if (!providerInterface) {
throw new Error(`Publishing source '${provider}' not found`);
}
}
await publishAction(options, providerInterface, path);
}
},
);
async function publishAction(
options: PublishCommandOptions,
provider?: PublishProvider,
path?: string,
) {
await initYamlIntelligence();
const publishOptions = await createPublishOptions(options, provider, path);
const doPublish = async (
publishProvider: PublishProvider,
accountPrompt: AccountPrompt,
publishTarget?: PublishRecord,
account?: AccountToken,
) => {
if (publishProvider.requiresRender && publishOptions.render === false) {
throw new Error(
`${publishProvider.description} requires rendering before publish.`,
);
}
account = (account && !publishOptions.prompt)
? account
: await resolveAccount(
publishProvider,
publishOptions.prompt ? accountPrompt : "never",
publishOptions,
account,
publishTarget,
);
if (account) {
await publish(
publishProvider,
account,
publishOptions,
publishTarget,
);
}
};
const deployment = (provider && publishOptions.id)
? {
provider,
target: {
id: publishOptions.id,
},
}
: await resolveDeployment(
publishOptions,
provider?.name,
);
provider = deployment?.provider || provider;
if (deployment) {
await doPublish(
deployment.provider,
deployment.account ? "multiple" : "always",
deployment.target,
deployment.account,
);
} else if (publishOptions.prompt) {
const providers = publishProviders();
if (!provider) {
const result = await prompt([{
indent: "",
name: "provider",
message: "Provider:",
options: providers
.filter((provider) => !provider.hidden)
.map((provider) => ({
name: provider.description,
value: provider.name,
})),
type: Select,
}]);
if (result.provider) {
provider = findProvider(result.provider);
}
}
if (provider) {
await doPublish(provider, "always");
}
} else {
throw new Error(
"No re-publishing target found (--no-prompt requires an existing 'publish' config to update)",
);
}
}
async function publish(
provider: PublishProvider,
account: AccountToken,
options: PublishOptions,
target?: PublishRecord,
): Promise<void> {
try {
const siteUrl = typeof (options.input) !== "string"
? await publishSite(
options.input,
provider,
account,
options,
target,
)
: await publishDocument(
options.input,
provider,
account,
options,
target,
);
if (siteUrl && options.browser) {
await openUrl(siteUrl.toString());
}
} catch (err) {
if (!(err instanceof Error)) {
throw err;
}
if (!(provider.isUnauthorized(err) && options.prompt)) {
throw err;
}
if (await handleUnauthorized(provider, account)) {
const authorizedAccount = await provider.authorizeToken(
options,
target,
);
if (authorizedAccount) {
return await publish(provider, authorizedAccount, options, target);
}
}
}
}
async function createPublishOptions(
options: PublishCommandOptions,
provider?: PublishProvider,
path?: string,
): Promise<PublishOptions> {
const nbContext = notebookContext();
path = path || Deno.cwd();
if (!existsSync(path)) {
throw new Error(
`The specified path (${path}) does not exist so cannot be published.`,
);
}
let input: ProjectContext | string | undefined;
if (provider && provider.resolveProjectPath) {
const resolvedPath = provider.resolveProjectPath(path);
try {
if (Deno.statSync(resolvedPath).isDirectory) {
path = resolvedPath;
}
} catch (_e) {
}
}
const project = (await projectContext(path, nbContext)) ||
(await singleFileProjectContext(path, nbContext));
if (Deno.statSync(path).isDirectory) {
if (projectIsWebsite(project)) {
input = project;
} else if (
projectIsManuscript(project) && project.files.input.length > 0
) {
input = project;
} else if (project.files.input.length === 1) {
input = project.files.input[0];
} else {
throw new Error(
`The specified path (${path}) is not a website, manuscript or book project so cannot be published.`,
);
}
}
else {
if (project && projectIsWebsite(project)) {
throw new Error(
`The specified path (${path}) is within a website or book project so cannot be published individually`,
);
}
input = path;
}
const interactive = isInteractiveTerminal() && !runningInCI() && !options.id;
return {
input,
server: options.server || null,
token: options.token,
id: options.id,
render: !!options.render,
prompt: !!options.prompt && interactive,
browser: !!options.browser && interactive && !isServerSession(),
};
}
async function initYamlIntelligence() {
setInitializer(initYamlIntelligenceResourcesFromFilesystem);
await initState();
}