@stacksjs/defaults 0.72.77 → 0.72.78

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.
@@ -17,8 +17,8 @@ The complete CLI runtime for the Stacks framework with 50+ commands, lazy-loaded
17
17
  - Lazy command registry: `storage/framework/core/buddy/src/lazy-commands.ts`
18
18
  - Config system: `storage/framework/core/buddy/src/config.ts`
19
19
  - Shell entry: `buddy` (shell script at project root that invokes `bun run ./storage/framework/core/buddy/src/cli.ts`)
20
- - Application commands: `app/Commands/`
21
- - Command registry: `app/Commands.ts`
20
+ - Application commands: `app/Commands/` (auto-discovered; no registration step)
21
+ - Optional registry: `app/Commands.ts`
22
22
  - Make templates: `storage/framework/defaults/`
23
23
 
24
24
  ## CLI Aliases
@@ -250,7 +250,7 @@ buddy make:certificate # generate SSL certificate (alias: make:cert)
250
250
  buddy make:command [name] # create CLI command in app/Commands/
251
251
  --signature [sig] # CLI command name
252
252
  --description [desc] # command description
253
- --no-register # skip registering in Commands.ts
253
+ --register # also add an entry to app/Commands.ts (optional)
254
254
  buddy make:component [name] # create STX component
255
255
  buddy make:database [name] # create database
256
256
  buddy make:factory [name] # create model factory (stub)
@@ -622,34 +622,63 @@ buddy stacks # Stacks framework commands (registered as 'stack'
622
622
 
623
623
  ## Adding Custom Commands
624
624
 
625
- ### Method 1: Commands.ts Registry (preferred)
625
+ ### Method 1: Drop a file in `app/Commands/` (preferred)
626
+
627
+ Every `.ts` file there is a command - no registration, no generated file.
628
+ Nested directories work too (`app/Commands/Archive/Run.ts`).
626
629
 
627
630
  ```typescript
628
- // app/Commands.ts
629
- export default {
630
- 'inspire': 'Inspire', // simple: maps to app/Commands/Inspire.ts
631
- 'deploy-hooks': { file: 'DeployHooks', enabled: true, aliases: ['dh'] }, // with options
632
- 'disabled-cmd': { file: 'Disabled', enabled: false }, // disabled command
633
- }
631
+ // app/Commands/Inspire.ts
632
+ import { defineCommand } from '@stacksjs/cli'
633
+
634
+ // Declarative: `options` is inferred from the flags declared above it.
635
+ export default defineCommand({
636
+ name: 'inspire',
637
+ description: 'Display inspirational quote',
638
+ aliases: ['insp'],
639
+ options: {
640
+ '--two, -t': { description: 'Show two quotes', default: false },
641
+ },
642
+ handle(options) {
643
+ console.log(randomQuote())
644
+ if (options.two)
645
+ console.log(randomQuote())
646
+ },
647
+ })
634
648
  ```
635
649
 
636
650
  ```typescript
637
- // app/Commands/Inspire.ts
638
- import type { CLI } from '@stacksjs/cli'
651
+ // Imperative, for a file that registers several commands or uses cli.on()
652
+ import { defineCommand } from '@stacksjs/cli'
639
653
 
640
- export default function (buddy: CLI) {
641
- buddy
642
- .command('inspire', 'Display inspirational quote')
643
- .option('-t, --two', 'Show two quotes')
644
- .action(async (options: { two?: boolean }) => {
645
- console.log(randomQuote())
646
- if (options.two) console.log(randomQuote())
647
- })
648
- }
654
+ export default defineCommand((buddy) => {
655
+ buddy.command('inspire', 'Display inspirational quote').action(() => {})
656
+ buddy.command('inspire:two', 'Two quotes').action(() => {})
657
+ })
649
658
  ```
650
659
 
