@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/commands/auth.js +4 -4
  3. package/dist/commands/init.js +90 -42
  4. package/dist/commands/publish.d.ts +2 -0
  5. package/dist/commands/publish.js +208 -0
  6. package/dist/index.js +2 -0
  7. package/dist/plugins/math-tools.d.ts +2 -0
  8. package/dist/plugins/math-tools.js +18 -0
  9. package/dist/services/api-client.d.ts +18 -0
  10. package/dist/services/api-client.js +70 -0
  11. package/dist/services/archiver.d.ts +4 -0
  12. package/dist/services/archiver.js +65 -0
  13. package/dist/services/build-poller.d.ts +10 -0
  14. package/dist/services/build-poller.js +63 -0
  15. package/dist/services/cli-config.d.ts +6 -0
  16. package/dist/services/cli-config.js +23 -0
  17. package/dist/services/generate-application.d.ts +2 -1
  18. package/dist/services/generate-application.js +31 -32
  19. package/dist/services/project-config.d.ts +24 -0
  20. package/dist/services/project-config.js +51 -0
  21. package/dist/services/run-server.js +29 -73
  22. package/dist/types/api.d.ts +44 -0
  23. package/dist/types/api.js +1 -0
  24. package/dist/types/applications.d.ts +44 -0
  25. package/dist/types/applications.js +1 -0
  26. package/dist/utils/ansi.d.ts +10 -0
  27. package/dist/utils/ansi.js +10 -0
  28. package/dist/utils/path-utils.d.ts +55 -0
  29. package/dist/utils/path-utils.js +99 -0
  30. package/package.json +4 -2
  31. package/templates/vite-react-typescript/CLAUDE.md +6 -5
  32. package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
  33. package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +367 -130
  34. 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
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import inquirer from 'inquirer';
3
- import { loadConfig, saveConfig } from '../services/config.js';
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 existingConfig = await loadConfig();
30
- const existingToken = existingConfig === null || existingConfig === void 0 ? void 0 : existingConfig.apiToken;
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 saveConfig({ ...existingConfig, apiToken: token });
65
+ await saveCliConfig({ ...existingCliConfig, apiToken: token });
66
66
  console.log('API token saved successfully!');
67
67
  }
@@ -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(projectName, options) {
14
- let name = projectName;
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
- if (!name)
21
- questions.push({
22
- type: 'input',
23
- name: 'name',
24
- message: 'What is the name of your application?',
25
- validate: (input) => {
26
- if (input.length > 0)
27
- return true;
28
- return 'Application name cannot be empty';
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
- if (questions.length !== 0) {
65
- const answers = await inquirer.prompt(questions);
66
- if (answers.name)
67
- name = answers.name;
68
- if (answers.version)
69
- version = answers.version;
70
- if (answers.description)
71
- description = answers.description;
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(process.cwd(), createdFilePath)}`);
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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const publishCommand: Command;
@@ -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,2 @@
1
+ import { type Plugin } from '@opencode-ai/plugin';
2
+ export declare const MathToolsPlugin: Plugin;
@@ -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,4 @@
1
+ export declare function createArchive(projectPath: string, onProgress?: (message: string) => void): Promise<{
2
+ buffer: Buffer;
3
+ filename: string;
4
+ }>;
@@ -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 {};