qwen-base 1.0.1 → 1.0.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.
Files changed (2) hide show
  1. package/bin/install.js +118 -141
  2. package/package.json +1 -1
package/bin/install.js CHANGED
@@ -3,8 +3,9 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const readline = require('readline');
7
+ const { execSync } = require('child_process');
6
8
 
7
- // Colors
8
9
  const green = '\x1b[32m';
9
10
  const cyan = '\x1b[36m';
10
11
  const yellow = '\x1b[33m';
@@ -14,12 +15,12 @@ const reset = '\x1b[0m';
14
15
  const pkg = require('../package.json');
15
16
 
16
17
  const banner = `
17
- ${green} ██████╗ ███████╗███████╗██████╗
18
- ██╔══██╗██╔════╝██╔════╝██╔══██╗
19
- ██████╔╝█████╗ █████╗ ██████╔╝
20
- ██╔══██╗██╔══╝ ██╔══╝ ██╔══██╗
21
- ██║ ██║███████╗███████╗██║ ██║
22
- ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝${reset}
18
+ ${green} \u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
19
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
20
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
21
+ \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551
22
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
23
+ \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u255d${reset}
23
24
 
24
25
  BASE ${dim}v${pkg.version}${reset}
25
26
  Builder's Automated State Engine
@@ -27,8 +28,9 @@ ${green} ██████╗ ███████╗███████╗
27
28
  `;
28
29
 
29
30
  const args = process.argv.slice(2);
30
- const hasHelp = args.includes('--help') || args.includes('-h');
31
+ const hasGlobal = args.includes('--global') || args.includes('-g');
31
32
  const hasLocal = args.includes('--local') || args.includes('-l');
33
+ const hasHelp = args.includes('--help') || args.includes('-h');
32
34
 
33
35
  function parseConfigDirArg() {
34
36
  const idx = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
@@ -41,16 +43,14 @@ function parseConfigDirArg() {
41
43
  return nextArg;
42
44
  }
43
45
  const configDirArg = args.find(arg => arg.startsWith('--config-dir=') || arg.startsWith('-c='));
44
- if (configDirArg) {
45
- return configDirArg.split('=')[1];
46
- }
46
+ if (configDirArg) return configDirArg.split('=')[1];
47
47
  return null;
48
48
  }
49
49
 
50
+ const explicitConfigDir = parseConfigDirArg();
51
+
50
52
  function expandTilde(filePath) {
51
- if (filePath && filePath.startsWith('~/')) {
52
- return path.join(os.homedir(), filePath.slice(2));
53
- }
53
+ if (filePath && filePath.startsWith('~/')) return path.join(os.homedir(), filePath.slice(2));
54
54
  return filePath;
55
55
  }
56
56
 
@@ -61,167 +61,144 @@ function copyDir(srcDir, destDir, skipDirs = []) {
61
61
  if (skipDirs.includes(entry.name)) continue;
62
62
  const srcPath = path.join(srcDir, entry.name);
63
63
  const destPath = path.join(destDir, entry.name);
64
- if (entry.isDirectory()) {
65
- copyDir(srcPath, destPath, skipDirs);
66
- } else {
67
- fs.copyFileSync(srcPath, destPath);
68
- }
64
+ if (entry.isDirectory()) copyDir(srcPath, destPath, skipDirs);
65
+ else fs.copyFileSync(srcPath, destPath);
69
66
  }
70
67
  }
71
68
 
