kushi-agents 5.9.0 → 5.9.1
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 +40 -10
- package/package.json +1 -1
- package/plugin/instructions/global-wiki.instructions.md +7 -2
- package/plugin/skills/global-wiki/SKILL.md +13 -4
- package/plugin/templates/init/kushi.template.yml +27 -0
- package/plugin/templates/user-config.json +20 -0
- package/src/constants.mjs +1 -0
- package/src/global-wiki-cli.mjs +230 -3
- package/src/global-wiki.mjs +133 -1
- package/src/seed-config.mjs +28 -0
- package/src/setup-wizard.mjs +13 -1
package/bin/cli.mjs
CHANGED
|
@@ -103,22 +103,47 @@ if (args.length > 0 && args[0] === 'lint') {
|
|
|
103
103
|
process.exit(0);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// ── one-shot wiki verb (v5.9.1+) ─────────────────────────────────────────────
|
|
107
|
+
// `kushi wiki` — deterministic do-the-wiki: resolve root, init if missing,
|
|
108
|
+
// print path + status + clickable file:// URL. Idempotent across
|
|
109
|
+
// "create wiki" / "do wiki" / "refresh wiki" / "update wiki".
|
|
110
|
+
if (args.length > 0 && args[0] === 'wiki') {
|
|
111
|
+
const cliMod = await import('../src/global-wiki-cli.mjs');
|
|
112
|
+
await cliMod.runWiki();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
// ── global verb (v5.3.0+) ────────────────────────────────────────────────────
|
|
107
117
|
if (args.length > 0 && args[0] === 'global') {
|
|
108
118
|
const sub = args[1] || '';
|
|
109
|
-
const validSubs = ['init', 'status', 'ask', 'lint'];
|
|
119
|
+
const validSubs = ['init', 'status', 'ask', 'lint', 'migrate', 'set-root', 'show-root'];
|
|
110
120
|
if (!validSubs.includes(sub)) {
|
|
111
|
-
console.error('\n Usage: kushi global init
|
|
112
|
-
console.error(' kushi global status
|
|
113
|
-
console.error(' kushi global ask <question>
|
|
114
|
-
console.error(' kushi global lint
|
|
121
|
+
console.error('\n Usage: kushi global init Scaffold State/ at the resolved root');
|
|
122
|
+
console.error(' kushi global status Show counts + freshness');
|
|
123
|
+
console.error(' kushi global ask <question> Ask the global wiki');
|
|
124
|
+
console.error(' kushi global lint Lint the global wiki');
|
|
125
|
+
console.error(' kushi global show-root Show how the root path is resolved');
|
|
126
|
+
console.error(' kushi global set-root <path> [--scope workspace|home]');
|
|
127
|
+
console.error(' Persist root (workspace shared by default,');
|
|
128
|
+
console.error(' falls back to ~/.kushi/config.json)');
|
|
129
|
+
console.error(' kushi global migrate <new-path> Copy wiki to a new root + persist setting\n');
|
|
130
|
+
console.error(' Tip: Set the root to a OneDrive-synced SharePoint folder to share');
|
|
131
|
+
console.error(' the wiki across a team. See docs/how-to/wiki-on-sharepoint.md.\n');
|
|
115
132
|
process.exit(1);
|
|
116
133
|
}
|
|
117
|
-
const
|
|
118
|
-
if (sub === 'init') await runGlobalInit();
|
|
119
|
-
else if (sub === 'status') await runGlobalStatus();
|
|
120
|
-
else if (sub === 'ask') await runGlobalAsk(args.slice(2).join(' '));
|
|
121
|
-
else if (sub === 'lint') await runGlobalLint();
|
|
134
|
+
const cliMod = await import('../src/global-wiki-cli.mjs');
|
|
135
|
+
if (sub === 'init') await cliMod.runGlobalInit();
|
|
136
|
+
else if (sub === 'status') await cliMod.runGlobalStatus();
|
|
137
|
+
else if (sub === 'ask') await cliMod.runGlobalAsk(args.slice(2).join(' '));
|
|
138
|
+
else if (sub === 'lint') await cliMod.runGlobalLint();
|
|
139
|
+
else if (sub === 'migrate') await cliMod.runGlobalMigrate(args[2]);
|
|
140
|
+
else if (sub === 'set-root') {
|
|
141
|
+
const scopeIdx = args.indexOf('--scope');
|
|
142
|
+
const scope = scopeIdx > 0 && args[scopeIdx + 1] ? args[scopeIdx + 1] : null;
|
|
143
|
+
const target = args.find((a, i) => i >= 2 && !a.startsWith('--') && args[i - 1] !== '--scope');
|
|
144
|
+
await cliMod.runGlobalSetRoot(target, { scope });
|
|
145
|
+
}
|
|
146
|
+
else if (sub === 'show-root') await cliMod.runGlobalShowRoot();
|
|
122
147
|
process.exit(0);
|
|
123
148
|
}
|
|
124
149
|
|
|
@@ -262,6 +287,11 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
262
287
|
lint <project> Run wiki-lint checks on State/
|
|
263
288
|
|
|
264
289
|
Workspace lifecycle (v5.9.0+):
|
|
290
|
+
wiki One-shot: resolve global wiki root, scaffold
|
|
291
|
+
if missing, print status + clickable file://
|
|
292
|
+
link to the wiki index. Idempotent —
|
|
293
|
+
"do wiki" / "refresh wiki" / "create wiki"
|
|
294
|
+
all map here.
|
|
265
295
|
uninstall [--keep-config] Remove <cwd>/.kushi/ (preserves Evidence/, State/).
|
|
266
296
|
--keep-config preserves config/user/ identity files.
|
|
267
297
|
upgrade npm i -g kushi-agents@latest then re-seed assets
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kushi-agents",
|
|
3
|
-
"version": "5.9.
|
|
3
|
+
"version": "5.9.1",
|
|
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": {
|
|
@@ -17,8 +17,13 @@ The global wiki is a personal, cross-engagement knowledge base that lives **outs
|
|
|
17
17
|
### 1. Location
|
|
18
18
|
|
|
19
19
|
- **Default:** `~/.kushi-global/State/` (`$HOME` on POSIX, `$env:USERPROFILE` on Windows).
|
|
20
|
-
- **
|
|
21
|
-
|
|
20
|
+
- **Resolution chain (v5.9.1+, first non-empty wins):**
|
|
21
|
+
1. `$KUSHI_GLOBAL_ROOT` env var (absolute path; tilde-expanded). Tests MUST set this to `.testtmp/.kushi-global/` — never touch the real path.
|
|
22
|
+
2. **`<workspace>/.kushi/config/shared/kushi.yml` → `globalRoot`** (workspace-shared, team-shareable, committed)
|
|
23
|
+
3. `~/.kushi/config.json` → `globalRoot` (per-user, per-machine)
|
|
24
|
+
4. Default `~/.kushi-global/`
|
|
25
|
+
- The global root is **per-user OR per-team** (workspace-shared scope), never per-project, never per-host.
|
|
26
|
+
- Inspect with `kushi global show-root`. Set with `kushi global set-root <path> [--scope workspace|home]` (default scope = workspace if a `.kushi/` is found above cwd, else home).
|
|
22
27
|
|
|
23
28
|
### 2. Shape
|
|
24
29
|
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "global-wiki"
|
|
3
3
|
version: "1.0.0"
|
|
4
|
-
description: "USE WHEN the user says 'init global wiki', 'kushi global init/status/ask/lint', 'show me my global wiki', or wants to manage the cross-engagement knowledge base
|
|
4
|
+
description: "USE WHEN the user says 'init global wiki', 'kushi global init/status/ask/lint/show-root/set-root', 'show me my global wiki', or wants to manage the cross-engagement knowledge base. DO NOT USE for promoting individual pages (use promote) or for project-scoped Q&A (use ask-project). Capability: scaffold + status + ask + lint + show-root + set-root over the global wiki; resolves location via 4-tier chain (env > workspace shared > home config > default ~/.kushi-global/State/)."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Skill: global-wiki
|
|
8
8
|
|
|
9
|
-
Manages the
|
|
9
|
+
Manages the global wiki at the resolved root (default `~/.kushi-global/State/`). Structurally identical to a project `State/` wiki but tagged `scope: global` in every page's frontmatter. Holds cross-engagement patterns that consultants want to keep across projects.
|
|
10
|
+
|
|
11
|
+
**Location resolution (v5.9.1+, first non-empty wins):**
|
|
12
|
+
|
|
13
|
+
1. `$KUSHI_GLOBAL_ROOT` env var (override; ideal for tests + temp redirection)
|
|
14
|
+
2. `<workspace>/.kushi/config/shared/kushi.yml → globalRoot` (team-shareable, committed)
|
|
15
|
+
3. `~/.kushi/config.json → globalRoot` (per-user, per-machine)
|
|
16
|
+
4. Default `~/.kushi-global/`
|
|
17
|
+
|
|
18
|
+
Use `kushi global show-root` to inspect the chain.
|
|
10
19
|
|
|
11
20
|
See `plugin/instructions/global-wiki.instructions.md` for the full doctrine and `multi-wiki-routing.instructions.md` for how readers + writers route between project and global.
|
|
12
21
|
|
|
@@ -27,14 +36,14 @@ See `plugin/instructions/global-wiki.instructions.md` for the full doctrine and
|
|
|
27
36
|
|
|
28
37
|
## Step checklist
|
|
29
38
|
|
|
30
|
-
- [ ] Step 1 — Resolve
|
|
39
|
+
- [ ] Step 1 — Resolve the global root via the 4-tier chain (env → workspace shared → home config → default).
|
|
31
40
|
- [ ] Step 2 — Dispatch to the requested sub-operation.
|
|
32
41
|
- [ ] Step 3 — Print a one-line summary + paths.
|
|
33
42
|
- [ ] Step 4 — Append a `log.md` entry on init / lint / ask-fileback.
|
|
34
43
|
|
|
35
44
|
### Step 1 — Resolve global root
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
Resolve via 4-tier chain: `$KUSHI_GLOBAL_ROOT` → `<workspace>/.kushi/config/shared/kushi.yml#globalRoot` → `~/.kushi/config.json#globalRoot` → default `~/.kushi-global/`. Tests MUST set `$env:KUSHI_GLOBAL_ROOT='.testtmp/.kushi-global'` and NEVER touch the real path. See `src/global-wiki.mjs#resolveGlobalRoot` for the canonical implementation.
|
|
38
47
|
|
|
39
48
|
### Step 2 — Sub-operations
|
|
40
49
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Kushi — shared workspace settings.
|
|
2
|
+
#
|
|
3
|
+
# Seeded by the installer on first install. Never overwritten on upgrade
|
|
4
|
+
# (unless you pass --force). Safe to commit — these are team-wide settings,
|
|
5
|
+
# not per-contributor identity.
|
|
6
|
+
#
|
|
7
|
+
# Resolution order (first non-empty wins):
|
|
8
|
+
# 1. $env:KUSHI_GLOBAL_ROOT (env override)
|
|
9
|
+
# 2. <workspace>/.kushi/config/shared/kushi.yml → globalRoot (THIS FILE)
|
|
10
|
+
# 3. ~/.kushi/config.json → globalRoot (home fallback)
|
|
11
|
+
# 4. ~/.kushi-global/ (default)
|
|
12
|
+
#
|
|
13
|
+
# Edit with: kushi global set-root <path>
|
|
14
|
+
# Or hand-edit this file directly.
|
|
15
|
+
|
|
16
|
+
# Global wiki root.
|
|
17
|
+
#
|
|
18
|
+
# Path to the team's shared kushi global wiki. State/ is created underneath.
|
|
19
|
+
#
|
|
20
|
+
# null → fall back to the home config file or the default ~/.kushi-global/.
|
|
21
|
+
# path → use this directory.
|
|
22
|
+
#
|
|
23
|
+
# Tip: set this to a OneDrive-synced SharePoint folder so every contributor
|
|
24
|
+
# on the team picks up the same wiki path automatically (when they pull the
|
|
25
|
+
# repo, this file already names the synced location).
|
|
26
|
+
# See docs/how-to/wiki-on-sharepoint.md.
|
|
27
|
+
globalRoot: null
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"purpose": "Per-user, per-machine kushi settings. Lives at ~/.kushi/config.json.",
|
|
4
|
+
"scope": "machine-user",
|
|
5
|
+
"version_managed_by": "kushi-agents npm install (seed-once)",
|
|
6
|
+
"edit_with": "kushi global set-root <path> (or edit this file directly)"
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
"globalRoot": null,
|
|
10
|
+
|
|
11
|
+
"_docs": {
|
|
12
|
+
"globalRoot": [
|
|
13
|
+
"Path to the global wiki root. State/ is created underneath.",
|
|
14
|
+
"null → kushi falls back to ~/.kushi-global/",
|
|
15
|
+
"Tip → set this to a OneDrive-synced SharePoint folder to share the wiki",
|
|
16
|
+
" across a team. See docs/how-to/wiki-on-sharepoint.md.",
|
|
17
|
+
"Override (per-shell): set $env:KUSHI_GLOBAL_ROOT — env always wins."
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/constants.mjs
CHANGED
package/src/global-wiki-cli.mjs
CHANGED
|
@@ -4,10 +4,60 @@
|
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import {
|
|
10
|
+
globalInit, globalStatus, globalAsk, globalLint, promote,
|
|
11
|
+
resolveGlobalRoot, explainGlobalRoot, writeUserConfig, userConfigPath,
|
|
12
|
+
findWorkspaceSharedConfig, writeWorkspaceSharedConfig,
|
|
13
|
+
} from './global-wiki.mjs';
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Persist the global wiki root.
|
|
17
|
+
* - If we're inside a workspace (a .kushi/config/shared/ exists somewhere
|
|
18
|
+
* above cwd), write to <workspace>/.kushi/config/shared/kushi.yml. This
|
|
19
|
+
* is the team-shared, committed location — the right answer when the
|
|
20
|
+
* whole team should pick up the same wiki path.
|
|
21
|
+
* - Otherwise, fall back to the per-user home file ~/.kushi/config.json.
|
|
22
|
+
* - On Windows, also setx KUSHI_GLOBAL_ROOT as a courtesy for tools that
|
|
23
|
+
* read the env var directly. The config file is the source of truth.
|
|
24
|
+
*
|
|
25
|
+
* Set `scope: 'home'` or `scope: 'workspace'` to force one tier.
|
|
26
|
+
*/
|
|
27
|
+
function persistGlobalRootEnv(rootPath, { scope = 'auto', cwd = process.cwd() } = {}) {
|
|
28
|
+
const target = path.resolve(rootPath);
|
|
29
|
+
let where;
|
|
30
|
+
let chosenScope;
|
|
31
|
+
|
|
32
|
+
const wsFile = findWorkspaceSharedConfig(cwd);
|
|
33
|
+
if (scope === 'workspace' || (scope === 'auto' && wsFile)) {
|
|
34
|
+
if (!wsFile) {
|
|
35
|
+
return { method: 'workspace', ok: false, message: `No .kushi/config/shared/kushi.yml found above ${cwd}. Run install in this workspace first, or use --scope home.` };
|
|
36
|
+
}
|
|
37
|
+
writeWorkspaceSharedConfig(wsFile, { globalRoot: target });
|
|
38
|
+
where = wsFile; chosenScope = 'workspace';
|
|
39
|
+
} else {
|
|
40
|
+
const cfg = writeUserConfig({ globalRoot: target });
|
|
41
|
+
where = cfg.path; chosenScope = 'home';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let envNote = '';
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
const r = spawnSync('setx', ['KUSHI_GLOBAL_ROOT', target], { encoding: 'utf8' });
|
|
47
|
+
envNote = r.status === 0
|
|
48
|
+
? ' (also set $env:KUSHI_GLOBAL_ROOT for User scope — visible in new shells)'
|
|
49
|
+
: '';
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
method: 'config-file',
|
|
53
|
+
scope: chosenScope,
|
|
54
|
+
ok: true,
|
|
55
|
+
message: `Wrote globalRoot to ${where}.${envNote}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runGlobalInit({ root, persistEnv = false } = {}) {
|
|
60
|
+
const result = globalInit(root ? { root } : {});
|
|
11
61
|
console.log('');
|
|
12
62
|
console.log(` Global wiki root: ${result.root}`);
|
|
13
63
|
console.log(` State dir : ${result.state}`);
|
|
@@ -17,11 +67,188 @@ export async function runGlobalInit() {
|
|
|
17
67
|
if (result.skipped.length) {
|
|
18
68
|
console.log(` Skipped (${result.skipped.length} already present)`);
|
|
19
69
|
}
|
|
70
|
+
if (persistEnv && root) {
|
|
71
|
+
const env = persistGlobalRootEnv(result.root);
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(` ${env.ok ? '✓' : '⚠'} ${env.message}`);
|
|
74
|
+
}
|
|
20
75
|
console.log('');
|
|
21
76
|
console.log(" Next: 'kushi global status' or 'kushi promote <project> <page>'");
|
|
22
77
|
console.log('');
|
|
23
78
|
}
|
|
24
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Migrate the existing global wiki from current root to a new path.
|
|
82
|
+
* Copies State/ contents, then persists KUSHI_GLOBAL_ROOT to the new location.
|
|
83
|
+
* Does NOT delete the old root — user can remove it manually after verifying.
|
|
84
|
+
*/
|
|
85
|
+
export async function runGlobalMigrate(newRootArg, { persistEnv = true } = {}) {
|
|
86
|
+
if (!newRootArg || !newRootArg.trim()) {
|
|
87
|
+
console.error('\n Usage: kushi global migrate <new-root-path>\n');
|
|
88
|
+
console.error(' Example (OneDrive-synced SharePoint):\n');
|
|
89
|
+
console.error(' kushi global migrate "C:\\Users\\you\\Microsoft\\TeamWiki - Documents\\KushiWiki"\n');
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const newRoot = path.resolve(newRootArg.trim());
|
|
94
|
+
const oldRoot = resolveGlobalRoot();
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(` Migrating global wiki:`);
|
|
97
|
+
console.log(` from: ${oldRoot}`);
|
|
98
|
+
console.log(` to : ${newRoot}`);
|
|
99
|
+
console.log('');
|
|
100
|
+
|
|
101
|
+
const oldState = path.join(oldRoot, 'State');
|
|
102
|
+
if (!fs.existsSync(oldState)) {
|
|
103
|
+
console.log(` ⚠ No existing State/ at ${oldState} — initializing fresh at ${newRoot}.`);
|
|
104
|
+
const result = globalInit({ root: newRoot });
|
|
105
|
+
console.log(` ✓ Created ${result.created.length} file(s) at ${result.state}`);
|
|
106
|
+
} else {
|
|
107
|
+
fs.mkdirSync(newRoot, { recursive: true });
|
|
108
|
+
const newState = path.join(newRoot, 'State');
|
|
109
|
+
let copied = 0, skipped = 0;
|
|
110
|
+
const copyTree = (src, dst) => {
|
|
111
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
112
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
113
|
+
const sp = path.join(src, entry.name);
|
|
114
|
+
const dp = path.join(dst, entry.name);
|
|
115
|
+
if (entry.isDirectory()) copyTree(sp, dp);
|
|
116
|
+
else if (entry.isFile()) {
|
|
117
|
+
if (fs.existsSync(dp)) { skipped++; continue; }
|
|
118
|
+
fs.copyFileSync(sp, dp);
|
|
119
|
+
copied++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
copyTree(oldState, newState);
|
|
124
|
+
console.log(` ✓ Copied ${copied} file(s); skipped ${skipped} that already existed at target.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (persistEnv) {
|
|
128
|
+
const env = persistGlobalRootEnv(newRoot);
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log(` ${env.ok ? '✓' : '⚠'} ${env.message}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(` Verify with: kushi global status`);
|
|
135
|
+
console.log(` Old root preserved at: ${oldRoot}`);
|
|
136
|
+
console.log(` Once verified, you can delete the old State/ folder there.`);
|
|
137
|
+
console.log('');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function runGlobalSetRoot(newRootArg, opts = {}) {
|
|
141
|
+
if (!newRootArg || !newRootArg.trim()) {
|
|
142
|
+
console.error('\n Usage: kushi global set-root <path> [--scope workspace|home]\n');
|
|
143
|
+
console.error(' Persists the global wiki root.');
|
|
144
|
+
console.error(' Default scope = workspace if a .kushi/ is found above cwd, else home.');
|
|
145
|
+
console.error(' Effect is immediate — no shell restart needed.\n');
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const newRoot = path.resolve(newRootArg.trim());
|
|
150
|
+
const env = persistGlobalRootEnv(newRoot, { scope: opts.scope || 'auto' });
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(` Target: ${newRoot}`);
|
|
153
|
+
console.log(` Scope : ${env.scope || '(none)'}`);
|
|
154
|
+
console.log(` ${env.ok ? '✓' : '⚠'} ${env.message}`);
|
|
155
|
+
if (!fs.existsSync(path.join(newRoot, 'State'))) {
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(` Note: ${newRoot}/State/ does not exist yet. Run \`kushi global init\` to create it.`);
|
|
158
|
+
}
|
|
159
|
+
console.log('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Show the resolution chain for the global wiki root: env var → config file → default.
|
|
164
|
+
* Helps users figure out why kushi is reading/writing where it is.
|
|
165
|
+
*/
|
|
166
|
+
export async function runGlobalShowRoot() {
|
|
167
|
+
const explain = explainGlobalRoot();
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(` Resolved global root: ${explain.resolved}`);
|
|
170
|
+
console.log(` Winning source : ${explain.winner ? explain.winner.source : 'default'}`);
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(' Resolution chain (first non-empty wins):');
|
|
173
|
+
for (const s of explain.sources) {
|
|
174
|
+
const mark = explain.winner && explain.winner.source === s.source ? '→' : ' ';
|
|
175
|
+
const val = s.value ? s.value : '(unset)';
|
|
176
|
+
console.log(` ${mark} [${s.source}] ${s.key}`);
|
|
177
|
+
console.log(` ${val}`);
|
|
178
|
+
}
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(` Config file (home): ${userConfigPath()}`);
|
|
181
|
+
console.log(' Change with: kushi global set-root <path> (workspace shared if available, else home)');
|
|
182
|
+
console.log(' kushi global set-root <path> --scope home');
|
|
183
|
+
console.log(' Or override: $env:KUSHI_GLOBAL_ROOT = "..." (env var wins for current shell)');
|
|
184
|
+
console.log('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
function expandIfTilde(p) {
|
|
189
|
+
if (typeof p !== 'string') return p;
|
|
190
|
+
if (p.startsWith('~')) {
|
|
191
|
+
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
192
|
+
return path.join(home, p.slice(1).replace(/^[\\/]/, ''));
|
|
193
|
+
}
|
|
194
|
+
return p;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One-shot deterministic "do the wiki" verb.
|
|
199
|
+
*
|
|
200
|
+
* - Resolves the global root via the 4-tier chain (env > workspace shared > home > default).
|
|
201
|
+
* - If the State/ scaffold is missing, runs `globalInit` to create it.
|
|
202
|
+
* - Prints the resolved root, winning source, status counts, and a clickable
|
|
203
|
+
* `file://` URL so the user can open the wiki immediately.
|
|
204
|
+
* - Idempotent: safe to re-run on every "do wiki" / "refresh wiki" / "create wiki".
|
|
205
|
+
*/
|
|
206
|
+
export async function runWiki() {
|
|
207
|
+
const explain = explainGlobalRoot();
|
|
208
|
+
const winner = explain.winner;
|
|
209
|
+
const root = winner ? expandIfTilde(winner.value) : explain.resolved;
|
|
210
|
+
const stateDir = path.join(root, 'State');
|
|
211
|
+
const initiallyMissing = !fs.existsSync(stateDir);
|
|
212
|
+
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log(` Wiki root : ${root}`);
|
|
215
|
+
console.log(` Winning source : ${winner ? winner.source : 'default'}`);
|
|
216
|
+
|
|
217
|
+
let initResult = null;
|
|
218
|
+
if (initiallyMissing) {
|
|
219
|
+
initResult = globalInit({ root });
|
|
220
|
+
console.log(` Scaffold : created ${initResult.created.length} file(s)`);
|
|
221
|
+
} else {
|
|
222
|
+
initResult = globalInit({ root });
|
|
223
|
+
if (initResult.created.length) {
|
|
224
|
+
console.log(` Scaffold : top-up — added ${initResult.created.length} missing file(s)`);
|
|
225
|
+
} else {
|
|
226
|
+
console.log(` Scaffold : already present (${initResult.skipped.length} files intact)`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const status = globalStatus({ root });
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log(` Pages : ${status.counts.pages}`);
|
|
233
|
+
console.log(` Answers : ${status.counts.answers}`);
|
|
234
|
+
console.log(` Reports : ${status.counts.reports}`);
|
|
235
|
+
console.log(` Open review : ${status.counts.review_items}`);
|
|
236
|
+
if (status.newest_iso) console.log(` Newest entry : ${status.newest_iso}`);
|
|
237
|
+
|
|
238
|
+
const indexFile = path.join(stateDir, 'index.md');
|
|
239
|
+
const fileUrl = 'file:///' + indexFile.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(` Open the wiki : ${fileUrl}`);
|
|
242
|
+
console.log(` Local path : ${indexFile}`);
|
|
243
|
+
console.log('');
|
|
244
|
+
console.log(' Next:');
|
|
245
|
+
console.log(' kushi global ask <question> Ask the wiki');
|
|
246
|
+
console.log(' kushi promote <project> <page> Promote a project page');
|
|
247
|
+
console.log(' kushi global lint Privacy + freshness scan');
|
|
248
|
+
console.log(' kushi global show-root Inspect the resolution chain');
|
|
249
|
+
console.log('');
|
|
250
|
+
}
|
|
251
|
+
|
|
25
252
|
export async function runGlobalStatus() {
|
|
26
253
|
const status = globalStatus();
|
|
27
254
|
console.log('');
|
package/src/global-wiki.mjs
CHANGED
|
@@ -85,18 +85,150 @@ Open items that need your attention. Populated by:
|
|
|
85
85
|
|
|
86
86
|
const SCAFFOLD_DIRS = ['answers', 'reports'];
|
|
87
87
|
|
|
88
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Per-user config file
|
|
90
|
+
//
|
|
91
|
+
// Kushi has TWO levels of config that can carry settings like `globalRoot`:
|
|
92
|
+
//
|
|
93
|
+
// workspace/shared : <cwd>/.kushi/config/shared/kushi.yml
|
|
94
|
+
// Team-shared (committed). Seeded by installer.
|
|
95
|
+
// Found by walking up from cwd looking for .kushi/.
|
|
96
|
+
//
|
|
97
|
+
// home (per-user) : ~/.kushi/config.json
|
|
98
|
+
// Last-resort fallback when not inside a workspace.
|
|
99
|
+
// Also seeded by installer.
|
|
100
|
+
//
|
|
101
|
+
// Resolution order for the global wiki root:
|
|
102
|
+
// 1. $KUSHI_GLOBAL_ROOT (env override; tests, ad-hoc redirects)
|
|
103
|
+
// 2. workspace shared kushi.yml (team-shared persistent setting)
|
|
104
|
+
// 3. ~/.kushi/config.json (per-machine persistent setting)
|
|
105
|
+
// 4. ~/.kushi-global/ (default)
|
|
106
|
+
//
|
|
107
|
+
// The shared file follows the same seed-once contract as integrations.yml:
|
|
108
|
+
// preserved on every reinstall unless --force.
|
|
109
|
+
|
|
110
|
+
export function userConfigPath(env = process.env) {
|
|
111
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
112
|
+
return path.join(home, '.kushi', 'config.json');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function readUserConfig(env = process.env) {
|
|
116
|
+
const f = userConfigPath(env);
|
|
117
|
+
if (!fs.existsSync(f)) return {};
|
|
118
|
+
try { return JSON.parse(fs.readFileSync(f, 'utf8')) || {}; }
|
|
119
|
+
catch { return {}; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function writeUserConfig(patch, env = process.env) {
|
|
123
|
+
const f = userConfigPath(env);
|
|
124
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
125
|
+
const cur = readUserConfig(env);
|
|
126
|
+
const next = { ...cur, ...patch };
|
|
127
|
+
fs.writeFileSync(f, JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
128
|
+
return { path: f, config: next };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Walk up from cwd to find the nearest workspace `.kushi/config/shared/kushi.yml`.
|
|
133
|
+
* Returns absolute path or null.
|
|
134
|
+
*/
|
|
135
|
+
export function findWorkspaceSharedConfig(startDir = process.cwd()) {
|
|
136
|
+
let dir = path.resolve(startDir);
|
|
137
|
+
const root = path.parse(dir).root;
|
|
138
|
+
while (true) {
|
|
139
|
+
const candidate = path.join(dir, '.kushi', 'config', 'shared', 'kushi.yml');
|
|
140
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
141
|
+
if (dir === root) return null;
|
|
142
|
+
const parent = path.dirname(dir);
|
|
143
|
+
if (parent === dir) return null;
|
|
144
|
+
dir = parent;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Tiny tolerant reader for the shared kushi.yml file. Only reads top-level
|
|
150
|
+
* scalar keys (globalRoot today). Avoids adding a YAML dep to src/.
|
|
151
|
+
*
|
|
152
|
+
* globalRoot: 'C:\path\to\wiki' → "C:\\path\\to\\wiki"
|
|
153
|
+
* globalRoot: null → null (treated as unset)
|
|
154
|
+
* globalRoot: ~ → null (yaml shorthand for null)
|
|
155
|
+
* globalRoot: → null (empty value)
|
|
156
|
+
*/
|
|
157
|
+
export function readWorkspaceSharedConfig(file) {
|
|
158
|
+
if (!file || !fs.existsSync(file)) return {};
|
|
159
|
+
const txt = fs.readFileSync(file, 'utf8');
|
|
160
|
+
const obj = {};
|
|
161
|
+
for (const rawLine of txt.split(/\r?\n/)) {
|
|
162
|
+
const line = rawLine.replace(/\s+#.*$/, '').trim();
|
|
163
|
+
if (!line || line.startsWith('#')) continue;
|
|
164
|
+
const m = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
|
|
165
|
+
if (!m) continue;
|
|
166
|
+
const key = m[1];
|
|
167
|
+
let val = m[2].trim();
|
|
168
|
+
if (val === '' || val === '~' || val.toLowerCase() === 'null') { obj[key] = null; continue; }
|
|
169
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
170
|
+
val = val.slice(1, -1);
|
|
171
|
+
}
|
|
172
|
+
obj[key] = val;
|
|
173
|
+
}
|
|
174
|
+
return obj;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function writeWorkspaceSharedConfig(file, patch) {
|
|
178
|
+
let txt = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
179
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
180
|
+
const valStr = (v === null || v === undefined || v === '')
|
|
181
|
+
? 'null'
|
|
182
|
+
: (/^[\w./\\:~+\-]+$/.test(String(v)) ? String(v) : `'${String(v).replace(/'/g, "''")}'`);
|
|
183
|
+
const re = new RegExp(`^${k}\\s*:.*$`, 'm');
|
|
184
|
+
if (re.test(txt)) txt = txt.replace(re, `${k}: ${valStr}`);
|
|
185
|
+
else txt += (txt.endsWith('\n') ? '' : '\n') + `${k}: ${valStr}\n`;
|
|
186
|
+
}
|
|
187
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
188
|
+
fs.writeFileSync(file, txt, 'utf8');
|
|
189
|
+
return { path: file };
|
|
190
|
+
}
|
|
191
|
+
|
|
88
192
|
// ───────────────────────────────────────────────────────────────────────────
|
|
89
193
|
// Resolution
|
|
90
194
|
|
|
91
|
-
export function resolveGlobalRoot(env = process.env) {
|
|
195
|
+
export function resolveGlobalRoot(env = process.env, opts = {}) {
|
|
92
196
|
const override = env.KUSHI_GLOBAL_ROOT;
|
|
93
197
|
if (override && override.trim()) {
|
|
94
198
|
return path.resolve(expandTilde(override.trim(), env));
|
|
95
199
|
}
|
|
200
|
+
const wsFile = findWorkspaceSharedConfig(opts.cwd || process.cwd());
|
|
201
|
+
if (wsFile) {
|
|
202
|
+
const ws = readWorkspaceSharedConfig(wsFile);
|
|
203
|
+
if (ws.globalRoot && typeof ws.globalRoot === 'string' && ws.globalRoot.trim()) {
|
|
204
|
+
return path.resolve(expandTilde(ws.globalRoot.trim(), env));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const cfg = readUserConfig(env);
|
|
208
|
+
if (cfg.globalRoot && typeof cfg.globalRoot === 'string' && cfg.globalRoot.trim()) {
|
|
209
|
+
return path.resolve(expandTilde(cfg.globalRoot.trim(), env));
|
|
210
|
+
}
|
|
96
211
|
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
97
212
|
return path.join(home, '.kushi-global');
|
|
98
213
|
}
|
|
99
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Returns the full resolution chain so `kushi global show-root` can display it.
|
|
217
|
+
*/
|
|
218
|
+
export function explainGlobalRoot(env = process.env, opts = {}) {
|
|
219
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
220
|
+
const wsFile = findWorkspaceSharedConfig(opts.cwd || process.cwd());
|
|
221
|
+
const wsVal = wsFile ? (readWorkspaceSharedConfig(wsFile).globalRoot || null) : null;
|
|
222
|
+
const sources = [
|
|
223
|
+
{ source: 'env', key: 'KUSHI_GLOBAL_ROOT', value: env.KUSHI_GLOBAL_ROOT || null },
|
|
224
|
+
{ source: 'workspace', key: wsFile ? `${wsFile} → globalRoot` : '(no workspace .kushi/config/shared/kushi.yml found)', value: wsVal, file: wsFile },
|
|
225
|
+
{ source: 'home', key: `${userConfigPath(env)} → globalRoot`, value: readUserConfig(env).globalRoot || null },
|
|
226
|
+
{ source: 'default', key: '~/.kushi-global', value: path.join(home, '.kushi-global') },
|
|
227
|
+
];
|
|
228
|
+
const winner = sources.find(s => s.value && String(s.value).trim());
|
|
229
|
+
return { resolved: resolveGlobalRoot(env, opts), winner, sources };
|
|
230
|
+
}
|
|
231
|
+
|
|
100
232
|
function expandTilde(p, env) {
|
|
101
233
|
if (p.startsWith('~')) {
|
|
102
234
|
const home = env.USERPROFILE || env.HOME || os.homedir();
|
package/src/seed-config.mjs
CHANGED
|
@@ -99,6 +99,34 @@ export function seedConfig(sourcePkgDir, destAbsolute) {
|
|
|
99
99
|
return result;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Seed ~/.kushi/config.json (per-user, per-machine settings) using the same
|
|
104
|
+
* seed-once contract as workspace user/ files: copied only if absent, preserved
|
|
105
|
+
* on reinstall unless `force: true` is passed.
|
|
106
|
+
*
|
|
107
|
+
* - Lives at <home>/.kushi/config.json (NOT inside a workspace .kushi/).
|
|
108
|
+
* - Template ships at plugin/templates/user-config.json.
|
|
109
|
+
* - Currently holds `globalRoot` (path to the global wiki). Designed to grow.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} sourcePkgDir – root of the npm package
|
|
112
|
+
* @param {{ force?: boolean, home?: string }} [opts]
|
|
113
|
+
* @returns {{ path: string, action: 'seeded'|'preserved'|'overwritten'|'no-template' }}
|
|
114
|
+
*/
|
|
115
|
+
export function seedGlobalUserConfig(sourcePkgDir, opts = {}) {
|
|
116
|
+
const home = opts.home || os.homedir();
|
|
117
|
+
const dst = path.join(home, '.kushi', 'config.json');
|
|
118
|
+
const src = path.join(sourcePkgDir, PLUGIN_SOURCE_DIR, 'templates', 'user-config.json');
|
|
119
|
+
if (!fs.existsSync(src)) return { path: dst, action: 'no-template' };
|
|
120
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
121
|
+
|
|
122
|
+
const exists = fs.existsSync(dst);
|
|
123
|
+
if (exists && !opts.force) {
|
|
124
|
+
return { path: dst, action: 'preserved' };
|
|
125
|
+
}
|
|
126
|
+
fs.cpSync(src, dst, { force: true });
|
|
127
|
+
return { path: dst, action: exists ? 'overwritten' : 'seeded' };
|
|
128
|
+
}
|
|
129
|
+
|
|
102
130
|
/**
|
|
103
131
|
* Mechanically derive defaults for freshly-seeded user-config files.
|
|
104
132
|
* Only modifies files in `seededFiles` (i.e. just-created in this run).
|
package/src/setup-wizard.mjs
CHANGED
|
@@ -73,6 +73,17 @@ export async function runSetupWizard({ args = [] } = {}) {
|
|
|
73
73
|
const root = (await p.ask(' Where is your engagement root?', detectedRoot, 'KUSHI_WIZARD_ROOT')) || detectedRoot;
|
|
74
74
|
const hosts = (await p.ask(' Install for clawpilot / vscode / both?', detectedHosts, 'KUSHI_WIZARD_HOSTS')) || detectedHosts;
|
|
75
75
|
const wantGlobal = (await p.ask(' Enable global wiki at ~/.kushi-global/?', 'y', 'KUSHI_WIZARD_GLOBAL')) || 'y';
|
|
76
|
+
let globalRootChoice = '';
|
|
77
|
+
if (/^y/i.test(wantGlobal)) {
|
|
78
|
+
const envRoot = process.env.KUSHI_GLOBAL_ROOT || '';
|
|
79
|
+
const defaultRoot = envRoot || '(default ~/.kushi-global/)';
|
|
80
|
+
globalRootChoice = (await p.ask(
|
|
81
|
+
' Global wiki path? (Enter for default; or a OneDrive-synced SharePoint folder for team sharing)',
|
|
82
|
+
defaultRoot,
|
|
83
|
+
'KUSHI_WIZARD_GLOBAL_ROOT',
|
|
84
|
+
)) || defaultRoot;
|
|
85
|
+
if (globalRootChoice === '(default ~/.kushi-global/)') globalRootChoice = '';
|
|
86
|
+
}
|
|
76
87
|
const wantWorkspace = (await p.ask(
|
|
77
88
|
` Scaffold current directory (${cwd}) as a kushi workspace?${cwdHasKushi ? ' (already present)' : ''}`,
|
|
78
89
|
defaultWorkspace,
|
|
@@ -85,6 +96,7 @@ export async function runSetupWizard({ args = [] } = {}) {
|
|
|
85
96
|
engagementRoot: root,
|
|
86
97
|
hosts: hosts.toLowerCase(),
|
|
87
98
|
globalWiki: /^y/i.test(wantGlobal),
|
|
99
|
+
globalWikiRoot: globalRootChoice ? globalRootChoice.trim() : '',
|
|
88
100
|
workspaceInstall: /^y/i.test(wantWorkspace),
|
|
89
101
|
workspacePath: cwd,
|
|
90
102
|
};
|
|
@@ -128,7 +140,7 @@ export async function runSetupWizard({ args = [] } = {}) {
|
|
|
128
140
|
if (answers.globalWiki) {
|
|
129
141
|
try {
|
|
130
142
|
const { runGlobalInit } = await import('./global-wiki-cli.mjs');
|
|
131
|
-
await runGlobalInit();
|
|
143
|
+
await runGlobalInit({ root: answers.globalWikiRoot || undefined, persistEnv: !!answers.globalWikiRoot });
|
|
132
144
|
} catch (err) {
|
|
133
145
|
console.error(` global init skipped: ${err.message}`);
|
|
134
146
|
}
|