@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/commands/auth.js +8 -15
  3. package/dist/commands/init.js +131 -68
  4. package/dist/commands/publish.d.ts +22 -0
  5. package/dist/commands/publish.js +238 -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 +10 -0
  16. package/dist/services/cli-config.js +45 -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 +14 -6
  32. package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +4 -28
  33. package/templates/vite-react-typescript/_claude/skills/tos-multi-mode/SKILL.md +359 -0
  34. package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
  35. package/templates/vite-react-typescript/_claude/skills/tos-render-kiosk-design/SKILL.md +384 -0
  36. package/templates/vite-react-typescript/_claude/skills/tos-render-signage-design/SKILL.md +515 -0
  37. package/templates/vite-react-typescript/_claude/skills/tos-render-ui-design/SKILL.md +325 -0
  38. package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +405 -125
  39. package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +96 -5
  40. package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +443 -269
  41. package/templates/vite-react-typescript/index.html +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
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
+
14
+ ## 1.10.0
15
+
16
+ ### Minor Changes
17
+
18
+ - Added MQTT, publish command, and other improvements
19
+ - Added MQTT support to the SDK
20
+ - Added a command `tos publish` that sends the project to be built on the platform (requires user token, so wont work right away)
21
+ - Added mock data for the currency service
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies
26
+ - @telemetryos/development-application-host-ui@1.10.0
27
+
3
28
  ## 1.9.0
4
29
 
5
30
  ### 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 { 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
