specline 1.3.1 → 1.3.3

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/cli.mjs CHANGED
@@ -5,6 +5,8 @@ import { join, dirname, resolve, relative, basename } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { createHash } from 'crypto';
7
7
  import { get } from 'https';
8
+ import { execSync, spawnSync } from 'child_process';
9
+ import { createInterface } from 'readline/promises';
8
10
 
9
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
12
  const TEMPLATES_DIR = join(__dirname, 'templates');
@@ -347,10 +349,10 @@ function cmd_init(targetPath) {
347
349
  process.exit(1);
348
350
  }
349
351
 
350
- const configFile = join(target, '.specline-config.yaml');
352
+ const lockFile = join(target, 'specline', '.specline-lock.yaml');
351
353
  const forceMode = process.argv.includes('--force') || process.argv.includes('-f');
352
354
 
353
- if (existsSync(configFile) && !forceMode) {
355
+ if (existsSync(lockFile) && !forceMode) {
354
356
  warn('Specline 已在此项目中初始化。使用 --force 强制覆盖。');
355
357
  process.exit(0);
356
358
  }
@@ -406,13 +408,6 @@ function cmd_init(targetPath) {
406
408
  const skillsCount = countFiles(join(target, '.cursor', 'skills'));
407
409
  const hooksCount = countFiles(join(target, '.cursor', 'hooks'));
408
410
 
409
- // 写入初始化配置
410
- const initConfig = `# Specline 项目配置
411
- version: "${VERSION}"
412
- initialized_at: "${new Date().toISOString()}"
413
- `;
414
- writeFileSync(configFile, initConfig, 'utf-8');
415
-
416
411
  success('Specline 初始化完成');
417
412
  log(`📁 文件: ${commandsCount} commands, ${skillsCount} skills, ${agentsCount} agents, ${hooksCount} hooks`);
418
413
  log('');
@@ -462,7 +457,26 @@ function fetchLatestVersion() {
462
457
  });
463
458
  }
464
459
 
