claude-yes 1.31.2 → 1.32.2

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.
Files changed (44) hide show
  1. package/README.md +225 -21
  2. package/dist/agent-yes.js +2 -0
  3. package/dist/amp-yes.js +2 -0
  4. package/dist/auggie-yes.js +2 -0
  5. package/dist/claude-yes.js +2 -20432
  6. package/dist/cli.js +18342 -10955
  7. package/dist/codex-yes.js +2 -20432
  8. package/dist/copilot-yes.js +2 -20432
  9. package/dist/cursor-yes.js +2 -20432
  10. package/dist/gemini-yes.js +2 -20432
  11. package/dist/grok-yes.js +2 -20432
  12. package/dist/index.js +16258 -13586
  13. package/dist/qwen-yes.js +2 -20432
  14. package/package.json +93 -81
  15. package/ts/ReadyManager.spec.ts +10 -10
  16. package/ts/ReadyManager.ts +1 -1
  17. package/ts/SUPPORTED_CLIS.ts +4 -0
  18. package/ts/catcher.spec.ts +69 -70
  19. package/ts/cli-idle.spec.ts +8 -8
  20. package/ts/cli.ts +18 -26
  21. package/ts/defineConfig.ts +4 -4
  22. package/ts/idleWaiter.spec.ts +9 -9
  23. package/ts/index.ts +474 -233
  24. package/ts/logger.ts +22 -0
  25. package/ts/parseCliArgs.spec.ts +146 -147
  26. package/ts/parseCliArgs.ts +127 -59
  27. package/ts/postbuild.ts +29 -15
  28. package/ts/pty-fix.ts +155 -0
  29. package/ts/pty.ts +19 -0
  30. package/ts/removeControlCharacters.spec.ts +37 -38
  31. package/ts/removeControlCharacters.ts +2 -1
  32. package/ts/runningLock.spec.ts +119 -125
  33. package/ts/runningLock.ts +44 -55
  34. package/ts/session-integration.spec.ts +34 -42
  35. package/ts/utils.spec.ts +35 -35
  36. package/ts/utils.ts +7 -7
  37. package/dist/cli.js.map +0 -365
  38. package/dist/index.js.map +0 -323
  39. package/ts/codex-resume.spec.ts +0 -239
  40. package/ts/codexSessionManager.spec.ts +0 -51
  41. package/ts/codexSessionManager.test.ts +0 -259
  42. package/ts/codexSessionManager.ts +0 -312
  43. package/ts/yesLog.spec.ts +0 -74
  44. package/ts/yesLog.ts +0 -27