- const existingConfig = await loadConfig();
30
- const existingToken = existingConfig === null || existingConfig === void 0 ? void 0 : existingConfig.apiToken;
22
+ const existingCliConfig = await loadCliConfig();
23
+ const existingToken = existingCliConfig.apiToken;
31
24
  if (existingToken && !options.force) {
32
25
  const { confirm } = await inquirer.prompt([
33
26
  {
@@ -62,6 +55,6 @@ async function handleAuthCommand(options) {
62
55
  ]);
63
56
  token = answers.token.trim();
64
57
  }
65
- await saveConfig({ ...existingConfig, apiToken: token });
58
+ await saveCliConfig({ ...existingCliConfig, apiToken: token });
66
59
  console.log('API token saved successfully!');
67
60
  }
@@ -1,100 +1,163 @@
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', '')
30
+ .option('-y, --yes', 'Skip all prompts and use defaults', false)
12
31
  .action(handleInitCommand);
13
- async function handleInitCommand(projectName, options) {
14
- let name = projectName;
32
+ async function handleInitCommand(projectPathArg, options) {
33
+ // Step 1: Resolve path and derive name
34
+ const cwd = process.cwd();
35
+ const inputPath = projectPathArg || '.';
36
+ const { resolvedPath, derivedName } = resolveProjectPathAndName(inputPath, cwd);
37
+ // Step 2: Validate the derived name BEFORE any destructive operations
38
+ const validation = validateProjectName(derivedName);
39
+ if (validation !== true) {
40
+ console.error(`\n${ansi.red}Error:${ansi.reset} Cannot initialize project - ${validation}`);
41
+ console.error(`\nDerived project name: ${ansi.yellow}${derivedName}${ansi.reset}`);
42
+ console.error(`From path: ${ansi.dim}${path.relative(cwd, resolvedPath)}${ansi.reset}`);
43
+ console.error(`\n${ansi.bold}Project names must:${ansi.reset}`);
44
+ console.error(' • Contain only lowercase letters, numbers, and hyphens');
45
+ console.error(' • Not start or end with a hyphen');
46
+ console.error(' • Not start with a dot (.) or underscore (_)');
47
+ console.error(' • Be between 1 and 214 characters');
48
+ console.error(' • Not be a reserved name (e.g., node_modules)');
49
+ console.error(`\n${ansi.yellow}Tip:${ansi.reset} Use a different directory name or specify a path`);
50
+ console.error(`Example: ${ansi.cyan}tos init my-app${ansi.reset}`);
51
+ process.exit(1);
52
+ }
53
+ // Step 3: Check for directory conflicts BEFORE prompting for details
54
+ const conflicts = await checkDirectoryConflicts(resolvedPath);
55
+ if (conflicts.length > 0) {
56
+ console.log(`\n${ansi.yellow}⚠${ansi.reset} Target directory contains files that could conflict:\n`);
57
+ conflicts.slice(0, 10).forEach((file) => console.log(` ${ansi.dim}${file}${ansi.reset}`));
58
+ if (conflicts.length > 10) {
59
+ console.log(` ${ansi.dim}... and ${conflicts.length - 10} more${ansi.reset}`);
60
+ }
61
+ console.log();
62
+ const action = await promptConflictResolution();
63
+ if (action === 'cancel') {
64
+ console.log('Operation cancelled');
65
+ process.exit(0);
66
+ }
67
+ else if (action === 'remove') {
68
+ console.log('\nRemoving existing files...');
69
+ await removeConflictingFiles(resolvedPath, conflicts);
70
+ console.log(`${ansi.green}✓${ansi.reset} Existing files removed\n`);
71
+ }
72
+ else {
73
+ // action === 'ignore' - continue with merge
74
+ console.log('\nContinuing with existing files (will overwrite conflicts)...\n');
75
+ }
76
+ }
77
+ // Step 4: Setup variables from options
78
+ let name = derivedName;
15
79
  let description = options.description;
16
80
  let author = options.author;
17
81
  let version = options.version;
18
82
  let template = options.template;
19
- const questions = [];
20
- if (!name)
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
21
100
  questions.push({
22
101
  type: 'input',
23
102
  name: 'name',
24
- message: 'What is the name of your application?',
103
+ message: 'Project name:',
104
+ default: derivedName,
25
105
  validate: (input) => {
26
- if (input.length > 0)
27
- return true;
28
- return 'Application name cannot be empty';
106
+ const result = validateProjectName(input);
107
+ return result === true ? true : result;
29
108
  },
30
109
  });
31
- if (!description)
32
- questions.push({
33
- type: 'input',
34
- name: 'description',
35
- message: 'What is the description of your application?',
36
- default: 'A telemetryOS application',
37
- });
38
- if (!author)
39
- questions.push({
40
- type: 'input',
41
- name: 'author',
42
- message: 'Who is the author of your application?',
43
- default: '',
44
- });
45
- if (!version)
46
- questions.push({
47
- type: 'input',
48
- name: 'version',
49
- message: 'What is the version of your application?',
50
- default: '0.1.0',
51
- validate: (input) => {
52
- if (/^\d+\.\d+\.\d+(-.+)?$/.test(input))
53
- return true;
54
- return 'Version must be in semver format (e.g. 1.0.0)';
55
- },
56
- });
57
- if (!template)
58
- questions.push({
59
- type: 'list',
60
- name: 'template',
61
- message: 'Which template would you like to use?',
62
- choices: [{ name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }],
63
- });
64
- if (questions.length !== 0) {
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
65
144
  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;
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;
76
150
  }
77
- const projectPath = path.join(process.cwd(), name);
151
+ // Step 7: Generate application
78
152
  await generateApplication({
79
153
  name,
80
154
  description,
81
155
  author,
82
156
  version,
83
157
  template,
84
- projectPath,
158
+ projectPath: resolvedPath,
85
159
  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;
160
+ console.log(`.${path.sep}${path.relative(cwd, createdFilePath)}`);
98
161
  },
99
162
  });
100
163
  }
@@ -0,0 +1,22 @@
1
+ import { Command } from 'commander';
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 };
@@ -0,0 +1,238 @@
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, callbacks) {
39
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
40
+ projectPath = path.resolve(process.cwd(), projectPath);
41
+ const logMessage = (callbacks === null || callbacks === void 0 ? void 0 : callbacks.onLog) || ((msg) => console.log(msg));
42
+ try {
43
+ // Step 1: Load project config
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}`);
46
+ const config = await loadProjectConfig(projectPath);
47
+ // Step 2: Resolve build configuration
48
+ let buildConfig = {
49
+ name: options.name || config.name || '',
50
+ baseImage: options.baseImage,
51
+ buildScript: options.buildScript,
52
+ buildOutput: options.buildOutput,
53
+ workingPath: options.workingPath,
54
+ };
55
+ if (options.interactive) {
56
+ // Interactive mode: prompt for all fields
57
+ buildConfig = await promptBuildConfig(buildConfig);
58
+ }
59
+ else if (!buildConfig.name) {
60
+ // Non-interactive mode but no name: prompt just for name
61
+ const answers = await inquirer.prompt([
62
+ {
63
+ type: 'input',
64
+ name: 'name',
65
+ message: 'Application name:',
66
+ validate: (input) => (input.trim() ? true : 'Application name is required'),
67
+ },
68
+ ]);
69
+ buildConfig.name = answers.name.trim();
70
+ }
71
+ if (!buildConfig.name) {
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;
78
+ }
79
+ logMessage(` Project: ${ansi.bold}${buildConfig.name}${ansi.reset}`);
80
+ if (config.version) {
81
+ logMessage(` Version: ${config.version}`);
82
+ }
83
+ // Step 3: Initialize API client
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}`);
86
+ const apiClient = await ApiClient.create();
87
+ logMessage(` ${ansi.green}Authenticated${ansi.reset}`);
88
+ // Step 4: Create archive
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}`);
91
+ const { buffer, filename } = await createArchive(projectPath, (msg) => {
92
+ logMessage(` ${ansi.dim}${msg}${ansi.reset}`);
93
+ });
94
+ // Step 5: Find or create application
95
+ logMessage(`\n${ansi.cyan}Checking for existing application...${ansi.reset}`);
96
+ const appsRes = await apiClient.get('/application');
97
+ const apps = (await appsRes.json());
98
+ let app = apps.find((a) => a.title === buildConfig.name) || null;
99
+ if (app) {
100
+ logMessage(` Found existing application: ${ansi.dim}${app.id}${ansi.reset}`);
101
+ const specifiedOptions = getSpecifiedBuildOptions(options);
102
+ if (specifiedOptions.length > 0) {
103
+ logMessage(` ${ansi.yellow}Warning: ${specifiedOptions.join(', ')} ignored for existing application${ansi.reset}`);
104
+ }
105
+ }
106
+ else {
107
+ logMessage(` Creating new application...`);
108
+ const createRequest = {
109
+ kind: 'uploaded',
110
+ title: buildConfig.name,
111
+ baseImage: (_e = buildConfig.baseImage) !== null && _e !== void 0 ? _e : APP_BUILD_DEFAULTS.baseImage,
112
+ baseImageRegistryAuth: '',
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,
116
+ };
117
+ const createRes = await apiClient.post('/application', { body: createRequest });
118
+ app = (await createRes.json());
119
+ logMessage(` ${ansi.green}Created application:${ansi.reset} ${ansi.dim}${app.id}${ansi.reset}`);
120
+ }
121
+ // Step 6: Upload archive
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}`);
124
+ const formData = new FormData();
125
+ const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
126
+ const blob = new Blob([arrayBuffer], { type: 'application/gzip' });
127
+ formData.append('file', blob, filename);
128
+ await apiClient.post(`/application/${app.id}/archive`, { body: formData });
129
+ logMessage(` ${ansi.green}Upload complete${ansi.reset}`);
130
+ // Step 7: Poll for build status and stream logs
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}`);
134
+ const build = await pollBuild(apiClient, app.id, {
135
+ onLog: (line) => {
136
+ logMessage(` ${ansi.dim}${line}${ansi.reset}`);
137
+ },
138
+ onStateChange: () => { },
139
+ onComplete: () => { },
140
+ onError: (error) => {
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);
144
+ },
145
+ });
146
+ // Step 8: Report result
147
+ if (build.state === 'success') {
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 = '';
153
+ if (build.finishedAt && build.startedAt) {
154
+ duration = calculateDuration(build.startedAt, build.finishedAt);
155
+ logMessage(` Duration: ${duration}`);
156
+ }
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
+ });
163
+ }
164
+ else {
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
+ }
174
+ }
175
+ }
176
+ catch (error) {
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
+ }
183
+ }
184
+ }
185
+ // Export for use by server
186
+ export { handlePublishCommand };
187
+ async function promptBuildConfig(defaults) {
188
+ var _a, _b, _c, _d;
189
+ const answers = await inquirer.prompt([
190
+ {
191
+ type: 'input',
192
+ name: 'name',
193
+ message: 'Application name:',
194
+ default: defaults.name || undefined,
195
+ validate: (input) => (input.trim() ? true : 'Application name is required'),
196
+ },
197
+ {
198
+ type: 'input',
199
+ name: 'baseImage',
200
+ message: 'Base image:',
201
+ default: (_a = defaults.baseImage) !== null && _a !== void 0 ? _a : APP_BUILD_DEFAULTS.baseImage,
202
+ },
203
+ {
204
+ type: 'input',
205
+ name: 'buildScript',
206
+ message: 'Build script:',
207
+ default: (_b = defaults.buildScript) !== null && _b !== void 0 ? _b : APP_BUILD_DEFAULTS.buildScript,
208
+ },
209
+ {
210
+ type: 'input',
211
+ name: 'buildOutput',
212
+ message: 'Build output path:',
213
+ default: (_c = defaults.buildOutput) !== null && _c !== void 0 ? _c : APP_BUILD_DEFAULTS.buildOutput,
214
+ },
215
+ {
216
+ type: 'input',
217
+ name: 'workingPath',
218
+ message: 'Working path:',
219
+ default: (_d = defaults.workingPath) !== null && _d !== void 0 ? _d : APP_BUILD_DEFAULTS.workingPath,
220
+ },
221
+ ]);
222
+ return {
223
+ name: answers.name.trim(),
224
+ baseImage: answers.baseImage.trim(),
225
+ buildScript: answers.buildScript.trim(),
226
+ buildOutput: answers.buildOutput.trim(),
227
+ workingPath: answers.workingPath.trim(),
228
+ };
229
+ }
230
+ function calculateDuration(start, end) {
231
+ const ms = new Date(end).getTime() - new Date(start).getTime();
232
+ const seconds = Math.floor(ms / 1000);
233
+ if (seconds < 60)
234
+ return `${seconds}s`;
235
+ const minutes = Math.floor(seconds / 60);
236
+ const remainingSeconds = seconds % 60;
237
+ return `${minutes}m ${remainingSeconds}s`;
238
+ }
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
+ }