@vercube/cli 0.0.48 → 1.0.0-beta.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 CHANGED
@@ -12,7 +12,7 @@
12
12
  ![GitHub License](<https://img.shields.io/github/license/vercube/vercube?style=for-the-badge&logo=gitbook&logoColor=rgba(255%2C%20255%2C%20255%2C%200.6)&labelColor=%23000&color=%232f2f2f>)
13
13
  ![Codecov](<https://img.shields.io/codecov/c/github/vercube/vercube?style=for-the-badge&logo=vitest&logoColor=rgba(255%2C%20255%2C%20255%2C%200.6)&labelColor=%23000&color=%232f2f2f>)
14
14
 
15
- **Dev server, production builds, all from the command line. `vercube dev` to start, `vercube build` when you're ready to ship.**
15
+ **Dev server, production builds, custom commands - all from the command line. `vercube dev` to start, `vercube build` when you're ready to ship.**
16
16
 
17
17
  [Website](https://vercube.dev) • [Documentation](https://vercube.dev/docs/getting-started)
18
18
 
@@ -22,6 +22,10 @@
22
22
 
23
23
  - **Dev server** - hot-reload out of the box
24
24
  - **Production build** - optimized bundles ready for deployment
25
+ - **Project scaffolding** - `vercube init` to create a new project from the starter template
26
+ - **Endpoint testing** - `vercube fetch /path` to test endpoints without a running server
27
+ - **Custom commands** - define your own commands with `@Command`, `@Arg`, `@Flag` decorators
28
+ - **Dependency injection** - commands integrate with `@vercube/di` for service injection
25
29
  - **Config loading** - reads `vercube.config.ts` automatically
26
30
 
27
31
  ## šŸ“¦ Installation
@@ -30,6 +34,26 @@
30
34
  pnpm add @vercube/cli
31
35
  ```
32
36
 
37
+ ### Custom commands
38
+
39
+ Import from `@vercube/cli/toolkit` and register in `vercube.config.ts`:
40
+
41
+ ```ts
42
+ import { BaseCommand, Command, Flag } from '@vercube/cli/toolkit';
43
+
44
+ @Command({ name: 'deploy', description: 'Deploy to production' })
45
+ export class DeployCommand extends BaseCommand {
46
+ @Flag({ name: 'env', description: 'Target environment', default: 'staging' })
47
+ public env: string;
48
+
49
+ public override async run(): Promise<void> {
50
+ console.log(`Deploying to ${this.env}...`);
51
+ }
52
+ }
53
+ ```
54
+
55
+ See the [Custom CLI Command](https://vercube.dev/docs/advanced/custom-cli-command) docs for the full API.
56
+
33
57
  ## šŸ“œ License
34
58
 
35
59
  [MIT](https://github.com/vercube/vercube/blob/main/LICENSE)
@@ -0,0 +1,136 @@
1
+ //#region src/BaseCommand.ts
2
+ /**
3
+ * Base class for all CLI commands.
4
+ *
5
+ * Extend this class and decorate with `@Command` to register a command.
6
+ * Dependencies can be injected via `@Inject` from `@vercube/di`.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * @Command({ name: 'deploy', description: 'Deploy application' })
11
+ * export class DeployCommand extends BaseCommand {
12
+ * @Inject(MyService)
13
+ * private readonly gMyService!: MyService;
14
+ *
15
+ * public override async run(): Promise<void> {
16
+ * await this.gMyService.deploy();
17
+ * }
18
+ * }
19
+ * ```
20
+ */
21
+ var BaseCommand = class {
22
+ /**
23
+ * CLI DI container, injected by CommandRegistry before `run()`.
24
+ * Prefer `@Inject` decorators over accessing this directly.
25
+ */
26
+ container;
27
+ };
28
+ //#endregion
29
+ //#region src/Decorators/Arg.ts
30
+ const COMMAND_ARGS_KEY = "__commandArgs";
31
+ /**
32
+ * Property decorator for positional CLI arguments.
33
+ * The value is injected from parsed CLI input before `run()` is called.
34
+ *
35
+ * @param options - argument configuration
36
+ * @returns property decorator
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * @Arg({ name: 'query', description: 'Phrase to search for' })
41
+ * public query: string;
42
+ * ```
43
+ */
44
+ function Arg(options) {
45
+ return (target, propertyKey) => {
46
+ if (!target["__commandArgs"]) target[COMMAND_ARGS_KEY] = [];
47
+ const def = {
48
+ kind: "positional",
49
+ property: propertyKey,
50
+ name: options.name,
51
+ description: options.description,
52
+ required: options.required
53
+ };
54
+ target[COMMAND_ARGS_KEY].push(def);
55
+ Object.defineProperty(target, propertyKey, {
56
+ configurable: true,
57
+ enumerable: true,
58
+ writable: true,
59
+ value: void 0
60
+ });
61
+ };
62
+ }
63
+ //#endregion
64
+ //#region src/Decorators/Command.ts
65
+ const COMMAND_META_KEY = "__commandMeta";
66
+ /**
67
+ * Class decorator that marks a class as a CLI command.
68
+ * Stores command metadata on the prototype so `CommandRegistry` can discover it.
69
+ *
70
+ * @param meta - command configuration (name, description, optional subcommands)
71
+ * @returns class decorator
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * @Command({ name: 'build', description: 'Build the project' })
76
+ * export class BuildCommand extends BaseCommand { ... }
77
+ * ```
78
+ */
79
+ function Command(meta) {
80
+ return (target) => {
81
+ target.prototype[COMMAND_META_KEY] = meta;
82
+ };
83
+ }
84
+ //#endregion
85
+ //#region src/Decorators/Flag.ts
86
+ /**
87
+ * Property decorator for named CLI flags (`--name value`).
88
+ * The value type is inferred from `default` when `type` is not set.
89
+ * The parsed value is injected before `run()` is called.
90
+ *
91
+ * @param options - flag configuration
92
+ * @returns property decorator
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * @Flag({ name: 'limit', description: 'Max results', default: 10 })
97
+ * public limit: number;
98
+ *
99
+ * @Flag({ name: 'json', description: 'Output as JSON', default: false })
100
+ * public json: boolean;
101
+ * ```
102
+ */
103
+ function Flag(options) {
104
+ return (target, propertyKey) => {
105
+ if (!target["__commandArgs"]) target[COMMAND_ARGS_KEY] = [];
106
+ const def = {
107
+ kind: "flag",
108
+ property: propertyKey,
109
+ name: options.name,
110
+ description: options.description,
111
+ default: options.default,
112
+ required: options.required,
113
+ valueType: options.type ?? inferType(options.default)
114
+ };
115
+ target[COMMAND_ARGS_KEY].push(def);
116
+ Object.defineProperty(target, propertyKey, {
117
+ configurable: true,
118
+ enumerable: true,
119
+ writable: true,
120
+ value: options.default
121
+ });
122
+ };
123
+ }
124
+ /**
125
+ * Infers the citty arg type from a default value.
126
+ *
127
+ * @param value - default value to inspect
128
+ * @returns corresponding citty arg type string
129
+ */
130
+ function inferType(value) {
131
+ if (typeof value === "boolean") return "boolean";
132
+ if (typeof value === "number") return "number";
133
+ return "string";
134
+ }
135
+ //#endregion
136
+ export { COMMAND_ARGS_KEY as a, Arg as i, COMMAND_META_KEY as n, BaseCommand as o, Command as r, Flag as t };
package/dist/index.mjs CHANGED
@@ -1,19 +1,530 @@
1
1
  #!/usr/bin/env node
2
+ import { i as Arg, n as COMMAND_META_KEY, o as BaseCommand, r as Command, t as Flag } from "./Flag-qtOTYeqz.mjs";
3
+ import { createRequire } from "node:module";
2
4
  import { defineCommand, runMain } from "citty";
5
+ import { Container } from "@vercube/di";
6
+ import { BaseLogger, Logger } from "@vercube/logger";
7
+ import { dirname, resolve } from "node:path";
8
+ import { loadVercubeConfig } from "@vercube/core";
9
+ import { createJiti } from "jiti";
10
+ import { build, createDevServer, createVercube, watch } from "@vercube/devkit";
11
+ import { cliFetch } from "srvx/cli";
12
+ import { existsSync } from "node:fs";
13
+ import { consola } from "consola";
14
+ import { colors } from "consola/utils";
15
+ import { downloadTemplate, startShell } from "giget";
16
+ import { installDependencies } from "nypm";
17
+ import { relative, resolve as resolve$1 } from "pathe";
18
+ import { hasTTY } from "std-env";
19
+ import { x } from "tinyexec";
20
+ //#region package.json
21
+ var version = "1.0.0-beta.2";
22
+ //#endregion
23
+ //#region src/Utils/CommandUtils.ts
24
+ /**
25
+ * Reads `@Command` metadata from a class prototype.
26
+ *
27
+ * @param CommandClass - command class constructor to inspect
28
+ * @returns command metadata stored by the `@Command` decorator
29
+ * @throws if the class is not decorated with `@Command`
30
+ */
31
+ function getCommandMeta(CommandClass) {
32
+ const meta = CommandClass.prototype[COMMAND_META_KEY];
33
+ if (!meta) throw new Error(`[CommandRegistry] Class "${CommandClass.name}" is missing the @Command decorator.`);
34
+ return meta;
35
+ }
36
+ /**
37
+ * Reads all `@Arg` / `@Flag` definitions from a class prototype.
38
+ *
39
+ * @param CommandClass - command class constructor to inspect
40
+ * @returns array of arg/flag definitions, or `[]` if none exist
41
+ */
42
+ function getArgDefs(CommandClass) {
43
+ return CommandClass.prototype["__commandArgs"] ?? [];
44
+ }
45
+ /**
46
+ * Converts `ArgDef` entries into a citty-compatible args record.
47
+ *
48
+ * @param defs - arg/flag definitions to convert
49
+ * @returns record of citty argument descriptors keyed by name
50
+ */
51
+ function buildCittyArgs(defs) {
52
+ const args = {};
53
+ for (const def of defs) if (def.kind === "positional") args[def.name] = {
54
+ type: "positional",
55
+ description: def.description,
56
+ required: def.required
57
+ };
58
+ else args[def.name] = {
59
+ type: def.valueType === "boolean" ? "boolean" : "string",
60
+ description: def.description,
61
+ default: def.valueType === "number" ? String(def.default ?? "") : def.default,
62
+ required: def.required
63
+ };
64
+ return args;
65
+ }
66
+ /**
67
+ * Injects parsed citty arg values into command instance properties
68
+ * using the mappings defined by `@Arg` / `@Flag` decorators.
69
+ * Handles `number` coercion for flags with `valueType: 'number'`.
70
+ *
71
+ * @param instance - command instance to inject into
72
+ * @param parsedArgs - parsed arg values from citty
73
+ * @param defs - arg/flag definitions describing the property mapping
74
+ */
75
+ function injectArgs(instance, parsedArgs, defs) {
76
+ for (const def of defs) if (def.name in parsedArgs) {
77
+ const raw = parsedArgs[def.name];
78
+ instance[def.property] = def.valueType === "number" && typeof raw === "string" ? Number(raw) : raw;
79
+ }
80
+ }
81
+ //#endregion
82
+ //#region src/CommandRegistry.ts
83
+ /**
84
+ * Registry that stores command classes and transforms them into citty CommandDefs.
85
+ * Both built-in and user-defined commands are registered here before the CLI runs.
86
+ */
87
+ var CommandRegistry = class {
88
+ fCommands = /* @__PURE__ */ new Map();
89
+ /**
90
+ * Registers a command class decorated with `@Command`.
91
+ *
92
+ * @param CommandClass - class to register
93
+ * @throws if the class is missing the `@Command` decorator
94
+ */
95
+ register(CommandClass) {
96
+ const meta = getCommandMeta(CommandClass);
97
+ this.fCommands.set(meta.name, CommandClass);
98
+ }
99
+ /**
100
+ * Converts a single command class into a citty `CommandDef`.
101
+ * Recursively processes subcommands defined in `@Command({ subCommands })`.
102
+ *
103
+ * @param CommandClass - command class to convert
104
+ * @param container - CLI DI container used to resolve instances
105
+ * @returns citty CommandDef ready to pass to `defineCommand` / `runMain`
106
+ */
107
+ toCommandDef(CommandClass, container) {
108
+ const meta = getCommandMeta(CommandClass);
109
+ const argDefs = getArgDefs(CommandClass);
110
+ const subCommands = {};
111
+ if (meta.subCommands) for (const SubCmd of meta.subCommands) {
112
+ const subMeta = getCommandMeta(SubCmd);
113
+ subCommands[subMeta.name] = () => this.toCommandDef(SubCmd, container);
114
+ }
115
+ return defineCommand({
116
+ meta: {
117
+ name: meta.name,
118
+ description: meta.description
119
+ },
120
+ args: buildCittyArgs(argDefs),
121
+ subCommands: Object.keys(subCommands).length > 0 ? subCommands : void 0,
122
+ run: async (ctx) => {
123
+ const instance = container.resolve(CommandClass);
124
+ instance.container = container;
125
+ injectArgs(instance, ctx.args, argDefs);
126
+ await instance.run();
127
+ }
128
+ });
129
+ }
130
+ /**
131
+ * Builds the full citty subcommands map from all registered commands.
132
+ *
133
+ * @param container - CLI DI container used when executing commands
134
+ * @returns record mapping command names to lazy `CommandDef` factories
135
+ */
136
+ buildCittyTree(container) {
137
+ const tree = {};
138
+ for (const CommandClass of this.fCommands.values()) {
139
+ const meta = getCommandMeta(CommandClass);
140
+ tree[meta.name] = () => this.toCommandDef(CommandClass, container);
141
+ }
142
+ return tree;
143
+ }
144
+ };
145
+ //#endregion
146
+ //#region src/CliContainer.ts
147
+ /**
148
+ * Creates and configures a dedicated DI container for the CLI.
149
+ * Separate from the application runtime container.
150
+ * Pre-configured with an evlog-backed Logger.
151
+ *
152
+ * @returns configured CLI container
153
+ */
154
+ function createCliContainer() {
155
+ const container = new Container({ context: "cli" });
156
+ container.bind(Logger, BaseLogger);
157
+ container.get(Logger).configure({ logLevel: "info" });
158
+ container.bind(CommandRegistry);
159
+ container.flushQueue();
160
+ return container;
161
+ }
162
+ //#endregion
163
+ //#region src/CommandLoader.ts
164
+ /** @internal */
165
+ const _require = createRequire(import.meta.url);
166
+ /**
167
+ * Babel (used by jiti) emits `_initializerWarningHelper` for decorated properties
168
+ * without an explicit initializer - a function that always throws.
169
+ * Our decorators (`@Arg`, `@Flag`, `@Inject`) already define the property via
170
+ * `Object.defineProperty`, so the guard is unnecessary. We patch it out with a no-op.
171
+ *
172
+ * @see https://github.com/babel/babel/issues/12672
173
+ */
174
+ const INIT_WARNING_HELPER_RE = /function _initializerWarningHelper\b[^{]*\{[^}]*\}/g;
175
+ /**
176
+ * jiti's bundled Babel transform, resolved via jiti's package.json because
177
+ * `jiti/dist/babel.cjs` is not listed in the package exports map.
178
+ */
179
+ const babelTransform = _require(resolve(dirname(_require.resolve("jiti/package.json")), "dist/babel.cjs"));
180
+ /**
181
+ * Custom jiti instance with a patched Babel transform that replaces
182
+ * `_initializerWarningHelper` with a no-op so user command classes
183
+ * don't need `!` or explicit default values on decorated properties.
184
+ */
185
+ const jiti = createJiti(import.meta.url, {
186
+ fsCache: false,
187
+ moduleCache: false,
188
+ transform(opts) {
189
+ return { code: babelTransform(opts).code.replace(INIT_WARNING_HELPER_RE, "function _initializerWarningHelper(){return void 0}") };
190
+ }
191
+ });
192
+ /**
193
+ * Loads user-defined command classes from `vercube.config.ts` in the given directory.
194
+ * Delegates file loading to the patched jiti instance so decorated properties
195
+ * work without `!` or explicit initializers.
196
+ *
197
+ * Returns an empty array when:
198
+ * - `vercube.config.ts` does not exist in `cwd`
199
+ * - the config file has no `cli.commands` and no plugin registered commands
200
+ *
201
+ * @param cwd - directory to look for `vercube.config.ts` in (typically `process.cwd()`)
202
+ * @returns array of command class constructors, or `[]` if none found
203
+ */
204
+ async function loadUserCommands(cwd) {
205
+ try {
206
+ return (await loadVercubeConfig(void 0, {
207
+ cwd,
208
+ import: (id) => jiti.import(id)
209
+ })).cli?.commands ?? [];
210
+ } catch {
211
+ return [];
212
+ }
213
+ }
214
+ //#endregion
215
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/decorate.js
216
+ function __decorate(decorators, target, key, desc) {
217
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
218
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
219
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
220
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
221
+ }
222
+ //#endregion
223
+ //#region src/Commands/Build.ts
224
+ let BuildCommand = class BuildCommand extends BaseCommand {
225
+ /** Custom entry file path. Uses default from vercube.config.ts when omitted. */
226
+ entry;
227
+ /**
228
+ * @returns resolves when the build is done
229
+ */
230
+ async run() {
231
+ await build(await createVercube({ build: { entry: this.entry } }));
232
+ }
233
+ };
234
+ __decorate([Flag({
235
+ name: "entry",
236
+ description: "Entry file",
237
+ type: "string"
238
+ })], BuildCommand.prototype, "entry", void 0);
239
+ BuildCommand = __decorate([Command({
240
+ name: "build",
241
+ description: "Build the project"
242
+ })], BuildCommand);
243
+ //#endregion
244
+ //#region src/Commands/Dev.ts
245
+ let DevCommand = class DevCommand extends BaseCommand {
246
+ /**
247
+ * @returns resolves when the watcher is ready
248
+ */
249
+ async run() {
250
+ const app = await createVercube();
251
+ createDevServer(app);
252
+ await watch(app);
253
+ }
254
+ };
255
+ DevCommand = __decorate([Command({
256
+ name: "dev",
257
+ description: "Start development server"
258
+ })], DevCommand);
259
+ //#endregion
260
+ //#region src/Commands/Fetch.ts
261
+ let FetchCommand = class FetchCommand extends BaseCommand {
262
+ /** Target URL path, e.g. `/api/users`. */
263
+ url;
264
+ /** Entry file relative to the output directory. */
265
+ entry;
266
+ /** HTTP method. */
267
+ method;
268
+ /** Request headers as `"Name: Value, Name: Value"`. */
269
+ headers;
270
+ /** Request body. `@-` reads from stdin, `@filename` reads from a file. */
271
+ data;
272
+ /** Print request and response headers alongside the body. */
273
+ verbose;
274
+ /**
275
+ * @returns resolves when the response has been printed
276
+ */
277
+ async run() {
278
+ const app = await createVercube({ build: { dts: false } });
279
+ await build(app);
280
+ await cliFetch({
281
+ verbose: this.verbose,
282
+ dir: app.config.build?.output?.dir ?? "dist",
283
+ entry: this.entry,
284
+ url: this.url,
285
+ method: this.method,
286
+ header: this.headers?.split(",") ?? [],
287
+ data: this.data
288
+ });
289
+ }
290
+ };
291
+ __decorate([Arg({
292
+ name: "url",
293
+ description: "URL to fetch"
294
+ })], FetchCommand.prototype, "url", void 0);
295
+ __decorate([Flag({
296
+ name: "entry",
297
+ description: "Entry point for the application",
298
+ default: "index.mjs"
299
+ })], FetchCommand.prototype, "entry", void 0);
300
+ __decorate([Flag({
301
+ name: "method",
302
+ description: "HTTP method",
303
+ default: "GET"
304
+ })], FetchCommand.prototype, "method", void 0);
305
+ __decorate([Flag({
306
+ name: "headers",
307
+ description: "Add header (format: \"Name: Value, ...\")",
308
+ type: "string"
309
+ })], FetchCommand.prototype, "headers", void 0);
310
+ __decorate([Flag({
311
+ name: "data",
312
+ description: "Request body (use @- for stdin, @file for file)",
313
+ type: "string"
314
+ })], FetchCommand.prototype, "data", void 0);
315
+ __decorate([Flag({
316
+ name: "verbose",
317
+ description: "Show request and response headers",
318
+ default: false
319
+ })], FetchCommand.prototype, "verbose", void 0);
320
+ FetchCommand = __decorate([Command({
321
+ name: "fetch",
322
+ description: "Fetch a request from the application"
323
+ })], FetchCommand);
324
+ //#endregion
325
+ //#region src/Utils/Logo.ts
326
+ const startColor = {
327
+ r: 149,
328
+ g: 100,
329
+ b: 245
330
+ };
331
+ const endColor = {
332
+ r: 111,
333
+ g: 114,
334
+ b: 245
335
+ };
336
+ const icon = [
337
+ " _ ",
338
+ " | | ",
339
+ " __ _____ _ __ ___ _ _| |__ ___ ",
340
+ " \\ \\ / / _ \\ '__/ __| | | | '_ \\ / _ \\",
341
+ String.raw` \ V / __/ | | (__| |_| | |_) | __/`,
342
+ String.raw` \_/ \___|_| \___|\__,_|_.__/ \___|`,
343
+ " ",
344
+ " "
345
+ ];
346
+ function interpolateColor(startColor, endColor, factor) {
347
+ return `\u001B[38;2;${Math.round(startColor.r + factor * (endColor.r - startColor.r))};${Math.round(startColor.g + factor * (endColor.g - startColor.g))};${Math.round(startColor.b + factor * (endColor.b - startColor.b))}m`;
348
+ }
349
+ function applyGradient(icon, startColor, endColor) {
350
+ const totalLines = icon.length;
351
+ return icon.map((line, index) => {
352
+ return interpolateColor(startColor, endColor, index / (totalLines - 1)) + line;
353
+ }).join("\n");
354
+ }
355
+ const vercubeIcon = applyGradient(icon, startColor, endColor);
356
+ //#endregion
357
+ //#region src/Commands/Init.ts
358
+ const logger = consola.withTag(colors.whiteBright(colors.bold(colors.bgGreenBright(" vercube "))));
359
+ const DEFAULT_REGISTRY = "https://raw.githubusercontent.com/vercube/starter/main/templates";
360
+ const DEFAULT_TEMPLATE_NAME = "vercube";
361
+ const packageManagerOptions = Object.keys({
362
+ npm: void 0,
363
+ pnpm: void 0,
364
+ yarn: void 0,
365
+ bun: void 0,
366
+ deno: void 0,
367
+ aube: void 0
368
+ });
369
+ let InitCommand = class InitCommand extends BaseCommand {
370
+ /** Target directory. Prompted interactively when omitted. */
371
+ dir;
372
+ /** Overwrite existing directory without prompting. */
373
+ force;
374
+ /** Set to `false` to skip dependency installation. */
375
+ install;
376
+ /** Run `git init` in the new project directory. */
377
+ gitInit;
378
+ /** Package manager for dependency installation. */
379
+ packageManager;
380
+ /** Open an interactive shell in the project directory after scaffolding. */
381
+ shell;
382
+ /**
383
+ * @returns resolves when scaffolding and setup are complete
384
+ */
385
+ async run() {
386
+ if (hasTTY) process.stdout.write(`\n${vercubeIcon}\n\n`);
387
+ consola.info(`Welcome to ${colors.bold("Vercube")}!`);
388
+ let dir = this.dir ?? "";
389
+ if (!dir) dir = await logger.prompt("Where would you like to create your project?", {
390
+ placeholder: "./vercube-app",
391
+ type: "text",
392
+ default: "vercube-app",
393
+ cancel: "reject"
394
+ }).catch(() => process.exit(1));
395
+ const cwd = resolve$1(process.cwd());
396
+ let templateDownloadPath = resolve$1(cwd, dir);
397
+ logger.info(`Creating a new project in ${colors.cyan(relative(cwd, templateDownloadPath) || templateDownloadPath)}.`);
398
+ let shouldForce = Boolean(this.force);
399
+ if (!shouldForce && existsSync(templateDownloadPath)) switch (await logger.prompt(`The directory ${colors.cyan(templateDownloadPath)} already exists. What would you like to do?`, {
400
+ type: "select",
401
+ options: [
402
+ "Override its contents",
403
+ "Select different directory",
404
+ "Abort"
405
+ ]
406
+ })) {
407
+ case "Override its contents":
408
+ shouldForce = true;
409
+ break;
410
+ case "Select different directory":
411
+ templateDownloadPath = resolve$1(cwd, await logger.prompt("Please specify a different directory:", {
412
+ type: "text",
413
+ cancel: "reject"
414
+ }).catch(() => process.exit(1)));
415
+ break;
416
+ default: process.exit(1);
417
+ }
418
+ let template;
419
+ try {
420
+ template = await downloadTemplate(DEFAULT_TEMPLATE_NAME, {
421
+ dir: templateDownloadPath,
422
+ force: shouldForce,
423
+ registry: DEFAULT_REGISTRY
424
+ });
425
+ } catch (error) {
426
+ if (process.env.DEBUG) throw error;
427
+ logger.error(error.toString());
428
+ process.exit(1);
429
+ }
430
+ const packageManagerArg = this.packageManager;
431
+ const selectedPackageManager = packageManagerOptions.includes(packageManagerArg) ? packageManagerArg : await logger.prompt("Which package manager would you like to use?", {
432
+ type: "select",
433
+ options: packageManagerOptions,
434
+ cancel: "reject"
435
+ }).catch(() => process.exit(1));
436
+ if (this.install === false) logger.info("Skipping install dependencies step.");
437
+ else {
438
+ logger.start("Installing dependencies...");
439
+ try {
440
+ await installDependencies({
441
+ cwd: template.dir,
442
+ packageManager: {
443
+ name: selectedPackageManager,
444
+ command: selectedPackageManager
445
+ }
446
+ });
447
+ } catch (error) {
448
+ if (process.env.DEBUG) throw error;
449
+ logger.error(error.toString());
450
+ process.exit(1);
451
+ }
452
+ logger.success("Installation completed.");
453
+ }
454
+ let gitInit = this.gitInit;
455
+ if (gitInit === void 0) gitInit = await logger.prompt("Initialize git repository?", {
456
+ type: "confirm",
457
+ cancel: "reject"
458
+ }).catch(() => process.exit(1));
459
+ if (gitInit) {
460
+ logger.info("Initializing git repository...\n");
461
+ try {
462
+ await x("git", ["init", template.dir], {
463
+ throwOnError: true,
464
+ nodeOptions: { stdio: "inherit" }
465
+ });
466
+ } catch (error) {
467
+ logger.warn(`Failed to initialize git repository: ${error}`);
468
+ }
469
+ }
470
+ logger.log("\n✨ Vercube project has been created! Next steps:");
471
+ const relativeTemplateDir = relative(process.cwd(), template.dir) || ".";
472
+ const runCmd = selectedPackageManager === "deno" ? "task" : "run";
473
+ const nextSteps = [!this.shell && relativeTemplateDir.length > 1 && `\`cd ${relativeTemplateDir}\``, `Start development server with \`${selectedPackageManager} ${runCmd} dev\``].filter(Boolean);
474
+ for (const step of nextSteps) logger.log(` › ${step}`);
475
+ if (this.shell) startShell(template.dir);
476
+ }
477
+ };
478
+ __decorate([Arg({
479
+ name: "dir",
480
+ description: "Project directory"
481
+ })], InitCommand.prototype, "dir", void 0);
482
+ __decorate([Flag({
483
+ name: "force",
484
+ description: "Override existing directory",
485
+ default: false
486
+ })], InitCommand.prototype, "force", void 0);
487
+ __decorate([Flag({
488
+ name: "install",
489
+ description: "Install dependencies",
490
+ default: true
491
+ })], InitCommand.prototype, "install", void 0);
492
+ __decorate([Flag({
493
+ name: "gitInit",
494
+ description: "Initialize git repository",
495
+ default: true
496
+ })], InitCommand.prototype, "gitInit", void 0);
497
+ __decorate([Flag({
498
+ name: "packageManager",
499
+ description: "Package manager choice (npm, pnpm, yarn, bun)",
500
+ default: "pnpm"
501
+ })], InitCommand.prototype, "packageManager", void 0);
502
+ __decorate([Flag({
503
+ name: "shell",
504
+ description: "Open shell in project directory",
505
+ default: false
506
+ })], InitCommand.prototype, "shell", void 0);
507
+ InitCommand = __decorate([Command({
508
+ name: "init",
509
+ description: "Initialize a new Vercube app"
510
+ })], InitCommand);
3
511
  //#endregion
4
512
  //#region src/index.ts
513
+ const container = createCliContainer();
514
+ const registry = container.resolve(CommandRegistry);
515
+ registry.register(BuildCommand);
516
+ registry.register(DevCommand);
517
+ registry.register(InitCommand);
518
+ registry.register(FetchCommand);
519
+ const userCommands = await loadUserCommands(process.cwd());
520
+ for (const UserCommand of userCommands) registry.register(UserCommand);
5
521
  runMain(defineCommand({
6
522
  meta: {
7
- name: "Vercube",
8
- version: "0.0.48",
523
+ name: "vercube",
524
+ version,
9
525
  description: "Vercube CLI"
10
526
  },
11
- subCommands: {
12
- build: () => import("./build-BjzKMqLM.mjs").then(({ buildCommand }) => buildCommand),
13
- dev: () => import("./dev-FNtcz-vW.mjs").then(({ devCommand }) => devCommand),
14
- init: () => import("./init-BHbW8wC8.mjs").then(({ initCommand }) => initCommand),
15
- fetch: () => import("./fetch-UyiACOy-.mjs").then(({ fetchCommand }) => fetchCommand)
16
- }
527
+ subCommands: registry.buildCittyTree(container)
17
528
  }));
18
529
  //#endregion
19
530
  export {};
@@ -0,0 +1,135 @@
1
+ import { Container } from "@vercube/di";
2
+
3
+ //#region src/BaseCommand.d.ts
4
+ /**
5
+ * Base class for all CLI commands.
6
+ *
7
+ * Extend this class and decorate with `@Command` to register a command.
8
+ * Dependencies can be injected via `@Inject` from `@vercube/di`.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * @Command({ name: 'deploy', description: 'Deploy application' })
13
+ * export class DeployCommand extends BaseCommand {
14
+ * @Inject(MyService)
15
+ * private readonly gMyService!: MyService;
16
+ *
17
+ * public override async run(): Promise<void> {
18
+ * await this.gMyService.deploy();
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ declare abstract class BaseCommand {
24
+ /**
25
+ * CLI DI container, injected by CommandRegistry before `run()`.
26
+ * Prefer `@Inject` decorators over accessing this directly.
27
+ */
28
+ protected container: Container;
29
+ /**
30
+ * Execute the command logic. Called after arg/flag injection and DI resolution.
31
+ * @returns promise that resolves when the command is done
32
+ */
33
+ abstract run(): Promise<void>;
34
+ }
35
+ //#endregion
36
+ //#region src/Types/CliTypes.d.ts
37
+ declare namespace CliTypes {
38
+ /**
39
+ * Metadata stored by the `@Command` decorator on a class prototype.
40
+ */
41
+ interface CommandMeta {
42
+ /** Command name used on the CLI (`vercube <name>`). */
43
+ name: string;
44
+ /** Short description shown in `--help`. */
45
+ description: string;
46
+ /** Optional child command classes. */
47
+ subCommands?: (new () => unknown)[];
48
+ }
49
+ /**
50
+ * Single arg/flag definition stored by `@Arg` / `@Flag` decorators.
51
+ */
52
+ interface ArgDef {
53
+ /** Whether this is a positional arg or a named flag. */
54
+ kind: 'positional' | 'flag';
55
+ /** Property name on the command class instance. */
56
+ property: string;
57
+ /** Name used in the CLI (citty arg key). */
58
+ name: string;
59
+ /** Short description shown in `--help`. */
60
+ description?: string;
61
+ /** Default value. */
62
+ default?: unknown;
63
+ /** Whether the arg/flag is required. */
64
+ required?: boolean;
65
+ /** For flags: the value type used by citty. */
66
+ valueType?: 'string' | 'boolean' | 'number';
67
+ }
68
+ }
69
+ //#endregion
70
+ //#region src/Decorators/Command.d.ts
71
+ /**
72
+ * Class decorator that marks a class as a CLI command.
73
+ * Stores command metadata on the prototype so `CommandRegistry` can discover it.
74
+ *
75
+ * @param meta - command configuration (name, description, optional subcommands)
76
+ * @returns class decorator
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * @Command({ name: 'build', description: 'Build the project' })
81
+ * export class BuildCommand extends BaseCommand { ... }
82
+ * ```
83
+ */
84
+ declare function Command(meta: CliTypes.CommandMeta): Function;
85
+ //#endregion
86
+ //#region src/Decorators/Arg.d.ts
87
+ interface ArgOptions {
88
+ name: string;
89
+ description?: string;
90
+ required?: boolean;
91
+ }
92
+ /**
93
+ * Property decorator for positional CLI arguments.
94
+ * The value is injected from parsed CLI input before `run()` is called.
95
+ *
96
+ * @param options - argument configuration
97
+ * @returns property decorator
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * @Arg({ name: 'query', description: 'Phrase to search for' })
102
+ * public query: string;
103
+ * ```
104
+ */
105
+ declare function Arg(options: ArgOptions): Function;
106
+ //#endregion
107
+ //#region src/Decorators/Flag.d.ts
108
+ interface FlagOptions {
109
+ name: string;
110
+ description?: string;
111
+ default?: unknown;
112
+ required?: boolean;
113
+ /** Explicit citty value type. Inferred from `default` when omitted. */
114
+ type?: 'string' | 'boolean' | 'number';
115
+ }
116
+ /**
117
+ * Property decorator for named CLI flags (`--name value`).
118
+ * The value type is inferred from `default` when `type` is not set.
119
+ * The parsed value is injected before `run()` is called.
120
+ *
121
+ * @param options - flag configuration
122
+ * @returns property decorator
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * @Flag({ name: 'limit', description: 'Max results', default: 10 })
127
+ * public limit: number;
128
+ *
129
+ * @Flag({ name: 'json', description: 'Output as JSON', default: false })
130
+ * public json: boolean;
131
+ * ```
132
+ */
133
+ declare function Flag(options: FlagOptions): Function;
134
+ //#endregion
135
+ export { Arg, BaseCommand, type CliTypes, Command, Flag };
@@ -0,0 +1,2 @@
1
+ import { i as Arg, o as BaseCommand, r as Command, t as Flag } from "./Flag-qtOTYeqz.mjs";
2
+ export { Arg, BaseCommand, Command, Flag };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercube/cli",
3
- "version": "0.0.48",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "CLI module for Vercube framework",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,6 +14,7 @@
14
14
  "module": "./dist/index.mjs",
15
15
  "exports": {
16
16
  ".": "./dist/index.mjs",
17
+ "./toolkit": "./dist/toolkit.mjs",
17
18
  "./package.json": "./package.json"
18
19
  },
19
20
  "types": "./dist/index.d.mts",
@@ -25,21 +26,25 @@
25
26
  "vercube": "./dist/index.mjs"
26
27
  },
27
28
  "dependencies": {
29
+ "c12": "4.0.0-beta.5",
28
30
  "citty": "0.2.2",
29
31
  "consola": "3.4.2",
30
- "giget": "3.2.0",
31
- "nypm": "0.6.6",
32
+ "giget": "3.3.0",
33
+ "jiti": "2.7.0",
34
+ "nypm": "0.6.7",
32
35
  "pathe": "2.0.3",
33
36
  "srvx": "0.11.16",
34
37
  "std-env": "4.1.0",
35
- "tinyexec": "1.2.2",
36
- "@vercube/logger": "0.0.48",
37
- "@vercube/devkit": "0.0.48"
38
+ "tinyexec": "1.2.4",
39
+ "@vercube/core": "1.0.0-beta.2",
40
+ "@vercube/di": "1.0.0-beta.2",
41
+ "@vercube/logger": "1.0.0-beta.2",
42
+ "@vercube/devkit": "1.0.0-beta.2"
38
43
  },
39
44
  "publishConfig": {
40
45
  "access": "public"
41
46
  },
42
47
  "scripts": {
43
- "build": "tsdown --config ../../tsdown.config.ts --config-loader=unrun"
48
+ "build": "tsdown --config tsdown.config.ts --config-loader=unrun"
44
49
  }
45
50
  }
