From a3a8c365d2abd2c8826ce7a00a3e2b91ed81cf81 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 10 Sep 2026 20:58:23 +0300 Subject: [PATCH] Add AI agent detection to CLI analytics Detect the agent running the CLI with @vercel/detect-agent and report it as n: inside SHOPIFY_CLI_AGENT_INFO, the packed format the Shopify AI toolkit already uses, so a detected name resolves through the same field as a declared one. Detection only fills the gap. It is skipped when a producer declared SHOPIFY_CLI_AGENT_INFO or SHOPIFY_CLI_AGENT_IDS, because writing INFO ourselves would clobber their whole packed value, not just the name. SHOPIFY_CLI_AGENT_DETECTION reports the outcome on every run, as one of detected, none, unusable_name, skipped or failed, so a detection error is measurable rather than indistinguishable from finding no agent. It is deliberately absent from the allowlist, which filters before the merge, so the CLI is the only thing that can set it. It describes the detector's own outcome, not whether the run had an agent: legacy SHOPIFY_CLI_AGENT does not suppress detection, so a legacy-only producer can yield that variable alongside none. Assisted-By: devx/105d6a35-ec6d-462e-9e36-a9fde4ec06fc Co-Authored-By: Claude Opus 5 (1M context) --- packages/cli-kit/package.json | 1 + .../src/private/node/analytics.test.ts | 75 ++++++++ .../cli-kit/src/private/node/analytics.ts | 11 +- .../src/private/node/context/agent.test.ts | 164 ++++++++++++++++++ .../cli-kit/src/private/node/context/agent.ts | 53 ++++++ pnpm-lock.yaml | 56 ++++++ 6 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 packages/cli-kit/src/private/node/analytics.test.ts create mode 100644 packages/cli-kit/src/private/node/context/agent.test.ts create mode 100644 packages/cli-kit/src/private/node/context/agent.ts diff --git a/packages/cli-kit/package.json b/packages/cli-kit/package.json index daa30b9eab2..47105b5096d 100644 --- a/packages/cli-kit/package.json +++ b/packages/cli-kit/package.json @@ -115,6 +115,7 @@ "@opentelemetry/exporter-metrics-otlp-http": "0.57.0", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-metrics": "1.30.1", + "@vercel/detect-agent": "1.2.5", "ajv": "8.20.0", "ansi-escapes": "6.2.1", "archiver": "5.3.2", diff --git a/packages/cli-kit/src/private/node/analytics.test.ts b/packages/cli-kit/src/private/node/analytics.test.ts new file mode 100644 index 00000000000..a9b8e4756b0 --- /dev/null +++ b/packages/cli-kit/src/private/node/analytics.test.ts @@ -0,0 +1,75 @@ +import {getSensitiveEnvironmentData} from './analytics.js' +import {determineAgent, type KnownAgentNames} from '@vercel/detect-agent' +import {afterEach, describe, expect, test, vi} from 'vitest' + +vi.mock('@vercel/detect-agent') +vi.mock('../../public/node/output.js') + +function agentDetected(name: string) { + vi.mocked(determineAgent).mockResolvedValue({isAgent: true, agent: {name: name as KnownAgentNames}}) +} + +function noAgentDetected() { + vi.mocked(determineAgent).mockResolvedValue({isAgent: false, agent: undefined}) +} + +const declaredAgentVariableNames = ['SHOPIFY_CLI_AGENT_INFO', 'SHOPIFY_CLI_AGENT_IDS'] + +function stubDeclaredAgentVariablesEmpty() { + declaredAgentVariableNames.forEach((variableName) => vi.stubEnv(variableName, undefined)) +} + +// Only `config.plugins.keys()` is read, so an empty plugin map is enough. +const config = {plugins: new Map()} as any + +async function shopifyEnvironmentVariables() { + const {env_shopify_variables: shopifyVariables} = await getSensitiveEnvironmentData(config) + return JSON.parse(shopifyVariables) +} + +describe('getSensitiveEnvironmentData', () => { + afterEach(() => { + vi.unstubAllEnvs() + }) + + test('adds detected agent variables when explicit attribution is missing', async () => { + agentDetected('claude') + stubDeclaredAgentVariablesEmpty() + + const got = await shopifyEnvironmentVariables() + + expect(got).toMatchObject({SHOPIFY_CLI_AGENT_INFO: 'n:claude-code', SHOPIFY_CLI_AGENT_DETECTION: 'detected'}) + }) + + test('does not add a detected agent to the payload when only SHOPIFY_CLI_AGENT_INFO is declared', async () => { + agentDetected('claude') + vi.stubEnv('SHOPIFY_CLI_AGENT_INFO', 'n:cursor|v:2.1.0|p:openai|m:gpt-5') + vi.stubEnv('SHOPIFY_CLI_AGENT_IDS', undefined) + + const got = await shopifyEnvironmentVariables() + + expect(got).toMatchObject({ + SHOPIFY_CLI_AGENT_INFO: 'n:cursor|v:2.1.0|p:openai|m:gpt-5', + SHOPIFY_CLI_AGENT_DETECTION: 'skipped', + }) + }) + + test('does not fail telemetry if determineAgent throws an error, and reports the failure', async () => { + vi.mocked(determineAgent).mockRejectedValue(new Error('EACCES: permission denied')) + stubDeclaredAgentVariablesEmpty() + + const got = await shopifyEnvironmentVariables() + + expect(got).not.toHaveProperty('SHOPIFY_CLI_AGENT_INFO') + expect(got).toMatchObject({SHOPIFY_CLI_AGENT_DETECTION: 'failed'}) + }) + + test('reports none when no agent is detected', async () => { + noAgentDetected() + stubDeclaredAgentVariablesEmpty() + + const got = await shopifyEnvironmentVariables() + + expect(got).toMatchObject({SHOPIFY_CLI_AGENT_DETECTION: 'none'}) + }) +}) diff --git a/packages/cli-kit/src/private/node/analytics.ts b/packages/cli-kit/src/private/node/analytics.ts index 0e0b5df491d..826bd74e637 100644 --- a/packages/cli-kit/src/private/node/analytics.ts +++ b/packages/cli-kit/src/private/node/analytics.ts @@ -1,5 +1,6 @@ import {getLastSeenAuthMethod} from './session.js' import {getAutoUpgradeEnabled} from './conf-store.js' +import {detectedAgentEnvironmentVariables} from './context/agent.js' import {hashString} from '../../public/node/crypto.js' import {getPackageManager, packageManagerFromUserAgent} from '../../public/node/node-package-manager.js' import BaseCommand from '../../public/node/base-command.js' @@ -115,14 +116,16 @@ export async function getEnvironmentData(config: Interfaces.Config): Promise allowedShopifyEnvironmentVariableNames.has(key)), +async function getShopifyEnvironmentVariables(env: NodeJS.ProcessEnv = process.env) { + const declaredVariables = Object.fromEntries( + Object.entries(env).filter(([key]) => allowedShopifyEnvironmentVariableNames.has(key)), ) + + return {...declaredVariables, ...(await detectedAgentEnvironmentVariables(env))} } function getPluginNames(config: Interfaces.Config) { diff --git a/packages/cli-kit/src/private/node/context/agent.test.ts b/packages/cli-kit/src/private/node/context/agent.test.ts new file mode 100644 index 00000000000..177531f7ba5 --- /dev/null +++ b/packages/cli-kit/src/private/node/context/agent.test.ts @@ -0,0 +1,164 @@ +import {detectedAgentEnvironmentVariables} from './agent.js' +import {determineAgent, type KnownAgentNames} from '@vercel/detect-agent' +import {afterEach, describe, expect, test, vi} from 'vitest' + +vi.mock('@vercel/detect-agent') +vi.mock('../../../public/node/output.js') + +function agentDetected(name: string) { + vi.mocked(determineAgent).mockResolvedValue({isAgent: true, agent: {name: name as KnownAgentNames}}) +} + +function noAgentDetected() { + vi.mocked(determineAgent).mockResolvedValue({isAgent: false, agent: undefined}) +} + +const declaredAgentVariableNames = ['SHOPIFY_CLI_AGENT_INFO', 'SHOPIFY_CLI_AGENT_IDS'] + +function stubDeclaredAgentVariablesEmpty() { + declaredAgentVariableNames.forEach((variableName) => vi.stubEnv(variableName, undefined)) +} + +describe('detectedAgentEnvironmentVariables', () => { + afterEach(() => { + vi.unstubAllEnvs() + }) + + test('reports skipped when SHOPIFY_CLI_AGENT_INFO was declared, so a guess cannot clobber it', async () => { + agentDetected('claude') + + const got = await detectedAgentEnvironmentVariables({ + SHOPIFY_CLI_AGENT_INFO: 'n:cursor|v:2.1.0|p:openai|m:gpt-5', + }) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_DETECTION: 'skipped'}) + }) + + test('reports skipped when SHOPIFY_CLI_AGENT_IDS was declared', async () => { + agentDetected('claude') + + const got = await detectedAgentEnvironmentVariables({SHOPIFY_CLI_AGENT_IDS: 's:session-id|r:run-id'}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_DETECTION: 'skipped'}) + }) + + test('leaves a declared SHOPIFY_CLI_AGENT_INFO value untouched', async () => { + agentDetected('claude') + const env = {SHOPIFY_CLI_AGENT_INFO: 'n:cursor|v:2.1.0'} + + await detectedAgentEnvironmentVariables(env) + + expect(env).toEqual({SHOPIFY_CLI_AGENT_INFO: 'n:cursor|v:2.1.0'}) + }) + + test('a legacy declaration does not suppress detection', async () => { + agentDetected('devin') + + const got = await detectedAgentEnvironmentVariables({SHOPIFY_CLI_AGENT: 'declared-by-the-producer'}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_INFO: 'n:devin', SHOPIFY_CLI_AGENT_DETECTION: 'detected'}) + }) + + test.each([ + ['undefined', undefined], + ['empty', ''], + ['whitespace-only', ' '], + ])('treats a %s declaration as absent and reports the detected agent', async (_description, declaredValue) => { + agentDetected('devin') + + const got = await detectedAgentEnvironmentVariables({ + SHOPIFY_CLI_AGENT_INFO: declaredValue, + SHOPIFY_CLI_AGENT_IDS: declaredValue, + }) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_INFO: 'n:devin', SHOPIFY_CLI_AGENT_DETECTION: 'detected'}) + }) + + test('reports the detected agent when nothing was declared', async () => { + agentDetected('devin') + + const got = await detectedAgentEnvironmentVariables({}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_INFO: 'n:devin', SHOPIFY_CLI_AGENT_DETECTION: 'detected'}) + }) + + test('reports none when no agent is detected', async () => { + noAgentDetected() + + const got = await detectedAgentEnvironmentVariables({}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_DETECTION: 'none'}) + }) + + test.each([ + ['claude', 'claude-code'], + ['gemini', 'gemini-cli'], + ])('reports the detected name %s using the toolkit name %s', async (detectedName, expectedName) => { + agentDetected(detectedName) + + const got = await detectedAgentEnvironmentVariables({}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_INFO: `n:${expectedName}`, SHOPIFY_CLI_AGENT_DETECTION: 'detected'}) + }) + + test.each([ + ['a known name needing no translation', 'cursor'], + ['an arbitrary AI_AGENT pass-through value', 'claude-code_2-1-267_agent'], + ])('reports %s verbatim', async (_description, detectedName) => { + agentDetected(detectedName) + + const got = await detectedAgentEnvironmentVariables({}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_INFO: `n:${detectedName}`, SHOPIFY_CLI_AGENT_DETECTION: 'detected'}) + }) + + test.each([['constructor'], ['toString'], ['__proto__']])( + 'reports the inherited object property name %s verbatim', + async (detectedName) => { + agentDetected(detectedName) + + const got = await detectedAgentEnvironmentVariables({}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_INFO: `n:${detectedName}`, SHOPIFY_CLI_AGENT_DETECTION: 'detected'}) + }, + ) + + test('replaces the tag separator in the detected name so it cannot inject tags', async () => { + agentDetected('devin|v:9.9.9') + + const got = await detectedAgentEnvironmentVariables({}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_INFO: 'n:devin_v:9.9.9', SHOPIFY_CLI_AGENT_DETECTION: 'detected'}) + }) + + test.each([ + ['whitespace-only', ' '], + ['nothing but the tag separator', '|'], + ['nothing but tag separators', '||'], + ])('reports unusable_name when the detected name is %s', async (_description, detectedName) => { + agentDetected(detectedName) + + const got = await detectedAgentEnvironmentVariables({}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_DETECTION: 'unusable_name'}) + }) + + test('reports failed when detection fails', async () => { + vi.mocked(determineAgent).mockRejectedValue(new Error('EACCES: permission denied, stat /opt/.devin')) + + const got = await detectedAgentEnvironmentVariables({}) + + expect(got).toEqual({SHOPIFY_CLI_AGENT_DETECTION: 'failed'}) + }) + + test('does not mutate process.env when reporting a detected agent', async () => { + agentDetected('claude') + stubDeclaredAgentVariablesEmpty() + const environmentBefore = {...process.env} + + const got = await detectedAgentEnvironmentVariables() + + expect(got).toEqual({SHOPIFY_CLI_AGENT_INFO: 'n:claude-code', SHOPIFY_CLI_AGENT_DETECTION: 'detected'}) + expect({...process.env}).toEqual(environmentBefore) + }) +}) diff --git a/packages/cli-kit/src/private/node/context/agent.ts b/packages/cli-kit/src/private/node/context/agent.ts new file mode 100644 index 00000000000..2f4153af890 --- /dev/null +++ b/packages/cli-kit/src/private/node/context/agent.ts @@ -0,0 +1,53 @@ +import {outputDebug} from '../../../public/node/output.js' +import {determineAgent, KNOWN_AGENTS} from '@vercel/detect-agent' + +const toolkitAgentNamesByDetectedName = new Map([ + [KNOWN_AGENTS.CLAUDE, 'claude-code'], + [KNOWN_AGENTS.GEMINI, 'gemini-cli'], +]) + +const explicitAgentVariableNames = ['SHOPIFY_CLI_AGENT_INFO', 'SHOPIFY_CLI_AGENT_IDS'] + +function hasExplicitAgentAttribution(env: NodeJS.ProcessEnv): boolean { + return explicitAgentVariableNames.some((variableName) => (env[variableName] ?? '').trim() !== '') +} + +type AgentDetectionState = 'skipped' | 'detected' | 'none' | 'unusable_name' | 'failed' + +function agentDetectionResult(state: AgentDetectionState, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { + return {...extra, SHOPIFY_CLI_AGENT_DETECTION: state} +} + +export async function detectedAgentEnvironmentVariables( + env: NodeJS.ProcessEnv = process.env, +): Promise { + // Emitting while a declaration exists would clobber the producer's whole packed value, not just the name. + if (hasExplicitAgentAttribution(env)) return agentDetectionResult('skipped') + + try { + const detection = await determineAgent() + if (!detection.isAgent) return agentDetectionResult('none') + + const rawDetectedName = detection.agent.name + const nameHasContentBeyondSeparators = /[^|\s]/.test(rawDetectedName) + // The name detection produced was real but unusable (nothing but separators/whitespace), which + // is different from no agent being detected at all, so it gets its own state. + if (!nameHasContentBeyondSeparators) return agentDetectionResult('unusable_name') + + // `|` separates tags, so a name containing one could otherwise fabricate tags we never detected. + const detectedName = rawDetectedName.replaceAll('|', '_').trim() + + return agentDetectionResult('detected', { + SHOPIFY_CLI_AGENT_INFO: `n:${toolkitAgentNamesByDetectedName.get(detectedName) ?? detectedName}`, + }) + + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (error) { + let message = 'Unable to detect which AI agent is running the CLI' + if (error instanceof Error) { + message = message.concat(`: ${error.message}`) + } + outputDebug(message) + return agentDetectionResult('failed') + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 669d33d5bf2..4c68ae7959f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -352,6 +352,9 @@ importers: '@shopify/toml-patch': specifier: 0.3.0 version: 0.3.0 + '@vercel/detect-agent': + specifier: 1.2.5 + version: 1.2.5 ajv: specifier: 8.20.0 version: 8.20.0 @@ -853,48 +856,56 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-arm64-gnu@0.43.0': resolution: {integrity: sha512-yJSRPxwwrvVW94J2rtaatcixSAGWcSaHNbAh6soXD6HXgq6I7uMc+cyMnJstFL789yd6Pu3QIhTlpD9VY0oYhw==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-arm64-gnu/-/napi-linux-arm64-gnu-0.43.0.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.34.1': resolution: {integrity: sha512-IXdqwTbkdqHrcuQb448Qzd82QdTqVFe/f0sSkFYQTic8P2qNzmiHsVnxgEFsQPPbe09BVAoZ885j3OnaNfcDYA==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-arm64-musl/-/napi-linux-arm64-musl-0.34.1.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@ast-grep/napi-linux-arm64-musl@0.43.0': resolution: {integrity: sha512-mknXLDsf66HvT/JEl18ZQSvR7/qWgWfqh3eHuVRqD2lE6cKBDXRnAzx9ZNZkUnL2Z5ph54Yk8dKVu09k45cegA==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-arm64-musl/-/napi-linux-arm64-musl-0.43.0.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.34.1': resolution: {integrity: sha512-on4LyIeN/zN7SIh8zr5v+NTzVu3kXm2mG28ib1Qe9GVcf35dz52ckf7bilulayKSa2MHZWAXMjuc6NYMiNEw+w==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-x64-gnu/-/napi-linux-x64-gnu-0.34.1.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-x64-gnu@0.43.0': resolution: {integrity: sha512-KM6M5KKFsHG9Y7VKCKnMsWQQ1sYwj/SPdyr91SKp66AeFJ5xMtXb13WVQ3Joe9NEsi84dzzOBIJgMddz+UMvQw==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-x64-gnu/-/napi-linux-x64-gnu-0.43.0.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.34.1': resolution: {integrity: sha512-l1R5L9LOp0jTPjs8C+LUndZOA8cRw7PFlvoVxVbi2jCfcns00dqatSYc4yA/ke6ng2K0LSxjoV/jS8tefve0sA==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-x64-musl/-/napi-linux-x64-musl-0.34.1.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@ast-grep/napi-linux-x64-musl@0.43.0': resolution: {integrity: sha512-NfjI74m7CEEOsLi7ZkYwcxY10CfKIHPHRrr9aqMulmnrC4FGvxQMk1qpDrTqkjmEd8LdZt3PsPrmBa8AZCErew==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-x64-musl/-/napi-linux-x64-musl-0.43.0.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.34.1': resolution: {integrity: sha512-eVsdMtnY7jmN2xQjYY9gaqIxRHA44+QYivlP1uLbg8w3P4YlZWTFgOJ7aa357Hg/257mjeQCpodCkr0lGRsSYQ==, tarball: https://registry.npmjs.org/@ast-grep/napi-win32-arm64-msvc/-/napi-win32-arm64-msvc-0.34.1.tgz} @@ -2998,21 +3009,25 @@ packages: resolution: {integrity: sha512-CfzWaS+b32lI/inlYPv1ZAK9CWTWFHzT7TPThvOHML65nrgFl8cQ4Z4FeGdyCbHu2NoG3vR1E36O2tPI2/DLGg==, tarball: https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.7.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@nx/nx-linux-arm64-musl@22.7.7': resolution: {integrity: sha512-53QFuODMEHc1gobCT2WOmYz89DZtEIuLphirdIgnXkkPBTdrP77LPbJUXMRXCMKr90BQaSWhTX+J/ubANRE8og==, tarball: https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.7.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@nx/nx-linux-x64-gnu@22.7.7': resolution: {integrity: sha512-unzmqGIDXGzujo1ZtbrMj2e5D5ldQ1feZmqDPhvO4casK2d96jpT8LtgIILpVDUfhLjl+By185gb0ArrWTsyKw==, tarball: https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.7.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@nx/nx-linux-x64-musl@22.7.7': resolution: {integrity: sha512-oTjMGuM105ok8aH5rlg2YP0Kgkjg0VfUj1exwp49S70gGBJ0r8v5rEtJwOSdCf1WTHuEh0xi66Y/b1S0khF8dg==, tarball: https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.7.tgz} cpu: [x64] os: [linux] + libc: [musl] '@nx/nx-win32-arm64-msvc@22.7.7': resolution: {integrity: sha512-K741meG/l48TPPeDqnhYTV7pP+1ljjZ+0DRJBxMrPFIu8qKE3Abko/7NKWmWRjcSXJvtxvYkgG3wZds4N2Bhow==, tarball: https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.7.7.tgz} @@ -3318,41 +3333,49 @@ packages: resolution: {integrity: sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.20.0.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.20.0': resolution: {integrity: sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.20.0.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.20.0': resolution: {integrity: sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.20.0.tgz} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.20.0': resolution: {integrity: sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.20.0.tgz} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.20.0': resolution: {integrity: sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.20.0.tgz} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.20.0': resolution: {integrity: sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.20.0.tgz} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.20.0': resolution: {integrity: sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.20.0.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.20.0': resolution: {integrity: sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.20.0.tgz} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.20.0': resolution: {integrity: sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.20.0.tgz} @@ -3403,36 +3426,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==, tarball: https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz} @@ -3556,66 +3585,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==, tarball: https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz} @@ -4124,41 +4166,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz} @@ -4180,6 +4230,10 @@ packages: cpu: [x64] os: [win32] + '@vercel/detect-agent@1.2.5': + resolution: {integrity: sha512-0krENrjuitlW8s6TJu0MlqCevyCU7K7JK63jZAf7xZ6n17tx+vUEwzHT3sTxawtwZxaW21hu+oFUpoOrm49FsQ==, tarball: https://registry.npmjs.org/@vercel/detect-agent/-/detect-agent-1.2.5.tgz} + engines: {node: '>=14'} + '@vitejs/plugin-react@5.2.0': resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==, tarball: https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} @@ -13057,6 +13111,8 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vercel/detect-agent@1.2.5': {} + '@vitejs/plugin-react@5.2.0(vite@6.4.3(@types/node@26.1.1)(jiti@2.6.1)(sass@1.100.0)(tsx@4.23.1)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0