clispark 1.3.0 → 1.4.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/README.md CHANGED
@@ -29,7 +29,7 @@ Every generated project includes:
29
29
  - **Structured logging** (`pino`, one log file per invocation in an OS-appropriate log directory) that automatically covers every command
30
30
  - **Consistent error handling** with no opt-out — clean `Error: <message>` output on failure, full stack trace captured in the log file
31
31
  - **A working test setup** (`vitest` + `@oclif/test`) with an example test to copy from
32
- - **A first example command** (`hello`) as a starting point for your own commands
32
+ - **Example commands** a minimal `hello` starting point plus a `task`/`task complete`/`task list` reference covering required args, optional args, enum-constrained args, integer and boolean args, and subcommands
33
33
  - **A clean build pipeline** (`tsup`) producing a directly runnable binary
34
34
 
35
35
  ## Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clispark",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Interactive scaffolding tool for new CLI projects",
5
5
  "keywords": [
6
6
  "cli",
@@ -23,6 +23,50 @@ export default class MyCommand extends BaseCommand {
23
23
 
24
24
  `BaseCommand` overrides oclif's `init()`/`catch()`/`finally()` lifecycle methods to log every command's start, failure, and completion automatically — no manual logging calls needed inside `run()`.
25
25
 
26
+ ## Argument Types
27
+
28
+ Every entry in a command's `static args` is built from `@oclif/core`'s `Args` helpers. `task.ts` / `task/complete.ts` / `task/list.ts` use a few of these; the full set `@oclif/core` ships:
29
+
30
+ - **`Args.string()`** — plain text, no parsing/validation. The default choice.
31
+ ```ts
32
+ title: Args.string({ description: 'Task title' })
33
+ ```
34
+ - **`Args.integer({ min?, max? })`** — parses digits into a real `number`, rejects non-numeric input, optional bounds.
35
+ ```ts
36
+ id: Args.integer({ description: 'Task ID' })
37
+ // `task complete abc` → "Expected an integer but received: abc"
38
+ ```
39
+ - **`Args.boolean()`** — parses into a real `boolean`. Any input is `true` except (case-insensitive) `0`, `false`, `n`, `no`, which parse to `false`.
40
+ ```ts
41
+ done: Args.boolean({ description: 'Only completed tasks' })
42
+ // `task list true` → done === true; `task list no` → done === false
43
+ ```
44
+ - **`Args.file({ exists? })`** / **`Args.directory({ exists? })`** — plain string path by default; with `exists: true`, verifies the path actually exists on disk (as a file/directory respectively) and throws otherwise.
45
+ ```ts
46
+ config: Args.file({ exists: true, description: 'Path to a config file' })
47
+ // missing file → "No file found at <path>"
48
+ ```
49
+ - **`Args.url()`** — parses into a real `URL` object, throws if the input isn't a valid URL.
50
+ ```ts
51
+ endpoint: Args.url({ description: 'API endpoint' })
52
+ ```
53
+ - **`Args.custom<T, Opts>({ parse })`** — build your own type when none of the above fit.
54
+ ```ts
55
+ const semver = Args.custom<string>({
56
+ parse: async (input) => {
57
+ if (!/^\d+\.\d+\.\d+$/.test(input)) throw new Error(`Not a valid semver: ${input}`);
58
+ return input;
59
+ },
60
+ });
61
+ ```
62
+ - **`options: [...]`** — a cross-cutting constraint any arg type can add (not a type itself): restricts input to a fixed list of values.
63
+ ```ts
64
+ priority: Args.string({ options: ['low', 'medium', 'high'] })
65
+ // `task <title> urgent` → "Expected urgent to be one of: low, medium, high"
66
+ ```
67
+
68
+ See `task.ts` (string + enum-constrained string), `task/complete.ts` (integer), and `task/list.ts` (string + boolean) for these in a real, runnable command.
69
+
26
70
  ## Command Discovery
27
71
 
28
72
  oclif discovers commands at runtime from the `oclif.commands` path in `package.json` (`./dist/commands`) — there is no custom filesystem-scanning code. Dropping a new file in `src/commands/` and building the project is enough for oclif to pick it up; nothing needs to be manually registered.
@@ -5,3 +5,19 @@ Generated with [clispark](https://github.com/martinwichner/clispark).
5
5
  ## Requirements
6
6
 
7
7
  Node.js **>=24** — this project's entry point (`bin/run.ts`) runs directly via Node's native TypeScript execution, with no build step. On an older Node version it fails with an `ERR_UNKNOWN_FILE_EXTENSION` error rather than a clear version message, so if you hit that, check `node --version` first.
8
+
9
+ ## Example commands
10
+
11
+ Two example commands ship in `src/commands/` as copy-paste starting points:
12
+
13
+ - **`hello`** (`src/commands/hello.ts`) — the minimal case: no args, no subcommands.
14
+ ```bash
15
+ node bin/run.ts hello
16
+ ```
17
+ - **`task`** / **`task complete`** / **`task list`** (`src/commands/task.ts`, `src/commands/task/`) — a reference for oclif's argument and subcommand patterns: required args, optional args, an enum-constrained arg, an integer arg, a boolean arg, and how a folder under `src/commands/` becomes a subcommand.
18
+ ```bash
19
+ node bin/run.ts task "Buy milk" high
20
+ node bin/run.ts task complete 1
21
+ node bin/run.ts task list groceries
22
+ ```
23
+ See `ARCHITECTURE.md`'s "Argument Types" section for the full set of argument types oclif supports, including a couple this example doesn't use.
@@ -0,0 +1,20 @@
1
+ // templates/base/src/commands/task/complete.test.ts
2
+ import { describe, it, expect } from 'vitest';
3
+ import { runCommand } from '@oclif/test';
4
+
5
+ describe('task complete', () => {
6
+ it('completes a task by numeric id', async () => {
7
+ const { stdout } = await runCommand('task complete 42');
8
+ expect(stdout).toContain('Completed task 42');
9
+ });
10
+
11
+ it('rejects a non-numeric id', async () => {
12
+ const { error } = await runCommand('task complete abc');
13
+ expect(error?.message).toContain('Expected an integer but received: abc');
14
+ });
15
+
16
+ it('requires an id', async () => {
17
+ const { error } = await runCommand('task complete');
18
+ expect(error?.message).toContain('Missing 1 required arg');
19
+ });
20
+ });
@@ -0,0 +1,16 @@
1
+ // templates/base/src/commands/task/complete.ts
2
+ import { Args } from '@oclif/core';
3
+ import { BaseCommand } from '../../base-command';
4
+
5
+ export default class TaskComplete extends BaseCommand {
6
+ static description = 'Complete a task (demonstrates a subcommand with a required integer argument)';
7
+ static args = {
8
+ id: Args.integer({ required: true, description: 'Task ID to complete' }),
9
+ };
10
+ static flags = {};
11
+
12
+ async run(): Promise<void> {
13
+ const { args } = await this.parse(TaskComplete);
14
+ this.log(`Completed task ${args.id}`);
15
+ }
16
+ }
@@ -0,0 +1,25 @@
1
+ // templates/base/src/commands/task/list.test.ts
2
+ import { describe, it, expect } from 'vitest';
3
+ import { runCommand } from '@oclif/test';
4
+
5
+ describe('task list', () => {
6
+ it('lists all tasks with no args', async () => {
7
+ const { stdout } = await runCommand('task list');
8
+ expect(stdout).toContain('Listing all tasks');
9
+ });
10
+
11
+ it('lists tasks matching a filter', async () => {
12
+ const { stdout } = await runCommand('task list groceries');
13
+ expect(stdout).toContain('Listing tasks matching "groceries"');
14
+ });
15
+
16
+ it('combines a filter with the done flag', async () => {
17
+ const { stdout } = await runCommand('task list groceries true');
18
+ expect(stdout).toContain('Listing tasks matching "groceries" (completed only: true)');
19
+ });
20
+
21
+ it('parses "no" as false for the done arg', async () => {
22
+ const { stdout } = await runCommand('task list groceries no');
23
+ expect(stdout).toContain('(completed only: false)');
24
+ });
25
+ });
@@ -0,0 +1,18 @@
1
+ // templates/base/src/commands/task/list.ts
2
+ import { Args } from '@oclif/core';
3
+ import { BaseCommand } from '../../base-command';
4
+
5
+ export default class TaskList extends BaseCommand {
6
+ static description = 'List tasks (demonstrates a subcommand with two optional arguments of different types)';
7
+ static args = {
8
+ filter: Args.string({ required: false, description: 'Optional filter term' }),
9
+ done: Args.boolean({ required: false, description: 'Only show completed tasks (true/false)' }),
10
+ };
11
+ static flags = {};
12
+
13
+ async run(): Promise<void> {
14
+ const { args } = await this.parse(TaskList);
15
+ const base = args.filter ? `Listing tasks matching "${args.filter}"` : 'Listing all tasks';
16
+ this.log(args.done !== undefined ? `${base} (completed only: ${args.done})` : base);
17
+ }
18
+ }
@@ -0,0 +1,26 @@
1
+ // templates/base/src/commands/task.test.ts
2
+ import { describe, it, expect } from 'vitest';
3
+ import { runCommand } from '@oclif/test';
4
+
5
+ describe('task', () => {
6
+ it('creates a task with just a title', async () => {
7
+ const { stdout } = await runCommand('task "Buy milk"');
8
+ expect(stdout).toContain('Created task: "Buy milk"');
9
+ expect(stdout).not.toContain('priority');
10
+ });
11
+
12
+ it('creates a task with a priority', async () => {
13
+ const { stdout } = await runCommand('task "Buy milk" high');
14
+ expect(stdout).toContain('Created task: "Buy milk" (priority: high)');
15
+ });
16
+
17
+ it('rejects a priority outside the allowed values', async () => {
18
+ const { error } = await runCommand('task "Buy milk" urgent');
19
+ expect(error?.message).toContain('Expected urgent to be one of: low, medium, high');
20
+ });
21
+
22
+ it('requires a title', async () => {
23
+ const { error } = await runCommand('task');
24
+ expect(error?.message).toContain('Missing 1 required arg');
25
+ });
26
+ });
@@ -0,0 +1,21 @@
1
+ // templates/base/src/commands/task.ts
2
+ import { Args } from '@oclif/core';
3
+ import { BaseCommand } from '../base-command';
4
+
5
+ export default class Task extends BaseCommand {
6
+ static description = 'Create a task (demonstrates a required string arg and an optional enum-constrained arg)';
7
+ static args = {
8
+ title: Args.string({ required: true, description: 'Task title' }),
9
+ priority: Args.string({
10
+ required: false,
11
+ options: ['low', 'medium', 'high'],
12
+ description: 'Optional priority',
13
+ }),
14
+ };
15
+ static flags = {};
16
+
17
+ async run(): Promise<void> {
18
+ const { args } = await this.parse(Task);
19
+ this.log(`Created task: "${args.title}"${args.priority ? ` (priority: ${args.priority})` : ''}`);
20
+ }
21
+ }