@vobs/cli 0.1.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/LICENSE +21 -0
- package/dist/enterprise-template.d.ts +83 -0
- package/dist/enterprise-template.js +656 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +271 -0
- package/dist/logo/cli-logo.d.ts +5 -0
- package/dist/logo/cli-logo.js +16 -0
- package/dist/scaffold.d.ts +23 -0
- package/dist/scaffold.js +449 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** @license MIT
|
|
3
|
+
* Copyright (c) 2026 vobsjs
|
|
4
|
+
* @vobs/cli
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { realpathSync } from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { checkViewProject } from '@vobs/compiler-dom';
|
|
13
|
+
import { renderCliLogo } from './logo/cli-logo.js';
|
|
14
|
+
import { createScaffold, isScaffoldTemplate, scaffoldSuccessText, } from './scaffold.js';
|
|
15
|
+
export function runVobsCli(args, streams = { stdout: process.stdout, stderr: process.stderr }) {
|
|
16
|
+
const { args: commandArgs, shouldShowLogo } = readLogoOption(args);
|
|
17
|
+
if (shouldShowLogo) {
|
|
18
|
+
streams.stdout.write(`${renderCliLogo()}\n\n`);
|
|
19
|
+
}
|
|
20
|
+
const parsed = parseArgs(commandArgs);
|
|
21
|
+
if (parsed.command === 'help') {
|
|
22
|
+
streams.stdout.write(helpText());
|
|
23
|
+
return Promise.resolve(0);
|
|
24
|
+
}
|
|
25
|
+
if (parsed.command === 'new') {
|
|
26
|
+
return runNewCommand(parsed, streams);
|
|
27
|
+
}
|
|
28
|
+
const result = checkViewProject({
|
|
29
|
+
tsconfigPath: parsed.tsconfigPath,
|
|
30
|
+
...(parsed.viewFiles === undefined ? {} : { viewFiles: parsed.viewFiles }),
|
|
31
|
+
});
|
|
32
|
+
const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === 'error');
|
|
33
|
+
const warnings = result.diagnostics.filter((diagnostic) => diagnostic.severity === 'warning');
|
|
34
|
+
if (errors.length === 0) {
|
|
35
|
+
if (warnings.length > 0) {
|
|
36
|
+
streams.stderr.write(`vobs check warnings\n${formatDiagnostics(warnings)}\n`);
|
|
37
|
+
}
|
|
38
|
+
streams.stdout.write(`vobs check passed (${result.units.length} view unit(s)).\n`);
|
|
39
|
+
return Promise.resolve(0);
|
|
40
|
+
}
|
|
41
|
+
streams.stderr.write(`vobs check failed\n${formatDiagnostics(errors)}${warnings.length === 0 ? '' : `\nvobs check warnings\n${formatDiagnostics(warnings)}`}\n`);
|
|
42
|
+
return Promise.resolve(1);
|
|
43
|
+
}
|
|
44
|
+
function readLogoOption(args) {
|
|
45
|
+
return {
|
|
46
|
+
args: args.filter((arg) => arg !== '--no-logo'),
|
|
47
|
+
shouldShowLogo: !args.includes('--no-logo'),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function parseArgs(args) {
|
|
51
|
+
const [command, ...rest] = args;
|
|
52
|
+
if (command === undefined || command === '--help' || command === '-h')
|
|
53
|
+
return { command: 'help' };
|
|
54
|
+
if (command === 'new' || command === 'create')
|
|
55
|
+
return parseNewArgs(rest, command === 'create');
|
|
56
|
+
if (command !== 'check')
|
|
57
|
+
throw new Error(`Unknown command: ${command}`);
|
|
58
|
+
let tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
59
|
+
const viewFiles = [];
|
|
60
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
61
|
+
const arg = rest[index];
|
|
62
|
+
if (arg === '--help' || arg === '-h')
|
|
63
|
+
return { command: 'help' };
|
|
64
|
+
if (arg === '--tsconfig' || arg === '-p') {
|
|
65
|
+
const value = rest[index + 1];
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
throw new Error(`${arg} requires a path`);
|
|
68
|
+
tsconfigPath = path.resolve(value);
|
|
69
|
+
index += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === '--view') {
|
|
73
|
+
const value = rest[index + 1];
|
|
74
|
+
if (value === undefined)
|
|
75
|
+
throw new Error('--view requires a path');
|
|
76
|
+
viewFiles.push(path.resolve(value));
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Unknown option: ${arg ?? ''}`);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
command: 'check',
|
|
84
|
+
tsconfigPath,
|
|
85
|
+
viewFiles: viewFiles.length === 0 ? undefined : viewFiles,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function parseNewArgs(args, deprecatedAlias) {
|
|
89
|
+
let type;
|
|
90
|
+
let name;
|
|
91
|
+
let template;
|
|
92
|
+
let withDi;
|
|
93
|
+
let force = false;
|
|
94
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
95
|
+
const arg = args[index];
|
|
96
|
+
if (arg === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
if (arg === '--help' || arg === '-h')
|
|
99
|
+
return { command: 'help' };
|
|
100
|
+
if (arg === '--template' || arg === '-t') {
|
|
101
|
+
const value = args[index + 1];
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
throw new Error(`${arg} requires a template`);
|
|
104
|
+
if (!isScaffoldTemplate(value))
|
|
105
|
+
throw new Error(`Unknown template: ${value}`);
|
|
106
|
+
template = value;
|
|
107
|
+
index += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (arg === '--with-di') {
|
|
111
|
+
withDi = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (arg === '--force') {
|
|
115
|
+
force = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (arg.startsWith('-'))
|
|
119
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
120
|
+
if (type === undefined) {
|
|
121
|
+
type = arg;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (name === undefined) {
|
|
125
|
+
name = arg;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Unexpected argument: ${arg ?? ''}`);
|
|
129
|
+
}
|
|
130
|
+
const resolvedType = template === undefined ? type : type === template ? undefined : type;
|
|
131
|
+
const resolvedName = template === undefined ? name : (name ?? type);
|
|
132
|
+
return {
|
|
133
|
+
command: 'new',
|
|
134
|
+
type: resolvedType,
|
|
135
|
+
name: resolvedName,
|
|
136
|
+
options: {
|
|
137
|
+
...(template === undefined ? {} : { template }),
|
|
138
|
+
...(withDi === undefined ? {} : { withDi }),
|
|
139
|
+
...(force ? { force } : {}),
|
|
140
|
+
},
|
|
141
|
+
deprecatedAlias,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function runNewCommand(command, streams) {
|
|
145
|
+
if (command.deprecatedAlias) {
|
|
146
|
+
streams.stderr.write('Warning: "vobs create" is deprecated. Use "vobs new" instead.\n');
|
|
147
|
+
}
|
|
148
|
+
const completed = await completeNewCommand(command, streams);
|
|
149
|
+
if (completed.name !== undefined &&
|
|
150
|
+
completed.options.force === false &&
|
|
151
|
+
existsSync(path.resolve(process.cwd(), completed.name))) {
|
|
152
|
+
streams.stdout.write('Cancelled.\n');
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
const result = createScaffold(completed.type, completed.name, completed.options);
|
|
156
|
+
streams.stdout.write(scaffoldSuccessText(result));
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
async function completeNewCommand(command, streams) {
|
|
160
|
+
const needsPrompt = command.type === undefined || command.name === undefined;
|
|
161
|
+
if (!needsPrompt)
|
|
162
|
+
return command;
|
|
163
|
+
const prompt = streams.prompt ?? createNodePrompt();
|
|
164
|
+
try {
|
|
165
|
+
const type = command.options.template ?? command.type ?? (await askTemplate(prompt));
|
|
166
|
+
const name = command.name ?? (await askProjectName(prompt));
|
|
167
|
+
const withDi = command.options.withDi ??
|
|
168
|
+
(type === 'app' || type === 'lib'
|
|
169
|
+
? await prompt.confirm('Include @vobs/di integration?', false)
|
|
170
|
+
: undefined);
|
|
171
|
+
const force = command.options.force === true
|
|
172
|
+
? true
|
|
173
|
+
: existsSync(path.resolve(process.cwd(), name))
|
|
174
|
+
? await prompt.confirm(`Directory "${name}" already exists. Overwrite?`, false)
|
|
175
|
+
: undefined;
|
|
176
|
+
return {
|
|
177
|
+
...command,
|
|
178
|
+
type,
|
|
179
|
+
name,
|
|
180
|
+
options: {
|
|
181
|
+
...command.options,
|
|
182
|
+
...(withDi === undefined ? {} : { withDi }),
|
|
183
|
+
...(force === undefined ? {} : { force }),
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
if (streams.prompt === undefined)
|
|
189
|
+
prompt.close?.();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function askTemplate(prompt) {
|
|
193
|
+
while (true) {
|
|
194
|
+
const value = await prompt.text('Template (app, app-di, enterprise, lib, lib-di)', 'app');
|
|
195
|
+
if (isScaffoldTemplate(value))
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function askProjectName(prompt) {
|
|
200
|
+
while (true) {
|
|
201
|
+
const value = await prompt.text('Project name');
|
|
202
|
+
if (/^[a-z0-9][a-z0-9-]*$/u.test(value))
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function createNodePrompt() {
|
|
207
|
+
const terminal = createInterface({
|
|
208
|
+
input: process.stdin,
|
|
209
|
+
output: process.stdout,
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
async text(question, initial) {
|
|
213
|
+
const suffix = initial === undefined ? ': ' : ` (${initial}): `;
|
|
214
|
+
const answer = (await terminal.question(`${question}${suffix}`)).trim();
|
|
215
|
+
return answer === '' && initial !== undefined ? initial : answer;
|
|
216
|
+
},
|
|
217
|
+
async confirm(question, initial = false) {
|
|
218
|
+
const suffix = initial ? ' (Y/n): ' : ' (y/N): ';
|
|
219
|
+
const answer = (await terminal.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
220
|
+
if (answer === '')
|
|
221
|
+
return initial;
|
|
222
|
+
return answer === 'y' || answer === 'yes';
|
|
223
|
+
},
|
|
224
|
+
close() {
|
|
225
|
+
terminal.close();
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function formatDiagnostics(diagnostics) {
|
|
230
|
+
return diagnostics.map(formatDiagnostic).join('\n');
|
|
231
|
+
}
|
|
232
|
+
function formatDiagnostic(diagnostic) {
|
|
233
|
+
const source = diagnostic.sourceId ?? 'project';
|
|
234
|
+
const position = diagnostic.start === undefined ? '' : `:${diagnostic.start.line}:${diagnostic.start.column}`;
|
|
235
|
+
return `${source}${position} ${diagnostic.severity.toUpperCase()} ${diagnostic.code}: ${diagnostic.message}`;
|
|
236
|
+
}
|
|
237
|
+
function helpText() {
|
|
238
|
+
return [
|
|
239
|
+
'Usage:',
|
|
240
|
+
' vobs check [--tsconfig tsconfig.json] [--view view.html]',
|
|
241
|
+
' vobs new [app|app-di|enterprise|lib|lib-di] [name] [--with-di] [--force]',
|
|
242
|
+
'',
|
|
243
|
+
'Check options:',
|
|
244
|
+
' -p, --tsconfig <path> TypeScript project file. Defaults to ./tsconfig.json.',
|
|
245
|
+
' --view <path> Require a specific HTML view to have exactly one owner.',
|
|
246
|
+
'',
|
|
247
|
+
'New options:',
|
|
248
|
+
' -t, --template <name> Template: app, app-di, enterprise, lib, lib-di.',
|
|
249
|
+
' --with-di Include DI integration for app/lib templates.',
|
|
250
|
+
' --force Overwrite an existing project directory.',
|
|
251
|
+
'',
|
|
252
|
+
'Global options:',
|
|
253
|
+
' --no-logo Hide the startup logo.',
|
|
254
|
+
' -h, --help Show this help.',
|
|
255
|
+
'',
|
|
256
|
+
'Example:',
|
|
257
|
+
' vobs new enterprise my-admin',
|
|
258
|
+
'',
|
|
259
|
+
].join('\n');
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
if (process.argv[1] !== undefined &&
|
|
263
|
+
realpathSync.native(fileURLToPath(import.meta.url)) ===
|
|
264
|
+
realpathSync.native(path.resolve(process.argv[1]))) {
|
|
265
|
+
process.exitCode = await runVobsCli(process.argv.slice(2));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/cli
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
const BRAND_COLOR = '#2DD4BF';
|
|
7
|
+
const LOGO_MARK = ['╭──────╮', '│ vobs │', '╰──────╯'];
|
|
8
|
+
const PRODUCT_NAME = 'vobs';
|
|
9
|
+
export function renderCliLogo() {
|
|
10
|
+
const brand = chalk.hex(BRAND_COLOR);
|
|
11
|
+
return [
|
|
12
|
+
brand(LOGO_MARK[0]),
|
|
13
|
+
`${brand(LOGO_MARK[1])} ${brand.bold(PRODUCT_NAME)}`,
|
|
14
|
+
brand(LOGO_MARK[2]),
|
|
15
|
+
].join('\n');
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/cli
|
|
4
|
+
*/
|
|
5
|
+
export type ScaffoldTemplate = 'app' | 'app-di' | 'enterprise' | 'lib' | 'lib-di';
|
|
6
|
+
export interface ScaffoldOptions {
|
|
7
|
+
readonly template?: ScaffoldTemplate;
|
|
8
|
+
readonly withDi?: boolean;
|
|
9
|
+
readonly force?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ScaffoldResult {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly targetDir: string;
|
|
14
|
+
readonly template: ScaffoldTemplate;
|
|
15
|
+
}
|
|
16
|
+
export interface TemplateFile {
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly content: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function createScaffold(type: string | undefined, name: string | undefined, options?: ScaffoldOptions, cwd?: string): ScaffoldResult;
|
|
21
|
+
export declare function resolveScaffoldTemplate(type: string | undefined, options?: ScaffoldOptions): ScaffoldTemplate;
|
|
22
|
+
export declare function isScaffoldTemplate(value: string): value is ScaffoldTemplate;
|
|
23
|
+
export declare function scaffoldSuccessText(result: ScaffoldResult): string;
|