@linyjs/cli 0.0.2

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Init command - Create a new plugin project from template
3
+ *
4
+ * Downloads the default template from npm and sets up a new project
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import chalk from 'chalk';
10
+ import ora from 'ora';
11
+ import { execSync } from 'child_process';
12
+
13
+ interface InitOptions {
14
+ name?: string;
15
+ verbose: boolean;
16
+ }
17
+
18
+ /**
19
+ * Initialize a new plugin project
20
+ */
21
+ export async function initPlugin(options: InitOptions) {
22
+ const targetDir = options.name || 'my-plugin';
23
+ const absolutePath = path.resolve(process.cwd(), targetDir);
24
+
25
+ console.log(chalk.blue('🚀 Ling Plugin Project Creator\n'));
26
+
27
+ // Check if directory already exists
28
+ if (fs.existsSync(absolutePath)) {
29
+ console.error(chalk.red(`Error: Directory "${targetDir}" already exists`));
30
+ process.exit(1);
31
+ }
32
+
33
+ const spinner1 = ora('Downloading template from npm...').start();
34
+
35
+ try {
36
+ // Download template using npm create
37
+ const npmCommand = `npm create @linyjs/plugin-templates@latest ${targetDir}`;
38
+
39
+ if (options.verbose) {
40
+ console.log(chalk.gray('Running:'), npmCommand);
41
+ }
42
+
43
+ execSync(npmCommand, {
44
+ stdio: options.verbose ? 'inherit' : 'pipe',
45
+ cwd: process.cwd()
46
+ });
47
+
48
+ spinner1.succeed('Template downloaded');
49
+
50
+ // Show next steps
51
+ console.log('\n' + chalk.green('✓ Project created successfully!\n'));
52
+ console.log(chalk.cyan('Next steps:'));
53
+ console.log(` 1. Navigate to your project:`);
54
+ console.log(` ${chalk.gray(`cd ${targetDir}`)}`);
55
+ console.log(` 2. Install dependencies:`);
56
+ console.log(` ${chalk.gray('npm install')}`);
57
+ console.log(` 3. Start development:`);
58
+ console.log(` ${chalk.gray('npm run dev')}`);
59
+ console.log(` 4. Pack for deployment:`);
60
+ console.log(` ${chalk.gray('npx @linyjs/cli pack')}\n`);
61
+
62
+ console.log(chalk.cyan('Learn more:'));
63
+ console.log(` - Plugin Development Guide: https://github.com/linyjs/ling/blob/main/spec/插件开发指南.md`);
64
+ console.log(` - CLI Documentation: https://github.com/linyjs/ling/blob/main/packages/cli/README.md\n`);
65
+
66
+ } catch (error) {
67
+ spinner1.fail('Failed to create project');
68
+ console.error(chalk.red('\nError:'), error instanceof Error ? error.message : error);
69
+ console.error(chalk.yellow('\nTroubleshooting:'));
70
+ console.error(' - Make sure you have Node.js >= 18 installed');
71
+ console.error(' - Check your internet connection');
72
+ console.error(' - Try again with --verbose flag for more details');
73
+ process.exit(1);
74
+ }
75
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Pack plugin into .mpk file
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import archiver from 'archiver';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import { buildProject, type BuildOptions } from '../utils/builder.js';
11
+
12
+ interface PackOptions {
13
+ output: string;
14
+ name?: string;
15
+ version?: string | boolean;
16
+ build: boolean;
17
+ manifest?: string;
18
+ verbose: boolean;
19
+ }
20
+
21
+ export async function packPlugin(options: PackOptions) {
22
+ const projectRoot = process.cwd();
23
+
24
+ console.log(chalk.blue('📦 Ling Plugin Packer\n'));
25
+
26
+ // Step 1: Read package.json
27
+ const spinner1 = ora('Reading package.json...').start();
28
+ const packageJsonPath = path.join(projectRoot, 'package.json');
29
+
30
+ if (!fs.existsSync(packageJsonPath)) {
31
+ spinner1.fail('package.json not found');
32
+ console.error(chalk.red('\nPlease run this command in a plugin project directory.'));
33
+ process.exit(1);
34
+ }
35
+
36
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
37
+ spinner1.succeed('Package.json loaded');
38
+
39
+ if (options.verbose) {
40
+ console.log(chalk.gray(' Name:'), packageJson.name);
41
+ console.log(chalk.gray(' Version:'), packageJson.version);
42
+ }
43
+
44
+ // Step 2: Build project (if needed)
45
+ if (options.build) {
46
+ const buildOptions: BuildOptions = {
47
+ projectRoot,
48
+ verbose: options.verbose,
49
+ // esbuild is used by default - no need for users to configure webpack/rollup
50
+ // CLI handles all build optimization automatically
51
+ minify: true, // Always minify for production bundles
52
+ external: [
53
+ 'react',
54
+ 'react-dom',
55
+ 'express',
56
+ '@linyjs/*',
57
+ '@ling/*'
58
+ ]
59
+ };
60
+
61
+ const buildResult = await buildProject(buildOptions);
62
+
63
+ if (options.verbose) {
64
+ console.log(chalk.gray('\n📊 Build Statistics:'));
65
+ console.log(chalk.gray(' Duration:'), `${buildResult.duration}ms`);
66
+ console.log(chalk.gray(' Files:'), buildResult.files.length);
67
+ console.log(chalk.gray(' Size:'), formatFileSize(buildResult.size));
68
+ console.log(chalk.gray(' Output:'), buildResult.outputDir);
69
+ }
70
+ } else {
71
+ console.log(chalk.yellow('⚠ Skipping build step (--no-build)'));
72
+ }
73
+
74
+ // Step 3: Generate manifest.json
75
+ const spinner3 = ora('Generating manifest.json...').start();
76
+ const distDir = path.join(projectRoot, 'dist');
77
+
78
+ if (!fs.existsSync(distDir)) {
79
+ spinner3.fail('dist/ directory not found');
80
+ console.error(chalk.red('\nPlease build the project first or remove --no-build flag.'));
81
+ process.exit(1);
82
+ }
83
+
84
+ const manifest = {
85
+ name: options.name || packageJson.name,
86
+ version: options.version || packageJson.version,
87
+ description: packageJson.description || '',
88
+ author: typeof packageJson.author === 'string'
89
+ ? packageJson.author
90
+ : packageJson.author?.name || '',
91
+ serverEntry: detectEntry(distDir, 'server'),
92
+ clientEntry: detectEntry(distDir, 'client'),
93
+ dependencies: extractDependencies(packageJson),
94
+ permissions: extractPermissions(packageJson)
95
+ };
96
+
97
+ // Write manifest.json to dist/
98
+ const manifestPath = path.join(distDir, 'manifest.json');
99
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
100
+ spinner3.succeed('Manifest generated');
101
+
102
+ if (options.verbose) {
103
+ console.log(chalk.gray(' Server entry:'), manifest.serverEntry);
104
+ console.log(chalk.gray(' Client entry:'), manifest.clientEntry);
105
+ }
106
+
107
+ // Step 4: Validate structure
108
+ const spinner4 = ora('Validating plugin structure...').start();
109
+ const validationErrors = validatePluginStructure(distDir, manifest);
110
+
111
+ if (validationErrors.length > 0) {
112
+ spinner4.fail('Validation failed');
113
+ console.error(chalk.red('\nValidation errors:'));
114
+ validationErrors.forEach(err => {
115
+ console.error(chalk.red(` - ${err}`));
116
+ });
117
+ process.exit(1);
118
+ }
119
+
120
+ spinner4.succeed('Structure validated');
121
+
122
+ // Step 5: Pack to .mpk
123
+ const outputDir = path.resolve(options.output);
124
+ const outputFile = path.join(outputDir, `${manifest.name}-${manifest.version}.mpk`);
125
+
126
+ // Ensure output directory exists
127
+ if (!fs.existsSync(outputDir)) {
128
+ fs.mkdirSync(outputDir, { recursive: true });
129
+ }
130
+
131
+ const spinner5 = ora('Packing plugin...').start();
132
+
133
+ try {
134
+ await createZipArchive(distDir, outputFile);
135
+ spinner5.succeed('Plugin packed successfully');
136
+ } catch (error) {
137
+ spinner5.fail('Pack failed');
138
+ console.error(chalk.red('\nPack error:'));
139
+ if (error instanceof Error) {
140
+ console.error(error.message);
141
+ }
142
+ process.exit(1);
143
+ }
144
+
145
+ // Step 6: Show result
146
+ const stats = fs.statSync(outputFile);
147
+ const fileSize = formatFileSize(stats.size);
148
+
149
+ console.log('\n' + chalk.green('✓ Plugin ready!'));
150
+ console.log(chalk.gray(' File:'), chalk.cyan(outputFile));
151
+ console.log(chalk.gray(' Size:'), chalk.cyan(fileSize));
152
+ console.log(chalk.gray(' Version:'), chalk.cyan(manifest.version));
153
+
154
+ console.log('\n' + chalk.blue('Next steps:'));
155
+ console.log(' 1. Test the plugin:');
156
+ console.log(chalk.gray(' cp ' + outputFile + ' /path/to/ling/packages/ssr/extensions/'));
157
+ console.log(' 2. Restart SSR server:');
158
+ console.log(chalk.gray(' cd packages/ssr && npx tsx src/demo/dev-server-with-pkg.tsx'));
159
+ console.log(' 3. Validate the plugin:');
160
+ console.log(chalk.gray(' npx @linyjs/cli validate ' + outputFile));
161
+ }
162
+
163
+ /**
164
+ * Detect entry file in dist directory
165
+ */
166
+ function detectEntry(distDir: string, type: 'server' | 'client'): string {
167
+ const entryPath = path.join(distDir, type, 'index.js');
168
+
169
+ if (fs.existsSync(entryPath)) {
170
+ return `${type}/index.js`;
171
+ }
172
+
173
+ // Try other common patterns
174
+ const alternatives = [
175
+ `${type}.js`,
176
+ `${type}/index.ts`,
177
+ `index.${type}.js`
178
+ ];
179
+
180
+ for (const alt of alternatives) {
181
+ if (fs.existsSync(path.join(distDir, alt))) {
182
+ return alt;
183
+ }
184
+ }
185
+
186
+ throw new Error(`Cannot find ${type} entry file in dist/`);
187
+ }
188
+
189
+ /**
190
+ * Extract dependencies from package.json
191
+ */
192
+ function extractDependencies(packageJson: any): string[] {
193
+ const deps: string[] = [];
194
+
195
+ // Check dependencies
196
+ if (packageJson.dependencies) {
197
+ Object.keys(packageJson.dependencies).forEach(dep => {
198
+ if (dep.startsWith('@linyjs/') || dep.startsWith('@ling/')) {
199
+ deps.push(`${dep}@${packageJson.dependencies[dep]}`);
200
+ }
201
+ });
202
+ }
203
+
204
+ return deps;
205
+ }
206
+
207
+ /**
208
+ * Extract permissions from package.json keywords or custom field
209
+ */
210
+ function extractPermissions(packageJson: any): string[] {
211
+ // Check for custom permissions field
212
+ if (packageJson.ling?.permissions) {
213
+ return packageJson.ling.permissions;
214
+ }
215
+
216
+ // Or extract from keywords
217
+ if (packageJson.keywords) {
218
+ return packageJson.keywords
219
+ .filter((k: string) => k.startsWith('permission:'))
220
+ .map((k: string) => k.replace('permission:', ''));
221
+ }
222
+
223
+ return [];
224
+ }
225
+
226
+ /**
227
+ * Validate plugin structure
228
+ */
229
+ function validatePluginStructure(distDir: string, manifest: any): string[] {
230
+ const errors: string[] = [];
231
+
232
+ // Check manifest.json
233
+ if (!fs.existsSync(path.join(distDir, 'manifest.json'))) {
234
+ errors.push('manifest.json is missing');
235
+ }
236
+
237
+ // Check server entry
238
+ if (manifest.serverEntry) {
239
+ const serverPath = path.join(distDir, manifest.serverEntry);
240
+ if (!fs.existsSync(serverPath)) {
241
+ errors.push(`Server entry not found: ${manifest.serverEntry}`);
242
+ }
243
+ }
244
+
245
+ // Check client entry
246
+ if (manifest.clientEntry) {
247
+ const clientPath = path.join(distDir, manifest.clientEntry);
248
+ if (!fs.existsSync(clientPath)) {
249
+ errors.push(`Client entry not found: ${manifest.clientEntry}`);
250
+ }
251
+ }
252
+
253
+ // Check for required fields
254
+ if (!manifest.name) {
255
+ errors.push('Plugin name is required');
256
+ }
257
+
258
+ if (!manifest.version) {
259
+ errors.push('Plugin version is required');
260
+ }
261
+
262
+ return errors;
263
+ }
264
+
265
+ /**
266
+ * Create ZIP archive
267
+ */
268
+ function createZipArchive(sourceDir: string, outputPath: string): Promise<void> {
269
+ return new Promise((resolve, reject) => {
270
+ const output = fs.createWriteStream(outputPath);
271
+ const archive = archiver('zip', {
272
+ zlib: { level: 9 } // Maximum compression
273
+ });
274
+
275
+ output.on('close', () => {
276
+ resolve();
277
+ });
278
+
279
+ archive.on('error', (err: Error) => {
280
+ reject(err);
281
+ });
282
+
283
+ archive.pipe(output);
284
+
285
+ // Add all files from dist directory
286
+ archive.directory(sourceDir, false, (entry: any) => {
287
+ // Exclude unnecessary files
288
+ if (entry.name.includes('node_modules') ||
289
+ entry.name.includes('.git') ||
290
+ entry.name.endsWith('.test.js') ||
291
+ entry.name.endsWith('.spec.js')) {
292
+ return false;
293
+ }
294
+ return entry;
295
+ });
296
+
297
+ archive.finalize();
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Format file size
303
+ */
304
+ function formatFileSize(bytes: number): string {
305
+ const units = ['B', 'KB', 'MB', 'GB'];
306
+ let size = bytes;
307
+ let unitIndex = 0;
308
+
309
+ while (size >= 1024 && unitIndex < units.length - 1) {
310
+ size /= 1024;
311
+ unitIndex++;
312
+ }
313
+
314
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
315
+ }
package/src/index.ts ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @linyjs/cli - Ling 框架命令行工具
5
+ *
6
+ * 提供插件打包、开发、发布等实用功能
7
+ *
8
+ * 使用 Cleye 作为 CLI 框架
9
+ * - 零配置 TypeScript
10
+ * - 自动类型推断
11
+ * - 简洁的函数式 API
12
+ */
13
+
14
+ import { cli } from 'cleye';
15
+ import { createRequire } from 'module';
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const packageJson = require('../package.json');
19
+
20
+ // Main CLI with subcommands
21
+ cli(
22
+ {
23
+ name: '@linyjs/cli',
24
+ version: packageJson.version,
25
+ },
26
+ async (argv: any) => {
27
+ // Get the command from argv._
28
+ const command = argv._[0];
29
+
30
+ switch (command) {
31
+ case 'pack':
32
+ const { packPlugin } = await import('./commands/pack.js');
33
+ await packPlugin({
34
+ output: argv.flags.output || '.',
35
+ name: argv.flags.name,
36
+ version: argv.flags.version,
37
+ build: argv.flags.build !== false,
38
+ manifest: argv.flags.manifest,
39
+ verbose: argv.flags.verbose || false
40
+ });
41
+ break;
42
+
43
+ case 'dev':
44
+ const { devPlugin } = await import('./commands/dev.js');
45
+ await devPlugin({
46
+ target: argv.flags.target,
47
+ verbose: argv.flags.verbose || false
48
+ });
49
+ break;
50
+
51
+ case 'init':
52
+ const { initPlugin } = await import('./commands/init.js');
53
+ await initPlugin({
54
+ name: argv._[1],
55
+ verbose: argv.flags.verbose || false
56
+ });
57
+ break;
58
+
59
+ default:
60
+ // Show help if no command or unknown command
61
+ console.log(`
62
+ 📦 @linyjs/cli v${packageJson.version}
63
+
64
+ Usage:
65
+ npx @linyjs/cli <command> [options]
66
+
67
+ Commands:
68
+ pack Pack a plugin project into .mpk file
69
+ dev Quick development workflow (pack + install + register)
70
+ init Create a new plugin project from template
71
+
72
+ Examples:
73
+ npx @linyjs/cli pack # Pack plugin
74
+ npx @linyjs/cli pack -o ./releases # Specify output directory
75
+ npx @linyjs/cli dev # Dev mode (pack + install)
76
+ npx @linyjs/cli dev --target /path # Specify extensions directory
77
+ npx @linyjs/cli init my-plugin # Create new plugin project
78
+
79
+ Run 'npx @linyjs/cli <command> --help' for more information.
80
+ `.trim());
81
+ break;
82
+ }
83
+ }
84
+ );