@levu/snap 0.1.1 → 0.2.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +26 -2
  3. package/dist/dx/terminal/index.d.ts +2 -1
  4. package/dist/dx/terminal/index.js +3 -1
  5. package/dist/dx/terminal/intro-outro.d.ts +4 -0
  6. package/dist/dx/terminal/intro-outro.js +44 -0
  7. package/dist/dx/terminal/output.d.ts +13 -1
  8. package/dist/dx/terminal/output.js +43 -2
  9. package/dist/dx/tui/index.d.ts +12 -0
  10. package/dist/dx/tui/index.js +12 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.js +2 -0
  13. package/dist/tui/component-adapters/autocomplete.d.ts +15 -0
  14. package/dist/tui/component-adapters/autocomplete.js +34 -0
  15. package/dist/tui/component-adapters/note.d.ts +7 -0
  16. package/dist/tui/component-adapters/note.js +23 -0
  17. package/dist/tui/component-adapters/password.d.ts +7 -0
  18. package/dist/tui/component-adapters/password.js +24 -0
  19. package/dist/tui/component-adapters/progress.d.ts +7 -0
  20. package/dist/tui/component-adapters/progress.js +44 -0
  21. package/dist/tui/component-adapters/spinner.d.ts +10 -0
  22. package/dist/tui/component-adapters/spinner.js +48 -0
  23. package/dist/tui/component-adapters/tasks.d.ts +9 -0
  24. package/dist/tui/component-adapters/tasks.js +31 -0
  25. package/docs/component-reference.md +474 -0
  26. package/docs/getting-started.md +242 -0
  27. package/docs/help-contract-spec.md +29 -0
  28. package/docs/integration-examples.md +677 -0
  29. package/docs/module-authoring-guide.md +156 -0
  30. package/docs/snap-args.md +323 -0
  31. package/docs/snap-help.md +372 -0
  32. package/docs/snap-runtime.md +394 -0
  33. package/docs/snap-terminal.md +410 -0
  34. package/docs/snap-tui.md +529 -0
  35. package/package.json +4 -2
