qwen-base 1.0.1 → 1.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.
Files changed (2) hide show
  1. package/bin/install.js +91 -143
  2. package/package.json +1 -1
package/bin/install.js CHANGED
@@ -3,8 +3,8 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const readline = require('readline');
6
7
 
7
- // Colors
8
8
  const green = '\x1b[32m';
9
9
  const cyan = '\x1b[36m';
10
10
  const yellow = '\x1b[33m';
@@ -14,12 +14,12 @@ const reset = '\x1b[0m';
14
14
  const pkg = require('../package.json');
15
15
 
16
16
  const banner = `
17
- ${green} ██████╗ ███████╗███████╗██████╗
18
- ██╔══██╗██╔════╝██╔════╝██╔══██╗
19
- ██████╔╝█████╗ █████╗ ██████╔╝
20
- ██╔══██╗██╔══╝ ██╔══╝ ██╔══██╗
21
- ██║ ██║███████╗███████╗██║ ██║
22
- ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝${reset}
17
+ ${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
18
+ \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
19
+ \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
20
+ \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
21
+ \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
22
+ \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
23
 
24
24
  BASE ${dim}v${pkg.version}${reset}
25
25
  Builder's Automated State Engine
@@ -27,8 +27,10 @@ ${green} ██████╗ ███████╗███████╗
27
27
  `;
28
28
 
29
29
  const args = process.argv.slice(2);
30
- const hasHelp = args.includes('--help') || args.includes('-h');
30
+ const hasGlobal = args.includes('--global') || args.includes('-g');
31
31
  const hasLocal = args.includes('--local') || args.includes('-l');
32
+ const hasHelp = args.includes('--help') || args.includes('-h');
33
+ const skipTools = args.includes('--skip-tools');
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,24 +61,17 @@ 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
  }
@@ -89,139 +82,94 @@ if (hasHelp) {
89
82
  console.log(` ${yellow}Usage:${reset} npx qwen-base [options]
90
83
 
91
84
  ${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
85
+ ${cyan}-g, --global${reset} Install globally (to Qwen config directory)
86
+ ${cyan}-l, --local${reset} Install locally (to ./.qwen/ in current directory)
87
+ ${cyan}-c, --config-dir <path>${reset} Specify custom Qwen config directory
88
+ ${cyan}--skip-tools${reset} Skip OSS analysis tool installation
89
+ ${cyan}-h, --help${reset} Show this help message
95
90
 
96
91
  ${yellow}Examples:${reset}
97
- ${dim}# Install globally (default)${reset}
98
- npx qwen-base
92
+ ${dim}# Install globally (recommended)${reset}
93
+ npx qwen-base --global
99
94
 
100
95
  ${dim}# Install to current project only${reset}
101
96
  npx qwen-base --local
102
97
 
103
98
  ${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
99
+ ${cyan}commands/qwen-base/${reset} 11 slash commands
100
+ ${cyan}base/ Framework (core, transform, domains, schemas, rules, tools)
111
101
  `);
112
102
  process.exit(0);
113
103
  }
114
104
 
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
- }
130
-
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}`);
105
+ function install(isGlobal) {
106
+ const src = path.join(__dirname, '..');
107
+ const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.QWEN_CONFIG_DIR);
108
+ const globalDir = configDir || path.join(os.homedir(), '.qwen');
109
+ const qwenDir = isGlobal ? globalDir : path.join(process.cwd(), '.qwen');
110
+ const aegisDest = path.join(qwenDir, 'aegis');
111
+ const cmdsDest = path.join(qwenDir, 'commands', 'qwen-base');
112
+
113
+ const locationLabel = isGlobal
114
+ ? qwenDir.replace(os.homedir(), '~')
115
+ : qwenDir.replace(process.cwd(), '.');
116
+
117
+ if (fs.existsSync(aegisDest) || fs.existsSync(cmdsDest)) {
118
+ console.log(` ${yellow}Existing installation found at ${locationLabel}${reset}`);
119
+ console.log(` Updating...`);
120
+ if (fs.existsSync(aegisDest)) fs.rmSync(aegisDest, { recursive: true, force: true });
121
+ if (fs.existsSync(cmdsDest)) fs.rmSync(cmdsDest, { recursive: true, force: true });
182
122
  }
183
- }
184
123
 
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) {}
124
+ console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
125
+
126
+ // Copy framework (note: BASE uses 'src/' structure)
127
+ if (fs.existsSync(path.join(src, 'src'))) {
128
+ copyDir(path.join(src, 'src'), path.join(qwenDir, 'base'));
129
+ console.log(` ${green}+${reset} base/ ${dim}(${countFiles(path.join(qwenDir, 'base'))} framework files)${reset}`);
191
130
  }
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 }] });
217
- }
131
+
132
+ // Copy commands
133
+ if (fs.existsSync(path.join(src, 'src', 'commands'))) {
134
+ fs.mkdirSync(cmdsDest, { recursive: true });
135
+ copyDir(path.join(src, 'src', 'commands'), cmdsDest);
136
+ console.log(` ${green}+${reset} commands/qwen-base/ ${dim}(${countFiles(cmdsDest)} commands)${reset}`);
218
137
  }
219
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
138
+
139
+ console.log(`
140
+ ${green}Done!${reset} Open Qwen Code and type ${cyan}/base${reset} to start.
141
+ `);
220
142
  }
221
143
 
222
- wireHooks(qwenDir, hooksDest);
223
- console.log(` ${green}+${reset} Hooks wired in settings.json${reset}`);
144
+ function promptLocation() {
145
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
224
146
 
225
- console.log(`
226
- ${green}Done!${reset} Open Qwen Code and type ${cyan}/base${reset} to start.
147
+ const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.QWEN_CONFIG_DIR);
148
+ const globalPath = configDir || path.join(os.homedir(), '.qwen');
149
+ const globalLabel = globalPath.replace(os.homedir(), '~');
150
+
151
+ console.log(` ${yellow}Where would you like to install?${reset}
152
+
153
+ ${cyan}1${reset}) Global ${dim}(${globalLabel})${reset} - available in all projects
154
+ ${cyan}2${reset}) Local ${dim}(./.qwen)${reset} - this project only
227
155
  `);
156
+
157
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
158
+ rl.close();
159
+ install(answer.trim() !== '2');
160
+ });
161
+ }
162
+
163
+ if (hasGlobal && hasLocal) {
164
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
165
+ process.exit(1);
166
+ } else if (explicitConfigDir && hasLocal) {
167
+ console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
168
+ process.exit(1);
169
+ } else if (hasGlobal) {
170
+ install(true);
171
+ } else if (hasLocal) {
172
+ install(false);
173
+ } else {
174
+ promptLocation();
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwen-base",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Builder's Automated State Engine — workspace lifecycle management for Qwen Code",
5
5
  "bin": {
6
6
  "qwen-base": "bin/install.js"