@triflare/fx 1.0.3 → 2.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/bin/fx.js +4 -1162
- package/package.json +8 -4
- package/product.yml +1 -1
- package/src/brand.js +9 -3
- package/src/catalog.js +103 -0
- package/src/cli/commands/build.js +98 -0
- package/src/cli/commands/catalog.js +42 -0
- package/src/cli/commands/check.js +44 -0
- package/src/cli/commands/completion.js +54 -0
- package/src/cli/commands/decompile.js +266 -0
- package/src/cli/commands/dev.js +179 -0
- package/src/cli/commands/extension.js +110 -0
- package/src/cli/commands/feature.js +114 -0
- package/src/cli/commands/fmt.js +173 -0
- package/src/cli/commands/index.js +45 -0
- package/src/cli/commands/init.js +169 -0
- package/src/cli/commands/lint.js +270 -0
- package/src/cli/commands/package.js +137 -0
- package/src/cli/commands/sprite/add.js +85 -0
- package/src/cli/commands/sprite/build.js +62 -0
- package/src/cli/commands/sprite/helpers.js +66 -0
- package/src/cli/commands/sprite/index.js +29 -0
- package/src/cli/commands/sprite/list.js +31 -0
- package/src/cli/commands/sprite/remove.js +51 -0
- package/src/cli/commands/vendor/add.js +98 -0
- package/src/cli/commands/vendor/helpers.js +50 -0
- package/src/cli/commands/vendor/index.js +39 -0
- package/src/cli/commands/vendor/install.js +64 -0
- package/src/cli/commands/vendor/list.js +45 -0
- package/src/cli/commands/vendor/migrate.js +47 -0
- package/src/cli/commands/vendor/outdated.js +51 -0
- package/src/cli/commands/vendor/remove.js +34 -0
- package/src/cli/commands/vendor/update.js +49 -0
- package/src/cli/commands/vendor/verify.js +44 -0
- package/src/cli/completion/bash.js +10 -0
- package/src/cli/completion/fish.js +9 -0
- package/src/cli/completion/index.js +97 -0
- package/src/cli/completion/zsh.js +11 -0
- package/src/cli/context.js +270 -0
- package/src/cli/define-command.js +147 -0
- package/src/cli/errors.js +111 -0
- package/src/cli/format.js +31 -0
- package/src/cli/prompts.js +183 -0
- package/src/cli/reporter.js +203 -0
- package/src/cli/run.js +248 -0
- package/src/cli/templates/index.js +112 -0
- package/src/cli/theme.js +84 -0
- package/src/compiler.js +133 -0
- package/src/config-edit.js +399 -1
- package/src/config.js +174 -4
- package/src/decompiler.js +45 -0
- package/src/extensions.js +72 -19
- package/src/features.js +339 -0
- package/src/formatter.js +499 -0
- package/src/index.js +31 -0
- package/src/lexer.js +45 -4
- package/src/lint/analyze.js +1414 -0
- package/src/lint/config-file.js +97 -0
- package/src/lint/config.js +174 -0
- package/src/lint/rules.js +461 -0
- package/src/linter.js +220 -0
- package/src/merge.js +109 -0
- package/src/parser.js +53 -13
- package/src/project.js +7 -87
- package/src/vendor.js +1323 -0
- package/vendor/pm-packager-dist/.fx-build.json +1 -1
- package/vendor/tempvars/fx-tempvars.js +321 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompt layer.
|
|
3
|
+
*
|
|
4
|
+
* `@clack/prompts` provides the widgets; this wrapper keeps Fx styling,
|
|
5
|
+
* non-interactive fallbacks, cancellation, and testability in one place.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
cancel,
|
|
10
|
+
confirm as clackConfirm,
|
|
11
|
+
intro as clackIntro,
|
|
12
|
+
isCancel,
|
|
13
|
+
note as clackNote,
|
|
14
|
+
outro as clackOutro,
|
|
15
|
+
select as clackSelect,
|
|
16
|
+
spinner as clackSpinner,
|
|
17
|
+
text as clackText,
|
|
18
|
+
updateSettings,
|
|
19
|
+
} from "@clack/prompts";
|
|
20
|
+
import { CliError, UsageError, EXIT } from "./errors.js";
|
|
21
|
+
|
|
22
|
+
updateSettings({
|
|
23
|
+
messages: {
|
|
24
|
+
cancel: "Cancelled",
|
|
25
|
+
error: "Something went wrong",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const CI_VARS = ["CI", "GITHUB_ACTIONS", "TRAVIS", "JENKINS_URL", "CIRCLECI"];
|
|
30
|
+
|
|
31
|
+
function cancelled() {
|
|
32
|
+
return new CliError("Cancelled", {
|
|
33
|
+
code: "CANCELLED",
|
|
34
|
+
exitCode: EXIT.INTERRUPTED,
|
|
35
|
+
silent: true,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unwrap(value) {
|
|
40
|
+
if (isCancel(value)) throw cancelled();
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {{
|
|
46
|
+
* reporter: ReturnType<import("./reporter.js").createReporter>,
|
|
47
|
+
* product: object,
|
|
48
|
+
* tty?: boolean,
|
|
49
|
+
* noInput?: boolean,
|
|
50
|
+
* json?: boolean,
|
|
51
|
+
* input?: NodeJS.ReadableStream,
|
|
52
|
+
* output?: NodeJS.WritableStream,
|
|
53
|
+
* }} options
|
|
54
|
+
*/
|
|
55
|
+
export function createPrompts({
|
|
56
|
+
reporter,
|
|
57
|
+
product,
|
|
58
|
+
tty = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
59
|
+
noInput = false,
|
|
60
|
+
json = false,
|
|
61
|
+
input = process.stdin,
|
|
62
|
+
output = process.stdout,
|
|
63
|
+
} = {}) {
|
|
64
|
+
const enabled =
|
|
65
|
+
tty && !noInput && !json && !CI_VARS.some((name) => process.env[name]);
|
|
66
|
+
const common = { input, output };
|
|
67
|
+
|
|
68
|
+
const requireFallback = (value, feature) => {
|
|
69
|
+
if (value !== undefined) return value;
|
|
70
|
+
throw new UsageError(
|
|
71
|
+
`${feature} requires an interactive terminal, --yes, or an explicit value`,
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const message = (text) => reporter.accent(String(text));
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
enabled,
|
|
79
|
+
|
|
80
|
+
intro(text) {
|
|
81
|
+
if (!enabled) return;
|
|
82
|
+
clackIntro(
|
|
83
|
+
reporter.brand(`${product?.name ?? "Fx"} ${product?.version ?? ""}`) +
|
|
84
|
+
(text ? `\n${message(text)}` : ""),
|
|
85
|
+
common,
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
outro(text) {
|
|
90
|
+
if (!enabled) return;
|
|
91
|
+
clackOutro(message(text), common);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
note(text, title) {
|
|
95
|
+
if (!enabled) return;
|
|
96
|
+
clackNote(message(text), title ? message(title) : undefined, common);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async text({ message: prompt, placeholder, defaultValue, validate }) {
|
|
100
|
+
if (!enabled) return requireFallback(defaultValue, prompt);
|
|
101
|
+
return unwrap(
|
|
102
|
+
await clackText(
|
|
103
|
+
{
|
|
104
|
+
...common,
|
|
105
|
+
message: message(prompt),
|
|
106
|
+
placeholder,
|
|
107
|
+
defaultValue,
|
|
108
|
+
validate,
|
|
109
|
+
},
|
|
110
|
+
common,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async select({ message: prompt, options, initialValue }) {
|
|
116
|
+
if (!enabled) return requireFallback(initialValue, prompt);
|
|
117
|
+
return unwrap(
|
|
118
|
+
await clackSelect(
|
|
119
|
+
{
|
|
120
|
+
...common,
|
|
121
|
+
message: message(prompt),
|
|
122
|
+
options,
|
|
123
|
+
initialValue,
|
|
124
|
+
},
|
|
125
|
+
common,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async confirm({ message: prompt, initialValue = true }) {
|
|
131
|
+
if (!enabled) return initialValue;
|
|
132
|
+
return unwrap(
|
|
133
|
+
await clackConfirm(
|
|
134
|
+
{
|
|
135
|
+
...common,
|
|
136
|
+
message: message(prompt),
|
|
137
|
+
initialValue,
|
|
138
|
+
},
|
|
139
|
+
common,
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async confirmDestructive({ message: prompt, yes = false }) {
|
|
145
|
+
if (yes) return true;
|
|
146
|
+
if (!enabled)
|
|
147
|
+
throw new UsageError(
|
|
148
|
+
`${prompt} is destructive; pass --yes to confirm in non-interactive mode`,
|
|
149
|
+
);
|
|
150
|
+
return unwrap(
|
|
151
|
+
await clackConfirm(
|
|
152
|
+
{
|
|
153
|
+
...common,
|
|
154
|
+
message: message(prompt),
|
|
155
|
+
initialValue: false,
|
|
156
|
+
},
|
|
157
|
+
common,
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
cancel(text = "Cancelled") {
|
|
163
|
+
if (!enabled) return;
|
|
164
|
+
cancel(message(text), common);
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
spinner() {
|
|
168
|
+
if (!enabled) {
|
|
169
|
+
return {
|
|
170
|
+
start() {},
|
|
171
|
+
stop() {},
|
|
172
|
+
message() {},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const spin = clackSpinner(common);
|
|
176
|
+
return {
|
|
177
|
+
start: (text) => spin.start(message(text)),
|
|
178
|
+
stop: (text) => spin.stop(text ? message(text) : undefined),
|
|
179
|
+
message: (text) => spin.message(message(text)),
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human and JSON output for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Commands call semantic methods (`success`, `detail`, `warn`, ...) and never
|
|
5
|
+
* write to stdout/stderr directly. In JSON mode human output is suppressed and
|
|
6
|
+
* warnings are collected for the command envelope.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { styleText } from "node:util";
|
|
10
|
+
import { brandText } from "../brand.js";
|
|
11
|
+
import { createTheme } from "./theme.js";
|
|
12
|
+
|
|
13
|
+
const STATUS = {
|
|
14
|
+
success: { symbol: "success", tone: "success" },
|
|
15
|
+
error: { symbol: "error", tone: "danger" },
|
|
16
|
+
warn: { symbol: "progress", tone: "warning" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function canColor(stream) {
|
|
20
|
+
return styleText("red", "x", { stream }).includes("\x1b");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createReporter({
|
|
24
|
+
product,
|
|
25
|
+
stdout = process.stdout,
|
|
26
|
+
stderr = process.stderr,
|
|
27
|
+
json = false,
|
|
28
|
+
quiet = false,
|
|
29
|
+
verbose = false,
|
|
30
|
+
color = "auto",
|
|
31
|
+
} = {}) {
|
|
32
|
+
const warnings = [];
|
|
33
|
+
const errors = [];
|
|
34
|
+
const themes = new Map();
|
|
35
|
+
|
|
36
|
+
const themeFor = (stream) => {
|
|
37
|
+
if (!themes.has(stream))
|
|
38
|
+
themes.set(stream, createTheme({ product, color, stream }));
|
|
39
|
+
return themes.get(stream);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const colorEnabled = (stream) => {
|
|
43
|
+
if (color === "never") return false;
|
|
44
|
+
if (color === "always") return true;
|
|
45
|
+
return canColor(stream);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const paint = (text, tone, stream = stdout) => {
|
|
49
|
+
if (!tone || !colorEnabled(stream)) return text;
|
|
50
|
+
const options = { stream };
|
|
51
|
+
if (color === "always") options.validateStream = false;
|
|
52
|
+
return styleText(tone, text, options);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const mark = (name) => product?.symbols?.[name] ?? "";
|
|
56
|
+
|
|
57
|
+
const status = (type, message) => {
|
|
58
|
+
const { symbol, tone } = STATUS[type];
|
|
59
|
+
const glyph = mark(symbol);
|
|
60
|
+
const stream = type === "success" ? stdout : stderr;
|
|
61
|
+
const theme = themeFor(stream);
|
|
62
|
+
const text = String(message);
|
|
63
|
+
const output = text
|
|
64
|
+
.split("\n")
|
|
65
|
+
.map((line, index) => {
|
|
66
|
+
const lead =
|
|
67
|
+
index === 0
|
|
68
|
+
? glyph
|
|
69
|
+
? `${theme.tone(tone, glyph)} `
|
|
70
|
+
: ""
|
|
71
|
+
: glyph
|
|
72
|
+
? " "
|
|
73
|
+
: "";
|
|
74
|
+
const body =
|
|
75
|
+
type === "success" ? theme.bold(line) : theme.tone(tone, line);
|
|
76
|
+
return `${lead}${body}`;
|
|
77
|
+
})
|
|
78
|
+
.join("\n");
|
|
79
|
+
stream.write(`${output}\n`);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const human = !json && !quiet;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
json,
|
|
86
|
+
quiet,
|
|
87
|
+
verbose,
|
|
88
|
+
warnings,
|
|
89
|
+
errors,
|
|
90
|
+
paint,
|
|
91
|
+
brand: (text) => brandText(text, stdout, { force: color === "always" }),
|
|
92
|
+
accent: (text) => themeFor(stdout).accent(text),
|
|
93
|
+
muted: (text) => themeFor(stdout).muted(text),
|
|
94
|
+
bold: (text) => themeFor(stdout).bold(text),
|
|
95
|
+
tone: (name, text) => themeFor(stdout).tone(name, text),
|
|
96
|
+
|
|
97
|
+
success(message) {
|
|
98
|
+
if (!human) return;
|
|
99
|
+
status("success", message);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
info(message) {
|
|
103
|
+
if (!human) return;
|
|
104
|
+
stdout.write(`${message}\n`);
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
bullet(message) {
|
|
108
|
+
if (!human) return;
|
|
109
|
+
const theme = themeFor(stdout);
|
|
110
|
+
const glyph = mark("bullet");
|
|
111
|
+
stdout.write(` ${glyph ? `${theme.accent(glyph)} ` : ""}${message}\n`);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
step(message) {
|
|
115
|
+
if (!human) return;
|
|
116
|
+
const theme = themeFor(stdout);
|
|
117
|
+
const glyph = mark("progress");
|
|
118
|
+
stdout.write(`${glyph ? `${theme.accent(glyph)} ` : ""}${message}\n`);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
heading(message) {
|
|
122
|
+
if (!human) return;
|
|
123
|
+
stdout.write(`${themeFor(stdout).heading(String(message))}\n`);
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/** Aligned `label value` row used by command summaries. */
|
|
127
|
+
detail(label, value, { width = 13 } = {}) {
|
|
128
|
+
if (!human) return;
|
|
129
|
+
const theme = themeFor(stdout);
|
|
130
|
+
const text = String(value);
|
|
131
|
+
const rendered = text === "(none)" ? theme.muted(text) : theme.bold(text);
|
|
132
|
+
stdout.write(
|
|
133
|
+
` ${theme.muted(String(label).padEnd(width))} ${rendered}\n`,
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/** Aligned name + description row used by listings. */
|
|
138
|
+
item(label, value = "", { width = 24, suffix = "" } = {}) {
|
|
139
|
+
if (!human) return;
|
|
140
|
+
const theme = themeFor(stdout);
|
|
141
|
+
const text = String(label);
|
|
142
|
+
const padded = text.padEnd(Math.max(width, text.length));
|
|
143
|
+
const detail = value ? ` ${value}` : "";
|
|
144
|
+
const note = suffix ? ` ${theme.muted(suffix)}` : "";
|
|
145
|
+
stdout.write(` ${theme.accent(padded)}${detail}${note}\n`);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/** Plain block of text with no decoration. */
|
|
149
|
+
text(message) {
|
|
150
|
+
if (!human) return;
|
|
151
|
+
stdout.write(`${message}\n`);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Unmodified product output, such as formatted source. Quiet does not
|
|
156
|
+
* suppress it because it is the command's result; JSON mode does.
|
|
157
|
+
*/
|
|
158
|
+
raw(text) {
|
|
159
|
+
if (json) return;
|
|
160
|
+
stdout.write(String(text));
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
/** Quiet, de-emphasized line for incidental notes. */
|
|
164
|
+
note(message) {
|
|
165
|
+
if (!human) return;
|
|
166
|
+
stdout.write(`${themeFor(stdout).muted(message)}\n`);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
warn(message) {
|
|
170
|
+
const text = String(message);
|
|
171
|
+
warnings.push(text);
|
|
172
|
+
if (json) return;
|
|
173
|
+
status("warn", text);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
error(message) {
|
|
177
|
+
const text = String(message);
|
|
178
|
+
errors.push(text);
|
|
179
|
+
if (json) return;
|
|
180
|
+
status("error", text);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
hint(message) {
|
|
184
|
+
if (!human) return;
|
|
185
|
+
const theme = themeFor(stdout);
|
|
186
|
+
stdout.write(
|
|
187
|
+
` ${theme.accent("↳")} ${theme.muted(`hint: ${message}`)}\n`,
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/** One JSON document, used for the command result envelope. */
|
|
192
|
+
document(value) {
|
|
193
|
+
if (!json) return;
|
|
194
|
+
stdout.write(`${JSON.stringify(value)}\n`);
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/** One newline-delimited JSON event, used by long-running `dev`. */
|
|
198
|
+
event(type, data = {}) {
|
|
199
|
+
if (!json) return;
|
|
200
|
+
stdout.write(`${JSON.stringify({ event: type, ...data })}\n`);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
package/src/cli/run.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entrypoint: commander setup, global options, and top-level error
|
|
3
|
+
* handling.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { Command, CommanderError } from "commander";
|
|
8
|
+
import { brandText } from "../brand.js";
|
|
9
|
+
import { loadProduct } from "../product.js";
|
|
10
|
+
import { registerCommands } from "./commands/index.js";
|
|
11
|
+
import {
|
|
12
|
+
CliError,
|
|
13
|
+
EXIT,
|
|
14
|
+
UsageError,
|
|
15
|
+
errorToJson,
|
|
16
|
+
formatError,
|
|
17
|
+
} from "./errors.js";
|
|
18
|
+
import { createReporter } from "./reporter.js";
|
|
19
|
+
import { createTheme } from "./theme.js";
|
|
20
|
+
|
|
21
|
+
const USAGE_CODES = new Set([
|
|
22
|
+
"commander.unknownCommand",
|
|
23
|
+
"commander.unknownOption",
|
|
24
|
+
"commander.missingArgument",
|
|
25
|
+
"commander.optionMissingArgument",
|
|
26
|
+
"commander.invalidArgument",
|
|
27
|
+
"commander.excessArguments",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function resolveColor(options = {}) {
|
|
31
|
+
if (options.color === false || options.color === "never") return "never";
|
|
32
|
+
if (options.color === "always") return "always";
|
|
33
|
+
return "auto";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Read the global flags needed before commander has parsed successfully. */
|
|
37
|
+
function scanGlobalFlags(argv) {
|
|
38
|
+
const result = {
|
|
39
|
+
json: false,
|
|
40
|
+
quiet: false,
|
|
41
|
+
verbose: false,
|
|
42
|
+
color: "auto",
|
|
43
|
+
};
|
|
44
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
45
|
+
const token = argv[i];
|
|
46
|
+
if (token === "--json") result.json = true;
|
|
47
|
+
else if (token === "-q" || token === "--quiet") result.quiet = true;
|
|
48
|
+
else if (token === "-v" || token === "--verbose") result.verbose = true;
|
|
49
|
+
else if (token === "--no-color") result.color = "never";
|
|
50
|
+
else if (token === "--color") result.color = argv[++i] ?? "auto";
|
|
51
|
+
else if (token.startsWith("--color="))
|
|
52
|
+
result.color = token.slice("--color=".length);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function commandFromArgv(argv, program) {
|
|
58
|
+
const names = new Set();
|
|
59
|
+
for (const command of program.commands)
|
|
60
|
+
for (const name of [command.name(), ...command.aliases()]) names.add(name);
|
|
61
|
+
for (const token of argv) if (names.has(token)) return token;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function styleUsage(text, theme) {
|
|
66
|
+
return text
|
|
67
|
+
.split(" ")
|
|
68
|
+
.map((word) => {
|
|
69
|
+
if (word === "[options]") return theme.option(word);
|
|
70
|
+
if (word === "[command]") return theme.command(word);
|
|
71
|
+
if (word.startsWith("[") || word.startsWith("<"))
|
|
72
|
+
return theme.argument(word);
|
|
73
|
+
return theme.command(word);
|
|
74
|
+
})
|
|
75
|
+
.join(" ");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function styleOptionTerm(text, theme) {
|
|
79
|
+
return text
|
|
80
|
+
.split(" ")
|
|
81
|
+
.map((word) =>
|
|
82
|
+
word.startsWith("<") || word.startsWith("[")
|
|
83
|
+
? theme.argument(word)
|
|
84
|
+
: theme.option(word),
|
|
85
|
+
)
|
|
86
|
+
.join(" ");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function styleSubcommandTerm(text, theme) {
|
|
90
|
+
return text
|
|
91
|
+
.split(" ")
|
|
92
|
+
.map((word) => {
|
|
93
|
+
if (word === "[options]") return theme.option(word);
|
|
94
|
+
if (word.startsWith("[") || word.startsWith("<"))
|
|
95
|
+
return theme.argument(word);
|
|
96
|
+
return theme.command(word);
|
|
97
|
+
})
|
|
98
|
+
.join(" ");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createProgram(product, theme, color) {
|
|
102
|
+
const program = new Command();
|
|
103
|
+
program
|
|
104
|
+
.name(product.command)
|
|
105
|
+
.description(product.tagline)
|
|
106
|
+
.version(
|
|
107
|
+
`${product.name} ${product.version}`,
|
|
108
|
+
"-V, --version",
|
|
109
|
+
"Print the version",
|
|
110
|
+
)
|
|
111
|
+
.helpOption("-h, --help", "Show help")
|
|
112
|
+
.helpCommand("help [command]", "Show help for a command")
|
|
113
|
+
.showSuggestionAfterError(true)
|
|
114
|
+
.configureHelp({
|
|
115
|
+
sortCommands: false,
|
|
116
|
+
sortOptions: false,
|
|
117
|
+
showGlobalOptions: true,
|
|
118
|
+
styleTitle: (text) => theme.heading(text),
|
|
119
|
+
styleUsage: (text) => styleUsage(text, theme),
|
|
120
|
+
styleCommandText: (text) => theme.command(text),
|
|
121
|
+
styleSubcommandText: (text) => theme.command(text),
|
|
122
|
+
styleOptionText: (text) => theme.option(text),
|
|
123
|
+
styleArgumentText: (text) => theme.argument(text),
|
|
124
|
+
styleCommandDescription: (text) => theme.muted(text),
|
|
125
|
+
styleOptionDescription: (text) => theme.muted(text),
|
|
126
|
+
styleSubcommandDescription: (text) => theme.muted(text),
|
|
127
|
+
styleArgumentDescription: (text) => theme.muted(text),
|
|
128
|
+
styleOptionTerm: (text) => styleOptionTerm(text, theme),
|
|
129
|
+
styleSubcommandTerm: (text) => styleSubcommandTerm(text, theme),
|
|
130
|
+
styleArgumentTerm: (text) => theme.argument(text),
|
|
131
|
+
})
|
|
132
|
+
.optionsGroup("Global Options:")
|
|
133
|
+
.option("-v, --verbose", "Show timings, stack traces, and extra detail")
|
|
134
|
+
.option("--json", "Emit machine-readable JSON")
|
|
135
|
+
.option("-q, --quiet", "Suppress success and informational output")
|
|
136
|
+
.option("--color <mode>", "Color output: auto, always, or never", "auto")
|
|
137
|
+
.option("--no-color", "Disable color output")
|
|
138
|
+
.option("--cwd <dir>", "Run as if started in <dir>")
|
|
139
|
+
.option("--config <file>", "Use a specific config file")
|
|
140
|
+
.option("--no-config", "Ignore the project config file")
|
|
141
|
+
.option("--no-input", "Never prompt for input")
|
|
142
|
+
.option("-y, --yes", "Accept defaults and confirm destructive actions")
|
|
143
|
+
.option(
|
|
144
|
+
"--no-upgrade-prompt",
|
|
145
|
+
"Do not offer to migrate project.extensions",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
program.addHelpText(
|
|
149
|
+
"before",
|
|
150
|
+
`\n ${brandText(product.name, process.stdout, {
|
|
151
|
+
force: color === "always",
|
|
152
|
+
})} ${theme.muted(product.version)}\n ${theme.muted(product.tagline)}\n`,
|
|
153
|
+
);
|
|
154
|
+
program.addHelpText(
|
|
155
|
+
"after",
|
|
156
|
+
`\n ${theme.accent("↳")} Run ${theme.command(
|
|
157
|
+
`${product.command} <command> --help`,
|
|
158
|
+
)} for command details.\n`,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return program;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Run the CLI and return an exit code instead of terminating the process.
|
|
166
|
+
*
|
|
167
|
+
* @param {string[]} [argv]
|
|
168
|
+
* @returns {Promise<number>}
|
|
169
|
+
*/
|
|
170
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
171
|
+
const product = loadProduct();
|
|
172
|
+
const raw = scanGlobalFlags(argv);
|
|
173
|
+
const color = resolveColor(raw);
|
|
174
|
+
const theme = createTheme({ product, color, stream: process.stdout });
|
|
175
|
+
const reporter = createReporter({
|
|
176
|
+
product,
|
|
177
|
+
json: raw.json,
|
|
178
|
+
quiet: raw.quiet,
|
|
179
|
+
verbose: raw.verbose,
|
|
180
|
+
color,
|
|
181
|
+
});
|
|
182
|
+
const state = { exitCode: EXIT.OK, command: null };
|
|
183
|
+
const program = createProgram(product, theme, color);
|
|
184
|
+
program.exitOverride();
|
|
185
|
+
program.configureOutput({
|
|
186
|
+
writeOut: (text) => process.stdout.write(text),
|
|
187
|
+
writeErr: (text) => process.stderr.write(text),
|
|
188
|
+
getOutHasColors: () => theme.enabled,
|
|
189
|
+
getErrHasColors: () => theme.enabled,
|
|
190
|
+
// Keep commander's usage text out of the stream; run() renders one
|
|
191
|
+
// consistent diagnostic (or JSON document) below.
|
|
192
|
+
outputError: () => {},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
registerCommands(program, { state, product });
|
|
196
|
+
// With no command, print help to stdout instead of commander's stderr usage.
|
|
197
|
+
program.action(() => program.outputHelp());
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await program.parseAsync(argv, { from: "user" });
|
|
201
|
+
return state.exitCode;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error instanceof CommanderError) {
|
|
204
|
+
if (
|
|
205
|
+
error.code === "commander.helpDisplayed" ||
|
|
206
|
+
error.code === "commander.help" ||
|
|
207
|
+
error.code === "commander.version"
|
|
208
|
+
)
|
|
209
|
+
return EXIT.OK;
|
|
210
|
+
|
|
211
|
+
const usage = new UsageError(error.message.replace(/^error:\s*/, ""), {
|
|
212
|
+
code: USAGE_CODES.has(error.code) ? "USAGE_ERROR" : "COMMANDER_ERROR",
|
|
213
|
+
hint: `run "${product.command} help" for usage`,
|
|
214
|
+
});
|
|
215
|
+
usage.command = state.command ?? commandFromArgv(argv, program);
|
|
216
|
+
usage.warnings = [];
|
|
217
|
+
return reportError(usage, reporter);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (error && typeof error === "object") {
|
|
221
|
+
error.warnings ??= [];
|
|
222
|
+
error.command ??= state.command ?? commandFromArgv(argv, program);
|
|
223
|
+
}
|
|
224
|
+
return reportError(error, reporter);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function reportError(error, reporter) {
|
|
229
|
+
const exitCode = error instanceof CliError ? error.exitCode : EXIT.FAILURE;
|
|
230
|
+
const command = error?.command ?? null;
|
|
231
|
+
|
|
232
|
+
if (reporter.json) {
|
|
233
|
+
reporter.document({
|
|
234
|
+
schemaVersion: 1,
|
|
235
|
+
command,
|
|
236
|
+
ok: false,
|
|
237
|
+
warnings: error?.warnings ?? [],
|
|
238
|
+
error: errorToJson(error),
|
|
239
|
+
});
|
|
240
|
+
return exitCode;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (error instanceof CliError && error.silent) return exitCode;
|
|
244
|
+
|
|
245
|
+
reporter.error(formatError(error, { verbose: reporter.verbose }));
|
|
246
|
+
if (error instanceof CliError && error.hint) reporter.hint(error.hint);
|
|
247
|
+
return exitCode;
|
|
248
|
+
}
|