@kustodian/cli 1.0.0 → 1.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 +88 -0
- package/package.json +4 -2
- package/src/bin.ts +4 -1
- package/src/commands/apply.ts +3 -3
- package/src/commands/init.ts +1 -1
- package/src/commands/sources.ts +439 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @kustodian/cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for Kustodian - a GitOps templating framework for Kubernetes with Flux CD.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add -g @kustodian/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
kustodian <command> [options]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `init <name>`
|
|
20
|
+
|
|
21
|
+
Initialize a new Kustodian project with example templates and cluster configuration.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
kustodian init my-project
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
- `--force, -f` - Overwrite existing files
|
|
29
|
+
|
|
30
|
+
### `validate`
|
|
31
|
+
|
|
32
|
+
Validate cluster and template configurations, including dependency graph validation.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
kustodian validate
|
|
36
|
+
kustodian validate --cluster production
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
- `--cluster, -c <name>` - Validate a specific cluster only
|
|
41
|
+
- `--project, -p <path>` - Path to project root (defaults to current directory)
|
|
42
|
+
|
|
43
|
+
### `apply`
|
|
44
|
+
|
|
45
|
+
Apply full cluster configuration: bootstrap nodes, install Flux CD, and deploy templates.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
kustodian apply --cluster production
|
|
49
|
+
kustodian apply --cluster local --dry-run
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
- `--cluster, -c <name>` - Cluster name to apply (required)
|
|
54
|
+
- `--provider, -P <name>` - Cluster provider for bootstrap (default: k0s)
|
|
55
|
+
- `--project, -p <path>` - Path to project root
|
|
56
|
+
- `--dry-run, -d` - Preview changes without applying
|
|
57
|
+
- `--skip-bootstrap` - Skip cluster bootstrap (use existing cluster)
|
|
58
|
+
- `--skip-flux` - Skip Flux CD installation
|
|
59
|
+
- `--skip-templates` - Skip template deployment
|
|
60
|
+
|
|
61
|
+
### `update`
|
|
62
|
+
|
|
63
|
+
Check and update image version substitutions from container registries.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
kustodian update --cluster production
|
|
67
|
+
kustodian update --cluster production --dry-run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
- `--cluster, -c <name>` - Cluster to update values for (required)
|
|
72
|
+
- `--project, -p <path>` - Path to project root
|
|
73
|
+
- `--dry-run, -d` - Show what would be updated without making changes
|
|
74
|
+
- `--json` - Output results as JSON
|
|
75
|
+
- `--substitution, -s <name>` - Only update specific substitution(s)
|
|
76
|
+
|
|
77
|
+
## Global Options
|
|
78
|
+
|
|
79
|
+
- `--help, -h` - Show help
|
|
80
|
+
- `--version, -v` - Show version
|
|
81
|
+
|
|
82
|
+
## Links
|
|
83
|
+
|
|
84
|
+
- [Repository](https://github.com/lucasilverentand/kustodian)
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kustodian/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI framework with DI and middleware for Kustodian",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
"@kustodian/plugins": "workspace:*",
|
|
46
46
|
"@kustodian/registry": "workspace:*",
|
|
47
47
|
"@kustodian/schema": "workspace:*",
|
|
48
|
-
"
|
|
48
|
+
"@kustodian/sources": "workspace:*",
|
|
49
|
+
"ora": "^9.0.0",
|
|
50
|
+
"yaml": "^2.8.2"
|
|
49
51
|
}
|
|
50
52
|
}
|
package/src/bin.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { apply_command } from './commands/apply.js';
|
|
4
4
|
import { init_command } from './commands/init.js';
|
|
5
|
+
import { sources_command } from './commands/sources.js';
|
|
5
6
|
import { update_command } from './commands/update.js';
|
|
6
7
|
import { validate_command } from './commands/validate.js';
|
|
7
8
|
import { create_container } from './container.js';
|
|
@@ -29,7 +30,8 @@ async function main() {
|
|
|
29
30
|
console.log(
|
|
30
31
|
' apply Apply full cluster configuration (generates, pushes OCI, deploys)',
|
|
31
32
|
);
|
|
32
|
-
console.log(' update Check and update image version substitutions
|
|
33
|
+
console.log(' update Check and update image version substitutions');
|
|
34
|
+
console.log(' sources Manage template sources (fetch, list, cache)\n');
|
|
33
35
|
console.log('Options:');
|
|
34
36
|
console.log(' --help, -h Show help');
|
|
35
37
|
console.log(' --version, -v Show version\n');
|
|
@@ -52,6 +54,7 @@ async function main() {
|
|
|
52
54
|
cli.command(validate_command);
|
|
53
55
|
cli.command(apply_command);
|
|
54
56
|
cli.command(update_command);
|
|
57
|
+
cli.command(sources_command);
|
|
55
58
|
|
|
56
59
|
// Create container
|
|
57
60
|
const container = create_container();
|
package/src/commands/apply.ts
CHANGED
|
@@ -93,7 +93,7 @@ export const apply_command = define_command({
|
|
|
93
93
|
console.log('\n[1/3] Loading project configuration...');
|
|
94
94
|
|
|
95
95
|
const root_result = await find_project_root(project_path);
|
|
96
|
-
if (!root_result
|
|
96
|
+
if (!is_success(root_result)) {
|
|
97
97
|
console.error(` ✗ Error: ${root_result.error.message}`);
|
|
98
98
|
return root_result;
|
|
99
99
|
}
|
|
@@ -102,7 +102,7 @@ export const apply_command = define_command({
|
|
|
102
102
|
console.log(` → Project root: ${project_root}`);
|
|
103
103
|
|
|
104
104
|
const project_result = await load_project(project_root);
|
|
105
|
-
if (!project_result
|
|
105
|
+
if (!is_success(project_result)) {
|
|
106
106
|
console.error(` ✗ Error: ${project_result.error.message}`);
|
|
107
107
|
return project_result;
|
|
108
108
|
}
|
|
@@ -296,7 +296,7 @@ export const apply_command = define_command({
|
|
|
296
296
|
{},
|
|
297
297
|
);
|
|
298
298
|
|
|
299
|
-
if (!gen_result
|
|
299
|
+
if (!is_success(gen_result)) {
|
|
300
300
|
console.error(` ✗ Generation failed: ${gen_result.error.message}`);
|
|
301
301
|
return gen_result;
|
|
302
302
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { failure, success } from '@kustodian/core';
|
|
4
|
+
import { find_project_root } from '@kustodian/loader';
|
|
5
|
+
import type { TemplateSourceType } from '@kustodian/schema';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_CACHE_DIR,
|
|
8
|
+
create_cache_manager,
|
|
9
|
+
get_fetcher_for_source,
|
|
10
|
+
load_templates_from_sources,
|
|
11
|
+
} from '@kustodian/sources';
|
|
12
|
+
import { parse } from 'yaml';
|
|
13
|
+
|
|
14
|
+
import { define_command } from '../command.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Project configuration with template sources.
|
|
18
|
+
*/
|
|
19
|
+
interface ProjectConfigType {
|
|
20
|
+
spec?: {
|
|
21
|
+
template_sources?: TemplateSourceType[];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reads template sources from project config.
|
|
27
|
+
*/
|
|
28
|
+
async function get_template_sources(project_root: string): Promise<TemplateSourceType[]> {
|
|
29
|
+
const config_path = path.join(project_root, 'kustodian.yaml');
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(config_path, 'utf-8');
|
|
32
|
+
const config = parse(content) as ProjectConfigType;
|
|
33
|
+
return config?.spec?.template_sources ?? [];
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Formats bytes to human-readable size.
|
|
41
|
+
*/
|
|
42
|
+
function format_bytes(bytes: number): string {
|
|
43
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
44
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
45
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
46
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sources command - manage template sources.
|
|
51
|
+
*/
|
|
52
|
+
export const sources_command = define_command({
|
|
53
|
+
name: 'sources',
|
|
54
|
+
description: 'Manage template sources',
|
|
55
|
+
subcommands: [
|
|
56
|
+
// sources fetch
|
|
57
|
+
{
|
|
58
|
+
name: 'fetch',
|
|
59
|
+
description: 'Fetch or update template sources',
|
|
60
|
+
options: [
|
|
61
|
+
{
|
|
62
|
+
name: 'force',
|
|
63
|
+
short: 'f',
|
|
64
|
+
description: 'Force refresh all sources (ignore cache)',
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'source',
|
|
69
|
+
short: 's',
|
|
70
|
+
description: 'Fetch a specific source only',
|
|
71
|
+
type: 'string',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'project',
|
|
75
|
+
short: 'p',
|
|
76
|
+
description: 'Path to project root',
|
|
77
|
+
type: 'string',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
handler: async (ctx) => {
|
|
81
|
+
const project_path = (ctx.options['project'] as string) || process.cwd();
|
|
82
|
+
const force_refresh = ctx.options['force'] as boolean;
|
|
83
|
+
const source_filter = ctx.options['source'] as string | undefined;
|
|
84
|
+
|
|
85
|
+
// Find project root
|
|
86
|
+
const root_result = await find_project_root(project_path);
|
|
87
|
+
if (!root_result.success) {
|
|
88
|
+
console.error(`Error: ${root_result.error.message}`);
|
|
89
|
+
return root_result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const project_root = root_result.value;
|
|
93
|
+
|
|
94
|
+
// Get template sources from project config
|
|
95
|
+
let sources = await get_template_sources(project_root);
|
|
96
|
+
|
|
97
|
+
if (sources.length === 0) {
|
|
98
|
+
console.log('No template sources configured in kustodian.yaml');
|
|
99
|
+
return success(undefined);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Filter if specific source requested
|
|
103
|
+
if (source_filter) {
|
|
104
|
+
sources = sources.filter((s) => s.name === source_filter);
|
|
105
|
+
if (sources.length === 0) {
|
|
106
|
+
console.error(`Source '${source_filter}' not found in configuration`);
|
|
107
|
+
return failure({ code: 'NOT_FOUND', message: `Source '${source_filter}' not found` });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`Fetching ${sources.length} template source(s)...`);
|
|
112
|
+
if (force_refresh) {
|
|
113
|
+
console.log('(force refresh enabled)\n');
|
|
114
|
+
} else {
|
|
115
|
+
console.log('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
|
|
119
|
+
const result = await load_templates_from_sources(sources, {
|
|
120
|
+
cache_dir,
|
|
121
|
+
force_refresh,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!result.success) {
|
|
125
|
+
console.error(`\nError: ${result.error.message}`);
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Report results
|
|
130
|
+
for (const resolved of result.value.resolved) {
|
|
131
|
+
const status = resolved.fetch_result.from_cache ? '(cached)' : '(fetched)';
|
|
132
|
+
console.log(` ✓ ${resolved.source.name} @ ${resolved.fetch_result.version} ${status}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`\n✓ Loaded ${result.value.templates.length} template(s) from sources`);
|
|
136
|
+
return success(undefined);
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// sources list
|
|
141
|
+
{
|
|
142
|
+
name: 'list',
|
|
143
|
+
description: 'List configured template sources',
|
|
144
|
+
options: [
|
|
145
|
+
{
|
|
146
|
+
name: 'cached',
|
|
147
|
+
short: 'c',
|
|
148
|
+
description: 'Show cached sources only',
|
|
149
|
+
type: 'boolean',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'project',
|
|
153
|
+
short: 'p',
|
|
154
|
+
description: 'Path to project root',
|
|
155
|
+
type: 'string',
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
handler: async (ctx) => {
|
|
159
|
+
const project_path = (ctx.options['project'] as string) || process.cwd();
|
|
160
|
+
const show_cached = ctx.options['cached'] as boolean;
|
|
161
|
+
|
|
162
|
+
// Find project root
|
|
163
|
+
const root_result = await find_project_root(project_path);
|
|
164
|
+
if (!root_result.success) {
|
|
165
|
+
console.error(`Error: ${root_result.error.message}`);
|
|
166
|
+
return root_result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const project_root = root_result.value;
|
|
170
|
+
const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
|
|
171
|
+
|
|
172
|
+
if (show_cached) {
|
|
173
|
+
// Show cached sources
|
|
174
|
+
const cache = create_cache_manager(cache_dir);
|
|
175
|
+
const entries_result = await cache.list();
|
|
176
|
+
|
|
177
|
+
if (!entries_result.success) {
|
|
178
|
+
console.error(`Error: ${entries_result.error.message}`);
|
|
179
|
+
return entries_result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const entries = entries_result.value;
|
|
183
|
+
if (entries.length === 0) {
|
|
184
|
+
console.log('No cached sources');
|
|
185
|
+
return success(undefined);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('Cached sources:\n');
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const expired = entry.expires_at && entry.expires_at < new Date() ? ' (expired)' : '';
|
|
191
|
+
const mutable = entry.expires_at ? '(mutable)' : '(immutable)';
|
|
192
|
+
console.log(` ${entry.source_name} @ ${entry.version}`);
|
|
193
|
+
console.log(` Type: ${entry.source_type} ${mutable}${expired}`);
|
|
194
|
+
console.log(` Fetched: ${entry.fetched_at.toISOString()}`);
|
|
195
|
+
console.log('');
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Show configured sources
|
|
199
|
+
const sources = await get_template_sources(project_root);
|
|
200
|
+
|
|
201
|
+
if (sources.length === 0) {
|
|
202
|
+
console.log('No template sources configured');
|
|
203
|
+
return success(undefined);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log('Configured sources:\n');
|
|
207
|
+
for (const source of sources) {
|
|
208
|
+
const type = source.git ? 'git' : source.http ? 'http' : source.oci ? 'oci' : 'unknown';
|
|
209
|
+
console.log(` ${source.name} (${type})`);
|
|
210
|
+
|
|
211
|
+
if (source.git) {
|
|
212
|
+
const ref = source.git.ref.tag ?? source.git.ref.branch ?? source.git.ref.commit;
|
|
213
|
+
console.log(` URL: ${source.git.url}`);
|
|
214
|
+
console.log(` Ref: ${ref}`);
|
|
215
|
+
if (source.git.path) console.log(` Path: ${source.git.path}`);
|
|
216
|
+
}
|
|
217
|
+
if (source.http) {
|
|
218
|
+
console.log(` URL: ${source.http.url}`);
|
|
219
|
+
if (source.http.checksum) console.log(` Checksum: ${source.http.checksum}`);
|
|
220
|
+
}
|
|
221
|
+
if (source.oci) {
|
|
222
|
+
const ref = source.oci.digest ?? source.oci.tag;
|
|
223
|
+
console.log(` Registry: ${source.oci.registry}/${source.oci.repository}`);
|
|
224
|
+
console.log(` Tag: ${ref}`);
|
|
225
|
+
}
|
|
226
|
+
if (source.ttl) console.log(` TTL: ${source.ttl}`);
|
|
227
|
+
console.log('');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return success(undefined);
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
// sources cache
|
|
236
|
+
{
|
|
237
|
+
name: 'cache',
|
|
238
|
+
description: 'Manage template cache',
|
|
239
|
+
subcommands: [
|
|
240
|
+
// sources cache info
|
|
241
|
+
{
|
|
242
|
+
name: 'info',
|
|
243
|
+
description: 'Show cache statistics',
|
|
244
|
+
options: [
|
|
245
|
+
{
|
|
246
|
+
name: 'project',
|
|
247
|
+
short: 'p',
|
|
248
|
+
description: 'Path to project root',
|
|
249
|
+
type: 'string',
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
handler: async (ctx) => {
|
|
253
|
+
const project_path = (ctx.options['project'] as string) || process.cwd();
|
|
254
|
+
|
|
255
|
+
const root_result = await find_project_root(project_path);
|
|
256
|
+
if (!root_result.success) {
|
|
257
|
+
console.error(`Error: ${root_result.error.message}`);
|
|
258
|
+
return root_result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const project_root = root_result.value;
|
|
262
|
+
const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
|
|
263
|
+
const cache = create_cache_manager(cache_dir);
|
|
264
|
+
|
|
265
|
+
const entries_result = await cache.list();
|
|
266
|
+
const size_result = await cache.size();
|
|
267
|
+
|
|
268
|
+
if (!entries_result.success || !size_result.success) {
|
|
269
|
+
console.log('Cache directory not found or empty');
|
|
270
|
+
return success(undefined);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const entries = entries_result.value;
|
|
274
|
+
const total_size = size_result.value;
|
|
275
|
+
const expired = entries.filter((e) => e.expires_at && e.expires_at < new Date()).length;
|
|
276
|
+
|
|
277
|
+
console.log('Cache statistics:\n');
|
|
278
|
+
console.log(` Location: ${cache_dir}`);
|
|
279
|
+
console.log(` Total entries: ${entries.length}`);
|
|
280
|
+
console.log(` Expired entries: ${expired}`);
|
|
281
|
+
console.log(` Total size: ${format_bytes(total_size)}`);
|
|
282
|
+
|
|
283
|
+
return success(undefined);
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
// sources cache prune
|
|
288
|
+
{
|
|
289
|
+
name: 'prune',
|
|
290
|
+
description: 'Remove expired cache entries',
|
|
291
|
+
options: [
|
|
292
|
+
{
|
|
293
|
+
name: 'project',
|
|
294
|
+
short: 'p',
|
|
295
|
+
description: 'Path to project root',
|
|
296
|
+
type: 'string',
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
handler: async (ctx) => {
|
|
300
|
+
const project_path = (ctx.options['project'] as string) || process.cwd();
|
|
301
|
+
|
|
302
|
+
const root_result = await find_project_root(project_path);
|
|
303
|
+
if (!root_result.success) {
|
|
304
|
+
console.error(`Error: ${root_result.error.message}`);
|
|
305
|
+
return root_result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const project_root = root_result.value;
|
|
309
|
+
const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
|
|
310
|
+
const cache = create_cache_manager(cache_dir);
|
|
311
|
+
|
|
312
|
+
console.log('Pruning expired cache entries...');
|
|
313
|
+
const result = await cache.prune();
|
|
314
|
+
|
|
315
|
+
if (!result.success) {
|
|
316
|
+
console.error(`Error: ${result.error.message}`);
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log(`✓ Removed ${result.value} expired entries`);
|
|
321
|
+
return success(undefined);
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// sources cache clear
|
|
326
|
+
{
|
|
327
|
+
name: 'clear',
|
|
328
|
+
description: 'Clear all cached templates',
|
|
329
|
+
options: [
|
|
330
|
+
{
|
|
331
|
+
name: 'project',
|
|
332
|
+
short: 'p',
|
|
333
|
+
description: 'Path to project root',
|
|
334
|
+
type: 'string',
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
handler: async (ctx) => {
|
|
338
|
+
const project_path = (ctx.options['project'] as string) || process.cwd();
|
|
339
|
+
|
|
340
|
+
const root_result = await find_project_root(project_path);
|
|
341
|
+
if (!root_result.success) {
|
|
342
|
+
console.error(`Error: ${root_result.error.message}`);
|
|
343
|
+
return root_result;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const project_root = root_result.value;
|
|
347
|
+
const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
|
|
348
|
+
const cache = create_cache_manager(cache_dir);
|
|
349
|
+
|
|
350
|
+
console.log('Clearing template cache...');
|
|
351
|
+
const result = await cache.clear();
|
|
352
|
+
|
|
353
|
+
if (!result.success) {
|
|
354
|
+
console.error(`Error: ${result.error.message}`);
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log('✓ Cache cleared');
|
|
359
|
+
return success(undefined);
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
// sources versions
|
|
366
|
+
{
|
|
367
|
+
name: 'versions',
|
|
368
|
+
description: 'List available versions for a source',
|
|
369
|
+
arguments: [
|
|
370
|
+
{
|
|
371
|
+
name: 'source',
|
|
372
|
+
description: 'Source name',
|
|
373
|
+
required: true,
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
options: [
|
|
377
|
+
{
|
|
378
|
+
name: 'project',
|
|
379
|
+
short: 'p',
|
|
380
|
+
description: 'Path to project root',
|
|
381
|
+
type: 'string',
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
handler: async (ctx) => {
|
|
385
|
+
const project_path = (ctx.options['project'] as string) || process.cwd();
|
|
386
|
+
const source_name = ctx.args[0] as string | undefined;
|
|
387
|
+
|
|
388
|
+
if (!source_name) {
|
|
389
|
+
console.error('Error: Source name is required');
|
|
390
|
+
return failure({ code: 'INVALID_ARGUMENT', message: 'Source name is required' });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const root_result = await find_project_root(project_path);
|
|
394
|
+
if (!root_result.success) {
|
|
395
|
+
console.error(`Error: ${root_result.error.message}`);
|
|
396
|
+
return root_result;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const project_root = root_result.value;
|
|
400
|
+
const sources = await get_template_sources(project_root);
|
|
401
|
+
const source = sources.find((s) => s.name === source_name);
|
|
402
|
+
|
|
403
|
+
if (!source) {
|
|
404
|
+
console.error(`Source '${source_name}' not found in configuration`);
|
|
405
|
+
return failure({ code: 'NOT_FOUND', message: `Source '${source_name}' not found` });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const fetcher = get_fetcher_for_source(source);
|
|
409
|
+
|
|
410
|
+
console.log(`Fetching versions for '${source_name}'...`);
|
|
411
|
+
const versions_result = await fetcher.list_versions(source);
|
|
412
|
+
|
|
413
|
+
if (!versions_result.success) {
|
|
414
|
+
console.error(`Error: ${versions_result.error.message}`);
|
|
415
|
+
return versions_result;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const versions = versions_result.value;
|
|
419
|
+
if (versions.length === 0) {
|
|
420
|
+
console.log('No versions found (this source type may not support version listing)');
|
|
421
|
+
return success(undefined);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log(`\nAvailable versions (${versions.length}):\n`);
|
|
425
|
+
for (const v of versions.slice(0, 50)) {
|
|
426
|
+
// Limit to 50
|
|
427
|
+
const digest = v.digest ? ` (${v.digest.slice(0, 12)})` : '';
|
|
428
|
+
console.log(` ${v.version}${digest}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (versions.length > 50) {
|
|
432
|
+
console.log(` ... and ${versions.length - 50} more`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return success(undefined);
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
});
|