vize 0.303.0 → 0.310.0

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/src/init.ts ADDED
@@ -0,0 +1,189 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { atomicWriteFile } from "./setup/config.js";
6
+ import { initHelp, parseInitArgs, type InitArgs } from "./init/args.js";
7
+ import { detectProject, withFramework, type ProjectDetection } from "./init/detect.js";
8
+ import { planInit, type InitCommand, type InitPlan } from "./init/plan.js";
9
+ import {
10
+ confirm,
11
+ createPromptDeps,
12
+ isNonInteractive,
13
+ selectFeatures,
14
+ type PromptDeps,
15
+ } from "./init/prompt.js";
16
+ import {
17
+ renderBlocked,
18
+ renderCancelled,
19
+ renderDetection,
20
+ renderEditors,
21
+ renderNonInteractiveRefusal,
22
+ renderPlan,
23
+ } from "./init/report.js";
24
+ import { defaultSelection, offerFeatures, type FeatureSelection } from "./init/select.js";
25
+
26
+ export type { InitCommand, InitPlan } from "./init/plan.js";
27
+ export type { ProjectDetection } from "./init/detect.js";
28
+
29
+ export interface InitOptions {
30
+ readonly root: string;
31
+ readonly args?: readonly string[];
32
+ readonly runCommand?: (command: InitCommand) => void;
33
+ readonly writeFile?: (filename: string, source: string) => void;
34
+ readonly output?: (chunk: string) => void;
35
+ readonly promptDeps?: PromptDeps;
36
+ readonly stdin?: NodeJS.ReadableStream;
37
+ }
38
+
39
+ /**
40
+ * Resolves the feature selection from detection, flags, and -- when the terminal
41
+ * allows it -- the user.
42
+ *
43
+ * A non-TTY stdin without `--yes` returns `null`: refusing is the only correct
44
+ * answer, because prompting would hang a CI job forever.
45
+ */
46
+ export async function resolveSelection(
47
+ detection: ProjectDetection,
48
+ args: InitArgs,
49
+ deps: {
50
+ readonly output: (chunk: string) => void;
51
+ readonly stdin: NodeJS.ReadableStream;
52
+ readonly promptDeps?: PromptDeps;
53
+ },
54
+ ): Promise<FeatureSelection | null> {
55
+ const offers = offerFeatures(detection);
56
+ const base = defaultSelection(offers);
57
+ // An explicit flag survives even when detection says the feature is
58
+ // unavailable: the planner then reports it as blocked, with the reason. That
59
+ // is more useful than dropping the flag the user typed.
60
+ const withOverrides: Record<string, boolean> = { ...base };
61
+ for (const [id, enabled] of Object.entries(args.overrides)) {
62
+ withOverrides[id] = enabled;
63
+ }
64
+ const selection = withOverrides as FeatureSelection;
65
+
66
+ if (args.yes) {
67
+ return selection;
68
+ }
69
+ if (deps.promptDeps === undefined && isNonInteractive(deps.stdin)) {
70
+ deps.output(renderNonInteractiveRefusal());
71
+ return null;
72
+ }
73
+ // Only a prompt this function created may be closed here; an injected one
74
+ // belongs to the caller.
75
+ const owned =
76
+ deps.promptDeps === undefined
77
+ ? createPromptDeps({ input: deps.stdin, output: process.stdout as NodeJS.WritableStream })
78
+ : null;
79
+ const promptDeps = deps.promptDeps ?? owned!;
80
+ try {
81
+ const chosen = await selectFeatures(offers, selection, promptDeps);
82
+ if (chosen !== null && (await confirm("Apply this selection?", promptDeps))) {
83
+ return chosen;
84
+ }
85
+ deps.output(renderCancelled());
86
+ return null;
87
+ } finally {
88
+ owned?.close?.();
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Runs `init` end to end.
94
+ *
95
+ * Detection is reported before anything is decided, the plan is reported before
96
+ * anything is written, and a blocked feature is reported as NOT configured
97
+ * rather than quietly downgraded.
98
+ */
99
+ export async function initProject(options: InitOptions): Promise<InitPlan | null> {
100
+ const args = parseInitArgs(options.args ?? []);
101
+ const output = options.output ?? ((chunk: string) => process.stdout.write(chunk));
102
+ const root = path.resolve(args.root ?? options.root);
103
+ const detection = withFramework(detectProject(root), args.bundlerOverride);
104
+ output(renderDetection(detection));
105
+
106
+ const selection = await resolveSelection(detection, args, {
107
+ output,
108
+ stdin: options.stdin ?? process.stdin,
109
+ promptDeps: options.promptDeps,
110
+ });
111
+ if (selection === null) {
112
+ return null;
113
+ }
114
+
115
+ const plan = planInit({
116
+ detection,
117
+ selection,
118
+ install: args.install,
119
+ packageManager: args.packageManager ?? undefined,
120
+ });
121
+ output(renderPlan(plan, args.dryRun));
122
+ output(renderBlocked(plan));
123
+ if (args.dryRun) {
124
+ return plan;
125
+ }
126
+
127
+ writePlannedFiles(plan, options.writeFile ?? writeProjectFile);
128
+ const runCommand = options.runCommand ?? runInitCommand;
129
+ for (const command of plan.commands) {
130
+ runCommand(command);
131
+ }
132
+ if (selection.editor) {
133
+ output(renderEditors());
134
+ }
135
+ return plan;
136
+ }
137
+
138
+ export async function runInitCli(args: readonly string[]): Promise<void> {
139
+ if (parseInitArgs(args).help) {
140
+ process.stdout.write(initHelp());
141
+ return;
142
+ }
143
+ const plan = await initProject({ root: process.cwd(), args });
144
+ if (plan === null) {
145
+ process.exitCode = 1;
146
+ return;
147
+ }
148
+ if (plan.features.some((feature) => feature.outcome === "blocked")) {
149
+ process.exitCode = 1;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Default writer.
155
+ *
156
+ * Creates the parent directory first so `.vscode/extensions.json` works in a
157
+ * project that has never had a `.vscode` folder, then reuses `setup`'s atomic
158
+ * write so a crash mid-run cannot leave a half-written config behind.
159
+ */
160
+ function writeProjectFile(filename: string, source: string): void {
161
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
162
+ atomicWriteFile(filename, source);
163
+ }
164
+
165
+ function writePlannedFiles(
166
+ plan: InitPlan,
167
+ writeFile: (filename: string, source: string) => void,
168
+ ): void {
169
+ const written: string[] = [];
170
+ for (const file of plan.files) {
171
+ try {
172
+ writeFile(file.filename, file.source);
173
+ } catch (error) {
174
+ if (written.length === 0) {
175
+ throw error;
176
+ }
177
+ throw new Error(
178
+ `init partially completed: wrote ${written.join(", ")} before ` +
179
+ `${path.relative(plan.root, file.filename)} failed. Run init again to finish.`,
180
+ { cause: error },
181
+ );
182
+ }
183
+ written.push(path.relative(plan.root, file.filename));
184
+ }
185
+ }
186
+
187
+ function runInitCommand(command: InitCommand): void {
188
+ execFileSync(command.command, [...command.args], { cwd: command.cwd, stdio: "inherit" });
189
+ }