@tixyel/cli 1.0.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 ADDED
@@ -0,0 +1,31 @@
1
+ # @tixyel/cli
2
+
3
+ CLI tool for Tixyel widgets.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @tixyel/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ tixyel --help
15
+ ```
16
+
17
+ ## Development
18
+
19
+ ```bash
20
+ npm run dev
21
+ ```
22
+
23
+ ## Build
24
+
25
+ ```bash
26
+ npm run build
27
+ ```
28
+
29
+ ## License
30
+
31
+ Apache-2.0
@@ -0,0 +1,24 @@
1
+ import type { Command as CommanderCommand } from 'commander';
2
+ /**
3
+ * Base class for all CLI commands
4
+ */
5
+ export declare abstract class Command {
6
+ /**
7
+ * Command name (e.g., 'init', 'build', 'generate')
8
+ */
9
+ abstract name: string;
10
+ /**
11
+ * Command description
12
+ */
13
+ abstract description: string;
14
+ /**
15
+ * Register the command with commander
16
+ * Override to add custom arguments/options
17
+ */
18
+ register(command: CommanderCommand): void;
19
+ /**
20
+ * Execute the command
21
+ * Implement in subclasses
22
+ */
23
+ abstract execute(...args: unknown[]): Promise<void>;
24
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Base class for all CLI commands
3
+ */
4
+ export class Command {
5
+ /**
6
+ * Register the command with commander
7
+ * Override to add custom arguments/options
8
+ */
9
+ register(command) {
10
+ command
11
+ .command(this.name)
12
+ .description(this.description)
13
+ .action((...args) => this.execute(...args));
14
+ }
15
+ }
@@ -0,0 +1,15 @@
1
+ import type { Command as CommanderCommand } from 'commander';
2
+ import { Command } from './base.js';
3
+ /**
4
+ * Build command
5
+ */
6
+ export declare class BuildCommand extends Command {
7
+ name: string;
8
+ description: string;
9
+ register(command: CommanderCommand): void;
10
+ execute(options?: {
11
+ depth?: string;
12
+ parallel?: boolean;
13
+ verbose?: boolean;
14
+ }): Promise<void>;
15
+ }
@@ -0,0 +1,155 @@
1
+ import inquirer from 'inquirer';
2
+ import { findWidgets } from '../utils/find-widgets.js';
3
+ import { bumpWidgetVersion } from '../utils/version.js';
4
+ import { loadCliConfig } from '../utils/load-cli-config.js';
5
+ import { validateWorkspaceInit } from '../utils/workspace.js';
6
+ import { buildWidget as processBuild } from '../utils/build-processor.js';
7
+ import { Command } from './base.js';
8
+ /**
9
+ * Build command
10
+ */
11
+ export class BuildCommand extends Command {
12
+ constructor() {
13
+ super(...arguments);
14
+ this.name = 'build';
15
+ this.description = 'Build widgets from the workspace';
16
+ }
17
+ register(command) {
18
+ command
19
+ .command(this.name)
20
+ .description(this.description)
21
+ .option('-d, --depth <number>', 'Maximum depth to search for widgets', '3')
22
+ .option('-p, --parallel', 'Build widgets in parallel')
23
+ .option('-v, --verbose', 'Show verbose output')
24
+ .action((...args) => this.execute(...args));
25
+ }
26
+ async execute(options = {}) {
27
+ try {
28
+ // Validate workspace is initialized
29
+ const workspaceRoot = await validateWorkspaceInit();
30
+ const rootPath = process.cwd();
31
+ // Load CLI config from workspace root
32
+ const cliConfig = await loadCliConfig(workspaceRoot);
33
+ const maxDepth = options.depth ? parseInt(options.depth, 10) : cliConfig.search.maxDepth;
34
+ console.log(`šŸ” Searching for widgets (max depth: ${maxDepth})...\n`);
35
+ // Find all widgets
36
+ const widgets = await findWidgets(rootPath, maxDepth, cliConfig.search.ignore);
37
+ if (widgets.length === 0) {
38
+ console.log('āŒ No widgets found with .tixyel configuration files.');
39
+ return;
40
+ }
41
+ console.log(`āœ… Found ${widgets.length} widget(s)\n`);
42
+ // Create choices for selection
43
+ const choices = widgets.map((widget) => ({
44
+ name: `${widget.config.name} (${widget.relativePath})`,
45
+ value: widget.path,
46
+ checked: false,
47
+ }));
48
+ let selectedPaths = [];
49
+ if (choices.length === 1) {
50
+ selectedPaths = [choices[0].value];
51
+ console.log(`šŸ”’ Only one widget found, auto-selected: ${choices[0].name}\n`);
52
+ }
53
+ else {
54
+ // Prompt for widget selection
55
+ const answers = await inquirer.prompt([
56
+ {
57
+ type: 'checkbox',
58
+ name: 'selectedWidgets',
59
+ message: 'Select widgets to build:',
60
+ choices,
61
+ pageSize: 10,
62
+ loop: false,
63
+ },
64
+ ]);
65
+ selectedPaths = answers.selectedWidgets;
66
+ }
67
+ if (selectedPaths.length === 0) {
68
+ console.log('\nāš ļø No widgets selected. Exiting...');
69
+ return;
70
+ }
71
+ // Prompt for version bump
72
+ const versionAnswers = await inquirer.prompt([
73
+ {
74
+ type: 'select',
75
+ name: 'versionBump',
76
+ message: 'Version bump type:',
77
+ choices: [
78
+ { name: 'No version bump', value: 'none' },
79
+ { name: 'Patch (x.x.1)', value: 'patch' },
80
+ { name: 'Minor (x.1.0)', value: 'minor' },
81
+ { name: 'Major (1.0.0)', value: 'major' },
82
+ ],
83
+ default: 'none',
84
+ loop: false,
85
+ pageSize: 4,
86
+ },
87
+ ]);
88
+ const versionBump = versionAnswers.versionBump;
89
+ // Resolve parallel setting (CLI option overrides config)
90
+ const useParallel = options.parallel ?? cliConfig.build.parallel;
91
+ const verbose = options.verbose ?? cliConfig.build.verbose;
92
+ console.log(`\nšŸš€ Building ${selectedPaths.length} widget(s)${useParallel ? ' (parallel)' : ''}...\n`);
93
+ // Build each selected widget
94
+ if (useParallel) {
95
+ // Parallel execution
96
+ await Promise.all(selectedPaths.map(async (widgetPath) => {
97
+ const widget = widgets.find((w) => w.path === widgetPath);
98
+ if (widget) {
99
+ await buildWidget(widget.path, widget.config.name, versionBump !== 'none' ? versionBump : undefined, verbose, widget.config, cliConfig);
100
+ }
101
+ }));
102
+ }
103
+ else {
104
+ // Sequential execution
105
+ for (const widgetPath of selectedPaths) {
106
+ const widget = widgets.find((w) => w.path === widgetPath);
107
+ if (widget) {
108
+ await buildWidget(widget.path, widget.config.name, versionBump !== 'none' ? versionBump : undefined, verbose, widget.config, cliConfig);
109
+ }
110
+ }
111
+ }
112
+ console.log('\n✨ Build complete!');
113
+ }
114
+ catch (error) {
115
+ console.error(`${error}`);
116
+ process.exit(1);
117
+ }
118
+ }
119
+ }
120
+ /**
121
+ * Builds a single widget
122
+ */
123
+ async function buildWidget(widgetPath, widgetName, versionBump, verbose, config, cliConfig) {
124
+ console.log(`šŸ“¦ Building ${widgetName}...`);
125
+ if (verbose) {
126
+ console.log(` Path: ${widgetPath}`);
127
+ }
128
+ // Bump version if requested
129
+ if (versionBump) {
130
+ const newVersion = await bumpWidgetVersion(widgetPath, versionBump);
131
+ if (newVersion) {
132
+ console.log(` šŸ“Œ Version bumped to: ${newVersion}`);
133
+ }
134
+ }
135
+ // Process build if config and cliConfig provided
136
+ if (config && cliConfig) {
137
+ try {
138
+ await processBuild({
139
+ widgetPath,
140
+ config,
141
+ cliConfig,
142
+ verbose,
143
+ });
144
+ }
145
+ catch (error) {
146
+ console.error(` āŒ Build failed: ${error}`);
147
+ throw error;
148
+ }
149
+ }
150
+ else {
151
+ console.error(' āŒ Missing configuration for build process.');
152
+ throw new Error('Missing configuration for build process.');
153
+ }
154
+ console.log(`āœ“ ${widgetName} built successfully\n`);
155
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Generate command handler
3
+ *
4
+ * Usage:
5
+ * - `tixyel generate` -> creates widget in current directory (01 - Widget)
6
+ * - `tixyel generate widgets` -> creates widgets/01 - Widget/
7
+ * - `tixyel generate widgets myname` -> creates widgets/myname/
8
+ * - `tixyel generate widgets/` -> creates widget at widgets/
9
+ * - All parameters except path are optional and can be provided via prompts
10
+ */
11
+ export declare function generateCommand(targetPath?: string, widgetName?: string, description?: string, tags?: string): Promise<void>;
@@ -0,0 +1,146 @@
1
+ import { resolve, basename, join } from 'path';
2
+ import { mkdir, writeFile } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import inquirer from 'inquirer';
5
+ import { loadCliConfig } from '../utils/load-cli-config.js';
6
+ /**
7
+ * Determines if a path should be treated as a parent directory or target directory
8
+ * Paths ending with / are treated as target directories
9
+ */
10
+ function isTargetDirectory(path) {
11
+ return path.endsWith('/') || path.endsWith('\\');
12
+ }
13
+ /**
14
+ * Gets the next widget number in a parent directory
15
+ */
16
+ async function getNextWidgetNumber(parentPath) {
17
+ try {
18
+ if (!existsSync(parentPath)) {
19
+ return '01';
20
+ }
21
+ // Check for existing widget folders
22
+ const { readdirSync } = await import('fs');
23
+ const entries = readdirSync(parentPath);
24
+ const widgetNumbers = entries
25
+ .filter((name) => /^\d+\s*-\s*/.test(name))
26
+ .map((name) => parseInt(name.split('-')[0], 10))
27
+ .filter((num) => !isNaN(num));
28
+ const maxNum = widgetNumbers.length > 0 ? Math.max(...widgetNumbers) : 0;
29
+ return String(maxNum + 1).padStart(2, '0');
30
+ }
31
+ catch {
32
+ return '01';
33
+ }
34
+ }
35
+ /**
36
+ * Generate command handler
37
+ *
38
+ * Usage:
39
+ * - `tixyel generate` -> creates widget in current directory (01 - Widget)
40
+ * - `tixyel generate widgets` -> creates widgets/01 - Widget/
41
+ * - `tixyel generate widgets myname` -> creates widgets/myname/
42
+ * - `tixyel generate widgets/` -> creates widget at widgets/
43
+ * - All parameters except path are optional and can be provided via prompts
44
+ */
45
+ export async function generateCommand(targetPath, widgetName, description, tags) {
46
+ const rootPath = process.cwd();
47
+ // Load CLI config
48
+ const cliConfig = await loadCliConfig(rootPath);
49
+ // Default to current directory if no path provided
50
+ const resolvedPath = targetPath ? resolve(rootPath, targetPath.replace(/[/\\]$/, '')) : rootPath;
51
+ const isTarget = targetPath ? isTargetDirectory(targetPath) : false;
52
+ console.log('šŸŽØ Generating widget...\n');
53
+ if (isTarget) {
54
+ // Path ends with / -> use as target directory
55
+ const folderName = basename(resolvedPath);
56
+ await createWidget(resolvedPath, folderName, description, tags, cliConfig);
57
+ }
58
+ else {
59
+ // Path without / -> use as parent directory
60
+ let finalWidgetName = widgetName;
61
+ if (!finalWidgetName) {
62
+ // Get next number and ask for name
63
+ const nextNum = await getNextWidgetNumber(resolvedPath);
64
+ const defaultName = `${nextNum} - Widget`;
65
+ const answers = await inquirer.prompt([
66
+ {
67
+ type: 'input',
68
+ name: 'name',
69
+ message: 'Widget name:',
70
+ default: defaultName,
71
+ },
72
+ ]);
73
+ finalWidgetName = answers.name;
74
+ }
75
+ const widgetPath = join(resolvedPath, finalWidgetName);
76
+ await createWidget(widgetPath, finalWidgetName, description, tags, cliConfig);
77
+ }
78
+ console.log('\n✨ Generation complete!');
79
+ }
80
+ /**
81
+ * Prompts for optional widget metadata
82
+ */
83
+ async function promptMetadata(widgetName, initialDescription, initialTags) {
84
+ const answers = await inquirer.prompt([
85
+ {
86
+ type: 'input',
87
+ name: 'description',
88
+ message: 'Widget description:',
89
+ default: initialDescription || '',
90
+ },
91
+ {
92
+ type: 'input',
93
+ name: 'tags',
94
+ message: 'Widget tags (comma-separated):',
95
+ default: initialTags || '',
96
+ },
97
+ ]);
98
+ const tagsArray = answers.tags
99
+ ? answers.tags
100
+ .split(',')
101
+ .map((tag) => tag.trim())
102
+ .filter((tag) => tag.length > 0)
103
+ : [];
104
+ return {
105
+ description: answers.description,
106
+ tags: tagsArray,
107
+ };
108
+ }
109
+ /**
110
+ * Creates a widget with .tixyel configuration file
111
+ */
112
+ async function createWidget(widgetPath, widgetName, description, tagsStr, cliConfig) {
113
+ try {
114
+ console.log(`šŸ“ Creating widget: ${widgetName}`);
115
+ console.log(` Path: ${widgetPath}`);
116
+ // Create directory
117
+ await mkdir(widgetPath, { recursive: true });
118
+ // Prompt for metadata if not provided
119
+ const metadata = await promptMetadata(widgetName, description, tagsStr);
120
+ // Create .tixyel configuration
121
+ const tixyelConfig = {
122
+ name: widgetName,
123
+ description: metadata.description,
124
+ metadata: {
125
+ author: cliConfig?.generationDefaults.author || 'Tixyel',
126
+ tags: metadata.tags,
127
+ platform: cliConfig?.generationDefaults.platform || 'streamelements',
128
+ },
129
+ };
130
+ const configPath = join(widgetPath, '.tixyel');
131
+ await writeFile(configPath, JSON.stringify(tixyelConfig, null, 2), 'utf-8');
132
+ console.log(`āœ“ Created ${widgetName} successfully`);
133
+ console.log(` - Name: ${widgetName}`);
134
+ if (metadata.description) {
135
+ console.log(` - Description: ${metadata.description}`);
136
+ }
137
+ if (metadata.tags.length > 0) {
138
+ console.log(` - Tags: ${metadata.tags.join(', ')}`);
139
+ }
140
+ console.log(` - .tixyel configuration created`);
141
+ }
142
+ catch (error) {
143
+ console.error(`āŒ Failed to create widget: ${error}`);
144
+ throw error;
145
+ }
146
+ }
@@ -0,0 +1,11 @@
1
+ import type { Command as CommanderCommand } from 'commander';
2
+ import { Command } from './base.js';
3
+ /**
4
+ * Generate command
5
+ */
6
+ export declare class GenerateCommand extends Command {
7
+ name: string;
8
+ description: string;
9
+ register(command: CommanderCommand): void;
10
+ execute(targetPath?: string, widgetName?: string, description?: string, tags?: string): Promise<void>;
11
+ }
@@ -0,0 +1,198 @@
1
+ import { resolve, basename, join } from 'path';
2
+ import { mkdir, writeFile } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import inquirer from 'inquirer';
5
+ import { loadCliConfig } from '../utils/load-cli-config.js';
6
+ import { validateWorkspaceInit, getConfigPathFromWidget } from '../utils/workspace.js';
7
+ import { Command } from './base.js';
8
+ /**
9
+ * Generate command
10
+ */
11
+ export class GenerateCommand extends Command {
12
+ constructor() {
13
+ super(...arguments);
14
+ this.name = 'generate';
15
+ this.description = 'Generate a new widget (path defaults to current directory)';
16
+ }
17
+ register(command) {
18
+ command
19
+ .command(`${this.name} [path] [name] [description] [tags]`)
20
+ .description(this.description)
21
+ .action((...args) => this.execute(...args));
22
+ }
23
+ async execute(targetPath, widgetName, description, tags) {
24
+ try {
25
+ // Validate workspace is initialized
26
+ const workspaceRoot = await validateWorkspaceInit();
27
+ const rootPath = process.cwd();
28
+ // Load CLI config from workspace root
29
+ const cliConfig = await loadCliConfig(workspaceRoot);
30
+ // Default to current directory if no path provided
31
+ const resolvedPath = targetPath ? resolve(rootPath, targetPath.replace(/[/\\]$/, '')) : rootPath;
32
+ const isTarget = targetPath ? isTargetDirectory(targetPath) : false;
33
+ console.log('šŸŽØ Generating widget...\n');
34
+ if (isTarget) {
35
+ // Path ends with / -> use as target directory
36
+ const folderName = basename(resolvedPath);
37
+ await createWidget(resolvedPath, folderName, description, tags, cliConfig, workspaceRoot);
38
+ }
39
+ else {
40
+ // Path without / -> use as parent directory
41
+ let finalWidgetName = widgetName;
42
+ if (!finalWidgetName) {
43
+ // Get next number and ask for name
44
+ const nextNum = await getNextWidgetNumber(resolvedPath);
45
+ const defaultName = `${nextNum} - Widget`;
46
+ const answers = await inquirer.prompt([
47
+ {
48
+ type: 'input',
49
+ name: 'name',
50
+ message: 'Widget name:',
51
+ default: defaultName,
52
+ },
53
+ ]);
54
+ finalWidgetName = answers.name;
55
+ }
56
+ const widgetPath = join(resolvedPath, finalWidgetName);
57
+ await createWidget(widgetPath, finalWidgetName, description, tags, cliConfig, workspaceRoot);
58
+ }
59
+ console.log('\n✨ Generation complete!');
60
+ }
61
+ catch (error) {
62
+ console.error(`${error}`);
63
+ process.exit(1);
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Determines if a path should be treated as a parent directory or target directory
69
+ * Paths ending with / are treated as target directories
70
+ */
71
+ function isTargetDirectory(path) {
72
+ return path.endsWith('/') || path.endsWith('\\');
73
+ }
74
+ /**
75
+ * Gets the next widget number in a parent directory
76
+ */
77
+ async function getNextWidgetNumber(parentPath) {
78
+ try {
79
+ if (!existsSync(parentPath)) {
80
+ return '01';
81
+ }
82
+ // Check for existing widget folders
83
+ const { readdirSync } = await import('fs');
84
+ const entries = readdirSync(parentPath);
85
+ const widgetNumbers = entries
86
+ .filter((name) => /^\d+\s*-\s*/.test(name))
87
+ .map((name) => parseInt(name.split('-')[0], 10))
88
+ .filter((num) => !isNaN(num));
89
+ const maxNum = widgetNumbers.length > 0 ? Math.max(...widgetNumbers) : 0;
90
+ return String(maxNum + 1).padStart(2, '0');
91
+ }
92
+ catch {
93
+ return '01';
94
+ }
95
+ }
96
+ /**
97
+ * Prompts for optional widget metadata
98
+ */
99
+ async function promptMetadata(widgetName, initialDescription, initialTags) {
100
+ const answers = await inquirer.prompt([
101
+ {
102
+ type: 'input',
103
+ name: 'description',
104
+ message: 'Widget description:',
105
+ default: initialDescription || '',
106
+ },
107
+ {
108
+ type: 'input',
109
+ name: 'tags',
110
+ message: 'Widget tags (comma-separated):',
111
+ default: initialTags || '',
112
+ },
113
+ ]);
114
+ const tagsArray = answers.tags
115
+ ? answers.tags
116
+ .split(',')
117
+ .map((tag) => tag.trim())
118
+ .filter((tag) => tag.length > 0)
119
+ : [];
120
+ return {
121
+ description: answers.description,
122
+ tags: tagsArray,
123
+ };
124
+ }
125
+ /**
126
+ * Creates a widget with .tixyel configuration file
127
+ */
128
+ async function createWidget(widgetPath, widgetName, description, tagsStr, cliConfig, workspaceRoot) {
129
+ try {
130
+ console.log(`šŸ“ Creating widget: ${widgetName}`);
131
+ console.log(` Path: ${widgetPath}`);
132
+ // Create directory
133
+ await mkdir(widgetPath, { recursive: true });
134
+ // Calculate config path if workspace root is provided
135
+ const configPath = workspaceRoot ? getConfigPathFromWidget(widgetPath, workspaceRoot) : undefined;
136
+ // Prompt for metadata if not provided
137
+ const metadata = await promptMetadata(widgetName, description, tagsStr);
138
+ // Create .tixyel configuration
139
+ const tixyelConfig = {
140
+ name: widgetName,
141
+ description: metadata.description,
142
+ };
143
+ if (configPath) {
144
+ tixyelConfig.configPath = configPath;
145
+ }
146
+ tixyelConfig.metadata = {
147
+ author: cliConfig?.generationDefaults.author || 'Tixyel',
148
+ tags: metadata.tags,
149
+ platform: cliConfig?.generationDefaults.platform || 'streamelements',
150
+ };
151
+ const configFilePath = join(widgetPath, '.tixyel');
152
+ await writeFile(configFilePath, JSON.stringify(tixyelConfig, null, 2), 'utf-8');
153
+ // Create scaffold files from config
154
+ const scaffold = cliConfig?.generationDefaults.scaffold || [];
155
+ let createdFiles = 0;
156
+ let createdFolders = 0;
157
+ async function processScaffoldItem(item, basePath) {
158
+ const fullPath = join(basePath, item.name);
159
+ if (item.type === 'folder') {
160
+ await mkdir(fullPath, { recursive: true });
161
+ createdFolders++;
162
+ // Process folder contents if any
163
+ if (item.content && Array.isArray(item.content)) {
164
+ for (const child of item.content) {
165
+ await processScaffoldItem(child, fullPath);
166
+ }
167
+ }
168
+ }
169
+ else if (item.type === 'file') {
170
+ // Write file
171
+ await writeFile(fullPath, item.content || '', 'utf-8');
172
+ createdFiles++;
173
+ }
174
+ }
175
+ for (const item of scaffold) {
176
+ await processScaffoldItem(item, widgetPath);
177
+ }
178
+ console.log(`āœ“ Created ${widgetName} successfully`);
179
+ console.log(` - Name: ${widgetName}`);
180
+ if (metadata.description) {
181
+ console.log(` - Description: ${metadata.description}`);
182
+ }
183
+ if (metadata.tags.length > 0) {
184
+ console.log(` - Tags: ${metadata.tags.join(', ')}`);
185
+ }
186
+ if (configPath) {
187
+ console.log(` - Config path: ${configPath}`);
188
+ }
189
+ console.log(` - .tixyel configuration created`);
190
+ if (createdFiles > 0 || createdFolders > 0) {
191
+ console.log(` - Scaffold: ${createdFiles} file(s), ${createdFolders} folder(s)`);
192
+ }
193
+ }
194
+ catch (error) {
195
+ console.error(`āŒ Failed to create widget: ${error}`);
196
+ throw error;
197
+ }
198
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from './base.js';
2
+ /**
3
+ * Init command - initializes a workspace
4
+ */
5
+ export declare class InitCommand extends Command {
6
+ name: string;
7
+ description: string;
8
+ execute(): Promise<void>;
9
+ }