package/ts/runningLock.ts CHANGED
@@ -1,15 +1,15 @@
1
- import { execSync } from 'child_process';
2
- import { existsSync } from 'fs';
3
- import { mkdir, readFile, rename, unlink, writeFile } from 'fs/promises';
4
- import { homedir } from 'os';
5
- import path from 'path';
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: 'running' | 'queued' | 'completed' | 'failed';
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 LOCK_DIR = path.join(homedir(), '.claude-yes');
28
- const LOCK_FILE = path.join(LOCK_DIR, 'running.lock.json');
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('git rev-parse --show-toplevel', {
51
+ const result = execSync("git rev-parse --show-toplevel", {
52
52
  cwd,
53
- encoding: 'utf8',
54
- stdio: ['pipe', 'pipe', 'ignore'],
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
- await mkdir(LOCK_DIR, { recursive: true });
97
+ const lockDir = getLockDir();
98
+ const lockFilePath = getLockFile();
99
+ await mkdir(lockDir, { recursive: true });
98
100
 
99
- if (!existsSync(LOCK_FILE)) {
101
+ if (!existsSync(lockFilePath)) {
100
102
  return { tasks: [] };
101
103
  }
102
104
 
103
- const content = await readFile(LOCK_FILE, 'utf8');
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 (error) {
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
- await mkdir(LOCK_DIR, { recursive: true });
126
+ const lockDir = getLockDir();
127
+ const lockFilePath = getLockFile();
128
+ await mkdir(lockDir, { recursive: true });
128
129
 
129
- const tempFile = `${LOCK_FILE}.tmp.${process.pid}`;
130
- await writeFile(tempFile, JSON.stringify(lockFile, null, 2), 'utf8');
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, LOCK_FILE);
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 !== 'running') return false; // Only check running tasks
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: 'queued' });
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 === 'b' || char === 'B') {
243
- console.log('\n⚡ Bypassing queue...');
234
+ if (char === "b" || char === "B") {
235
+ console.log("\n⚡ Bypassing queue...");
244
236
  bypassed = true;
245
- } else if (char === 'k' || char === 'K') {
246
- console.log('\n🔪 Killing previous instance...');
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('data', keyHandler);
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, 'running');
258
- console.log('✓ Queue bypassed, starting task...');
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, 'SIGTERM');
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, 'running');
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('data', keyHandler);
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 = 'no prompt provided',
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: 'running',
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['status'],
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(cwd: string): boolean {
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 'bun:test';
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('Session Extraction Test', () => {
8
- it('should extract session IDs from various codex output formats', async () => {
9
- console.log('\n=== Session ID Extraction Test ===');
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: 'Direct UUID in output',
15
- output: 'Session started with ID: 0199e659-0e5f-7843-8876-5a65c64e77c0',
16
- expected: '0199e659-0e5f-7843-8876-5a65c64e77c0',
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: 'UUID in brackets',
20
- output:
21
- 'Using session [0199e659-0e5f-7843-8876-5a65c64e77c0] for this conversation',
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: 'Mixed case UUID',
26
- output: 'SESSION_ID: 0199E659-0E5F-7843-8876-5A65C64E77C0',
27
- expected: '0199E659-0E5F-7843-8876-5A65C64E77C0',
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: 'No UUID present',
31
- output: 'Welcome to codex! Type your message and press enter.',
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: 'Multiple UUIDs (should get first)',
31
+ name: "Multiple UUIDs (should get first)",
36
32
  output:
37
- 'Old: 1111e659-0e5f-7843-8876-5a65c64e77c0 New: 2222e659-0e5f-7843-8876-5a65c64e77c0',
38
- expected: '1111e659-0e5f-7843-8876-5a65c64e77c0',
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('✅ All session extraction tests passed!\n');
47
+ console.log("✅ All session extraction tests passed!\n");
52
48
  });
53
49
 
54
- it('should extract session ID from session metadata JSON', async () => {
55
- console.log('\n=== Session Metadata Extraction Test ===');
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('0199e659-0e5f-7843-8876-5a65c64e77c0');
58
+ expect(sessionId).toBe("0199e659-0e5f-7843-8876-5a65c64e77c0");
63
59
 
64
- console.log('✅ Session metadata extraction test passed!\n');
60
+ console.log("✅ Session metadata extraction test passed!\n");
65
61
  });
66
62
 
67
- it('should demonstrate session tracking workflow', async () => {
68
- console.log('\n=== Session Tracking Workflow Demo ===');
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: 'logs/cwd1',
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: 'logs/cwd2',
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('Simulating session capture from codex output:');
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('✅ Workflow demonstration completed!\n');
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 'vitest';
2
- import { type DeepPartial, deepMixin, sleepms } from './utils';
1
+ import { describe, expect, it } from "vitest";
2
+ import { type DeepPartial, deepMixin, sleepms } from "./utils";
3
3
 
4
- describe('utils', () => {
5
- describe('sleepms', () => {
6
- it('should return a promise', () => {
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('should resolve after some time', async () => {
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('should handle zero milliseconds', async () => {
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('deepMixin', () => {
27
- it('should merge simple properties', () => {
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('should merge nested objects', () => {
36
+ it("should merge nested objects", () => {
37
37
  const target = {
38
- user: { name: 'John', age: 30 },
39
- settings: { theme: 'dark' },
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: 'en' } as any,
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: 'John', age: 31 },
50
- settings: { theme: 'dark', language: 'en' },
49
+ user: { name: "John", age: 31 },
50
+ settings: { theme: "dark", language: "en" },
51
51
  });
52
52
  });
53
53
 
54
- it('should create nested objects when target property is null', () => {
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('should create nested objects when target property is primitive', () => {
66
- const target: any = { config: 'string' };
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('should handle arrays by replacing them', () => {
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('should ignore undefined values', () => {
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('should handle null values', () => {
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('should handle deeply nested structures', () => {
103
+ it("should handle deeply nested structures", () => {
104
104
  const target = {
105
105
  level1: {
106
106
  level2: {
107
- level3: { value: 'old' },
107
+ level3: { value: "old" },
108
108
  },
109
109
  },
110
110
  };
111
111
  const source = {
112
112
  level1: {
113
113
  level2: {
114
- level3: { value: 'new', extra: 'added' },
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: 'new', extra: 'added' },
124
+ level3: { value: "new", extra: "added" },
125
125
  },
126
126
  },
127
127
  });
128
128
  });
129
129
 
130
- it('should handle empty objects', () => {
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('should handle complex mixed types', () => {
140
+ it("should handle complex mixed types", () => {
141
141
  const target: any = {
142
- string: 'value',
142
+ string: "value",
143
143
  number: 42,
144
144
  boolean: true,
145
- object: { nested: 'value' },
145
+ object: { nested: "value" },
146
146
  array: [1, 2, 3],
147
147
  };
148
148
  const source: any = {
149
- string: 'new value',
149
+ string: "new value",
150
150
  number: 100,
151
151
  boolean: false,
152
- object: { nested: 'new value', added: 'property' },
152
+ object: { nested: "new value", added: "property" },
153
153
  array: [4, 5],
154
- newProp: 'added',
154
+ newProp: "added",
155
155
  };
156
156
 
157
157
  deepMixin(target, source);
158
158
 
159
159
  expect(target).toEqual({
160
- string: 'new value',
160
+ string: "new value",
161
161
  number: 100,
162
162
  boolean: false,
163
- object: { nested: 'new value', added: 'property' },
163
+ object: { nested: "new value", added: "property" },
164
164
  array: [4, 5],
165
- newProp: 'added',
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
- source[key] &&
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> = {