goke 6.12.3 → 6.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tests for the daemon background process support.
3
+ *
4
+ * Tests the DaemonContext lifecycle: PID file management, isDaemon detection,
5
+ * start/stop/isRunning, forCommand, heartbeat, and instance ID safety.
6
+ *
7
+ * Server-mode tests (isDaemon=true) run in child processes to avoid
8
+ * scheduling process.exit() timers inside the vitest runner.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=daemon.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.test.d.ts","sourceRoot":"","sources":["../../src/__test__/daemon.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Tests for the daemon background process support.
3
+ *
4
+ * Tests the DaemonContext lifecycle: PID file management, isDaemon detection,
5
+ * start/stop/isRunning, forCommand, heartbeat, and instance ID safety.
6
+ *
7
+ * Server-mode tests (isDaemon=true) run in child processes to avoid
8
+ * scheduling process.exit() timers inside the vitest runner.
9
+ */
10
+ import { describe, expect, test, afterEach } from 'vitest';
11
+ import { execFile } from 'node:child_process';
12
+ import { promisify } from 'node:util';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import os from 'node:os';
16
+ const execFileAsync = promisify(execFile);
17
+ const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons');
18
+ function pidFilePath(cliName, commandName) {
19
+ const safeName = `${cliName}--${commandName}`
20
+ .replace(/\s+/g, '-')
21
+ .replace(/[^a-zA-Z0-9_-]/g, '');
22
+ return path.join(DAEMON_DIR, `${safeName}.pid.json`);
23
+ }
24
+ function readPidFile(filePath) {
25
+ try {
26
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function isProcessAlive(pid) {
33
+ try {
34
+ process.kill(pid, 0);
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ async function killIfAlive(pid) {
42
+ if (isProcessAlive(pid)) {
43
+ try {
44
+ process.kill(pid, 'SIGKILL');
45
+ }
46
+ catch { }
47
+ }
48
+ }
49
+ // Track PIDs to clean up after tests
50
+ const spawnedPids = [];
51
+ const testPidFiles = [];
52
+ afterEach(async () => {
53
+ for (const pid of spawnedPids) {
54
+ await killIfAlive(pid);
55
+ }
56
+ spawnedPids.length = 0;
57
+ for (const f of testPidFiles) {
58
+ try {
59
+ fs.unlinkSync(f);
60
+ }
61
+ catch { }
62
+ }
63
+ testPidFiles.length = 0;
64
+ });
65
+ // Helper script that simulates a daemon process: writes PID file with
66
+ // instance ID and heartbeat, stays alive until SIGTERM or timeout.
67
+ function writeDaemonHelper(scriptPath) {
68
+ fs.writeFileSync(scriptPath, `
69
+ import fs from 'node:fs'
70
+ import path from 'node:path'
71
+ import os from 'node:os'
72
+ import crypto from 'node:crypto'
73
+
74
+ const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
75
+ const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
76
+ const cmdName = process.env.TEST_CMD_NAME || 'bg'
77
+ const safeName = cliName + '--' + cmdName
78
+ const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
79
+
80
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true })
81
+
82
+ const instanceId = crypto.randomBytes(8).toString('hex')
83
+ const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
84
+ fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
85
+
86
+ const hb = setInterval(() => {
87
+ try {
88
+ const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
89
+ if (current.id === instanceId) {
90
+ current.heartbeatAt = Date.now()
91
+ fs.writeFileSync(pidFile, JSON.stringify(current), { mode: 0o600 })
92
+ }
93
+ } catch {}
94
+ }, 2000)
95
+ hb.unref()
96
+
97
+ const timeoutMs = Number(process.env.GOKE_DAEMON_TIMEOUT) || 60000
98
+ const timer = setTimeout(() => {
99
+ try {
100
+ const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
101
+ if (current.id === instanceId) fs.unlinkSync(pidFile)
102
+ } catch {}
103
+ process.exit(0)
104
+ }, timeoutMs)
105
+
106
+ process.on('SIGTERM', () => {
107
+ clearTimeout(timer)
108
+ clearInterval(hb)
109
+ try {
110
+ const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
111
+ if (current.id === instanceId) fs.unlinkSync(pidFile)
112
+ } catch {}
113
+ process.exit(0)
114
+ })
115
+
116
+ process.on('exit', () => {
117
+ try {
118
+ const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
119
+ if (current.id === instanceId) fs.unlinkSync(pidFile)
120
+ } catch {}
121
+ })
122
+ `);
123
+ }
124
+ describe('DaemonContext', () => {
125
+ test('isDaemon is false by default (client mode)', async () => {
126
+ const { default: goke } = await import('../index.js');
127
+ const cli = goke('test-cli');
128
+ let isDaemon;
129
+ cli.command('run', 'test').action((opts, ctx) => {
130
+ isDaemon = ctx.daemon.isDaemon;
131
+ });
132
+ await cli.parse(['node', 'test', 'run'], { run: true });
133
+ expect(isDaemon).toBe(false);
134
+ });
135
+ test('isDaemon is true when GOKE_DAEMON=1 (tested in child process)', async () => {
136
+ // Run a small script in a child process that creates a DaemonContext
137
+ // with the env var set and prints isDaemon. This avoids scheduling
138
+ // process.exit() timers inside the vitest process.
139
+ // Uses the compiled dist/ so plain Node can import it (no tsx needed).
140
+ const scriptPath = path.join(os.tmpdir(), 'goke-daemon-is-daemon-test.mjs');
141
+ const distDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', 'dist');
142
+ fs.writeFileSync(scriptPath, `
143
+ import { DaemonContext } from '${distDir}/daemon.js'
144
+ const ctx = new DaemonContext('test-is-daemon', 'cmd', ['node', 'test'])
145
+ console.log(ctx.isDaemon ? 'SERVER' : 'CLIENT')
146
+ // Exit immediately to not leave the daemon alive
147
+ process.exit(0)
148
+ `);
149
+ testPidFiles.push(pidFilePath('test-is-daemon', 'cmd'));
150
+ const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
151
+ env: { ...process.env, GOKE_DAEMON: '1', GOKE_DAEMON_TIMEOUT: '1000' },
152
+ });
153
+ expect(stdout.trim()).toBe('SERVER');
154
+ }, 10_000);
155
+ test('isRunning returns false when no daemon is running', async () => {
156
+ const { default: goke } = await import('../index.js');
157
+ const cli = goke('test-cli');
158
+ let running;
159
+ cli.command('check', 'test').action(async (opts, ctx) => {
160
+ running = await ctx.daemon.isRunning();
161
+ });
162
+ await cli.parse(['node', 'test', 'check'], { run: true });
163
+ expect(running).toBe(false);
164
+ });
165
+ test('start spawns a detached daemon, isRunning returns true, stop kills it', async () => {
166
+ const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-start.mjs');
167
+ writeDaemonHelper(helperScript);
168
+ testPidFiles.push(pidFilePath('test-daemon-cli', 'bg'));
169
+ const { DaemonContext } = await import('../daemon.js');
170
+ const ctx = new DaemonContext('test-daemon-cli', 'bg', [process.execPath, helperScript, 'bg']);
171
+ expect(ctx.isDaemon).toBe(false);
172
+ expect(await ctx.isRunning()).toBe(false);
173
+ await ctx.start({ timeoutMs: 30_000 });
174
+ expect(await ctx.isRunning()).toBe(true);
175
+ const pidData = readPidFile(pidFilePath('test-daemon-cli', 'bg'));
176
+ expect(pidData).not.toBeNull();
177
+ expect(pidData.id).toBeTruthy();
178
+ spawnedPids.push(pidData.pid);
179
+ await ctx.stop();
180
+ await new Promise((r) => setTimeout(r, 200));
181
+ expect(await ctx.isRunning()).toBe(false);
182
+ try {
183
+ fs.unlinkSync(helperScript);
184
+ }
185
+ catch { }
186
+ }, 15_000);
187
+ test('start kills existing daemon before spawning new one', async () => {
188
+ const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-replace.mjs');
189
+ writeDaemonHelper(helperScript);
190
+ testPidFiles.push(pidFilePath('test-daemon-cli', 'bg'));
191
+ const { DaemonContext } = await import('../daemon.js');
192
+ const ctx = new DaemonContext('test-daemon-cli', 'bg', [process.execPath, helperScript, 'bg']);
193
+ await ctx.start({ timeoutMs: 30_000 });
194
+ const firstPid = readPidFile(pidFilePath('test-daemon-cli', 'bg'));
195
+ expect(firstPid).not.toBeNull();
196
+ spawnedPids.push(firstPid.pid);
197
+ const firstId = firstPid.id;
198
+ await ctx.start({ timeoutMs: 30_000 });
199
+ const secondPid = readPidFile(pidFilePath('test-daemon-cli', 'bg'));
200
+ expect(secondPid).not.toBeNull();
201
+ spawnedPids.push(secondPid.pid);
202
+ // PIDs and instance IDs should differ
203
+ expect(secondPid.pid).not.toBe(firstPid.pid);
204
+ expect(secondPid.id).not.toBe(firstId);
205
+ expect(isProcessAlive(firstPid.pid)).toBe(false);
206
+ expect(isProcessAlive(secondPid.pid)).toBe(true);
207
+ await ctx.stop();
208
+ try {
209
+ fs.unlinkSync(helperScript);
210
+ }
211
+ catch { }
212
+ }, 15_000);
213
+ test('stop is idempotent when no daemon running', async () => {
214
+ const { DaemonContext } = await import('../daemon.js');
215
+ const ctx = new DaemonContext('nonexistent-cli', 'nope', ['node', 'nope']);
216
+ await ctx.stop();
217
+ await ctx.stop();
218
+ });
219
+ test('forCommand returns context for a different command', async () => {
220
+ const { DaemonContext } = await import('../daemon.js');
221
+ const loginCtx = new DaemonContext('myapp', 'login', ['node', 'myapp', 'login']);
222
+ const meCtx = loginCtx.forCommand('me');
223
+ // They should reference different PID files
224
+ expect(await loginCtx.isRunning()).toBe(false);
225
+ expect(await meCtx.isRunning()).toBe(false);
226
+ // forCommand context is always client mode
227
+ expect(meCtx.isDaemon).toBe(false);
228
+ });
229
+ test('forCommand can check and stop another commands daemon', async () => {
230
+ const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-forcommand.mjs');
231
+ writeDaemonHelper(helperScript);
232
+ testPidFiles.push(pidFilePath('test-daemon-cli', 'bg'));
233
+ const { DaemonContext } = await import('../daemon.js');
234
+ // Start daemon for "bg" command
235
+ const bgCtx = new DaemonContext('test-daemon-cli', 'bg', [process.execPath, helperScript, 'bg']);
236
+ await bgCtx.start({ timeoutMs: 30_000 });
237
+ const pidData = readPidFile(pidFilePath('test-daemon-cli', 'bg'));
238
+ if (pidData)
239
+ spawnedPids.push(pidData.pid);
240
+ // Create a context for "me" command and use forCommand to check "bg"
241
+ const meCtx = new DaemonContext('test-daemon-cli', 'me', ['node', 'test', 'me']);
242
+ const bgFromMe = meCtx.forCommand('bg');
243
+ expect(await bgFromMe.isRunning()).toBe(true);
244
+ await bgFromMe.stop();
245
+ await new Promise((r) => setTimeout(r, 200));
246
+ expect(await bgFromMe.isRunning()).toBe(false);
247
+ try {
248
+ fs.unlinkSync(helperScript);
249
+ }
250
+ catch { }
251
+ }, 15_000);
252
+ test('stale PID file is cleaned up by isRunning', async () => {
253
+ const pidFile = pidFilePath('stale-test', 'cmd');
254
+ testPidFiles.push(pidFile);
255
+ // Write a PID file with a PID that doesn't exist
256
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
257
+ fs.writeFileSync(pidFile, JSON.stringify({
258
+ pid: 999999999,
259
+ id: 'stale-instance',
260
+ startedAt: Date.now(),
261
+ heartbeatAt: Date.now(),
262
+ }));
263
+ const { DaemonContext } = await import('../daemon.js');
264
+ const ctx = new DaemonContext('stale-test', 'cmd', ['node', 'test']);
265
+ expect(await ctx.isRunning()).toBe(false);
266
+ // PID file should have been cleaned up
267
+ expect(fs.existsSync(pidFile)).toBe(false);
268
+ });
269
+ test('PID file with stale heartbeat is treated as not running', async () => {
270
+ const pidFile = pidFilePath('heartbeat-test', 'cmd');
271
+ testPidFiles.push(pidFile);
272
+ // Write a PID file with current process PID but very old heartbeat
273
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
274
+ fs.writeFileSync(pidFile, JSON.stringify({
275
+ pid: process.pid, // alive PID
276
+ id: 'old-heartbeat',
277
+ startedAt: Date.now() - 60000,
278
+ heartbeatAt: Date.now() - 60000, // 60s old, well past the 15s threshold
279
+ }));
280
+ const { DaemonContext } = await import('../daemon.js');
281
+ const ctx = new DaemonContext('heartbeat-test', 'cmd', ['node', 'test']);
282
+ // Should return false because heartbeat is stale (even though PID is alive)
283
+ expect(await ctx.isRunning()).toBe(false);
284
+ });
285
+ test('daemon context has correct command name from parsed cli', async () => {
286
+ const { default: goke } = await import('../index.js');
287
+ const cli = goke('my-app');
288
+ let capturedDaemon;
289
+ cli.command('auth login', 'Login').action((opts, ctx) => {
290
+ capturedDaemon = ctx.daemon;
291
+ });
292
+ await cli.parse(['node', 'my-app', 'auth', 'login'], { run: true });
293
+ expect(capturedDaemon).toBeDefined();
294
+ expect(capturedDaemon.isDaemon).toBe(false);
295
+ });
296
+ });
@@ -115,7 +115,7 @@ describe('error formatting', () => {
115
115
  expect(exitCode).toBe(1);
116
116
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: connection refused"`);
117
117
  });
118
- test('error output includes stack trace', async () => {
118
+ test('GokeError (validation) omits stack trace', async () => {
119
119
  const stderr = createTestOutputStream();
120
120
  const cli = goke('mycli', { stderr, exit: () => { } });
121
121
  cli
@@ -125,10 +125,26 @@ describe('error formatting', () => {
125
125
  await cli.parse('node bin build --unknown'.split(' '));
126
126
  }
127
127
  catch { }
128
- // Verify that stderr contains "error:" prefix and a stack trace with "at" lines
129
128
  const text = stderr.text;
130
129
  expect(text).toContain('error:');
131
130
  expect(text).toContain('Unknown option `--unknown`');
131
+ // GokeError is a user-facing error; stack trace should be suppressed
132
+ expect(text).not.toMatch(/at /);
133
+ });
134
+ test('unexpected error still includes stack trace', async () => {
135
+ const stderr = createTestOutputStream();
136
+ const cli = goke('mycli', { stderr, exit: () => { } });
137
+ cli
138
+ .command('deploy', 'Deploy app')
139
+ .action(async () => {
140
+ throw new Error('unexpected crash');
141
+ });
142
+ await cli.parse('node bin deploy'.split(' '));
143
+ await new Promise(resolve => setTimeout(resolve, 10));
144
+ const text = stderr.text;
145
+ expect(text).toContain('error:');
146
+ expect(text).toContain('unexpected crash');
147
+ // Non-GokeError should still show the stack trace
132
148
  expect(text).toMatch(/at /);
133
149
  });
134
150
  });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Browser-safe daemon stub.
3
+ *
4
+ * Provides the same DaemonContext interface as daemon.ts but without
5
+ * Node.js dependencies. start() throws; everything else is a no-op.
6
+ * Used via the #daemon conditional import in browser/edge runtimes.
7
+ */
8
+ declare class DaemonContext {
9
+ readonly isDaemon: false;
10
+ constructor();
11
+ forCommand(_commandName: string): DaemonContext;
12
+ start(): Promise<void>;
13
+ stop(): Promise<void>;
14
+ isRunning(): Promise<boolean>;
15
+ }
16
+ declare function createDaemonContext(): DaemonContext;
17
+ export { DaemonContext, createDaemonContext };
18
+ export type { DaemonStartOptions } from './daemon.js';
19
+ //# sourceMappingURL=daemon-browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-browser.d.ts","sourceRoot":"","sources":["../src/daemon-browser.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAM,aAAa;IACjB,QAAQ,CAAC,QAAQ,EAAG,KAAK,CAAS;;IAIlC,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa;IAIzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAErB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAGpC;AAED,iBAAS,mBAAmB,IAAI,aAAa,CAE5C;AAED,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Browser-safe daemon stub.
3
+ *
4
+ * Provides the same DaemonContext interface as daemon.ts but without
5
+ * Node.js dependencies. start() throws; everything else is a no-op.
6
+ * Used via the #daemon conditional import in browser/edge runtimes.
7
+ */
8
+ class DaemonContext {
9
+ isDaemon = false;
10
+ constructor() { }
11
+ forCommand(_commandName) {
12
+ return new DaemonContext();
13
+ }
14
+ async start() {
15
+ throw new Error('ctx.daemon.start() is only available in Node.js runtimes.');
16
+ }
17
+ async stop() { }
18
+ async isRunning() {
19
+ return false;
20
+ }
21
+ }
22
+ function createDaemonContext() {
23
+ return new DaemonContext();
24
+ }
25
+ export { DaemonContext, createDaemonContext };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Background daemon support for goke CLIs.
3
+ *
4
+ * Lets a command fork itself into a detached background process. The daemon
5
+ * is identified by CLI name + command name, with a PID file for lifecycle
6
+ * management. No HTTP server, no ports. Communication between client and
7
+ * daemon happens via shared files (config, auth, state) that the CLI
8
+ * already manages.
9
+ *
10
+ * How it works:
11
+ * 1. Command action checks `ctx.daemon.isDaemon` to branch behavior
12
+ * 2. Client calls `ctx.daemon.start()` which re-spawns the same CLI
13
+ * command with GOKE_DAEMON=1 env var, detached + unref'd
14
+ * 3. Daemon process runs the same action, but `isDaemon` is true
15
+ * 4. Daemon auto-exits after timeoutMs
16
+ * 5. PID file tracks the running daemon for stop/isRunning checks
17
+ *
18
+ * PID file safety:
19
+ * Each daemon writes a unique instance ID (random hex) into the PID file.
20
+ * A heartbeat timestamp is updated every 5 seconds. `isRunning()` checks
21
+ * both that the PID is alive AND the heartbeat is recent (< 15s). This
22
+ * prevents false positives from PID reuse after a daemon crash.
23
+ * Cleanup handlers only remove the PID file if its ID matches the current
24
+ * instance, so a new daemon won't have its file deleted by an old one's
25
+ * exit handler firing late.
26
+ */
27
+ interface DaemonStartOptions {
28
+ /** Auto-exit timeout in milliseconds. Default: 10 minutes. */
29
+ timeoutMs?: number;
30
+ /** Extra environment variables passed to the daemon process. */
31
+ env?: Record<string, string>;
32
+ }
33
+ /**
34
+ * Daemon context available on every command's execution context.
35
+ *
36
+ * Lets a command fork itself into a background process. The client side
37
+ * calls `start()` to spawn the daemon. The daemon side checks `isDaemon`
38
+ * and does its work. Communication happens via shared files.
39
+ *
40
+ * Use `forCommand()` to get a daemon context for a different command.
41
+ * This is useful for commands like `me` or `logout` that need to check
42
+ * or stop the `login` daemon.
43
+ */
44
+ declare class DaemonContext {
45
+ #private;
46
+ /** True when this process IS the background daemon. */
47
+ readonly isDaemon: boolean;
48
+ constructor(cliName: string, commandName: string, argv: string[], env?: Record<string, string | undefined>);
49
+ /**
50
+ * Get a daemon context for a different command on the same CLI.
51
+ * Useful for cross-command daemon management (e.g. `me` checking `login` daemon).
52
+ *
53
+ * The returned context is always in client mode (isDaemon=false) regardless
54
+ * of the current process's daemon state, since it represents a different command.
55
+ */
56
+ forCommand(commandName: string): DaemonContext;
57
+ /**
58
+ * Spawn the current command as a detached background daemon process.
59
+ * Kills any existing daemon for this command first.
60
+ */
61
+ start(options?: DaemonStartOptions): Promise<void>;
62
+ /**
63
+ * Stop the running daemon for this command.
64
+ */
65
+ stop(): Promise<void>;
66
+ /**
67
+ * Check if the daemon for this command is currently running.
68
+ * Verifies both that the PID is alive and the heartbeat is recent
69
+ * to protect against PID reuse after a crash.
70
+ */
71
+ isRunning(): Promise<boolean>;
72
+ }
73
+ /**
74
+ * Create a DaemonContext for a command.
75
+ * Called internally by goke when building the execution context.
76
+ */
77
+ declare function createDaemonContext(cliName: string, commandName: string, argv: string[], env?: Record<string, string | undefined>): DaemonContext;
78
+ export { DaemonContext, createDaemonContext };
79
+ export type { DaemonStartOptions };
80
+ //# sourceMappingURL=daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA6IH,UAAU,kBAAkB;IAC1B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC7B;AAED;;;;;;;;;;GAUG;AACH,cAAM,aAAa;;IACjB,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;gBAYxB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAc1C;;;;;;OAMG;IACH,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa;IAkF9C;;;OAGG;IACG,KAAK,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCxD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAcpC;AAED;;;GAGG;AACH,iBAAS,mBAAmB,CAC1B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACvC,aAAa,CAEf;AAED,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,CAAA"}