@telemetryos/cli 1.9.0 → 1.10.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 +14 -0
- package/dist/commands/auth.js +4 -4
- package/dist/commands/init.js +90 -42
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +208 -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 +6 -0
- package/dist/services/cli-config.js +23 -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 +6 -5
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
- package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +367 -130
- package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +443 -269
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @telemetryos/cli
|
|
2
2
|
|
|
3
|
+
## 1.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Added MQTT, publish command, and other improvements
|
|
8
|
+
- Added MQTT support to the SDK
|
|
9
|
+
- Added a command `tos publish` that sends the project to be built on the platform (requires user token, so wont work right away)
|
|
10
|
+
- Added mock data for the currency service
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @telemetryos/development-application-host-ui@1.10.0
|
|
16
|
+
|
|
3
17
|
## 1.9.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
|
-
import {
|
|
3
|
+
import { 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)')
|
|
@@ -26,8 +26,8 @@ function validateToken(input) {
|
|
|
26
26
|
}
|
|
27
27
|
async function handleAuthCommand(options) {
|
|
28
28
|
let token = options.token;
|
|
29
|
-
const
|
|
30
|
-
const existingToken =
|
|
29
|
+
const existingCliConfig = await loadCliConfig();
|
|
30
|
+
const existingToken = existingCliConfig === null || existingCliConfig === void 0 ? void 0 : existingCliConfig.apiToken;
|
|
31
31
|
if (existingToken && !options.force) {
|
|
32
32
|
const { confirm } = await inquirer.prompt([
|
|
33
33
|
{
|
|
@@ -62,6 +62,6 @@ async function handleAuthCommand(options) {
|
|
|
62
62
|
]);
|
|
63
63
|
token = answers.token.trim();
|
|
64
64
|
}
|
|
65
|
-
await
|
|
65
|
+
await saveCliConfig({ ...existingCliConfig, apiToken: token });
|
|
66
66
|
console.log('API token saved successfully!');
|
|
67
67
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,33 +1,98 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { generateApplication } from '../services/generate-application.js';
|
|
2
|
+
import { generateApplication, checkDirectoryConflicts, removeConflictingFiles, } from '../services/generate-application.js';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { resolveProjectPathAndName, validateProjectName } from '../utils/path-utils.js';
|
|
6
|
+
import { ansi } from '../utils/ansi.js';
|
|
7
|
+
async function promptConflictResolution() {
|
|
8
|
+
const { action } = await inquirer.prompt([
|
|
9
|
+
{
|
|
10
|
+
type: 'list',
|
|
11
|
+
name: 'action',
|
|
12
|
+
message: 'Target directory is not empty. Please choose how to proceed:',
|
|
13
|
+
choices: [
|
|
14
|
+
{ name: 'Cancel operation', value: 'cancel' },
|
|
15
|
+
{ name: 'Remove existing files and continue', value: 'remove' },
|
|
16
|
+
{ name: 'Ignore files and continue', value: 'ignore' },
|
|
17
|
+
],
|
|
18
|
+
default: 'cancel',
|
|
19
|
+
},
|
|
20
|
+
]);
|
|
21
|
+
return action;
|
|
22
|
+
}
|
|
5
23
|
export const initCommand = new Command('init')
|
|
6
24
|
.description('Initializes a new telemetryOS application')
|
|
25
|
+
.argument('[project-path]', 'Path to create the project (defaults to current directory)', '')
|
|
7
26
|
.option('-d, --description <string>', 'The description of the application', '')
|
|
8
27
|
.option('-a, --author <string>', 'The author of the application', '')
|
|
9
28
|
.option('-v, --version <string>', 'The version of the application', '0.1.0')
|
|
10
29
|
.option('-t, --template <string>', 'The template to use (vite-react-typescript)', '')
|
|
11
|
-
.argument('[project-name]', 'The name of the application', '')
|
|
12
30
|
.action(handleInitCommand);
|
|
13
|
-
async function handleInitCommand(
|
|
14
|
-
|
|
31
|
+
async function handleInitCommand(projectPathArg, options) {
|
|
32
|
+
// Step 1: Resolve path and derive name
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
const inputPath = projectPathArg || '.';
|
|
35
|
+
const { resolvedPath, derivedName } = resolveProjectPathAndName(inputPath, cwd);
|
|
36
|
+
// Step 2: Validate the derived name BEFORE any destructive operations
|
|
37
|
+
const validation = validateProjectName(derivedName);
|
|
38
|
+
if (validation !== true) {
|
|
39
|
+
console.error(`\n${ansi.red}Error:${ansi.reset} Cannot initialize project - ${validation}`);
|
|
40
|
+
console.error(`\nDerived project name: ${ansi.yellow}${derivedName}${ansi.reset}`);
|
|
41
|
+
console.error(`From path: ${ansi.dim}${path.relative(cwd, resolvedPath)}${ansi.reset}`);
|
|
42
|
+
console.error(`\n${ansi.bold}Project names must:${ansi.reset}`);
|
|
43
|
+
console.error(' • Contain only lowercase letters, numbers, and hyphens');
|
|
44
|
+
console.error(' • Not start or end with a hyphen');
|
|
45
|
+
console.error(' • Not start with a dot (.) or underscore (_)');
|
|
46
|
+
console.error(' • Be between 1 and 214 characters');
|
|
47
|
+
console.error(' • Not be a reserved name (e.g., node_modules)');
|
|
48
|
+
console.error(`\n${ansi.yellow}Tip:${ansi.reset} Use a different directory name or specify a path`);
|
|
49
|
+
console.error(`Example: ${ansi.cyan}tos init my-app${ansi.reset}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
// Step 3: Check for directory conflicts BEFORE prompting for details
|
|
53
|
+
const conflicts = await checkDirectoryConflicts(resolvedPath);
|
|
54
|
+
if (conflicts.length > 0) {
|
|
55
|
+
console.log(`\n${ansi.yellow}⚠${ansi.reset} Target directory contains files that could conflict:\n`);
|
|
56
|
+
conflicts.slice(0, 10).forEach((file) => console.log(` ${ansi.dim}${file}${ansi.reset}`));
|
|
57
|
+
if (conflicts.length > 10) {
|
|
58
|
+
console.log(` ${ansi.dim}... and ${conflicts.length - 10} more${ansi.reset}`);
|
|
59
|
+
}
|
|
60
|
+
console.log();
|
|
61
|
+
const action = await promptConflictResolution();
|
|
62
|
+
if (action === 'cancel') {
|
|
63
|
+
console.log('Operation cancelled');
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
else if (action === 'remove') {
|
|
67
|
+
console.log('\nRemoving existing files...');
|
|
68
|
+
await removeConflictingFiles(resolvedPath, conflicts);
|
|
69
|
+
console.log(`${ansi.green}✓${ansi.reset} Existing files removed\n`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// action === 'ignore' - continue with merge
|
|
73
|
+
console.log('\nContinuing with existing files (will overwrite conflicts)...\n');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Step 4: Setup variables from options
|
|
77
|
+
let name = derivedName;
|
|
15
78
|
let description = options.description;
|
|
16
79
|
let author = options.author;
|
|
17
80
|
let version = options.version;
|
|
18
81
|
let template = options.template;
|
|
82
|
+
// Step 5: Build prompt questions
|
|
19
83
|
const questions = [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
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
|
+
});
|
|
31
96
|
if (!description)
|
|
32
97
|
questions.push({
|
|
33
98
|
type: 'input',
|
|
@@ -61,40 +126,23 @@ async function handleInitCommand(projectName, options) {
|
|
|
61
126
|
message: 'Which template would you like to use?',
|
|
62
127
|
choices: [{ name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }],
|
|
63
128
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (answers.author)
|
|
73
|
-
author = answers.author;
|
|
74
|
-
if (answers.template)
|
|
75
|
-
template = answers.template;
|
|
76
|
-
}
|
|
77
|
-
const projectPath = path.join(process.cwd(), name);
|
|
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;
|
|
136
|
+
// Step 7: Generate application
|
|
78
137
|
await generateApplication({
|
|
79
138
|
name,
|
|
80
139
|
description,
|
|
81
140
|
author,
|
|
82
141
|
version,
|
|
83
142
|
template,
|
|
84
|
-
projectPath,
|
|
143
|
+
projectPath: resolvedPath,
|
|
85
144
|
progressFn: (createdFilePath) => {
|
|
86
|
-
console.log(`.${path.sep}${path.relative(
|
|
87
|
-
},
|
|
88
|
-
confirmOverwrite: async () => {
|
|
89
|
-
const { proceed } = await inquirer.prompt([
|
|
90
|
-
{
|
|
91
|
-
type: 'confirm',
|
|
92
|
-
name: 'proceed',
|
|
93
|
-
message: 'Do you want to continue and overwrite these files?',
|
|
94
|
-
default: false,
|
|
95
|
-
},
|
|
96
|
-
]);
|
|
97
|
-
return proceed;
|
|
145
|
+
console.log(`.${path.sep}${path.relative(cwd, createdFilePath)}`);
|
|
98
146
|
},
|
|
99
147
|
});
|
|
100
148
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ApiClient } from '../services/api-client.js';
|
|
5
|
+
import { loadProjectConfig } from '../services/project-config.js';
|
|
6
|
+
import { createArchive } from '../services/archiver.js';
|
|
7
|
+
import { pollBuild } from '../services/build-poller.js';
|
|
8
|
+
import { ansi } from '../utils/ansi.js';
|
|
9
|
+
// Default values for new application builds
|
|
10
|
+
const APP_BUILD_DEFAULTS = {
|
|
11
|
+
baseImage: 'node:20',
|
|
12
|
+
buildScript: 'npm install\nnpm run build',
|
|
13
|
+
buildOutput: '/dist',
|
|
14
|
+
workingPath: '/',
|
|
15
|
+
};
|
|
16
|
+
export const publishCommand = new Command('publish')
|
|
17
|
+
.description('Publish an application to TelemetryOS')
|
|
18
|
+
.argument('[project-path]', 'Path to the project directory. Defaults to current working directory', process.cwd())
|
|
19
|
+
.option('-i, --interactive', 'Interactively prompt for all build configuration')
|
|
20
|
+
.option('--name <name>', 'Application name (overrides config)')
|
|
21
|
+
.option('--base-image <image>', 'Docker base image for build')
|
|
22
|
+
.option('--build-script <script>', 'Build script commands')
|
|
23
|
+
.option('--build-output <path>', 'Build output directory')
|
|
24
|
+
.option('--working-path <path>', 'Working directory in container')
|
|
25
|
+
.action(handlePublishCommand);
|
|
26
|
+
function getSpecifiedBuildOptions(options) {
|
|
27
|
+
const specified = [];
|
|
28
|
+
if (options.baseImage !== undefined)
|
|
29
|
+
specified.push('--base-image');
|
|
30
|
+
if (options.buildScript !== undefined)
|
|
31
|
+
specified.push('--build-script');
|
|
32
|
+
if (options.buildOutput !== undefined)
|
|
33
|
+
specified.push('--build-output');
|
|
34
|
+
if (options.workingPath !== undefined)
|
|
35
|
+
specified.push('--working-path');
|
|
36
|
+
return specified;
|
|
37
|
+
}
|
|
38
|
+
async function handlePublishCommand(projectPath, options) {
|
|
39
|
+
var _a, _b, _c, _d;
|
|
40
|
+
projectPath = path.resolve(process.cwd(), projectPath);
|
|
41
|
+
try {
|
|
42
|
+
// Step 1: Load project config
|
|
43
|
+
console.log(`\n${ansi.cyan}Loading project configuration...${ansi.reset}`);
|
|
44
|
+
const config = await loadProjectConfig(projectPath);
|
|
45
|
+
// Step 2: Resolve build configuration
|
|
46
|
+
let buildConfig = {
|
|
47
|
+
name: options.name || config.name || '',
|
|
48
|
+
baseImage: options.baseImage,
|
|
49
|
+
buildScript: options.buildScript,
|
|
50
|
+
buildOutput: options.buildOutput,
|
|
51
|
+
workingPath: options.workingPath,
|
|
52
|
+
};
|
|
53
|
+
if (options.interactive) {
|
|
54
|
+
// Interactive mode: prompt for all fields
|
|
55
|
+
buildConfig = await promptBuildConfig(buildConfig);
|
|
56
|
+
}
|
|
57
|
+
else if (!buildConfig.name) {
|
|
58
|
+
// Non-interactive mode but no name: prompt just for name
|
|
59
|
+
const answers = await inquirer.prompt([
|
|
60
|
+
{
|
|
61
|
+
type: 'input',
|
|
62
|
+
name: 'name',
|
|
63
|
+
message: 'Application name:',
|
|
64
|
+
validate: (input) => (input.trim() ? true : 'Application name is required'),
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
buildConfig.name = answers.name.trim();
|
|
68
|
+
}
|
|
69
|
+
if (!buildConfig.name) {
|
|
70
|
+
console.error(`${ansi.red}Error: Application name is required${ansi.reset}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
console.log(` Project: ${ansi.bold}${buildConfig.name}${ansi.reset}`);
|
|
74
|
+
if (config.version) {
|
|
75
|
+
console.log(` Version: ${config.version}`);
|
|
76
|
+
}
|
|
77
|
+
// Step 3: Initialize API client
|
|
78
|
+
console.log(`\n${ansi.cyan}Authenticating...${ansi.reset}`);
|
|
79
|
+
const apiClient = await ApiClient.create();
|
|
80
|
+
console.log(` ${ansi.green}Authenticated${ansi.reset}`);
|
|
81
|
+
// Step 4: Create archive
|
|
82
|
+
console.log(`\n${ansi.cyan}Archiving project...${ansi.reset}`);
|
|
83
|
+
const { buffer, filename } = await createArchive(projectPath, (msg) => {
|
|
84
|
+
console.log(` ${ansi.dim}${msg}${ansi.reset}`);
|
|
85
|
+
});
|
|
86
|
+
// Step 5: Find or create application
|
|
87
|
+
console.log(`\n${ansi.cyan}Checking for existing application...${ansi.reset}`);
|
|
88
|
+
const appsRes = await apiClient.get('/application');
|
|
89
|
+
const apps = (await appsRes.json());
|
|
90
|
+
let app = apps.find((a) => a.title === buildConfig.name) || null;
|
|
91
|
+
if (app) {
|
|
92
|
+
console.log(` Found existing application: ${ansi.dim}${app.id}${ansi.reset}`);
|
|
93
|
+
const specifiedOptions = getSpecifiedBuildOptions(options);
|
|
94
|
+
if (specifiedOptions.length > 0) {
|
|
95
|
+
console.log(` ${ansi.yellow}Warning: ${specifiedOptions.join(', ')} ignored for existing application${ansi.reset}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log(` Creating new application...`);
|
|
100
|
+
const createRequest = {
|
|
101
|
+
kind: 'uploaded',
|
|
102
|
+
title: buildConfig.name,
|
|
103
|
+
baseImage: (_a = buildConfig.baseImage) !== null && _a !== void 0 ? _a : APP_BUILD_DEFAULTS.baseImage,
|
|
104
|
+
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,
|
|
108
|
+
};
|
|
109
|
+
const createRes = await apiClient.post('/application', { body: createRequest });
|
|
110
|
+
app = (await createRes.json());
|
|
111
|
+
console.log(` ${ansi.green}Created application:${ansi.reset} ${ansi.dim}${app.id}${ansi.reset}`);
|
|
112
|
+
}
|
|
113
|
+
// Step 6: Upload archive
|
|
114
|
+
console.log(`\n${ansi.cyan}Uploading archive...${ansi.reset}`);
|
|
115
|
+
const formData = new FormData();
|
|
116
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
117
|
+
const blob = new Blob([arrayBuffer], { type: 'application/gzip' });
|
|
118
|
+
formData.append('file', blob, filename);
|
|
119
|
+
await apiClient.post(`/application/${app.id}/archive`, { body: formData });
|
|
120
|
+
console.log(` ${ansi.green}Upload complete${ansi.reset}`);
|
|
121
|
+
// 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}`);
|
|
124
|
+
const build = await pollBuild(apiClient, app.id, {
|
|
125
|
+
onLog: (line) => {
|
|
126
|
+
console.log(` ${ansi.dim}${line}${ansi.reset}`);
|
|
127
|
+
},
|
|
128
|
+
onStateChange: () => { },
|
|
129
|
+
onComplete: () => { },
|
|
130
|
+
onError: (error) => {
|
|
131
|
+
console.error(`${ansi.red}Polling error: ${error.message}${ansi.reset}`);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// Step 8: Report result
|
|
135
|
+
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}`);
|
|
139
|
+
if (build.finishedAt && build.startedAt) {
|
|
140
|
+
const duration = calculateDuration(build.startedAt, build.finishedAt);
|
|
141
|
+
console.log(` Duration: ${duration}`);
|
|
142
|
+
}
|
|
143
|
+
console.log('');
|
|
144
|
+
}
|
|
145
|
+
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);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.error(`\n${ansi.red}Error: ${error.message}${ansi.reset}\n`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function promptBuildConfig(defaults) {
|
|
158
|
+
var _a, _b, _c, _d;
|
|
159
|
+
const answers = await inquirer.prompt([
|
|
160
|
+
{
|
|
161
|
+
type: 'input',
|
|
162
|
+
name: 'name',
|
|
163
|
+
message: 'Application name:',
|
|
164
|
+
default: defaults.name || undefined,
|
|
165
|
+
validate: (input) => (input.trim() ? true : 'Application name is required'),
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: 'input',
|
|
169
|
+
name: 'baseImage',
|
|
170
|
+
message: 'Base image:',
|
|
171
|
+
default: (_a = defaults.baseImage) !== null && _a !== void 0 ? _a : APP_BUILD_DEFAULTS.baseImage,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'input',
|
|
175
|
+
name: 'buildScript',
|
|
176
|
+
message: 'Build script:',
|
|
177
|
+
default: (_b = defaults.buildScript) !== null && _b !== void 0 ? _b : APP_BUILD_DEFAULTS.buildScript,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
type: 'input',
|
|
181
|
+
name: 'buildOutput',
|
|
182
|
+
message: 'Build output path:',
|
|
183
|
+
default: (_c = defaults.buildOutput) !== null && _c !== void 0 ? _c : APP_BUILD_DEFAULTS.buildOutput,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: 'input',
|
|
187
|
+
name: 'workingPath',
|
|
188
|
+
message: 'Working path:',
|
|
189
|
+
default: (_d = defaults.workingPath) !== null && _d !== void 0 ? _d : APP_BUILD_DEFAULTS.workingPath,
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
return {
|
|
193
|
+
name: answers.name.trim(),
|
|
194
|
+
baseImage: answers.baseImage.trim(),
|
|
195
|
+
buildScript: answers.buildScript.trim(),
|
|
196
|
+
buildOutput: answers.buildOutput.trim(),
|
|
197
|
+
workingPath: answers.workingPath.trim(),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function calculateDuration(start, end) {
|
|
201
|
+
const ms = new Date(end).getTime() - new Date(start).getTime();
|
|
202
|
+
const seconds = Math.floor(ms / 1000);
|
|
203
|
+
if (seconds < 60)
|
|
204
|
+
return `${seconds}s`;
|
|
205
|
+
const minutes = Math.floor(seconds / 60);
|
|
206
|
+
const remainingSeconds = seconds % 60;
|
|
207
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
208
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { authCommand } from './commands/auth.js';
|
|
3
3
|
import { initCommand } from './commands/init.js';
|
|
4
|
+
import { publishCommand } from './commands/publish.js';
|
|
4
5
|
import { rootCommand } from './commands/root.js';
|
|
5
6
|
import { serveCommand } from './commands/serve.js';
|
|
6
7
|
rootCommand.addCommand(authCommand);
|
|
7
8
|
rootCommand.addCommand(serveCommand);
|
|
8
9
|
rootCommand.addCommand(initCommand);
|
|
10
|
+
rootCommand.addCommand(publishCommand);
|
|
9
11
|
rootCommand.parse(process.argv);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin';
|
|
2
|
+
export const MathToolsPlugin = async (ctx) => {
|
|
3
|
+
return {
|
|
4
|
+
tool: {
|
|
5
|
+
multiply: tool({
|
|
6
|
+
description: 'Multiply two numbers',
|
|
7
|
+
args: {
|
|
8
|
+
a: tool.schema.number().describe('First number'),
|
|
9
|
+
b: tool.schema.number().describe('Second number'),
|
|
10
|
+
},
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
const result = args.a * args.b;
|
|
13
|
+
return result.toString();
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type RequestOptions = {
|
|
2
|
+
body?: unknown;
|
|
3
|
+
headers?: Record<string, string>;
|
|
4
|
+
query?: Record<string, string | number | boolean>;
|
|
5
|
+
};
|
|
6
|
+
export declare class ApiClient {
|
|
7
|
+
private baseUrl;
|
|
8
|
+
private token;
|
|
9
|
+
constructor(baseUrl: string, token: string);
|
|
10
|
+
static create(): Promise<ApiClient>;
|
|
11
|
+
get(path: string, opts?: RequestOptions): Promise<Response>;
|
|
12
|
+
post(path: string, opts?: RequestOptions): Promise<Response>;
|
|
13
|
+
put(path: string, opts?: RequestOptions): Promise<Response>;
|
|
14
|
+
patch(path: string, opts?: RequestOptions): Promise<Response>;
|
|
15
|
+
delete(path: string, opts?: RequestOptions): Promise<Response>;
|
|
16
|
+
private request;
|
|
17
|
+
private buildUrl;
|
|
18
|
+
}
|
|
@@ -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 === null || config === void 0 ? void 0 : 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 {};
|