@openvcs/sdk 0.2.1 → 0.2.2
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 +2 -0
- package/lib/dist.js +24 -15
- package/lib/init.d.ts +30 -0
- package/lib/init.js +69 -36
- package/package.json +1 -1
- package/src/lib/dist.ts +22 -19
- package/src/lib/init.ts +79 -45
- package/test/init.test.js +65 -0
package/README.md
CHANGED
|
@@ -57,6 +57,8 @@ openvcs init my-plugin
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
The generated module template includes TypeScript and Node typings (`@types/node`).
|
|
60
|
+
Plugin IDs entered during scaffold must not be `.`/`..` and must not contain path
|
|
61
|
+
separators (`/` or `\\`).
|
|
60
62
|
|
|
61
63
|
Interactive theme plugin scaffold:
|
|
62
64
|
|
package/lib/dist.js
CHANGED
|
@@ -63,12 +63,30 @@ function parseDistArgs(args) {
|
|
|
63
63
|
}
|
|
64
64
|
function readManifest(pluginDir) {
|
|
65
65
|
const manifestPath = path.join(pluginDir, "openvcs.plugin.json");
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
66
|
+
let manifestRaw;
|
|
67
|
+
let manifestFd;
|
|
69
68
|
let manifest;
|
|
70
69
|
try {
|
|
71
|
-
|
|
70
|
+
manifestFd = fs.openSync(manifestPath, "r");
|
|
71
|
+
const manifestStat = fs.fstatSync(manifestFd);
|
|
72
|
+
if (!manifestStat.isFile()) {
|
|
73
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
74
|
+
}
|
|
75
|
+
manifestRaw = fs.readFileSync(manifestFd, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code === "ENOENT") {
|
|
79
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
if (typeof manifestFd === "number") {
|
|
85
|
+
fs.closeSync(manifestFd);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
manifest = JSON.parse(manifestRaw);
|
|
72
90
|
}
|
|
73
91
|
catch (error) {
|
|
74
92
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -122,24 +140,15 @@ function runCommand(program, args, cwd, verbose) {
|
|
|
122
140
|
}
|
|
123
141
|
const result = (0, node_child_process_1.spawnSync)(program, args, {
|
|
124
142
|
cwd,
|
|
125
|
-
|
|
126
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
143
|
+
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
127
144
|
});
|
|
128
145
|
if (result.error) {
|
|
129
146
|
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
130
147
|
}
|
|
131
148
|
if (result.status === 0) {
|
|
132
|
-
if (verbose) {
|
|
133
|
-
if (result.stdout?.trim()) {
|
|
134
|
-
process.stderr.write(`${result.stdout.trim()}\n`);
|
|
135
|
-
}
|
|
136
|
-
if (result.stderr?.trim()) {
|
|
137
|
-
process.stderr.write(`${result.stderr.trim()}\n`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
149
|
return;
|
|
141
150
|
}
|
|
142
|
-
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}
|
|
151
|
+
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
|
|
143
152
|
}
|
|
144
153
|
function ensurePackageLock(pluginDir, verbose) {
|
|
145
154
|
if (!hasPackageJson(pluginDir)) {
|
package/lib/init.d.ts
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
|
+
interface InitAnswers {
|
|
2
|
+
targetDir: string;
|
|
3
|
+
kind: "module" | "theme";
|
|
4
|
+
pluginId: string;
|
|
5
|
+
pluginName: string;
|
|
6
|
+
pluginVersion: string;
|
|
7
|
+
defaultEnabled: boolean;
|
|
8
|
+
runNpmInstall: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface CollectAnswersOptions {
|
|
11
|
+
forceTheme: boolean;
|
|
12
|
+
targetHint?: string;
|
|
13
|
+
}
|
|
14
|
+
interface PromptDriver {
|
|
15
|
+
promptText(label: string, defaultValue?: string): Promise<string>;
|
|
16
|
+
promptBoolean(label: string, defaultValue: boolean): Promise<boolean>;
|
|
17
|
+
close(): void;
|
|
18
|
+
}
|
|
1
19
|
interface InitCommandError {
|
|
2
20
|
code?: string;
|
|
3
21
|
}
|
|
4
22
|
export declare function initUsage(commandName?: string): string;
|
|
23
|
+
declare function sanitizeIdToken(raw: string): string;
|
|
24
|
+
declare function defaultPluginIdFromDir(targetDir: string): string;
|
|
25
|
+
declare function validatePluginId(pluginId: string): string | undefined;
|
|
26
|
+
declare function createReadlinePromptDriver(output?: NodeJS.WritableStream): PromptDriver;
|
|
27
|
+
declare function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions, promptDriver?: PromptDriver, output?: NodeJS.WritableStream): Promise<InitAnswers>;
|
|
5
28
|
export declare function runInitCommand(args: string[]): Promise<string>;
|
|
6
29
|
export declare function isUsageError(error: unknown): error is InitCommandError;
|
|
30
|
+
export declare const __private: {
|
|
31
|
+
collectAnswers: typeof collectAnswers;
|
|
32
|
+
createReadlinePromptDriver: typeof createReadlinePromptDriver;
|
|
33
|
+
defaultPluginIdFromDir: typeof defaultPluginIdFromDir;
|
|
34
|
+
sanitizeIdToken: typeof sanitizeIdToken;
|
|
35
|
+
validatePluginId: typeof validatePluginId;
|
|
36
|
+
};
|
|
7
37
|
export {};
|
package/lib/init.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.__private = void 0;
|
|
3
4
|
exports.initUsage = initUsage;
|
|
4
5
|
exports.runInitCommand = runInitCommand;
|
|
5
6
|
exports.isUsageError = isUsageError;
|
|
@@ -45,28 +46,48 @@ function defaultPluginNameFromId(pluginId) {
|
|
|
45
46
|
.map((word) => word[0].toUpperCase() + word.slice(1));
|
|
46
47
|
return words.length > 0 ? words.join(" ") : "OpenVCS Plugin";
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
async function promptBoolean(rl, label, defaultValue) {
|
|
55
|
-
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
56
|
-
while (true) {
|
|
57
|
-
const answer = await rl.question(`${label} (${suffix}): `);
|
|
58
|
-
const normalized = answer.trim().toLowerCase();
|
|
59
|
-
if (!normalized) {
|
|
60
|
-
return defaultValue;
|
|
61
|
-
}
|
|
62
|
-
if (normalized === "y" || normalized === "yes") {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
if (normalized === "n" || normalized === "no") {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
process.stderr.write("Please answer yes or no.\n");
|
|
49
|
+
function validatePluginId(pluginId) {
|
|
50
|
+
if (!pluginId) {
|
|
51
|
+
return "Plugin id is required.";
|
|
52
|
+
}
|
|
53
|
+
if (pluginId === "." || pluginId === "..") {
|
|
54
|
+
return "Plugin id must not be '.' or '..'.";
|
|
69
55
|
}
|
|
56
|
+
if (pluginId.includes("/") || pluginId.includes("\\")) {
|
|
57
|
+
return "Plugin id must not contain path separators (/ or \\).";
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function createReadlinePromptDriver(output = process.stderr) {
|
|
62
|
+
const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
|
|
63
|
+
return {
|
|
64
|
+
async promptText(label, defaultValue = "") {
|
|
65
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
66
|
+
const answer = await rl.question(`${label}${suffix}: `);
|
|
67
|
+
const trimmed = answer.trim();
|
|
68
|
+
return trimmed || defaultValue;
|
|
69
|
+
},
|
|
70
|
+
async promptBoolean(label, defaultValue) {
|
|
71
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
72
|
+
while (true) {
|
|
73
|
+
const answer = await rl.question(`${label} (${suffix}): `);
|
|
74
|
+
const normalized = answer.trim().toLowerCase();
|
|
75
|
+
if (!normalized) {
|
|
76
|
+
return defaultValue;
|
|
77
|
+
}
|
|
78
|
+
if (normalized === "y" || normalized === "yes") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (normalized === "n" || normalized === "no") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
output.write("Please answer yes or no.\n");
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
close() {
|
|
88
|
+
rl.close();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
70
91
|
}
|
|
71
92
|
function writeJson(filePath, value) {
|
|
72
93
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -80,11 +101,10 @@ function directoryHasEntries(targetDir) {
|
|
|
80
101
|
const entries = fs.readdirSync(targetDir);
|
|
81
102
|
return entries.length > 0;
|
|
82
103
|
}
|
|
83
|
-
async function collectAnswers({ forceTheme, targetHint }) {
|
|
104
|
+
async function collectAnswers({ forceTheme, targetHint }, promptDriver = createReadlinePromptDriver(), output = process.stderr) {
|
|
84
105
|
const defaultTarget = targetHint || path.join(process.cwd(), "openvcs-plugin");
|
|
85
|
-
const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
|
|
86
106
|
try {
|
|
87
|
-
const targetText = await promptText(
|
|
107
|
+
const targetText = await promptDriver.promptText("Target directory", defaultTarget);
|
|
88
108
|
const targetDir = path.resolve(targetText);
|
|
89
109
|
let kind = "module";
|
|
90
110
|
if (forceTheme) {
|
|
@@ -92,7 +112,7 @@ async function collectAnswers({ forceTheme, targetHint }) {
|
|
|
92
112
|
}
|
|
93
113
|
else {
|
|
94
114
|
while (true) {
|
|
95
|
-
const value = (await promptText(
|
|
115
|
+
const value = (await promptDriver.promptText("Template type (module/theme)", "module"))
|
|
96
116
|
.trim()
|
|
97
117
|
.toLowerCase();
|
|
98
118
|
if (value === "module" || value === "m") {
|
|
@@ -103,25 +123,31 @@ async function collectAnswers({ forceTheme, targetHint }) {
|
|
|
103
123
|
kind = "theme";
|
|
104
124
|
break;
|
|
105
125
|
}
|
|
106
|
-
|
|
126
|
+
output.write("Please choose 'module' or 'theme'.\n");
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
const defaultId = defaultPluginIdFromDir(targetDir);
|
|
110
|
-
let pluginId
|
|
130
|
+
let pluginId;
|
|
111
131
|
while (!pluginId) {
|
|
112
|
-
|
|
132
|
+
const candidateId = (await promptDriver.promptText("Plugin id", defaultId)).trim();
|
|
133
|
+
const validationError = validatePluginId(candidateId);
|
|
134
|
+
if (!validationError) {
|
|
135
|
+
pluginId = candidateId;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
output.write(`${validationError}\n`);
|
|
113
139
|
}
|
|
114
140
|
const defaultName = defaultPluginNameFromId(pluginId);
|
|
115
141
|
let pluginName = "";
|
|
116
142
|
while (!pluginName) {
|
|
117
|
-
pluginName = (await promptText(
|
|
143
|
+
pluginName = (await promptDriver.promptText("Plugin name", defaultName)).trim();
|
|
118
144
|
}
|
|
119
145
|
let pluginVersion = "";
|
|
120
146
|
while (!pluginVersion) {
|
|
121
|
-
pluginVersion = (await promptText(
|
|
147
|
+
pluginVersion = (await promptDriver.promptText("Version", "0.1.0")).trim();
|
|
122
148
|
}
|
|
123
|
-
const defaultEnabled = await promptBoolean(
|
|
124
|
-
const runNpmInstall = await promptBoolean(
|
|
149
|
+
const defaultEnabled = await promptDriver.promptBoolean("Default enabled", true);
|
|
150
|
+
const runNpmInstall = await promptDriver.promptBoolean("Run npm install now", true);
|
|
125
151
|
return {
|
|
126
152
|
targetDir,
|
|
127
153
|
kind,
|
|
@@ -133,7 +159,7 @@ async function collectAnswers({ forceTheme, targetHint }) {
|
|
|
133
159
|
};
|
|
134
160
|
}
|
|
135
161
|
finally {
|
|
136
|
-
|
|
162
|
+
promptDriver.close();
|
|
137
163
|
}
|
|
138
164
|
}
|
|
139
165
|
function runNpmInstall(targetDir) {
|
|
@@ -244,15 +270,15 @@ async function runInitCommand(args) {
|
|
|
244
270
|
throw new Error(`target path exists but is not a directory: ${answers.targetDir}`);
|
|
245
271
|
}
|
|
246
272
|
else if (directoryHasEntries(answers.targetDir)) {
|
|
247
|
-
const
|
|
273
|
+
const promptDriver = createReadlinePromptDriver();
|
|
248
274
|
try {
|
|
249
|
-
const proceed = await promptBoolean(
|
|
275
|
+
const proceed = await promptDriver.promptBoolean(`Directory ${answers.targetDir} is not empty. Continue and overwrite known files`, false);
|
|
250
276
|
if (!proceed) {
|
|
251
277
|
throw new Error("aborted by user");
|
|
252
278
|
}
|
|
253
279
|
}
|
|
254
280
|
finally {
|
|
255
|
-
|
|
281
|
+
promptDriver.close();
|
|
256
282
|
}
|
|
257
283
|
}
|
|
258
284
|
if (answers.kind === "module") {
|
|
@@ -272,3 +298,10 @@ function isUsageError(error) {
|
|
|
272
298
|
"code" in error &&
|
|
273
299
|
typeof error.code === "string");
|
|
274
300
|
}
|
|
301
|
+
exports.__private = {
|
|
302
|
+
collectAnswers,
|
|
303
|
+
createReadlinePromptDriver,
|
|
304
|
+
defaultPluginIdFromDir,
|
|
305
|
+
sanitizeIdToken,
|
|
306
|
+
validatePluginId,
|
|
307
|
+
};
|
package/package.json
CHANGED
package/src/lib/dist.ts
CHANGED
|
@@ -30,8 +30,6 @@ interface ManifestInfo {
|
|
|
30
30
|
interface CommandResult {
|
|
31
31
|
status: number | null;
|
|
32
32
|
error?: Error;
|
|
33
|
-
stdout?: string | null;
|
|
34
|
-
stderr?: string | null;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
function npmExecutable(): string {
|
|
@@ -92,13 +90,29 @@ export function parseDistArgs(args: string[]): DistArgs {
|
|
|
92
90
|
|
|
93
91
|
function readManifest(pluginDir: string): ManifestInfo {
|
|
94
92
|
const manifestPath = path.join(pluginDir, "openvcs.plugin.json");
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
let manifestRaw: string;
|
|
94
|
+
let manifestFd: number | undefined;
|
|
95
|
+
let manifest: unknown;
|
|
96
|
+
try {
|
|
97
|
+
manifestFd = fs.openSync(manifestPath, "r");
|
|
98
|
+
const manifestStat = fs.fstatSync(manifestFd);
|
|
99
|
+
if (!manifestStat.isFile()) {
|
|
100
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
101
|
+
}
|
|
102
|
+
manifestRaw = fs.readFileSync(manifestFd, "utf8");
|
|
103
|
+
} catch (error: unknown) {
|
|
104
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
105
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
} finally {
|
|
109
|
+
if (typeof manifestFd === "number") {
|
|
110
|
+
fs.closeSync(manifestFd);
|
|
111
|
+
}
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
let manifest: unknown;
|
|
100
114
|
try {
|
|
101
|
-
manifest = JSON.parse(
|
|
115
|
+
manifest = JSON.parse(manifestRaw);
|
|
102
116
|
} catch (error: unknown) {
|
|
103
117
|
const detail = error instanceof Error ? error.message : String(error);
|
|
104
118
|
throw new Error(`parse ${manifestPath}: ${detail}`);
|
|
@@ -161,28 +175,17 @@ function runCommand(program: string, args: string[], cwd: string, verbose: boole
|
|
|
161
175
|
|
|
162
176
|
const result = spawnSync(program, args, {
|
|
163
177
|
cwd,
|
|
164
|
-
|
|
165
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
178
|
+
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
166
179
|
}) as CommandResult;
|
|
167
180
|
|
|
168
181
|
if (result.error) {
|
|
169
182
|
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
170
183
|
}
|
|
171
184
|
if (result.status === 0) {
|
|
172
|
-
if (verbose) {
|
|
173
|
-
if (result.stdout?.trim()) {
|
|
174
|
-
process.stderr.write(`${result.stdout.trim()}\n`);
|
|
175
|
-
}
|
|
176
|
-
if (result.stderr?.trim()) {
|
|
177
|
-
process.stderr.write(`${result.stderr.trim()}\n`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
185
|
return;
|
|
181
186
|
}
|
|
182
187
|
|
|
183
|
-
throw new Error(
|
|
184
|
-
`command failed (${program} ${args.join(" ")}), exit code ${result.status}, stdout='${(result.stdout || "").trim()}', stderr='${(result.stderr || "").trim()}'`
|
|
185
|
-
);
|
|
188
|
+
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
function ensurePackageLock(pluginDir: string, verbose: boolean): void {
|
package/src/lib/init.ts
CHANGED
|
@@ -23,6 +23,12 @@ interface CollectAnswersOptions {
|
|
|
23
23
|
targetHint?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
interface PromptDriver {
|
|
27
|
+
promptText(label: string, defaultValue?: string): Promise<string>;
|
|
28
|
+
promptBoolean(label: string, defaultValue: boolean): Promise<boolean>;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
interface InitCommandError {
|
|
27
33
|
code?: string;
|
|
28
34
|
}
|
|
@@ -68,37 +74,49 @@ function defaultPluginNameFromId(pluginId: string): string {
|
|
|
68
74
|
return words.length > 0 ? words.join(" ") : "OpenVCS Plugin";
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
function validatePluginId(pluginId: string): string | undefined {
|
|
78
|
+
if (!pluginId) {
|
|
79
|
+
return "Plugin id is required.";
|
|
80
|
+
}
|
|
81
|
+
if (pluginId === "." || pluginId === "..") {
|
|
82
|
+
return "Plugin id must not be '.' or '..'.";
|
|
83
|
+
}
|
|
84
|
+
if (pluginId.includes("/") || pluginId.includes("\\")) {
|
|
85
|
+
return "Plugin id must not contain path separators (/ or \\).";
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
rl
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
function createReadlinePromptDriver(output: NodeJS.WritableStream = process.stderr): PromptDriver {
|
|
91
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
92
|
+
return {
|
|
93
|
+
async promptText(label: string, defaultValue = ""): Promise<string> {
|
|
94
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
95
|
+
const answer = await rl.question(`${label}${suffix}: `);
|
|
96
|
+
const trimmed = answer.trim();
|
|
97
|
+
return trimmed || defaultValue;
|
|
98
|
+
},
|
|
99
|
+
async promptBoolean(label: string, defaultValue: boolean): Promise<boolean> {
|
|
100
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
101
|
+
while (true) {
|
|
102
|
+
const answer = await rl.question(`${label} (${suffix}): `);
|
|
103
|
+
const normalized = answer.trim().toLowerCase();
|
|
104
|
+
if (!normalized) {
|
|
105
|
+
return defaultValue;
|
|
106
|
+
}
|
|
107
|
+
if (normalized === "y" || normalized === "yes") {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (normalized === "n" || normalized === "no") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
output.write("Please answer yes or no.\n");
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
close(): void {
|
|
117
|
+
rl.close();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
function writeJson(filePath: string, value: unknown): void {
|
|
@@ -116,11 +134,14 @@ function directoryHasEntries(targetDir: string): boolean {
|
|
|
116
134
|
return entries.length > 0;
|
|
117
135
|
}
|
|
118
136
|
|
|
119
|
-
async function collectAnswers(
|
|
137
|
+
async function collectAnswers(
|
|
138
|
+
{ forceTheme, targetHint }: CollectAnswersOptions,
|
|
139
|
+
promptDriver: PromptDriver = createReadlinePromptDriver(),
|
|
140
|
+
output: NodeJS.WritableStream = process.stderr
|
|
141
|
+
): Promise<InitAnswers> {
|
|
120
142
|
const defaultTarget = targetHint || path.join(process.cwd(), "openvcs-plugin");
|
|
121
|
-
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
122
143
|
try {
|
|
123
|
-
const targetText = await promptText(
|
|
144
|
+
const targetText = await promptDriver.promptText("Target directory", defaultTarget);
|
|
124
145
|
const targetDir = path.resolve(targetText);
|
|
125
146
|
|
|
126
147
|
let kind: "module" | "theme" = "module";
|
|
@@ -128,7 +149,7 @@ async function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions)
|
|
|
128
149
|
kind = "theme";
|
|
129
150
|
} else {
|
|
130
151
|
while (true) {
|
|
131
|
-
const value = (await promptText(
|
|
152
|
+
const value = (await promptDriver.promptText("Template type (module/theme)", "module"))
|
|
132
153
|
.trim()
|
|
133
154
|
.toLowerCase();
|
|
134
155
|
if (value === "module" || value === "m") {
|
|
@@ -139,29 +160,35 @@ async function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions)
|
|
|
139
160
|
kind = "theme";
|
|
140
161
|
break;
|
|
141
162
|
}
|
|
142
|
-
|
|
163
|
+
output.write("Please choose 'module' or 'theme'.\n");
|
|
143
164
|
}
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
const defaultId = defaultPluginIdFromDir(targetDir);
|
|
147
|
-
let pluginId
|
|
168
|
+
let pluginId: string | undefined;
|
|
148
169
|
while (!pluginId) {
|
|
149
|
-
|
|
170
|
+
const candidateId = (await promptDriver.promptText("Plugin id", defaultId)).trim();
|
|
171
|
+
const validationError = validatePluginId(candidateId);
|
|
172
|
+
if (!validationError) {
|
|
173
|
+
pluginId = candidateId;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
output.write(`${validationError}\n`);
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
const defaultName = defaultPluginNameFromId(pluginId);
|
|
153
180
|
let pluginName = "";
|
|
154
181
|
while (!pluginName) {
|
|
155
|
-
pluginName = (await promptText(
|
|
182
|
+
pluginName = (await promptDriver.promptText("Plugin name", defaultName)).trim();
|
|
156
183
|
}
|
|
157
184
|
|
|
158
185
|
let pluginVersion = "";
|
|
159
186
|
while (!pluginVersion) {
|
|
160
|
-
pluginVersion = (await promptText(
|
|
187
|
+
pluginVersion = (await promptDriver.promptText("Version", "0.1.0")).trim();
|
|
161
188
|
}
|
|
162
189
|
|
|
163
|
-
const defaultEnabled = await promptBoolean(
|
|
164
|
-
const runNpmInstall = await promptBoolean(
|
|
190
|
+
const defaultEnabled = await promptDriver.promptBoolean("Default enabled", true);
|
|
191
|
+
const runNpmInstall = await promptDriver.promptBoolean("Run npm install now", true);
|
|
165
192
|
|
|
166
193
|
return {
|
|
167
194
|
targetDir,
|
|
@@ -173,7 +200,7 @@ async function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions)
|
|
|
173
200
|
runNpmInstall,
|
|
174
201
|
};
|
|
175
202
|
} finally {
|
|
176
|
-
|
|
203
|
+
promptDriver.close();
|
|
177
204
|
}
|
|
178
205
|
}
|
|
179
206
|
|
|
@@ -292,10 +319,9 @@ export async function runInitCommand(args: string[]): Promise<string> {
|
|
|
292
319
|
} else if (!fs.lstatSync(answers.targetDir).isDirectory()) {
|
|
293
320
|
throw new Error(`target path exists but is not a directory: ${answers.targetDir}`);
|
|
294
321
|
} else if (directoryHasEntries(answers.targetDir)) {
|
|
295
|
-
const
|
|
322
|
+
const promptDriver = createReadlinePromptDriver();
|
|
296
323
|
try {
|
|
297
|
-
const proceed = await promptBoolean(
|
|
298
|
-
rl,
|
|
324
|
+
const proceed = await promptDriver.promptBoolean(
|
|
299
325
|
`Directory ${answers.targetDir} is not empty. Continue and overwrite known files`,
|
|
300
326
|
false
|
|
301
327
|
);
|
|
@@ -303,7 +329,7 @@ export async function runInitCommand(args: string[]): Promise<string> {
|
|
|
303
329
|
throw new Error("aborted by user");
|
|
304
330
|
}
|
|
305
331
|
} finally {
|
|
306
|
-
|
|
332
|
+
promptDriver.close();
|
|
307
333
|
}
|
|
308
334
|
}
|
|
309
335
|
|
|
@@ -328,3 +354,11 @@ export function isUsageError(error: unknown): error is InitCommandError {
|
|
|
328
354
|
typeof (error as InitCommandError).code === "string"
|
|
329
355
|
);
|
|
330
356
|
}
|
|
357
|
+
|
|
358
|
+
export const __private = {
|
|
359
|
+
collectAnswers,
|
|
360
|
+
createReadlinePromptDriver,
|
|
361
|
+
defaultPluginIdFromDir,
|
|
362
|
+
sanitizeIdToken,
|
|
363
|
+
validatePluginId,
|
|
364
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
const { __private } = require("../lib/init");
|
|
6
|
+
|
|
7
|
+
test("validatePluginId accepts regular ids", () => {
|
|
8
|
+
assert.equal(__private.validatePluginId("my.plugin"), undefined);
|
|
9
|
+
assert.equal(__private.validatePluginId("my-plugin_1"), undefined);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("validatePluginId rejects empty id", () => {
|
|
13
|
+
assert.match(__private.validatePluginId(""), /required/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("validatePluginId rejects dot segments", () => {
|
|
17
|
+
assert.match(__private.validatePluginId("."), /must not be/);
|
|
18
|
+
assert.match(__private.validatePluginId(".."), /must not be/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("validatePluginId rejects path separators", () => {
|
|
22
|
+
assert.match(__private.validatePluginId("bad/id"), /path separators/);
|
|
23
|
+
assert.match(__private.validatePluginId("bad\\id"), /path separators/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("collectAnswers re-prompts invalid plugin id", async () => {
|
|
27
|
+
const prompts = [
|
|
28
|
+
"plugin-dir",
|
|
29
|
+
"module",
|
|
30
|
+
"bad/id",
|
|
31
|
+
"good-id",
|
|
32
|
+
"Good Plugin",
|
|
33
|
+
"0.2.0",
|
|
34
|
+
];
|
|
35
|
+
const booleans = [true, false];
|
|
36
|
+
const messages = [];
|
|
37
|
+
|
|
38
|
+
const promptDriver = {
|
|
39
|
+
async promptText() {
|
|
40
|
+
return prompts.shift() || "";
|
|
41
|
+
},
|
|
42
|
+
async promptBoolean() {
|
|
43
|
+
return booleans.shift() || false;
|
|
44
|
+
},
|
|
45
|
+
close() {},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const output = {
|
|
49
|
+
write(message) {
|
|
50
|
+
messages.push(message);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const answers = await __private.collectAnswers(
|
|
55
|
+
{ forceTheme: false, targetHint: "plugin-dir" },
|
|
56
|
+
promptDriver,
|
|
57
|
+
output
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
assert.equal(answers.pluginId, "good-id");
|
|
61
|
+
assert.equal(answers.targetDir, path.resolve("plugin-dir"));
|
|
62
|
+
assert.equal(answers.defaultEnabled, true);
|
|
63
|
+
assert.equal(answers.runNpmInstall, false);
|
|
64
|
+
assert.equal(messages.some((message) => message.includes("must not contain path separators")), true);
|
|
65
|
+
});
|