@telemetryos/cli 1.2.0 → 1.4.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.
- package/CHANGELOG.md +26 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +79 -0
- package/dist/commands/root.js +2 -2
- package/dist/commands/serve.js +1 -1
- package/dist/generate-application.d.ts +10 -0
- package/dist/generate-application.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/run-server.d.ts +5 -0
- package/dist/run-server.js +100 -0
- package/package.json +2 -2
- package/templates/vite-react-typescript/AGENTS.md +7 -0
- package/templates/vite-react-typescript/CLAUDE.md +625 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @telemetryos/cli
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- This change updates the CLI package to properly host applications with
|
|
8
|
+
full support for all SDK features.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
- @telemetryos/development-application-host-ui@1.4.0
|
|
14
|
+
|
|
15
|
+
## 1.3.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- Add Weather API
|
|
20
|
+
|
|
21
|
+
Adds a weather API for applications that wish to use it. It can be found on
|
|
22
|
+
client instances under .weather.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies
|
|
27
|
+
- @telemetryos/development-application-host-ui@1.3.0
|
|
28
|
+
|
|
3
29
|
## 1.2.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { generateApplication } from '../generate-application.js';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
export const initCommand = new Command('init')
|
|
6
|
+
.description('Initializes a new telemetryOS application')
|
|
7
|
+
.option('-n, --name <string>', 'The name of the application', '')
|
|
8
|
+
.option('-d, --description <string>', 'The description of the application', 'A telemetryOS application')
|
|
9
|
+
.option('-a, --author <string>', 'The author of the application', '')
|
|
10
|
+
.option('-v, --version <string>', 'The version of the application', '0.1.0')
|
|
11
|
+
.option('-t, --template <string>', 'The template to use (vite-react-typescript)', '')
|
|
12
|
+
.argument('[project-path]', 'Path to create the new telemetry application project. Defaults to a folder current working directory with the same name as your project', '')
|
|
13
|
+
.action(handleInitCommand);
|
|
14
|
+
async function handleInitCommand(projectPath, options) {
|
|
15
|
+
let name = options.name;
|
|
16
|
+
let description = options.description;
|
|
17
|
+
let author = options.author;
|
|
18
|
+
let version = options.version;
|
|
19
|
+
let template = options.template;
|
|
20
|
+
const questions = [];
|
|
21
|
+
if (!name)
|
|
22
|
+
questions.push({
|
|
23
|
+
type: 'input',
|
|
24
|
+
name: 'name',
|
|
25
|
+
message: 'What is the name of your application?',
|
|
26
|
+
validate: (input) => input.length > 0 || 'Application name cannot be empty'
|
|
27
|
+
});
|
|
28
|
+
if (!description)
|
|
29
|
+
questions.push({
|
|
30
|
+
type: 'input',
|
|
31
|
+
name: 'description',
|
|
32
|
+
message: 'What is the description of your application?',
|
|
33
|
+
default: ''
|
|
34
|
+
});
|
|
35
|
+
if (!author)
|
|
36
|
+
questions.push({
|
|
37
|
+
type: 'input',
|
|
38
|
+
name: 'author',
|
|
39
|
+
message: 'Who is the author of your application?',
|
|
40
|
+
default: ''
|
|
41
|
+
});
|
|
42
|
+
if (!version)
|
|
43
|
+
questions.push({
|
|
44
|
+
type: 'input',
|
|
45
|
+
name: 'version',
|
|
46
|
+
message: 'What is the version of your application?',
|
|
47
|
+
default: '0.1.0',
|
|
48
|
+
validate: (input) => /^\d+\.\d+\.\d+(-.+)?$/.test(input) || 'Version must be in semver format (e.g. 1.0.0)'
|
|
49
|
+
});
|
|
50
|
+
if (!template)
|
|
51
|
+
questions.push({
|
|
52
|
+
type: 'list',
|
|
53
|
+
name: 'template',
|
|
54
|
+
message: 'Which template would you like to use?',
|
|
55
|
+
choices: [
|
|
56
|
+
{ name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
if (questions.length !== 0) {
|
|
60
|
+
const answers = await inquirer.prompt(questions);
|
|
61
|
+
if (answers.name)
|
|
62
|
+
name = answers.name;
|
|
63
|
+
if (answers.template)
|
|
64
|
+
template = answers.template;
|
|
65
|
+
}
|
|
66
|
+
if (!projectPath)
|
|
67
|
+
projectPath = path.join(process.cwd(), name);
|
|
68
|
+
await generateApplication({
|
|
69
|
+
name,
|
|
70
|
+
description,
|
|
71
|
+
author,
|
|
72
|
+
version,
|
|
73
|
+
template,
|
|
74
|
+
projectPath,
|
|
75
|
+
progressFn: (createdFilePath) => {
|
|
76
|
+
console.log(`.${path.sep}${path.relative(process.cwd(), createdFilePath)}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
package/dist/commands/root.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import pkg from '../../package.json' with { type: 'json' };
|
|
3
|
-
export const rootCommand = new Command('
|
|
4
|
-
.description('
|
|
3
|
+
export const rootCommand = new Command('tos')
|
|
4
|
+
.description('TelemetryOS Application CLI')
|
|
5
5
|
.version(pkg.version);
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { runServer } from '../run-server.js';
|
|
3
3
|
export const serveCommand = new Command('serve')
|
|
4
|
-
.description('Serves a
|
|
4
|
+
.description('Serves a telemetryOS application locally for development')
|
|
5
5
|
.option('-p, --port <number>', 'Port to run the development ui on', '6969')
|
|
6
6
|
.argument('[project-path]', 'Path to the telemetry application project. Defaults to current working directory', process.cwd())
|
|
7
7
|
.action(runServer);
|
|
@@ -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,45 @@
|
|
|
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
|
+
];
|
|
10
|
+
const templatesDir = path.join(import.meta.dirname, '../templates');
|
|
11
|
+
export async function generateApplication(options) {
|
|
12
|
+
const { name, description, author, version, template, projectPath, progressFn } = options;
|
|
13
|
+
await fs.mkdir(projectPath, { recursive: true });
|
|
14
|
+
await copyDir(path.join(templatesDir, template), projectPath, {
|
|
15
|
+
name,
|
|
16
|
+
description,
|
|
17
|
+
author,
|
|
18
|
+
version
|
|
19
|
+
}, progressFn);
|
|
20
|
+
}
|
|
21
|
+
async function copyDir(source, destination, replacements, progressFn) {
|
|
22
|
+
const dirListing = await fs.readdir(source);
|
|
23
|
+
for (const dirEntry of dirListing) {
|
|
24
|
+
if (ignoredTelemplateFiles.includes(dirEntry))
|
|
25
|
+
continue;
|
|
26
|
+
const sourcePath = path.join(source, dirEntry);
|
|
27
|
+
const destinationPath = path.join(destination, dirEntry);
|
|
28
|
+
const stats = await fs.stat(sourcePath);
|
|
29
|
+
if (stats.isDirectory()) {
|
|
30
|
+
await fs.mkdir(destinationPath, { recursive: true });
|
|
31
|
+
await copyDir(sourcePath, destinationPath, replacements, progressFn);
|
|
32
|
+
}
|
|
33
|
+
else if (stats.isFile()) {
|
|
34
|
+
await copyFile(sourcePath, destinationPath, replacements, progressFn);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function copyFile(source, destination, replacements, progressFn) {
|
|
39
|
+
let contents = await fs.readFile(source, 'utf-8');
|
|
40
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
41
|
+
contents = contents.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
|
42
|
+
}
|
|
43
|
+
await fs.writeFile(destination, contents, 'utf-8');
|
|
44
|
+
progressFn(destination);
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { initCommand } from './commands/init.js';
|
|
3
|
+
import { rootCommand } from './commands/root.js';
|
|
4
|
+
import { serveCommand } from './commands/serve.js';
|
|
5
|
+
rootCommand.addCommand(serveCommand);
|
|
6
|
+
rootCommand.addCommand(initCommand);
|
|
7
|
+
rootCommand.parse(process.argv);
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
const ansiWhite = '\u001b[37m';
|
|
8
|
+
const ansiYellow = '\u001b[33m';
|
|
9
|
+
const ansiCyan = '\u001b[36m';
|
|
10
|
+
const ansiBold = '\u001b[1m';
|
|
11
|
+
const ansiReset = '\u001b[0m';
|
|
12
|
+
export async function runServer(projectPath, flags) {
|
|
13
|
+
printSplashScreen();
|
|
14
|
+
projectPath = path.resolve(process.cwd(), projectPath);
|
|
15
|
+
const telemetryConfig = await loadConfigFile(projectPath);
|
|
16
|
+
if (!telemetryConfig) {
|
|
17
|
+
console.error('No telemetry configuration found. Are you in the right directory?');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
await serveDevelopmentApplicationHostUI(flags.port, telemetryConfig);
|
|
21
|
+
await serveTelemetryApplication(projectPath, telemetryConfig);
|
|
22
|
+
}
|
|
23
|
+
async function serveDevelopmentApplicationHostUI(port, telemetryConfig) {
|
|
24
|
+
const hostUiPath = await import.meta.resolve('@telemetryos/development-application-host-ui/dist');
|
|
25
|
+
const serveConfig = { public: hostUiPath.replace('file://', '') };
|
|
26
|
+
const server = http.createServer();
|
|
27
|
+
server.on('request', (req, res) => {
|
|
28
|
+
const url = new URL(req.url, `http://${req.headers.origin}`);
|
|
29
|
+
if (url.pathname === '/__tos-config__') {
|
|
30
|
+
res.setHeader('Content-Type', 'application/json');
|
|
31
|
+
res.end(JSON.stringify(telemetryConfig));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
serveHandler(req, res, serveConfig).catch((err) => {
|
|
35
|
+
console.error('Error handling request:', err);
|
|
36
|
+
res.statusCode = 500;
|
|
37
|
+
res.end('Internal Server Error');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
printServerInfo(port);
|
|
41
|
+
server.listen(port);
|
|
42
|
+
}
|
|
43
|
+
async function serveTelemetryApplication(rootPath, telemetryConfig) {
|
|
44
|
+
var _a;
|
|
45
|
+
if (!((_a = telemetryConfig === null || telemetryConfig === void 0 ? void 0 : telemetryConfig.devServer) === null || _a === void 0 ? void 0 : _a.runCommand))
|
|
46
|
+
return;
|
|
47
|
+
const runCommand = telemetryConfig.devServer.runCommand;
|
|
48
|
+
const binPath = path.join(rootPath, 'node_modules', '.bin');
|
|
49
|
+
const childProcess = spawn(runCommand, {
|
|
50
|
+
shell: true,
|
|
51
|
+
env: { ...process.env, FORCE_COLOR: '1', PATH: `${binPath}:${process.env.PATH}` },
|
|
52
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
+
cwd: rootPath,
|
|
54
|
+
});
|
|
55
|
+
const stdoutReadline = readable.createInterface({
|
|
56
|
+
input: childProcess.stdout,
|
|
57
|
+
crlfDelay: Infinity,
|
|
58
|
+
});
|
|
59
|
+
const stderrReadline = readable.createInterface({
|
|
60
|
+
input: childProcess.stderr,
|
|
61
|
+
crlfDelay: Infinity,
|
|
62
|
+
});
|
|
63
|
+
stdoutReadline.on('line', (line) => {
|
|
64
|
+
console.log(`[application]: ${line}`);
|
|
65
|
+
});
|
|
66
|
+
stderrReadline.on('line', (line) => {
|
|
67
|
+
console.error(`[application]: ${line}`);
|
|
68
|
+
});
|
|
69
|
+
process.on('exit', () => {
|
|
70
|
+
childProcess.kill();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function loadConfigFile(rootPath) {
|
|
74
|
+
const configFilePath = path.join(rootPath, 'telemetry.config.json');
|
|
75
|
+
try {
|
|
76
|
+
const fileContent = await readFile(configFilePath, 'utf-8');
|
|
77
|
+
const config = JSON.parse(fileContent);
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function printSplashScreen() {
|
|
85
|
+
console.log(`
|
|
86
|
+
|
|
87
|
+
${ansiWhite} █ █ █ ${ansiYellow}▄▀▀▀▄ ▄▀▀▀▄
|
|
88
|
+
${ansiWhite} █ █ █ ${ansiYellow}█ █ █
|
|
89
|
+
${ansiWhite}▀█▀ ▄▀▀▄ █ ▄▀▀▄ █▀▄▀▄ ▄▀▀▄ ▀█▀ █▄▀ █ █ ${ansiYellow}█ █ ▀▀▀▄
|
|
90
|
+
${ansiWhite} █ █▀▀▀ █ █▀▀▀ █ █ █ █▀▀▀ █ █ █ █ ${ansiYellow}█ █ █
|
|
91
|
+
${ansiWhite} ▀▄ ▀▄▄▀ █ ▀▄▄▀ █ █ █ ▀▄▄▀ ▀▄ █ █ ${ansiYellow}▀▄▄▄▀ ▀▄▄▄▀
|
|
92
|
+
${ansiWhite} ▄▀ ${ansiReset}`);
|
|
93
|
+
}
|
|
94
|
+
function printServerInfo(port) {
|
|
95
|
+
console.log(`
|
|
96
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
97
|
+
║ ${ansiBold}Development environment running at: ${ansiCyan}http://localhost:${port}${ansiReset} ║
|
|
98
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
99
|
+
`);
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetryos/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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.
|
|
28
|
+
"@telemetryos/development-application-host-ui": "^1.4.0",
|
|
29
29
|
"@types/serve-handler": "^6.1.4",
|
|
30
30
|
"commander": "^14.0.0",
|
|
31
31
|
"inquirer": "^12.9.6",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Agent Guidelines
|
|
2
|
+
|
|
3
|
+
This document outlines guidelines for interacting with and developing for AI agents within this project.
|
|
4
|
+
|
|
5
|
+
## Important Notes
|
|
6
|
+
|
|
7
|
+
* **Claude-specific Guidelines:** If you are working with Claude models, please refer to `CLAUDE.md` for specific guidelines and best practices.
|
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
# TelemetryOS SDK Reference
|
|
2
|
+
|
|
3
|
+
**Application:** [Your App Name]
|
|
4
|
+
**Purpose:** [What this application does]
|
|
5
|
+
|
|
6
|
+
## Platform Architecture
|
|
7
|
+
|
|
8
|
+
TelemetryOS applications are web apps that run on digital signage devices. Applications have up to 4 components:
|
|
9
|
+
|
|
10
|
+
1. **Render** (`/render`) - Content displayed on devices (runs on device in Chrome/iframe)
|
|
11
|
+
2. **Settings** (`/settings`) - Config UI in Studio admin portal (runs in Studio browser)
|
|
12
|
+
3. **Workers** (optional) - Background JavaScript (runs on device, no DOM)
|
|
13
|
+
4. **Containers** (optional) - Docker containers for backend services (runs on device)
|
|
14
|
+
|
|
15
|
+
**Runtime Environment:**
|
|
16
|
+
- Chrome browser (platform-controlled version)
|
|
17
|
+
- Iframe sandbox execution
|
|
18
|
+
- Client-side only (no SSR, no Node.js APIs)
|
|
19
|
+
- Modern web APIs available (Fetch, WebSockets, WebGL, Canvas)
|
|
20
|
+
- External APIs require CORS proxy
|
|
21
|
+
|
|
22
|
+
**Communication:**
|
|
23
|
+
- Settings and Render share instance storage
|
|
24
|
+
- Settings saves config → Render subscribes to config
|
|
25
|
+
- Device storage only available in Render (not Settings)
|
|
26
|
+
|
|
27
|
+
## Project Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
project-root/
|
|
31
|
+
├── telemetry.config.json # Platform configuration
|
|
32
|
+
├── package.json
|
|
33
|
+
├── tsconfig.json
|
|
34
|
+
├── vite.config.ts
|
|
35
|
+
├── index.html
|
|
36
|
+
└── src/
|
|
37
|
+
├── main.tsx # Entry point (configure SDK here)
|
|
38
|
+
├── App.tsx # Routing logic
|
|
39
|
+
├── views/
|
|
40
|
+
│ ├── Settings.tsx # /settings mount point
|
|
41
|
+
│ └── Render.tsx # /render mount point
|
|
42
|
+
├── components/ # Reusable components
|
|
43
|
+
├── hooks/ # Custom React hooks
|
|
44
|
+
├── types/ # TypeScript interfaces
|
|
45
|
+
└── utils/ # Helper functions
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Configuration Files
|
|
49
|
+
|
|
50
|
+
### telemetry.config.json (project root)
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"name": "app-name",
|
|
54
|
+
"version": "1.0.0",
|
|
55
|
+
"mountPoints": {
|
|
56
|
+
"render": "/render",
|
|
57
|
+
"settings": "/settings"
|
|
58
|
+
},
|
|
59
|
+
"devServer": {
|
|
60
|
+
"runCommand": "vite --port 3000",
|
|
61
|
+
"url": "http://localhost:3000"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### package.json scripts
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"scripts": {
|
|
70
|
+
"dev": "vite",
|
|
71
|
+
"build": "tsc && vite build",
|
|
72
|
+
"preview": "vite preview"
|
|
73
|
+
},
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"@telemetryos/sdk": "latest",
|
|
76
|
+
"react": "latest",
|
|
77
|
+
"react-dom": "latest"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@types/react": "latest",
|
|
81
|
+
"@types/react-dom": "latest",
|
|
82
|
+
"@vitejs/plugin-react": "latest",
|
|
83
|
+
"typescript": "latest",
|
|
84
|
+
"vite": "latest"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Complete File Implementations
|
|
90
|
+
|
|
91
|
+
### src/main.tsx (Entry Point)
|
|
92
|
+
```typescript
|
|
93
|
+
import { configure } from '@telemetryos/sdk';
|
|
94
|
+
import React from 'react';
|
|
95
|
+
import ReactDOM from 'react-dom/client';
|
|
96
|
+
import App from './App';
|
|
97
|
+
import './index.css';
|
|
98
|
+
|
|
99
|
+
// Configure SDK ONCE before React renders
|
|
100
|
+
// Name must match telemetry.config.json
|
|
101
|
+
configure('app-name');
|
|
102
|
+
|
|
103
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
104
|
+
<React.StrictMode>
|
|
105
|
+
<App />
|
|
106
|
+
</React.StrictMode>
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### src/App.tsx (Routing)
|
|
111
|
+
```typescript
|
|
112
|
+
import Settings from './views/Settings';
|
|
113
|
+
import Render from './views/Render';
|
|
114
|
+
|
|
115
|
+
export default function App() {
|
|
116
|
+
const path = window.location.pathname;
|
|
117
|
+
|
|
118
|
+
if (path === '/settings') return <Settings />;
|
|
119
|
+
if (path === '/render') return <Render />;
|
|
120
|
+
|
|
121
|
+
return <div>Invalid mount point: {path}</div>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### src/views/Settings.tsx (Complete Reference)
|
|
126
|
+
```typescript
|
|
127
|
+
import { useEffect, useState, FormEvent } from 'react';
|
|
128
|
+
import { store } from '@telemetryos/sdk';
|
|
129
|
+
|
|
130
|
+
interface Config {
|
|
131
|
+
city: string;
|
|
132
|
+
units: 'celsius' | 'fahrenheit';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default function Settings() {
|
|
136
|
+
const [config, setConfig] = useState<Config>({ city: '', units: 'celsius' });
|
|
137
|
+
const [loading, setLoading] = useState(false);
|
|
138
|
+
const [error, setError] = useState<string | null>(null);
|
|
139
|
+
|
|
140
|
+
// Load existing config on mount
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
store().instance.get<Config>('config')
|
|
143
|
+
.then(saved => { if (saved) setConfig(saved); })
|
|
144
|
+
.catch(err => setError(err.message));
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
const handleSave = async (e: FormEvent) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
setLoading(true);
|
|
150
|
+
setError(null);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const success = await store().instance.set('config', config);
|
|
154
|
+
if (!success) throw new Error('Storage operation failed');
|
|
155
|
+
} catch (err) {
|
|
156
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
157
|
+
} finally {
|
|
158
|
+
setLoading(false);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div>
|
|
164
|
+
<h2>Settings</h2>
|
|
165
|
+
{error && <div style={{ color: 'red' }}>{error}</div>}
|
|
166
|
+
<form onSubmit={handleSave}>
|
|
167
|
+
<div>
|
|
168
|
+
<label htmlFor="city">City:</label>
|
|
169
|
+
<input
|
|
170
|
+
id="city"
|
|
171
|
+
value={config.city}
|
|
172
|
+
onChange={(e) => setConfig({ ...config, city: e.target.value })}
|
|
173
|
+
required
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
<div>
|
|
177
|
+
<label htmlFor="units">Units:</label>
|
|
178
|
+
<select
|
|
179
|
+
id="units"
|
|
180
|
+
value={config.units}
|
|
181
|
+
onChange={(e) => setConfig({ ...config, units: e.target.value as Config['units'] })}
|
|
182
|
+
>
|
|
183
|
+
<option value="celsius">Celsius</option>
|
|
184
|
+
<option value="fahrenheit">Fahrenheit</option>
|
|
185
|
+
</select>
|
|
186
|
+
</div>
|
|
187
|
+
<button type="submit" disabled={loading}>
|
|
188
|
+
{loading ? 'Saving...' : 'Save'}
|
|
189
|
+
</button>
|
|
190
|
+
</form>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### src/views/Render.tsx (Complete Reference)
|
|
197
|
+
```typescript
|
|
198
|
+
import { useEffect, useState } from 'react';
|
|
199
|
+
import { store, proxy } from '@telemetryos/sdk';
|
|
200
|
+
|
|
201
|
+
interface Config {
|
|
202
|
+
city: string;
|
|
203
|
+
units: 'celsius' | 'fahrenheit';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface WeatherData {
|
|
207
|
+
temperature: number;
|
|
208
|
+
conditions: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default function Render() {
|
|
212
|
+
const [config, setConfig] = useState<Config | null>(null);
|
|
213
|
+
const [weather, setWeather] = useState<WeatherData | null>(null);
|
|
214
|
+
const [loading, setLoading] = useState(false);
|
|
215
|
+
const [error, setError] = useState<string | null>(null);
|
|
216
|
+
|
|
217
|
+
// Subscribe to config changes from Settings
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
store().instance.get<Config>('config').then(setConfig);
|
|
220
|
+
|
|
221
|
+
const unsubscribe = store().instance.subscribe('config', (newConfig: Config) => {
|
|
222
|
+
setConfig(newConfig);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return () => unsubscribe();
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
// Fetch weather when config changes
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!config?.city) return;
|
|
231
|
+
|
|
232
|
+
const fetchWeather = async () => {
|
|
233
|
+
setLoading(true);
|
|
234
|
+
setError(null);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const response = await proxy().fetch(
|
|
238
|
+
`https://api.example.com/weather?city=${config.city}&units=${config.units}`
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (!response.ok) throw new Error(`API error: ${response.status}`);
|
|
242
|
+
|
|
243
|
+
const data = await response.json();
|
|
244
|
+
setWeather({ temperature: data.temp, conditions: data.conditions });
|
|
245
|
+
|
|
246
|
+
// Cache for offline
|
|
247
|
+
await store().device.set('cached', { data, timestamp: Date.now() });
|
|
248
|
+
} catch (err) {
|
|
249
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
250
|
+
|
|
251
|
+
// Try cached data
|
|
252
|
+
const cached = await store().device.get<any>('cached');
|
|
253
|
+
if (cached) setWeather(cached.data);
|
|
254
|
+
} finally {
|
|
255
|
+
setLoading(false);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
fetchWeather();
|
|
260
|
+
}, [config]);
|
|
261
|
+
|
|
262
|
+
// States
|
|
263
|
+
if (!config) return <div>Configure in Settings</div>;
|
|
264
|
+
if (loading && !weather) return <div>Loading...</div>;
|
|
265
|
+
if (error && !weather) return <div>Error: {error}</div>;
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div>
|
|
269
|
+
<h1>{config.city}</h1>
|
|
270
|
+
<div>{weather?.temperature}°{config.units === 'celsius' ? 'C' : 'F'}</div>
|
|
271
|
+
<div>{weather?.conditions}</div>
|
|
272
|
+
{error && <div style={{ color: 'orange' }}>Showing cached data</div>}
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## SDK API Reference
|
|
279
|
+
|
|
280
|
+
Import from `@telemetryos/sdk`.
|
|
281
|
+
|
|
282
|
+
### Initialization
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
configure(applicationName: string): void
|
|
286
|
+
```
|
|
287
|
+
- Call once in main.tsx before React renders
|
|
288
|
+
- Name must match telemetry.config.json
|
|
289
|
+
- Throws if called multiple times
|
|
290
|
+
|
|
291
|
+
### Storage API
|
|
292
|
+
|
|
293
|
+
**Type Signatures:**
|
|
294
|
+
```typescript
|
|
295
|
+
store().application.set(key: string, value: any): Promise<boolean>
|
|
296
|
+
store().application.get<T>(key: string): Promise<T | null>
|
|
297
|
+
store().application.subscribe(key: string, handler: (value: any) => void): () => void
|
|
298
|
+
store().application.delete(key: string): Promise<boolean>
|
|
299
|
+
store().application.keys(): Promise<string[]>
|
|
300
|
+
|
|
301
|
+
// Same methods for instance, device, shared(namespace)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Four Scopes:**
|
|
305
|
+
|
|
306
|
+
1. **application** - Shared across all instances of app in account
|
|
307
|
+
```typescript
|
|
308
|
+
await store().application.set('companyLogo', 'https://...');
|
|
309
|
+
const logo = await store().application.get<string>('companyLogo');
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
2. **instance** - This specific app instance (Settings ↔ Render communication)
|
|
313
|
+
```typescript
|
|
314
|
+
// Settings saves
|
|
315
|
+
await store().instance.set('config', { city: 'NYC' });
|
|
316
|
+
|
|
317
|
+
// Render subscribes
|
|
318
|
+
const unsubscribe = store().instance.subscribe('config', (newConfig) => {
|
|
319
|
+
updateDisplay(newConfig);
|
|
320
|
+
});
|
|
321
|
+
// Later: unsubscribe();
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
3. **device** - This physical device only (NOT available in Settings)
|
|
325
|
+
```typescript
|
|
326
|
+
// Only in Render mount point
|
|
327
|
+
await store().device.set('cache', data);
|
|
328
|
+
const cached = await store().device.get<CacheType>('cache');
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
4. **shared(namespace)** - Inter-app communication
|
|
332
|
+
```typescript
|
|
333
|
+
// App A publishes
|
|
334
|
+
await store().shared('weather').set('temp', '72°F');
|
|
335
|
+
|
|
336
|
+
// App B subscribes
|
|
337
|
+
store().shared('weather').subscribe('temp', (temp) => console.log(temp));
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Constraints:**
|
|
341
|
+
- All operations timeout after 30 seconds (throws Error)
|
|
342
|
+
- Returns Promise<boolean> for set/delete (true = success)
|
|
343
|
+
- Returns Promise<T | null> for get
|
|
344
|
+
- subscribe() returns unsubscribe function
|
|
345
|
+
- Device scope throws Error in Settings mount point
|
|
346
|
+
|
|
347
|
+
### Proxy API
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
proxy().fetch(url: string, options?: RequestInit): Promise<Response>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
- Same interface as standard fetch()
|
|
354
|
+
- Use for ALL external API calls to avoid CORS errors
|
|
355
|
+
- Returns standard Response object
|
|
356
|
+
- Handles CORS server-side
|
|
357
|
+
|
|
358
|
+
**Example:**
|
|
359
|
+
```typescript
|
|
360
|
+
import { proxy } from '@telemetryos/sdk';
|
|
361
|
+
|
|
362
|
+
const response = await proxy().fetch('https://api.example.com/data');
|
|
363
|
+
const json = await response.json();
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Media API
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
media().getAllByTag(tag: string): Promise<MediaContent[]>
|
|
370
|
+
media().getById(id: string): Promise<MediaContent | null>
|
|
371
|
+
|
|
372
|
+
interface MediaContent {
|
|
373
|
+
id: string;
|
|
374
|
+
url: string;
|
|
375
|
+
type: 'image' | 'video';
|
|
376
|
+
tags: string[];
|
|
377
|
+
metadata: Record<string, any>;
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Playlist API
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
playlist().nextPage(): Promise<boolean>
|
|
385
|
+
playlist().previousPage(): Promise<boolean>
|
|
386
|
+
playlist().jumpToPage(index: number): Promise<boolean>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Overrides API
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
overrides().setOverride(id: string): Promise<boolean>
|
|
393
|
+
overrides().clearOverride(): Promise<boolean>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Note: Override IDs must be pre-configured in Freeform Editor.
|
|
397
|
+
|
|
398
|
+
### Platform Information
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
accounts().getCurrent(): Promise<Account>
|
|
402
|
+
users().getCurrent(): Promise<User>
|
|
403
|
+
devices().getCurrent(): Promise<Device> // Render only
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Hard Constraints
|
|
407
|
+
|
|
408
|
+
**These cause runtime errors:**
|
|
409
|
+
|
|
410
|
+
1. **Device storage in Settings**
|
|
411
|
+
- Settings runs in Studio browser, not on devices
|
|
412
|
+
- `store().device.*` throws Error in Settings
|
|
413
|
+
- Use `store().instance` or `store().application` instead
|
|
414
|
+
|
|
415
|
+
2. **External API without proxy**
|
|
416
|
+
- Direct `fetch()` to external domains fails with CORS error
|
|
417
|
+
- Must use `proxy().fetch()` for all external requests
|
|
418
|
+
|
|
419
|
+
3. **Missing configure()**
|
|
420
|
+
- SDK methods throw "SDK not configured" Error
|
|
421
|
+
- Call `configure()` once in main.tsx before React renders
|
|
422
|
+
|
|
423
|
+
4. **Subscription memory leaks**
|
|
424
|
+
- `subscribe()` returns unsubscribe function
|
|
425
|
+
- Must call unsubscribe on component unmount
|
|
426
|
+
- Return unsubscribe from useEffect cleanup
|
|
427
|
+
|
|
428
|
+
5. **Timeout errors**
|
|
429
|
+
- All SDK operations timeout after 30 seconds
|
|
430
|
+
- Throws Error with message containing 'timeout'
|
|
431
|
+
- Handle with try/catch
|
|
432
|
+
|
|
433
|
+
## TypeScript Patterns
|
|
434
|
+
|
|
435
|
+
**Define interfaces for all configs and data:**
|
|
436
|
+
```typescript
|
|
437
|
+
interface AppConfig {
|
|
438
|
+
city: string;
|
|
439
|
+
units: 'celsius' | 'fahrenheit';
|
|
440
|
+
refreshInterval: number;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const config = await store().instance.get<AppConfig>('config');
|
|
444
|
+
if (config) {
|
|
445
|
+
console.log(config.city); // TypeScript knows this exists
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**Component with proper types:**
|
|
450
|
+
```typescript
|
|
451
|
+
interface Props {
|
|
452
|
+
data: WeatherData;
|
|
453
|
+
onRefresh: () => void;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export default function WeatherCard({ data, onRefresh }: Props) {
|
|
457
|
+
return <div>{data.temperature}</div>;
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## React Patterns
|
|
462
|
+
|
|
463
|
+
**Error handling:**
|
|
464
|
+
```typescript
|
|
465
|
+
const [error, setError] = useState<string | null>(null);
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
await store().instance.set('key', value);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Loading states:**
|
|
475
|
+
```typescript
|
|
476
|
+
const [loading, setLoading] = useState(false);
|
|
477
|
+
|
|
478
|
+
const handleAction = async () => {
|
|
479
|
+
setLoading(true);
|
|
480
|
+
try {
|
|
481
|
+
await someAsyncOperation();
|
|
482
|
+
} finally {
|
|
483
|
+
setLoading(false);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Subscription cleanup:**
|
|
489
|
+
```typescript
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
const unsubscribe = store().instance.subscribe('key', handler);
|
|
492
|
+
return () => unsubscribe();
|
|
493
|
+
}, []);
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Empty deps for mount-only effects:**
|
|
497
|
+
```typescript
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
// Runs once on mount
|
|
500
|
+
store().instance.get('config').then(setConfig);
|
|
501
|
+
}, []); // Empty deps array
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Code Style
|
|
505
|
+
|
|
506
|
+
**Naming:**
|
|
507
|
+
- Components: PascalCase (`WeatherCard.tsx`)
|
|
508
|
+
- Functions: camelCase (`fetchWeatherData`)
|
|
509
|
+
- Constants: UPPER_SNAKE_CASE (`API_BASE_URL`)
|
|
510
|
+
- Interfaces: PascalCase (`WeatherData`, `AppConfig`)
|
|
511
|
+
|
|
512
|
+
**Imports order:**
|
|
513
|
+
```typescript
|
|
514
|
+
// 1. SDK imports
|
|
515
|
+
import { configure, store, proxy } from '@telemetryos/sdk';
|
|
516
|
+
|
|
517
|
+
// 2. React imports
|
|
518
|
+
import { useEffect, useState } from 'react';
|
|
519
|
+
|
|
520
|
+
// 3. Local imports
|
|
521
|
+
import WeatherCard from '@/components/WeatherCard';
|
|
522
|
+
import type { WeatherData } from '@/types';
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**TypeScript:**
|
|
526
|
+
- Use strict mode
|
|
527
|
+
- Define interfaces for all configs and data
|
|
528
|
+
- Use generics with storage: `get<Type>(key)`
|
|
529
|
+
- Prefer `interface` over `type` for objects
|
|
530
|
+
|
|
531
|
+
**React:**
|
|
532
|
+
- Functional components only
|
|
533
|
+
- Use hooks (useState, useEffect, useMemo, useCallback)
|
|
534
|
+
- Implement loading, error, empty states
|
|
535
|
+
- Clean up subscriptions in useEffect return
|
|
536
|
+
|
|
537
|
+
## Development Commands
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
# Install dependencies
|
|
541
|
+
npm install
|
|
542
|
+
|
|
543
|
+
# Start local dev server
|
|
544
|
+
tos serve
|
|
545
|
+
# Or: npm run dev
|
|
546
|
+
|
|
547
|
+
# Build for production
|
|
548
|
+
npm run build
|
|
549
|
+
|
|
550
|
+
# Type check
|
|
551
|
+
tsc --noEmit
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**Local testing:**
|
|
555
|
+
- Settings: http://localhost:3000/settings
|
|
556
|
+
- Render: http://localhost:3000/render
|
|
557
|
+
|
|
558
|
+
**Deployment:**
|
|
559
|
+
```bash
|
|
560
|
+
git add .
|
|
561
|
+
git commit -m "Description"
|
|
562
|
+
git push origin main
|
|
563
|
+
# GitHub integration auto-deploys
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## Common Errors
|
|
567
|
+
|
|
568
|
+
**"SDK not configured"**
|
|
569
|
+
→ Call `configure('app-name')` in main.tsx before React renders
|
|
570
|
+
|
|
571
|
+
**"device storage not available"**
|
|
572
|
+
→ Using `store().device` in Settings - use `store().instance` instead
|
|
573
|
+
|
|
574
|
+
**CORS error**
|
|
575
|
+
→ Using direct `fetch()` - use `proxy().fetch()` instead
|
|
576
|
+
|
|
577
|
+
**"Request timeout"**
|
|
578
|
+
→ SDK operation exceeded 30 seconds - handle with try/catch
|
|
579
|
+
|
|
580
|
+
**Render not updating**
|
|
581
|
+
→ Missing subscription - use `store().instance.subscribe()` in Render
|
|
582
|
+
|
|
583
|
+
**Memory leak**
|
|
584
|
+
→ Not returning unsubscribe from useEffect
|
|
585
|
+
|
|
586
|
+
## Project-Specific Context
|
|
587
|
+
|
|
588
|
+
[Add your project details here:]
|
|
589
|
+
|
|
590
|
+
**Application Name:** [Your app name]
|
|
591
|
+
**External APIs:**
|
|
592
|
+
- [API name]: [endpoint]
|
|
593
|
+
- Authentication: [method]
|
|
594
|
+
- Rate limits: [limits]
|
|
595
|
+
|
|
596
|
+
**Custom Components:**
|
|
597
|
+
- [ComponentName]: [purpose]
|
|
598
|
+
- Location: [path]
|
|
599
|
+
- Props: [interface]
|
|
600
|
+
|
|
601
|
+
**Business Logic:**
|
|
602
|
+
- [Key algorithms or calculations]
|
|
603
|
+
- [Data transformation rules]
|
|
604
|
+
|
|
605
|
+
## Technical References
|
|
606
|
+
|
|
607
|
+
**SDK API Documentation:**
|
|
608
|
+
- [Storage API](https://docs.telemetryos.com/docs/storage-methods) - Complete storage scope reference
|
|
609
|
+
- [Platform API](https://docs.telemetryos.com/docs/platform-methods) - Proxy, media, accounts, users, devices
|
|
610
|
+
- [Playlist API](https://docs.telemetryos.com/docs/playlist-methods) - Page navigation methods
|
|
611
|
+
- [Overrides API](https://docs.telemetryos.com/docs/overrides-methods) - Dynamic content control
|
|
612
|
+
- [Client API](https://docs.telemetryos.com/docs/client-methods) - Device client interactions
|
|
613
|
+
- [Media API](https://docs.telemetryos.com/docs/media-methods) - Media content queries
|
|
614
|
+
|
|
615
|
+
**Critical Context:**
|
|
616
|
+
- [CORS Guide](https://docs.telemetryos.com/docs/cors) - Why proxy().fetch() is required
|
|
617
|
+
- [Mount Points](https://docs.telemetryos.com/docs/mount-points) - /render vs /settings execution
|
|
618
|
+
- [Languages Supported](https://docs.telemetryos.com/docs/languages-supported) - Runtime environment constraints
|
|
619
|
+
- [Configuration](https://docs.telemetryos.com/docs/configuration) - telemetry.config.json schema
|
|
620
|
+
- [Workers](https://docs.telemetryos.com/docs/workers) - Background JavaScript patterns
|
|
621
|
+
- [Containers](https://docs.telemetryos.com/docs/containers) - Docker integration patterns
|
|
622
|
+
|
|
623
|
+
**Code Examples:**
|
|
624
|
+
- [Code Examples](https://docs.telemetryos.com/docs/code-examples) - Real-world implementations
|
|
625
|
+
- [LLMS.txt](https://docs.telemetryos.com/llms.txt) - Complete documentation index
|