@telemetryos/cli 1.4.4 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @telemetryos/cli
2
2
 
3
+ ## 1.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Update cli enforce correct qs params
8
+ - Updated dependencies
9
+ - @telemetryos/development-application-host-ui@1.5.1
10
+
11
+ ## 1.5.0
12
+
13
+ ### Minor Changes
14
+
15
+ - Add message interceptors for root applications.
16
+
17
+ This allows root applications to listen for messages from their descendant
18
+ applications, and implement custom functionality those applications
19
+ can use, or allow for interop.
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies
24
+ - @telemetryos/development-application-host-ui@1.5.0
25
+
3
26
  ## 1.4.4
4
27
 
5
28
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const authCommand: Command;
@@ -0,0 +1,67 @@
1
+ import { Command } from 'commander';
2
+ import inquirer from 'inquirer';
3
+ import { loadConfig, saveConfig } from '../services/config.js';
4
+ export const authCommand = new Command('auth')
5
+ .description('Authenticate with TelemetryOS by saving your API token')
6
+ .option('-t, --token <string>', 'API token (skip interactive prompt)')
7
+ .option('-f, --force', 'Replace existing token without confirmation')
8
+ .action(handleAuthCommand);
9
+ function maskToken(token) {
10
+ if (token.length <= 8)
11
+ return '****';
12
+ return `${token.slice(0, 4)}...${token.slice(-4)}`;
13
+ }
14
+ function validateToken(input) {
15
+ if (!input || input.trim().length === 0) {
16
+ return 'Token cannot be empty';
17
+ }
18
+ const trimmed = input.trim();
19
+ if (!/^[a-f0-9-]+$/i.test(trimmed)) {
20
+ return 'Token must be a valid string';
21
+ }
22
+ if (trimmed.length < 24) {
23
+ return 'Token must be at least 24 characters';
24
+ }
25
+ return true;
26
+ }
27
+ async function handleAuthCommand(options) {
28
+ let token = options.token;
29
+ const existingConfig = await loadConfig();
30
+ const existingToken = existingConfig === null || existingConfig === void 0 ? void 0 : existingConfig.apiToken;
31
+ if (existingToken && !options.force) {
32
+ const { confirm } = await inquirer.prompt([
33
+ {
34
+ type: 'confirm',
35
+ name: 'confirm',
36
+ message: `A token already exists (${maskToken(existingToken)}). Do you want to replace it?`,
37
+ default: false,
38
+ },
39
+ ]);
40
+ if (!confirm) {
41
+ console.log('Authentication cancelled.');
42
+ return;
43
+ }
44
+ }
45
+ if (token) {
46
+ const validation = validateToken(token);
47
+ if (validation !== true) {
48
+ console.error(validation);
49
+ process.exit(1);
50
+ }
51
+ token = token.trim();
52
+ }
53
+ else {
54
+ console.log('In Studio, go to Settings > API Tokens to get your API token.\n');
55
+ const answers = await inquirer.prompt([
56
+ {
57
+ type: 'input',
58
+ name: 'token',
59
+ message: 'Paste your API token:',
60
+ validate: validateToken,
61
+ },
62
+ ]);
63
+ token = answers.token.trim();
64
+ }
65
+ await saveConfig({ ...existingConfig, apiToken: token });
66
+ console.log('API token saved successfully!');
67
+ }
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { generateApplication } from '../generate-application.js';
2
+ import { generateApplication } from '../services/generate-application.js';
3
3
  import inquirer from 'inquirer';
4
4
  import path from 'path';
5
5
  export const initCommand = new Command('init')
@@ -22,21 +22,21 @@ async function handleInitCommand(projectName, options) {
22
22
  type: 'input',
23
23
  name: 'name',
24
24
  message: 'What is the name of your application?',
25
- validate: (input) => input.length > 0 || 'Application name cannot be empty'
25
+ validate: (input) => input.length > 0 || 'Application name cannot be empty',
26
26
  });
27
27
  if (!description)
28
28
  questions.push({
29
29
  type: 'input',
30
30
  name: 'description',
31
31
  message: 'What is the description of your application?',
32
- default: 'A telemetryOS application'
32
+ default: 'A telemetryOS application',
33
33
  });
34
34
  if (!author)
35
35
  questions.push({
36
36
  type: 'input',
37
37
  name: 'author',
38
38
  message: 'Who is the author of your application?',
39
- default: ''
39
+ default: '',
40
40
  });
41
41
  if (!version)
42
42
  questions.push({
@@ -44,16 +44,14 @@ async function handleInitCommand(projectName, options) {
44
44
  name: 'version',
45
45
  message: 'What is the version of your application?',
46
46
  default: '0.1.0',
47
- validate: (input) => /^\d+\.\d+\.\d+(-.+)?$/.test(input) || 'Version must be in semver format (e.g. 1.0.0)'
47
+ validate: (input) => /^\d+\.\d+\.\d+(-.+)?$/.test(input) || 'Version must be in semver format (e.g. 1.0.0)',
48
48
  });
49
49
  if (!template)
50
50
  questions.push({
51
51
  type: 'list',
52
52
  name: 'template',
53
53
  message: 'Which template would you like to use?',
54
- choices: [
55
- { name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }
56
- ]
54
+ choices: [{ name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }],
57
55
  });
58
56
  if (questions.length !== 0) {
59
57
  const answers = await inquirer.prompt(questions);
@@ -78,6 +76,6 @@ async function handleInitCommand(projectName, options) {
78
76
  projectPath,
79
77
  progressFn: (createdFilePath) => {
80
78
  console.log(`.${path.sep}${path.relative(process.cwd(), createdFilePath)}`);
81
- }
79
+ },
82
80
  });
