@solazah/solazah-cli 0.2.17 → 0.2.18

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/index.js DELETED
@@ -1,1108 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import path from "path";
4
- import fs from "fs-extra";
5
- import inquirer from "inquirer";
6
- import chalk from "chalk";
7
- import ora from "ora";
8
- import { exec, spawn } from "child_process";
9
- import { promisify } from "util";
10
- import validatePackageName from "validate-npm-package-name";
11
- import { fileURLToPath } from "url";
12
- import os from "os";
13
- import semver from "semver";
14
- import conventionalChangelog from "conventional-changelog-core";
15
- function validatePluginName(name) {
16
- const errors = [];
17
- if (!name) {
18
- errors.push("Plugin name is required");
19
- return { valid: false, errors };
20
- }
21
- const validation = validatePackageName(name);
22
- if (!validation.validForNewPackages) {
23
- if (validation.errors) {
24
- errors.push(...validation.errors);
25
- }
26
- if (validation.warnings) {
27
- errors.push(...validation.warnings);
28
- }
29
- }
30
- return {
31
- valid: errors.length === 0,
32
- errors
33
- };
34
- }
35
- function extractPluginName(packageName) {
36
- return packageName.replace(/^@[^/]+\//, "");
37
- }
38
- function extractPluginSimpleName(packageName) {
39
- let name = packageName.replace(/^@[^/]+\//, "");
40
- name = name.replace(/^plugin-/, "").replace(/^solazah-plugin-/, "");
41
- return name;
42
- }
43
- function generateDisplayName(name) {
44
- return name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
45
- }
46
- const __filename$1 = fileURLToPath(import.meta.url);
47
- const __dirname$1 = path.dirname(__filename$1);
48
- async function isDirectoryEmpty(dirPath) {
49
- try {
50
- const files = await fs.readdir(dirPath);
51
- return files.length === 0;
52
- } catch {
53
- return true;
54
- }
55
- }
56
- async function ensureDirectory(dirPath) {
57
- await fs.ensureDir(dirPath);
58
- }
59
- function getTemplatesRootDir() {
60
- return path.resolve(__dirname$1, "../templates");
61
- }
62
- function getTemplateDir(templateName) {
63
- return path.join(getTemplatesRootDir(), templateName);
64
- }
65
- async function getAvailableTemplates() {
66
- const templatesRoot = getTemplatesRootDir();
67
- const templates = [];
68
- try {
69
- const entries = await fs.readdir(templatesRoot, { withFileTypes: true });
70
- for (const entry of entries) {
71
- if (entry.isDirectory()) {
72
- const templateJsonPath = path.join(templatesRoot, entry.name, "template.json");
73
- if (await fs.pathExists(templateJsonPath)) {
74
- const templateInfo = await fs.readJson(templateJsonPath);
75
- templates.push(templateInfo);
76
- }
77
- }
78
- }
79
- } catch (error) {
80
- console.error("Failed to read templates:", error);
81
- }
82
- return templates;
83
- }
84
- async function copyTemplate(templateDir, targetDir) {
85
- await fs.copy(templateDir, targetDir, {
86
- filter: (src) => {
87
- const basename = path.basename(src);
88
- return !["node_modules", ".git", "dist", "target", "release"].includes(basename);
89
- }
90
- });
91
- }
92
- async function replacePlaceholders(filePath, replacements) {
93
- let content = await fs.readFile(filePath, "utf-8");
94
- for (const [key, value] of Object.entries(replacements)) {
95
- const placeholder = `{{${key}}}`;
96
- content = content.replaceAll(placeholder, value);
97
- content = content.replaceAll(key, value);
98
- }
99
- await fs.writeFile(filePath, content, "utf-8");
100
- }
101
- async function replaceInDirectory(dirPath, replacements, extensions = [".ts", ".tsx", ".js", ".jsx", ".json", ".html", ".md"]) {
102
- const files = await fs.readdir(dirPath, { withFileTypes: true });
103
- for (const file of files) {
104
- const filePath = path.join(dirPath, file.name);
105
- if (file.isDirectory()) {
106
- await replaceInDirectory(filePath, replacements, extensions);
107
- } else if (file.isFile()) {
108
- const ext = path.extname(file.name);
109
- if (extensions.includes(ext) || file.name === "manifest.json") {
110
- await replacePlaceholders(filePath, replacements);
111
- }
112
- }
113
- }
114
- }
115
- const execAsync$2 = promisify(exec);
116
- async function createPluginCommand(options) {
117
- console.log(chalk.cyan.bold("\n🚀 Solazah Plugin Creator\n"));
118
- try {
119
- const availableTemplates = await getAvailableTemplates();
120
- if (availableTemplates.length === 0) {
121
- console.error(chalk.red("❌ No templates found"));
122
- process.exit(1);
123
- }
124
- let selectedTemplate = options.template || "react";
125
- if (!options.template && availableTemplates.length > 1) {
126
- const { template } = await inquirer.prompt([
127
- {
128
- type: "list",
129
- name: "template",
130
- message: "Select a template:",
131
- choices: availableTemplates.map((t) => ({
132
- name: `${t.displayName} - ${t.description}`,
133
- value: t.name
134
- })),
135
- default: "react"
136
- }
137
- ]);
138
- selectedTemplate = template;
139
- }
140
- const templateInfo = availableTemplates.find((t) => t.name === selectedTemplate);
141
- if (!templateInfo) {
142
- console.error(chalk.red(`❌ Template "${selectedTemplate}" not found`));
143
- process.exit(1);
144
- }
145
- console.log(chalk.gray(`Using template: ${templateInfo.displayName}
146
- `));
147
- let pluginName = options.name;
148
- if (!pluginName) {
149
- const answers2 = await inquirer.prompt([
150
- {
151
- type: "input",
152
- name: "name",
153
- message: "Plugin name (e.g., my-plugin or @scope/plugin-name):",
154
- validate: (input) => {
155
- const result = validatePluginName(input);
156
- return result.valid || result.errors.join(", ");
157
- }
158
- }
159
- ]);
160
- pluginName = answers2.name;
161
- }
162
- const validation = validatePluginName(pluginName);
163
- if (!validation.valid) {
164
- console.error(chalk.red("❌ Invalid plugin name:"));
165
- validation.errors.forEach((error) => console.error(chalk.red(` - ${error}`)));
166
- process.exit(1);
167
- }
168
- const packageName = extractPluginName(pluginName);
169
- const simpleName = extractPluginSimpleName(pluginName);
170
- const defaultDisplayName = generateDisplayName(simpleName);
171
- const defaultTargetDir = options.dir ? path.resolve(options.dir, packageName) : path.resolve(".");
172
- const { targetDir: confirmedTargetDir } = await inquirer.prompt([
173
- {
174
- type: "input",
175
- name: "targetDir",
176
- message: "Target directory:",
177
- default: defaultTargetDir
178
- }
179
- ]);
180
- const targetDir = path.resolve(confirmedTargetDir);
181
- const answers = await inquirer.prompt([
182
- {
183
- type: "input",
184
- name: "displayName",
185
- message: "Display name:",
186
- default: defaultDisplayName
187
- },
188
- {
189
- type: "input",
190
- name: "description",
191
- message: "Description:",
192
- default: `A Solazah plugin - ${defaultDisplayName}`
193
- },
194
- {
195
- type: "input",
196
- name: "author",
197
- message: "Author:",
198
- default: ""
199
- },
200
- {
201
- type: "number",
202
- name: "port",
203
- message: "Development server port:",
204
- default: 5200
205
- }
206
- ]);
207
- const dirExists = await fs.pathExists(targetDir);
208
- if (dirExists) {
209
- const isEmpty = await isDirectoryEmpty(targetDir);
210
- if (!isEmpty) {
211
- const { overwrite } = await inquirer.prompt([
212
- {
213
- type: "confirm",
214
- name: "overwrite",
215
- message: `Directory ${chalk.cyan(targetDir)} is not empty. Overwrite?`,
216
- default: false
217
- }
218
- ]);
219
- if (!overwrite) {
220
- console.log(chalk.yellow("❌ Cancelled"));
221
- process.exit(0);
222
- }
223
- await fs.remove(targetDir);
224
- }
225
- }
226
- await ensureDirectory(targetDir);
227
- const spinner = ora("Copying template files...").start();
228
- const templateDir = getTemplateDir(selectedTemplate);
229
- if (!await fs.pathExists(templateDir)) {
230
- spinner.fail(chalk.red(`Template directory not found: ${templateDir}`));
231
- process.exit(1);
232
- }
233
- await copyTemplate(templateDir, targetDir);
234
- spinner.succeed("Template files copied");
235
- spinner.start("Updating files...");
236
- const replacements = {
237
- PLUGIN_NAME: pluginName,
238
- PLUGIN_DISPLAY_NAME: answers.displayName,
239
- PLUGIN_DESCRIPTION: answers.description,
240
- PLUGIN_AUTHOR: answers.author || "",
241
- PLUGIN_VERSION: "0.0.1",
242
- DEV_PORT: answers.port.toString(),
243
- PLUGIN_SIMPLE_NAME: simpleName,
244
- PLUGIN_CLASS_NAME: `solazah-${simpleName}`
245
- };
246
- await replaceInDirectory(targetDir, replacements);
247
- const packageJsonPath = path.join(targetDir, "package.json");
248
- const packageJson = await fs.readJson(packageJsonPath);
249
- packageJson.name = pluginName;
250
- packageJson.version = "0.0.1";
251
- packageJson.description = answers.description;
252
- if (answers.author) {
253
- packageJson.author = answers.author;
254
- }
255
- packageJson.scripts.dev = `vite --port ${answers.port}`;
256
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
257
- const manifestPath = path.join(targetDir, "manifest.json");
258
- const manifest = await fs.readJson(manifestPath);
259
- manifest.name = pluginName;
260
- manifest.displayName = answers.displayName;
261
- manifest.commands = [answers.displayName];
262
- manifest.development.main = `http://localhost:${answers.port}`;
263
- await fs.writeJson(manifestPath, manifest, { spaces: 2 });
264
- const filesToRemove = [
265
- "template.json"
266
- ];
267
- for (const file of filesToRemove) {
268
- const filePath = path.join(targetDir, file);
269
- if (await fs.pathExists(filePath)) {
270
- await fs.remove(filePath);
271
- }
272
- }
273
- spinner.succeed("Files updated");
274
- if (!options.skipInstall) {
275
- spinner.start("Installing dependencies...");
276
- try {
277
- await execAsync$2("npm install", { cwd: targetDir });
278
- spinner.succeed("Dependencies installed");
279
- } catch (error) {
280
- spinner.fail("Failed to install dependencies");
281
- console.log(chalk.yellow("You can install them manually by running: npm install"));
282
- }
283
- }
284
- console.log(chalk.green.bold("\n✅ Plugin created successfully!\n"));
285
- console.log(chalk.cyan("Next steps:"));
286
- const relativePath = path.relative(process.cwd(), targetDir);
287
- if (relativePath && relativePath !== ".") {
288
- console.log(chalk.gray(` cd ${relativePath}`));
289
- }
290
- if (options.skipInstall) {
291
- console.log(chalk.gray(" npm install"));
292
- }
293
- console.log(chalk.gray(" npm run dev"));
294
- console.log();
295
- } catch (error) {
296
- console.error(chalk.red("❌ Error:"), error);
297
- process.exit(1);
298
- }
299
- }
300
- const execAsync$1 = promisify(exec);
301
- function resolveFilesToCopy(projectDir, manifest) {
302
- const entries = [];
303
- entries.push("manifest.json");
304
- for (const doc of ["CHANGELOG.md", "README.md", "icon.png"]) {
305
- if (fs.existsSync(path.join(projectDir, doc))) {
306
- entries.push(doc);
307
- }
308
- }
309
- const main = manifest.main;
310
- if (main) {
311
- const mainDir = path.dirname(main);
312
- entries.push({ from: "dist", to: mainDir === "." ? "." : mainDir });
313
- } else {
314
- entries.push({ from: "dist", to: "." });
315
- }
316
- const preload = manifest.preload;
317
- if (preload) {
318
- const preloadDir = path.dirname(preload);
319
- const srcPreload = path.join(projectDir, "src", "preload");
320
- if (fs.existsSync(srcPreload)) {
321
- entries.push({ from: "src/preload", to: preloadDir });
322
- }
323
- }
324
- const srcSkills = path.join(projectDir, "src", "skills");
325
- if (fs.existsSync(srcSkills)) {
326
- entries.push({ from: "src/skills", to: "skills" });
327
- }
328
- const srcScreenshots = path.join(projectDir, "src", "screenshots");
329
- if (fs.existsSync(srcScreenshots)) {
330
- entries.push({ from: "src/screenshots", to: "screenshots" });
331
- }
332
- return entries;
333
- }
334
- function cleanReleaseDir(releaseDir) {
335
- if (fs.existsSync(releaseDir)) {
336
- fs.rmSync(releaseDir, { recursive: true, force: true });
337
- }
338
- fs.mkdirSync(releaseDir, { recursive: true });
339
- }
340
- function copyFiles(projectDir, releaseDir, files) {
341
- for (const file of files) {
342
- if (typeof file === "string") {
343
- const src = path.resolve(projectDir, file);
344
- const dest = path.resolve(releaseDir, file);
345
- fs.cpSync(src, dest, { recursive: true });
346
- } else {
347
- const src = path.resolve(projectDir, file.from);
348
- const dest = path.resolve(releaseDir, file.to);
349
- fs.cpSync(src, dest, { recursive: true });
350
- }
351
- }
352
- }
353
- function processPackageJson(projectDir, releaseDir) {
354
- const raw = fs.readFileSync(path.resolve(projectDir, "package.json"), "utf-8");
355
- const pkg = JSON.parse(raw);
356
- const cleaned = {
357
- name: pkg.name,
358
- main: pkg.main,
359
- type: pkg.type,
360
- version: pkg.version,
361
- description: pkg.description,
362
- author: pkg.author,
363
- license: pkg.license,
364
- homepage: pkg.homepage,
365
- repository: pkg.repository,
366
- keywords: pkg.keywords,
367
- peerDependencies: pkg.peerDependencies
368
- };
369
- fs.writeFileSync(
370
- path.resolve(releaseDir, "package.json"),
371
- JSON.stringify(cleaned, null, 2)
372
- );
373
- }
374
- function cleanDistDir(projectDir) {
375
- const distDir = path.join(projectDir, "dist");
376
- if (!fs.existsSync(distDir)) return;
377
- const maxRetries = 5;
378
- const retryDelayMs = 200;
379
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
380
- try {
381
- fs.rmSync(distDir, { recursive: true, force: true });
382
- return;
383
- } catch (err) {
384
- if (attempt === maxRetries) throw err;
385
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, retryDelayMs);
386
- }
387
- }
388
- }
389
- async function packagePluginCommand(options) {
390
- const projectDir = path.resolve(options.dir || ".");
391
- console.log(chalk.cyan.bold("\n📦 Solazah Plugin Packager\n"));
392
- const packageJsonPath = path.join(projectDir, "package.json");
393
- if (!await fs.pathExists(packageJsonPath)) {
394
- console.error(chalk.red("❌ No package.json found in the target directory."));
395
- process.exit(1);
396
- }
397
- const manifestPath = path.join(projectDir, "manifest.json");
398
- if (!await fs.pathExists(manifestPath)) {
399
- console.error(chalk.red("❌ No manifest.json found. Is this a Solazah plugin project?"));
400
- process.exit(1);
401
- }
402
- const packageData = await fs.readJson(packageJsonPath);
403
- const manifest = await fs.readJson(manifestPath);
404
- console.log(chalk.gray(`Plugin: ${packageData.name || "unknown"} v${packageData.version || "0.0.0"}
405
- `));
406
- const buildSpinner = ora("Building plugin...").start();
407
- try {
408
- await execAsync$1("npm run build", { cwd: projectDir });
409
- buildSpinner.succeed("Build completed");
410
- } catch (error) {
411
- buildSpinner.fail("Build failed");
412
- console.error(chalk.red(error.stderr || error.message));
413
- process.exit(1);
414
- }
415
- const packageSpinner = ora("Packaging plugin...").start();
416
- try {
417
- const releaseDir = path.join(projectDir, "release");
418
- const filesToCopy = resolveFilesToCopy(projectDir, manifest);
419
- cleanReleaseDir(releaseDir);
420
- copyFiles(projectDir, releaseDir, filesToCopy);
421
- processPackageJson(projectDir, releaseDir);
422
- cleanDistDir(projectDir);
423
- packageSpinner.succeed("Package created in release/");
424
- } catch (error) {
425
- packageSpinner.fail("Packaging failed");
426
- console.error(chalk.red(error.message));
427
- process.exit(1);
428
- }
429
- const tgzSpinner = ora("Creating .tgz archive...").start();
430
- try {
431
- const { stdout } = await execAsync$1("npm pack ./release --pack-destination ./release", { cwd: projectDir });
432
- const tgzFile = stdout.trim();
433
- tgzSpinner.succeed(`Archive created: ${chalk.cyan("release/" + tgzFile)}`);
434
- } catch (error) {
435
- tgzSpinner.fail("Failed to create .tgz archive");
436
- console.error(chalk.red(error.stderr || error.message));
437
- process.exit(1);
438
- }
439
- console.log(chalk.green.bold("\n✅ Plugin packaged successfully!\n"));
440
- }
441
- const AUTH_DIR = path.join(os.homedir(), ".solazah");
442
- const AUTH_FILE = path.join(AUTH_DIR, "auth.json");
443
- const EXPIRY_SKEW_SECONDS = 30;
444
- async function saveTokens(tokens) {
445
- await fs.ensureDir(AUTH_DIR);
446
- await fs.writeJson(AUTH_FILE, tokens, { spaces: 2 });
447
- try {
448
- await fs.chmod(AUTH_FILE, 384);
449
- } catch {
450
- }
451
- }
452
- async function loadTokens() {
453
- try {
454
- return await fs.readJson(AUTH_FILE);
455
- } catch (err) {
456
- if (err.code === "ENOENT") {
457
- return null;
458
- }
459
- throw err;
460
- }
461
- }
462
- async function clearTokens() {
463
- try {
464
- await fs.remove(AUTH_FILE);
465
- } catch {
466
- }
467
- }
468
- function isExpired(tokens) {
469
- if (!tokens.expires_at) return false;
470
- return Math.floor(Date.now() / 1e3) >= tokens.expires_at - EXPIRY_SKEW_SECONDS;
471
- }
472
- function toStoredTokens(tokens, issuer) {
473
- const now = Math.floor(Date.now() / 1e3);
474
- return {
475
- access_token: tokens.access_token,
476
- refresh_token: tokens.refresh_token,
477
- id_token: tokens.id_token,
478
- token_type: tokens.token_type,
479
- scope: tokens.scope,
480
- expires_at: tokens.expires_in ? now + tokens.expires_in : void 0,
481
- issuer
482
- };
483
- }
484
- function mergeRefreshedTokens(previous, refreshed, issuer) {
485
- const next = toStoredTokens(refreshed, issuer);
486
- if (!next.refresh_token) {
487
- next.refresh_token = previous.refresh_token;
488
- }
489
- return next;
490
- }
491
- const DEFAULT_ISSUER = "https://solazah.seayona.com/account";
492
- const DEFAULT_CLIENT_ID = "solazah-cli";
493
- const DEFAULT_CLIENT_SECRET = "solazah-cli";
494
- const DEFAULT_SCOPE = "openid profile offline_access";
495
- const DEFAULT_TOKEN_TYPE = "Bearer";
496
- const DEFAULT_VERIFICATION_URI = "https://solazah.seayona.com/account/#/device";
497
- const GRANT_TYPE = {
498
- DEVICE_CODE: "urn:ietf:params:oauth:grant-type:device_code",
499
- REFRESH_TOKEN: "refresh_token"
500
- };
501
- const TOKEN_HINT = {
502
- ACCESS: "access_token",
503
- REFRESH: "refresh_token"
504
- };
505
- const DEVICE_ERROR = {
506
- AUTH_PENDING: "authorization_pending",
507
- SLOW_DOWN: "slow_down",
508
- ACCESS_DENIED: "access_denied",
509
- EXPIRED_TOKEN: "expired_token"
510
- };
511
- async function discover(issuer = DEFAULT_ISSUER) {
512
- const url = `${issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`;
513
- const res = await fetch(url);
514
- if (!res.ok) {
515
- throw new Error(`Failed to load OIDC discovery (${res.status}): ${url}`);
516
- }
517
- return await res.json();
518
- }
519
- async function postForm(url, params) {
520
- const body = new URLSearchParams(params);
521
- const res = await fetch(url, {
522
- method: "POST",
523
- headers: {
524
- "Content-Type": "application/x-www-form-urlencoded",
525
- Accept: "application/json"
526
- },
527
- body: body.toString(),
528
- redirect: "manual"
529
- });
530
- if (res.status >= 300 && res.status < 400) {
531
- const location = res.headers.get("location") ?? "(unknown)";
532
- throw new Error(
533
- `服务器返回了重定向 (${res.status}) → ${location},请确认客户端 client_id 已在授权服务器注册为公共客户端`
534
- );
535
- }
536
- const text = await res.text();
537
- let data;
538
- try {
539
- data = JSON.parse(text);
540
- } catch {
541
- throw new Error(
542
- `服务器返回了非 JSON 响应 (${res.status}): ${text.slice(0, 200)}`
543
- );
544
- }
545
- return { ok: res.ok, status: res.status, data };
546
- }
547
- function str(v) {
548
- return typeof v === "string" ? v : void 0;
549
- }
550
- function num(v) {
551
- return typeof v === "number" ? v : void 0;
552
- }
553
- async function requestDeviceCode(options) {
554
- const { discovery } = options;
555
- if (!discovery.device_authorization_endpoint) {
556
- throw new Error("授权服务器未提供 device_authorization_endpoint");
557
- }
558
- const { ok, status, data } = await postForm(discovery.device_authorization_endpoint, {
559
- client_id: options.clientId ?? DEFAULT_CLIENT_ID,
560
- client_secret: DEFAULT_CLIENT_SECRET,
561
- scope: options.scope ?? DEFAULT_SCOPE
562
- });
563
- if (!ok) {
564
- throw new Error(`设备授权请求失败 (${status}): ${JSON.stringify(data)}`);
565
- }
566
- const deviceCode = str(data.device_code) ?? str(data.deviceCode);
567
- const userCode = str(data.user_code) ?? str(data.userCode);
568
- const expiresIn = num(data.expires_in) ?? num(data.expiresIn);
569
- const interval = num(data.interval);
570
- if (!deviceCode || !userCode || expiresIn == null) {
571
- throw new Error(
572
- `设备授权响应缺少必要字段(device_code/user_code/expires_in),实际响应:${JSON.stringify(data)}`
573
- );
574
- }
575
- const verificationUri = DEFAULT_VERIFICATION_URI;
576
- const verificationUriComplete = `${verificationUri}?user_code=${encodeURIComponent(userCode)}`;
577
- return {
578
- device_code: deviceCode,
579
- user_code: userCode,
580
- verification_uri: verificationUri,
581
- verification_uri_complete: verificationUriComplete,
582
- expires_in: expiresIn,
583
- interval
584
- };
585
- }
586
- async function pollToken(options) {
587
- const { ok, data } = await postForm(options.discovery.token_endpoint, {
588
- grant_type: GRANT_TYPE.DEVICE_CODE,
589
- device_code: options.deviceCode,
590
- client_id: options.clientId ?? DEFAULT_CLIENT_ID,
591
- client_secret: DEFAULT_CLIENT_SECRET
592
- });
593
- if (ok && typeof data.access_token === "string") {
594
- return { status: "success", tokens: tokenResponseFrom(data) };
595
- }
596
- const err = typeof data.error === "string" ? data.error : "error";
597
- const errorDescription = typeof data.error_description === "string" ? data.error_description : void 0;
598
- const status = err === DEVICE_ERROR.AUTH_PENDING ? "pending" : err === DEVICE_ERROR.SLOW_DOWN ? "slow_down" : err === DEVICE_ERROR.ACCESS_DENIED ? "denied" : err === DEVICE_ERROR.EXPIRED_TOKEN ? "expired" : "error";
599
- return { status, error: err, errorDescription };
600
- }
601
- async function fetchUserInfo(options) {
602
- if (!options.discovery.userinfo_endpoint) {
603
- throw new Error("授权服务器未提供 userinfo_endpoint");
604
- }
605
- const res = await fetch(options.discovery.userinfo_endpoint, {
606
- headers: {
607
- Authorization: `Bearer ${options.accessToken}`,
608
- Accept: "application/json"
609
- }
610
- });
611
- if (!res.ok) {
612
- const text = await res.text();
613
- throw new Error(`获取用户信息失败 (${res.status}): ${text}`);
614
- }
615
- return await res.json();
616
- }
617
- async function refreshTokens(options) {
618
- const { ok, status, data } = await postForm(options.discovery.token_endpoint, {
619
- grant_type: GRANT_TYPE.REFRESH_TOKEN,
620
- refresh_token: options.refreshToken,
621
- client_id: options.clientId ?? DEFAULT_CLIENT_ID,
622
- client_secret: DEFAULT_CLIENT_SECRET
623
- });
624
- if (!ok) {
625
- throw new Error(`刷新令牌失败 (${status}): ${JSON.stringify(data)}`);
626
- }
627
- return tokenResponseFrom(data);
628
- }
629
- async function revokeToken(options) {
630
- if (!options.discovery.revocation_endpoint) {
631
- return;
632
- }
633
- const params = {
634
- token: options.token,
635
- client_id: options.clientId ?? DEFAULT_CLIENT_ID,
636
- client_secret: DEFAULT_CLIENT_SECRET
637
- };
638
- if (options.tokenTypeHint) {
639
- params.token_type_hint = options.tokenTypeHint;
640
- }
641
- await postForm(options.discovery.revocation_endpoint, params).catch(() => {
642
- });
643
- }
644
- async function revokeAll(options) {
645
- if (!options.discovery.revocation_endpoint) return;
646
- const jobs = [];
647
- if (options.refreshToken) {
648
- jobs.push(revokeToken({
649
- discovery: options.discovery,
650
- token: options.refreshToken,
651
- tokenTypeHint: TOKEN_HINT.REFRESH,
652
- clientId: options.clientId
653
- }));
654
- }
655
- if (options.accessToken) {
656
- jobs.push(revokeToken({
657
- discovery: options.discovery,
658
- token: options.accessToken,
659
- tokenTypeHint: TOKEN_HINT.ACCESS,
660
- clientId: options.clientId
661
- }));
662
- }
663
- await Promise.all(jobs);
664
- }
665
- function tokenResponseFrom(data) {
666
- return {
667
- access_token: String(data.access_token ?? ""),
668
- refresh_token: typeof data.refresh_token === "string" ? data.refresh_token : void 0,
669
- id_token: typeof data.id_token === "string" ? data.id_token : void 0,
670
- token_type: typeof data.token_type === "string" ? data.token_type : DEFAULT_TOKEN_TYPE,
671
- expires_in: typeof data.expires_in === "number" ? data.expires_in : void 0,
672
- scope: typeof data.scope === "string" ? data.scope : void 0
673
- };
674
- }
675
- const MARKETPLACE_BASE_URL = "https://solazah.seayona.com/marketplace";
676
- async function ensureAuth() {
677
- const tokens = await loadTokens();
678
- if (!tokens) {
679
- console.log(chalk.red("当前未登录,请先执行:solazah-cli account login"));
680
- process.exit(1);
681
- }
682
- if (isExpired(tokens) && tokens.refresh_token) {
683
- const spinner = ora("令牌已过期,正在刷新...").start();
684
- try {
685
- const discovery = await discover(tokens.issuer);
686
- const refreshed = await refreshTokens({ discovery, refreshToken: tokens.refresh_token });
687
- const updated = mergeRefreshedTokens(tokens, refreshed, discovery.issuer);
688
- await saveTokens(updated);
689
- spinner.succeed("令牌刷新成功");
690
- return updated;
691
- } catch {
692
- spinner.fail("令牌刷新失败,请重新登录:solazah-cli account login");
693
- process.exit(1);
694
- }
695
- }
696
- return tokens;
697
- }
698
- function findTgzByManifest(projectDir) {
699
- const manifestPath = path.join(projectDir, "manifest.json");
700
- if (!fs.existsSync(manifestPath)) {
701
- console.log(chalk.red("❌ 未找到 manifest.json,无法确定插件包文件名"));
702
- process.exit(1);
703
- }
704
- const manifest = fs.readJsonSync(manifestPath);
705
- const name = manifest.name;
706
- const version = manifest.version;
707
- if (!name || !version) {
708
- console.log(chalk.red("❌ manifest.json 中缺少 name 或 version 字段"));
709
- process.exit(1);
710
- }
711
- const tgzName = name.replace(/^@/, "").replace(/\//, "-");
712
- const tgzFile = `${tgzName}-${version}.tgz`;
713
- return path.join(projectDir, "release", tgzFile);
714
- }
715
- async function publishPluginCommand(options) {
716
- let filePath;
717
- if (options.file) {
718
- filePath = path.resolve(options.file);
719
- } else {
720
- const projectDir = path.resolve(options.dir || ".");
721
- filePath = findTgzByManifest(projectDir);
722
- }
723
- if (!await fs.pathExists(filePath)) {
724
- console.log(chalk.red(`文件不存在:${filePath}`));
725
- process.exit(1);
726
- }
727
- if (!filePath.endsWith(".tgz")) {
728
- console.log(chalk.red("仅支持 .tgz 格式的插件包"));
729
- process.exit(1);
730
- }
731
- const tokens = await ensureAuth();
732
- const fileName = path.basename(filePath);
733
- const spinner = ora(`正在发布插件包:${fileName}`).start();
734
- try {
735
- const fileBuffer = await fs.readFile(filePath);
736
- const blob = new Blob([fileBuffer], { type: "application/gzip" });
737
- const formData = new FormData();
738
- formData.append("package", blob, fileName);
739
- const res = await fetch(`${MARKETPLACE_BASE_URL}/api/v1/publish-requests`, {
740
- method: "POST",
741
- headers: {
742
- Authorization: `Bearer ${tokens.access_token}`
743
- },
744
- body: formData
745
- });
746
- const data = await res.json().catch(() => ({}));
747
- if (!res.ok) {
748
- const code = data.code ?? "";
749
- const message = data.message ?? res.statusText;
750
- if (code === "DUPLICATE_SUBMISSION") {
751
- spinner.fail("发布失败:该版本已提交过发布申请,请勿重复提交");
752
- } else {
753
- spinner.fail(`发布失败 (${res.status}): ${message}`);
754
- }
755
- process.exit(1);
756
- }
757
- const result = data;
758
- spinner.succeed("插件发布申请已提交");
759
- console.log("");
760
- console.log(chalk.green(" 申请详情:"));
761
- console.log(` 申请ID: ${result.pluginPublishRequestId}`);
762
- console.log(` 插件名称: ${result.pluginName}`);
763
- console.log(` 状态: ${result.status}`);
764
- console.log("");
765
- console.log(chalk.gray(" 发布申请已提交,等待管理员审核。"));
766
- } catch (err) {
767
- spinner.fail(`发布失败:${err.message}`);
768
- process.exit(1);
769
- }
770
- }
771
- const execAsync = promisify(exec);
772
- function loadVersionrc(projectDir) {
773
- for (const name of [".versionrc", ".versionrc.json"]) {
774
- const p = path.join(projectDir, name);
775
- if (fs.existsSync(p)) {
776
- try {
777
- return fs.readJsonSync(p);
778
- } catch {
779
- }
780
- }
781
- }
782
- return {};
783
- }
784
- function buildChangelogConfig(versionrc) {
785
- const types = versionrc.types;
786
- if (!types) return {};
787
- const parserOpts = {
788
- noteKeywords: ["BREAKING CHANGE", "BREAKING-CHANGE"]
789
- };
790
- const writerOpts = {
791
- transform: (commit) => {
792
- const match = types.find((t) => t.type === commit.type);
793
- if (!match) return false;
794
- if (match.hidden) return false;
795
- commit.type = match.section ?? commit.type;
796
- return commit;
797
- },
798
- groupBy: "type",
799
- commitGroupsSort: "title",
800
- commitsSort: ["scope", "subject"]
801
- };
802
- return { parserOpts, writerOpts };
803
- }
804
- async function generateChangelogEntry(projectDir, newVersion, versionrc) {
805
- return new Promise((resolve, reject) => {
806
- const { parserOpts, writerOpts } = buildChangelogConfig(versionrc);
807
- versionrc.header ?? "# CHANGELOG\n\n";
808
- const stream = conventionalChangelog(
809
- { preset: "angular", parserOpts, writerOpts },
810
- { version: newVersion },
811
- void 0,
812
- void 0,
813
- void 0
814
- );
815
- let content = "";
816
- stream.on("data", (chunk) => {
817
- content += chunk.toString();
818
- });
819
- stream.on("end", () => resolve(content));
820
- stream.on("error", reject);
821
- stream.cwd = projectDir;
822
- });
823
- }
824
- function bumpVersionInFile(filePath, newVersion) {
825
- if (!fs.existsSync(filePath)) return;
826
- const content = fs.readFileSync(filePath, "utf-8");
827
- const updated = content.replace(
828
- /^(\s*"version"\s*:\s*)"[^"]*"/m,
829
- `$1"${newVersion}"`
830
- );
831
- fs.writeFileSync(filePath, updated, "utf-8");
832
- }
833
- function prependChangelog(projectDir, entry, header) {
834
- const changelogPath = path.join(projectDir, "CHANGELOG.md");
835
- const existing = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, "utf-8") : "";
836
- const body = existing.startsWith(header.trim()) ? existing.slice(header.trim().length).trimStart() : existing;
837
- fs.writeFileSync(changelogPath, header.trim() + "\n\n" + entry + (body ? "\n" + body : ""), "utf-8");
838
- }
839
- async function versionPluginCommand(options) {
840
- const projectDir = path.resolve(options.dir || ".");
841
- const packageJsonPath = path.join(projectDir, "package.json");
842
- const manifestPath = path.join(projectDir, "manifest.json");
843
- if (!await fs.pathExists(packageJsonPath)) {
844
- console.error(chalk.red("❌ No package.json found."));
845
- process.exit(1);
846
- }
847
- const pkg = await fs.readJson(packageJsonPath);
848
- const currentVersion = pkg.version ?? "0.0.0";
849
- let releaseType = options.releaseAs || "patch";
850
- const newVersion = semver.inc(currentVersion, releaseType);
851
- if (!newVersion) {
852
- console.error(chalk.red(`❌ Invalid version: ${currentVersion} + ${releaseType}`));
853
- process.exit(1);
854
- }
855
- const versionrc = loadVersionrc(projectDir);
856
- const header = versionrc.header ?? "# CHANGELOG\n\n";
857
- console.log(chalk.gray(`
858
- ${currentVersion} → ${chalk.green(newVersion)}
859
- `));
860
- const changelogSpinner = ora("Generating changelog...").start();
861
- let changelogEntry = "";
862
- try {
863
- const prevCwd = process.cwd();
864
- process.chdir(projectDir);
865
- changelogEntry = await generateChangelogEntry(projectDir, newVersion, versionrc);
866
- process.chdir(prevCwd);
867
- changelogSpinner.succeed("Changelog generated");
868
- } catch (err) {
869
- changelogSpinner.warn(`Changelog generation skipped: ${err.message}`);
870
- }
871
- const bumpSpinner = ora("Bumping version files...").start();
872
- bumpVersionInFile(packageJsonPath, newVersion);
873
- bumpVersionInFile(path.join(projectDir, "package-lock.json"), newVersion);
874
- bumpVersionInFile(manifestPath, newVersion);
875
- bumpSpinner.succeed("Version files updated");
876
- if (changelogEntry.trim()) {
877
- prependChangelog(projectDir, changelogEntry.trim(), header);
878
- ora("").succeed("CHANGELOG.md updated");
879
- }
880
- const gitSpinner = ora("Creating git commit and tag...").start();
881
- try {
882
- const filesToStage = ["package.json", "manifest.json", "CHANGELOG.md", "package-lock.json"].filter((f) => fs.existsSync(path.join(projectDir, f)));
883
- await execAsync(`git add ${filesToStage.join(" ")}`, { cwd: projectDir });
884
- await execAsync(`git commit -m "chore(release): ${newVersion}"`, { cwd: projectDir });
885
- try {
886
- await execAsync(`git tag -a v${newVersion} -m "chore(release): ${newVersion}"`, { cwd: projectDir });
887
- } catch (tagErr) {
888
- if (!tagErr.message?.includes("already exists")) throw tagErr;
889
- gitSpinner.warn(`Tag v${newVersion} already exists, skipping`);
890
- return;
891
- }
892
- gitSpinner.succeed(`Tagged release v${newVersion}`);
893
- } catch (err) {
894
- gitSpinner.fail(`Git operations failed: ${err.stderr || err.message}`);
895
- process.exit(1);
896
- }
897
- console.log(chalk.green.bold(`
898
- ✅ Version bumped to ${newVersion}
899
- `));
900
- }
901
- async function releasePluginCommand(options) {
902
- const projectDir = path.resolve(options.dir || ".");
903
- if (!await fs.pathExists(path.join(projectDir, "package.json"))) {
904
- console.error(chalk.red("❌ No package.json found in the target directory."));
905
- process.exit(1);
906
- }
907
- await versionPluginCommand({ dir: projectDir });
908
- await packagePluginCommand({ dir: projectDir });
909
- await publishPluginCommand({ dir: projectDir });
910
- }
911
- function openBrowser(url) {
912
- try {
913
- const platform = process.platform;
914
- let command;
915
- let args;
916
- if (platform === "win32") {
917
- command = "cmd";
918
- args = ["/c", "start", '""', url.replace(/&/g, "^&")];
919
- } else if (platform === "darwin") {
920
- command = "open";
921
- args = [url];
922
- } else {
923
- command = "xdg-open";
924
- args = [url];
925
- }
926
- const child = spawn(command, args, {
927
- detached: true,
928
- stdio: "ignore",
929
- shell: false
930
- });
931
- child.on("error", () => {
932
- });
933
- child.unref();
934
- return true;
935
- } catch {
936
- return false;
937
- }
938
- }
939
- const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
940
- function loadDiscovery(issuer) {
941
- return discover(issuer || DEFAULT_ISSUER);
942
- }
943
- async function ensureValidTokens(discovery, tokens) {
944
- if (!isExpired(tokens)) {
945
- return tokens;
946
- }
947
- if (!tokens.refresh_token) {
948
- throw new Error("访问令牌已过期且无 refresh_token,请重新登录");
949
- }
950
- const refreshed = await refreshTokens({
951
- discovery,
952
- refreshToken: tokens.refresh_token
953
- });
954
- const stored = mergeRefreshedTokens(tokens, refreshed, discovery.issuer);
955
- await saveTokens(stored);
956
- return stored;
957
- }
958
- async function loginCommand(options) {
959
- console.log(chalk.cyan.bold("\nSolazah 账户登录\n"));
960
- const spinner = ora("加载授权服务器元数据...").start();
961
- let discovery;
962
- try {
963
- discovery = await loadDiscovery(options.issuer);
964
- spinner.succeed("已加载授权服务器元数据");
965
- } catch (err) {
966
- spinner.fail("加载授权服务器元数据失败");
967
- console.error(chalk.red(err.message));
968
- process.exit(1);
969
- }
970
- spinner.start("申请设备授权码...");
971
- let device;
972
- try {
973
- device = await requestDeviceCode({ discovery });
974
- spinner.succeed("已获取设备授权码");
975
- } catch (err) {
976
- spinner.fail("申请设备授权码失败");
977
- console.error(chalk.red(err.message));
978
- process.exit(1);
979
- }
980
- const verificationUri = device.verification_uri_complete || device.verification_uri;
981
- console.log();
982
- console.log(chalk.bold("请在浏览器中完成授权:"));
983
- if (device.verification_uri_complete) {
984
- console.log(` 一键链接:${chalk.cyan(device.verification_uri_complete)}`);
985
- }
986
- console.log(` 用户码 :${chalk.yellow.bold(device.user_code)}`);
987
- console.log(` 有效期 :${device.expires_in} 秒`);
988
- console.log();
989
- const opened = openBrowser(verificationUri);
990
- console.log(chalk.gray(
991
- opened ? "已尝试为你打开浏览器,若未自动打开请手动访问上面的一键链接。" : "请手动在浏览器中打开上面的一键链接。"
992
- ));
993
- console.log();
994
- let interval = Math.max(device.interval ?? 5, 5);
995
- const deadline = Date.now() + device.expires_in * 1e3;
996
- const poll = ora("等待用户授权...").start();
997
- while (Date.now() < deadline) {
998
- await delay(interval * 1e3);
999
- let result;
1000
- try {
1001
- result = await pollToken({ discovery, deviceCode: device.device_code });
1002
- } catch (err) {
1003
- poll.text = `轮询出错,将重试:${err.message}`;
1004
- continue;
1005
- }
1006
- if (result.status === "success" && result.tokens) {
1007
- const stored = toStoredTokens(result.tokens, discovery.issuer);
1008
- await saveTokens(stored);
1009
- poll.succeed("授权成功");
1010
- if (discovery.userinfo_endpoint) {
1011
- try {
1012
- const info = await fetchUserInfo({
1013
- discovery,
1014
- accessToken: stored.access_token
1015
- });
1016
- console.log();
1017
- console.log(chalk.green("已登录:"));
1018
- console.log(` sub :${info.sub}`);
1019
- const username = info.username ?? info.preferred_username;
1020
- if (username) console.log(` username:${username}`);
1021
- if (info.name) console.log(` name :${info.name}`);
1022
- if (info.email) console.log(` email :${info.email}`);
1023
- } catch {
1024
- }
1025
- }
1026
- console.log();
1027
- return;
1028
- }
1029
- if (result.status === "pending") continue;
1030
- if (result.status === "slow_down") {
1031
- interval += 5;
1032
- poll.text = `服务器要求降低轮询频率(${interval}s)...`;
1033
- continue;
1034
- }
1035
- if (result.status === "denied") {
1036
- poll.fail("用户拒绝了授权");
1037
- process.exit(1);
1038
- }
1039
- if (result.status === "expired") {
1040
- poll.fail("设备码已过期,请重新执行登录命令");
1041
- process.exit(1);
1042
- }
1043
- poll.text = `授权失败(将重试):${result.error}${result.errorDescription ? " - " + result.errorDescription : ""}`;
1044
- }
1045
- poll.fail("等待授权超时,请重新执行登录命令");
1046
- process.exit(1);
1047
- }
1048
- async function logoutCommand(options) {
1049
- const tokens = await loadTokens();
1050
- if (!tokens) {
1051
- console.log(chalk.yellow("当前未登录。"));
1052
- return;
1053
- }
1054
- const spinner = ora("正在登出...").start();
1055
- try {
1056
- const discovery = await loadDiscovery(tokens.issuer || options.issuer).catch(() => null);
1057
- if (discovery) {
1058
- await revokeAll({
1059
- discovery,
1060
- accessToken: tokens.access_token,
1061
- refreshToken: tokens.refresh_token
1062
- });
1063
- }
1064
- await clearTokens();
1065
- spinner.succeed("已登出");
1066
- } catch (err) {
1067
- await clearTokens();
1068
- spinner.warn("本地凭证已清除,但撤销令牌时发生错误");
1069
- console.error(chalk.gray(err.message));
1070
- }
1071
- }
1072
- async function userinfoCommand(options) {
1073
- const tokens = await loadTokens();
1074
- if (!tokens) {
1075
- console.log(chalk.yellow("当前未登录,请先执行:solazah-cli account login"));
1076
- process.exit(1);
1077
- }
1078
- try {
1079
- const discovery = await loadDiscovery(tokens.issuer || options.issuer);
1080
- const valid = await ensureValidTokens(discovery, tokens);
1081
- const info = await fetchUserInfo({
1082
- discovery,
1083
- accessToken: valid.access_token
1084
- });
1085
- console.log(JSON.stringify(info, null, 2));
1086
- } catch (err) {
1087
- console.error(chalk.red("获取用户信息失败:"), err.message);
1088
- process.exit(1);
1089
- }
1090
- }
1091
- const program = new Command();
1092
- program.name("solazah-cli").description("Solazah CLI - Command line tool for Solazah plugin development").version("0.0.1");
1093
- const plugin = program.command("plugin").description("Manage Solazah plugins");
1094
- plugin.command("create").description("Create a new Solazah plugin").option("-n, --name <name>", "Plugin name (e.g., my-plugin or @scope/plugin-name)").option("-d, --dir <directory>", "Target directory", ".").option("-t, --template <template>", "Template to use (e.g., react)", "react").option("--skip-install", "Skip npm install").action(createPluginCommand);
1095
- plugin.command("package").description("Build and package a Solazah plugin for distribution").option("-d, --dir <directory>", "Plugin project directory", ".").action(packagePluginCommand);
1096
- plugin.command("publish").description("Publish a plugin package to Solazah marketplace").option("-f, --file <file>", "Path to the .tgz plugin package file").option("-d, --dir <directory>", "Plugin project directory", ".").action(publishPluginCommand);
1097
- plugin.command("version").description("Bump plugin version, update CHANGELOG and create git tag").option("-d, --dir <directory>", "Plugin project directory", ".").option("-r, --release-as <type>", "Specify release type: major, minor, patch").action(versionPluginCommand);
1098
- plugin.command("release").description("Bump version, package and publish a plugin in one step").option("-d, --dir <directory>", "Plugin project directory", ".").action(releasePluginCommand);
1099
- const account = program.command("account").description("Manage Solazah account authentication");
1100
- const withIssuer = (cmd) => cmd.option("--issuer <issuer>", "OAuth2 issuer URL");
1101
- withIssuer(account.command("login")).description("Login via OAuth2 device code flow").action(loginCommand);
1102
- withIssuer(account.command("logout")).description("Logout and revoke stored tokens").action(logoutCommand);
1103
- withIssuer(account.command("userinfo")).description("Show current user info").action(userinfoCommand);
1104
- program.parseAsync().catch((err) => {
1105
- console.error(err);
1106
- process.exit(1);
1107
- });
1108
- //# sourceMappingURL=index.js.map