nano-spec 1.1.2 → 1.1.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/README.md CHANGED
@@ -12,8 +12,8 @@ npm install -g nano-spec
12
12
  # 2) 初始化(默认交互式选择 AI 工具)
13
13
  nanospec init
14
14
 
15
- # 3) 创建任务
16
- nanospec new "优化登录流程"
15
+ # 3) 创建任务(可直接回车使用默认名“待命名”)
16
+ nanospec new
17
17
  ```
18
18
 
19
19
  在 AI 工具中按顺序执行:
@@ -22,18 +22,23 @@ nanospec new "优化登录流程"
22
22
  - `/spec.2-plan`:生成实施方案与任务拆解
23
23
  - `/spec.3-execute`:执行任务并更新状态
24
24
 
25
+ `/init` 使用建议(已初始化项目):
26
+
27
+ - 直接给出任务目标:`/init 创建用户认证功能`
28
+ - 避免只输入 `/init` 或“创建任务”这类泛化描述,否则应先补充任务名与一句话目标
29
+
25
30
  ## 核心命令
26
31
 
27
- | 命令 | 说明 |
28
- |---|---|
29
- | `nanospec init` | 初始化项目结构与命令模板 |
30
- | `nanospec new [name]` | 创建任务目录并设为当前任务 |
31
- | `nanospec switch [name]` | 切换当前任务 |
32
- | `nanospec status` | 查看当前任务状态 |
33
- | `nanospec sync [--adapter <name>]` | 同步命令到 AI 工具目录 |
34
- | `nanospec preset list/install/uninstall` | 预设包管理 |
35
- | `nanospec config` | 查看当前配置 |
36
- | `nanospec config get/set/unset/list` | 读写配置(支持 `--global`) |
32
+ | 命令 | 简写 | 说明 |
33
+ |---|---|---|
34
+ | `nanospec init` | `nanospec i` | 初始化项目结构与命令模板 |
35
+ | `nanospec new [name]` | `nanospec n [name]` | 创建任务目录并设为当前任务;不带 `name` 时进入交互输入(默认“待命名”) |
36
+ | `nanospec switch [name]` | `nanospec s [name]` | 切换当前任务 |
37
+ | `nanospec status` | `nanospec st` | 查看当前任务状态 |
38
+ | `nanospec sync [--adapter <name>]` | `nanospec sy [--adapter <name>]` | 同步命令到 AI 工具目录 |
39
+ | `nanospec preset list/install/uninstall` | `nanospec p ls/add/rm` | 预设包管理 |
40
+ | `nanospec config` | `nanospec c` | 查看当前配置 |
41
+ | `nanospec config get/set/unset/list` | `nanospec c g/s/u/ls` | 读写配置(支持 `--global`) |
37
42
 
38
43
  ## 工作流概览
39
44
 
package/bin/nanospec.js CHANGED
@@ -1,2 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import '../dist/index.js';
2
+ import {createProgram} from '../dist/index.js';
3
+
4
+ createProgram().parse();
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import inquirer from 'inquirer';
3
4
  import { loadConfig } from '../config/config.js';
4
5
  import { setCurrentTask } from '../config/task-pointer.js';
5
6
  export async function newTask(name) {
@@ -11,7 +12,18 @@ export async function newTask(name) {
11
12
  return;
12
13
  }
13
14
  const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
14
- const taskName = name || '待命名';
15
+ let taskName = name?.trim();
16
+ if (!taskName) {
17
+ const result = await inquirer.prompt([
18
+ {
19
+ type: 'input',
20
+ name: 'taskName',
21
+ message: '任务名称:',
22
+ default: '待命名',
23
+ },
24
+ ]);
25
+ taskName = result.taskName?.trim() || '待命名';
26
+ }
15
27
  const dirName = `${date}-${taskName}`;
16
28
  const taskDir = join(nanospecDir, dirName);
17
29
  if (existsSync(taskDir)) {
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import inquirer from 'inquirer';
3
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
5
  import { newTask } from './new.js';
5
6
  describe('new command', () => {
@@ -32,10 +33,12 @@ describe('new command', () => {
32
33
  it('应该创建带日期戳的任务目录', async () => {
33
34
  // 先初始化项目
34
35
  mkdirSync(nanospecDir, { recursive: true });
36
+ const promptSpy = vi.spyOn(inquirer, 'prompt');
35
37
  await newTask('用户认证功能');
36
38
  const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
37
39
  const taskDir = join(nanospecDir, `${date}-用户认证功能`);
38
40
  expect(existsSync(taskDir)).toBe(true);
41
+ expect(promptSpy).not.toHaveBeenCalled();
39
42
  });
40
43
  it('应该创建任务目录结构(brief.md、assets/、outputs/)', async () => {
41
44
  mkdirSync(nanospecDir, { recursive: true });
@@ -57,10 +60,25 @@ describe('new command', () => {
57
60
  });
58
61
  it('应该使用"待命名"作为默认名称', async () => {
59
62
  mkdirSync(nanospecDir, { recursive: true });
63
+ const promptSpy = vi
64
+ .spyOn(inquirer, 'prompt')
65
+ .mockResolvedValue({ taskName: '' });
60
66
  await newTask();
61
67
  const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
62
68
  const taskDir = join(nanospecDir, `${date}-待命名`);
63
69
  expect(existsSync(taskDir)).toBe(true);
70
+ expect(promptSpy).toHaveBeenCalledOnce();
71
+ });
72
+ it('应该在无参时进入交互模式并使用用户输入名称', async () => {
73
+ mkdirSync(nanospecDir, { recursive: true });
74
+ const promptSpy = vi
75
+ .spyOn(inquirer, 'prompt')
76
+ .mockResolvedValue({ taskName: '登录体验优化' });
77
+ await newTask();
78
+ const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
79
+ const taskDir = join(nanospecDir, `${date}-登录体验优化`);
80
+ expect(existsSync(taskDir)).toBe(true);
81
+ expect(promptSpy).toHaveBeenCalledOnce();
64
82
  });
65
83
  it('应该在目录已存在时显示警告', async () => {
66
84
  mkdirSync(nanospecDir, { recursive: true });
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import { Command } from 'commander';
3
+ export declare function createProgram(): Command;
package/dist/index.js CHANGED
@@ -7,72 +7,88 @@ import { showStatus } from './commands/status.js';
7
7
  import { listPresets, installPreset, uninstallPreset } from './commands/preset.js';
8
8
  import { syncCommands } from './commands/sync.js';
9
9
  import { config } from './commands/config.js';
10
- const program = new Command();
11
- program
12
- .name('nanospec')
13
- .description('nanospec - Spec 驱动开发工作流')
14
- .version('1.0.0');
15
- program
16
- .command('init')
17
- .description('初始化 nanospec 项目结构')
18
- .option('--ai <tool>', 'AI 工具类型(非交互式快速初始化)')
19
- .option('-f, --force', '强制覆盖已存在的文件')
20
- .action((options) => init(options));
21
- program
22
- .command('new [name]')
23
- .description('创建新的任务目录')
24
- .action((name) => newTask(name));
25
- program
26
- .command('switch [name]')
27
- .description('切换当前任务')
28
- .action((name) => switchTask(name));
29
- program
30
- .command('status')
31
- .description('显示当前状态')
32
- .action(() => showStatus());
33
- const presetCmd = program
34
- .command('preset')
35
- .description('预设包管理');
36
- presetCmd
37
- .command('list')
38
- .description('列出所有可用预设')
39
- .action(() => listPresets());
40
- presetCmd
41
- .command('install [name]')
42
- .description('安装预设(不指定名称时使用交互式选择)')
43
- .action((name) => installPreset(name));
44
- presetCmd
45
- .command('uninstall <name>')
46
- .description('卸载预设')
47
- .action((name) => uninstallPreset(name));
48
- program
49
- .command('sync')
50
- .description('同步命令到 AI 工具')
51
- .option('--adapter <name>', '指定 AI 工具')
52
- .action((options) => syncCommands(options));
53
- const configCmd = program
54
- .command('config')
55
- .description('配置管理');
56
- configCmd
57
- .command('get <key>')
58
- .description('获取配置值')
59
- .option('-g, --global', '操作全局配置')
60
- .action((key, options) => config('get', key, undefined, options));
61
- configCmd
62
- .command('set <key> <value>')
63
- .description('设置配置')
64
- .option('-g, --global', '操作全局配置')
65
- .action((key, value, options) => config('set', key, value, options));
66
- configCmd
67
- .command('unset <key>')
68
- .description('删除配置项')
69
- .option('-g, --global', '操作全局配置')
70
- .action((key, options) => config('unset', key, undefined, options));
71
- configCmd
72
- .command('list')
73
- .description('列出所有配置项')
74
- .option('-g, --global', '操作全局配置')
75
- .action((options) => config(undefined, undefined, undefined, { ...options, list: true }));
76
- configCmd
77
- .action((options) => config(undefined, undefined, undefined, options));
78
- program.parse();
10
+ export function createProgram() {
11
+ const program = new Command();
12
+ program
13
+ .name('nanospec')
14
+ .description('nanospec - Spec 驱动开发工作流')
15
+ .version('1.0.0');
16
+ program
17
+ .command('init')
18
+ .alias('i')
19
+ .description('初始化 nanospec 项目结构')
20
+ .option('--ai <tool>', 'AI 工具类型(非交互式快速初始化)')
21
+ .option('-f, --force', '强制覆盖已存在的文件')
22
+ .action((options) => init(options));
23
+ program
24
+ .command('new [name]')
25
+ .alias('n')
26
+ .description('创建新的任务目录')
27
+ .action((name) => newTask(name));
28
+ program
29
+ .command('switch [name]')
30
+ .alias('s')
31
+ .description('切换当前任务')
32
+ .action((name) => switchTask(name));
33
+ program
34
+ .command('status')
35
+ .alias('st')
36
+ .description('显示当前状态')
37
+ .action(() => showStatus());
38
+ const presetCmd = program
39
+ .command('preset')
40
+ .alias('p')
41
+ .description('预设包管理');
42
+ presetCmd
43
+ .command('list')
44
+ .alias('ls')
45
+ .description('列出所有可用预设')
46
+ .action(() => listPresets());
47
+ presetCmd
48
+ .command('install [name]')
49
+ .alias('add')
50
+ .description('安装预设(不指定名称时使用交互式选择)')
51
+ .action((name) => installPreset(name));
52
+ presetCmd
53
+ .command('uninstall <name>')
54
+ .alias('rm')
55
+ .description('卸载预设')
56
+ .action((name) => uninstallPreset(name));
57
+ program
58
+ .command('sync')
59
+ .alias('sy')
60
+ .description('同步命令到 AI 工具')
61
+ .option('--adapter <name>', '指定 AI 工具')
62
+ .action((options) => syncCommands(options));
63
+ const configCmd = program
64
+ .command('config')
65
+ .alias('c')
66
+ .description('配置管理');
67
+ configCmd
68
+ .command('get <key>')
69
+ .alias('g')
70
+ .description('获取配置值')
71
+ .option('-g, --global', '操作全局配置')
72
+ .action((key, options) => config('get', key, undefined, options));
73
+ configCmd
74
+ .command('set <key> <value>')
75
+ .alias('s')
76
+ .description('设置配置')
77
+ .option('-g, --global', '操作全局配置')
78
+ .action((key, value, options) => config('set', key, value, options));
79
+ configCmd
80
+ .command('unset <key>')
81
+ .alias('u')
82
+ .description('删除配置项')
83
+ .option('-g, --global', '操作全局配置')
84
+ .action((key, options) => config('unset', key, undefined, options));
85
+ configCmd
86
+ .command('list')
87
+ .alias('ls')
88
+ .description('列出所有配置项')
89
+ .option('-g, --global', '操作全局配置')
90
+ .action((options) => config(undefined, undefined, undefined, { ...options, list: true }));
91
+ configCmd
92
+ .action((options) => config(undefined, undefined, undefined, options));
93
+ return program;
94
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createProgram } from './index.js';
3
+ describe('cli command registration', () => {
4
+ it('should register aliases for top-level commands', () => {
5
+ const program = createProgram();
6
+ const expectedAliases = [
7
+ ['init', 'i'],
8
+ ['new', 'n'],
9
+ ['switch', 's'],
10
+ ['status', 'st'],
11
+ ['preset', 'p'],
12
+ ['sync', 'sy'],
13
+ ['config', 'c'],
14
+ ];
15
+ for (const [name, alias] of expectedAliases) {
16
+ const command = program.commands.find((cmd) => cmd.name() === name);
17
+ expect(command, `missing command: ${name}`).toBeDefined();
18
+ expect(command?.aliases()).toContain(alias);
19
+ }
20
+ });
21
+ it('should register alias for switch command as s', () => {
22
+ const program = createProgram();
23
+ const switchCommand = program.commands.find((cmd) => cmd.name() === 'switch');
24
+ expect(switchCommand).toBeDefined();
25
+ expect(switchCommand?.aliases()).toContain('s');
26
+ });
27
+ });
@@ -14,8 +14,18 @@ describe('command templates', () => {
14
14
  it('should use nanospec commands in init and run templates', () => {
15
15
  const initTemplate = readFileSync(join(__dirname, 'spec.init.toml'), 'utf-8');
16
16
  const runTemplate = readFileSync(join(__dirname, 'spec.run.toml'), 'utf-8');
17
- expect(initTemplate).toContain('nanospec init');
18
17
  expect(initTemplate).toContain('nanospec new');
19
18
  expect(runTemplate).toContain('nanospec new');
20
19
  });
20
+ it('should keep init template focused on task creation without environment detection', () => {
21
+ const initTemplate = readFileSync(join(__dirname, 'spec.init.toml'), 'utf-8');
22
+ expect(initTemplate).not.toContain('Step 1: 检测环境');
23
+ expect(initTemplate).not.toContain('nanospec init --ai cursor');
24
+ expect(initTemplate).toContain('执行创建任务流程');
25
+ });
26
+ it('should require clarification when task name cannot be extracted in init template', () => {
27
+ const initTemplate = readFileSync(join(__dirname, 'spec.init.toml'), 'utf-8');
28
+ expect(initTemplate).toContain('如果无法明确提取任务名');
29
+ expect(initTemplate).toContain('不执行 `nanospec new`');
30
+ });
21
31
  });
@@ -19,8 +19,22 @@ describe('command templates', () => {
19
19
  const initTemplate = readFileSync(join(__dirname, 'spec.init.toml'), 'utf-8');
20
20
  const runTemplate = readFileSync(join(__dirname, 'spec.run.toml'), 'utf-8');
21
21
 
22
- expect(initTemplate).toContain('nanospec init');
23
22
  expect(initTemplate).toContain('nanospec new');
24
23
  expect(runTemplate).toContain('nanospec new');
25
24
  });
25
+
26
+ it('should keep init template focused on task creation without environment detection', () => {
27
+ const initTemplate = readFileSync(join(__dirname, 'spec.init.toml'), 'utf-8');
28
+
29
+ expect(initTemplate).not.toContain('Step 1: 检测环境');
30
+ expect(initTemplate).not.toContain('nanospec init --ai cursor');
31
+ expect(initTemplate).toContain('执行创建任务流程');
32
+ });
33
+
34
+ it('should require clarification when task name cannot be extracted in init template', () => {
35
+ const initTemplate = readFileSync(join(__dirname, 'spec.init.toml'), 'utf-8');
36
+
37
+ expect(initTemplate).toContain('如果无法明确提取任务名');
38
+ expect(initTemplate).toContain('不执行 `nanospec new`');
39
+ });
26
40
  });
@@ -1,44 +1,29 @@
1
1
  # Command: spec.init
2
- # Description: 初始化或创建任务 - 检测环境并初始化,或创建新任务
2
+ # Description: 创建任务 - 从用户意图创建任务并填写 brief
3
3
  # Category: nanospec
4
4
  # Version: 1
5
5
 
6
- description = "初始化或创建任务 - 检测环境并初始化,或创建新任务"
6
+ description = "创建任务 - 从用户意图创建任务并填写 brief"
7
7
 
8
8
  prompt = """
9
- # /init - 初始化或创建任务
9
+ # /init - 创建任务
10
10
 
11
11
  > 遵循 `<config_dir>/AGENTS.md` 通用规范
12
12
 
13
13
  ## Role
14
14
 
15
- 你是"环境初始化助手":检测当前环境状态,执行初始化或创建任务。
15
+ 你是"任务创建助手":从用户输入中提取任务名称和意图,创建任务并填写 brief。
16
16
 
17
17
  ## Objective
18
18
 
19
- 根据当前环境状态执行不同操作:
20
- 1. **未初始化**:执行 `nanospec init` 初始化目录结构
21
- 2. **已初始化**:执行 `nanospec new` 创建任务,并根据用户意图填写 brief.md
22
-
23
- ## Decision Protocol
24
-
25
- ### Step 1: 检测环境
26
-
27
- 检查 `<config_dir>/AGENTS.md` 是否存在:
28
- - **不存在** → 执行初始化流程
29
- - **存在** → 执行创建任务流程
30
-
31
- ### Step 2A: 初始化流程(未初始化时)
32
-
33
- 1. 执行 shell 命令:`nanospec init --ai cursor`
34
- 2. 确认生成了 `<config_dir>/AGENTS.md`
35
- 3. 提示用户初始化完成,可以开始创建任务
36
-
37
- ### Step 2B: 创建任务流程(已初始化时)
38
-
19
+ 执行创建任务流程:
39
20
  1. 从用户输入中提取任务名称和意图
40
- 2. 执行 shell 命令:`nanospec new "<任务名称>"`
41
- 3. 根据用户意图,填写 `<specs_dir>/<task_name>/brief.md`:
21
+ 2. 对任务名称做有效性检查:
22
+ - 任务名应当和要做的事情直接相关(功能、页面、模块、问题)
23
+ - 禁止使用泛化命名:`初始化或创建任务`、`新任务`、`临时任务`、`todo` 等
24
+ - 如果无法明确提取任务名,先向用户提问后再继续,不得直接执行 `nanospec new`
25
+ 3. 执行 shell 命令:`nanospec new "<任务名称>"`
26
+ 4. 根据用户意图,填写 `<specs_dir>/<task_name>/brief.md`:
42
27
  - 背景:用户的原始描述
43
28
  - 目标:从意图中提炼的目标
44
29
  - 约束:如有提及则填写,否则留空待补充
@@ -46,17 +31,14 @@ prompt = """
46
31
  ## Input
47
32
 
48
33
  用户的自然语言描述,例如:
49
- - "初始化 nano-spec"
50
34
  - "创建一个用户认证功能"
51
35
  - "我要做一个支持暗黑模式的设置页面"
36
+ - "/init 创建用户认证功能(支持邮箱+验证码登录)"
37
+ - "/init 修复订单页在移动端按钮遮挡问题"
52
38
 
53
39
  ## Output
54
40
 
55
- ### 初始化场景
56
-
57
- 执行命令并报告结果。
58
-
59
- ### 创建任务场景
41
+ ### 创建任务
60
42
 
61
43
  1. 执行 `nanospec new` 命令
62
44
  2. 填写 brief.md,格式:
@@ -75,10 +57,16 @@ prompt = """
75
57
 
76
58
  3. 提示用户下一步使用 `/{{cmd_prefix}}.1-spec` 生成规格说明
77
59
 
60
+ ### 信息不足场景
61
+
62
+ 1. 不执行 `nanospec new`
63
+ 2. 先向用户提一个简短问题:请提供任务名称和一句话目标
64
+ 3. 得到补充后再继续创建任务
65
+
78
66
  ## Checklist
79
67
 
80
- - 正确判断了环境状态
81
- - 执行了相应的 nanospec 命令
82
- - (创建任务时)brief.md 已填写用户意图
68
+ - 已提取并校验任务名
69
+ - 执行了 `nanospec new`
70
+ - brief.md 已填写用户意图
83
71
 
84
72
  """
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-spec",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "A minimal extendable Spec-Driven framework. Not just for code -- for writing, research, and anything you want to get done.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",