xiaozuoassistant 0.1.45 → 0.1.48

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/bin/cli.js CHANGED
@@ -4,6 +4,7 @@ import { spawn } from 'child_process';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import fs from 'fs';
7
+ import os from 'os';
7
8
  import { createGzip } from 'zlib';
8
9
  import { pipeline } from 'stream';
9
10
  import { promisify } from 'util';
@@ -23,24 +24,85 @@ const packageRoot = path.resolve(__dirname, '..');
23
24
 
24
25
  const args = process.argv.slice(2);
25
26
  const command = args[0];
27
+ const commandArgs = args.slice(1);
26
28
 
27
29
  const EXPORT_FILENAME = 'xiaozuoAssistant-backup.tar.gz';
28
30
 
29
31
  // Helper to get user's current working directory where they ran the command
32
+ // This is kept only for display/backward compatibility; runtime data is stored in APP_HOME.
30
33
  const CWD = process.cwd();
31
34
 
32
- // Define important paths relative to where the user *runs* the app, NOT the package location
33
- // Assuming standard structure where config.json and data folders are in CWD
35
+ function getAppHome() {
36
+ const fromFlag = getFlagValue('--home');
37
+ const fromEnv = process.env.XIAOZUOASSISTANT_HOME;
38
+ const base = (fromFlag || fromEnv || path.join(os.homedir(), '.xiaozuoassistant')).trim();
39
+ return path.resolve(base);
40
+ }
41
+
42
+ const APP_HOME = getAppHome();
43
+
44
+ // Unified runtime data directory
34
45
  const DATA_PATHS = [
35
- 'config.json',
36
- 'memories', // Vector DB and others
37
- 'data', // SQLite or other data
38
- 'logs'
46
+ 'config.json',
47
+ 'memories',
48
+ 'data',
49
+ 'logs',
50
+ 'sessions',
51
+ 'workspace'
39
52
  ];
40
53
 
