kushi-agents 5.7.2 → 5.7.3
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": "5.7.
|
|
3
|
+
"version": "5.7.3",
|
|
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 src/get-kushi-config.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 src/seed-config-derived.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,30 @@ Newest on top. Format defined in [`README.md`](./README.md). Use this file when
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
### 2026-05-29 — `<auto>` placeholder check was blocking fresh installs
|
|
8
|
+
|
|
9
|
+
**Symptom**: After a clean `npx kushi-agents@latest --all-hosts` install, the very first command users tried (`& '.\.kushi\lib\Get-KushiConfig.ps1' -Name 'm365-auth'`) threw a placeholder error citing `<auto>`. But `<auto>` is the explicit "Kushi will resolve this at runtime from identity / OS / WorkIQ" sentinel, by design — not an unfilled placeholder.
|
|
10
|
+
|
|
11
|
+
**Root cause**: `<auto>` was wrongly listed alongside `__FILL_ME_IN__` in the blocking-sentinels array of `Get-KushiConfig.ps1`. That conflated two different concepts:
|
|
12
|
+
|
|
13
|
+
- **Hard-blocking placeholders** (`__FILL_ME_IN__`, `<TENANT_ID>`, `<engagement-root>`, `<ProjectA>`): values that genuinely cannot be derived without user input. These should block.
|
|
14
|
+
- **Runtime-resolution sentinels** (`<auto>`, `<YYYY-MM-DD>`): values that Kushi *can* derive at use-time from `os.userInfo()`, `Date.now()`, or WorkIQ whoami. These should NOT block; they're a feature flag for lazy fill.
|
|
15
|
+
|
|
16
|
+
**Fix shipped (v5.7.3, 2026-05-29)**:
|
|
17
|
+
|
|
18
|
+
1. **Removed `<auto>` from blocking sentinels** in `plugin/lib/Get-KushiConfig.ps1`. Added a comment explaining why so future contributors don't add it back.
|
|
19
|
+
2. **Added `applyDerivedDefaults()` to `src/seed-config.mjs`** — runs at install time on freshly-seeded user/ files (never touches preserved/edited copies):
|
|
20
|
+
- `m365-auth.json#_meta.last_reviewed` ← today (YYYY-MM-DD)
|
|
21
|
+
- `m365-auth.json#_meta.owner` ← `${os.userInfo().username}@microsoft.com`
|
|
22
|
+
- `m365-auth.json#m365Auth.sharePointContext.localProjectsRoot` ← auto-detected from `$env:OneDriveCommercial`, `~/OneDrive - Microsoft/ISE/Engagement Assets`, or other `OneDrive - <Tenant>/ISE/Engagement Assets` paths if exactly one exists
|
|
23
|
+
- `project-evidence.yml#projects_root` ← same OneDrive auto-detection
|
|
24
|
+
3. **Install now prints what was auto-filled** under `Config.user/ auto-filled defaults:` so users can see exactly what Kushi figured out and what they still need to edit.
|
|
25
|
+
4. **Six new unit tests** in `src/seed-config-derived.test.mjs` + an integration test in `src/get-kushi-config.test.mjs` that does a real install and asserts `Get-KushiConfig.ps1 -Name m365-auth` passes (or fails ONLY with `__FILL_ME_IN__`, never `<auto>`).
|
|
26
|
+
|
|
27
|
+
**Lesson**: When a config schema mixes hard-blocking placeholders with explicit "auto-resolve" sentinels, keep them in *separate lists*. Conflating them in one validation pass is a footgun: every new lazy-fill marker becomes a fresh-install bug. Also: install-time derivation is cheaper than runtime resolution — fill what you can mechanically (today's date, OS username, env vars, well-known paths) before a single user prompt is needed.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
7
31
|
### 2026-05-29 — `[switch]` parameter collides with same-name local var in PowerShell
|
|
8
32
|
|
|
9
33
|
**Symptom**: Calling `.\.kushi\lib\Get-KushiConfig.ps1 -Name 'm365-auth' -Raw` threw at the **call site**:
|
|
@@ -79,7 +79,10 @@ $script:RequiredFields = @{
|
|
|
79
79
|
# Sentinel substrings that indicate "still a placeholder."
|
|
80
80
|
$script:Sentinels = @(
|
|
81
81
|
'__FILL_ME_IN__',
|
|
82
|
-
'<auto>'
|
|
82
|
+
# NOTE: '<auto>' is intentionally NOT in this list. It is a runtime-resolution
|
|
83
|
+
# marker meaning "Kushi will derive this from identity/env at use time" — not
|
|
84
|
+
# an unfilled placeholder. Callers that need a concrete value should resolve
|
|
85
|
+
# <auto> themselves (e.g. via WorkIQ whoami or os.userInfo()).
|
|
83
86
|
'<TENANT_ID>',
|
|
84
87
|
'<NOTEBOOK_ID>',
|
|
85
88
|
'<NOTEBOOK_NAME>',
|
|
@@ -87,7 +90,9 @@ $script:Sentinels = @(
|
|
|
87
90
|
'<SP_LOCAL_ROOT>',
|
|
88
91
|
'<your-alias>',
|
|
89
92
|
'<Your Full Name>',
|
|
90
|
-
'your.email@example.com'
|
|
93
|
+
'your.email@example.com',
|
|
94
|
+
'<engagement-root>',
|
|
95
|
+
'<ProjectA>'
|
|
91
96
|
)
|
|
92
97
|
|
|
93
98
|
function Test-Sentinel {
|
|
@@ -67,3 +67,26 @@ test('Get-KushiConfig.ps1 -Path returns absolute resolved path', () => {
|
|
|
67
67
|
assert.match(r.stdout, /m365-auth\.json/, `expected resolved path, got stdout=${r.stdout}`);
|
|
68
68
|
assert.doesNotMatch(r.stderr, /SwitchParameter/i);
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
test('Get-KushiConfig.ps1 -Name m365-auth (no -AllowPlaceholders) passes on fresh install with auto-filled defaults', () => {
|
|
72
|
+
// Regression for: bootstrap rejected fresh installs because the template's
|
|
73
|
+
// <auto>, <YYYY-MM-DD>, and __FILL_ME_IN__ markers all triggered the
|
|
74
|
+
// placeholder check. Fix: <auto> removed from blocking sentinels (it's a
|
|
75
|
+
// runtime-resolve marker), and seed-config now mechanically fills
|
|
76
|
+
// last_reviewed (today), _meta.owner (from $env:USER), and localProjectsRoot
|
|
77
|
+
// (auto-detected from OneDrive) at install time.
|
|
78
|
+
const cwd = freshInstall();
|
|
79
|
+
const script = path.join(cwd, '.kushi', 'lib', 'Get-KushiConfig.ps1');
|
|
80
|
+
const r = pwsh(cwd, [`& '${script}' -Name 'm365-auth' -Raw | Out-Null; if ($?) { 'OK' } else { 'FAIL' }`]);
|
|
81
|
+
// Note: localProjectsRoot may still be __FILL_ME_IN__ on machines with no OneDrive,
|
|
82
|
+
// so accept either (a) clean pass, or (b) a placeholder error that mentions ONLY
|
|
83
|
+
// __FILL_ME_IN__ (not <auto>, not <YYYY-MM-DD>).
|
|
84
|
+
if (r.stdout.includes('OK')) {
|
|
85
|
+
assert.doesNotMatch(r.stderr, /SwitchParameter/i);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// If it failed, the only acceptable reason is genuine __FILL_ME_IN__ on machines
|
|
89
|
+
// without an auto-detectable OneDrive root.
|
|
90
|
+
assert.match(r.stderr, /__FILL_ME_IN__/, `expected only __FILL_ME_IN__ blocker, got: ${r.stderr}`);
|
|
91
|
+
assert.doesNotMatch(r.stderr, /<auto>/, `<auto> must NOT block — it is a runtime-resolve marker`);
|
|
92
|
+
});
|
package/src/main.mjs
CHANGED
|
@@ -231,6 +231,10 @@ function printSeedReport(r, dispDestSlash) {
|
|
|
231
231
|
for (const f of r.seededUser) console.log(` - ${f} -> seeded (edit before first bootstrap)`);
|
|
232
232
|
for (const f of r.preservedUser) console.log(` - ${f} -> preserved (already exists)`);
|
|
233
233
|
}
|
|
234
|
+
if (r.derived && r.derived.length) {
|
|
235
|
+
console.log(` auto-filled defaults:`);
|
|
236
|
+
for (const d of r.derived) console.log(` - ${d.file}#${d.field} -> ${d.value}`);
|
|
237
|
+
}
|
|
234
238
|
if (r.gitignore === 'created' || r.gitignore === 'appended') {
|
|
235
239
|
console.log(` .gitignore ${r.gitignore === 'created' ? 'created' : 'appended'} -> config/user/ excluded`);
|
|
236
240
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Tests for applyDerivedDefaults() in seed-config.mjs.
|
|
2
|
+
//
|
|
3
|
+
// Verifies that mechanically-derivable fields (today's date, OS-derived
|
|
4
|
+
// identity, OneDrive auto-detection) are filled at install time so a fresh
|
|
5
|
+
// install passes the Get-KushiConfig.ps1 placeholder check without manual
|
|
6
|
+
// editing or `@Kushi setup` rituals.
|
|
7
|
+
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { seedConfig } from './seed-config.mjs';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const PKG = path.resolve(__dirname, '..');
|
|
18
|
+
|
|
19
|
+
function tmp() {
|
|
20
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'kushi-derived-'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('applyDerivedDefaults: fills last_reviewed with today (m365-auth.json)', () => {
|
|
24
|
+
const dest = tmp();
|
|
25
|
+
try {
|
|
26
|
+
const r = seedConfig(PKG, dest);
|
|
27
|
+
assert.ok(r.seededUser.includes('m365-auth.json'));
|
|
28
|
+
|
|
29
|
+
const cfgPath = path.join(dest, 'config', 'user', 'm365-auth.json');
|
|
30
|
+
const obj = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
31
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
32
|
+
assert.equal(obj._meta.last_reviewed, today, 'last_reviewed should be today');
|
|
33
|
+
assert.notEqual(obj._meta.last_reviewed, '<YYYY-MM-DD>', 'placeholder should be replaced');
|
|
34
|
+
|
|
35
|
+
assert.ok(r.derived.some((d) => d.field === '_meta.last_reviewed'), 'derived list should record last_reviewed');
|
|
36
|
+
} finally {
|
|
37
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('applyDerivedDefaults: fills _meta.owner from OS username', () => {
|
|
42
|
+
const dest = tmp();
|
|
43
|
+
try {
|
|
44
|
+
seedConfig(PKG, dest);
|
|
45
|
+
const cfgPath = path.join(dest, 'config', 'user', 'm365-auth.json');
|
|
46
|
+
const obj = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
47
|
+
const expectedUser = (os.userInfo().username || '').toLowerCase().replace(/[^a-z0-9._-]/g, '');
|
|
48
|
+
assert.equal(obj._meta.owner, `${expectedUser}@microsoft.com`);
|
|
49
|
+
assert.notEqual(obj._meta.owner, '<alias>@microsoft.com');
|
|
50
|
+
} finally {
|
|
51
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('applyDerivedDefaults: leaves <auto> identity markers alone (resolved at runtime)', () => {
|
|
56
|
+
const dest = tmp();
|
|
57
|
+
try {
|
|
58
|
+
seedConfig(PKG, dest);
|
|
59
|
+
const cfgPath = path.join(dest, 'config', 'user', 'm365-auth.json');
|
|
60
|
+
const obj = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
61
|
+
// <auto> in defaultLinkOwner is a runtime-resolution sentinel; seed-config
|
|
62
|
+
// must NOT preempt it (callers like pull-onenote resolve via WorkIQ whoami).
|
|
63
|
+
assert.equal(obj.m365Auth.oneNote.defaultLinkOwner, '<auto>');
|
|
64
|
+
} finally {
|
|
65
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('applyDerivedDefaults: produces valid JSON (no syntax errors)', () => {
|
|
70
|
+
const dest = tmp();
|
|
71
|
+
try {
|
|
72
|
+
seedConfig(PKG, dest);
|
|
73
|
+
const cfgPath = path.join(dest, 'config', 'user', 'm365-auth.json');
|
|
74
|
+
const txt = fs.readFileSync(cfgPath, 'utf8');
|
|
75
|
+
assert.doesNotThrow(() => JSON.parse(txt), 'seeded m365-auth.json must be parseable');
|
|
76
|
+
assert.ok(txt.endsWith('\n'), 'should end with newline');
|
|
77
|
+
} finally {
|
|
78
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('applyDerivedDefaults: project-evidence.yml stays parseable as YAML', () => {
|
|
83
|
+
const dest = tmp();
|
|
84
|
+
try {
|
|
85
|
+
seedConfig(PKG, dest);
|
|
86
|
+
const cfgPath = path.join(dest, 'config', 'user', 'project-evidence.yml');
|
|
87
|
+
const txt = fs.readFileSync(cfgPath, 'utf8');
|
|
88
|
+
// Sanity: top-level keys still present
|
|
89
|
+
assert.match(txt, /^alias:/m);
|
|
90
|
+
assert.match(txt, /^email:/m);
|
|
91
|
+
assert.match(txt, /^projects_root:/m);
|
|
92
|
+
} finally {
|
|
93
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('applyDerivedDefaults: does not re-derive on reinstall when files preserved', () => {
|
|
98
|
+
const dest = tmp();
|
|
99
|
+
try {
|
|
100
|
+
seedConfig(PKG, dest);
|
|
101
|
+
const cfgPath = path.join(dest, 'config', 'user', 'm365-auth.json');
|
|
102
|
+
// Edit the seeded file as a user would
|
|
103
|
+
const obj = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
104
|
+
obj._meta.last_reviewed = '2020-01-01';
|
|
105
|
+
fs.writeFileSync(cfgPath, JSON.stringify(obj, null, 2) + '\n');
|
|
106
|
+
|
|
107
|
+
// Reinstall — file is preserved, derived must not re-run on it
|
|
108
|
+
const r2 = seedConfig(PKG, dest);
|
|
109
|
+
assert.ok(r2.preservedUser.includes('m365-auth.json'));
|
|
110
|
+
assert.equal(r2.derived.length, 0, 'derived should be empty when no files were freshly seeded');
|
|
111
|
+
|
|
112
|
+
const after = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
113
|
+
assert.equal(after._meta.last_reviewed, '2020-01-01', 'user-edited value preserved');
|
|
114
|
+
} finally {
|
|
115
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
package/src/seed-config.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {
|
|
4
5
|
CONFIG_DIR,
|
|
@@ -86,12 +87,92 @@ export function seedConfig(sourcePkgDir, destAbsolute) {
|
|
|
86
87
|
overwriteAlways(CONFIG_DOCTRINE_FILES, sharedDir, result.doctrine);
|
|
87
88
|
overwriteAlways(CONFIG_EXAMPLE_FILES, sharedDir, result.examples);
|
|
88
89
|
|
|
90
|
+
// Post-process freshly-seeded user/ files: mechanically derive defaults
|
|
91
|
+
// (today's date, OneDrive root, OS-derived identity) so a brand-new install
|
|
92
|
+
// can be used immediately without manual editing or `@Kushi setup` rituals.
|
|
93
|
+
// Only runs on files we just seeded — never touches user-edited copies.
|
|
94
|
+
result.derived = applyDerivedDefaults(userDir, result.seededUser);
|
|
95
|
+
|
|
89
96
|
// .gitignore at <dest>/.gitignore — append our marker block if not already present
|
|
90
97
|
result.gitignore = ensureGitignore(destAbsolute, CONFIG_GITIGNORE_LINES);
|
|
91
98
|
|
|
92
99
|
return result;
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Mechanically derive defaults for freshly-seeded user-config files.
|
|
104
|
+
* Only modifies files in `seededFiles` (i.e. just-created in this run).
|
|
105
|
+
* Returns a list of {file, field, value} for the seed report.
|
|
106
|
+
*/
|
|
107
|
+
export function applyDerivedDefaults(userDir, seededFiles) {
|
|
108
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
109
|
+
const username = (os.userInfo().username || '').toLowerCase().replace(/[^a-z0-9._-]/g, '');
|
|
110
|
+
const oneDriveRoot = detectOneDriveProjectsRoot();
|
|
111
|
+
const filled = [];
|
|
112
|
+
|
|
113
|
+
if (seededFiles.includes('m365-auth.json')) {
|
|
114
|
+
const f = path.join(userDir, 'm365-auth.json');
|
|
115
|
+
try {
|
|
116
|
+
const txt = fs.readFileSync(f, 'utf8');
|
|
117
|
+
const obj = JSON.parse(txt);
|
|
118
|
+
if (obj?._meta?.last_reviewed === '<YYYY-MM-DD>') {
|
|
119
|
+
obj._meta.last_reviewed = today; filled.push({ file: 'm365-auth.json', field: '_meta.last_reviewed', value: today });
|
|
120
|
+
}
|
|
121
|
+
if (username && obj?._meta?.owner === '<alias>@microsoft.com') {
|
|
122
|
+
obj._meta.owner = `${username}@microsoft.com`; filled.push({ file: 'm365-auth.json', field: '_meta.owner', value: obj._meta.owner });
|
|
123
|
+
}
|
|
124
|
+
if (oneDriveRoot && obj?.m365Auth?.sharePointContext?.localProjectsRoot === '__FILL_ME_IN__') {
|
|
125
|
+
obj.m365Auth.sharePointContext.localProjectsRoot = oneDriveRoot;
|
|
126
|
+
filled.push({ file: 'm365-auth.json', field: 'm365Auth.sharePointContext.localProjectsRoot', value: oneDriveRoot });
|
|
127
|
+
}
|
|
128
|
+
fs.writeFileSync(f, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
|
129
|
+
} catch { /* leave file as-is on parse error */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (seededFiles.includes('project-evidence.yml')) {
|
|
133
|
+
const f = path.join(userDir, 'project-evidence.yml');
|
|
134
|
+
try {
|
|
135
|
+
let txt = fs.readFileSync(f, 'utf8');
|
|
136
|
+
if (oneDriveRoot && txt.includes("projects_root: '<engagement-root>'")) {
|
|
137
|
+
txt = txt.replace("projects_root: '<engagement-root>'", `projects_root: '${oneDriveRoot}'`);
|
|
138
|
+
filled.push({ file: 'project-evidence.yml', field: 'projects_root', value: oneDriveRoot });
|
|
139
|
+
}
|
|
140
|
+
fs.writeFileSync(f, txt, 'utf8');
|
|
141
|
+
} catch { /* leave file as-is on read error */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return filled;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Best-effort detection of the user's "Engagement Assets" parent folder.
|
|
149
|
+
* Probes (in order):
|
|
150
|
+
* 1. $env:OneDriveCommercial/ISE/Engagement Assets (Windows env-var)
|
|
151
|
+
* 2. ~/OneDrive - Microsoft/ISE/Engagement Assets (canonical Microsoft consultant path)
|
|
152
|
+
* 3. ~/OneDrive - <Tenant>/ISE/Engagement Assets (single OneDrive-* folder match)
|
|
153
|
+
* Returns the first existing path or null.
|
|
154
|
+
*/
|
|
155
|
+
function detectOneDriveProjectsRoot() {
|
|
156
|
+
const candidates = [];
|
|
157
|
+
if (process.env.OneDriveCommercial) {
|
|
158
|
+
candidates.push(path.join(process.env.OneDriveCommercial, 'ISE', 'Engagement Assets'));
|
|
159
|
+
}
|
|
160
|
+
candidates.push(path.join(os.homedir(), 'OneDrive - Microsoft', 'ISE', 'Engagement Assets'));
|
|
161
|
+
// Probe other OneDrive - * tenants (e.g. for non-Microsoft consultants)
|
|
162
|
+
try {
|
|
163
|
+
const home = os.homedir();
|
|
164
|
+
for (const entry of fs.readdirSync(home, { withFileTypes: true })) {
|
|
165
|
+
if (entry.isDirectory() && entry.name.startsWith('OneDrive - ') && entry.name !== 'OneDrive - Microsoft') {
|
|
166
|
+
candidates.push(path.join(home, entry.name, 'ISE', 'Engagement Assets'));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch { /* homedir not readable - skip */ }
|
|
170
|
+
for (const p of candidates) {
|
|
171
|
+
try { if (fs.statSync(p).isDirectory()) return p; } catch { /* not present */ }
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
95
176
|
function ensureGitignore(destAbsolute, lines) {
|
|
96
177
|
const target = path.join(destAbsolute, '.gitignore');
|
|
97
178
|
const marker = lines[0];
|