zephyr-cli 0.0.2 → 0.1.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 (55) hide show
  1. package/.eslintignore +3 -0
  2. package/.eslintrc.json +22 -0
  3. package/LICENSE +40 -21
  4. package/README.md +91 -18
  5. package/dist/cli.d.ts +25 -0
  6. package/dist/cli.js +141 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/deploy.d.ts +12 -0
  9. package/dist/commands/deploy.js +60 -0
  10. package/dist/commands/deploy.js.map +1 -0
  11. package/dist/commands/run.d.ts +9 -0
  12. package/dist/commands/run.js +161 -0
  13. package/dist/commands/run.js.map +1 -0
  14. package/dist/index.js +48 -60
  15. package/dist/index.js.map +1 -0
  16. package/dist/lib/build-stats.d.ts +7 -0
  17. package/dist/lib/build-stats.js +12 -0
  18. package/dist/lib/build-stats.js.map +1 -0
  19. package/dist/lib/command-detector.d.ts +38 -0
  20. package/dist/lib/command-detector.js +453 -0
  21. package/dist/lib/command-detector.js.map +1 -0
  22. package/dist/lib/config-readers.d.ts +37 -0
  23. package/dist/lib/config-readers.js +239 -0
  24. package/dist/lib/config-readers.js.map +1 -0
  25. package/dist/lib/extract-assets.d.ts +6 -0
  26. package/dist/lib/extract-assets.js +95 -0
  27. package/dist/lib/extract-assets.js.map +1 -0
  28. package/dist/lib/shell-parser.d.ts +40 -0
  29. package/dist/lib/shell-parser.js +190 -0
  30. package/dist/lib/shell-parser.js.map +1 -0
  31. package/dist/lib/spawn-helper.d.ts +14 -0
  32. package/dist/lib/spawn-helper.js +36 -0
  33. package/dist/lib/spawn-helper.js.map +1 -0
  34. package/dist/lib/upload.d.ts +14 -0
  35. package/dist/lib/upload.js +32 -0
  36. package/dist/lib/upload.js.map +1 -0
  37. package/dist/package.json +48 -0
  38. package/dist/tsconfig.tsbuildinfo +1 -0
  39. package/jest.config.ts +10 -0
  40. package/package.json +39 -22
  41. package/project.json +34 -0
  42. package/src/cli.ts +150 -0
  43. package/src/commands/deploy.ts +74 -0
  44. package/src/commands/run.ts +196 -0
  45. package/src/index.ts +58 -0
  46. package/src/lib/build-stats.ts +13 -0
  47. package/src/lib/command-detector.ts +600 -0
  48. package/src/lib/config-readers.ts +269 -0
  49. package/src/lib/extract-assets.ts +111 -0
  50. package/src/lib/shell-parser.ts +229 -0
  51. package/src/lib/spawn-helper.ts +49 -0
  52. package/src/lib/upload.ts +39 -0
  53. package/tsconfig.json +22 -0
  54. package/tsconfig.lib.json +10 -0
  55. package/tsconfig.spec.json +14 -0
