kushi-agents 4.2.0 → 4.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kushi-agents",
3
- "version": "4.2.0",
3
+ "version": "4.2.2",
4
4
  "description": "Install Kushi — multi-source project evidence agent with snapshot+stream capture across Email, Teams, OneNote, SharePoint, Meetings, CRM, ADO. WorkIQ-only for M365 sources (Graph / m365_* FORBIDDEN as fallbacks; user-paste is first-class). Host-agnostic.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,6 +42,11 @@
42
42
  "url": "https://github.com/gim-home/kushi/issues"
43
43
  },
44
44
  "license": "MIT",
45
+ "scripts": {
46
+ "test": "node --test src/check-workiq.test.mjs",
47
+ "smoke": "node scripts/smoke.mjs",
48
+ "prepublishOnly": "npm test && npm run smoke"
49
+ },
45
50
  "publishConfig": {
46
51
  "access": "public"
47
52
  }
@@ -61,12 +61,32 @@ function findOnPath(name) {
61
61
  const args = isWin ? [name] : ['-v', name];
62
62
  const res = spawnSync(cmd, args, { encoding: 'utf-8', shell: !isWin });
63
63
  if (res.status !== 0) return null;
64
- const first = (res.stdout || '').split(/\r?\n/).map((s) => s.trim()).find(Boolean);
65
- return first || null;
64
+ const all = (res.stdout || '')
65
+ .split(/\r?\n/)
66
+ .map((s) => s.trim())
67
+ .filter(Boolean);
68
+ if (!isWin) return all[0] || null;
69
+ // Windows `where` may return multiple matches (e.g. npm shims produce both
70
+ // `workiq` (bash script, no extension) and `workiq.cmd`). Prefer
71
+ // executable extensions in priority order; fall back to the first match.
72
+ const priority = ['.cmd', '.exe', '.bat', '.ps1'];
73
+ for (const ext of priority) {
74
+ const hit = all.find((p) => p.toLowerCase().endsWith(ext));
75
+ if (hit) return hit;
76
+ }
77
+ return all[0] || null;
66
78
  }
67
79
 
68
80
  function runVersion(binPath) {
69
- const res = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10_000 });
81
+ // Windows requires shell:true to spawn .cmd / .bat / shim files. The DEP0190
82
+ // warning is harmless here because we control both binPath (resolved via
83
+ // `where` / explicit --workiq-path) and args (literal ['--version']).
84
+ const isWin = process.platform === 'win32';
85
+ const res = spawnSync(binPath, ['--version'], {
86
+ encoding: 'utf-8',
87
+ timeout: 10_000,
88
+ shell: isWin,
89
+ });
70
90
  if (res.status !== 0) {
71
91
  return {
72
92
  ok: false,
@@ -0,0 +1,93 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import fs from 'node:fs';
8
+
9
+ import { checkWorkIQ } from './check-workiq.mjs';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const isWin = process.platform === 'win32';
13
+
14
+ function makeTempBin(name, body) {
15
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kushi-test-'));
16
+ const full = path.join(dir, name);
17
+ fs.writeFileSync(full, body);
18
+ if (!isWin) fs.chmodSync(full, 0o755);
19
+ return { dir, full };
20
+ }
21
+
22
+ test('checkWorkIQ: --workiq-path pointing at non-existent file returns path-invalid', () => {
23
+ const r = checkWorkIQ({ workiqPath: '/definitely/does/not/exist/workiq' });
24
+ assert.equal(r.ok, false);
25
+ assert.equal(r.reason, 'path-invalid');
26
+ assert.match(r.hint, /does not point at an existing file/i);
27
+ });
28
+
29
+ test('checkWorkIQ: --workiq-path pointing at a runnable script returns ok with version', () => {
30
+ // Build a tiny fake that prints a version string and exits 0.
31
+ const name = isWin ? 'fake-workiq.cmd' : 'fake-workiq';
32
+ const body = isWin
33
+ ? '@echo off\r\necho 9.9.9-fake\r\nexit /b 0\r\n'
34
+ : '#!/usr/bin/env bash\necho 9.9.9-fake\nexit 0\n';
35
+ const { full, dir } = makeTempBin(name, body);
36
+ try {
37
+ const r = checkWorkIQ({ workiqPath: full });
38
+ assert.equal(r.ok, true, `expected ok, got ${JSON.stringify(r)}`);
39
+ assert.equal(r.path, full);
40
+ assert.match(r.version, /9\.9\.9-fake/);
41
+ } finally {
42
+ fs.rmSync(dir, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ test('checkWorkIQ: --workiq-path pointing at a failing binary returns not-executable', () => {
47
+ const name = isWin ? 'broken-workiq.cmd' : 'broken-workiq';
48
+ const body = isWin
49
+ ? '@echo off\r\nexit /b 17\r\n'
50
+ : '#!/usr/bin/env bash\nexit 17\n';
51
+ const { full, dir } = makeTempBin(name, body);
52
+ try {
53
+ const r = checkWorkIQ({ workiqPath: full });
54
+ assert.equal(r.ok, false);
55
+ assert.equal(r.reason, 'not-executable');
56
+ assert.match(r.hint, /failed/i);
57
+ } finally {
58
+ fs.rmSync(dir, { recursive: true, force: true });
59
+ }
60
+ });
61
+
62
+ test('checkWorkIQ: with no PATH match and no override returns not-found with platform hint', () => {
63
+ const savedPath = process.env.PATH;
64
+ const savedPathCap = process.env.Path;
65
+ const savedHome = process.env.HOME;
66
+ const savedProfile = process.env.USERPROFILE;
67
+ try {
68
+ const fakeRoot = path.join(os.tmpdir(), `kushi-empty-${Date.now()}`);
69
+ process.env.PATH = fakeRoot;
70
+ if (isWin) process.env.Path = fakeRoot;
71
+ process.env.HOME = fakeRoot;
72
+ if (isWin) process.env.USERPROFILE = fakeRoot;
73
+
74
+ const r = checkWorkIQ();
75
+ assert.equal(r.ok, false);
76
+ assert.equal(r.reason, 'not-found');
77
+ if (isWin) {
78
+ assert.match(r.hint, /winget install Microsoft\.WorkIQ/);
79
+ } else if (process.platform === 'darwin') {
80
+ assert.match(r.hint, /brew install --cask microsoft-workiq/);
81
+ } else {
82
+ assert.match(r.hint, /install-workiq/);
83
+ }
84
+ } finally {
85
+ process.env.PATH = savedPath;
86
+ if (savedPathCap !== undefined) process.env.Path = savedPathCap;
87
+ else delete process.env.Path;
88
+ if (savedHome !== undefined) process.env.HOME = savedHome;
89
+ else delete process.env.HOME;
90
+ if (savedProfile !== undefined) process.env.USERPROFILE = savedProfile;
91
+ else delete process.env.USERPROFILE;
92
+ }
93
+ });