651
- ### Method 2: Auto-discovery (fallback)
652
- If `app/Commands.ts` does not exist, all `.ts` files in `app/Commands/` are auto-discovered and loaded. Each must export a default function that receives the CLI instance.
660
+ A command file can also configure itself with named exports:
661
+
662
+ ```typescript
663
+ export const aliases = ['emails', 'mail'] // extra aliases
664
+ export const enabled = false // keep the file, hide the command
665
+ ```
666
+
667
+ ### Method 2: `app/Commands.ts` (optional overlay)
668
+
669
+ Only worth keeping to control listing order, or to alias/disable a command
670
+ without editing its file. Files it does not mention still load.
671
+
672
+ ```typescript
673
+ // app/Commands.ts
674
+ import { defineCommands } from '@stacksjs/cli'
675
+
676
+ export default defineCommands({
677
+ 'inspire': 'Inspire', // maps to app/Commands/Inspire.ts
678
+ 'deploy-hooks': { file: 'DeployHooks', enabled: true, aliases: ['dh'] }, // with options
679
+ 'disabled-cmd': { file: 'Disabled', enabled: false }, // disabled command
680
+ })
681
+ ```
653
682
 
654
683
  ### Method 3: buddy.config.ts
655
684
  ```typescript
@@ -14,40 +14,92 @@ The `@stacksjs/cli` package provides the foundation for building CLI commands, u
14
14
  - Core package: `storage/framework/core/cli/src/`
15
15
  - CLI configuration: `config/cli.ts`
16
16
  - Application commands: `app/Commands/`
17
- - Command registry: `app/Commands.ts`
17
+ - Optional registry: `app/Commands.ts` (not needed - see below)
18
18
 
19
19
  ## Creating Commands
20
20
 
21
- ### Simple Command
21
+ Every `.ts` file in `app/Commands/` is a command. There is no registration step
22
+ and no generated registry file: drop the file in and it is live, nested
23
+ directories included (`app/Commands/Archive/Run.ts`).
24
+
25
+ ### Declarative form (preferred)
26
+
27
+ `defineCommand()` infers the handler's `options` from the flags declared above
28
+ it, so there is no hand-written options interface to drift out of step.
29
+
22
30
  ```typescript
23
31
  // app/Commands/Greet.ts
24
- export default {
25
- name: 'greet',
32
+ import { defineCommand, log } from '@stacksjs/cli'
33
+
34
+ export default defineCommand({
35
+ name: 'greet <who>',
26
36
  description: 'Greet a user',
27
- alias: 'g',
37
+ aliases: ['g'],
28
38
  options: {
29
- name: { alias: 'n', description: 'Name to greet', default: 'World' },
30
- loud: { alias: 'l', description: 'Shout the greeting', type: 'boolean' }
39
+ '--loud, -l': { description: 'Shout the greeting', default: false },
40
+ '--title <title>': 'Prefix the name',
41
+ '--times <n>': { description: 'How often', default: 1, type: [Number] },
31
42
  },
32
- handle: async ({ options }) => {
33
- const greeting = `Hello, ${options.name}!`
34
- console.log(options.loud ? greeting.toUpperCase() : greeting)
35
- }
36
- }
43
+ handle(options, who) {
44
+ // options.loud -> boolean
45
+ // options.title -> string | undefined
46
+ // options.times -> number
47
+ const greeting = `Hello, ${options.title ?? ''}${who}!`
48
+
49
+ for (let i = 0; i < options.times; i++)
50
+ log.info(options.loud ? greeting.toUpperCase() : greeting)
51
+ },
52
+ })
37
53
  ```
38
54
 
39
- ### Register in app/Commands.ts
55
+ Flag to property: `--dry-run` -> `dryRun`, `--two, -t` -> `two`, `--no-cache`
56
+ -> `cache`. Value type: `<x>` -> `string`, `[x]` -> `string | true`, `<x...>`
57
+ -> `string[]`, no argument -> `boolean`. A `default` makes the property always
58
+ present; a `type: [Number]` makes it a `number`.
59
+
60
+ ### Imperative form
61
+
62
+ For a file that registers several commands or needs `cli.on()`:
63
+
40
64
  ```typescript