72
- function countFiles(dir, ext) {
69
+ function countFiles(dir) {
73
70
  let count = 0;
74
71
  const entries = fs.readdirSync(dir, { withFileTypes: true });
75
72
  for (const entry of entries) {
76
- const fullPath = path.join(dir, entry.name);
77
- if (entry.isDirectory()) {
78
- count += countFiles(fullPath, ext);
79
- } else if (!ext || entry.name.endsWith(ext)) {
80
- count++;
81
- }
73
+ if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
74
+ else count++;
82
75
  }
83
76
  return count;
84
77
  }
85
78
 
79
+ function wireMcp(workspaceDir, mcpIndexPath) {
80
+ const mcpJsonPath = path.join(workspaceDir, '.mcp.json');
81
+ let mcpConfig = {};
82
+ if (fs.existsSync(mcpJsonPath)) {
83
+ try { mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8')); } catch (e) {}
84
+ }
85
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
86
+ const normalizedPath = mcpIndexPath.replace(/\\/g, '/');
87
+ mcpConfig.mcpServers['base-mcp'] = { command: 'node', args: [normalizedPath], type: 'stdio' };
88
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
89
+ }
90
+
86
91
  console.log(banner);
87
92
 
88
93
  if (hasHelp) {
89
94
  console.log(` ${yellow}Usage:${reset} npx qwen-base [options]
90
95
 
91
96
  ${yellow}Options:${reset}
92
- ${cyan}-l, --local${reset} Install to ./.qwen/commands/ instead of global
93
- ${cyan}-c, --config-dir <path>${reset} Specify custom Qwen config directory
94
- ${cyan}-h, --help${reset} Show this help message
97
+ ${cyan}-g, --global${reset} Install globally (to Qwen config directory)
98
+ ${cyan}-l, --local${reset} Install locally (to ./.qwen/ in current directory)
99
+ ${cyan}-c, --config-dir <path>${reset} Specify custom Qwen config directory
100
+ ${cyan}-h, --help${reset} Show this help message
95
101
 
96
102
  ${yellow}Examples:${reset}
97
- ${dim}# Install globally (default)${reset}
98
- npx qwen-base
103
+ ${dim}# Install globally (recommended)${reset}
104
+ npx qwen-base --global
99
105
 
100
106
  ${dim}# Install to current project only${reset}
101
107
  npx qwen-base --local
102
108
 
103
109
  ${yellow}What gets installed:${reset}
104
- ${cyan}commands/qwen-base/${reset}
105
- base.md Entry point (routing + persona)
106
- framework/ Tasks, templates, frameworks (11 commands)
107
- commands/ Command definitions
108
- hooks/ 7 Python hooks for workspace intelligence
109
- templates/ operator.json, workspace.json templates
110
- ${cyan}.qwen/hooks/${reset} Python hooks registration
110
+ ${cyan}commands/qwen-base/${reset} 11 slash commands
111
+ ${cyan}base/ Framework (core, transform, domains, schemas, rules, tools)
112
+ ${cyan}.mcp.json base-mcp server registration
111
113
  `);
112
114
  process.exit(0);
113
115
  }
114
116
 
115
- const explicitConfigDir = parseConfigDirArg();
116
- const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.QWEN_CONFIG_DIR);
117
- const globalDir = configDir || path.join(os.homedir(), '.qwen');
118
- const qwenDir = hasLocal ? path.join(process.cwd(), '.qwen') : globalDir;
119
- const baseDest = path.join(qwenDir, 'commands', 'qwen-base');
120
-
121
- const locationLabel = hasLocal
122
- ? baseDest.replace(process.cwd(), '.')
123
- : baseDest.replace(os.homedir(), '~');
124
-
125
- if (fs.existsSync(baseDest)) {
126
- console.log(` ${yellow}Existing installation found at ${locationLabel}${reset}`);
127
- console.log(` Updating...`);
128
- fs.rmSync(baseDest, { recursive: true, force: true });
129
- }
117
+ function install(isGlobal) {
118
+ const src = path.join(__dirname, '..');
119
+ const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.QWEN_CONFIG_DIR);
120
+ const globalDir = configDir || path.join(os.homedir(), '.qwen');
121
+ const qwenDir = isGlobal ? globalDir : path.join(process.cwd(), '.qwen');
122
+ const baseDest = path.join(qwenDir, 'base');
123
+ const cmdsDest = path.join(qwenDir, 'commands', 'qwen-base');
124
+ const workspaceRoot = isGlobal ? os.homedir() : process.cwd();
125
+
126
+ const locationLabel = isGlobal
127
+ ? qwenDir.replace(os.homedir(), '~')
128
+ : qwenDir.replace(process.cwd(), '.');
129
+
130
+ if (fs.existsSync(baseDest) || fs.existsSync(cmdsDest)) {
131
+ console.log(` ${yellow}Existing installation found at ${locationLabel}${reset}`);
132
+ console.log(` Updating...`);
133
+ if (fs.existsSync(baseDest)) fs.rmSync(baseDest, { recursive: true, force: true });
134
+ if (fs.existsSync(cmdsDest)) fs.rmSync(cmdsDest, { recursive: true, force: true });
135
+ }
130
136
 
131
- console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
132
-
133
- const src = path.join(__dirname, '..');
134
-
135
- // Copy entry point
136
- fs.mkdirSync(baseDest, { recursive: true });
137
- fs.copyFileSync(path.join(src, 'src', 'skill', 'base.md'), path.join(baseDest, 'base.md'));
138
- console.log(` ${green}+${reset} base.md ${dim}(entry point)${reset}`);
139
-
140
- // Copy framework
141
- const fwSrc = path.join(src, 'src', 'framework');
142
- const fwDest = path.join(baseDest, 'framework');
143
- copyDir(fwSrc, fwDest);
144
- const fwCount = countFiles(fwSrc);
145
- console.log(` ${green}+${reset} framework/ ${dim}(${fwCount} files)${reset}`);
146
-
147
- // Copy commands
148
- const cmdSrc = path.join(src, 'src', 'commands');
149
- const cmdDest = path.join(baseDest, 'commands');
150
- copyDir(cmdSrc, cmdDest);
151
- const cmdCount = countFiles(cmdSrc);
152
- console.log(` ${green}+${reset} commands/ ${dim}(${cmdCount} files)${reset}`);
153
-
154
- // Copy hooks
155
- const hooksSrc = path.join(src, 'src', 'hooks');
156
- const hooksDest = path.join(baseDest, 'hooks');
157
- copyDir(hooksSrc, hooksDest);
158
- const hookCount = countFiles(hooksSrc);
159
- console.log(` ${green}+${reset} hooks/ ${dim}(${hookCount} Python hooks)${reset}`);
160
-
161
- // Copy templates
162
- const tplSrc = path.join(src, 'src', 'templates');
163
- const tplDest = path.join(baseDest, 'templates');
164
- copyDir(tplSrc, tplDest);
165
- console.log(` ${green}+${reset} templates/ ${dim}(workspace templates)${reset}`);
166
-
167
- // Copy MCP server
168
- const mcpSrc = path.join(src, 'src', 'packages', 'base-mcp');
169
- if (fs.existsSync(mcpSrc)) {
170
- const mcpDest = path.join(qwenDir, 'base-mcp');
171
- copyDir(mcpSrc, mcpDest);
172
- console.log(` ${green}+${reset} base-mcp/ ${dim}(MCP server)${reset}`);
173
-
174
- // Install MCP deps
175
- try {
176
- require('child_process').execSync('npm install --production --silent', {
177
- cwd: mcpDest, stdio: 'pipe'
178
- });
179
- console.log(` ${green}+${reset} MCP dependencies installed${reset}`);
180
- } catch (e) {
181
- console.log(` ${yellow}!${reset} MCP deps install failed — run cd ${mcpDest} && npm install${reset}`);
137
+ console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
138
+
139
+ // Copy framework (src/ → base/)
140
+ if (fs.existsSync(path.join(src, 'src'))) {
141
+ copyDir(path.join(src, 'src'), baseDest);
142
+ console.log(` ${green}+${reset} base/ ${dim}(${countFiles(baseDest)} framework files)${reset}`);
182
143
  }
183
- }
184
144
 
185
- // Wire hooks into .qwen/settings.json
186
- function wireHooks(qwenDir, hooksDir) {
187
- const settingsPath = path.join(qwenDir, 'settings.json');
188
- let settings = {};
189
- if (fs.existsSync(settingsPath)) {
190
- try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {}
145
+ // Copy commands
146
+ if (fs.existsSync(path.join(src, 'src', 'commands'))) {
147
+ fs.mkdirSync(cmdsDest, { recursive: true });
148
+ copyDir(path.join(src, 'src', 'commands'), cmdsDest);
149
+ console.log(` ${green}+${reset} commands/qwen-base/ ${dim}(${countFiles(cmdsDest)} commands)${reset}`);
191
150
  }
192
- if (!settings.hooks) settings.hooks = {};
193
-
194
- const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.py') && !f.startsWith('_'));
195
- for (const hookFile of hookFiles) {
196
- const hookName = hookFile.replace('.py', '');
197
- const hookPath = path.join(hooksDir, hookFile).replace(/\\/g, '/');
198
- const hookCommand = `python3 ${hookPath}`;
199
-
200
- // Determine which event
201
- let eventName = 'SessionStart';
202
- if (hookFile === 'active-hook.py') eventName = 'SessionStart';
203
- else if (hookFile === 'psmm-injector.py') eventName = 'SessionStart';
204
- else if (hookFile === 'backlog-hook.py') eventName = 'SessionStart';
205
- else if (hookFile === 'operator.py') eventName = 'SessionStart';
206
- else if (hookFile === 'satellite-detection.py') eventName = 'SessionStart';
207
- else if (hookFile === 'apex-insights.py') eventName = 'Stop';
208
- else if (hookFile === 'base-pulse-check.py') eventName = 'SessionStart';
209
-
210
- if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
211
- const exists = settings.hooks[eventName].some(h =>
212
- (h.command && h.command.includes(hookFile)) ||
213
- (h.hooks && h.hooks.some(i => i.command && i.command.includes(hookFile)))
214
- );
215
- if (!exists) {
216
- settings.hooks[eventName].push({ hooks: [{ type: 'command', command: hookCommand }] });
151
+
152
+ // Wire BASE MCP server into .mcp.json
153
+ const mcpIndexPath = path.join(baseDest, 'packages', 'base-mcp', 'index.js');
154
+ if (fs.existsSync(mcpIndexPath)) {
155
+ wireMcp(workspaceRoot, mcpIndexPath);
156
+ console.log(` ${green}+${reset} Wired base-mcp in .mcp.json`);
157
+
158
+ // Install MCP dependencies
159
+ const mcpDir = path.join(baseDest, 'packages', 'base-mcp');
160
+ try {
161
+ execSync('npm install --production --silent', { cwd: mcpDir, stdio: 'pipe' });
162
+ console.log(` ${green}+${reset} MCP dependencies installed`);
163
+ } catch (e) {
164
+ console.log(` ${yellow}!${reset} MCP deps install failed run cd ${mcpDir} && npm install${reset}`);
217
165
  }
218
166
  }
219
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
167
+
168
+ console.log(`
169
+ ${green}Done!${reset} Open Qwen Code and type ${cyan}/base${reset} to start.
170
+ `);
220
171
  }
221
172
 
222
- wireHooks(qwenDir, hooksDest);
223
- console.log(` ${green}+${reset} Hooks wired in settings.json${reset}`);
173
+ function promptLocation() {
174
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
224
175
 
225
- console.log(`
226
- ${green}Done!${reset} Open Qwen Code and type ${cyan}/base${reset} to start.
176
+ const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.QWEN_CONFIG_DIR);
177
+ const globalPath = configDir || path.join(os.homedir(), '.qwen');
178
+ const globalLabel = globalPath.replace(os.homedir(), '~');
179
+
180
+ console.log(` ${yellow}Where would you like to install?${reset}
181
+
182
+ ${cyan}1${reset}) Global ${dim}(${globalLabel})${reset} - available in all projects
183
+ ${cyan}2${reset}) Local ${dim}(./.qwen)${reset} - this project only
227
184
  `);
185
+
186
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
187
+ rl.close();
188
+ install(answer.trim() !== '2');
189
+ });
190
+ }
191
+
192
+ if (hasGlobal && hasLocal) {
193
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
194
+ process.exit(1);
195
+ } else if (explicitConfigDir && hasLocal) {
196
+ console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
197
+ process.exit(1);
198
+ } else if (hasGlobal) {
199
+ install(true);
200
+ } else if (hasLocal) {
201
+ install(false);
202
+ } else {
203
+ promptLocation();
204
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwen-base",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Builder's Automated State Engine — workspace lifecycle management for Qwen Code",
5
5
  "bin": {
6
6
  "qwen-base": "bin/install.js"