83
81
  }
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { runServer } from '../run-server.js';
2
+ import { runServer } from '../services/run-server.js';
3
3
  export const serveCommand = new Command('serve')
4
4
  .description('Serves a telemetryOS application locally for development')
5
5
  .option('-p, --port <number>', 'Port to run the development ui on', '6969')
@@ -1,63 +1,27 @@
1
- import { spawn } from 'child_process';
2
- import fs from 'fs/promises';
3
- import path from 'path';
4
- const ignoredTemplateFiles = ['.DS_Store', 'thumbs.db', 'node_modules', '.git', 'dist'];
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ const ignoredTelemplateFiles = [
4
+ '.DS_Store',
5
+ 'thumbs.db',
6
+ 'node_modules',
7
+ '.git',
8
+ 'dist'
9
+ ];
5
10
  const templatesDir = path.join(import.meta.dirname, '../templates');
6
11
  export async function generateApplication(options) {
7
12
  const { name, description, author, version, template, projectPath, progressFn } = options;
8
13
  await fs.mkdir(projectPath, { recursive: true });
9
14
  await copyDir(path.join(templatesDir, template), projectPath, {
10
15
  name,
11
- version,
12
16
  description,
13
17
  author,
18
+ version
14
19
  }, progressFn);
15
- // Install latest versions of @telemetryos/sdk and @telemetryos/cli
16
- console.log('\nInstalling dependencies...');
17
- await installPackages(projectPath);
18
- }
19
- async function installPackages(projectPath) {
20
- // Install SDK as a regular dependency
21
- await new Promise((resolve, reject) => {
22
- const sdkInstall = spawn('npm', ['install', '@telemetryos/sdk'], {
23
- cwd: projectPath,
24
- stdio: 'inherit',
25
- });
26
- sdkInstall.on('close', (code) => {
27
- if (code === 0) {
28
- resolve();
29
- }
30
- else {
31
- reject(new Error(`npm install @telemetryos/sdk failed with code ${code}`));
32
- }
33
- });
34
- sdkInstall.on('error', (error) => {
35
- reject(error);
36
- });
37
- });
38
- // Install CLI as a dev dependency
39
- await new Promise((resolve, reject) => {
40
- const cliInstall = spawn('npm', ['install', '-D', '@telemetryos/cli'], {
41
- cwd: projectPath,
42
- stdio: 'inherit',
43
- });
44
- cliInstall.on('close', (code) => {
45
- if (code === 0) {
46
- resolve();
47
- }
48
- else {
49
- reject(new Error(`npm install -D @telemetryos/cli failed with code ${code}`));
50
- }
51
- });
52
- cliInstall.on('error', (error) => {
53
- reject(error);
54
- });
55
- });
56
20
  }
