@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.
- package/CHANGELOG.md +41 -0
- package/README.md +26 -2
- package/dist/dx/terminal/index.d.ts +2 -1
- package/dist/dx/terminal/index.js +3 -1
- package/dist/dx/terminal/intro-outro.d.ts +4 -0
- package/dist/dx/terminal/intro-outro.js +44 -0
- package/dist/dx/terminal/output.d.ts +13 -1
- package/dist/dx/terminal/output.js +43 -2
- package/dist/dx/tui/index.d.ts +12 -0
- package/dist/dx/tui/index.js +12 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/tui/component-adapters/autocomplete.d.ts +15 -0
- package/dist/tui/component-adapters/autocomplete.js +34 -0
- package/dist/tui/component-adapters/note.d.ts +7 -0
- package/dist/tui/component-adapters/note.js +23 -0
- package/dist/tui/component-adapters/password.d.ts +7 -0
- package/dist/tui/component-adapters/password.js +24 -0
- package/dist/tui/component-adapters/progress.d.ts +7 -0
- package/dist/tui/component-adapters/progress.js +44 -0
- package/dist/tui/component-adapters/spinner.d.ts +10 -0
- package/dist/tui/component-adapters/spinner.js +48 -0
- package/dist/tui/component-adapters/tasks.d.ts +9 -0
- package/dist/tui/component-adapters/tasks.js +31 -0
- package/docs/component-reference.md +474 -0
- package/docs/getting-started.md +242 -0
- package/docs/help-contract-spec.md +29 -0
- package/docs/integration-examples.md +677 -0
- package/docs/module-authoring-guide.md +156 -0
- package/docs/snap-args.md +323 -0
- package/docs/snap-help.md +372 -0
- package/docs/snap-runtime.md +394 -0
- package/docs/snap-terminal.md +410 -0
- package/docs/snap-tui.md +529 -0
- package/package.json +4 -2
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# SnapTerminal - Terminal Output Helpers
|
|
2
|
+
|
|
3
|
+
`SnapTerminal` provides a simple, testable interface for writing to the terminal.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import * as SnapTerminal from 'snap-framework';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API Reference
|
|
12
|
+
|
|
13
|
+
### `createTerminalOutput(stdout?, stderr?)`
|
|
14
|
+
|
|
15
|
+
Creates a terminal output interface with write methods.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
const terminal = SnapTerminal.createTerminalOutput();
|
|
19
|
+
// Or with custom streams:
|
|
20
|
+
const customTerminal = SnapTerminal.createTerminalOutput(customStdout, customStderr);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Default:** Uses `process.stdout` and `process.stderr`
|
|
24
|
+
|
|
25
|
+
### TerminalOutput Interface
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
interface TerminalOutput {
|
|
29
|
+
line(message: string): void;
|
|
30
|
+
lines(messages: readonly string[]): void;
|
|
31
|
+
error(message: string): void;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
#### `line(message)`
|
|
36
|
+
|
|
37
|
+
Writes a single line to stdout.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
terminal.line('Hello, world!');
|
|
41
|
+
// Output: Hello, world!
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### `lines(messages)`
|
|
45
|
+
|
|
46
|
+
Writes multiple lines to stdout.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
terminal.lines([
|
|
50
|
+
'Line 1',
|
|
51
|
+
'Line 2',
|
|
52
|
+
'Line 3'
|
|
53
|
+
]);
|
|
54
|
+
// Output:
|
|
55
|
+
// Line 1
|
|
56
|
+
// Line 2
|
|
57
|
+
// Line 3
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### `error(message)`
|
|
61
|
+
|
|
62
|
+
Writes an error message to stderr.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
terminal.error('Something went wrong!');
|
|
66
|
+
// Output (stderr): Something went wrong!
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage in Actions
|
|
70
|
+
|
|
71
|
+
The `RuntimeContext` includes a `terminal` property:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
run: async (context) => {
|
|
75
|
+
// Access terminal via context
|
|
76
|
+
context.terminal.line('Starting operation...');
|
|
77
|
+
context.terminal.lines(['Progress:', ' - Step 1 complete', ' - Step 2 complete']);
|
|
78
|
+
|
|
79
|
+
// Errors
|
|
80
|
+
context.terminal.error('Operation failed!');
|
|
81
|
+
|
|
82
|
+
return { ok: true, mode: context.mode, exitCode: ExitCode.SUCCESS, data: {} };
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Complete Examples
|
|
87
|
+
|
|
88
|
+
### Basic Output
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import type { ModuleContract } from 'snap-framework';
|
|
92
|
+
import { ExitCode } from 'snap-framework';
|
|
93
|
+
|
|
94
|
+
const echoModule: ModuleContract = {
|
|
95
|
+
moduleId: 'echo',
|
|
96
|
+
description: 'Echo messages',
|
|
97
|
+
actions: [
|
|
98
|
+
{
|
|
99
|
+
actionId: 'say',
|
|
100
|
+
description: 'Echo a message',
|
|
101
|
+
tui: { steps: ['collect-message'] },
|
|
102
|
+
commandline: { requiredArgs: ['message'] },
|
|
103
|
+
help: {
|
|
104
|
+
summary: 'Echo a message to the terminal.',
|
|
105
|
+
args: [{ name: 'message', required: true, description: 'Message to echo' }]
|
|
106
|
+
},
|
|
107
|
+
run: async (context) => {
|
|
108
|
+
const message = String(context.args.message ?? '');
|
|
109
|
+
|
|
110
|
+
// Output the message
|
|
111
|
+
context.terminal.line(message);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
mode: context.mode,
|
|
116
|
+
exitCode: ExitCode.SUCCESS,
|
|
117
|
+
data: { echoed: message }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Progress Updates
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const deployModule: ModuleContract = {
|
|
129
|
+
moduleId: 'deploy',
|
|
130
|
+
description: 'Deploy with progress',
|
|
131
|
+
actions: [
|
|
132
|
+
{
|
|
133
|
+
actionId: 'start',
|
|
134
|
+
description: 'Start deployment',
|
|
135
|
+
tui: { steps: ['collect-input'] },
|
|
136
|
+
commandline: { requiredArgs: ['environment'] },
|
|
137
|
+
help: {
|
|
138
|
+
summary: 'Deploy to environment with progress updates.',
|
|
139
|
+
args: [{ name: 'environment', required: true, description: 'Target env' }]
|
|
140
|
+
},
|
|
141
|
+
run: async (context) => {
|
|
142
|
+
const environment = String(context.args.environment ?? '');
|
|
143
|
+
|
|
144
|
+
context.terminal.line(`Deploying to ${environment}...`);
|
|
145
|
+
context.terminal.line('');
|
|
146
|
+
|
|
147
|
+
// Simulate deployment steps
|
|
148
|
+
const steps = [
|
|
149
|
+
'Building application...',
|
|
150
|
+
'Running tests...',
|
|
151
|
+
'Uploading assets...',
|
|
152
|
+
'Running migrations...',
|
|
153
|
+
'Starting services...'
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (const step of steps) {
|
|
157
|
+
context.terminal.line(` ✓ ${step}`);
|
|
158
|
+
// Simulate work
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
context.terminal.line('');
|
|
163
|
+
context.terminal.line('Deployment complete!');
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
ok: true,
|
|
167
|
+
mode: context.mode,
|
|
168
|
+
exitCode: ExitCode.SUCCESS,
|
|
169
|
+
data: { environment, status: 'deployed' }
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Error Reporting
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const validationModule: ModuleContract = {
|
|
181
|
+
moduleId: 'validate',
|
|
182
|
+
description: 'Configuration validation',
|
|
183
|
+
actions: [
|
|
184
|
+
{
|
|
185
|
+
actionId: 'check',
|
|
186
|
+
description: 'Validate configuration',
|
|
187
|
+
tui: { steps: ['collect-config'] },
|
|
188
|
+
commandline: { requiredArgs: ['config'] },
|
|
189
|
+
help: {
|
|
190
|
+
summary: 'Validate configuration file.',
|
|
191
|
+
args: [{ name: 'config', required: true, description: 'Config file path' }]
|
|
192
|
+
},
|
|
193
|
+
run: async (context) => {
|
|
194
|
+
const configPath = String(context.args.config ?? '');
|
|
195
|
+
|
|
196
|
+
context.terminal.line(`Validating ${configPath}...`);
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const config = await loadConfig(configPath);
|
|
200
|
+
const errors = validateConfig(config);
|
|
201
|
+
|
|
202
|
+
if (errors.length > 0) {
|
|
203
|
+
context.terminal.error('Configuration validation failed:');
|
|
204
|
+
context.terminal.lines(errors.map(e => ` ✗ ${e}`));
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
mode: context.mode,
|
|
209
|
+
exitCode: ExitCode.VALIDATION_ERROR,
|
|
210
|
+
errorMessage: errors.join('; ')
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
context.terminal.line('Configuration is valid!');
|
|
215
|
+
context.terminal.lines([
|
|
216
|
+
` Environment: ${config.environment}`,
|
|
217
|
+
` Region: ${config.region}`,
|
|
218
|
+
` Log level: ${config.logLevel}`
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
mode: context.mode,
|
|
224
|
+
exitCode: ExitCode.SUCCESS,
|
|
225
|
+
data: { valid: true, config }
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
context.terminal.error(`Failed to load config: ${error}`);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
mode: context.mode,
|
|
233
|
+
exitCode: ExitCode.RUNTIME_ERROR,
|
|
234
|
+
errorMessage: 'Failed to load configuration'
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
};
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Table Output
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
const listModule: ModuleContract = {
|
|
247
|
+
moduleId: 'list',
|
|
248
|
+
description: 'List resources',
|
|
249
|
+
actions: [
|
|
250
|
+
{
|
|
251
|
+
actionId: 'users',
|
|
252
|
+
description: 'List all users',
|
|
253
|
+
tui: { steps: [] },
|
|
254
|
+
commandline: { requiredArgs: [] },
|
|
255
|
+
help: {
|
|
256
|
+
summary: 'List all users in the system.'
|
|
257
|
+
},
|
|
258
|
+
run: async (context) => {
|
|
259
|
+
const users = await fetchUsers();
|
|
260
|
+
|
|
261
|
+
context.terminal.line('Users:');
|
|
262
|
+
context.terminal.line('');
|
|
263
|
+
|
|
264
|
+
if (users.length === 0) {
|
|
265
|
+
context.terminal.line(' No users found.');
|
|
266
|
+
} else {
|
|
267
|
+
// Simple table formatting
|
|
268
|
+
const maxNameLength = Math.max(...users.map(u => u.name.length));
|
|
269
|
+
const maxEmailLength = Math.max(...users.map(u => u.email.length));
|
|
270
|
+
|
|
271
|
+
context.terminal.line(
|
|
272
|
+
` ${'Name'.padEnd(maxNameLength)} ${'Email'.padEnd(maxEmailLength)} Role`
|
|
273
|
+
);
|
|
274
|
+
context.terminal.line(
|
|
275
|
+
` ${'─'.repeat(maxNameLength)} ${'─'.repeat(maxEmailLength)} ──────`
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
for (const user of users) {
|
|
279
|
+
context.terminal.line(
|
|
280
|
+
` ${user.name.padEnd(maxNameLength)} ${user.email.padEnd(maxEmailLength)} ${user.role}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
context.terminal.line('');
|
|
285
|
+
context.terminal.line(` Total: ${users.length} user(s)`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
ok: true,
|
|
290
|
+
mode: context.mode,
|
|
291
|
+
exitCode: ExitCode.SUCCESS,
|
|
292
|
+
data: { count: users.length, users }
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Testing with Terminal Output
|
|
301
|
+
|
|
302
|
+
`createTerminalOutput` accepts custom Writable streams for testing:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { PassThrough } from 'node:stream';
|
|
306
|
+
import * as SnapTerminal from 'snap-framework';
|
|
307
|
+
|
|
308
|
+
describe('my action', () => {
|
|
309
|
+
it('should output messages', async () => {
|
|
310
|
+
// Create in-memory streams
|
|
311
|
+
const stdout = new PassThrough();
|
|
312
|
+
const stderr = new PassThrough();
|
|
313
|
+
const outputChunks: string[] = [];
|
|
314
|
+
|
|
315
|
+
stdout.on('data', (chunk) => outputChunks.push(chunk.toString()));
|
|
316
|
+
|
|
317
|
+
const terminal = SnapTerminal.createTerminalOutput(stdout, stderr);
|
|
318
|
+
|
|
319
|
+
// Use in your action or test
|
|
320
|
+
terminal.line('Hello, world!');
|
|
321
|
+
terminal.error('Error!');
|
|
322
|
+
|
|
323
|
+
// Verify output
|
|
324
|
+
expect(outputChunks).toContain('Hello, world!\n');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Mocking for Unit Tests
|
|
330
|
+
|
|
331
|
+
Simple mock for tests:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const mockTerminal = {
|
|
335
|
+
line: vi.fn(),
|
|
336
|
+
lines: vi.fn(),
|
|
337
|
+
error: vi.fn()
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Use in test
|
|
341
|
+
const context = {
|
|
342
|
+
// ... other properties
|
|
343
|
+
terminal: mockTerminal
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
await action.run(context);
|
|
347
|
+
|
|
348
|
+
expect(mockTerminal.line).toHaveBeenCalledWith('Starting...');
|
|
349
|
+
expect(mockTerminal.error).not.toHaveBeenCalled();
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Best Practices
|
|
353
|
+
|
|
354
|
+
1. **Use line() for single messages** - Most common case
|
|
355
|
+
2. **Use lines() for bulk output** - More efficient than multiple line() calls
|
|
356
|
+
3. **Use error() for errors only** - Goes to stderr, not stdout
|
|
357
|
+
4. **Add blank lines for readability** - `context.terminal.line('')` for spacing
|
|
358
|
+
5. **Keep output structured** - Use consistent formatting
|
|
359
|
+
6. **Consider non-TTY environments** - Avoid complex ANSI codes without detection
|
|
360
|
+
7. **Test output separately** - Use custom streams in tests
|
|
361
|
+
|
|
362
|
+
## Output Patterns
|
|
363
|
+
|
|
364
|
+
### Status Messages
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
context.terminal.line('✓ Operation completed');
|
|
368
|
+
context.terminal.line('✗ Operation failed');
|
|
369
|
+
context.terminal.line('→ Processing...');
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Section Headers
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
context.terminal.line('');
|
|
376
|
+
context.terminal.line('=== Deployment Summary ===');
|
|
377
|
+
context.terminal.line('');
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Key-Value Pairs
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
context.terminal.line(`Environment: ${env}`);
|
|
384
|
+
context.terminal.line(`Region: ${region}`);
|
|
385
|
+
context.terminal.line(`Status: ${status}`);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Lists
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
context.terminal.line('Changes:');
|
|
392
|
+
context.terminal.lines([
|
|
393
|
+
' - Added user authentication',
|
|
394
|
+
' - Updated dependencies',
|
|
395
|
+
' - Fixed login bug'
|
|
396
|
+
]);
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Integration with Context
|
|
400
|
+
|
|
401
|
+
The `RuntimeContext` always has a `terminal` property:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
interface RuntimeContext {
|
|
405
|
+
terminal: TerminalOutput;
|
|
406
|
+
// ... other properties
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
No need to create your own in actions - just use `context.terminal`.
|