41
- export default {
42
- 'greet': 'Greet',
43
- 'inspire': 'Inspire',
44
- }
65
+ import { defineCommand, log } from '@stacksjs/cli'
66
+
67
+ export default defineCommand((cli) => {
68
+ cli.command('inspire', 'Inspire yourself').alias('insp').action(() => {})
69
+ cli.command('inspire:two', 'Two quotes').action(() => {})
70
+
71
+ cli.on('inspire:*', () => log.error('Invalid command'))
72
+ })
73
+ ```
74
+
75
+ ### Per-file configuration
76
+
77
+ Named exports beside the default one, so a command owns its own configuration:
78
+
79
+ ```typescript
80
+ export const aliases = ['emails', 'mail'] // extra aliases
81
+ export const enabled = false // keep the file, hide the command
82
+ ```
83
+
84
+ ### The optional registry (`app/Commands.ts`)
85
+
86
+ Only worth keeping when you want to control the order commands are listed in,
87
+ or to alias/disable a command without editing its file. A command file the
88
+ registry never mentions still loads.
89
+
90
+ ```typescript
91
+ import { defineCommands } from '@stacksjs/cli'
92
+
93
+ export default defineCommands({
94
+ 'send-emails <type>': { file: 'SendEmails', aliases: ['emails'] },
95
+ 'legacy': { file: 'Legacy', enabled: false },
96
+ })
45
97
  ```
46
98
 
47
- ### Three Registration Methods
48
- 1. **app/Commands.ts** — name → file mapping
99
+ ### Registration Methods
100
+ 1. **Drop a file in `app/Commands/`** — auto-discovered, nothing else to do
49
101
  2. **Event listeners** — CLI events in `app/Listeners/Console.ts`
50
- 3. **Inline** — direct command definitions
102
+ 3. **`app/Commands.ts`** — optional, for ordering / aliases / disabling
51
103
 
52
104
  ## CLI Event Listeners
53
105
 
@@ -104,7 +156,7 @@ buddy build:cli # build CLI binary
104
156
 
105
157
  ## Gotchas
106
158
  - Application commands go in `app/Commands/`, framework commands in `storage/framework/core/buddy/src/commands/`
107
- - Commands are auto-discovered from `app/Commands.ts` registry
159
+ - Commands are auto-discovered from `app/Commands/`; `app/Commands.ts` is optional and additive
108
160
  - CLI events support wildcards (`*`) and default handlers (`!`)
109
161
  - The buddy CLI lazy-loads commands — not all load at startup
110
162
  - Output formatting uses ANSI colors from `@stacksjs/utils`
@@ -1,16 +1,11 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { resolveCommands } from '@stacksjs/cli'
2
3
  import { existsSync, readFileSync, statSync } from 'node:fs'
3
4
  import { join, relative } from 'node:path'
4
5
  import process from 'node:process'
5
6
  import { parseCommandSource } from '../Source/source-inventory'
6
7
  import { dashboardOperationalError } from '../dashboard-response'
7
8
 
8
- interface CommandConfig {
9
- file: string
10
- enabled?: boolean
11
- aliases?: string[]
12
- }
13
-
14
9
  export default new Action({
15
10
  name: 'CommandIndexAction',
16
11
  description: 'Lists commands registered by the application.',
@@ -18,23 +13,27 @@ export default new Action({
18
13
  async handle() {
19
14
  try {
20
15
  const projectRoot = process.cwd()
21
- const registryPath = join(projectRoot, 'app/Commands.ts')
22
- const registryModule = await import(registryPath)
23
- const registry = (registryModule.default || {}) as Record<string, string | CommandConfig>
24
- const items = Object.entries(registry).flatMap(([signature, value]) => {
25
- const config = typeof value === 'string'
26
- ? { file: value, enabled: true, aliases: [] }
27
- : { enabled: true, aliases: [], ...value }
28
- const file = join(projectRoot, 'app/Commands', `${config.file}.ts`)
29
- if (!existsSync(file))
16
+ const commandsDir = join(projectRoot, 'app/Commands')
17
+
18
+ // Every file under app/Commands is a command; app/Commands.ts is an
19
+ // optional overlay. Listing only registry entries used to hide any
20
+ // command the project never bothered to register - which, now that
21
+ // registering is unnecessary, would have been most of them.
22
+ const commands = await resolveCommands({
23
+ commandsDir,
24
+ registryPath: join(projectRoot, 'app/Commands.ts'),
25
+ })
26
+
27
+ const items = commands.flatMap((command) => {
28
+ if (!existsSync(command.path))
30
29
  return []
31
30
 
32
31
  return [parseCommandSource(
33
- readFileSync(file, 'utf8'),
34
- relative(projectRoot, file),
35
- signature,
36
- config.aliases,
37
- statSync(file).mtime.toISOString(),
32
+ readFileSync(command.path, 'utf8'),
33
+ relative(projectRoot, command.path),
34
+ command.signature ?? command.file,
35
+ command.aliases,
36
+ statSync(command.path).mtime.toISOString(),
38
37
  )]
39
38
  })
40
39
 
@@ -44,7 +43,7 @@ export default new Action({
44
43
  total: items.length,
45
44
  aliases: items.reduce((sum, item) => sum + (item.aliases?.length || 0), 0),
46
45
  options: items.reduce((sum, item) => sum + (item.options?.length || 0), 0),
47
- registered: Object.keys(registry).length,
46
+ registered: commands.filter(command => command.source === 'registry').length,
48
47
  },
49
48
  }
50
49
  }
@@ -1,16 +1,17 @@
1
- import type { CLI } from '@stacksjs/types'
2
1
  // triggered via `$your-cli inspire` and `buddy inspire`
3
2
  import process from 'node:process'
4
- import { log, quotes } from '@stacksjs/cli'
3
+ import { defineCommand, log, quotes } from '@stacksjs/cli'
5
4
  import { ExitCode } from '@stacksjs/types'
6
5
 
7
- // for enhanced type-safety & autocompletion,
8
- // you may want to define the options' interface
6
+ // Every file in this directory is a command - there is nothing to register.
7
+ // `defineCommand` types the CLI it hands you; the declarative form
8
+ // (`defineCommand({ name, options, handle })`) additionally infers the
9
+ // handler's options from the flags it declares.
9
10
  interface InspireOptions {
10
11
  two: boolean
11
12
  }
12
13
 
13
- export default function (cli: CLI) {
14
+ export default defineCommand((cli) => {
14
15
  cli
15
16
  .command('inspire', 'Inspire yourself with a random quote')
16
17
  .option('--two, -t', 'Inspire yourself with two random quotes', {
@@ -53,5 +54,5 @@ export default function (cli: CLI) {
53
54
  process.exit(1)
54
55
  })
55
56
 
56
- return cli // TODO: this may not be needed
57
- }
57
+ return cli
58
+ })
@@ -7,60 +7,71 @@ Stacks allows you to easily create & manage CLIs. This is done through the use o
7
7
 
8
8
  ## Get Started
9
9
 
10
- The following command will bootstrap a new action file in the `app/Commands` directory.
10
+ The following command will bootstrap a new command file in the `app/Commands` directory.
11
11
 
12
12
  ```sh
