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