kushi-agents 5.7.0 → 5.7.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/README.md +1 -0
- package/bin/cli.mjs +7 -0
- package/package.json +2 -2
- package/plugin/learnings/cross-cutting.md +59 -0
- package/plugin/lib/Get-KushiConfig.ps1 +6 -6
- package/plugin/runners/lib/workiq.mjs +53 -3
- package/src/get-kushi-config.test.mjs +69 -0
- package/src/main.mjs +1 -1
- package/src/multi-host.mjs +35 -0
package/README.md
CHANGED
|
@@ -260,6 +260,7 @@ npx kushi-agents --clawpilot # Clawpilot only
|
|
|
260
260
|
npx kushi-agents --vscode # VS Code Chat only
|
|
261
261
|
|
|
262
262
|
# Install to BOTH at once (auto-detects what's present + targets both)
|
|
263
|
+
# v5.7.1+: when run from inside a project dir, also refreshes <cwd>/.kushi/.
|
|
263
264
|
npx kushi-agents --all-hosts
|
|
264
265
|
|
|
265
266
|
# Uninstall
|
package/bin/cli.mjs
CHANGED
|
@@ -182,10 +182,16 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
182
182
|
--clawpilot Install to ~/.copilot/m-skills/kushi/
|
|
183
183
|
--vscode Install to ~/.vscode/chat/skills/kushi/ (a.k.a. GitHub Copilot Chat)
|
|
184
184
|
--all-hosts Install to BOTH hosts
|
|
185
|
+
--no-workspace Skip the auto workspace install (host install only)
|
|
185
186
|
--uninstall [--clawpilot|--vscode|--all]
|
|
186
187
|
Cleanly remove the kushi install + skills-metadata.json entry
|
|
187
188
|
from the chosen host(s). Default = all detected hosts.
|
|
188
189
|
|
|
190
|
+
Note: when run from inside a project directory (any of: package.json, .git,
|
|
191
|
+
.kushi/, Evidence/, etc.), host installs ALSO refresh the workspace
|
|
192
|
+
.kushi/ install in cwd. One command covers both. Pass --no-workspace to
|
|
193
|
+
suppress, or run from a non-project directory.
|
|
194
|
+
|
|
189
195
|
Workspace install (legacy / default when no host flag is given):
|
|
190
196
|
--target vscode Install to <cwd>/.kushi/ + update .vscode/settings.json [default]
|
|
191
197
|
--target clawpilot Alias for --clawpilot (kept for back-compat)
|
|
@@ -274,6 +280,7 @@ if (wantsVscode || wantsAllHosts || wantsUninstall) {
|
|
|
274
280
|
all,
|
|
275
281
|
uninstall: wantsUninstall,
|
|
276
282
|
profile: getFlag('--profile'),
|
|
283
|
+
includeWorkspace: !args.includes('--no-workspace'),
|
|
277
284
|
}).catch((err) => {
|
|
278
285
|
console.error(`\n ${err.message}\n`);
|
|
279
286
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kushi-agents",
|
|
3
|
-
"version": "5.7.
|
|
3
|
+
"version": "5.7.2",
|
|
4
4
|
"description": "Install Kushi — multi-source project evidence agent with Comprehensive Structured Capture (CSC) into weekly-only files across Email, Teams, OneNote, Loop, SharePoint, Meetings, CRM, ADO. Meetings retain a sibling verbatim/ audit folder. WorkIQ-only for M365 sources (Graph / m365_* FORBIDDEN as fallbacks; user-paste is first-class). Host-agnostic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"scripts": {
|
|
46
|
-
"test": "node --test src/check-workiq.test.mjs src/seed-config.test.mjs src/sanitize-workiq-input.test.mjs src/detect-vertex-repo.test.mjs src/vertex-validate.test.mjs src/emit-vertex.e2e.test.mjs src/config-root-resolve.test.mjs src/forbidden-workiq-phrasings.test.mjs src/multi-host-install.test.mjs src/eval-aggregator.test.mjs src/eval-runner.test.mjs src/skill-creator.test.mjs src/skill-checker.test.mjs src/hooks-dispatcher.test.mjs src/parallel-refresh.test.mjs src/otel-emit.test.mjs src/teach.test.mjs src/schema-evolve.test.mjs src/global-wiki.test.mjs src/promote.test.mjs src/doctor.test.mjs src/setup-wizard.test.mjs src/cli-no-args.test.mjs src/cli-no-args-tty.test.mjs src/per-user-files.test.mjs src/layout-portable.test.mjs src/profile-coverage.test.mjs plugin/runners/test/unit/*.test.mjs",
|
|
46
|
+
"test": "node --test src/check-workiq.test.mjs src/seed-config.test.mjs src/sanitize-workiq-input.test.mjs src/detect-vertex-repo.test.mjs src/vertex-validate.test.mjs src/emit-vertex.e2e.test.mjs src/config-root-resolve.test.mjs src/forbidden-workiq-phrasings.test.mjs src/multi-host-install.test.mjs src/eval-aggregator.test.mjs src/eval-runner.test.mjs src/skill-creator.test.mjs src/skill-checker.test.mjs src/hooks-dispatcher.test.mjs src/parallel-refresh.test.mjs src/otel-emit.test.mjs src/teach.test.mjs src/schema-evolve.test.mjs src/global-wiki.test.mjs src/promote.test.mjs src/doctor.test.mjs src/setup-wizard.test.mjs src/cli-no-args.test.mjs src/cli-no-args-tty.test.mjs src/per-user-files.test.mjs src/layout-portable.test.mjs src/profile-coverage.test.mjs src/get-kushi-config.test.mjs plugin/runners/test/unit/*.test.mjs",
|
|
47
47
|
"test:runners": "node --test plugin/runners/test/unit/*.test.mjs",
|
|
48
48
|
"test:runners:integration": "node --test plugin/runners/test/integration/*.test.mjs",
|
|
49
49
|
"test:integration:bootstrap": "node src/bootstrap-dryrun.integration.test.mjs",
|
|
@@ -4,6 +4,65 @@ Newest on top. Format defined in [`README.md`](./README.md). Use this file when
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
### 2026-05-29 — `[switch]` parameter collides with same-name local var in PowerShell
|
|
8
|
+
|
|
9
|
+
**Symptom**: Calling `.\.kushi\lib\Get-KushiConfig.ps1 -Name 'm365-auth' -Raw` threw at the **call site**:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Get-KushiConfig.ps1: Cannot convert the "{ ...full file contents... }" value of type "System.String" to type "System.Management.Automation.SwitchParameter".
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The error wrapped the entire file content as the failed-coercion value — confusing, because the user's invocation only passed `-Name '...' -Raw` (no positional value).
|
|
16
|
+
|
|
17
|
+
**Root cause**: PowerShell variables are **case-insensitive**. The script declared `[switch] $Raw` as a parameter, then used a local `$raw = Get-Content -LiteralPath $resolved -Raw` to read the file. Those are the *same variable*. The string assignment to the typed switch parameter triggered SwitchParameter coercion, which fails on any non-empty string. Crucially, the error is reported at the *call site* (the `&` invocation line), not at the assignment line — so the symptom looked like a parameter-binding bug.
|
|
18
|
+
|
|
19
|
+
**Fix shipped (v5.7.2, 2026-05-29)**: Renamed the local var to `$rawText` everywhere in `Get-KushiConfig.ps1`. Audited all other `[switch]` params in `plugin/lib/*.ps1` for the same collision pattern — none found.
|
|
20
|
+
|
|
21
|
+
**Why it wasn't caught**: 280 unit tests cover only `.mjs` runners. `Get-KushiConfig.ps1` and other shipped `.ps1` libs had **zero** automated test coverage. Added `src/get-kushi-config.test.mjs` (3 tests) that installs kushi into a temp dir and invokes the live shipped script via `pwsh`, asserting no SwitchParameter coercion error in stderr. Now wired into `npm test` (283 tests).
|
|
22
|
+
|
|
23
|
+
**Lesson**: Any `.ps1` file shipped to users needs *integration* tests that invoke the real script via `pwsh`, not just unit tests of the JS that calls it. Avoid using `[switch]` parameter names that collide with common local variable names (`$raw`, `$path`, `$name`, `$content`) — PS case-insensitivity makes the collision invisible until something assigns a value.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### 2026-05-29 — Two issues that silently broke `bootstrap` end-to-end on Windows
|
|
28
|
+
|
|
29
|
+
**Symptom**: After v5.7.0 shipped the auto-chain (bootstrap → discover → refresh), users ran the chain on Windows and saw discover return `skipped_reason: EINVAL` for every source, then refresh exit 0 with zero pulls. The runner *appeared* to be working — `status: ok`, no error message — but no boundaries were filled.
|
|
30
|
+
|
|
31
|
+
**Root causes** (two stacked):
|
|
32
|
+
|
|
33
|
+
1. **Node 20.12+ refuses to spawn `.cmd`/`.bat` files without `shell:true`** (CVE-2024-27980 hardening). `lib/workiq.mjs` was using `spawn(exe, args, { shell: false })` and the Windows workiq distribution ships as a `workiq.cmd` shim. Every spawn returned `EINVAL` immediately. The runner caught it and recorded `skipped_reason: EINVAL` per-source, but didn't escalate — it looked like 7 individual source-not-available skips, not one runtime-level failure.
|
|
34
|
+
2. **`resolveWorkiqBin()` only checked `~/.copilot/bin/workiq.cmd`**. Users who installed WorkIQ via `npm i -g @microsoft/workiq` had it at `C:\Users\<u>\AppData\Roaming\npm\workiq.cmd` — invisible to kushi, even though `workiq --version` worked fine in their shell.
|
|
35
|
+
|
|
36
|
+
**Fixes shipped (v5.7.1, 2026-05-29)**:
|
|
37
|
+
|
|
38
|
+
1. **`lib/workiq.mjs` `runProcess()` routes `.cmd`/`.bat` through `cmd.exe`** with `windowsVerbatimArguments: true`. Verbatim args preserve quoting in long CSC prompts.
|
|
39
|
+
2. **`resolveWorkiqBin()` searches PATH first** (with `PATHEXT`-aware `whichSync`), falls back to `~/.copilot/bin/`. So `npm`-installed workiq is now picked up automatically.
|
|
40
|
+
3. **Lesson for future runners**: a per-source `skipped_reason` in JSON output isn't a substitute for a top-level error. If every source returns the SAME error code, that's a runtime failure, not 7 missing-source signals. Consider adding a `runtime_error` envelope check in discover that flips the top-level `status` to `error` if ≥N sources skip with the same `EXXX` code.
|
|
41
|
+
|
|
42
|
+
**Discovered during**: Soak test of v5.7.0 in `kushi-wp` workspace — discover printed `{"status":"ok",...}` with all 7 sources `skipped_reason: EINVAL`, refresh ran clean, but no boundary was actually filled. Symptom looked like "WorkIQ has nothing for this project" until the user ran `node -e "spawn(...)"` to repro the EINVAL.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### 2026-05-29 — Two-command install (`--all-hosts` then workspace) is the wrong default
|
|
47
|
+
|
|
48
|
+
**Symptom**: User runs:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
npx kushi-agents@latest --all-hosts --profile full
|
|
52
|
+
cd <repo-root>-wp
|
|
53
|
+
npx kushi-agents@latest --profile full
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
…and asks: *"this is weird running both. is there a better way?"*
|
|
57
|
+
|
|
58
|
+
**Root cause**: The CLI dispatched on the presence of host flags. `--all-hosts` took the `runMultiHost()` code path which did host-only installs. `npx kushi-agents` (no flag) took the legacy `main.mjs` workspace-install path. The two paths were mutually exclusive, even though the user's mental model was *"install Kushi everywhere"* — both globally and in the current repo.
|
|
59
|
+
|
|
60
|
+
**Fix shipped (v5.7.1)**: After the host loop in `runMultiHost()`, detect if cwd is a recognized project (uses the existing `findProjectMarker()` from `main.mjs` — `.git`, `package.json`, `.kushi/`, `Evidence/`, etc.). If yes, also call `main({ target: 'vscode', yes: true, force: true })` to refresh the workspace install. Suppress with `--no-workspace`. From a non-project directory, prints `Workspace install skipped` and only does host install.
|
|
61
|
+
|
|
62
|
+
**Result**: `npx kushi-agents@latest --all-hosts --profile full` from inside a repo now does the entire install in one shot. Help text + `docs/getting-started/install.md` updated to recommend the unified form first; legacy two-command form preserved in a `<details>` block for users on shared dev machines.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
7
66
|
### 2026-05-29 — `bootstrap → "fill the templates" → refresh` is the wrong default
|
|
8
67
|
|
|
9
68
|
**Symptom**: User runs `bootstrap Northwind`. Runner scaffolds folders + empty `boundaries.yml` + empty `integrations.yml`. SKILL.md tells the agent to reply *"now fill these two files and run refresh"*. User runs `refresh` immediately; refresh emits a configuration-blocked report and exits 0 with zero pulls. User's reaction: *"we know we need to fill them, why did the tool stop and ask? is this not a methodical do?"*
|
|
@@ -180,12 +180,12 @@ Run ``npx kushi-agents@latest`` (vscode) or ``npx kushi-agents@latest --clawpilo
|
|
|
180
180
|
|
|
181
181
|
if ($Path) { return $resolved }
|
|
182
182
|
|
|
183
|
-
$
|
|
183
|
+
$rawText = Get-Content -LiteralPath $resolved -Raw
|
|
184
184
|
|
|
185
185
|
if (-not $AllowPlaceholders) {
|
|
186
186
|
$sentinelHit = $false
|
|
187
187
|
foreach ($s in $script:Sentinels) {
|
|
188
|
-
if ($
|
|
188
|
+
if ($rawText -like "*$s*") { $sentinelHit = $true; break }
|
|
189
189
|
}
|
|
190
190
|
if ($sentinelHit) {
|
|
191
191
|
throw @"
|
|
@@ -195,16 +195,16 @@ Edit the file with your actual values, or pass -AllowPlaceholders to bypass this
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
if ($Raw) { return $
|
|
198
|
+
if ($Raw) { return $rawText }
|
|
199
199
|
|
|
200
200
|
$parsed = switch ($ext) {
|
|
201
201
|
'json' {
|
|
202
|
-
$
|
|
202
|
+
$rawText | ConvertFrom-Json -Depth 100
|
|
203
203
|
}
|
|
204
204
|
{ $_ -in 'yml','yaml' } {
|
|
205
205
|
if (Get-Module -ListAvailable -Name 'powershell-yaml') {
|
|
206
206
|
Import-Module powershell-yaml -ErrorAction Stop
|
|
207
|
-
ConvertFrom-Yaml -Yaml $
|
|
207
|
+
ConvertFrom-Yaml -Yaml $rawText
|
|
208
208
|
} else {
|
|
209
209
|
Write-Warning "powershell-yaml not installed; required-field validation skipped. Install with: Install-Module powershell-yaml -Scope CurrentUser"
|
|
210
210
|
$null
|
|
@@ -223,5 +223,5 @@ Edit the file with your actual values, or pass -AllowPlaceholders to bypass this
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
if ($null -eq $parsed) { return $
|
|
226
|
+
if ($null -eq $parsed) { return $rawText }
|
|
227
227
|
return $parsed
|
|
@@ -4,15 +4,45 @@
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { platform } from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { promises as fs } from 'node:fs';
|
|
7
|
+
import { promises as fs, existsSync } from 'node:fs';
|
|
8
8
|
|
|
9
9
|
/** Resolve the workiq binary for the current platform. */
|
|
10
10
|
export function resolveWorkiqBin(explicit = process.env.KUSHI_WORKIQ_BIN) {
|
|
11
|
-
if (explicit)
|
|
11
|
+
if (explicit) {
|
|
12
|
+
if (path.isAbsolute(explicit)) return explicit;
|
|
13
|
+
const found = whichSync(explicit);
|
|
14
|
+
return found || explicit;
|
|
15
|
+
}
|
|
16
|
+
// 1. Try PATH first (npm-installed workiq lands in AppData\Roaming\npm).
|
|
17
|
+
const onPath = whichSync(platform() === 'win32' ? 'workiq.cmd' : 'workiq');
|
|
18
|
+
if (onPath) return onPath;
|
|
19
|
+
// 2. Fallback to Clawpilot-managed location.
|
|
12
20
|
if (platform() === 'win32') return path.join(process.env.USERPROFILE || '', '.copilot', 'bin', 'workiq.cmd');
|
|
13
21
|
return path.join(process.env.HOME || '', '.copilot', 'bin', 'workiq');
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
function whichSync(name) {
|
|
25
|
+
const PATH = process.env.PATH || process.env.Path || '';
|
|
26
|
+
const sep = platform() === 'win32' ? ';' : ':';
|
|
27
|
+
const exts = platform() === 'win32'
|
|
28
|
+
? (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')
|
|
29
|
+
: [''];
|
|
30
|
+
const hasExt = /\.[a-zA-Z0-9]{1,5}$/.test(name);
|
|
31
|
+
for (const dir of PATH.split(sep)) {
|
|
32
|
+
if (!dir) continue;
|
|
33
|
+
if (hasExt) {
|
|
34
|
+
const candidate = path.join(dir, name);
|
|
35
|
+
if (existsSync(candidate)) return candidate;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
for (const ext of exts) {
|
|
39
|
+
const candidate = path.join(dir, name + ext);
|
|
40
|
+
if (existsSync(candidate)) return candidate;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
16
46
|
/**
|
|
17
47
|
* Ask WorkIQ a question. Returns the raw stdout text and any parsed CSC blocks.
|
|
18
48
|
*
|
|
@@ -86,7 +116,20 @@ async function pathExists(p) { try { await fs.access(p); return true; } catch {
|
|
|
86
116
|
|
|
87
117
|
function runProcess(exe, args, { timeoutMs, env }) {
|
|
88
118
|
return new Promise((resolve, reject) => {
|
|
89
|
-
|
|
119
|
+
// Node 20.12+ refuses to spawn .cmd/.bat on Windows without shell:true
|
|
120
|
+
// (CVE-2024-27980). Detect and route through cmd.exe explicitly with
|
|
121
|
+
// verbatim args so quoting in the prompt survives intact.
|
|
122
|
+
const isWinShim = platform() === 'win32' && /\.(cmd|bat)$/i.test(exe);
|
|
123
|
+
const spawnOpts = { env, shell: false };
|
|
124
|
+
let spawnExe = exe;
|
|
125
|
+
let spawnArgs = args;
|
|
126
|
+
if (isWinShim) {
|
|
127
|
+
const quoted = [exe, ...args].map(quoteForCmd).join(' ');
|
|
128
|
+
spawnExe = process.env.ComSpec || 'cmd.exe';
|
|
129
|
+
spawnArgs = ['/d', '/s', '/c', quoted];
|
|
130
|
+
spawnOpts.windowsVerbatimArguments = true;
|
|
131
|
+
}
|
|
132
|
+
const child = spawn(spawnExe, spawnArgs, spawnOpts);
|
|
90
133
|
let stdout = '';
|
|
91
134
|
let stderr = '';
|
|
92
135
|
let timeoutTimer = null;
|
|
@@ -102,3 +145,10 @@ function runProcess(exe, args, { timeoutMs, env }) {
|
|
|
102
145
|
child.on('close', code => { if (timeoutTimer) clearTimeout(timeoutTimer); resolve({ stdout, stderr, exitCode: code ?? 0 }); });
|
|
103
146
|
});
|
|
104
147
|
}
|
|
148
|
+
|
|
149
|
+
function quoteForCmd(s) {
|
|
150
|
+
if (s === '' || /[\s"&|<>^()]/.test(s)) {
|
|
151
|
+
return '"' + String(s).replace(/"/g, '""') + '"';
|
|
152
|
+
}
|
|
153
|
+
return String(s);
|
|
154
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Regression tests for plugin/lib/Get-KushiConfig.ps1.
|
|
2
|
+
//
|
|
3
|
+
// History: v5.7.1 had a SwitchParameter coercion bug. The script's
|
|
4
|
+
// [switch] $Raw parameter collided with a local $raw = Get-Content variable
|
|
5
|
+
// (PowerShell variables are case-insensitive). Assigning a string into the
|
|
6
|
+
// typed switch parameter caused "Cannot convert String to SwitchParameter"
|
|
7
|
+
// at the call site whenever the script was invoked from outside its own
|
|
8
|
+
// scope.
|
|
9
|
+
//
|
|
10
|
+
// These tests install kushi into a temp dir, then invoke the live shipped
|
|
11
|
+
// Get-KushiConfig.ps1 with various flag combinations and assert that the
|
|
12
|
+
// invocation succeeds with no error stream output.
|
|
13
|
+
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const REPO = path.resolve(HERE, '..');
|
|
24
|
+
|
|
25
|
+
function makeTmp(prefix) {
|
|
26
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function freshInstall() {
|
|
30
|
+
const cwd = makeTmp('kushi-getconfig-');
|
|
31
|
+
const r = spawnSync(process.execPath, [path.join(REPO, 'bin', 'cli.mjs'), '--profile', 'full', '--skip-workiq-check', '--yes'], {
|
|
32
|
+
cwd, encoding: 'utf-8', shell: false,
|
|
33
|
+
});
|
|
34
|
+
assert.equal(r.status, 0, `installer failed: ${r.stderr || r.stdout}`);
|
|
35
|
+
return cwd;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pwsh(cwd, args) {
|
|
39
|
+
return spawnSync('pwsh', ['-NoProfile', '-Command', ...args], {
|
|
40
|
+
cwd, encoding: 'utf-8', shell: false,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('Get-KushiConfig.ps1 -Name m365-auth -Raw does not raise SwitchParameter coercion error', () => {
|
|
45
|
+
const cwd = freshInstall();
|
|
46
|
+
const script = path.join(cwd, '.kushi', 'lib', 'Get-KushiConfig.ps1');
|
|
47
|
+
const r = pwsh(cwd, [`& '${script}' -Name 'm365-auth' -Raw -AllowPlaceholders | Out-Null; if ($?) { 'OK' } else { 'FAIL' }`]);
|
|
48
|
+
assert.equal(r.status, 0, `pwsh exited ${r.status}; stderr=${r.stderr}`);
|
|
49
|
+
assert.match(r.stdout, /OK/, `expected OK, got stdout=${r.stdout} stderr=${r.stderr}`);
|
|
50
|
+
assert.doesNotMatch(r.stderr, /SwitchParameter/i, `SwitchParameter coercion error leaked: ${r.stderr}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('Get-KushiConfig.ps1 -Name project-evidence -AllowPlaceholders -Raw works on fresh install', () => {
|
|
54
|
+
const cwd = freshInstall();
|
|
55
|
+
const script = path.join(cwd, '.kushi', 'lib', 'Get-KushiConfig.ps1');
|
|
56
|
+
const r = pwsh(cwd, [`& '${script}' -Name 'project-evidence' -AllowPlaceholders -Raw | Out-Null; if ($?) { 'OK' } else { 'FAIL' }`]);
|
|
57
|
+
assert.equal(r.status, 0, `pwsh exited ${r.status}; stderr=${r.stderr}`);
|
|
58
|
+
assert.match(r.stdout, /OK/, `expected OK, got stdout=${r.stdout} stderr=${r.stderr}`);
|
|
59
|
+
assert.doesNotMatch(r.stderr, /SwitchParameter/i, `SwitchParameter coercion error leaked: ${r.stderr}`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('Get-KushiConfig.ps1 -Path returns absolute resolved path', () => {
|
|
63
|
+
const cwd = freshInstall();
|
|
64
|
+
const script = path.join(cwd, '.kushi', 'lib', 'Get-KushiConfig.ps1');
|
|
65
|
+
const r = pwsh(cwd, [`& '${script}' -Name 'm365-auth' -Path`]);
|
|
66
|
+
assert.equal(r.status, 0, `pwsh exited ${r.status}; stderr=${r.stderr}`);
|
|
67
|
+
assert.match(r.stdout, /m365-auth\.json/, `expected resolved path, got stdout=${r.stdout}`);
|
|
68
|
+
assert.doesNotMatch(r.stderr, /SwitchParameter/i);
|
|
69
|
+
});
|
package/src/main.mjs
CHANGED
|
@@ -394,7 +394,7 @@ function warnIfNotProjectRoot(projectRoot) {
|
|
|
394
394
|
* @param {string} dir
|
|
395
395
|
* @returns {{ name: string, kind: 'code'|'engagement', type: 'file'|'dir' } | null}
|
|
396
396
|
*/
|
|
397
|
-
function findProjectMarker(dir) {
|
|
397
|
+
export function findProjectMarker(dir) {
|
|
398
398
|
let entries;
|
|
399
399
|
try {
|
|
400
400
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
package/src/multi-host.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import { copyAssets } from './copy-assets.mjs';
|
|
38
38
|
import { installRunnerDeps } from './install-runner-deps.mjs';
|
|
39
39
|
import { seedConfig } from './seed-config.mjs';
|
|
40
|
+
import { findProjectMarker } from './main.mjs';
|
|
40
41
|
import {
|
|
41
42
|
resolveProfile,
|
|
42
43
|
makeIncludeFilter,
|
|
@@ -275,6 +276,40 @@ export async function runMultiHost(opts) {
|
|
|
275
276
|
results.push(installHost(t.id, { home, force: true, profile: opts.profile }));
|
|
276
277
|
}
|
|
277
278
|
}
|
|
279
|
+
|
|
280
|
+
// Unified install (v5.7.1+): when the user upgrades hosts AND is sitting in
|
|
281
|
+
// a project directory, also refresh the workspace `.kushi/` install in cwd.
|
|
282
|
+
// This collapses the historical two-command dance:
|
|
283
|
+
// npx kushi-agents@latest --all-hosts --profile full
|
|
284
|
+
// cd <repo> && npx kushi-agents@latest --profile full
|
|
285
|
+
// into a single invocation. Suppress with `--no-workspace`. Skipped on
|
|
286
|
+
// uninstall and when cwd is not a recognized project (no marker, empty dir,
|
|
287
|
+
// or just plain files).
|
|
288
|
+
if (!opts.uninstall && opts.includeWorkspace !== false) {
|
|
289
|
+
const cwd = opts.cwd || process.cwd();
|
|
290
|
+
const marker = findProjectMarker(cwd);
|
|
291
|
+
if (marker) {
|
|
292
|
+
console.log('');
|
|
293
|
+
console.log(` Workspace install detected (${marker.kind}: ${marker.name}) — installing into ${cwd}/.kushi/ ...`);
|
|
294
|
+
try {
|
|
295
|
+
const { main } = await import('./main.mjs');
|
|
296
|
+
await main({
|
|
297
|
+
target: 'vscode',
|
|
298
|
+
yes: true,
|
|
299
|
+
force: true,
|
|
300
|
+
profile: opts.profile,
|
|
301
|
+
skipWorkiqCheck: true,
|
|
302
|
+
});
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.error(` Workspace install failed: ${err.message}`);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
console.log('');
|
|
308
|
+
console.log(' Workspace install skipped — current directory is not a recognized project.');
|
|
309
|
+
console.log(' (cd into a repo or engagement folder and re-run, or pass --no-workspace to silence.)');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
278
313
|
console.log('');
|
|
279
314
|
console.log(` Done. ${results.length} host(s) processed.\n`);
|
|
280
315
|
return results;
|