@tyvm/knowhow 0.0.108-dev.4a8ba55 → 0.0.108-dev.501f36f
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 +1 -1
- package/src/chat/CliChatService.ts +3 -0
- package/src/cli.ts +14 -0
- package/src/clients/index.ts +6 -5
- package/src/cloudWorker.ts +110 -122
- package/src/commands/misc.ts +5 -0
- package/src/commands/services.ts +5 -0
- package/src/commands/workers.ts +9 -1
- package/src/fileSync.ts +50 -17
- package/src/logger.ts +197 -0
- package/src/services/EventService.ts +61 -1
- package/src/services/KnowhowClient.ts +12 -2
- package/src/services/S3.ts +0 -10
- package/src/services/modules/index.ts +17 -6
- package/src/services/modules/types.ts +2 -0
- package/tests/unit/commands/github-credentials.test.ts +211 -0
- package/tests/unit/modules/moduleLoading.test.ts +39 -12
- package/ts_build/package.json +1 -1
- package/ts_build/src/chat/CliChatService.js +3 -0
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/cli.js +7 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/index.js +2 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/cloudWorker.d.ts +5 -0
- package/ts_build/src/cloudWorker.js +69 -66
- package/ts_build/src/cloudWorker.js.map +1 -1
- package/ts_build/src/commands/misc.js +2 -0
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/services.js +2 -1
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/commands/workers.js +6 -1
- package/ts_build/src/commands/workers.js.map +1 -1
- package/ts_build/src/fileSync.d.ts +6 -0
- package/ts_build/src/fileSync.js +37 -12
- package/ts_build/src/fileSync.js.map +1 -1
- package/ts_build/src/logger.d.ts +21 -0
- package/ts_build/src/logger.js +106 -0
- package/ts_build/src/logger.js.map +1 -0
- package/ts_build/src/services/EventService.d.ts +6 -1
- package/ts_build/src/services/EventService.js +29 -0
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/KnowhowClient.js +8 -2
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/S3.js +0 -7
- package/ts_build/src/services/S3.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +1 -1
- package/ts_build/src/services/modules/index.js +10 -5
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +2 -0
- package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
- package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
- package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -7
- package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
package/src/logger.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { LogLevel } from "./services/EventService";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* App-wide logger utility.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* 1. `logger.info/warn/error(source, message)` — routes through EventService
|
|
8
|
+
* 2. `Logger.of("ClassName")` — creates a bound logger so you don't repeat the source
|
|
9
|
+
* 3. `logger.installConsoleOverload()` — replaces console.log/warn/error/info with
|
|
10
|
+
* our closure, so ALL output (including third-party modules) goes through us
|
|
11
|
+
* 4. `logger.silence()` / `logger.unsilence()` — suppress all output, useful for
|
|
12
|
+
* commands that need clean stdout (e.g. github-credentials)
|
|
13
|
+
*
|
|
14
|
+
* Usage (module-level):
|
|
15
|
+
* import { logger } from "../logger";
|
|
16
|
+
* logger.info("MyService", "Something happened");
|
|
17
|
+
*
|
|
18
|
+
* Usage (class-level):
|
|
19
|
+
* import { Logger } from "../logger";
|
|
20
|
+
* class MyClass {
|
|
21
|
+
* private logger = Logger.of("MyClass");
|
|
22
|
+
* doThing() { this.logger.info("Something happened"); }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Silence mode (for clean-stdout commands):
|
|
26
|
+
* logger.silence(); // suppress everything
|
|
27
|
+
* // ... do work that must produce clean stdout ...
|
|
28
|
+
* logger.unsilence(); // restore
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// ---- Internal state ---------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
let silenced = false;
|
|
34
|
+
|
|
35
|
+
// Original console methods — saved before any overload is installed
|
|
36
|
+
const _originalConsole = {
|
|
37
|
+
log: console.log.bind(console),
|
|
38
|
+
warn: console.warn.bind(console),
|
|
39
|
+
error: console.error.bind(console),
|
|
40
|
+
info: console.info.bind(console),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let consoleOverloadInstalled = false;
|
|
44
|
+
|
|
45
|
+
// ---- EventService lazy accessor ---------------------------------------------
|
|
46
|
+
|
|
47
|
+
function getEvents() {
|
|
48
|
+
try {
|
|
49
|
+
const { services } = require("./services") as typeof import("./services");
|
|
50
|
+
return services().Events;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- Core emit logic --------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function emit(source: string, message: string, level: LogLevel): void {
|
|
59
|
+
if (silenced) return;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const events = getEvents();
|
|
63
|
+
if (events) {
|
|
64
|
+
events.log(source, message, level);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// fall through to direct console output
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fallback: use original console methods (bypasses any overload we installed)
|
|
72
|
+
const prefix = source ? `[${source}] ` : "";
|
|
73
|
+
if (level === "warn") _originalConsole.warn(`${prefix}${message}`);
|
|
74
|
+
else if (level === "error") _originalConsole.error(`${prefix}${message}`);
|
|
75
|
+
else _originalConsole.log(`${prefix}${message}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---- Bound logger (returned by Logger.of) -----------------------------------
|
|
79
|
+
|
|
80
|
+
export interface BoundLogger {
|
|
81
|
+
log(message: string, level?: LogLevel): void;
|
|
82
|
+
info(message: string): void;
|
|
83
|
+
warn(message: string): void;
|
|
84
|
+
error(message: string): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeBoundLogger(source: string): BoundLogger {
|
|
88
|
+
return {
|
|
89
|
+
log(message: string, level: LogLevel = "info"): void {
|
|
90
|
+
emit(source, message, level);
|
|
91
|
+
},
|
|
92
|
+
info(message: string): void {
|
|
93
|
+
emit(source, message, "info");
|
|
94
|
+
},
|
|
95
|
+
warn(message: string): void {
|
|
96
|
+
emit(source, message, "warn");
|
|
97
|
+
},
|
|
98
|
+
error(message: string): void {
|
|
99
|
+
emit(source, message, "error");
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- Public API -------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
export const logger = {
|
|
107
|
+
log(source: string, message: string, level: LogLevel = "info"): void {
|
|
108
|
+
emit(source, message, level);
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
info(source: string, message: string): void {
|
|
112
|
+
emit(source, message, "info");
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
warn(source: string, message: string): void {
|
|
116
|
+
emit(source, message, "warn");
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
error(source: string, message: string): void {
|
|
120
|
+
emit(source, message, "error");
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Suppress all log output. Useful for commands that need clean stdout
|
|
125
|
+
* (e.g. git credential helpers). All logger.* calls and overloaded
|
|
126
|
+
* console.* calls become no-ops until unsilence() is called.
|
|
127
|
+
*/
|
|
128
|
+
silence(): void {
|
|
129
|
+
silenced = true;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Restore log output after a silence() call.
|
|
134
|
+
*/
|
|
135
|
+
unsilence(): void {
|
|
136
|
+
silenced = false;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns true if the logger is currently silenced.
|
|
141
|
+
*/
|
|
142
|
+
isSilenced(): boolean {
|
|
143
|
+
return silenced;
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Install console overload. After this call, console.log/warn/error/info
|
|
148
|
+
* all route through our closure (respecting silence mode).
|
|
149
|
+
* Safe to call multiple times — only installs once.
|
|
150
|
+
*
|
|
151
|
+
* Call this early in CLI startup (before any modules are loaded) to ensure
|
|
152
|
+
* third-party module logs don't bypass the silence mechanism.
|
|
153
|
+
*/
|
|
154
|
+
installConsoleOverload(): void {
|
|
155
|
+
if (consoleOverloadInstalled) return;
|
|
156
|
+
consoleOverloadInstalled = true;
|
|
157
|
+
|
|
158
|
+
const route = (originalFn: (...args: any[]) => void, args: any[]) => {
|
|
159
|
+
if (silenced) return;
|
|
160
|
+
originalFn(...args);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
console.log = (...args: any[]) => route(_originalConsole.log, args);
|
|
164
|
+
console.info = (...args: any[]) => route(_originalConsole.info, args);
|
|
165
|
+
console.warn = (...args: any[]) => route(_originalConsole.warn, args);
|
|
166
|
+
// Note: console.error is intentionally NOT overloaded — real errors (stack
|
|
167
|
+
// traces, crash reports) should always be visible. Only suppress via silence().
|
|
168
|
+
// If you want to suppress errors too, call logger.silence() which checks the flag
|
|
169
|
+
// before the overloaded console.warn/log routes reach here anyway.
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Remove the console overload and restore original console methods.
|
|
174
|
+
*/
|
|
175
|
+
uninstallConsoleOverload(): void {
|
|
176
|
+
if (!consoleOverloadInstalled) return;
|
|
177
|
+
console.log = _originalConsole.log;
|
|
178
|
+
console.info = _originalConsole.info;
|
|
179
|
+
console.warn = _originalConsole.warn;
|
|
180
|
+
consoleOverloadInstalled = false;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Factory for creating a bound logger with a fixed source name.
|
|
186
|
+
* Ideal for class-level loggers:
|
|
187
|
+
*
|
|
188
|
+
* class MyClass {
|
|
189
|
+
* private logger = Logger.of("MyClass");
|
|
190
|
+
* doThing() { this.logger.info("hello"); }
|
|
191
|
+
* }
|
|
192
|
+
*/
|
|
193
|
+
export const Logger = {
|
|
194
|
+
of(source: string): BoundLogger {
|
|
195
|
+
return makeBoundLogger(source);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import { IAgent } from "../agents/interface";
|
|
3
3
|
|
|
4
|
+
export type LogLevel = "info" | "warn" | "error";
|
|
5
|
+
|
|
4
6
|
export type EventHandlerFn = (...args: any[]) => any;
|
|
5
7
|
|
|
6
8
|
export interface EventHandler {
|
|
@@ -31,9 +33,38 @@ type ManagedListenerRecord = {
|
|
|
31
33
|
blocking: boolean;
|
|
32
34
|
};
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Default console handler for plugin:log events.
|
|
38
|
+
* Active when no renderer has taken over (e.g. worker mode, CLI before chat starts).
|
|
39
|
+
* Can be suppressed by calling suppressDefaultLogger() when a renderer is active.
|
|
40
|
+
*
|
|
41
|
+
* IMPORTANT: Uses process.stdout/stderr directly to avoid infinite recursion
|
|
42
|
+
* with logger.installConsoleOverload() which overrides console.log/warn.
|
|
43
|
+
*/
|
|
44
|
+
function defaultConsoleLogHandler(event: {
|
|
45
|
+
source: string;
|
|
46
|
+
message: string;
|
|
47
|
+
level: LogLevel;
|
|
48
|
+
}): void {
|
|
49
|
+
const prefix = event.source ? `[${event.source}] ` : "";
|
|
50
|
+
const line = `${prefix}${event.message}\n`;
|
|
51
|
+
switch (event.level) {
|
|
52
|
+
case "warn":
|
|
53
|
+
process.stderr.write(line);
|
|
54
|
+
break;
|
|
55
|
+
case "error":
|
|
56
|
+
process.stderr.write(line);
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
process.stdout.write(line);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
export class EventService extends EventEmitter {
|
|
35
64
|
private blockingHandlers: Map<string, EventHandler[]> = new Map();
|
|
36
65
|
private managedListeners: Map<string, ManagedListenerRecord> = new Map();
|
|
66
|
+
private defaultLoggerActive = true;
|
|
67
|
+
private boundDefaultLogHandler = defaultConsoleLogHandler;
|
|
37
68
|
|
|
38
69
|
eventTypes = {
|
|
39
70
|
agentMsg: "agent:msg",
|
|
@@ -45,6 +76,35 @@ export class EventService extends EventEmitter {
|
|
|
45
76
|
constructor() {
|
|
46
77
|
super();
|
|
47
78
|
this.setMaxListeners(100);
|
|
79
|
+
// Register the default console logger so Events.log() always produces output
|
|
80
|
+
// even before a renderer is attached (worker mode, module loading, etc.)
|
|
81
|
+
this.on(this.eventTypes.pluginLog, this.boundDefaultLogHandler);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Suppress the default console logger.
|
|
86
|
+
* Call this when a renderer has taken over and will handle plugin:log events.
|
|
87
|
+
* This prevents double-printing when both the renderer and the default handler fire.
|
|
88
|
+
*/
|
|
89
|
+
suppressDefaultLogger(): void {
|
|
90
|
+
if (this.defaultLoggerActive) {
|
|
91
|
+
this.removeListener(
|
|
92
|
+
this.eventTypes.pluginLog,
|
|
93
|
+
this.boundDefaultLogHandler
|
|
94
|
+
);
|
|
95
|
+
this.defaultLoggerActive = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Restore the default console logger.
|
|
101
|
+
* Call this when the renderer is torn down.
|
|
102
|
+
*/
|
|
103
|
+
restoreDefaultLogger(): void {
|
|
104
|
+
if (!this.defaultLoggerActive) {
|
|
105
|
+
this.on(this.eventTypes.pluginLog, this.boundDefaultLogHandler);
|
|
106
|
+
this.defaultLoggerActive = true;
|
|
107
|
+
}
|
|
48
108
|
}
|
|
49
109
|
|
|
50
110
|
/**
|
|
@@ -232,7 +292,7 @@ export class EventService extends EventEmitter {
|
|
|
232
292
|
log(
|
|
233
293
|
source: string,
|
|
234
294
|
message: string,
|
|
235
|
-
level:
|
|
295
|
+
level: LogLevel = "info"
|
|
236
296
|
): void {
|
|
237
297
|
this.emit(this.eventTypes.pluginLog, {
|
|
238
298
|
source,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
1
2
|
import http from "../utils/http";
|
|
2
3
|
import fs from "fs";
|
|
3
4
|
import { Message } from "../clients/types";
|
|
@@ -663,8 +664,10 @@ export class KnowhowSimpleClient {
|
|
|
663
664
|
/**
|
|
664
665
|
* Get presigned S3 URL for uploading a file to Knowhow FS.
|
|
665
666
|
* First finds or creates the file by path, then gets its upload URL.
|
|
667
|
+
* Computes SHA256 hash of the file content and stores it as S3 metadata
|
|
668
|
+
* so any client can determine if they already have this version without downloading.
|
|
666
669
|
*/
|
|
667
|
-
async getOrgFilePresignedUploadUrl(filePath: string): Promise<string> {
|
|
670
|
+
async getOrgFilePresignedUploadUrl(filePath: string, localFilePath?: string): Promise<string> {
|
|
668
671
|
await this.checkJwt();
|
|
669
672
|
|
|
670
673
|
// Find or create the file by path
|
|
@@ -675,10 +678,17 @@ export class KnowhowSimpleClient {
|
|
|
675
678
|
const fileName =
|
|
676
679
|
lastSlash >= 0 ? filePath.substring(lastSlash + 1) : filePath;
|
|
677
680
|
|
|
681
|
+
// Compute SHA256 hash if we have the local file path, so S3 stores it as metadata
|
|
682
|
+
let sha256Hash: string | undefined;
|
|
683
|
+
if (localFilePath) {
|
|
684
|
+
const fileContent = fs.readFileSync(localFilePath);
|
|
685
|
+
sha256Hash = createHash("sha256").update(fileContent).digest("base64");
|
|
686
|
+
}
|
|
687
|
+
|
|
678
688
|
// Get upload URL using the file ID
|
|
679
689
|
const response = await http.post<{ uploadUrl: string }>(
|
|
680
690
|
`${this.baseUrl}/api/org-files/upload/${file.id}`,
|
|
681
|
-
{ fileName },
|
|
691
|
+
{ fileName, sha256Hash },
|
|
682
692
|
{ headers: this.headers }
|
|
683
693
|
);
|
|
684
694
|
return response.data.uploadUrl;
|
package/src/services/S3.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { createWriteStream, createReadStream } from "fs";
|
|
3
|
-
import * as crypto from "crypto";
|
|
4
3
|
import { pipeline, Readable } from "stream";
|
|
5
4
|
import * as util from "util";
|
|
6
5
|
|
|
@@ -15,19 +14,10 @@ export class S3Service {
|
|
|
15
14
|
const fileContent = fs.readFileSync(filePath);
|
|
16
15
|
const fileStats = await fs.promises.stat(filePath);
|
|
17
16
|
|
|
18
|
-
// Compute SHA-256 checksum (base64) — required when presigned URL was
|
|
19
|
-
// generated with ChecksumAlgorithm: SHA256
|
|
20
|
-
const sha256Base64 = crypto
|
|
21
|
-
.createHash("sha256")
|
|
22
|
-
.update(fileContent)
|
|
23
|
-
.digest("base64");
|
|
24
|
-
|
|
25
17
|
const response = await fetch(presignedUrl, {
|
|
26
18
|
method: "PUT",
|
|
27
19
|
headers: {
|
|
28
20
|
"Content-Length": String(fileStats.size),
|
|
29
|
-
"x-amz-checksum-sha256": sha256Base64,
|
|
30
|
-
"x-amz-sdk-checksum-algorithm": "SHA256",
|
|
31
21
|
},
|
|
32
22
|
body: fileContent,
|
|
33
23
|
// @ts-ignore
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
|
|
1
3
|
import { getConfig, getGlobalConfig } from "../../config";
|
|
2
4
|
import { KnowhowModule, ModuleContext } from "./types";
|
|
3
5
|
import { services } from "../";
|
|
4
|
-
import
|
|
6
|
+
import { toUniqueArray } from "../../utils";
|
|
5
7
|
|
|
6
8
|
export class ModulesService {
|
|
7
9
|
async getDefaultContext() {
|
|
@@ -33,11 +35,17 @@ export class ModulesService {
|
|
|
33
35
|
: modulePath;
|
|
34
36
|
const rawModule = require(resolvedPath);
|
|
35
37
|
const importedModule = (rawModule.default || rawModule) as KnowhowModule;
|
|
36
|
-
|
|
38
|
+
context.Events?.log(
|
|
39
|
+
"ModulesService",
|
|
37
40
|
`🔌 Loading module: ${modulePath} (resolved: ${resolvedPath})`
|
|
38
41
|
);
|
|
39
|
-
await importedModule.init({
|
|
40
|
-
|
|
42
|
+
await importedModule.init({
|
|
43
|
+
config,
|
|
44
|
+
cwd: process.cwd(),
|
|
45
|
+
context: context as ModuleContext,
|
|
46
|
+
});
|
|
47
|
+
context.Events?.log(
|
|
48
|
+
"ModulesService",
|
|
41
49
|
`✅ Module initialized: ${modulePath} (tools: ${importedModule.tools.length}, agents: ${importedModule.agents.length}, plugins: ${importedModule.plugins.length}, clients: ${importedModule.clients.length})`
|
|
42
50
|
);
|
|
43
51
|
|
|
@@ -52,7 +60,10 @@ export class ModulesService {
|
|
|
52
60
|
if (context.Tools) {
|
|
53
61
|
for (const tool of importedModule.tools) {
|
|
54
62
|
context.Tools.addTool(tool.definition);
|
|
55
|
-
context.Tools.setFunction(
|
|
63
|
+
context.Tools.setFunction(
|
|
64
|
+
tool.definition.function.name,
|
|
65
|
+
tool.handler
|
|
66
|
+
);
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
69
|
|
|
@@ -84,7 +95,7 @@ export class ModulesService {
|
|
|
84
95
|
];
|
|
85
96
|
|
|
86
97
|
return this.loadModulesFrom(
|
|
87
|
-
{ ...config, modules: allModulePaths },
|
|
98
|
+
{ ...config, modules: toUniqueArray(allModulePaths) },
|
|
88
99
|
context
|
|
89
100
|
);
|
|
90
101
|
}
|
|
@@ -11,6 +11,7 @@ import { AIClient } from "../../clients";
|
|
|
11
11
|
import { ToolsService } from "../Tools";
|
|
12
12
|
import { MediaProcessorService } from "../MediaProcessorService";
|
|
13
13
|
import { TunnelHandler } from "@tyvm/knowhow-tunnel";
|
|
14
|
+
import { EventService } from "../EventService";
|
|
14
15
|
|
|
15
16
|
/*
|
|
16
17
|
*
|
|
@@ -54,6 +55,7 @@ export interface ModuleContext {
|
|
|
54
55
|
Plugins: PluginService;
|
|
55
56
|
Clients: AIClient;
|
|
56
57
|
Tools: ToolsService;
|
|
58
|
+
Events: EventService;
|
|
57
59
|
MediaProcessor?: MediaProcessorService;
|
|
58
60
|
Tunnel?: TunnelHandler;
|
|
59
61
|
Program?: Command;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the github-credentials command.
|
|
3
|
+
*
|
|
4
|
+
* Key invariant: running `github-credentials` must NEVER write anything other than
|
|
5
|
+
* the credential lines to stdout. Module loading logs, warnings, etc. must be
|
|
6
|
+
* silenced so the git credential helper protocol is not corrupted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Mock config before any imports that depend on it
|
|
10
|
+
jest.mock("../../../src/config", () => ({
|
|
11
|
+
getConfig: jest.fn().mockResolvedValue({ modules: [] }),
|
|
12
|
+
getGlobalConfig: jest.fn().mockResolvedValue({ modules: [] }),
|
|
13
|
+
getConfigSync: jest.fn().mockReturnValue({}),
|
|
14
|
+
migrateConfig: jest.fn().mockResolvedValue(undefined),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock clients to avoid openai.ts side-effects
|
|
18
|
+
jest.mock("../../../src/clients", () => ({
|
|
19
|
+
AIClient: jest.fn(),
|
|
20
|
+
Clients: { registerClient: jest.fn(), registerModels: jest.fn() },
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock KnowhowSimpleClient so we control what getGitCredential returns
|
|
24
|
+
// without needing a real JWT or network connection
|
|
25
|
+
jest.mock("../../../src/services/KnowhowClient", () => ({
|
|
26
|
+
KnowhowSimpleClient: jest.fn().mockImplementation(() => ({
|
|
27
|
+
getGitCredential: jest.fn().mockResolvedValue({
|
|
28
|
+
protocol: "https",
|
|
29
|
+
host: "github.com",
|
|
30
|
+
username: "x-access-token",
|
|
31
|
+
password: "ghu_TESTTOKEN123",
|
|
32
|
+
}),
|
|
33
|
+
})),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Mock readline so the 'get' action doesn't hang waiting for stdin
|
|
37
|
+
jest.mock("readline", () => ({
|
|
38
|
+
createInterface: jest.fn().mockReturnValue({
|
|
39
|
+
on: jest.fn().mockImplementation(function (event: string, cb: Function) {
|
|
40
|
+
// Immediately fire 'close' so the readline promise resolves
|
|
41
|
+
if (event === "close") {
|
|
42
|
+
setImmediate(() => cb());
|
|
43
|
+
}
|
|
44
|
+
return this;
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
import { Command } from "commander";
|
|
50
|
+
import { addGithubCredentialsCommand } from "../../../src/commands/misc";
|
|
51
|
+
import { logger } from "../../../src/logger";
|
|
52
|
+
|
|
53
|
+
describe("github-credentials command", () => {
|
|
54
|
+
/**
|
|
55
|
+
* This test verifies the EARLY silencing logic in cli.ts main().
|
|
56
|
+
* The problem: modules load BEFORE parseAsync, so any module that emits
|
|
57
|
+
* warnings (e.g. Terminal module: no TunnelHandler) does so before the
|
|
58
|
+
* action's logger.silence() call can stop it.
|
|
59
|
+
*
|
|
60
|
+
* The fix: cli.ts checks process.argv before module loading and silences early.
|
|
61
|
+
* This test simulates that logic directly.
|
|
62
|
+
*/
|
|
63
|
+
describe("early silencing (pre-module-load)", () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
logger.unsilence();
|
|
66
|
+
logger.installConsoleOverload();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
logger.unsilence();
|
|
71
|
+
logger.uninstallConsoleOverload();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("silences before module loading when github-credentials is in argv", () => {
|
|
75
|
+
const originalArgv = process.argv;
|
|
76
|
+
process.argv = ["node", "knowhow", "github-credentials", "get"];
|
|
77
|
+
|
|
78
|
+
// Simulate the exact early-detection logic from cli.ts main()
|
|
79
|
+
const rawArgs = process.argv.slice(2);
|
|
80
|
+
const SILENT_COMMANDS = ["github-credentials"];
|
|
81
|
+
if (rawArgs.some((a) => SILENT_COMMANDS.includes(a))) {
|
|
82
|
+
logger.silence();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Now any module-load-time console.log/warn should be suppressed
|
|
86
|
+
const consoleSpy = jest.spyOn(process.stdout, "write");
|
|
87
|
+
console.warn("⚠️ Terminal module: no TunnelHandler in context — terminal addon not registered");
|
|
88
|
+
console.log("some other module loading noise");
|
|
89
|
+
|
|
90
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
91
|
+
consoleSpy.mockRestore();
|
|
92
|
+
process.argv = originalArgv;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("does NOT silence for other commands", () => {
|
|
96
|
+
const originalArgv = process.argv;
|
|
97
|
+
process.argv = ["node", "knowhow", "chat"];
|
|
98
|
+
|
|
99
|
+
const rawArgs = process.argv.slice(2);
|
|
100
|
+
const SILENT_COMMANDS = ["github-credentials"];
|
|
101
|
+
if (rawArgs.some((a) => SILENT_COMMANDS.includes(a))) {
|
|
102
|
+
logger.silence();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
expect(logger.isSilenced()).toBe(false);
|
|
106
|
+
process.argv = originalArgv;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let program: Command;
|
|
111
|
+
let stdoutSpy: jest.SpyInstance;
|
|
112
|
+
let writtenToStdout: string[];
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
jest.clearAllMocks();
|
|
116
|
+
|
|
117
|
+
// Reset logger silence state between tests
|
|
118
|
+
logger.unsilence();
|
|
119
|
+
|
|
120
|
+
// Capture process.stdout.write — this is what the credential helper uses
|
|
121
|
+
writtenToStdout = [];
|
|
122
|
+
stdoutSpy = jest
|
|
123
|
+
.spyOn(process.stdout, "write")
|
|
124
|
+
.mockImplementation((chunk: any) => {
|
|
125
|
+
writtenToStdout.push(typeof chunk === "string" ? chunk : chunk.toString());
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
program = new Command();
|
|
130
|
+
program.exitOverride(); // prevent process.exit during tests
|
|
131
|
+
addGithubCredentialsCommand(program);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
stdoutSpy.mockRestore();
|
|
136
|
+
logger.unsilence();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("outputs only credential lines to stdout for 'get' action", async () => {
|
|
140
|
+
await program.parseAsync([
|
|
141
|
+
"node", "knowhow", "github-credentials", "get", "--repo", "myorg/myrepo",
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
expect(writtenToStdout).toHaveLength(1);
|
|
145
|
+
expect(writtenToStdout[0]).toBe(
|
|
146
|
+
"protocol=https\nhost=github.com\nusername=x-access-token\npassword=ghu_TESTTOKEN123\n"
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("silences the logger immediately so module logs don't pollute stdout", async () => {
|
|
151
|
+
await program.parseAsync([
|
|
152
|
+
"node", "knowhow", "github-credentials", "get", "--repo", "myorg/myrepo",
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// The action must have called logger.silence() — state persists after action
|
|
156
|
+
expect(logger.isSilenced()).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("produces exactly 4 credential field lines and nothing else", async () => {
|
|
160
|
+
await program.parseAsync([
|
|
161
|
+
"node", "knowhow", "github-credentials", "get", "--repo", "myorg/myrepo",
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
const allOutput = writtenToStdout.join("");
|
|
165
|
+
const lines = allOutput.trim().split("\n");
|
|
166
|
+
|
|
167
|
+
expect(lines).toHaveLength(4);
|
|
168
|
+
expect(lines[0]).toMatch(/^protocol=/);
|
|
169
|
+
expect(lines[1]).toMatch(/^host=/);
|
|
170
|
+
expect(lines[2]).toMatch(/^username=/);
|
|
171
|
+
expect(lines[3]).toMatch(/^password=/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("exits cleanly for 'store' action without writing credentials", async () => {
|
|
175
|
+
let exitCode: number | undefined;
|
|
176
|
+
// Throw to stop execution after exit() is called — otherwise the mock
|
|
177
|
+
// just sets a flag and the action continues to fetch credentials.
|
|
178
|
+
const exitSpy = jest
|
|
179
|
+
.spyOn(process, "exit")
|
|
180
|
+
.mockImplementation(((code?: number) => {
|
|
181
|
+
exitCode = code ?? 0;
|
|
182
|
+
throw new Error(`process.exit(${exitCode})`);
|
|
183
|
+
}) as any);
|
|
184
|
+
|
|
185
|
+
await expect(
|
|
186
|
+
program.parseAsync(["node", "knowhow", "github-credentials", "store"])
|
|
187
|
+
).rejects.toThrow("process.exit(0)");
|
|
188
|
+
|
|
189
|
+
expect(exitCode).toBe(0);
|
|
190
|
+
expect(writtenToStdout).toHaveLength(0);
|
|
191
|
+
exitSpy.mockRestore();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("exits cleanly for 'erase' action without writing credentials", async () => {
|
|
195
|
+
let exitCode: number | undefined;
|
|
196
|
+
const exitSpy = jest
|
|
197
|
+
.spyOn(process, "exit")
|
|
198
|
+
.mockImplementation(((code?: number) => {
|
|
199
|
+
exitCode = code ?? 0;
|
|
200
|
+
throw new Error(`process.exit(${exitCode})`);
|
|
201
|
+
}) as any);
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
program.parseAsync(["node", "knowhow", "github-credentials", "erase"])
|
|
205
|
+
).rejects.toThrow("process.exit(0)");
|
|
206
|
+
|
|
207
|
+
expect(exitCode).toBe(0);
|
|
208
|
+
expect(writtenToStdout).toHaveLength(0);
|
|
209
|
+
exitSpy.mockRestore();
|
|
210
|
+
});
|
|
211
|
+
});
|