llm-wiki-kit 0.2.3 → 0.2.5

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.
@@ -0,0 +1,178 @@
1
+ import { realpathSync } from 'fs';
2
+ import { access, lstat, readFile } from 'fs/promises';
3
+ import { constants as fsConstants } from 'fs';
4
+ import { delimiter, dirname, extname, join, resolve } from 'path';
5
+ import { packageName, packageRoot } from './version.js';
6
+
7
+ export function runtimePlatform(options = {}) {
8
+ return options.platform || process.platform;
9
+ }
10
+
11
+ export function isWindows(options = {}) {
12
+ return runtimePlatform(options) === 'win32';
13
+ }
14
+
15
+ export function pathDelimiter(options = {}) {
16
+ return isWindows(options) ? ';' : delimiter;
17
+ }
18
+
19
+ export function pathEnvValue(env = process.env, options = {}) {
20
+ if (!isWindows(options)) return env.PATH || '';
21
+ const key = Object.keys(env).find((name) => name.toLowerCase() === 'path');
22
+ return key ? env[key] || '' : env.PATH || '';
23
+ }
24
+
25
+ export function dataHomeRelative(options = {}) {
26
+ return isWindows(options)
27
+ ? join('AppData', 'Local')
28
+ : join('.local', 'share');
29
+ }
30
+
31
+ export function cacheHomeRelative(options = {}) {
32
+ return isWindows(options)
33
+ ? join('AppData', 'Local', 'llm-wiki-kit', 'cache')
34
+ : '.cache';
35
+ }
36
+
37
+ export function commandQuote(value, options = {}) {
38
+ return isWindows(options)
39
+ ? `"${String(value).replace(/"/g, '""')}"`
40
+ : `"${String(value).replace(/(["\\$`])/g, '\\$1')}"`;
41
+ }
42
+
43
+ export function commandForNodeScript(scriptPath, args = [], options = {}) {
44
+ if (!isWindows(options)) {
45
+ return [commandQuote(process.execPath, options), commandQuote(scriptPath, options), ...args].join(' ');
46
+ }
47
+ return [process.execPath, scriptPath, ...args]
48
+ .map((part) => commandQuote(part, options))
49
+ .join(' ');
50
+ }
51
+
52
+ export function realpathOrOriginal(path) {
53
+ if (!path) return null;
54
+ try {
55
+ return realpathSync(path);
56
+ } catch {
57
+ return path;
58
+ }
59
+ }
60
+
61
+ export function sameResolvedPath(left, right) {
62
+ const resolvedLeft = realpathOrOriginal(left);
63
+ const resolvedRight = realpathOrOriginal(right);
64
+ return Boolean(resolvedLeft && resolvedRight && resolvedLeft === resolvedRight);
65
+ }
66
+
67
+ export function samePath(left, right) {
68
+ if (!left || !right) return false;
69
+ return resolve(left) === resolve(right);
70
+ }
71
+
72
+ function unique(values) {
73
+ return [...new Set(values.filter(Boolean))];
74
+ }
75
+
76
+ function windowsExtensions(env = process.env) {
77
+ return unique((env.PATHEXT || '.COM;.EXE;.BAT;.CMD;.PS1')
78
+ .split(';')
79
+ .map((part) => part.trim())
80
+ .filter(Boolean)
81
+ .flatMap((part) => [part, part.toLowerCase(), part.toUpperCase()]));
82
+ }
83
+
84
+ function commandCandidates(dir, command, options = {}) {
85
+ const direct = join(dir, command);
86
+ if (!isWindows(options) || extname(command)) return [direct];
87
+ return [direct, ...windowsExtensions(options.env).map((ext) => join(dir, `${command}${ext}`))];
88
+ }
89
+
90
+ async function commandFileExists(path, options = {}) {
91
+ try {
92
+ await access(path, isWindows(options) ? fsConstants.F_OK : fsConstants.X_OK);
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ export async function commandPaths(command, options = {}) {
100
+ const env = options.env || process.env;
101
+ const pathValue = pathEnvValue(env, options);
102
+ const paths = [];
103
+ for (const dir of pathValue.split(pathDelimiter(options)).filter(Boolean)) {
104
+ for (const candidate of commandCandidates(dir, command, { ...options, env })) {
105
+ if (await commandFileExists(candidate, options)) paths.push(candidate);
106
+ }
107
+ }
108
+ return unique(paths);
109
+ }
110
+
111
+ function normalizePathText(value) {
112
+ return String(value || '').replace(/\\/g, '/').toLowerCase();
113
+ }
114
+
115
+ async function commandShimReferencesRuntime(commandPath, binPath, options = {}) {
116
+ if (!isWindows(options) || !commandPath) return false;
117
+ const extension = extname(commandPath).toLowerCase();
118
+ if (!['', '.cmd', '.bat', '.ps1'].includes(extension)) return false;
119
+ let content = '';
120
+ try {
121
+ content = await readFile(commandPath, 'utf8');
122
+ } catch {
123
+ return false;
124
+ }
125
+ const normalized = normalizePathText(content);
126
+ const normalizedBin = normalizePathText(binPath);
127
+ if (normalized.includes(normalizedBin)) return true;
128
+ if (options.installSource === 'npm') {
129
+ const commandDirPackageRoot = join(dirname(commandPath), 'node_modules', packageName());
130
+ return sameResolvedPath(commandDirPackageRoot, packageRoot) &&
131
+ normalized.includes(`node_modules/${packageName()}/bin/llm-wiki.js`);
132
+ }
133
+ return false;
134
+ }
135
+
136
+ export async function commandMatchesRuntime(commandPath, binPath, options = {}) {
137
+ if (!commandPath || !binPath) return false;
138
+ if (sameResolvedPath(commandPath, binPath)) return true;
139
+ return commandShimReferencesRuntime(commandPath, binPath, options);
140
+ }
141
+
142
+ export async function inspectCommandPath(commandPath, binPath, options = {}) {
143
+ if (!commandPath) {
144
+ return {
145
+ path: null,
146
+ exists: false,
147
+ symlink: false,
148
+ target: null,
149
+ resolved: null,
150
+ managed: false,
151
+ matchesRuntime: false,
152
+ };
153
+ }
154
+ try {
155
+ const stat = await lstat(commandPath);
156
+ const resolved = realpathOrOriginal(commandPath);
157
+ const matchesRuntime = await commandMatchesRuntime(commandPath, binPath, options);
158
+ return {
159
+ path: commandPath,
160
+ exists: true,
161
+ symlink: stat.isSymbolicLink(),
162
+ target: null,
163
+ resolved,
164
+ managed: matchesRuntime,
165
+ matchesRuntime,
166
+ };
167
+ } catch {
168
+ return {
169
+ path: commandPath,
170
+ exists: false,
171
+ symlink: false,
172
+ target: null,
173
+ resolved: null,
174
+ managed: false,
175
+ matchesRuntime: false,
176
+ };
177
+ }
178
+ }
package/src/projects.js CHANGED
@@ -58,12 +58,17 @@ async function looksLikeProjectRoot(root) {
58
58
  export async function discoverProjectRoots(searchRoot, options = {}) {
59
59
  const root = resolve(searchRoot || process.cwd());
60
60
  const maxDirs = options.maxDirs || 5000;
61
+ const progressEvery = options.progressEvery || 1000;
62
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
61
63
  const roots = new Set();
62
64
  let seen = 0;
63
65
 
64
66
  async function walk(dir) {
65
67
  if (seen >= maxDirs) return;
66
68
  seen += 1;
69
+ if (onProgress && seen % progressEvery === 0) {
70
+ onProgress(`scanned ${seen}/${maxDirs} directories while discovering projects`);
71
+ }
67
72
 
68
73
  if (await looksLikeProjectRoot(dir)) {
69
74
  roots.add(dir);
@@ -91,6 +96,9 @@ export async function discoverProjectRoots(searchRoot, options = {}) {
91
96
  }
92
97
 
93
98
  await walk(root);
99
+ if (onProgress) {
100
+ onProgress(`project discovery scanned ${seen} director${seen === 1 ? 'y' : 'ies'}; found ${roots.size}`);
101
+ }
94
102
  return [...roots].sort();
95
103
  }
96
104
 
package/src/update.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawnSync } from 'child_process';
1
+ import { spawn } from 'child_process';
2
2
  import { join, resolve } from 'path';
3
3
  import { exists } from './fs-utils.js';
4
4
  import { appendWikiLog } from './project.js';
@@ -7,18 +7,101 @@ import { applyProjectTemplateUpdate, inspectProjectState } from './project-state
7
7
  import { knownProjectRoots, recordProject } from './projects.js';
8
8
  import { binPath, detectInstallSource, packageName, runtimeVersion } from './version.js';
9
9
 
10
- function runCommand(command, args, options = {}) {
11
- const result = spawnSync(command, args, {
12
- encoding: 'utf8',
13
- env: options.env || process.env,
14
- timeout: options.timeout || 120000,
10
+ function commandLine(command, args) {
11
+ return [command, ...args].join(' ');
12
+ }
13
+
14
+ function elapsedMs(startedAt) {
15
+ return Date.now() - startedAt;
16
+ }
17
+
18
+ function progress(options, message) {
19
+ if (typeof options.onProgress === 'function') options.onProgress(message);
20
+ }
21
+
22
+ async function runCommand(command, args, options = {}) {
23
+ const timeout = options.timeout || 120000;
24
+ const killGraceMs = options.killGraceMs || 2000;
25
+ const label = options.label || commandLine(command, args);
26
+ const startedAt = Date.now();
27
+ const detached = process.platform !== 'win32';
28
+ let stdout = '';
29
+ let stderr = '';
30
+ let settled = false;
31
+ let timedOut = false;
32
+ let timeoutId = null;
33
+ let killId = null;
34
+
35
+ progress(options, `starting ${label}`);
36
+
37
+ return new Promise((resolveResult) => {
38
+ let child;
39
+ const finish = (result) => {
40
+ if (settled) return;
41
+ settled = true;
42
+ clearTimeout(timeoutId);
43
+ clearTimeout(killId);
44
+ progress(options, `${label} finished in ${elapsedMs(startedAt)}ms`);
45
+ resolveResult({
46
+ ...result,
47
+ stdout,
48
+ stderr,
49
+ elapsedMs: elapsedMs(startedAt),
50
+ timedOut,
51
+ });
52
+ };
53
+ const killChild = (signal) => {
54
+ if (!child?.pid) return;
55
+ try {
56
+ if (detached) {
57
+ process.kill(-child.pid, signal);
58
+ } else {
59
+ child.kill(signal);
60
+ }
61
+ } catch {
62
+ // The process may have already exited.
63
+ }
64
+ };
65
+
66
+ try {
67
+ child = spawn(command, args, {
68
+ detached,
69
+ env: options.env || process.env,
70
+ stdio: ['ignore', 'pipe', 'pipe'],
71
+ });
72
+ } catch (error) {
73
+ finish({ status: null, signal: null, error });
74
+ return;
75
+ }
76
+
77
+ child.stdout.setEncoding('utf8');
78
+ child.stderr.setEncoding('utf8');
79
+ child.stdout.on('data', (chunk) => {
80
+ stdout += chunk;
81
+ });
82
+ child.stderr.on('data', (chunk) => {
83
+ stderr += chunk;
84
+ });
85
+ child.on('error', (error) => {
86
+ finish({ status: null, signal: null, error });
87
+ });
88
+ child.on('close', (status, signal) => {
89
+ const error = timedOut
90
+ ? Object.assign(new Error(`${label} timed out after ${timeout}ms`), { code: 'ETIMEDOUT' })
91
+ : null;
92
+ finish({ status, signal, error });
93
+ });
94
+
95
+ timeoutId = setTimeout(() => {
96
+ timedOut = true;
97
+ progress(options, `${label} exceeded ${timeout}ms; terminating`);
98
+ killChild('SIGTERM');
99
+ killId = setTimeout(() => {
100
+ progress(options, `${label} did not exit after SIGTERM; killing`);
101
+ killChild('SIGKILL');
102
+ }, killGraceMs);
103
+ }, timeout);
15
104
  });
16
- return {
17
- status: result.status,
18
- stdout: result.stdout || '',
19
- stderr: result.stderr || '',
20
- error: result.error || null,
21
- };
22
105
  }
23
106
 
24
107
  function npmCommand() {
@@ -31,7 +114,11 @@ function binCommand() {
31
114
 
32
115
  function assertCommandOk(result, label) {
33
116
  if (result.status === 0 && !result.error) return;
34
- const detail = result.stderr.trim() || result.stdout.trim() || result.error?.message || 'unknown error';
117
+ const detail = result.stderr.trim() ||
118
+ result.stdout.trim() ||
119
+ result.error?.message ||
120
+ (result.signal ? `signal ${result.signal}` : '') ||
121
+ 'unknown error';
35
122
  throw new Error(`${label} failed: ${detail}`);
36
123
  }
37
124
 
@@ -45,7 +132,13 @@ function shouldUpdateAllProjects(options = {}) {
45
132
 
46
133
  async function projectRootsForUpdate(workspace, options = {}) {
47
134
  if (!shouldUpdateAllProjects(options)) return [workspace];
48
- const roots = await knownProjectRoots({ workspace });
135
+ progress(options, `discovering project roots under ${workspace}`);
136
+ const roots = await knownProjectRoots({
137
+ workspace,
138
+ maxDirs: options.maxDirs,
139
+ onProgress: options.onProgress,
140
+ });
141
+ progress(options, `discovered ${roots.length} project root(s)`);
49
142
  return roots.length > 0 ? roots : [workspace];
50
143
  }
51
144
 
@@ -97,7 +190,9 @@ export function compareVersions(a, b) {
97
190
  export async function checkForUpdate(options = {}) {
98
191
  const target = options.to || 'latest';
99
192
  const installedVersion = runtimeVersion();
100
- const result = runCommand(npmCommand(), ['view', `${packageName()}@${target}`, 'version'], {
193
+ const result = await runCommand(npmCommand(), ['view', `${packageName()}@${target}`, 'version'], {
194
+ ...options,
195
+ label: 'npm view',
101
196
  timeout: options.timeout || 30000,
102
197
  });
103
198
  assertCommandOk(result, 'npm view');
@@ -120,9 +215,16 @@ export async function postUpdate(options = {}) {
120
215
  });
121
216
 
122
217
  if (options.all) {
123
- const workspaces = await knownProjectRoots({ workspace });
218
+ progress(options, `discovering project roots under ${workspace}`);
219
+ const workspaces = await knownProjectRoots({
220
+ workspace,
221
+ maxDirs: options.maxDirs,
222
+ onProgress: options.onProgress,
223
+ });
224
+ progress(options, `post-update will inspect ${workspaces.length} project root(s)`);
124
225
  const projects = [];
125
226
  for (const projectRoot of workspaces) {
227
+ progress(options, `applying managed templates to ${projectRoot}`);
126
228
  const projectResult = await applyTemplatesToProject(projectRoot, options);
127
229
  if (!options.noProject && await hasProjectWiki(projectRoot)) {
128
230
  await appendWikiLog(projectRoot, `llm-wiki-kit post-update applied runtime ${runtimeVersion()}; changed templates: ${projectResult.changed.length}`);
@@ -194,7 +296,9 @@ export async function update(options = {}) {
194
296
  const target = options.to || 'latest';
195
297
  const shouldRunNpmInstall = check.updateAvailable;
196
298
  const installResult = shouldRunNpmInstall
197
- ? runCommand(npmCommand(), ['install', '-g', `${packageName()}@${target}`], {
299
+ ? await runCommand(npmCommand(), ['install', '-g', `${packageName()}@${target}`], {
300
+ ...options,
301
+ label: 'npm install -g',
198
302
  timeout: options.timeout || 120000,
199
303
  })
200
304
  : {
@@ -210,7 +314,13 @@ export async function update(options = {}) {
210
314
  if (options.noProject) postArgs.push('--no-project');
211
315
  if (options.codex === false) postArgs.push('--no-codex');
212
316
  if (options.claude === false) postArgs.push('--no-claude');
213
- const postResult = runCommand(process.execPath, postArgs, {
317
+ const postResult = await runCommand(process.execPath, postArgs, {
318
+ ...options,
319
+ env: {
320
+ ...process.env,
321
+ LLM_WIKI_KIT_PROGRESS: process.env.LLM_WIKI_KIT_PROGRESS || '1',
322
+ },
323
+ label: 'post-update',
214
324
  timeout: options.timeout || 120000,
215
325
  });
216
326
  assertCommandOk(postResult, 'post-update');