kushi-agents 4.1.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/bin/cli.mjs +10 -0
- package/package.json +6 -1
- package/plugin/instructions/identity-resolution.instructions.md +66 -0
- package/plugin/instructions/workiq-only.instructions.md +1 -0
- package/plugin/skills/bootstrap-project/SKILL.md +16 -1
- package/plugin/templates/init/project-evidence.template.yml +17 -7
- package/src/check-workiq.mjs +145 -0
- package/src/check-workiq.test.mjs +93 -0
- package/src/main.mjs +54 -0
package/bin/cli.mjs
CHANGED
|
@@ -29,6 +29,13 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
29
29
|
check (useful for scripted or agent-driven installs)
|
|
30
30
|
--no-settings Skip .vscode/settings.json update (vscode target only)
|
|
31
31
|
--no-instructions Skip .github/copilot-instructions.md merge (vscode target only)
|
|
32
|
+
|
|
33
|
+
WorkIQ (REQUIRED — Kushi cannot pull evidence without it):
|
|
34
|
+
--with-workiq Auto-install WorkIQ via winget (Windows) / brew (macOS)
|
|
35
|
+
--workiq-path <abs> Use this explicit path to the workiq binary
|
|
36
|
+
--skip-workiq-check Bypass the WorkIQ pre-flight check (CI / inspection only —
|
|
37
|
+
bootstrap/refresh will block until WorkIQ is installed)
|
|
38
|
+
|
|
32
39
|
--help, -h Show this help
|
|
33
40
|
|
|
34
41
|
After install, talk to Kushi:
|
|
@@ -62,6 +69,9 @@ const options = {
|
|
|
62
69
|
noInstructions: args.includes('--no-instructions'),
|
|
63
70
|
target,
|
|
64
71
|
profile: getFlag('--profile'),
|
|
72
|
+
withWorkiq: args.includes('--with-workiq'),
|
|
73
|
+
workiqPath: getFlag('--workiq-path'),
|
|
74
|
+
skipWorkiqCheck: args.includes('--skip-workiq-check'),
|
|
65
75
|
};
|
|
66
76
|
|
|
67
77
|
main(options).catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kushi-agents",
|
|
3
|
-
"version": "4.
|
|
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
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**"
|
|
3
|
+
description: "Identity auto-resolution — Kushi never asks the user for alias / email / display_name. On the first bootstrap (or whenever those fields are <auto> / placeholder in .kushi/config/project-evidence.yml), Kushi resolves them from WorkIQ in a single call, persists them, and continues. Skipped entirely if the user has set explicit values."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Identity Resolution — Don't Ask, Probe
|
|
7
|
+
|
|
8
|
+
Kushi must not prompt the user for `alias`, `email`, or `display_name`. These are derivable from WorkIQ in one call, and asking is poor UX.
|
|
9
|
+
|
|
10
|
+
## When to resolve
|
|
11
|
+
|
|
12
|
+
On the **first step of every prompt** that reads contributor identity (bootstrap, refresh, aggregate, ask, fde-*, propose-ado, apply-ado), check `<workspace>/.kushi/config/project-evidence.yml`:
|
|
13
|
+
|
|
14
|
+
* If `alias`, `email`, or `display_name` is **missing**, set to `<auto>`, or matches a placeholder pattern (`<your-alias>`, `<Your Full Name>`, `your.email@example.com`) → **resolve from WorkIQ**.
|
|
15
|
+
* If all three are explicit non-placeholder values → **skip**. Respect the user's override.
|
|
16
|
+
|
|
17
|
+
## How to resolve
|
|
18
|
+
|
|
19
|
+
Single WorkIQ call:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
workiq ask -q "Who am I? Return my UPN (email), display name, and mail nickname as JSON: {\"upn\":\"...\",\"displayName\":\"...\",\"mailNickname\":\"...\"}"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Map the response:
|
|
26
|
+
|
|
27
|
+
| Config field | Source | Fallback if missing |
|
|
28
|
+
|----------------|-------------------------------------|--------------------------------------|
|
|
29
|
+
| `email` | `upn` | Block — must be present |
|
|
30
|
+
| `display_name` | `displayName` | Same as `alias` |
|
|
31
|
+
| `alias` | `mailNickname` | `upn.split('@')[0]` (lowercase) |
|
|
32
|
+
|
|
33
|
+
## After resolution
|
|
34
|
+
|
|
35
|
+
1. **Persist back** to `<workspace>/.kushi/config/project-evidence.yml` (preserving comments + indentation). The user sees the resolved values on next open; no surprise.
|
|
36
|
+
2. **Echo back** to the user, one line:
|
|
37
|
+
> ✓ Identity: `Alex Smith <alex@microsoft.com>` (alias=`alex`). Edit `.kushi/config/project-evidence.yml` to override.
|
|
38
|
+
3. **Continue** the prompt.
|
|
39
|
+
|
|
40
|
+
## Failure modes
|
|
41
|
+
|
|
42
|
+
| Scenario | Behavior |
|
|
43
|
+
|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
|
|
44
|
+
| WorkIQ returns auth error | Block: "Sign in to WorkIQ first: `workiq accept-eula && workiq ask -q ping`". Do not proceed. |
|
|
45
|
+
| WorkIQ returns empty / NO_RESULTS | Block: "WorkIQ could not resolve your identity. Try `workiq ask -q 'who am I'` and retry." |
|
|
46
|
+
| WorkIQ binary missing | This should already have been caught by the installer's pre-flight. If reached, block with the same install hint. |
|
|
47
|
+
| User has explicit non-placeholder values | Skip resolution entirely. Never overwrite user-set values. |
|
|
48
|
+
| Alias collision with another contributor | Bootstrap detects existing `Evidence/<alias>/` whose `contributors.yml` records a different email → ask the user to disambiguate (suggest `<alias>-<tenant-prefix>` e.g. `alex-ms`). |
|
|
49
|
+
|
|
50
|
+
## What NOT to do
|
|
51
|
+
|
|
52
|
+
* Do NOT ask the user `What alias should Kushi use?`. The legacy onboarding prompt is gone.
|
|
53
|
+
* Do NOT call `m365_*` / Graph as a fallback. WorkIQ is the single source of identity truth (`workiq-first.instructions.md`).
|
|
54
|
+
* Do NOT resolve on every run. Once persisted, the config values are authoritative.
|
|
55
|
+
|
|
56
|
+
## Integration with other doctrine
|
|
57
|
+
|
|
58
|
+
* `workiq-first.instructions.md` — identity resolution is the canonical example of WorkIQ-first. Add to the inventory.
|
|
59
|
+
* `bootstrap-project` SKILL — Step 0 is identity resolution. Step 1 is project context.
|
|
60
|
+
* `tracking.instructions.md` — the resolved identity goes into the tracking artifact's frontmatter under `actor:`.
|
|
61
|
+
|
|
62
|
+
## References
|
|
63
|
+
|
|
64
|
+
* `workiq-first.instructions.md` — the parent doctrine.
|
|
65
|
+
* `engagement-root-resolution.instructions.md` — `projects_root` resolution (separate from identity).
|
|
66
|
+
* `templates/init/project-evidence.template.yml` — defaults to `<auto>` for these three fields.
|
|
@@ -28,6 +28,7 @@ Applies to ALL evidence retrieval from these M365 sources:
|
|
|
28
28
|
- Email bodies and attachments
|
|
29
29
|
- SharePoint file contents (when text extraction is needed)
|
|
30
30
|
- Calendar events (when not already known by id)
|
|
31
|
+
- **Contributor identity** (UPN / displayName / mailNickname) — see `identity-resolution.instructions.md` for the canonical "who am I?" probe.
|
|
31
32
|
|
|
32
33
|
**Out of scope** (these are NOT WorkIQ — they remain on their direct paths):
|
|
33
34
|
|
|
@@ -35,10 +35,25 @@ After every run (success or coverage-gaps), write `<project>/bootstrap-status.md
|
|
|
35
35
|
|
|
36
36
|
- `<project>` — engagement name (fuzzy-matched per `engagement-root-resolution.instructions.md`).
|
|
37
37
|
- `<window>` — defaults to **last 30 days** (override with `last N days` / `since <date>`).
|
|
38
|
-
- Implicit: current contributor `<alias>` (
|
|
38
|
+
- Implicit: current contributor `<alias>` (resolved in Step 0 — see below).
|
|
39
39
|
|
|
40
40
|
## Steps
|
|
41
41
|
|
|
42
|
+
### Step 0 — Identity resolution (REQUIRED, never asks the user)
|
|
43
|
+
|
|
44
|
+
Per `identity-resolution.instructions.md`. Read `<workspace>/.kushi/config/project-evidence.yml`:
|
|
45
|
+
|
|
46
|
+
* If `alias`, `email`, or `display_name` is missing / `<auto>` / matches a placeholder → call WorkIQ once:
|
|
47
|
+
```
|
|
48
|
+
workiq ask -q "Who am I? Return UPN, displayName, mailNickname as JSON."
|
|
49
|
+
```
|
|
50
|
+
Map `upn → email`, `displayName → display_name`, `mailNickname → alias` (fallback `email.split('@')[0]`).
|
|
51
|
+
* Persist resolved values back to the YAML (preserve comments).
|
|
52
|
+
* Echo one line: `✓ Identity: <displayName> <<UPN>> (alias=<alias>). Edit .kushi/config/project-evidence.yml to override.`
|
|
53
|
+
* If all three are already explicit non-placeholder values → skip silently.
|
|
54
|
+
|
|
55
|
+
Hard stop if WorkIQ returns auth error or empty — print the WorkIQ sign-in hint and exit. Never fall back to asking the user.
|
|
56
|
+
|
|
42
57
|
### Step 1 — Machine preflight (SETUP)
|
|
43
58
|
|
|
44
59
|
Verify in order. Stop on hard failures.
|
|
@@ -5,14 +5,23 @@
|
|
|
5
5
|
# maintains their own. Nothing here is a secret, but it IS personal —
|
|
6
6
|
# add `.kushi/config/` to .gitignore before committing the rest of .kushi/.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
8
|
+
# IDENTITY — auto-detected on first bootstrap.
|
|
9
|
+
# On the first run of `bootstrap <project>`, Kushi asks WorkIQ "who am I?"
|
|
10
|
+
# and fills these three fields in for you (UPN, displayName, mailNickname).
|
|
11
|
+
# You only need to override them if you want a different folder name or
|
|
12
|
+
# a friendlier display label. Leave them at the placeholder values to
|
|
13
|
+
# trigger auto-detection.
|
|
9
14
|
|
|
10
|
-
#
|
|
11
|
-
|
|
15
|
+
# Optional override — short id used as your Evidence/ subfolder name.
|
|
16
|
+
# Leave as <auto> to derive from the part before "@" in your email.
|
|
17
|
+
alias: <auto>
|
|
12
18
|
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
# Optional override — friendly label used in run logs.
|
|
20
|
+
# Leave as <auto> to pull from WorkIQ's displayName.
|
|
21
|
+
display_name: <auto>
|
|
22
|
+
|
|
23
|
+
# Optional override — your work email. Leave as <auto> to pull from WorkIQ.
|
|
24
|
+
email: <auto>
|
|
16
25
|
|
|
17
26
|
# FILL ME IN — where your engagement-root lives. The parent folder containing
|
|
18
27
|
# one subfolder per project (typically synced from a team SharePoint library).
|
|
@@ -35,4 +44,5 @@ active_projects:
|
|
|
35
44
|
# 2. `workiq` on PATH
|
|
36
45
|
# 3. ~/.kushi/bin/workiq.cmd (Windows) / ~/.kushi/bin/workiq (Linux/macOS)
|
|
37
46
|
# workiq:
|
|
38
|
-
# cli_path: 'C:\Users\<you>\.kushi\bin\workiq.cmd'
|
|
47
|
+
# cli_path: 'C:\Users\<you>\.kushi\bin\workiq.cmd'
|
|
48
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Probe for a working WorkIQ install.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order:
|
|
10
|
+
* 1. explicit absolute path supplied via --workiq-path / opts.workiqPath
|
|
11
|
+
* 2. `workiq` on PATH (via `where` on Windows, `command -v` elsewhere)
|
|
12
|
+
* 3. Kushi-managed bin: ~/.kushi/bin/workiq.cmd (Win) or ~/.kushi/bin/workiq
|
|
13
|
+
*
|
|
14
|
+
* Returns:
|
|
15
|
+
* { ok: true, path, version } — usable WorkIQ found
|
|
16
|
+
* { ok: false, reason, hint } — not found / not runnable
|
|
17
|
+
*
|
|
18
|
+
* Reasons:
|
|
19
|
+
* 'not-found' — no workiq binary anywhere we looked
|
|
20
|
+
* 'path-invalid' — explicit --workiq-path was given but the file doesn't exist
|
|
21
|
+
* 'not-executable' — found but `--version` failed (corrupt install, missing deps, etc.)
|
|
22
|
+
*/
|
|
23
|
+
export function checkWorkIQ(opts = {}) {
|
|
24
|
+
const isWin = process.platform === 'win32';
|
|
25
|
+
const homeBin = path.join(
|
|
26
|
+
os.homedir(),
|
|
27
|
+
'.kushi',
|
|
28
|
+
'bin',
|
|
29
|
+
isWin ? 'workiq.cmd' : 'workiq',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (opts.workiqPath) {
|
|
33
|
+
const abs = path.resolve(opts.workiqPath);
|
|
34
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
reason: 'path-invalid',
|
|
38
|
+
hint: `--workiq-path "${opts.workiqPath}" does not point at an existing file.`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return runVersion(abs);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const onPath = findOnPath('workiq');
|
|
45
|
+
if (onPath) return runVersion(onPath);
|
|
46
|
+
|
|
47
|
+
if (existsSync(homeBin) && statSync(homeBin).isFile()) {
|
|
48
|
+
return runVersion(homeBin);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: 'not-found',
|
|
54
|
+
hint: installHint(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findOnPath(name) {
|
|
59
|
+
const isWin = process.platform === 'win32';
|
|
60
|
+
const cmd = isWin ? 'where' : 'command';
|
|
61
|
+
const args = isWin ? [name] : ['-v', name];
|
|
62
|
+
const res = spawnSync(cmd, args, { encoding: 'utf-8', shell: !isWin });
|
|
63
|
+
if (res.status !== 0) return 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;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runVersion(binPath) {
|
|
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
|
+
});
|
|
90
|
+
if (res.status !== 0) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
reason: 'not-executable',
|
|
94
|
+
hint: `Found WorkIQ at ${binPath} but \`workiq --version\` failed (exit ${res.status}). Try reinstalling. ${installHint()}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const version = (res.stdout || res.stderr || '').trim().split(/\r?\n/)[0] || 'unknown';
|
|
98
|
+
return { ok: true, path: binPath, version };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function installHint() {
|
|
102
|
+
const plat = process.platform;
|
|
103
|
+
if (plat === 'win32') {
|
|
104
|
+
return 'Install with: winget install Microsoft.WorkIQ';
|
|
105
|
+
}
|
|
106
|
+
if (plat === 'darwin') {
|
|
107
|
+
return 'Install with: brew install --cask microsoft-workiq';
|
|
108
|
+
}
|
|
109
|
+
return 'See https://gim-home.github.io/kushi/getting-started/install-workiq/ for Linux instructions.';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Best-effort auto-install via the platform's package manager.
|
|
114
|
+
* Only invoked when --with-workiq is passed. Non-blocking on failure —
|
|
115
|
+
* caller decides whether to hard-fail.
|
|
116
|
+
*
|
|
117
|
+
* Returns { ok, reason, output }.
|
|
118
|
+
*/
|
|
119
|
+
export function tryInstallWorkIQ() {
|
|
120
|
+
const plat = process.platform;
|
|
121
|
+
let cmd, args;
|
|
122
|
+
if (plat === 'win32') {
|
|
123
|
+
cmd = 'winget';
|
|
124
|
+
args = ['install', '--id', 'Microsoft.WorkIQ', '-e', '--accept-package-agreements', '--accept-source-agreements'];
|
|
125
|
+
} else if (plat === 'darwin') {
|
|
126
|
+
cmd = 'brew';
|
|
127
|
+
args = ['install', '--cask', 'microsoft-workiq'];
|
|
128
|
+
} else {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
reason: 'unsupported-platform',
|
|
132
|
+
output: 'Auto-install is not supported on Linux. See https://gim-home.github.io/kushi/getting-started/install-workiq/',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const res = spawnSync(cmd, args, { encoding: 'utf-8', stdio: 'inherit' });
|
|
137
|
+
if (res.error || res.status !== 0) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
reason: 'install-failed',
|
|
141
|
+
output: res.error ? res.error.message : `${cmd} exited with status ${res.status}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { ok: true, output: `${cmd} ${args.join(' ')} succeeded` };
|
|
145
|
+
}
|
|
@@ -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
|
+
});
|
package/src/main.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { copyAssets, copyProjectFiles } from './copy-assets.mjs';
|
|
|
17
17
|
import { mergeSettings } from './settings.mjs';
|
|
18
18
|
import { mergeCopilotInstructions } from './copilot-instructions.mjs';
|
|
19
19
|
import { seedConfig } from './seed-config.mjs';
|
|
20
|
+
import { checkWorkIQ, tryInstallWorkIQ } from './check-workiq.mjs';
|
|
20
21
|
import {
|
|
21
22
|
resolveProfile,
|
|
22
23
|
makeIncludeFilter,
|
|
@@ -71,6 +72,9 @@ export async function main(options = {}) {
|
|
|
71
72
|
console.log(` Profile chain: ${resolved.chain.join(' -> ')}`);
|
|
72
73
|
console.log(` ${resolved.description}\n`);
|
|
73
74
|
|
|
75
|
+
// Hard prerequisite: WorkIQ. Kushi cannot pull evidence without it.
|
|
76
|
+
await preflightWorkIQ(options);
|
|
77
|
+
|
|
74
78
|
if (target === TARGET_CLAWPILOT) {
|
|
75
79
|
await installClawpilot(options, resolved, version);
|
|
76
80
|
} else {
|
|
@@ -290,6 +294,56 @@ async function confirmOverwriteIfExists(fullDest, displayDest, force) {
|
|
|
290
294
|
}
|
|
291
295
|
}
|
|
292
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Hard prerequisite: WorkIQ must be installed and runnable before Kushi will
|
|
299
|
+
* copy any assets. Kushi's pull-* skills are useless without it. Three modes:
|
|
300
|
+
*
|
|
301
|
+
* --skip-workiq-check → print a warning and continue (CI / asset-inspection only)
|
|
302
|
+
* --with-workiq → attempt auto-install via winget/brew first, then re-check
|
|
303
|
+
* default → probe; on failure, print install hint and exit 1
|
|
304
|
+
*
|
|
305
|
+
* Always prints the resolved WorkIQ path + version on success so users have a
|
|
306
|
+
* record in their terminal scrollback.
|
|
307
|
+
*/
|
|
308
|
+
async function preflightWorkIQ(options) {
|
|
309
|
+
if (options.skipWorkiqCheck) {
|
|
310
|
+
console.warn(
|
|
311
|
+
' ⚠️ WorkIQ pre-flight skipped (--skip-workiq-check).',
|
|
312
|
+
);
|
|
313
|
+
console.warn(
|
|
314
|
+
' Kushi will install, but bootstrap/refresh will block until WorkIQ is present.\n',
|
|
315
|
+
);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let result = checkWorkIQ({ workiqPath: options.workiqPath });
|
|
320
|
+
|
|
321
|
+
if (!result.ok && options.withWorkiq) {
|
|
322
|
+
console.log(' WorkIQ not found — attempting auto-install (--with-workiq)…\n');
|
|
323
|
+
const ins = tryInstallWorkIQ();
|
|
324
|
+
if (!ins.ok) {
|
|
325
|
+
console.error(`\n ✖ Auto-install failed: ${ins.output}\n`);
|
|
326
|
+
console.error(` ${result.hint || ''}\n`);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
result = checkWorkIQ({ workiqPath: options.workiqPath });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!result.ok) {
|
|
333
|
+
console.error('\n ✖ WorkIQ is required and was not found.\n');
|
|
334
|
+
console.error(' Kushi cannot capture evidence without WorkIQ. Install it first:\n');
|
|
335
|
+
console.error(` ${result.hint}\n`);
|
|
336
|
+
console.error(' Then run `workiq ask -q "ping"` to confirm sign-in, and re-run this installer.\n');
|
|
337
|
+
console.error(' Escape hatches (NOT recommended):');
|
|
338
|
+
console.error(' • --workiq-path <abs> supply a non-standard install path');
|
|
339
|
+
console.error(' • --with-workiq let the installer try winget/brew');
|
|
340
|
+
console.error(' • --skip-workiq-check install assets anyway (bootstrap will still block)\n');
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log(` ✓ WorkIQ detected at ${result.path} (${result.version})\n`);
|
|
345
|
+
}
|
|
346
|
+
|
|
293
347
|
/**
|
|
294
348
|
* Detect whether the cwd looks like a sane install target and, if not, print
|
|
295
349
|
* an actionable message. Three cases:
|