@@ -1,19 +0,0 @@
1
- import { defineCommand } from "citty";
2
- import { build, createVercube } from "@vercube/devkit";
3
- //#region src/commands/build.ts
4
- const buildCommand = defineCommand({
5
- meta: {
6
- name: "build",
7
- description: "Build the project"
8
- },
9
- args: { entry: {
10
- type: "string",
11
- description: "Entry file",
12
- default: void 0
13
- } },
14
- run: async (ctx) => {
15
- await build(await createVercube({ build: { entry: ctx?.args?.entry ?? void 0 } }));
16
- }
17
- });
18
- //#endregion
19
- export { buildCommand };
@@ -1,16 +0,0 @@
1
- import { defineCommand } from "citty";
2
- import { createDevServer, createVercube, watch } from "@vercube/devkit";
3
- //#region src/commands/dev.ts
4
- const devCommand = defineCommand({
5
- meta: {
6
- name: "dev",
7
- description: "Start development server"
8
- },
9
- async run() {
10
- const app = await createVercube();
11
- createDevServer(app);
12
- await watch(app);
13
- }
14
- });
15
- //#endregion
16
- export { devCommand };
@@ -1,60 +0,0 @@
1
- import { defineCommand } from "citty";
2
- import { build, createVercube } from "@vercube/devkit";
3
- import { cliFetch } from "srvx/cli";
4
- //#region src/commands/fetch.ts
5
- const fetchCommand = defineCommand({
6
- meta: {
7
- name: "fetch",
8
- description: "Fetch a request from the application"
9
- },
10
- args: {
11
- url: {
12
- type: "positional",
13
- description: "URL to fetch",
14
- default: "/"
15
- },
16
- entry: {
17
- type: "string",
18
- description: "Entry point for the application",
19
- default: "index.mjs"
20
- },
21
- method: {
22
- type: "string",
23
- description: "HTTP method (default: GET, or POST if body is provided)",
24
- default: "GET",
25
- alias: "X",
26
- valueHint: "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS"
27
- },
28
- headers: {
29
- type: "string",
30
- description: "Add header (format: \"Name: Value, Name: Value, ...\")",
31
- alias: "H"
32
- },
33
- data: {
34
- type: "string",
35
- description: "Request body (use @- for stdin, @file for file)",
36
- alias: "d"
37
- },
38
- verbose: {
39
- type: "boolean",
40
- description: "Show request and response headers",
41
- alias: "v",
42
- default: false
43
- }
44
- },
45
- async run(ctx) {
46
- const app = await createVercube({ build: { dts: false } });
47
- await build(app);
48
- await cliFetch({
49
- verbose: ctx.args.verbose,
50
- dir: app.config.build?.output?.dir ?? "dist",
51
- entry: ctx.args.entry,
52
- url: ctx.args.url,
53
- method: ctx.args.method,
54
- header: ctx.args.headers?.split(",") ?? [],
55
- data: ctx.args.data
56
- });
57
- }
58
- });
59
- //#endregion
60
- export { fetchCommand };
@@ -1,181 +0,0 @@
1
- import { defineCommand } from "citty";
2
- import { existsSync } from "node:fs";
3
- import { consola } from "consola";
4
- import { colors } from "consola/utils";
5
- import { downloadTemplate, startShell } from "giget";
6
- import { installDependencies } from "nypm";
7
- import { relative, resolve } from "pathe";
8
- import { hasTTY } from "std-env";
9
- import { x } from "tinyexec";
10
- //#region src/utils/logo.ts
11
- const startColor = {
12
- r: 149,
13
- g: 100,
14
- b: 245
15
- };
16
- const endColor = {
17
- r: 111,
18
- g: 114,
19
- b: 245
20
- };
21
- const icon = [
22
- " _ ",
23
- " | | ",
24
- " __ _____ _ __ ___ _ _| |__ ___ ",
25
- " \\ \\ / / _ \\ '__/ __| | | | '_ \\ / _ \\",
26
- String.raw` \ V / __/ | | (__| |_| | |_) | __/`,
27
- String.raw` \_/ \___|_| \___|\__,_|_.__/ \___|`,
28
- " ",
29
- " "
30
- ];
31
- function interpolateColor(startColor, endColor, factor) {
32
- return `\u001B[38;2;${Math.round(startColor.r + factor * (endColor.r - startColor.r))};${Math.round(startColor.g + factor * (endColor.g - startColor.g))};${Math.round(startColor.b + factor * (endColor.b - startColor.b))}m`;
33
- }
34
- function applyGradient(icon, startColor, endColor) {
35
- const totalLines = icon.length;
36
- return icon.map((line, index) => {
37
- return interpolateColor(startColor, endColor, index / (totalLines - 1)) + line;
38
- }).join("\n");
39
- }
40
- const vercubeIcon = applyGradient(icon, startColor, endColor);
41
- //#endregion
42
- //#region src/commands/init.ts
43
- const logger = consola.withTag(colors.whiteBright(colors.bold(colors.bgGreenBright(" vercube "))));
44
- const DEFAULT_REGISTRY = "https://raw.githubusercontent.com/vercube/starter/main/templates";
45
- const DEFAULT_TEMPLATE_NAME = "vercube";
46
- const packageManagerOptions = Object.keys({
47
- npm: void 0,
48
- pnpm: void 0,
49
- yarn: void 0,
50
- bun: void 0,
51
- deno: void 0
52
- });
53
- const initCommand = defineCommand({
54
- meta: {
55
- name: "init",
56
- description: "Initialize a new Vercube app"
57
- },
58
- args: {
59
- dir: {
60
- type: "positional",
61
- description: "Project directory",
62
- default: ""
63
- },
64
- force: {
65
- type: "boolean",
66
- alias: "f",
67
- description: "Override existing directory"
68
- },
69
- install: {
70
- type: "boolean",
71
- default: true,
72
- description: "Skip installing dependencies"
73
- },
74
- gitInit: {
75
- type: "boolean",
76
- description: "Initialize git repository",
77
- default: true
78
- },
79
- packageManager: {
80
- type: "string",
81
- description: "Package manager choice (npm, pnpm, yarn, bun)",
82
- default: "pnpm"
83
- }
84
- },
85
- async run(ctx) {
86
- if (hasTTY) process.stdout.write(`\n${vercubeIcon}\n\n`);
87
- consola.info(`Welcome to ${colors.bold("Vercube")}!`);
88
- let dir = ctx.args.dir;
89
- if (ctx.args.dir === "") dir = await logger.prompt("Where would you like to create your project?", {
90
- placeholder: "./vercube-app",
91
- type: "text",
92
- default: "vercube-app",
93
- cancel: "reject"
94
- }).catch(() => process.exit(1));
95
- const cwd = resolve(process.cwd());
96
- let templateDownloadPath = resolve(cwd, dir);
97
- logger.info(`Creating a new project in ${colors.cyan(relative(cwd, templateDownloadPath) || templateDownloadPath)}.`);
98
- let shouldForce = Boolean(ctx.args.force);
99
- if (!shouldForce && existsSync(templateDownloadPath)) switch (await logger.prompt(`The directory ${colors.cyan(templateDownloadPath)} already exists. What would you like to do?`, {
100
- type: "select",
101
- options: [
102
- "Override its contents",
103
- "Select different directory",
104
- "Abort"
105
- ]
106
- })) {
107
- case "Override its contents":
108
- shouldForce = true;
109
- break;
110
- case "Select different directory":
111
- templateDownloadPath = resolve(cwd, await logger.prompt("Please specify a different directory:", {
112
- type: "text",
113
- cancel: "reject"
114
- }).catch(() => process.exit(1)));
115
- break;
116
- default: process.exit(1);
117
- }
118
- let template;
119
- try {
120
- template = await downloadTemplate(DEFAULT_TEMPLATE_NAME, {
121
- dir: templateDownloadPath,
122
- force: shouldForce,
123
- offline: Boolean(ctx.args.offline),
124
- preferOffline: Boolean(ctx.args.preferOffline),
125
- registry: DEFAULT_REGISTRY
126
- });
127
- } catch (error) {
128
- if (process.env.DEBUG) throw error;
129
- logger.error(error.toString());
130
- process.exit(1);
131
- }
132
- const packageManagerArg = ctx.args.packageManager;
133
- const selectedPackageManager = packageManagerOptions.includes(packageManagerArg) ? packageManagerArg : await logger.prompt("Which package manager would you like to use?", {
134
- type: "select",
135
- options: packageManagerOptions,
136
- cancel: "reject"
137
- }).catch(() => process.exit(1));
138
- if (ctx.args.install === false) logger.info("Skipping install dependencies step.");
139
- else {
140
- logger.start("Installing dependencies...");
141
- try {
142
- await installDependencies({
143
- cwd: template.dir,
144
- packageManager: {
145
- name: selectedPackageManager,
146
- command: selectedPackageManager
147
- }
148
- });
149
- } catch (error) {
150
- if (process.env.DEBUG) throw error;
151
- logger.error(error.toString());
152
- process.exit(1);
153
- }
154
- logger.success("Installation completed.");
155
- }
156
- let gitInit = ctx.args.gitInit;
157
- if (gitInit === void 0) gitInit = await logger.prompt("Initialize git repository?", {
158
- type: "confirm",
159
- cancel: "reject"
160
- }).catch(() => process.exit(1));
161
- if (ctx.args.gitInit) {
162
- logger.info("Initializing git repository...\n");
163
- try {
164
- await x("git", ["init", template.dir], {
165
- throwOnError: true,
166
- nodeOptions: { stdio: "inherit" }
167
- });
168
- } catch (error) {
169
- logger.warn(`Failed to initialize git repository: ${error}`);
170
- }
171
- }
172
- logger.log("\n✨ Vercube project has been created! Next steps:");
173
- const relativeTemplateDir = relative(process.cwd(), template.dir) || ".";
174
- const runCmd = selectedPackageManager === "deno" ? "task" : "run";
175
- const nextSteps = [!ctx.args.shell && relativeTemplateDir.length > 1 && `\`cd ${relativeTemplateDir}\``, `Start development server with \`${selectedPackageManager} ${runCmd} dev\``].filter(Boolean);
176
- for (const step of nextSteps) logger.log(` › ${step}`);
177
- if (ctx.args.shell) startShell(template.dir);
178
- }
179
- });
180
- //#endregion
181
- export { initCommand };