@telemetryos/cli 1.10.0 → 1.11.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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # @telemetryos/cli
2
2
 
3
+ ## 1.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added media select component, improved tos init tos publish and tos auth commands, add new store hook
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @telemetryos/development-application-host-ui@1.11.0
13
+
3
14
  ## 1.10.0
4
15
 
5
16
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import inquirer from 'inquirer';
3
- import { loadCliConfig, saveCliConfig } from '../services/cli-config.js';
3
+ import { apiTokenSchema, loadCliConfig, saveCliConfig } from '../services/cli-config.js';
4
4
  export const authCommand = new Command('auth')
5
5
  .description('Authenticate with TelemetryOS by saving your API token')
6
6
  .option('-t, --token <string>', 'API token (skip interactive prompt)')
@@ -12,22 +12,15 @@ function maskToken(token) {
12
12
  return `${token.slice(0, 4)}...${token.slice(-4)}`;
13
13
  }
14
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;
15
+ const result = apiTokenSchema.safeParse(input.trim());
16
+ if (result.success)
17
+ return true;
18
+ return result.error.issues[0].message;
26
19
  }
27
20
  async function handleAuthCommand(options) {
28
21
  let token = options.token;
29
22
  const existingCliConfig = await loadCliConfig();
30
- const existingToken = existingCliConfig === null || existingCliConfig === void 0 ? void 0 : existingCliConfig.apiToken;
23
+ const existingToken = existingCliConfig.apiToken;
31
24
  if (existingToken && !options.force) {
32
25
  const { confirm } = await inquirer.prompt([
33
26
  {
@@ -27,6 +27,7 @@ export const initCommand = new Command('init')
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
29
  .option('-t, --template <string>', 'The template to use (vite-react-typescript)', '')
30
+ .option('-y, --yes', 'Skip all prompts and use defaults', false)
30
31
  .action(handleInitCommand);
31
32
  async function handleInitCommand(projectPathArg, options) {
32
33
  // Step 1: Resolve path and derive name
@@ -79,60 +80,74 @@ async function handleInitCommand(projectPathArg, options) {
79
80
  let author = options.author;
80
81
  let version = options.version;
81
82
  let template = options.template;
82
- // Step 5: Build prompt questions
83
- const questions = [];
84
- // Always show the derived name with option to override
85
- // This provides transparency and control
86
- questions.push({
87
- type: 'input',
88
- name: 'name',
89
- message: 'Project name:',
90
- default: derivedName,
91
- validate: (input) => {
92
- const result = validateProjectName(input);
93
- return result === true ? true : result;
94
- },
95
- });
96
- if (!description)
97
- questions.push({
98
- type: 'input',
99
- name: 'description',
100
- message: 'What is the description of your application?',
101
- default: 'A telemetryOS application',
102
- });
103
- if (!author)
104
- questions.push({
105
- type: 'input',
106
- name: 'author',
107
- message: 'Who is the author of your application?',
108
- default: '',
109
- });
110
- if (!version)
83
+ // Step 5: Build prompt questions (skipped with --yes)
84
+ if (options.yes) {
85
+ if (!name) {
86
+ console.error('Project name is required when using --yes');
87
+ process.exit(1);
88
+ }
89
+ if (!description)
90
+ description = 'A telemetryOS application';
91
+ if (!template)
92
+ template = 'vite-react-typescript';
93
+ if (!version)
94
+ version = '0.1.0';
95
+ }
96
+ else {
97
+ const questions = [];
98
+ // Always show the derived name with option to override
99
+ // This provides transparency and control
111
100
  questions.push({
112
101
  type: 'input',
113
- name: 'version',
114
- message: 'What is the version of your application?',
115
- default: '0.1.0',
102
+ name: 'name',
103
+ message: 'Project name:',
104
+ default: derivedName,
116
105
  validate: (input) => {
117
- if (/^\d+\.\d+\.\d+(-.+)?$/.test(input))
118
- return true;
119
- return 'Version must be in semver format (e.g. 1.0.0)';
106
+ const result = validateProjectName(input);
107
+ return result === true ? true : result;
120
108
  },
121
109
  });
122
- if (!template)
123
- questions.push({
124
- type: 'list',
125
- name: 'template',
126
- message: 'Which template would you like to use?',
127
- choices: [{ name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }],
128
- });
129
- // Step 6: Prompt user
130
- const answers = await inquirer.prompt(questions);
131
- name = answers.name || name;
132
- version = answers.version || version;
133
- description = answers.description || description;
134
- author = answers.author || author;
135
- template = answers.template || template;
110
+ if (!description)
111
+ questions.push({
112
+ type: 'input',
113
+ name: 'description',
114
+ message: 'What is the description of your application?',
115
+ default: 'A telemetryOS application',
116
+ });
117
+ if (!author)
118
+ questions.push({
119
+ type: 'input',
120
+ name: 'author',
121
+ message: 'Who is the author of your application?',
122
+ default: '',
123
+ });
124
+ if (!version)
125
+ questions.push({
126
+ type: 'input',
127
+ name: 'version',
128
+ message: 'What is the version of your application?',
129
+ default: '0.1.0',
130
+ validate: (input) => {
131
+ if (/^\d+\.\d+\.\d+(-.+)?$/.test(input))
132
+ return true;
133
+ return 'Version must be in semver format (e.g. 1.0.0)';
134
+ },
135
+ });
136
+ if (!template)
137
+ questions.push({
138
+ type: 'list',
139
+ name: 'template',
140
+ message: 'Which template would you like to use?',
141
+ choices: [{ name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }],
142
+ });
143
+ // Step 6: Prompt user
144
+ const answers = await inquirer.prompt(questions);
145
+ name = answers.name || name;
146
+ version = answers.version || version;
147
+ description = answers.description || description;
148
+ author = answers.author || author;
149
+ template = answers.template || template;
150
+ }
136
151
  // Step 7: Generate application
137
152
  await generateApplication({
138
153
  name,
@@ -1,2 +1,22 @@
1
1
  import { Command } from 'commander';
2
2
  export declare const publishCommand: Command;
3
+ type PublishOptions = {
4
+ interactive?: boolean;
5
+ name?: string;
6
+ baseImage?: string;
7
+ buildScript?: string;
8
+ buildOutput?: string;
9
+ workingPath?: string;
10
+ };
11
+ type PublishCallbacks = {
12
+ onLog?: (line: string) => void;
13
+ onStateChange?: (state: string) => void;
14
+ onComplete?: (data: {
15
+ success: boolean;
16
+ buildIndex?: number;
17
+ duration?: string;
18
+ }) => void;
19
+ onError?: (error: Error) => void;
20
+ };
21
+ declare function handlePublishCommand(projectPath: string, options: PublishOptions, callbacks?: PublishCallbacks): Promise<void>;
22
+ export { handlePublishCommand, type PublishCallbacks, type PublishOptions };
@@ -35,12 +35,14 @@ function getSpecifiedBuildOptions(options) {
35
35
  specified.push('--working-path');
36
36
  return specified;
37
37
  }
38
- async function handlePublishCommand(projectPath, options) {
39
- var _a, _b, _c, _d;
38
+ async function handlePublishCommand(projectPath, options, callbacks) {
39
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
40
40
  projectPath = path.resolve(process.cwd(), projectPath);
41
+ const logMessage = (callbacks === null || callbacks === void 0 ? void 0 : callbacks.onLog) || ((msg) => console.log(msg));
41
42
  try {
42
43
  // Step 1: Load project config
43
- console.log(`\n${ansi.cyan}Loading project configuration...${ansi.reset}`);
44
+ (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _a === void 0 ? void 0 : _a.call(callbacks, 'loading');
45
+ logMessage(`\n${ansi.cyan}Loading project configuration...${ansi.reset}`);
44
46
  const config = await loadProjectConfig(projectPath);
45
47
  // Step 2: Resolve build configuration
46
48
  let buildConfig = {
@@ -67,93 +69,121 @@ async function handlePublishCommand(projectPath, options) {
67
69
  buildConfig.name = answers.name.trim();
68
70
  }
69
71
  if (!buildConfig.name) {
70
- console.error(`${ansi.red}Error: Application name is required${ansi.reset}`);
71
- process.exit(1);
72
+ const error = new Error('Application name is required');
73
+ logMessage(`${ansi.red}Error: Application name is required${ansi.reset}`);
74
+ (_b = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onError) === null || _b === void 0 ? void 0 : _b.call(callbacks, error);
75
+ if (!callbacks)
76
+ process.exit(1);
77
+ return;
72
78
  }
73
- console.log(` Project: ${ansi.bold}${buildConfig.name}${ansi.reset}`);
79
+ logMessage(` Project: ${ansi.bold}${buildConfig.name}${ansi.reset}`);
74
80
  if (config.version) {
75
- console.log(` Version: ${config.version}`);
81
+ logMessage(` Version: ${config.version}`);
76
82
  }
77
83
  // Step 3: Initialize API client
78
- console.log(`\n${ansi.cyan}Authenticating...${ansi.reset}`);
84
+ (_c = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _c === void 0 ? void 0 : _c.call(callbacks, 'authenticating');
85
+ logMessage(`\n${ansi.cyan}Authenticating...${ansi.reset}`);
79
86
  const apiClient = await ApiClient.create();
80
- console.log(` ${ansi.green}Authenticated${ansi.reset}`);
87
+ logMessage(` ${ansi.green}Authenticated${ansi.reset}`);
81
88
  // Step 4: Create archive
82
- console.log(`\n${ansi.cyan}Archiving project...${ansi.reset}`);
89
+ (_d = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _d === void 0 ? void 0 : _d.call(callbacks, 'archiving');
90
+ logMessage(`\n${ansi.cyan}Archiving project...${ansi.reset}`);
83
91
  const { buffer, filename } = await createArchive(projectPath, (msg) => {
84
- console.log(` ${ansi.dim}${msg}${ansi.reset}`);
92
+ logMessage(` ${ansi.dim}${msg}${ansi.reset}`);
85
93
  });
86
94
  // Step 5: Find or create application
87
- console.log(`\n${ansi.cyan}Checking for existing application...${ansi.reset}`);
95
+ logMessage(`\n${ansi.cyan}Checking for existing application...${ansi.reset}`);
88
96
  const appsRes = await apiClient.get('/application');
89
97
  const apps = (await appsRes.json());
90
98
  let app = apps.find((a) => a.title === buildConfig.name) || null;
91
99
  if (app) {
92
- console.log(` Found existing application: ${ansi.dim}${app.id}${ansi.reset}`);
100
+ logMessage(` Found existing application: ${ansi.dim}${app.id}${ansi.reset}`);
93
101
  const specifiedOptions = getSpecifiedBuildOptions(options);
94
102
  if (specifiedOptions.length > 0) {
95
- console.log(` ${ansi.yellow}Warning: ${specifiedOptions.join(', ')} ignored for existing application${ansi.reset}`);
103
+ logMessage(` ${ansi.yellow}Warning: ${specifiedOptions.join(', ')} ignored for existing application${ansi.reset}`);
96
104
  }
97
105
  }
98
106
  else {
99
- console.log(` Creating new application...`);
107
+ logMessage(` Creating new application...`);
100
108
  const createRequest = {
101
109
  kind: 'uploaded',
102
110
  title: buildConfig.name,
103
- baseImage: (_a = buildConfig.baseImage) !== null && _a !== void 0 ? _a : APP_BUILD_DEFAULTS.baseImage,
111
+ baseImage: (_e = buildConfig.baseImage) !== null && _e !== void 0 ? _e : APP_BUILD_DEFAULTS.baseImage,
104
112
  baseImageRegistryAuth: '',
105
- buildWorkingPath: (_b = buildConfig.workingPath) !== null && _b !== void 0 ? _b : APP_BUILD_DEFAULTS.workingPath,
106
- buildScript: (_c = buildConfig.buildScript) !== null && _c !== void 0 ? _c : APP_BUILD_DEFAULTS.buildScript,
107
- buildOutputPath: (_d = buildConfig.buildOutput) !== null && _d !== void 0 ? _d : APP_BUILD_DEFAULTS.buildOutput,
113
+ buildWorkingPath: (_f = buildConfig.workingPath) !== null && _f !== void 0 ? _f : APP_BUILD_DEFAULTS.workingPath,
114
+ buildScript: (_g = buildConfig.buildScript) !== null && _g !== void 0 ? _g : APP_BUILD_DEFAULTS.buildScript,
115
+ buildOutputPath: (_h = buildConfig.buildOutput) !== null && _h !== void 0 ? _h : APP_BUILD_DEFAULTS.buildOutput,
108
116
  };
109
117
  const createRes = await apiClient.post('/application', { body: createRequest });
110
118
  app = (await createRes.json());
111
- console.log(` ${ansi.green}Created application:${ansi.reset} ${ansi.dim}${app.id}${ansi.reset}`);
119
+ logMessage(` ${ansi.green}Created application:${ansi.reset} ${ansi.dim}${app.id}${ansi.reset}`);
112
120
  }
113
121
  // Step 6: Upload archive
114
- console.log(`\n${ansi.cyan}Uploading archive...${ansi.reset}`);
122
+ (_j = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _j === void 0 ? void 0 : _j.call(callbacks, 'uploading');
123
+ logMessage(`\n${ansi.cyan}Uploading archive...${ansi.reset}`);
115
124
  const formData = new FormData();
116
125
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
117
126
  const blob = new Blob([arrayBuffer], { type: 'application/gzip' });
118
127
  formData.append('file', blob, filename);
119
128
  await apiClient.post(`/application/${app.id}/archive`, { body: formData });
120
- console.log(` ${ansi.green}Upload complete${ansi.reset}`);
129
+ logMessage(` ${ansi.green}Upload complete${ansi.reset}`);
121
130
  // Step 7: Poll for build status and stream logs
122
- console.log(`\n${ansi.cyan}Building...${ansi.reset}`);
123
- console.log(`${ansi.white}Build Logs${ansi.reset}`);
131
+ (_k = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _k === void 0 ? void 0 : _k.call(callbacks, 'building');
132
+ logMessage(`\n${ansi.cyan}Building...${ansi.reset}`);
133
+ logMessage(`${ansi.white}Build Logs${ansi.reset}`);
124
134
  const build = await pollBuild(apiClient, app.id, {
125
135
  onLog: (line) => {
126
- console.log(` ${ansi.dim}${line}${ansi.reset}`);
136
+ logMessage(` ${ansi.dim}${line}${ansi.reset}`);
127
137
  },
128
138
  onStateChange: () => { },
129
139
  onComplete: () => { },
130
140
  onError: (error) => {
131
- console.error(`${ansi.red}Polling error: ${error.message}${ansi.reset}`);
141
+ var _a;
142
+ logMessage(`${ansi.red}Polling error: ${error.message}${ansi.reset}`);
143
+ (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onError) === null || _a === void 0 ? void 0 : _a.call(callbacks, error);
132
144
  },
133
145
  });
134
146
  // Step 8: Report result
135
147
  if (build.state === 'success') {
136
- console.log(`\n${ansi.green}${ansi.bold}Build successful!${ansi.reset}`);
137
- console.log(`\n Application: ${buildConfig.name}`);
138
- console.log(` Build: #${build.index}`);
148
+ (_l = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _l === void 0 ? void 0 : _l.call(callbacks, 'success');
149
+ logMessage(`\n${ansi.green}${ansi.bold}Build successful!${ansi.reset}`);
150
+ logMessage(`\n Application: ${buildConfig.name}`);
151
+ logMessage(` Build: #${build.index}`);
152
+ let duration = '';
139
153
  if (build.finishedAt && build.startedAt) {
140
- const duration = calculateDuration(build.startedAt, build.finishedAt);
141
- console.log(` Duration: ${duration}`);
154
+ duration = calculateDuration(build.startedAt, build.finishedAt);
155
+ logMessage(` Duration: ${duration}`);
142
156
  }
143
- console.log('');
157
+ logMessage('');
158
+ (_m = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onComplete) === null || _m === void 0 ? void 0 : _m.call(callbacks, {
159
+ success: true,
160
+ buildIndex: build.index,
161
+ duration,
162
+ });
144
163
  }
145
164
  else {
146
- console.log(`\n${ansi.red}${ansi.bold}Build failed${ansi.reset}`);
147
- console.log(`\n State: ${build.state}`);
148
- console.log('');
149
- process.exit(1);
165
+ (_o = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _o === void 0 ? void 0 : _o.call(callbacks, 'failure');
166
+ logMessage(`\n${ansi.red}${ansi.bold}Build failed${ansi.reset}`);
167
+ logMessage(`\n State: ${build.state}`);
168
+ logMessage('');
169
+ const error = new Error(`Build failed with state: ${build.state}`);
170
+ (_p = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onError) === null || _p === void 0 ? void 0 : _p.call(callbacks, error);
171
+ if (!callbacks) {
172
+ process.exit(1);
173
+ }
150
174
  }
151
175
  }
152
176
  catch (error) {
153
- console.error(`\n${ansi.red}Error: ${error.message}${ansi.reset}\n`);
154
- process.exit(1);
177
+ const err = error;
178
+ logMessage(`\n${ansi.red}Error: ${err.message}${ansi.reset}\n`);
179
+ (_q = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onError) === null || _q === void 0 ? void 0 : _q.call(callbacks, err);
180
+ if (!callbacks) {
181
+ process.exit(1);
182
+ }
155
183
  }
156
184
  }
185
+ // Export for use by server
186
+ export { handlePublishCommand };
157
187
  async function promptBuildConfig(defaults) {
158
188
  var _a, _b, _c, _d;
159
189
  const answers = await inquirer.prompt([
@@ -7,7 +7,7 @@ export class ApiClient {
7
7
  }
8
8
  static async create() {
9
9
  const config = await loadCliConfig();
10
- if (!(config === null || config === void 0 ? void 0 : config.apiToken)) {
10
+ if (!config.apiToken) {
11
11
  throw new Error('Not authenticated. Run `tos auth` first.');
12
12
  }
13
13
  const baseUrl = process.env.TOS_API_URL || config.apiUrl || DEFAULT_API_URL;
@@ -1,6 +1,10 @@
1
- export type CliConfig = {
2
- apiToken?: string;
3
- apiUrl?: string;
4
- };
5
- export declare function loadCliConfig(): Promise<CliConfig | null>;
1
+ import z from 'zod';
2
+ export declare const apiTokenSchema: z.ZodString;
3
+ declare const cliConfigSchema: z.ZodObject<{
4
+ apiToken: z.ZodOptional<z.ZodString>;
5
+ apiUrl: z.ZodOptional<z.ZodURL>;
6
+ }, z.z.core.$strip>;
7
+ export type CliConfig = z.infer<typeof cliConfigSchema>;
8
+ export declare function loadCliConfig(): Promise<CliConfig>;
6
9
  export declare function saveCliConfig(config: CliConfig): Promise<void>;
10
+ export {};
@@ -1,23 +1,45 @@
1
1
  import { mkdir, readFile, writeFile } from 'fs/promises';
2
2
  import { homedir } from 'os';
3
3
  import path from 'path';
4
+ import z from 'zod';
4
5
  function getCliConfigDir() {
5
6
  const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config');
6
- return path.join(xdgConfig, 'telemetryos-cli');
7
+ return path.join(xdgConfig, 'telemetryos', 'cli');
7
8
  }
8
9
  function getCliConfigPath() {
9
10
  return path.join(getCliConfigDir(), 'config.json');
10
11
  }
12
+ export const apiTokenSchema = z
13
+ .string()
14
+ .regex(/^u[a-f0-9]{32}$/i, 'Token must start with "u" followed by 32 hex characters');
15
+ const cliConfigSchema = z.object({
16
+ apiToken: z.string().optional(),
17
+ apiUrl: z.url().optional(),
18
+ });
11
19
  export async function loadCliConfig() {
12
20
  try {
13
- const content = await readFile(getCliConfigPath(), 'utf-8');
14
- return JSON.parse(content);
21
+ const configPath = getCliConfigPath();
22
+ const content = await readFile(configPath, 'utf-8');
23
+ const parsed = JSON.parse(content);
24
+ return cliConfigSchema.parse(parsed);
15
25
  }
16
- catch {
17
- return null;
26
+ catch (error) {
27
+ if (error.code === 'ENOENT') {
28
+ return cliConfigSchema.parse({});
29
+ }
30
+ const configPath = getCliConfigPath();
31
+ if (error instanceof SyntaxError) {
32
+ throw new Error(`Invalid JSON in CLI config file (${configPath}):\n ${error.message}`);
33
+ }
34
+ if (error instanceof z.ZodError) {
35
+ const issues = error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
36
+ throw new Error(`Invalid CLI config file (${configPath}):\n${issues}`);
37
+ }
38
+ throw error;
18
39
  }
19
40
  }
20
41
  export async function saveCliConfig(config) {
42
+ const validated = cliConfigSchema.parse(config);
21
43
  await mkdir(getCliConfigDir(), { recursive: true });
22
- await writeFile(getCliConfigPath(), JSON.stringify(config, null, 2));
44
+ await writeFile(getCliConfigPath(), JSON.stringify(validated, null, 2));
23
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetryos/cli",
3
- "version": "1.10.0",
3
+ "version": "1.11.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.10.0",
28
+ "@telemetryos/development-application-host-ui": "^1.11.0",
29
29
  "@types/serve-handler": "^6.1.4",
30
30
  "commander": "^14.0.0",
31
31
  "ignore": "^6.0.2",
@@ -15,7 +15,9 @@ tos serve # Start dev server (or: npm run dev)
15
15
  **Development Host:** http://localhost:2026
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
- The Settings mount point shows in the settings tab of the right sidebar.
18
+ The Settings mount point shows in the right sidebar.
19
+
20
+ **The development host is already running!** The user has already started it and the agent doesn't need to run it
19
21
 
20
22
  ## Architecture
21
23
 
@@ -100,12 +102,17 @@ const response = await proxy().fetch('https://api.example.com/data')
100
102
 
101
103
  **IMPORTANT:** You MUST invoke the relevant skill BEFORE writing code for these tasks:
102
104
 
105
+ **For Render views:** ALWAYS read `tos-render-ui-design` first, then the appropriate specialized skill.
106
+
103
107
  | Task | Required Skill | Why |
104
108
  |------|----------------|-----|
105
109
  | Starting new project | `tos-requirements` | Gather requirements before coding MUST USE |
106
- | Building Render views | `tos-render-design` | Digital signage constraints, UI scaling, no hover/scroll |
110
+ | Building ANY Render view | `tos-render-ui-design` | UI scaling foundation - ALWAYS read first |
111
+ | Building digital signage | `tos-render-signage-design` | Display-only patterns (no interaction) |
112
+ | Building interactive kiosk | `tos-render-kiosk-design` | Touch interaction, idle timeout, navigation |
107
113
  | Adding ANY Settings UI | `tos-settings-ui` | SDK components are required - raw HTML won't work |
108
114
  | Adding store keys | `tos-store-sync` | Hook patterns ensure Settings↔Render sync |
115
+ | Building multi-mode apps | `tos-multi-mode` | Entity-scoped data, mode switching, namespace patterns |
109
116
  | Calling external APIs | `tos-proxy-fetch` | Proxy patterns prevent CORS errors |
110
117
  | Media library access | `tos-media-api` | SDK media methods and types |
111
118
  | Weather integration | `tos-weather-api` | API-specific patterns and credentials |
@@ -232,15 +232,11 @@ export function App() {
232
232
 
233
233
  ### Local Development
234
234
 
235
- ```bash
236
- # Start dev server
237
- tos serve
238
- # Or: npm run dev
235
+ **Development Host:** http://localhost:2026
236
+ Both the render and settings mounts points are visible in the development host.
237
+ The Render mount point is presented in a resizable pane.
238
+ The Settings mount point shows in the right sidebar.
239
239
 
240
- # Access locally:
241
- # Settings: http://localhost:3000/settings
242
- # Render: http://localhost:3000/render
243
- ```
244
240
 
245
241
  ### Build & Deploy
246
242
 
@@ -255,26 +251,6 @@ git add . && git commit -m "Update" && git push
255
251
 
256
252
  ## Common Patterns
257
253
 
258
- ### Check Mount Point
259
-
260
- ```typescript
261
- const isSettings = window.location.pathname === '/settings'
262
- const isRender = window.location.pathname === '/render'
263
- ```
264
-
265
- ### Conditional Features
266
-
267
- ```typescript
268
- // In a shared component
269
- const isRender = window.location.pathname === '/render'
270
-
271
- // Only fetch external data in Render
272
- useEffect(() => {
273
- if (!isRender) return
274
- fetchExternalData()
275
- }, [isRender])
276
- ```
277
-
278
254
  ### Handle Missing Config
279
255
 
280
256
  ```typescript