claude-yes 1.31.1 → 1.32.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 +225 -21
- package/dist/agent-yes.js +2 -0
- package/dist/amp-yes.js +2 -0
- package/dist/auggie-yes.js +2 -0
- package/dist/claude-yes.js +2 -20432
- package/dist/cli.js +18341 -10955
- package/dist/cli.js.map +141 -150
- package/dist/codex-yes.js +2 -20432
- package/dist/copilot-yes.js +2 -20432
- package/dist/cursor-yes.js +2 -20432
- package/dist/gemini-yes.js +2 -20432
- package/dist/grok-yes.js +2 -20432
- package/dist/index.js +16258 -13586
- package/dist/index.js.map +176 -191
- package/dist/qwen-yes.js +2 -20432
- package/package.json +95 -84
- package/ts/ReadyManager.spec.ts +10 -10
- package/ts/ReadyManager.ts +1 -1
- package/ts/SUPPORTED_CLIS.ts +4 -0
- package/ts/catcher.spec.ts +69 -70
- package/ts/cli-idle.spec.ts +8 -8
- package/ts/cli.ts +18 -26
- package/ts/defineConfig.ts +4 -4
- package/ts/idleWaiter.spec.ts +9 -9
- package/ts/index.ts +474 -233
- package/ts/logger.ts +22 -0
- package/ts/parseCliArgs.spec.ts +146 -147
- package/ts/parseCliArgs.ts +127 -59
- package/ts/postbuild.ts +29 -15
- package/ts/pty-fix.ts +155 -0
- package/ts/pty.ts +19 -0
- package/ts/removeControlCharacters.spec.ts +37 -38
- package/ts/removeControlCharacters.ts +2 -1
- package/ts/runningLock.spec.ts +119 -125
- package/ts/runningLock.ts +44 -55
- package/ts/session-integration.spec.ts +34 -42
- package/ts/utils.spec.ts +35 -35
- package/ts/utils.ts +7 -7
- package/ts/codex-resume.spec.ts +0 -239
- package/ts/codexSessionManager.spec.ts +0 -51
- package/ts/codexSessionManager.test.ts +0 -259
- package/ts/codexSessionManager.ts +0 -312
- package/ts/yesLog.spec.ts +0 -74
- package/ts/yesLog.ts +0 -27
package/ts/runningLock.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { execSync } from
|
|
2
|
-
import { existsSync } from
|
|
3
|
-
import { mkdir, readFile, rename,
|
|
4
|
-
import { homedir } from
|
|
5
|
-
import path from
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import path from "path";
|
|
6
6
|
|
|
7
7
|
export interface Task {
|
|
8
8
|
cwd: string;
|
|
9
9
|
gitRoot?: string;
|
|
10
10
|
task: string;
|
|
11
11
|
pid: number;
|
|
12
|
-
status:
|
|
12
|
+
status: "running" | "queued" | "completed" | "failed";
|
|
13
13
|
startedAt: number;
|
|
14
14
|
lockedAt: number;
|
|
15
15
|
}
|
|
@@ -24,8 +24,8 @@ interface LockCheckResult {
|
|
|
24
24
|
lockKey: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
const
|
|
27
|
+
const getLockDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
|
|
28
|
+
const getLockFile = () => path.join(getLockDir(), "running.lock.json");
|
|
29
29
|
const MAX_RETRIES = 5;
|
|
30
30
|
const RETRY_DELAYS = [50, 100, 200, 400, 800]; // exponential backoff in ms
|
|
31
31
|
const POLL_INTERVAL = 2000; // 2 seconds
|
|
@@ -48,10 +48,10 @@ function isProcessRunning(pid: number): boolean {
|
|
|
48
48
|
*/
|
|
49
49
|
function getGitRoot(cwd: string): string | null {
|
|
50
50
|
try {
|
|
51
|
-
const result = execSync(
|
|
51
|
+
const result = execSync("git rev-parse --show-toplevel", {
|
|
52
52
|
cwd,
|
|
53
|
-
encoding:
|
|
54
|
-
stdio: [
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
55
55
|
});
|
|
56
56
|
return result.trim();
|
|
57
57
|
} catch {
|
|
@@ -94,13 +94,15 @@ function sleep(ms: number): Promise<void> {
|
|
|
94
94
|
*/
|
|
95
95
|
async function readLockFile(): Promise<LockFile> {
|
|
96
96
|
try {
|
|
97
|
-
|
|
97
|
+
const lockDir = getLockDir();
|
|
98
|
+
const lockFilePath = getLockFile();
|
|
99
|
+
await mkdir(lockDir, { recursive: true });
|
|
98
100
|
|
|
99
|
-
if (!existsSync(
|
|
101
|
+
if (!existsSync(lockFilePath)) {
|
|
100
102
|
return { tasks: [] };
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
const content = await readFile(
|
|
105
|
+
const content = await readFile(lockFilePath, "utf8");
|
|
104
106
|
const lockFile = JSON.parse(content) as LockFile;
|
|
105
107
|
|
|
106
108
|
// Clean stale locks while reading
|
|
@@ -110,7 +112,7 @@ async function readLockFile(): Promise<LockFile> {
|
|
|
110
112
|
});
|
|
111
113
|
|
|
112
114
|
return lockFile;
|
|
113
|
-
} catch
|
|
115
|
+
} catch {
|
|
114
116
|
// If file is corrupted or doesn't exist, return empty lock file
|
|
115
117
|
return { tasks: [] };
|
|
116
118
|
}
|
|
@@ -119,18 +121,17 @@ async function readLockFile(): Promise<LockFile> {
|
|
|
119
121
|
/**
|
|
120
122
|
* Write lock file atomically with retry logic
|
|
121
123
|
*/
|
|
122
|
-
async function writeLockFile(
|
|
123
|
-
lockFile: LockFile,
|
|
124
|
-
retryCount = 0,
|
|
125
|
-
): Promise<void> {
|
|
124
|
+
async function writeLockFile(lockFile: LockFile, retryCount = 0): Promise<void> {
|
|
126
125
|
try {
|
|
127
|
-
|
|
126
|
+
const lockDir = getLockDir();
|
|
127
|
+
const lockFilePath = getLockFile();
|
|
128
|
+
await mkdir(lockDir, { recursive: true });
|
|
128
129
|
|
|
129
|
-
const tempFile = `${
|
|
130
|
-
await writeFile(tempFile, JSON.stringify(lockFile, null, 2),
|
|
130
|
+
const tempFile = `${lockFilePath}.tmp.${process.pid}`;
|
|
131
|
+
await writeFile(tempFile, JSON.stringify(lockFile, null, 2), "utf8");
|
|
131
132
|
|
|
132
133
|
// Atomic rename
|
|
133
|
-
await rename(tempFile,
|
|
134
|
+
await rename(tempFile, lockFilePath);
|
|
134
135
|
} catch (error) {
|
|
135
136
|
if (retryCount < MAX_RETRIES) {
|
|
136
137
|
// Exponential backoff retry
|
|
@@ -144,10 +145,7 @@ async function writeLockFile(
|
|
|
144
145
|
/**
|
|
145
146
|
* Check if lock exists for the current working directory
|
|
146
147
|
*/
|
|
147
|
-
async function checkLock(
|
|
148
|
-
cwd: string,
|
|
149
|
-
prompt: string,
|
|
150
|
-
): Promise<LockCheckResult> {
|
|
148
|
+
async function checkLock(cwd: string, _prompt: string): Promise<LockCheckResult> {
|
|
151
149
|
const resolvedCwd = resolveRealPath(cwd);
|
|
152
150
|
const gitRoot = isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null;
|
|
153
151
|
const lockKey = gitRoot || resolvedCwd;
|
|
@@ -157,7 +155,7 @@ async function checkLock(
|
|
|
157
155
|
// Find running tasks for this location
|
|
158
156
|
const blockingTasks = lockFile.tasks.filter((task) => {
|
|
159
157
|
if (!isProcessRunning(task.pid)) return false; // Skip stale locks
|
|
160
|
-
if (task.status !==
|
|
158
|
+
if (task.status !== "running") return false; // Only check running tasks
|
|
161
159
|
|
|
162
160
|
if (gitRoot && task.gitRoot) {
|
|
163
161
|
// In git repo: check by git root
|
|
@@ -191,10 +189,7 @@ async function addTask(task: Task): Promise<void> {
|
|
|
191
189
|
/**
|
|
192
190
|
* Update task status
|
|
193
191
|
*/
|
|
194
|
-
async function updateTaskStatus(
|
|
195
|
-
pid: number,
|
|
196
|
-
status: Task['status'],
|
|
197
|
-
): Promise<void> {
|
|
192
|
+
async function updateTaskStatus(pid: number, status: Task["status"]): Promise<void> {
|
|
198
193
|
const lockFile = await readLockFile();
|
|
199
194
|
const task = lockFile.tasks.find((t) => t.pid === pid);
|
|
200
195
|
|
|
@@ -216,17 +211,14 @@ async function removeTask(pid: number): Promise<void> {
|
|
|
216
211
|
/**
|
|
217
212
|
* Wait for lock to be released
|
|
218
213
|
*/
|
|
219
|
-
async function waitForUnlock(
|
|
220
|
-
blockingTasks: Task[],
|
|
221
|
-
currentTask: Task,
|
|
222
|
-
): Promise<void> {
|
|
214
|
+
async function waitForUnlock(blockingTasks: Task[], currentTask: Task): Promise<void> {
|
|
223
215
|
const blockingTask = blockingTasks[0];
|
|
224
216
|
if (!blockingTask) return;
|
|
225
217
|
console.log(`⏳ Queueing for unlock of: ${blockingTask.task}`);
|
|
226
218
|
console.log(` Press 'b' to bypass queue, 'k' to kill previous instance`);
|
|
227
219
|
|
|
228
220
|
// Add current task as 'queued'
|
|
229
|
-
await addTask({ ...currentTask, status:
|
|
221
|
+
await addTask({ ...currentTask, status: "queued" });
|
|
230
222
|
|
|
231
223
|
// Set up keyboard input handling
|
|
232
224
|
const stdin = process.stdin;
|
|
@@ -239,30 +231,30 @@ async function waitForUnlock(
|
|
|
239
231
|
|
|
240
232
|
const keyHandler = (key: Buffer) => {
|
|
241
233
|
const char = key.toString();
|
|
242
|
-
if (char ===
|
|
243
|
-
console.log(
|
|
234
|
+
if (char === "b" || char === "B") {
|
|
235
|
+
console.log("\n⚡ Bypassing queue...");
|
|
244
236
|
bypassed = true;
|
|
245
|
-
} else if (char ===
|
|
246
|
-
console.log(
|
|
237
|
+
} else if (char === "k" || char === "K") {
|
|
238
|
+
console.log("\n🔪 Killing previous instance...");
|
|
247
239
|
killed = true;
|
|
248
240
|
}
|
|
249
241
|
};
|
|
250
242
|
|
|
251
|
-
stdin.on(
|
|
243
|
+
stdin.on("data", keyHandler);
|
|
252
244
|
|
|
253
245
|
let dots = 0;
|
|
254
246
|
while (true) {
|
|
255
247
|
if (bypassed) {
|
|
256
248
|
// Force bypass - update status to running immediately
|
|
257
|
-
await updateTaskStatus(currentTask.pid,
|
|
258
|
-
console.log(
|
|
249
|
+
await updateTaskStatus(currentTask.pid, "running");
|
|
250
|
+
console.log("✓ Queue bypassed, starting task...");
|
|
259
251
|
break;
|
|
260
252
|
}
|
|
261
253
|
|
|
262
254
|
if (killed && blockingTask) {
|
|
263
255
|
// Kill the blocking task's process
|
|
264
256
|
try {
|
|
265
|
-
process.kill(blockingTask.pid,
|
|
257
|
+
process.kill(blockingTask.pid, "SIGTERM");
|
|
266
258
|
console.log(`✓ Killed process ${blockingTask.pid}`);
|
|
267
259
|
// Wait a bit for the process to be killed
|
|
268
260
|
await sleep(1000);
|
|
@@ -278,20 +270,18 @@ async function waitForUnlock(
|
|
|
278
270
|
|
|
279
271
|
if (!lockCheck.isLocked) {
|
|
280
272
|
// Lock released, update status to running
|
|
281
|
-
await updateTaskStatus(currentTask.pid,
|
|
273
|
+
await updateTaskStatus(currentTask.pid, "running");
|
|
282
274
|
console.log(`\n✓ Lock released, starting task...`);
|
|
283
275
|
break;
|
|
284
276
|
}
|
|
285
277
|
|
|
286
278
|
// Show progress indicator
|
|
287
279
|
dots = (dots + 1) % 4;
|
|
288
|
-
process.stdout.write(
|
|
289
|
-
`\r⏳ Queueing${'.'.repeat(dots)}${' '.repeat(3 - dots)}`,
|
|
290
|
-
);
|
|
280
|
+
process.stdout.write(`\r⏳ Queueing${".".repeat(dots)}${" ".repeat(3 - dots)}`);
|
|
291
281
|
}
|
|
292
282
|
|
|
293
283
|
// Clean up keyboard handler
|
|
294
|
-
stdin.off(
|
|
284
|
+
stdin.off("data", keyHandler);
|
|
295
285
|
stdin.setRawMode?.(wasRaw);
|
|
296
286
|
if (!wasRaw) stdin.pause();
|
|
297
287
|
}
|
|
@@ -320,7 +310,7 @@ export async function cleanStaleLocks(): Promise<void> {
|
|
|
320
310
|
*/
|
|
321
311
|
export async function acquireLock(
|
|
322
312
|
cwd: string,
|
|
323
|
-
prompt: string =
|
|
313
|
+
prompt: string = "no prompt provided",
|
|
324
314
|
): Promise<void> {
|
|
325
315
|
const resolvedCwd = resolveRealPath(cwd);
|
|
326
316
|
const gitRoot = isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null;
|
|
@@ -330,7 +320,7 @@ export async function acquireLock(
|
|
|
330
320
|
gitRoot: gitRoot || undefined,
|
|
331
321
|
task: prompt.substring(0, 100), // Limit task description length
|
|
332
322
|
pid: process.pid,
|
|
333
|
-
status:
|
|
323
|
+
status: "running",
|
|
334
324
|
startedAt: Date.now(),
|
|
335
325
|
lockedAt: Date.now(),
|
|
336
326
|
};
|
|
@@ -355,7 +345,7 @@ export async function releaseLock(pid: number = process.pid): Promise<void> {
|
|
|
355
345
|
* Update status of current task
|
|
356
346
|
*/
|
|
357
347
|
export async function updateCurrentTaskStatus(
|
|
358
|
-
status: Task[
|
|
348
|
+
status: Task["status"],
|
|
359
349
|
pid: number = process.pid,
|
|
360
350
|
): Promise<void> {
|
|
361
351
|
await updateTaskStatus(pid, status);
|
|
@@ -365,8 +355,7 @@ export async function updateCurrentTaskStatus(
|
|
|
365
355
|
* Check if we should use locking for this directory
|
|
366
356
|
* Only use locking if we're in a git repository
|
|
367
357
|
*/
|
|
368
|
-
export function shouldUseLock(
|
|
369
|
-
const resolvedCwd = resolveRealPath(cwd);
|
|
358
|
+
export function shouldUseLock(_cwd: string): boolean {
|
|
370
359
|
// Only use lock if in git repo OR if explicitly requested
|
|
371
360
|
// For now, use lock for all cases to handle same-dir conflicts
|
|
372
361
|
return true;
|
|
@@ -1,41 +1,37 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import {
|
|
3
|
-
extractSessionId,
|
|
4
|
-
extractSessionIdFromSessionMeta,
|
|
5
|
-
} from './codexSessionManager';
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { extractSessionId, extractSessionIdFromSessionMeta } from "./resume/codexSessionManager";
|
|
6
3
|
|
|
7
|
-
describe(
|
|
8
|
-
it(
|
|
9
|
-
console.log(
|
|
4
|
+
describe("Session Extraction Test", () => {
|
|
5
|
+
it("should extract session IDs from various codex output formats", async () => {
|
|
6
|
+
console.log("\n=== Session ID Extraction Test ===");
|
|
10
7
|
|
|
11
8
|
// Test different formats where session IDs might appear
|
|
12
9
|
const testCases = [
|
|
13
10
|
{
|
|
14
|
-
name:
|
|
15
|
-
output:
|
|
16
|
-
expected:
|
|
11
|
+
name: "Direct UUID in output",
|
|
12
|
+
output: "Session started with ID: 0199e659-0e5f-7843-8876-5a65c64e77c0",
|
|
13
|
+
expected: "0199e659-0e5f-7843-8876-5a65c64e77c0",
|
|
17
14
|
},
|
|
18
15
|
{
|
|
19
|
-
name:
|
|
20
|
-
output:
|
|
21
|
-
|
|
22
|
-
expected: '0199e659-0e5f-7843-8876-5a65c64e77c0',
|
|
16
|
+
name: "UUID in brackets",
|
|
17
|
+
output: "Using session [0199e659-0e5f-7843-8876-5a65c64e77c0] for this conversation",
|
|
18
|
+
expected: "0199e659-0e5f-7843-8876-5a65c64e77c0",
|
|
23
19
|
},
|
|
24
20
|
{
|
|
25
|
-
name:
|
|
26
|
-
output:
|
|
27
|
-
expected:
|
|
21
|
+
name: "Mixed case UUID",
|
|
22
|
+
output: "SESSION_ID: 0199E659-0E5F-7843-8876-5A65C64E77C0",
|
|
23
|
+
expected: "0199E659-0E5F-7843-8876-5A65C64E77C0",
|
|
28
24
|
},
|
|
29
25
|
{
|
|
30
|
-
name:
|
|
31
|
-
output:
|
|
26
|
+
name: "No UUID present",
|
|
27
|
+
output: "Welcome to codex! Type your message and press enter.",
|
|
32
28
|
expected: null,
|
|
33
29
|
},
|
|
34
30
|
{
|
|
35
|
-
name:
|
|
31
|
+
name: "Multiple UUIDs (should get first)",
|
|
36
32
|
output:
|
|
37
|
-
|
|
38
|
-
expected:
|
|
33
|
+
"Old: 1111e659-0e5f-7843-8876-5a65c64e77c0 New: 2222e659-0e5f-7843-8876-5a65c64e77c0",
|
|
34
|
+
expected: "1111e659-0e5f-7843-8876-5a65c64e77c0",
|
|
39
35
|
},
|
|
40
36
|
];
|
|
41
37
|
|
|
@@ -48,54 +44,50 @@ describe('Session Extraction Test', () => {
|
|
|
48
44
|
expect(result).toBe(testCase.expected);
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
console.log(
|
|
47
|
+
console.log("✅ All session extraction tests passed!\n");
|
|
52
48
|
});
|
|
53
49
|
|
|
54
|
-
it(
|
|
55
|
-
console.log(
|
|
50
|
+
it("should extract session ID from session metadata JSON", async () => {
|
|
51
|
+
console.log("\n=== Session Metadata Extraction Test ===");
|
|
56
52
|
|
|
57
53
|
const sessionMetaJson = `{"timestamp":"2025-10-15T05:30:20.265Z","type":"session_meta","payload":{"id":"0199e659-0e5f-7843-8876-5a65c64e77c0","timestamp":"2025-10-15T05:30:20.127Z","cwd":"/v1/code/project","originator":"codex_cli_rs"}}
|
|
58
54
|
{"timestamp":"2025-10-15T05:30:20.415Z","type":"response_item","payload":{"type":"message","role":"user"}}`;
|
|
59
55
|
|
|
60
56
|
const sessionId = extractSessionIdFromSessionMeta(sessionMetaJson);
|
|
61
57
|
console.log(`Extracted session ID: ${sessionId}`);
|
|
62
|
-
expect(sessionId).toBe(
|
|
58
|
+
expect(sessionId).toBe("0199e659-0e5f-7843-8876-5a65c64e77c0");
|
|
63
59
|
|
|
64
|
-
console.log(
|
|
60
|
+
console.log("✅ Session metadata extraction test passed!\n");
|
|
65
61
|
});
|
|
66
62
|
|
|
67
|
-
it(
|
|
68
|
-
console.log(
|
|
63
|
+
it("should demonstrate session tracking workflow", async () => {
|
|
64
|
+
console.log("\n=== Session Tracking Workflow Demo ===");
|
|
69
65
|
|
|
70
66
|
// Simulate codex output that would contain session information
|
|
71
67
|
const mockCodexOutputs = [
|
|
72
68
|
{
|
|
73
|
-
directory:
|
|
74
|
-
output:
|
|
75
|
-
'Starting new conversation... Session ID: aaaa1111-2222-3333-4444-bbbbccccdddd',
|
|
69
|
+
directory: "logs/cwd1",
|
|
70
|
+
output: "Starting new conversation... Session ID: aaaa1111-2222-3333-4444-bbbbccccdddd",
|
|
76
71
|
},
|
|
77
72
|
{
|
|
78
|
-
directory:
|
|
79
|
-
output:
|
|
80
|
-
'Resuming session... Using ID: bbbb2222-3333-4444-5555-ccccddddeeee',
|
|
73
|
+
directory: "logs/cwd2",
|
|
74
|
+
output: "Resuming session... Using ID: bbbb2222-3333-4444-5555-ccccddddeeee",
|
|
81
75
|
},
|
|
82
76
|
];
|
|
83
77
|
|
|
84
|
-
console.log(
|
|
78
|
+
console.log("Simulating session capture from codex output:");
|
|
85
79
|
|
|
86
80
|
for (const mock of mockCodexOutputs) {
|
|
87
81
|
const sessionId = extractSessionId(mock.output);
|
|
88
82
|
console.log(`Directory: ${mock.directory}`);
|
|
89
83
|
console.log(`Output: ${mock.output}`);
|
|
90
84
|
console.log(`Captured Session ID: ${sessionId}`);
|
|
91
|
-
console.log(
|
|
85
|
+
console.log("---");
|
|
92
86
|
|
|
93
87
|
expect(sessionId).toBeTruthy();
|
|
94
|
-
expect(sessionId).toMatch(
|
|
95
|
-
/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i,
|
|
96
|
-
);
|
|
88
|
+
expect(sessionId).toMatch(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i);
|
|
97
89
|
}
|
|
98
90
|
|
|
99
|
-
console.log(
|
|
91
|
+
console.log("✅ Workflow demonstration completed!\n");
|
|
100
92
|
});
|
|
101
93
|
});
|
package/ts/utils.spec.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import { type DeepPartial, deepMixin, sleepms } from
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { type DeepPartial, deepMixin, sleepms } from "./utils";
|
|
3
3
|
|
|
4
|
-
describe(
|
|
5
|
-
describe(
|
|
6
|
-
it(
|
|
4
|
+
describe("utils", () => {
|
|
5
|
+
describe("sleepms", () => {
|
|
6
|
+
it("should return a promise", () => {
|
|
7
7
|
const result = sleepms(100);
|
|
8
8
|
expect(result).toBeInstanceOf(Promise);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
it(
|
|
11
|
+
it("should resolve after some time", async () => {
|
|
12
12
|
const start = Date.now();
|
|
13
13
|
await sleepms(10);
|
|
14
14
|
const end = Date.now();
|
|
15
15
|
expect(end - start).toBeGreaterThanOrEqual(5); // Allow some margin
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
it(
|
|
18
|
+
it("should handle zero milliseconds", async () => {
|
|
19
19
|
const start = Date.now();
|
|
20
20
|
await sleepms(0);
|
|
21
21
|
const end = Date.now();
|
|
@@ -23,8 +23,8 @@ describe('utils', () => {
|
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
describe(
|
|
27
|
-
it(
|
|
26
|
+
describe("deepMixin", () => {
|
|
27
|
+
it("should merge simple properties", () => {
|
|
28
28
|
const target = { a: 1, b: 2 };
|
|
29
29
|
const source = { b: 3, c: 4 };
|
|
30
30
|
const result = deepMixin(target, source);
|
|
@@ -33,25 +33,25 @@ describe('utils', () => {
|
|
|
33
33
|
expect(result).toBe(target); // Should modify original object
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it(
|
|
36
|
+
it("should merge nested objects", () => {
|
|
37
37
|
const target = {
|
|
38
|
-
user: { name:
|
|
39
|
-
settings: { theme:
|
|
38
|
+
user: { name: "John", age: 30 },
|
|
39
|
+
settings: { theme: "dark" },
|
|
40
40
|
};
|
|
41
41
|
const source: DeepPartial<typeof target> = {
|
|
42
42
|
user: { age: 31 },
|
|
43
|
-
settings: { language:
|
|
43
|
+
settings: { language: "en" } as any,
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
deepMixin(target, source);
|
|
47
47
|
|
|
48
48
|
expect(target).toEqual({
|
|
49
|
-
user: { name:
|
|
50
|
-
settings: { theme:
|
|
49
|
+
user: { name: "John", age: 31 },
|
|
50
|
+
settings: { theme: "dark", language: "en" },
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it(
|
|
54
|
+
it("should create nested objects when target property is null", () => {
|
|
55
55
|
const target: any = { config: null };
|
|
56
56
|
const source = { config: { enabled: true } };
|
|
57
57
|
|
|
@@ -62,8 +62,8 @@ describe('utils', () => {
|
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it(
|
|
66
|
-
const target: any = { config:
|
|
65
|
+
it("should create nested objects when target property is primitive", () => {
|
|
66
|
+
const target: any = { config: "string" };
|
|
67
67
|
const source = { config: { enabled: true } };
|
|
68
68
|
|
|
69
69
|
deepMixin(target, source);
|
|
@@ -73,7 +73,7 @@ describe('utils', () => {
|
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
it(
|
|
76
|
+
it("should handle arrays by replacing them", () => {
|
|
77
77
|
const target = { items: [1, 2, 3] };
|
|
78
78
|
const source = { items: [4, 5] };
|
|
79
79
|
|
|
@@ -82,7 +82,7 @@ describe('utils', () => {
|
|
|
82
82
|
expect(target).toEqual({ items: [4, 5] });
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
it(
|
|
85
|
+
it("should ignore undefined values", () => {
|
|
86
86
|
const target = { a: 1, b: 2 };
|
|
87
87
|
const source = { a: undefined, c: 3 };
|
|
88
88
|
|
|
@@ -91,7 +91,7 @@ describe('utils', () => {
|
|
|
91
91
|
expect(target).toEqual({ a: 1, b: 2, c: 3 });
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
it(
|
|
94
|
+
it("should handle null values", () => {
|
|
95
95
|
const target = { a: 1, b: 2 };
|
|
96
96
|
const source = { a: null, c: 3 };
|
|
97
97
|
|
|
@@ -100,18 +100,18 @@ describe('utils', () => {
|
|
|
100
100
|
expect(target).toEqual({ a: null, b: 2, c: 3 });
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
it(
|
|
103
|
+
it("should handle deeply nested structures", () => {
|
|
104
104
|
const target = {
|
|
105
105
|
level1: {
|
|
106
106
|
level2: {
|
|
107
|
-
level3: { value:
|
|
107
|
+
level3: { value: "old" },
|
|
108
108
|
},
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
111
|
const source = {
|
|
112
112
|
level1: {
|
|
113
113
|
level2: {
|
|
114
|
-
level3: { value:
|
|
114
|
+
level3: { value: "new", extra: "added" },
|
|
115
115
|
},
|
|
116
116
|
},
|
|
117
117
|
};
|
|
@@ -121,13 +121,13 @@ describe('utils', () => {
|
|
|
121
121
|
expect(target).toEqual({
|
|
122
122
|
level1: {
|
|
123
123
|
level2: {
|
|
124
|
-
level3: { value:
|
|
124
|
+
level3: { value: "new", extra: "added" },
|
|
125
125
|
},
|
|
126
126
|
},
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
it(
|
|
130
|
+
it("should handle empty objects", () => {
|
|
131
131
|
const target = {};
|
|
132
132
|
const source = {};
|
|
133
133
|
|
|
@@ -137,32 +137,32 @@ describe('utils', () => {
|
|
|
137
137
|
expect(result).toBe(target);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
it(
|
|
140
|
+
it("should handle complex mixed types", () => {
|
|
141
141
|
const target: any = {
|
|
142
|
-
string:
|
|
142
|
+
string: "value",
|
|
143
143
|
number: 42,
|
|
144
144
|
boolean: true,
|
|
145
|
-
object: { nested:
|
|
145
|
+
object: { nested: "value" },
|
|
146
146
|
array: [1, 2, 3],
|
|
147
147
|
};
|
|
148
148
|
const source: any = {
|
|
149
|
-
string:
|
|
149
|
+
string: "new value",
|
|
150
150
|
number: 100,
|
|
151
151
|
boolean: false,
|
|
152
|
-
object: { nested:
|
|
152
|
+
object: { nested: "new value", added: "property" },
|
|
153
153
|
array: [4, 5],
|
|
154
|
-
newProp:
|
|
154
|
+
newProp: "added",
|
|
155
155
|
};
|
|
156
156
|
|
|
157
157
|
deepMixin(target, source);
|
|
158
158
|
|
|
159
159
|
expect(target).toEqual({
|
|
160
|
-
string:
|
|
160
|
+
string: "new value",
|
|
161
161
|
number: 100,
|
|
162
162
|
boolean: false,
|
|
163
|
-
object: { nested:
|
|
163
|
+
object: { nested: "new value", added: "property" },
|
|
164
164
|
array: [4, 5],
|
|
165
|
-
newProp:
|
|
165
|
+
newProp: "added",
|
|
166
166
|
});
|
|
167
167
|
});
|
|
168
168
|
});
|
package/ts/utils.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
export function sleepms(ms: number) {
|
|
2
2
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
3
|
}
|
|
4
|
-
export function deepMixin<T>(target: T, source: DeepPartial<T>): T {
|
|
4
|
+
export function deepMixin<T>(target: T, source: DeepPartial<T>, ...more: DeepPartial<T>[]): T {
|
|
5
5
|
for (const key in source) {
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
typeof source[key] === 'object' &&
|
|
9
|
-
!Array.isArray(source[key])
|
|
10
|
-
) {
|
|
11
|
-
if (!target[key] || typeof target[key] !== 'object') {
|
|
6
|
+
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
7
|
+
if (!target[key] || typeof target[key] !== "object") {
|
|
12
8
|
(target as any)[key] = {};
|
|
13
9
|
}
|
|
14
10
|
deepMixin(target[key], source[key] as any);
|
|
@@ -16,6 +12,10 @@ export function deepMixin<T>(target: T, source: DeepPartial<T>): T {
|
|
|
16
12
|
(target as any)[key] = source[key];
|
|
17
13
|
}
|
|
18
14
|
}
|
|
15
|
+
|
|
16
|
+
for (const moreSource of more) {
|
|
17
|
+
deepMixin(target, moreSource);
|
|
18
|
+
}
|
|
19
19
|
return target;
|
|
20
20
|
}
|
|
21
21
|
export type DeepPartial<T> = {
|