@celilo/cli 0.1.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 +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fuel-Gauge Tests
|
|
3
|
+
* Tests pure rendering functions without terminal interaction
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
import { FuelGauge } from './fuel-gauge';
|
|
8
|
+
|
|
9
|
+
describe('FuelGauge', () => {
|
|
10
|
+
describe('buildProgressBar', () => {
|
|
11
|
+
test('builds bar with gradient at position 0 moving right', () => {
|
|
12
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
13
|
+
const bar = gauge.buildProgressBar(0, 20, 1, false);
|
|
14
|
+
|
|
15
|
+
// Gradient (4 chars) at position 0, moving right: ░▒▓█
|
|
16
|
+
expect(bar).toBe('░▒▓█················');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('builds bar with gradient at position 0 moving left', () => {
|
|
20
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
21
|
+
const bar = gauge.buildProgressBar(0, 20, -1, false);
|
|
22
|
+
|
|
23
|
+
// Gradient (4 chars) at position 0, moving left: █▓▒░
|
|
24
|
+
expect(bar).toBe('█▓▒░················');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('builds bar with gradient at middle position moving right', () => {
|
|
28
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
29
|
+
const bar = gauge.buildProgressBar(5, 20, 1, false);
|
|
30
|
+
|
|
31
|
+
// Gradient at position 5, moving right
|
|
32
|
+
expect(bar).toBe('·····░▒▓█···········');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('builds bar with gradient at middle position moving left', () => {
|
|
36
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
37
|
+
const bar = gauge.buildProgressBar(5, 20, -1, false);
|
|
38
|
+
|
|
39
|
+
// Gradient at position 5, moving left
|
|
40
|
+
expect(bar).toBe('·····█▓▒░···········');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('builds bar with gradient at end position', () => {
|
|
44
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
45
|
+
const bar = gauge.buildProgressBar(16, 20, 1, false);
|
|
46
|
+
|
|
47
|
+
// Gradient at position 16 (near right edge)
|
|
48
|
+
expect(bar).toBe('················░▒▓█');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('handles narrow bar width', () => {
|
|
52
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
53
|
+
const bar = gauge.buildProgressBar(0, 15, 1, false);
|
|
54
|
+
|
|
55
|
+
expect(bar).toHaveLength(15);
|
|
56
|
+
expect(bar).toMatch(/^[░▒▓█]+·+$/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('handles wide bar width', () => {
|
|
60
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
61
|
+
const bar = gauge.buildProgressBar(20, 100, 1, false);
|
|
62
|
+
|
|
63
|
+
expect(bar).toHaveLength(100);
|
|
64
|
+
expect(bar).toMatch(/^·+[░▒▓█]+·+$/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('pulses front block when waiting (pulse=true, moving right)', () => {
|
|
68
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
69
|
+
|
|
70
|
+
// pulseState starts at 0, front block (right-most) alternates █ -> ▓
|
|
71
|
+
const bar1 = gauge.buildProgressBar(0, 20, 1, true);
|
|
72
|
+
expect(bar1).toBe('░▒▓█················'); // pulseState % 2 = 0, shows solid █
|
|
73
|
+
|
|
74
|
+
// After pulseState increments to 1, front block shows next level
|
|
75
|
+
// (Note: pulseState is private, we can't easily test the actual alternation)
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('pulses front block when waiting (pulse=true, moving left)', () => {
|
|
79
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
80
|
+
|
|
81
|
+
// pulseState starts at 0, front block (left-most) alternates █ -> ▓
|
|
82
|
+
const bar1 = gauge.buildProgressBar(0, 20, -1, true);
|
|
83
|
+
expect(bar1).toBe('█▓▒░················'); // pulseState % 2 = 0, shows solid █
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('formatOutputLines', () => {
|
|
88
|
+
test('returns last N lines', () => {
|
|
89
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
90
|
+
const lines = ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5'];
|
|
91
|
+
|
|
92
|
+
const formatted = gauge.formatOutputLines(lines, 3);
|
|
93
|
+
|
|
94
|
+
expect(formatted).toHaveLength(3);
|
|
95
|
+
expect(formatted[0]).toContain('Line 3');
|
|
96
|
+
expect(formatted[1]).toContain('Line 4');
|
|
97
|
+
expect(formatted[2]).toContain('Line 5');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('pads lines with indentation', () => {
|
|
101
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
102
|
+
const lines = ['Test output'];
|
|
103
|
+
|
|
104
|
+
const formatted = gauge.formatOutputLines(lines, 1);
|
|
105
|
+
|
|
106
|
+
expect(formatted[0]).toBe(' Test output');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('truncates long lines', () => {
|
|
110
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
111
|
+
const longLine = 'a'.repeat(200);
|
|
112
|
+
|
|
113
|
+
const formatted = gauge.formatOutputLines([longLine], 1);
|
|
114
|
+
|
|
115
|
+
// Should be truncated with ...
|
|
116
|
+
expect(formatted[0].length).toBeLessThan(200);
|
|
117
|
+
expect(formatted[0]).toMatch(/\.\.\.$/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('handles empty lines array', () => {
|
|
121
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
122
|
+
|
|
123
|
+
const formatted = gauge.formatOutputLines([], 3);
|
|
124
|
+
|
|
125
|
+
expect(formatted).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('handles fewer lines than max', () => {
|
|
129
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
130
|
+
const lines = ['Line 1', 'Line 2'];
|
|
131
|
+
|
|
132
|
+
const formatted = gauge.formatOutputLines(lines, 5);
|
|
133
|
+
|
|
134
|
+
// Should return all available lines
|
|
135
|
+
expect(formatted).toHaveLength(2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('addOutput and getOutput', () => {
|
|
140
|
+
test('stores output lines', () => {
|
|
141
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
142
|
+
|
|
143
|
+
gauge.addOutput('Line 1');
|
|
144
|
+
gauge.addOutput('Line 2');
|
|
145
|
+
gauge.addOutput('Line 3');
|
|
146
|
+
|
|
147
|
+
const output = gauge.getOutput();
|
|
148
|
+
expect(output).toEqual(['Line 1', 'Line 2', 'Line 3']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('strips ANSI codes from output', () => {
|
|
152
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
153
|
+
|
|
154
|
+
// Add line with ANSI color codes
|
|
155
|
+
gauge.addOutput('\x1b[31mRed text\x1b[0m');
|
|
156
|
+
gauge.addOutput('\x1b[36mCyan text\x1b[0m');
|
|
157
|
+
|
|
158
|
+
const output = gauge.getOutput();
|
|
159
|
+
expect(output[0]).toBe('Red text');
|
|
160
|
+
expect(output[1]).toBe('Cyan text');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('limits stored lines to 100', () => {
|
|
164
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
165
|
+
|
|
166
|
+
// Add 150 lines
|
|
167
|
+
for (let i = 0; i < 150; i++) {
|
|
168
|
+
gauge.addOutput(`Line ${i}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const output = gauge.getOutput();
|
|
172
|
+
expect(output).toHaveLength(100);
|
|
173
|
+
// Should have last 100 lines (50-149)
|
|
174
|
+
expect(output[0]).toBe('Line 50');
|
|
175
|
+
expect(output[99]).toBe('Line 149');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('start and stop', () => {
|
|
180
|
+
test('start sets running state in test mode', () => {
|
|
181
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
182
|
+
|
|
183
|
+
gauge.start();
|
|
184
|
+
|
|
185
|
+
// Should not throw and should be testable
|
|
186
|
+
expect(gauge).toBeDefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('stop clears running state in test mode', () => {
|
|
190
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
191
|
+
|
|
192
|
+
gauge.start();
|
|
193
|
+
gauge.stop(true);
|
|
194
|
+
|
|
195
|
+
// Should complete without error
|
|
196
|
+
expect(gauge).toBeDefined();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('handles stop before start', () => {
|
|
200
|
+
const gauge = new FuelGauge('Test', { skipAnimation: true });
|
|
201
|
+
|
|
202
|
+
// Should not throw
|
|
203
|
+
gauge.stop(true);
|
|
204
|
+
|
|
205
|
+
expect(gauge).toBeDefined();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fuel-Gauge Progress Indicator
|
|
3
|
+
*
|
|
4
|
+
* Custom Cylon-style progress bar with scrolling output preview
|
|
5
|
+
* Features:
|
|
6
|
+
* - Back-and-forth animated bar (like Battlestar Galactica Cylon eye)
|
|
7
|
+
* - 3-4 lines of scrolling greyed-out output above the bar
|
|
8
|
+
* - On success: clears output, shows only success message
|
|
9
|
+
* - On error: shows last 8 lines of output for debugging
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { stdout } from 'node:process';
|
|
13
|
+
import * as p from '@clack/prompts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ANSI color codes for terminal output
|
|
17
|
+
*/
|
|
18
|
+
const colors = {
|
|
19
|
+
cyan: (text: string) => `\x1b[36m${text}\x1b[0m`,
|
|
20
|
+
dim: (text: string) => `\x1b[2m${text}\x1b[0m`,
|
|
21
|
+
mutedPurple: (text: string) => `\x1b[38;5;238m${text}\x1b[0m`, // 256-color very dark gray-purple (barely visible)
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface FuelGaugeOptions {
|
|
26
|
+
output?: NodeJS.WriteStream;
|
|
27
|
+
skipAnimation?: boolean; // For testing
|
|
28
|
+
onBackground?: () => void; // Callback when ESC pressed
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fuel-Gauge progress indicator with Cylon-style animation
|
|
33
|
+
*
|
|
34
|
+
* Rendering strategy: builds the entire frame as a single string and
|
|
35
|
+
* writes it in one output.write() call to prevent flicker. Uses
|
|
36
|
+
* "move cursor up N lines" + overwrite rather than clear-then-draw.
|
|
37
|
+
*/
|
|
38
|
+
export class FuelGauge {
|
|
39
|
+
private title: string;
|
|
40
|
+
private outputLines: string[] = [];
|
|
41
|
+
private barPosition = 0;
|
|
42
|
+
private barDirection = 1; // 1 = right, -1 = left
|
|
43
|
+
private intervalId: NodeJS.Timeout | null = null;
|
|
44
|
+
private readonly maxDisplayLines = 4; // Show 3-4 lines of output
|
|
45
|
+
private readonly errorDisplayLines = 100; // Show full output on error
|
|
46
|
+
private readonly output: NodeJS.WriteStream;
|
|
47
|
+
private readonly skipAnimation: boolean;
|
|
48
|
+
private readonly onBackground?: () => void;
|
|
49
|
+
private running = false;
|
|
50
|
+
private hasNewOutput = false; // Track if new output arrived
|
|
51
|
+
private pulseState = 0; // Track pulse animation state (0-3)
|
|
52
|
+
private keyListener?: (chunk: Buffer) => void;
|
|
53
|
+
private sigintHandler?: () => void;
|
|
54
|
+
private alreadyCleanedUp = false; // Track if we've already cleaned up terminal
|
|
55
|
+
private linesDrawn = 0; // Track exactly how many lines were drawn last frame
|
|
56
|
+
|
|
57
|
+
constructor(title: string, options: FuelGaugeOptions = {}) {
|
|
58
|
+
this.title = title;
|
|
59
|
+
this.output = options.output || stdout;
|
|
60
|
+
this.skipAnimation = options.skipAnimation || false;
|
|
61
|
+
this.onBackground = options.onBackground;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Start the fuel-gauge animation
|
|
66
|
+
*/
|
|
67
|
+
start(): void {
|
|
68
|
+
if (this.skipAnimation) {
|
|
69
|
+
// Test mode: just track state
|
|
70
|
+
this.running = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.running = true;
|
|
75
|
+
|
|
76
|
+
// Hide cursor
|
|
77
|
+
this.output.write('\x1B[?25l');
|
|
78
|
+
|
|
79
|
+
// Set up SIGINT handler to restore terminal on Ctrl+C (not in test mode)
|
|
80
|
+
if (!this.skipAnimation) {
|
|
81
|
+
this.sigintHandler = () => {
|
|
82
|
+
if (this.alreadyCleanedUp) {
|
|
83
|
+
process.exit(130);
|
|
84
|
+
}
|
|
85
|
+
this.alreadyCleanedUp = true;
|
|
86
|
+
|
|
87
|
+
// Restore terminal: clear gauge area and show cursor
|
|
88
|
+
this.writeClearSequence();
|
|
89
|
+
this.output.write('\x1B[?25h');
|
|
90
|
+
|
|
91
|
+
if (this.intervalId) {
|
|
92
|
+
clearInterval(this.intervalId);
|
|
93
|
+
this.intervalId = null;
|
|
94
|
+
}
|
|
95
|
+
if (this.keyListener && process.stdin.isTTY) {
|
|
96
|
+
process.stdin.off('data', this.keyListener);
|
|
97
|
+
process.stdin.setRawMode(false);
|
|
98
|
+
process.stdin.pause();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
process.exit(130);
|
|
102
|
+
};
|
|
103
|
+
process.on('SIGINT', this.sigintHandler);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Set up keyboard listener for ESC key and Ctrl+C
|
|
107
|
+
if (this.onBackground && process.stdin.isTTY) {
|
|
108
|
+
process.stdin.setRawMode(true);
|
|
109
|
+
process.stdin.resume();
|
|
110
|
+
|
|
111
|
+
this.keyListener = (chunk: Buffer) => {
|
|
112
|
+
if (chunk[0] === 0x03) {
|
|
113
|
+
// Ctrl+C — raw mode swallows SIGINT, handle manually
|
|
114
|
+
if (this.sigintHandler) this.sigintHandler();
|
|
115
|
+
} else if (chunk[0] === 0x1b && chunk.length === 1) {
|
|
116
|
+
this.background();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
process.stdin.on('data', this.keyListener);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Draw initial frame
|
|
124
|
+
this.writeFrame();
|
|
125
|
+
|
|
126
|
+
// Animate at 100ms intervals
|
|
127
|
+
this.intervalId = setInterval(() => {
|
|
128
|
+
this.render();
|
|
129
|
+
}, 100);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Add output line (will be shown in scrolling preview)
|
|
134
|
+
*/
|
|
135
|
+
addOutput(line: string): void {
|
|
136
|
+
const cleaned = this.stripAnsi(line);
|
|
137
|
+
this.outputLines.push(cleaned);
|
|
138
|
+
|
|
139
|
+
if (this.outputLines.length > 100) {
|
|
140
|
+
this.outputLines.shift();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.hasNewOutput = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Background the animation (user pressed ESC)
|
|
148
|
+
*/
|
|
149
|
+
private background(): void {
|
|
150
|
+
if (!this.running) return;
|
|
151
|
+
|
|
152
|
+
this.cleanup();
|
|
153
|
+
p.log.info(`${this.title} (backgrounded)`);
|
|
154
|
+
|
|
155
|
+
if (this.onBackground) {
|
|
156
|
+
this.onBackground();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Stop the animation and finalize
|
|
162
|
+
*/
|
|
163
|
+
stop(success: boolean): void {
|
|
164
|
+
if (!this.running) return;
|
|
165
|
+
|
|
166
|
+
this.running = false;
|
|
167
|
+
|
|
168
|
+
if (this.skipAnimation) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.cleanup();
|
|
173
|
+
|
|
174
|
+
if (success) {
|
|
175
|
+
p.log.success(this.title);
|
|
176
|
+
} else {
|
|
177
|
+
p.log.error(this.title);
|
|
178
|
+
console.log('');
|
|
179
|
+
console.log(colors.dim('Last output:'));
|
|
180
|
+
const errorLines = this.outputLines.slice(-this.errorDisplayLines);
|
|
181
|
+
for (const line of errorLines) {
|
|
182
|
+
if (line.trim()) {
|
|
183
|
+
console.log(colors.dim(` ${line}`));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Clean up resources (keyboard listener, animation, cursor)
|
|
192
|
+
*/
|
|
193
|
+
private cleanup(): void {
|
|
194
|
+
if (this.alreadyCleanedUp) return;
|
|
195
|
+
this.alreadyCleanedUp = true;
|
|
196
|
+
|
|
197
|
+
if (this.intervalId) {
|
|
198
|
+
clearInterval(this.intervalId);
|
|
199
|
+
this.intervalId = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (this.sigintHandler) {
|
|
203
|
+
process.off('SIGINT', this.sigintHandler);
|
|
204
|
+
this.sigintHandler = undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (this.keyListener && process.stdin.isTTY) {
|
|
208
|
+
process.stdin.off('data', this.keyListener);
|
|
209
|
+
process.stdin.setRawMode(false);
|
|
210
|
+
process.stdin.pause();
|
|
211
|
+
this.keyListener = undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!this.skipAnimation) {
|
|
215
|
+
this.writeClearSequence();
|
|
216
|
+
this.output.write('\x1B[?25h');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get all captured output
|
|
222
|
+
*/
|
|
223
|
+
getOutput(): string[] {
|
|
224
|
+
return [...this.outputLines];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Build progress bar string (pure function for testing)
|
|
229
|
+
*/
|
|
230
|
+
buildProgressBar(position: number, width: number, direction: number, pulse: boolean): string {
|
|
231
|
+
const gradient = ['█', '▓', '▒', '░'];
|
|
232
|
+
const barEmpty = '·';
|
|
233
|
+
const barLength = gradient.length;
|
|
234
|
+
|
|
235
|
+
let bar = '';
|
|
236
|
+
for (let i = 0; i < width; i++) {
|
|
237
|
+
const offset = i - position;
|
|
238
|
+
|
|
239
|
+
if (offset >= 0 && offset < barLength) {
|
|
240
|
+
let charIndex: number;
|
|
241
|
+
let isFrontBlock = false;
|
|
242
|
+
|
|
243
|
+
if (direction === 1) {
|
|
244
|
+
charIndex = barLength - 1 - offset;
|
|
245
|
+
isFrontBlock = offset === barLength - 1;
|
|
246
|
+
} else {
|
|
247
|
+
charIndex = offset;
|
|
248
|
+
isFrontBlock = offset === 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (pulse && isFrontBlock) {
|
|
252
|
+
const pulseToggle = Math.floor(this.pulseState / 6) % 2;
|
|
253
|
+
bar += gradient[pulseToggle];
|
|
254
|
+
} else {
|
|
255
|
+
bar += gradient[charIndex];
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
bar += barEmpty;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return bar;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Format output lines for display (pure function for testing)
|
|
266
|
+
*/
|
|
267
|
+
formatOutputLines(lines: string[], maxLines: number): string[] {
|
|
268
|
+
const recentLines = lines.slice(-maxLines);
|
|
269
|
+
const termWidth = this.output.columns || 80;
|
|
270
|
+
const maxLen = termWidth - 4;
|
|
271
|
+
|
|
272
|
+
return recentLines.map((line) => {
|
|
273
|
+
const truncated = line.length > maxLen ? `${line.slice(0, maxLen - 3)}...` : line;
|
|
274
|
+
return ` ${truncated}`;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Strip ANSI codes from text
|
|
280
|
+
*/
|
|
281
|
+
private stripAnsi(text: string): string {
|
|
282
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC character needed for ANSI code matching
|
|
283
|
+
return text.replace(/\u001b\[[0-9;]*m/g, '');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Render one frame (called by animation loop)
|
|
288
|
+
*/
|
|
289
|
+
private render(): void {
|
|
290
|
+
const termWidth = this.output.columns || 80;
|
|
291
|
+
const barWidth = Math.max(termWidth - 4, 20);
|
|
292
|
+
|
|
293
|
+
if (this.hasNewOutput) {
|
|
294
|
+
this.barPosition += this.barDirection;
|
|
295
|
+
if (this.barPosition >= barWidth - 4) {
|
|
296
|
+
this.barDirection = -1;
|
|
297
|
+
} else if (this.barPosition <= 0) {
|
|
298
|
+
this.barDirection = 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.pulseState++;
|
|
303
|
+
|
|
304
|
+
// Build frame, move cursor up over previous frame, write new frame
|
|
305
|
+
// All in a single output.write() to prevent flicker
|
|
306
|
+
const frame = this.buildFrame();
|
|
307
|
+
const moveUp = this.linesDrawn > 0 ? `\x1b[${this.linesDrawn}A\r` : '';
|
|
308
|
+
this.output.write(moveUp + frame);
|
|
309
|
+
|
|
310
|
+
this.hasNewOutput = false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Write the initial frame (no cursor movement needed)
|
|
315
|
+
*/
|
|
316
|
+
private writeFrame(): void {
|
|
317
|
+
const frame = this.buildFrame();
|
|
318
|
+
this.output.write(frame);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Write a sequence to clear the gauge area (for cleanup/stop)
|
|
323
|
+
*/
|
|
324
|
+
private writeClearSequence(): void {
|
|
325
|
+
if (this.linesDrawn <= 0) return;
|
|
326
|
+
const termWidth = this.output.columns || 80;
|
|
327
|
+
const blankLine = ' '.repeat(termWidth);
|
|
328
|
+
// Move up, then overwrite each line with blanks
|
|
329
|
+
let seq = `\x1b[${this.linesDrawn}A\r`;
|
|
330
|
+
for (let i = 0; i < this.linesDrawn; i++) {
|
|
331
|
+
seq += `${blankLine}\n`;
|
|
332
|
+
}
|
|
333
|
+
// Move back up to where we started
|
|
334
|
+
seq += `\x1b[${this.linesDrawn}A\r`;
|
|
335
|
+
this.output.write(seq);
|
|
336
|
+
this.linesDrawn = 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Build the entire frame as a single string.
|
|
341
|
+
* Each line is padded to terminal width to overwrite previous content.
|
|
342
|
+
*/
|
|
343
|
+
private buildFrame(): string {
|
|
344
|
+
const termWidth = this.output.columns || 80;
|
|
345
|
+
let lineCount = 0;
|
|
346
|
+
let frame = '';
|
|
347
|
+
|
|
348
|
+
const pad = (s: string, visibleLen: number) => {
|
|
349
|
+
// Pad with spaces to fill the terminal width, clearing any leftover chars
|
|
350
|
+
const remaining = Math.max(0, termWidth - visibleLen);
|
|
351
|
+
return s + ' '.repeat(remaining);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Title line
|
|
355
|
+
const titleText = `▸ ${this.title}`;
|
|
356
|
+
frame += `${pad(colors.cyan(titleText), titleText.length + 2)}\n`;
|
|
357
|
+
lineCount++;
|
|
358
|
+
|
|
359
|
+
// Output preview lines
|
|
360
|
+
const displayLines = this.formatOutputLines(this.outputLines, this.maxDisplayLines);
|
|
361
|
+
for (const line of displayLines) {
|
|
362
|
+
frame += `${pad(colors.dim(line), line.length)}\n`;
|
|
363
|
+
lineCount++;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Padding lines
|
|
367
|
+
const paddingLines = this.maxDisplayLines - displayLines.length;
|
|
368
|
+
for (let i = 0; i < paddingLines; i++) {
|
|
369
|
+
frame += `${pad('', 0)}\n`;
|
|
370
|
+
lineCount++;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Progress bar
|
|
374
|
+
const hintText = '(esc to bg; ^C to cancel)';
|
|
375
|
+
const barWidth = Math.max(termWidth - 4 - hintText.length - 2, 20);
|
|
376
|
+
const shouldPulse = !this.hasNewOutput;
|
|
377
|
+
const plainBar = this.buildProgressBar(
|
|
378
|
+
this.barPosition,
|
|
379
|
+
barWidth,
|
|
380
|
+
this.barDirection,
|
|
381
|
+
shouldPulse,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Colorize bar
|
|
385
|
+
let coloredBar = ' ';
|
|
386
|
+
let visibleBarLen = 2; // leading spaces
|
|
387
|
+
for (let i = 0; i < plainBar.length; i++) {
|
|
388
|
+
const char = plainBar[i];
|
|
389
|
+
if (char === '·') {
|
|
390
|
+
coloredBar += colors.mutedPurple('·');
|
|
391
|
+
} else {
|
|
392
|
+
coloredBar += colors.cyan(char);
|
|
393
|
+
}
|
|
394
|
+
visibleBarLen++;
|
|
395
|
+
}
|
|
396
|
+
coloredBar += ` ${colors.mutedPurple(hintText)}`;
|
|
397
|
+
visibleBarLen += 2 + hintText.length;
|
|
398
|
+
|
|
399
|
+
frame += `${pad(coloredBar, visibleBarLen)}\n`;
|
|
400
|
+
lineCount++;
|
|
401
|
+
|
|
402
|
+
this.linesDrawn = lineCount;
|
|
403
|
+
return frame;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { COMMANDS } from './command-registry';
|
|
3
|
+
import type { CommandDef } from './command-registry';
|
|
4
|
+
import { generateRichZshCompletion } from './generate-zsh-completion';
|
|
5
|
+
|
|
6
|
+
describe('Zsh Completion Generator', () => {
|
|
7
|
+
const output = generateRichZshCompletion(COMMANDS);
|
|
8
|
+
|
|
9
|
+
test('produces valid compdef header', () => {
|
|
10
|
+
expect(output).toMatch(/^#compdef celilo/);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('contains auto-generated notice', () => {
|
|
14
|
+
expect(output).toContain('Auto-generated by: celilo completion zsh');
|
|
15
|
+
expect(output).toContain('Do not edit manually');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('contains main _celilo function', () => {
|
|
19
|
+
expect(output).toContain('_celilo()');
|
|
20
|
+
expect(output).toContain('_celilo_commands()');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('contains all top-level commands in _celilo_commands', () => {
|
|
24
|
+
for (const cmd of COMMANDS) {
|
|
25
|
+
expect(output).toContain(`'${cmd.name}:`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('generates functions for all commands with subcommands', () => {
|
|
30
|
+
function checkSubcommands(commands: CommandDef[], path: string[]): void {
|
|
31
|
+
for (const cmd of commands) {
|
|
32
|
+
if (cmd.subcommands && cmd.subcommands.length > 0) {
|
|
33
|
+
const fnName = `_celilo${path.length > 0 ? `_${[...path, cmd.name].join('_')}` : `_${cmd.name}`}`;
|
|
34
|
+
expect(output).toContain(`${fnName}()`);
|
|
35
|
+
expect(output).toContain(`${fnName}_commands()`);
|
|
36
|
+
|
|
37
|
+
// Recurse
|
|
38
|
+
checkSubcommands(cmd.subcommands, [...path, cmd.name]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
checkSubcommands(COMMANDS, []);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('generates dynamic completion functions for database-backed args', () => {
|
|
46
|
+
expect(output).toContain('_celilo_module_ids()');
|
|
47
|
+
expect(output).toContain('_celilo_service_ids()');
|
|
48
|
+
expect(output).toContain('_celilo_machine_hostnames()');
|
|
49
|
+
expect(output).toContain('_celilo_capability_names()');
|
|
50
|
+
expect(output).toContain('_celilo_config_keys()');
|
|
51
|
+
expect(output).toContain('_celilo_system_config_keys()');
|
|
52
|
+
expect(output).toContain('_celilo_system_secret_keys()');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('dynamic completions query sqlite3', () => {
|
|
56
|
+
expect(output).toContain('sqlite3 "$db_path"');
|
|
57
|
+
expect(output).toContain('SELECT id FROM modules');
|
|
58
|
+
expect(output).toContain('SELECT service_id, name FROM container_services');
|
|
59
|
+
expect(output).toContain('SELECT hostname FROM machines');
|
|
60
|
+
expect(output).toContain('SELECT capability_name, module_id FROM capabilities');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('includes flag completions for commands with flags', () => {
|
|
64
|
+
// Module import has --target and --auto-generate-secrets
|
|
65
|
+
expect(output).toContain('--target');
|
|
66
|
+
expect(output).toContain('--auto-generate-secrets');
|
|
67
|
+
|
|
68
|
+
// Machine add has --ip, --ssh-user, etc.
|
|
69
|
+
expect(output).toContain('--ip');
|
|
70
|
+
expect(output).toContain('--ssh-user');
|
|
71
|
+
expect(output).toContain('--ssh-key-file');
|
|
72
|
+
|
|
73
|
+
// Service remove has --force
|
|
74
|
+
expect(output).toContain('--force');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('ends with _celilo invocation', () => {
|
|
78
|
+
expect(output.trimEnd()).toMatch(/_celilo "\$@"$/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('all subcommand descriptions are present', () => {
|
|
82
|
+
function checkDescriptions(commands: CommandDef[]): void {
|
|
83
|
+
for (const cmd of commands) {
|
|
84
|
+
if (cmd.subcommands) {
|
|
85
|
+
for (const sub of cmd.subcommands) {
|
|
86
|
+
// Description should appear in the _commands function
|
|
87
|
+
expect(output).toContain(sub.description.replace(/:/g, '\\:'));
|
|
88
|
+
}
|
|
89
|
+
checkDescriptions(cmd.subcommands);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
checkDescriptions(COMMANDS);
|
|
94
|
+
});
|
|
95
|
+
});
|