13
13
  buddy make:command SendEmails
14
14
  ```
15
15
 
16
- Because commands are automatically registered, you can use them in your CLI immediately.
17
-
18
- ```sh
16
+ Every `.ts` file in this directory is a command. There is no registration step and no generated
17
+ registry file - drop the file in and it is live, nested directories included
18
+ (`app/Commands/Archive/Run.ts`).
19
19
 
20
20
  ### Example
21
21
 
22
- A simple example of a command that prints a random quote to the console. _For a closer look, take a peak at the [Inspire.ts](./Inspire.ts) command._
22
+ `defineCommand()` infers the handler's `options` from the flags declared above it, so there is no
23
+ hand-written options interface to keep in step.
24
+
25
+ ```ts
26
+ import { defineCommand, log } from '@stacksjs/cli'
27
+
28
+ export default defineCommand({
29
+ name: 'send-emails <type>',
30
+ description: 'Send the queued emails of one type',
31
+ aliases: ['emails'],
32
+ options: {
33
+ '--dry-run': { description: 'Report what would be sent, send nothing', default: false },
34
+ '--limit <n>': { description: 'Stop after this many', default: 100, type: [Number] },
35
+ },
36
+ async handle(options, type) {
37
+ // options.dryRun -> boolean, options.limit -> number, type -> string
38
+ log.info(`Sending ${options.limit} ${type} emails${options.dryRun ? ' (dry run)' : ''}`)
39
+ },
40
+ })
41
+ ```
42
+
43
+ For a file that registers several commands, or needs `cli.on()`, take the CLI directly - it is typed
44
+ either way:
45
+
46
+ ```ts
47
+ import { defineCommand, log } from '@stacksjs/cli'
48
+
49
+ export default defineCommand((cli) => {
50
+ cli.command('inspire', 'Inspire yourself with a random quote').alias('insp').action(() => {})
51
+ cli.command('inspire:two', 'Inspire yourself with two random quotes').action(() => {})
52
+
53
+ cli.on('inspire:*', () => log.error('Invalid command.'))
54
+ })
55
+ ```
56
+
57
+ ### Configuring a command
58
+
59
+ Named exports beside the default one, so a command owns its own configuration:
23
60
 