54
+ function ensureAppHome() {
55
+ try {
56
+ fs.mkdirSync(APP_HOME, { recursive: true });
57
+ } catch (e) {
58
+ console.error(`[CLI] 无法创建数据目录:${APP_HOME}`);
59
+ console.error(e);
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ function ensureDefaultConfig() {
65
+ const configPath = path.join(APP_HOME, 'config.json');
66
+ if (fs.existsSync(configPath)) return;
67
+ const templatePath = path.join(packageRoot, 'config.json');
68
+ try {
69
+ if (fs.existsSync(templatePath)) {
70
+ fs.copyFileSync(templatePath, configPath);
71
+ return;
72
+ }
73
+ } catch (e) {
74
+ // ignore
75
+ }
76
+
77
+ const fallback = {
78
+ server: { port: 3001, host: 'localhost' },
79
+ llm: { apiKey: '', baseURL: '', model: '', temperature: 0.7 },
80
+ logging: { level: 'info' },
81
+ channels: {},
82
+ systemPrompt: ''
83
+ };
84
+ try {
85
+ fs.writeFileSync(configPath, JSON.stringify(fallback, null, 2));
86
+ } catch (e) {
87
+ console.error(`[CLI] 无法写入默认配置:${configPath}`);
88
+ console.error(e);
89
+ process.exit(1);
90
+ }
91
+ }
92
+
93
+ function ensureDefaultDirs() {
94
+ for (const dir of ['logs', 'data', 'memories', 'sessions', 'workspace']) {
95
+ try {
96
+ fs.mkdirSync(path.join(APP_HOME, dir), { recursive: true });
97
+ } catch {
98
+ // ignore
99
+ }
100
+ }
101
+ }
102
+
41
103
  function getPortFromConfig() {
42
104
  let port = 3001;
43
- const configPath = path.join(CWD, 'config.json');
105
+ const configPath = path.join(APP_HOME, 'config.json');
44
106
  if (fs.existsSync(configPath)) {
45
107
  try {
46
108
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
@@ -55,7 +117,23 @@ function getPortFromConfig() {
55
117
  }
56
118
 
57
119
  function getPidFilePath() {
58
- return path.join(CWD, 'logs', 'server.pid');
120
+ return path.join(APP_HOME, 'logs', 'server.pid');
121
+ }
122
+
123
+ function hasFlag(flag) {
124
+ return commandArgs.includes(flag);
125
+ }
126
+
127
+ function getFlagValue(name) {
128
+ const idx = commandArgs.findIndex(a => a === name);
129
+ if (idx < 0) return null;
130
+ const next = commandArgs[idx + 1];
131
+ if (!next || next.startsWith('-')) return null;
132
+ return next;
133
+ }
134
+
135
+ function getRegistry() {
136
+ return getFlagValue('--registry') || process.env.npm_config_registry || 'https://registry.npmjs.org';
59
137
  }
60
138
 
61
139
  function isProcessRunning(pid) {
@@ -186,13 +264,140 @@ async function stopServer() {
186
264
  console.error('[CLI] ❌ 停止失败:端口仍在占用。');
187
265
  }
188
266
 
267
+ async function runCommand(cmd, cmdArgs, options = {}) {
268
+ const child = spawn(cmd, cmdArgs, { stdio: 'inherit', ...options });
269
+ const code = await new Promise((resolve) => child.on('close', resolve));
270
+ return typeof code === 'number' ? code : 1;
271
+ }
272
+
273
+ async function runWithSudoIfNeeded(cmd, cmdArgs, options = {}) {
274
+ const code = await runCommand(cmd, cmdArgs, options);
275
+ if (code === 0) return 0;
276
+
277
+ const canSudo = process.platform !== 'win32' && process.stdin.isTTY;
278
+ if (!canSudo) return code;
279
+
280
+ console.log('[CLI] 权限不足,尝试使用 sudo 继续...');
281
+ return await runCommand('sudo', [cmd, ...cmdArgs], options);
282
+ }
283
+
284
+ async function updateApp() {
285
+ const registry = getRegistry();
286
+ const port = getPortFromConfig();
287
+ const pidFile = getPidFilePath();
288
+
289
+ const runningByPid = fs.existsSync(pidFile) && (() => {
290
+ try {
291
+ const pid = Number(fs.readFileSync(pidFile, 'utf-8').trim());
292
+ return Number.isFinite(pid) && pid > 0 && isProcessRunning(pid);
293
+ } catch {
294
+ return false;
295
+ }
296
+ })();
297
+ const runningByPort = await isPortOpen(port);
298
+ const wasRunning = runningByPid || runningByPort;
299
+
300
+ if (wasRunning) {
301
+ console.log('[CLI] 检测到服务正在运行,更新前将自动停止...');
302
+ await stopServer();
303
+ }
304
+
305
+ console.log(`[CLI] 正在更新 xiaozuoassistant(registry=${registry})...`);
306
+ const code = await runWithSudoIfNeeded('npm', ['install', '-g', 'xiaozuoassistant@latest', `--registry=${registry}`]);
307
+ if (code !== 0) {
308
+ console.error('[CLI] ❌ 更新失败。');
309
+ if (wasRunning) {
310
+ console.log('[CLI] 更新失败,尝试恢复启动旧版本服务...');
311
+ await runCommand('node', [path.join(packageRoot, 'bin', 'cli.js'), 'start'], { cwd: APP_HOME });
312
+ }
313
+ process.exit(code);
314
+ }
315
+
316
+ console.log('[CLI] ✅ 更新完成。');
317
+
318
+ if (wasRunning) {
319
+ console.log('[CLI] 正在自动重启服务...');
320
+ const restartCode = await runCommand('xiaozuoAssistant', ['start'], { cwd: APP_HOME });
321
+ process.exit(restartCode);
322
+ }
323
+ }
324
+
325
+ async function removeApp() {
326
+ const registry = getRegistry();
327
+ const targets = ['config.json', 'memories', 'data', 'logs', 'sessions', 'workspace']
328
+ .map(p => path.join(APP_HOME, p));
329
+
330
+ const doRemove = async () => {
331
+ await stopServer();
332
+
333
+ console.log('[CLI] 正在删除本地数据(当前目录)...');
334
+ for (const target of targets) {
335
+ try {
336
+ if (fs.existsSync(target)) {
337
+ fs.rmSync(target, { recursive: true, force: true });
338
+ console.log(`[CLI] 已删除:${target}`);
339
+ }
340
+ } catch (e) {
341
+ console.error(`[CLI] 删除失败:${target}`);
342
+ console.error(e);
343
+ }
344
+ }
345
+
346
+ console.log('[CLI] 正在卸载全局包 xiaozuoassistant...');
347
+ const code = await runWithSudoIfNeeded('npm', ['uninstall', '-g', 'xiaozuoassistant', `--registry=${registry}`]);
348
+ if (code !== 0) {
349
+ console.error('[CLI] ❌ 卸载失败。');
350
+ process.exit(code);
351
+ }
352
+
353
+ console.log('[CLI] ✅ 已卸载并清理数据。');
354
+ };
355
+
356
+ if (hasFlag('--yes') || hasFlag('-y')) {
357
+ await doRemove();
358
+ return;
359
+ }
360
+
361
+ console.log('[CLI] remove 将执行以下操作:');
362
+ console.log(' 1) 停止后台服务');
363
+ console.log(' 2) 删除当前目录下的数据:');
364
+ for (const target of targets) console.log(` - ${target}`);
365
+ console.log(' 3) 卸载全局 npm 包:xiaozuoassistant');
366
+ process.stdout.write('确认继续?(y/N) ');
367
+ process.stdin.once('data', async (data) => {
368
+ const answer = data.toString().trim().toLowerCase();
369
+ if (answer === 'y' || answer === 'yes') {
370
+ await doRemove();
371
+ } else {
372
+ console.log('已取消。');
373
+ }
374
+ process.exit(0);
375
+ });
376
+ }
377
+
378
+ function printVersion() {
379
+ try {
380
+ const pkgPath = path.join(packageRoot, 'package.json');
381
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
382
+ console.log(`${pkg.name}@${pkg.version}`);
383
+ console.log('Package Root:', packageRoot);
384
+ console.log('Node Version:', process.version);
385
+ } catch {
386
+ console.log('xiaozuoassistant@unknown');
387
+ console.log('Package Root:', packageRoot);
388
+ console.log('Node Version:', process.version);
389
+ }
390
+ }
391
+
189
392
  async function exportData() {
393
+ ensureAppHome();
394
+ ensureDefaultDirs();
190
395
  console.log('📦 Starting data export...');
191
396
 
192
397
  const filesToArchive = [];
193
398
 
194
399
  for (const p of DATA_PATHS) {
195
- if (fs.existsSync(path.join(CWD, p))) {
400
+ if (fs.existsSync(path.join(APP_HOME, p))) {
196
401
  filesToArchive.push(p);
197
402
  console.log(` - Found: ${p}`);
198
403
  }
@@ -207,12 +412,12 @@ async function exportData() {
207
412
  await tar.c(
208
413
  {
209
414
  gzip: true,
210
- file: EXPORT_FILENAME,
211
- cwd: CWD
415
+ file: path.join(APP_HOME, EXPORT_FILENAME),
416
+ cwd: APP_HOME
212
417
  },
213
418
  filesToArchive
214
419
  );
215
- console.log(`✅ Export successful! Backup created at: ${path.join(CWD, EXPORT_FILENAME)}`);
420
+ console.log(`✅ Export successful! Backup created at: ${path.join(APP_HOME, EXPORT_FILENAME)}`);
216
421
  console.log(` Copy this file to your new machine to import.`);
217
422
  } catch (err) {
218
423
  console.error('❌ Export failed:', err);
@@ -220,11 +425,12 @@ async function exportData() {
220
425
  }
221
426
 
222
427
  async function importData() {
223
- const backupPath = path.join(CWD, EXPORT_FILENAME);
428
+ ensureAppHome();
429
+ const backupPath = path.join(APP_HOME, EXPORT_FILENAME);
224
430
 
225
431
  if (!fs.existsSync(backupPath)) {
226
432
  console.error(`❌ Backup file not found: ${backupPath}`);
227
- console.log(` Please ensure '${EXPORT_FILENAME}' is in the current directory.`);
433
+ console.log(` Please ensure '${EXPORT_FILENAME}' is in ${APP_HOME}`);
228
434
  return;
229
435
  }
230
436
 
@@ -238,13 +444,13 @@ async function importData() {
238
444
  if (answer === 'y' || answer === 'yes') {
239
445
  try {
240
446
  await tar.x({
241
- file: EXPORT_FILENAME,
242
- cwd: CWD
447
+ file: backupPath,
448
+ cwd: APP_HOME
243
449
  });
244
450
  console.log('✅ Import successful! Data restored.');
245
451
 
246
452
  // Auto-configure workspace path in config.json
247
- const configPath = path.join(CWD, 'config.json');
453
+ const configPath = path.join(APP_HOME, 'config.json');
248
454
  if (fs.existsSync(configPath)) {
249
455
  try {
250
456
  const configContent = fs.readFileSync(configPath, 'utf-8');
@@ -252,20 +458,20 @@ async function importData() {
252
458
 
253
459
  // Update workspace to current directory
254
460
  const oldWorkspace = config.workspace;
255
- config.workspace = CWD;
461
+ config.workspace = APP_HOME;
256
462
 
257
463
  // Also update System Prompt if it contains the old workspace path
258
464
  if (config.systemPrompt && typeof config.systemPrompt === 'string') {
259
465
  if (oldWorkspace && config.systemPrompt.includes(oldWorkspace)) {
260
- config.systemPrompt = config.systemPrompt.replace(oldWorkspace, CWD);
466
+ config.systemPrompt = config.systemPrompt.replace(oldWorkspace, APP_HOME);
261
467
  } else if (config.systemPrompt.includes('Current Workspace:')) {
262
468
  // Fallback regex replacement if exact string match fails
263
- config.systemPrompt = config.systemPrompt.replace(/Current Workspace: .*/, `Current Workspace: ${CWD}`);
469
+ config.systemPrompt = config.systemPrompt.replace(/Current Workspace: .*/, `Current Workspace: ${APP_HOME}`);
264
470
  }
265
471
  }
266
472
 
267
473
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
268
- console.log(`✅ Auto-configured workspace path to: ${CWD}`);
474
+ console.log(`✅ Auto-configured workspace path to: ${APP_HOME}`);
269
475
  } catch (e) {
270
476
  console.warn('⚠️ Failed to auto-update config.json path:', e);
271
477
  }
@@ -283,12 +489,15 @@ async function importData() {
283
489
  }
284
490
 
285
491
  if (command === 'start') {
492
+ ensureAppHome();
493
+ ensureDefaultDirs();
494
+ ensureDefaultConfig();
286
495
  console.log('Starting xiaozuoAssistant...');
287
496
 
288
497
  const serverPath = path.join(packageRoot, 'dist', 'server', 'index.js');
289
498
 
290
- // Ensure logs directory exists in CWD
291
- const logDir = path.join(CWD, 'logs');
499
+ // Ensure logs directory exists in APP_HOME
500
+ const logDir = path.join(APP_HOME, 'logs');
292
501
  if (!fs.existsSync(logDir)) {
293
502
  try {
294
503
  fs.mkdirSync(logDir, { recursive: true });
@@ -339,7 +548,7 @@ if (command === 'start') {
339
548
  const child = spawn('node', [serverPath, ...args.slice(1)], {
340
549
  detached: true, // Allow child to run independently
341
550
  stdio: ['ignore', out, err], // Disconnect stdin, redirect stdout/stderr
342
- cwd: CWD, // Run in user's current directory
551
+ cwd: APP_HOME, // Run in unified app home
343
552
  env: {
344
553
  ...process.env,
345
554
  NODE_ENV: 'production'
@@ -408,6 +617,8 @@ if (command === 'start') {
408
617
  process.exit(1);
409
618
  }
410
619
  } else if (command === 'doctor') {
620
+ ensureAppHome();
621
+ ensureDefaultDirs();
411
622
  console.log('Running doctor check...');
412
623
  try {
413
624
  const pkgPath = path.join(packageRoot, 'package.json');
@@ -420,11 +631,19 @@ if (command === 'start') {
420
631
  console.log('Package Root:', packageRoot);
421
632
  console.log('Node Version:', process.version);
422
633
  console.log('Doctor check complete.');
634
+ } else if (command === 'version') {
635
+ ensureAppHome();
636
+ printVersion();
637
+ } else if (command === 'update') {
638
+ updateApp();
639
+ } else if (command === 'remove') {
640
+ removeApp();
423
641
  } else if (command === 'export') {
424
642
  exportData();
425
643
  } else if (command === 'import') {
426
644
  importData();
427
645
  } else if (command === 'stop') {
646
+ ensureAppHome();
428
647
  stopServer();
429
648
  } else {
430
649
  console.log('Usage: xiaozuoAssistant <command>');
@@ -432,6 +651,12 @@ if (command === 'start') {
432
651
  console.log(' start Start the xiaozuoAssistant server');
433
652
  console.log(' stop Stop the xiaozuoAssistant server');
434
653
  console.log(' doctor Check the health and configuration');
654
+ console.log(' version Print package version and environment');
655
+ console.log(' update Update xiaozuoassistant and auto-restart if running');
656
+ console.log(' remove Uninstall and delete data in current directory');
435
657
  console.log(' export Backup local data (config, memories) to a file');
436
658
  console.log(' import Restore data from a backup file');
659
+ console.log('');
660
+ console.log(`Data Dir: ${APP_HOME}`);
661
+ console.log(' Use --home <path> or env XIAOZUOASSISTANT_HOME to override.');
437
662
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "xiaozuoassistant",
3
3
  "private": false,
4
4
  "description": "Your personal, locally-hosted AI assistant for office productivity.",
5
- "version": "0.1.45",
5
+ "version": "0.1.48",
6
6
  "author": "mantle.lau",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -25,6 +25,7 @@
25
25
  "files": [
26
26
  "dist",
27
27
  "bin",
28
+ "scripts",
28
29
  "public",
29
30
  "config.json",
30
31
  "README.md"
@@ -37,7 +38,7 @@
37
38
  "check": "tsc --noEmit",
38
39
  "server:dev": "nodemon --watch src --watch config.json --exec tsx src/index.ts",
39
40
  "dev": "concurrently \"npm run client:dev\" \"npm run server:dev\"",
40
- "postinstall": "cp node_modules/@lancedb/lancedb-darwin-x64/lancedb.darwin-x64.node node_modules/@lancedb/lancedb/dist/ 2>/dev/null || true && npm rebuild better-sqlite3"
41
+ "postinstall": "node scripts/init-app-home.cjs || true && cp node_modules/@lancedb/lancedb-darwin-x64/lancedb.darwin-x64.node node_modules/@lancedb/lancedb/dist/ 2>/dev/null || true && npm rebuild better-sqlite3"
41
42
  },
42
43
  "dependencies": {
43
44
  "@lancedb/lancedb": "0.22.3",
@@ -0,0 +1,43 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ function guessHomeFromSudoUser() {
6
+ const sudoUser = process.env.SUDO_USER;
7
+ if (!sudoUser) return null;
8
+
9
+ const candidates = [
10
+ path.join('/Users', sudoUser),
11
+ path.join('/home', sudoUser)
12
+ ];
13
+
14
+ for (const p of candidates) {
15
+ try {
16
+ if (fs.existsSync(p)) return p;
17
+ } catch {
18
+ // ignore
19
+ }
20
+ }
21
+
22
+ return null;
23
+ }
24
+
25
+ function resolveTargetHome() {
26
+ const guessed = guessHomeFromSudoUser();
27
+ if (guessed) return guessed;
28
+ return os.homedir();
29
+ }
30
+
31
+ function main() {
32
+ const home = resolveTargetHome();
33
+ const appHome = path.join(home, '.xiaozuoassistant');
34
+
35
+ try {
36
+ fs.mkdirSync(appHome, { recursive: true });
37
+ } catch {
38
+ process.exit(0);
39
+ }
40
+ }
41
+
42
+ main();
43
+