@@ -0,0 +1,156 @@
1
+ # Module Authoring Guide
2
+
3
+ ## Goal
4
+
5
+ Define actions once with full triad contract:
6
+
7
+ - `tui` flow steps (legacy `steps` or structured `flow`)
8
+ - `commandline` required/optional args
9
+ - `help` metadata
10
+
11
+ ## Minimal Module Shape
12
+
13
+ ```ts
14
+ import type { ModuleContract } from '../core/contracts/module-contract.js';
15
+ import { ExitCode } from '../core/errors/framework-errors.js';
16
+
17
+ const moduleContract: ModuleContract = {
18
+ moduleId: 'example',
19
+ description: 'Example module',
20
+ actions: [
21
+ {
22
+ actionId: 'run',
23
+ description: 'Run example action',
24
+ tui: { steps: ['collect-input', 'confirm'] },
25
+ commandline: { requiredArgs: ['name'] },
26
+ help: {
27
+ summary: 'Run example with one name argument.',
28
+ args: [{ name: 'name', required: true, description: 'Target name' }],
29
+ examples: ['hub example run --name=alice'],
30
+ useCases: [{ name: 'default', description: 'Basic usage', command: 'hub example run --name=alice' }],
31
+ keybindings: ['Enter confirm', 'Esc cancel']
32
+ },
33
+ run: async (context) => ({
34
+ ok: true,
35
+ mode: context.mode,
36
+ exitCode: ExitCode.SUCCESS,
37
+ data: `hello ${String(context.args.name ?? '')}`
38
+ })
39
+ }
40
+ ]
41
+ };
42
+
43
+ export default moduleContract;
44
+ ```
45
+
46
+ ## Structured TUI Flow (Optional)
47
+
48
+ Use `tui.flow` when you need explicit component/step definitions:
49
+
50
+ ```ts
51
+ tui: {
52
+ flow: {
53
+ entryStepId: 'operation',
54
+ steps: [
55
+ {
56
+ stepId: 'operation',
57
+ title: 'Choose operation',
58
+ components: [
59
+ {
60
+ componentId: 'op',
61
+ type: 'select',
62
+ label: 'Operation',
63
+ arg: 'op',
64
+ required: true,
65
+ options: [
66
+ { value: 'list', label: 'List profiles' },
67
+ { value: 'upsert', label: 'Create/update profile' }
68
+ ]
69
+ }
70
+ ]
71
+ }
72
+ ]
73
+ }
74
+ }
75
+ ```
76
+
77
+ `tui.steps` remains supported and backward compatible.
78
+
79
+ ## Custom TUI Components
80
+
81
+ For advanced prompts (for example custom searchable selectors inspired by Clack patterns), declare custom components in flow metadata:
82
+
83
+ ```ts
84
+ import { SnapTui } from '../src/index.js';
85
+
86
+ const flow = SnapTui.defineTuiFlow({
87
+ entryStepId: 'choose',
88
+ steps: [
89
+ {
90
+ stepId: 'choose',
91
+ title: 'Choose target',
92
+ components: [
93
+ SnapTui.defineCustomTuiComponent({
94
+ componentId: 'target',
95
+ label: 'Target',
96
+ arg: 'target',
97
+ renderer: 'searchable-select',
98
+ config: { maxItems: 8, allowCustomValue: true }
99
+ })
100
+ ]
101
+ }
102
+ ]
103
+ });
104
+ ```
105
+
106
+ `renderer` identifies your adapter implementation while preserving a typed, framework-level contract.
107
+
108
+ ## Register Module
109
+
110
+ Add module to registry bootstrapping in `/Users/khang/Documents/repo/snap/src/cli-entry.ts`.
111
+
112
+ ## CLI Bootstrapping Helpers
113
+
114
+ Use Snap CLI helpers so module/tool authors do not re-implement argv parsing and dispatch:
115
+
116
+ ```ts
117
+ import { createRegistry, runMultiModuleCli, runSingleModuleCli, runSubmoduleCli } from '../src/index.js';
118
+ ```
119
+
120
+ - `runMultiModuleCli`: standard `tool <module> <action> --args`.
121
+ - `runSingleModuleCli`: dedicated tool package mapped to one module, with optional default action.
122
+ - `runSubmoduleCli`: dedicated tool package with multiple sub-modules (`tool <submodule> ...`) plus optional default sub-module.
123
+ - Supports `-h/--help`.
124
+ - Parses `--key=value`, `--key value`, and boolean flags.
125
+ - Routes help and action dispatch through framework runtime.
126
+
127
+ ## DX Helpers
128
+
129
+ Snap exposes optional helpers so action authors avoid low-level terminal and argv plumbing:
130
+
131
+ ```ts
132
+ import { SnapArgs, SnapHelp, SnapRuntime, SnapTui } from '../src/index.js';
133
+ ```
134
+
135
+ - `SnapArgs`: typed argument readers and parsers (`readStringArg`, `readBooleanArg`, `collectUpperSnakeCaseEnvArgs`).
136
+ - `SnapHelp`: help/commandline builder from arg schema (`defineArgSchema`, `buildHelpFromArgSchema`, `commandlineFromArgSchema`).
137
+ - `SnapRuntime`: action result helpers (`runActionSafely`, standardized success/error envelopes).
138
+ - `SnapTui`: typed TUI flow/component definitions (`defineTuiFlow`, `defineCustomTuiComponent`).
139
+
140
+ Action runtime context already includes friendly APIs:
141
+
142
+ ```ts
143
+ context.prompts.text(...)
144
+ context.prompts.select(...)
145
+ context.terminal.line(...)
146
+ context.flow.next()
147
+ ```
148
+
149
+ Interactive prompt calls are rendered with Clack components through Snap adapters, including option hints and cancellation handling.
150
+
151
+ No direct `process.stdout` / state-machine wiring is required in module actions.
152
+
153
+ ## Validation Rules
154
+
155
+ Registration fails if any action misses triad data (`tui`, `commandline`, `help`).
156
+ A valid `tui` must provide either non-empty `steps` or non-empty `flow.steps`.
@@ -0,0 +1,323 @@
1
+ # SnapArgs - Type-Safe Argument Reading
2
+
3
+ `SnapArgs` provides typed helper functions for reading and parsing command-line arguments and environment variables.
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import * as SnapArgs from 'snap-framework';
9
+ ```
10
+
11
+ ## API Reference
12
+
13
+ ### `readStringArg(args, ...keys)`
14
+
15
+ Reads a string argument from multiple possible keys, returning the first non-empty value.
16
+
17
+ ```typescript
18
+ const name = SnapArgs.readStringArg(context.args, 'name', 'username', 'user');
19
+ // Returns: string | undefined
20
+ ```
21
+
22
+ **Usage Example:**
23
+
24
+ ```typescript
25
+ run: async (context) => {
26
+ const name = SnapArgs.readStringArg(context.args, 'name', 'username');
27
+ if (!name) {
28
+ return { ok: false, errorMessage: 'Name is required' };
29
+ }
30
+ return { ok: true, data: `Hello, ${name}` };
31
+ }
32
+ ```
33
+
34
+ ### `readRequiredStringArg(args, key, message?)`
35
+
36
+ Reads a required string argument, throwing an error if not present.
37
+
38
+ ```typescript
39
+ const name = SnapArgs.readRequiredStringArg(context.args, 'name', 'Name is required');
40
+ // Returns: string (throws if missing)
41
+ ```
42
+
43
+ **Usage Example:**
44
+
45
+ ```typescript
46
+ run: async (context) => {
47
+ try {
48
+ const name = SnapArgs.readRequiredStringArg(context.args, 'name');
49
+ return { ok: true, data: `Hello, ${name}` };
50
+ } catch (error) {
51
+ return {
52
+ ok: false,
53
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
54
+ };
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### `readBooleanArg(args, ...keys)`
60
+
61
+ Reads a boolean argument, parsing common string representations.
62
+
63
+ ```typescript
64
+ const verbose = SnapArgs.readBooleanArg(context.args, 'verbose', 'v');
65
+ // Returns: boolean | undefined
66
+ ```
67
+
68
+ **Supported string values:**
69
+ - **True**: `'1'`, `'true'`, `'yes'`, `'y'`, `'on'`
70
+ - **False**: `'0'`, `'false'`, `'no'`, `'n'`, `'off'`
71
+
72
+ **Usage Example:**
73
+
74
+ ```typescript
75
+ run: async (context) => {
76
+ const verbose = SnapArgs.readBooleanArg(context.args, 'verbose', 'v');
77
+ const debug = SnapArgs.readBooleanArg(context.args, 'debug');
78
+
79
+ if (verbose) {
80
+ context.terminal.line('Verbose mode enabled');
81
+ }
82
+
83
+ return { ok: true, data: { debug, verbose } };
84
+ }
85
+ ```
86
+
87
+ ### `parseBooleanLike(value)`
88
+
89
+ Parses a boolean-like string value into a boolean.
90
+
91
+ ```typescript
92
+ const isTrue = SnapArgs.parseBooleanLike('yes'); // true
93
+ const isFalse = SnapArgs.parseBooleanLike('0'); // false
94
+ const isUndefined = SnapArgs.parseBooleanLike(undefined); // undefined
95
+ ```
96
+
97
+ **Usage Example:**
98
+
99
+ ```typescript
100
+ run: async (context) => {
101
+ const forceValue = context.args.force;
102
+
103
+ if (forceValue !== undefined) {
104
+ const force = SnapArgs.parseBooleanLike(forceValue);
105
+ // Use force boolean
106
+ }
107
+
108
+ return { ok: true, data: {} };
109
+ }
110
+ ```
111
+
112
+ ### `collectUpperSnakeCaseEnvArgs(args, prefix)`
113
+
114
+ Collects environment variables with a given prefix into a typed object.
115
+
116
+ ```typescript
117
+ const envArgs = SnapArgs.collectUpperSnakeCaseEnvArgs(context.args, 'MYAPP_');
118
+ // Returns: Partial<Record<UpperSnakeCaseKey, string>>
119
+ ```
120
+
121
+ **Usage Example:**
122
+
123
+ ```typescript
124
+ // With environment variables: MYAPP_API_KEY, MYAPP_TIMEOUT
125
+ run: async (context) => {
126
+ const envArgs = SnapArgs.collectUpperSnakeCaseEnvArgs(context.args, 'MYAPP_');
127
+
128
+ const apiKey = envArgs.MYAPP_API_KEY;
129
+ const timeout = envArgs.MYAPP_TIMEOUT;
130
+
131
+ return { ok: true, data: { apiKey, timeout } };
132
+ }
133
+ ```
134
+
135
+ ## Complete Example Module
136
+
137
+ Here's a complete module using `SnapArgs` helpers:
138
+
139
+ ```typescript
140
+ import type { ModuleContract } from 'snap-framework';
141
+ import { ExitCode } from 'snap-framework';
142
+ import * as SnapArgs from 'snap-framework';
143
+
144
+ const deployModule: ModuleContract = {
145
+ moduleId: 'deploy',
146
+ description: 'Deployment management',
147
+ actions: [
148
+ {
149
+ actionId: 'start',
150
+ description: 'Start a deployment',
151
+ tui: {
152
+ steps: ['collect-environment', 'collect-options', 'confirm'],
153
+ flow: {
154
+ entryStepId: 'collect-environment',
155
+ steps: [
156
+ {
157
+ stepId: 'collect-environment',
158
+ title: 'Select Environment',
159
+ components: [
160
+ {
161
+ componentId: 'env',
162
+ type: 'select',
163
+ label: 'Environment',
164
+ arg: 'environment',
165
+ required: true,
166
+ options: [
167
+ { value: 'development', label: 'Development' },
168
+ { value: 'staging', label: 'Staging' },
169
+ { value: 'production', label: 'Production' }
170
+ ]
171
+ }
172
+ ]
173
+ },
174
+ {
175
+ stepId: 'collect-options',
176
+ title: 'Deployment Options',
177
+ components: [
178
+ {
179
+ componentId: 'verbose',
180
+ type: 'confirm',
181
+ label: 'Enable verbose logging',
182
+ arg: 'verbose'
183
+ },
184
+ {
185
+ componentId: 'force',
186
+ type: 'confirm',
187
+ label: 'Force deployment (skip checks)',
188
+ arg: 'force'
189
+ }
190
+ ]
191
+ },
192
+ {
193
+ stepId: 'confirm',
194
+ title: 'Confirm Deployment'
195
+ }
196
+ ]
197
+ }
198
+ },
199
+ commandline: {
200
+ requiredArgs: ['environment'],
201
+ optionalArgs: ['verbose', 'force']
202
+ },
203
+ help: {
204
+ summary: 'Deploy application to the specified environment.',
205
+ args: [
206
+ {
207
+ name: 'environment',
208
+ required: true,
209
+ description: 'Target deployment environment',
210
+ example: '--environment=production'
211
+ },
212
+ {
213
+ name: 'verbose',
214
+ required: false,
215
+ description: 'Enable verbose output',
216
+ example: '--verbose=true'
217
+ },
218
+ {
219
+ name: 'force',
220
+ required: false,
221
+ description: 'Skip pre-deployment checks',
222
+ example: '--force=yes'
223
+ }
224
+ ],
225
+ examples: [
226
+ 'mytool deploy start --environment=production',
227
+ 'mytool deploy start --environment=staging --verbose=true'
228
+ ],
229
+ useCases: [
230
+ {
231
+ name: 'production',
232
+ description: 'Deploy to production',
233
+ command: 'mytool deploy start --environment=production'
234
+ }
235
+ ],
236
+ keybindings: ['Enter confirm', 'Esc cancel']
237
+ },
238
+ run: async (context) => {
239
+ // Read required environment
240
+ const environment = SnapArgs.readRequiredStringArg(
241
+ context.args,
242
+ 'environment',
243
+ 'Environment is required'
244
+ );
245
+
246
+ // Read optional boolean flags
247
+ const verbose = SnapArgs.readBooleanArg(context.args, 'verbose') ?? false;
248
+ const force = SnapArgs.readBooleanArg(context.args, 'force') ?? false;
249
+
250
+ if (verbose) {
251
+ context.terminal.line(`Deploying to ${environment}...`);
252
+ }
253
+
254
+ if (force) {
255
+ context.terminal.line('Warning: Skipping pre-deployment checks!');
256
+ }
257
+
258
+ // Perform deployment logic here...
259
+
260
+ return {
261
+ ok: true,
262
+ mode: context.mode,
263
+ exitCode: ExitCode.SUCCESS,
264
+ data: `Deployed to ${environment}`
265
+ };
266
+ }
267
+ }
268
+ ]
269
+ };
270
+
271
+ export default deployModule;
272
+ ```
273
+
274
+ ## Environment Variable Integration
275
+
276
+ Combine commandline args with environment variables:
277
+
278
+ ```typescript
279
+ run: async (context) => {
280
+ // Try commandline first, fall back to environment
281
+ const apiKey = SnapArgs.readStringArg(
282
+ context.args,
283
+ 'api-key',
284
+ 'apiKey',
285
+ 'API_KEY'
286
+ );
287
+
288
+ if (!apiKey) {
289
+ return {
290
+ ok: false,
291
+ errorMessage: 'API key must be provided via --api-key or API_KEY env var'
292
+ };
293
+ }
294
+
295
+ return { ok: true, data: { apiKey } };
296
+ }
297
+ ```
298
+
299
+ ## Best Practices
300
+
301
+ 1. **Always validate required args**: Use `readRequiredStringArg` for critical arguments
302
+ 2. **Provide fallback values**: Use `??` operator for optional defaults
303
+ 3. **Support multiple key aliases**: Use variadic args for flexible naming
304
+ 4. **Parse booleans safely**: Always use `readBooleanArg` for flag arguments
305
+ 5. **Collect environment prefixes**: Use `collectUpperSnakeCaseEnvArgs` for env var grouping
306
+
307
+ ## Type Safety
308
+
309
+ `SnapArgs` provides full TypeScript type safety:
310
+
311
+ ```typescript
312
+ import type { CliArgs } from 'snap-framework';
313
+
314
+ const args: CliArgs = {
315
+ name: 'Alice',
316
+ verbose: 'true',
317
+ count: '42'
318
+ };
319
+
320
+ // TypeScript knows these return types
321
+ const name: string | undefined = SnapArgs.readStringArg(args, 'name');
322
+ const verbose: boolean | undefined = SnapArgs.readBooleanArg(args, 'verbose');
323
+ ```