460
+ /**
461
+ * 交互式确认提问:回车/Y/y/Yes/yes → true, N/n/No/no → false
462
+ * 非 TTY 环境直接返回 true(无人值守模式)
463
+ */
464
+ async function askConfirm(question) {
465
+ if (!process.stdin.isTTY) return true;
466
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
467
+ try {
468
+ const answer = await rl.question(question + ' ');
469
+ const trimmed = answer.trim().toLowerCase();
470
+ return trimmed === '' || trimmed === 'y' || trimmed === 'yes';
471
+ } catch {
472
+ return false;
473
+ } finally {
474
+ rl.close();
475
+ }
476
+ }
477
+
465
478
  async function cmd_update() {
479
+ // 1. 从 npm registry 获取最新版本
466
480
  let latest;
467
481
  try {
468
482
  latest = await fetchLatestVersion();
@@ -480,25 +494,60 @@ async function cmd_update() {
480
494
  process.exit(0);
481
495
  }
482
496
 
483
- const currentParts = VERSION.split('.').map(Number);
484
- const latestParts = latest.split('.').map(Number);
497
+ // 2. 版本比较
498
+ if (compareVersions(VERSION, latest) >= 0) {
499
+ success('已是最新版本 (v' + VERSION + ')');
500
+ process.exit(0);
501
+ }
502
+
503
+ // 3. 交互确认
504
+ log('✨ 新版本可用: v' + latest + '(当前: v' + VERSION + ')');
485
505
 
486
- let isNewer = false;
487
- for (let i = 0; i < 3; i++) {
488
- const c = currentParts[i] || 0;
489
- const l = latestParts[i] || 0;
490
- if (l > c) {
491
- isNewer = true;
492
- break;
493
- } else if (l < c) {
494
- break;
506
+ if (!process.stdin.isTTY) {
507
+ log('在非交互环境中无法自动升级,请手动执行: npm install -g specline@latest');
508
+ process.exit(0);
509
+ }
510
+
511
+ const proceed = await askConfirm('是否升级到 v' + latest + '?[Y/n]');
512
+ if (!proceed) {
513
+ log('已取消升级');
514
+ process.exit(0);
515
+ }
516
+
517
+ // 4. 执行 npm install -g specline@latest
518
+ log('正在升级 specline...');
519
+ try {
520
+ execSync('npm install -g specline@latest', { stdio: 'inherit' });
521
+ } catch (err) {
522
+ const stderr = (err.stderr || '').toString();
523
+ if (stderr.includes('EACCES') || stderr.includes('permission denied')) {
524
+ error('权限不足。请尝试:');
525
+ log(' sudo npm install -g specline@latest');
526
+ log(' 或使用 Node 版本管理器(nvm / fnm / n)');
527
+ } else {
528
+ error('升级失败:' + (stderr || err.message));
495
529
  }
530
+ process.exit(1);
496
531
  }
497
532
 
498
- if (isNewer) {
499
- log('✨ 新版本可用: v' + latest + '(当前: v' + VERSION + ')\n运行 npm install -g specline@latest 更新');
500
- } else {
501
- success('已是最新版本 (v' + VERSION + ')');
533
+ success('已升级至 v' + latest);
534
+
535
+ // 5. 检测是否为 specline 项目,询问是否同步模板
536
+ const cwd = process.cwd();
537
+ const lockFile = join(cwd, 'specline', '.specline-lock.yaml');
538
+ if (existsSync(lockFile)) {
539
+ const doSync = await askConfirm('检测到 specline 项目,是否同步最新模板?[Y/n]');
540
+ if (doSync) {
541
+ log('正在同步模板文件...');
542
+ try {
543
+ const result = spawnSync('specline', ['sync'], { stdio: 'inherit' });
544
+ if (result.status !== 0) {
545
+ warn('模板同步失败(退出码: ' + result.status + '),请手动运行 specline sync');
546
+ }
547
+ } catch (err) {
548
+ warn('无法运行 specline sync:' + err.message);
549
+ }
550
+ }
502
551
  }
503
552
 
504
553
  process.exit(0);
@@ -509,10 +558,19 @@ function cmd_sync({ dryRun, targetPath }) {
509
558
  const target = resolve(cwd, targetPath || '.');
510
559
 
511
560
  // 1. 检查项目是否已初始化
512
- const configFile = join(target, '.specline-config.yaml');
513
- if (!existsSync(configFile)) {
514
- error('未检测到 Specline 项目,请先运行 specline init');
515
- process.exit(1);
561
+ const lockFile = join(target, 'specline', '.specline-lock.yaml');
562
+ if (!existsSync(lockFile)) {
563
+ // 向后兼容:检查旧版 .specline-config.yaml
564
+ const oldMarker = join(target, '.specline-config.yaml');
565
+ if (existsSync(oldMarker)) {
566
+ warn('检测到旧版项目,正在自动迁移...');
567
+ const lockData = buildLockData(target, target);
568
+ writeLockFile(target, lockData);
569
+ success('已从旧版项目迁移,生成了锁文件');
570
+ } else {
571
+ error('未检测到 Specline 项目,请先运行 specline init');
572
+ process.exit(1);
573
+ }
516
574
  }
517
575
 
518
576
  // 2. 构建上游模板哈希映射
@@ -545,10 +603,6 @@ function cmd_sync({ dryRun, targetPath }) {
545
603
  // 6. 分类
546
604
  const results = [];
547
605
  for (const path of allPaths) {
548
- if (path === '.specline-config.yaml') {
549
- // 项目标识文件,由 specline init 生成(含时间戳),sync 不覆盖
550
- continue;
551
- }
552
606
  const templateHash = upstreamFiles.get(path) || null;
553
607
  const lockEntry = lockData ? (lockData.files.get(path) || null) : null;
554
608
  const projectPath = join(target, path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specline",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Spec-driven AI coding pipeline with deterministic quality gates for Cursor IDE",
5
5
  "bin": {
6
6
  "specline": "./cli.mjs"
@@ -1 +0,0 @@
1
- version: "1.0.0"