@@ -0,0 +1,269 @@
1
+ import { cosmiconfig } from 'cosmiconfig';
2
+ import { parse as parseJsonc } from 'jsonc-parser';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ export interface PackageJsonConfig {
7
+ scripts?: Record<string, string>;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ export interface TsConfigJson {
12
+ compilerOptions?: {
13
+ outDir?: string;
14
+ rootDir?: string;
15
+ [key: string]: unknown;
16
+ };
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ /** Read and parse package.json from the given directory */
21
+ export function readPackageJson(cwd: string): PackageJsonConfig | null {
22
+ const packageJsonPath = join(cwd, 'package.json');
23
+
24
+ if (!existsSync(packageJsonPath)) {
25
+ return null;
26
+ }
27
+
28
+ try {
29
+ const content = readFileSync(packageJsonPath, 'utf-8');
30
+ return parseJsonc(content);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /** Read and parse tsconfig.json from the given directory */
37
+ export function readTsConfig(
38
+ cwd: string,
39
+ configPath = 'tsconfig.json'
40
+ ): TsConfigJson | null {
41
+ const tsConfigPath = join(cwd, configPath);
42
+
43
+ if (!existsSync(tsConfigPath)) {
44
+ return null;
45
+ }
46
+
47
+ try {
48
+ const content = readFileSync(tsConfigPath, 'utf-8');
49
+ // Parse JSON with comments support using jsonc-parser
50
+ return parseJsonc(content);
51
+ } catch (error) {
52
+ console.error(`Error reading tsconfig.json: ${error}`);
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /** Check if a file exists and has a .js or .mjs extension */
58
+ export function isJavaScriptConfig(cwd: string, baseName: string): boolean {
59
+ const jsExtensions = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'];
60
+
61
+ for (const ext of jsExtensions) {
62
+ const configPath = join(cwd, `${baseName}${ext}`);
63
+ if (existsSync(configPath)) {
64
+ return true;
65
+ }
66
+ }
67
+
68
+ return false;
69
+ }
70
+
71
+ /** Check if a configuration file exists (any extension) */
72
+ export function configFileExists(cwd: string, baseName: string): string | null {
73
+ const extensions = ['.json', '.js', '.mjs', '.cjs', '.ts'];
74
+
75
+ for (const ext of extensions) {
76
+ const configPath = join(cwd, `${baseName}${ext}`);
77
+ if (existsSync(configPath)) {
78
+ return configPath;
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ /** Generic interface for framework configuration with output directory */
86
+ export interface FrameworkConfig {
87
+ /** The detected output directory from the config */
88
+ outputDir?: string | null;
89
+ /** The full configuration object (framework-specific) */
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ config: any;
92
+ /** The file path where the config was loaded from */
93
+ filepath: string;
94
+ }
95
+
96
+ /** Load Webpack configuration using cosmiconfig */
97
+ export async function loadWebpackConfig(cwd: string): Promise<FrameworkConfig | null> {
98
+ try {
99
+ const explorer = cosmiconfig('webpack', {
100
+ searchPlaces: [
101
+ 'webpack.config.js',
102
+ 'webpack.config.mjs',
103
+ 'webpack.config.cjs',
104
+ 'webpack.config.ts',
105
+ ],
106
+ });
107
+
108
+ const result = await explorer.search(cwd);
109
+ if (!result || !result.config) {
110
+ return null;
111
+ }
112
+
113
+ // Extract output.path from webpack config
114
+ let outputDir: string | null = null;
115
+ const config = result.config;
116
+
117
+ // Handle function configs (they receive env and argv)
118
+ if (typeof config === 'function') {
119
+ try {
120
+ const resolvedConfig = await config({}, { mode: 'production' });
121
+ outputDir = resolvedConfig?.output?.path || null;
122
+ } catch {
123
+ // If function execution fails, we can't extract the output
124
+ outputDir = null;
125
+ }
126
+ } else if (config?.output?.path) {
127
+ outputDir = config.output.path;
128
+ }
129
+
130
+ return {
131
+ outputDir,
132
+ config: result.config,
133
+ filepath: result.filepath,
134
+ };
135
+ } catch {
136
+ // If loading fails, return null
137
+ return null;
138
+ }
139
+ }
140
+
141
+ /** Load Vite configuration using cosmiconfig */
142
+ export async function loadViteConfig(cwd: string): Promise<FrameworkConfig | null> {
143
+ try {
144
+ const explorer = cosmiconfig('vite', {
145
+ searchPlaces: [
146
+ 'vite.config.js',
147
+ 'vite.config.mjs',
148
+ 'vite.config.cjs',
149
+ 'vite.config.ts',
150
+ ],
151
+ });
152
+
153
+ const result = await explorer.search(cwd);
154
+ if (!result || !result.config) {
155
+ return null;
156
+ }
157
+
158
+ // Extract build.outDir from vite config
159
+ let outputDir: string | null = null;
160
+ const config = result.config;
161
+
162
+ // Handle function configs (they receive config env)
163
+ if (typeof config === 'function') {
164
+ try {
165
+ const resolvedConfig = await config({ mode: 'production', command: 'build' });
166
+ outputDir = resolvedConfig?.build?.outDir || 'dist';
167
+ } catch {
168
+ outputDir = 'dist'; // Vite default
169
+ }
170
+ } else if (config?.build?.outDir) {
171
+ outputDir = config.build.outDir;
172
+ } else {
173
+ outputDir = 'dist'; // Vite default
174
+ }
175
+
176
+ return {
177
+ outputDir,
178
+ config: result.config,
179
+ filepath: result.filepath,
180
+ };
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+
186
+ /** Load Rollup configuration using cosmiconfig */
187
+ export async function loadRollupConfig(cwd: string): Promise<FrameworkConfig | null> {
188
+ try {
189
+ const explorer = cosmiconfig('rollup', {
190
+ searchPlaces: [
191
+ 'rollup.config.js',
192
+ 'rollup.config.mjs',
193
+ 'rollup.config.cjs',
194
+ 'rollup.config.ts',
195
+ ],
196
+ });
197
+
198
+ const result = await explorer.search(cwd);
199
+ if (!result || !result.config) {
200
+ return null;
201
+ }
202
+
203
+ // Extract output.dir or output.file from rollup config
204
+ let outputDir: string | null = null;
205
+ const config = result.config;
206
+
207
+ // Handle function configs
208
+ if (typeof config === 'function') {
209
+ try {
210
+ const resolvedConfig = await config({});
211
+ if (Array.isArray(resolvedConfig)) {
212
+ // Multiple outputs - take the first one
213
+ outputDir =
214
+ resolvedConfig[0]?.output?.dir || resolvedConfig[0]?.output?.file || null;
215
+ } else {
216
+ outputDir = resolvedConfig?.output?.dir || resolvedConfig?.output?.file || null;
217
+ }
218
+ } catch {
219
+ outputDir = null;
220
+ }
221
+ } else if (Array.isArray(config)) {
222
+ // Multiple outputs - take the first one
223
+ outputDir = config[0]?.output?.dir || config[0]?.output?.file || null;
224
+ } else if (config?.output) {
225
+ const output = Array.isArray(config.output) ? config.output[0] : config.output;
226
+ outputDir = output?.dir || output?.file || null;
227
+ }
228
+
229
+ // If outputDir is a file path, extract the directory
230
+ if (outputDir && outputDir.includes('.')) {
231
+ const lastSlash = Math.max(outputDir.lastIndexOf('/'), outputDir.lastIndexOf('\\'));
232
+ if (lastSlash !== -1) {
233
+ outputDir = outputDir.substring(0, lastSlash);
234
+ }
235
+ }
236
+
237
+ return {
238
+ outputDir,
239
+ config: result.config,
240
+ filepath: result.filepath,
241
+ };
242
+ } catch {
243
+ return null;
244
+ }
245
+ }
246
+
247
+ /** Load SWC configuration using cosmiconfig */
248
+ export async function loadSwcConfig(cwd: string): Promise<FrameworkConfig | null> {
249
+ try {
250
+ const explorer = cosmiconfig('swc', {
251
+ searchPlaces: ['.swcrc', '.swcrc.json', '.swcrc.js', '.swcrc.mjs', '.swcrc.cjs'],
252
+ });
253
+
254
+ const result = await explorer.search(cwd);
255
+ if (!result || !result.config) {
256
+ return null;
257
+ }
258
+
259
+ // SWC doesn't have a standard output directory config
260
+ // Most projects use a custom output directory
261
+ return {
262
+ outputDir: null,
263
+ config: result.config,
264
+ filepath: result.filepath,
265
+ };
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
@@ -0,0 +1,111 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, relative, sep } from 'node:path';
3
+ import { buildAssetsMap, logFn, type ZeBuildAssetsMap } from 'zephyr-agent';
4
+
5
+ interface DirectoryAsset {
6
+ content: Buffer | string;
7
+ type: string;
8
+ }
9
+
10
+ function extractBuffer(asset: DirectoryAsset): Buffer | string | undefined {
11
+ return asset.content;
12
+ }
13
+
14
+ function getAssetType(asset: DirectoryAsset): string {
15
+ return asset.type;
16
+ }
17
+
18
+ /** Normalize path separators to forward slashes for cross-platform consistency */
19
+ function normalizePath(filePath: string): string {
20
+ return filePath.split(sep).join('/');
21
+ }
22
+
23
+ /**
24
+ * Extract assets map from a directory by recursively walking through it. Similar to
25
+ * extractAstroAssetsMap but for any directory.
26
+ */
27
+ export async function extractAssetsFromDirectory(
28
+ buildDir: string
29
+ ): Promise<ZeBuildAssetsMap> {
30
+ const assets: Record<string, DirectoryAsset> = {};
31
+
32
+ // Recursively walk through the build directory
33
+ async function walkDir(dirPath: string): Promise<void> {
34
+ try {
35
+ const entries = await readdir(dirPath, { withFileTypes: true });
36
+
37
+ for (const entry of entries) {
38
+ const fullPath = join(dirPath, entry.name);
39
+
40
+ if (entry.isDirectory()) {
41
+ await walkDir(fullPath);
42
+ } else if (entry.isFile()) {
43
+ // Get relative path from build directory
44
+ const relativePath = normalizePath(relative(buildDir, fullPath));
45
+
46
+ // Skip certain files that shouldn't be uploaded
47
+ if (shouldSkipFile(relativePath)) {
48
+ continue;
49
+ }
50
+
51
+ try {
52
+ const content = await readFile(fullPath);
53
+ const fileType = getFileType(relativePath);
54
+
55
+ assets[relativePath] = {
56
+ content,
57
+ type: fileType,
58
+ };
59
+ } catch (readError) {
60
+ logFn('warn', `Failed to read file ${fullPath}: ${readError}`);
61
+ }
62
+ }
63
+ }
64
+ } catch (error) {
65
+ logFn('warn', `Failed to walk directory ${dirPath}: ${error}`);
66
+ }
67
+ }
68
+
69
+ await walkDir(buildDir);
70
+
71
+ return buildAssetsMap(assets, extractBuffer, getAssetType);
72
+ }
73
+
74
+ function shouldSkipFile(filePath: string): boolean {
75
+ // Skip common files that shouldn't be uploaded
76
+ const skipPatterns = [
77
+ /\.map$/, // Source maps
78
+ /node_modules/, // Node modules
79
+ /\.git/, // Git files
80
+ /\.DS_Store$/, // macOS files
81
+ /thumbs\.db$/i, // Windows files
82
+ ];
83
+
84
+ return skipPatterns.some((pattern) => pattern.test(filePath));
85
+ }
86
+
87
+ function getFileType(filePath: string): string {
88
+ const extension = filePath.split('.').pop()?.toLowerCase() || '';
89
+
90
+ const typeMap: Record<string, string> = {
91
+ html: 'text/html',
92
+ css: 'text/css',
93
+ js: 'application/javascript',
94
+ mjs: 'application/javascript',
95
+ json: 'application/json',
96
+ png: 'image/png',
97
+ jpg: 'image/jpeg',
98
+ jpeg: 'image/jpeg',
99
+ gif: 'image/gif',
100
+ svg: 'image/svg+xml',
101
+ ico: 'image/x-icon',
102
+ woff: 'font/woff',
103
+ woff2: 'font/woff2',
104
+ ttf: 'font/ttf',
105
+ eot: 'application/vnd.ms-fontobject',
106
+ xml: 'text/xml',
107
+ txt: 'text/plain',
108
+ };
109
+
110
+ return typeMap[extension] || 'application/octet-stream';
111
+ }
@@ -0,0 +1,229 @@
1
+ import { ZeErrors, ZephyrError } from 'zephyr-agent';
2
+
3
+ export interface ParsedCommand {
4
+ /** The main command to execute (without environment variables) */
5
+ command: string;
6
+ /** Environment variables set for this command */
7
+ envVars: Record<string, string>;
8
+ /** Arguments passed to the command */
9
+ args: string[];
10
+ /** The full command line as a string */
11
+ fullCommand: string;
12
+ }
13
+
14
+ /**
15
+ * Parse a shell command to extract the actual command, args, and environment variables.
16
+ * Uses simple regex-based parsing to handle common cases like:
17
+ *
18
+ * - `pnpm build`
19
+ * - `NODE_ENV=production webpack --mode production`
20
+ * - `KEY1=value1 KEY2=value2 command arg1 arg2`
21
+ *
22
+ * @example
23
+ * parseShellCommand('NODE_ENV=production webpack --mode production');
24
+ * // Returns: { command: 'webpack', envVars: { NODE_ENV: 'production' }, args: ['--mode', 'production'] }
25
+ *
26
+ * @example
27
+ * parseShellCommand('pnpm build');
28
+ * // Returns: { command: 'pnpm', envVars: {}, args: ['build'] }
29
+ */
30
+ export function parseShellCommand(commandLine: string): ParsedCommand {
31
+ const envVars: Record<string, string> = {};
32
+ const trimmed = commandLine.trim();
33
+
34
+ if (!trimmed) {
35
+ throw new ZephyrError(ZeErrors.ERR_UNKNOWN, {
36
+ message: 'Empty command line',
37
+ });
38
+ }
39
+
40
+ // Split by whitespace while respecting quotes
41
+ const tokens = tokenizeCommand(trimmed);
42
+
43
+ if (tokens.length === 0) {
44
+ throw new ZephyrError(ZeErrors.ERR_UNKNOWN, {
45
+ message: `Failed to parse command: ${commandLine}`,
46
+ });
47
+ }
48
+
49
+ let i = 0;
50
+
51
+ // Extract environment variables (KEY=VALUE format at the beginning)
52
+ while (i < tokens.length) {
53
+ const token = tokens[i];
54
+ const match = token.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
55
+
56
+ if (match) {
57
+ const [, name, value] = match;
58
+ envVars[name] = value;
59
+ i++;
60
+ } else {
61
+ break;
62
+ }
63
+ }
64
+
65
+ // The next token should be the command
66
+ if (i >= tokens.length) {
67
+ throw new ZephyrError(ZeErrors.ERR_UNKNOWN, {
68
+ message: `Failed to parse command: ${commandLine}\nNo command found after environment variables`,
69
+ });
70
+ }
71
+
72
+ const command = tokens[i];
73
+ i++;
74
+
75
+ // Remaining tokens are arguments
76
+ const args = tokens.slice(i);
77
+
78
+ return {
79
+ command,
80
+ envVars,
81
+ args,
82
+ fullCommand: commandLine,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Split a command line into multiple commands based on shell operators (;, &&) Respects
88
+ * quotes and escapes.
89
+ *
90
+ * @example
91
+ * splitCommands('npm run build && npm run test');
92
+ * // Returns: ['npm run build', 'npm run test']
93
+ *
94
+ * @example
95
+ * splitCommands('echo "hello; world" && npm run build');
96
+ * // Returns: ['echo "hello; world"', 'npm run build']
97
+ */
98
+ export function splitCommands(commandLine: string): string[] {
99
+ const commands: string[] = [];
100
+ let current = '';
101
+ let inSingleQuote = false;
102
+ let inDoubleQuote = false;
103
+ let escaped = false;
104
+
105
+ for (let i = 0; i < commandLine.length; i++) {
106
+ const char = commandLine[i];
107
+ const nextChar = i + 1 < commandLine.length ? commandLine[i + 1] : '';
108
+
109
+ if (escaped) {
110
+ current += char;
111
+ escaped = false;
112
+ continue;
113
+ }
114
+
115
+ if (char === '\\') {
116
+ current += char;
117
+ escaped = true;
118
+ continue;
119
+ }
120
+
121
+ if (char === "'" && !inDoubleQuote) {
122
+ inSingleQuote = !inSingleQuote;
123
+ current += char;
124
+ continue;
125
+ }
126
+
127
+ if (char === '"' && !inSingleQuote) {
128
+ inDoubleQuote = !inDoubleQuote;
129
+ current += char;
130
+ continue;
131
+ }
132
+
133
+ // Check for shell operators outside quotes
134
+ if (!inSingleQuote && !inDoubleQuote) {
135
+ // Check for &&
136
+ if (char === '&' && nextChar === '&') {
137
+ const trimmed = current.trim();
138
+ if (trimmed) {
139
+ commands.push(trimmed);
140
+ }
141
+ current = '';
142
+ i++; // Skip the next &
143
+ continue;
144
+ }
145
+
146
+ // Check for ;
147
+ if (char === ';') {
148
+ const trimmed = current.trim();
149
+ if (trimmed) {
150
+ commands.push(trimmed);
151
+ }
152
+ current = '';
153
+ continue;
154
+ }
155
+ }
156
+
157
+ current += char;
158
+ }
159
+
160
+ // Add the last command if any
161
+ const trimmed = current.trim();
162
+ if (trimmed) {
163
+ commands.push(trimmed);
164
+ }
165
+
166
+ if (inSingleQuote || inDoubleQuote) {
167
+ throw new ZephyrError(ZeErrors.ERR_UNKNOWN, {
168
+ message: 'Unmatched quote in command line',
169
+ });
170
+ }
171
+
172
+ return commands;
173
+ }
174
+
175
+ /** Tokenize a command line string, respecting quotes and escapes */
176
+ function tokenizeCommand(commandLine: string): string[] {
177
+ const tokens: string[] = [];
178
+ let current = '';
179
+ let inSingleQuote = false;
180
+ let inDoubleQuote = false;
181
+ let escaped = false;
182
+
183
+ for (let i = 0; i < commandLine.length; i++) {
184
+ const char = commandLine[i];
185
+
186
+ if (escaped) {
187
+ current += char;
188
+ escaped = false;
189
+ continue;
190
+ }
191
+
192
+ if (char === '\\') {
193
+ escaped = true;
194
+ continue;
195
+ }
196
+
197
+ if (char === "'" && !inDoubleQuote) {
198
+ inSingleQuote = !inSingleQuote;
199
+ continue;
200
+ }
201
+
202
+ if (char === '"' && !inSingleQuote) {
203
+ inDoubleQuote = !inDoubleQuote;
204
+ continue;
205
+ }
206
+
207
+ if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) {
208
+ if (current) {
209
+ tokens.push(current);
210
+ current = '';
211
+ }
212
+ continue;
213
+ }
214
+
215
+ current += char;
216
+ }
217
+
218
+ if (current) {
219
+ tokens.push(current);
220
+ }
221
+
222
+ if (inSingleQuote || inDoubleQuote) {
223
+ throw new ZephyrError(ZeErrors.ERR_UNKNOWN, {
224
+ message: 'Unmatched quote in command line',
225
+ });
226
+ }
227
+
228
+ return tokens;
229
+ }
@@ -0,0 +1,49 @@
1
+ import { spawn } from 'node:child_process';
2
+ import type { ParsedCommand } from './shell-parser';
3
+
4
+ export interface SpawnResult {
5
+ exitCode: number;
6
+ signal: NodeJS.Signals | null;
7
+ }
8
+
9
+ /**
10
+ * Execute a command with full stdio passthrough. All stdin, stdout, and stderr are
11
+ * proxied between the parent and child process.
12
+ *
13
+ * @param parsed - The parsed command to execute
14
+ * @param cwd - The working directory
15
+ * @returns Promise that resolves with exit code and signal
16
+ */
17
+ export async function executeCommand(
18
+ parsed: ParsedCommand,
19
+ cwd: string
20
+ ): Promise<SpawnResult> {
21
+ return new Promise((resolve, reject) => {
22
+ const { command, args, envVars } = parsed;
23
+
24
+ // Merge environment variables
25
+ const env = {
26
+ ...process.env,
27
+ ...envVars,
28
+ };
29
+
30
+ // Spawn the command
31
+ const child = spawn(command, args, {
32
+ cwd,
33
+ env,
34
+ stdio: 'inherit', // This passes stdin, stdout, and stderr through
35
+ shell: true, // Use shell to handle complex commands
36
+ });
37
+
38
+ child.on('error', (error) => {
39
+ reject(error);
40
+ });
41
+
42
+ child.on('close', (code, signal) => {
43
+ resolve({
44
+ exitCode: code ?? 1,
45
+ signal,
46
+ });
47
+ });
48
+ });
49
+ }
@@ -0,0 +1,39 @@
1
+ import type { ZeBuildAssetsMap } from 'zephyr-edge-contract';
2
+ import type { ZephyrEngine } from 'zephyr-agent';
3
+ import { logFn, ZephyrError } from 'zephyr-agent';
4
+ import { getBuildStats } from './build-stats';
5
+
6
+ export interface UploadOptions {
7
+ zephyr_engine: ZephyrEngine;
8
+ assetsMap: ZeBuildAssetsMap;
9
+ }
10
+
11
+ /**
12
+ * Orchestrate the upload process:
13
+ *
14
+ * 1. Start a new build
15
+ * 2. Upload assets with build stats
16
+ * 3. Finish the build
17
+ */
18
+ export async function uploadAssets(options: UploadOptions): Promise<void> {
19
+ const { zephyr_engine, assetsMap } = options;
20
+
21
+ try {
22
+ // Start a new build
23
+ await zephyr_engine.start_new_build();
24
+
25
+ // Generate build stats
26
+ const buildStats = await getBuildStats(zephyr_engine);
27
+
28
+ // Upload assets and finish the build
29
+ await zephyr_engine.upload_assets({
30
+ assetsMap,
31
+ buildStats,
32
+ });
33
+
34
+ await zephyr_engine.build_finished();
35
+ } catch (error) {
36
+ logFn('error', ZephyrError.format(error));
37
+ throw error;
38
+ }
39
+ }