Organize ts libs, add global workspace

This commit is contained in:
Morgan Dean
2025-06-23 16:05:47 -07:00
parent 0246d18347
commit adbf3f378c
19 changed files with 1324 additions and 4795 deletions

View File

@@ -1,13 +0,0 @@
{
"folders": [
{
"name": "computer-ts",
"path": "../libs/typescript/computer"
}
],
"extensions": {
"recommendations": [
"biomejs.biome",
]
}
}

View File

@@ -1,8 +1,8 @@
{
"folders": [
{
"name": "core-ts",
"path": "../libs/typescript/core"
"name": "libs-ts",
"path": "../libs/typescript"
}
],
"extensions": {

5
libs/typescript/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
*.log
.DS_Store
.eslintcache

View File

@@ -38,6 +38,7 @@
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"@cua/core": "link:../core",
"pino": "^9.7.0",
"ws": "^8.18.0"
},

View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@cua/core':
specifier: link:../core
version: link:../core
pino:
specifier: ^9.7.0
version: 9.7.0

View File

@@ -1,3 +0,0 @@
onlyBuiltDependencies:
- "@biomejs/biome"
- sharp

View File

@@ -1,3 +1,4 @@
import { Telemetry } from "@cua/core";
import type { OSType } from "../../types";
import type { BaseComputerConfig, Display, VMProviderType } from "../types";
import pino from "pino";
@@ -11,10 +12,18 @@ export abstract class BaseComputer {
protected name: string;
protected osType: OSType;
protected vmProvider?: VMProviderType;
protected telemetry: Telemetry
constructor(config: BaseComputerConfig) {
this.name = config.name;
this.osType = config.osType;
this.telemetry = new Telemetry();
this.telemetry.recordEvent('module_init', {
module: "computer",
version: process.env.npm_package_version,
node_version: process.version,
},
)
}
/**

View File

@@ -1,86 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": [
"dist",
"node_modules"
]
},
"formatter": {
"enabled": true,
"useEditorconfig": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 80,
"attributePosition": "auto",
"bracketSpacing": true
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"useSelfClosingElements": "warn",
"noUnusedTemplateLiteral": "warn",
"noNonNullAssertion": "off"
},
"a11y": {
"useMediaCaption": "off",
"useKeyWithClickEvents": "warn",
"useKeyWithMouseEvents": "warn",
"noSvgWithoutTitle": "off",
"useButtonType": "warn",
"noAutofocus": "off"
},
"suspicious": {
"noArrayIndexKey": "off"
},
"correctness": {
"noUnusedVariables": "warn",
"noUnusedFunctionParameters": "warn",
"noUnusedImports": "warn"
},
"complexity": {
"useOptionalChain": "info"
},
"nursery": {
"useSortedClasses": {
"level": "warn",
"fix": "safe",
"options": {
"attributes": [
"className"
],
"functions": [
"cn"
]
}
}
}
}
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto",
"bracketSpacing": true
}
}
}

View File

@@ -38,7 +38,10 @@
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"pino": "^9.7.0"
"@types/uuid": "^10.0.0",
"pino": "^9.7.0",
"posthog-node": "^5.1.1",
"uuid": "^11.1.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,7 @@
export const myFunction = () => {
return 'Hello, world!'
}
/**
* This module provides the core telemetry functionality for CUA libraries.
*
* It provides a low-overhead way to collect anonymous usage data.
*/
export * from './telemetry';

View File

@@ -0,0 +1 @@
export * from "./posthog";

View File

@@ -0,0 +1,309 @@
/**
* Telemetry client using PostHog for collecting anonymous usage data.
*/
import { PostHog } from 'posthog-node';
import { v4 as uuidv4 } from 'uuid';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { pino } from 'pino';
const logger = pino({ name: 'core.telemetry' });
// Controls how frequently telemetry will be sent (percentage)
export const TELEMETRY_SAMPLE_RATE = 100; // 100% sampling rate
// Public PostHog config for anonymous telemetry
// These values are intentionally public and meant for anonymous telemetry only
// https://posthog.com/docs/product-analytics/troubleshooting#is-it-ok-for-my-api-key-to-be-exposed-and-public
export const PUBLIC_POSTHOG_API_KEY =
'phc_eSkLnbLxsnYFaXksif1ksbrNzYlJShr35miFLDppF14';
export const PUBLIC_POSTHOG_HOST = 'https://eu.i.posthog.com';
export class PostHogTelemetryClient {
private config: {
enabled: boolean;
sampleRate: number;
posthog: { apiKey: string; host: string };
};
private installationId: string;
private initialized = false;
private queuedEvents: {
name: string;
properties: Record<string, unknown>;
timestamp: number;
}[] = [];
private startTime: number; // seconds
private posthogClient?: PostHog;
private counters: Record<string, number> = {};
constructor() {
// set up config
this.config = {
enabled: true,
sampleRate: TELEMETRY_SAMPLE_RATE,
posthog: { apiKey: PUBLIC_POSTHOG_API_KEY, host: PUBLIC_POSTHOG_HOST },
};
// Check for multiple environment variables that can disable telemetry:
// CUA_TELEMETRY=off to disable telemetry (legacy way)
// CUA_TELEMETRY_DISABLED=1 to disable telemetry (new, more explicit way)
const telemetryDisabled =
process.env.CUA_TELEMETRY?.toLowerCase() === 'off' ||
['1', 'true', 'yes', 'on'].includes(
process.env.CUA_TELEMETRY_DISABLED?.toLowerCase() || ''
);
this.config.enabled = !telemetryDisabled;
this.config.sampleRate = Number.parseFloat(
process.env.CUA_TELEMETRY_SAMPLE_RATE || String(TELEMETRY_SAMPLE_RATE)
);
// init client
this.installationId = this._getOrCreateInstallationId();
this.startTime = Date.now() / 1000; // Convert to seconds
// Log telemetry status on startup
if (this.config.enabled) {
logger.info(`Telemetry enabled (sampling at ${this.config.sampleRate}%)`);
// Initialize PostHog client if config is available
this._initializePosthog();
} else {
logger.info('Telemetry disabled');
}
}
/**
* Get or create a random installation ID.
* This ID is not tied to any personal information.
*/
private _getOrCreateInstallationId(): string {
const homeDir = os.homedir();
const idFile = path.join(homeDir, '.cua', 'installation_id');
try {
if (fs.existsSync(idFile)) {
return fs.readFileSync(idFile, 'utf-8').trim();
}
} catch (error) {
logger.debug(`Failed to read installation ID: ${error}`);
}
// Create new ID if not exists
const newId = uuidv4();
try {
const dir = path.dirname(idFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(idFile, newId);
return newId;
} catch (error) {
logger.debug(`Failed to write installation ID: ${error}`);
}
// Fallback to in-memory ID if file operations fail
return newId;
}
/**
* Initialize the PostHog client with configuration.
*/
private _initializePosthog(): boolean {
if (this.initialized) {
return true;
}
try {
this.posthogClient = new PostHog(this.config.posthog.apiKey, {
host: this.config.posthog.host,
flushAt: 20, // Number of events to batch before sending
flushInterval: 30000, // Send events every 30 seconds
});
this.initialized = true;
logger.debug('PostHog client initialized successfully');
// Process any queued events
this._processQueuedEvents();
return true;
} catch (error) {
logger.error(`Failed to initialize PostHog client: ${error}`);
return false;
}
}
/**
* Process any events that were queued before initialization.
*/
private _processQueuedEvents(): void {
if (!this.posthogClient || this.queuedEvents.length === 0) {
return;
}
for (const event of this.queuedEvents) {
this._captureEvent(event.name, event.properties);
}
this.queuedEvents = [];
}
/**
* Capture an event with PostHog.
*/
private _captureEvent(
eventName: string,
properties?: Record<string, unknown>
): void {
if (!this.posthogClient) {
return;
}
try {
// Add standard properties
const eventProperties = {
...properties,
version: process.env.npm_package_version || 'unknown',
platform: process.platform,
node_version: process.version,
is_ci: this._isCI,
};
this.posthogClient.capture({
distinctId: this.installationId,
event: eventName,
properties: eventProperties,
});
} catch (error) {
logger.debug(`Failed to capture event: ${error}`);
}
}
private get _isCI(): boolean {
/**
* Detect if running in CI environment.
*/
return !!(
process.env.CI ||
process.env.CONTINUOUS_INTEGRATION ||
process.env.GITHUB_ACTIONS ||
process.env.GITLAB_CI ||
process.env.CIRCLECI ||
process.env.TRAVIS ||
process.env.JENKINS_URL
);
}
increment(counterName: string, value = 1) {
/**
* Increment a named counter.
*/
if (!this.config.enabled) {
return;
}
if (!(counterName in this.counters)) {
this.counters[counterName] = 0;
}
this.counters[counterName] += value;
}
recordEvent(eventName: string, properties?: Record<string, unknown>): void {
/**
* Record an event with optional properties.
*/
if (!this.config.enabled) {
return;
}
// Increment counter for this event type
const counterKey = `event:${eventName}`;
this.increment(counterKey);
// Apply sampling
if (Math.random() * 100 > this.config.sampleRate) {
return;
}
const event = {
name: eventName,
properties: properties || {},
timestamp: Date.now() / 1000,
};
if (this.initialized && this.posthogClient) {
this._captureEvent(eventName, properties);
} else {
// Queue event if not initialized
this.queuedEvents.push(event);
// Try to initialize again
if (this.config.enabled && !this.initialized) {
this._initializePosthog();
}
}
}
/**
* Flush any pending events to PostHog.
*/
async flush(): Promise<boolean> {
if (!this.config.enabled || !this.posthogClient) {
return false;
}
try {
// Send counter data as a single event
if (Object.keys(this.counters).length > 0) {
this._captureEvent('telemetry_counters', {
counters: { ...this.counters },
duration: Date.now() / 1000 - this.startTime,
});
}
await this.posthogClient.flush();
logger.debug('Telemetry flushed successfully');
// Clear counters after sending
this.counters = {};
return true;
} catch (error) {
logger.debug(`Failed to flush telemetry: ${error}`);
return false;
}
}
enable(): void {
/**
* Enable telemetry collection.
*/
this.config.enabled = true;
logger.info('Telemetry enabled');
if (!this.initialized) {
this._initializePosthog();
}
}
async disable(): Promise<void> {
/**
* Disable telemetry collection.
*/
this.config.enabled = false;
await this.posthogClient?.disable();
logger.info('Telemetry disabled');
}
get enabled(): boolean {
/**
* Check if telemetry is enabled.
*/
return this.config.enabled;
}
async shutdown(): Promise<void> {
/**
* Shutdown the telemetry client and flush any pending events.
*/
if (this.posthogClient) {
await this.flush();
await this.posthogClient.shutdown();
this.initialized = false;
this.posthogClient = undefined;
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* This module provides the core telemetry functionality for CUA libraries.
*
* It provides a low-overhead way to collect anonymous usage data.
*/
export { PostHogTelemetryClient as Telemetry } from './clients';

View File

@@ -1,6 +0,0 @@
import { expect, test } from 'vitest'
import { myFunction } from '../src'
test('myFunction', () => {
expect(myFunction()).toBe('Hello, world!')
})

View File

@@ -0,0 +1,30 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { Telemetry } from '../src/';
describe('Telemetry', () => {
let telemetry: Telemetry;
beforeEach(() => {
process.env.CUA_TELEMETRY = '';
process.env.CUA_TELEMETRY_DISABLED = '';
telemetry = new Telemetry();
});
describe('telemetry.enabled', () => {
it('should return false when CUA_TELEMETRY is off', () => {
process.env.CUA_TELEMETRY = 'off';
telemetry = new Telemetry();
expect(telemetry.enabled).toBe(false);
});
it('should return true when CUA_TELEMETRY is not set', () => {
process.env.CUA_TELEMETRY = '';
telemetry = new Telemetry();
expect(telemetry.enabled).toBe(true);
});
it('should return false if CUA_TELEMETRY_DISABLED is 1', () => {
process.env.CUA_TELEMETRY_DISABLED = '1';
telemetry = new Telemetry();
expect(telemetry.enabled).toBe(false);
});
});
});

View File

@@ -0,0 +1,21 @@
{
"name": "cua-ts",
"version": "1.0.0",
"description": "The c/ua typescript libs.",
"keywords": [],
"author": "c/ua",
"license": "MIT",
"scripts": {
"lint": "biome lint .",
"lint:fix": "biome lint --fix ."
},
"packageManager": "pnpm@10.6.5",
"devDependencies": {
"@biomejs/biome": "^1.9.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"@biomejs/biome"
]
}
}

105
libs/typescript/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,105 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@biomejs/biome':
specifier: ^1.9.4
version: 1.9.4
packages:
'@biomejs/biome@1.9.4':
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@1.9.4':
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@1.9.4':
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.9.4':
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@1.9.4':
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
snapshots:
'@biomejs/biome@1.9.4':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.9.4
'@biomejs/cli-darwin-x64': 1.9.4
'@biomejs/cli-linux-arm64': 1.9.4
'@biomejs/cli-linux-arm64-musl': 1.9.4
'@biomejs/cli-linux-x64': 1.9.4
'@biomejs/cli-linux-x64-musl': 1.9.4
'@biomejs/cli-win32-arm64': 1.9.4
'@biomejs/cli-win32-x64': 1.9.4
'@biomejs/cli-darwin-arm64@1.9.4':
optional: true
'@biomejs/cli-darwin-x64@1.9.4':
optional: true
'@biomejs/cli-linux-arm64-musl@1.9.4':
optional: true
'@biomejs/cli-linux-arm64@1.9.4':
optional: true
'@biomejs/cli-linux-x64-musl@1.9.4':
optional: true
'@biomejs/cli-linux-x64@1.9.4':
optional: true
'@biomejs/cli-win32-arm64@1.9.4':
optional: true
'@biomejs/cli-win32-x64@1.9.4':
optional: true