@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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup List Command
|
|
3
|
+
* Lists available backups with metadata.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { formatSize, getBackup, listBackups } from '../../services/backup-metadata';
|
|
7
|
+
import { getBackupStorage } from '../../services/backup-storage';
|
|
8
|
+
import { celiloIntro } from '../prompts';
|
|
9
|
+
import type { CommandResult } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a timestamp as a human-friendly relative string.
|
|
13
|
+
* Uses relative terms for recent backups and dates for older ones.
|
|
14
|
+
*/
|
|
15
|
+
function formatRelativeDate(date: Date): string {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const diffMs = now - date.getTime();
|
|
18
|
+
const diffMins = Math.floor(diffMs / 60_000);
|
|
19
|
+
const diffHours = Math.floor(diffMs / 3_600_000);
|
|
20
|
+
const diffDays = Math.floor(diffMs / 86_400_000);
|
|
21
|
+
|
|
22
|
+
if (diffMins < 1) return 'just now';
|
|
23
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
24
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
25
|
+
if (diffDays === 1) return 'yesterday';
|
|
26
|
+
if (diffDays < 7) return `${diffDays} days ago`;
|
|
27
|
+
if (diffDays < 14) return 'last week';
|
|
28
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
|
|
29
|
+
if (diffDays < 60) return 'last month';
|
|
30
|
+
return `${Math.floor(diffDays / 30)} months ago`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Show detailed info for a single backup
|
|
35
|
+
*/
|
|
36
|
+
function showBackupDetail(backupId: string): CommandResult {
|
|
37
|
+
const backup = getBackup(backupId);
|
|
38
|
+
if (!backup) {
|
|
39
|
+
return { success: false, error: `Backup not found: ${backupId}` };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const storage = getBackupStorage(backup.storageId);
|
|
43
|
+
const isSystem = backup.backupType === 'system_state';
|
|
44
|
+
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(` ID: ${backup.id}`);
|
|
47
|
+
console.log(` Short ID: ${backup.id.substring(0, 8)}`);
|
|
48
|
+
if (backup.name) {
|
|
49
|
+
console.log(` Name: ${backup.name}`);
|
|
50
|
+
}
|
|
51
|
+
console.log(` Type: ${isSystem ? 'System State' : 'Module Data'}`);
|
|
52
|
+
if (!isSystem) {
|
|
53
|
+
console.log(` Module: ${backup.moduleId ?? 'unknown'}`);
|
|
54
|
+
}
|
|
55
|
+
if (backup.moduleVersion) {
|
|
56
|
+
console.log(` Module Version: ${backup.moduleVersion}`);
|
|
57
|
+
}
|
|
58
|
+
if (backup.schemaVersion) {
|
|
59
|
+
console.log(` Schema Version: ${backup.schemaVersion}`);
|
|
60
|
+
}
|
|
61
|
+
console.log(` Status: ${backup.status}`);
|
|
62
|
+
console.log(` Size: ${formatSize(backup.sizeBytes)}`);
|
|
63
|
+
console.log(
|
|
64
|
+
` Date: ${new Date(backup.startedAt).toISOString().replace('T', ' ').substring(0, 19)} UTC`,
|
|
65
|
+
);
|
|
66
|
+
console.log(` Storage: ${storage?.storageId ?? 'unknown'} (${storage?.name ?? '?'})`);
|
|
67
|
+
console.log(` Path: ${backup.storagePath}`);
|
|
68
|
+
if (backup.errorMessage) {
|
|
69
|
+
console.log(` Error: ${backup.errorMessage}`);
|
|
70
|
+
}
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
return { success: true, message: `Backup detail: ${backupId}` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function handleBackupList(
|
|
77
|
+
args: string[],
|
|
78
|
+
flags: Record<string, boolean | string> = {},
|
|
79
|
+
): Promise<CommandResult> {
|
|
80
|
+
try {
|
|
81
|
+
const moduleIdOrBackupId = args[0];
|
|
82
|
+
const limit = typeof flags.limit === 'string' ? Number.parseInt(flags.limit, 10) : 20;
|
|
83
|
+
|
|
84
|
+
// If the argument looks like a backup ID (hex chars), show detail view
|
|
85
|
+
if (moduleIdOrBackupId && /^[0-9a-f]{8,}$/i.test(moduleIdOrBackupId)) {
|
|
86
|
+
const backup = getBackup(moduleIdOrBackupId);
|
|
87
|
+
if (backup) {
|
|
88
|
+
celiloIntro('Backup Detail');
|
|
89
|
+
return showBackupDetail(moduleIdOrBackupId);
|
|
90
|
+
}
|
|
91
|
+
// Fall through to module filter if not a backup ID
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
celiloIntro('Available Backups');
|
|
95
|
+
|
|
96
|
+
const moduleId = moduleIdOrBackupId;
|
|
97
|
+
const backupList = listBackups({ moduleId, limit });
|
|
98
|
+
|
|
99
|
+
if (backupList.length === 0) {
|
|
100
|
+
console.log('No backups found.\n');
|
|
101
|
+
console.log('Create a backup:');
|
|
102
|
+
console.log(' celilo backup create');
|
|
103
|
+
return { success: true, message: 'No backups found' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('');
|
|
107
|
+
|
|
108
|
+
for (const backup of backupList) {
|
|
109
|
+
const id = backup.id.substring(0, 8);
|
|
110
|
+
const module = backup.backupType === 'system_state' ? '[system]' : (backup.moduleId ?? '?');
|
|
111
|
+
const when = formatRelativeDate(new Date(backup.startedAt));
|
|
112
|
+
const size = formatSize(backup.sizeBytes);
|
|
113
|
+
const statusIcon =
|
|
114
|
+
backup.status === 'completed' ? '✓' : backup.status === 'failed' ? '✗' : '…';
|
|
115
|
+
|
|
116
|
+
const nameTag = backup.name ? ` "${backup.name}"` : '';
|
|
117
|
+
console.log(
|
|
118
|
+
` ${statusIcon} ${id} ${module.padEnd(12)} ${size.padEnd(10)} ${when}${nameTag}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(`\n${backupList.length} backup${backupList.length === 1 ? '' : 's'} shown.`);
|
|
123
|
+
console.log('Run "celilo backup list <id>" for details.\n');
|
|
124
|
+
|
|
125
|
+
return { success: true, message: `Found ${backupList.length} backup(s)` };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: `Failed to list backups: ${error instanceof Error ? error.message : String(error)}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Name Command
|
|
3
|
+
* Set or view a human-readable name/annotation on a backup.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* celilo backup name <backup-id> <name...> Set a name
|
|
7
|
+
* celilo backup name <backup-id> Show current name
|
|
8
|
+
* celilo backup name <backup-id> --clear Remove the name
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { formatSize, getBackup, updateBackupName } from '../../services/backup-metadata';
|
|
12
|
+
import { celiloIntro, celiloOutro } from '../prompts';
|
|
13
|
+
import type { CommandResult } from '../types';
|
|
14
|
+
|
|
15
|
+
export async function handleBackupName(
|
|
16
|
+
args: string[],
|
|
17
|
+
flags: Record<string, boolean | string> = {},
|
|
18
|
+
): Promise<CommandResult> {
|
|
19
|
+
try {
|
|
20
|
+
const backupId = args[0];
|
|
21
|
+
|
|
22
|
+
if (!backupId) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
error:
|
|
26
|
+
'Backup ID is required\n\nUsage:\n celilo backup name <backup-id> <name...>\n celilo backup name <backup-id> --clear',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const backup = getBackup(backupId);
|
|
31
|
+
if (!backup) {
|
|
32
|
+
return { success: false, error: `Backup not found: ${backupId}` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const shortId = backup.id.substring(0, 8);
|
|
36
|
+
|
|
37
|
+
// Clear the name
|
|
38
|
+
if (flags.clear) {
|
|
39
|
+
celiloIntro('Clear Backup Name');
|
|
40
|
+
updateBackupName(backup.id, null);
|
|
41
|
+
celiloOutro(`Name cleared for backup ${shortId}.`);
|
|
42
|
+
return { success: true, message: `Cleared name for ${shortId}` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Remaining args after the backup ID form the name
|
|
46
|
+
const nameParts = args.slice(1);
|
|
47
|
+
|
|
48
|
+
// No name provided — show current name
|
|
49
|
+
if (nameParts.length === 0) {
|
|
50
|
+
celiloIntro('Backup Name');
|
|
51
|
+
const module =
|
|
52
|
+
backup.backupType === 'system_state' ? '[system]' : (backup.moduleId ?? 'unknown');
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(` Backup: ${shortId} (${module}, ${formatSize(backup.sizeBytes)})`);
|
|
55
|
+
console.log(` Name: ${backup.name ?? '(none)'}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
return { success: true, message: backup.name ?? '(none)' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Set the name (join all remaining args — supports both quoted and unquoted)
|
|
61
|
+
const name = nameParts.join(' ');
|
|
62
|
+
celiloIntro('Set Backup Name');
|
|
63
|
+
updateBackupName(backup.id, name);
|
|
64
|
+
celiloOutro(`Backup ${shortId} named: "${name}"`);
|
|
65
|
+
|
|
66
|
+
return { success: true, message: `Named ${shortId}: ${name}` };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: `Failed to update backup name: ${error instanceof Error ? error.message : String(error)}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Prune Command
|
|
3
|
+
* Remove old backups according to retention policies defined in module manifests.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { eq } from 'drizzle-orm';
|
|
7
|
+
import { getDb } from '../../db/client';
|
|
8
|
+
import { modules } from '../../db/schema';
|
|
9
|
+
import type { ModuleManifest } from '../../manifest/schema';
|
|
10
|
+
import { findBackupEligibleModules } from '../../services/backup-create';
|
|
11
|
+
import { pruneBackupsForModule } from '../../services/backup-retention';
|
|
12
|
+
import { celiloIntro, celiloOutro } from '../prompts';
|
|
13
|
+
import type { CommandResult } from '../types';
|
|
14
|
+
|
|
15
|
+
export async function handleBackupPrune(
|
|
16
|
+
args: string[],
|
|
17
|
+
flags: Record<string, boolean | string> = {},
|
|
18
|
+
): Promise<CommandResult> {
|
|
19
|
+
try {
|
|
20
|
+
celiloIntro('Prune Old Backups');
|
|
21
|
+
|
|
22
|
+
const specificModule = args[0];
|
|
23
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
24
|
+
|
|
25
|
+
if (dryRun) {
|
|
26
|
+
console.log('(dry run — no backups will be deleted)\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let totalDeleted = 0;
|
|
30
|
+
|
|
31
|
+
if (specificModule) {
|
|
32
|
+
// Prune a specific module
|
|
33
|
+
const db = getDb();
|
|
34
|
+
const mod = db.select().from(modules).where(eq(modules.id, specificModule)).get();
|
|
35
|
+
if (!mod) {
|
|
36
|
+
return { success: false, error: `Module not found: ${specificModule}` };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const manifest = mod.manifestData as unknown as ModuleManifest;
|
|
40
|
+
const retention = manifest.backup?.retention;
|
|
41
|
+
if (!retention) {
|
|
42
|
+
console.log(`Module '${specificModule}' has no retention policy defined.`);
|
|
43
|
+
return { success: true, message: 'No retention policy' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await pruneBackupsForModule(
|
|
47
|
+
specificModule,
|
|
48
|
+
{ count: retention.count, maxAgeDays: retention.max_age_days },
|
|
49
|
+
dryRun,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (result.deleted > 0) {
|
|
53
|
+
const verb = dryRun ? 'Would delete' : 'Deleted';
|
|
54
|
+
console.log(`${verb} ${result.deleted} backup(s) for ${specificModule}:`);
|
|
55
|
+
for (const path of result.deletedPaths) {
|
|
56
|
+
console.log(` - ${path}`);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
console.log(`No expired backups for ${specificModule}.`);
|
|
60
|
+
}
|
|
61
|
+
totalDeleted = result.deleted;
|
|
62
|
+
} else {
|
|
63
|
+
// Prune all modules with retention policies
|
|
64
|
+
const eligible = findBackupEligibleModules();
|
|
65
|
+
|
|
66
|
+
for (const { module: mod, manifest } of eligible) {
|
|
67
|
+
const retention = manifest.backup?.retention;
|
|
68
|
+
if (!retention) continue;
|
|
69
|
+
|
|
70
|
+
const result = await pruneBackupsForModule(
|
|
71
|
+
mod.id,
|
|
72
|
+
{ count: retention.count, maxAgeDays: retention.max_age_days },
|
|
73
|
+
dryRun,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (result.deleted > 0) {
|
|
77
|
+
const verb = dryRun ? 'Would delete' : 'Deleted';
|
|
78
|
+
console.log(`${verb} ${result.deleted} backup(s) for ${mod.id}`);
|
|
79
|
+
totalDeleted += result.deleted;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (totalDeleted === 0) {
|
|
84
|
+
console.log('No expired backups found.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const verb = dryRun ? 'would be deleted' : 'deleted';
|
|
89
|
+
celiloOutro(totalDeleted > 0 ? `${totalDeleted} backup(s) ${verb}.` : 'No backups to prune.');
|
|
90
|
+
|
|
91
|
+
return { success: true, message: `Pruned ${totalDeleted} backup(s)` };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: `Failed to prune backups: ${error instanceof Error ? error.message : String(error)}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Restore Command
|
|
3
|
+
* Restores from a backup archive, executing the module's on_restore hook.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as p from '@clack/prompts';
|
|
7
|
+
import { formatSize, getBackup } from '../../services/backup-metadata';
|
|
8
|
+
import { restoreModuleBackup, restoreSystemStateBackup } from '../../services/backup-restore';
|
|
9
|
+
import { getBackupStorage } from '../../services/backup-storage';
|
|
10
|
+
import { celiloIntro, celiloOutro } from '../prompts';
|
|
11
|
+
import type { CommandResult } from '../types';
|
|
12
|
+
|
|
13
|
+
export async function handleBackupRestore(
|
|
14
|
+
args: string[],
|
|
15
|
+
flags: Record<string, boolean | string> = {},
|
|
16
|
+
): Promise<CommandResult> {
|
|
17
|
+
try {
|
|
18
|
+
celiloIntro('Restore Backup');
|
|
19
|
+
|
|
20
|
+
const backupId = args[0];
|
|
21
|
+
if (!backupId) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error: 'Backup ID is required\n\nUsage: celilo backup restore <backup-id>',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const backup = getBackup(backupId);
|
|
29
|
+
if (!backup) {
|
|
30
|
+
return { success: false, error: `Backup not found: ${backupId}` };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (backup.status !== 'completed') {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error: `Cannot restore backup with status '${backup.status}'. Only completed backups can be restored.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Show backup details
|
|
41
|
+
const storage = getBackupStorage(backup.storageId);
|
|
42
|
+
const isSystem = backup.backupType === 'system_state';
|
|
43
|
+
|
|
44
|
+
console.log('\nBackup Details:');
|
|
45
|
+
console.log(` Type: ${isSystem ? 'System State' : 'Module Data'}`);
|
|
46
|
+
if (!isSystem) {
|
|
47
|
+
console.log(` Module: ${backup.moduleId ?? 'unknown'}`);
|
|
48
|
+
if (backup.moduleVersion) {
|
|
49
|
+
console.log(` Module Version: ${backup.moduleVersion}`);
|
|
50
|
+
}
|
|
51
|
+
if (backup.schemaVersion) {
|
|
52
|
+
console.log(` Schema Version: ${backup.schemaVersion}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
console.log(
|
|
56
|
+
` Date: ${new Date(backup.startedAt).toISOString().replace('T', ' ').substring(0, 19)} UTC`,
|
|
57
|
+
);
|
|
58
|
+
console.log(` Size: ${formatSize(backup.sizeBytes)}`);
|
|
59
|
+
console.log(` Storage: ${storage?.storageId ?? 'unknown'}`);
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
if (isSystem) {
|
|
63
|
+
console.log('⚠ This will replace the current Celilo database with the backup version.');
|
|
64
|
+
console.log(' All module configs, secrets, and state will be overwritten.\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Confirm unless --yes
|
|
68
|
+
if (!flags.yes) {
|
|
69
|
+
const confirmed = await p.confirm({
|
|
70
|
+
message: 'Proceed with restore?',
|
|
71
|
+
initialValue: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
75
|
+
p.cancel('Restore cancelled');
|
|
76
|
+
return { success: false, error: 'Cancelled by user' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isSystem) {
|
|
81
|
+
// System state restore
|
|
82
|
+
console.log('\n▸ Downloading and decrypting backup...');
|
|
83
|
+
const result = await restoreSystemStateBackup(backup);
|
|
84
|
+
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
console.log(`✗ ${result.error}`);
|
|
87
|
+
return { success: false, error: result.error ?? 'Restore failed' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('✓ System state restored');
|
|
91
|
+
celiloOutro('Restore complete. The database has been replaced with the backup version.');
|
|
92
|
+
} else {
|
|
93
|
+
// Module data restore
|
|
94
|
+
console.log('\n▸ Downloading and decrypting backup...');
|
|
95
|
+
console.log('▸ Executing on_restore hook...');
|
|
96
|
+
|
|
97
|
+
const result = await restoreModuleBackup(backup, { runHealthCheck: true });
|
|
98
|
+
|
|
99
|
+
if (!result.success) {
|
|
100
|
+
console.log(`✗ ${result.error}`);
|
|
101
|
+
return { success: false, error: result.error ?? 'Restore failed' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log('✓ on_restore completed');
|
|
105
|
+
|
|
106
|
+
if (result.healthCheckPassed === true) {
|
|
107
|
+
console.log('✓ Health check passed');
|
|
108
|
+
} else if (result.healthCheckPassed === false) {
|
|
109
|
+
console.log('⚠ Health check failed after restore');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
celiloOutro('Restore complete.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { success: true, message: `Restored backup: ${backupId}` };
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: `Restore failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getDb } from '../../db/client';
|
|
3
|
+
import { capabilities, capabilitySecrets, modules } from '../../db/schema';
|
|
4
|
+
import { getArg, validateRequiredArgs } from '../parser';
|
|
5
|
+
import type { CommandResult } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handle capability info command
|
|
9
|
+
*
|
|
10
|
+
* Usage: celilo capability info <name>
|
|
11
|
+
*
|
|
12
|
+
* @param args - Command arguments
|
|
13
|
+
* @returns Command result with capability details
|
|
14
|
+
*/
|
|
15
|
+
export async function handleCapabilityInfo(args: string[]): Promise<CommandResult> {
|
|
16
|
+
const error = validateRequiredArgs(args, 1);
|
|
17
|
+
if (error) {
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
error: `${error}\n\nUsage: celilo capability info <name>`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const capabilityName = getArg(args, 0);
|
|
25
|
+
|
|
26
|
+
if (!capabilityName) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
error: 'Capability name is required',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const db = getDb();
|
|
34
|
+
|
|
35
|
+
const capability = db
|
|
36
|
+
.select({
|
|
37
|
+
id: capabilities.id,
|
|
38
|
+
capabilityName: capabilities.capabilityName,
|
|
39
|
+
version: capabilities.version,
|
|
40
|
+
data: capabilities.data,
|
|
41
|
+
moduleId: capabilities.moduleId,
|
|
42
|
+
moduleState: modules.state,
|
|
43
|
+
registeredAt: capabilities.registeredAt,
|
|
44
|
+
})
|
|
45
|
+
.from(capabilities)
|
|
46
|
+
.innerJoin(modules, eq(capabilities.moduleId, modules.id))
|
|
47
|
+
.where(eq(capabilities.capabilityName, capabilityName))
|
|
48
|
+
.get();
|
|
49
|
+
|
|
50
|
+
if (!capability) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: `Capability not found: ${capabilityName}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get capability secrets
|
|
58
|
+
const secretRows = db
|
|
59
|
+
.select({
|
|
60
|
+
name: capabilitySecrets.name,
|
|
61
|
+
description: capabilitySecrets.description,
|
|
62
|
+
hasValue: capabilitySecrets.encryptedValue,
|
|
63
|
+
})
|
|
64
|
+
.from(capabilitySecrets)
|
|
65
|
+
.where(eq(capabilitySecrets.capabilityId, capability.id))
|
|
66
|
+
.all();
|
|
67
|
+
|
|
68
|
+
const sections: string[] = [];
|
|
69
|
+
|
|
70
|
+
// Section 1: Capability metadata
|
|
71
|
+
const metaLines = [
|
|
72
|
+
`Capability: ${capability.capabilityName}`,
|
|
73
|
+
`Version: ${capability.version}`,
|
|
74
|
+
`Provider: ${capability.moduleId} (state: ${capability.moduleState})`,
|
|
75
|
+
`Registered: ${capability.registeredAt.toISOString()}`,
|
|
76
|
+
];
|
|
77
|
+
sections.push(metaLines.join('\n'));
|
|
78
|
+
|
|
79
|
+
// Section 2: Capability data
|
|
80
|
+
if (capability.data && Object.keys(capability.data).length > 0) {
|
|
81
|
+
const dataLines = ['Data:'];
|
|
82
|
+
formatData(capability.data, dataLines, 1);
|
|
83
|
+
sections.push(dataLines.join('\n'));
|
|
84
|
+
} else {
|
|
85
|
+
sections.push('Data: (none)');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Section 3: Secrets
|
|
89
|
+
if (secretRows.length > 0) {
|
|
90
|
+
const secretLines = ['Secrets:'];
|
|
91
|
+
for (const secret of secretRows) {
|
|
92
|
+
const status = secret.hasValue ? '[configured]' : '[not set]';
|
|
93
|
+
const desc = secret.description ? ` - ${secret.description}` : '';
|
|
94
|
+
secretLines.push(` ${secret.name}: ${status}${desc}`);
|
|
95
|
+
}
|
|
96
|
+
sections.push(secretLines.join('\n'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
message: sections.join('\n\n'),
|
|
102
|
+
data: { capability, secrets: secretRows },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format nested data object for display
|
|
108
|
+
*/
|
|
109
|
+
function formatData(data: Record<string, unknown>, lines: string[], depth: number): void {
|
|
110
|
+
const indent = ' '.repeat(depth);
|
|
111
|
+
for (const [key, value] of Object.entries(data)) {
|
|
112
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
113
|
+
lines.push(`${indent}${key}:`);
|
|
114
|
+
formatData(value as Record<string, unknown>, lines, depth + 1);
|
|
115
|
+
} else if (Array.isArray(value)) {
|
|
116
|
+
lines.push(`${indent}${key}: [${value.join(', ')}]`);
|
|
117
|
+
} else {
|
|
118
|
+
lines.push(`${indent}${key}: ${value}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getDb } from '../../db/client';
|
|
3
|
+
import { capabilities, modules } from '../../db/schema';
|
|
4
|
+
import type { CommandResult } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handle capability list command
|
|
8
|
+
*
|
|
9
|
+
* Usage: celilo capability list
|
|
10
|
+
*
|
|
11
|
+
* @returns Command result with all registered capabilities
|
|
12
|
+
*/
|
|
13
|
+
export async function handleCapabilityList(): Promise<CommandResult> {
|
|
14
|
+
const db = getDb();
|
|
15
|
+
|
|
16
|
+
const rows = db
|
|
17
|
+
.select({
|
|
18
|
+
capabilityName: capabilities.capabilityName,
|
|
19
|
+
version: capabilities.version,
|
|
20
|
+
moduleId: capabilities.moduleId,
|
|
21
|
+
moduleState: modules.state,
|
|
22
|
+
})
|
|
23
|
+
.from(capabilities)
|
|
24
|
+
.innerJoin(modules, eq(capabilities.moduleId, modules.id))
|
|
25
|
+
.all();
|
|
26
|
+
|
|
27
|
+
if (rows.length === 0) {
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
message:
|
|
31
|
+
'No capabilities registered\n\nCapabilities are registered when modules with "provides.capabilities" are imported.',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lines = ['Registered capabilities:', ''];
|
|
36
|
+
for (const row of rows) {
|
|
37
|
+
lines.push(
|
|
38
|
+
`${row.capabilityName} v${row.version} (provided by: ${row.moduleId}, state: ${row.moduleState})`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
message: lines.join('\n'),
|
|
45
|
+
data: rows,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion Command
|
|
3
|
+
* Generate shell completion scripts for bash/zsh
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { COMMANDS } from '../command-registry';
|
|
7
|
+
import { generateBashCompletion } from '../completion';
|
|
8
|
+
import { generateRichZshCompletion } from '../generate-zsh-completion';
|
|
9
|
+
import { celiloIntro } from '../prompts';
|
|
10
|
+
import type { CommandResult } from '../types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handle completion command
|
|
14
|
+
*
|
|
15
|
+
* @param args - Command arguments: [shell]
|
|
16
|
+
* @param flags - Command flags
|
|
17
|
+
*/
|
|
18
|
+
export async function handleCompletion(
|
|
19
|
+
args: string[],
|
|
20
|
+
_flags: Record<string, boolean | string> = {},
|
|
21
|
+
): Promise<CommandResult> {
|
|
22
|
+
try {
|
|
23
|
+
const shell = args[0];
|
|
24
|
+
|
|
25
|
+
if (!shell) {
|
|
26
|
+
celiloIntro('Shell Completion');
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: `Shell argument required.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
celilo completion bash Generate bash completion script
|
|
34
|
+
celilo completion zsh Generate zsh completion script
|
|
35
|
+
|
|
36
|
+
Install zsh completions:
|
|
37
|
+
celilo completion zsh > ~/.zsh/completions/_celilo
|
|
38
|
+
exec zsh
|
|
39
|
+
`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (shell === 'bash') {
|
|
44
|
+
const script = generateBashCompletion();
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
message: script,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (shell === 'zsh') {
|
|
52
|
+
const script = generateRichZshCompletion(COMMANDS);
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
message: script,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
celiloIntro('Shell Completion');
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
error: `Unknown shell: ${shell}
|
|
64
|
+
|
|
65
|
+
Supported shells:
|
|
66
|
+
bash Generate bash completion script
|
|
67
|
+
zsh Generate zsh completion script
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
# Bash
|
|
71
|
+
celilo completion bash > /etc/bash_completion.d/celilo
|
|
72
|
+
# OR for user install:
|
|
73
|
+
celilo completion bash >> ~/.bashrc
|
|
74
|
+
|
|
75
|
+
# Zsh
|
|
76
|
+
celilo completion zsh > ~/.zsh/completions/_celilo
|
|
77
|
+
# OR for user install:
|
|
78
|
+
celilo completion zsh > /usr/local/share/zsh/site-functions/_celilo
|
|
79
|
+
`,
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
error: `Failed to generate completion script: ${error instanceof Error ? error.message : String(error)}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|