24
61
  ```ts
62
+ export const aliases = ['emails', 'mail'] // extra aliases
63
+ export const enabled = false // keep the file, hide the command
64
+ ```
25
65
 
26
- interface InspireOptions {
27
- two: boolean
28
- }
29
-
30
- export default function (cli: CLI) {
31
- cli
32
- .command('inspire', 'Inspire yourself with a random quote')
33
- .option('--two, -t', 'Inspire yourself with two random quotes', { default: false })
34
- .alias('insp')
35
- .action((options: InspireOptions) => {
36
- if (options.two)
37
- // @ts-expect-error - this is safe because we hard-coded the quotes
38
- quotes.random(2).map((quote, index) => log.info(`${index + 1}. ${quote}`))
39
- else
40
- log.info(quotes.random())
41
-
42
- log.success('Have a great day!')
43
- process.exit(ExitCode.Success)
44
- })
45
-
46
- cli
47
- .command('inspire:two', 'Inspire yourself with two random quotes')
48
- .action(() => {
49
- // @ts-expect-error - this is safe because we hard-coded the quotes
50
- quotes.random(2).map((quote, index) => log.info(`${index + 1}. ${quote}`))
51
-
52
- log.success('Have a great day!')
53
- process.exit(ExitCode.Success)
54
- })
55
-
56
- cli.on('inspire:*', () => {
57
- log.error('Invalid command: %s\nSee --help for a list of available commands.', cli.args.join(' '))
58
- process.exit(1)
59
- })
60
-
61
- return cli
62
- }
66
+ `app/Commands.ts` is optional. Keep one only to control the order commands are listed in, or to
67
+ alias or disable a command without editing its file - a command it never mentions still loads.
68
+
69
+ ```ts
70
+ import { defineCommands } from '@stacksjs/cli'
63
71
 
72
+ export default defineCommands({
73
+ 'send-emails <type>': { file: 'SendEmails', aliases: ['emails'] },
74
+ })
64
75
  ```
65
76
 
66
77
  ## 🚜 Contributing
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.72.77",
5
+ "version": "0.72.78",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.77",
5
+ "version": "0.72.78",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -51,7 +51,7 @@
51
51
  "dependencies": {
52
52
  "@iconify-json/f7": "^1.2.2",
53
53
  "@iconify-json/hugeicons": "^1.2.27",
54
- "@stacksjs/mobile": "^0.72.77",
54
+ "@stacksjs/mobile": "^0.72.78",
55
55
  "@stacksjs/sanitizer": "^0.2.113"
56
56
  },
57
57
  "scripts": {
package/app/Commands.ts DELETED
@@ -1,38 +0,0 @@
1
- export interface CommandConfig {
2
- /** The command file name (without .ts extension) */
3
- file: string
4
- /** Whether the command is enabled */
5
- enabled?: boolean
6
- /** Command aliases */
7
- aliases?: string[]
8
- }
9
-
10
- export type CommandRegistry = Record<string, string | CommandConfig>
11
-
12
- /**
13
- * The application's command registry.
14
- *
15
- * Commands listed here will be auto-loaded by the CLI.
16
- * You can use a simple string (file name) or a config object for more control.
17
- *
18
- * @example
19
- * // Simple registration
20
- * 'inspire': 'Inspire',
21
- *
22
- * // With config
23
- * 'send-emails': {
24
- * file: 'SendEmails',
25
- * enabled: true,
26
- * aliases: ['emails', 'mail'],
27
- * },
28
- */
29
- export default {
30
- 'inspire': 'Inspire',
31
- // Add more commands here
32
- // 'my-command': 'MyCommand',
33
- // 'send-emails': {
34
- // file: 'SendEmails',
35
- // enabled: true,
36
- // aliases: ['emails'],
37
- // },
38
- } satisfies CommandRegistry