@pi-unipi/utility 0.1.1 → 0.2.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/README.md +121 -21
- package/package.json +16 -7
- package/skills/utility/SKILL.md +70 -0
- package/src/analytics/collector.ts +293 -0
- package/src/cache/ttl-cache.ts +311 -0
- package/src/commands.ts +186 -0
- package/src/diagnostics/engine.ts +298 -0
- package/src/display/capabilities.ts +200 -0
- package/src/display/width.ts +226 -0
- package/src/index.ts +172 -0
- package/src/info-screen.ts +80 -0
- package/src/lifecycle/cleanup.ts +332 -0
- package/src/lifecycle/process.ts +162 -0
- package/src/tools/batch.ts +229 -0
- package/src/tools/env.ts +134 -0
- package/src/tui/settings-inspector.ts +303 -0
- package/src/types.ts +257 -0
- package/commands.ts +0 -38
- package/index.ts +0 -34
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Process Lifecycle Manager
|
|
3
|
+
*
|
|
4
|
+
* General-purpose process lifecycle management:
|
|
5
|
+
* - Parent PID polling (orphan detection)
|
|
6
|
+
* - Signal handlers for graceful shutdown
|
|
7
|
+
* - Cleanup callbacks registry
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
CleanupFn,
|
|
12
|
+
LifecycleState,
|
|
13
|
+
ProcessLifecycleOptions,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
|
|
16
|
+
/** Default options */
|
|
17
|
+
const DEFAULTS: Required<ProcessLifecycleOptions> = {
|
|
18
|
+
pollIntervalMs: 30000,
|
|
19
|
+
handleSignals: true,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ProcessLifecycle manages the lifecycle of the utility process.
|
|
24
|
+
* Detects orphan status via parent PID polling and provides
|
|
25
|
+
* graceful shutdown with registered cleanup callbacks.
|
|
26
|
+
*/
|
|
27
|
+
export class ProcessLifecycle {
|
|
28
|
+
private state: LifecycleState = "running";
|
|
29
|
+
private parentPid: number | null = null;
|
|
30
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
31
|
+
private cleanups: Set<CleanupFn> = new Set();
|
|
32
|
+
private opts: Required<ProcessLifecycleOptions>;
|
|
33
|
+
|
|
34
|
+
constructor(options: ProcessLifecycleOptions = {}) {
|
|
35
|
+
this.opts = { ...DEFAULTS, ...options };
|
|
36
|
+
this.parentPid = process.ppid ?? null;
|
|
37
|
+
|
|
38
|
+
if (this.opts.handleSignals) {
|
|
39
|
+
this.installSignalHandlers();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.startPolling();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Current lifecycle state */
|
|
46
|
+
get currentState(): LifecycleState {
|
|
47
|
+
return this.state;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether the process is shutting down */
|
|
51
|
+
get isShuttingDown(): boolean {
|
|
52
|
+
return this.state === "shutting_down";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether the process has been orphaned */
|
|
56
|
+
get isOrphaned(): boolean {
|
|
57
|
+
return this.state === "orphaned";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Register a cleanup function to run on shutdown */
|
|
61
|
+
registerCleanup(fn: CleanupFn): () => void {
|
|
62
|
+
this.cleanups.add(fn);
|
|
63
|
+
return () => {
|
|
64
|
+
this.cleanups.delete(fn);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Unregister a cleanup function */
|
|
69
|
+
unregisterCleanup(fn: CleanupFn): void {
|
|
70
|
+
this.cleanups.delete(fn);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Initiate graceful shutdown */
|
|
74
|
+
async shutdown(reason: string = "requested"): Promise<void> {
|
|
75
|
+
if (this.state !== "running") return;
|
|
76
|
+
this.state = "shutting_down";
|
|
77
|
+
this.stopPolling();
|
|
78
|
+
|
|
79
|
+
const fns = Array.from(this.cleanups);
|
|
80
|
+
this.cleanups.clear();
|
|
81
|
+
|
|
82
|
+
for (const fn of fns) {
|
|
83
|
+
try {
|
|
84
|
+
await fn();
|
|
85
|
+
} catch {
|
|
86
|
+
// Best-effort cleanup — don't let one failure stop others
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.state = "error";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Start parent PID polling for orphan detection */
|
|
94
|
+
private startPolling(): void {
|
|
95
|
+
if (this.pollTimer) return;
|
|
96
|
+
|
|
97
|
+
this.pollTimer = setInterval(() => {
|
|
98
|
+
this.checkParent();
|
|
99
|
+
}, this.opts.pollIntervalMs);
|
|
100
|
+
|
|
101
|
+
// Don't block process exit on the timer
|
|
102
|
+
if (this.pollTimer.unref) {
|
|
103
|
+
this.pollTimer.unref();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Stop polling */
|
|
108
|
+
private stopPolling(): void {
|
|
109
|
+
if (this.pollTimer) {
|
|
110
|
+
clearInterval(this.pollTimer);
|
|
111
|
+
this.pollTimer = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Check if parent process is still alive */
|
|
116
|
+
private checkParent(): void {
|
|
117
|
+
if (this.parentPid === null) return;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Sending signal 0 checks if process exists without affecting it
|
|
121
|
+
process.kill(this.parentPid, 0);
|
|
122
|
+
} catch {
|
|
123
|
+
// Parent is gone — we're orphaned
|
|
124
|
+
this.state = "orphaned";
|
|
125
|
+
this.stopPolling();
|
|
126
|
+
this.shutdown("orphaned").catch(() => {
|
|
127
|
+
// Ignore shutdown errors in orphan path
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Install SIGTERM / SIGINT handlers */
|
|
133
|
+
private installSignalHandlers(): void {
|
|
134
|
+
const handler = (signal: string) => {
|
|
135
|
+
this.shutdown(signal).then(() => {
|
|
136
|
+
process.exit(0);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
process.once("SIGTERM", () => handler("SIGTERM"));
|
|
141
|
+
process.once("SIGINT", () => handler("SIGINT"));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Global singleton instance */
|
|
146
|
+
let globalLifecycle: ProcessLifecycle | null = null;
|
|
147
|
+
|
|
148
|
+
/** Get or create the global lifecycle manager */
|
|
149
|
+
export function getLifecycle(options?: ProcessLifecycleOptions): ProcessLifecycle {
|
|
150
|
+
if (!globalLifecycle) {
|
|
151
|
+
globalLifecycle = new ProcessLifecycle(options);
|
|
152
|
+
}
|
|
153
|
+
return globalLifecycle;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Dispose the global lifecycle manager */
|
|
157
|
+
export function disposeLifecycle(): void {
|
|
158
|
+
if (globalLifecycle) {
|
|
159
|
+
globalLifecycle.shutdown("dispose").catch(() => {});
|
|
160
|
+
globalLifecycle = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Batch Execution Tool
|
|
3
|
+
*
|
|
4
|
+
* Atomic batch of commands + searches with rollback on failure.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
BatchCommand,
|
|
9
|
+
BatchOptions,
|
|
10
|
+
BatchResult,
|
|
11
|
+
BatchReport,
|
|
12
|
+
} from "../types.js";
|
|
13
|
+
|
|
14
|
+
/** Default options */
|
|
15
|
+
const DEFAULTS: Required<BatchOptions> = {
|
|
16
|
+
failFast: true,
|
|
17
|
+
commandTimeoutMs: 30000,
|
|
18
|
+
totalTimeoutMs: 300000,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Executor function type — provided by the host environment */
|
|
22
|
+
export type CommandExecutor = (
|
|
23
|
+
command: BatchCommand,
|
|
24
|
+
) => Promise<unknown>;
|
|
25
|
+
|
|
26
|
+
/** Rollback function type */
|
|
27
|
+
export type RollbackFn = (
|
|
28
|
+
results: BatchResult[],
|
|
29
|
+
) => Promise<void>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute a batch of commands atomically.
|
|
33
|
+
*
|
|
34
|
+
* @param commands - Array of commands to execute
|
|
35
|
+
* @param executor - Function that executes a single command
|
|
36
|
+
* @param options - Batch execution options
|
|
37
|
+
* @param rollback - Optional rollback function called on failure
|
|
38
|
+
*/
|
|
39
|
+
export async function executeBatch(
|
|
40
|
+
commands: BatchCommand[],
|
|
41
|
+
executor: CommandExecutor,
|
|
42
|
+
options: BatchOptions = {},
|
|
43
|
+
rollback?: RollbackFn,
|
|
44
|
+
): Promise<BatchReport> {
|
|
45
|
+
const opts = { ...DEFAULTS, ...options };
|
|
46
|
+
const results: BatchResult[] = [];
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
|
|
49
|
+
// Total timeout guard
|
|
50
|
+
const totalDeadline = startTime + opts.totalTimeoutMs;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < commands.length; i++) {
|
|
53
|
+
const command = commands[i];
|
|
54
|
+
const cmdStart = Date.now();
|
|
55
|
+
|
|
56
|
+
// Check total timeout
|
|
57
|
+
if (Date.now() > totalDeadline) {
|
|
58
|
+
const timeoutResult: BatchResult = {
|
|
59
|
+
command,
|
|
60
|
+
success: false,
|
|
61
|
+
error: `Total batch timeout exceeded (${opts.totalTimeoutMs}ms)`,
|
|
62
|
+
durationMs: Date.now() - cmdStart,
|
|
63
|
+
};
|
|
64
|
+
results.push(timeoutResult);
|
|
65
|
+
|
|
66
|
+
if (opts.failFast) {
|
|
67
|
+
const report = createReport(results, startTime, !!rollback);
|
|
68
|
+
if (rollback) {
|
|
69
|
+
await rollback(results).catch(() => {
|
|
70
|
+
// Best-effort rollback
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return report;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Execute with per-command timeout
|
|
79
|
+
try {
|
|
80
|
+
const result = await withTimeout(
|
|
81
|
+
executor(command),
|
|
82
|
+
opts.commandTimeoutMs,
|
|
83
|
+
`Command timeout exceeded (${opts.commandTimeoutMs}ms)`,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
results.push({
|
|
87
|
+
command,
|
|
88
|
+
success: true,
|
|
89
|
+
result,
|
|
90
|
+
durationMs: Date.now() - cmdStart,
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const errorResult: BatchResult = {
|
|
94
|
+
command,
|
|
95
|
+
success: false,
|
|
96
|
+
error: (err as Error).message,
|
|
97
|
+
durationMs: Date.now() - cmdStart,
|
|
98
|
+
};
|
|
99
|
+
results.push(errorResult);
|
|
100
|
+
|
|
101
|
+
if (opts.failFast) {
|
|
102
|
+
const report = createReport(results, startTime, !!rollback);
|
|
103
|
+
if (rollback) {
|
|
104
|
+
await rollback(results).catch(() => {
|
|
105
|
+
// Best-effort rollback
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return report;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return createReport(results, startTime, false);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Create a batch report from results */
|
|
117
|
+
function createReport(
|
|
118
|
+
results: BatchResult[],
|
|
119
|
+
startTime: number,
|
|
120
|
+
rolledBack: boolean,
|
|
121
|
+
): BatchReport {
|
|
122
|
+
const allSuccess = results.every((r) => r.success);
|
|
123
|
+
return {
|
|
124
|
+
success: allSuccess && !rolledBack,
|
|
125
|
+
results,
|
|
126
|
+
totalDurationMs: Date.now() - startTime,
|
|
127
|
+
rolledBack,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Wrap a promise with a timeout */
|
|
132
|
+
function withTimeout<T>(
|
|
133
|
+
promise: Promise<T>,
|
|
134
|
+
timeoutMs: number,
|
|
135
|
+
message: string,
|
|
136
|
+
): Promise<T> {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
reject(new Error(message));
|
|
140
|
+
}, timeoutMs);
|
|
141
|
+
|
|
142
|
+
promise
|
|
143
|
+
.then((value) => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
resolve(value);
|
|
146
|
+
})
|
|
147
|
+
.catch((err) => {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
reject(err);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Format a batch report as markdown */
|
|
155
|
+
export function formatBatchReport(report: BatchReport): string {
|
|
156
|
+
const lines = [
|
|
157
|
+
"## 📦 Batch Execution Report",
|
|
158
|
+
"",
|
|
159
|
+
`**Success:** ${report.success ? "✓ Yes" : "✗ No"}`,
|
|
160
|
+
`**Commands:** ${report.results.length}`,
|
|
161
|
+
`**Duration:** ${report.totalDurationMs}ms`,
|
|
162
|
+
report.rolledBack ? "**Rolled back:** Yes" : "",
|
|
163
|
+
"",
|
|
164
|
+
].filter(Boolean);
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < report.results.length; i++) {
|
|
167
|
+
const r = report.results[i];
|
|
168
|
+
const icon = r.success ? "✓" : "✗";
|
|
169
|
+
lines.push(
|
|
170
|
+
`### ${i + 1}. ${icon} ${r.command.type}:${r.command.name}`,
|
|
171
|
+
`**Duration:** ${r.durationMs}ms`,
|
|
172
|
+
);
|
|
173
|
+
if (r.success) {
|
|
174
|
+
lines.push(`**Result:** \`${JSON.stringify(r.result).slice(0, 200)}\``);
|
|
175
|
+
} else {
|
|
176
|
+
lines.push(`**Error:** ${r.error}`);
|
|
177
|
+
}
|
|
178
|
+
lines.push("");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return lines.join("\n");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Create a simple command batch builder */
|
|
185
|
+
export class BatchBuilder {
|
|
186
|
+
private commands: BatchCommand[] = [];
|
|
187
|
+
private opts: BatchOptions = {};
|
|
188
|
+
private rollbackFn?: RollbackFn;
|
|
189
|
+
|
|
190
|
+
/** Add a command to the batch */
|
|
191
|
+
addCommand(name: string, args?: Record<string, unknown>): this {
|
|
192
|
+
this.commands.push({ type: "command", name, args });
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Add a tool call to the batch */
|
|
197
|
+
addTool(name: string, args?: Record<string, unknown>): this {
|
|
198
|
+
this.commands.push({ type: "tool", name, args });
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Add a search to the batch */
|
|
203
|
+
addSearch(name: string, args?: Record<string, unknown>): this {
|
|
204
|
+
this.commands.push({ type: "search", name, args });
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Set batch options */
|
|
209
|
+
withOptions(options: BatchOptions): this {
|
|
210
|
+
this.opts = { ...this.opts, ...options };
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Set rollback function */
|
|
215
|
+
withRollback(rollback: RollbackFn): this {
|
|
216
|
+
this.rollbackFn = rollback;
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Execute the batch */
|
|
221
|
+
async execute(executor: CommandExecutor): Promise<BatchReport> {
|
|
222
|
+
return executeBatch(this.commands, executor, this.opts, this.rollbackFn);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Get the command list */
|
|
226
|
+
getCommands(): readonly BatchCommand[] {
|
|
227
|
+
return this.commands;
|
|
228
|
+
}
|
|
229
|
+
}
|
package/src/tools/env.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Environment Info Tool
|
|
3
|
+
*
|
|
4
|
+
* Show environment information for debugging.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import type { EnvironmentInfo } from "../types.js";
|
|
11
|
+
|
|
12
|
+
/** Collect environment information */
|
|
13
|
+
export function getEnvironmentInfo(): EnvironmentInfo {
|
|
14
|
+
const unipiModules: string[] = [];
|
|
15
|
+
const configPaths: string[] = [];
|
|
16
|
+
const extensionPaths: string[] = [];
|
|
17
|
+
|
|
18
|
+
// Try to discover unipi modules from node_modules
|
|
19
|
+
try {
|
|
20
|
+
const nodeModules = resolve(process.cwd(), "node_modules");
|
|
21
|
+
if (existsSync(nodeModules)) {
|
|
22
|
+
const scopePath = join(nodeModules, "@pi-unipi");
|
|
23
|
+
if (existsSync(scopePath)) {
|
|
24
|
+
for (const entry of readdirSync(scopePath)) {
|
|
25
|
+
if (entry.startsWith(".")) continue;
|
|
26
|
+
const pkgPath = join(scopePath, entry, "package.json");
|
|
27
|
+
if (existsSync(pkgPath)) {
|
|
28
|
+
unipiModules.push(`@pi-unipi/${entry}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Best effort
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Config paths
|
|
38
|
+
const globalConfig = join(homedir(), ".unipi", "config");
|
|
39
|
+
const projectConfig = resolve(process.cwd(), ".unipi", "config");
|
|
40
|
+
|
|
41
|
+
if (existsSync(globalConfig)) {
|
|
42
|
+
configPaths.push(globalConfig);
|
|
43
|
+
}
|
|
44
|
+
if (existsSync(projectConfig)) {
|
|
45
|
+
configPaths.push(projectConfig);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extension paths (pi-specific)
|
|
49
|
+
try {
|
|
50
|
+
const piDir = join(homedir(), ".pi");
|
|
51
|
+
if (existsSync(piDir)) {
|
|
52
|
+
extensionPaths.push(piDir);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Best effort
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
nodeVersion: process.version,
|
|
60
|
+
piVersion: getPiVersion(),
|
|
61
|
+
os: `${process.platform} ${process.arch}`,
|
|
62
|
+
platform: process.platform,
|
|
63
|
+
unipiModules,
|
|
64
|
+
configPaths,
|
|
65
|
+
extensionPaths,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Try to determine Pi version */
|
|
70
|
+
function getPiVersion(): string {
|
|
71
|
+
try {
|
|
72
|
+
// Try to read from pi's package
|
|
73
|
+
const piPkg = resolve(
|
|
74
|
+
process.cwd(),
|
|
75
|
+
"node_modules",
|
|
76
|
+
"@mariozechner",
|
|
77
|
+
"pi-coding-agent",
|
|
78
|
+
"package.json",
|
|
79
|
+
);
|
|
80
|
+
if (existsSync(piPkg)) {
|
|
81
|
+
const { readFileSync } = require("node:fs");
|
|
82
|
+
const pkg = JSON.parse(readFileSync(piPkg, "utf-8"));
|
|
83
|
+
return pkg.version || "unknown";
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Best effort
|
|
87
|
+
}
|
|
88
|
+
return "unknown";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Format environment info as markdown */
|
|
92
|
+
export function formatEnvironmentInfo(info: EnvironmentInfo): string {
|
|
93
|
+
const lines = [
|
|
94
|
+
"## 🖥️ Environment",
|
|
95
|
+
"",
|
|
96
|
+
`| Key | Value |`,
|
|
97
|
+
`|-----|-------|`,
|
|
98
|
+
`| Node.js | ${info.nodeVersion} |`,
|
|
99
|
+
`| Pi | ${info.piVersion} |`,
|
|
100
|
+
`| OS | ${info.os} |`,
|
|
101
|
+
`| Platform | ${info.platform} |`,
|
|
102
|
+
"",
|
|
103
|
+
"### Unipi Modules",
|
|
104
|
+
"",
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
if (info.unipiModules.length === 0) {
|
|
108
|
+
lines.push("*No @pi-unipi modules detected in node_modules.*");
|
|
109
|
+
} else {
|
|
110
|
+
for (const mod of info.unipiModules) {
|
|
111
|
+
lines.push(`- ${mod}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lines.push("", "### Config Paths", "");
|
|
116
|
+
if (info.configPaths.length === 0) {
|
|
117
|
+
lines.push("*No config paths found.*");
|
|
118
|
+
} else {
|
|
119
|
+
for (const path of info.configPaths) {
|
|
120
|
+
lines.push(`- \`${path}\``);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
lines.push("", "### Extension Paths", "");
|
|
125
|
+
if (info.extensionPaths.length === 0) {
|
|
126
|
+
lines.push("*No extension paths found.*");
|
|
127
|
+
} else {
|
|
128
|
+
for (const path of info.extensionPaths) {
|
|
129
|
+
lines.push(`- \`${path}\``);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|