@telemetryos/cli 1.9.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 +25 -0
- package/dist/commands/auth.js +8 -15
- package/dist/commands/init.js +131 -68
- package/dist/commands/publish.d.ts +22 -0
- package/dist/commands/publish.js +238 -0
- package/dist/index.js +2 -0
- package/dist/plugins/math-tools.d.ts +2 -0
- package/dist/plugins/math-tools.js +18 -0
- package/dist/services/api-client.d.ts +18 -0
- package/dist/services/api-client.js +70 -0
- package/dist/services/archiver.d.ts +4 -0
- package/dist/services/archiver.js +65 -0
- package/dist/services/build-poller.d.ts +10 -0
- package/dist/services/build-poller.js +63 -0
- package/dist/services/cli-config.d.ts +10 -0
- package/dist/services/cli-config.js +45 -0
- package/dist/services/generate-application.d.ts +2 -1
- package/dist/services/generate-application.js +31 -32
- package/dist/services/project-config.d.ts +24 -0
- package/dist/services/project-config.js +51 -0
- package/dist/services/run-server.js +29 -73
- package/dist/types/api.d.ts +44 -0
- package/dist/types/api.js +1 -0
- package/dist/types/applications.d.ts +44 -0
- package/dist/types/applications.js +1 -0
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +10 -0
- package/dist/utils/path-utils.d.ts +55 -0
- package/dist/utils/path-utils.js +99 -0
- package/package.json +4 -2
- package/templates/vite-react-typescript/CLAUDE.md +14 -6
- package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +4 -28
- package/templates/vite-react-typescript/_claude/skills/tos-multi-mode/SKILL.md +359 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
- package/templates/vite-react-typescript/_claude/skills/tos-render-kiosk-design/SKILL.md +384 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-signage-design/SKILL.md +515 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-ui-design/SKILL.md +325 -0
- package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +405 -125
- package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +96 -5
- package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +443 -269
- package/templates/vite-react-typescript/index.html +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { loadCliConfig } from './cli-config.js';
|
|
2
|
+
const DEFAULT_API_URL = 'https://api.telemetryos.com';
|
|
3
|
+
export class ApiClient {
|
|
4
|
+
constructor(baseUrl, token) {
|
|
5
|
+
this.baseUrl = baseUrl;
|
|
6
|
+
this.token = token;
|
|
7
|
+
}
|
|
8
|
+
static async create() {
|
|
9
|
+
const config = await loadCliConfig();
|
|
10
|
+
if (!config.apiToken) {
|
|
11
|
+
throw new Error('Not authenticated. Run `tos auth` first.');
|
|
12
|
+
}
|
|
13
|
+
const baseUrl = process.env.TOS_API_URL || config.apiUrl || DEFAULT_API_URL;
|
|
14
|
+
return new ApiClient(baseUrl, config.apiToken);
|
|
15
|
+
}
|
|
16
|
+
async get(path, opts) {
|
|
17
|
+
return this.request('GET', path, opts);
|
|
18
|
+
}
|
|
19
|
+
async post(path, opts) {
|
|
20
|
+
return this.request('POST', path, opts);
|
|
21
|
+
}
|
|
22
|
+
async put(path, opts) {
|
|
23
|
+
return this.request('PUT', path, opts);
|
|
24
|
+
}
|
|
25
|
+
async patch(path, opts) {
|
|
26
|
+
return this.request('PATCH', path, opts);
|
|
27
|
+
}
|
|
28
|
+
async delete(path, opts) {
|
|
29
|
+
return this.request('DELETE', path, opts);
|
|
30
|
+
}
|
|
31
|
+
async request(method, path, opts) {
|
|
32
|
+
const url = this.buildUrl(path, opts === null || opts === void 0 ? void 0 : opts.query);
|
|
33
|
+
const headers = {
|
|
34
|
+
Authorization: `Bearer ${this.token}`,
|
|
35
|
+
...opts === null || opts === void 0 ? void 0 : opts.headers,
|
|
36
|
+
};
|
|
37
|
+
// Don't set Content-Type for FormData - fetch sets it with boundary
|
|
38
|
+
if ((opts === null || opts === void 0 ? void 0 : opts.body) && !(opts.body instanceof FormData)) {
|
|
39
|
+
headers['Content-Type'] = 'application/json';
|
|
40
|
+
}
|
|
41
|
+
const response = await fetch(url, {
|
|
42
|
+
method,
|
|
43
|
+
headers,
|
|
44
|
+
body: (opts === null || opts === void 0 ? void 0 : opts.body) instanceof FormData
|
|
45
|
+
? opts.body
|
|
46
|
+
: (opts === null || opts === void 0 ? void 0 : opts.body)
|
|
47
|
+
? JSON.stringify(opts.body)
|
|
48
|
+
: undefined,
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const errorBody = await response.text().catch(() => 'Unknown error');
|
|
52
|
+
if (response.status === 401) {
|
|
53
|
+
throw new Error('Authentication failed. Run `tos auth` to re-authenticate.');
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`API Error ${response.status}: ${errorBody}`);
|
|
56
|
+
}
|
|
57
|
+
return response;
|
|
58
|
+
}
|
|
59
|
+
buildUrl(path, query) {
|
|
60
|
+
const url = new URL(path, this.baseUrl);
|
|
61
|
+
if (query) {
|
|
62
|
+
for (const [key, value] of Object.entries(query)) {
|
|
63
|
+
if (value !== undefined && value !== null) {
|
|
64
|
+
url.searchParams.append(key, String(value));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return url.toString();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile, stat, unlink } from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import * as tar from 'tar';
|
|
5
|
+
import ignoreModule from 'ignore';
|
|
6
|
+
const ignore = ignoreModule.default || ignoreModule;
|
|
7
|
+
const DEFAULT_IGNORES = [
|
|
8
|
+
'node_modules',
|
|
9
|
+
'.git',
|
|
10
|
+
'dist',
|
|
11
|
+
'.DS_Store',
|
|
12
|
+
'*.log',
|
|
13
|
+
'.env',
|
|
14
|
+
'.env.*',
|
|
15
|
+
'.turbo',
|
|
16
|
+
'coverage',
|
|
17
|
+
];
|
|
18
|
+
export async function createArchive(projectPath, onProgress) {
|
|
19
|
+
const projectName = path.basename(projectPath);
|
|
20
|
+
const filename = `${projectName}-${Date.now()}.tar.gz`;
|
|
21
|
+
const tempPath = path.join(tmpdir(), filename);
|
|
22
|
+
// Build ignore filter
|
|
23
|
+
const ig = ignore();
|
|
24
|
+
// Load .gitignore patterns if present, otherwise use defaults
|
|
25
|
+
try {
|
|
26
|
+
const gitignoreContent = await readFile(path.join(projectPath, '.gitignore'), 'utf-8');
|
|
27
|
+
ig.add(gitignoreContent);
|
|
28
|
+
ig.add('.git'); // .git is always ignored by git itself
|
|
29
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress('Using .gitignore patterns');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
ig.add(DEFAULT_IGNORES);
|
|
33
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress('No .gitignore found, using default exclusions');
|
|
34
|
+
}
|
|
35
|
+
// Create tar.gz archive
|
|
36
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress('Creating archive...');
|
|
37
|
+
await tar.create({
|
|
38
|
+
gzip: true,
|
|
39
|
+
file: tempPath,
|
|
40
|
+
cwd: projectPath,
|
|
41
|
+
filter: (filePath) => {
|
|
42
|
+
// Always include root
|
|
43
|
+
if (filePath === '.')
|
|
44
|
+
return true;
|
|
45
|
+
// Normalize path for ignore matching
|
|
46
|
+
const relativePath = filePath.startsWith('./') ? filePath.slice(2) : filePath;
|
|
47
|
+
const ignored = ig.ignores(relativePath);
|
|
48
|
+
if (!ignored)
|
|
49
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress(` ${relativePath}`);
|
|
50
|
+
return !ignored;
|
|
51
|
+
},
|
|
52
|
+
}, ['.']);
|
|
53
|
+
const buffer = await readFile(tempPath);
|
|
54
|
+
const stats = await stat(tempPath);
|
|
55
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress(`Archive created: ${formatBytes(stats.size)}`);
|
|
56
|
+
await unlink(tempPath).catch(() => { });
|
|
57
|
+
return { buffer, filename };
|
|
58
|
+
}
|
|
59
|
+
function formatBytes(bytes) {
|
|
60
|
+
if (bytes < 1024)
|
|
61
|
+
return `${bytes} B`;
|
|
62
|
+
if (bytes < 1024 * 1024)
|
|
63
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
64
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ApiClient } from './api-client.js';
|
|
2
|
+
import type { ApplicationBuild } from '../types/applications.js';
|
|
3
|
+
type BuildPollerCallbacks = {
|
|
4
|
+
onLog: (line: string) => void;
|
|
5
|
+
onStateChange: (state: string) => void;
|
|
6
|
+
onComplete: (build: ApplicationBuild) => void;
|
|
7
|
+
onError: (error: Error) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function pollBuild(apiClient: ApiClient, applicationId: string, callbacks: BuildPollerCallbacks): Promise<ApplicationBuild>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const POLL_INTERVAL = 1000; // 1 second, matching Studio UI
|
|
2
|
+
const TERMINAL_STATES = ['success', 'failure', 'failed', 'cancelled'];
|
|
3
|
+
export async function pollBuild(apiClient, applicationId, callbacks) {
|
|
4
|
+
let lastLogIndex = 0;
|
|
5
|
+
let lastState = '';
|
|
6
|
+
let lastBuildId = null;
|
|
7
|
+
const poll = async () => {
|
|
8
|
+
try {
|
|
9
|
+
const res = await apiClient.get(`/application/${applicationId}/build`);
|
|
10
|
+
const builds = (await res.json());
|
|
11
|
+
// Get the most recent build (highest index)
|
|
12
|
+
const build = builds.sort((a, b) => b.index - a.index)[0];
|
|
13
|
+
if (!build)
|
|
14
|
+
return null;
|
|
15
|
+
// Reset log tracking when build changes
|
|
16
|
+
if (build.id !== lastBuildId) {
|
|
17
|
+
lastBuildId = build.id;
|
|
18
|
+
lastLogIndex = 0;
|
|
19
|
+
}
|
|
20
|
+
// Notify on state change
|
|
21
|
+
if (build.state !== lastState) {
|
|
22
|
+
lastState = build.state;
|
|
23
|
+
callbacks.onStateChange(build.state);
|
|
24
|
+
}
|
|
25
|
+
// Stream new log lines (logs array grows incrementally)
|
|
26
|
+
if (build.logs && build.logs.length > lastLogIndex) {
|
|
27
|
+
const newLogs = build.logs.slice(lastLogIndex);
|
|
28
|
+
newLogs.forEach((line) => callbacks.onLog(line));
|
|
29
|
+
lastLogIndex = build.logs.length;
|
|
30
|
+
}
|
|
31
|
+
// Check if complete
|
|
32
|
+
if (TERMINAL_STATES.includes(build.state)) {
|
|
33
|
+
callbacks.onComplete(build);
|
|
34
|
+
return build;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
callbacks.onError(error);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
// Initial poll - build may already exist
|
|
44
|
+
let result = await poll();
|
|
45
|
+
if (result)
|
|
46
|
+
return result;
|
|
47
|
+
// Continue polling until terminal state
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const interval = setInterval(async () => {
|
|
50
|
+
try {
|
|
51
|
+
result = await poll();
|
|
52
|
+
if (result) {
|
|
53
|
+
clearInterval(interval);
|
|
54
|
+
resolve(result);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
clearInterval(interval);
|
|
59
|
+
reject(error);
|
|
60
|
+
}
|
|
61
|
+
}, POLL_INTERVAL);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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>;
|
|
9
|
+
export declare function saveCliConfig(config: CliConfig): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import z from 'zod';
|
|
5
|
+
function getCliConfigDir() {
|
|
6
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config');
|
|
7
|
+
return path.join(xdgConfig, 'telemetryos', 'cli');
|
|
8
|
+
}
|
|
9
|
+
function getCliConfigPath() {
|
|
10
|
+
return path.join(getCliConfigDir(), 'config.json');
|
|
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
|
+
});
|
|
19
|
+
export async function loadCliConfig() {
|
|
20
|
+
try {
|
|
21
|
+
const configPath = getCliConfigPath();
|
|
22
|
+
const content = await readFile(configPath, 'utf-8');
|
|
23
|
+
const parsed = JSON.parse(content);
|
|
24
|
+
return cliConfigSchema.parse(parsed);
|
|
25
|
+
}
|
|
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;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function saveCliConfig(config) {
|
|
42
|
+
const validated = cliConfigSchema.parse(config);
|
|
43
|
+
await mkdir(getCliConfigDir(), { recursive: true });
|
|
44
|
+
await writeFile(getCliConfigPath(), JSON.stringify(validated, null, 2));
|
|
45
|
+
}
|
|
@@ -6,6 +6,7 @@ export type GenerateApplicationOptions = {
|
|
|
6
6
|
template: string;
|
|
7
7
|
projectPath: string;
|
|
8
8
|
progressFn: (createdFilePath: string) => void;
|
|
9
|
-
confirmOverwrite?: () => Promise<boolean>;
|
|
10
9
|
};
|
|
11
10
|
export declare function generateApplication(options: GenerateApplicationOptions): Promise<void>;
|
|
11
|
+
export declare function checkDirectoryConflicts(projectPath: string): Promise<string[]>;
|
|
12
|
+
export declare function removeConflictingFiles(projectPath: string, conflicts: string[]): Promise<void>;
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { spawn, execSync } from 'child_process';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { ansi } from '../utils/ansi.js';
|
|
4
5
|
const ignoredTemplateFiles = ['.DS_Store', 'thumbs.db', 'node_modules', '.git', 'dist'];
|
|
5
6
|
const templatesDir = path.join(import.meta.dirname, '../../templates');
|
|
6
7
|
const dotfileNames = ['_gitignore', '_claude'];
|
|
7
|
-
// ANSI color codes for terminal output
|
|
8
|
-
const ansiGreen = '\u001b[32m';
|
|
9
|
-
const ansiCyan = '\u001b[36m';
|
|
10
|
-
const ansiBold = '\u001b[1m';
|
|
11
|
-
const ansiReset = '\u001b[0m';
|
|
12
8
|
// Files that can exist in a directory without being considered conflicts
|
|
13
9
|
const safeExistingFiles = [
|
|
14
10
|
'.DS_Store',
|
|
@@ -22,25 +18,7 @@ const safeExistingFiles = [
|
|
|
22
18
|
'README.md',
|
|
23
19
|
];
|
|
24
20
|
export async function generateApplication(options) {
|
|
25
|
-
const { name, description, author, version, template, projectPath, progressFn
|
|
26
|
-
// Check for directory conflicts before proceeding
|
|
27
|
-
const conflicts = await checkDirectoryConflicts(projectPath);
|
|
28
|
-
if (conflicts.length > 0) {
|
|
29
|
-
console.log(`\nThe directory ${name} contains files that could conflict:\n`);
|
|
30
|
-
conflicts.forEach((file) => console.log(` ${file}`));
|
|
31
|
-
console.log();
|
|
32
|
-
if (confirmOverwrite) {
|
|
33
|
-
const proceed = await confirmOverwrite();
|
|
34
|
-
if (!proceed) {
|
|
35
|
-
console.log('Aborting installation');
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
console.log('Aborting installation');
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
21
|
+
const { name, description, author, version, template, projectPath, progressFn } = options;
|
|
44
22
|
await fs.mkdir(projectPath, { recursive: true });
|
|
45
23
|
// Initialize git repo early (before template dependencies that may have git hooks)
|
|
46
24
|
const gitInitialized = tryGitInit(projectPath);
|
|
@@ -68,7 +46,7 @@ async function installPackages(projectPath) {
|
|
|
68
46
|
const sdkInstall = spawn('npm', ['install', '@telemetryos/sdk'], {
|
|
69
47
|
cwd: projectPath,
|
|
70
48
|
stdio: 'inherit',
|
|
71
|
-
shell: true
|
|
49
|
+
shell: true,
|
|
72
50
|
});
|
|
73
51
|
sdkInstall.on('close', (code) => {
|
|
74
52
|
if (code === 0) {
|
|
@@ -87,7 +65,7 @@ async function installPackages(projectPath) {
|
|
|
87
65
|
const cliInstall = spawn('npm', ['install', '-D', '@telemetryos/cli'], {
|
|
88
66
|
cwd: projectPath,
|
|
89
67
|
stdio: 'inherit',
|
|
90
|
-
shell: true
|
|
68
|
+
shell: true,
|
|
91
69
|
});
|
|
92
70
|
cliInstall.on('close', (code) => {
|
|
93
71
|
if (code === 0) {
|
|
@@ -148,7 +126,7 @@ async function tryGitCommit(projectPath) {
|
|
|
148
126
|
return false;
|
|
149
127
|
}
|
|
150
128
|
}
|
|
151
|
-
async function checkDirectoryConflicts(projectPath) {
|
|
129
|
+
export async function checkDirectoryConflicts(projectPath) {
|
|
152
130
|
try {
|
|
153
131
|
const entries = await fs.readdir(projectPath);
|
|
154
132
|
return entries.filter((file) => !safeExistingFiles.includes(file));
|
|
@@ -161,22 +139,43 @@ async function checkDirectoryConflicts(projectPath) {
|
|
|
161
139
|
throw error;
|
|
162
140
|
}
|
|
163
141
|
}
|
|
142
|
+
export async function removeConflictingFiles(projectPath, conflicts) {
|
|
143
|
+
for (const file of conflicts) {
|
|
144
|
+
const filePath = path.join(projectPath, file);
|
|
145
|
+
try {
|
|
146
|
+
const stat = await fs.stat(filePath);
|
|
147
|
+
if (stat.isDirectory()) {
|
|
148
|
+
await fs.rm(filePath, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await fs.unlink(filePath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
// Ignore errors for files that may have already been removed
|
|
156
|
+
console.error(`Warning: Could not remove ${file}: ${error instanceof Error ? error.message : error}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
164
160
|
function printSuccessMessage(name, projectPath) {
|
|
161
|
+
// Calculate relative path from cwd for cleaner display
|
|
162
|
+
const relativePath = path.relative(process.cwd(), projectPath);
|
|
163
|
+
const displayPath = relativePath || '.';
|
|
165
164
|
console.log(`
|
|
166
|
-
${
|
|
165
|
+
${ansi.green}Success!${ansi.reset} Created ${ansi.bold}${name}${ansi.reset} at ${ansi.cyan}${displayPath}${ansi.reset}
|
|
167
166
|
|
|
168
167
|
Inside that directory, you can run:
|
|
169
168
|
|
|
170
|
-
${
|
|
169
|
+
${ansi.cyan}npm run dev${ansi.reset}
|
|
171
170
|
Starts the development server
|
|
172
171
|
|
|
173
|
-
${
|
|
172
|
+
${ansi.cyan}npm run build${ansi.reset}
|
|
174
173
|
Builds the app for production
|
|
175
174
|
|
|
176
175
|
You may begin with:
|
|
177
176
|
|
|
178
|
-
${
|
|
179
|
-
${
|
|
177
|
+
${ansi.cyan}cd ${displayPath}${ansi.reset}
|
|
178
|
+
${ansi.cyan}npm run dev${ansi.reset}
|
|
180
179
|
|
|
181
180
|
`);
|
|
182
181
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export declare const projectConfigSchema: z.ZodObject<{
|
|
3
|
+
name: z.ZodOptional<z.ZodString>;
|
|
4
|
+
version: z.ZodOptional<z.ZodString>;
|
|
5
|
+
thumbnailPath: z.ZodOptional<z.ZodString>;
|
|
6
|
+
mountPoints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
7
|
+
path: z.ZodString;
|
|
8
|
+
}, z.z.core.$strip>, z.ZodString]>>>;
|
|
9
|
+
backgroundWorkers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
10
|
+
path: z.ZodString;
|
|
11
|
+
}, z.z.core.$strip>, z.ZodString]>>>;
|
|
12
|
+
serverWorkers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
13
|
+
path: z.ZodString;
|
|
14
|
+
}, z.z.core.$strip>, z.ZodString]>>>;
|
|
15
|
+
containers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
16
|
+
image: z.ZodString;
|
|
17
|
+
}, z.z.core.$strip>, z.ZodString]>>>;
|
|
18
|
+
devServer: z.ZodOptional<z.ZodObject<{
|
|
19
|
+
runCommand: z.ZodOptional<z.ZodString>;
|
|
20
|
+
url: z.ZodString;
|
|
21
|
+
}, z.z.core.$strip>>;
|
|
22
|
+
}, z.z.core.$strip>;
|
|
23
|
+
export type ProjectConfig = z.infer<typeof projectConfigSchema>;
|
|
24
|
+
export declare function loadProjectConfig(projectPath: string): Promise<ProjectConfig>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
const mountPointSchema = z.object({
|
|
5
|
+
path: z.string(),
|
|
6
|
+
});
|
|
7
|
+
const backgroundWorkerSchema = z.object({
|
|
8
|
+
path: z.string(),
|
|
9
|
+
});
|
|
10
|
+
const serverWorkerSchema = z.object({
|
|
11
|
+
path: z.string(),
|
|
12
|
+
});
|
|
13
|
+
const containerSchema = z.object({
|
|
14
|
+
image: z.string(),
|
|
15
|
+
});
|
|
16
|
+
export const projectConfigSchema = z.object({
|
|
17
|
+
name: z.string().optional(),
|
|
18
|
+
version: z.string().optional(),
|
|
19
|
+
thumbnailPath: z.string().optional(),
|
|
20
|
+
mountPoints: z.record(z.string(), z.union([mountPointSchema, z.string()])).optional(),
|
|
21
|
+
backgroundWorkers: z.record(z.string(), z.union([backgroundWorkerSchema, z.string()])).optional(),
|
|
22
|
+
serverWorkers: z.record(z.string(), z.union([serverWorkerSchema, z.string()])).optional(),
|
|
23
|
+
containers: z.record(z.string(), z.union([containerSchema, z.string()])).optional(),
|
|
24
|
+
devServer: z
|
|
25
|
+
.object({
|
|
26
|
+
runCommand: z.string().optional(),
|
|
27
|
+
url: z.string(),
|
|
28
|
+
})
|
|
29
|
+
.optional(),
|
|
30
|
+
});
|
|
31
|
+
export async function loadProjectConfig(projectPath) {
|
|
32
|
+
const configPath = path.join(projectPath, 'telemetry.config.json');
|
|
33
|
+
try {
|
|
34
|
+
const content = await readFile(configPath, 'utf-8');
|
|
35
|
+
const parsed = JSON.parse(content);
|
|
36
|
+
return projectConfigSchema.parse(parsed);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code === 'ENOENT') {
|
|
40
|
+
throw new Error(`No telemetry.config.json found in ${projectPath}`);
|
|
41
|
+
}
|
|
42
|
+
if (error instanceof z.ZodError) {
|
|
43
|
+
const issues = error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
44
|
+
throw new Error(`Invalid telemetry.config.json:\n${issues}`);
|
|
45
|
+
}
|
|
46
|
+
if (error instanceof SyntaxError) {
|
|
47
|
+
throw new Error(`Invalid JSON in telemetry.config.json: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -5,61 +5,24 @@ import http from 'http';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import readable from 'readline/promises';
|
|
7
7
|
import serveHandler from 'serve-handler';
|
|
8
|
-
import z from 'zod';
|
|
9
8
|
import pkg from '../../package.json' with { type: 'json' };
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const ansiCyan = '\u001b[36m';
|
|
13
|
-
const ansiBold = '\u001b[1m';
|
|
14
|
-
const ansiReset = '\u001b[0m';
|
|
15
|
-
const mountPointSchema = z.object({
|
|
16
|
-
path: z.string(),
|
|
17
|
-
});
|
|
18
|
-
const backgroundWorkerSchema = z.object({
|
|
19
|
-
path: z.string(),
|
|
20
|
-
});
|
|
21
|
-
const serverWorkerSchema = z.object({
|
|
22
|
-
path: z.string(),
|
|
23
|
-
});
|
|
24
|
-
const containerSchema = z.object({
|
|
25
|
-
image: z.string(),
|
|
26
|
-
});
|
|
27
|
-
const configSchema = z.object({
|
|
28
|
-
name: z.string().optional(),
|
|
29
|
-
version: z.string().optional(),
|
|
30
|
-
thumbnailPath: z.string().optional(),
|
|
31
|
-
mountPoints: z.record(z.string(), z.union([mountPointSchema, z.string()])).optional(),
|
|
32
|
-
backgroundWorkers: z.record(z.string(), z.union([backgroundWorkerSchema, z.string()])).optional(),
|
|
33
|
-
serverWorkers: z.record(z.string(), z.union([serverWorkerSchema, z.string()])).optional(),
|
|
34
|
-
containers: z.record(z.string(), z.union([containerSchema, z.string()])).optional(),
|
|
35
|
-
devServer: z
|
|
36
|
-
.object({
|
|
37
|
-
runCommand: z.string().optional(),
|
|
38
|
-
url: z.string(),
|
|
39
|
-
})
|
|
40
|
-
.optional(),
|
|
41
|
-
});
|
|
9
|
+
import { loadProjectConfig } from './project-config.js';
|
|
10
|
+
import { ansi } from '../utils/ansi.js';
|
|
42
11
|
export async function runServer(projectPath, flags) {
|
|
43
12
|
printSplashScreen();
|
|
44
13
|
projectPath = path.resolve(process.cwd(), projectPath);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
process.exit(1);
|
|
14
|
+
let projectConfig;
|
|
15
|
+
try {
|
|
16
|
+
projectConfig = await loadProjectConfig(projectPath);
|
|
49
17
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
console.error('Invalid telemetry.config.json:');
|
|
53
|
-
validationResult.error.issues.forEach((issue) => {
|
|
54
|
-
console.error(` ${issue.path.join('.')}: ${issue.message}`);
|
|
55
|
-
});
|
|
56
|
-
console.error('\n');
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error(error.message);
|
|
57
20
|
process.exit(1);
|
|
58
21
|
}
|
|
59
|
-
await serveDevelopmentApplicationHostUI(projectPath, flags.port,
|
|
60
|
-
await serveTelemetryApplication(projectPath,
|
|
22
|
+
await serveDevelopmentApplicationHostUI(projectPath, flags.port, projectConfig);
|
|
23
|
+
await serveTelemetryApplication(projectPath, projectConfig);
|
|
61
24
|
}
|
|
62
|
-
async function serveDevelopmentApplicationHostUI(projectPath, port,
|
|
25
|
+
async function serveDevelopmentApplicationHostUI(projectPath, port, projectConfig) {
|
|
63
26
|
const hostUiPath = await import.meta.resolve('@telemetryos/development-application-host-ui/dist');
|
|
64
27
|
const serveConfig = { public: fileURLToPath(hostUiPath) };
|
|
65
28
|
const server = http.createServer();
|
|
@@ -68,19 +31,19 @@ async function serveDevelopmentApplicationHostUI(projectPath, port, telemetryCon
|
|
|
68
31
|
const url = new URL(req.url, `http://${req.headers.origin}`);
|
|
69
32
|
if (url.pathname === '/__tos-config__') {
|
|
70
33
|
res.setHeader('Content-Type', 'application/json');
|
|
71
|
-
res.end(JSON.stringify(
|
|
34
|
+
res.end(JSON.stringify(projectConfig));
|
|
72
35
|
return;
|
|
73
36
|
}
|
|
74
37
|
if (url.pathname === '/__tos-thumbnail__') {
|
|
75
|
-
if (!
|
|
38
|
+
if (!projectConfig.thumbnailPath) {
|
|
76
39
|
res.statusCode = 404;
|
|
77
40
|
res.end('No thumbnail configured');
|
|
78
41
|
return;
|
|
79
42
|
}
|
|
80
|
-
const thumbnailFullPath = path.join(projectPath,
|
|
43
|
+
const thumbnailFullPath = path.join(projectPath, projectConfig.thumbnailPath);
|
|
81
44
|
try {
|
|
82
45
|
const imageData = await readFile(thumbnailFullPath);
|
|
83
|
-
const ext = path.extname(
|
|
46
|
+
const ext = path.extname(projectConfig.thumbnailPath).toLowerCase();
|
|
84
47
|
const mimeTypes = {
|
|
85
48
|
'.jpg': 'image/jpeg',
|
|
86
49
|
'.jpeg': 'image/jpeg',
|
|
@@ -168,17 +131,21 @@ async function serveDevelopmentApplicationHostUI(projectPath, port, telemetryCon
|
|
|
168
131
|
printServerInfo(port);
|
|
169
132
|
server.listen(port);
|
|
170
133
|
}
|
|
171
|
-
async function serveTelemetryApplication(rootPath,
|
|
134
|
+
async function serveTelemetryApplication(rootPath, projectConfig) {
|
|
172
135
|
var _a;
|
|
173
|
-
if (!((_a =
|
|
136
|
+
if (!((_a = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.devServer) === null || _a === void 0 ? void 0 : _a.runCommand)) {
|
|
174
137
|
console.log('No value in config at devServer.runCommand');
|
|
175
138
|
return;
|
|
176
139
|
}
|
|
177
|
-
const runCommand =
|
|
140
|
+
const runCommand = projectConfig.devServer.runCommand;
|
|
178
141
|
const binPath = path.join(rootPath, 'node_modules', '.bin');
|
|
179
142
|
const childProcess = spawn(runCommand, {
|
|
180
143
|
shell: true,
|
|
181
|
-
env: {
|
|
144
|
+
env: {
|
|
145
|
+
...process.env,
|
|
146
|
+
FORCE_COLOR: '1',
|
|
147
|
+
PATH: `${binPath}${path.delimiter}${process.env.PATH}`,
|
|
148
|
+
},
|
|
182
149
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
183
150
|
cwd: rootPath,
|
|
184
151
|
});
|
|
@@ -200,32 +167,21 @@ async function serveTelemetryApplication(rootPath, telemetryConfig) {
|
|
|
200
167
|
childProcess.kill();
|
|
201
168
|
});
|
|
202
169
|
}
|
|
203
|
-
async function loadConfigFile(rootPath) {
|
|
204
|
-
const configFilePath = path.join(rootPath, 'telemetry.config.json');
|
|
205
|
-
try {
|
|
206
|
-
const fileContent = await readFile(configFilePath, 'utf-8');
|
|
207
|
-
const config = JSON.parse(fileContent);
|
|
208
|
-
return config;
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
170
|
function printSplashScreen() {
|
|
215
171
|
console.log(`
|
|
216
172
|
|
|
217
|
-
${
|
|
218
|
-
${
|
|
219
|
-
${
|
|
220
|
-
${
|
|
221
|
-
${
|
|
222
|
-
${
|
|
173
|
+
${ansi.white} █ █ █ ${ansi.yellow}▄▀▀▀▄ ▄▀▀▀▄
|
|
174
|
+
${ansi.white} █ █ █ ${ansi.yellow}█ █ █
|
|
175
|
+
${ansi.white}▀█▀ ▄▀▀▄ █ ▄▀▀▄ █▀▄▀▄ ▄▀▀▄ ▀█▀ █▄▀ █ █ ${ansi.yellow}█ █ ▀▀▀▄
|
|
176
|
+
${ansi.white} █ █▀▀▀ █ █▀▀▀ █ █ █ █▀▀▀ █ █ █ █ ${ansi.yellow}█ █ █
|
|
177
|
+
${ansi.white} ▀▄ ▀▄▄▀ █ ▀▄▄▀ █ █ █ ▀▄▄▀ ▀▄ █ █ ${ansi.yellow}▀▄▄▄▀ ▀▄▄▄▀
|
|
178
|
+
${ansi.white} ▄▀ ${ansi.reset}
|
|
223
179
|
v${pkg.version}`);
|
|
224
180
|
}
|
|
225
181
|
function printServerInfo(port) {
|
|
226
182
|
console.log(`
|
|
227
183
|
╔═══════════════════════════════════════════════════════════╗
|
|
228
|
-
║ ${
|
|
184
|
+
║ ${ansi.bold}Development environment running at: ${ansi.cyan}http://localhost:${port}${ansi.reset} ║
|
|
229
185
|
╚═══════════════════════════════════════════════════════════╝
|
|
230
186
|
`);
|
|
231
187
|
}
|