@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 +31 -0
- package/dist/commands/base.d.ts +24 -0
- package/dist/commands/base.js +15 -0
- package/dist/commands/build.d.ts +15 -0
- package/dist/commands/build.js +155 -0
- package/dist/commands/generate-new.d.ts +11 -0
- package/dist/commands/generate-new.js +146 -0
- package/dist/commands/generate.d.ts +11 -0
- package/dist/commands/generate.js +198 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +140 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +39 -0
- package/dist/types/tixyel-cli-config.d.ts +149 -0
- package/dist/types/tixyel-cli-config.js +162 -0
- package/dist/types/tixyel-config.d.ts +83 -0
- package/dist/types/tixyel-config.js +43 -0
- package/dist/utils/build-processor.d.ts +12 -0
- package/dist/utils/build-processor.js +156 -0
- package/dist/utils/config.d.ts +23 -0
- package/dist/utils/config.js +34 -0
- package/dist/utils/find-widgets.d.ts +19 -0
- package/dist/utils/find-widgets.js +35 -0
- package/dist/utils/load-cli-config.d.ts +5 -0
- package/dist/utils/load-cli-config.js +66 -0
- package/dist/utils/version.d.ts +12 -0
- package/dist/utils/version.js +49 -0
- package/dist/utils/workspace.d.ts +13 -0
- package/dist/utils/workspace.js +43 -0
- package/package.json +62 -0
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
|
+
}
|