Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/create/cmd.ts
6433 views
1
/*
2
* cmd.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { extensionArtifactCreator } from "./artifacts/extension.ts";
8
import { projectArtifactCreator } from "./artifacts/project.ts";
9
import { kEditorInfos, scanForEditors } from "./editor.ts";
10
11
import {
12
isInteractiveTerminal,
13
isPositWorkbench,
14
} from "../../core/platform.ts";
15
import { runningInCI } from "../../core/ci-info.ts";
16
import { initializeProjectContextAndEngines } from "../command-utils.ts";
17
18
import { Command } from "cliffy/command/mod.ts";
19
import { prompt, Select, SelectValueOptions } from "cliffy/prompt/mod.ts";
20
import { readLines } from "../../deno_ral/io.ts";
21
import { info } from "../../deno_ral/log.ts";
22
import { ArtifactCreator, CreateDirective, CreateResult } from "./cmd-types.ts";
23
24
// The registered artifact creators
25
const kArtifactCreators: ArtifactCreator[] = [
26
projectArtifactCreator,
27
extensionArtifactCreator,
28
// documentArtifactCreator, CT: Disabled for 1.2 as it arrived too late on the scene
29
];
30
31
export const createCommand = new Command()
32
.name("create")
33
.description("Create a Quarto project or extension")
34
.option(
35
"--open [editor:string]",
36
`Open new artifact in this editor (${
37
kEditorInfos.map((info) => info.id).join(", ")
38
})`,
39
)
40
.option("--no-open", "Do not open in an editor")
41
.option("--no-prompt", "Do not prompt to confirm actions")
42
.option("--json", "Pass serialized creation options via stdin", {
43
hidden: true,
44
})
45
.arguments("[type] [commands...]")
46
.action(
47
async (
48
options: {
49
prompt: boolean;
50
json?: boolean;
51
open?: string | boolean;
52
},
53
type?: string,
54
...commands: string[]
55
) => {
56
// Initialize engines before any artifact creation
57
await initializeProjectContextAndEngines();
58
59
if (options.json) {
60
await createFromStdin();
61
} else {
62
// Compute a sane default for prompting
63
const isInteractive = isInteractiveTerminal() && !runningInCI();
64
const allowPrompt = isInteractive && !!options.prompt && !options.json;
65
66
// Specific case where opening automatically in an editor is not allowed
67
if (options.open !== false && isPositWorkbench()) {
68
if (options.open !== undefined) {
69
info(
70
`The --open option is not supported in Posit Workbench - ignoring`,
71
);
72
}
73
options.open = false;
74
}
75
76
// Resolve the type into an artifact
77
const resolved = await resolveArtifact(
78
type,
79
options.prompt,
80
);
81
const resolvedArtifact = resolved.artifact;
82
if (resolvedArtifact) {
83
// Resolve the arguments that the user provided into options
84
// for the artifact provider
85
86
// If we aliased the type, shift the args (including what was
87
// the type alias in the list of args for the artifact creator
88
// to resolve)
89
const args = commands;
90
91
const commandOpts = resolvedArtifact.resolveOptions(args);
92
const createOptions = {
93
cwd: Deno.cwd(),
94
options: commandOpts,
95
};
96
97
if (allowPrompt) {
98
// Prompt the user until the options have been fully realized
99
let nextPrompt = resolvedArtifact.nextPrompt(createOptions);
100
while (nextPrompt !== undefined) {
101
if (nextPrompt) {
102
const result = await prompt([nextPrompt]);
103
createOptions.options = {
104
...createOptions.options,
105
...result,
106
};
107
}
108
nextPrompt = resolvedArtifact.nextPrompt(createOptions);
109
}
110
}
111
112
// Complete the defaults
113
const createDirective = resolvedArtifact.finalizeOptions(
114
createOptions,
115
);
116
117
// Create the artifact using the options
118
const createResult = await resolvedArtifact.createArtifact(
119
createDirective,
120
);
121
122
// Now that the article was created, offer to open the item
123
if (allowPrompt && options.open !== false) {
124
const resolvedEditor = await resolveEditor(
125
createResult,
126
typeof (options.open) === "string" ? options.open : undefined,
127
);
128
if (resolvedEditor) {
129
resolvedEditor.open();
130
}
131
}
132
}
133
}
134
},
135
);
136
137
// Resolves the artifact string (or undefined) into an
138
// Artifact interface which will provide the functions
139
// needed to complete the creation
140
const resolveArtifact = async (type?: string, prompt?: boolean) => {
141
// Finds an artifact
142
const findArtifact = (type: string) => {
143
return kArtifactCreators.find((artifact) =>
144
artifact.type === type && artifact.enabled !== false
145
);
146
};
147
148
// Use the provided type to search (or prompt the user)
149
let artifact = type ? findArtifact(type) : undefined;
150
151
while (artifact === undefined) {
152
if (!prompt) {
153
// We can't prompt to resolve this, so just throw an Error
154
if (type) {
155
throw new Error(`Failed to create ${type} - the type isn't recognized`);
156
} else {
157
throw new Error(
158
`Creation failed - you must provide a type to create when using '--no-prompt'`,
159
);
160
}
161
}
162
163
if (type) {
164
// The user provided a type, but it isn't recognized
165
info(`Unknown type ${type} - please select from the following:`);
166
}
167
168
// Prompt the user to select a type
169
type = await promptForType();
170
171
// Find the type (this should always work since we provided the id)
172
artifact = findArtifact(type);
173
}
174
return {
175
artifact,
176
};
177
};
178
179
// Wrapper that will provide keyboard selection hint (if necessary)
180
async function promptSelect(
181
message: string,
182
options: SelectValueOptions,
183
) {
184
return await Select.prompt({
185
message,
186
options,
187
});
188
}
189
190
// Prompts from the type of artifact to create
191
const promptForType = async () => {
192
return await promptSelect(
193
"Create",
194
kArtifactCreators.map((artifact) => {
195
return {
196
name: artifact.displayName.toLowerCase(),
197
value: artifact.type,
198
};
199
}),
200
);
201
};
202
203
// Determine the selected editor that should be used to open
204
// the artifact once created
205
const resolveEditor = async (createResult: CreateResult, editor?: string) => {
206
// Find supported editors
207
const editors = await scanForEditors(kEditorInfos, createResult);
208
209
const defaultEditor = editors.find((ed) => {
210
return ed.id === editor;
211
});
212
if (defaultEditor) {
213
// If an editor is specified, use that
214
return defaultEditor;
215
} else {
216
// See if we are executing inside of an editor, and just use
217
// that editor
218
const inEditor = editors.find((ed) => ed.inEditor);
219
if (inEditor) {
220
return inEditor;
221
} else if (editors.length > 0) {
222
// Prompt the user to select an editor
223
const editorOptions = editors.map((editor) => {
224
return {
225
name: editor.name.toLowerCase(),
226
value: editor.name,
227
};
228
});
229
230
// Add an option to not open
231
const options = [...editorOptions, {
232
name: "(don't open)",
233
value: "do not open",
234
}];
235
const name = await promptSelect("Open With", options);
236
237
// Return the matching editor (if any)
238
const selectedEditor = editors.find((edit) => edit.name === name);
239
return selectedEditor;
240
} else {
241
return undefined;
242
}
243
}
244
};
245
246
async function createFromStdin() {
247
/* Read a single line (should be json) like:
248
{
249
type: "project",
250
directive: {
251
"directory" : "",
252
"template" : "",
253
"name": ""
254
}
255
}
256
*/
257
258
// Read the stdin and then close it
259
const { value: input } = await readLines(Deno.stdin).next();
260
Deno.stdin.close();
261
262
// Parse options
263
const jsonOptions = JSON.parse(input);
264
265
// A type is required in the JSON no matter what
266
const type = jsonOptions.type;
267
if (!type) {
268
throw new Error(
269
"The provided json for create artifacts must include a valid type",
270
);
271
}
272
273
// Validate other required fields
274
if (
275
!jsonOptions.directive || !jsonOptions.directive.name ||
276
!jsonOptions.directive.directory || !jsonOptions.directive.template
277
) {
278
throw new Error(
279
"The provided json for create artifacts must include a directive with a name, directory, and template.",
280
);
281
}
282
283
// Find the artifact creator
284
const resolved = await resolveArtifact(
285
type,
286
false,
287
);
288
const createDirective = jsonOptions.directive as CreateDirective;
289
290
// Create the artifact using the options
291
const createResult = await resolved.artifact.createArtifact(
292
createDirective,
293
true,
294
);
295
const resultJSON = JSON.stringify(createResult, undefined);
296
Deno.stdout.writeSync(new TextEncoder().encode(resultJSON));
297
}
298
299