57
21
  async function copyDir(source, destination, replacements, progressFn) {
58
22
  const dirListing = await fs.readdir(source);
59
23
  for (const dirEntry of dirListing) {
60
- if (ignoredTemplateFiles.includes(dirEntry))
24
+ if (ignoredTelemplateFiles.includes(dirEntry))
61
25
  continue;
62
26
  const sourcePath = path.join(source, dirEntry);
63
27
  const destinationPath = path.join(destination, dirEntry);
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { authCommand } from './commands/auth.js';
2
3
  import { initCommand } from './commands/init.js';
3
4
  import { rootCommand } from './commands/root.js';
4
5
  import { serveCommand } from './commands/serve.js';
6
+ rootCommand.addCommand(authCommand);
5
7
  rootCommand.addCommand(serveCommand);
6
8
  rootCommand.addCommand(initCommand);
7
9
  rootCommand.parse(process.argv);
@@ -4,35 +4,10 @@ import http from 'http';
4
4
  import path from 'path';
5
5
  import readable from 'readline/promises';
6
6
  import serveHandler from 'serve-handler';
7
- import z from 'zod';
8
- const ansiWhite = '\u001b[37m';
9
7
  const ansiYellow = '\u001b[33m';
10
8
  const ansiCyan = '\u001b[36m';
11
9
  const ansiBold = '\u001b[1m';
12
10
  const ansiReset = '\u001b[0m';
13
- const mountPointSchema = z.object({
14
- path: z.string(),
15
- });
16
- const workerScriptSchema = z.object({
17
- path: z.string(),
18
- });
19
- const containerSchema = z.object({
20
- image: z.string(),
21
- });
22
- const configSchema = z.object({
23
- name: z.string().optional(),
24
- version: z.string().optional(),
25
- thumbnailPath: z.string().optional(),
26
- mountPoints: z.record(z.string(), z.union([mountPointSchema, z.string()])).optional(),
27
- workerScripts: z.record(z.string(), z.union([workerScriptSchema, z.string()])).optional(),
28
- containers: z.record(z.string(), z.union([containerSchema, z.string()])).optional(),
29
- devServer: z
30
- .object({
31
- runCommand: z.string().optional(),
32
- url: z.string(),
33
- })
34
- .optional(),
35
- });
36
11
  export async function runServer(projectPath, flags) {
37
12
  printSplashScreen();
38
13
  projectPath = path.resolve(process.cwd(), projectPath);
@@ -41,17 +16,8 @@ export async function runServer(projectPath, flags) {
41
16
  console.error('No telemetry configuration found. Are you in the right directory?');
42
17
  process.exit(1);
43
18
  }
44
- const validationResult = configSchema.safeParse(telemetryConfig);
45
- if (!validationResult.success) {
46
- console.error('Invalid telemetry.config.json:');
47
- validationResult.error.issues.forEach((issue) => {
48
- console.error(` ${issue.path.join('.')}: ${issue.message}`);
49
- });
50
- console.error('\n');
51
- process.exit(1);
52
- }
53
- await serveDevelopmentApplicationHostUI(flags.port, validationResult.data);
54
- await serveTelemetryApplication(projectPath, validationResult.data);
19
+ await serveDevelopmentApplicationHostUI(flags.port, telemetryConfig);
20
+ await serveTelemetryApplication(projectPath, telemetryConfig);
55
21
  }
56
22
  async function serveDevelopmentApplicationHostUI(port, telemetryConfig) {
57
23
  const hostUiPath = await import.meta.resolve('@telemetryos/development-application-host-ui/dist');
@@ -75,10 +41,8 @@ async function serveDevelopmentApplicationHostUI(port, telemetryConfig) {
75
41
  }
76
42
  async function serveTelemetryApplication(rootPath, telemetryConfig) {
77
43
  var _a;
78
- if (!((_a = telemetryConfig === null || telemetryConfig === void 0 ? void 0 : telemetryConfig.devServer) === null || _a === void 0 ? void 0 : _a.runCommand)) {
79
- console.log('No value in config at devServer.runCommand');
44
+ if (!((_a = telemetryConfig === null || telemetryConfig === void 0 ? void 0 : telemetryConfig.devServer) === null || _a === void 0 ? void 0 : _a.runCommand))
80
45
  return;
81
- }
82
46
  const runCommand = telemetryConfig.devServer.runCommand;
83
47
  const binPath = path.join(rootPath, 'node_modules', '.bin');
84
48
  const childProcess = spawn(runCommand, {
@@ -117,14 +81,19 @@ async function loadConfigFile(rootPath) {
117
81
  }
118
82
  }
119
83
  function printSplashScreen() {
120
- console.log(`
121
-
122
- ${ansiWhite} █ █ ${ansiYellow}▄▀▀▀▄ ▄▀▀▀▄
123
- ${ansiWhite} █ █ ${ansiYellow}█ █
124
- ${ansiWhite}▀█▀ ▄▀▀▄ ▄▀▀▄ █▀▄▀▄ ▄▀▀▄ ▀█▀ █▄▀ █ █ ${ansiYellow}█ ▀▀▀▄
125
- ${ansiWhite} █ █▀▀▀ █▀▀▀ █▀▀▀ █ █ █ ${ansiYellow}█ █
126
- ${ansiWhite} ▀▄ ▀▄▄▀▀▄▄▀ █ █ ▀▄▄▀ ▀▄█ ${ansiYellow}▀▄▄▄▀ ▀▄▄▄▀
127
- ${ansiWhite} ▄▀ ${ansiReset}`);
84
+ console.log(`${ansiYellow}
85
+ ▄ ▄
86
+ ▄▀▐
87
+ █ █ ▄▀ ▐
88
+ █ █ █ █ █ █ ▄▀
89
+
90
+ ▀█▀ ▄▀▀▄▄▀▀▄ █▀▄▀▄ ▄▀▀▄ ▀█▀ █▄▀ █ █ ▄▀
91
+ █ █▀▀▀ █ █▀▀▀ █ █ █ █▀▀▀ █ █ █ █ ▄▀ █ █ █
92
+ ▀▄ ▀▄▄▀ █ ▀▄▄▀ █ █ █ ▀▄▄▀ ▀▄ █ █ ▄▀ ▄▀ ▀ █ █ █
93
+ ▄▀ ▌ ▄▀ ▀ █ █
94
+ ▌▄▀ ▀ █
95
+ ▀ ▀
96
+ ${ansiReset}`);
128
97
  }
129
98
  function printServerInfo(port) {
130
99
  console.log(`
@@ -0,0 +1,6 @@
1
+ export declare const configPath: string;
2
+ export type Config = {
3
+ apiToken?: string;
4
+ };
5
+ export declare function loadConfig(): Promise<Config | null>;
6
+ export declare function saveConfig(config: Config): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ import { homedir } from 'os';
3
+ import path from 'path';
4
+ export const configPath = path.join(homedir(), '.telemetryos-cli.json');
5
+ export async function loadConfig() {
6
+ try {
7
+ const content = await readFile(configPath, 'utf-8');
8
+ return JSON.parse(content);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export async function saveConfig(config) {
15
+ await writeFile(configPath, JSON.stringify(config, null, 2));
16
+ }
@@ -0,0 +1,10 @@
1
+ export type GenerateApplicationOptions = {
2
+ name: string;
3
+ description: string;
4
+ author: string;
5
+ version: string;
6
+ template: string;
7
+ projectPath: string;
8
+ progressFn: (createdFilePath: string) => void;
9
+ };
10
+ export declare function generateApplication(options: GenerateApplicationOptions): Promise<void>;
@@ -0,0 +1,82 @@
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ const ignoredTemplateFiles = ['.DS_Store', 'thumbs.db', 'node_modules', '.git', 'dist'];
5
+ const templatesDir = path.join(import.meta.dirname, '../templates');
6
+ const dotfileNames = ['gitignore', 'npmrc', 'npmignore', 'env.example'];
7
+ export async function generateApplication(options) {
8
+ const { name, description, author, version, template, projectPath, progressFn } = options;
9
+ await fs.mkdir(projectPath, { recursive: true });
10
+ await copyDir(path.join(templatesDir, template), projectPath, {
11
+ name,
12
+ version,
13
+ description,
14
+ author,
15
+ }, progressFn);
16
+ // Install latest versions of @telemetryos/sdk and @telemetryos/cli
17
+ console.log('\nInstalling dependencies...');
18
+ await installPackages(projectPath);
19
+ }
20
+ async function installPackages(projectPath) {
21
+ // Install SDK as a regular dependency
22
+ await new Promise((resolve, reject) => {
23
+ const sdkInstall = spawn('npm', ['install', '@telemetryos/sdk'], {
24
+ cwd: projectPath,
25
+ stdio: 'inherit',
26
+ });
27
+ sdkInstall.on('close', (code) => {
28
+ if (code === 0) {
29
+ resolve();
30
+ }
31
+ else {
32
+ reject(new Error(`npm install @telemetryos/sdk failed with code ${code}`));
33
+ }
34
+ });
35
+ sdkInstall.on('error', (error) => {
36
+ reject(error);
37
+ });
38
+ });
39
+ // Install CLI as a dev dependency
40
+ await new Promise((resolve, reject) => {
41
+ const cliInstall = spawn('npm', ['install', '-D', '@telemetryos/cli'], {
42
+ cwd: projectPath,
43
+ stdio: 'inherit',
44
+ });
45
+ cliInstall.on('close', (code) => {
46
+ if (code === 0) {
47
+ resolve();
48
+ }
49
+ else {
50
+ reject(new Error(`npm install -D @telemetryos/cli failed with code ${code}`));
51
+ }
52
+ });
53
+ cliInstall.on('error', (error) => {
54
+ reject(error);
55
+ });
56
+ });
57
+ }
58
+ async function copyDir(source, destination, replacements, progressFn) {
59
+ const dirListing = await fs.readdir(source);
60
+ for (const dirEntry of dirListing) {
61
+ if (ignoredTemplateFiles.includes(dirEntry))
62
+ continue;
63
+ const sourcePath = path.join(source, dirEntry);
64
+ const destinationPath = path.join(destination, dotfileNames.includes(dirEntry) ? `.${dirEntry}` : dirEntry);
65
+ const stats = await fs.stat(sourcePath);
66
+ if (stats.isDirectory()) {
67
+ await fs.mkdir(destinationPath, { recursive: true });
68
+ await copyDir(sourcePath, destinationPath, replacements, progressFn);
69
+ }
70
+ else if (stats.isFile()) {
71
+ await copyFile(sourcePath, destinationPath, replacements, progressFn);
72
+ }
73
+ }
74
+ }
75
+ async function copyFile(source, destination, replacements, progressFn) {
76
+ let contents = await fs.readFile(source, 'utf-8');
77
+ for (const [key, value] of Object.entries(replacements)) {
78
+ contents = contents.replace(new RegExp(`{{${key}}}`, 'g'), value);
79
+ }
80
+ await fs.writeFile(destination, contents, 'utf-8');
81
+ progressFn(destination);
82
+ }
@@ -0,0 +1,5 @@
1
+ type Flags = {
2
+ port: number;
3
+ };
4
+ export declare function runServer(projectPath: string, flags: Flags): Promise<void>;
5
+ export {};
@@ -0,0 +1,226 @@
1
+ import { spawn } from 'child_process';
2
+ import { readFile } from 'fs/promises';
3
+ import http from 'http';
4
+ import path from 'path';
5
+ import readable from 'readline/promises';
6
+ import serveHandler from 'serve-handler';
7
+ import z from 'zod';
8
+ import pkg from '../../package.json' with { type: 'json' };
9
+ const ansiWhite = '\u001b[37m';
10
+ const ansiYellow = '\u001b[33m';
11
+ const ansiCyan = '\u001b[36m';
12
+ const ansiBold = '\u001b[1m';
13
+ const ansiReset = '\u001b[0m';
14
+ const mountPointSchema = z.object({
15
+ path: z.string(),
16
+ });
17
+ const workerScriptSchema = z.object({
18
+ path: z.string(),
19
+ });
20
+ const containerSchema = z.object({
21
+ image: z.string(),
22
+ });
23
+ const configSchema = z.object({
24
+ name: z.string().optional(),
25
+ version: z.string().optional(),
26
+ thumbnailPath: z.string().optional(),
27
+ mountPoints: z.record(z.string(), z.union([mountPointSchema, z.string()])).optional(),
28
+ workerScripts: z.record(z.string(), z.union([workerScriptSchema, z.string()])).optional(),
29
+ containers: z.record(z.string(), z.union([containerSchema, z.string()])).optional(),
30
+ devServer: z
31
+ .object({
32
+ runCommand: z.string().optional(),
33
+ url: z.string(),
34
+ })
35
+ .optional(),
36
+ });
37
+ export async function runServer(projectPath, flags) {
38
+ printSplashScreen();
39
+ projectPath = path.resolve(process.cwd(), projectPath);
40
+ const telemetryConfig = await loadConfigFile(projectPath);
41
+ if (!telemetryConfig) {
42
+ console.error('No telemetry configuration found. Are you in the right directory?');
43
+ process.exit(1);
44
+ }
45
+ const validationResult = configSchema.safeParse(telemetryConfig);
46
+ if (!validationResult.success) {
47
+ console.error('Invalid telemetry.config.json:');
48
+ validationResult.error.issues.forEach((issue) => {
49
+ console.error(` ${issue.path.join('.')}: ${issue.message}`);
50
+ });
51
+ console.error('\n');
52
+ process.exit(1);
53
+ }
54
+ await serveDevelopmentApplicationHostUI(projectPath, flags.port, validationResult.data);
55
+ await serveTelemetryApplication(projectPath, validationResult.data);
56
+ }
57
+ async function serveDevelopmentApplicationHostUI(projectPath, port, telemetryConfig) {
58
+ const hostUiPath = await import.meta.resolve('@telemetryos/development-application-host-ui/dist');
59
+ const serveConfig = { public: hostUiPath.replace('file://', '') };
60
+ const server = http.createServer();
61
+ server.on('request', async (req, res) => {
62
+ var _a, _b;
63
+ const url = new URL(req.url, `http://${req.headers.origin}`);
64
+ if (url.pathname === '/__tos-config__') {
65
+ res.setHeader('Content-Type', 'application/json');
66
+ res.end(JSON.stringify(telemetryConfig));
67
+ return;
68
+ }
69
+ if (url.pathname === '/__tos-thumbnail__') {
70
+ if (!telemetryConfig.thumbnailPath) {
71
+ res.statusCode = 404;
72
+ res.end('No thumbnail configured');
73
+ return;
74
+ }
75
+ const thumbnailFullPath = path.join(projectPath, telemetryConfig.thumbnailPath);
76
+ try {
77
+ const imageData = await readFile(thumbnailFullPath);
78
+ const ext = path.extname(telemetryConfig.thumbnailPath).toLowerCase();
79
+ const mimeTypes = {
80
+ '.jpg': 'image/jpeg',
81
+ '.jpeg': 'image/jpeg',
82
+ '.png': 'image/png',
83
+ '.gif': 'image/gif',
84
+ '.svg': 'image/svg+xml',
85
+ '.webp': 'image/webp',
86
+ };
87
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
88
+ res.setHeader('Content-Type', contentType);
89
+ res.end(imageData);
90
+ }
91
+ catch {
92
+ res.statusCode = 404;
93
+ res.end('Thumbnail file not found');
94
+ }
95
+ return;
96
+ }
97
+ if (url.pathname === '/__dev_proxy__' && req.method === 'POST' && res) {
98
+ let body = '';
99
+ for await (const chunk of req) {
100
+ body += chunk;
101
+ }
102
+ try {
103
+ const { url, method, headers, body: requestBody } = JSON.parse(body);
104
+ const response = await fetch(url, {
105
+ method,
106
+ headers,
107
+ body: requestBody !== null && requestBody !== void 0 ? requestBody : undefined,
108
+ });
109
+ const contentType = response.headers.get('content-type') || '';
110
+ const isJson = contentType.includes('application/json');
111
+ const isText = contentType.includes('text/') ||
112
+ contentType.includes('application/javascript') ||
113
+ contentType.includes('application/xml');
114
+ let responseBody;
115
+ let bodyType;
116
+ if (isJson) {
117
+ const text = await response.text();
118
+ responseBody = text ? JSON.parse(text) : null;
119
+ bodyType = 'json';
120
+ }
121
+ else if (isText) {
122
+ responseBody = await response.text();
123
+ bodyType = 'text';
124
+ }
125
+ else {
126
+ // Binary data - convert to base64 for JSON transport
127
+ const arrayBuffer = await response.arrayBuffer();
128
+ responseBody = Buffer.from(arrayBuffer).toString('base64');
129
+ bodyType = 'binary';
130
+ }
131
+ const responseHeaders = {};
132
+ response.headers.forEach((value, key) => {
133
+ responseHeaders[key] = value;
134
+ });
135
+ res.writeHead(200, { 'Content-Type': 'application/json' });
136
+ res.end(JSON.stringify({
137
+ success: true,
138
+ status: response.status,
139
+ statusText: response.statusText,
140
+ headers: responseHeaders,
141
+ body: responseBody,
142
+ bodyType,
143
+ ok: response.ok,
144
+ url: response.url,
145
+ }));
146
+ }
147
+ catch (error) {
148
+ res.writeHead(200, { 'Content-Type': 'application/json' });
149
+ res.end(JSON.stringify({
150
+ success: false,
151
+ errorMessage: `Proxy fetch failed: ${String(error)}`,
152
+ errorCause: error.cause ? String((_b = (_a = error.cause) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : error.cause) : undefined,
153
+ }));
154
+ }
155
+ return;
156
+ }
157
+ serveHandler(req, res, serveConfig).catch((err) => {
158
+ console.error('Error handling request:', err);
159
+ res.statusCode = 500;
160
+ res.end('Internal Server Error');
161
+ });
162
+ });
163
+ printServerInfo(port);
164
+ server.listen(port);
165
+ }
166
+ async function serveTelemetryApplication(rootPath, telemetryConfig) {
167
+ var _a;
168
+ if (!((_a = telemetryConfig === null || telemetryConfig === void 0 ? void 0 : telemetryConfig.devServer) === null || _a === void 0 ? void 0 : _a.runCommand)) {
169
+ console.log('No value in config at devServer.runCommand');
170
+ return;
171
+ }
172
+ const runCommand = telemetryConfig.devServer.runCommand;
173
+ const binPath = path.join(rootPath, 'node_modules', '.bin');
174
+ const childProcess = spawn(runCommand, {
175
+ shell: true,
176
+ env: { ...process.env, FORCE_COLOR: '1', PATH: `${binPath}:${process.env.PATH}` },
177
+ stdio: ['ignore', 'pipe', 'pipe'],
178
+ cwd: rootPath,
179
+ });
180
+ const stdoutReadline = readable.createInterface({
181
+ input: childProcess.stdout,
182
+ crlfDelay: Infinity,
183
+ });
184
+ const stderrReadline = readable.createInterface({
185
+ input: childProcess.stderr,
186
+ crlfDelay: Infinity,
187
+ });
188
+ stdoutReadline.on('line', (line) => {
189
+ console.log(`[application]: ${line}`);
190
+ });
191
+ stderrReadline.on('line', (line) => {
192
+ console.error(`[application]: ${line}`);
193
+ });
194
+ process.on('exit', () => {
195
+ childProcess.kill();
196
+ });
197
+ }
198
+ async function loadConfigFile(rootPath) {
199
+ const configFilePath = path.join(rootPath, 'telemetry.config.json');
200
+ try {
201
+ const fileContent = await readFile(configFilePath, 'utf-8');
202
+ const config = JSON.parse(fileContent);
203
+ return config;
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ function printSplashScreen() {
210
+ console.log(`
211
+
212
+ ${ansiWhite} █ █ █ ${ansiYellow}▄▀▀▀▄ ▄▀▀▀▄
213
+ ${ansiWhite} █ █ █ ${ansiYellow}█ █ █
214
+ ${ansiWhite}▀█▀ ▄▀▀▄ █ ▄▀▀▄ █▀▄▀▄ ▄▀▀▄ ▀█▀ █▄▀ █ █ ${ansiYellow}█ █ ▀▀▀▄
215
+ ${ansiWhite} █ █▀▀▀ █ █▀▀▀ █ █ █ █▀▀▀ █ █ █ █ ${ansiYellow}█ █ █
216
+ ${ansiWhite} ▀▄ ▀▄▄▀ █ ▀▄▄▀ █ █ █ ▀▄▄▀ ▀▄ █ █ ${ansiYellow}▀▄▄▄▀ ▀▄▄▄▀
217
+ ${ansiWhite} ▄▀ ${ansiReset}
218
+ v${pkg.version}`);
219
+ }
220
+ function printServerInfo(port) {
221
+ console.log(`
222
+ ╔═══════════════════════════════════════════════════════════╗
223
+ ║ ${ansiBold}Development environment running at: ${ansiCyan}http://localhost:${port}${ansiReset} ║
224
+ ╚═══════════════════════════════════════════════════════════╝
225
+ `);
226
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetryos/cli",
3
- "version": "1.4.4",
3
+ "version": "1.5.1",
4
4
  "description": "The official TelemetryOS application CLI package. Use it to build applications that run on the TelemetryOS platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "license": "",
26
26
  "repository": "github:TelemetryTV/Application-API",
27
27
  "dependencies": {
28
- "@telemetryos/development-application-host-ui": "^1.4.4",
28
+ "@telemetryos/development-application-host-ui": "^1.5.1",
29
29
  "@types/serve-handler": "^6.1.4",
30
30
  "commander": "^14.0.0",
31
31
  "inquirer": "^12.9.6",
@@ -42,6 +42,9 @@
42
42
  "eslint": "^9.26.0",
43
43
  "eslint-config-prettier": "^10.1.2",
44
44
  "eslint-plugin-prettier": "^5.4.0",
45
+ "globals": "^16.0.0",
46
+ "prettier": "^3.5.3",
47
+ "typescript-eslint": "^8.32.0",
45
48
  "typescript": "^5.8.3"
46
49
  },
47
50
  "scripts": {
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
3
+ <svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="258pt" height="258pt" viewBox="0 0 258 258" style="" preserveAspectRatio="xMidYMid meet">
4
+ <g fill="#000000FF">
5
+ <path d="M 50.000 2.594 C 26.451 7.639 7.292 27.262 2.532 51.208 C 0.208 62.898 0.258 195.123 2.591 206.643 C 6.873 227.792 21.909 245.496 41.686 252.675 C 52.935 256.758 62.175 257.162 135.500 256.771 C 197.509 256.441 205.104 256.227 210.465 254.658 C 232.534 248.202 248.346 232.415 254.695 210.500 C 256.258 205.103 256.466 197.335 256.760 133.195 C 257.134 51.729 257.005 49.878 249.920 35.226 C 242.061 18.972 225.455 6.257 207.185 2.503 C 196.815 0.372 59.999 0.451 50.000 2.594 M 218.171 67.655 C 219.177 68.565 220.000 70.360 220.000 71.644 C 220.000 74.019 217.097 85.638 214.346 94.269 C 212.009 101.603 210.642 102.000 187.705 102.000 C 170.049 102.000 168.159 102.167 166.622 103.865 C 165.656 104.932 160.251 123.192 153.980 146.574 C 146.078 176.037 142.457 187.988 140.988 189.460 C 139.175 191.274 137.454 191.536 125.419 191.829 C 105.566 192.311 104.849 191.534 109.420 174.500 C 110.748 169.550 115.247 152.778 119.417 137.229 C 123.588 121.680 127.000 107.932 127.000 106.679 C 127.000 102.305 125.651 102.000 106.300 102.000 C 89.333 102.000 87.854 101.854 86.000 100.000 C 84.900 98.900 84.000 97.434 84.000 96.742 C 84.000 96.050 85.538 89.872 87.417 83.012 C 90.090 73.257 91.409 70.045 93.473 68.270 L 96.112 66.000 156.227 66.000 C 210.762 66.000 216.512 66.154 218.171 67.655 M 113.171 112.655 C 114.177 113.565 115.000 115.215 115.000 116.322 C 115.000 119.129 109.072 140.911 107.719 143.079 C 105.740 146.247 102.139 147.000 88.953 147.000 C 77.333 147.000 75.794 146.794 74.000 145.000 C 72.900 143.900 72.001 142.438 72.002 141.750 C 72.004 139.717 77.964 117.689 79.145 115.352 C 80.934 111.808 83.867 111.139 97.921 111.068 C 109.071 111.011 111.652 111.280 113.171 112.655 M 77.495 66.636 C 80.995 67.979 80.987 71.343 77.456 84.594 C 75.311 92.642 73.282 98.164 71.996 99.451 C 70.173 101.276 68.473 101.536 56.419 101.829 C 36.114 102.322 35.816 101.938 41.058 82.083 C 45.310 65.978 45.281 66.000 62.474 66.000 C 69.823 66.000 76.583 66.286 77.495 66.636 "/></g>
6
+ <g fill="#F7B435FF">
7
+ <path d="M 93.473 68.270 C 91.409 70.045 90.090 73.257 87.417 83.012 C 85.538 89.872 84.000 96.050 84.000 96.742 C 84.000 97.434 84.900 98.900 86.000 100.000 C 87.854 101.854 89.333 102.000 106.300 102.000 C 125.651 102.000 127.000 102.305 127.000 106.679 C 127.000 107.932 123.588 121.680 119.417 137.229 C 115.247 152.778 110.748 169.550 109.420 174.500 C 104.849 191.534 105.566 192.311 125.419 191.829 C 137.454 191.536 139.175 191.274 140.988 189.460 C 142.457 187.988 146.078 176.037 153.980 146.574 C 160.251 123.192 165.656 104.932 166.622 103.865 C 168.159 102.167 170.049 102.000 187.705 102.000 C 210.642 102.000 212.009 101.603 214.346 94.269 C 217.097 85.638 220.000 74.019 220.000 71.644 C 220.000 70.360 219.177 68.565 218.171 67.655 C 216.512 66.154 210.762 66.000 156.227 66.000 L 96.112 66.000 93.473 68.270 "/></g>
8
+ <g fill="#F7B435FF">
9
+ <path d="M 82.365 112.169 C 79.454 113.579 78.540 115.677 75.032 129.000 C 71.441 142.634 71.453 142.453 74.000 145.000 C 75.794 146.794 77.333 147.000 88.953 147.000 C 102.139 147.000 105.740 146.247 107.719 143.079 C 109.072 140.911 115.000 119.129 115.000 116.322 C 115.000 111.825 112.301 110.995 97.921 111.068 C 90.540 111.105 83.539 111.601 82.365 112.169 "/></g>
10
+ <g fill="#F7B435FF">
11
+ <path d="M 46.614 68.250 C 44.748 69.931 43.343 73.430 41.058 82.083 C 35.816 101.938 36.114 102.322 56.419 101.829 C 68.473 101.536 70.173 101.276 71.996 99.451 C 73.282 98.164 75.311 92.642 77.456 84.594 C 80.987 71.343 80.995 67.979 77.495 66.636 C 76.583 66.286 69.823 66.000 62.474 66.000 C 50.023 66.000 48.941 66.154 46.614 68.250 "/></g>
12
+ </svg>
@@ -0,0 +1,26 @@
1
+ # Dependencies
2
+ node_modules
3
+
4
+ # Build output
5
+ dist
6
+
7
+ # Environment variables
8
+ .env
9
+ .env.local
10
+
11
+ # IDE
12
+ .vscode
13
+ .idea
14
+ *.swp
15
+ *.swo
16
+
17
+ # OS
18
+ .DS_Store
19
+ Thumbs.db
20
+
21
+ # Logs
22
+ *.log
23
+ npm-debug.log*
24
+ pnpm-debug.log*
25
+ yarn-debug.log*
26
+ yarn-error.log*
@@ -6,13 +6,16 @@ import wordMarkPath from '../../assets/telemetryos-wordmark.svg'
6
6
  export function Render() {
7
7
  const [subtitle, setSubtitle] = useState('')
8
8
 
9
- const subscribeSubtitleEffect = () => {
10
- store().instance.subscribe<string>('subtitle', (value) => {
11
- const fallbackSubtitle = "Change this line in settings ⚙️ ↗️"
9
+ useEffect(() => {
10
+ const handler = (value?: string) => {
11
+ const fallbackSubtitle = 'Change this line in settings ⚙️ ↗️'
12
12
  setSubtitle(value ?? fallbackSubtitle)
13
- }).catch(console.error)
14
- }
15
- useEffect(subscribeSubtitleEffect, [])
13
+ }
14
+ store().instance.subscribe<string>('subtitle', handler).catch(console.error)
15
+ return () => {
16
+ store().instance.unsubscribe('subtitle', handler).catch(console.error)
17
+ }
18
+ }, [])
16
19
 
17
20
  return (
18
21
  <div className="render">
@@ -22,7 +25,9 @@ export function Render() {
22
25
  <div className="render__hero-subtitle">{subtitle}</div>
23
26
  </div>
24
27
  <div className="render__docs-information">
25
- <div className="render__docs-information-title">To get started, edit the Render.tsx and Settings.tsx files</div>
28
+ <div className="render__docs-information-title">
29
+ To get started, edit the Render.tsx and Settings.tsx files
30
+ </div>
26
31
  <div className="render__docs-information-text">
27
32
  Visit our documentation on building applications to learn more
28
33
  </div>
@@ -1,34 +1,36 @@
1
1
  .settings {
2
- padding: 1.4rem;
2
+ padding: 16px;
3
+ font-family: 'Rubik', sans-serif;
4
+ font-feature-settings: "rlig" 1, "calt" 1;
3
5
  }
4
6
 
5
7
  .form-field {
6
8
  display: flex;
7
9
  flex-direction: column;
8
- margin-bottom: 1.4rem;
10
+ margin-bottom: 12px;
9
11
  }
10
12
 
11
13
  .form-field-label {
12
- font-size: 1.225rem;
13
- margin-bottom: 0.4rem;
14
+ font-size: 12px;
15
+ margin-bottom: 6px;
14
16
  font-weight: 500;
15
17
  }
16
18
 
17
19
  .form-field-frame {
18
20
  display: flex;
19
- border: 0.1rem solid rgb(100, 100, 100);
20
- border-radius: 0.4rem;
21
- background: rgb(30, 37, 43);
22
- color: rgb(212, 212, 212);
21
+ border: 1px solid hsl(212 24% 19%);
22
+ border-radius: 6px;
23
+ background: hsl(212 28% 10%);
24
+ color: hsl(210 40% 88%);
23
25
  }
24
26
 
25
27
  .form-field-input {
26
28
  flex: 1;
27
- font-size: 1.4rem;
29
+ font-size: 13px;
28
30
  line-height: 1em;
29
- padding: 0.8rem;
31
+ padding: 8px 12px;
30
32
  border: none;
31
- border-radius: 0.4rem;
33
+ border-radius: 6px;
32
34
  background: transparent;
33
- color: rgb(212, 212, 212);
34
- }
35
+ color: hsl(210 40% 88%);
36
+ }
@@ -2,38 +2,46 @@ import './Settings.css'
2
2
 
3
3
  import { useEffect, useState } from 'react'
4
4
  import { store } from '@telemetryos/sdk'
5
+ import { useLayoutHeightSync } from '../hooks/useLayoutHeightSync'
5
6
 
6
7
  export function Settings() {
8
+ const containerRef = useLayoutHeightSync()
7
9
  const [subtitleText, setSubtitleText] = useState('')
8
10
  const [isLoadingValue, setIsLoadingValue] = useState(true)
9
11
 
10
- const fetchSubtitleEffect = () => {
11
- (async () => {
12
- let subtitle = await store().instance.get<string>('subtitle')
13
- if (subtitle !== undefined) {
14
- setSubtitleText(subtitle)
15
- } else {
16
- const defaultSubtitle = "Change this line in settings ⚙️ ↗️"
17
- await store().instance.set('subtitle', defaultSubtitle)
18
- setSubtitleText(defaultSubtitle)
19
- }
20
- setIsLoadingValue(false)
21
- })().catch(console.error)
22
- }
23
-
24
- useEffect(fetchSubtitleEffect, [])
12
+ useEffect(() => {
13
+ store()
14
+ .instance.get<string>('subtitle')
15
+ .then((subtitle) => {
16
+ if (subtitle !== undefined) {
17
+ setSubtitleText(subtitle)
18
+ } else {
19
+ const defaultSubtitle = 'Change this line in settings ⚙️ ↗️'
20
+ store().instance.set('subtitle', defaultSubtitle).catch(console.error)
21
+ setSubtitleText(defaultSubtitle)
22
+ }
23
+ setIsLoadingValue(false)
24
+ })
25
+ .catch(console.error)
26
+ }, [])
25
27
 
26
- const handleSubtitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
27
- setSubtitleText(event.target.value)
28
- store().instance.set('subtitle', event.target.value).catch(console.error)
28
+ const handleSubtitleChange = (subtitle: string) => {
29
+ setSubtitleText(subtitle)
30
+ store().instance.set('subtitle', subtitle).catch(console.error)
29
31
  }
30
32
 
31
33
  return (
32
- <div className="settings">
34
+ <div ref={containerRef} className="settings">
33
35
  <div className="form-field">
34
36
  <div className="form-field-label">Subtitle Text</div>
35
37
  <div className="form-field-frame">
36
- <input className="form-field-input" type="text" value={subtitleText} onChange={handleSubtitleChange} disabled={isLoadingValue} />
38
+ <input
39
+ className="form-field-input"
40
+ type="text"
41
+ value={subtitleText}
42
+ onChange={(e) => handleSubtitleChange(e.target.value)}
43
+ disabled={isLoadingValue}
44
+ />
37
45
  </div>
38
46
  </div>
39
47
  </div>
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "{{name}}",
3
+ "thumbnailPath": "assets/tos-app.svg",
3
4
  "version": "{{version}}",
4
5
  "mountPoints": {
5
6
  "render": "/render",