@telemetryos/cli 1.12.0 → 1.13.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 +37 -0
- package/dist/commands/claude-code.d.ts +2 -0
- package/dist/commands/claude-code.js +29 -0
- package/dist/commands/init.js +22 -9
- package/dist/index.js +2 -0
- package/dist/services/create-project.d.ts +13 -0
- package/dist/services/create-project.js +188 -0
- package/dist/services/project-config.d.ts +3 -0
- package/dist/services/project-config.js +3 -0
- package/dist/services/run-server.js +63 -26
- package/dist/utils/template.d.ts +2 -0
- package/dist/utils/template.js +30 -0
- package/package.json +3 -3
- package/templates/{vite-react-typescript → claude-code}/CLAUDE.md +15 -3
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-architecture/SKILL.md +140 -63
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-debugging/SKILL.md +6 -7
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-media-api/SKILL.md +3 -3
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-multi-mode/SKILL.md +97 -4
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-requirements/SKILL.md +70 -5
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-store-sync/SKILL.md +4 -2
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-weather-api/SKILL.md +7 -6
- package/templates/claude-code/_claude/skills/tos-web-ui-design/SKILL.md +373 -0
- package/templates/vite-react-typescript/_gitignore +4 -2
- package/templates/vite-react-typescript/public/assets/telemetryos-wordmark.svg +11 -0
- package/templates/vite-react-typescript/public/assets/tos-app.svg +12 -0
- package/templates/vite-react-typescript/src/index.tsx +1 -1
- package/templates/vite-react-typescript/src/views/Render.tsx +1 -2
- package/templates/vite-react-typescript/telemetry.config.json +2 -1
- package/templates/vite-react-typescript-web/_gitignore +32 -0
- package/templates/vite-react-typescript-web/index.html +15 -0
- package/templates/vite-react-typescript-web/package.json +24 -0
- package/templates/vite-react-typescript-web/src/App.tsx +25 -0
- package/templates/vite-react-typescript-web/src/hooks/store.ts +8 -0
- package/templates/vite-react-typescript-web/src/index.css +24 -0
- package/templates/vite-react-typescript-web/src/index.tsx +11 -0
- package/templates/vite-react-typescript-web/src/views/Render.css +67 -0
- package/templates/vite-react-typescript-web/src/views/Render.tsx +44 -0
- package/templates/vite-react-typescript-web/src/views/Settings.tsx +72 -0
- package/templates/vite-react-typescript-web/src/views/Web.css +105 -0
- package/templates/vite-react-typescript-web/src/views/Web.tsx +52 -0
- package/templates/vite-react-typescript-web/telemetry.config.json +16 -0
- package/templates/vite-react-typescript-web/tsconfig.json +19 -0
- package/templates/vite-react-typescript-web/vite.config.ts +18 -0
- /package/templates/{vite-react-typescript → claude-code}/AGENTS.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/settings.local.json +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-proxy-fetch/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-kiosk-design/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-signage-design/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-ui-design/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-settings-ui/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → vite-react-typescript-web}/assets/telemetryos-wordmark.svg +0 -0
- /package/templates/{vite-react-typescript → vite-react-typescript-web}/assets/tos-app.svg +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @telemetryos/cli
|
|
2
2
|
|
|
3
|
+
## 1.13.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- - **SDK client refactor:** Removed `applicationName` from the client and `configure()` API (still accepted for backwards compatibility). The SDK now resolves `applicationSpecifier` from URL search params first with subdomain fallback, validates it as a 40-char hex hash, and adds `deviceId` from URL params. Store scoping updated accordingly. Fixed a listener leak when calling `configure()` multiple times.
|
|
8
|
+
- **Template assets moved to `public/`:** SDK template assets (logo, thumbnail) moved from `assets/` to `public/assets/` so Vite includes them in the build output automatically. Dev server updated to resolve from `public/` first.
|
|
9
|
+
- **Documentation updates for v1.13.0:** Added docs for web mount point, Navigation API, `tos claude-code` command, `vite-react-typescript-web` template, `playlist.getDuration()`, `SettingsMediaSelect` component, and custom logo support. Fixed scope availability, store scope guidance for web mount points, and various inconsistencies.
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @telemetryos/development-application-host-ui@1.13.1
|
|
13
|
+
|
|
14
|
+
## 1.13.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- ### New Features
|
|
19
|
+
- **Web mount point support** — Applications can now define a third UI surface (`/web`) alongside Render and Settings, enabling web-portal-style interfaces with full navigation control via a postMessage bridge.
|
|
20
|
+
- **`playlist.getDuration()`** — New async method returns the playlist-configured page duration in seconds.
|
|
21
|
+
- **Custom logo support** — Projects can specify a `logoPath` in `telemetry.config.json` for branding in the dev host.
|
|
22
|
+
- **`tos claude-code` command** — New CLI command to apply or update Claude Code skills and settings in existing projects.
|
|
23
|
+
- **Web project template** — New `vite-react-typescript-web` template scaffolds a project with all three mount points pre-configured.
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
- Fixed SDK client not properly validating bridge message responses.
|
|
27
|
+
- Fixed canvas not resizing when the sidebar toggles or window resizes after a manual drag-resize.
|
|
28
|
+
- Fixed `isLoading` usage and `useEffect` dependency arrays in Claude Code skill examples.
|
|
29
|
+
- Fixed stale active tab when the tabs array changes in the dev host.
|
|
30
|
+
|
|
31
|
+
### Infrastructure
|
|
32
|
+
- Renamed `generate-application` to `create-project`; extracted shared template utilities.
|
|
33
|
+
- Added unit tests for Navigation, Currency, and Weather classes.
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Updated dependencies
|
|
38
|
+
- @telemetryos/development-application-host-ui@1.13.0
|
|
39
|
+
|
|
3
40
|
## 1.12.0
|
|
4
41
|
|
|
5
42
|
### Minor Changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { loadProjectConfig } from '../services/project-config.js';
|
|
4
|
+
import { copyDir, templatesDir } from '../utils/template.js';
|
|
5
|
+
import { ansi } from '../utils/ansi.js';
|
|
6
|
+
export const claudeCodeCommand = new Command('claude-code')
|
|
7
|
+
.description('Apply or update Claude Code skills and settings in a project')
|
|
8
|
+
.argument('[project-path]', 'Path to the telemetryOS project (defaults to current directory)', process.cwd())
|
|
9
|
+
.action(handleClaudeCodeCommand);
|
|
10
|
+
async function handleClaudeCodeCommand(projectPath) {
|
|
11
|
+
var _a, _b, _c;
|
|
12
|
+
const resolvedPath = path.resolve(projectPath);
|
|
13
|
+
let config;
|
|
14
|
+
try {
|
|
15
|
+
config = await loadProjectConfig(resolvedPath);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error(`\n${ansi.red}Error:${ansi.reset} ${error instanceof Error ? error.message : error}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const name = (_a = config.name) !== null && _a !== void 0 ? _a : '';
|
|
22
|
+
const description = (_b = config.description) !== null && _b !== void 0 ? _b : '';
|
|
23
|
+
const version = (_c = config.version) !== null && _c !== void 0 ? _c : '';
|
|
24
|
+
console.log(`\nApplying Claude Code skills and settings...\n`);
|
|
25
|
+
await copyDir(path.join(templatesDir, 'claude-code'), resolvedPath, { name, version, description, author: '' }, (createdFilePath) => {
|
|
26
|
+
console.log(`.${path.sep}${path.relative(process.cwd(), createdFilePath)}`);
|
|
27
|
+
});
|
|
28
|
+
console.log(`\n${ansi.green}Done!${ansi.reset} Claude Code skills and settings have been applied.`);
|
|
29
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
2
|
+
import { createProject, checkDirectoryConflicts, removeConflictingFiles, } from '../services/create-project.js';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { resolveProjectPathAndName, validateProjectName } from '../utils/path-utils.js';
|
|
@@ -26,10 +26,11 @@ export const initCommand = new Command('init')
|
|
|
26
26
|
.option('-d, --description <string>', 'The description of the application', '')
|
|
27
27
|
.option('-a, --author <string>', 'The author of the application', '')
|
|
28
28
|
.option('-v, --version <string>', 'The version of the application', '0.1.0')
|
|
29
|
-
.option('-t, --template <string>', 'The template to use (vite-react-typescript)', '')
|
|
29
|
+
.option('-t, --template <string>', 'The template to use (vite-react-typescript, vite-react-typescript-web)', '')
|
|
30
30
|
.option('-y, --yes', 'Skip all prompts and use defaults', false)
|
|
31
31
|
.action(handleInitCommand);
|
|
32
32
|
async function handleInitCommand(projectPathArg, options) {
|
|
33
|
+
var _a, _b, _c, _d, _e, _f;
|
|
33
34
|
// Step 1: Resolve path and derive name
|
|
34
35
|
const cwd = process.cwd();
|
|
35
36
|
const inputPath = projectPathArg || '.';
|
|
@@ -80,6 +81,7 @@ async function handleInitCommand(projectPathArg, options) {
|
|
|
80
81
|
let author = options.author;
|
|
81
82
|
let version = options.version;
|
|
82
83
|
let template = options.template;
|
|
84
|
+
let claudeCode = true;
|
|
83
85
|
// Step 5: Build prompt questions (skipped with --yes)
|
|
84
86
|
if (options.yes) {
|
|
85
87
|
if (!name) {
|
|
@@ -138,23 +140,34 @@ async function handleInitCommand(projectPathArg, options) {
|
|
|
138
140
|
type: 'list',
|
|
139
141
|
name: 'template',
|
|
140
142
|
message: 'Which template would you like to use?',
|
|
141
|
-
choices: [
|
|
143
|
+
choices: [
|
|
144
|
+
{ name: 'Vite + React + TypeScript', value: 'vite-react-typescript' },
|
|
145
|
+
{ name: 'Vite + React + TypeScript + Web', value: 'vite-react-typescript-web' },
|
|
146
|
+
],
|
|
142
147
|
});
|
|
148
|
+
questions.push({
|
|
149
|
+
type: 'confirm',
|
|
150
|
+
name: 'claudeCode',
|
|
151
|
+
message: 'Include Claude Code skills and settings?',
|
|
152
|
+
default: true,
|
|
153
|
+
});
|
|
143
154
|
// Step 6: Prompt user
|
|
144
155
|
const answers = await inquirer.prompt(questions);
|
|
145
|
-
name = answers.name
|
|
146
|
-
version = answers.version
|
|
147
|
-
description = answers.description
|
|
148
|
-
author = answers.author
|
|
149
|
-
template = answers.template
|
|
156
|
+
name = (_a = answers.name) !== null && _a !== void 0 ? _a : name;
|
|
157
|
+
version = (_b = answers.version) !== null && _b !== void 0 ? _b : version;
|
|
158
|
+
description = (_c = answers.description) !== null && _c !== void 0 ? _c : description;
|
|
159
|
+
author = (_d = answers.author) !== null && _d !== void 0 ? _d : author;
|
|
160
|
+
template = (_e = answers.template) !== null && _e !== void 0 ? _e : template;
|
|
161
|
+
claudeCode = (_f = answers.claudeCode) !== null && _f !== void 0 ? _f : claudeCode;
|
|
150
162
|
}
|
|
151
163
|
// Step 7: Generate application
|
|
152
|
-
await
|
|
164
|
+
await createProject({
|
|
153
165
|
name,
|
|
154
166
|
description,
|
|
155
167
|
author,
|
|
156
168
|
version,
|
|
157
169
|
template,
|
|
170
|
+
claudeCode,
|
|
158
171
|
projectPath: resolvedPath,
|
|
159
172
|
progressFn: (createdFilePath) => {
|
|
160
173
|
console.log(`.${path.sep}${path.relative(cwd, createdFilePath)}`);
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { authCommand } from './commands/auth.js';
|
|
3
|
+
import { claudeCodeCommand } from './commands/claude-code.js';
|
|
3
4
|
import { initCommand } from './commands/init.js';
|
|
4
5
|
import { publishCommand } from './commands/publish.js';
|
|
5
6
|
import { rootCommand } from './commands/root.js';
|
|
6
7
|
import { serveCommand } from './commands/serve.js';
|
|
7
8
|
rootCommand.addCommand(authCommand);
|
|
9
|
+
rootCommand.addCommand(claudeCodeCommand);
|
|
8
10
|
rootCommand.addCommand(serveCommand);
|
|
9
11
|
rootCommand.addCommand(initCommand);
|
|
10
12
|
rootCommand.addCommand(publishCommand);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type CreateProjectOptions = {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
author: string;
|
|
5
|
+
version: string;
|
|
6
|
+
template: string;
|
|
7
|
+
claudeCode: boolean;
|
|
8
|
+
projectPath: string;
|
|
9
|
+
progressFn: (createdFilePath: string) => void;
|
|
10
|
+
};
|
|
11
|
+
export declare function createProject(options: CreateProjectOptions): Promise<void>;
|
|
12
|
+
export declare function checkDirectoryConflicts(projectPath: string): Promise<string[]>;
|
|
13
|
+
export declare function removeConflictingFiles(projectPath: string, conflicts: string[]): Promise<void>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ansi } from '../utils/ansi.js';
|
|
5
|
+
import { copyDir, templatesDir } from '../utils/template.js';
|
|
6
|
+
// Files that can exist in a directory without being considered conflicts
|
|
7
|
+
const safeExistingFiles = [
|
|
8
|
+
'.DS_Store',
|
|
9
|
+
'.git',
|
|
10
|
+
'.gitignore',
|
|
11
|
+
'.gitattributes',
|
|
12
|
+
'.idea',
|
|
13
|
+
'.vscode',
|
|
14
|
+
'Thumbs.db',
|
|
15
|
+
'LICENSE',
|
|
16
|
+
'README.md',
|
|
17
|
+
];
|
|
18
|
+
export async function createProject(options) {
|
|
19
|
+
const { name, description, author, version, template, claudeCode, projectPath, progressFn } = options;
|
|
20
|
+
await fs.mkdir(projectPath, { recursive: true });
|
|
21
|
+
// Initialize git repo early (before template dependencies that may have git hooks)
|
|
22
|
+
const gitInitialized = tryGitInit(projectPath);
|
|
23
|
+
if (gitInitialized) {
|
|
24
|
+
console.log('\nInitialized a git repository');
|
|
25
|
+
}
|
|
26
|
+
await copyDir(path.join(templatesDir, template), projectPath, {
|
|
27
|
+
name,
|
|
28
|
+
version,
|
|
29
|
+
description,
|
|
30
|
+
author,
|
|
31
|
+
}, progressFn);
|
|
32
|
+
// Optionally apply Claude Code overlay (skills and settings)
|
|
33
|
+
if (claudeCode) {
|
|
34
|
+
await copyDir(path.join(templatesDir, 'claude-code'), projectPath, {
|
|
35
|
+
name,
|
|
36
|
+
version,
|
|
37
|
+
description,
|
|
38
|
+
author,
|
|
39
|
+
}, progressFn);
|
|
40
|
+
}
|
|
41
|
+
// Install latest versions of @telemetryos/sdk and @telemetryos/cli
|
|
42
|
+
console.log('\nInstalling dependencies...');
|
|
43
|
+
await installPackages(projectPath);
|
|
44
|
+
// Create initial commit after all files are in place
|
|
45
|
+
if (gitInitialized) {
|
|
46
|
+
await tryGitCommit(projectPath);
|
|
47
|
+
}
|
|
48
|
+
printSuccessMessage(name, projectPath);
|
|
49
|
+
}
|
|
50
|
+
async function installPackages(projectPath) {
|
|
51
|
+
// Install SDK as a regular dependency
|
|
52
|
+
await new Promise((resolve, reject) => {
|
|
53
|
+
const sdkInstall = spawn('npm', ['install', '@telemetryos/sdk'], {
|
|
54
|
+
cwd: projectPath,
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
shell: true,
|
|
57
|
+
});
|
|
58
|
+
sdkInstall.on('close', (code) => {
|
|
59
|
+
if (code === 0) {
|
|
60
|
+
resolve();
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
reject(new Error(`npm install @telemetryos/sdk failed with code ${code}`));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
sdkInstall.on('error', (error) => {
|
|
67
|
+
reject(error);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// Install CLI as a dev dependency
|
|
71
|
+
await new Promise((resolve, reject) => {
|
|
72
|
+
const cliInstall = spawn('npm', ['install', '-D', '@telemetryos/cli'], {
|
|
73
|
+
cwd: projectPath,
|
|
74
|
+
stdio: 'inherit',
|
|
75
|
+
shell: true,
|
|
76
|
+
});
|
|
77
|
+
cliInstall.on('close', (code) => {
|
|
78
|
+
if (code === 0) {
|
|
79
|
+
resolve();
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
reject(new Error(`npm install -D @telemetryos/cli failed with code ${code}`));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
cliInstall.on('error', (error) => {
|
|
86
|
+
reject(error);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function isInGitRepository(cwd) {
|
|
91
|
+
try {
|
|
92
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore' });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function tryGitInit(projectPath) {
|
|
100
|
+
try {
|
|
101
|
+
execSync('git --version', { stdio: 'ignore' });
|
|
102
|
+
if (isInGitRepository(projectPath)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
execSync('git init', { cwd: projectPath, stdio: 'ignore' });
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function tryGitCommit(projectPath) {
|
|
113
|
+
try {
|
|
114
|
+
execSync('git add -A', { cwd: projectPath, stdio: 'ignore' });
|
|
115
|
+
execSync('git commit -m "Initialize project using TelemetryOS CLI"', {
|
|
116
|
+
cwd: projectPath,
|
|
117
|
+
stdio: 'ignore',
|
|
118
|
+
});
|
|
119
|
+
console.log('Created initial commit');
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Commit failed (e.g., git user not configured)
|
|
124
|
+
// Remove .git directory to avoid half-initialized state
|
|
125
|
+
console.log('Git commit not created');
|
|
126
|
+
console.log('Removing .git directory...');
|
|
127
|
+
try {
|
|
128
|
+
await fs.rm(path.join(projectPath, '.git'), { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Ignore cleanup errors
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function checkDirectoryConflicts(projectPath) {
|
|
137
|
+
try {
|
|
138
|
+
const entries = await fs.readdir(projectPath);
|
|
139
|
+
return entries.filter((file) => !safeExistingFiles.includes(file));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// Directory doesn't exist, no conflicts
|
|
143
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export async function removeConflictingFiles(projectPath, conflicts) {
|
|
150
|
+
for (const file of conflicts) {
|
|
151
|
+
const filePath = path.join(projectPath, file);
|
|
152
|
+
try {
|
|
153
|
+
const stat = await fs.stat(filePath);
|
|
154
|
+
if (stat.isDirectory()) {
|
|
155
|
+
await fs.rm(filePath, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await fs.unlink(filePath);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
// Ignore errors for files that may have already been removed
|
|
163
|
+
console.error(`Warning: Could not remove ${file}: ${error instanceof Error ? error.message : error}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function printSuccessMessage(name, projectPath) {
|
|
168
|
+
// Calculate relative path from cwd for cleaner display
|
|
169
|
+
const relativePath = path.relative(process.cwd(), projectPath);
|
|
170
|
+
const displayPath = relativePath || '.';
|
|
171
|
+
console.log(`
|
|
172
|
+
${ansi.green}Success!${ansi.reset} Created ${ansi.bold}${name}${ansi.reset} at ${ansi.cyan}${displayPath}${ansi.reset}
|
|
173
|
+
|
|
174
|
+
Inside that directory, you can run:
|
|
175
|
+
|
|
176
|
+
${ansi.cyan}npm run dev${ansi.reset}
|
|
177
|
+
Starts the development server
|
|
178
|
+
|
|
179
|
+
${ansi.cyan}npm run build${ansi.reset}
|
|
180
|
+
Builds the app for production
|
|
181
|
+
|
|
182
|
+
You may begin with:
|
|
183
|
+
|
|
184
|
+
${ansi.cyan}cd ${displayPath}${ansi.reset}
|
|
185
|
+
${ansi.cyan}npm run dev${ansi.reset}
|
|
186
|
+
|
|
187
|
+
`);
|
|
188
|
+
}
|
|
@@ -2,6 +2,8 @@ import z from 'zod';
|
|
|
2
2
|
export declare const projectConfigSchema: z.ZodObject<{
|
|
3
3
|
name: z.ZodOptional<z.ZodString>;
|
|
4
4
|
version: z.ZodOptional<z.ZodString>;
|
|
5
|
+
description: z.ZodOptional<z.ZodString>;
|
|
6
|
+
logoPath: z.ZodOptional<z.ZodString>;
|
|
5
7
|
thumbnailPath: z.ZodOptional<z.ZodString>;
|
|
6
8
|
mountPoints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
7
9
|
path: z.ZodString;
|
|
@@ -15,6 +17,7 @@ export declare const projectConfigSchema: z.ZodObject<{
|
|
|
15
17
|
containers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
16
18
|
image: z.ZodString;
|
|
17
19
|
}, z.z.core.$strip>, z.ZodString]>>>;
|
|
20
|
+
useSpaRouting: z.ZodOptional<z.ZodBoolean>;
|
|
18
21
|
devServer: z.ZodOptional<z.ZodObject<{
|
|
19
22
|
runCommand: z.ZodOptional<z.ZodString>;
|
|
20
23
|
url: z.ZodString;
|
|
@@ -16,11 +16,14 @@ const containerSchema = z.object({
|
|
|
16
16
|
export const projectConfigSchema = z.object({
|
|
17
17
|
name: z.string().optional(),
|
|
18
18
|
version: z.string().optional(),
|
|
19
|
+
description: z.string().optional(),
|
|
20
|
+
logoPath: z.string().optional(),
|
|
19
21
|
thumbnailPath: z.string().optional(),
|
|
20
22
|
mountPoints: z.record(z.string(), z.union([mountPointSchema, z.string()])).optional(),
|
|
21
23
|
backgroundWorkers: z.record(z.string(), z.union([backgroundWorkerSchema, z.string()])).optional(),
|
|
22
24
|
serverWorkers: z.record(z.string(), z.union([serverWorkerSchema, z.string()])).optional(),
|
|
23
25
|
containers: z.record(z.string(), z.union([containerSchema, z.string()])).optional(),
|
|
26
|
+
useSpaRouting: z.boolean().optional(),
|
|
24
27
|
devServer: z
|
|
25
28
|
.object({
|
|
26
29
|
runCommand: z.string().optional(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import { readFile } from 'fs/promises';
|
|
2
|
+
import { access, readFile } from 'fs/promises';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import path from 'path';
|
|
@@ -8,6 +8,15 @@ import serveHandler from 'serve-handler';
|
|
|
8
8
|
import pkg from '../../package.json' with { type: 'json' };
|
|
9
9
|
import { loadProjectConfig } from './project-config.js';
|
|
10
10
|
import { ansi, ansiRegex } from '../utils/ansi.js';
|
|
11
|
+
// import { handlePublishCommand } from '../commands/publish.js'
|
|
12
|
+
const IMAGE_MIME_TYPES = {
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.jpeg': 'image/jpeg',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.gif': 'image/gif',
|
|
17
|
+
'.svg': 'image/svg+xml',
|
|
18
|
+
'.webp': 'image/webp',
|
|
19
|
+
};
|
|
11
20
|
export async function runServer(projectPath, flags) {
|
|
12
21
|
printSplashScreen();
|
|
13
22
|
projectPath = path.resolve(process.cwd(), projectPath);
|
|
@@ -36,32 +45,12 @@ async function serveDevelopmentApplicationHostUI(projectPath, port, projectConfi
|
|
|
36
45
|
res.end(JSON.stringify(projectConfig));
|
|
37
46
|
return;
|
|
38
47
|
}
|
|
48
|
+
if (url.pathname === '/__tos-logo__') {
|
|
49
|
+
await serveImageFile(res, projectPath, projectConfig.logoPath, 'logo');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
39
52
|
if (url.pathname === '/__tos-thumbnail__') {
|
|
40
|
-
|
|
41
|
-
res.statusCode = 404;
|
|
42
|
-
res.end('No thumbnail configured');
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const thumbnailFullPath = path.join(projectPath, projectConfig.thumbnailPath);
|
|
46
|
-
try {
|
|
47
|
-
const imageData = await readFile(thumbnailFullPath);
|
|
48
|
-
const ext = path.extname(projectConfig.thumbnailPath).toLowerCase();
|
|
49
|
-
const mimeTypes = {
|
|
50
|
-
'.jpg': 'image/jpeg',
|
|
51
|
-
'.jpeg': 'image/jpeg',
|
|
52
|
-
'.png': 'image/png',
|
|
53
|
-
'.gif': 'image/gif',
|
|
54
|
-
'.svg': 'image/svg+xml',
|
|
55
|
-
'.webp': 'image/webp',
|
|
56
|
-
};
|
|
57
|
-
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
58
|
-
res.setHeader('Content-Type', contentType);
|
|
59
|
-
res.end(imageData);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
res.statusCode = 404;
|
|
63
|
-
res.end('Thumbnail file not found');
|
|
64
|
-
}
|
|
53
|
+
await serveImageFile(res, projectPath, projectConfig.thumbnailPath, 'thumbnail');
|
|
65
54
|
return;
|
|
66
55
|
}
|
|
67
56
|
if (url.pathname === '/__dev_proxy__' && req.method === 'POST' && res) {
|
|
@@ -192,6 +181,54 @@ async function serveDevelopmentApplicationHostUI(projectPath, port, projectConfi
|
|
|
192
181
|
});
|
|
193
182
|
server.listen(port);
|
|
194
183
|
}
|
|
184
|
+
async function serveImageFile(res, projectPath, filePath, label) {
|
|
185
|
+
if (!filePath) {
|
|
186
|
+
res.statusCode = 404;
|
|
187
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
188
|
+
res.end(`No ${label} configured`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const projectRoot = path.resolve(projectPath);
|
|
192
|
+
const validatePath = (fullPath) => fullPath.startsWith(projectRoot + path.sep) || fullPath === projectRoot;
|
|
193
|
+
// Try public/ first (Vite convention), then project root (backward compat)
|
|
194
|
+
const publicPath = path.resolve(projectPath, 'public', filePath);
|
|
195
|
+
const rootPath = path.resolve(projectPath, filePath);
|
|
196
|
+
let resolvedPath;
|
|
197
|
+
if (validatePath(publicPath)) {
|
|
198
|
+
try {
|
|
199
|
+
await access(publicPath);
|
|
200
|
+
resolvedPath = publicPath;
|
|
201
|
+
}
|
|
202
|
+
catch { }
|
|
203
|
+
}
|
|
204
|
+
if (!resolvedPath && validatePath(rootPath)) {
|
|
205
|
+
resolvedPath = rootPath;
|
|
206
|
+
}
|
|
207
|
+
if (!resolvedPath) {
|
|
208
|
+
res.statusCode = 403;
|
|
209
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
210
|
+
res.end('Forbidden: path escapes project root');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const imageData = await readFile(resolvedPath);
|
|
215
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
216
|
+
const contentType = IMAGE_MIME_TYPES[ext] || 'application/octet-stream';
|
|
217
|
+
res.setHeader('Content-Type', contentType);
|
|
218
|
+
res.end(imageData);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
222
|
+
if ((error === null || error === void 0 ? void 0 : error.code) === 'ENOENT') {
|
|
223
|
+
res.statusCode = 404;
|
|
224
|
+
res.end(`${label.charAt(0).toUpperCase() + label.slice(1)} file not found`);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
res.statusCode = 500;
|
|
228
|
+
res.end(`Error reading ${label}: ${(error === null || error === void 0 ? void 0 : error.message) || 'unknown error'}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
195
232
|
async function serveTelemetryApplication(rootPath, projectConfig) {
|
|
196
233
|
return new Promise((resolve, reject) => {
|
|
197
234
|
var _a;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export const templatesDir = path.join(import.meta.dirname, '../../templates');
|
|
4
|
+
const ignoredTemplateFiles = ['.DS_Store', 'thumbs.db', 'node_modules', '.git', 'dist'];
|
|
5
|
+
const dotfileNames = ['_gitignore', '_claude'];
|
|
6
|
+
export async function copyDir(source, destination, replacements, progressFn) {
|
|
7
|
+
const dirListing = await fs.readdir(source);
|
|
8
|
+
for (const dirEntry of dirListing) {
|
|
9
|
+
if (ignoredTemplateFiles.includes(dirEntry))
|
|
10
|
+
continue;
|
|
11
|
+
const sourcePath = path.join(source, dirEntry);
|
|
12
|
+
const destinationPath = path.join(destination, dotfileNames.includes(dirEntry) ? `.${dirEntry.slice(1)}` : dirEntry);
|
|
13
|
+
const stats = await fs.stat(sourcePath);
|
|
14
|
+
if (stats.isDirectory()) {
|
|
15
|
+
await fs.mkdir(destinationPath, { recursive: true });
|
|
16
|
+
await copyDir(sourcePath, destinationPath, replacements, progressFn);
|
|
17
|
+
}
|
|
18
|
+
else if (stats.isFile()) {
|
|
19
|
+
await copyFile(sourcePath, destinationPath, replacements, progressFn);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function copyFile(source, destination, replacements, progressFn) {
|
|
24
|
+
let contents = await fs.readFile(source, 'utf-8');
|
|
25
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
26
|
+
contents = contents.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
|
27
|
+
}
|
|
28
|
+
await fs.writeFile(destination, contents, 'utf-8');
|
|
29
|
+
progressFn(destination);
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetryos/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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,13 +25,13 @@
|
|
|
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.13.1",
|
|
29
29
|
"@types/serve-handler": "^6.1.4",
|
|
30
30
|
"commander": "^14.0.0",
|
|
31
31
|
"ignore": "^6.0.2",
|
|
32
32
|
"inquirer": "^12.9.6",
|
|
33
33
|
"serve-handler": "^6.1.6",
|
|
34
|
-
"tar": "^7.
|
|
34
|
+
"tar": "^7.5.9",
|
|
35
35
|
"zod": "^4.1.12"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -16,33 +16,43 @@ tos serve # Start dev server (or: npm run dev)
|
|
|
16
16
|
Both the render and settings mounts points are visible in the development host.
|
|
17
17
|
The Render mount point is presented in a resizable pane.
|
|
18
18
|
The Settings mount point shows in the right sidebar.
|
|
19
|
+
The Web mount point (if configured) appears as a tab in the development host, displayed in the same area as the other mount points.
|
|
19
20
|
|
|
20
21
|
**The development host is already running!** The user has already started it and the agent doesn't need to run it
|
|
21
22
|
|
|
22
23
|
## Architecture
|
|
23
24
|
|
|
24
|
-
TelemetryOS apps have two mount points:
|
|
25
|
+
TelemetryOS apps have at least two mount points. Apps that need a browser-accessible interface add a third: the Web mount point.
|
|
25
26
|
|
|
26
27
|
| Mount | Purpose | Runs On |
|
|
27
28
|
|-------|---------|---------|
|
|
28
29
|
| `/render` | Content displayed on devices | Physical device (TV, kiosk) |
|
|
29
30
|
| `/settings` | Configuration UI | Studio admin portal |
|
|
31
|
+
| `/web` (optional) | Browser-accessible interface | Any browser (phone, tablet, desktop) |
|
|
30
32
|
|
|
31
|
-
Settings
|
|
33
|
+
Instance store hooks sync Settings ↔ Render. Application store hooks are accessible from Settings, Render, and Web. Web views typically use application and dynamic namespace store hooks for their data.
|
|
32
34
|
|
|
33
35
|
## Project Structure
|
|
34
36
|
|
|
35
37
|
```
|
|
38
|
+
public/
|
|
39
|
+
└── assets/ # Static assets copied to dist/ during build
|
|
40
|
+
└── tos-app.svg # App logo (referenced by telemetry.config.json logoPath)
|
|
36
41
|
src/
|
|
37
42
|
├── index.tsx # Entry point (configure SDK here)
|
|
38
43
|
├── App.tsx # Mount point routing
|
|
39
44
|
├── views/
|
|
40
45
|
│ ├── Settings.tsx # Configuration UI
|
|
41
|
-
│
|
|
46
|
+
│ ├── Render.tsx # Display content
|
|
47
|
+
│ ├── Render.css # Render styles
|
|
48
|
+
│ ├── Web.tsx # Browser interface (if using web)
|
|
49
|
+
│ └── Web.css # Web view styles (if using web)
|
|
42
50
|
└── hooks/
|
|
43
51
|
└── store.ts # Store state hooks
|
|
44
52
|
```
|
|
45
53
|
|
|
54
|
+
> **Note:** Files in `public/` are copied as-is to `dist/` by Vite (without the `public/` prefix). The `logoPath` and `thumbnailPath` in `telemetry.config.json` must reference the output path (e.g., `"assets/tos-app.svg"`, not `"public/assets/tos-app.svg"`).
|
|
55
|
+
|
|
46
56
|
## Core Pattern
|
|
47
57
|
|
|
48
58
|
### Store Hooks (Settings ↔ Render sync)
|
|
@@ -60,6 +70,7 @@ export const useUnitsState = createUseInstanceStoreState<'imperial' | 'metric'>(
|
|
|
60
70
|
import { useCityState } from '../hooks/store'
|
|
61
71
|
|
|
62
72
|
const [isLoading, city, setCity] = useCityState()
|
|
73
|
+
if (isLoading) return <div>Loading...</div>
|
|
63
74
|
```
|
|
64
75
|
|
|
65
76
|
### Settings Components
|
|
@@ -113,6 +124,7 @@ const response = await proxy().fetch('https://api.example.com/data')
|
|
|
113
124
|
| Adding ANY Settings UI | `tos-settings-ui` | SDK components are required - raw HTML won't work |
|
|
114
125
|
| Adding store keys | `tos-store-sync` | Hook patterns ensure Settings↔Render sync |
|
|
115
126
|
| Building multi-mode apps | `tos-multi-mode` | Entity-scoped data, mode switching, namespace patterns |
|
|
127
|
+
| Building web views | `tos-web-ui-design` | Web mount point routing, styling, store access |
|
|
116
128
|
| Calling external APIs | `tos-proxy-fetch` | Proxy patterns prevent CORS errors |
|
|
117
129
|
| Media library access | `tos-media-api` | SDK media methods and types |
|
|
118
130
|
| Weather integration | `tos-weather-api` | API-specific patterns and credentials |
|